Compare commits

...

42 Commits

Author SHA1 Message Date
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
63 changed files with 1702 additions and 385 deletions
@@ -22,26 +22,34 @@ from . import ui, operator, prop
classes = (
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)
@@ -24,8 +24,10 @@ 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 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 +40,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 +392,132 @@ 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(context.active_object)
for rel in element.BoundedBy or []:
boundary_obj = loader.load_boundary(rel, context.active_object)
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"}
@@ -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(
@@ -673,7 +673,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 +686,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 +704,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
@@ -732,7 +738,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
@@ -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)
@@ -47,6 +47,10 @@ def float_is_zero(f):
def bm_mesh_clean_up(bm):
# leave only object's footprint
min_z = min([v.co.z for v in bm.verts])
bmesh.ops.delete(bm, geom=[v for v in bm.verts if not float_is_zero(v.co.z - min_z)], context="VERTS")
# remove internal edges and faces
# adding missing faces so we could rely on `e.is_boundary` later
bmesh.ops.contextual_create(bm, geom=bm.edges[:])
@@ -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
@@ -135,6 +135,10 @@ class BIM_PT_authoring(Panel):
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 = self.layout.row(align=True)
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)
@@ -57,6 +57,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")]}),
@@ -274,11 +275,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):
@@ -581,6 +580,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
@@ -71,6 +73,18 @@ 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()
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):
@@ -113,3 +127,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,
@@ -19,6 +19,7 @@
import bpy
import blenderbim.tool as tool
import ifcopenshell
from ifcopenshell.util.doc import get_predefined_type_doc
def refresh():
@@ -34,6 +35,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 +223,27 @@ 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 WorkPlansData:
data = {}
@@ -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
@@ -275,13 +275,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 +1295,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 +1310,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 +1327,46 @@ 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)
@@ -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,8 @@ 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)
class BIMTaskTreeProperties(PropertyGroup):
@@ -501,3 +522,4 @@ class BIMAnimationProperties(PropertyGroup):
description="color picker",
update=update_color_progress,
)
should_show_task_bar_options: BoolProperty(name="Show Task Bar Options", default=False)
@@ -117,15 +117,19 @@ class BIM_PT_work_schedules(Panel):
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)
@@ -133,17 +137,43 @@ class BIM_PT_work_schedules(Panel):
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 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")
grid = self.layout.grid_flow(columns=2, even_columns=True)
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
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"
@@ -161,6 +191,8 @@ class BIM_PT_work_schedules(Panel):
self.draw_column_ui()
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)
def draw_task_operators(self):
@@ -178,18 +210,28 @@ 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")
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 +268,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 +279,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 +357,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 +373,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":
@@ -430,7 +505,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 +594,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 +658,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 +666,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 +682,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 +914,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())
+47 -15
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,21 +129,24 @@ 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()
@@ -165,7 +168,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()
@@ -430,6 +433,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 +467,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 +476,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 +541,18 @@ 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()
+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
+3 -2
View File
@@ -84,6 +84,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 +98,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 +317,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
+82 -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,83 @@ 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)
print("Resource:", current_resource, "productivity", productivity)
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,
)
+73 -37
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
@@ -1512,9 +1525,8 @@ class Sequence(blenderbim.core.tool.Sequence):
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 +1538,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
@@ -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]
@@ -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():
@@ -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):
@@ -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
+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