Compare commits

...

59 Commits

Author SHA1 Message Date
Sigma Dimensions 9422f804fb minor fix 2023-05-03 04:22:02 +01:00
Sigma Dimensions 415a7746be BBIM 4D Gantt: Support multiple languages, human readable durations, add convenient Print Function with different Sheet Formats,
start improving overall page style; refactor code to split js and css code away from mustache file
2023-05-02 17:27:47 +01:00
Andrej730 ee903208dc Small fix for text alignment in scheduler 2023-05-02 17:41:56 +05:00
Dion Moult f89e834fea Fix #3085. Prioritise shipped BlenderBIM Add-on packages instead of system packages to avoid dependency clashes. 2023-05-02 22:20:34 +10:00
Dion Moult e8b4335741 Minor UI cleanup. Remove align buttons from architectural tool panel. 2023-05-02 22:20:34 +10:00
Dion Moult 83010f0542 See #1676. You can now add basic space boundaries with the BIM Tool with Shift-B, either by selecting both a space and an element (e.g. wall) or selecting a space and placing the 3D cursor on a face. 2023-05-02 22:20:34 +10:00
Dion Moult 04e5f5036c See #1676. Deleting space boundaries now also removes connection geometry. 2023-05-02 22:20:34 +10:00
Andrej730 5d86d38a14 Support cells text alignment building schedule from ods #2885
Before - https://i.imgur.com/9QZ2YW2.png
After - https://i.imgur.com/M7ZHSJq.png
2023-05-02 17:04:48 +05:00
Andrej730 a82db1f622 Support merged columns building schedule from ods #2885
Before - https://i.imgur.com/p1l6ugn.png
After - https://i.imgur.com/9QZ2YW2.png
2023-05-02 16:24:30 +05:00
Andrej730 fb618d536b Keep rows height building svg schedule from ods #2885
Before - https://i.imgur.com/erMH4Ku.png
After - https://i.imgur.com/p1l6ugn.png
2023-05-02 14:56:05 +05:00
Andrej730 23f96779fa More safe adding roof modifier #3080 2023-05-02 10:38:25 +05:00
Dion Moult 7b67854654 Fix #3088. Prioritise body curves to be loaded before trying to load 2D annotations. 2023-05-02 13:11:01 +10:00
Sigma Dimensions 2e7f941465 BBIM 4D: Auto Update Resource Usage from resource tree 2023-05-02 02:47:22 +01:00
Sigma Dimensions 1c1706fde3 BBIM feature to create & view work schedule baselines 2023-05-02 02:43:54 +01:00
Sigma Dimensions ca77a895e0 IOS.API consider removing baseline tasks when deleting tasks 2023-05-02 02:31:48 +01:00
Sigma Dimensions 0f447f964e IOS.API usecase to create a work schedule baseline 2023-05-02 02:30:38 +01:00
Sigma Dimensions 2c31fc6b25 run black 2023-05-02 02:27:32 +01:00
Dion Moult 0ba79dc105 Accommodate space boundaries with no connection geometry, fetch spatial structure for IFC2X3, and fix scale bug when editing boundary profiles. 2023-05-02 10:23:54 +10:00
dependabot[bot] 52c686b656 Bump flask from 2.0.1 to 2.3.2 in /src/opencdeserver
Bumps [flask](https://github.com/pallets/flask) from 2.0.1 to 2.3.2.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/2.0.1...2.3.2)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-02 10:06:31 +10:00
Thomas Krijnen 64d98f1295 #2676 2023-05-01 20:16:39 +02:00
Andrej730 ceb2c4075c Option to scale symbol from ifc tag by scaling ifc text object #2946
Also added some text scaling in viewport if there is some symbol associated with text annotation to roughly visually indicate how big this thing is going to be in the svg.

Example - https://user-images.githubusercontent.com/9417531/235445393-447f3aba-8425-4da6-81d0-6bf9004fcf2b.mp4
2023-05-01 16:25:04 +05:00
Andrej730 166e7d52ec Keep text rotation for text annotation with symbols #2946 2023-05-01 16:25:04 +05:00
Dion Moult 644c2652ea See #1676. You can now toggle boundaries on or off in the BIM tool via Alt-B, with a special boundary decorator. 2023-05-01 21:19:29 +10:00
Andrej730 63b201fd95 Test command to load blend file during test scenario for debugging 2023-05-01 15:35:27 +05:00
Andrej730 d70616056c Moving decorators colors from scene to addon props #3033 2023-05-01 14:54:47 +05:00
Dion Moult a65e376886 See #1676. Tab now toggles space boundary surface profile editing. 2023-05-01 19:40:49 +10:00
Andrej730 a985c70f0c Fixed error on adding Roof modifier to non-footprint objects #3080 2023-05-01 14:03:28 +05:00
Andrej730 0d09e02e7e Fixed bug with double conversion of height/extrusion length #3073
Also fixed some tests
2023-05-01 13:46:24 +05:00
Thomas Krijnen 23e3551f27 Fix regex escape in rule_executor.py 2023-04-30 19:09:43 +02:00
Shohei Kunimatsu 885bca0865 Fix wrong attribute name usage (ValueComponent -> UnitComponent) in IfcFile::getUnit 2023-04-30 19:06:58 +02:00
Sigma Dimensions 0f874dad51 BBIM 4D features & fixes:
- feature to re-order tasks
- fixes for task sorting
- further workSchedule UI/UX improvements
- forgotten bits and bobs
2023-04-30 15:44:13 +01:00
Sigma Dimensions 107873cf6e - refactor IOS.API resource calculation to re-use shared utilities
- run black
2023-04-30 15:35:17 +01:00
Sigma Dimensions 4a8f5af7ba - BBIM 4D: features to review and edit Resource productivity data on the fly
- IOS: new resource utilitiy functions
2023-04-30 15:31:36 +01:00
Dion Moult 83084aeb7b Add profile-style (like slab editing) enable editing, edit, and disable editing operators for energy boundary surface geometry. 2023-04-30 20:57:51 +10:00
Dion Moult 51640ccc28 Fix #3030. Don't bother layerset slicing walls with a single layer. 2023-04-30 13:43:43 +10:00
Dion Moult aefbc134ea Cut merging now only applies to particular elements for the majority of cases. Drawing generation can be twice as fast due to this. 2023-04-30 13:43:43 +10:00
Massimo Fabbro 29e1d70334 Generate space from walls refactor and cleaning around 2023-04-29 22:33:19 +02:00
Massimo Fabbro 9118e529e6 Add generate space button in UI in N-Panel -> BlenderBim -> Architectural 2023-04-29 22:33:19 +02:00
Massimo Fabbro 8ee0a1611d Now it's possible to create spaces automatically from selected walls 2023-04-29 22:33:19 +02:00
Sigma Dimensions 2af4388f40 Fix duplicate task logic in IOS.API 2023-04-29 13:53:04 +01:00
Dion Moult 93a2d18ea6 Filter elements visible in the camera view. Drawing generation is now significantly faster on larger projects (e.g. 24s down to 10s for a portion of the Revit sample project) 2023-04-29 16:33:44 +10:00
Dion Moult fc49859ea4 Fallback to unmerged polygons if merging fails. More investigation needed. 2023-04-29 16:30:40 +10:00
Dion Moult c0ac9cb380 Viewport layerset slicing now uses a cache for speed. 2023-04-28 22:46:49 +10:00
Andrej730 d5cc6c3c8e Fixed double conversion in bim tool props after b4b6404
No more 1000000 and 3000000 mm values
2023-04-28 17:32:19 +05:00
Andrej730 9b22b5000f Added support for different railing cap types
Very similar to this - https://i.imgur.com/SNyvfgh.png
2023-04-28 17:24:41 +05:00
Dion Moult 3b69a4cc78 Fix bug where numerical tolerance caused certain objects to be unnecessarily updated. 2023-04-28 22:15:09 +10:00
Dion Moult adf54efcc5 Fix bug in cut decorator layersets where walls could be joined to odd non-layered things. 2023-04-28 22:14:26 +10:00
Dion Moult f6962eb58e Fix #3053. Bug where very thin meshes (<1mm) couldn't be reverse engineered into extrusions. 2023-04-28 21:05:13 +10:00
Andrej730 11601d6532 Fixed bug with wrong position of support in wall mounted railing
The problem was that that support's disk was not rotated according to the railing path.
2023-04-28 11:43:15 +05:00
Sigma Dimensions be2131d62c BBIM 4D: Auto-refresh task re-ordering 2023-04-27 13:58:08 +01:00
Dion Moult 3ffbf46ac2 Fix #3066. Regression crash when deleting openings. 2023-04-27 22:24:12 +10:00
Sigma Dimensions 3943107eec forgotten core functions & run black 2023-04-27 13:05:51 +01:00
Sigma Dimensions 94e3f75341 Sequence Tools clean-up 2023-04-27 13:03:32 +01:00
Sigma Dimensions 8bc8b950e7 BBIM 4D: Features to select a Work Schedule's assigned / unassigned products 2023-04-27 12:55:31 +01:00
Sigma Dimensions 05a95ac666 Work Schedule Panel UX improvement 2023-04-27 12:53:27 +01:00
Sigma Dimensions f016c803e4 Fix schedule task sorting 2023-04-27 12:49:27 +01:00
Dion Moult bac0dd37fa Fix #3052. Deep deletion of large geometries is now thousands of times faster. 2023-04-27 20:15:17 +10:00
Bruno Postle 75a37cf737 Fix crash when switching between git revisions 2023-04-26 23:50:36 +01:00
Bruno Postle bb7e9c4610 Fix check for invalid Git branch name 2023-04-26 22:25:27 +01:00
82 changed files with 2749 additions and 591 deletions
+2 -1
View File
@@ -34,7 +34,8 @@ bl_info = {
if sys.modules.get("bpy", None):
# Process *.pth in /libs/site/packages to setup globally importable modules
# This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda
site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
# site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
import blenderbim.bim
@@ -1,67 +1,53 @@
<link href="jsgantt.css" rel="stylesheet" type="text/css"/>
<link href="../../module/sequence/gantt/main.css" rel="stylesheet" type="text/css"/>
<script src="jsgantt.js" type="text/javascript"></script>
<style>
.gtaskcellwkend,
.gtaskcellcurrent,
.gminorheadingwkend {
background-color: #e1e1e1;
}
.gitemhighlight td {
background-color: #ffdaaa;
}
.gtaskblue {
background: #4281A4;
}
.gtaskred {
background: #C1666B;
}
.gtaskgreen {
background: #48A9A6;
}
.gtaskyellow {
background: #D4B483;
}
.gmainleft {
overflow: visible;
flex: 0 1 auto;
}
</style>
<a href="#print" id="print">Print Mode</a>
<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
<script type="text/javascript">
var g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'day');
g.setOptions({
vCaptionType: 'Caption', // Set to Show Caption : None,Caption,Resource,Duration,Complete,
vQuarterColWidth: 36,
vDateTaskDisplayFormat: 'day dd month yyyy', // Shown in tool tip box
vDayMajorDateDisplayFormat: 'mon yyyy - Week ww',// Set format to dates in the "Major" header of the "Day" view
vWeekMinorDateDisplayFormat: 'dd mon', // Set format to display dates in the "Minor" header of the "Week" view
vLang: 'en',
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
vShowRes: false, // Disable the resource column.
vShowComp: false, // Disable the completion column.
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
vAdditionalHeaders: {ifcduration: {title: 'Duration'}},
vUseToolTip: false, // Disable tooltips.
vTotalHeight: 900,
});
document.getElementById('print').addEventListener('click', function() {
g.setTotalHeight("");
g.Draw();
});
var json_data = `
{{{json_data}}}
`;
JSGantt.parseJSONString(json_data, g);
g.Draw();
<script type="text/javascript" src="../../module/sequence/gantt/index.js"></script>
<script>
var json_data = `{{{json_data}}}`;
</script>
<div class="no-print">
<h4>Choose a language:
<select id="lang" class="sweet_button" onchange="set_language(event)">
<option value='cn'>Chinese (cn)</option>
<option value='cs'>Czech (cs)</option>
<option value='nl'>Dutch (Standard)</option>
<option value='en' selected>English (en)</option>
<option value='fr'>French (fr)</option>
<option value='de'>German (de)</option>
<option value='hu'>Hungarian (hu)</option>
<option value='id'>Indonesian (id)</option>
<option value='it'>Italian (it)</option>
<option value='ja'>Japanese (ja)</option>
<option value='pt'>Portuguese (pt)</option>
<option value='ru'>Russian (ru)</option>
<option value='es'>Spanish (es)</option>
<option value='sv'>Swedish (sv)</option>
<option value='tr'>Turkish (tr)</option>
</select>
<h4>
<div id="print_options">
<select class="sweet_button" id="print_page_size">
<option value="210,297">A4 Portrait</option>
<option value="297,210">A4 Landscape</option>
<option value="297,420">A3 Portrait</option>
<option value="420,297">A3 Landscape</option>
<option value="420,594">A2 Portrait</option>
<option value="594,420">A2 Landscape</option>
<option value="594,841">A1 Portrait</option>
<option value="841,594">A1 Landscape</option>
<option value="841,1189">A0 Portrait</option>
<option value="1189,841">A0 Landscape</option>
</select>
</div>
<br>
</div>
<div id="schedule-data"></div>
<div style="position:relative" class="gantt" id="GanttChartDIV"></div>
<script>
var name = `{{{name}}}`;
setupPage(name);
create_gantt_chart(json_data)
</script>
+20 -46
View File
@@ -178,6 +178,11 @@ class IfcImporter:
self.settings.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
self.settings.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
self.settings.set(self.settings.STRICT_TOLERANCE, True)
self.settings_curve = ifcopenshell.geom.settings()
self.settings_curve.set_deflection_tolerance(self.ifc_import_settings.deflection_tolerance)
self.settings_curve.set_angular_tolerance(self.ifc_import_settings.angular_tolerance)
self.settings_curve.set(self.settings_curve.STRICT_TOLERANCE, True)
self.settings_curve.set(self.settings_curve.INCLUDE_CURVES, True)
self.settings_native = ifcopenshell.geom.settings()
self.settings_native.set(self.settings_native.INCLUDE_CURVES, True)
self.settings_2d = ifcopenshell.geom.settings()
@@ -683,28 +688,31 @@ class IfcImporter:
def create_generic_elements(self, elements):
# Based on my experience in viewing BIM models, representations are prioritised as follows:
# 1. 3D Body, 2. 2D Plans, 3. Point clouds, 4. No representation
# If an element has a representation that doesn't follow 1, 2, or 3, it will not show by default.
# 1. 3D Body, 2. 2D Body, 3. 2D Plans / annotations, 4. Point clouds, 5. No representation
# If an element has a representation that doesn't follow 1, 2, 3, or 4, it will not show by default.
# The user can load them later if they want to view them.
products = self.create_products(elements)
elements -= products
products = self.create_curve_products(elements)
products = self.create_products(elements, settings=self.settings_curve)
elements -= products
products = self.create_products(elements, settings=self.settings_2d)
elements -= products
products = self.create_pointclouds(elements)
elements -= products
for element in elements:
self.create_product(element)
def create_products(self, products):
def create_products(self, products, settings=None):
if settings is None:
settings = self.settings
results = set()
if not products:
return results
if self.ifc_import_settings.should_use_cpu_multiprocessing:
iterator = ifcopenshell.geom.iterator(
self.settings, self.file, multiprocessing.cpu_count(), include=products
)
iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products)
else:
iterator = ifcopenshell.geom.iterator(self.settings, self.file, include=products)
iterator = ifcopenshell.geom.iterator(settings, self.file, include=products)
if self.ifc_import_settings.should_cache:
cache = IfcStore.get_cache()
if cache:
@@ -771,10 +779,10 @@ class IfcImporter:
self.structural_collection.children.link(self.structural_connection_collection)
self.project["blender"].children.link(self.structural_collection)
self.create_curve_products(self.file.by_type("IfcStructuralCurveMember"))
self.create_curve_products(self.file.by_type("IfcStructuralCurveConnection"))
self.create_curve_products(self.file.by_type("IfcStructuralSurfaceMember"))
self.create_curve_products(self.file.by_type("IfcStructuralSurfaceConnection"))
self.create_products(self.file.by_type("IfcStructuralCurveMember"), settings=self.settings_2d)
self.create_products(self.file.by_type("IfcStructuralCurveConnection"), settings=self.settings_2d)
self.create_products(self.file.by_type("IfcStructuralSurfaceMember"), settings=self.settings_2d)
self.create_products(self.file.by_type("IfcStructuralSurfaceConnection"), settings=self.settings_2d)
self.create_structural_point_connections()
def create_structural_point_connections(self):
@@ -862,40 +870,6 @@ class IfcImporter:
self.link_element(product, obj)
return product
def create_curve_products(self, products):
results = set()
if not products:
return results
if self.ifc_import_settings.should_use_cpu_multiprocessing:
iterator = ifcopenshell.geom.iterator(
self.settings_2d, self.file, multiprocessing.cpu_count(), include=products
)
else:
iterator = ifcopenshell.geom.iterator(self.settings_2d, self.file, include=products)
if self.ifc_import_settings.should_cache:
cache = IfcStore.get_cache()
if cache:
iterator.set_cache(cache)
valid_file = iterator.initialize()
if not valid_file:
return results
checkpoint = time.time()
total = 0
while True:
total += 1
if total % 250 == 0:
print("{} elements processed in {:.2f}s ...".format(total, time.time() - checkpoint))
checkpoint = time.time()
shape = iterator.get()
if shape:
product = self.file.by_id(shape.id)
self.create_product(product, shape)
results.add(product)
if not iterator.next():
break
print("Done creating geometry")
return results
def create_product(self, element, shape=None, mesh=None):
if element is None:
return
@@ -20,28 +20,37 @@ import bpy
from . import ui, operator, prop
classes = (
operator.AddBoundary,
operator.ColourByRelatedBuildingElement,
operator.DisableEditingBoundary,
operator.DisableEditingBoundaryGeometry,
operator.EditBoundaryAttributes,
operator.EditBoundaryGeometry,
operator.EnableEditingBoundary,
operator.EnableEditingBoundaryGeometry,
operator.HideBoundaries,
operator.LoadBoundary,
operator.LoadProjectSpaceBoundaries,
operator.LoadSpaceBoundaries,
operator.LoadBoundary,
operator.SelectRelatedElementBoundaries,
operator.SelectProjectBoundaries,
operator.SelectRelatedElementBoundaries,
operator.SelectRelatedElementTypeBoundaries,
operator.SelectSpaceBoundaries,
operator.ShowBoundaries,
operator.UpdateBoundaryGeometry,
ui.BIM_PT_Boundary,
ui.BIM_PT_SceneBoundaries,
ui.BIM_PT_SpaceBoundaries,
prop.BIMBoundaryProperties,
prop.BIMObjectBoundaryProperties,
)
def register():
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
bpy.types.Scene.BIMBoundaryProperties = bpy.props.PointerProperty(type=prop.BIMBoundaryProperties)
bpy.types.Object.bim_boundary_properties = bpy.props.PointerProperty(type=prop.BIMObjectBoundaryProperties)
def unregister():
del bpy.types.Scene.BIMBoundaryProperties
del bpy.types.Object.bim_boundary_properties
@@ -0,0 +1,112 @@
# BlenderBIM Add-on - OpenBIM Blender Add-on
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of BlenderBIM Add-on.
#
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BlenderBIM Add-on is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
import gpu
import bmesh
import blenderbim.tool as tool
from bpy.types import SpaceView3D
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
class BoundaryDecorator:
installed = None
@classmethod
def install(cls, context):
if cls.installed:
cls.uninstall()
handler = cls()
cls.installed = SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")
@classmethod
def uninstall(cls):
try:
SpaceView3D.draw_handler_remove(cls.installed, "WINDOW")
except ValueError:
pass
cls.installed = None
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def __call__(self, context):
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
self.line_shader = gpu.shader.from_builtin("3D_POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.line_shader.uniform_float("lineWidth", 2.0)
# general shader
self.shader = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
selected_vertices = []
selected_edges = []
selected_tris = []
unselected_vertices = []
unselected_edges = []
unselected_tris = []
for boundary in context.scene.BIMBoundaryProperties.boundaries:
obj = boundary.obj
if not obj or not obj.data: # A boundary may not have data if it has no connection geometry
continue
if obj.mode == "EDIT":
continue # A profile decorator or something else is used here.
else:
bm = bmesh.new()
bm.from_mesh(obj.data)
obj.data.calc_loop_triangles()
if obj.select_get():
offset = len(selected_vertices)
selected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
selected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
else:
offset = len(unselected_vertices)
unselected_vertices.extend([tuple(obj.matrix_world @ v.co) for v in bm.verts])
unselected_edges.extend([tuple([v.index + offset for v in e.verts]) for e in bm.edges])
unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
if obj.mode != "EDIT":
bm.free()
if unselected_edges:
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
if selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
@@ -20,12 +20,17 @@ import bpy
import bmesh
import mathutils
import logging
import numpy as np
import ifcopenshell.api
import ifcopenshell.util.placement
import ifcopenshell.util.unit
import blenderbim.tool as tool
from blenderbim.bim.ifc import IfcStore
import blenderbim.bim.import_ifc as import_ifc
from math import pi, inf
from mathutils import Vector
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.model.decorator import ProfileDecorator
from blenderbim.bim.module.boundary.decorator import BoundaryDecorator
def get_boundaries_collection(blender_space):
@@ -38,6 +43,20 @@ def get_boundaries_collection(blender_space):
return boundaries_collection
def disable_editing_boundary_geometry(context):
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
element = tool.Ifc.get_entity(obj)
old_mesh = obj.data
loader = Loader()
obj.data = loader.create_mesh(element)
tool.Geometry.delete_data(old_mesh)
return {"FINISHED"}
class Loader:
def __init__(self):
self.ifc_file = None
@@ -376,3 +395,237 @@ class UpdateBoundaryGeometry(bpy.types.Operator):
settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object)
ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings)
return {"FINISHED"}
class EnableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_boundary_geometry"
bl_label = "Enable Editing Boundary Geometry"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
obj = context.active_object
element = tool.Ifc.get_entity(obj)
if element.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry"):
surface = element.ConnectionGeometry.SurfaceOnRelatingElement
tool.Model.import_surface(surface, obj)
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context))
if not bpy.app.background:
bpy.ops.wm.tool_set_by_id(tool.Blender.get_viewport_context(), name="bim.cad_tool")
return {"FINISHED"}
class EditBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_boundary_geometry"
bl_label = "Edit Boundary Geometry"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
ProfileDecorator.uninstall()
bpy.ops.object.mode_set(mode="OBJECT")
obj = context.active_object
element = tool.Ifc.get_entity(obj)
if element.ConnectionGeometry.is_a("IfcConnectionSurfaceGeometry"):
surface = tool.Model.export_surface(obj)
if not surface:
def msg(self, context):
self.layout.label(text="INVALID PROFILE")
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
ProfileDecorator.install(
context, exit_edit_mode_callback=lambda: disable_editing_boundary_geometry(context)
)
bpy.ops.object.mode_set(mode="EDIT")
return
old_surface = element.ConnectionGeometry.SurfaceOnRelatingElement
for inverse in tool.Ifc.get().get_inverse(old_surface):
ifcopenshell.util.element.replace_attribute(inverse, old_surface, surface)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_surface)
old_mesh = obj.data
loader = Loader()
obj.data = loader.create_mesh(element)
tool.Geometry.delete_data(old_mesh)
class DisableEditingBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.disable_editing_boundary_geometry"
bl_label = "Disable Editing Boundary Geometry"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.selected_objects
def _execute(self, context):
return disable_editing_boundary_geometry(context)
class ShowBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.show_boundaries"
bl_label = "Show Boundaries"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMBoundaryProperties
loader = Loader()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element or not getattr(element, "BoundedBy", None):
continue
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
element = tool.Ifc.get_entity(obj)
for rel in element.BoundedBy or []:
boundary_obj = loader.load_boundary(rel, obj)
new = props.boundaries.add()
new.obj = boundary_obj
BoundaryDecorator.install(bpy.context)
return {"FINISHED"}
class HideBoundaries(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.hide_boundaries"
bl_label = "Hide Boundaries"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
to_delete = set()
spaces = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcSpace"):
spaces.add(element)
elif element.is_a("IfcRelSpaceBoundary"):
spaces.add(element.RelatingSpace)
for element in spaces:
for boundary in element.BoundedBy or []:
boundary_obj = tool.Ifc.get_object(boundary)
if boundary_obj:
to_delete.add(boundary_obj)
for boundary_obj in to_delete:
tool.Ifc.unlink(obj=boundary_obj)
bpy.data.objects.remove(boundary_obj)
context.scene.BIMBoundaryProperties.boundaries.clear()
return {"FINISHED"}
class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_boundary"
bl_label = "Add Boundary"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
relating_space = None
related_building_element = None
relating_space_obj = None
related_building_element_obj = None
objs = context.selected_objects
if len(objs) == 2:
# The user may select two objects, a space and its related building element
for obj in objs:
element = tool.Ifc.get_entity(obj)
if not element:
continue
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = obj
else:
related_building_element = element
related_building_element_obj = obj
elif len(objs) == 1:
# Optionally the user may select just the space, and the building element shall be auto-detected
def msg(self, context):
self.layout.label(text="NO ACTIVE STOREY")
element = tool.Ifc.get_entity(objs[0])
if element.is_a("IfcSpace"):
relating_space = element
relating_space_obj = objs[0]
target = bpy.context.scene.cursor.location
collection = context.view_layer.active_layer_collection.collection
collection_obj = bpy.data.objects.get(collection.name)
if not collection_obj:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR")
return
for subelement in ifcopenshell.util.element.get_decomposition(spatial_element):
if not (subelement.is_a("IfcWall") or subelement.is_a("IfcSlab")):
continue
obj = tool.Ifc.get_object(subelement)
if obj:
raycast = obj.closest_point_on_mesh(obj.matrix_world.inverted() @ target, distance=0.1)
if raycast[0]:
related_building_element = subelement
related_building_element_obj = obj
break
if not relating_space or not related_building_element:
return
bm = bmesh.new()
bm.from_mesh(relating_space_obj.data)
bmesh.ops.dissolve_limit(bm, angle_limit=pi * 2 / 360, verts=bm.verts, edges=bm.edges)
target_distance = inf
target_face = None
for face in bm.faces:
centroid = relating_space_obj.matrix_world @ face.calc_center_median()
raycast = related_building_element_obj.closest_point_on_mesh(related_building_element_obj.matrix_world.inverted() @ centroid, distance=1)
if raycast[0]:
distance = (related_building_element_obj.matrix_world @ raycast[1] - centroid).length
if distance < target_distance:
target_face = face
target_distance = distance
if not target_face:
return
bm2 = bmesh.new()
bm2.faces.new([bm2.verts.new(v.co.copy()) for v in target_face.verts])
mesh = bpy.data.meshes.new("Boundary")
bm2.to_mesh(mesh)
bm2.free()
bm.free()
obj = bpy.data.objects.new("Boundary", mesh)
obj.matrix_world = relating_space_obj.matrix_world.copy()
bpy.context.scene.collection.objects.link(obj)
surface = tool.Model.export_surface(obj)
connection_geometry = tool.Ifc.get().createIfcConnectionSurfaceGeometry(surface)
boundary = tool.Ifc.run("root.create_entity", ifc_class="IfcRelSpaceBoundary")
boundary.RelatingSpace = relating_space
boundary.RelatedBuildingElement = related_building_element
boundary.ConnectionGeometry = connection_geometry
bpy.data.objects.remove(obj)
bpy.ops.bim.show_boundaries()
obj = tool.Ifc.get_object(boundary)
obj.select_set(True)
@@ -18,6 +18,7 @@
import bpy
from bpy.types import PropertyGroup
from blenderbim.bim.prop import ObjProperty
from bpy.props import (
PointerProperty,
StringProperty,
@@ -52,9 +53,13 @@ def element_filter(self, object):
return False
class BIMBoundaryProperties(PropertyGroup):
class BIMObjectBoundaryProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_space: PointerProperty(name="RelatingSpace", type=bpy.types.Object, poll=space_filter)
related_building_element: PointerProperty(name="RelatedBuildingElement", type=bpy.types.Object, poll=element_filter)
parent_boundary: PointerProperty(name="ParentBoundary", type=bpy.types.Object, poll=boundary_filter)
corresponding_boundary: PointerProperty(name="CorrespondingBoundary", type=bpy.types.Object, poll=boundary_filter)
class BIMBoundaryProperties(PropertyGroup):
boundaries: bpy.props.CollectionProperty(type=ObjProperty)
@@ -46,7 +46,7 @@ class CadTool(WorkSpaceTool):
("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
)
def draw_settings(context, layout, tool):
def draw_settings(context, layout, workspace_tool):
obj = context.active_object
if not obj or not obj.data:
return
@@ -54,14 +54,19 @@ class CadTool(WorkSpaceTool):
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_Q")
if obj.BIMObjectProperties.ifc_definition_id:
row.operator("bim.edit_extrusion_profile", text="Save Profile")
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
else:
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
element = tool.Ifc.get_entity(obj)
if element:
if element.is_a("IfcProfileDef"):
row.operator("bim.edit_arbitrary_profile", text="Save Profile")
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL")
elif element.is_a("IfcRelSpaceBoundary"):
row.operator("bim.edit_boundary_geometry", text="Save Profile")
row.operator("bim.disable_editing_boundary_geometry", text="", icon="CANCEL")
else:
row.operator("bim.edit_extrusion_profile", text="Save Profile")
row.operator("bim.align_view_to_profile", text="", icon="AXIS_FRONT")
row.operator("bim.disable_editing_extrusion_profile", text="", icon="CANCEL")
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -255,28 +260,30 @@ class CadHotkey(bpy.types.Operator):
bpy.ops.bim.cad_offset(distance=self.props.distance)
def hotkey_S_Q(self):
if tool.Ifc.get_entity(bpy.context.active_object):
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
element = tool.Ifc.get_entity(bpy.context.active_object)
if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE":
if element.is_a("IfcProfileDef"):
bpy.ops.bim.edit_arbitrary_profile()
elif element.is_a("IfcProfileDef"):
bpy.ops.bim.edit_boundary_geometry()
else:
bpy.ops.bim.edit_extrusion_profile()
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
bpy.ops.bim.edit_extrusion_axis()
elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS":
bpy.ops.bim.edit_extrusion_axis()
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and bpy.context.active_object.BIMRailingProperties.is_editing_path
):
bpy.ops.bim.finish_editing_railing_path()
elif (
(RailingData.is_loaded or not RailingData.load())
and RailingData.data["parameters"]
and bpy.context.active_object.BIMRailingProperties.is_editing_path
):
bpy.ops.bim.finish_editing_railing_path()
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.finish_editing_roof_path()
else:
bpy.ops.bim.edit_arbitrary_profile()
elif (
(RoofData.is_loaded or not RoofData.load())
and RoofData.data["parameters"]
and bpy.context.active_object.BIMRoofProperties.is_editing_path
):
bpy.ops.bim.finish_editing_roof_path()
def hotkey_S_R(self):
if self.is_profile():
@@ -32,6 +32,7 @@ def refresh():
AnnotationData.is_loaded = False
DecoratorData.data = {}
DecoratorData.cut_cache = {}
DecoratorData.layerset_cache = {}
class ProductAssignmentsData:
@@ -155,6 +156,7 @@ class DecoratorData:
# stores 1 type of data per object
data = {}
cut_cache = {}
layerset_cache = {}
# used by Ifc Annotations with ObjectType = "BATTING"
@classmethod
@@ -226,9 +228,11 @@ class DecoratorData:
props = obj.BIMTextProperties
# getting font size
classes = ifcopenshell.util.element.get_pset(element, "EPset_Annotation", "Classes")
pset_data = ifcopenshell.util.element.get_pset(element, "EPset_Annotation") or {}
# use `regular` as default
if classes:
# get font size
if classes := pset_data.get("Classes", None):
classes_split = classes.split()
# prioritize smaller font sizes just like in svg
font_size_type = next(
@@ -238,6 +242,9 @@ class DecoratorData:
font_size_type = "regular"
font_size = FONT_SIZES[font_size_type]
# get symbol
symbol = pset_data.get("Symbol", None)
# other attributes
props_literals = props.literals
props_literals_n = len(props.literals)
@@ -255,7 +262,7 @@ class DecoratorData:
literals_data.append(literal_data)
text_data = {"Literals": literals_data, "FontSize": font_size}
text_data = {"Literals": literals_data, "FontSize": font_size, "Symbol": symbol}
cls.data[obj.name] = text_data
return text_data
@@ -268,7 +268,7 @@ class BaseDecorator:
region = context.region
region3d = context.region_data
color = context.scene.DocProperties.decorations_colour
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
fmt = GPUVertFormat()
fmt.attr_add(id="pos", comp_type="F32", len=3, fetch_mode="FLOAT")
@@ -341,7 +341,7 @@ class BaseDecorator:
dpi = context.preferences.system.dpi
color = context.scene.DocProperties.decorations_colour
color = context.preferences.addons["blenderbim"].preferences.decorations_colour
ang = -Vector((1, 0)).angle_signed(text_dir)
cos = math.cos(ang)
@@ -437,12 +437,16 @@ class BaseDecorator:
props = obj.BIMTextProperties
text_data = props.get_text_edited_data() if props.is_editing else DecoratorData.get_ifc_text_data(obj)
literals_data = text_data["Literals"]
symbol = text_data["Symbol"]
text_scale = 1.0
# draw asterisk symbol to visually indicate that there are multiple literals
if len(literals_data) > 1:
# draw asterisk symbol to indicate that there is some symbol that's not shown in viewport
if symbol:
verts = [text_world_position]
idxs = [(0, 0)]
self.draw_lines(context, obj, verts, idxs)
# NOTE: for now we assume that scale is uniform
text_scale = obj.scale.x
line_i = 0
for literal_data in literals_data:
@@ -457,7 +461,7 @@ class BaseDecorator:
gap=0,
center=False,
vcenter=False,
font_size_mm=text_data["FontSize"],
font_size_mm=text_data["FontSize"] * text_scale,
line_no=line_i,
box_alignment=box_alignment,
)
@@ -1910,8 +1914,8 @@ class CutDecorator:
cls.installed = None
def __call__(self, context):
self.model_props = context.scene.BIMModelProperties
selected_elements_color = self.model_props.decorator_color_selected
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
all_vertices = []
all_edges = []
@@ -1993,6 +1997,8 @@ class CutDecorator:
if verts is False:
return None, None
elif verts:
return verts, edges
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.cut_cache[element.id()] = (False, False)
@@ -2004,14 +2010,35 @@ class CutDecorator:
return verts, edges
def slice_layersets(self, context, obj, cut_verts, cut_edges):
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
imat = obj.matrix_world.inverted()
element = tool.Ifc.get_entity(obj)
if "LAYER" not in (tool.Model.get_usage_type(element) or ""):
# Currently selected objects shall not be cached as they may be being moved / edited.
# If the camera is selected, we also disable the cache as the user may be moving the camera.
if obj.select_get() or context.scene.camera.select_get():
verts, edges = None, None
else:
verts, edges = DecoratorData.layerset_cache.get(element.id(), (None, None))
if verts is False:
return None, None
elif verts is not None:
return verts, edges
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
if tool.Model.get_usage_type(element) != "LAYER2":
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
layers = self.get_layer_data(element)
if not layers:
DecoratorData.layerset_cache[element.id()] = (False, False)
return None, None
minx = min([co[0] for co in obj.bound_box])
maxx = max([co[0] for co in obj.bound_box])
min_edge = [Vector((minx, layers["offset"])), Vector((maxx, layers["offset"]))]
@@ -2106,6 +2133,7 @@ class CutDecorator:
edges.extend([(i + offset, i + 1 + offset) for i in range(0, len(linestring) - 1)])
offset += len(linestring)
DecoratorData.layerset_cache[element.id()] = (verts, edges)
return verts, edges
def bisect_mesh(self, obj, bm, camera):
@@ -2140,6 +2168,10 @@ class CutDecorator:
usage = ifcopenshell.util.element.get_material(element)
offset = usage.OffsetFromReferenceLine * self.unit_scale
layer_set = usage.ForLayerSet
if len(layer_set.MaterialLayers) == 1:
return # No use slicing if there's only one layer
total_thickness = layer_set.TotalThickness
half_thickness = total_thickness / 2
min_layers = []
@@ -2191,22 +2223,30 @@ class CutDecorator:
def get_connections(self, wall, obj, centerline, min_edge, max_edge):
connections = {"ATEND": None, "ATSTART": None, "ATPATH": [], "MINPATH": [], "MAXPATH": []}
for rel in wall.ConnectedTo:
# How do you join to a non layered element? Not sure.
if tool.Model.get_usage_type(rel.RelatedElement) != "LAYER2":
continue
if rel.RelatingConnectionType == "ATPATH":
connections["ATPATH"].append(
self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
)
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections["ATPATH"].append(metadata)
else:
connections[rel.RelatingConnectionType] = self.get_connection_metadata(
obj, rel.RelatedElement, centerline, min_edge, max_edge
)
metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections[rel.RelatingConnectionType] = metadata
for rel in wall.ConnectedFrom:
if tool.Model.get_usage_type(rel.RelatingElement) != "LAYER2":
continue
# We only consider ATPATH since in this situation, we have the
# priority. The non-priority wall never has any layers that need to
# "turn a corner".
if rel.RelatedConnectionType == "ATPATH":
connections["ATPATH"].append(
self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
)
metadata = self.get_connection_metadata(obj, rel.RelatingElement, centerline, min_edge, max_edge)
if not metadata:
continue
connections["ATPATH"].append(metadata)
connections["ATPATH"] = sorted(connections["ATPATH"], key=lambda c: c["intersection"].x)
for connection in connections["ATPATH"]:
if connection["angle"] > 0:
@@ -2216,9 +2256,10 @@ class CutDecorator:
return connections
def get_connection_metadata(self, obj, rel_element, centerline, min_edge, max_edge):
imat = obj.matrix_world.inverted()
rel_obj = tool.Ifc.get_object(rel_element)
layers = self.get_layer_data(rel_element)
if not layers:
return
minx = min([co[0] for co in rel_obj.bound_box])
maxx = max([co[0] for co in rel_obj.bound_box])
rel_centerline = [
@@ -2227,7 +2268,11 @@ class CutDecorator:
]
rel_centerline = [obj.matrix_world.inverted() @ rel_obj.matrix_world @ v.to_3d() for v in rel_centerline]
rel_centerline = [v.to_2d() for v in rel_centerline]
intersection, _ = tool.Cad.intersect_edges(centerline, rel_centerline)
intersection = tool.Cad.intersect_edges(centerline, rel_centerline)
if intersection:
intersection, _ = intersection
else:
return
closest_centerline_point = tool.Cad.closest_vector(intersection, tuple(rel_centerline))
if closest_centerline_point == rel_centerline[1]:
rel_centerline = [rel_centerline[1], rel_centerline[0]]
@@ -829,6 +829,10 @@ class CreateDrawing(bpy.types.Operator):
ifc = tool.Ifc.get()
for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"):
element = ifc.by_guid(el.get("{http://www.ifcopenshell.org/ns}guid"))
# Architectural convention only merges these objects. E.g. pipe segments and fittings shouldn't merge.
if not element.is_a("IfcWall") and not element.is_a("IfcSlab"):
continue
classes = self.get_svg_classes(element)
classes.append("cut")
@@ -865,7 +869,11 @@ class CreateDrawing(bpy.types.Operator):
if is_closed_polygon:
el.getparent().remove(el)
merged_polygons = shapely.ops.unary_union(polygons)
try:
merged_polygons = shapely.ops.unary_union(polygons)
except:
print("Warning. Portions of the merge failed. Please report a bug!", polygons)
merged_polygons = polygons
if type(merged_polygons) == shapely.MultiPolygon:
merged_polygons = merged_polygons.geoms
@@ -312,9 +312,6 @@ class DocProperties(PropertyGroup):
ifc_files: CollectionProperty(name="IFCs", type=StrProperty)
drawing_styles: CollectionProperty(name="Drawing Styles", type=DrawingStyle)
should_draw_decorations: BoolProperty(name="Should Draw Decorations", update=toggleDecorations)
decorations_colour: FloatVectorProperty(
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
sheets_dir: StringProperty(default=os.path.join("sheets") + os.path.sep, name="Default Sheets Directory")
layouts_dir: StringProperty(default=os.path.join("layouts") + os.path.sep, name="Default Layouts Directory")
titleblocks_dir: StringProperty(
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
from blenderbim.bim.module.drawing.svgwriter import SvgWriter
import svgwrite
from odf.opendocument import load
@@ -23,6 +24,8 @@ from odf.table import Table, TableRow, TableColumn, TableCell
from odf.text import P
from odf.style import Style
FONT_SIZE = 4.13
class Scheduler:
def schedule(self, infile, outfile):
@@ -37,32 +40,87 @@ class Scheduler:
styles = {}
for style in doc.getElementsByType(Style):
name = style.getAttribute("name")
# looking for table-column-properties or table-row-properties
if not style.firstChild:
continue
styles[name] = {key[1]: value for key, value in style.firstChild.attributes.items()}
if style.firstChild.tagName in ["style:table-column-properties", "style:table-row-properties"]:
style_children = [style.firstChild]
else:
# for style:table-cell-properties we need to collect also text and paragraph properties
style_children = style.childNodes
styles[name] = {}
for child in style_children:
child_params = {key[1]: value for key, value in child.attributes.items()}
styles[name].update(child_params)
table = doc.getElementsByType(Table)[0]
# collect columns width
column_widths = []
for col in table.getElementsByType(TableColumn):
style_name = col.getAttribute("stylename")
repeat = col.getAttribute("numbercolumnsrepeated")
repeat = int(repeat) if repeat else 1
for i in range(0, repeat):
for i in range(repeat):
if not style_name or "column-width" not in styles[style_name]:
column_widths.append(50)
column_width = 50
else:
column_widths.append(self.convert_to_mm(styles[style_name]["column-width"]))
column_width = self.convert_to_mm(styles[style_name]["column-width"])
column_widths.append(column_width)
# collect rows height
row_heights = []
for col in table.getElementsByType(TableRow):
style_name = col.getAttribute("stylename")
repeat = col.getAttribute("numberrowsrepeated")
repeat = int(repeat) if repeat else 1
for i in range(repeat):
if not style_name or "row-height" not in styles[style_name]:
row_height = 6
else:
row_height = self.convert_to_mm(styles[style_name]["row-height"])
row_heights.append(row_height)
# draw table
y = self.margin
for tri, tr in enumerate(table.getElementsByType(TableRow)):
x = self.margin
height = 6
height = row_heights[tri]
tdi = 0
for td in tr.getElementsByType(TableCell):
column_span = td.getAttribute("numbercolumnsspanned")
column_span = int(column_span) if column_span else 1
repeat = td.getAttribute("numbercolumnsrepeated")
repeat = int(repeat) if repeat else 1
# figuring text alignment
style_name = td.getAttribute("stylename")
style = styles[style_name] if style_name else {}
if style_name and "vertical-align" in style and style["vertical-align"] != "automatic":
vertical_align = style["vertical-align"]
else:
vertical_align = "bottom"
if style_name and "text-align" in style and style["text-align"] != "automatic":
horizontal_align = style["text-align"]
horizontal_align = "middle" if horizontal_align == "center" else horizontal_align
else:
horizontal_align = "left"
if vertical_align == "middle" and horizontal_align == "middle":
box_alignment = "center"
else:
box_alignment = f"{vertical_align}-{horizontal_align}"
# for future use
wrap_text = style.get("wrap-option", None) == "wrap"
# drawing cells and text
for i in range(0, repeat):
width = column_widths[tdi]
width = sum(column_widths[tdi : tdi + int(column_span)])
self.svg.add(
self.svg.rect(
insert=(x, y),
@@ -72,9 +130,26 @@ class Scheduler:
)
value = td.getElementsByType(P)
if value:
self.add_text(value[0], x + self.padding, y + self.padding)
# figuring text position based on alignment
text_position = [0, 0]
if box_alignment.endswith("left"):
text_position[0] = x + self.padding
elif box_alignment.endswith("middle") or box_alignment == "center":
text_position[0] = x + width / 2
elif box_alignment.endswith("right"):
text_position[0] = x + width - self.padding
if box_alignment.startswith("top"):
text_position[1] = y + self.padding
elif box_alignment.startswith("middle") or box_alignment == "center":
text_position[1] = y + height / 2
elif box_alignment.startswith("bottom"):
text_position[1] = y + height - self.padding
self.add_text(value[0], *text_position, box_alignment=box_alignment)
x += width
tdi += 1
tdi += column_span
y += height
total_width = sum(column_widths) + (self.margin * 2)
self.svg["width"] = "{}mm".format(total_width)
@@ -82,20 +157,16 @@ class Scheduler:
self.svg["viewBox"] = "0 0 {} {}".format(total_width, y)
self.svg.save(pretty=True)
def add_text(self, text, x, y):
self.svg.add(
self.svg.text(
str(text).upper(),
insert=tuple((x, y)),
**{
"font-size": 4.13,
"font-family": "OpenGost Type B TT",
"text-anchor": "start",
"alignment-baseline": "baseline",
"dominant-baseline": "hanging",
}
)
)
def add_text(self, text, x, y, box_alignment="bottom-left"):
box_alignment_params = SvgWriter.get_box_alignment_parameters(box_alignment)
text_params = {
"font-size": FONT_SIZE,
"font-family": "OpenGost Type B TT",
"text-anchor": "start",
}
text_params.update(box_alignment_params)
text_tag = self.svg.text(str(text).upper(), insert=tuple((x, y)), **text_params)
self.svg.add(text_tag)
def convert_to_mm(self, value):
# XSL is what defines the units of measurements in ODF
@@ -234,7 +234,7 @@ class SvgWriter:
rl = ifcopenshell.util.geolocation.auto_z2e(tool.Ifc.get(), rl)
rl *= unit_scale
rl = "{:.3f}m".format(rl)
text_style = self.get_box_alignment_parameters("bottom-left")
text_style = SvgWriter.get_box_alignment_parameters("bottom-left")
self.svg.add(self.svg.text(f"RL +{rl}", insert=tuple(text_position), class_="SECTIONLEVEL", **text_style))
if tag:
self.svg.add(self.svg.text(tag, insert=(text_position[0], text_position[1] - 5), **text_style))
@@ -263,7 +263,7 @@ class SvgWriter:
"UP",
insert=tuple(text_position),
class_="STAIR",
**self.get_box_alignment_parameters("center"),
**SvgWriter.get_box_alignment_parameters("center"),
)
)
@@ -287,7 +287,7 @@ class SvgWriter:
)
line["stroke-dasharray"] = "12.5, 3, 3, 3"
axis_tag = tool.Ifc.get_entity(obj).Name
text_style = self.get_box_alignment_parameters("center")
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(
self.svg.text(
axis_tag,
@@ -557,7 +557,7 @@ class SvgWriter:
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
text_position = symbol_position_svg
text_style = self.get_box_alignment_parameters("center")
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(
self.svg.text(
reference_id,
@@ -590,7 +590,7 @@ class SvgWriter:
reference_id, sheet_id = self.get_reference_and_sheet_id_from_annotation(tool.Ifc.get_entity(obj))
text_position = symbol_position_svg
text_style = self.get_box_alignment_parameters("center")
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(
self.svg.text(
reference_id, insert=(text_position[0], text_position[1] - 2.5), class_="ELEVATION", **text_style
@@ -617,7 +617,8 @@ class SvgWriter:
return (reference_id, sheet_id)
return ("-", "-")
def get_box_alignment_parameters(self, box_alignment):
@staticmethod
def get_box_alignment_parameters(box_alignment):
"""Convenience method to get svg parameters for text alignment
in a readable way.
@@ -673,7 +674,6 @@ class SvgWriter:
text_dir = (self.camera.matrix_world.inverted().to_quaternion() @ text_dir_world_x_axis).to_2d().normalized()
angle = math.degrees(-text_dir.angle_signed(Vector((1, 0))))
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
classes = self.get_attribute_classes(text_obj)
classes_str = " ".join(classes)
@@ -687,7 +687,13 @@ class SvgWriter:
# if there is a symbol with template text fields
# then we just populate it's fields with the data from text literals
if template_text_fields:
symbol_xml.attrib["transform"] = f"translate({', '.join(map(str, text_position_svg))})"
# NOTE: for now we assume that scale is uniform
symbol_transform = (
f"translate({', '.join(map(str, text_position_svg))})"
f" rotate({angle})"
f" scale({text_obj.scale.x})"
)
symbol_xml.attrib["transform"] = symbol_transform
symbol_xml.attrib.pop("id")
# note: zip makes sure that we iterate over the shortest list
for field, text_literal in zip(template_text_fields, text_literals):
@@ -699,6 +705,7 @@ class SvgWriter:
if not symbol_svg or not template_text_fields:
self.svg.add(self.svg.use(f"#{symbol}", insert=text_position_svg))
transform = "rotate({}, {}, {})".format(angle, *text_position_svg)
for text_literal in text_literals:
# after pretty indentation some redundant spaces can occur in svg tags
# this is why we apply "font-size: 0;" to the text tag to remove those spaces
@@ -715,7 +722,7 @@ class SvgWriter:
text_tag = self.svg.text(
"",
**(attribs | {"filter": "url(#fill-background)"}) if add_fill_bg else attribs,
**self.get_box_alignment_parameters(text_literal.BoxAlignment),
**SvgWriter.get_box_alignment_parameters(text_literal.BoxAlignment),
)
self.svg.add(text_tag)
@@ -732,7 +739,6 @@ class SvgWriter:
add_text_tag(True)
add_text_tag(False)
def draw_break_annotations(self, obj):
x_offset = self.raw_width / 2
y_offset = self.raw_height / 2
@@ -790,7 +796,7 @@ class SvgWriter:
rl = "{:.3f}m".format(rl)
box_alignment = "bottom-left" if projected_points[0].x <= projected_points[-1].x else "bottom-right"
text_style = self.get_box_alignment_parameters(box_alignment)
text_style = SvgWriter.get_box_alignment_parameters(box_alignment)
self.svg.add(
self.svg.text(
"RL +{}".format(rl),
@@ -912,7 +918,7 @@ class SvgWriter:
text_offset = (text_position - center_position).xy.normalized() * 5
text_position += text_offset
text_style = self.get_box_alignment_parameters("center")
text_style = SvgWriter.get_box_alignment_parameters("center")
angle_text = abs(round(math.degrees(angle), 3))
if is_reflex:
angle_text = 360 - angle_text
@@ -973,7 +979,7 @@ class SvgWriter:
)
text_position += text_offset
text_style = self.get_box_alignment_parameters("center")
text_style = SvgWriter.get_box_alignment_parameters("center")
radius = (points[-1].co - points[-2].co).length
radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places)
tag = element.Description or f"R{radius}"
@@ -1046,7 +1052,7 @@ class SvgWriter:
)
text_position += text_offset
text_style = self.get_box_alignment_parameters("center")
text_style = SvgWriter.get_box_alignment_parameters("center")
self.svg.add(self.svg.text(tag, insert=tuple(text_position), class_="RADIUS", **text_style))
def draw_diameter_annotations(self, obj):
@@ -1146,7 +1152,7 @@ class SvgWriter:
text_format(text),
insert=text_position,
class_="DIMENSION",
**(text_kwargs | self.get_box_alignment_parameters(box_alignment)),
**(text_kwargs | SvgWriter.get_box_alignment_parameters(box_alignment)),
)
if not show_description_only:
@@ -38,7 +38,7 @@ class Helper:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bm.faces.ensure_lookup_table()
face = None
@@ -62,7 +62,7 @@ class Helper:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bm.faces.ensure_lookup_table()
potential_faces = []
@@ -90,7 +90,7 @@ class Helper:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bm.faces.ensure_lookup_table()
potential_faces = []
@@ -292,7 +292,7 @@ class Helper:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bm.faces.ensure_lookup_table()
potential_faces = []
@@ -389,7 +389,7 @@ class Helper:
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 1, verts=bm.verts, edges=bm.edges)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bm.faces.ensure_lookup_table()
faces = bm.faces
@@ -720,6 +720,11 @@ class OverrideModeSetEdit(bpy.types.Operator):
if context.active_object:
context.active_object.select_set(True)
element = tool.Ifc.get_entity(context.active_object)
if element.is_a("IfcRelSpaceBoundary"):
return bpy.ops.bim.enable_editing_boundary_geometry()
for obj in objs:
if not obj:
continue
@@ -834,6 +839,11 @@ class OverrideModeSetObject(bpy.types.Operator):
if not tool.Ifc.get():
return {"FINISHED"}
if context.active_object:
element = tool.Ifc.get_entity(context.active_object)
if element.is_a("IfcRelSpaceBoundary"):
return bpy.ops.bim.edit_boundary_geometry()
objs = context.selected_objects or [context.active_object]
self.edited_objs = []
@@ -88,7 +88,7 @@ class CommitChanges(bpy.types.Operator):
repo
and repo.head.is_detached
and (
not tool.is_valid_ref_format(props.new_branch_name)
not tool.IfcGit.is_valid_ref_format(props.new_branch_name)
or props.new_branch_name in [branch.name for branch in repo.branches]
)
):
@@ -96,6 +96,7 @@ classes = (
slab.ResetVertex,
slab.SetArcIndex,
space.GenerateSpace,
space.GenerateSpacesFromWalls,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -59,6 +59,7 @@ class AuthoringData:
cls.data["type_thumbnail"] = cls.type_thumbnail()
cls.data["is_voidable_element"] = cls.is_voidable_element()
cls.data["has_visible_openings"] = cls.has_visible_openings()
cls.data["has_visible_boundaries"] = cls.has_visible_boundaries()
cls.data["active_class"] = cls.active_class()
cls.data["active_material_usage"] = cls.active_material_usage()
cls.data["active_representation_type"] = cls.active_representation_type()
@@ -69,7 +70,10 @@ class AuthoringData:
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
names = [d.name() for d in declarations]
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialElementType")
if tool.Ifc.get_schema() == "IFC2X3":
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialStructureElementType")
else:
declaration = tool.Ifc.schema().declaration_by_name("IfcSpatialElementType")
declarations = ifcopenshell.util.schema.get_subtypes(declaration)
names.extend([d.name() for d in declarations])
@@ -161,6 +165,17 @@ class AuthoringData:
return True
return False
@classmethod
def has_visible_boundaries(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
if element:
if element.is_a("IfcRelSpaceBoundary"):
return True
for boundary in getattr(element, "BoundedBy", []):
if tool.Ifc.get_object(boundary):
return True
return False
@classmethod
def active_class(cls):
element = tool.Ifc.get_entity(bpy.context.active_object)
@@ -177,7 +192,7 @@ class AuthoringData:
def active_representation_type(cls):
if bpy.context.active_object:
representation = tool.Geometry.get_active_representation(bpy.context.active_object)
if representation:
if representation and representation.is_a("IfcShapeRepresentation"):
return representation.RepresentationType
@classmethod
@@ -82,14 +82,14 @@ class ProfileDecorator:
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
faces_color = transparent_color(self.model_props.decorator_color_special)
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
self.draw_batch("TRIS", vertices_coords, faces_color, face_indices)
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
self.model_props = context.scene.BIMModelProperties
selected_elements_color = self.model_props.decorator_color_selected
unselected_elements_color = self.model_props.decorator_color_unselected
special_elements_color = self.model_props.decorator_color_special
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
obj = context.active_object
@@ -214,8 +214,8 @@ class ProfileDecorator:
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR)
self.draw_batch("POINTS", special_vertices, self.model_props.decorator_color_special)
self.draw_batch("POINTS", selected_vertices, self.model_props.decorator_color_selected)
self.draw_batch("POINTS", special_vertices, special_elements_color)
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
# Draw arcs
arc_centroids = []
@@ -657,7 +657,7 @@ class ShowOpenings(Operator, tool.Ifc.Operator):
props = bpy.context.scene.BIMModelProperties
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
if not element:
if not element or not getattr(element, "HasOpenings", None):
continue
if tool.Ifc.is_moved(obj):
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
@@ -682,7 +682,6 @@ class HideOpenings(Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
props = bpy.context.scene.BIMModelProperties
to_delete = set()
for obj in context.selected_objects:
element = tool.Ifc.get_entity(obj)
@@ -694,7 +693,7 @@ class HideOpenings(Operator, tool.Ifc.Operator):
if opening_obj:
to_delete.add(opening_obj)
for opening_obj in to_delete:
tool.Ifc.unlink(element=opening, obj=opening_obj)
tool.Ifc.unlink(obj=opening_obj)
bpy.data.objects.remove(opening_obj)
tool.Model.clear_scene_openings()
return {"FINISHED"}
@@ -793,10 +792,10 @@ class DecorationsHandler:
batch.draw(shader)
def __call__(self, context):
self.model_props = context.scene.BIMModelProperties
selected_elements_color = self.model_props.decorator_color_selected
unselected_elements_color = self.model_props.decorator_color_unselected
special_elements_color = self.model_props.decorator_color_special
self.addon_prefs = context.preferences.addons["blenderbim"].preferences
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
def transparent_color(color, alpha=0.1):
color = [i for i in color]
@@ -54,7 +54,7 @@ class DumbProfileGenerator:
props = bpy.context.scene.BIMModelProperties
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.depth = props.extrusion_depth * self.unit_scale
self.depth = props.extrusion_depth
self.rotation = 0
self.location = Vector((0, 0, 0))
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
@@ -141,33 +141,6 @@ class BIMModelProperties(PropertyGroup):
type_class: bpy.props.EnumProperty(items=get_type_class, name="IFC Class", update=update_type_class)
type_predefined_type: bpy.props.EnumProperty(items=get_type_predefined_type, name="Predefined Type", default=None)
type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
decorator_color_selected: bpy.props.FloatVectorProperty(
name="Selected Elements Color",
subtype="COLOR",
default=(0.545, 0.863, 0, 1), # green
min=0.0,
max=1.0,
size=4,
description="Color of selected verts/edges (used in profile editing mode)",
)
decorator_color_unselected: bpy.props.FloatVectorProperty(
name="Not Selected Elements Color",
subtype="COLOR",
default=(1, 1, 1, 1), # green
min=0.0,
max=1.0,
size=4,
description="Color of not selected verts/edges (used in profile editing mode)",
)
decorator_color_special: bpy.props.FloatVectorProperty(
name="Special Elements Color",
subtype="COLOR",
default=(0.157, 0.565, 1, 1), # blue
min=0.0,
max=1.0,
size=4,
description="Color of special selected verts/edges (openings, preview verts/edges in roof editing, verts with arcs/circles in profile editing)",
)
class BIMArrayProperties(PropertyGroup):
@@ -499,12 +472,12 @@ class BIMRailingProperties(PropertyGroup):
("WALL_MOUNTED_HANDRAIL", "WALL_MOUNTED_HANDRAIL", ""),
)
cap_types = (
("none", "none", ""),
("TO_END_POST_AND_FLOOR", "TO_END_POST_AND_FLOOR", ""),
("TO_END_POST", "TO_END_POST", ""),
("TO_FLOOR", "TO_FLOOR", ""),
("TO_WALL", "TO_WALL", ""),
("180", "180", ""),
("to_wall", "to_wall", ""),
("to_floor", "to_floor", ""),
("to_end_post", "to_end_post", ""),
("to_end_post_and_floor", "to_end_post_and_floor", ""),
("NONE", "NONE", ""),
)
railing_added_previously: bpy.props.BoolProperty(default=False)
@@ -448,18 +448,30 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator):
if not props.roof_added_previously:
convert_property_group_from_si(props, skip_props=NON_SI_ROOF_PROPS)
# rejecting original roof shape to be safe
# taking into account only it's bounding box dimensions
if obj.dimensions.x == 0 or obj.dimensions.y == 0:
min_x, min_y = -5, -5
max_x, max_y = 5, 5
min_z = 0
else:
bbox = tool.Blender.get_object_bounding_box(obj)
min_x = bbox["min_x"]
min_y = bbox["min_y"]
max_x = bbox["max_x"]
max_y = bbox["max_y"]
min_z = bbox["min_z"]
roof_data = props.get_general_kwargs()
path_data = get_path_data(obj)
if not path_data:
path_data = {
"edges": [[0, 1], [1, 2], [2, 3], [3, 0]],
"verts": [
Vector([-5.0, -5.0, 0.0]) / si_conversion,
Vector([-5.0, 5.0, 0.0]) / si_conversion,
Vector([5.0, 5.0, 0.0]) / si_conversion,
Vector([5.0, -5.0, 0.0]) / si_conversion,
],
}
path_data = {
"edges": [[0, 1], [1, 2], [2, 3], [3, 0]],
"verts": [
Vector([min_x, min_y, min_z]) / si_conversion,
Vector([min_x, max_y, min_z]) / si_conversion,
Vector([max_x, max_y, min_z]) / si_conversion,
Vector([max_x, min_y, min_z]) / si_conversion,
],
}
roof_data["path_data"] = path_data
update_bbim_roof_pset(element, roof_data)
@@ -26,12 +26,20 @@ import blenderbim.tool as tool
import blenderbim.core.type
from math import pi
from mathutils import Vector, Matrix
from shapely import Polygon
class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_space"
bl_label = "Generate Space"
bl_options = {"REGISTER"}
bl_description = "Create a space from the cursor position. Move the cursor position into the desired position, select the right space collection and run the operator"
@classmethod
def poll(cls, context):
collection = context.view_layer.active_layer_collection.collection
collection_obj = bpy.data.objects.get(collection.name)
return tool.Ifc.get_entity(collection_obj)
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
@@ -166,3 +174,157 @@ class GenerateSpace(bpy.types.Operator, tool.Ifc.Operator):
(obj.matrix_world @ Vector((max_x, max_y, 0.0))).to_2d(),
],
}
class GenerateSpacesFromWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.generate_spaces_from_walls"
bl_label = "Generate Spaces From Walls"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Generate spaces from selected walls. The active object must be a wall."
@classmethod
def poll(cls, context):
active_obj = bpy.context.active_object
element = tool.Ifc.get_entity(active_obj)
return context.selected_objects and element.is_a("IfcWall")
def _execute(self, context):
# This only works based on a 2D plan only considering the standard
# walls (i.e. prismatic) in the active object storey.
# In order to run, the active objct must be a wall and
# have to be selected walls
props = context.scene.BIMModelProperties
active_obj = bpy.context.active_object
if not active_obj:
self.report({'ERROR'}, "No active object. Please select a wall")
return
element = None
element = tool.Ifc.get_entity(active_obj)
if not element.is_a("IfcWall"):
self.report({'ERROR'}, "The active object is not a wall. Please select a wall.")
return
collection = active_obj.users_collection[0]
collection_obj = bpy.data.objects.get(collection.name)
if not collection_obj:
self.report({'ERROR'}, "No collection found. Please insert one.")
return
spatial_element = tool.Ifc.get_entity(collection_obj)
if not spatial_element:
self.report({'ERROR'}, "The collection hasn't an ifc space entity. Please provide one.")
return
if not bpy.context.selected_objects:
self.report({'ERROR'}, "No selected objects found. Please select walls.")
return
x, y, z = active_obj.matrix_world.translation.xyz
mat = active_obj.matrix_world
h = active_obj.dimensions.z
selected_objects = bpy.context.selected_objects
boundary_elements = self.get_boundary_elements(selected_objects)
polys = self.get_polygons(boundary_elements)
converted_tolerance = self.get_converted_tolerance(tolerance = 0.03)
union = shapely.ops.unary_union(polys).buffer(converted_tolerance, cap_style = 2, join_style = 2)
i=0
for linear_ring in union.interiors:
poly = Polygon(linear_ring)
poly = poly.buffer(converted_tolerance, single_sided=True, cap_style = 2, join_style = 2)
bm = self.get_bmesh_from_polygon(poly, mat, h)
name = "Space" + str(i)
mesh = bpy.data.meshes.new(name = name)
bm.to_mesh(mesh)
bm.free()
obj = bpy.data.objects.new(name, mesh)
obj.matrix_world = mat
collection.objects.link(obj)
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
i+=1
return {"FINISHED"}
def get_boundary_elements(self, selected_objects):
boundary_elements = []
for obj in selected_objects:
subelement = tool.Ifc.get_entity(obj)
if subelement.is_a("IfcWall"):
boundary_elements.append(subelement)
return boundary_elements
def get_polygons(self, boundary_elements):
polys = []
for boundary_element in boundary_elements:
obj = tool.Ifc.get_object(boundary_element)
if not obj:
continue
points = []
base = self.get_obj_base_points(obj)
for index in ["low_left", "low_right", "high_right", "high_left"]:
point = base[index]
points.append(point)
polys.append(Polygon(points))
return polys
def get_obj_base_points(self, obj):
x_values = [(obj.matrix_world @ Vector(v)).x for v in obj.bound_box]
y_values = [(obj.matrix_world @ Vector(v)).y for v in obj.bound_box]
return {
"low_left": (x_values[0], y_values[0]),
"high_left": (x_values[3], y_values[3]),
"low_right": (x_values[4], y_values[4]),
"high_right": (x_values[7], y_values[7]),
}
def get_converted_tolerance(self, tolerance):
model = tool.Ifc.get()
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT")
prefix=getattr(project_unit, "Prefix", None)
converted_tolerance = ifcopenshell.util.unit.convert(
value = tolerance,
from_prefix = None,
from_unit = "METRE",
to_prefix = prefix,
to_unit = project_unit.Name,
)
return tolerance
def get_bmesh_from_polygon(self, poly, mat, h):
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
mat_invert = mat.inverted()
new_verts = [bm.verts.new(mat_invert @ Vector([v[0], v[1], 0])) for v in poly.exterior.coords[0:-1]]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.triangle_fill(bm, edges=bm.edges)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
bmesh.ops.recalc_face_normals(bm, faces = bm.faces)
return bm
@@ -132,9 +132,9 @@ class BIM_PT_authoring(Panel):
def draw(self, context):
row = self.layout.row(align=True)
row.operator("bim.align_wall", icon="ANCHOR_TOP", text="Ext.").align_type = "EXTERIOR"
row.operator("bim.align_wall", icon="ANCHOR_CENTER", text="C/L").align_type = "CENTERLINE"
row.operator("bim.align_wall", icon="ANCHOR_BOTTOM", text="Int.").align_type = "INTERIOR"
row.operator("bim.generate_space")
row = self.layout.row(align=True)
row.operator("bim.generate_spaces_from_walls")
class BIM_PT_array(bpy.types.Panel):
@@ -436,8 +436,8 @@ class DumbWallGenerator:
self.collection = bpy.context.view_layer.active_layer_collection.collection
self.collection_obj = bpy.data.objects.get(self.collection.name)
self.width = self.layers["thickness"]
self.height = props.extrusion_depth * self.unit_scale
self.length = props.length * self.unit_scale
self.height = props.extrusion_depth
self.length = props.length
self.rotation = 0.0
self.location = Vector((0, 0, 0))
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else radians(props.x_angle)
@@ -44,6 +44,7 @@ class BimTool(WorkSpaceTool):
# ("mesh.add_wall", {"type": 'LEFTMOUSE', "value": 'PRESS'}, {"properties": []}),
# ("bim.sync_modeling", {"type": 'MOUSEMOVE', "value": 'ANY'}, {"properties": []}),
("bim.hotkey", {"type": "A", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_A")]}),
("bim.hotkey", {"type": "B", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_B")]}),
("bim.hotkey", {"type": "C", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_C")]}),
("bim.hotkey", {"type": "E", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_E")]}),
("bim.hotkey", {"type": "F", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_F")]}),
@@ -57,6 +58,7 @@ class BimTool(WorkSpaceTool):
("bim.hotkey", {"type": "V", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_V")]}),
("bim.hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}),
("bim.hotkey", {"type": "Y", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_Y")]}),
("bim.hotkey", {"type": "B", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_B")]}),
("bim.hotkey", {"type": "D", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_D")]}),
("bim.hotkey", {"type": "E", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_E")]}),
("bim.hotkey", {"type": "O", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_O")]}),
@@ -260,6 +262,11 @@ class BimToolUI:
else:
row.operator("bim.add_potential_opening", text="Add Void")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
row.label(text="", icon="EVENT_B")
row.operator("bim.add_boundary", text="Add Boundary")
if AuthoringData.data["is_voidable_element"]:
if AuthoringData.data["has_visible_openings"]:
row.operator("bim.edit_openings", icon="CHECKMARK", text="")
@@ -274,11 +281,9 @@ class BimToolUI:
add_layout_hotkey_operator(cls.layout, "Mirror", "S_M", bpy.ops.bim.mirror_elements.__doc__)
cls.layout.row(align=True).label(text="Mode")
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Show / edit openings")
row = cls.layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="", icon="EVENT_D")
row.operator("bim.hotkey", text="Decomposition").hotkey = "A_D"
add_layout_hotkey_operator(cls.layout, "Void", "A_O", "Toggle openings")
add_layout_hotkey_operator(cls.layout, "Decomposition", "A_D", "Select decomposition")
add_layout_hotkey_operator(cls.layout, "Boundaries", "A_B", "Toggle boundaries")
@classmethod
def draw_header_interface(cls):
@@ -571,6 +576,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
elif self.active_material_usage == "PROFILE":
bpy.ops.bim.extend_profile(join_type="V")
def hotkey_S_B(self):
bpy.ops.bim.add_boundary()
def hotkey_S_O(self):
if len(bpy.context.selected_objects) == 2:
bpy.ops.bim.add_opening()
@@ -581,6 +589,14 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
self.props.y = self.y
self.props.z = self.z
def hotkey_A_B(self):
if not bpy.context.selected_objects:
return
if AuthoringData.data["has_visible_boundaries"]:
bpy.ops.bim.hide_boundaries()
else:
bpy.ops.bim.show_boundaries()
def hotkey_A_D(self):
if not bpy.context.selected_objects:
return
@@ -136,6 +136,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
props = context.scene.BIMProfileProperties
profile = tool.Ifc.get().by_id(props.active_profile_id)
obj = tool.Model.import_profile(profile)
tool.Ifc.link(profile, obj)
bpy.context.scene.collection.objects.link(obj)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode="EDIT")
@@ -49,9 +49,12 @@ classes = (
operator.DisableEditingResourceCostValue,
operator.CalculateResourceWork,
operator.ImportResources,
operator.EditProductivityData,
prop.Resource,
prop.BIMResourceProperties,
prop.BIMResourceTreeProperties,
prop.ISODuration,
prop.BIMResourceProductivity,
ui.BIM_PT_resources,
ui.BIM_UL_resources,
)
@@ -60,8 +63,10 @@ classes = (
def register():
bpy.types.Scene.BIMResourceProperties = bpy.props.PointerProperty(type=prop.BIMResourceProperties)
bpy.types.Scene.BIMResourceTreeProperties = bpy.props.PointerProperty(type=prop.BIMResourceTreeProperties)
bpy.types.Scene.BIMResourceProductivity = bpy.props.PointerProperty(type=prop.BIMResourceProductivity)
def unregister():
del bpy.types.Scene.BIMResourceProperties
del bpy.types.Scene.BIMResourceTreeProperties
del bpy.types.Scene.BIMResourceProductivity
@@ -52,9 +52,25 @@ class ResourceData:
if resource.BaseQuantity:
base_quantity = resource.BaseQuantity.get_info()
del base_quantity["Unit"]
results[resource.id()] = {"type": resource.is_a(), "BaseQuantity": base_quantity}
results[resource.id()] = {
"type": resource.is_a(),
"BaseQuantity": base_quantity,
}
if resource.is_a() in ["IfcLaborResource", "IfcConstructionEquipmentResource"]:
results[resource.id()]["Productivity"] = {}
productivity = cls.get_productivity(resource)
if productivity:
results[resource.id()]["Productivity"] = {
"QuantityProduced": ifcopenshell.util.resource.get_quantity_produced(productivity),
"TimeConsumed": ifcopenshell.util.resource.get_unit_consumed(productivity),
"QuantityProducedName": ifcopenshell.util.resource.get_quantity_produced_name(productivity),
}
return results
@classmethod
def get_productivity(cls, resource):
return ifcopenshell.util.resource.get_productivity(resource, should_inherit=False)
@classmethod
def cost_values(cls):
results = []
@@ -346,3 +346,13 @@ class ImportResources(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
def _execute(self, context):
core.import_resources(tool.Resource, file_path=self.filepath)
class EditProductivityData(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_productivity_data"
bl_description = "Apply"
bl_label = "Edit Productivity"
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.edit_productivity_pset(tool.Ifc, tool.Resource)
@@ -18,7 +18,9 @@
import bpy
import ifcopenshell.api
import ifcopenshell.util.resource
from blenderbim.bim.ifc import IfcStore
import blenderbim.tool as tool
import blenderbim.bim.module.pset.data
from blenderbim.bim.prop import StrProperty, Attribute
from bpy.types import PropertyGroup
@@ -44,13 +46,12 @@ def purge():
def updateResourceName(self, context):
props = context.scene.BIMResourceProperties
if not props.is_resource_update_enabled or self.name == "Unnamed":
if not props.is_resource_update_enabled:
return
self.file = IfcStore.get_file()
ifcopenshell.api.run(
tool.Ifc.run(
"resource.edit_resource",
self.file,
**{"resource": self.file.by_id(self.ifc_definition_id), "attributes": {"Name": self.name}},
resource=tool.Ifc.get().by_id(self.ifc_definition_id),
attributes={"Name": self.name},
)
if props.active_resource_id == self.ifc_definition_id:
attribute = props.resource_attributes.get("Name")
@@ -71,12 +72,42 @@ def get_quantity_types(self, context):
def update_active_resource_index(self, context):
blenderbim.bim.module.pset.data.refresh()
if self.should_show_productivity:
tool.Resource.load_productivity_data()
def updateResourceUsage(self, context):
props = context.scene.BIMResourceProperties
if not props.is_resource_update_enabled:
return
if self.schedule_usage == "":
return
resource = tool.Ifc.get().by_id(self.ifc_definition_id)
if not resource.Usage:
tool.Ifc.run(
"resource.add_resource_time",
resource=resource,
)
resource.Usage.ScheduleUsage = self.schedule_usage
blenderbim.bim.module.pset.data.refresh()
tool.Resource.load_resource_properties()
class ISODuration(PropertyGroup):
name: StringProperty(name="Name")
years: IntProperty(name="Years", default=0)
months: IntProperty(name="Months", default=0)
days: IntProperty(name="Days", default=0)
hours: IntProperty(name="Hours", default=0)
minutes: IntProperty(name="Minutes", default=0)
seconds: IntProperty(name="Seconds", default=0)
class Resource(PropertyGroup):
name: StringProperty(name="Name", update=updateResourceName)
ifc_definition_id: IntProperty(name="IFC Definition ID")
schedule_usage: FloatProperty(name="Schedule Usage")
schedule_usage: FloatProperty(name="Schedule Usage", update=updateResourceUsage)
has_children: BoolProperty(name="Has Children")
is_expanded: BoolProperty(name="Is Expanded")
level_index: IntProperty(name="Level Index")
@@ -113,3 +144,11 @@ class BIMResourceProperties(PropertyGroup):
quantity_types: EnumProperty(items=get_quantity_types, name="Quantity Types")
is_editing_quantity: BoolProperty(name="Is Editing Quantity")
quantity_attributes: CollectionProperty(name="Quantity Attributes", type=Attribute)
should_show_productivity: BoolProperty(name="Edit Productivity", update=update_active_resource_index)
class BIMResourceProductivity(PropertyGroup):
ifc_definition_id: IntProperty(name="IFC Definition ID")
quantity_consumed: CollectionProperty(name="Duration", type=ISODuration)
quantity_produced: FloatProperty(name="Quantity Produced")
quantity_produced_name: StringProperty(name="Quantity Produced Name")
@@ -65,7 +65,11 @@ class BIM_PT_resources(Panel):
self.props,
"active_resource_index",
)
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self.props, "should_show_productivity", icon="RECOVER_LAST")
if self.props.should_show_productivity:
self.draw_productivity_ui(context)
if self.props.active_resource_id and self.props.editing_resource_type == "ATTRIBUTES":
self.draw_editable_resource_attributes_ui()
elif self.props.active_resource_id and self.props.editing_resource_type == "QUANTITY":
@@ -75,6 +79,45 @@ class BIM_PT_resources(Panel):
elif self.props.active_resource_id and self.props.editing_resource_type == "USAGE":
self.draw_editable_resource_time_attributes_ui()
def draw_productivity_ui(self, context):
total_resources = len(self.tprops.resources)
if not total_resources or self.props.active_resource_index >= total_resources:
return
ifc_definition_id = self.tprops.resources[self.props.active_resource_index].ifc_definition_id
resource = ResourceData.data["resources"][ifc_definition_id]
if not resource["type"] in ["IfcConstructionEquipmentResource", "IfcLaborResource"]:
row = self.layout.row(align=True)
row.label(text="Resource type cannot have productivity data", icon="ERROR")
return
self.productivity_props = context.scene.BIMResourceProductivity
if resource["Productivity"]:
produtivitiy_rate_message = "Current Rate: {}/{}".format(
resource["Productivity"]["QuantityProduced"], resource["Productivity"]["TimeConsumed"]
)
row = self.layout.row()
row.alignment = "LEFT"
row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA")
else:
row = self.layout.row(align=True)
row.alignment = "LEFT"
produtivitiy_rate_message = "No productivity data found"
row.label(text=produtivitiy_rate_message, icon="ARMATURE_DATA")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self.productivity_props, "quantity_produced", text="Quantity Produced")
row.prop(self.productivity_props, "quantity_produced_name", text="Quantity Name")
row = self.layout.row()
row.alignment = "RIGHT"
self.draw_duration_property(self.productivity_props.quantity_consumed, row)
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.edit_productivity_data", text="Apply", icon="CHECKMARK")
def draw_resource_operators(self):
row = self.layout.row(align=True)
op = row.operator("bim.add_resource", text="Add SubContract", icon="TEXT")
@@ -111,8 +154,9 @@ class BIM_PT_resources(Panel):
if not self.props.active_resource_id:
if resource["type"] in ["IfcLaborResource", "IfcConstructionEquipmentResource"]:
op = row.operator("bim.calculate_resource_work", text="", icon="TEMP")
op.resource = ifc_definition_id
if resource["Productivity"]:
op = row.operator("bim.calculate_resource_work", text="", icon="TEMP")
op.resource = ifc_definition_id
row.operator("bim.enable_editing_resource_time", text="", icon="TIME").resource = ifc_definition_id
op = row.operator("bim.enable_editing_resource_base_quantity", text="", icon="PROPERTIES")
op.resource = ifc_definition_id
@@ -204,10 +248,20 @@ class BIM_PT_resources(Panel):
op.parent = parent_id
op.cost_value = cost_value_id
def draw_duration_property(self, duration_props, layout):
for duration_prop in duration_props:
if duration_prop.name == "BaseQuantityConsumed":
layout.label(text=duration_prop.name)
layout.prop(duration_prop, "years", text="Y")
layout.prop(duration_prop, "months", text="M")
layout.prop(duration_prop, "days", text="D")
layout.prop(duration_prop, "hours", text="H")
layout.prop(duration_prop, "minutes", text="Min")
layout.prop(duration_prop, "seconds", text="S")
class BIM_UL_resources(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
resource = ResourceData.data["resources"][item.ifc_definition_id]
icon_map = {
"IfcSubContractResource": "TEXT",
"IfcCrewResource": "COMMUNITY",
@@ -217,6 +271,7 @@ class BIM_UL_resources(UIList):
"IfcConstructionProductResource": "PACKAGE",
}
if item:
resource = ResourceData.data["resources"][item.ifc_definition_id]
props = context.scene.BIMResourceProperties
row = layout.row(align=True)
for i in range(0, item.level_index):
@@ -97,8 +97,11 @@ classes = (
operator.RemoveWorkPlan,
operator.RemoveWorkSchedule,
operator.RemoveWorkTime,
operator.ReorderTask,
operator.SelectTaskRelatedProducts,
operator.SelectTaskRelatedInputs,
operator.SelectWorkScheduleProducts,
operator.SelectUnassignedWorkScheduleProducts,
operator.SetTaskSortColumn,
operator.SetupDefaultTaskColumns,
operator.UnassignLagTime,
@@ -114,6 +117,7 @@ classes = (
operator.DisableEditingTaskAnimationColors,
operator.LoadProductTasks,
operator.HighlightTask,
operator.CreateBaseline,
prop.WorkPlan,
prop.BIMWorkPlanProperties,
prop.Task,
@@ -19,12 +19,15 @@
import bpy
import blenderbim.tool as tool
import ifcopenshell
from ifcopenshell.util.doc import get_predefined_type_doc
import ifcopenshell.util.date as dateutil
def refresh():
SequenceData.is_loaded = False
WorkPlansData.is_loaded = False
TaskICOMData.is_loaded = False
WorkScheduleData.is_loaded = False
class SequenceData:
@@ -34,6 +37,7 @@ class SequenceData:
@classmethod
def load(cls):
cls.data = {
"predefined_types": cls.get_work_schedule_types(),
"has_work_plans": cls.has_work_plans(),
"has_work_schedules": cls.has_work_schedules(),
"has_work_calendars": cls.has_work_calendars(),
@@ -221,8 +225,66 @@ class SequenceData:
if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl:
if rel.RelatingControl.is_a("IfcWorkCalendar"):
data["HasAssignmentsWorkCalendar"].append(rel.RelatingControl.id())
data["NestingIndex"] = None
for rel in task.Nests or []:
data["NestingIndex"] = rel.RelatedObjects.index(task)
cls.data["tasks"][task.id()] = data
@classmethod
def get_work_schedule_types(cls):
results = []
declaration = tool.Ifc().schema().declaration_by_name("IfcWorkSchedule")
version = tool.Ifc.get_schema()
for attribute in declaration.attributes():
if attribute.name() == "PredefinedType":
results.extend(
[
(e, e, get_predefined_type_doc(version, "IfcWorkSchedule", e))
for e in attribute.type_of_attribute().declared_type().enumeration_items()
]
)
break
return results
class WorkScheduleData:
data = {}
is_loaded = False
@classmethod
def load(cls):
cls.data = {
"can_have_baselines": cls.can_have_baselines(),
"active_work_schedule_baselines": cls.active_work_schedule_baselines(),
}
cls.is_loaded = True
@classmethod
def can_have_baselines(cls):
if not bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id:
return False
return (
tool.Ifc.get().by_id(bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id).PredefinedType
== "PLANNED"
)
@classmethod
def active_work_schedule_baselines(cls):
results = []
if not bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id:
return []
for rel in tool.Ifc.get().by_id(bpy.context.scene.BIMWorkScheduleProperties.active_work_schedule_id).Declares:
for work_schedule in rel.RelatedObjects:
if work_schedule.PredefinedType == "BASELINE":
results.append(
{
"id": work_schedule.id(),
"name": work_schedule.Name or "Unnamed",
"date": str(dateutil.ifc2datetime(work_schedule.CreationDate)),
}
)
return results
class WorkPlansData:
data = {}
@@ -0,0 +1,57 @@
let g;
function create_gantt_chart(json_data) {
g = new JSGantt.GanttChart(document.getElementById('GanttChartDIV'), 'week');
g.setOptions({
vCaptionType: 'Caption', // Set to Show Caption : None,Caption,Resource,Duration,Complete,
vQuarterColWidth: 36,
vDateTaskDisplayFormat: 'day dd month yyyy', // Shown in tool tip box
vDayMajorDateDisplayFormat: 'mon yyyy - Week ww',// Set format to dates in the "Major" header of the "Day" view
vWeekMinorDateDisplayFormat: 'dd mon', // Set format to display dates in the "Minor" header of the "Week" view
vLang: "en",
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
vFormatArr: ['Day', 'Week', 'Month', 'Quarter'], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
vShowRes: false, // Disable the resource column.
vShowComp: false, // Disable the completion column.
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
vAdditionalHeaders: {ifcduration: {title: 'Duration'}},
vUseToolTip: false, // Disable tooltips.
vTotalHeight: 900,
});
JSGantt.parseJSONString(json_data, g);
g.Draw();
}
function set_language(e){
lang = e && e.target ? e.target.value : "en";
g.setOptions({
vLang: lang,
});
g.Draw();
}
function setupPage(name){
let print_button = document.createElement("button");
print_button.id = "print-schedule";
print_button.innerHTML = "Print";
print_button.addEventListener("click", function(){
g.setTotalHeight("");
g.Draw();
var values = document.getElementById("print_page_size").value.split(",")
let css =
"@media print {\n @page {\n size: " + values[0] + "mm " + values[1] + "mm;\n }\n /* set gantt container to the same width as the page */\n .gantt {\n width: " + values[0] + "mm;\n }\n };";
g.printChart(values[0], values[1], css);
g.setTotalHeight(900);
g.Draw();
});
document.getElementById("print_options").appendChild(print_button);
// add a nice layout to show the schedule name
let schedule_name = document.createElement("h1");
// create string to say "Schedule: <name>"
let schedule_name_string = document.createTextNode("Schedule: " + name);
// add the string to the h1 element
schedule_name.appendChild(schedule_name_string);
document.getElementById("schedule-data").appendChild(schedule_name);
}
@@ -0,0 +1,122 @@
.gtaskcellwkend,
.gtaskcellcurrent,
.gminorheadingwkend {
background-color: #e1e1e1;
}
.gitemhighlight td {
background-color: #ffdaaa;
}
.gtaskblue {
background: #4281A4;
}
.gtaskred {
background: #C1666B;
}
.gtaskgreen {
background: #48A9A6;
}
.gtaskyellow {
background: #D4B483;
}
.gmainleft {
overflow: visible;
flex: 0 1 auto;
}
.gname.ggroupitem {
background-color: #4c92ec;
font-weight: bold;
}
.gtaskcellwkend,
.gtaskcellcurrent,
.gminorheadingwkend {
background-color: #e1e1e1;
}
.gitemhighlight td {
background-color: #ffdaaa;
}
.gtaskblue {
background: #4281A4;
}
.gtaskred {
background: #C1666B;
}
.gtaskgreen {
background: #48A9A6;
}
.gtaskyellow {
background: #D4B483;
}
.gmainleft {
resize: horizontal;
height:100%;
overflow-x: visible;
flex: 0 1 auto;
}
.gmainright {
height:100%;
overflow: auto;
font-size: 1.5rem;
}
.gcaption {
/* github style header 3 */
font-size: 0.75rem;
font-weight: 200;
line-height: 1.25;
color: black;
}
.gchartgrid {
background-color: transparent;
height: fit-content;
}
.gchartcontainer {
border: 1px solid #e1e1e1;
height: fit-content;
}
@media print {
.no-print {
display: none;
}
}
button, .sweet_button {
border: none;
background-color: #ffffff;
color: black;
padding: 0.5rem 1rem;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 1rem;
margin: 0.25rem 0.5rem;
cursor: pointer;
border-radius: 0.25rem;
/* add shadow */
box-shadow: 0 0 0.7rem rgba(95, 95, 95, 0.4);
}
h4 {
font-size: 1.2rem;
font-weight: 500;
line-height: 1.25;
color: black;
}
@@ -67,9 +67,11 @@ def parse_duration_as_blender_props(dt, simplify=True):
def simplify_duration(durations_attributes, duration_type, prop_name):
for item in durations_attributes:
if item.name == prop_name:
duration_props = item
duration_props = None
for collection in durations_attributes:
if collection.name == prop_name:
duration_props = collection
break
if duration_props and not duration_type or duration_type == "ELAPSEDTIME":
duration_string = "P{}Y{}M{}DT{}H{}M{}S".format(
duration_props.years if duration_props.years else 0,
@@ -114,11 +116,19 @@ def simplify_duration(durations_attributes, duration_type, prop_name):
hours, seconds = divmod(seconds_left, 3600)
minutes, seconds = divmod(seconds, 60)
return "P{}Y{}M{}DT{}H{}M{}S".format(
int(years),
int(months),
int(days),
int(hours),
int(minutes),
int(seconds),
)
duration_string = "P"
if years > 0:
duration_string += "{}Y".format(int(years))
if months > 0:
duration_string += "{}M".format(int(months))
if total_days > 0:
duration_string += "{}D".format(int(total_days))
if hours > 0 or minutes > 0 or seconds > 0:
duration_string += "T"
if hours > 0:
duration_string += "{}H".format(int(hours))
if minutes > 0:
duration_string += "{}M".format(int(minutes))
if seconds > 0:
duration_string += "{}S".format(int(seconds))
return duration_string
@@ -168,15 +168,14 @@ class EnableEditingWorkSchedule(bpy.types.Operator):
return {"FINISHED"}
class EnableEditingWorkScheduleTasks(bpy.types.Operator):
class EnableEditingWorkScheduleTasks(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.enable_editing_work_schedule_tasks"
bl_label = "Enable Editing Work Schedule Tasks"
bl_options = {"REGISTER", "UNDO"}
work_schedule: bpy.props.IntProperty()
def execute(self, context):
def _execute(self, context):
core.enable_editing_work_schedule_tasks(tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule))
return {"FINISHED"}
class LoadTaskProperties(bpy.types.Operator):
@@ -275,13 +274,13 @@ class EditTaskTime(bpy.types.Operator, tool.Ifc.Operator):
class EnableEditingTask(bpy.types.Operator):
bl_idname = "bim.enable_editing_task"
bl_label = "Enable Editing Task"
bl_idname = "bim.enable_editing_task_attributes"
bl_label = "Enable Editing Task Attributes"
bl_options = {"REGISTER", "UNDO"}
task: bpy.props.IntProperty()
def execute(self, context):
core.enable_editing_task(tool.Sequence, task=tool.Ifc.get().by_id(self.task))
core.enable_editing_task_attributes(tool.Sequence, task=tool.Ifc.get().by_id(self.task))
return {"FINISHED"}
@@ -1295,7 +1294,7 @@ class CopyTask(bpy.types.Operator, tool.Ifc.Operator):
class LoadProductTasks(bpy.types.Operator):
bl_idname = "bim.load_product_tasks"
bl_idname = "bim.load_product_related_tasks"
bl_label = "Load Product Tasks"
bl_options = {"REGISTER", "UNDO"}
@@ -1310,7 +1309,7 @@ class LoadProductTasks(bpy.types.Operator):
return True
def execute(self, context):
core.load_product_tasks(
core.load_product_related_tasks(
tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id)
)
return {"FINISHED"}
@@ -1327,3 +1326,66 @@ class HighlightTask(bpy.types.Operator):
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
class SelectWorkScheduleProducts(bpy.types.Operator):
bl_idname = "bim.select_work_schedule_products"
bl_label = "Select Work Schedule Products"
bl_options = {"REGISTER", "UNDO"}
work_schedule: bpy.props.IntProperty()
def execute(self, context):
r = core.select_work_schedule_products(
tool.Sequence, tool.Spatial, work_schedule=tool.Ifc.get().by_id(self.work_schedule)
)
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
class SelectUnassignedWorkScheduleProducts(bpy.types.Operator):
bl_idname = "bim.select_unassigned_work_schedule_products"
bl_label = "Select Unassigned Work Schedule Products"
bl_options = {"REGISTER", "UNDO"}
work_schedule: bpy.props.IntProperty()
def execute(self, context):
r = core.select_unassigned_work_schedule_products(tool.Ifc, tool.Sequence, tool.Spatial)
if isinstance(r, str):
self.report({"WARNING"}, r)
return {"FINISHED"}
class ReorderTask(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.reorder_task_nesting"
bl_label = "Reorder Nesting"
bl_options = {"REGISTER", "UNDO"}
new_index: bpy.props.IntProperty()
task: bpy.props.IntProperty()
def _execute(self, context):
r = core.reorder_task_nesting(
tool.Ifc, tool.Sequence, task=tool.Ifc.get().by_id(self.task), new_index=self.new_index
)
if isinstance(r, str):
self.report({"WARNING"}, r)
class CreateBaseline(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.create_baseline"
bl_label = "Create Schedule Baseline"
bl_options = {"REGISTER", "UNDO"}
work_schedule: bpy.props.IntProperty()
name: bpy.props.StringProperty()
def _execute(self, context):
core.create_baseline(
tool.Ifc, tool.Sequence, work_schedule=tool.Ifc.get().by_id(self.work_schedule), name=self.name
)
def draw(self, context):
layout = self.layout
layout.prop(self, "name", text="Baseline Name")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@@ -20,6 +20,8 @@ import bpy
import isodate
import ifcopenshell.api
import ifcopenshell.util.attribute
import blenderbim.tool as tool
import blenderbim.core.sequence as core
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.module.sequence.data import SequenceData
import blenderbim.bim.module.pset.data
@@ -37,7 +39,6 @@ from bpy.props import (
CollectionProperty,
)
taskcolumns_enum = []
tasktimecolumns_enum = []
@@ -108,7 +109,7 @@ def updateTaskName(self, context):
props = context.scene.BIMWorkScheduleProperties
if not props.is_task_update_enabled or self.name == "Unnamed":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"sequence.edit_task",
self.file,
@@ -124,7 +125,7 @@ def updateTaskIdentification(self, context):
props = context.scene.BIMWorkScheduleProperties
if not props.is_task_update_enabled or self.identification == "XXX":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
ifcopenshell.api.run(
"sequence.edit_task",
self.file,
@@ -160,7 +161,7 @@ def updateTaskTimeDateTime(self, context, startfinish):
if startfinish_value == "-":
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
try:
startfinish_datetime = parser.isoparse(startfinish_value)
@@ -208,7 +209,7 @@ def updateTaskDuration(self, context):
self.duration = "-"
return
self.file = IfcStore.get_file()
self.file = tool.Ifc.get()
task = self.file.by_id(self.ifc_definition_id)
if task.TaskTime:
task_time = task.TaskTime
@@ -228,6 +229,12 @@ def updateTaskDuration(self, context):
bpy.ops.bim.load_task_properties()
def get_schedule_predefined_types(self, context):
if not SequenceData.is_loaded:
SequenceData.load()
return SequenceData.data["predefined_types"]
def update_visualisation_start(self, context):
update_visualisation_start_finish(self, context, "visualisation_start")
@@ -278,6 +285,14 @@ def update_color_progress(self, context):
color[2] = color_progress.b
def update_sort_reversed(self, context):
if context.scene.BIMWorkScheduleProperties.active_work_schedule_id:
core.load_task_tree(
tool.Sequence,
work_schedule=tool.Ifc.get().by_id(context.scene.BIMWorkScheduleProperties.active_work_schedule_id),
)
class Task(PropertyGroup):
name: StringProperty(name="Name", update=updateTaskName)
identification: StringProperty(name="Identification", update=updateTaskIdentification)
@@ -335,6 +350,9 @@ class ISODuration(PropertyGroup):
class BIMWorkScheduleProperties(PropertyGroup):
work_schedule_predefined_types: EnumProperty(
items=get_schedule_predefined_types, name="Predefined Type", default=None
)
durations_attributes: CollectionProperty(name="Durations Attributes", type=ISODuration)
work_calendars: EnumProperty(items=getWorkCalendars, name="Work Calendars")
work_schedule_attributes: CollectionProperty(name="Work Schedule Attributes", type=Attribute)
@@ -346,12 +364,13 @@ class BIMWorkScheduleProperties(PropertyGroup):
active_task_id: IntProperty(name="Active Task Id")
task_attributes: CollectionProperty(name="Task Attributes", type=Attribute)
should_show_visualisation_ui: BoolProperty(name="Should Show Visualisation UI", default=False)
should_show_bar_visual_option: BoolProperty(name="Should Show Settings UI", default=False)
should_show_task_bar_selection: BoolProperty(name="Add to task bar", default=False)
should_show_snapshot_ui: BoolProperty(name="Should Show Snapshot UI", default=False)
should_show_column_ui: BoolProperty(name="Should Show Column UI", default=False)
columns: CollectionProperty(name="Columns", type=Attribute)
active_column_index: IntProperty(name="Active Column Index")
sort_column: StringProperty(name="Sort Column")
is_sort_reversed: BoolProperty(name="Is Sort Reversed")
is_sort_reversed: BoolProperty(name="Is Sort Reversed", update=update_sort_reversed)
column_types: EnumProperty(
items=[
("IfcTask", "IfcTask", ""),
@@ -404,6 +423,9 @@ class BIMWorkScheduleProperties(PropertyGroup):
product_output_tasks: CollectionProperty(name="Product Task Outputs", type=TaskProduct)
active_product_output_task_index: IntProperty(name="Active Product Output Task Index")
active_product_input_task_index: IntProperty(name="Active Product Input Task Index")
enable_reorder: BoolProperty(name="Enable Reorder", default=False)
show_task_operators: BoolProperty(name="Show Task Options", default=True)
should_show_schedule_baseline_ui: BoolProperty(name="Baselines", default=False)
class BIMTaskTreeProperties(PropertyGroup):
@@ -501,3 +523,4 @@ class BIMAnimationProperties(PropertyGroup):
description="color picker",
update=update_color_progress,
)
should_show_task_bar_options: BoolProperty(name="Show Task Bar Options", default=False)
@@ -21,7 +21,7 @@ import blenderbim.bim.helper
from bpy.types import Panel, UIList
from blenderbim.bim.ifc import IfcStore
from blenderbim.bim.helper import draw_attributes
from blenderbim.bim.module.sequence.data import WorkPlansData, SequenceData, TaskICOMData
from blenderbim.bim.module.sequence.data import WorkPlansData, WorkScheduleData, SequenceData, TaskICOMData
class BIM_PT_work_plans(Panel):
@@ -113,55 +113,101 @@ class BIM_PT_work_schedules(Panel):
def draw(self, context):
if not SequenceData.is_loaded:
SequenceData.load()
if not WorkScheduleData.is_loaded:
WorkScheduleData.load()
self.props = context.scene.BIMWorkScheduleProperties
self.tprops = context.scene.BIMTaskTreeProperties
self.animation_props = context.scene.BIMAnimationProperties
row = self.layout.row()
if SequenceData.data["has_work_schedules"]:
row.label(
text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]),
icon="TEXT",
)
else:
row.label(text="No Work Schedules found.", icon="TEXT")
row.operator("bim.add_work_schedule", text="", icon="ADD")
if not self.props.active_work_schedule_id:
row = self.layout.row()
if SequenceData.data["has_work_schedules"]:
row.label(
text="{} Work Schedules Found".format(SequenceData.data["number_of_work_schedules_loaded"]),
icon="TEXT",
)
else:
row.label(text="No Work Schedules found.", icon="TEXT")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.prop(self.props, "work_schedule_predefined_types")
row.operator("bim.add_work_schedule", text="Add new", icon="ADD")
for work_schedule_id, work_schedule in SequenceData.data["work_schedules"].items():
self.draw_work_schedule_ui(work_schedule_id, work_schedule)
def draw_work_schedule_ui(self, work_schedule_id, work_schedule):
row = self.layout.row(align=True)
row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON")
if not work_schedule["PredefinedType"] == "BASELINE":
row = self.layout.row(align=True)
row.label(text=work_schedule["Name"] or "Unnamed", icon="LINENUMBERS_ON")
if self.props.active_work_schedule_id == work_schedule_id:
if self.props.editing_type == "WORK_SCHEDULE":
row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK")
elif self.props.editing_type == "TASKS":
grid = self.layout.grid_flow(columns=2, even_columns=True)
if self.props.active_work_schedule_id == work_schedule_id:
if self.props.editing_type == "WORK_SCHEDULE":
row.operator("bim.edit_work_schedule", text="", icon="CHECKMARK")
elif self.props.editing_type == "TASKS":
row.prop(self.props, "should_show_column_ui", text="", icon="SHORTDISPLAY")
row.prop(self.props, "should_show_visualisation_ui", text="", icon="CAMERA_STEREO")
row.operator("bim.generate_gantt_chart", text="", icon="NLA").work_schedule = work_schedule_id
row.operator("bim.recalculate_schedule", text="", icon="FILE_REFRESH").work_schedule = work_schedule_id
row.operator("bim.add_summary_task", text="", icon="ADD").work_schedule = work_schedule_id
row.operator("bim.disable_editing_work_schedule", text="", icon="CANCEL")
else:
row.operator(
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
).work_schedule = work_schedule_id
row.operator(
"bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL"
).work_schedule = work_schedule_id
row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id
col = grid.column()
row1 = col.row(align=True)
row1.alignment = "LEFT"
row1.label(text="Schedule tools")
row1 = col.row(align=True)
row1.alignment = "RIGHT"
row1.operator(
"bim.generate_gantt_chart", text="Generate Gantt", icon="NLA"
).work_schedule = work_schedule_id
row1.operator(
"bim.recalculate_schedule", text="Re-calculate Schedule", icon="FILE_REFRESH"
).work_schedule = work_schedule_id
row2 = col.row(align=True)
row2.alignment = "RIGHT"
row2.operator(
"bim.select_work_schedule_products", text="Select Assigned", icon="RESTRICT_SELECT_OFF"
).work_schedule = work_schedule_id
row2.operator(
"bim.select_unassigned_work_schedule_products",
text="Select Unassigned",
icon="RESTRICT_SELECT_OFF",
).work_schedule = work_schedule_id
if WorkScheduleData.data["can_have_baselines"]:
row3 = col.row()
row3.alignment = "RIGHT"
row3.prop(self.props, "should_show_schedule_baseline_ui", icon="RESTRICT_INSTANCED_OFF")
col = grid.column()
row1 = col.row(align=True)
row1.alignment = "LEFT"
row1.label(text="Settings")
row1 = col.row(align=True)
row1.alignment = "RIGHT"
row1.prop(self.props, "should_show_column_ui", text="Schedule Columns", icon="SHORTDISPLAY")
row2 = col.row(align=True)
row2.prop(
self.props, "should_show_visualisation_ui", text="Animation Options", icon="CAMERA_STEREO"
)
row2.prop(self.props, "should_show_snapshot_ui", text="Snapshot Options", icon="CAMERA_STEREO")
row.operator("bim.disable_editing_work_schedule", text="Disable editing", icon="CANCEL")
else:
row.operator(
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
).work_schedule = work_schedule_id
row.operator(
"bim.enable_editing_work_schedule", text="", icon="GREASEPENCIL"
).work_schedule = work_schedule_id
row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = work_schedule_id
if self.props.active_work_schedule_id == work_schedule_id:
if self.props.editing_type == "WORK_SCHEDULE":
self.draw_editable_work_schedule_ui()
elif self.props.editing_type == "TASKS":
if self.props.should_show_column_ui:
if self.props.active_work_schedule_id == work_schedule_id:
if self.props.editing_type == "WORK_SCHEDULE":
self.draw_editable_work_schedule_ui()
elif self.props.editing_type == "TASKS":
self.draw_baseline_ui(work_schedule_id)
self.draw_column_ui()
if self.props.should_show_visualisation_ui:
self.draw_visualisation_ui()
self.draw_editable_task_ui(work_schedule_id)
if self.props.should_show_visualisation_ui:
self.draw_visualisation_ui()
if self.props.should_show_snapshot_ui:
self.draw_snapshot_ui()
self.draw_editable_task_ui(work_schedule_id)
else:
self.draw_readonly_work_schedule_ui(work_schedule_id)
def draw_task_operators(self):
row = self.layout.row(align=True)
@@ -178,18 +224,30 @@ class BIM_PT_work_schedules(Panel):
row.operator("bim.edit_task", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_task", text="", icon="CANCEL")
else:
row.prop(self.props, "should_show_bar_visual_option", text="", icon="NLA_PUSHDOWN")
row.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING").task = ifc_definition_id
row.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
row.operator("bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO").task = ifc_definition_id
row.operator("bim.enable_editing_task", text="", icon="GREASEPENCIL").task = ifc_definition_id
row.operator("bim.add_task", text="", icon="ADD").task = ifc_definition_id
row.operator("bim.duplicate_task", text="", icon="DUPLICATE").task = ifc_definition_id
row.operator("bim.remove_task", text="", icon="X").task = ifc_definition_id
row.prop(self.props, "show_task_operators", text="Edit", icon="GREASEPENCIL")
if self.props.show_task_operators:
row2 = self.layout.row(align=True)
row2.alignment = "RIGHT"
row2.prop(self.props, "enable_reorder", text="", icon="SORTALPHA")
row2.operator("bim.enable_editing_task_sequence", text="", icon="TRACKING").task = ifc_definition_id
row2.operator("bim.enable_editing_task_time", text="", icon="TIME").task = ifc_definition_id
row2.operator(
"bim.enable_editing_task_calendar", text="", icon="VIEW_ORTHO"
).task = ifc_definition_id
row2.operator(
"bim.enable_editing_task_attributes", text="", icon="GREASEPENCIL"
).task = ifc_definition_id
row.operator("bim.add_task", text="Add", icon="ADD").task = ifc_definition_id
row.operator("bim.duplicate_task", text="Copy", icon="DUPLICATE").task = ifc_definition_id
row.operator("bim.remove_task", text="Delete", icon="X").task = ifc_definition_id
def draw_column_ui(self):
row = self.layout.row(align=True)
row.operator("bim.setup_default_task_columns", text="Show Default Columns", icon="ANCHOR_BOTTOM")
if not self.props.should_show_column_ui:
return
row = self.layout.row()
row.operator("bim.setup_default_task_columns", text="Add Default Columns", icon="ANCHOR_BOTTOM")
row.alignment = "RIGHT"
row = self.layout.row(align=True)
row.prop(self.props, "column_types", text="")
column_type = self.props.column_types
@@ -226,7 +284,7 @@ class BIM_PT_work_schedules(Panel):
op.work_schedule = self.props.active_work_schedule_id
row = self.layout.row(align=True)
row.label(text="Animation Options")
row.label(text="Speed Settings")
row = self.layout.row(align=True)
row.prop(self.props, "speed_types", text="")
if self.props.speed_types == "FRAME_SPEED":
@@ -237,21 +295,50 @@ class BIM_PT_work_schedules(Panel):
row.prop(self.props, "speed_real_duration", text="")
elif self.props.speed_types == "MULTIPLIER_SPEED":
row.prop(self.props, "speed_multiplier", text="")
row = self.layout.row(align=True)
row.label(text="Display Settings")
row = self.layout.row(align=True)
if not self.animation_props.is_editing:
op = row.operator(
"bim.enable_editing_task_animation_colors", text="Customize Animation Colors", icon="SEQUENCE_COLOR_04"
"bim.enable_editing_task_animation_colors", text="Customize Object Colors", icon="SEQUENCE_COLOR_04"
)
else:
op = row.operator(
"bim.disable_editing_task_animation_colors", text="Hide Animation Colors", icon="SEQUENCE_COLOR_01"
"bim.disable_editing_task_animation_colors", text="Hide Object Colors", icon="SEQUENCE_COLOR_01"
)
row.prop(self.animation_props, "should_show_task_bar_options", text="Task Bar", icon="NLA_PUSHDOWN")
if self.animation_props.should_show_task_bar_options:
row = self.layout.row()
row.label(text="Task Bar Options", icon="NLA_PUSHDOWN")
row.alignment = "LEFT"
row = self.layout.row(align=True)
row.prop(self.props, "should_show_task_bar_selection", text="Enable Selection", icon="NLA_PUSHDOWN")
row.operator("bim.add_task_bars", text="Generate bars", icon="NLA_PUSHDOWN")
grid = self.layout.grid_flow(columns=2, even_columns=True)
# Column1
col = grid.column()
row = col.row(align=True)
row.prop(self.animation_props, "color_progress")
row = col.row(align=True)
row.prop(self.animation_props, "color_full")
if self.animation_props.is_editing:
self.draw_visualisation_settings_ui()
row = self.layout.row()
row = self.layout.row(align=True)
op = row.operator("bim.visualise_work_schedule_date_range", text="Create Animation", icon="OUTLINER_OB_CAMERA")
op.work_schedule = self.props.active_work_schedule_id
def draw_snapshot_ui(self):
row = self.layout.row(align=True)
row.label(text="Create Construction Snapshot:")
row = self.layout.row(align=True)
op = row.operator("bim.datepicker", text=self.props.visualisation_start or "Date", icon="REW")
op.target_prop = "BIMWorkScheduleProperties.visualisation_start"
op = row.operator("bim.visualise_work_schedule_date", text="Create SnapShot", icon="RESTRICT_RENDER_OFF")
op.work_schedule = self.props.active_work_schedule_id
@@ -286,6 +373,13 @@ class BIM_PT_work_schedules(Panel):
draw_attributes(self.props.work_schedule_attributes, self.layout)
def draw_editable_task_ui(self, work_schedule_id):
row = self.layout.row(align=True)
row.label(text="")
row = self.layout.row(align=True)
row.alignment = "RIGHT"
row.operator("bim.add_summary_task", text="Add Summary Task", icon="ADD").work_schedule = work_schedule_id
row.operator("bim.expand_all_tasks", text="Expand All")
row.operator("bim.contract_all_tasks", text="Contract All")
self.draw_task_operators()
self.layout.template_list(
"BIM_UL_tasks",
@@ -295,9 +389,6 @@ class BIM_PT_work_schedules(Panel):
self.props,
"active_task_index",
)
row = self.layout.row()
row.operator("bim.expand_all_tasks", text="Expand All")
row.operator("bim.contract_all_tasks", text="Contract All")
if self.props.active_task_id and self.props.editing_task_type == "ATTRIBUTES":
self.draw_editable_task_attributes_ui()
elif self.props.active_task_id and self.props.editing_task_type == "CALENDAR":
@@ -381,6 +472,48 @@ class BIM_PT_work_schedules(Panel):
def draw_editable_task_time_attributes_ui(self):
blenderbim.bim.helper.draw_attributes(self.props.task_time_attributes, self.layout)
def draw_baseline_ui(self, work_schedule_id):
if not self.props.should_show_schedule_baseline_ui:
return
row3 = self.layout.row()
row3.alignment = "RIGHT"
row3.operator("bim.create_baseline", text="Add Baseline", icon="ADD").work_schedule = work_schedule_id
if WorkScheduleData.data["active_work_schedule_baselines"]:
for baseline in WorkScheduleData.data["active_work_schedule_baselines"]:
baseline_row = self.layout.row()
baseline_row.alignment = "RIGHT"
baseline_row.label(
text="{} @ {}".format(baseline["name"], baseline["date"]), icon="RESTRICT_INSTANCED_OFF"
)
baseline_row.operator("bim.remove_work_schedule", text="", icon="X").work_schedule = baseline["id"]
baseline_row.operator(
"bim.enable_editing_work_schedule_tasks", text="", icon="ACTION"
).work_schedule = baseline["id"]
def draw_readonly_work_schedule_ui(self, work_schedule_id):
if self.props.active_work_schedule_id == work_schedule_id:
row = self.layout.row()
row.alignment = "RIGHT"
row.operator("bim.disable_editing_work_schedule", text="Disable editing", icon="CANCEL")
grid = self.layout.grid_flow(columns=2, even_columns=True)
col = grid.column()
row1 = col.row(align=True)
row1.alignment = "LEFT"
row1.label(text="Settings")
row1 = col.row(align=True)
row1.alignment = "RIGHT"
row1.prop(self.props, "should_show_column_ui", text="Schedule Columns", icon="SHORTDISPLAY")
row2 = col.row(align=True)
if self.props.editing_type == "TASKS":
self.draw_column_ui()
self.layout.template_list(
"BIM_UL_tasks",
"",
self.tprops,
"tasks",
self.props,
"active_task_index",
)
class BIM_PT_task_icom(Panel):
bl_label = "IFC Task ICOM"
@@ -430,7 +563,7 @@ class BIM_PT_task_icom(Panel):
input_id = self.props.task_inputs[self.props.active_task_input_index].ifc_definition_id
op.related_object = input_id
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="")
op = row2.operator("bim.select_task_related_inputs", icon="RESTRICT_SELECT_OFF", text="Select")
op.task = task.ifc_definition_id
row2 = col.row()
@@ -519,7 +652,7 @@ class BIM_UL_task_resources(UIList):
if item:
row = layout.row(align=True)
row.prop(item, "name", emboss=False, text="")
row.prop(item, "schedule_usage", emboss=False, text="")
row.label(text=str(item.schedule_usage))
class BIM_UL_animation_colors(UIList):
@@ -583,7 +716,7 @@ class BIM_UL_tasks(UIList):
text="",
emboss=False,
)
if self.props.should_show_bar_visual_option:
if self.props.should_show_task_bar_selection:
row.prop(
item,
"has_bar_visual",
@@ -591,6 +724,8 @@ class BIM_UL_tasks(UIList):
text="",
emboss=False,
)
if self.props.enable_reorder:
self.draw_order_operator(row, item.ifc_definition_id)
if self.props.active_task_id:
if self.props.editing_task_type == "SEQUENCE" and self.props.active_task_id != item.ifc_definition_id:
if item.is_predecessor:
@@ -605,6 +740,18 @@ class BIM_UL_tasks(UIList):
op = row.operator("bim.assign_successor", text="", icon="TRACKING_FORWARDS", emboss=False)
op.task = item.ifc_definition_id
def draw_order_operator(self, row, ifc_definition_id):
task = SequenceData.data["tasks"][ifc_definition_id]
if task["NestingIndex"] is not None:
if task["NestingIndex"] == 0:
op = row.operator("bim.reorder_task_nesting", icon="TRIA_DOWN", text="")
op.task = ifc_definition_id
op.new_index = task["NestingIndex"] + 1
elif task["NestingIndex"] > 0:
op = row.operator("bim.reorder_task_nesting", icon="TRIA_UP", text="")
op.task = ifc_definition_id
op.new_index = task["NestingIndex"] - 1
def draw_hierarchy(self, row, item):
for i in range(0, item.level_index):
row.label(text="", icon="BLANK1")
@@ -825,7 +972,7 @@ class BIM_PT_4D_Tools(Panel):
def draw(self, context):
self.props = context.scene.BIMWorkScheduleProperties
row = self.layout.row()
row.operator("bim.load_product_tasks", text="Load Tasks", icon="FILE_REFRESH")
row.operator("bim.load_product_related_tasks", text="Load Tasks", icon="FILE_REFRESH")
grid = self.layout.grid_flow(columns=2, even_columns=True)
col1 = grid.column()
+34 -4
View File
@@ -120,6 +120,36 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
name="Should Make A Cha-Ching Sound When Project Costs Updates", default=False
)
lock_grids_on_import: BoolProperty(name="Should Lock Grids By Default", default=True)
decorations_colour: bpy.props.FloatVectorProperty(
name="Decorations Colour", subtype="COLOR", default=(1, 1, 1, 1), min=0.0, max=1.0, size=4
)
decorator_color_selected: bpy.props.FloatVectorProperty(
name="Selected Elements Color",
subtype="COLOR",
default=(0.545, 0.863, 0, 1), # green
min=0.0,
max=1.0,
size=4,
description="Color of selected verts/edges (used in profile editing mode)",
)
decorator_color_unselected: bpy.props.FloatVectorProperty(
name="Not Selected Elements Color",
subtype="COLOR",
default=(1, 1, 1, 1), # white
min=0.0,
max=1.0,
size=4,
description="Color of not selected verts/edges (used in profile editing mode)",
)
decorator_color_special: bpy.props.FloatVectorProperty(
name="Special Elements Color",
subtype="COLOR",
default=(0.157, 0.565, 1, 1), # blue
min=0.0,
max=1.0,
size=4,
description="Color of special selected verts/edges (openings, preview verts/edges in roof editing, verts with arcs/circles in profile editing)",
)
def draw(self, context):
layout = self.layout
@@ -172,13 +202,13 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
row.prop(context.scene.BIMModelProperties, "occurrence_name_function")
row = self.layout.row()
row.prop(context.scene.DocProperties, "decorations_colour")
row.prop(self, "decorations_colour")
row = self.layout.row()
row.prop(context.scene.BIMModelProperties, "decorator_color_selected")
row.prop(self, "decorator_color_selected")
row = self.layout.row()
row.prop(context.scene.BIMModelProperties, "decorator_color_unselected")
row.prop(self, "decorator_color_unselected")
row = self.layout.row()
row.prop(context.scene.BIMModelProperties, "decorator_color_special")
row.prop(self, "decorator_color_special")
row = self.layout.row(align=True)
row.prop(context.scene.BIMProperties, "schema_dir")
@@ -170,3 +170,15 @@ def unassign_resource(ifc, spatial, resource=None, products=None):
products = spatial.get_selected_products()
for product in products:
ifc.run("resource.unassign_resource", relating_resource=resource, related_object=product)
def edit_productivity_pset(ifc, resource_tool):
resource = resource_tool.get_highlighted_resource()
if resource is None:
return
productivity = resource_tool.get_productivity(resource)
if productivity:
pset = ifc.get().by_id(productivity["id"])
else:
pset = ifc.run("pset.add_pset", product=resource, name="EPset_Productivity")
ifc.run("pset.edit_pset", pset=pset, properties=resource_tool.get_productivity_attributes())
+56 -16
View File
@@ -74,47 +74,47 @@ def enable_editing_work_schedule(sequence, work_schedule=None):
def enable_editing_work_schedule_tasks(sequence, work_schedule=None):
sequence.enable_editing_work_schedule_tasks(work_schedule)
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def create_task_tree(sequence, work_schedule):
sequence.create_task_tree(work_schedule)
def load_task_tree(sequence, work_schedule):
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def expand_task(sequence, task=None):
sequence.expand_task(task)
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def expand_all_tasks(sequence):
sequence.expand_all_tasks()
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def contract_task(sequence, task=None):
sequence.contract_task(task)
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def contract_all_tasks(sequence):
sequence.contract_all_tasks()
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def remove_task(ifc, sequence, task=None):
ifc.run("sequence.remove_task", task=task)
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
sequence.disable_selecting_deleted_task()
@@ -129,22 +129,26 @@ def disable_editing_work_schedule(sequence):
def add_summary_task(ifc, sequence, work_schedule=None):
ifc.run("sequence.add_task", work_schedule=work_schedule)
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def add_task(ifc, sequence, parent_task=None):
ifc.run("sequence.add_task", parent_task=parent_task)
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def enable_editing_task(sequence, task=None):
sequence.load_task_attributes(task)
sequence.enable_editing_task(task)
def enable_editing_task_attributes(sequence, task=None):
sequence.load_task_attributes(task)
sequence.enable_editing_task_attributes(task)
def edit_task(ifc, sequence, task=None):
attributes = sequence.get_task_attributes()
ifc.run("sequence.edit_task", task=task, attributes=attributes)
@@ -165,7 +169,7 @@ def copy_task_attribute(ifc, sequence, attribute_name=None):
def duplicate_task(ifc, sequence, task=None):
ifc.run("sequence.duplicate_task", task=task)
work_schedule = sequence.get_active_work_schedule()
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
@@ -245,7 +249,10 @@ def unassign_input_products(ifc, sequence, spatial, task=None, products=None):
def assign_resource(ifc, sequence, task=None):
resource = sequence.get_selected_resource()
sub_resource = ifc.run(
"resource.add_resource", parent_resource=resource, ifc_class=resource.is_a(), name=resource.Name
"resource.add_resource",
parent_resource=resource,
ifc_class=resource.is_a(),
name="{}/{}".format(resource.Name or "Unnamed", task.Name or ""),
)
ifc.run("sequence.assign_process", relating_process=task, related_object=sub_resource)
resources = sequence.get_task_resources(task)
@@ -430,6 +437,20 @@ def select_task_inputs(sequence, spatial, task=None):
spatial.select_products(products=sequence.get_task_inputs(task))
def select_work_schedule_products(sequence, spatial, work_schedule=None):
products = sequence.get_work_schedule_products(work_schedule)
spatial.select_products(products)
def select_unassigned_work_schedule_products(ifc, sequence, spatial):
spatial.deselect_all()
products = ifc.get().by_type("IfcElement")
work_schedule = sequence.get_active_work_schedule()
schedule_products = sequence.get_work_schedule_products(work_schedule)
selection = [product for product in products if product not in schedule_products]
spatial.select_products(selection)
def recalculate_schedule(ifc, work_schedule=None):
ifc.run("sequence.recalculate_schedule", work_schedule=work_schedule)
@@ -450,7 +471,7 @@ def calculate_task_duration(ifc, sequence, task=None):
ifc.run("sequence.calculate_task_duration", task=task)
work_schedule = sequence.get_active_work_schedule()
if work_schedule:
sequence.create_task_tree(work_schedule)
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
@@ -459,6 +480,8 @@ def highlight_task(sequence, task=None):
is_work_schedule_active = sequence.is_work_schedule_active(work_schedule)
if is_work_schedule_active:
sequence.highlight_task(task)
else:
return "Work schedule is not active"
def highlight_product_related_task(sequence, spatial, product_type=None):
@@ -522,5 +545,22 @@ def generate_gantt_chart(sequence, work_schedule):
json = sequence.create_tasks_json(work_schedule)
sequence.generate_gantt_browser_chart(json)
def load_product_tasks(sequence, product=None):
sequence.load_product_tasks(product)
def load_product_related_tasks(sequence, product=None):
sequence.load_product_related_tasks(product)
def reorder_task_nesting(ifc, sequence, task, new_index):
is_sorting_enabled = sequence.is_sorting_enabled()
is_sort_reversed = sequence.is_sort_reversed()
if is_sorting_enabled or is_sort_reversed:
return "Remove manual sorting"
else:
ifc.run("nest.reorder_nesting", item=task, new_index=new_index)
work_schedule = sequence.get_active_work_schedule()
sequence.load_task_tree(work_schedule)
sequence.load_task_properties()
def create_baseline(ifc, sequence, work_schedule, name):
ifc.run("sequence.create_baseline", work_schedule=work_schedule, name=name)
+47 -24
View File
@@ -562,32 +562,37 @@ class Qto:
@interface
class Resource:
def load_resources(cls): pass
def load_resource_properties(cls): pass
def clear_productivity_data(cls, props): pass
def contract_resource(cls, resource): pass
def disable_editing_resource_cost_value(cls): pass
def disable_editing_resource_quantity(cls): pass
def disable_editing_resource(cls): pass
def disable_resource_editing_ui(cls): pass
def load_resource_attributes(cls, resource): pass
def enable_editing_resource(cls, resource): pass
def get_resource_attributes(cls): pass
def enable_editing_resource_time(cls, resource): pass
def get_resource_time(cls, resource): pass
def load_resource_time_attributes(cls, resource_time): pass
def get_resource_time_attributes(cls): pass
def enable_editing_resource_costs(cls, resource): pass
def disable_editing_resource_cost_value(cls): pass
def enable_editing_resource_cost_value_formula(cls, cost_value): pass
def load_cost_value_attributes(cls, cost_value): pass
def edit_productivity_pset(cls, resource, attributes): pass
def enable_editing_cost_value_attributes(cls, cost_value): pass
def get_resource_cost_value_formula(cls): pass
def get_resource_cost_value_attributes(cls): pass
def enable_editing_resource_base_quantity(cls, resource): pass
def enable_editing_resource_cost_value_formula(cls, cost_value): pass
def enable_editing_resource_costs(cls, resource): pass
def enable_editing_resource_quantity(cls, resource_quantity): pass
def disable_editing_resource_quantity(cls): pass
def get_resource_quantity_attributes(cls): pass
def enable_editing_resource_time(cls, resource): pass
def enable_editing_resource(cls, resource): pass
def expand_resource(cls, resource): pass
def contract_resource(cls, resource): pass
def get_highlighted_resource(cls): pass
def get_productivity_attributes(cls): pass
def get_productivity(cls, resource, should_inherit): pass
def get_resource_attributes(cls): pass
def get_resource_cost_value_attributes(cls): pass
def get_resource_cost_value_formula(cls): pass
def get_resource_quantity_attributes(cls): pass
def get_resource_time_attributes(cls): pass
def get_resource_time(cls, resource): pass
def import_resources(cls, file_path): pass
def load_cost_value_attributes(cls, cost_value): pass
def load_productivity_data(cls): pass
def load_resource_attributes(cls, resource): pass
def load_resource_properties(cls): pass
def load_resource_time_attributes(cls, resource_time): pass
def load_resources(cls): pass
@interface
class Root:
@@ -619,11 +624,24 @@ class Search:
@interface
class Sequence:
def add_task_column(cls, column_type, name, data_type): pass
def add_text_animation_handler(cls, settings): pass
def animate_consumption(cls, obj, start_frame, product_frame, color, animation_type): pass
def animate_creation(cls, obj, start_frame, product_frame, color): pass
def animate_destruction(cls, obj, start_frame, color, animation_type): pass
def animate_input(cls, obj, start_frame, product_frame, animation_type): pass
def animate_movement_from(cls, obj, start_frame, color, animation_type): pass
def animate_movement_to(cls, obj, start_frame, product_frame, color): pass
def animate_objects(cls, settings, frames, clear_previous, animation_type): pass
def animate_operation(cls, obj, start_frame, product_frame, color): pass
def animate_output(cls, obj, start_frame, product_frame): pass
def clear_object_animation(cls, obj): pass
def clear_objects_animation(cls, include_blender_objects): pass
def contract_all_tasks(cls): pass
def contract_task(cls, task): pass
def create_bars(cls, tasks): pass
def create_bars(cls, tasks):pass
def create_task_tree(cls, work_schedule): pass
def create_new_task_json(cls, task, json, type_map=None): pass
def load_task_tree(cls, work_schedule): pass
def create_tasks_json(cls, work_schedule=None): pass
def disable_editing_rel_sequence(cls): pass
def disable_editing_task_animation_colors(cls): pass
def disable_editing_task_time(cls): pass
@@ -640,7 +658,7 @@ class Sequence:
def enable_editing_task_calendar(cls, task): pass
def enable_editing_task_sequence(cls, task): pass
def enable_editing_task_time(cls, task): pass
def enable_editing_task(cls, task): pass
def enable_editing_task_attributes(cls, task): pass
def enable_editing_work_calendar_times(cls, work_calendar): pass
def enable_editing_work_calendar(cls, work_calendar): pass
def enable_editing_work_plan_schedules(cls, work_plan): pass
@@ -651,11 +669,13 @@ class Sequence:
def expand_all_tasks(cls): pass
def expand_task(cls, task): pass
def find_related_input_tasks(cls, product): pass
def find_related_output_tasks(cls, column): pass
def find_related_output_tasks(cls, product): pass
def generate_gantt_browser_chart(cls, task_json): pass
def get_active_task(cls): pass
def get_active_work_schedule(cls): pass
def get_animation_bar_tasks(cls): pass
def get_animation_product_frames(cls, work_schedule, settings): pass
def get_animation_settings(cls): pass
def get_checked_tasks(cls): pass
def get_direct_nested_tasks(cls, task):pass
def get_direct_task_outputs(cls, task): pass
@@ -665,7 +685,6 @@ class Sequence:
def get_recurrence_pattern_attributes(cls, recurrence_pattern): pass
def get_recurrence_pattern_times(cls): pass
def get_rel_sequence_attributes(cls): pass
def get_selected_resource(cls): pass
def get_start_date(cls): pass
def get_task_attribute_value(cls, attribute_name): pass
@@ -678,12 +697,15 @@ class Sequence:
def get_work_calendar_attributes(cls): pass
def get_work_plan_attributes(cls): pass
def get_work_schedule_attributes(cls): pass
def get_work_schedule_products(cls, work_schedule): pass
def get_work_schedule(cls, task): pass
def get_work_time_attributes(cls): pass
def guess_date_range(cls, work_schedule): pass
def has_task_assignments(cls, product, cost_schedule=None): pass
def highlight_task(cls, task): pass
def is_work_schedule_active(cls, work_schedule): pass
def load_lag_time_attributes(cls, lag_time): pass
def load_product_related_tasks(cls, product): pass
def load_rel_sequence_attributes(cls, rel_sequence): pass
def load_resources(cls): pass
def load_task_animation_colors(cls): pass
@@ -707,6 +729,7 @@ class Sequence:
def show_snapshot(cls, product_states): pass
def update_visualisation_date(cls, start_date, finish_date): pass
@interface
class Spatial:
def can_contain(cls, structure_obj, element_obj): pass
+39 -9
View File
@@ -1347,15 +1347,16 @@ class Drawing(blenderbim.core.tool.Drawing):
"""returns a set of elements that are included in the drawing"""
ifc_file = tool.Ifc.get()
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), bpy.data.objects)
include = pset.get("Include", None)
if include:
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include))
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements))
else:
if tool.Ifc.get_schema() == "IFC2X3":
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialStructureElement"))
else:
elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
elements = {e for e in elements if e.is_a() != "IfcSpace"}
base_elements = set(ifc_file.by_type("IfcElement") + ifc_file.by_type("IfcSpatialElement"))
elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"}
annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing))
elements.update(annotations)
@@ -1370,7 +1371,7 @@ class Drawing(blenderbim.core.tool.Drawing):
ifc_file = tool.Ifc.get()
pset = ifcopenshell.util.element.get_psets(drawing).get("EPset_Drawing", {})
include = pset.get("Include", None)
elements = set(ifc_file.by_type("IfcSpace"))
elements = cls.get_elements_in_camera_view(tool.Ifc.get_object(drawing), [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSpace")])
if include:
elements = set(ifcopenshell.util.selector.Selector.parse(ifc_file, include, elements=elements))
exclude = pset.get("Exclude", None)
@@ -1449,14 +1450,13 @@ class Drawing(blenderbim.core.tool.Drawing):
# Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude
drawing = tool.Ifc.get_entity(camera)
all_elements = set(tool.Ifc.get().by_type("IfcElement")) - set(tool.Ifc.get().by_type("IfcOpeningElement"))
filtered_elements = cls.get_drawing_elements(drawing)
hidden_elements = list(all_elements - filtered_elements)
hidden_objs = [tool.Ifc.get_object(e) for e in hidden_elements]
# Running operators is much more efficient in this scenario than looping through each element
bpy.ops.object.hide_view_clear()
filtered_elements = cls.get_drawing_elements(drawing) | cls.get_drawing_spaces(drawing)
hidden_objs = [o for o in bpy.context.visible_objects if tool.Ifc.get_entity(o) not in filtered_elements]
for hidden_obj in hidden_objs:
if bpy.context.view_layer.objects.get(hidden_obj.name):
hidden_obj.hide_set(True)
@@ -1492,6 +1492,36 @@ class Drawing(blenderbim.core.tool.Drawing):
)
break
@classmethod
def get_elements_in_camera_view(cls, camera, objs):
if bpy.context.scene.render.resolution_x > bpy.context.scene.render.resolution_y:
x = camera.data.ortho_scale / 2
y = (camera.data.BIMCameraProperties.raster_y / camera.data.BIMCameraProperties.raster_x) * x
else:
y = camera.data.ortho_scale / 2
x = (camera.data.BIMCameraProperties.raster_x / camera.data.BIMCameraProperties.raster_y) * y
camera_inverse_matrix = camera.matrix_world.inverted()
return set([
tool.Ifc.get_entity(o)
for o in objs
if cls.is_in_camera_view(o, camera_inverse_matrix, x, y, camera.data.clip_start, camera.data.clip_end)
and tool.Ifc.get_entity(o)
])
@classmethod
def is_in_camera_view(cls, obj, camera_inverse_matrix, x, y, clip_start, clip_end):
local_bbox = [camera_inverse_matrix @ obj.matrix_world @ Vector(v) for v in obj.bound_box]
for v in local_bbox:
if v.z < -clip_start and v.z > -clip_end and abs(v.x) < x and abs(v.y) < y:
return True
if any([v.z > -clip_start for v in local_bbox]) and any([v.z < -clip_end for v in local_bbox]):
return True
elif any([v.x < -x for v in local_bbox]) and any([v.x > x for v in local_bbox]):
return True
elif any([v.y < -y for v in local_bbox]) and any([v.y > y for v in local_bbox]):
return True
@classmethod
def is_intersecting_camera(cls, obj, camera):
# Based on separating axis theorem
+6 -2
View File
@@ -71,6 +71,9 @@ class Geometry(blenderbim.core.tool.Geometry):
return
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
return blenderbim.core.drawing.remove_drawing(tool.Ifc, tool.Drawing, drawing=element)
if element.is_a("IfcRelSpaceBoundary"):
ifcopenshell.api.run("boundary.remove_boundary", tool.Ifc.get(), boundary=element)
return bpy.data.objects.remove(obj)
if obj.users_collection and obj.users_collection[0].name == obj.name:
parent = ifcopenshell.util.element.get_aggregate(element)
if not parent:
@@ -84,6 +87,7 @@ class Geometry(blenderbim.core.tool.Geometry):
bpy.data.collections.remove(obj.users_collection[0])
if getattr(element, "FillsVoids", None):
bpy.ops.bim.remove_filling(filling=element.id())
if element.is_a("IfcOpeningElement"):
if element.HasFillings:
for rel in element.HasFillings:
@@ -97,7 +101,7 @@ class Geometry(blenderbim.core.tool.Geometry):
bpy.ops.bim.remove_opening(opening_id=rel.RelatedOpeningElement.id())
for port in ifcopenshell.util.system.get_ports(element):
blenderbim.core.system.remove_port(tool.Ifc, tool.System, port=port)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element)
try:
obj.name
bpy.data.objects.remove(obj)
@@ -316,7 +320,7 @@ class Geometry(blenderbim.core.tool.Geometry):
@classmethod
def is_edited(cls, obj):
return list(obj.scale) != [1.0, 1.0, 1.0] or obj in IfcStore.edited_objs
return not all([tool.Cad.is_x(o, 1.0) for o in obj.scale]) or obj in IfcStore.edited_objs
@classmethod
def is_mapped_representation(cls, representation):
-3
View File
@@ -285,9 +285,6 @@ class IfcGit:
for obj in blender_collection.objects:
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(blender_collection)
for collection in bpy.data.collections:
if not collection.users:
bpy.data.collections.remove(collection)
@classmethod
def is_valid_branch_name(cls, new_branch_name):
+92
View File
@@ -115,6 +115,56 @@ class Model(blenderbim.core.tool.Model):
cls.bm.free()
return profile
@classmethod
def export_surface(cls, obj):
p1, p2, p3 = [v.co.copy() for v in obj.data.vertices[0:3]]
edge1 = p2 - p1
edge2 = p3 - p1
normal = edge1.cross(edge2)
z_axis = normal.normalized()
x_axis = p2 - p1
x_axis.normalize()
y_axis = z_axis.cross(x_axis)
position = Matrix()
position.col[0][:3] = x_axis
position.col[1][:3] = y_axis
position.col[2][:3] = z_axis
position.col[3][:3] = p1
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
helper = Helper(tool.Ifc.get())
indices = helper.auto_detect_arbitrary_profile_with_voids(obj, obj.data)
if isinstance(indices, tuple) and indices[0] is False: # Ugly
return
cls.bm = bmesh.new()
cls.bm.from_mesh(obj.data)
cls.bm.verts.ensure_lookup_table()
cls.bm.edges.ensure_lookup_table()
surface = tool.Ifc.get().createIfcCurveBoundedPlane()
surface.BasisSurface = tool.Ifc.get().createIfcPlane(tool.Ifc.get().createIfcAxis2Placement3D(
tool.Ifc.get().createIfcCartesianPoint([o / cls.unit_scale for o in p1]),
tool.Ifc.get().createIfcDirection([float(o) for o in z_axis]),
tool.Ifc.get().createIfcDirection([float(o) for o in x_axis]),
))
if tool.Ifc.get().schema != "IFC2X3":
cls.points = cls.export_points(position, indices["points"])
surface.OuterBoundary = cls.export_curve(position, indices["profile"])
results = []
for inner_curve in indices["inner_curves"]:
results.append(cls.export_curve(position, inner_curve))
surface.InnerBoundaries = results
cls.bm.free()
return surface
@classmethod
def generate_occurrence_name(cls, element_type, ifc_class):
props = bpy.context.scene.BIMModelProperties
@@ -218,6 +268,44 @@ class Model(blenderbim.core.tool.Model):
return obj
@classmethod
def import_surface(cls, surface, obj=None):
cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
cls.vertices = []
cls.edges = []
cls.arcs = []
cls.circles = []
if surface.is_a("IfcCurveBoundedPlane"):
position = Matrix(ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist())
position[0][3] *= cls.unit_scale
position[1][3] *= cls.unit_scale
position[2][3] *= cls.unit_scale
cls.import_curve(obj, position, surface.OuterBoundary)
for inner_boundary in surface.InnerBoundaries:
cls.import_curve(obj, position, inner_boundary)
mesh = bpy.data.meshes.new("Surface")
mesh.from_pydata(cls.vertices, cls.edges, [])
mesh.BIMMeshProperties.subshape_type = "PROFILE"
if obj is None:
obj = bpy.data.objects.new("Surface", mesh)
else:
obj.data = mesh
for arc in cls.arcs:
group = obj.vertex_groups.new(name="IFCARCINDEX")
group.add(arc, 1, "REPLACE")
for circle in cls.circles:
group = obj.vertex_groups.new(name="IFCCIRCLE")
group.add(circle, 1, "REPLACE")
return obj
@classmethod
def import_curve(cls, obj, position, curve):
offset = len(cls.vertices)
@@ -232,6 +320,10 @@ class Model(blenderbim.core.tool.Model):
cls.vertices.append(global_point)
cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices))])
cls.edges[-1] = (len(cls.vertices) - 1, offset) # Close the loop
elif curve.is_a("IfcCompositeCurve"):
# This is a first pass incomplete implementation only for simple polylines, and misses many details.
for segment in curve.Segments:
cls.import_curve(obj, position, segment.ParentCurve)
elif curve.is_a("IfcIndexedPolyCurve"):
is_arc = False
is_closed = False
+2 -2
View File
@@ -133,8 +133,8 @@ class Project(blenderbim.core.tool.Project):
def set_default_modeling_dimensions(cls):
props = bpy.context.scene.BIMModelProperties
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
props.extrusion_depth = 3 / unit_scale
props.length = 1 / unit_scale
props.extrusion_depth = 3
props.length = 1
props.rl1 = 0
props.rl2 = 1 / unit_scale
props.x = 0.5 / unit_scale
+81 -4
View File
@@ -30,6 +30,8 @@ from datetime import datetime
from dateutil import parser
import ifcopenshell.util.date as ifcdateutils
import ifcopenshell.util.cost
import ifcopenshell.util.resource
import blenderbim.bim.schema
class Resource(blenderbim.core.tool.Resource):
@@ -58,6 +60,7 @@ class Resource(blenderbim.core.tool.Resource):
if not resource.HasContext:
continue
create_new_resource_li(resource, 0)
cls.load_productivity_data()
props.is_editing = True
@classmethod
@@ -299,8 +302,82 @@ class Resource(blenderbim.core.tool.Resource):
@classmethod
def get_highlighted_resource(cls):
return tool.Ifc.get().by_id(
bpy.context.scene.BIMResourceTreeProperties.resources[
bpy.context.scene.BIMResourceProperties.active_resource_index
resources = len(bpy.context.scene.BIMResourceTreeProperties.resources)
if resources and resources > bpy.context.scene.BIMResourceProperties.active_resource_index:
return tool.Ifc.get().by_id(
bpy.context.scene.BIMResourceTreeProperties.resources[
bpy.context.scene.BIMResourceProperties.active_resource_index
].ifc_definition_id
)
)
@classmethod
def clear_productivity_data(cls, props):
for duration_prop in props.quantity_consumed or []:
if duration_prop.name == "BaseQuantityConsumed":
duration_prop.years = 0
duration_prop.months = 0
duration_prop.days = 0
duration_prop.hours = 0
duration_prop.minutes = 0
duration_prop.seconds = 0
props.quantity_produced = 0
props.quantity_produced_name = ""
@classmethod
def load_productivity_data(cls):
duration_props = None
for collection_prop in bpy.context.scene.BIMResourceProductivity.quantity_consumed:
duration_props = collection_prop if collection_prop.name == "BaseQuantityConsumed" else None
break
if not duration_props:
duration_props = bpy.context.scene.BIMResourceProductivity.quantity_consumed.add()
duration_props.name = "BaseQuantityConsumed"
cls.clear_productivity_data(bpy.context.scene.BIMResourceProductivity)
current_resource = tool.Resource.get_highlighted_resource()
if current_resource:
productivity = cls.get_productivity(current_resource)
if productivity:
bpy.context.scene.BIMResourceProductivity.quantity_produced = (
ifcopenshell.util.resource.get_quantity_produced(productivity)
)
bpy.context.scene.BIMResourceProductivity.quantity_produced_name = (
ifcopenshell.util.resource.get_quantity_produced_name(productivity)
)
time_consumed = ifcopenshell.util.resource.get_unit_consumed(productivity)
if time_consumed:
durations_attributes = helper.parse_duration_as_blender_props(time_consumed)
duration_props.years = durations_attributes["years"]
duration_props.months = durations_attributes["months"]
duration_props.days = durations_attributes["days"]
duration_props.hours = durations_attributes["hours"]
duration_props.minutes = durations_attributes["minutes"]
duration_props.seconds = durations_attributes["seconds"]
@classmethod
def get_productivity_attributes(cls):
props = bpy.context.scene.BIMResourceProductivity
productivity = {}
if props.quantity_consumed:
productivity["BaseQuantityConsumed"] = helper.simplify_duration(
props.quantity_consumed, "ELAPSEDTIME", "BaseQuantityConsumed"
)
productivity["BaseQuantityProducedValue"] = props.quantity_produced
productivity["BaseQuantityProducedName"] = props.quantity_produced_name
return productivity
@classmethod
def get_productivity(cls, resource, should_inherit=False):
return ifcopenshell.util.resource.get_productivity(resource, should_inherit=should_inherit)
@classmethod
def edit_productivity_pset(cls, resource, attributes):
productivity = cls.get_productivity(resource)
if productivity:
pset = tool.Ifc.get().by_id(productivity["id"])
else:
pset = tool.Ifc.run("pset.add_pset", product=resource, name="EPset_Productivity")
tool.Ifc.run(
"pset.edit_pset",
pset=pset,
properties=attributes,
)
+76 -39
View File
@@ -27,6 +27,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.unit
import json
import blenderbim.core.tool
import blenderbim.core
import blenderbim.tool as tool
import blenderbim.bim.helper
import blenderbim.bim.module.sequence.helper as helper
@@ -132,7 +133,7 @@ class Sequence(blenderbim.core.tool.Sequence):
props.editing_type = "TASKS"
@classmethod
def create_task_tree(cls, work_schedule):
def load_task_tree(cls, work_schedule):
bpy.context.scene.BIMTaskTreeProperties.tasks.clear()
props = bpy.context.scene.BIMWorkScheduleProperties
cls.contracted_tasks = json.loads(props.contracted_tasks)
@@ -143,10 +144,30 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def get_sorted_tasks_ids(cls, tasks):
cls.sort_keys = {task.id(): cls.get_sort_key(task) for task in tasks}
related_object_ids = sorted(cls.sort_keys, key=cls.natural_sort_key)
def get_sort_key(task):
# Sorting only applies to actual tasks, not the WBS
# for rel in task.IsNestedBy:
# for object in rel.RelatedObjects:
# if object.is_a("IfcTask"):
# return "0000000000" + (task.Identification or "")
column_type, name = bpy.context.scene.BIMWorkScheduleProperties.sort_column.split(".")
if column_type == "IfcTask":
return task.get_info(task)[name] or ""
elif column_type == "IfcTaskTime" and task.TaskTime:
return task.TaskTime.get_info(task)[name]
return task.Identification or ""
def natural_sort_key(i, _nsre=re.compile("([0-9]+)")):
s = sort_keys[i]
return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)]
if bpy.context.scene.BIMWorkScheduleProperties.sort_column:
sort_keys = {task.id(): get_sort_key(task) for task in tasks}
related_object_ids = sorted(sort_keys, key=natural_sort_key)
else:
related_object_ids = [task.id() for task in tasks]
if bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed:
return related_object_ids.reverse()
related_object_ids.reverse()
return related_object_ids
@classmethod
@@ -162,27 +183,6 @@ class Sequence(blenderbim.core.tool.Sequence):
for related_object_id in cls.get_sorted_tasks_ids(ifcopenshell.util.sequence.get_nested_tasks(task)):
cls.create_new_task_li(related_object_id, level_index + 1)
@classmethod
def natural_sort_key(cls, i, _nsre=re.compile("([0-9]+)")):
s = cls.sort_keys[i]
return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)]
@classmethod
def get_sort_key(cls, task):
# Sorting only applies to actual tasks, not the WBS
for rel in task.IsNestedBy:
for object in rel.RelatedObjects:
if object.is_a("IfcTask"):
return "0000000000" + (task.Identification or "")
if not bpy.context.scene.BIMWorkScheduleProperties.sort_column:
return task.Identification or ""
column_type, name = bpy.context.scene.BIMWorkScheduleProperties.sort_column.split(".")
if column_type == "IfcTask":
return task.Name or ""
elif column_type == "IfcTaskTime" and task.TaskTime:
return task.TaskTime.Name or ""
return task.Identification or ""
@classmethod
def load_task_properties(cls, task=None):
def canonicalise_time(time):
@@ -330,7 +330,7 @@ class Sequence(blenderbim.core.tool.Sequence):
blenderbim.bim.helper.import_attributes2(task, props.task_attributes)
@classmethod
def enable_editing_task(cls, task):
def enable_editing_task_attributes(cls, task):
props = bpy.context.scene.BIMWorkScheduleProperties
props.active_task_id = task.id()
props.editing_task_type = "ATTRIBUTES"
@@ -409,16 +409,15 @@ class Sequence(blenderbim.core.tool.Sequence):
def load_task_resources(cls, resources):
props = bpy.context.scene.BIMWorkScheduleProperties
props.task_resources.clear()
if resources:
for resource in resources:
new = props.task_resources.add()
new.ifc_definition_id = resource.id()
new.name = resource.Name or "Unnamed"
new.schedule_usage = resource.Usage.ScheduleUsage or 1 if resource.Usage else 0
for resource in resources or []:
new = props.task_resources.add()
new.ifc_definition_id = resource.id()
new.name = resource.Name or "Unnamed"
new.schedule_usage = resource.Usage.ScheduleUsage or 1 if resource.Usage else 0
@classmethod
def load_resources(cls):
bpy.ops.bim.load_resources() # remove and refactor
blenderbim.core.resource.load_resources(tool.Resource)
@classmethod
def get_task_inputs(cls, task):
@@ -430,10 +429,22 @@ class Sequence(blenderbim.core.tool.Sequence):
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_outputs
return ifcopenshell.util.sequence.get_task_outputs(task, is_deep)
@classmethod
def are_entities_same_class(cls, entities):
if not entities:
return False
if len(entities) == 1:
return True
first = entities[0]
for entity in entities:
if entity.is_a() != first.is_a():
return False
return True
@classmethod
def get_task_resources(cls, task):
is_deep = bpy.context.scene.BIMWorkScheduleProperties.show_nested_resources
return ifcopenshell.util.sequence.get_task_resource(task, is_deep)
return ifcopenshell.util.sequence.get_task_resources(task, is_deep)
@classmethod
def load_task_inputs(cls, inputs):
@@ -734,6 +745,8 @@ class Sequence(blenderbim.core.tool.Sequence):
def remove_task_column(cls, name):
props = bpy.context.scene.BIMWorkScheduleProperties
props.columns.remove(props.columns.find(name))
if props.sort_column == name:
props.sort_column = ""
@classmethod
def set_task_sort_column(cls, column):
@@ -781,7 +794,7 @@ class Sequence(blenderbim.core.tool.Sequence):
bpy.context.scene.BIMWorkScheduleProperties.contracted_tasks = json.dumps(contracted_tasks)
expand_ancestors(parent_task)
work_schedule = cls.get_active_work_schedule()
cls.create_task_tree(work_schedule)
cls.load_task_tree(work_schedule)
cls.load_task_properties()
task_props = bpy.context.scene.BIMTaskTreeProperties
@@ -1485,7 +1498,7 @@ class Sequence(blenderbim.core.tool.Sequence):
"pParent": task.Nests[0].RelatingObject.id() if task.Nests else 0,
"pOpen": 1,
"pCost": 1,
"ifcduration": task_time.ScheduleDuration if task_time else "",
"ifcduration": str(ifcopenshell.util.date.ifc2datetime(task_time.ScheduleDuration)) if (task_time and task_time.ScheduleDuration) else "",
}
if task_time and task_time.IsCritical:
@@ -1506,15 +1519,15 @@ class Sequence(blenderbim.core.tool.Sequence):
@classmethod
def generate_gantt_browser_chart(cls, task_json):
active_work_schedule = cls.get_active_work_schedule()
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f:
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.mustache"), "r") as t:
f.write(pystache.render(t.read(), {"json_data": json.dumps(task_json)}))
f.write(pystache.render(t.read(), {"json_data": json.dumps(task_json), "name":active_work_schedule.Name or "Unnamed"}))
webbrowser.open("file://" + os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"))
@classmethod
def load_product_tasks(cls, product):
def load_product_related_tasks(cls, product):
props = bpy.context.scene.BIMWorkScheduleProperties
props.is_task_update_enabled = False
props.product_input_tasks.clear()
props.product_output_tasks.clear()
task_inputs, task_ouputs = ifcopenshell.util.sequence.get_tasks_for_product(product)
@@ -1526,3 +1539,27 @@ class Sequence(blenderbim.core.tool.Sequence):
new = props.product_output_tasks.add()
new.name = task.Name or "Unnamed"
new.ifc_definition_id = task.id()
@classmethod
def get_work_schedule_products(cls, work_schedule):
products = []
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
products.extend(ifcopenshell.util.sequence.get_task_inputs(task, is_deep=True))
products.extend(ifcopenshell.util.sequence.get_task_outputs(task, is_deep=True))
return products
@classmethod
def has_task_assignments(cls, product, work_schedule=None):
task_inputs, task_ouputs = ifcopenshell.util.sequence.get_tasks_for_product(product)
if work_schedule:
task_inputs = [task for task in task_inputs or [] if cls.get_work_schedule(task) == work_schedule]
task_ouputs = [task for task in task_ouputs or [] if cls.get_work_schedule(task) == work_schedule]
return bool(task_inputs or task_ouputs)
@classmethod
def is_sorting_enabled(cls):
return bpy.context.scene.BIMWorkScheduleProperties.sort_column
@classmethod
def is_sort_reversed(cls):
return bpy.context.scene.BIMWorkScheduleProperties.is_sort_reversed
@@ -241,6 +241,22 @@ Scenario: Remove Resource Quantity
When I press "bim.remove_resource_quantity(resource={labor_resource})"
Then nothing happens
Scenario: Add Productivity data
Given an empty IFC project
When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
And I set "scene.BIMResourceProperties.active_resource_index" to "1"
And I set "scene.BIMResourceProperties.should_show_productivity" to "True"
And I set "scene.BIMResourceProductivity.quantity_produced" to "5.00"
And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea"
And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5"
When I press "bim.edit_productivity_data()"
And the variable "productivity_data" is "IfcStore.get_file().by_type('IfcPropertySet')[-1].id()"
Scenario: Calculate Resource Work
Given an empty IFC project
And I press "bim.add_work_schedule"
@@ -248,7 +264,7 @@ Scenario: Calculate Resource Work
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
And I press "bim.add_summary_task(work_schedule={work_schedule})"
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I press "bim.enable_editing_task(task={task})"
And I press "bim.enable_editing_task_attributes(task={task})"
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "CONSTRUCTION"
And I press "bim.edit_task"
And I press "bim.enable_editing_task_time(task={task})"
@@ -261,12 +277,23 @@ Scenario: Calculate Resource Work
And I press "bim.assign_class"
And the object "IfcWall/Cube" is selected
And I set "active_object.PsetProperties.qto_name" to "Qto_WallBaseQuantities"
When I press "bim.add_pset(obj='IfcWall/Cube', obj_type='Object')"
And the variable "qto" is "{ifc}.by_type('IfcElementQuantity')[-1].id()"
And I press "bim.enable_pset_editing(obj='IfcWall/Cube', obj_type='Object', pset_id={qto})"
#And I set "active_object.PsetProperties.properties[2].metadata.float_value" to "3.00"
# TO DO: complete
Then nothing happens
When I press "bim.add_qto(obj='IfcWall/Cube', obj_type='Object')"
And I set "active_object.PsetProperties.properties[5].metadata.float_value" to "50.00"
And I press "bim.edit_pset(obj='IfcWall/Cube', obj_type='Object')"
When I press "bim.load_resources"
When I press "bim.add_resource(ifc_class="IfcCrewResource")"
And the variable "crew_resource" is "IfcStore.get_file().by_type('IfcCrewResource')[0].id()"
When I press "bim.add_resource(ifc_class="IfcLaborResource", parent_resource={crew_resource})"
And the variable "labor_resource" is "IfcStore.get_file().by_type('IfcLaborResource')[0].id()"
And I set "scene.BIMResourceProperties.active_resource_index" to "1"
And I set "scene.BIMResourceProperties.should_show_productivity" to "True"
And I set "scene.BIMResourceProductivity.quantity_produced" to "5.00"
And I set "scene.BIMResourceProductivity.quantity_produced_name" to "GrossSideArea"
And I set "scene.BIMResourceProductivity.quantity_consumed[0].hours" to "5"
When I press "bim.edit_productivity_data()"
And the variable "productivity_data" is "IfcStore.get_file().by_type('IfcPropertySet')[-1].id()"
When I press "bim.calculate_resource_work(resource={labor_resource})"
Then nothing happens
Scenario: Assign Resource
@@ -225,7 +225,7 @@ Scenario: Enable editing task
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
And I press "bim.add_summary_task(work_schedule={work_schedule})"
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
When I press "bim.enable_editing_task(task={task})"
When I press "bim.enable_editing_task_attributes(task={task})"
Then nothing happens
Scenario: Copy task attribute
@@ -236,7 +236,7 @@ Scenario: Copy task attribute
And I press "bim.add_summary_task(work_schedule={work_schedule})"
And I press "bim.add_summary_task(work_schedule={work_schedule})"
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I press "bim.enable_editing_task(task={task})"
And I press "bim.enable_editing_task_attributes(task={task})"
And I set "scene.BIMWorkScheduleProperties.task_attributes[2].string_value" to "Foo"
When I set "scene.BIMTaskTreeProperties.tasks[1].is_selected" to "True"
And I press "bim.copy_task_attribute(name='Description')"
@@ -253,7 +253,7 @@ Scenario: Unassign task Successor
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
When I press "bim.add_task(task={task})"
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
And I press "bim.enable_editing_task(task={nested_task_one})"
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
And I press "bim.assign_successor(task={nested_task_two})"
When I press "bim.unassign_successor(task={nested_task_two})"
Then nothing happens
@@ -269,7 +269,7 @@ Scenario: Edit time Lag
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
When I press "bim.add_task(task={task})"
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
And I press "bim.enable_editing_task(task={nested_task_one})"
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
When I press "bim.assign_successor(task={nested_task_two})"
And the variable "rel_sequence" is "IfcStore.get_file().by_type('IfcRelSequence')[0].id()"
When I press "bim.assign_lag_time(sequence={rel_sequence})"
@@ -290,7 +290,7 @@ Scenario: Unassign time Lag
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
When I press "bim.add_task(task={task})"
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
And I press "bim.enable_editing_task(task={nested_task_one})"
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
When I press "bim.assign_successor(task={nested_task_two})"
And the variable "rel_sequence" is "IfcStore.get_file().by_type('IfcRelSequence')[0].id()"
When I press "bim.assign_lag_time(sequence={rel_sequence})"
@@ -309,7 +309,7 @@ Scenario: Edit Sequence Relationship
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
When I press "bim.add_task(task={task})"
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
And I press "bim.enable_editing_task(task={nested_task_one})"
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
When I press "bim.assign_successor(task={nested_task_two})"
And the variable "rel_sequence" is "IfcStore.get_file().by_type('IfcRelSequence')[0].id()"
And I press "bim.enable_editing_sequence_attributes(sequence={rel_sequence})"
@@ -340,7 +340,7 @@ Scenario: Animate the construction of a wall
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
And I press "bim.add_summary_task(work_schedule={work_schedule})"
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I press "bim.enable_editing_task(task={task})"
And I press "bim.enable_editing_task_attributes(task={task})"
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "CONSTRUCTION"
And I press "bim.edit_task"
And I press "bim.enable_editing_task_time(task={task})"
@@ -376,7 +376,7 @@ Scenario: Animate the demolition of a wall
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
And I press "bim.add_summary_task(work_schedule={work_schedule})"
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I press "bim.enable_editing_task(task={task})"
And I press "bim.enable_editing_task_attributes(task={task})"
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "DEMOLITION"
And I press "bim.edit_task"
And I press "bim.enable_editing_task_time(task={task})"
@@ -415,7 +415,7 @@ Scenario: Animate the operation of a wall
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
And I press "bim.add_summary_task(work_schedule={work_schedule})"
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I press "bim.enable_editing_task(task={task})"
And I press "bim.enable_editing_task_attributes(task={task})"
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "OPERATION"
And I press "bim.edit_task"
And I press "bim.enable_editing_task_time(task={task})"
@@ -448,7 +448,7 @@ Scenario: Animate the movement of a wall
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
And I press "bim.add_summary_task(work_schedule={work_schedule})"
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I press "bim.enable_editing_task(task={task})"
And I press "bim.enable_editing_task_attributes(task={task})"
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "MOVE"
And I press "bim.edit_task"
And I press "bim.enable_editing_task_time(task={task})"
@@ -539,7 +539,7 @@ Scenario: Generate Gantt Chart
And I press "bim.enable_editing_work_schedule_tasks(work_schedule={work_schedule})"
And I press "bim.add_summary_task(work_schedule={work_schedule})"
And the variable "task" is "IfcStore.get_file().by_type('IfcTask')[0].id()"
And I press "bim.enable_editing_task(task={task})"
And I press "bim.enable_editing_task_attributes(task={task})"
And I set "scene.BIMWorkScheduleProperties.task_attributes.get('PredefinedType').enum_value" to "CONSTRUCTION"
And I press "bim.edit_task"
And I press "bim.enable_editing_task_time(task={task})"
@@ -788,7 +788,7 @@ Scenario: Duplicate Task
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
When I press "bim.add_task(task={task})"
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
And I press "bim.enable_editing_task(task={nested_task_one})"
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
And I press "bim.assign_successor(task={nested_task_two})"
When I press "bim.duplicate_task(task={task})"
Then nothing happens
@@ -804,7 +804,7 @@ Scenario: Duplicate Task and edit sequence Relationship
And the variable "nested_task_one" is "IfcStore.get_file().by_type('IfcTask')[1].id()"
When I press "bim.add_task(task={task})"
And the variable "nested_task_two" is "IfcStore.get_file().by_type('IfcTask')[2].id()"
And I press "bim.enable_editing_task(task={nested_task_one})"
And I press "bim.enable_editing_task_attributes(task={nested_task_one})"
And I press "bim.assign_successor(task={nested_task_two})"
And I press "bim.duplicate_task(task={task})"
When I press "bim.enable_editing_task_sequence(task={nested_task_one})"
+8
View File
@@ -751,6 +751,14 @@ def saving_sample_test_files(and_open_in_blender=None):
bpy.ops.wm.save_as_mainfile(filepath=f"{filepath}.blend")
@given(parsers.parse("I load test blend file"))
@when(parsers.parse("I load test blend file"))
@then(parsers.parse("I load test blend file"))
def opening_sample_test_files_in_blender():
filepath = f"{variables['cwd']}/test/files/temp/sample_test_file.blend"
bpy.ops.wm.open_mainfile(filepath=filepath, display_file_selector=False)
# TODO: merge to single fixture with `saving_sample_test_files`; add "and wait"
@given(parsers.parse("I save sample test files and open in blender"))
@when(parsers.parse("I save sample test files and open in blender"))
+2 -2
View File
@@ -127,8 +127,8 @@ class TestSetDefaultModelingDimensions(NewFile):
ifcopenshell.api.run("unit.assign_unit", ifc)
subject.set_default_modeling_dimensions()
props = bpy.context.scene.BIMModelProperties
assert props.extrusion_depth == 3000
assert props.length == 1000
assert props.extrusion_depth == 3
assert props.length == 1
assert props.rl1 == 0
assert props.rl2 == 1000
assert props.x == 500
@@ -0,0 +1,51 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, boundary=None):
"""Removes a space boundary
The relating space or related building element is untouched. Only the
boundary and its connection geometry is removed.
:param boundary: The IfcRelSpaceBoundary you want to remove.
:type boundary: ifcopenshell.entity_instance.entity_instance
:return: None
:rtype: None
Example:
# A boring boundary with no geometry. Note that this boundary is
# invalid and does not relate to any space or building element.
boundary = ifcopenshell.api.run("root.create_entity", model, ifc_class="IfcRelSpaceBoundary")
# Let's remove it!
ifcopenshell.api.run("grid.remove_grid_axis", model, axis=axis_2)
"""
self.file = file
self.settings = {"boundary": boundary}
def execute(self):
geometry = self.settings["boundary"].ConnectionGeometry
if geometry:
self.settings["boundary"].ConnectionGeometry = None
ifcopenshell.util.element.remove_deep2(self.file, geometry)
self.file.remove(self.settings["boundary"])
@@ -19,11 +19,11 @@
import ifcopenshell.util.unit
from ifcopenshell.util.shape_builder import ShapeBuilder, V
from itertools import chain
from mathutils import Vector
from mathutils import Vector, Matrix
import collections
import mathutils
from pprint import pprint
from math import pi, cos, sin, tan
from math import pi, cos, sin, tan, radians
def mm(x):
@@ -75,7 +75,8 @@ class Usecase:
railing_radius = self.settings["railing_diameter"] / 2
support_spacing = self.settings["support_spacing"]
clear_width = self.settings["clear_width"]
height = self.settings["height"]
# for calculations purposes we use height without railing radius
height = self.settings["height"] - railing_radius
cap_type = self.settings["terminal_type"]
ifc_context = self.settings["context"]
railing_coords = self.settings["railing_path"]
@@ -106,8 +107,11 @@ class Usecase:
solid = builder.create_swept_disk_solid(polyline, support_radius)
support_disk_circle = builder.circle(radius=support_disk_radius)
angle = V(0, 1).angle_signed(ortho_dir.xy)
y_extrusion_kwargs = builder.rotate_extrusion_kwargs_by_z(builder.extrude_by_y_kwargs(), angle)
support_disk = builder.extrude(
support_disk_circle, support_disk_depth, position=support_points[-1], **builder.extrude_by_y_kwargs()
support_disk_circle, support_disk_depth, position=support_points[-1], **y_extrusion_kwargs
)
return [solid, support_disk]
@@ -127,7 +131,7 @@ class Usecase:
)[0]
midpointco = center + ((fillet_v1co.lerp(fillet_v2co, 0.5) - center).normalized() * radius)
return fillet_v1co, midpointco, fillet_v2co
return [fillet_v1co, midpointco, fillet_v2co]
def add_arcs_on_turnings_points(base_points):
"""add 3 point fillet arcs on turning points of the railing path"""
@@ -204,12 +208,54 @@ class Usecase:
# TODO: implement more cap types
railing_coords_for_cap = railing_coords[::-1] if start else railing_coords
start = railing_coords_for_cap[-1]
start_point = railing_coords_for_cap[-1]
cap_dir = (railing_coords_for_cap[-1] - railing_coords_for_cap[-2]).xy.to_3d().normalized()
arc_point = start + cap_dir * terminal_radius + terminal_radius * z_down
arc_points.append(arc_point)
cap_coords = [arc_point, start + terminal_radius * 2 * z_down]
ortho_dir = (cap_dir.yx * V(1, -1)).to_3d().normalized()
if start:
ortho_dir = -ortho_dir
arc_middle_point_cos = sin(radians(45))
if cap_type in ('180', 'TO_END_POST'):
arc_point = start_point + cap_dir * terminal_radius + terminal_radius * z_down
arc_points.append(arc_point)
cap_coords = [arc_point, start_point + terminal_radius * 2 * z_down]
if cap_type == 'TO_END_POST':
end_point = railing_coords_for_cap[-2].copy()
end_point.z -= terminal_radius * 2
cap_coords.append(end_point)
elif cap_type == 'TO_WALL':
arc_point = start_point + cap_dir * clear_width * arc_middle_point_cos + ortho_dir * clear_width * (1-arc_middle_point_cos)
arc_points.append(arc_point)
cap_coords = [arc_point, start_point + ortho_dir * clear_width + cap_dir * clear_width]
elif cap_type == 'TO_FLOOR':
arc_point = start_point + cap_dir * terminal_radius * arc_middle_point_cos + z_down * terminal_radius * (1-arc_middle_point_cos)
arc_points.append(arc_point)
arc_end = start_point + cap_dir * terminal_radius + terminal_radius * z_down
cap_coords = [
arc_point,
arc_end,
arc_end+z_down*(height-terminal_radius),
]
elif cap_type == 'TO_END_POST_AND_FLOOR':
first_arc_end = start_point + cap_dir * terminal_radius + terminal_radius * z_down
first_arc_coords = get_fillet_points(
start_point, start_point + cap_dir * terminal_radius,
first_arc_end, terminal_radius)
arc_points.append(first_arc_coords[1])
end_point = railing_coords_for_cap[-2].copy()
end_point.z -= height
second_arc_coords = get_fillet_points(
first_arc_end, first_arc_end + z_down * terminal_radius, end_point, terminal_radius
)
arc_points.append(second_arc_coords[1])
cap_coords = [start_point] + first_arc_coords + second_arc_coords + [end_point]
railing_coords = railing_coords_for_cap + cap_coords
if start:
@@ -219,8 +265,9 @@ class Usecase:
items_3d.extend(create_supports_items(railing_coords, manual_supports=use_manual_supports))
railing_coords = add_arcs_on_turnings_points(railing_coords)
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=True)
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=False)
if cap_type != 'NONE':
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=True)
railing_coords, arc_points = add_cap(railing_coords, arc_points, start=False)
railing_path = builder.polyline(
railing_coords, closed=False, arc_points=[railing_coords.index(p) for p in arc_points]
@@ -37,7 +37,7 @@ class Usecase:
axis_1 = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="1", uvw_axes="VAxes", grid=grid)
# Let's create a third so we can remvoe it later
# Let's create a third so we can remove it later
axis_2 = ifcopenshell.api.run("grid.create_grid_axis", model,
axis_tag="2", uvw_axes="VAxes", grid=grid)
@@ -21,7 +21,12 @@ import ifcopenshell.api
class Usecase:
def __init__(
self, file, parent_resource=None, ifc_class="IfcCrewResource", name=None, predefined_type="NOTDEFINED"
self,
file,
parent_resource=None,
ifc_class="IfcCrewResource",
name=None,
predefined_type="NOTDEFINED",
):
"""Add a new construction resource
@@ -20,6 +20,7 @@ import math
import ifcopenshell.api
import ifcopenshell.util.date
import ifcopenshell.util.element
import ifcopenshell.util.resource
class Usecase:
@@ -59,28 +60,10 @@ class Usecase:
self.settings = {"resource": resource}
def execute(self):
self.productivity = ifcopenshell.util.element.get_psets(
amount_worked = ifcopenshell.util.resource.get_resource_required_work(
self.settings["resource"]
).get("EPset_Productivity", None)
if not self.productivity:
# Proposal for Schema - If instance doesn't have any productivity, use the parent's productivity - if any.
if not self.settings["resource"].Nests:
return
else:
parent_resource = self.settings["resource"].Nests[0].RelatingObject
self.productivity = ifcopenshell.util.element.get_psets(
parent_resource
).get("EPset_Productivity", None)
if not self.productivity:
return
unit_consumed = self.get_unit_consumed()
self.unit_produced_name = self.productivity.get(
"BaseQuantityProducedName", None
)
unit_produced = self.productivity.get("BaseQuantityProducedValue", None)
total_produced = self.get_total_produced()
if not unit_consumed or not unit_produced or not total_produced:
if not amount_worked:
return
if not self.settings["resource"].Usage:
ifcopenshell.api.run(
@@ -88,37 +71,4 @@ class Usecase:
self.file,
resource=self.settings["resource"],
)
if "T" in self.productivity.get("BaseQuantityConsumed", None):
seconds = (unit_consumed.days * 24 * 60 * 60) + unit_consumed.seconds
amount_worked = total_produced / unit_produced * seconds
self.settings[
"resource"
].Usage.ScheduleWork = f"PT{amount_worked / 60 / 60}H"
else:
days = unit_consumed.days + (unit_consumed.seconds / (24 * 60 * 60))
amount_worked = total_produced / unit_produced * days
self.settings["resource"].Usage.ScheduleWork = f"P{amount_worked}D"
def get_unit_consumed(self):
duration = self.productivity.get("BaseQuantityConsumed", None)
if not duration:
return
return ifcopenshell.util.date.ifc2datetime(duration)
def get_total_produced(self):
total = 0
for rel in self.settings["resource"].HasAssignments or []:
if not rel.is_a("IfcRelAssignsToProcess"):
continue
for rel2 in rel.RelatingProcess.HasAssignments or []:
if not rel2.is_a("IfcRelAssignsToProduct"):
continue
if self.unit_produced_name == "Count":
total += 1
else:
psets = ifcopenshell.util.element.get_psets(rel2.RelatingProduct)
for pset in psets.values():
for name, value in pset.items():
if name == self.unit_produced_name:
total += float(value)
return total
self.settings["resource"].Usage.ScheduleWork = amount_worked
@@ -51,7 +51,10 @@ class Usecase:
physical_quantity=time, attributes={"TimeValue": 8.0})
"""
self.file = file
self.settings = {"physical_quantity": physical_quantity, "attributes": attributes or {}}
self.settings = {
"physical_quantity": physical_quantity,
"attributes": attributes or {},
}
def execute(self):
for name, value in self.settings["attributes"].items():
@@ -105,7 +105,7 @@ class Usecase:
else:
ifcopenshell.api.run("type.unassign_type", self.file, related_object=self.settings["product"])
elif inverse.is_a("IfcRelSpaceBoundary"):
self.file.remove(inverse)
ifcopenshell.api.run("boundary.remove_boundary", self.file, boundary=inverse)
elif inverse.is_a("IfcRelFillsElement"):
self.file.remove(inverse)
elif inverse.is_a("IfcRelVoidsElement"):
@@ -165,7 +165,9 @@ class Usecase:
"IfcRelAssignsToControl",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"OwnerHistory": ifcopenshell.api.run(
"owner.create_owner_history", self.file
),
"RelatedObjects": [task],
"RelatingControl": self.settings["work_schedule"],
}
@@ -178,5 +180,9 @@ class Usecase:
relating_object=self.settings["parent_task"],
)
if self.settings["parent_task"].Identification:
task.Identification = self.settings["parent_task"].Identification + "." + str(len(rel.RelatedObjects))
task.Identification = (
self.settings["parent_task"].Identification
+ "."
+ str(len(rel.RelatedObjects))
)
return task
@@ -22,7 +22,14 @@ from datetime import datetime
class Usecase:
def __init__(self, file, name="Unnamed", predefined_type="NOTDEFINED", start_time=None, work_plan=None):
def __init__(
self,
file,
name="Unnamed",
predefined_type="NOTDEFINED",
start_time=None,
work_plan=None,
):
"""Add a new work schedule
A work schedule is a group of tasks, where the tasks are typically
@@ -20,7 +20,9 @@ import ifcopenshell.util.date
class Usecase:
def __init__(self, file, rel_sequence=None, lag_value=None, duration_type="WORKTIME"):
def __init__(
self, file, rel_sequence=None, lag_value=None, duration_type="WORKTIME"
):
"""Assign a lag time to a sequence relationship between tasks
A task sequence (e.g. finish to start) may optionally have a lag time
@@ -93,7 +93,6 @@ class Usecase:
"related_object": related_object,
}
def execute(self):
if self.settings["related_object"].HasAssignments:
for assignment in self.settings["related_object"].HasAssignments:
@@ -21,7 +21,13 @@ import ifcopenshell.api
class Usecase:
def __init__(self, file, relating_process=None, related_process=None, sequence_type="FINISH_START"):
def __init__(
self,
file,
relating_process=None,
related_process=None,
sequence_type="FINISH_START",
):
"""Assign a sequential relationship between tasks
Tasks in construction sequencing typically have sequence relationships
@@ -118,7 +124,9 @@ class Usecase:
"IfcRelSequence",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run("owner.create_owner_history", self.file),
"OwnerHistory": ifcopenshell.api.run(
"owner.create_owner_history", self.file
),
"RelatingProcess": self.settings["relating_process"],
"RelatedProcess": self.settings["related_process"],
"SequenceType": self.settings["sequence_type"],
@@ -0,0 +1,216 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021-2022 Dion Moult <dion@thinkmoult.com>, Yassine Oualid <yassine@sigmadimensions.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell
import ifcopenshell.util.system
import ifcopenshell.util.element
class Usecase:
def __init__(self, file, work_schedule=None, name=None):
"""Creates a baseline for your Work Schedule
Using a IfcWorkSchdule having PredefinedType=PLANNED,
We can create a baseline for our work schedule. This IfcWorkSchedule will have PredefinedType=BASELINE
and the IfcWorkSchedule.CreationDate indicating the date of the baseline creation, and IfcWorkSchedule.Name indicating the name of the baseline.
The following relationships are also baselined:
* Same Tasks & attributes
* Same Task Relationships
* Same Construction Resources
* Same Resource Relationships
:param work_schedule: The planned work_schedule to baseline
:type work_schedule: ifcopenshell.entity_instance.entity_instance
:return: The baseline work_schedule
:rtype: ifcopenshell.entity_instance.entity_instance
Example:
.. code:: python
# We have a Work Schedule
planned_work_schedule = WorkSchedule(name="Design new feature",predefinedType="PLANNED", deadline="2023-03-01")
# And now we have a baseline for our Work Schedule
baseline_work_schedule = ifcopenshell.api.run("sequence.create_baseline",file, work_schedule= planned_work_schedule, name="Baseline 1")
"""
self.file = file
self.settings = {"work_schedule": work_schedule, "name": name}
def execute(self):
result = self.create_baseline_work_schedule(self.settings["work_schedule"])
return result
def create_baseline_work_schedule(self, work_schedule):
# create work schedule
if not work_schedule.PredefinedType == "PLANNED":
return
baseline_work_schedule = ifcopenshell.api.run(
"sequence.add_work_schedule",
self.file,
name=work_schedule.Name,
predefined_type="BASELINE",
)
baseline_work_schedule.Name = self.settings["name"]
self.create_baseline_reference(work_schedule, baseline_work_schedule)
self.task_tracker = {"current": [], "duplicate": []}
self.copy_tasks_with_reference(
work_schedule, baseline_work_schedule
) # TODO: Fix task nesting order when duplicating children
self.copy_sequence_relationship(
self.task_tracker["current"], self.task_tracker["duplicate"]
)
def create_baseline_reference(self, relating_object, related_object):
referenced_by = None
if relating_object.Declares:
referenced_by = relating_object.Declares[0]
if referenced_by:
related_objects = list(referenced_by.RelatedObjects)
related_objects.append(related_object)
referenced_by.RelatedObjects = related_objects
ifcopenshell.api.run(
"owner.update_owner_history", self.file, **{"element": referenced_by}
)
else:
referenced_by = self.file.create_entity(
"IfcRelDefinesByObject",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run(
"owner.create_owner_history", self.file
),
"RelatedObjects": [related_object],
"RelatingObject": relating_object,
}
)
return referenced_by
def copy_tasks_with_reference(self, work_schedule, baseline_work_schedule):
for task in ifcopenshell.util.sequence.get_root_tasks(work_schedule):
new_task = self.copy_task(task)
self.file.create_entity(
"IfcRelAssignsToControl",
**{
"GlobalId": ifcopenshell.guid.new(),
"OwnerHistory": ifcopenshell.api.run(
"owner.create_owner_history", self.file
),
"RelatedObjects": [new_task],
"RelatingControl": baseline_work_schedule,
}
)
def copy_task(self, task):
new_task = ifcopenshell.util.element.copy_deep(self.file, task)
self.task_tracker["current"].append(task)
self.task_tracker["duplicate"].append(new_task)
self.create_baseline_reference(task, new_task)
self.copy_indirect_attributes(task, new_task)
return new_task
def copy_indirect_attributes(self, from_element, to_element):
for inverse in self.file.get_inverse(from_element):
if inverse.is_a("IfcRelDefinesByProperties"):
inverse = ifcopenshell.util.element.copy(self.file, inverse)
inverse.RelatedObjects = [to_element]
pset = ifcopenshell.util.element.copy_deep(
self.file, inverse.RelatingPropertyDefinition
)
inverse.RelatingPropertyDefinition = pset
elif inverse.is_a("IfcRelNests") and inverse.RelatingObject == from_element:
nested_tasks = [e for e in inverse.RelatedObjects]
if nested_tasks:
new_tasks = []
for t in nested_tasks:
new_task = self.copy_task(t)
new_tasks.append(new_task)
inverse = ifcopenshell.util.element.copy(self.file, inverse)
inverse.RelatingObject = to_element
inverse.RelatedObjects = new_tasks
for t in new_tasks:
ifcopenshell.api.run(
"nest.unassign_object", self.file, related_object=t
)
rel = ifcopenshell.api.run(
"nest.assign_object",
self.file,
related_object=t,
relating_object=to_element,
)
elif inverse.is_a("IfcRelSequence") and (
inverse.RelatingProcess == from_element
or inverse.RelatedProcess == from_element
):
continue
elif inverse.is_a(
"IfcRelAssignsToControl"
) and inverse.RelatingControl.is_a("IfcWorkSchedule"):
continue
elif inverse.is_a("IfcRelDefinesByObject"):
continue
else:
for i, value in enumerate(inverse):
if value == from_element:
new_inverse = ifcopenshell.util.element.copy(self.file, inverse)
new_inverse[i] = to_element
elif isinstance(value, (tuple, list)) and from_element in value:
new_value = list(value)
new_value.append(to_element)
inverse[i] = new_value
def copy_sequence_relationship(self, original_tasks, duplicated_tasks):
for i, original_task in enumerate(original_tasks):
for inverse in self.file.get_inverse(original_task):
if inverse.is_a("IfcRelSequence") and (
inverse.RelatingProcess == original_task
or inverse.RelatedProcess == original_task
):
original_task_index = original_tasks.index(original_task)
duplicated_task = duplicated_tasks[original_task_index]
relating_process, related_process = None, None
if inverse.RelatingProcess == original_task:
relating_process = duplicated_task
else:
related_process = duplicated_task
if inverse.RelatedProcess in original_tasks:
related_process_index = original_tasks.index(
inverse.RelatedProcess
)
related_process = duplicated_tasks[related_process_index]
if inverse.RelatingProcess in original_tasks:
relating_process_index = original_tasks.index(
inverse.RelatingProcess
)
relating_process = duplicated_tasks[relating_process_index]
if relating_process and related_process:
rel = ifcopenshell.api.run(
"sequence.assign_sequence",
self.file,
relating_process=relating_process,
related_process=related_process,
)
if inverse.TimeLag:
ifcopenshell.api.run(
"sequence.assign_lag_time",
self.file,
rel_sequence=rel,
lag_value= ifcopenshell.util.date.ifc2datetime(inverse.TimeLag.LagValue.wrappedValue) if inverse.TimeLag.LagValue else None,
duration_type=inverse.TimeLag.DurationType,
)
@@ -49,8 +49,9 @@ class Usecase:
self.settings = {"task": task}
def execute(self):
result = ifcopenshell.util.element.copy(self.file, self.settings["task"])
result = ifcopenshell.util.element.copy_deep(self.file, self.settings["task"])
self.copy_indirect_attributes(self.settings["task"], result)
self.copy_sequence_relationship([self.settings["task"]], [result])
return result
def copy_sequence_relationship(self, original_tasks, duplicated_tasks):
@@ -54,7 +54,10 @@ class Usecase:
recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]})
"""
self.file = file
self.settings = {"recurrence_pattern": recurrence_pattern, "attributes": attributes or {}}
self.settings = {
"recurrence_pattern": recurrence_pattern,
"attributes": attributes or {},
}
def execute(self):
for name, value in self.settings["attributes"].items():
@@ -110,6 +110,16 @@ class Usecase:
related_objects = list(inverse.RelatedObjects)
related_objects.remove(self.settings["task"])
inverse.RelatedObjects = related_objects
elif inverse.is_a("IfcRelAssignsToObject"):
if (
inverse.RelatingObject == self.settings["task"]
or len(inverse.RelatedObjects) == 1
):
self.file.remove(inverse)
else:
related_objects = list(inverse.RelatedObjects)
related_objects.remove(self.settings["task"])
inverse.RelatedObjects = related_objects
elif inverse.is_a("IfcRelAssignsToProcess"):
self.file.remove(inverse)
@@ -55,10 +55,32 @@ class Usecase:
definition=self.settings["work_schedule"],
relating_context=self.file.by_type("IfcContext")[0],
)
for rel in self.settings["work_schedule"].Controls:
for related_object in rel.RelatedObjects:
if related_object.is_a("IfcTask"):
if self.settings["work_schedule"].Declares:
for rel in self.settings["work_schedule"].Declares:
for work_schedule in rel.RelatedObjects:
ifcopenshell.api.run(
"sequence.remove_work_schedule",
self.file,
work_schedule=work_schedule,
)
for inverse in self.file.get_inverse(self.settings["work_schedule"]):
if inverse.is_a("IfcRelDefinesByObject"):
if (
inverse.RelatingObject == self.settings["work_schedule"]
or len(inverse.RelatedObjects) == 1
):
self.file.remove(inverse)
else:
related_objects = list(inverse.RelatedObjects)
related_objects.remove(self.settings["work_schedule"])
inverse.RelatedObjects = related_objects
elif inverse.is_a("IfcRelAssignsToControl"):
[
ifcopenshell.api.run(
"sequence.remove_task", self.file, task=related_object
)
for related_object in inverse.RelatedObjects
if related_object.is_a("IfcTask")
]
self.file.remove(self.settings["work_schedule"])
@@ -9,7 +9,7 @@ from codegen import indent
def reverse_compile(s):
return re.sub(
"\s*\-\s*EXPRESS_ONE_BASED_INDEXING",
r"\s*\-\s*EXPRESS_ONE_BASED_INDEXING",
"",
re.sub(
", )?+.(, INDETERMINATE)\\"[::-1],
@@ -216,6 +216,11 @@ def get_bounding_box_center(bbox):
def serialize_shape(shape):
shapes = BRepTools.BRepTools_ShapeSet()
# @todo provide method to get ifcopenshell's built-in occt version to
# see whether this is necessary
shapes.SetFormatNb(2)
shapes.Add(shape)
return shapes.WriteToString()
@@ -781,6 +781,18 @@ def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]):
):
to_delete.add(subelement)
subelement_queue.extend(ifc_file.traverse(subelement, max_levels=1)[1:])
# See #3052. IfcOpenShell is extremely slow in removing elements if
# the element has an inverse, and that inverse references that
# element in a big list. The most common example is an
# IfcPolygonalFaceSet with a Faces attribute of tens of thousands
# of IfcIndexedPolygonalFace. In this situation, removing a
# IfcIndexedPolygonalFace will take very, very long. If we are
# going to delete an element (i.e. added to the to_delete set), we
# clear any large lists (10 is an arbitrary threshold) to prevent
# this issue.
for i, attribute in enumerate(subelement):
if isinstance(attribute, tuple) and len(attribute) > 10:
subelement[i] = []
# We delete elements from subgraph in reverse order to allow batching to work
for subelement in filter(lambda e: e in to_delete, subgraph[::-1]):
ifc_file.remove(subelement)
@@ -0,0 +1,102 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import ifcopenshell.util.element
import ifcopenshell.util.date
def get_productivity(resource, should_inherit=True):
productivity = ifcopenshell.util.element.get_psets(
resource
).get("EPset_Productivity", None)
if should_inherit and not productivity:
# Proposal for Schema - If instance doesn't have any productivity, inherit it's parent's productivity
if not resource.Nests:
return None
else:
parent_resource = resource.Nests[0].RelatingObject
productivity = ifcopenshell.util.element.get_psets(
parent_resource
).get("EPset_Productivity", None)
return productivity
def get_unit_consumed(productivity):
duration = productivity.get("BaseQuantityConsumed", None)
if not duration:
return
return ifcopenshell.util.date.ifc2datetime(duration)
def get_quantity_produced(productivity):
if not productivity:
return 0
return productivity.get("BaseQuantityProducedValue", 0)
def get_quantity_produced_name(productivity):
if not productivity:
return ""
return productivity.get("BaseQuantityProducedName", "")
def get_total_quantity_produced(resource, quantity_name_in_process):
def get_product_quantity(product, quantity_name):
psets = ifcopenshell.util.element.get_psets(product)
for pset in psets.values():
for name, value in pset.items():
if name == quantity_name:
return float(value)
total = 0
products = get_parametric_resource_products(resource)
if quantity_name_in_process == "Count":
total = len(products)
else:
for product in products:
total += get_product_quantity(product, quantity_name_in_process)
return total
def get_parametric_resource_products(resource):
products = []
for rel in resource.HasAssignments or []:
if not rel.is_a("IfcRelAssignsToProcess"):
continue
for rel2 in rel.RelatingProcess.HasAssignments or []:
if not rel2.is_a("IfcRelAssignsToProduct"):
continue
products.append(rel2.RelatingProduct)
return products
def get_resource_required_work(resource):
productivity = get_productivity(resource)
if productivity:
quantity_produced = get_quantity_produced(productivity)
time_consumed = get_unit_consumed(productivity)
quantity_name_in_process = get_quantity_produced_name(productivity)
total_quantity_to_produce = get_total_quantity_produced(resource, quantity_name_in_process)
if not time_consumed or not quantity_produced or not total_quantity_to_produce:
return
iso_string = ""
if "T" in productivity.get("BaseQuantityConsumed", ""):
seconds = (time_consumed.days * 24 * 60 * 60) + time_consumed.seconds
productivity_ratio = seconds / quantity_produced
required_work = total_quantity_to_produce * productivity_ratio
iso_string = f"PT{required_work / 60 / 60}H"
else:
days = time_consumed.days + (time_consumed.seconds / (24 * 60 * 60))
productivity_ratio = days / quantity_produced
required_work = total_quantity_to_produce * productivity_ratio
iso_string = f"P{required_work}D"
return iso_string
@@ -568,3 +568,18 @@ class ShapeBuilder:
"position_z_axis": Vector((0, -1, 0)),
"extrusion_vector": Vector((0, 0, -1)),
}
def rotate_extrusion_kwargs_by_z(self, kwargs, angle, counter_clockwise=False):
"""shortcut to rotate extrusion kwargs by z axis
`kwargs` expected to have `position_x_axis` and `position_z_axis` keys
`angle` is a rotation value in radians
by default rotation is clockwise, to make it counter clockwise use `counter_clockwise` flag
"""
rot = Matrix.Rotation(-angle, 3, "Z")
kwargs = kwargs.copy() # prevent mutation of original kwargs
kwargs["position_x_axis"].rotate(rot)
kwargs["position_z_axis"].rotate(rot)
return kwargs
@@ -0,0 +1,35 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2023 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import test.bootstrap
import ifcopenshell.api
class TestRemoveBoundary(test.bootstrap.IFC4):
def test_run(self):
boundary = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcRelSpaceBoundary")
ifcopenshell.api.run("boundary.remove_boundary", self.file, boundary=boundary)
assert not self.file.by_type("IfcRelSpaceBoundary")
def test_removing_connection_geometry(self):
geometry = self.file.createIfcConnectionSurfaceGeometry()
boundary = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcRelSpaceBoundary")
boundary.ConnectionGeometry = geometry
ifcopenshell.api.run("boundary.remove_boundary", self.file, boundary=boundary)
assert not self.file.by_type("IfcRelSpaceBoundary")
assert not self.file.by_type("IfcConnectionSurfaceGeometry")
+1 -1
View File
@@ -2600,7 +2600,7 @@ std::pair<IfcUtil::IfcBaseClass*, double> IfcFile::getUnit(const std::string& un
);
IfcUtil::IfcBaseClass* unc = *mu->data().getArgument(
mu->declaration().as_entity()->attribute_index("ValueComponent")
mu->declaration().as_entity()->attribute_index("UnitComponent")
);
return_value.second *= static_cast<double>(*vlc->data().getArgument(0));
+1 -1
View File
@@ -1,6 +1,6 @@
Authlib==0.15.4
email-validator==1.1.3
Flask==2.0.1
Flask==2.3.2
Flask-Login==0.5.0
Flask-SQLAlchemy==2.5.1
Flask-WTF==0.15.1