From af03953ef7938ea2eed970293f58d79eb1872926 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 Feb 2025 07:26:09 +1100 Subject: [PATCH 001/476] Apparently you couldn't edit composite profile defs. Now you can. --- src/bonsai/bonsai/bim/module/profile/ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/profile/ui.py b/src/bonsai/bonsai/bim/module/profile/ui.py index 6718396412..9dc0d06ae8 100644 --- a/src/bonsai/bonsai/bim/module/profile/ui.py +++ b/src/bonsai/bonsai/bim/module/profile/ui.py @@ -99,6 +99,7 @@ class BIM_PT_profiles(Panel): if active_profile.ifc_class in ( "IfcArbitraryClosedProfileDef", "IfcArbitraryProfileDefWithVoids", + "IfcCompositeProfileDef", ): if self.props.active_arbitrary_profile_id: row = self.layout.row(align=True) From f6c6cc9a1bfd93e75912d0292b008cba930d49c2 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 Feb 2025 07:35:55 +1100 Subject: [PATCH 002/476] Fix #6131. Fix bug where you could potentially create an invalid composite profile with no profiles. --- src/bonsai/bonsai/bim/module/profile/operator.py | 2 +- src/bonsai/bonsai/tool/model.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index da91595dc6..a2e5ca5626 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -288,7 +288,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): if not profile: def msg(self, context): - self.layout.label(text="INVALID PROFILE: " + indices[1]) + self.layout.label(text="INVALID PROFILE") bpy.context.window_manager.popup_menu(msg, title="Error", icon="ERROR") ProfileDecorator.install( diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 8b7a65bd6e..317e04ee1f 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1535,7 +1535,7 @@ class Model(bonsai.core.tool.Model): mesh: bpy.types.Mesh, position: Matrix | None = None, x_angle: Optional[float] = None, - ) -> Union[tuple, dict]: + ) -> tuple | dict | None: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if position is None: @@ -1750,7 +1750,9 @@ class Model(bonsai.core.tool.Model): else: profile_defs.append(tmp.createIfcArbitraryClosedProfileDef("AREA", None, curve)) - if len(profile_defs) == 1: + if total_profile_defs := len(profile_defs) == 0: + return + elif total_profile_defs == 1: profile_def = profile_defs[0] else: profile_def = tmp.createIfcCompositeProfileDef("AREA", None, profile_defs) From 79046e297061acc8f635fb6ae873959914f83719 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:47:51 -0800 Subject: [PATCH 003/476] Compute horizontal project of vertical spiral curve --- src/ifcgeom/mapping/IfcCurveSegment.cpp | 30 +++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/ifcgeom/mapping/IfcCurveSegment.cpp b/src/ifcgeom/mapping/IfcCurveSegment.cpp index f82569f840..c7e4ca1cfa 100644 --- a/src/ifcgeom/mapping/IfcCurveSegment.cpp +++ b/src/ifcgeom/mapping/IfcCurveSegment.cpp @@ -251,6 +251,7 @@ class curve_segment_evaluator { start_ = translate_if_param_value(inst->ParentCurve(), inst->SegmentStart()) * length_unit; length_ = translate_if_param_value(inst->ParentCurve(), inst->SegmentLength()) * length_unit; + projected_length_ = length_; // initialize with something reasonable if (inst) { curve_segment_placement_ = taxonomy::cast(mapping_->map(inst->Placement()))->ccomponents(); @@ -337,8 +338,6 @@ class curve_segment_evaluator { void set_spiral_function(double s, std::function fnX, std::function fnY) { if (segment_type_ == ST_HORIZONTAL || segment_type_ == ST_VERTICAL) { - projected_length_ = length_; - // start of trimmed curve double pcStartX = 0.0, pcStartY = 0.0; double pcStartDx = 1.0, pcStartDy = 0.0; @@ -400,6 +399,33 @@ class curve_segment_evaluator { m.col(3) = Eigen::Vector4d(x, y, 0, 1); return m; }); + + if (segment_type_ == ST_VERTICAL) { + // for vertical, the input curve length is measured along the spiral. + // projected_length_ is the domain of the curve, measured in the horizontal "Distance Along" coordinate + // The quickest way to get the projected length is the difference of the i-ordinates at the start and end of the spiral. + + // Evalute the parent curve at the start and end + Eigen::Matrix4d m1 = (*parent_curve_fn_)(start_); + Eigen::Matrix4d m2 = (*parent_curve_fn_)(start_ + length_); + + // parent curve point at start + double x1 = m1(0, 3); + double y1 = m1(1, 3); + + // parent curve point at end + double x2 = m2(0, 3); + double y2 = m2(1, 3); + + // direction of tangent at start of parent curve in curve coordinates + auto dx = (*curve_segment_placement_)(0, 0); + auto dy = -(*curve_segment_placement_)(1, 0); // -1 to rotation in opposite direction + auto X1 = x1 * dx - y1 * dy; // X of start point in global coordinates + auto X2 = x2 * dx - y2 * dy; // X of end point in global coordinates + projected_length_ = X2 - X1; // distance between points on the global X-axis + } else { + projected_length_ = length_; + } } else if (segment_type_ == ST_CANT) { Logger::Error(std::runtime_error("Unexpected segment type encountered - cant is handled in set_cant_spiral_function - should never get here")); parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); From 1e946fd98e1f7739d43c3c310a23aa4c0b1713f6 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 Feb 2025 13:07:29 +1100 Subject: [PATCH 004/476] Fix three small but significant typos in previous fixes. BTW @andrej730 that optimisation is awesome. Here's a timeit: import bpy import numpy as np import timeit obj = bpy.context.active_object verts = obj.data.vertices def foreach_get_method(): coords = np.empty(len(verts) * 3, dtype=np.float32) obj.data.vertices.foreach_get("co", coords) coords = coords.reshape(-1, 3) coords = coords.astype("d") coords /= 10 return coords def list_comprehension_method(): coords = [v.co / 10 for v in verts] return coords # Number of times each function will be executed num_runs = 10000 t_listcomp = timeit.timeit(list_comprehension_method, number=num_runs) t_foreach = timeit.timeit(foreach_get_method, number=num_runs) print("foreach method: {:.6f} seconds over {} runs".format(t_foreach, num_runs)) print("List comprehension method: {:.6f} seconds over {} runs".format(t_listcomp, num_runs)) # foreach method: 0.088656 seconds over 10000 runs # List comprehension method: 0.644446 seconds over 10000 runs --- src/bonsai/bonsai/bim/module/geometry/operator.py | 2 +- src/bonsai/bonsai/tool/blender.py | 1 + src/bonsai/bonsai/tool/model.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index e231f878dc..f592c0f98d 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -113,7 +113,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator): representation_type = representation.RepresentationType if representation_type in ("Brep", "AdvancedBrep"): item = builder.faceted_brep(verts, faces) - elif representation_type in ("Tessellation"): + elif representation_type == "Tessellation": item = builder.mesh(verts, faces) else: assert False, f"Unexpected representation type: '{representation_type}'." diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index f7e997158a..5a73420e25 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1150,6 +1150,7 @@ class Blender(bonsai.core.tool.Blender): # It's faster to get them as f and then convert to d # with .astype("d"), if precision is needed. coords = np.empty(len(verts) * 3, dtype="f") + verts.foreach_get("co", coords) coords = coords.reshape(-1, 3) return coords diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 317e04ee1f..2eb32b68f0 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1750,7 +1750,7 @@ class Model(bonsai.core.tool.Model): else: profile_defs.append(tmp.createIfcArbitraryClosedProfileDef("AREA", None, curve)) - if total_profile_defs := len(profile_defs) == 0: + if (total_profile_defs := len(profile_defs)) == 0: return elif total_profile_defs == 1: profile_def = profile_defs[0] From 39a30f2577461c1b0b712f73f1c6590226db6a92 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 Feb 2025 19:18:02 +1100 Subject: [PATCH 005/476] Redesign profile edit interface to be consistent like all other list+edit button interfaces I kept on clicking the wrong thing --- src/bonsai/bonsai/bim/module/profile/ui.py | 51 ++++++++++------------ 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/profile/ui.py b/src/bonsai/bonsai/bim/module/profile/ui.py index 9dc0d06ae8..9b8f65660a 100644 --- a/src/bonsai/bonsai/bim/module/profile/ui.py +++ b/src/bonsai/bonsai/bim/module/profile/ui.py @@ -80,8 +80,30 @@ class BIM_PT_profiles(Panel): else: row.prop(self.props, "profile_classes", text="") row.operator("bim.add_profile_def", text="", icon="ADD") - row.operator("bim.duplicate_profile_def", icon="DUPLICATE", text="") - row.operator("bim.select_by_profile", icon="RESTRICT_SELECT_OFF", text="") + + if active_profile: + row = self.layout.row(align=True) + row.alignment = "RIGHT" + + is_editable = active_profile.ifc_class in ( + "IfcArbitraryClosedProfileDef", + "IfcArbitraryProfileDefWithVoids", + "IfcCompositeProfileDef", + ) + if self.props.active_profile_id == active_profile.ifc_definition_id: + row.operator("bim.edit_profile", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_profile", text="", icon="CANCEL") + elif self.props.active_arbitrary_profile_id: + row.operator("bim.edit_arbitrary_profile", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL") + else: + row.operator("bim.duplicate_profile_def", icon="DUPLICATE", text="") + row.operator("bim.select_by_profile", icon="RESTRICT_SELECT_OFF", text="") + if is_editable: + row.operator("bim.enable_editing_arbitrary_profile", text="", icon="ITALIC") + op = row.operator("bim.enable_editing_profile", text="", icon="GREASEPENCIL") + op.profile = active_profile.ifc_definition_id + row.operator("bim.remove_profile_def", text="", icon="X").profile = active_profile.ifc_definition_id self.layout.template_list( "BIM_UL_profiles", @@ -96,21 +118,6 @@ class BIM_PT_profiles(Panel): row.prop(self.props, "is_filtering_material_profiles", text="Filter Material Profiles") if active_profile: - if active_profile.ifc_class in ( - "IfcArbitraryClosedProfileDef", - "IfcArbitraryProfileDefWithVoids", - "IfcCompositeProfileDef", - ): - if self.props.active_arbitrary_profile_id: - row = self.layout.row(align=True) - row.operator("bim.edit_arbitrary_profile", text="Save Arbitrary Profile", icon="CHECKMARK") - row.operator("bim.disable_editing_arbitrary_profile", text="", icon="CANCEL") - else: - row = self.layout.row() - row.operator( - "bim.enable_editing_arbitrary_profile", text="Edit Arbitrary Profile", icon="GREASEPENCIL" - ) - users_of_profile = ProfileData.data["active_profile_users"] self.layout.label(icon="INFO", text=f"Profile has {users_of_profile} inverse relationship(s) in project") @@ -128,13 +135,3 @@ class BIM_UL_profiles(UIList): row = layout.row(align=True) row.prop(item, "name", text="", emboss=False) row.label(text=item.ifc_class) - - if props.active_profile_id == item.ifc_definition_id: - row.operator("bim.edit_profile", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_profile", text="", icon="CANCEL") - elif props.active_profile_id: - row.operator("bim.remove_profile_def", text="", icon="X").profile = item.ifc_definition_id - else: - op = row.operator("bim.enable_editing_profile", text="", icon="GREASEPENCIL") - op.profile = item.ifc_definition_id - row.operator("bim.remove_profile_def", text="", icon="X").profile = item.ifc_definition_id From 2d54a48f8e6e5124299b2963351c5eb57964dbeb Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 Feb 2025 19:18:17 +1100 Subject: [PATCH 006/476] Add option for door glazing material --- src/bonsai/bonsai/bim/module/model/door.py | 2 ++ src/bonsai/bonsai/bim/module/model/prop.py | 2 ++ src/bonsai/bonsai/bim/module/model/ui.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/model/door.py b/src/bonsai/bonsai/bim/module/model/door.py index dbeb5f5dca..f6714717c9 100644 --- a/src/bonsai/bonsai/bim/module/model/door.py +++ b/src/bonsai/bonsai/bim/module/model/door.py @@ -94,6 +94,7 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None: if fallback_material := (int(props.lining_material) or int(props.panel_material)): lining_material = tool.Ifc.get().by_id(int(props.lining_material) or fallback_material) panel_material = tool.Ifc.get().by_id(int(props.panel_material) or fallback_material) + glazing_material = tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material) should_create_new_material_set = False if material := ifcopenshell.util.element.get_material(element): if ( @@ -122,6 +123,7 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None: styles = { "Lining": ifcopenshell.util.representation.get_material_style(lining_material, body), "Framing": ifcopenshell.util.representation.get_material_style(panel_material, body), + "Glazing": ifcopenshell.util.representation.get_material_style(glazing_material, body), } for item in model_representation.Items: if aspect := ifcopenshell.util.representation.get_item_shape_aspect(model_representation, item): diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 86efd9a026..39998a3a15 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -667,6 +667,7 @@ class BIMDoorProperties(PropertyGroup): # Material properties panel_material: bpy.props.EnumProperty(name="Panel Material", items=get_materials, options=set()) lining_material: bpy.props.EnumProperty(name="Lining Material", items=get_materials, options=set()) + glazing_material: bpy.props.EnumProperty(name="Glazing Material", items=get_materials, options=set()) if TYPE_CHECKING: is_editing: bool @@ -703,6 +704,7 @@ class BIMDoorProperties(PropertyGroup): # Material. panel_material: str lining_material: str + glazing_material: str def get_general_kwargs(self, convert_to_project_units=False): kwargs = { diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index bb5899113a..3d05611b6f 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -549,6 +549,8 @@ class BIM_PT_door(bpy.types.Panel): self.layout.label(text="Material Properties") self.layout.prop(props, "lining_material") self.layout.prop(props, "panel_material") + if props.transom_thickness: + self.layout.prop(props, "glazing_material") update_door_modifier_bmesh(context) From 9db1783062d2307ec905f634d0f48cc21b0b275e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 13 Feb 2025 15:33:55 +0500 Subject: [PATCH 007/476] more numpy arrays to match expected buffer type --- src/bonsai/bonsai/tool/loader.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 8e44f29c94..75aa1b04d2 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1057,11 +1057,10 @@ class Loader(bonsai.core.tool.Loader): mesh.polygons.foreach_set("loop_total", loop_total) mesh.polygons.foreach_set("use_smooth", use_smooth) else: - # TODO: optimize using correct numpy array types. faces_array = np.array(geometry.faces, dtype=object) - loop_total = np.array(tuple(len(face) for face in faces_array), dtype="I") - loop_start = np.cumsum((0,) + loop_total)[:-1] - vertex_index = np.concatenate(faces_array) + loop_total = np.fromiter((len(face) for face in faces_array), dtype="I") + loop_start = np.cumsum((0,) + loop_total, dtype="I")[:-1] + vertex_index = np.concatenate(faces_array, dtype="I", casting="unsafe") use_smooth = np.zeros(num_vertex_indices, dtype="?") mesh.loops.add(len(vertex_index)) From 0b32c89c2619e44818cfbc86efc87ae96157de85 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 13 Feb 2025 15:40:23 +0500 Subject: [PATCH 008/476] use_smooth - use correct buffer length apparently it wasn't creating any issues but still creating larger buffer than necessary --- src/bonsai/bonsai/tool/loader.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 75aa1b04d2..cb16446c5a 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1048,7 +1048,7 @@ class Loader(bonsai.core.tool.Loader): if is_triangulated: loop_start = np.arange(0, num_vertex_indices, 3, dtype="I") loop_total = np.full(total_faces, 3, dtype="I") - use_smooth = np.zeros(num_vertex_indices, dtype="?") + use_smooth = np.zeros(total_faces, dtype="?") mesh.loops.add(num_vertex_indices) mesh.loops.foreach_set("vertex_index", faces.ravel().astype("I")) @@ -1058,14 +1058,15 @@ class Loader(bonsai.core.tool.Loader): mesh.polygons.foreach_set("use_smooth", use_smooth) else: faces_array = np.array(geometry.faces, dtype=object) + total_faces = len(faces_array) loop_total = np.fromiter((len(face) for face in faces_array), dtype="I") loop_start = np.cumsum((0,) + loop_total, dtype="I")[:-1] vertex_index = np.concatenate(faces_array, dtype="I", casting="unsafe") - use_smooth = np.zeros(num_vertex_indices, dtype="?") + use_smooth = np.zeros(total_faces, dtype="?") mesh.loops.add(len(vertex_index)) mesh.loops.foreach_set("vertex_index", vertex_index) - mesh.polygons.add(len(loop_start)) + mesh.polygons.add(total_faces) mesh.polygons.foreach_set("loop_start", loop_start) mesh.polygons.foreach_set("loop_total", loop_total) mesh.polygons.foreach_set("use_smooth", use_smooth) From b88fd964bc2e4344a9cea1ec55ec2f48dba0102f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 13 Feb 2025 15:59:55 +0500 Subject: [PATCH 009/476] bim.refresh_library - add missing undo flag It's unsafe in Blender to leave operators that edit blend data without undo flag. --- src/bonsai/bonsai/bim/module/project/operator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 80ac7d7b31..9dcfc7ac60 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -210,6 +210,7 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector): class RefreshLibrary(bpy.types.Operator): bl_idname = "bim.refresh_library" bl_label = "Refresh Library" + bl_options = {"UNDO"} def execute(self, context): self.props = tool.Project.get_project_props() From 33d9e050ecf1812cd8ef2b98a2437b507b044853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 11 Feb 2025 16:38:29 -0300 Subject: [PATCH 010/476] Fix #6123 --- src/bonsai/bonsai/tool/snap.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index a54edb1559..37037473e9 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -546,19 +546,16 @@ class Snap(bonsai.core.tool.Snap): if snap_group["group"] == "Polyline": for p in snap_group["points"]: snapping_points.append((p["point"], p["type"], None)) - break if snap_group["group"] == "Measure": for p in snap_group["points"]: snapping_points.append((p["point"], p["type"], None)) if p["type"] == "Edge": edges.append(p) - break if snap_group["group"] == "Edge-Vertex": for p in snap_group["points"]: snapping_points.append((p["point"], p["type"], snap_group["object"])) if p["type"] == "Edge": edges.append(p) - if snap_group["group"] == "Object": obj = snap_group["object"] matrix = obj.matrix_world.copy() @@ -574,14 +571,12 @@ class Snap(bonsai.core.tool.Snap): snapping_points.append((p["point"], p["type"], obj)) if p["type"] == "Edge": edges.append(p) - break for snap_group in filtered_snaps: if snap_group["group"] == "Axis": axis_start = snap_group["axis_start"] axis_end = snap_group["axis_end"] snapping_points.append((snap_group["point"], "Axis", snap_obj)) - if snap_group["group"] == "Plane": snapping_points.append((snap_group["point"], "Plane", snap_obj)) From 2b7108a1d804e193dcaabfe7204be642ab31a820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 12 Feb 2025 15:26:11 -0300 Subject: [PATCH 011/476] Refactor snapping points data structure. --- .../bonsai/bim/module/model/polyline.py | 4 +- src/bonsai/bonsai/tool/raycast.py | 56 ++++++++++++++---- src/bonsai/bonsai/tool/snap.py | 58 +++++++++++-------- 3 files changed, 80 insertions(+), 38 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 435e05659a..d12417637e 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -657,7 +657,7 @@ class PolylineOperator: self.info = [ f"Axis: {self.tool_state.axis_method}", f"Plane: {self.tool_state.plane_method}", - f"Snap: {self.snapping_points[0][1]}", + f"Snap: {self.snapping_points[0]['type']}", ] instructions = self.instructions | custom_instructions if custom_instructions else self.instructions @@ -869,7 +869,7 @@ class PolylineOperator: detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) - if self.snapping_points[0][1] not in {"Plane", "Axis"}: + if self.snapping_points[0]["type"] not in {"Plane", "Axis"}: should_round = False tool.Polyline.calculate_distance_and_angle( diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 4b2d16c4ea..d896159ece 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -22,9 +22,8 @@ import copy from bpy_extras import view3d_utils import bonsai.core.tool import bonsai.tool as tool -import math import mathutils -from mathutils import Matrix, Vector +from mathutils import Vector class Raycast(bonsai.core.tool.Raycast): @@ -170,10 +169,12 @@ class Raycast(bonsai.core.tool.Raycast): distance = (v - intersection).length if distance < 0.2: snap_point = { + "object": obj, "type": "Vertex", "point": v, + "distance": distance - stick_factor, } - points.append([(distance - stick_factor), snap_point]) + points.append(snap_point) for edge in bm.edges: v1 = edge.verts[0].co @@ -187,10 +188,12 @@ class Raycast(bonsai.core.tool.Raycast): distance = (division_point - intersection).length if distance < 0.2: snap_point = { + "object": obj, "type": "Edge Center", "point": division_point, + "distance": distance, } - points.append([distance, snap_point]) + points.append(snap_point) intersection = tool.Cad.intersect_edges_v2((ray_target, loc), (v1, v2)) if intersection[0]: @@ -198,20 +201,22 @@ class Raycast(bonsai.core.tool.Raycast): distance = (intersection[1] - intersection[0]).length if distance < 0.2: snap_point = { + "object": obj, "type": "Edge", "point": intersection[1], "edge_verts": (v1, v2), + "distance": distance + 2 * stick_factor, } - points.append([(distance + 2 * stick_factor), snap_point]) + points.append(snap_point) bm.free() - snapping_points = [] - sorted_points = sorted(points, key=lambda x: x[0]) - for p in sorted_points: - point = copy.deepcopy(p) - snapping_points.append(point[1]) + # snapping_points = [] + # sorted_points = sorted(points, key=lambda x: x[0]) + # for p in sorted_points: + # point = copy.deepcopy(p) + # snapping_points.append(point[1]) - return snapping_points + return points @classmethod def ray_cast_to_polyline(cls, context, event): @@ -281,3 +286,32 @@ class Raycast(bonsai.core.tool.Raycast): intersection = Vector((0, 0, default_container_elevation)) return intersection + + @classmethod + def ray_cast_to_edge_intersection(cls, context, event, edges): + region = context.region + rv3d = context.region_data + mouse_pos = event.mouse_region_x, event.mouse_region_y + ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event) + + try: + loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction) + except: + loc = Vector((0, 0, 0)) + + for e1, e2 in zip(edges, edges[1:] + [edges[0]]): + if tool.Cad.are_vectors_equal(e1["point"], e2["point"], tolerance=0.1): + edge_intersection = tool.Cad.intersect_edges_v2(e1["edge_verts"], e2["edge_verts"]) + if edge_intersection[1]: + mouse_intersection, _ = mathutils.geometry.intersect_point_line(edge_intersection[1], ray_target, loc) + distance = (edge_intersection[1] - mouse_intersection).length + if distance < 0.2: + snap_point = { + "object": None, + "type": "Edge Intersection", + "point": edge_intersection[1], + "distance": distance, + } + return snap_point + + diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 37037473e9..3ce96b6aa7 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -428,7 +428,7 @@ class Snap(bonsai.core.tool.Snap): ) if obj.type == "EMPTY": snap_point = { - "points": [{"type": "Vertex", "point": obj.location}], + "points": [{"type": "Vertex", "point": obj.location, "distance": 10, "object": obj}], # TODO Get the real distance "group": "Edge-Vertex", "object": obj, } @@ -446,6 +446,7 @@ class Snap(bonsai.core.tool.Snap): "group": "Object", "object": snap_obj, "face_index": face_index, + "distance": 10, # TODO Get the real distance } detected_snaps.append(snap_point) else: @@ -456,6 +457,7 @@ class Snap(bonsai.core.tool.Snap): "group": "Object", "object": snap_obj, "face_index": face_index, + "distance": 10, # TODO Get the real distance } detected_snaps.append(snap_point) @@ -503,15 +505,21 @@ class Snap(bonsai.core.tool.Snap): if rot_intersection and polyline_points: snap_point = { "point": rot_intersection, + "object": None, "group": "Axis", + "type": "Axis", "axis_start": axis_start, "axis_end": axis_end, + "distance": 10, # TODO Get the real distance } detected_snaps.append(snap_point) snap_point = { "point": intersection, - "group": "Plane", + "object": None, + "group": "Plane", + "type": "Plane", + "distance": 10, # TODO Get the real distance } detected_snaps.append(snap_point) @@ -526,7 +534,7 @@ class Snap(bonsai.core.tool.Snap): if getattr(props, prop): options.append(props.rna_type.properties[prop].name) - filtered_points = [point for point in snapping_points if point[1] in options] + filtered_points = [point for point in snapping_points if point["type"] in options] return filtered_points def filter_snapping_groups_based_on_settings(detected_snaps): @@ -542,18 +550,17 @@ class Snap(bonsai.core.tool.Snap): snapping_points = [] edges = [] # Get edges to create edge-intersection snap for snap_group in filtered_snaps: - snap_obj = None if snap_group["group"] == "Polyline": for p in snap_group["points"]: - snapping_points.append((p["point"], p["type"], None)) + snapping_points.append(p) if snap_group["group"] == "Measure": for p in snap_group["points"]: - snapping_points.append((p["point"], p["type"], None)) + snapping_points.append(p) if p["type"] == "Edge": edges.append(p) if snap_group["group"] == "Edge-Vertex": for p in snap_group["points"]: - snapping_points.append((p["point"], p["type"], snap_group["object"])) + snapping_points.append(p) if p["type"] == "Edge": edges.append(p) if snap_group["group"] == "Object": @@ -565,10 +572,11 @@ class Snap(bonsai.core.tool.Snap): verts.append(matrix @ obj.data.vertices[i].co) snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj, face) if not snap_points: - snapping_points.append((snap_group["point"], "Face", obj)) + snap_group["type"] = "Face" + snapping_points.append(snap_group) else: for p in snap_points: - snapping_points.append((p["point"], p["type"], obj)) + snapping_points.append(p) if p["type"] == "Edge": edges.append(p) @@ -576,36 +584,36 @@ class Snap(bonsai.core.tool.Snap): if snap_group["group"] == "Axis": axis_start = snap_group["axis_start"] axis_end = snap_group["axis_end"] - snapping_points.append((snap_group["point"], "Axis", snap_obj)) + snapping_points.append(snap_group) if snap_group["group"] == "Plane": - snapping_points.append((snap_group["point"], "Plane", snap_obj)) + snapping_points.append(snap_group) # Edges intersection snap if edges: - for e1, e2 in zip(edges, edges[1:] + [edges[0]]): - if tool.Cad.are_vectors_equal(e1["point"], e2["point"], tolerance=0.1): - intersection = tool.Cad.intersect_edges_v2(e1["edge_verts"], e2["edge_verts"]) - if intersection[1]: - snapping_points.insert(0, (intersection[1], "Edge Intersection", None)) + snap_point = tool.Raycast.ray_cast_to_edge_intersection(context, event, edges) + if snap_point: + snapping_points.insert(0, snap_point) filtered_snapping_points = filter_snapping_points_based_on_settings(snapping_points) + filtered_snapping_points = sorted(filtered_snapping_points, key=lambda x: x["distance"]) # Make Axis first priority if tool_state.lock_axis or tool_state.axis_method in {"X", "Y", "Z"}: - cls.update_snapping_ref(filtered_snapping_points[0][0], filtered_snapping_points[0][1]) + cls.update_snapping_ref(filtered_snapping_points[0]["point"], filtered_snapping_points[0]["type"]) for point in filtered_snapping_points: - if point[1] == "Axis": - if filtered_snapping_points[0][1] not in {"Axis", "Plane"}: + if point["point"] == "Axis": + if filtered_snapping_points[0]["type"] not in {"Axis", "Plane"}: + # TODO Fix this based on the dictionary mixed_snap = cls.mix_snap_and_axis(filtered_snapping_points[0], axis_start, axis_end) for mixed_point in mixed_snap: filtered_snapping_points.insert(0, mixed_point) - cls.update_snapping_point(mixed_snap[0][0], mixed_snap[0][1]) + cls.update_snapping_point(mixed_snap[0]["point"], mixed_snap[0]["type"]) return filtered_snapping_points - cls.update_snapping_point(point[0], point[1]) + cls.update_snapping_point(point["point"], point["type"]) return filtered_snapping_points cls.update_snapping_point( - filtered_snapping_points[0][0], filtered_snapping_points[0][1], filtered_snapping_points[0][2] + filtered_snapping_points[0]["point"], filtered_snapping_points[0]["type"], filtered_snapping_points[0]["object"] ) return filtered_snapping_points @@ -613,10 +621,10 @@ class Snap(bonsai.core.tool.Snap): def modify_snapping_point_selection(cls, snapping_points, lock_axis=False): shifted_list = snapping_points[1:] + snapping_points[:1] if lock_axis: # Will only cycle through mix or axis - non_axis_snap = [point for point in snapping_points if point[1] not in {"Axis", "Mix"}] - axis_snap = [point for point in snapping_points if point[1] in {"Axis", "Mix"}] + non_axis_snap = [point for point in snapping_points if point["type"] not in {"Axis", "Mix"}] + axis_snap = [point for point in snapping_points if point["type"] in {"Axis", "Mix"}] shifted_list = axis_snap[1:] + axis_snap[:1] shifted_list.extend(non_axis_snap) - cls.update_snapping_point(shifted_list[0][0], shifted_list[0][1]) + cls.update_snapping_point(shifted_list[0]["point"], shifted_list[0]["type"]) return shifted_list From 1165ef3f3304c4dc28df2bd4289e66e737bf2900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 12 Feb 2025 16:55:25 -0300 Subject: [PATCH 012/476] More refactor of the snapping system, after 7e239ac511e991fd8ccf32add878024d921db38b --- src/bonsai/bonsai/tool/raycast.py | 7 +- src/bonsai/bonsai/tool/snap.py | 168 ++++++++++++------------------ 2 files changed, 69 insertions(+), 106 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index d896159ece..04e1a1fb9b 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -210,11 +210,6 @@ class Raycast(bonsai.core.tool.Raycast): points.append(snap_point) bm.free() - # snapping_points = [] - # sorted_points = sorted(points, key=lambda x: x[0]) - # for p in sorted_points: - # point = copy.deepcopy(p) - # snapping_points.append(point[1]) return points @@ -245,6 +240,8 @@ class Raycast(bonsai.core.tool.Raycast): snap_point = { "type": "Vertex", "point": vertex, + "distance": distance, + "object": None, } polyline_verts.append(snap_point) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 3ce96b6aa7..27c97a86f5 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -245,10 +245,10 @@ class Snap(bonsai.core.tool.Snap): # Creates a mixed snap point between the locked axis and the object snap # Then it sorts them to get the shortest first intersections = [] - intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((1, 0, 0)))) - intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((0, 1, 0)))) - intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((0, 0, 1)))) - sorted_intersections = sorted(((i, "Mix") for i in intersections if i is not None), reverse=False) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, Vector((1, 0, 0)))) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, Vector((0, 1, 0)))) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, Vector((0, 0, 1)))) + sorted_intersections = sorted((i for i in intersections if i is not None), reverse=False) return sorted_intersections @classmethod @@ -382,12 +382,9 @@ class Snap(bonsai.core.tool.Snap): if polyline_points: snap_points = tool.Raycast.ray_cast_to_polyline(context, event) if snap_points: - detected_snaps.append( - { - "group": "Polyline", - "points": snap_points, - } - ) + for point in snap_points: + point["group"] = "Polyline" + detected_snaps.append(point) # Measure measure_data = context.scene.BIMPolylineProperties.measurement_polyline @@ -395,12 +392,9 @@ class Snap(bonsai.core.tool.Snap): measure_points = measure.polyline_points snap_points = tool.Raycast.ray_cast_to_measure(context, event, measure_points) if snap_points: - detected_snaps.append( - { - "group": "Measure", - "points": snap_points, - } - ) + for point in snap_points: + point["group"] = "Measure" + detected_snaps.append(point) # Edge-Vertex for obj in objs_to_raycast: @@ -408,29 +402,23 @@ class Snap(bonsai.core.tool.Snap): if len(obj.data.polygons) == 0: snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj) if snap_points: - detected_snaps.append( - { - "group": "Edge-Vertex", - "object": obj, - "points": snap_points, - } - ) + for point in snap_points: + point["group"] = "Edge-Vertex" + detected_snaps.append(point) if obj.type == "CURVE": new_object = bpy.data.objects.new("new_object", obj.to_mesh().copy()) snap_points = tool.Raycast.ray_cast_by_proximity(context, event, new_object) if snap_points: - detected_snaps.append( - { - "group": "Edge-Vertex", - "object": obj, - "points": snap_points, - } - ) + for point in snap_points: + point["group"] = "Edge-Vertex" + detected_snaps.append(point) if obj.type == "EMPTY": snap_point = { - "points": [{"type": "Vertex", "point": obj.location, "distance": 10, "object": obj}], # TODO Get the real distance + "type": "Vertex", + "point": obj.location, + "distance": 10,# TODO Get the real distance + "object": obj, "group": "Edge-Vertex", - "object": obj, } detected_snaps.append(snap_point) @@ -438,28 +426,32 @@ class Snap(bonsai.core.tool.Snap): if (space.shading.type == "SOLID" and space.shading.show_xray) or ( space.shading.type == "WIREFRAME" and space.shading.show_xray_wireframe ): + results = [] for obj in objs_to_raycast: - snap_obj, hit, face_index = cast_rays_to_single_object(obj, mouse_pos) - if hit is not None: + results.append(cast_rays_to_single_object(obj, mouse_pos)) + else: + results = [] + results.append(cast_rays_and_get_best_object(objs_to_raycast, mouse_pos)) + for result in results: + snap_obj = result[0] + hit = result[1] + face_index = result[2] + if hit is not None: + snap_points = tool.Raycast.ray_cast_by_proximity(context, event, snap_obj, snap_obj.data.polygons[face_index]) + if snap_points: + for point in snap_points: + point["group"] = "Object" + detected_snaps.append(point) + else: snap_point = { "point": hit, + "type": "Face", "group": "Object", "object": snap_obj, "face_index": face_index, "distance": 10, # TODO Get the real distance } detected_snaps.append(snap_point) - else: - snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast, mouse_pos) - if hit is not None: - snap_point = { - "point": hit, - "group": "Object", - "object": snap_obj, - "face_index": face_index, - "distance": 10, # TODO Get the real distance - } - detected_snaps.append(snap_point) # Axis and Plane elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z @@ -527,7 +519,7 @@ class Snap(bonsai.core.tool.Snap): @classmethod def select_snapping_points(cls, context, event, tool_state, detected_snaps): - def filter_snapping_points_based_on_settings(snapping_points): + def filter_snapping_points_by_type(snapping_points): options = ["Plane", "Axis"] props = context.scene.BIMSnapProperties for prop in props.__annotations__.keys(): @@ -537,7 +529,7 @@ class Snap(bonsai.core.tool.Snap): filtered_points = [point for point in snapping_points if point["type"] in options] return filtered_points - def filter_snapping_groups_based_on_settings(detected_snaps): + def filter_snapping_points_by_group(detected_snaps): options = ["Edge-Vertex", "Axis", "Plane"] props = context.scene.BIMSnapGroups for prop in props.__annotations__.keys(): @@ -546,76 +538,50 @@ class Snap(bonsai.core.tool.Snap): filtered_groups = [group for group in detected_snaps if group["group"] in options] return filtered_groups - filtered_snaps = filter_snapping_groups_based_on_settings(detected_snaps) - snapping_points = [] + snaps_by_group = filter_snapping_points_by_group(detected_snaps) edges = [] # Get edges to create edge-intersection snap - for snap_group in filtered_snaps: - if snap_group["group"] == "Polyline": - for p in snap_group["points"]: - snapping_points.append(p) - if snap_group["group"] == "Measure": - for p in snap_group["points"]: - snapping_points.append(p) - if p["type"] == "Edge": - edges.append(p) - if snap_group["group"] == "Edge-Vertex": - for p in snap_group["points"]: - snapping_points.append(p) - if p["type"] == "Edge": - edges.append(p) - if snap_group["group"] == "Object": - obj = snap_group["object"] - matrix = obj.matrix_world.copy() - face = obj.data.polygons[snap_group["face_index"]] - verts = [] - for i in face.vertices: - verts.append(matrix @ obj.data.vertices[i].co) - snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj, face) - if not snap_points: - snap_group["type"] = "Face" - snapping_points.append(snap_group) - else: - for p in snap_points: - snapping_points.append(p) - if p["type"] == "Edge": - edges.append(p) - - for snap_group in filtered_snaps: - if snap_group["group"] == "Axis": - axis_start = snap_group["axis_start"] - axis_end = snap_group["axis_end"] - snapping_points.append(snap_group) - if snap_group["group"] == "Plane": - snapping_points.append(snap_group) + for snapping_point in snaps_by_group: + if snapping_point["group"] in {"Polyline", "Measure", "Edge-Vertex", "Object"}: + if snapping_point["type"] == "Edge": + edges.append(snapping_point) + if snapping_point["group"] == "Axis": + axis_start = snapping_point["axis_start"] + axis_end = snapping_point["axis_end"] # Edges intersection snap if edges: snap_point = tool.Raycast.ray_cast_to_edge_intersection(context, event, edges) if snap_point: - snapping_points.insert(0, snap_point) + snaps_by_group.insert(0, snap_point) - filtered_snapping_points = filter_snapping_points_based_on_settings(snapping_points) - filtered_snapping_points = sorted(filtered_snapping_points, key=lambda x: x["distance"]) + snaps_by_type = filter_snapping_points_by_type(snaps_by_group) + snaps_by_type = sorted(snaps_by_type, key=lambda x: x["distance"]) # Make Axis first priority if tool_state.lock_axis or tool_state.axis_method in {"X", "Y", "Z"}: - cls.update_snapping_ref(filtered_snapping_points[0]["point"], filtered_snapping_points[0]["type"]) - for point in filtered_snapping_points: - if point["point"] == "Axis": - if filtered_snapping_points[0]["type"] not in {"Axis", "Plane"}: + cls.update_snapping_ref(snaps_by_type[0]["point"], snaps_by_type[0]["type"]) + for point in snaps_by_type: + if point["type"] == "Axis": + if snaps_by_type[0]["type"] not in {"Axis", "Plane"}: # TODO Fix this based on the dictionary - mixed_snap = cls.mix_snap_and_axis(filtered_snapping_points[0], axis_start, axis_end) + obj = snaps_by_type[0]["object"] + mixed_snap = cls.mix_snap_and_axis(snaps_by_type[0]["point"], axis_start, axis_end) for mixed_point in mixed_snap: - filtered_snapping_points.insert(0, mixed_point) - cls.update_snapping_point(mixed_snap[0]["point"], mixed_snap[0]["type"]) - return filtered_snapping_points + snap_point = { + "point": mixed_point, + "type": "Mix", + "object": obj, + } + snaps_by_type.insert(0, snap_point) + cls.update_snapping_point(snap_point["point"], snap_point["type"]) + return snaps_by_type cls.update_snapping_point(point["point"], point["type"]) - return filtered_snapping_points + return snaps_by_type cls.update_snapping_point( - filtered_snapping_points[0]["point"], filtered_snapping_points[0]["type"], filtered_snapping_points[0]["object"] + snaps_by_type[0]["point"], snaps_by_type[0]["type"], snaps_by_type[0]["object"] ) - return filtered_snapping_points + return snaps_by_type @classmethod def modify_snapping_point_selection(cls, snapping_points, lock_axis=False): From 8acd5321b0c8fc57569a8b31f331eb8b5cae7926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Feb 2025 14:08:55 -0300 Subject: [PATCH 013/476] Fix polyline tool angle lock when using XZ and YZ plane --- src/bonsai/bonsai/tool/cad.py | 2 +- src/bonsai/bonsai/tool/polyline.py | 4 ++++ src/bonsai/bonsai/tool/snap.py | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/tool/cad.py b/src/bonsai/bonsai/tool/cad.py index 36d97fbd09..9812497967 100644 --- a/src/bonsai/bonsai/tool/cad.py +++ b/src/bonsai/bonsai/tool/cad.py @@ -110,7 +110,7 @@ class Cad: parameter = ( round(axis.z, 2) < 0 or (round(axis.y, 2) == 0 and round(axis.x < 0)) - or (round(axis.x, 2) == 0 and round(axis.y < 0)) + or (round(axis.x, 2) == 0 and round(axis.y > 0)) ) if new_angle is not None: rot_mat = Matrix.Rotation(new_angle, 3, axis) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index a3585c5a9d..17733c3392 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -155,8 +155,12 @@ class Polyline(bonsai.core.tool.Polyline): # Creates a fake "second to last" point away from the first point but in the same x axis # this allows to calculate the angle relative to x axis when there is only one point second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) + if tool_state.plane_method == "YZ": + second_to_last_point = Vector((last_point.x, last_point.y + 1000, last_point.z)) world_second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) + if tool_state.plane_method == "YZ": + world_second_to_last_point = Vector((last_point.x, last_point.y + 1000, last_point.z)) distance = (snap_vector - last_point).length if distance < 0: diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 27c97a86f5..b083c87bd4 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -140,7 +140,7 @@ class Snap(bonsai.core.tool.Snap): bpy.context.scene.BIMPolylineProperties.snap_mouse_ref.clear() @classmethod - def snap_on_axis(cls, intersection, tool_state, lock_angle=False): + def snap_on_axis(cls, intersection, tool_state): def create_axis_line_data(rot_mat, origin): length = 1000 direction = Vector((1, 0, 0)) @@ -488,10 +488,10 @@ class Snap(bonsai.core.tool.Snap): tool_state.snap_angle = 90 if tool_state.lock_axis or tool_state.axis_method: # Doesn't update snap_angle so that it keeps in the same axis - rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state, True) + rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state) else: rot_intersection, tool_state.snap_angle, axis_start, axis_end = cls.snap_on_axis( - intersection, tool_state, False + intersection, tool_state ) if rot_intersection and polyline_points: From b87cd33b121b2d574562cf7d03ad6c4b86bf4a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Feb 2025 14:14:15 -0300 Subject: [PATCH 014/476] Polyline tool a snapping system small refactor. --- src/bonsai/bonsai/tool/polyline.py | 22 +++++++++++----------- src/bonsai/bonsai/tool/snap.py | 28 ++++++---------------------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 17733c3392..f306491a8a 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -123,7 +123,7 @@ class Polyline(bonsai.core.tool.Polyline): default_container_elevation = 0 last_point_data = None - snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0] + mouse_point = context.scene.BIMPolylineProperties.snap_mouse_point[0] if last_point_data: last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) @@ -132,18 +132,18 @@ class Polyline(bonsai.core.tool.Polyline): if tool_state.is_input_on: if tool_state.use_default_container: - snap_vector = Vector( + mouse_vector = Vector( (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), default_container_elevation) ) else: - snap_vector = Vector( + mouse_vector = Vector( (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), input_ui.get_number_value("Z")) ) else: if tool_state.use_default_container: - snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) + mouse_vector = Vector((mouse_point.x, mouse_point.y, default_container_elevation)) else: - snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) + mouse_vector = Vector((mouse_point.x, mouse_point.y, mouse_point.z)) second_to_last_point = None if len(polyline_points) > 1: @@ -162,19 +162,19 @@ class Polyline(bonsai.core.tool.Polyline): if tool_state.plane_method == "YZ": world_second_to_last_point = Vector((last_point.x, last_point.y + 1000, last_point.z)) - distance = (snap_vector - last_point).length + distance = (mouse_vector - last_point).length if distance < 0: return if distance > 0: angle = tool.Cad.angle_3_vectors( - second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True + second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True ) # Round angle to the nearest 0.05 angle = round(angle / 0.05) * 0.05 orientation_angle = tool.Cad.angle_3_vectors( - world_second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True + world_second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True ) # Round angle to the nearest 0.05 @@ -188,10 +188,10 @@ class Polyline(bonsai.core.tool.Polyline): angle = 5 * round(angle / 5) factor = tool.Snap.get_increment_snap_value(context) distance = factor * round(distance / factor) - input_ui.set_value("X", snap_vector.x) - input_ui.set_value("Y", snap_vector.y) + input_ui.set_value("X", mouse_vector.x) + input_ui.set_value("Y", mouse_vector.y) if input_ui.get_number_value("Z") is not None: - input_ui.set_value("Z", snap_vector.z) + input_ui.set_value("Z", mouse_vector.z) input_ui.set_value("D", distance) input_ui.set_value("A", angle) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index b083c87bd4..f86daa6024 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -152,26 +152,6 @@ class Snap(bonsai.core.tool.Snap): return start, end - def create_axis_rectangle_data(origin): - size = 0.5 - direction = Vector((1, 0, 0)) - if tool_state.plane_method == "YZ": - direction = Vector((0, 0, 1)) - rot_mat = Matrix.Rotation(math.radians(360), 3, pivot_axis) - rot_dir = rot_mat.inverted() @ direction - v1 = origin + rot_dir * 0 - v2 = origin + rot_dir * size - if tool_state.plane_method == "XY": - angle = 270 - else: - angle = 90 - rot_mat = Matrix.Rotation(math.radians(angle), 3, pivot_axis) - rot_dir = rot_mat.inverted() @ direction - v3 = origin + rot_dir * size - v4 = v2 + rot_dir * size - - return (v1, v2, v3, v4) - # Makes the snapping point more or less sticky than others # It changes the distance and affects how the snapping point is sorted stick_factor = 0.15 @@ -217,14 +197,18 @@ class Snap(bonsai.core.tool.Snap): if is_on_rot_axis: elegible_axis.append((abs(proximity), axis)) - # Get the elegible axis with the lowest proximity + # Get the eligible axis with the lowest proximity if elegible_axis: proximity, axis = sorted(elegible_axis)[0] else: pass - # If lock axis is on it will use the snap angle so there is no need to search for elegible axis + # If lock axis is on it will use the snap angle so there is no need to search for eligible axis if elegible_axis or tool_state.lock_axis: + if tool_state.plane_method == "XZ": + axis = -axis + if tool_state.plane_method == "YZ": + axis = 90 - (axis * -1) # Don't know why this works rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis) rot_intersection = rot_mat @ translated_intersection start, end = create_axis_line_data(rot_mat, last_point) From c23a20df8493f59db6de970058277c820c842260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Feb 2025 14:15:32 -0300 Subject: [PATCH 015/476] Small fix --- src/bonsai/bonsai/tool/snap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index f86daa6024..39de027aae 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -459,7 +459,7 @@ class Snap(bonsai.core.tool.Snap): tool_state.snap_angle = 90 if tool_state.axis_method: # Doesn't update snap_angle so that it keeps in the same axis - rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state, True) + rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state) if tool_state.plane_method: if tool_state.plane_method in {"XY", "XZ"} and tool_state.axis_method == "X": From 5fe87bf5aa6c0be7f2f14a29669d3fe4cd3677f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Feb 2025 14:47:30 -0300 Subject: [PATCH 016/476] Modify `mix_snap_and_axis()` to select the snapping point closest to the last point. --- src/bonsai/bonsai/tool/snap.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 39de027aae..125c234caf 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -232,7 +232,16 @@ class Snap(bonsai.core.tool.Snap): intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, Vector((1, 0, 0)))) intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, Vector((0, 1, 0)))) intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, Vector((0, 0, 1)))) - sorted_intersections = sorted((i for i in intersections if i is not None), reverse=False) + + polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline + polyline_points = polyline_data[0].polyline_points if polyline_data else [] + if polyline_points: + last_point_data = polyline_points[-1] + last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) + else: + last_point = Vector(0, 0, 0) + + sorted_intersections = sorted((i for i in intersections if i is not None), key=lambda x: (x - last_point).length, reverse=True) return sorted_intersections @classmethod From f20bdd0ab636da08f2094430917e94e7b6d732ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Feb 2025 16:49:37 -0300 Subject: [PATCH 017/476] Fix issue where detected snap vector where being changed by other function. It was probably a reference issue. Copying the vectors solved the issue. --- src/bonsai/bonsai/tool/raycast.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 04e1a1fb9b..44dc52bec2 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -171,7 +171,7 @@ class Raycast(bonsai.core.tool.Raycast): snap_point = { "object": obj, "type": "Vertex", - "point": v, + "point": v.copy(), "distance": distance - stick_factor, } points.append(snap_point) @@ -190,7 +190,7 @@ class Raycast(bonsai.core.tool.Raycast): snap_point = { "object": obj, "type": "Edge Center", - "point": division_point, + "point": division_point.copy(), "distance": distance, } points.append(snap_point) @@ -203,12 +203,11 @@ class Raycast(bonsai.core.tool.Raycast): snap_point = { "object": obj, "type": "Edge", - "point": intersection[1], + "point": intersection[1].copy(), "edge_verts": (v1, v2), "distance": distance + 2 * stick_factor, } points.append(snap_point) - bm.free() return points From fb4ad5290f34119bbb0d942de6ddf97c5e93911c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Feb 2025 17:20:10 -0300 Subject: [PATCH 018/476] Fix `mix_snap_and_axis()` to ignore intersection too close from polyline. Follows 5fe87bf5aa6c0be7f2f14a29669d3fe4cd3677f9 --- src/bonsai/bonsai/tool/snap.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 125c234caf..05ad119960 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -241,7 +241,14 @@ class Snap(bonsai.core.tool.Snap): else: last_point = Vector(0, 0, 0) - sorted_intersections = sorted((i for i in intersections if i is not None), key=lambda x: (x - last_point).length, reverse=True) + valid_intersections = [] + for i in intersections: + if i is not None: + distance = (i-last_point).length + if not math.isclose(distance, 0.0, abs_tol=1e-4): + valid_intersections.append(i) + + sorted_intersections = sorted(valid_intersections, key=lambda x: (x - last_point).length, reverse=True) return sorted_intersections @classmethod From 768bf17b050c16944659054f2d446aaca6c82e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Feb 2025 18:26:44 -0300 Subject: [PATCH 019/476] Small fix in snap. --- src/bonsai/bonsai/tool/snap.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 05ad119960..c1c42a5482 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -205,10 +205,11 @@ class Snap(bonsai.core.tool.Snap): # If lock axis is on it will use the snap angle so there is no need to search for eligible axis if elegible_axis or tool_state.lock_axis: + # Adapt axis to make snap angle work with other plane method if tool_state.plane_method == "XZ": axis = -axis if tool_state.plane_method == "YZ": - axis = 90 - (axis * -1) # Don't know why this works + axis = 90 - (axis * -1) rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis) rot_intersection = rot_mat @ translated_intersection start, end = create_axis_line_data(rot_mat, last_point) @@ -272,18 +273,6 @@ class Snap(bonsai.core.tool.Snap): (offset, -offset), ) - # TODO Snap like Blender snap increment. Enable this when we have a proper snap settings. - # Still need adjustments to improve the feel - def round_vector_with_increment(intersection, relative_point): - for i in range(len(intersection)): - interval = round(abs(relative_point[i] % increment), 4) - dist = round(abs(intersection[i] % increment), 4) - reference = interval - dist - if reference <= 0.5: - intersection[i] = intersection[i] + reference - else: - intersection[i] = intersection[i] - (1 - reference) - def select_plane_method(): if not last_polyline_point: plane_origin = Vector((0, 0, 0)) @@ -416,7 +405,7 @@ class Snap(bonsai.core.tool.Snap): snap_point = { "type": "Vertex", "point": obj.location, - "distance": 10,# TODO Get the real distance + "distance": 10, # High value so it has low priority "object": obj, "group": "Edge-Vertex", } @@ -449,7 +438,7 @@ class Snap(bonsai.core.tool.Snap): "group": "Object", "object": snap_obj, "face_index": face_index, - "distance": 10, # TODO Get the real distance + "distance": 10, # High value so it has low priority } detected_snaps.append(snap_point) @@ -464,7 +453,6 @@ class Snap(bonsai.core.tool.Snap): axis_start = None axis_end = None - # TODO It only work for XY plane. Make it work also for None plane_method rot_intersection = None if not tool_state.plane_method: if tool_state.axis_method == "X": @@ -502,7 +490,7 @@ class Snap(bonsai.core.tool.Snap): "type": "Axis", "axis_start": axis_start, "axis_end": axis_end, - "distance": 10, # TODO Get the real distance + "distance": 10, # High value so it has low priority } detected_snaps.append(snap_point) @@ -511,7 +499,7 @@ class Snap(bonsai.core.tool.Snap): "object": None, "group": "Plane", "type": "Plane", - "distance": 10, # TODO Get the real distance + "distance": 10, # High value so it has low priority } detected_snaps.append(snap_point) @@ -563,7 +551,6 @@ class Snap(bonsai.core.tool.Snap): for point in snaps_by_type: if point["type"] == "Axis": if snaps_by_type[0]["type"] not in {"Axis", "Plane"}: - # TODO Fix this based on the dictionary obj = snaps_by_type[0]["object"] mixed_snap = cls.mix_snap_and_axis(snaps_by_type[0]["point"], axis_start, axis_end) for mixed_point in mixed_snap: From 3832077d0f0f42707ee85f75e04014645a0efa52 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 14 Feb 2025 13:06:04 +1100 Subject: [PATCH 020/476] Fix recent regression where util.shape.get_vertices returns a read-only array, so you can't edit in place (e.g. for offsets) --- src/bonsai/bonsai/bim/import_ifc.py | 5 ++--- src/bonsai/bonsai/tool/geometry.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 6eb0a99a9c..d2bdc2ed70 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -1040,7 +1040,7 @@ class IfcImporter: if cartesian_point_offset is False: mesh["has_cartesian_point_offset"] = False elif cartesian_point_offset is not None: - verts -= cartesian_point_offset + verts = verts - cartesian_point_offset mesh["has_cartesian_point_offset"] = True mesh["cartesian_point_offset"] = ( @@ -1048,9 +1048,8 @@ class IfcImporter: ) elif verts.size and tool.Loader.is_point_far_away(verts[0], is_meters=True): # Shift geometry close to the origin based off that first vert it found - verts_array = np.array(geometry.verts) offset = verts[0] - verts -= offset + verts = verts - offset mesh["has_cartesian_point_offset"] = True mesh["cartesian_point_offset"] = f"{offset[0]},{offset[1]},{offset[2]}" diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 40ee6dccd1..fca6541088 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1759,7 +1759,7 @@ class Geometry(bonsai.core.tool.Geometry): geometry = tool.Loader.create_generic_shape(item) verts = ifcopenshell.util.shape.get_vertices(geometry) if (cartesian_point_offset := cls.get_cartesian_point_offset(rep_obj)) is not None: - verts -= cartesian_point_offset + verts = verts - cartesian_point_offset tool.Loader.convert_geometry_to_mesh(geometry, obj.data, verts=verts) if ios_materials := list(obj.data["ios_materials"]): From 1014487d82ac9b73e037af4c30298e9e4b99c9f3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Feb 2025 17:49:43 +0500 Subject: [PATCH 021/476] black . --- src/bonsai/bonsai/tool/raycast.py | 6 +++--- src/bonsai/bonsai/tool/snap.py | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 44dc52bec2..f519462361 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -299,7 +299,9 @@ class Raycast(bonsai.core.tool.Raycast): if tool.Cad.are_vectors_equal(e1["point"], e2["point"], tolerance=0.1): edge_intersection = tool.Cad.intersect_edges_v2(e1["edge_verts"], e2["edge_verts"]) if edge_intersection[1]: - mouse_intersection, _ = mathutils.geometry.intersect_point_line(edge_intersection[1], ray_target, loc) + mouse_intersection, _ = mathutils.geometry.intersect_point_line( + edge_intersection[1], ray_target, loc + ) distance = (edge_intersection[1] - mouse_intersection).length if distance < 0.2: snap_point = { @@ -309,5 +311,3 @@ class Raycast(bonsai.core.tool.Raycast): "distance": distance, } return snap_point - - diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index c1c42a5482..fe84703d4e 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -245,7 +245,7 @@ class Snap(bonsai.core.tool.Snap): valid_intersections = [] for i in intersections: if i is not None: - distance = (i-last_point).length + distance = (i - last_point).length if not math.isclose(distance, 0.0, abs_tol=1e-4): valid_intersections.append(i) @@ -405,8 +405,8 @@ class Snap(bonsai.core.tool.Snap): snap_point = { "type": "Vertex", "point": obj.location, - "distance": 10, # High value so it has low priority - "object": obj, + "distance": 10, # High value so it has low priority + "object": obj, "group": "Edge-Vertex", } detected_snaps.append(snap_point) @@ -423,10 +423,12 @@ class Snap(bonsai.core.tool.Snap): results.append(cast_rays_and_get_best_object(objs_to_raycast, mouse_pos)) for result in results: snap_obj = result[0] - hit = result[1] + hit = result[1] face_index = result[2] if hit is not None: - snap_points = tool.Raycast.ray_cast_by_proximity(context, event, snap_obj, snap_obj.data.polygons[face_index]) + snap_points = tool.Raycast.ray_cast_by_proximity( + context, event, snap_obj, snap_obj.data.polygons[face_index] + ) if snap_points: for point in snap_points: point["group"] = "Object" @@ -487,7 +489,7 @@ class Snap(bonsai.core.tool.Snap): "point": rot_intersection, "object": None, "group": "Axis", - "type": "Axis", + "type": "Axis", "axis_start": axis_start, "axis_end": axis_end, "distance": 10, # High value so it has low priority @@ -496,9 +498,9 @@ class Snap(bonsai.core.tool.Snap): snap_point = { "point": intersection, - "object": None, - "group": "Plane", - "type": "Plane", + "object": None, + "group": "Plane", + "type": "Plane", "distance": 10, # High value so it has low priority } detected_snaps.append(snap_point) @@ -565,9 +567,7 @@ class Snap(bonsai.core.tool.Snap): cls.update_snapping_point(point["point"], point["type"]) return snaps_by_type - cls.update_snapping_point( - snaps_by_type[0]["point"], snaps_by_type[0]["type"], snaps_by_type[0]["object"] - ) + cls.update_snapping_point(snaps_by_type[0]["point"], snaps_by_type[0]["type"], snaps_by_type[0]["object"]) return snaps_by_type @classmethod From 20e1fc2618a282356afa2edf90b7ea24cdc616d0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Feb 2025 17:49:01 +0500 Subject: [PATCH 022/476] Fix bug in get_container not passing specified ifc_class to the parent #6155 --- .../ifcopenshell/util/element.py | 2 +- .../test/util/test_element.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index fb7bde9a2d..b8ef0711d5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -935,7 +935,7 @@ def get_container( return container container = get_aggregate(container) elif parent := get_parent(element): - return get_container(parent, should_get_direct) + return get_container(parent, should_get_direct, ifc_class) def get_referenced_structures(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index 1ab002f7b0..f40cb295da 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -793,6 +793,26 @@ class TestGetContainerIFC4(test.bootstrap.IFC4): ifcopenshell.api.aggregate.assign_object(self.file, products=[subelement], relating_object=element) assert subject.get_container(subelement, should_get_direct=True) is None + def test_getting_the_specific_spatial_container_of_an_element(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + building = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuilding") + storey = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuildingStorey") + ifcopenshell.api.aggregate.assign_object(self.file, products=[storey], relating_object=building) + ifcopenshell.api.spatial.assign_container(self.file, products=[element], relating_structure=storey) + assert subject.get_container(element, ifc_class="IfcBuilding") == building + assert subject.get_container(element, ifc_class="IfcSite") == None + + def test_getting_the_specific_spatial_container_of_an_element_indirectly(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcElementAssembly") + subelement = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.aggregate.assign_object(self.file, products=[subelement], relating_object=element) + building = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuilding") + storey = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcBuildingStorey") + ifcopenshell.api.aggregate.assign_object(self.file, products=[storey], relating_object=building) + ifcopenshell.api.spatial.assign_container(self.file, products=[element], relating_structure=storey) + assert subject.get_container(subelement, ifc_class="IfcBuilding") == building + assert subject.get_container(subelement, ifc_class="IfcSite") == None + class TestGetReferencedStructures(test.bootstrap.IFC4): def test_getting_references_of_an_element(self): From 3e1d1bf7fa1fc4e2e4f17b6158e4044e780dbf5b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Feb 2025 17:12:03 +0500 Subject: [PATCH 023/476] Library UI - tree view Example of tree view with libraries hierarchy - https://imgur.com/a/zPuaoPv Example of library assignment in non-tree view - https://imgur.com/a/9kZFblj --- .../bonsai/bim/module/project/__init__.py | 1 + src/bonsai/bonsai/bim/module/project/data.py | 9 +- .../bonsai/bim/module/project/operator.py | 143 +++++++++++++----- src/bonsai/bonsai/bim/module/project/prop.py | 74 ++++++--- src/bonsai/bonsai/bim/module/project/ui.py | 15 +- src/bonsai/bonsai/tool/project.py | 107 ++++++++----- 6 files changed, 235 insertions(+), 114 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index d87814c026..6cc33bb7b9 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -66,6 +66,7 @@ classes = ( operator.UnlinkIfc, operator.UnloadLink, workspace.ExploreHotkey, + prop.LibraryBreadcrumb, prop.LibraryElement, prop.FilterCategory, prop.Link, diff --git a/src/bonsai/bonsai/bim/module/project/data.py b/src/bonsai/bonsai/bim/module/project/data.py index fe9d5a8d47..50aad50e07 100644 --- a/src/bonsai/bonsai/bim/module/project/data.py +++ b/src/bonsai/bonsai/bim/module/project/data.py @@ -134,10 +134,11 @@ class ProjectLibraryData: @classmethod def project_libraries_enum(cls) -> list[tuple[str, str, str, str, int]]: - results = [ - ("*", "All Libraries", "Show all elements", "", 0), - ("-", "No Library", "Show elements without library assigned", "", 1), - ] + results = [] + project_libraries = cls.data["project_libraries"].values() + if not project_libraries: + results.append(("-", "No Library", "", "", 0)) + props = tool.Project.get_project_props() libs = [] for i, data in enumerate(cls.data["project_libraries"].values(), len(results)): diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 9dcfc7ac60..d4b864ec62 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -60,9 +60,10 @@ from bpy.app.handlers import persistent from ifcopenshell.geom import ShapeElementType from bonsai.bim.module.project.data import LinksData, ProjectLibraryData from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator, MeasureDecorator +from bonsai.bim.module.project.prop import BreadcrumbType from bonsai.bim.module.model.decorator import PolylineDecorator from bonsai.bim.module.model.polyline import PolylineOperator -from typing import Union, TYPE_CHECKING +from typing import Union, TYPE_CHECKING, Literal, get_args class NewProject(bpy.types.Operator): @@ -184,6 +185,7 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector): context.area.tag_redraw() if self.append_all: bpy.ops.bim.append_entire_library() + ProjectLibraryData.load() return {"FINISHED"} def invoke(self, context, event): @@ -218,16 +220,27 @@ class RefreshLibrary(bpy.types.Operator): self.props.library_elements.clear() self.props.library_breadcrumb.clear() - self.props.active_library_element = "" - library_file = IfcStore.library_file assert library_file - condition = tool.Project.get_filter_for_active_library() - for importable_type in sorted(tool.Project.get_appendable_asset_types()): - if (elements := library_file.by_type(importable_type)) and (elements := list(condition(elements))): - elements = self.props.add_library_asset_group(importable_type, len(elements)) + if not self.props.show_library_tree: + for appendable_type in sorted(tool.Project.get_appendable_asset_types()): + elements = library_file.by_type(appendable_type) + self.props.add_library_asset_class(appendable_type, len(elements)) + return {"FINISHED"} + # Library tree. + # Add entry for unassigned elements. + elements = set() + for importable_type in sorted(tool.Project.get_appendable_asset_types()): + elements.update(library_file.by_type(importable_type)) + rels = tool.Project.get_project_library_rels(library_file) + elements = {e for e in elements if not tool.Project.is_element_assigned_to_project_library(e, rels)} + self.props.add_library_project_library("Unassigned", len(elements), 0) + + ifc_project = library_file.by_type("IfcProject")[0] + hierarchy = tool.Project.get_project_hierarchy(library_file) + tool.Project.load_project_libraries_to_ui(ifc_project, hierarchy) return {"FINISHED"} @@ -235,10 +248,14 @@ class ChangeLibraryElement(bpy.types.Operator): bl_idname = "bim.change_library_element" bl_label = "Change Library Element" bl_options = {"REGISTER", "UNDO"} - element_name: bpy.props.StringProperty(description="IFC class to select") + element_name: bpy.props.StringProperty() + breadcrumb_type: bpy.props.EnumProperty(items=[(i, i, "") for i in get_args(BreadcrumbType)]) + library_id: bpy.props.IntProperty() if TYPE_CHECKING: element_name: str + breadcrumb_type: BreadcrumbType + library_id: int def execute(self, context): self.props = tool.Project.get_project_props() @@ -246,33 +263,81 @@ class ChangeLibraryElement(bpy.types.Operator): library_file = IfcStore.library_file assert library_file self.library_file = library_file - self.props.active_library_element = self.element_name crumb = self.props.library_breadcrumb.add() crumb.name = self.element_name + crumb.breadcrumb_type = self.breadcrumb_type + if self.breadcrumb_type == "LIBRARY": + crumb.library_id = self.library_id - filter_elements = tool.Project.get_filter_for_active_library() - elements = self.library_file.by_type(self.element_name) - elements = list(filter_elements(elements)) - ifc_classes_elements: dict[str, list[ifcopenshell.entity_instance]] = defaultdict(list) - for element in elements: - ifc_classes_elements[element.is_a()].append(element) + active_project_library = None + library_elements = None + project_library_rels = None + # Reverse to get last library in hierarchy. + for entry in reversed(self.props.library_breadcrumb): + if entry.breadcrumb_type == "LIBRARY": + if entry.library_id == 0: + # For unassigned elements. + active_project_library = "NO_LIBRARY" + project_library_rels = tool.Project.get_project_library_rels(library_file) + else: + active_project_library = library_file.by_id(entry.library_id) + library_elements = tool.Project.get_project_library_elements(active_project_library) + break + + def filter_elements(elements: list[ifcopenshell.entity_instance]) -> list[ifcopenshell.entity_instance]: + if active_project_library is None: + return elements + elif active_project_library == "NO_LIBRARY": + assert project_library_rels is not None + return [ + element + for element in elements + if not tool.Project.is_element_assigned_to_project_library(element, project_library_rels) + ] + else: + assert library_elements is not None + return [e for e in elements if e in library_elements] self.props.library_elements.clear() - if len(ifc_classes_elements) == 1 and list(ifc_classes_elements)[0] == self.element_name: - for name, ifc_definition_id in sorted( - [(self.get_name(e), e.id()) for e in ifc_classes_elements[self.element_name]] - ): - self.add_library_asset(name, ifc_definition_id) - else: - for ifc_class in sorted(ifc_classes_elements): - if ifc_class == self.element_name: - continue - self.props.add_library_asset_group(ifc_class, len(ifc_classes_elements[ifc_class])) - elements_ = ifc_classes_elements[self.element_name] - for name, ifc_definition_id, ifc_class in sorted([(self.get_name(e), e.id(), e.is_a()) for e in elements_]): - self.add_library_asset(name, ifc_definition_id) + if self.breadcrumb_type == "LIBRARY": + hierarchy = tool.Project.get_project_hierarchy(library_file) + assert active_project_library is not None + if active_project_library == "NO_LIBRARY" or not hierarchy[active_project_library]: + for appendable_type in sorted(tool.Project.get_appendable_asset_types()): + elements = library_file.by_type(appendable_type) + if elements := filter_elements(elements): + self.props.add_library_asset_class(appendable_type, len(elements)) + else: + tool.Project.load_project_libraries_to_ui(active_project_library, hierarchy) + else: # breadcrumb_type CLASS. + elements = self.library_file.by_type(self.element_name) + elements = list(filter_elements(elements)) + ifc_classes_elements: dict[str, list[ifcopenshell.entity_instance]] = defaultdict(list) + for element in elements: + ifc_classes_elements[element.is_a()].append(element) + + if len(ifc_classes_elements) == 1 and list(ifc_classes_elements)[0] == self.element_name: + for name, ifc_definition_id in sorted( + [(self.get_name(e), e.id()) for e in ifc_classes_elements[self.element_name]] + ): + self.add_library_asset(name, ifc_definition_id) + else: + for ifc_class in sorted(ifc_classes_elements): + if ifc_class == self.element_name: + continue + self.props.add_library_asset_class(ifc_class, len(ifc_classes_elements[ifc_class])) + elements_ = ifc_classes_elements[self.element_name] + for name, ifc_definition_id, ifc_class in sorted( + [(self.get_name(e), e.id(), e.is_a()) for e in elements_] + ): + self.add_library_asset(name, ifc_definition_id) + + # Could occur if all elements were assigned to a different library. + if len(self.props.library_elements) == 0: + bpy.ops.bim.rewind_library() + return {"FINISHED"} def get_name(self, element: ifcopenshell.entity_instance) -> str: @@ -300,10 +365,7 @@ class ChangeLibraryElement(bpy.types.Operator): elif has_context := element.HasContext: relating_context: ifcopenshell.entity_instance relating_context = has_context[0].RelatingContext - if selected_library in ("-", "*"): - new.is_declared = relating_context.is_a("IfcProjectLibrary") - else: - new.is_declared = relating_context == self.library_file.by_id(int(selected_library)) + new.is_declared = relating_context == self.library_file.by_id(int(selected_library)) # is_appended. try: @@ -329,10 +391,17 @@ class RewindLibrary(bpy.types.Operator): if total_breadcrumbs < 2: bpy.ops.bim.refresh_library() return {"FINISHED"} - element_name = self.props.library_breadcrumb[total_breadcrumbs - 2].name + current_element = self.props.library_breadcrumb[total_breadcrumbs - 2] + element_name = current_element.name + breadcrumb_type = current_element.breadcrumb_type + library_id = current_element.library_id self.props.library_breadcrumb.remove(total_breadcrumbs - 1) self.props.library_breadcrumb.remove(total_breadcrumbs - 2) - bpy.ops.bim.change_library_element(element_name=element_name) + bpy.ops.bim.change_library_element( + element_name=element_name, + breadcrumb_type=breadcrumb_type, + library_id=library_id, + ) return {"FINISHED"} @@ -360,10 +429,7 @@ class AssignLibraryDeclaration(bpy.types.Operator): library_file = IfcStore.library_file assert library_file - if props.selected_project_library in ("*", "-"): - project_library = library_file.by_type("IfcProjectLibrary")[0] - else: - project_library = library_file.by_id(int(props.selected_project_library)) + project_library = library_file.by_id(int(props.selected_project_library)) ifcopenshell.api.project.assign_declaration( library_file, @@ -648,6 +714,7 @@ class EditProjectLibrary(bpy.types.Operator): ifcopenshell.api.nest.assign_object(library_file, [project_library], new_parent_library) props.is_editing_project_library = False + bpy.ops.bim.refresh_library() return {"FINISHED"} def rollback(self, data): diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index fe23355846..c94de56614 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -35,7 +35,7 @@ from bpy.props import ( IntProperty, StringProperty, ) -from typing import TYPE_CHECKING, Literal, Union +from typing import TYPE_CHECKING, Literal, Union, get_args def get_export_schema(self: "BIMProjectProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: @@ -67,15 +67,15 @@ def update_library_file(self: "BIMProjectProperties", context: bpy.types.Context bpy.ops.bim.select_library_file(filepath=filepath.__str__()) ProjectLibraryData.load() props = tool.Project.get_project_props() - props.selected_project_library = "*" + library_file = IfcStore.library_file + assert library_file + project_library = next(iter(library_file.by_type("IfcProjectLibrary")), None) + props.selected_project_library = str(project_library.id()) if project_library else "-" def update_selected_project_library(self: "BIMProjectProperties", context: bpy.types.Context) -> None: - if self.filter_by_library: - bpy.ops.bim.refresh_library() - else: - # Ensure `.is_declared` up to date. - tool.Project.update_current_library_page() + # Ensure `.is_declared` up to date. + tool.Project.update_current_library_page() def get_project_libaries(self: "BIMProjectProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: @@ -84,10 +84,7 @@ def get_project_libaries(self: "BIMProjectProperties", context: bpy.types.Contex return ProjectLibraryData.data["project_libraries_enum"] -def filter_by_library_update(self: "BIMProjectProperties", context: bpy.types.Context) -> None: - if self.filter_by_library and self.selected_project_library == "*": - # Filter is toggled from OFF to ON, so it was showing all elements previously either way. - return +def show_library_tree_update(self: "BIMProjectProperties", context: bpy.types.Context) -> None: bpy.ops.bim.refresh_library() @@ -150,8 +147,12 @@ def update_filter_mode(self: "BIMProjectProperties", context: bpy.types.Context) new.total_elements = len(ifcopenshell.util.element.get_types(ifc_type)) +LibraryElementType = Literal["ASSET", "CLASS", "LIBRARY"] + + class LibraryElement(PropertyGroup): name: StringProperty(name="Name") + element_type: EnumProperty(items=[(i, i, "") for i in get_args(LibraryElementType)], name="Element Type") # Asset group. asset_count: IntProperty(name="Asset Count") # Asset. @@ -166,6 +167,7 @@ class LibraryElement(PropertyGroup): if TYPE_CHECKING: name: str + element_type: LibraryElementType asset_count: int ifc_definition_id: int is_declared: bool @@ -217,6 +219,18 @@ class EditedObj(PropertyGroup): obj: Union[bpy.types.Object, None] +BreadcrumbType = Literal["LIBRARY", "CLASS"] + + +class LibraryBreadcrumb(PropertyGroup): + breadcrumb_type: EnumProperty(items=[(i, i, "") for i in get_args(BreadcrumbType)]) + library_id: IntProperty(description="IFC Definition ID for libraries.") + + if TYPE_CHECKING: + breadcrumb_type: BreadcrumbType + library_id: int + + class BIMProjectProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing", default=False) is_loading: BoolProperty(name="Is Loading", default=False) @@ -226,8 +240,7 @@ class BIMProjectProperties(PropertyGroup): organisation_name: StringProperty(name="Organisation") organisation_email: StringProperty(name="Organisation Email") authorisation: StringProperty(name="Authoriser") - active_library_element: StringProperty(name="Enable Authoring Mode", default="") - library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=StrProperty) + library_breadcrumb: CollectionProperty(name="Library Breadcrumb", type=LibraryBreadcrumb) library_elements: CollectionProperty(name="Library Elements", type=LibraryElement) active_library_element_index: IntProperty(name="Active Library Element Index") filter_mode: bpy.props.EnumProperty( @@ -321,15 +334,15 @@ class BIMProjectProperties(PropertyGroup): library_file: EnumProperty(items=get_library_file, name="Library File", update=update_library_file) selected_project_library: EnumProperty( items=get_project_libaries, - name="Project Library", - description="Project library to display elements from", + name="Selected Project Library", + description="Selected project library to edit or to assign elements to", update=update_selected_project_library, ) - filter_by_library: BoolProperty( - name="Filter by Library", - description="Filter library elements based on selected library. If unselected can be used to assign selected library to library elements.", + show_library_tree: BoolProperty( + name="Show Library Tree", + description="Show project libraries hierarchy or just show the assets classes.", default=True, - update=filter_by_library_update, + update=show_library_tree_update, ) is_editing_project_library: BoolProperty( name="Is Editing Project Library", @@ -357,10 +370,19 @@ class BIMProjectProperties(PropertyGroup): def clipping_planes_objs(self) -> list[bpy.types.Object]: return list({cp.obj for cp in self.clipping_planes if cp.obj}) - def add_library_asset_group(self, name: str, asset_count: int) -> LibraryElement: + def add_library_project_library(self, name: str, asset_count: int, ifc_definition_id: int) -> LibraryElement: new = self.library_elements.add() new.name = name new.asset_count = asset_count + new.element_type = "LIBRARY" + new.ifc_definition_id = ifc_definition_id + return new + + def add_library_asset_class(self, name: str, asset_count: int) -> LibraryElement: + new = self.library_elements.add() + new.name = name + new.asset_count = asset_count + new.element_type = "CLASS" return new def get_library_element_index(self, lib_element: LibraryElement) -> int: @@ -375,8 +397,7 @@ class BIMProjectProperties(PropertyGroup): organisation_name: str organisation_email: str authorisation: str - active_library_element: str - library_breadcrumb: bpy.types.bpy_prop_collection_idprop[StrProperty] + library_breadcrumb: bpy.types.bpy_prop_collection_idprop[LibraryBreadcrumb] library_elements: bpy.types.bpy_prop_collection_idprop[LibraryElement] active_library_element_index: int filter_mode: Literal["NONE", "DECOMPOSITION", "IFC_CLASS", "IFC_TYPE", "WHITELIST", "BLACKLIST"] @@ -410,8 +431,8 @@ class BIMProjectProperties(PropertyGroup): template_file: str library_file: str - selected_project_library: Union[Literal["*", "-"], str] - filter_by_library: bool + selected_project_library: Union[Literal["-"], str] + show_library_tree: bool is_editing_project_library: bool editing_project_library_id: int project_library_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] @@ -424,6 +445,11 @@ class BIMProjectProperties(PropertyGroup): clipping_planes_active: int edited_objs: bpy.types.bpy_prop_collection_idprop[EditedObj] + def get_active_library_breadcrumb(self) -> Union[LibraryBreadcrumb, None]: + if self.library_breadcrumb: + return self.library_breadcrumb[-1] + return None + class MeasureToolSettings(PropertyGroup): measurement_type_items = [ diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 51505ed46e..361f2d1a61 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -392,7 +392,7 @@ class BIM_PT_project_library(Panel): library_file = IfcStore.library_file assert library_file - library_is_selected = props.selected_project_library not in ("*", "-") + library_is_selected = props.selected_project_library != "-" row = layout.row(align=True) row.prop(self.props, "selected_project_library", text="") @@ -403,7 +403,7 @@ class BIM_PT_project_library(Panel): if library_is_selected and not props.is_editing_project_library: row.prop(props, "is_editing_project_library", text="", icon="GREASEPENCIL") - row.prop(self.props, "filter_by_library", text="", icon="FILTER") + row.prop(self.props, "show_library_tree", text="", icon="OUTLINER") if props.is_editing_project_library: row = layout.row(align=True) @@ -420,8 +420,9 @@ class BIM_PT_project_library(Panel): return row = self.layout.row(align=True) - row.label(text=self.props.active_library_element or "Top Level Assets") - if self.props.active_library_element: + active_library_element = self.props.get_active_library_breadcrumb() + row.label(text=(active_library_element.name if active_library_element else "Top Level Assets")) + if active_library_element: row.operator("bim.rewind_library", icon="FRAME_PREV", text="") row.operator("bim.refresh_library", icon="FILE_REFRESH", text="") self.layout.template_list( @@ -511,9 +512,11 @@ class BIM_UL_library(UIList): ): if item: row = layout.row(align=True) - if not item.ifc_definition_id: + if item.element_type != "ASSET" and item.asset_count > 0: op = row.operator("bim.change_library_element", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) op.element_name = item.name + op.breadcrumb_type = item.element_type + op.library_id = item.ifc_definition_id row.label(text=item.name) if item.ifc_definition_id and item.is_declarable: if item.is_declared: @@ -522,7 +525,7 @@ class BIM_UL_library(UIList): else: op = row.operator("bim.assign_library_declaration", text="", icon="KEYFRAME", emboss=False) op.definition = item.ifc_definition_id - if item.ifc_definition_id: + if item.element_type == "ASSET": if item.is_appended: row.label(text="", icon="CHECKMARK") else: diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 96a5c58c36..ae966b49de 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -31,6 +31,7 @@ import bonsai.core.unit import bonsai.core.owner import bonsai.bim.schema import bonsai.tool as tool +from collections import defaultdict from bonsai.bim.ifc import IfcStore from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES from pathlib import Path @@ -39,6 +40,8 @@ from typing import Optional, Union, TYPE_CHECKING, Generator, Callable if TYPE_CHECKING: from bonsai.bim.module.project.prop import BIMProjectProperties +HiearchyDict = dict[ifcopenshell.entity_instance, "HiearchyDict"] + class Project(bonsai.core.tool.Project): @classmethod @@ -296,54 +299,43 @@ class Project(bonsai.core.tool.Project): ifcopenshell.api.document.remove_reference(ifc_file, reference) @classmethod - def get_filter_for_active_library( + def get_project_library_elements( + cls, project_library: ifcopenshell.entity_instance + ) -> set[ifcopenshell.entity_instance]: + return set(element for rel in project_library.Declares for element in rel.RelatedDefinitions) + + @classmethod + def get_project_library_rels(cls, ifc_file: ifcopenshell.file) -> set[ifcopenshell.entity_instance]: + return set(rel for lib in ifc_file.by_type("IfcProjectLibrary") for rel in lib.Declares) + + @classmethod + def is_element_assigned_to_project_library( cls, - ) -> Callable[[list[ifcopenshell.entity_instance]], Generator[ifcopenshell.entity_instance, None, None]]: - props = cls.get_project_props() - library_file = IfcStore.library_file - assert library_file - - selected_project_library = props.selected_project_library if props.filter_by_library else "*" - - if selected_project_library == "*": - - def condition(elements: list[ifcopenshell.entity_instance]): - yield from elements - - elif selected_project_library == "-": - - def condition(elements: list[ifcopenshell.entity_instance]): - for element in elements: - if not getattr(element, "HasContext", False): - yield element - - else: - project_library = library_file.by_id(int(selected_project_library)) - project_library_rels = set(project_library.Declares) - if project_library_rels: - - def condition(elements: list[ifcopenshell.entity_instance]): - for element in elements: - for rel in getattr(element, "HasContext", ()): - if rel in project_library_rels: - yield element - break - - else: - - def condition(elements: list[ifcopenshell.entity_instance]): - # Hacky way to create empty generator. - return - yield - - return condition + element: ifcopenshell.entity_instance, + project_library_rels: set[ifcopenshell.entity_instance], + ) -> bool: + if not (has_context := getattr(element, "HasContext", ())): + return False + return any(rel in project_library_rels for rel in has_context) @classmethod def update_current_library_page(cls): props = cls.get_project_props() - element_name = props.active_library_element + active_library_breadcrumb = props.get_active_library_breadcrumb() + change_back = False + if active_library_breadcrumb: + name = active_library_breadcrumb.name + breadcrumb_type = active_library_breadcrumb.breadcrumb_type + library_id = active_library_breadcrumb.library_id + change_back = True + bpy.ops.bim.rewind_library() - bpy.ops.bim.change_library_element(element_name=element_name) + if change_back: + bpy.ops.bim.change_library_element( + element_name=name, + breadcrumb_type=breadcrumb_type, + library_id=library_id, + ) @classmethod def get_parent_library(cls, project_library: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: @@ -352,3 +344,34 @@ class Project(bonsai.core.tool.Project): return nests[0].RelatingObject # IfcProject. return project_library.HasContext[0].RelatingContext + + @classmethod + def get_project_hierarchy(cls, ifc_file: ifcopenshell.file) -> HiearchyDict: + """Get project hierarchy in the following form: + + { + IfcProject: { IfcProjectLibrary A: { ... }, }, + IfcProjectLibrary A: { IfcProjectLibrary B: { ... } }, + IfcProjectLibrary B: { ... }, + } + + Use IfcProject to get hierarchy root. + + """ + hierarchy: HiearchyDict = defaultdict(dict) + for project_library in ifc_file.by_type("IfcProjectLibrary"): + parent_library = cls.get_parent_library(project_library) + hierarchy[parent_library][project_library] = hierarchy[project_library] + return hierarchy + + @classmethod + def load_project_libraries_to_ui( + cls, parent_library: ifcopenshell.entity_instance, hierarchy: HiearchyDict + ) -> None: + libraries = hierarchy[parent_library] + props = cls.get_project_props() + for project_library in libraries: + library_elements = tool.Project.get_project_library_elements(project_library) + props.add_library_project_library( + project_library.Name or "Unnamed", len(library_elements), project_library.id() + ) From a63c8e1d709ed9050b6866f7783b251450095c72 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 15 Feb 2025 16:09:26 +1100 Subject: [PATCH 024/476] Don't burn CPU cycles recalculating the door every draw call in parametric door interface --- src/bonsai/bonsai/bim/module/model/prop.py | 57 ++++++++++++++++------ src/bonsai/bonsai/bim/module/model/ui.py | 5 -- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 39998a3a15..4f5273e4b2 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -26,6 +26,7 @@ from bonsai.bim.module.model.data import AuthoringData from bpy.types import PropertyGroup, NodeTree from math import pi, radians from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator +from bonsai.bim.module.model.door import update_door_modifier_bmesh from typing import TYPE_CHECKING, Literal, get_args @@ -125,6 +126,10 @@ def update_x_angle(self, context): self.x_angle = 0 +def update_door(self, context): + update_door_modifier_bmesh(context) + + class BIMModelProperties(PropertyGroup): ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class) relating_type_id: bpy.props.EnumProperty( @@ -600,14 +605,19 @@ class BIMDoorProperties(PropertyGroup): non_si_units_props = ("is_editing", "door_type", "panel_width_ratio") is_editing: bpy.props.BoolProperty(default=False) door_type: bpy.props.EnumProperty( - name="Door Operation Type", items=tuple((i, i, "") for i in get_args(DoorType)), default="SINGLE_SWING_LEFT" + name="Door Operation Type", + items=tuple((i, i, "") for i in get_args(DoorType)), + default="SINGLE_SWING_LEFT", + update=update_door, ) - overall_height: bpy.props.FloatProperty(name="Overall Height", default=2.0, subtype="DISTANCE") - overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.9, subtype="DISTANCE") + overall_height: bpy.props.FloatProperty(name="Overall Height", default=2.0, subtype="DISTANCE", update=update_door) + overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.9, subtype="DISTANCE", update=update_door) # lining properties - lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE") - lining_thickness: bpy.props.FloatProperty(name="Lining Thickness", default=0.050, subtype="DISTANCE") + lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE", update=update_door) + lining_thickness: bpy.props.FloatProperty( + name="Lining Thickness", default=0.050, subtype="DISTANCE", update=update_door + ) lining_offset: bpy.props.FloatProperty( name="Lining Offset", description="Offset from the outer side of the wall (by Y-axis). " @@ -615,12 +625,13 @@ class BIMDoorProperties(PropertyGroup): "`0.025 mm` is good as default value", default=0.0, subtype="DISTANCE", + update=update_door, ) lining_to_panel_offset_x: bpy.props.FloatProperty( - name="Lining to Panel Offset X", default=0.025, subtype="DISTANCE" + name="Lining to Panel Offset X", default=0.025, subtype="DISTANCE", update=update_door ) lining_to_panel_offset_y: bpy.props.FloatProperty( - name="Lining to Panel Offset Y", default=0.025, subtype="DISTANCE" + name="Lining to Panel Offset Y", default=0.025, subtype="DISTANCE", update=update_door ) transom_thickness: bpy.props.FloatProperty( @@ -628,12 +639,14 @@ class BIMDoorProperties(PropertyGroup): description="Set values > 0 to add a transom.\n" "`0.050 mm` is good as default value", default=0.000, subtype="DISTANCE", + update=update_door, ) transom_offset: bpy.props.FloatProperty( name="Transom Offset", description="Distance from the bottom door opening to the beginning of the transom (unlike windows)", default=1.525, subtype="DISTANCE", + update=update_door, ) casing_thickness: bpy.props.FloatProperty( @@ -641,28 +654,44 @@ class BIMDoorProperties(PropertyGroup): description="Set values > 0 and LiningOffset = 0 to add a casing.", default=0.075, subtype="DISTANCE", + update=update_door, ) - casing_depth: bpy.props.FloatProperty(name="Casing Depth", default=0.005, subtype="DISTANCE") + casing_depth: bpy.props.FloatProperty(name="Casing Depth", default=0.005, subtype="DISTANCE", update=update_door) threshold_thickness: bpy.props.FloatProperty( - name="Threshold Thickness", description="Set values > 0 to add a threshold.", default=0.025, subtype="DISTANCE" + name="Threshold Thickness", + description="Set values > 0 to add a threshold.", + default=0.025, + subtype="DISTANCE", + update=update_door, + ) + threshold_depth: bpy.props.FloatProperty( + name="Threshold Depth", default=0.1, subtype="DISTANCE", update=update_door ) - threshold_depth: bpy.props.FloatProperty(name="Threshold Depth", default=0.1, subtype="DISTANCE") threshold_offset: bpy.props.FloatProperty( - name="Threshold Offset", description="`0.025 mm` is good as default value", default=0.000, subtype="DISTANCE" + name="Threshold Offset", + description="`0.025 mm` is good as default value", + default=0.000, + subtype="DISTANCE", + update=update_door, ) # panel properties - panel_depth: bpy.props.FloatProperty(name="Panel Depth", default=0.035, subtype="DISTANCE") + panel_depth: bpy.props.FloatProperty(name="Panel Depth", default=0.035, subtype="DISTANCE", update=update_door) panel_width_ratio: bpy.props.FloatProperty( name="Panel Width Ratio", description="Width of this panel, given as ratio " "relative to the total clear opening width of the door", default=1.0, soft_min=0, soft_max=1, + update=update_door, + ) + frame_thickness: bpy.props.FloatProperty( + name="Window Frame Thickness", default=0.035, subtype="DISTANCE", update=update_door + ) + frame_depth: bpy.props.FloatProperty( + name="Window Frame Depth", default=0.035, subtype="DISTANCE", update=update_door ) - frame_thickness: bpy.props.FloatProperty(name="Window Frame Thickness", default=0.035, subtype="DISTANCE") - frame_depth: bpy.props.FloatProperty(name="Window Frame Depth", default=0.035, subtype="DISTANCE") # Material properties panel_material: bpy.props.EnumProperty(name="Panel Material", items=get_materials, options=set()) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 3d05611b6f..69f9229400 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -33,10 +33,8 @@ from bonsai.bim.module.model.data import ( from bonsai.bim.module.model.prop import get_ifc_class from bonsai.bim.module.model.stair import regenerate_stair_mesh from bonsai.bim.module.model.window import update_window_modifier_bmesh -from bonsai.bim.module.model.door import update_door_modifier_bmesh from bonsai.bim.module.model.railing import update_railing_modifier_bmesh from bonsai.bim.module.model.roof import update_roof_modifier_bmesh -from bonsai.bim.helper import prop_with_search from collections.abc import Iterable @@ -551,9 +549,6 @@ class BIM_PT_door(bpy.types.Panel): self.layout.prop(props, "panel_material") if props.transom_thickness: self.layout.prop(props, "glazing_material") - - update_door_modifier_bmesh(context) - else: row.operator("bim.enable_editing_door", icon="GREASEPENCIL", text="") row.operator("bim.remove_door", icon="X", text="") From a0fa861b92637bcb4c6a434af21ee385d4f88756 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 15 Feb 2025 16:10:12 +1100 Subject: [PATCH 025/476] Shape builder can now created triangulated face sets --- .../ifcopenshell/util/shape_builder.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 81203c288f..fb33cdbede 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -1305,6 +1305,22 @@ class ShapeBuilder: ] return self.file.createIfcFacetedBrep(self.file.createIfcClosedShell(faces)) + def triangulated_face_set( + self, points: SequenceOfVectors, faces: Sequence[Sequence[int]] + ) -> ifcopenshell.entity_instance: + """ + Generate an IfcTriangulatedFaceSet + + Note that this is not available in IFC2X3. + + :param points: list of 3d coordinates + :param faces: list of triangles consisted of point indices (points indices starting from 0) + :return: IfcTriangulatedFaceSet + """ + ifc_points = self.file.createIfcCartesianPointList3D(ifc_safe_vector_type(points)) + ifc_faces = [[i + 1 for i in face][:3] for face in faces] + return self.file.createIfcTriangulatedFaceSet(Coordinates=ifc_points, CoordIndex=ifc_faces) + def polygonal_face_set( self, points: SequenceOfVectors, faces: Sequence[Sequence[int]] ) -> ifcopenshell.entity_instance: From 81b7f3dc3038d5d70de23ca594e2ac57e0c14cd8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 15 Feb 2025 18:26:13 +1100 Subject: [PATCH 026/476] New API function to set shape aspect constituents --- .../ifcopenshell/api/material/__init__.py | 2 + .../material/set_shape_aspect_constituents.py | 122 ++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/material/__init__.py index 6e57e28210..f836c5bd4f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/__init__.py @@ -55,6 +55,7 @@ from .remove_material import remove_material from .remove_material_set import remove_material_set from .remove_profile import remove_profile from .reorder_set_item import reorder_set_item +from .set_shape_aspect_constituents import set_shape_aspect_constituents from .unassign_material import unassign_material wrap_usecases(__path__, __name__) @@ -83,5 +84,6 @@ __all__ = [ "remove_material_set", "remove_profile", "reorder_set_item", + "set_shape_aspect_constituents", "unassign_material", ] diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py b/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py new file mode 100644 index 0000000000..acd00335da --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py @@ -0,0 +1,122 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# 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 . + +import ifcopenshell.api.style +import ifcopenshell.api.material +import ifcopenshell.util.element +import ifcopenshell.util.representation + + +def set_shape_aspect_constituents( + file: ifcopenshell.file, + *, + element: ifcopenshell.entity_instance, + context: ifcopenshell.entity_instance, + materials: dict[str, ifcopenshell.entity_instance], +) -> None: + """Assigns a material constituent set and sets styles based on shape aspects + + An IFC element may be assigned to a set of material constituents. For + example, a window may have a framing material and a glazing material. Each + constituent may have a name, such as "Framing" (which may be assigned to an + "Aluminium" material), and "Glazing" (assigned to a "Laminated Low-e Glass" + material). + + An IFC element's geometry may be composed of multiple geometric items. + These geometric items may have names, known as "Shape Aspects". For + example a solid extrusion for the framing named "Framing" and a solid + extrusion for the glass panel named "Glazing". + + A material may be associated with a style (i.e. colour). For example, a + grey style for the "Aluminium" material and a transparent blue style for + the "Laminated Low-e Glass" material. + + These three concepts of material constituents, shape aspects, and + associated styles are correlated. For example, if the name (e.g. "Framing") + of a material constituent and a shape aspect correlate, that means that the + geometric item inherits the style (i.e. grey). + + This function lets you specify named material constituents, and it'll + create a constituent set assigned to the element with those names. It'll + then find any geometric representation items with shape aspects matching + those names, and assign the correlating style. + + If an assigned material constituent set already exists matching those + values, it will be reused. If the values do not match, the existing + material constituent set will be removed if it is not used by anything + else. + + :param element: The IfcProduct or IfcTypeProduct + :param context: The IfcGeometricRepresentationContext, typically the body + context. You can get this via + :func:`ifcopenshell.util.representation.get_context`. + :param materials: The key is the name of the constituent, and the value is + the IfcMaterial. + + Example: + + .. code:: python + + # Create two materials + aluminium = ifcopenshell.api.material.add_material(model, name="AL01", category="aluminium") + glass = ifcopenshell.api.material.add_material(model, name="GLZ01", category="glass") + + # Auto assign material constituents and styles to items based on shape aspects + ifcopenshell.api.material.set_shape_aspect_constituents( + model, element=window, context=body, materials={ + "Framing": aluminium + "Lining": aluminium + "Glazing": glass + }) + """ + should_create_new_material_set = False + if material := ifcopenshell.util.element.get_material(element): + if ( + material.is_a("IfcMaterialConstituent") + and len(names := [c.Name for c in material.MaterialConstituents]) == len(materials) + and set(names) == set(materials.keys()) + ): + should_create_new_material_set = False + else: + should_create_new_material_set = True + ifcopenshell.api.material.unassign_material(file, products=[element]) + if not material.is_a("IfcMaterial") and not file.get_total_inverses(material): + ifcopenshell.api.material.remove_material_set(file, material=material) + else: + should_create_new_material_set = True + if should_create_new_material_set: + material_set = ifcopenshell.api.material.add_material_set(file, set_type="IfcMaterialConstituentSet") + for name, material in materials.items(): + ifcopenshell.api.material.add_constituent( + file, constituent_set=material_set, material=material, name=name + ) + ifcopenshell.api.material.assign_material(file, products=[element], material=material_set) + + styles = {n: ifcopenshell.util.representation.get_material_style(m, context) for n, m in materials.items()} + print('styels are', styles) + representation = ifcopenshell.util.representation.get_representation(element, context=context) + print('rep is', representation) + representation = ifcopenshell.util.representation.resolve_representation(representation) + print('rrep is', representation) + for item in representation.Items: + print('checking item', item) + if aspect := ifcopenshell.util.representation.get_item_shape_aspect(representation, item): + print('found', aspect.Name) + if style := styles.get(aspect.Name, None): + print('... and correlating style', style) + ifcopenshell.api.style.assign_item_style(file, item=item, style=style) From 71668fbaf13bc5d124059f2892f1108c2176bf88 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 15 Feb 2025 18:29:13 +1100 Subject: [PATCH 027/476] Only switch representation once when creating a door Previously it would switch representation (and sync changes) every single time a new representation was created (in the replace_obj_ifc_representation function). I think this is misplaced responsibility. The function should change the representation, but not be responsible for switching. --- src/bonsai/bonsai/bim/module/model/door.py | 94 +++++----------------- src/bonsai/bonsai/bim/module/model/prop.py | 2 +- src/bonsai/bonsai/bim/module/model/ui.py | 2 +- src/bonsai/bonsai/tool/model.py | 9 --- 4 files changed, 24 insertions(+), 83 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/door.py b/src/bonsai/bonsai/bim/module/model/door.py index f6714717c9..18e8b90f84 100644 --- a/src/bonsai/bonsai/bim/module/model/door.py +++ b/src/bonsai/bonsai/bim/module/model/door.py @@ -73,7 +73,7 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None: }, } - previously_active_context = tool.Geometry.get_active_representation_context(obj) + active_context = tool.Geometry.get_active_representation_context(obj) # ELEVATION_VIEW representation profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW") @@ -91,49 +91,22 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None: representation_data["part_of_product"] = ifcopenshell.util.representation.get_part_of_product(element, body) model_representation = ifcopenshell.api.run("geometry.add_door_representation", ifc_file, **representation_data) representation_data["part_of_product"] = None - if fallback_material := (int(props.lining_material) or int(props.panel_material)): - lining_material = tool.Ifc.get().by_id(int(props.lining_material) or fallback_material) - panel_material = tool.Ifc.get().by_id(int(props.panel_material) or fallback_material) - glazing_material = tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material) - should_create_new_material_set = False - if material := ifcopenshell.util.element.get_material(element): - if ( - material.is_a("IfcMaterialConstituent") - and len(names := [c.Name for c in material.MaterialConstituents]) == 2 - and set(names) == {"Lining", "Framing"} - ): - should_create_new_material_set = False - else: - should_create_new_material_set = True - ifcopenshell.api.material.unassign_material(ifc_file, products=[element]) - if not material.is_a("IfcMaterial") and not ifc_file.get_total_inverses(material): - ifcopenshell.api.material.remove_material_set(ifc_file, material=material) - else: - should_create_new_material_set = True - if should_create_new_material_set: - material_set = ifcopenshell.api.material.add_material_set(ifc_file, set_type="IfcMaterialConstituentSet") - ifcopenshell.api.material.add_constituent( - ifc_file, constituent_set=material_set, material=lining_material, name="Lining" - ) - ifcopenshell.api.material.add_constituent( - ifc_file, constituent_set=material_set, material=panel_material, name="Framing" - ) - ifcopenshell.api.material.assign_material(ifc_file, products=[element], material=material_set) - - styles = { - "Lining": ifcopenshell.util.representation.get_material_style(lining_material, body), - "Framing": ifcopenshell.util.representation.get_material_style(panel_material, body), - "Glazing": ifcopenshell.util.representation.get_material_style(glazing_material, body), - } - for item in model_representation.Items: - if aspect := ifcopenshell.util.representation.get_item_shape_aspect(model_representation, item): - if style := styles.get(aspect.Name, None): - ifcopenshell.api.style.assign_item_style(ifc_file, item=item, style=style) + tool.Model.replace_object_ifc_representation(body, obj, model_representation) + if fallback_material := (int(props.lining_material) or int(props.framing_material) or int(props.glazing_material)): + ifcopenshell.api.material.set_shape_aspect_constituents( + ifc_file, + element=element, + context=body, + materials={ + "Lining": tool.Ifc.get().by_id(int(props.lining_material) or fallback_material), + "Framing": tool.Ifc.get().by_id(int(props.framing_material) or fallback_material), + "Glazing": tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material), + }, + ) elif material := ifcopenshell.util.element.get_material(element): ifcopenshell.api.material.unassign_material(ifc_file, products=[element]) if not material.is_a("IfcMaterial") and not ifc_file.get_total_inverses(material): ifcopenshell.api.material.remove_material_set(ifc_file, material=material) - tool.Model.replace_object_ifc_representation(body, obj, model_representation) # Body/PLAN_VIEW representation plan_body = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW") @@ -165,38 +138,15 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None: ) tool.Model.replace_object_ifc_representation(plan_annotation, obj, plan_representation) - # adding switch representation at the end instead of changing order of representations - # to prevent #2744 - if tool.Geometry.get_active_representation_context(obj) != previously_active_context: - previously_active_representation = ifcopenshell.util.representation.get_representation( - element, - previously_active_context.ContextType, - previously_active_context.ContextIdentifier, - previously_active_context.TargetView, - ) - - if not previously_active_representation: - # we assume there is no representation because it was - # Plan/Annotation/PLAN_VIEW - previously_active_context = ifcopenshell.util.representation.get_context( - ifc_file, "Plan", "Body", "PLAN_VIEW" - ) - previously_active_representation = ifcopenshell.util.representation.get_representation( - element, - previously_active_context.ContextType, - previously_active_context.ContextIdentifier, - previously_active_context.TargetView, - ) - - bonsai.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=previously_active_representation, - should_reload=True, - is_global=True, - should_sync_changes_first=True, - ) + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=ifcopenshell.util.representation.get_representation(element, active_context), + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) # type attributes if tool.Ifc.get_schema() != "IFC2X3": diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 4f5273e4b2..779de2b553 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -694,8 +694,8 @@ class BIMDoorProperties(PropertyGroup): ) # Material properties - panel_material: bpy.props.EnumProperty(name="Panel Material", items=get_materials, options=set()) lining_material: bpy.props.EnumProperty(name="Lining Material", items=get_materials, options=set()) + framing_material: bpy.props.EnumProperty(name="Framing Material", items=get_materials, options=set()) glazing_material: bpy.props.EnumProperty(name="Glazing Material", items=get_materials, options=set()) if TYPE_CHECKING: diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 69f9229400..f7a80ef616 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -546,7 +546,7 @@ class BIM_PT_door(bpy.types.Panel): self.layout.use_property_split = True self.layout.label(text="Material Properties") self.layout.prop(props, "lining_material") - self.layout.prop(props, "panel_material") + self.layout.prop(props, "framing_material", text="Panel Material") if props.transom_thickness: self.layout.prop(props, "glazing_material") else: diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 2eb32b68f0..0956dd6041 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -950,15 +950,6 @@ class Model(bonsai.core.tool.Model): ifcopenshell.api.run( "geometry.assign_representation", ifc_file, product=ifc_element, representation=new_representation ) - geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=new_representation, - should_reload=True, - is_global=True, - should_sync_changes_first=False, - ) @classmethod def update_thumbnail_for_element(cls, element: ifcopenshell.entity_instance, refresh: bool = False) -> None: From d23cfcb2cfab6b556d0ee9675e17386114ed4764 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 15 Feb 2025 18:31:25 +1100 Subject: [PATCH 028/476] Fix #2981. Standard case windows can now have different materials and implement shape aspects. --- src/bonsai/bonsai/bim/module/model/prop.py | 5 ++ src/bonsai/bonsai/bim/module/model/ui.py | 8 +++- src/bonsai/bonsai/bim/module/model/window.py | 46 +++++++++++-------- .../api/geometry/add_window_representation.py | 35 +++++++++++++- 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 779de2b553..abf6fa9517 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -521,6 +521,11 @@ class BIMWindowProperties(PropertyGroup): name="Frame Thickness", size=3, default=[0.035] * 3, subtype="TRANSLATION" ) + # Material properties + lining_material: bpy.props.EnumProperty(name="Lining Material", items=get_materials, options=set()) + framing_material: bpy.props.EnumProperty(name="Framing Material", items=get_materials, options=set()) + glazing_material: bpy.props.EnumProperty(name="Glazing Material", items=get_materials, options=set()) + def get_general_kwargs(self, convert_to_project_units=False): kwargs = { "window_type": self.window_type, diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index f7a80ef616..91c4e3a0d1 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -448,8 +448,13 @@ class BIM_PT_window(bpy.types.Panel): for panel_i in range(number_of_panels): cols[panel_i + 1].prop(props, prop, index=panel_i, text="") - update_window_modifier_bmesh(context) + self.layout.use_property_split = True + self.layout.label(text="Material Properties") + self.layout.prop(props, "lining_material") + self.layout.prop(props, "framing_material", text="Panel Material") + self.layout.prop(props, "glazing_material") + update_window_modifier_bmesh(context) else: row.operator("bim.enable_editing_window", icon="GREASEPENCIL", text="") row.operator("bim.remove_window", icon="X", text="") @@ -492,7 +497,6 @@ class BIM_PT_window(bpy.types.Panel): prop_value = panel_props[prop_name][panel_i] r.label(text=str(prop_value)) r = cols[panel_i + 1].row() - else: row = self.layout.row() row.label(text="No Window Found") diff --git a/src/bonsai/bonsai/bim/module/model/window.py b/src/bonsai/bonsai/bim/module/model/window.py index c82847eacd..e940ec194d 100644 --- a/src/bonsai/bonsai/bim/module/model/window.py +++ b/src/bonsai/bonsai/bim/module/model/window.py @@ -73,7 +73,7 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None: } representation_data["panel_properties"].append(panel_data) - previously_active_context = tool.Geometry.get_active_representation_context(obj) + active_context = tool.Geometry.get_active_representation_context(obj) # ELEVATION_VIEW representation profile = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Profile", "ELEVATION_VIEW") @@ -88,8 +88,25 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None: # (Model/Body defined only BEFORE Plan/Body to prevent #2744) body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") representation_data["context"] = body + representation_data["part_of_product"] = ifcopenshell.util.representation.get_part_of_product(element, body) model_representation = ifcopenshell.api.run("geometry.add_window_representation", ifc_file, **representation_data) + representation_data["part_of_product"] = None tool.Model.replace_object_ifc_representation(body, obj, model_representation) + if fallback_material := (int(props.lining_material) or int(props.framing_material) or int(props.glazing_material)): + ifcopenshell.api.material.set_shape_aspect_constituents( + ifc_file, + element=element, + context=body, + materials={ + "Lining": tool.Ifc.get().by_id(int(props.lining_material) or fallback_material), + "Framing": tool.Ifc.get().by_id(int(props.framing_material) or fallback_material), + "Glazing": tool.Ifc.get().by_id(int(props.glazing_material) or fallback_material), + }, + ) + elif material := ifcopenshell.util.element.get_material(element): + ifcopenshell.api.material.unassign_material(ifc_file, products=[element]) + if not material.is_a("IfcMaterial") and not ifc_file.get_total_inverses(material): + ifcopenshell.api.material.remove_material_set(ifc_file, material=material) # PLAN_VIEW representation plan = ifcopenshell.util.representation.get_context(ifc_file, "Plan", "Body", "PLAN_VIEW") @@ -100,24 +117,15 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None: ) tool.Model.replace_object_ifc_representation(plan, obj, plan_representation) - # adding switch representation at the end instead of changing order of representations - # to prevent #2744 - if tool.Geometry.get_active_representation_context(obj) != previously_active_context: - previously_active_representation = ifcopenshell.util.representation.get_representation( - element, - previously_active_context.ContextType, - previously_active_context.ContextIdentifier, - previously_active_context.TargetView, - ) - bonsai.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=previously_active_representation, - should_reload=True, - is_global=True, - should_sync_changes_first=True, - ) + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=ifcopenshell.util.representation.get_representation(element, active_context), + should_reload=True, + is_global=True, + should_sync_changes_first=True, + ) # type attributes if tool.Ifc.get_schema() != "IFC2X3": diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py index b6ba723695..22fb99d583 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py @@ -232,7 +232,7 @@ def create_ifc_window( output_items = (lining_items, frame_extruded_items, [glass]) builder.translate(chain(*output_items), position) - return output_items + return {"Lining": lining_items, "Framing": frame_extruded_items, "Glazing": [glass]} # we use dataclass as we need default values for arguments @@ -363,6 +363,7 @@ def add_window_representation( partition_type: WINDOW_TYPE = "SINGLE_PANEL", lining_properties: Optional[Union[WindowLiningProperties, dict[str, Any]]] = None, panel_properties: Optional[list[Union[WindowPanelProperties, dict[str, Any]]]] = None, + part_of_product: Optional[ifcopenshell.entity_instance] = None, unit_scale: Optional[float] = None, ) -> ifcopenshell.entity_instance: """units in usecase_settings expected to be in ifc project units @@ -415,6 +416,7 @@ def add_window_representation( "partition_type": partition_type, "lining_properties": lining_properties, "panel_properties": panel_properties, + "part_of_product": part_of_product, } ) @@ -440,6 +442,9 @@ class Usecase: accumulated_height = [0] * len(panel_schema[0]) built_panels: list[int] = [] window_items: list[ifcopenshell.entity_instance] = [] + lining_items: list[ifcopenshell.entity_instance] = [] + framing_items: list[ifcopenshell.entity_instance] = [] + glazing_items: list[ifcopenshell.entity_instance] = [] lining_props: dict[str, Any] = self.settings["lining_properties"] lining_thickness: float = lining_props["LiningThickness"] @@ -728,13 +733,39 @@ class Usecase: x_offsets, ) built_panels.append(panel_i) - window_items.extend(chain(*current_window_items)) + window_items.extend(chain(*current_window_items.values())) + lining_items.extend(current_window_items["Lining"]) + framing_items.extend(current_window_items["Framing"]) + glazing_items.extend(current_window_items["Glazing"]) accumulated_height[column_i] += panel_height accumulated_width += panel_width builder.translate(window_items, (0, lining_offset, 0)) # wall offset representation = builder.get_representation(self.settings["context"], window_items) + if self.settings["part_of_product"]: + ifcopenshell.api.geometry.add_shape_aspect( + self.file, + "Lining", + items=lining_items, + representation=representation, + part_of_product=self.settings["part_of_product"], + ) + ifcopenshell.api.geometry.add_shape_aspect( + self.file, + "Framing", + items=framing_items, + representation=representation, + part_of_product=self.settings["part_of_product"], + ) + ifcopenshell.api.geometry.add_shape_aspect( + self.file, + "Glazing", + items=glazing_items, + representation=representation, + part_of_product=self.settings["part_of_product"], + ) + return representation @overload From 6ac6f13018191b39c8440bfc480298e9944359cc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 15 Feb 2025 19:03:52 +1100 Subject: [PATCH 029/476] Fix #5824. Prioritise 3D over 2D. Only then consider subcontexts. It's unexpected in our 3D default viewing world to have 2D first over 3D. --- .../ifcopenshell/util/representation.py | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/representation.py b/src/ifcopenshell-python/ifcopenshell/util/representation.py index 1255621608..b032501a20 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/representation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/representation.py @@ -343,9 +343,9 @@ def get_prioritised_contexts(ifc_file: ifcopenshell.file) -> list[ifcopenshell.e you may want to prioritise visualising certain contexts over others, determined by the context type, identifier, target view, and target scale. - The default prioritises subcontexts, then contexts. It then prioritises 3D, - then 2D. It then prioritises bodies, then others. It also prioritises model - views, then plan views, then others. + The default prioritises 3D, then 2D. It then prioritises subcontexts, then + contexts. It then prioritises bodies, then others. It also prioritises + model views, then plan views, then others. :param ifc_file: The model containing contexts :return: A list of IfcGeometricRepresentationContext (or SubContext) from @@ -383,14 +383,6 @@ def get_prioritised_contexts(ifc_file: ifcopenshell.file) -> list[ifcopenshell.e def sort_context(context): priority = [] - if context.ContextType in type_priority: - priority.append(len(type_priority) - type_priority.index(context.ContextType)) - else: - priority.append(0) - return tuple(priority) - - def sort_subcontext(context): - priority = [] if context.ContextType in type_priority: priority.append(len(type_priority) - type_priority.index(context.ContextType)) @@ -402,21 +394,16 @@ def get_prioritised_contexts(ifc_file: ifcopenshell.file) -> list[ifcopenshell.e else: priority.append(0) - if context.TargetView in target_view_priority: + if getattr(context, "TargetView", None) in target_view_priority: priority.append(len(target_view_priority) - target_view_priority.index(context.TargetView)) else: priority.append(0) - priority.append(context.TargetScale or 0) # Big then small + priority.append(getattr(context, "TargetScale", None) or 0) # Big then small return tuple(priority) - # Ideally, all representations should be in a subcontext, but some BIM programs don't do this correctly - return sorted(ifc_file.by_type("IfcGeometricRepresentationSubContext"), key=sort_subcontext, reverse=True) + sorted( - ifc_file.by_type("IfcGeometricRepresentationContext", include_subtypes=False), - key=sort_context, - reverse=True, - ) + return sorted(ifc_file.by_type("IfcGeometricRepresentationContext"), key=sort_context, reverse=True) def get_part_of_product( From aad4dad9a7689a745d40c18f8a3609cf3dc9e98b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Feb 2025 07:46:22 +1100 Subject: [PATCH 030/476] Migrate documentation for IfcTester and make optional deps mandatory in PyPI I don't like this, but PyPI makes it really, really hard to "discover" optional deps and what they're used for. So it's just easier to make it a mandatory dependency and advanced users can always strip it out. This stops user reports about "it doesn't work out of the box I only did pip install ifctester" --- src/ifcopenshell-python/docs/ifctester.rst | 93 ++++++++++++++++++++++ src/ifctester/README.md | 62 +-------------- src/ifctester/pyproject.toml | 2 +- 3 files changed, 95 insertions(+), 62 deletions(-) diff --git a/src/ifcopenshell-python/docs/ifctester.rst b/src/ifcopenshell-python/docs/ifctester.rst index 0682bff803..56208dd198 100644 --- a/src/ifcopenshell-python/docs/ifctester.rst +++ b/src/ifcopenshell-python/docs/ifctester.rst @@ -11,3 +11,96 @@ PyPI .. code-block:: pip install ifctester + +Examples +-------- + +You can execute IfcTester using a CLI. + +.. code-block:: console + + # Validate an IFC with an IDS and report to console + python -m ifctester example.ids example.ifc + + # Generate a HTML report instead + python -m ifctester example.ids example.ifc -r Html -o report.html + +Alternatively, you can use Python: + +.. code-block:: python + + import ifcopenshell + from ifctester import ids, reporter + + # Create new IDS + specs = ids.Ids(title="My IDS") + + # add specification to it + spec = ids.Specification(name="My first specification") + spec.applicability.append(ids.Entity(name="IFCWALL")) + requirement = ids.Property( + baseName="IsExternal", + value="TRUE", + propertySet="Pset_WallCommon", + dataType="IfcBoolean", + uri="https://identifier.buildingsmart.org/uri/.../prop/LoadBearing", + instructions="Walls need to be load bearing.", + cardinality="required") + spec.requirements.append(requirement) + specs.specifications.append(spec) + + # Save to a file + specs.to_xml("IDS.xml") + + # Open IFC file: + my_ifc = ifcopenshell.open("model.ifc") + + # Validate IFC model against IDS requirements: + specs.validate(my_ifc) + + # Show results in a console + reporter.Console(specs).report() + + # Alternatively, to JSON + report = reporter.Json(specs) + report.report() + report.to_file("report.json") + + # Or to ODS spreadsheet + report = reporter.Ods(specs) + report.report() + report.to_file("report.ods") + + # Or to HTML spreadsheet + report = reporter.Html(specs) + report.report() + report.to_file("report.html") + + # Or to BCF + report = reporter.Bcf(specs) + report.report() + report.to_file("report.bcf") + +CLI manual +---------- + +.. code-block:: console + + $ python -m ifctester -h + + usage: __main__.py [-h] [-r REPORTER] [--no-color] [--excel-safe] [-o OUTPUT] ids [ifc] + + Uses an IDS to audit an IFC + + positional arguments: + ids Path to an IDS + ifc Path to an IFC + + options: + -h, --help show this help message and exit + -r REPORTER, --reporter REPORTER + The reporting method to view audit results + --no-color Disable colour output (supported by Console reporting) + --excel-safe Make sure exported ODS is safely exported for Excel + -o OUTPUT, --output OUTPUT + Output file (supported for all types of reporting except Console) diff --git a/src/ifctester/README.md b/src/ifctester/README.md index 98c82ec22f..d97685161d 100644 --- a/src/ifctester/README.md +++ b/src/ifctester/README.md @@ -1,63 +1,3 @@ # ifctester -With **IfcTester**, you can author and read **Information Delivery Specification** - **IDS** - files and validate your IFC models against IDS to see if your model is compliant. After the audit, you can generate reports in console, as a web page, JSON or BCF file. It works from the command line, as a web app, or as a library. - -## How to use it - -### Command line use - - -```bash -# run console reporter -python -m ifctester example.ids example.ifc -python -m ifctester example.ids example.ifc -r Html -o report.html -``` - -Available flags: - -- ``-r`` / ``--reporter``: The reporting method to view audit results. Availabe reporters: Console, Txt, Json, Html, Ods, Bcf -- ``--no-color``: Disable colour output (supported by Console reporting). -- ``--excel-safe``: Make sure exported ODS is safely exported for Excel. -- ``-o`` / ``--output``: Output file (supported for all types of reporting except Console). - -### Code example - -```python -import ifcopenshell -from ifctester import ids, reporter - - -# create new IDS -my_ids = ids.Ids(title="My IDS") - -# add specification to it -my_spec = ids.Specification(name="My first specification") -my_spec.applicability.append(ids.Entity(name="IFCWALL")) -property = ids.Property( - baseName="IsExternal", - value="TRUE", - propertySet="Pset_WallCommon", - dataType="IfcBoolean", - uri="https://identifier.buildingsmart.org/uri/.../prop/LoadBearing", - instructions="Walls need to be load bearing.", - cardinality="required") -my_spec.requirements.append(property) -my_ids.specifications.append(my_spec) - -# Save such IDS to file -result = my_ids.to_xml("SampleIDS.xml") - -# open IFC file: -my_ifc = ifcopenshell.open("MyIfcModel.ifc") - -# validate IFC model against IDS requirements: -my_ids.validate(my_ifc) - -# show results: -reporter.Console(my_ids).report() -``` - - -### ifctester web app - -Can be started by `cd webapp && python app.py`. +Experimental webapp can be started by `cd webapp && python app.py`. diff --git a/src/ifctester/pyproject.toml b/src/ifctester/pyproject.toml index 04f67b5770..16ca9b1333 100644 --- a/src/ifctester/pyproject.toml +++ b/src/ifctester/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", ] -dependencies = ["ifcopenshell", "python-dateutil", "xmlschema", "numpy"] +dependencies = ["ifcopenshell", "python-dateutil", "xmlschema", "numpy", "odfpy", "pystache", "bcf-client"] [project.urls] Homepage = "http://ifcopenshell.org" From aff90e89f732eb1b9b4bddb24c1caf75903fa966 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Feb 2025 07:47:03 +1100 Subject: [PATCH 031/476] Don't burn CPU cycles for updating parametric windows --- src/bonsai/bonsai/bim/module/model/prop.py | 44 ++++++++++++++----- src/bonsai/bonsai/bim/module/model/ui.py | 3 -- .../material/set_shape_aspect_constituents.py | 6 --- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index abf6fa9517..7466a4f70b 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -27,6 +27,7 @@ from bpy.types import PropertyGroup, NodeTree from math import pi, radians from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator from bonsai.bim.module.model.door import update_door_modifier_bmesh +from bonsai.bim.module.model.window import update_window_modifier_bmesh from typing import TYPE_CHECKING, Literal, get_args @@ -130,6 +131,10 @@ def update_door(self, context): update_door_modifier_bmesh(context) +def update_window(self, context): + update_window_modifier_bmesh(context) + + class BIMModelProperties(PropertyGroup): ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class) relating_type_id: bpy.props.EnumProperty( @@ -439,6 +444,7 @@ def window_type_prop_update(self, context): number_of_panels, panels_data = self.window_types_panels[self.window_type] self.first_mullion_offset, self.second_mullion_offset = panels_data[0] self.first_transom_offset, self.second_transom_offset = panels_data[1] + update_window(self, context) # default prop values are in mm and converted later @@ -475,50 +481,66 @@ class BIMWindowProperties(PropertyGroup): window_type: bpy.props.EnumProperty( name="Window Type", items=window_types, default="SINGLE_PANEL", update=window_type_prop_update ) - overall_height: bpy.props.FloatProperty(name="Overall Height", default=0.9, subtype="DISTANCE") - overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.6, subtype="DISTANCE") + overall_height: bpy.props.FloatProperty( + name="Overall Height", default=0.9, subtype="DISTANCE", update=update_window + ) + overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.6, subtype="DISTANCE", update=update_window) # lining properties - lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE") - lining_thickness: bpy.props.FloatProperty(name="Lining Thickness", default=0.050, subtype="DISTANCE") - lining_offset: bpy.props.FloatProperty(name="Lining Offset", default=0.050, subtype="DISTANCE") + lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE", update=update_window) + lining_thickness: bpy.props.FloatProperty( + name="Lining Thickness", default=0.050, subtype="DISTANCE", update=update_window + ) + lining_offset: bpy.props.FloatProperty( + name="Lining Offset", default=0.050, subtype="DISTANCE", update=update_window + ) lining_to_panel_offset_x: bpy.props.FloatProperty( - name="Lining to Panel Offset X", default=0.025, subtype="DISTANCE" + name="Lining to Panel Offset X", default=0.025, subtype="DISTANCE", update=update_window ) lining_to_panel_offset_y: bpy.props.FloatProperty( - name="Lining to Panel Offset Y", default=0.025, subtype="DISTANCE" + name="Lining to Panel Offset Y", default=0.025, subtype="DISTANCE", update=update_window + ) + mullion_thickness: bpy.props.FloatProperty( + name="Mullion Thickness", default=0.050, subtype="DISTANCE", update=update_window ) - mullion_thickness: bpy.props.FloatProperty(name="Mullion Thickness", default=0.050, subtype="DISTANCE") first_mullion_offset: bpy.props.FloatProperty( name="First Mullion Offset", description="Distance from the first lining to the first mullion center", default=0.3, subtype="DISTANCE", + update=update_window, ) second_mullion_offset: bpy.props.FloatProperty( name="Second Mullion Offset", description="Distance from the first lining to the second mullion center", default=0.45, subtype="DISTANCE", + update=update_window, + ) + transom_thickness: bpy.props.FloatProperty( + name="Transom Thickness", default=0.050, subtype="DISTANCE", update=update_window ) - transom_thickness: bpy.props.FloatProperty(name="Transom Thickness", default=0.050, subtype="DISTANCE") first_transom_offset: bpy.props.FloatProperty( name="First Transom Offset", description="Distance from the first lining to the first transom center", default=0.3, subtype="DISTANCE", + update=update_window, ) second_transom_offset: bpy.props.FloatProperty( name="Second Transom Offset", description="Distance from the first lining to the second transom center", default=0.6, subtype="DISTANCE", + update=update_window, ) # panel properties - frame_depth: bpy.props.FloatVectorProperty(name="Frame Depth", size=3, default=[0.035] * 3, subtype="TRANSLATION") + frame_depth: bpy.props.FloatVectorProperty( + name="Frame Depth", size=3, default=[0.035] * 3, subtype="TRANSLATION", update=update_window + ) frame_thickness: bpy.props.FloatVectorProperty( - name="Frame Thickness", size=3, default=[0.035] * 3, subtype="TRANSLATION" + name="Frame Thickness", size=3, default=[0.035] * 3, subtype="TRANSLATION", update=update_window ) # Material properties diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 91c4e3a0d1..ba9bfeb60e 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -32,7 +32,6 @@ from bonsai.bim.module.model.data import ( ) from bonsai.bim.module.model.prop import get_ifc_class from bonsai.bim.module.model.stair import regenerate_stair_mesh -from bonsai.bim.module.model.window import update_window_modifier_bmesh from bonsai.bim.module.model.railing import update_railing_modifier_bmesh from bonsai.bim.module.model.roof import update_roof_modifier_bmesh from collections.abc import Iterable @@ -453,8 +452,6 @@ class BIM_PT_window(bpy.types.Panel): self.layout.prop(props, "lining_material") self.layout.prop(props, "framing_material", text="Panel Material") self.layout.prop(props, "glazing_material") - - update_window_modifier_bmesh(context) else: row.operator("bim.enable_editing_window", icon="GREASEPENCIL", text="") row.operator("bim.remove_window", icon="X", text="") diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py b/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py index acd00335da..4df3770083 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py @@ -108,15 +108,9 @@ def set_shape_aspect_constituents( ifcopenshell.api.material.assign_material(file, products=[element], material=material_set) styles = {n: ifcopenshell.util.representation.get_material_style(m, context) for n, m in materials.items()} - print('styels are', styles) representation = ifcopenshell.util.representation.get_representation(element, context=context) - print('rep is', representation) representation = ifcopenshell.util.representation.resolve_representation(representation) - print('rrep is', representation) for item in representation.Items: - print('checking item', item) if aspect := ifcopenshell.util.representation.get_item_shape_aspect(representation, item): - print('found', aspect.Name) if style := styles.get(aspect.Name, None): - print('... and correlating style', style) ifcopenshell.api.style.assign_item_style(file, item=item, style=style) From 774a770c4897d17d3b9b22b60e2944f20c22a702 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Feb 2025 08:20:59 +1100 Subject: [PATCH 032/476] Purge usage of mathutils in grid API functions The only thing left is add_representation which is Blender specific anyway and is slowly being refactored out. --- .../bonsai/bim/module/geometry/operator.py | 2 +- src/bonsai/bonsai/bim/module/model/grid.py | 4 +- src/bonsai/bonsai/tool/model.py | 8 ++ .../ifcopenshell/api/geometry/__init__.py | 22 ++--- .../ifcopenshell/api/grid/__init__.py | 5 +- .../api/grid/create_axis_curve.py | 80 +++++++------------ src/ifcopenshell-python/pyproject.toml | 3 - 7 files changed, 46 insertions(+), 78 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index f592c0f98d..c07f427851 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -474,7 +474,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): if product.is_a("IfcGridAxis"): # Grid geometry does not follow the "representation" paradigm and needs to be treated specially - ifcopenshell.api.grid.create_axis_curve(self.file, axis_curve=obj, grid_axis=product) + tool.Model.create_axis_curve(obj, product) return elif product.is_a("IfcRelSpaceBoundary"): # TODO refactor diff --git a/src/bonsai/bonsai/bim/module/model/grid.py b/src/bonsai/bonsai/bim/module/model/grid.py index 2d4cdfa501..0411f2875d 100644 --- a/src/bonsai/bonsai/bim/module/model/grid.py +++ b/src/bonsai/bonsai/bim/module/model/grid.py @@ -50,7 +50,7 @@ def add_object(self, context): "grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="UAxes", grid=grid ) tool.Ifc.link(result, obj) - ifcopenshell.api.run("grid.create_axis_curve", tool.Ifc.get(), axis_curve=obj, grid_axis=result) + tool.Model.create_axis_curve(obj, result) tool.Collector.assign(obj) for i in range(0, self.total_v): @@ -69,7 +69,7 @@ def add_object(self, context): "grid.create_grid_axis", tool.Ifc.get(), axis_tag=tag, uvw_axes="VAxes", grid=grid ) tool.Ifc.link(result, obj) - ifcopenshell.api.run("grid.create_axis_curve", tool.Ifc.get(), axis_curve=obj, grid_axis=result) + tool.Model.create_axis_curve(obj, result) tool.Collector.assign(obj) tool.Root.reload_grid_decorator() diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 0956dd6041..c20f2762fd 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1978,3 +1978,11 @@ class Model(bonsai.core.tool.Model): x_angle = Vector((0, 1)).angle_signed(Vector((y, z))) return x_angle + + @classmethod + def create_axis_curve(cls, obj: bpy.types.Object, grid_axis: ifcopenshell.entity_instance): + m = tool.Surveyor.get_absolute_matrix(obj) + points = [m @ np.array(v.co.to_4d()) for v in obj.data.vertices[0:2]] + ifcopenshell.api.grid.create_axis_curve( + tool.Ifc.get(), p1=points[0], p2=points[1], is_si=True, grid_axis=grid_axis + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py index 8a253eee73..49bfe5f758 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py @@ -26,32 +26,20 @@ geometry extrusions). from .. import wrap_usecases from .add_axis_representation import add_axis_representation from .add_boolean import add_boolean - -try: - from .add_door_representation import add_door_representation -except ModuleNotFoundError as e: - print(f"Note: API not available due to missing dependencies: geometry.add_door_representation - {e}") +from .add_door_representation import add_door_representation from .add_footprint_representation import add_footprint_representation from .add_mesh_representation import add_mesh_representation from .add_profile_representation import add_profile_representation - -try: - from .add_railing_representation import add_railing_representation -except ModuleNotFoundError as e: - print(f"Note: API not available due to missing dependencies: geometry.add_railing_representation - {e}") +from .add_railing_representation import add_railing_representation try: from .add_representation import add_representation -except ModuleNotFoundError as e: - print(f"Note: API not available due to missing dependencies: geometry.add_representation - {e}") +except ModuleNotFoundError: + pass # Silently fail. This is Blender / Bonsai specific and on its way out. from .add_shape_aspect import add_shape_aspect from .add_slab_representation import add_slab_representation from .add_wall_representation import add_wall_representation - -try: - from .add_window_representation import add_window_representation -except ModuleNotFoundError as e: - print(f"Note: API not available due to missing dependencies: geometry.add_window_representation - {e}") +from .add_window_representation import add_window_representation from .assign_representation import assign_representation from .connect_element import connect_element from .connect_path import connect_path diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/grid/__init__.py index 5cd7199e63..cc759ce7e0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/__init__.py @@ -23,10 +23,7 @@ A grid in IFC may contain two or more axes running in two or more directions. from .. import wrap_usecases -try: - from .create_axis_curve import create_axis_curve -except ModuleNotFoundError as e: - print(f"Note: API not available due to missing dependencies: grid.create_axis_curve - {e}") +from .create_axis_curve import create_axis_curve from .create_grid_axis import create_grid_axis from .remove_grid_axis import remove_grid_axis diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py b/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py index 7e3840753b..d65a96ee54 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py @@ -21,27 +21,26 @@ import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.unit import ifcopenshell.util.placement -from mathutils import Matrix # For now, we depend on Blender -import bpy.types +import numpy as np def create_axis_curve( - file: ifcopenshell.file, axis_curve: bpy.types.Object, grid_axis: ifcopenshell.entity_instance + file: ifcopenshell.file, + *, + p1: np.ndarray, + p2: np.ndarray, + grid_axis: ifcopenshell.entity_instance, + is_si: bool = True, ) -> None: """Adds curve geometry to a grid axis to represent the axis extents - This currently depends on the Blender geometry kernel to function. - An IFC grid will have a minimum of two axes (typically perpendicular). Each axis will then have a line which represents the extents of the axis. - :param axis_curve: The Blender object that contains a mesh data block with a - single edge. - :type axis_curve: bpy.types.Object + :param p1: The first point of the grid axis + :param p2: The second point of the grid axis :param grid_axis: The IfcGridAxis element to add geometry to. - :type grid_axis: ifcopenshell.entity_instance - :return: None - :rtype: None + :param is_si: If true, the points are in meters, not project units Example: @@ -54,48 +53,27 @@ def create_axis_curve( axis_1 = ifcopenshell.api.grid.create_grid_axis(model, axis_tag="1", uvw_axes="VAxes", grid=grid) - # Assume you have these Blender objects in your active Blender session - obj1 = bpy.data.objects.get("AxisA") - obj2 = bpy.data.objects.get("Axis1") - ifcopenshell.api.grid.create_axis_curve(model, axis_curve=obj1, grid_axis=axis_a) - ifcopenshell.api.grid.create_axis_curve(model, axis_curve=obj2, grid_axis=axis_1) + # By convention, alphabetic grids are horizontal, and numeric are vertical + ifcopenshell.api.grid.create_axis_curve( + model, p1=np.array((0., 0., 0.)), p2=np.array((10., 0., 0.)), grid_axis=axis_a) + ifcopenshell.api.grid.create_axis_curve( + model, p1=np.array((0., 0., 0.)), p2=np.array((0., 10., 0.)), grid_axis=axis_1) """ - usecase = Usecase() - usecase.file = file - usecase.settings = { - "axis_curve": axis_curve, # A Blender object - "grid_axis": grid_axis, - } - return usecase.execute() + existing_curve = grid_axis.AxisCurve + if is_si: + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) + p1 /= unit_scale + p2 /= unit_scale -class Usecase: - def execute(self): - existing_curve = self.settings["grid_axis"].AxisCurve - - self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - grid = [i for i in self.file.get_inverse(self.settings["grid_axis"]) if i.is_a("IfcGrid")][0] - grid_matrix_i = Matrix(ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)) - grid_matrix_i.translation *= self.settings["unit_scale"] - grid_matrix_i = grid_matrix_i.inverted() - points = [ - grid_matrix_i @ (self.settings["axis_curve"].matrix_world @ v.co) - for v in self.settings["axis_curve"].data.vertices[0:2] - ] - self.settings["grid_axis"].AxisCurve = self.file.createIfcPolyline( - [ - self.create_cartesian_point(points[0][0], points[0][1]), - self.create_cartesian_point(points[1][0], points[1][1]), - ] + grid = [i for i in file.get_inverse(grid_axis) if i.is_a("IfcGrid")][0] + grid_matrix_i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)) + grid_axis.AxisCurve = file.createIfcPolyline( + ( + file.createIfcCartesianPoint((grid_matrix_i @ p1).tolist()), + file.createIfcCartesianPoint((grid_matrix_i @ p2).tolist()), ) + ) - if existing_curve: - ifcopenshell.util.element.remove_deep2(self.file, existing_curve) - - def create_cartesian_point(self, x, y): - x = self.convert_si_to_unit(x) - y = self.convert_si_to_unit(y) - return self.file.createIfcCartesianPoint((x, y)) - - def convert_si_to_unit(self, co): - return co / self.settings["unit_scale"] + if existing_curve: + ifcopenshell.util.element.remove_deep2(file, existing_curve) diff --git a/src/ifcopenshell-python/pyproject.toml b/src/ifcopenshell-python/pyproject.toml index ce752e4720..02fcf321cc 100644 --- a/src/ifcopenshell-python/pyproject.toml +++ b/src/ifcopenshell-python/pyproject.toml @@ -25,9 +25,6 @@ dependencies = [ ] [project.optional-dependencies] -# mathutils is broken on Python < 3.10. -# See: https://gitlab.com/ideasman42/blender-mathutils/-/merge_requests/3 -geometry = ["mathutils"] dev = ["pytest"] [project.urls] From b226a79ee8b71deda8699ecd9384b7c5b76c4edc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Feb 2025 11:48:51 +1100 Subject: [PATCH 033/476] See #5888. Fix bug where polyline tools weren't part of the undo system. --- src/bonsai/bonsai/bim/ifc.py | 15 +++++++++++---- src/bonsai/bonsai/bim/module/geometry/operator.py | 4 ++-- src/bonsai/bonsai/bim/module/model/product.py | 8 +++++++- src/bonsai/bonsai/bim/module/model/profile.py | 14 ++++++++++---- src/bonsai/bonsai/bim/module/model/slab.py | 11 ++++++++--- src/bonsai/bonsai/bim/module/model/wall.py | 8 +++++++- src/bonsai/bonsai/bim/module/root/operator.py | 2 +- .../api/material/set_shape_aspect_constituents.py | 8 +++----- 8 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 9576c60459..f6413ee798 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -407,7 +407,12 @@ class IfcStore: obj.BIMObjectProperties.ifc_definition_id = 0 @staticmethod - def execute_ifc_operator(operator: tool.Ifc.Operator, context: bpy.types.Context, is_invoke=False) -> set[str]: + def execute_ifc_operator( + operator: tool.Ifc.Operator, + context: bpy.types.Context, + event=None, + method: Literal["EXECUTE", "INVOKE", "MODAL"] = "EXECUTE", + ) -> set[str]: bonsai.last_actions.append({"type": "operator", "name": operator.bl_idname}) bpy.context.scene.BIMProperties.is_dirty = True is_top_level_operator = not bool(IfcStore.current_transaction) @@ -436,10 +441,12 @@ class IfcStore: bonsai.bim.handler.refresh_ui_data() try: - if is_invoke: - result = getattr(operator, "_invoke")(context, None) - else: + if method == "EXECUTE": result = getattr(operator, "_execute")(context) + elif method == "INVOKE": + result = getattr(operator, "_invoke")(context, event) + elif method == "MODAL": + result = getattr(operator, "_modal")(context, event) except: bonsai.last_error = traceback.format_exc() # Try to ensure undo will work since Blender undo does work in case of errors. diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index c07f427851..c5d82936fb 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -1913,7 +1913,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def invoke(self, context, event): - return IfcStore.execute_ifc_operator(self, context, is_invoke=True) + return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE") def _invoke(self, context, event): if not tool.Ifc.get(): @@ -2089,7 +2089,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): should_save: bpy.props.BoolProperty(name="Should Save", default=True) def invoke(self, context, event): - return IfcStore.execute_ifc_operator(self, context, is_invoke=True) + return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE") def _invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: if not tool.Ifc.get(): diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 7edf9323f7..06ae1a4fa0 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -154,7 +154,7 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.add_element() -class AddOccurrence(bpy.types.Operator, PolylineOperator): +class AddOccurrence(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): bl_idname = "bim.add_occurrence" bl_label = "Add Occurrence" bl_options = {"REGISTER", "UNDO"} @@ -199,6 +199,9 @@ class AddOccurrence(bpy.types.Operator, PolylineOperator): snap_obj.select_set(False) def modal(self, context, event): + return IfcStore.execute_ifc_operator(self, context, event, method="MODAL") + + def _modal(self, context, event): # Ensure state of BIM tool props is valid props = tool.Model.get_model_props() relating_type_id = tool.Blender.get_enum_safe(props, "relating_type_id") @@ -251,6 +254,9 @@ class AddOccurrence(bpy.types.Operator, PolylineOperator): return {"RUNNING_MODAL"} def invoke(self, context, event): + return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE") + + def _invoke(self, context, event): super().invoke(context, event) ProductDecorator.install(context) self.tool_state.use_default_container = True diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index bbc2671314..00e594323b 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -32,9 +32,9 @@ import bonsai.core.type import bonsai.core.geometry import bonsai.core.material import bonsai.core.root -from math import pi, degrees, inf, atan2 -from mathutils import Vector, Matrix, Quaternion -from bonsai.bim.module.geometry.helper import Helper +from bonsai.bim.ifc import IfcStore +from math import pi, degrees, atan2 +from mathutils import Vector, Matrix from bonsai.bim.module.model.wall import DumbWallRecalculator from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator from bonsai.bim.module.model.polyline import PolylineOperator @@ -1109,7 +1109,7 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} -class DrawPolylineProfile(bpy.types.Operator, PolylineOperator): +class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): bl_idname = "bim.draw_polyline_profile" bl_label = "Draw Polyline Profile" bl_options = {"REGISTER", "UNDO"} @@ -1142,6 +1142,9 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator): DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"]) def modal(self, context, event): + return IfcStore.execute_ifc_operator(self, context, event, method="MODAL") + + def _modal(self, context, event): if not self.relating_type: self.report({"WARNING"}, "You need to select a profile type.") PolylineDecorator.uninstall() @@ -1192,6 +1195,9 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator): return {"RUNNING_MODAL"} def invoke(self, context, event): + return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE") + + def _invoke(self, context, event): super().invoke(context, event) ProductDecorator.install(context) self.tool_state.use_default_container = True diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index b362f1e736..de3078819d 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -31,9 +31,8 @@ import bonsai.core.geometry import bonsai.core.root import bonsai.tool as tool from bonsai.bim.ifc import IfcStore -from math import cos, radians +from math import cos from mathutils import Vector, Matrix -from bonsai.bim.module.geometry.helper import Helper from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator from bonsai.bim.module.model.polyline import PolylineOperator from bonsai.bim.module.model.wall import DumbWallRecalculator @@ -864,7 +863,7 @@ class AddSlabFromWall(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} -class DrawPolylineSlab(bpy.types.Operator, PolylineOperator): +class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): bl_idname = "bim.draw_polyline_slab" bl_label = "Draw Polyline Slab" bl_options = {"REGISTER", "UNDO"} @@ -905,6 +904,9 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator): DumbSlabPlaner().regenerate_from_occurence(element, material_set_usage) def modal(self, context, event): + return IfcStore.execute_ifc_operator(self, context, event, method="MODAL") + + def _modal(self, context, event): if not self.relating_type: self.report({"WARNING"}, "You need to select a slab type.") PolylineDecorator.uninstall() @@ -975,6 +977,9 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator): return {"RUNNING_MODAL"} def invoke(self, context, event): + return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE") + + def _invoke(self, context, event): super().invoke(context, event) ProductDecorator.install(context) self.tool_state.use_default_container = True diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 73df5e8d95..5a94be7528 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -350,7 +350,7 @@ class AddWallsFromSlab(bpy.types.Operator, tool.Ifc.Operator): DumbWallJoiner().join_V(wall2["obj"], wall1["obj"]) -class DrawPolylineWall(bpy.types.Operator, PolylineOperator): +class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): bl_idname = "bim.draw_polyline_wall" bl_label = "Draw Polyline Wall" bl_options = {"REGISTER", "UNDO"} @@ -399,6 +399,9 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator): DumbWallJoiner().join_V(wall2["obj"], wall1["obj"]) def modal(self, context, event): + return IfcStore.execute_ifc_operator(self, context, event, method="MODAL") + + def _modal(self, context, event): if not self.relating_type: self.report({"WARNING"}, "You need to select a wall type.") PolylineDecorator.uninstall() @@ -471,6 +474,9 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator): return {"RUNNING_MODAL"} def invoke(self, context, event): + return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE") + + def _invoke(self, context, event): super().invoke(context, event) ProductDecorator.install(context) self.tool_state.use_default_container = True diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 0ba93cc800..4801d3f999 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -355,7 +355,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): ifc_class: bpy.props.StringProperty(options={"SKIP_SAVE"}) def invoke(self, context, event): - return IfcStore.execute_ifc_operator(self, context, is_invoke=True) + return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE") def _invoke(self, context, event): props = context.scene.BIMRootProperties diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py b/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py index 4df3770083..d95f877fcf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/set_shape_aspect_constituents.py @@ -44,7 +44,7 @@ def set_shape_aspect_constituents( A material may be associated with a style (i.e. colour). For example, a grey style for the "Aluminium" material and a transparent blue style for - the "Laminated Low-e Glass" material. + the "Laminated Low-e Glass" material. These three concepts of material constituents, shape aspects, and associated styles are correlated. For example, if the name (e.g. "Framing") @@ -75,7 +75,7 @@ def set_shape_aspect_constituents( # Create two materials aluminium = ifcopenshell.api.material.add_material(model, name="AL01", category="aluminium") glass = ifcopenshell.api.material.add_material(model, name="GLZ01", category="glass") - + # Auto assign material constituents and styles to items based on shape aspects ifcopenshell.api.material.set_shape_aspect_constituents( model, element=window, context=body, materials={ @@ -102,9 +102,7 @@ def set_shape_aspect_constituents( if should_create_new_material_set: material_set = ifcopenshell.api.material.add_material_set(file, set_type="IfcMaterialConstituentSet") for name, material in materials.items(): - ifcopenshell.api.material.add_constituent( - file, constituent_set=material_set, material=material, name=name - ) + ifcopenshell.api.material.add_constituent(file, constituent_set=material_set, material=material, name=name) ifcopenshell.api.material.assign_material(file, products=[element], material=material_set) styles = {n: ifcopenshell.util.representation.get_material_style(m, context) for n, m in materials.items()} From 9f453d54d89f98a11dc1ac9c38a12c4d1e165ce3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Feb 2025 15:43:03 +1100 Subject: [PATCH 034/476] Piles should also default to vertical, just like columns --- src/bonsai/bonsai/bim/module/model/polyline.py | 9 +++++---- src/bonsai/bonsai/bim/module/model/profile.py | 3 +-- src/bonsai/bonsai/bim/module/model/workspace.py | 15 ++++++++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index d12417637e..2f780015a1 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -886,10 +886,11 @@ class PolylineOperator: tool.Blender.update_viewport() def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_isntance): - if tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() not in {"IfcColumnType"}: - data = get_horizontal_profile_preview_data(context, relating_type) - elif tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type.is_a() in {"IfcColumnType"}: - data = get_vertical_profile_preview_data(context, relating_type) + if tool.Model.get_usage_type(relating_type) == "PROFILE": + if relating_type.is_a() in {"IfcColumnType", "IfcPileType"}: + data = get_vertical_profile_preview_data(context, relating_type) + else: + data = get_horizontal_profile_preview_data(context, relating_type) elif tool.Model.get_usage_type(relating_type) == "LAYER2": data = get_wall_preview_data(context, relating_type) elif tool.Model.get_usage_type(relating_type) == "LAYER3": diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 00e594323b..6286422f99 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -105,9 +105,8 @@ class DumbProfileGenerator: obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh) matrix_world = Matrix() - if not self.relating_type.is_a("IfcColumnType"): + if self.relating_type.is_a() not in ("IfcColumnType", "IfcPileType"): matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world - matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world matrix_world.translation = self.location diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index ffb0737015..ad4446b9b6 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -632,11 +632,11 @@ class CreateObjectUI: data=cls.props, property="x_angle", text="Slope" if ui_context != "TOOL_HEADER" else "A", icon="FILE_3D" ) - elif ifc_class in ("IfcColumnType", "IfcMemberType"): + elif ifc_class in ("IfcColumnType", "IfcPileType"): row.prop(data=cls.props, property="cardinal_point", text="Axis") row.prop(data=cls.props, property="extrusion_depth", text="Height" if ui_context != "TOOL_HEADER" else "H") - elif ifc_class in ("IfcBeamType"): + elif ifc_class in ("IfcBeamType", "IfcMemberType"): row.prop(data=cls.props, property="cardinal_point", text="Axis") row.prop(data=cls.props, property="extrusion_depth", text="Length" if ui_context != "TOOL_HEADER" else "L") @@ -817,7 +817,9 @@ class EditObjectUI: op.cardinal_point = int(cls.props.cardinal_point) row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row label = ( - "Height" if AuthoringData.data["active_class"] in ("IfcColumn", "IfcColumnStandardCase") else "Length" + "Height" + if AuthoringData.data["active_class"] in ("IfcColumn", "IfcColumnStandardCase", "IfcPile") + else "Length" ) row.prop( data=cls.props, property="extrusion_depth", text=label if ui_context != "TOOL_HEADER" else label[0] @@ -1168,7 +1170,10 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT") elif tool.Model.get_usage_type(relating_type) == "LAYER3": return bpy.ops.bim.draw_polyline_slab("INVOKE_DEFAULT") - elif tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type_class != "IfcColumnType": + elif tool.Model.get_usage_type(relating_type) == "PROFILE" and relating_type_class not in ( + "IfcColumnType", + "IfcPileType", + ): return bpy.ops.bim.draw_polyline_profile("INVOKE_DEFAULT") return bpy.ops.bim.add_occurrence("INVOKE_DEFAULT") @@ -1350,7 +1355,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return if self.active_material_usage == "LAYER2": bpy.ops.bim.rotate_90(axis="Z") - elif self.active_class in ("IfcColumn", "IfcColumnStandardCase"): + elif self.active_class in ("IfcColumn", "IfcColumnStandardCase", "IfcPile"): bpy.ops.bim.rotate_90(axis="Z") elif self.active_class in ("IfcBeam", "IfcBeamStandardCase", "IfcMember", "IfcMemberStandardCase"): bpy.ops.bim.rotate_90(axis="Y") From aaa4be1a940dd716f27e08c910ed53bbda5cb4f3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Feb 2025 15:54:28 +1100 Subject: [PATCH 035/476] Fix #5691. UI tweak to show less colour, more text for usability. --- src/bonsai/bonsai/bim/module/search/ui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/search/ui.py b/src/bonsai/bonsai/bim/module/search/ui.py index a18a7e2409..ce0b346027 100644 --- a/src/bonsai/bonsai/bim/module/search/ui.py +++ b/src/bonsai/bonsai/bim/module/search/ui.py @@ -141,12 +141,12 @@ class BIM_PT_select_similar(Panel): class BIM_UL_colourscheme(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - props = context.scene.BIMWorkScheduleProperties if not item: return row = layout.row(align=True) - row.label(text=f"{item.name} ({item.total})") - row.prop(item, "colour", text="") + split = row.split(factor=0.85) + split.label(text=f"{item.name} ({item.total})") + split.prop(item, "colour", text="") class BIM_UL_ifc_class_filter(bpy.types.UIList): From e0a93626a5b99df65af704f3fdad5529bc4ed134 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Feb 2025 16:17:18 +1100 Subject: [PATCH 036/476] Fix #6038. Standardardise passing around as_posix() paths --- src/bonsai/bonsai/bim/module/project/operator.py | 7 ++++--- src/bonsai/bonsai/bim/ui.py | 2 +- src/bonsai/bonsai/tool/blender.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index d4b864ec62..df6196b3d4 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -906,7 +906,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): if not filepath: return tooltip filepath = Path(filepath) - tooltip += f".\n" + tooltip += ".\n" if not filepath.exists(): tooltip += "\nFile does not exist" return tooltip @@ -977,7 +977,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): bpy.ops.bim.convert_to_blender() context.scene.BIMProperties.ifc_file = filepath - if not (ifc_file := tool.Ifc.get()): + if not tool.Ifc.get(): self.report( {"ERROR"}, f"Error loading IFC file from filepath '{filepath}'. See logs above in the system console for the details.", @@ -1593,6 +1593,7 @@ class ExportIFC(bpy.types.Operator): output_file = bpy.path.ensure_ext(self.filepath, ".ifcjson") else: output_file = bpy.path.ensure_ext(self.filepath, ".ifc") + output_file = Path(output_file).as_posix().replace("\\", "/") settings = export_ifc.IfcExportSettings.factory(context, output_file, logger) settings.json_version = self.json_version @@ -1612,7 +1613,7 @@ class ExportIFC(bpy.types.Operator): new.name = output_file if context.scene.BIMProjectProperties.use_relative_project_path and bpy.data.is_saved: output_file = os.path.relpath(output_file, bpy.path.abspath("//")) - if scene.BIMProperties.ifc_file != output_file and extension not in ["ifczip", "ifcjson"]: + if scene.BIMProperties.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): scene.BIMProperties.ifc_file = output_file save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath) if save_blend_file: diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 8326ab1b66..a666d3fd94 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -66,7 +66,7 @@ class IFCFileSelector: if self.use_relative_path: filepath = filepath.relative_to(bpy.path.abspath("//")) - return filepath.as_posix() + return filepath.as_posix().replace("\\", "/") def draw(self, context: bpy.types.Context) -> None: assert isinstance(context.space_data, bpy.types.SpaceFileBrowser) diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 5a73420e25..605bcbab90 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -869,7 +869,7 @@ class Blender(bonsai.core.tool.Blender): cls.open_file_or_folder(filepath.as_posix()) return {"PASS_THROUGH"} - # holding sHIFT - open file + # holding SHIFT - open file if not filepath.exists(): operator.report({"ERROR"}, f'Cannot open non-existing file: "{filepath.as_posix()}"') return {"CANCELLED"} From 42e184c9362e040831789032beb80857d834c8f0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Feb 2025 17:08:34 +1100 Subject: [PATCH 037/476] Fix #5839. Appending an asset now also considers shape aspects. --- .../ifcopenshell/api/project/append_asset.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index aca33128aa..9411307a66 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -261,7 +261,10 @@ class Usecase: self.base_material_class: ["HasExternalReferences", "HasProperties", "HasRepresentation"], "IfcRepresentationItem": ["StyledByItem", "LayerAssignment"], "IfcRepresentation": ["LayerAssignments"], + "IfcProductDefinitionShape": ["HasShapeAspects"], + "IfcRepresentationMap": ["HasShapeAspects"], } + print('appending type product!') self.existing_contexts = self.file.by_type("IfcGeometricRepresentationContext") element = self.add_element(self.settings["element"]) self.reuse_existing_contexts() @@ -278,6 +281,8 @@ class Usecase: "LayerAssignments" if self.file.schema == "IFC2X3" else "LayerAssignment", ], "IfcRepresentation": ["LayerAssignments"], + "IfcProductDefinitionShape": ["HasShapeAspects"], + "IfcRepresentationMap": ["HasShapeAspects"], } self.existing_contexts = self.file.by_type("IfcGeometricRepresentationContext") element = self.add_element(self.settings["element"]) From 8f261be338b8b585743fa4078fcf62787211587c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Feb 2025 17:08:55 +1100 Subject: [PATCH 038/476] See #5839. More accurately consider the relating product definition when checking for shape aspects. --- .../bonsai/bim/module/geometry/operator.py | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index c5d82936fb..5451c611f4 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -2370,11 +2370,30 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): if obj.data and hasattr(obj.data, "BIMMeshProperties"): active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id - element = tool.Ifc.get().by_id(active_representation_id) + representation = tool.Ifc.get().by_id(active_representation_id) + + # Shape aspects must be considered from the PartOfProductDefinitionShape level + element = tool.Ifc.get_entity(obj) + product_reps = [] + if element.is_a("IfcProduct"): + product_reps = [element.Representation] + if element_type := ifcopenshell.util.element.get_type(element): + product_reps.extend(element_type.RepresentationMaps or []) + elif element.is_a("IfcTypeProduct"): + product_reps = element.RepresentationMaps + item_aspect = {} + for product_rep in product_reps: + for aspect in product_rep.HasShapeAspects: + for aspect_rep in aspect.ShapeRepresentations: + if aspect_rep.ContextOfItems != representation.ContextOfItems: + continue + for item in aspect_rep.Items: + item_aspect[item] = aspect + # IfcShapeRepresentation or IfcTopologyRepresentation. - if not element.is_a("IfcShapeModel"): + if not representation.is_a("IfcShapeModel"): return - queue = list(element.Items) + queue = list(representation.Items) while queue: item = queue.pop() if item.is_a("IfcMappedItem"): @@ -2397,16 +2416,15 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): elif inverse.is_a("IfcPresentationLayerAssignment"): new.layer = inverse.Name or "Unnamed" new.layer_id = inverse.id() - elif inverse.is_a("IfcShapeRepresentation"): - if inverse.OfShapeAspect: - shape_aspect = inverse.OfShapeAspect[0] - new.shape_aspect = shape_aspect.Name - new.shape_aspect_id = shape_aspect.id() elif inverse.is_a("IfcIndexedTextureMap"): add_tag(new, "UV") elif inverse.is_a("IfcIndexedColourMap"): add_tag(new, "Colour") + if aspect := item_aspect.get(item, None): + new.shape_aspect = aspect.Name + new.shape_aspect_id = aspect.id() + # sort created items sorted_items = sorted(props.items[:], key=lambda i: (not i.shape_aspect, i.shape_aspect)) for i, item in enumerate(sorted_items[:-1]): # last item is sorted automatically From b0fbea60ae1076278d990b3695021b1d0223d55d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Feb 2025 12:33:15 +0500 Subject: [PATCH 039/476] typing --- src/bonsai/bonsai/bim/module/bcf/bcfstore.py | 6 +- src/bonsai/bonsai/bim/module/bcf/operator.py | 121 ++++++++++-------- src/bonsai/bonsai/bim/module/bcf/prop.py | 79 +++++++++++- src/bonsai/bonsai/bim/module/bcf/ui.py | 16 +-- .../bonsai/bim/module/classification/data.py | 2 +- .../bonsai/bim/module/classification/ui.py | 2 +- .../bonsai/bim/module/debug/operator.py | 12 +- .../bonsai/bim/module/geometry/operator.py | 5 +- src/bonsai/bonsai/bim/module/library/data.py | 5 +- src/bonsai/bonsai/bim/module/library/prop.py | 14 ++ src/bonsai/bonsai/bim/module/library/ui.py | 18 ++- src/bonsai/bonsai/bim/module/material/data.py | 5 +- .../bonsai/bim/module/material/operator.py | 23 ++-- src/bonsai/bonsai/bim/module/material/prop.py | 2 +- src/bonsai/bonsai/bim/module/material/ui.py | 18 ++- src/bonsai/bonsai/bim/module/profile/data.py | 4 +- .../bonsai/bim/module/profile/operator.py | 24 ++-- src/bonsai/bonsai/bim/module/profile/prop.py | 15 ++- src/bonsai/bonsai/bim/module/profile/ui.py | 19 ++- src/bonsai/bonsai/bim/module/pset/data.py | 4 +- src/bonsai/bonsai/bim/module/pset/prop.py | 2 +- src/bonsai/bonsai/bim/module/pset/ui.py | 8 +- src/bonsai/bonsai/tool/bcf.py | 13 +- src/bonsai/bonsai/tool/blender.py | 10 +- src/bonsai/bonsai/tool/library.py | 36 ++++-- src/bonsai/bonsai/tool/material.py | 30 +++-- src/bonsai/bonsai/tool/model.py | 5 +- src/bonsai/bonsai/tool/profile.py | 7 +- src/bonsai/test/tool/test_library.py | 17 ++- src/bonsai/test/tool/test_material.py | 32 +++-- .../api/grid/create_axis_curve.py | 11 +- .../ifcopenshell/api/project/append_asset.py | 1 - 32 files changed, 381 insertions(+), 185 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/bcf/bcfstore.py b/src/bonsai/bonsai/bim/module/bcf/bcfstore.py index b80e724be3..13be3faf1d 100644 --- a/src/bonsai/bonsai/bim/module/bcf/bcfstore.py +++ b/src/bonsai/bonsai/bim/module/bcf/bcfstore.py @@ -21,6 +21,7 @@ import bpy import bcf import bcf.bcfxml import bcf.v2.bcfxml +import bonsai.tool as tool from typing import Union @@ -30,7 +31,8 @@ class BcfStore: @classmethod def get_bcfxml(cls) -> Union[bcf.bcfxml.BcfXml, None]: if not cls.bcfxml: - bcf_filepath = bpy.context.scene.BCFProperties.bcf_file + props = tool.Bcf.get_bcf_props() + bcf_filepath = props.bcf_file if not os.path.isabs(bcf_filepath): bcf_filepath = os.path.abspath(os.path.join(bpy.path.abspath("//"), bcf_filepath)) if bcf_filepath: @@ -46,7 +48,7 @@ class BcfStore: @classmethod def set(cls, bcfxml: Union[bcf.bcfxml.BcfXml, None], filepath: str) -> None: cls.bcfxml = bcfxml - props = bpy.context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() props.bcf_file = filepath # Set bcf_version prop on load. diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index 0295d2cdde..bfcc40b8a9 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -55,7 +55,7 @@ class NewBcfProject(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() bcf_v2 = props.bcf_version == "2" bcf_class = bcf.v2.bcfxml.BcfXml if bcf_v2 else bcf.v3.bcfxml.BcfXml bcfxml = bcf_class.create_new("New Project") @@ -103,7 +103,8 @@ class LoadBcfProject(bpy.types.Operator): assert bcfxml.project if bcfxml.project.name is None: bcfxml.project.name = nameless - context.scene.BCFProperties.name = bcfxml.project.name + props = tool.Bcf.get_bcf_props() + props.name = bcfxml.project.name bpy.ops.bim.load_bcf_topics() self.report({"INFO"}, f"BCF Project '{Path(self.filepath).name}' is loaded.") return {"FINISHED"} @@ -131,7 +132,7 @@ class LoadBcfTopics(bpy.types.Operator): def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() props.topics.clear() # workaround, one non standard topic would break reading entire bcf # ignored these topics ATM @@ -163,7 +164,8 @@ class LoadBcfTopic(bpy.types.Operator): assert bcfxml topic = bcfxml.topics[self.topic_guid] bcfxml.get_header(self.topic_guid) - new = context.scene.BCFProperties.topics[self.topic_index] + props = tool.Bcf.get_bcf_props() + new = props.topics[self.topic_index] data_map = { "name": topic.guid, "title": topic.topic.title, @@ -244,7 +246,8 @@ class LoadBcfComments(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - blender_topic = context.scene.BCFProperties.topics.get(self.topic_guid) + props = tool.Bcf.get_bcf_props() + blender_topic = props.topics.get(self.topic_guid) blender_topic.comments.clear() for comment in bcfxml.topics[self.topic_guid].comments: new = blender_topic.comments.add() @@ -274,7 +277,9 @@ class EditBcfProjectName(bpy.types.Operator): # Bonsai creates default project on load. assert bcfxml.project - bcfxml.project.name = context.scene.BCFProperties.name + + props = tool.Bcf.get_bcf_props() + bcfxml.project.name = props.name return {"FINISHED"} @@ -284,7 +289,7 @@ class EditBcfTopicName(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml @@ -300,7 +305,7 @@ class EditBcfTopic(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml @@ -371,13 +376,15 @@ class AddBcfTopic(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.BCFProperties.author + props = tool.Bcf.get_bcf_props() + return props.author def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - bcfxml.add_topic("New Topic", "", context.scene.BCFProperties.author) + props = tool.Bcf.get_bcf_props() + bcfxml.add_topic("New Topic", "", props.author) bpy.ops.bim.load_bcf_topics() return {"FINISHED"} @@ -389,7 +396,7 @@ class AddBcfBimSnippet(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() props_are_filled = all( (getattr(props, attr) for attr in ("bim_snippet_reference", "bim_snippet_schema", "bim_snippet_type")) ) @@ -403,7 +410,7 @@ class AddBcfBimSnippet(bpy.types.Operator): assert bcfxml bcf_v2 = (bcfxml.version.version_id or "").startswith("2") - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] is_external = "://" in props.bim_snippet_reference @@ -435,7 +442,7 @@ class AddBcfRelatedTopic(bpy.types.Operator): assert bcfxml bcf_v2 = (bcfxml.version.version_id or "").startswith("2") - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] related_topics = tool.Bcf.get_topic_related_topics(topic) @@ -462,14 +469,15 @@ class AddBcfHeaderFile(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.BCFProperties.file_reference + props = tool.Bcf.get_bcf_props() + return props.file_reference def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml bcf_v2 = (bcfxml.version.version_id or "").startswith("2") - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] @@ -519,9 +527,10 @@ class ViewBcfTopic(bpy.types.Operator): topic_guid: bpy.props.StringProperty() def execute(self, context): - for index, topic in enumerate(context.scene.BCFProperties.topics): + props = tool.Bcf.get_bcf_props() + for index, topic in enumerate(props.topics): if topic.name.lower() == self.topic_guid.lower(): - context.scene.BCFProperties.active_topic_index = index + props.active_topic_index = index break return {"FINISHED"} @@ -546,7 +555,7 @@ class AddBcfViewpoint(bpy.types.Operator): blender_camera = context.scene.camera assert blender_camera - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] @@ -675,7 +684,7 @@ class RemoveBcfViewpoint(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() if not bcfxml: return False - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() topic = props.active_topic if not topic: return False @@ -690,7 +699,7 @@ class RemoveBcfViewpoint(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] del topic.viewpoints[blender_topic.viewpoints] @@ -715,7 +724,7 @@ class RemoveBcfFile(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] header_files = tool.Bcf.get_topic_header_files(topic) @@ -733,13 +742,14 @@ class RemoveBcfTopic(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.BCFProperties.topics + props = tool.Bcf.get_bcf_props() + return props.topics def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() del bcfxml.topics[props.active_topic.name] bpy.ops.bim.load_bcf_topics() return {"FINISHED"} @@ -752,13 +762,14 @@ class AddBcfReferenceLink(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.BCFProperties.reference_link + props = tool.Bcf.get_bcf_props() + return bool(props.reference_link) def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] reference_links = tool.Bcf.get_topic_reference_links(topic) @@ -776,14 +787,15 @@ class AddBcfDocumentReference(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.BCFProperties.document_reference + props = tool.Bcf.get_bcf_props() + return bool(props.document_reference) def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml bcf_v2 = (bcfxml.version.version_id or "").startswith("2") - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] @@ -855,13 +867,14 @@ class AddBcfLabel(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.BCFProperties.label + props = tool.Bcf.get_bcf_props() + return bool(props.label) def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] new = blender_topic.labels.add() @@ -883,7 +896,7 @@ class EditBcfReferenceLinks(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] reference_links = [r.name for r in blender_topic.reference_links] @@ -900,7 +913,7 @@ class EditBcfLabels(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] labels = [l.name for l in blender_topic.labels] @@ -918,7 +931,7 @@ class RemoveBcfReferenceLink(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] reference_links = tool.Bcf.get_topic_reference_links(topic) @@ -938,7 +951,7 @@ class RemoveBcfLabel(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] labels = tool.Bcf.get_topic_labels(topic) @@ -957,7 +970,7 @@ class RemoveBcfBimSnippet(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] tool.Bcf.set_topic_bim_snippet(topic, None) @@ -978,7 +991,7 @@ class RemoveBcfDocumentReference(bpy.types.Operator): assert bcfxml bcf_v2 = (bcfxml.version.version_id or "").startswith("2") - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] @@ -1043,7 +1056,7 @@ class RemoveBcfRelatedTopic(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] related_topics = tool.Bcf.get_topic_related_topics(topic) @@ -1063,7 +1076,7 @@ class RemoveBcfComment(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] comments = topic.comments @@ -1084,7 +1097,7 @@ class EditBcfComment(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic blender_comment = blender_topic.comments.get(self.comment_guid) topic = bcfxml.topics[blender_topic.name] @@ -1092,7 +1105,7 @@ class EditBcfComment(bpy.types.Operator): if comment.guid == self.comment_guid: comment.comment = blender_comment.comment comment.modified_date = XmlDateTime.now() - comment.modified_author = context.scene.BCFProperties.author + comment.modified_author = props.author bpy.ops.bim.load_bcf_comments(topic_guid=topic.guid) return {"FINISHED"} @@ -1105,7 +1118,7 @@ class AddBcfComment(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() if not props.comment: cls.poll_message_set("No comment to add.") return False @@ -1128,7 +1141,7 @@ class AddBcfComment(bpy.types.Operator): assert bcfxml bcf_v2 = (bcfxml.version.version_id or "").startswith("2") - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] comments = topic.comments @@ -1136,7 +1149,7 @@ class AddBcfComment(bpy.types.Operator): if bcf_v2: comment = bcf.v2.model.Comment( date=XmlDateTime.now(), - author=context.scene.BCFProperties.author, + author=props.author, comment=props.comment, guid=str(uuid.uuid4()), ) @@ -1149,7 +1162,7 @@ class AddBcfComment(bpy.types.Operator): else: comment = bcf.v3.model.Comment( date=XmlDateTime.now(), - author=context.scene.BCFProperties.author, + author=props.author, comment=props.comment, guid=str(uuid.uuid4()), ) @@ -1179,7 +1192,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic if blender_topic is None: cls.poll_message_set("No topic is active.") @@ -1197,7 +1210,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() blender_topic = props.active_topic topic = bcfxml.topics[blender_topic.name] if self.viewpoint_guid: @@ -1517,7 +1530,8 @@ class OpenBcfReferenceLink(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - webbrowser.open(context.scene.BCFProperties.topic_links[self.index].name) + props = tool.Bcf.get_bcf_props() + webbrowser.open(props.topic_links[self.index].name) return {"FINISHED"} @@ -1530,7 +1544,8 @@ class SelectBcfHeaderFile(bpy.types.Operator): def execute(self, context): if self.filepath: - context.scene.BCFProperties.file_reference = self.filepath + props = tool.Bcf.get_bcf_props() + props.file_reference = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -1546,7 +1561,8 @@ class SelectBcfBimSnippetReference(bpy.types.Operator): def execute(self, context): if self.filepath: - context.scene.BCFProperties.bim_snippet_reference = self.filepath + props = tool.Bcf.get_bcf_props() + props.bim_snippet_reference = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -1562,7 +1578,8 @@ class SelectBcfDocumentReference(bpy.types.Operator): def execute(self, context): if self.filepath: - context.scene.BCFProperties.document_reference = self.filepath + props = tool.Bcf.get_bcf_props() + props.document_reference = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -1585,7 +1602,8 @@ class LoadBcfHeaderIfcFile(bpy.types.Operator): assert bcfxml bcf_path = tool.Bcf.get_path() - topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name] + props = tool.Bcf.get_bcf_props() + topic = bcfxml.topics[props.active_topic.name] entity = tool.Bcf.get_topic_header_files(topic)[self.index] ifc_path = bcf.agnostic.topic.extract_file(topic, entity) bpy.ops.bim.load_project(filepath=ifc_path) @@ -1605,7 +1623,8 @@ class ExtractBcfFile(bpy.types.Operator): bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml - topic = bcfxml.topics[context.scene.BCFProperties.active_topic.name] + props = tool.Bcf.get_bcf_props() + topic = bcfxml.topics[props.active_topic.name] if self.entity_type == "HEADER_FILE": entity = tool.Bcf.get_topic_header_files(topic)[self.index] diff --git a/src/bonsai/bonsai/bim/module/bcf/prop.py b/src/bonsai/bonsai/bim/module/bcf/prop.py index ef681ace10..17a15597a7 100644 --- a/src/bonsai/bonsai/bim/module/bcf/prop.py +++ b/src/bonsai/bonsai/bim/module/bcf/prop.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from . import bcfstore from bonsai.bim.prop import StrProperty from bpy.types import PropertyGroup @@ -34,6 +35,7 @@ from functools import partial from typing import Literal from typing_extensions import assert_never from bcf.agnostic.extensions import get_extensions_attributes +from typing import TYPE_CHECKING, Union bcfviewpoints_enum = None @@ -95,7 +97,7 @@ def getBcfViewpoints(self, context, force_update=False): global bcfviewpoints_enum if bcfviewpoints_enum is None or force_update: # Retrieving Viewpoints is slow. Make sure we only do when needed bcfviewpoints_enum = [] - props = context.scene.BCFProperties + props = tool.Bcf.get_bcf_props() bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml topic = props.active_topic @@ -110,6 +112,12 @@ class BcfBimSnippet(PropertyGroup): type: StringProperty(name="Type") is_external: BoolProperty(name="Is External") + if TYPE_CHECKING: + schema: str + reference: str + type: str + is_external: bool + class BcfDocumentReference(PropertyGroup): reference: StringProperty(name="Reference") @@ -117,6 +125,12 @@ class BcfDocumentReference(PropertyGroup): guid: StringProperty(name="GUID") is_external: BoolProperty(name="Is External") + if TYPE_CHECKING: + reference: str + description: str + guid: str + is_external: bool + class BcfComment(PropertyGroup): name: StringProperty(name="GUID") @@ -128,6 +142,16 @@ class BcfComment(PropertyGroup): modified_author: StringProperty(name="Modified Author") is_editable: BoolProperty(name="Is Editable", default=False, update=updateBcfCommentIsEditable) + if TYPE_CHECKING: + name: str + date: str + author: str + comment: str + viewpoint: str + modified_date: str + modified_author: str + is_editable: bool + def get_extensions_items( self: "BCFProperties", context: bpy.types.Context, edit_text: str, extensions_attr: str @@ -183,6 +207,30 @@ class BcfTopic(PropertyGroup): comments: CollectionProperty(name="Comments", type=BcfComment) is_editable: BoolProperty(name="Edit Topic Attributes", default=False, update=updateBcfTopicIsEditable) + if TYPE_CHECKING: + name: str + title: str + type: str + status: str + priority: str + stage: str + creation_date: str + creation_author: str + modified_date: str + modified_author: str + assigned_to: str + due_date: str + description: str + viewpoints: str + files: bpy.types.bpy_prop_collection_idprop[StrProperty] + reference_links: bpy.types.bpy_prop_collection_idprop[BcfReferenceLink] + labels: bpy.types.bpy_prop_collection_idprop[BcfLabel] + bim_snippet: BcfBimSnippet + document_references: bpy.types.bpy_prop_collection_idprop[BcfDocumentReference] + related_topics: bpy.types.bpy_prop_collection_idprop[StrProperty] + comments: bpy.types.bpy_prop_collection_idprop[BcfComment] + is_editable: bool + def get_related_topics(self: "BCFProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: global RELATED_TOPICS_ENUM_ITEMS @@ -235,7 +283,30 @@ class BCFProperties(PropertyGroup): comment: StringProperty(default="", name="Comment") has_related_viewpoint: BoolProperty(name="Has Related Viewpoint", default=False) - def clear_input_fields(self): + if TYPE_CHECKING: + bcf_file: str + bcf_version: str + comment_text_width: int + name: str + author: str + topics: bpy.types.bpy_prop_collection_idprop[BcfTopic] + active_topic_index: int + file_reference: str + file_ifc_project: str + file_ifc_spatial_structure_element: str + reference_link: str + label: str + bim_snippet_reference: str + bim_snippet_type: str + bim_snippet_schema: str + document_reference: str + document_reference_description: str + document_description: str + related_topic: str + comment: str + has_related_viewpoint: bool + + def clear_input_fields(self) -> None: self.file_reference = "" self.file_ifc_project = "" self.file_ifc_spatial_structure_element = "" @@ -250,7 +321,7 @@ class BCFProperties(PropertyGroup): self.has_related_viewpoint = False @property - def active_topic(self): + def active_topic(self) -> Union[BcfTopic, None]: if len(self.topics) == 0: return None if self.active_topic_index < 0: @@ -259,5 +330,5 @@ class BCFProperties(PropertyGroup): self.active_topic_index = len(self.topics) - 1 return self.topics[self.active_topic_index] - def refresh_topic(self, context): + def refresh_topic(self, context: bpy.types.Context) -> None: refreshBcfTopic(self, context) diff --git a/src/bonsai/bonsai/bim/module/bcf/ui.py b/src/bonsai/bonsai/bim/module/bcf/ui.py index 5efa118a8c..83c80557f8 100644 --- a/src/bonsai/bonsai/bim/module/bcf/ui.py +++ b/src/bonsai/bonsai/bim/module/bcf/ui.py @@ -37,8 +37,7 @@ class BIM_PT_bcf(Panel): layout.use_property_split = True layout.use_property_decorate = False - scene = context.scene - props = scene.BCFProperties + props = tool.Bcf.get_bcf_props() if not bcfstore.BcfStore.get_bcfxml(): row = layout.row(align=True) @@ -63,18 +62,17 @@ class BIM_PT_bcf(Panel): row = layout.row() row.prop(props, "author") - props = context.scene.BCFProperties row = layout.row() row.template_list("BIM_UL_topics", "", props, "topics", props, "active_topic_index") col = row.column(align=True) col.operator("bim.add_bcf_topic", icon="ADD", text="") col.operator("bim.remove_bcf_topic", icon="REMOVE", text="") - if props.active_topic_index < len(props.topics): - topic = props.active_topic + + topic = props.active_topic + if topic is not None: is_editable = topic.is_editable col.prop(topic, "is_editable", icon="CHECKMARK" if topic.is_editable else "GREASEPENCIL", icon_only=True) - topic = props.active_topic row = layout.row() row.enabled = is_editable row.prop(topic, "description", text="") @@ -131,8 +129,7 @@ class BIM_PT_bcf_metadata(Panel): layout.use_property_split = True layout.use_property_decorate = False - scene = context.scene - props = scene.BCFProperties + props = tool.Bcf.get_bcf_props() bcfxml = bcfstore.BcfStore.get_bcfxml() if not bcfxml or props.active_topic_index >= len(props.topics): @@ -296,8 +293,7 @@ class BIM_PT_bcf_comments(Panel): layout.use_property_split = True layout.use_property_decorate = False - scene = context.scene - props = scene.BCFProperties + props = tool.Bcf.get_bcf_props() if props.active_topic_index >= len(props.topics): layout.label(text="No BCF project is loaded") diff --git a/src/bonsai/bonsai/bim/module/classification/data.py b/src/bonsai/bonsai/bim/module/classification/data.py index 1dff8c3875..e4c5e30ca8 100644 --- a/src/bonsai/bonsai/bim/module/classification/data.py +++ b/src/bonsai/bonsai/bim/module/classification/data.py @@ -118,7 +118,7 @@ class MaterialClassificationsData(ReferencesData): def references(cls): results = [] - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() if props.materials and props.active_material_index < len(props.materials): material = props.materials[props.active_material_index] if material.ifc_definition_id: diff --git a/src/bonsai/bonsai/bim/module/classification/ui.py b/src/bonsai/bonsai/bim/module/classification/ui.py index fcef0ad4bd..87257fbaaa 100644 --- a/src/bonsai/bonsai/bim/module/classification/ui.py +++ b/src/bonsai/bonsai/bim/module/classification/ui.py @@ -312,7 +312,7 @@ class BIM_PT_material_classifications(Panel, ReferenceUI): def poll(cls, context): if not tool.Ifc.get(): return False - props = context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() if props.is_editing and (material := props.active_material) and material.ifc_definition_id: return True return False diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 0058c70a3a..33100956ef 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -689,13 +689,15 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator): scene = context.scene if object_type == "PROFILE": - if scene.BIMProfileProperties.is_editing: + props = tool.Profile.get_profile_props() + if props.is_editing: bpy.ops.bim.load_profiles() elif object_type == "STYLE": if scene.BIMStylesProperties.is_editing: bpy.ops.bim.load_styles() elif object_type == "MATERIAL": - if scene.BIMMaterialProperties.is_editing: + props = tool.Material.get_material_props() + if props.is_editing: bpy.ops.bim.load_materials() @@ -737,13 +739,15 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator): scene = context.scene if object_type == "PROFILE": - if scene.BIMProfileProperties.is_editing: + props = tool.Profile.get_profile_props() + if props.is_editing: bpy.ops.bim.load_profiles() elif object_type == "STYLE": if scene.BIMStylesProperties.is_editing: bpy.ops.bim.load_styles() elif object_type == "MATERIAL": - if scene.BIMMaterialProperties.is_editing: + props = tool.Material.get_material_props() + if props.is_editing: bpy.ops.bim.load_materials() diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 5451c611f4..de59b7a61c 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -2136,7 +2136,8 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): continue if tool.Profile.is_editing_profile(): - profile_id = context.scene.BIMProfileProperties.active_profile_id + props = tool.Profile.get_profile_props() + profile_id = props.active_profile_id if profile_id: profile = tool.Ifc.get().by_id(profile_id) if tool.Ifc.get_object(profile): # We are editing an arbitrary profile @@ -2148,7 +2149,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): elif tool.Model.get_usage_type(element) == "PROFILE": bpy.ops.bim.edit_extrusion_axis() # if in the process of editing arbitrary profile - elif context.scene.BIMProfileProperties.active_arbitrary_profile_id: + elif props.active_arbitrary_profile_id: bpy.ops.bim.edit_arbitrary_profile() else: bpy.ops.bim.edit_extrusion_profile() diff --git a/src/bonsai/bonsai/bim/module/library/data.py b/src/bonsai/bonsai/bim/module/library/data.py index 145dfbc83a..87ad7c23ae 100644 --- a/src/bonsai/bonsai/bim/module/library/data.py +++ b/src/bonsai/bonsai/bim/module/library/data.py @@ -47,7 +47,8 @@ class LibrariesData: @classmethod def library_attributes(cls): - library_id = bpy.context.scene.BIMLibraryProperties.active_library_id + props = tool.Library.get_library_props() + library_id = props.active_library_id if not library_id: return [] results = [] @@ -63,7 +64,7 @@ class LibrariesData: @classmethod def reference_attributes(cls): - props = bpy.context.scene.BIMLibraryProperties + props = tool.Library.get_library_props() try: reference_id = props.references[props.active_reference_index].ifc_definition_id except: diff --git a/src/bonsai/bonsai/bim/module/library/prop.py b/src/bonsai/bonsai/bim/module/library/prop.py index b7ddb74ed6..896bf0da2e 100644 --- a/src/bonsai/bonsai/bim/module/library/prop.py +++ b/src/bonsai/bonsai/bim/module/library/prop.py @@ -30,6 +30,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Literal def update_active_reference_index(self, context): @@ -40,6 +41,10 @@ class LibraryReference(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + name: str + ifc_definition_id: int + class BIMLibraryProperties(PropertyGroup): editing_mode: EnumProperty( @@ -58,3 +63,12 @@ class BIMLibraryProperties(PropertyGroup): active_reference_id: IntProperty(name="Active Reference Id") references: CollectionProperty(type=LibraryReference, name="References") active_reference_index: IntProperty(name="Active Reference Index", update=update_active_reference_index) + + if TYPE_CHECKING: + editing_mode: Literal["NONE", "LIBRARY", "REFERENCES", "REFERENCE"] + library_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + active_library_id: int + reference_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + active_reference_id: int + references: bpy.types.bpy_prop_collection_idprop[LibraryReference] + active_reference_index: int diff --git a/src/bonsai/bonsai/bim/module/library/ui.py b/src/bonsai/bonsai/bim/module/library/ui.py index 79797b9f54..1c9f389a7e 100644 --- a/src/bonsai/bonsai/bim/module/library/ui.py +++ b/src/bonsai/bonsai/bim/module/library/ui.py @@ -16,10 +16,16 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations +import bpy import bonsai.bim.helper import bonsai.tool as tool from bpy.types import Panel, UIList from bonsai.bim.module.library.data import LibrariesData, LibraryReferencesData +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.library.prop import BIMLibraryProperties, LibraryReference class BIM_PT_libraries(Panel): @@ -38,7 +44,7 @@ class BIM_PT_libraries(Panel): def draw(self, context): if not LibrariesData.is_loaded: LibrariesData.load() - self.props = context.scene.BIMLibraryProperties + self.props = tool.Library.get_library_props() if self.props.editing_mode == "LIBRARY": self.draw_editable_library_ui() @@ -110,7 +116,7 @@ class BIM_PT_library_references(Panel): def draw(self, context): if not LibraryReferencesData.is_loaded: LibraryReferencesData.load() - self.props = context.scene.BIMLibraryProperties + self.props = tool.Library.get_library_props() if self.props.editing_mode == "REFERENCES": self.layout.template_list( @@ -129,7 +135,9 @@ class BIM_PT_library_references(Panel): class BIM_UL_library_references(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, context, layout: bpy.types.UILayout, data, item: LibraryReference, icon, active_data, active_propname + ): if item: row = layout.row(align=True) row.label(text=item.name) @@ -140,7 +148,9 @@ class BIM_UL_library_references(UIList): class BIM_UL_object_library_references(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, context, layout: bpy.types.UILayout, data, item: LibraryReference, icon, active_data, active_propname + ): if item: row = layout.row(align=True) row.label(text=item.name) diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index 95e68468c6..50a03603df 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -48,7 +48,8 @@ class MaterialsData: @classmethod def total_materials(cls): - return len(tool.Ifc.get().by_type(bpy.context.scene.BIMMaterialProperties.material_type)) + props = tool.Material.get_material_props() + return len(tool.Ifc.get().by_type(props.material_type)) @classmethod def material_types(cls): @@ -101,7 +102,7 @@ class MaterialsData: @classmethod def material_styles_data(cls) -> dict[int, list[dict[str, Any]]]: - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() material_styles_data: dict[int, list[dict[str, Any]]] = {} for material_item in props.materials: diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index c6b92145b5..820ac5359e 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -19,6 +19,7 @@ import bpy import json import ifcopenshell.api +import ifcopenshell.api.material import ifcopenshell.util.element import ifcopenshell.util.attribute import ifcopenshell.util.representation @@ -40,7 +41,8 @@ class LoadMaterials(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - core.load_materials(tool.Material, context.scene.BIMMaterialProperties.material_type) + props = tool.Material.get_material_props() + core.load_materials(tool.Material, props.material_type) return {"FINISHED"} @@ -327,12 +329,13 @@ class AddProfile(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() + props = tool.Material.get_material_props() ifcopenshell.api.run( "material.add_profile", self.file, profile_set=self.file.by_id(self.profile_set), material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material)), - profile=self.file.by_id(int(context.scene.BIMMaterialProperties.profiles)), + profile=self.file.by_id(int(props.profiles)), ) @@ -662,7 +665,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.mprops = context.scene.BIMMaterialProperties + self.mprops = tool.Material.get_material_props() self.props = obj.BIMObjectMaterialProperties self.props.active_material_set_item_id = self.material_set_item @@ -706,7 +709,7 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = IfcStore.get_file() props = obj.BIMObjectMaterialProperties - mprops = context.scene.BIMMaterialProperties + mprops = tool.Material.get_material_props() element = tool.Ifc.get_entity(obj) material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) @@ -763,7 +766,7 @@ class ExpandMaterialCategory(bpy.types.Operator): return self.execute(context) def execute(self, context): - props = context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() for index, category in ( (i, c) for i, c in enumerate(props.materials) @@ -792,7 +795,7 @@ class ContractMaterialCategory(bpy.types.Operator): return self.execute(context) def execute(self, context): - props = context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() for index, category in ( (i, c) for i, c in enumerate(props.materials) @@ -812,7 +815,7 @@ class EnableEditingMaterialStyle(bpy.types.Operator): material: bpy.props.IntProperty() def execute(self, context): - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() props.active_material_id = self.material props.editing_material_type = "STYLE" @@ -843,7 +846,7 @@ class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() ifc_file = tool.Ifc.get() material = ifc_file.by_id(props.active_material_id) style = ifc_file.by_id(int(props.styles)) @@ -867,7 +870,7 @@ class UnassignMaterialStyle(bpy.types.Operator, tool.Ifc.Operator): context: bpy.props.IntProperty() def _execute(self, context): - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() material = tool.Ifc.get().by_id(props.materials[props.active_material_index].ifc_definition_id) style = tool.Ifc.get().by_id(self.style) context = tool.Ifc.get().by_id(self.context) @@ -888,7 +891,7 @@ class SelectMaterialInMaterialsUI(bpy.types.Operator): material_id: int def execute(self, context): - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() ifc_file = tool.Ifc.get() material_id = self.material_id material = ifc_file.by_id(material_id) diff --git a/src/bonsai/bonsai/bim/module/material/prop.py b/src/bonsai/bonsai/bim/module/material/prop.py index aa680abe9b..f21a34a702 100644 --- a/src/bonsai/bonsai/bim/module/material/prop.py +++ b/src/bonsai/bonsai/bim/module/material/prop.py @@ -121,7 +121,7 @@ def set_material_name(self: "Material", new_category_name: str) -> None: material.Category = new_category_name # Reload UI elements if necessary. - props: "BIMMaterialProperties" = self.id_data.BIMMaterialProperties + props = tool.Material.get_material_props() new_category_name_already_in_use = bool( next((m for m in props.materials if m.is_category and m.name == new_category_name), None) ) diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index c4c9eda9c4..eef2adb670 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -29,7 +29,7 @@ from bonsai.bim.module.drawing.helper import format_distance from typing import TYPE_CHECKING if TYPE_CHECKING: - from bonsai.bim.module.material.prop import Material + from bonsai.bim.module.material.prop import Material, BIMMaterialProperties class BIM_PT_materials(Panel): @@ -49,7 +49,7 @@ class BIM_PT_materials(Panel): if not MaterialsData.is_loaded: MaterialsData.load() - self.props = context.scene.BIMMaterialProperties + self.props = tool.Material.get_material_props() material = tool.Material.get_active_material_item() material_id = material.ifc_definition_id if material else None @@ -156,7 +156,7 @@ class BIM_PT_object_material(Panel): self.file = IfcStore.get_file() self.oprops = context.active_object.BIMObjectProperties self.props = context.active_object.BIMObjectMaterialProperties - self.mprops = context.scene.BIMMaterialProperties + self.mprops = tool.Material.get_material_props() if not ObjectMaterialData.data["materials"]: row = self.layout.row(align=True) @@ -401,10 +401,16 @@ class BIM_PT_object_material(Panel): class BIM_UL_materials(UIList): def draw_item( - self, context, layout: bpy.types.UILayout, data, item: Material, icon, active_data, active_propname + self, + context, + layout: bpy.types.UILayout, + data: BIMMaterialProperties, + item: Material, + icon, + active_data, + active_propname, ) -> None: - mprops = context.scene.BIMMaterialProperties - material_type = mprops.material_type + material_type = data.material_type if item: row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/profile/data.py b/src/bonsai/bonsai/bim/module/profile/data.py index 9f89cf01ed..88bd85197c 100644 --- a/src/bonsai/bonsai/bim/module/profile/data.py +++ b/src/bonsai/bonsai/bim/module/profile/data.py @@ -51,7 +51,7 @@ class ProfileData: @classmethod def active_profile_users(cls): - profiles_props = bpy.context.scene.BIMProfileProperties + profiles_props = tool.Profile.get_profile_props() if profiles_props.active_profile_index >= len(profiles_props.profiles): return 0 profile_prop = profiles_props.profiles[profiles_props.active_profile_index] @@ -83,7 +83,7 @@ class ProfileData: @classmethod def is_arbitrary_profile(cls): - props = bpy.context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() if props.active_profile_id: profile = tool.Ifc.get().by_id(props.active_profile_id) if profile.is_a("IfcArbitraryClosedProfileDef"): diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index a2e5ca5626..b0d5073429 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -35,7 +35,7 @@ class LoadProfiles(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() props.profiles.clear() filter_material_profiles = props.is_filtering_material_profiles @@ -64,7 +64,8 @@ class DisableProfileEditingUI(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.BIMProfileProperties.is_editing = False + props = tool.Profile.get_profile_props() + props.is_editing = False return {"FINISHED"} @@ -75,7 +76,7 @@ class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator): profile: bpy.props.IntProperty() def _execute(self, context): - props = context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() current_index = props.active_profile_index ifc_file = tool.Ifc.get() @@ -113,7 +114,7 @@ class EnableEditingProfile(bpy.types.Operator): profile: bpy.props.IntProperty() def execute(self, context): - props = context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() props.profile_attributes.clear() bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.profile), props.profile_attributes) props.active_profile_id = self.profile @@ -126,7 +127,8 @@ class DisableEditingProfile(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.BIMProfileProperties.active_profile_id = 0 + props = tool.Profile.get_profile_props() + props.active_profile_id = 0 bpy.ops.bim.disable_editing_arbitrary_profile() return {"FINISHED"} @@ -137,7 +139,7 @@ class EditProfile(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() attributes = bonsai.bim.helper.export_attributes(props.profile_attributes) profile = tool.Ifc.get().by_id(props.active_profile_id) ifcopenshell.api.run("profile.edit_profile", tool.Ifc.get(), profile=profile, attributes=attributes) @@ -152,7 +154,7 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() profile_class = props.profile_classes if profile_class == "IfcArbitraryClosedProfileDef": obj = props.object_to_profile @@ -229,7 +231,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() active_profile = props.profiles[props.active_profile_index] profile_id = active_profile.ifc_definition_id props.active_arbitrary_profile_id = profile_id @@ -253,7 +255,7 @@ def disable_editing_arbitrary_profile(context): bpy.data.objects.remove(obj) bpy.data.meshes.remove(profile_mesh) - props = context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() props.active_arbitrary_profile_id = 0 # need to update profile manager ui # if this was called from decorator @@ -276,7 +278,7 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() old_profile = tool.Ifc.get().by_id(props.active_arbitrary_profile_id) obj = context.active_object @@ -322,7 +324,7 @@ class SelectProfileInProfilesUI(bpy.types.Operator): profile_id: bpy.props.IntProperty() def execute(self, context): - props = bpy.context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() ifc_file = tool.Ifc.get() profile = ifc_file.by_id(self.profile_id) bpy.ops.bim.load_profiles() diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py index 801e7e0204..9a2da938e6 100644 --- a/src/bonsai/bonsai/bim/module/profile/prop.py +++ b/src/bonsai/bonsai/bim/module/profile/prop.py @@ -35,7 +35,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union def get_profile_classes(self, context): @@ -91,6 +91,17 @@ class BIMProfileProperties(PropertyGroup): poll=lambda self, obj: obj.type == "MESH", ) + if TYPE_CHECKING: + is_editing: bool + profiles: bpy.types.bpy_prop_collection_idprop[Profile] + active_profile_index: int + active_profile_id: int + active_arbitrary_profile_id: int + profile_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + profile_classes: str + is_filtering_material_profiles: bool + object_to_profile: Union[bpy.types.Object, None] + def generate_thumbnail_for_active_profile(): from PIL import Image, ImageDraw @@ -98,7 +109,7 @@ def generate_thumbnail_for_active_profile(): if bpy.app.background: return - props = bpy.context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() ifc_file = tool.Ifc.get() preview_collection = ProfileData.preview_collection diff --git a/src/bonsai/bonsai/bim/module/profile/ui.py b/src/bonsai/bonsai/bim/module/profile/ui.py index 9b8f65660a..fa5e9896b3 100644 --- a/src/bonsai/bonsai/bim/module/profile/ui.py +++ b/src/bonsai/bonsai/bim/module/profile/ui.py @@ -16,12 +16,17 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import bonsai.bim.helper import bonsai.tool as tool from bpy.types import Panel, UIList from bonsai.bim.module.profile.data import ProfileData from bonsai.bim.module.profile.prop import generate_thumbnail_for_active_profile +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.profile.prop import BIMProfileProperties, Profile class BIM_PT_profiles(Panel): @@ -40,7 +45,7 @@ class BIM_PT_profiles(Panel): def draw(self, context): if not ProfileData.is_loaded: ProfileData.load() - self.props = context.scene.BIMProfileProperties + self.props = tool.Profile.get_profile_props() active_profile = None if self.props.is_editing and (active_profile := tool.Profile.get_active_profile_ui()): @@ -129,8 +134,16 @@ class BIM_PT_profiles(Panel): class BIM_UL_profiles(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - props = context.scene.BIMProfileProperties + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMProfileProperties, + item: Profile, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) row.prop(item, "name", text="", emboss=False) diff --git a/src/bonsai/bonsai/bim/module/pset/data.py b/src/bonsai/bonsai/bim/module/pset/data.py index ab89d67b31..88d830d976 100644 --- a/src/bonsai/bonsai/bim/module/pset/data.py +++ b/src/bonsai/bonsai/bim/module/pset/data.py @@ -175,7 +175,7 @@ class MaterialPsetsData(Data): @classmethod def load(cls): ifc_definition_id = None - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() if props.materials and props.active_material_index < len(props.materials): ifc_definition_id = props.materials[props.active_material_index].ifc_definition_id @@ -188,7 +188,7 @@ class MaterialPsetsData(Data): @classmethod def pset_name(cls): - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() if props.materials and props.active_material_index < len(props.materials): material = props.materials[props.active_material_index] if material.ifc_definition_id: diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py index 2014ff1bd6..86a3363557 100644 --- a/src/bonsai/bonsai/bim/module/pset/prop.py +++ b/src/bonsai/bonsai/bim/module/pset/prop.py @@ -173,7 +173,7 @@ def get_group_qto_names(self, context): def get_profile_pset_names(self, context): global psetnames - pprops = context.scene.BIMProfileProperties + pprops = tool.Profile.get_profile_props() ifc_class = IfcStore.get_file().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a() if ifc_class not in psetnames: psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema()) diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index 8db5c36389..21f3c1b083 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -403,13 +403,13 @@ class BIM_PT_material_psets(Panel): ifc_file = tool.Ifc.get() if not ifc_file or ifc_file.schema == "IFC2X3": return False # We don't support material psets in IFC2X3 because they suck - props = context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() if props.is_editing and (material := props.active_material) and material.ifc_definition_id: return True return False def draw(self, context): - props = context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() if props.materials and props.active_material_index < len(props.materials): ifc_definition_id = props.materials[props.active_material_index].ifc_definition_id @@ -663,10 +663,10 @@ class BIM_PT_profile_psets(Panel): @classmethod def poll(cls, context): - props = context.scene.BIMProfileProperties + props = tool.Profile.get_profile_props() if not props.is_editing: return False - total_profiles = len(context.scene.BIMProfileProperties.profiles) + total_profiles = len(props.profiles) if total_profiles > 0 and props.active_profile_index < total_profiles: return True return False diff --git a/src/bonsai/bonsai/tool/bcf.py b/src/bonsai/bonsai/tool/bcf.py index 4a4068034a..c427a22e90 100644 --- a/src/bonsai/bonsai/tool/bcf.py +++ b/src/bonsai/bonsai/tool/bcf.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bcf.v2.visinfo import bonsai.core.tool import bonsai.tool as tool @@ -32,16 +33,24 @@ import bcf.agnostic.model import bcf.agnostic.topic import bcf.agnostic.visinfo -from typing import Any, Union, TypeVar, TypeGuard, Optional +from typing import Any, Union, TypeVar, TypeGuard, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.bcf.prop import BCFProperties T = TypeVar("T") class Bcf(bonsai.core.tool.Bcf): + @classmethod + def get_bcf_props(cls) -> "BCFProperties": + return bpy.context.scene.BCFProperties + @classmethod def get_path(cls) -> str: - return bpy.context.scene.BCFProperties.bcf_file + props = cls.get_bcf_props() + return props.bcf_file @classmethod def is_list_of(cls, a: list[Any], t: type[T]) -> TypeGuard[list[T]]: diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 605bcbab90..ad62b1fabc 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -191,9 +191,8 @@ class Blender(bonsai.core.tool.Blender): if obj_type == "Object": return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id elif obj_type == "Material": - return context.scene.BIMMaterialProperties.materials[ - context.scene.BIMMaterialProperties.active_material_index - ].ifc_definition_id + props = tool.Material.get_material_props() + return props.materials[props.active_material_index].ifc_definition_id elif obj_type == "MaterialSetItem": return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id elif obj_type == "Task": @@ -208,9 +207,8 @@ class Blender(bonsai.core.tool.Blender): context.scene.BIMResourceProperties.active_resource_index ].ifc_definition_id elif obj_type == "Profile": - return context.scene.BIMProfileProperties.profiles[ - context.scene.BIMProfileProperties.active_profile_index - ].ifc_definition_id + props = tool.Profile.get_profile_props() + return props.profiles[props.active_profile_index].ifc_definition_id elif obj_type == "WorkSchedule": return context.scene.BIMWorkScheduleProperties.active_work_schedule_id elif obj_type == "Group": diff --git a/src/bonsai/bonsai/tool/library.py b/src/bonsai/bonsai/tool/library.py index c28173482b..f968a21625 100644 --- a/src/bonsai/bonsai/tool/library.py +++ b/src/bonsai/bonsai/tool/library.py @@ -16,52 +16,62 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import ifcopenshell import bpy import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool -from typing import Literal, Any, Union +from typing import Literal, Any, Union, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.library.prop import BIMLibraryProperties class Library(bonsai.core.tool.Library): + @classmethod + def get_library_props(cls) -> BIMLibraryProperties: + return bpy.context.scene.BIMLibraryProperties + @classmethod def clear_editing_mode(cls) -> None: - bpy.context.scene.BIMLibraryProperties.editing_mode = "NONE" + cls.get_library_props().editing_mode = "NONE" @classmethod def export_library_attributes(cls) -> dict[str, Any]: - props = bpy.context.scene.BIMLibraryProperties + props = cls.get_library_props() return bonsai.bim.helper.export_attributes(props.library_attributes) @classmethod def export_reference_attributes(cls) -> dict[str, Any]: - props = bpy.context.scene.BIMLibraryProperties + props = cls.get_library_props() return bonsai.bim.helper.export_attributes(props.reference_attributes) @classmethod def get_active_library(cls) -> ifcopenshell.entity_instance: - return tool.Ifc.get().by_id(bpy.context.scene.BIMLibraryProperties.active_library_id) + props = cls.get_library_props() + return tool.Ifc.get().by_id(props.active_library_id) @classmethod def get_active_reference(cls) -> ifcopenshell.entity_instance: - return tool.Ifc.get().by_id(bpy.context.scene.BIMLibraryProperties.active_reference_id) + props = cls.get_library_props() + return tool.Ifc.get().by_id(props.active_reference_id) @classmethod def import_library_attributes(cls, library: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMLibraryProperties + props = cls.get_library_props() props.library_attributes.clear() bonsai.bim.helper.import_attributes2(library, props.library_attributes) @classmethod def import_reference_attributes(cls, reference: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMLibraryProperties + props = cls.get_library_props() props.reference_attributes.clear() bonsai.bim.helper.import_attributes2(reference, props.reference_attributes) @classmethod def import_references(cls, library: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMLibraryProperties + props = cls.get_library_props() props.references.clear() if tool.Ifc.get_schema() == "IFC2X3": references = library.LibraryReference @@ -74,7 +84,7 @@ class Library(bonsai.core.tool.Library): @classmethod def set_active_library(cls, library: Union[ifcopenshell.entity_instance, None]) -> None: - props = bpy.context.scene.BIMLibraryProperties + props = cls.get_library_props() if library is None: props.active_library_id = 0 else: @@ -82,8 +92,10 @@ class Library(bonsai.core.tool.Library): @classmethod def set_active_reference(cls, reference: ifcopenshell.entity_instance) -> None: - bpy.context.scene.BIMLibraryProperties.active_reference_id = reference.id() + props = cls.get_library_props() + props.active_reference_id = reference.id() @classmethod def set_editing_mode(cls, mode: Literal["LIBRARY", "REFERENCES", "REFERENCE"]) -> None: - bpy.context.scene.BIMLibraryProperties.editing_mode = mode + props = cls.get_library_props() + props.editing_mode = mode diff --git a/src/bonsai/bonsai/tool/material.py b/src/bonsai/bonsai/tool/material.py index 5dad3cbf63..0d32bb1ebd 100644 --- a/src/bonsai/bonsai/tool/material.py +++ b/src/bonsai/bonsai/tool/material.py @@ -33,12 +33,18 @@ from typing_extensions import assert_never if TYPE_CHECKING: # Avoid circular imports. from bonsai.bim.module.material.prop import Material as MaterialItem + from bonsai.bim.module.material.prop import BIMMaterialProperties class Material(bonsai.core.tool.Material): + @classmethod + def get_material_props(cls) -> BIMMaterialProperties: + return bpy.context.scene.BIMMaterialProperties + @classmethod def disable_editing_materials(cls) -> None: - bpy.context.scene.BIMMaterialProperties.is_editing = False + props = tool.Material.get_material_props() + props.is_editing = False @classmethod def duplicate_material(cls, material: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: @@ -55,11 +61,13 @@ class Material(bonsai.core.tool.Material): @classmethod def enable_editing_materials(cls) -> None: - bpy.context.scene.BIMMaterialProperties.is_editing = True + props = tool.Material.get_material_props() + props.is_editing = True @classmethod def get_active_material_type(cls) -> str: - return bpy.context.scene.BIMMaterialProperties.material_type + props = tool.Material.get_material_props() + return props.material_type @classmethod def get_elements_by_material(cls, material: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: @@ -68,7 +76,7 @@ class Material(bonsai.core.tool.Material): @classmethod def get_active_material_item(cls) -> Union[MaterialItem, None]: """Get active material props item if index is valid, otherwise, return None.""" - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() if 0 <= props.active_material_index < len(props.materials): return props.materials[props.active_material_index] return None @@ -80,7 +88,7 @@ class Material(bonsai.core.tool.Material): @classmethod def import_material_definitions(cls, material_type: str) -> None: - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() # Store active category name to reselect it later. # Occurs when we expand/contract all categories. @@ -140,7 +148,8 @@ class Material(bonsai.core.tool.Material): @classmethod def is_editing_materials(cls) -> bool: - return bpy.context.scene.BIMMaterialProperties.is_editing + props = tool.Material.get_material_props() + return props.is_editing @classmethod def is_material_used_in_sets(cls, material: ifcopenshell.entity_instance) -> bool: @@ -156,23 +165,24 @@ class Material(bonsai.core.tool.Material): @classmethod def load_material_attributes(cls, material: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() props.material_attributes.clear() bonsai.bim.helper.import_attributes2(material, props.material_attributes) @classmethod def enable_editing_material(cls, material: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() props.active_material_id = material.id() props.editing_material_type = "ATTRIBUTES" @classmethod def get_material_attributes(cls) -> dict[str, Any]: - return bonsai.bim.helper.export_attributes(bpy.context.scene.BIMMaterialProperties.material_attributes) + props = tool.Material.get_material_props() + return bonsai.bim.helper.export_attributes(props.material_attributes) @classmethod def disable_editing_material(cls) -> None: - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() props.active_material_id = 0 props.editing_material_type = "" diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index c20f2762fd..6db9718d4d 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -26,6 +26,7 @@ import collections.abc import numpy as np import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.grid import ifcopenshell.api.pset import ifcopenshell.geom import ifcopenshell.util.element @@ -1973,14 +1974,14 @@ class Model(bonsai.core.tool.Model): extrusion.Position = position @classmethod - def get_existing_x_angle(cls, extrusion): + def get_existing_x_angle(cls, extrusion: ifcopenshell.entity_instance) -> float: x, y, z = extrusion.ExtrudedDirection.DirectionRatios x_angle = Vector((0, 1)).angle_signed(Vector((y, z))) return x_angle @classmethod - def create_axis_curve(cls, obj: bpy.types.Object, grid_axis: ifcopenshell.entity_instance): + def create_axis_curve(cls, obj: bpy.types.Object, grid_axis: ifcopenshell.entity_instance) -> None: m = tool.Surveyor.get_absolute_matrix(obj) points = [m @ np.array(v.co.to_4d()) for v in obj.data.vertices[0:2]] ifcopenshell.api.grid.create_axis_curve( diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py index 9100383769..d230835b09 100644 --- a/src/bonsai/bonsai/tool/profile.py +++ b/src/bonsai/bonsai/tool/profile.py @@ -33,9 +33,14 @@ from typing import Union, TYPE_CHECKING if TYPE_CHECKING: import bonsai.bim.module.profile.prop + from bonsai.bim.module.profile.prop import BIMProfileProperties class Profile(bonsai.core.tool.Profile): + @classmethod + def get_profile_props(cls) -> BIMProfileProperties: + return bpy.context.scene.BIMProfileProperties + @classmethod def draw_image_for_ifc_profile( cls, draw: PIL.ImageDraw.ImageDraw, profile: ifcopenshell.entity_instance, size: float @@ -116,7 +121,7 @@ class Profile(bonsai.core.tool.Profile): @classmethod def get_active_profile_ui(cls) -> Union[bonsai.bim.module.profile.prop.Profile, None]: - props = bpy.context.scene.BIMProfileProperties + props = cls.get_profile_props() index = props.active_profile_index if len(props.profiles) > index >= 0: return props.profiles[index] diff --git a/src/bonsai/test/tool/test_library.py b/src/bonsai/test/tool/test_library.py index 7ce2308d69..12209824ea 100644 --- a/src/bonsai/test/tool/test_library.py +++ b/src/bonsai/test/tool/test_library.py @@ -31,7 +31,7 @@ class TestImplementsTool(NewFile): class TestClearEditingMode(NewFile): def test_run(self): - props = bpy.context.scene.BIMLibraryProperties + props = tool.Library.get_library_props() props.editing_mode = "LIBRARY" subject.clear_editing_mode() assert props.editing_mode == "NONE" @@ -78,7 +78,7 @@ class TestImportLibraryAttributes(NewFile): tool.Ifc.set(ifc := ifcopenshell.file()) library = ifc.createIfcLibraryInformation("Name", "Version", None, "VersionDate", "Location", "Description") subject.import_library_attributes(library) - props = bpy.context.scene.BIMLibraryProperties + props = tool.Library.get_library_props() assert props.library_attributes.get("Name").string_value == "Name" assert props.library_attributes.get("Version").string_value == "Version" assert props.library_attributes.get("VersionDate").string_value == "VersionDate" @@ -91,7 +91,7 @@ class TestImportReferenceAttributes(NewFile): tool.Ifc.set(ifc := ifcopenshell.file()) reference = ifc.createIfcLibraryReference("Location", "Identification", "Name", "Description", "Language") subject.import_reference_attributes(reference) - props = bpy.context.scene.BIMLibraryProperties + props = tool.Library.get_library_props() assert props.reference_attributes.get("Location").string_value == "Location" assert props.reference_attributes.get("Identification").string_value == "Identification" assert props.reference_attributes.get("Name").string_value == "Name" @@ -105,7 +105,7 @@ class TestImportReferences(NewFile): library = ifc.createIfcLibraryInformation() reference = ifc.createIfcLibraryReference(Name="Reference", ReferencedLibrary=library) subject.import_references(library) - props = bpy.context.scene.BIMLibraryProperties + props = tool.Library.get_library_props() assert props.references[0].ifc_definition_id == reference.id() assert props.references[0].name == "Reference" @@ -116,7 +116,8 @@ class TestSetActiveLibrary(NewFile): tool.Ifc.set(ifc) library = ifc.createIfcLibraryInformation() subject.set_active_library(library) - assert bpy.context.scene.BIMLibraryProperties.active_library_id == library.id() + props = tool.Library.get_library_props() + assert props.active_library_id == library.id() class TestSetActiveReference(NewFile): @@ -125,10 +126,12 @@ class TestSetActiveReference(NewFile): tool.Ifc.set(ifc) reference = ifc.createIfcLibraryReference() subject.set_active_reference(reference) - assert bpy.context.scene.BIMLibraryProperties.active_reference_id == reference.id() + props = tool.Library.get_library_props() + assert props.active_reference_id == reference.id() class TestSetEditingMode(NewFile): def test_run(self): subject.set_editing_mode("LIBRARY") - assert bpy.context.scene.BIMLibraryProperties.editing_mode == "LIBRARY" + props = tool.Library.get_library_props() + assert props.editing_mode == "LIBRARY" diff --git a/src/bonsai/test/tool/test_material.py b/src/bonsai/test/tool/test_material.py index 281c8918ac..c980135b5a 100644 --- a/src/bonsai/test/tool/test_material.py +++ b/src/bonsai/test/tool/test_material.py @@ -35,25 +35,28 @@ class TestImplementsTool(NewFile): class TestDisableEditingMaterials(NewFile): def test_run(self): - bpy.context.scene.BIMMaterialProperties.is_editing = True + props = tool.Material.get_material_props() + props.is_editing = True subject.disable_editing_materials() - assert bpy.context.scene.BIMMaterialProperties.is_editing is False + assert props.is_editing is False class TestEnableEditingMaterials(NewFile): def test_run(self): - bpy.context.scene.BIMMaterialProperties.is_editing = False + props = tool.Material.get_material_props() + props.is_editing = False subject.enable_editing_materials() - assert bpy.context.scene.BIMMaterialProperties.is_editing is True + assert props.is_editing is True class TestGetActiveMaterialType(NewFile): def test_run(self): ifc = ifcopenshell.file() tool.Ifc.set(ifc) - bpy.context.scene.BIMMaterialProperties.material_type = "IfcMaterial" + props = tool.Material.get_material_props() + props.material_type = "IfcMaterial" assert subject.get_active_material_type() == "IfcMaterial" - bpy.context.scene.BIMMaterialProperties.material_type = "IfcMaterialLayerSet" + props.material_type = "IfcMaterialLayerSet" assert subject.get_active_material_type() == "IfcMaterialLayerSet" @@ -73,7 +76,7 @@ class TestImportMaterialDefinitions(NewFile): tool.Ifc.set(ifc) material = ifc.createIfcMaterial(Name="Name", Category="Category") subject.import_material_definitions("IfcMaterial") - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() assert props.materials[0].ifc_definition_id == 0 assert props.materials[0].name == "Category" assert props.materials[0].is_category is True @@ -85,7 +88,7 @@ class TestImportMaterialDefinitions(NewFile): tool.Ifc.set(ifc) material = ifc.createIfcMaterial(Name="Name", Category="Category") subject.import_material_definitions("IfcMaterial") - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() props.materials[0].is_expanded = True subject.import_material_definitions("IfcMaterial") assert len(props.materials) == 2 @@ -98,7 +101,7 @@ class TestImportMaterialDefinitions(NewFile): tool.Ifc.set(ifc) material = ifc.createIfcMaterialLayerSet(LayerSetName="Name") subject.import_material_definitions("IfcMaterialLayerSet") - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() assert props.materials[0].ifc_definition_id == material.id() assert props.materials[0].name == "Name" assert props.materials[0].total_elements == 0 @@ -108,7 +111,7 @@ class TestImportMaterialDefinitions(NewFile): tool.Ifc.set(ifc) material = ifc.createIfcMaterialProfileSet(Name="Name") subject.import_material_definitions("IfcMaterialProfileSet") - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() assert props.materials[0].ifc_definition_id == material.id() assert props.materials[0].name == "Name" assert props.materials[0].total_elements == 0 @@ -118,7 +121,7 @@ class TestImportMaterialDefinitions(NewFile): tool.Ifc.set(ifc) material = ifc.createIfcMaterialConstituentSet(Name="Name") subject.import_material_definitions("IfcMaterialConstituentSet") - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() assert props.materials[0].ifc_definition_id == material.id() assert props.materials[0].name == "Name" assert props.materials[0].total_elements == 0 @@ -128,7 +131,7 @@ class TestImportMaterialDefinitions(NewFile): tool.Ifc.set(ifc) material = ifc.createIfcMaterialList() subject.import_material_definitions("IfcMaterialList") - props = bpy.context.scene.BIMMaterialProperties + props = tool.Material.get_material_props() assert props.materials[0].ifc_definition_id == material.id() assert props.materials[0].name == "Unnamed" assert props.materials[0].total_elements == 0 @@ -136,9 +139,10 @@ class TestImportMaterialDefinitions(NewFile): class TestIsEditingMaterials(NewFile): def test_run(self): - bpy.context.scene.BIMMaterialProperties.is_editing = False + props = tool.Material.get_material_props() + props.is_editing = False assert subject.is_editing_materials() is False - bpy.context.scene.BIMMaterialProperties.is_editing = True + props.is_editing = True assert subject.is_editing_materials() is True diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py b/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py index d65a96ee54..4e692008e3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/create_axis_curve.py @@ -22,13 +22,14 @@ import ifcopenshell.util.element import ifcopenshell.util.unit import ifcopenshell.util.placement import numpy as np +from ifcopenshell.util.shape_builder import VectorType, V, ifc_safe_vector_type def create_axis_curve( file: ifcopenshell.file, *, - p1: np.ndarray, - p2: np.ndarray, + p1: VectorType, + p2: VectorType, grid_axis: ifcopenshell.entity_instance, is_si: bool = True, ) -> None: @@ -60,7 +61,7 @@ def create_axis_curve( model, p1=np.array((0., 0., 0.)), p2=np.array((0., 10., 0.)), grid_axis=axis_1) """ existing_curve = grid_axis.AxisCurve - + p1, p2 = V(p1), V(p2) if is_si: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) p1 /= unit_scale @@ -70,8 +71,8 @@ def create_axis_curve( grid_matrix_i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(grid.ObjectPlacement)) grid_axis.AxisCurve = file.createIfcPolyline( ( - file.createIfcCartesianPoint((grid_matrix_i @ p1).tolist()), - file.createIfcCartesianPoint((grid_matrix_i @ p2).tolist()), + file.createIfcCartesianPoint(ifc_safe_vector_type(grid_matrix_i @ p1)), + file.createIfcCartesianPoint(ifc_safe_vector_type(grid_matrix_i @ p2)), ) ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 9411307a66..2109bca618 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -264,7 +264,6 @@ class Usecase: "IfcProductDefinitionShape": ["HasShapeAspects"], "IfcRepresentationMap": ["HasShapeAspects"], } - print('appending type product!') self.existing_contexts = self.file.by_type("IfcGeometricRepresentationContext") element = self.add_element(self.settings["element"]) self.reuse_existing_contexts() From 32608ff19492296ec41f065b4b0b3d45af5616d1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Feb 2025 11:43:39 +0500 Subject: [PATCH 040/476] py -m ifcopenshell.validate to use argparse So command would fail if no arguments provided and there would hints on how to use it. --- .../ifcopenshell/validate.py | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/validate.py b/src/ifcopenshell-python/ifcopenshell/validate.py index 4632e090c9..8f2d4d22ef 100644 --- a/src/ifcopenshell-python/ifcopenshell/validate.py +++ b/src/ifcopenshell-python/ifcopenshell/validate.py @@ -25,11 +25,21 @@ Can be used to run validation on IFC file from the command line: python -m ifcopenshell.validate /path/to/model.ifc --rules -Available flags: +``` +$ python -m ifcopenshell.validate -h +usage: validate.py [-h] [--rules] [--json] [--fields] [--spf] files [files ...] + +positional arguments: + files The IFC file to validate. + +options: + -h, --help show this help message and exit + --rules Run express rules. + --json Output in JSON format. + --fields Output more detailed information about failed entities (only with --json). + --spf Output entities in SPF format (only with --json). +``` -- ``--rules``: Also check express rules. -- ``--json``: Produce JSON output. -- ``--fields``: Output more detailed information about failed entities (available only with ``--json``). """ import os @@ -37,6 +47,7 @@ import sys import json import functools import types +import argparse from collections import namedtuple from typing import Union, Iterator, Any, Optional @@ -647,13 +658,24 @@ if __name__ == "__main__": sys.excepthook = handle_exception - filenames = [x for x in sys.argv[1:] if not x.startswith("--")] - flags = set(x for x in sys.argv[1:] if x.startswith("--")) + parser = argparse.ArgumentParser() + parser.add_argument("files", nargs="+", help="The IFC file to validate.") + parser.add_argument("--rules", action="store_true", help="Run express rules.") + parser.add_argument("--json", action="store_true", help="Output in JSON format.") + parser.add_argument( + "--fields", + action="store_true", + help="Output more detailed information about failed entities (only with --json).", + ) + parser.add_argument("--spf", action="store_true", help="Output entities in SPF format (only with --json).") + args = parser.parse_args() + + filenames: list[str] = args.files some_file_is_invalid = False for fn in filenames: handler = None - if "--json" in flags: + if args.json: logger = json_logger() else: logger = logging.getLogger("validate") @@ -662,14 +684,14 @@ if __name__ == "__main__": logger.setLevel(logging.DEBUG) print("Validating", fn, file=sys.stderr) - validate(fn, logger, "--rules" in flags) + validate(fn, logger, args.rules) - if "--json" in flags: + if args.json: sys.stdout.reconfigure(encoding="utf-8") conv = str - if "--spf" in flags: + if args.spf: conv = lambda x: x.to_string() if isinstance(x, ifcopenshell.entity_instance) else str(x) - if "--fields" in flags: + if args.fields: def conv(x): if isinstance(x, ifcopenshell.entity_instance): From 8f1446bfa4722b94b9b685c22bc100aedc1828a3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Feb 2025 13:13:05 +0500 Subject: [PATCH 041/476] Library UI - rename assets and libraries from UI Example - https://imgur.com/a/0kcQrb2 --- .../bonsai/bim/module/project/operator.py | 7 ++--- src/bonsai/bonsai/bim/module/project/prop.py | 31 +++++++++++++++++-- src/bonsai/bonsai/bim/module/project/ui.py | 5 ++- src/bonsai/bonsai/tool/project.py | 6 ++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index df6196b3d4..aa1c8b5403 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -341,13 +341,12 @@ class ChangeLibraryElement(bpy.types.Operator): return {"FINISHED"} def get_name(self, element: ifcopenshell.entity_instance) -> str: - if element.is_a("IfcProfileDef"): - return element.ProfileName or "Unnamed" - return element.Name or "Unnamed" + attr_name = tool.Project.get_library_element_attr_name(element) + return getattr(element, attr_name) or "Unnamed" def add_library_asset(self, name: str, ifc_definition_id: int) -> None: new = self.props.library_elements.add() - new.name = name + new["name"] = name new.ifc_definition_id = ifc_definition_id element = self.library_file.by_id(ifc_definition_id) diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index c94de56614..e56826be57 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -36,6 +36,7 @@ from bpy.props import ( StringProperty, ) from typing import TYPE_CHECKING, Literal, Union, get_args +from typing_extensions import assert_never def get_export_schema(self: "BIMProjectProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: @@ -147,11 +148,35 @@ def update_filter_mode(self: "BIMProjectProperties", context: bpy.types.Context) new.total_elements = len(ifcopenshell.util.element.get_types(ifc_type)) +def update_library_element_name(self: "LibraryElement", context: bpy.types.Context) -> None: + library_file = IfcStore.library_file + assert library_file + + if self.element_type == "CLASS": + raise Exception("Unexpected element type for rename: 'CLASS'.") + + def update_element_name(ifc_definition_id: int, name: str) -> None: + element = library_file.by_id(ifc_definition_id) + attr_name = tool.Project.get_library_element_attr_name(element) + previous_name = getattr(element, attr_name) + if name == previous_name: + return + setattr(element, attr_name, name) + + if self.element_type == "ASSET": + update_element_name(self.ifc_definition_id, self.name) + elif self.element_type == "LIBRARY": + assert self.ifc_definition_id, "Renaming for unassigned elements library is not supported." + update_element_name(self.ifc_definition_id, self.name) + else: + assert_never(self.element_type) + + LibraryElementType = Literal["ASSET", "CLASS", "LIBRARY"] class LibraryElement(PropertyGroup): - name: StringProperty(name="Name") + name: StringProperty(name="Name", update=update_library_element_name) element_type: EnumProperty(items=[(i, i, "") for i in get_args(LibraryElementType)], name="Element Type") # Asset group. asset_count: IntProperty(name="Asset Count") @@ -372,7 +397,7 @@ class BIMProjectProperties(PropertyGroup): def add_library_project_library(self, name: str, asset_count: int, ifc_definition_id: int) -> LibraryElement: new = self.library_elements.add() - new.name = name + new["name"] = name new.asset_count = asset_count new.element_type = "LIBRARY" new.ifc_definition_id = ifc_definition_id @@ -380,7 +405,7 @@ class BIMProjectProperties(PropertyGroup): def add_library_asset_class(self, name: str, asset_count: int) -> LibraryElement: new = self.library_elements.add() - new.name = name + new["name"] = name new.asset_count = asset_count new.element_type = "CLASS" return new diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 361f2d1a61..cd0c69512f 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -517,7 +517,10 @@ class BIM_UL_library(UIList): op.element_name = item.name op.breadcrumb_type = item.element_type op.library_id = item.ifc_definition_id - row.label(text=item.name) + if item.ifc_definition_id: + row.prop(item, "name", text="", emboss=False) + else: + row.label(text=item.name) if item.ifc_definition_id and item.is_declarable: if item.is_declared: op = row.operator("bim.unassign_library_declaration", text="", icon="KEYFRAME_HLT", emboss=False) diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index ae966b49de..7b529cdea9 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -375,3 +375,9 @@ class Project(bonsai.core.tool.Project): props.add_library_project_library( project_library.Name or "Unnamed", len(library_elements), project_library.id() ) + + @classmethod + def get_library_element_attr_name(cls, library_element: ifcopenshell.entity_instance) -> str: + if library_element.is_a("IfcProfileDef"): + return "ProfileName" + return "Name" From 8c2d71b3066393fc7dec491d8c03c1bd82a29e48 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Feb 2025 13:23:38 +0500 Subject: [PATCH 042/476] Rename library references from UI Example - https://imgur.com/a/Ez99d1N --- src/bonsai/bonsai/bim/module/library/prop.py | 13 ++++++++++++- src/bonsai/bonsai/bim/module/library/ui.py | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/library/prop.py b/src/bonsai/bonsai/bim/module/library/prop.py index 896bf0da2e..24defdf9ea 100644 --- a/src/bonsai/bonsai/bim/module/library/prop.py +++ b/src/bonsai/bonsai/bim/module/library/prop.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bonsai.bim.prop import StrProperty, Attribute from bonsai.bim.module.library.data import LibrariesData from bpy.types import PropertyGroup @@ -37,8 +38,18 @@ def update_active_reference_index(self, context): LibrariesData.is_loaded = False +def update_library_element_name(self: "LibraryReference", context: bpy.types.Context): + ifc_file = tool.Ifc.get() + element = ifc_file.by_id(self.ifc_definition_id) + previous_name = element.Name + if self.name == previous_name: + return + element.Name = self.name + LibrariesData.is_loaded = False + + class LibraryReference(PropertyGroup): - name: StringProperty(name="Name") + name: StringProperty(name="Name", update=update_library_element_name) ifc_definition_id: IntProperty(name="IFC Definition ID") if TYPE_CHECKING: diff --git a/src/bonsai/bonsai/bim/module/library/ui.py b/src/bonsai/bonsai/bim/module/library/ui.py index 1c9f389a7e..bfc64e3020 100644 --- a/src/bonsai/bonsai/bim/module/library/ui.py +++ b/src/bonsai/bonsai/bim/module/library/ui.py @@ -140,7 +140,7 @@ class BIM_UL_library_references(UIList): ): if item: row = layout.row(align=True) - row.label(text=item.name) + row.prop(item, "name", text="", emboss=False) op = row.operator("bim.enable_editing_library_reference", text="", icon="GREASEPENCIL") op.reference = item.ifc_definition_id op = row.operator("bim.remove_library_reference", text="", icon="X") @@ -153,6 +153,6 @@ class BIM_UL_object_library_references(UIList): ): if item: row = layout.row(align=True) - row.label(text=item.name) + row.prop(item, "name", text="", emboss=False) op = row.operator("bim.assign_library_reference", text="", icon="ADD") op.reference = item.ifc_definition_id From 93ee92c401597cab760d1fb0b934aed351216778 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Feb 2025 16:22:45 +0500 Subject: [PATCH 043/476] Profiles, Materials UI - indicate in UI currently edited element --- src/bonsai/bonsai/bim/module/material/ui.py | 2 ++ src/bonsai/bonsai/bim/module/profile/ui.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index eef2adb670..19865be4e8 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -431,6 +431,8 @@ class BIM_UL_materials(UIList): row.prop(item, "name", text="", emboss=False) else: row.label(text="", icon="BLANK1") + if item.ifc_definition_id == data.active_material_id: + row.label(text="", icon="GREASEPENCIL") if material_type == "IfcMaterialList": row.label(text=item.name, icon="MATERIAL") else: diff --git a/src/bonsai/bonsai/bim/module/profile/ui.py b/src/bonsai/bonsai/bim/module/profile/ui.py index fa5e9896b3..ee2a93f107 100644 --- a/src/bonsai/bonsai/bim/module/profile/ui.py +++ b/src/bonsai/bonsai/bim/module/profile/ui.py @@ -146,5 +146,7 @@ class BIM_UL_profiles(UIList): ): if item: row = layout.row(align=True) + if item.ifc_definition_id == data.active_profile_id: + row.label(text="", icon="GREASEPENCIL") row.prop(item, "name", text="", emboss=False) row.label(text=item.ifc_class) From 5d8c2f2ec2edfcf56d75dd7a45e5ace0dc3ef66b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Feb 2025 17:09:38 +0500 Subject: [PATCH 044/476] deprecate test-safe #5192 --- .github/workflows/ci.yml | 1 - src/ifcopenshell-python/Makefile | 6 ------ 2 files changed, 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f92bc1c126..5845b9d8d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,6 @@ jobs: run: | python -m pip install --upgrade pip pip install xmlschema xsdata numpy lxml pytest isodate lark networkx tabulate python-dateutil shapely - pip install mathutils pip install src/bcf --no-deps pip install https://github.com/Andrej730/aud/archive/refs/heads/master-reduced-size.zip diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index 44a08b2c96..6e99b5d6d9 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -61,12 +61,6 @@ IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.8.1-c test: pytest -p no:pytest-blender test -# safe version of tests without mathutils dependency -# for tests to work for github workflow with python <3.10 #3895 -.PHONY: test-safe -test-safe: - pytest -p no:pytest-blender test --ignore=test/util/test_shape_builder.py - .PHONY: build-ids-docs build-ids-docs: mkdir -p test/build From 5a3a032fe6a92e7f736c77bf51edfee8dc9dc777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 14 Feb 2025 10:57:50 -0300 Subject: [PATCH 045/476] Snap - modify intersection plane origin to be related to the view location. --- src/bonsai/bonsai/tool/snap.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index fe84703d4e..e5db636050 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -274,14 +274,11 @@ class Snap(bonsai.core.tool.Snap): ) def select_plane_method(): - if not last_polyline_point: - plane_origin = Vector((0, 0, 0)) - plane_normal = Vector((0, 0, 1)) - if not tool_state.plane_method: - camera_rotation = rv3d.view_rotation - plane_origin = Vector((0, 0, 0)) - view_direction = Vector((0, 0, -1)) @ camera_rotation.to_matrix().transposed() + view_rotation = rv3d.view_rotation + view_location = rv3d.view_location + view_direction = Vector((0, 0, -1)) @ view_rotation.to_matrix().transposed() + plane_origin = view_location + view_direction * 10 plane_normal = view_direction.normalized() if tool_state.plane_method == "XY" or ( @@ -451,6 +448,7 @@ class Snap(bonsai.core.tool.Snap): tool_state.plane_origin = plane_origin # This will be used along with plane method intersection = tool.Raycast.ray_cast_to_plane(context, event, plane_origin, plane_normal) + print(intersection) axis_start = None axis_end = None From 247bff40755ba0eb61181cb6d85b798133440202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 17 Feb 2025 16:35:52 -0300 Subject: [PATCH 046/476] Polyline tool initial support for custom transformations. Still early development. Only works for XY plane. https://imgur.com/a/qW8gMFW --- .../bonsai/bim/module/model/decorator.py | 2 ++ src/bonsai/bonsai/tool/polyline.py | 36 ++++++++++++++----- src/bonsai/bonsai/tool/snap.py | 16 ++++++--- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 126dad57ad..264865cd3a 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -695,6 +695,8 @@ class PolylineDecorator: self.draw_batch("LINES", mouse_point + projection_point, decorator_color_unselected, edges) if axis1 and axis2: + axis1 = [tuple(tool.Polyline.use_transform_orientations(Vector(v))) for v in axis1] + axis2 = [tuple(tool.Polyline.use_transform_orientations(Vector(v))) for v in axis2] self.line_shader.uniform_float("lineWidth", 1.5) self.draw_batch("LINES", axis1, highlight_color(axis_color1), [(0, 1)]) self.draw_batch("LINES", axis2, highlight_color(axis_color2), [(0, 1)]) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index f306491a8a..a37b56bcad 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -27,7 +27,7 @@ from dataclasses import dataclass from lark import Lark, Transformer from math import degrees, radians, sin, cos, tan from mathutils import Vector, Matrix -from typing import Optional, Union, Literal +from typing import Optional, Union, Literal, List class Polyline(bonsai.core.tool.Polyline): @@ -154,13 +154,15 @@ class Polyline(bonsai.core.tool.Polyline): else: # Creates a fake "second to last" point away from the first point but in the same x axis # this allows to calculate the angle relative to x axis when there is only one point - second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) + second_to_last_point = Vector((last_point.x + 1000000000, last_point.y, last_point.z)) if tool_state.plane_method == "YZ": - second_to_last_point = Vector((last_point.x, last_point.y + 1000, last_point.z)) + second_to_last_point = Vector((last_point.x, last_point.y + 1000000000, last_point.z)) + second_to_last_point = tool.Polyline.use_transform_orientations(second_to_last_point) - world_second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) + world_second_to_last_point = Vector((last_point.x + 1000000000, last_point.y, last_point.z)) if tool_state.plane_method == "YZ": - world_second_to_last_point = Vector((last_point.x, last_point.y + 1000, last_point.z)) + world_second_to_last_point = Vector((last_point.x, last_point.y + 1000000000, last_point.z)) + world_second_to_last_point = tool.Polyline.use_transform_orientations(world_second_to_last_point) distance = (mouse_vector - last_point).length if distance < 0: @@ -278,7 +280,10 @@ class Polyline(bonsai.core.tool.Polyline): else: # Creates a fake "second to last" point away from the first point but in the same x axis # this allows to calculate the angle relative to x axis when there is only one point - second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) + second_to_last_point = Vector((last_point.x + 1000000000, last_point.y, last_point.z)) + if tool_state.plane_method == "YZ": + second_to_last_point = Vector((last_point.x, last_point.y + 1000000000, last_point.z)) + second_to_last_point = tool.Polyline.use_transform_orientations(second_to_last_point) distance = input_ui.get_number_value("D") @@ -288,8 +293,8 @@ class Polyline(bonsai.core.tool.Polyline): rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True) # When the angle in 180 degrees it might create a rotation vector that is equal to - # when the angle is 0 degress, leading the insertion point to the opposite direction - # This prevents the issue by ensuring the the negative x direction + # when the angle is 0 degrees, leading the insertion point to the opposite direction + # This prevents the issue by ensuring the negative x direction if round(angle, 4) == round(math.pi, 4): rot_vector.x = -1.0 @@ -439,7 +444,7 @@ class Polyline(bonsai.core.tool.Polyline): transformer = InputTransform() result = transformer.transform(parse_tree) - result = round(result, 5) + result = round(result, 4) return True, str(result) except: return False, "0" @@ -574,3 +579,16 @@ class Polyline(bonsai.core.tool.Polyline): measurement_data.total_length = polyline_data[0].total_length measurement_data.area = polyline_data[0].area + + @classmethod + def use_transform_orientations(cls, value:Union[Vector, Matrix]) -> Union[Vector, Matrix]: + custom_orientation = bpy.context.scene.transform_orientation_slots[0].custom_orientation + if custom_orientation: + custom_matrix = custom_orientation.matrix + if isinstance(value, Vector): + result = custom_matrix @ value + else: + result = custom_matrix.inverted() @ value + return result + return value + diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index e5db636050..b06d9a65bb 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -188,6 +188,7 @@ class Snap(bonsai.core.tool.Snap): if not axis: continue rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis) + rot_mat = tool.Polyline.use_transform_orientations(rot_mat) rot_intersection = rot_mat @ translated_intersection proximity = rot_intersection.y if tool_state.plane_method == "XZ": @@ -211,6 +212,7 @@ class Snap(bonsai.core.tool.Snap): if tool_state.plane_method == "YZ": axis = 90 - (axis * -1) rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis) + rot_mat = tool.Polyline.use_transform_orientations(rot_mat) rot_intersection = rot_mat @ translated_intersection start, end = create_axis_line_data(rot_mat, last_point) PolylineDecorator.set_angle_axis_line(start, end) @@ -229,10 +231,13 @@ class Snap(bonsai.core.tool.Snap): def mix_snap_and_axis(cls, snap_point, axis_start, axis_end): # Creates a mixed snap point between the locked axis and the object snap # Then it sorts them to get the shortest first + x_axis = tool.Polyline.use_transform_orientations(Vector((1, 0, 0))) + y_axis = tool.Polyline.use_transform_orientations(Vector((0, 1, 0))) + z_axis = tool.Polyline.use_transform_orientations(Vector((0, 0, 1))) intersections = [] - intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, Vector((1, 0, 0)))) - intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, Vector((0, 1, 0)))) - intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, Vector((0, 0, 1)))) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, x_axis)) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, y_axis)) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, z_axis)) polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline polyline_points = polyline_data[0].polyline_points if polyline_data else [] @@ -274,6 +279,9 @@ class Snap(bonsai.core.tool.Snap): ) def select_plane_method(): + if not last_polyline_point: + plane_origin = Vector((0, 0, 0)) + plane_normal = Vector((0, 0, 1)) if not tool_state.plane_method: view_rotation = rv3d.view_rotation view_location = rv3d.view_location @@ -302,6 +310,7 @@ class Snap(bonsai.core.tool.Snap): plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z)) plane_normal = Vector((1, 0, 0)) + plane_normal = tool.Polyline.use_transform_orientations(plane_normal) return plane_origin, plane_normal def cast_rays_to_single_object(obj, mouse_pos): @@ -448,7 +457,6 @@ class Snap(bonsai.core.tool.Snap): tool_state.plane_origin = plane_origin # This will be used along with plane method intersection = tool.Raycast.ray_cast_to_plane(context, event, plane_origin, plane_normal) - print(intersection) axis_start = None axis_end = None From c609aea1de6e4bcfeda124ce95ee6f8a89616dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 17 Feb 2025 16:46:13 -0300 Subject: [PATCH 047/476] Black . --- src/bonsai/bonsai/tool/polyline.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index a37b56bcad..29b076a274 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -31,7 +31,6 @@ from typing import Optional, Union, Literal, List class Polyline(bonsai.core.tool.Polyline): - @dataclass class PolylineUI: _D: str = "" @@ -581,7 +580,7 @@ class Polyline(bonsai.core.tool.Polyline): measurement_data.area = polyline_data[0].area @classmethod - def use_transform_orientations(cls, value:Union[Vector, Matrix]) -> Union[Vector, Matrix]: + def use_transform_orientations(cls, value: Union[Vector, Matrix]) -> Union[Vector, Matrix]: custom_orientation = bpy.context.scene.transform_orientation_slots[0].custom_orientation if custom_orientation: custom_matrix = custom_orientation.matrix @@ -591,4 +590,3 @@ class Polyline(bonsai.core.tool.Polyline): result = custom_matrix.inverted() @ value return result return value - From a8960957c00b5b9e927bd5109ee0bbbd390f8998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 17 Feb 2025 17:09:13 -0300 Subject: [PATCH 048/476] Fix #6083 --- src/bonsai/bonsai/tool/polyline.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 29b076a274..385a979fe6 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -505,6 +505,10 @@ class Polyline(bonsai.core.tool.Polyline): for point in polyline_points[1:]: # The first can be repeated to form a wall loop if (x, y, z) == (point.x, point.y, point.z): return "Cannot create two points at the same location" + # Avoids duplicating an edge + if len(polyline_points) > 1: + if Vector((x, y, z)) == Vector((polyline_points[-2].x, polyline_points[-2].y, polyline_points[-2].z)): + return # TODO move this limitation to be Wall tool specific. Right now it also affects Measure tool # Avoids creating segments smaller then 0.1. This is a limitation from create_wall_from_2_points length = ( From 693f7c5397f150f27176f5e2a724dbfe60522fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 17 Feb 2025 18:03:25 -0300 Subject: [PATCH 049/476] Fix #6119 --- src/bonsai/bonsai/tool/polyline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 385a979fe6..00fbad7483 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -456,8 +456,9 @@ class Polyline(bonsai.core.tool.Polyline): else: precision = None + value = value if is_area else value / unit_scale return format_distance( - value / unit_scale, + value, precision=precision, hide_units=False, isArea=is_area, From ae76d3253d396e053e12e139e74e16482ea3e157 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 18 Feb 2025 16:27:53 +1100 Subject: [PATCH 050/476] See #5888. Fix bug where loading type thumbnails made undo history not synced with Blender. If a tool.Ifc.Operator updates a prop, the prop update function will only call _after_ the operator finishes (and therefore adds an undo step to the undo stack). If the prop update function then calls another tool.Ifc.Operator, that will result in another "top-level" operator call. This second operator _won't_ get added to Blender's undo history, yet the Bonsai history / IfcOpenShell history will have another undo step added. Yikes! TL;DR don't call tool.Ifc.Operator from a prop update function. --- src/bonsai/bonsai/bim/module/model/product.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 06ae1a4fa0..ddc9eb5fea 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -618,7 +618,7 @@ class AlignProduct(bpy.types.Operator): return results -class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator): +class LoadTypeThumbnails(bpy.types.Operator): bl_idname = "bim.load_type_thumbnails" bl_label = "Load Type Thumbnails" bl_options = {"REGISTER", "UNDO"} @@ -626,9 +626,9 @@ class LoadTypeThumbnails(bpy.types.Operator, tool.Ifc.Operator): limit: bpy.props.IntProperty() offset: bpy.props.IntProperty() - def _execute(self, context): + def execute(self, context): if bpy.app.background: - return + return {"FINISHED"} props = tool.Model.get_model_props() # Only process at most one paginated class at a time. From 645298e9d6a09ce5603a5a7203a606ba19541bad Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 18 Feb 2025 16:30:27 +1100 Subject: [PATCH 051/476] See #5888. Fix crash due to improper modal undo handling. It now considers both FINISH and CANCELLED states. --- src/bonsai/bonsai/bim/ifc.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index f6413ee798..19bea87ca5 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -415,7 +415,8 @@ class IfcStore: ) -> set[str]: bonsai.last_actions.append({"type": "operator", "name": operator.bl_idname}) bpy.context.scene.BIMProperties.is_dirty = True - is_top_level_operator = not bool(IfcStore.current_transaction) + # Modals don't nest, and Blender handles the loop that continuously calls modal() + is_top_level_operator = not bool(IfcStore.current_transaction) or (method == "MODAL") if is_top_level_operator: IfcStore.begin_transaction(operator) @@ -458,7 +459,22 @@ class IfcStore: end_top_level_operator() raise - end_top_level_operator() + if method == "MODAL": + if result == {"FINISHED"}: + end_top_level_operator() + elif result == {"CANCELLED"}: + # Please read the docs: https://docs.blender.org/api/current/bpy.types.Operator.html + # > "when an operator returns {'CANCELLED'}, no undo step will be created". + # This means that if your modal edits IFC data, then the user + # cancels it, Blender's undo history will not be in sync with + # Bonsai / IfcOpenShell's undo history. Instead of hoping for + # Bonsai devs to remember to handle the "cancel" state (i.e. + # detect escape keypress) and return {"FINISHED"}, we instead + # always enforce an undo step. + bpy.ops.ed.undo_push(message=f"Cancel {operator.bl_idname}") + end_top_level_operator() + else: + end_top_level_operator() return result @staticmethod From c79020aeabcfe8e92f9132fc9ee8052dc0eed3f8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 18 Feb 2025 16:31:36 +1100 Subject: [PATCH 052/476] See #5888. Debugging undos is hard, so include the operator name in the event log. --- src/bonsai/bonsai/bim/ifc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 19bea87ca5..1c5d7800b1 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -479,7 +479,7 @@ class IfcStore: @staticmethod def begin_transaction(operator: tool.Ifc.Operator) -> None: - IfcStore.current_transaction = str(uuid.uuid4()) + IfcStore.current_transaction = str(uuid.uuid4()) + operator.__class__.__name__ operator.transaction_key = IfcStore.current_transaction @staticmethod From 1e3ddce5c260484cc9f7a205805ff40bef1a9a18 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 18 Feb 2025 18:39:22 +1100 Subject: [PATCH 053/476] Fix #6174. Accommodate invalid index maps, and also implement an early return for single colours. A single colour index map is a waste but some IFCs have it apparently. --- src/bonsai/bonsai/tool/loader.py | 57 ++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index cb16446c5a..f79f4b4509 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -592,33 +592,42 @@ class Loader(bonsai.core.tool.Loader): opacity = opacity if opacity is not None else 1.0 data_list = [d + (opacity,) for d in data_list] - faces_tex_coord_data = {} - for tex_coord_index, face_remap in zip(texture_map, faces_remap, strict=True): - faces_tex_coord_data[tuple(face_remap)] = (tex_coord_index, face_remap) + if index_map.is_a("IfcIndexedColourMap") and len(index_map.Colours.ColourList) == 1: + # Early return scenario in case there is only one colour + data_colour = data_list[0] + for bface in bm.faces: + for loop in bface.loops: + loop[layer] = data_colour + elif len(texture_map) != len(faces_remap): + print(f"Warning: invalid index map found: {index_map}") + else: + faces_tex_coord_data = {} + for tex_coord_index, face_remap in zip(texture_map, faces_remap, strict=True): + faces_tex_coord_data[tuple(face_remap)] = (tex_coord_index, face_remap) - # Apply attribute to each face - for bface in bm.faces: - face = tuple(loop.vert.index for loop in bface.loops) - # Find the corresponding index in data list by matching ifc faceset with blender face. - data_index = None - if tex_coord_data := faces_tex_coord_data.get(face): - tex_coord_index, face_remap = tex_coord_data - # Subtract 1 as tex_coord_index starts with 1. - if map_type == "UV": - data_index = [tex_coord_index[face_remap.index(i)] - 1 for i in face] + # Apply attribute to each face + for bface in bm.faces: + face = tuple(loop.vert.index for loop in bface.loops) + # Find the corresponding index in data list by matching ifc faceset with blender face. + data_index = None + if tex_coord_data := faces_tex_coord_data.get(face): + tex_coord_index, face_remap = tex_coord_data + # Subtract 1 as tex_coord_index starts with 1. + if map_type == "UV": + data_index = [tex_coord_index[face_remap.index(i)] - 1 for i in face] + else: + data_index = [tex_coord_index - 1 for i in face] else: - data_index = [tex_coord_index - 1 for i in face] - else: - # This face may be part of another representation item - # Or we couldn't match it due to georeferencing. - continue + # This face may be part of another representation item + # Or we couldn't match it due to georeferencing. + continue - # apply uv to each loop - for loop, i in zip(bface.loops, data_index): - if map_type == "UV": - loop[layer].uv = data_list[i] - else: - loop[layer] = data_list[i] + # apply uv to each loop + for loop, i in zip(bface.loops, data_index): + if map_type == "UV": + loop[layer].uv = data_list[i] + else: + loop[layer] = data_list[i] # Finish up, write the bmesh back to the mesh bm.to_mesh(mesh) From 7ca2c043b5ab4427f35e369ce88d84f3c4b8a241 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 18 Feb 2025 20:37:50 +1100 Subject: [PATCH 054/476] Fix #6179. Allow searching for materials in parametric object UI. --- src/bonsai/bonsai/bim/module/model/ui.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index ba9bfeb60e..b0a59a3916 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -20,6 +20,7 @@ import bpy import bonsai.bim import bonsai.tool as tool from bpy.types import Panel, Menu +from bonsai.bim.helper import prop_with_search from bonsai.bim.module.model.data import ( AuthoringData, ArrayData, @@ -449,9 +450,9 @@ class BIM_PT_window(bpy.types.Panel): self.layout.use_property_split = True self.layout.label(text="Material Properties") - self.layout.prop(props, "lining_material") - self.layout.prop(props, "framing_material", text="Panel Material") - self.layout.prop(props, "glazing_material") + prop_with_search(self.layout, props, "lining_material") + prop_with_search(self.layout, props, "framing_material", text="Panel Material") + prop_with_search(self.layout, props, "glazing_material") else: row.operator("bim.enable_editing_window", icon="GREASEPENCIL", text="") row.operator("bim.remove_window", icon="X", text="") @@ -546,10 +547,10 @@ class BIM_PT_door(bpy.types.Panel): self.layout.use_property_split = True self.layout.label(text="Material Properties") - self.layout.prop(props, "lining_material") - self.layout.prop(props, "framing_material", text="Panel Material") + prop_with_search(self.layout, props, "lining_material") + prop_with_search(self.layout, props, "framing_material", text="Panel Material") if props.transom_thickness: - self.layout.prop(props, "glazing_material") + prop_with_search(self.layout, props, "glazing_material") else: row.operator("bim.enable_editing_door", icon="GREASEPENCIL", text="") row.operator("bim.remove_door", icon="X", text="") From 37baa7a57b5896834bc67a70f510ef730f515b04 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 18 Feb 2025 20:45:05 +1100 Subject: [PATCH 055/476] Fix #5845. Allow non GRAPH_VIEW reference representations. Apparently it's a thing in the docs. If anybody can explain to me how Reference is meant to be used (the example in the docs is really ambiguous) please do :) --- .../ifcopenshell/api/geometry/add_representation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py index 0e5940f3d4..13d06e59c8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py @@ -204,6 +204,7 @@ class Usecase: elif self.settings["context"].ContextIdentifier == "Reference": if self.settings["context"].TargetView == "GRAPH_VIEW": return self.create_structural_reference_representation() + return self.create_variable_representation() elif self.settings["context"].ContextIdentifier == "Profile": return self.create_curve3d_representation() elif self.settings["context"].ContextIdentifier == "SurveyPoints": From 83a351b1c75783f5832741aedb6caffec773211a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 18 Feb 2025 13:37:53 +0500 Subject: [PATCH 056/476] black . --- src/bonsai/bonsai/bim/module/search/operator.py | 1 + src/bonsai/scripts/replace_drawing_path.py | 2 +- src/ifc4d/ifc4d/wpattern.py | 2 +- src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py | 2 +- src/ifcopenshell-python/ifcopenshell/util/attribute.py | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index be4f2e1e9e..5083fb2445 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -556,6 +556,7 @@ class ResetObjectColours(Operator): class ToggleFilterSelection(Operator): "Click to select/deselect current selection" + bl_idname = "bim.toggle_filter_selection" bl_label = "Toggle Filter Selection" action: EnumProperty(items=(("SELECT", "Select", ""), ("DESELECT", "Deselect", ""))) diff --git a/src/bonsai/scripts/replace_drawing_path.py b/src/bonsai/scripts/replace_drawing_path.py index 97f00c8986..b874614502 100644 --- a/src/bonsai/scripts/replace_drawing_path.py +++ b/src/bonsai/scripts/replace_drawing_path.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcPatch. If not, see . -""" This script regenerates all drawing paths in case they have been created in a different operating system. +"""This script regenerates all drawing paths in case they have been created in a different operating system. It is useful when an annotation drawing has been created on windows and you want to recreate the drawig in linux. diff --git a/src/ifc4d/ifc4d/wpattern.py b/src/ifc4d/ifc4d/wpattern.py index 72a150aea9..2e434223d5 100644 --- a/src/ifc4d/ifc4d/wpattern.py +++ b/src/ifc4d/ifc4d/wpattern.py @@ -1,7 +1,7 @@ """ This class parses the calendar work pattern and retruns a list The list returned has a key DayOfWeek which takes a value Sunday to Saturday -for each day as a key there is a list of working times with the format +for each day as a key there is a list of working times with the format {"Start": datetime.time, "Finish": datetime.time} """ diff --git a/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py b/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py index 21ef97ac65..eb3fa7ac52 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/occ_utils.py @@ -232,7 +232,7 @@ def serialize_shape(shape): def create_shape_from_serialization( - brep_object: Union[ifcopenshell_wrapper.SerializedElement, ifcopenshell_wrapper.Serialization] + brep_object: Union[ifcopenshell_wrapper.SerializedElement, ifcopenshell_wrapper.Serialization], ) -> Union[shape_tuple, TopoDS.TopoDS_Shape]: brep_data, occ_shape, styles, style_ids = None, None, (), () diff --git a/src/ifcopenshell-python/ifcopenshell/util/attribute.py b/src/ifcopenshell-python/ifcopenshell/util/attribute.py index f29f60ed8b..06cd10fcff 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/attribute.py +++ b/src/ifcopenshell-python/ifcopenshell/util/attribute.py @@ -21,7 +21,7 @@ from typing import Union def get_primitive_type( - attribute_or_data_type: Union[ifcopenshell_wrapper.attribute, ifcopenshell_wrapper.parameter_type] + attribute_or_data_type: Union[ifcopenshell_wrapper.attribute, ifcopenshell_wrapper.parameter_type], ) -> Union[str, tuple[str, list[str]]]: if hasattr(attribute_or_data_type, "type_of_attribute"): data_type = str(attribute_or_data_type.type_of_attribute()) From 17642ca4e93e0177c2b6b6d0a5fdd35f1d82a9e9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 18 Feb 2025 15:18:23 +0500 Subject: [PATCH 057/476] typing --- .../bonsai/bim/module/boundary/operator.py | 3 +- .../bonsai/bim/module/geometry/operator.py | 3 +- .../bonsai/bim/module/profile/operator.py | 8 +-- .../api/attribute/edit_attributes.py | 3 - .../boundary/assign_connection_geometry.py | 56 ++++++++------- .../api/classification/add_classification.py | 30 ++++---- .../api/classification/add_reference.py | 12 ++-- .../api/classification/edit_classification.py | 9 +-- .../api/classification/edit_reference.py | 9 +-- .../classification/remove_classification.py | 13 ++-- .../api/classification/remove_reference.py | 23 +++--- .../api/constraint/add_metric_reference.py | 8 +-- .../api/constraint/assign_constraint.py | 27 +++---- .../api/constraint/edit_metric.py | 9 +-- .../api/constraint/edit_objective.py | 9 +-- .../api/constraint/remove_metric.py | 16 +++-- .../api/constraint/unassign_constraint.py | 32 ++++----- .../ifcopenshell/api/context/edit_context.py | 9 +-- .../ifcopenshell/api/cost/copy_cost_item.py | 25 ++++--- .../ifcopenshell/api/cost/edit_cost_item.py | 9 +-- .../api/cost/edit_cost_item_quantity.py | 9 +-- .../api/cost/edit_cost_schedule.py | 10 +-- .../ifcopenshell/api/cost/edit_cost_value.py | 11 +-- .../api/cost/unassign_cost_item_quantity.py | 34 +++++---- .../api/document/assign_document.py | 31 ++++---- .../api/document/edit_information.py | 9 +-- .../api/document/edit_reference.py | 9 +-- .../api/drawing/edit_text_literal.py | 9 +-- .../api/geometry/add_axis_representation.py | 5 +- .../geometry/add_footprint_representation.py | 11 +-- .../api/geometry/add_mesh_representation.py | 5 +- .../geometry/add_profile_representation.py | 3 + .../api/geometry/add_window_representation.py | 3 + .../ifcopenshell/api/geometry/connect_path.py | 55 +++++--------- .../ifcopenshell/api/group/assign_group.py | 24 +++---- .../ifcopenshell/api/group/edit_group.py | 9 +-- .../ifcopenshell/api/group/unassign_group.py | 13 ++-- .../api/group/update_group_products.py | 15 ++-- .../ifcopenshell/api/layer/edit_layer.py | 6 +- .../ifcopenshell/api/layer/unassign_layer.py | 12 +--- .../ifcopenshell/api/library/edit_library.py | 3 - .../api/library/edit_reference.py | 9 +-- .../api/material/assign_profile.py | 2 + .../api/material/edit_assigned_material.py | 9 +-- .../api/material/edit_constituent.py | 12 +--- .../ifcopenshell/api/material/edit_layer.py | 14 ++-- .../api/material/edit_layer_usage.py | 9 +-- .../ifcopenshell/api/material/edit_profile.py | 24 ++----- .../api/material/edit_profile_usage.py | 45 ++++++------ .../api/material/remove_constituent.py | 1 + .../api/material/remove_list_item.py | 8 +-- .../api/material/reorder_set_item.py | 19 ++--- .../api/material/unassign_material.py | 4 ++ .../ifcopenshell/api/owner/add_application.py | 5 +- .../ifcopenshell/api/owner/add_role.py | 14 ++-- .../ifcopenshell/api/owner/edit_actor.py | 9 +-- .../ifcopenshell/api/owner/edit_address.py | 9 +-- .../api/owner/edit_organisation.py | 9 +-- .../ifcopenshell/api/owner/edit_person.py | 9 +-- .../ifcopenshell/api/owner/edit_role.py | 9 +-- .../api/profile/add_arbitrary_profile.py | 51 ++++++------- .../add_arbitrary_profile_with_voids.py | 71 +++++++++++-------- .../ifcopenshell/api/profile/edit_profile.py | 9 +-- .../ifcopenshell/api/pset/add_pset.py | 50 ++++++------- .../ifcopenshell/api/pset/add_qto.py | 21 ++++-- .../api/pset_template/edit_prop_template.py | 3 - .../api/pset_template/edit_pset_template.py | 9 +-- .../api/resource/add_resource_quantity.py | 13 ++-- .../api/resource/edit_resource.py | 9 +-- .../api/resource/edit_resource_quantity.py | 12 +--- .../api/resource/edit_resource_time.py | 40 +++++------ .../ifcopenshell/api/root/copy_class.py | 4 ++ .../ifcopenshell/api/root/create_entity.py | 5 +- .../ifcopenshell/api/root/reassign_class.py | 3 +- .../api/sequence/add_task_time.py | 9 +-- .../api/sequence/assign_lag_time.py | 22 ++---- .../api/sequence/assign_recurrence_pattern.py | 21 +++--- .../api/sequence/assign_sequence.py | 21 ++---- .../api/sequence/calculate_task_duration.py | 29 ++++---- .../api/sequence/create_baseline.py | 20 +++--- .../api/sequence/duplicate_task.py | 10 ++- .../api/sequence/edit_lag_time.py | 11 +-- .../api/sequence/edit_recurrence_pattern.py | 12 +--- .../api/sequence/edit_sequence.py | 13 ++-- .../ifcopenshell/api/sequence/edit_task.py | 9 +-- .../api/sequence/edit_task_time.py | 70 ++++++++---------- .../api/sequence/edit_work_calendar.py | 9 +-- .../api/sequence/edit_work_plan.py | 9 +-- .../api/sequence/edit_work_schedule.py | 9 +-- .../api/sequence/edit_work_time.py | 13 ++-- .../api/sequence/recalculate_schedule.py | 37 +++++----- .../api/spatial/dereference_structure.py | 12 ++-- .../api/spatial/reference_structure.py | 16 ++--- .../edit_structural_analysis_model.py | 10 +-- .../edit_structural_boundary_condition.py | 9 +-- .../structural/edit_structural_item_axis.py | 14 ++-- .../api/structural/edit_structural_load.py | 9 +-- .../structural/edit_structural_load_case.py | 9 +-- .../api/style/add_surface_style.py | 4 -- .../api/style/assign_material_style.py | 4 ++ .../api/style/edit_presentation_style.py | 9 +-- .../api/style/edit_surface_style.py | 49 +++++++------ .../ifcopenshell/api/style/remove_style.py | 1 - .../api/style/remove_surface_style.py | 2 - .../style/unassign_representation_styles.py | 4 ++ .../ifcopenshell/api/system/connect_port.py | 5 +- .../ifcopenshell/api/system/edit_system.py | 10 +-- .../ifcopenshell/api/system/unassign_port.py | 4 ++ .../ifcopenshell/api/type/assign_type.py | 5 +- .../ifcopenshell/api/unit/add_derived_unit.py | 11 +-- .../ifcopenshell/api/unit/assign_unit.py | 5 +- .../api/unit/edit_derived_unit.py | 9 +-- .../api/unit/edit_monetary_unit.py | 9 +-- .../ifcopenshell/api/unit/edit_named_unit.py | 13 ++-- .../ifcopenshell/api/unit/unassign_unit.py | 19 ++--- src/ifcopenshell-python/ifcopenshell/file.py | 1 + .../ifcopenshell/util/placement.py | 2 - 117 files changed, 683 insertions(+), 1005 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py index e3bc51683c..5d83a88597 100644 --- a/src/bonsai/bonsai/bim/module/boundary/operator.py +++ b/src/bonsai/bonsai/bim/module/boundary/operator.py @@ -25,6 +25,7 @@ import mathutils import numpy as np import multiprocessing import ifcopenshell.api +import ifcopenshell.api.boundary import ifcopenshell.geom import ifcopenshell.util.unit import ifcopenshell.util.shape @@ -416,7 +417,7 @@ class UpdateBoundaryGeometry(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): tool.Boundary.move_origin_to_space_origin(context.active_object) settings = tool.Boundary.get_assign_connection_geometry_settings(context.active_object) - ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings) + ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index de59b7a61c..6a234800bf 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -29,6 +29,7 @@ import ifcopenshell.util.representation import ifcopenshell.util.shape_builder import ifcopenshell.util.unit import ifcopenshell.api +import ifcopenshell.api.boundary import ifcopenshell.api.grid import bonsai.core.geometry import bonsai.core.geometry as core @@ -479,7 +480,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): elif product.is_a("IfcRelSpaceBoundary"): # TODO refactor settings = tool.Boundary.get_assign_connection_geometry_settings(obj) - ifcopenshell.api.run("boundary.assign_connection_geometry", tool.Ifc.get(), **settings) + ifcopenshell.api.boundary.assign_connection_geometry(tool.Ifc.get(), **settings) return if tool.Ifc.is_moved(obj) or tool.Geometry.is_scaled(obj): diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index b0d5073429..1a330917db 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -18,6 +18,7 @@ import bpy import ifcopenshell.api +import ifcopenshell.api.profile import ifcopenshell.util.element import bonsai.bim.helper import bonsai.tool as tool @@ -174,12 +175,12 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator): props.object_to_profile = None if not indices: points = [(0, 0), (0.1, 0), (0.1, 0.1), (0, 0.1), (0, 0)] - profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points) + profile = ifcopenshell.api.profile.add_arbitrary_profile(tool.Ifc.get(), profile=points) else: if "inner_curves" not in indices: points = [(obj.data.vertices[i].co.x, obj.data.vertices[i].co.y) for i in indices["profile"]] points.append(points[0]) - profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points) + profile = ifcopenshell.api.profile.add_arbitrary_profile(tool.Ifc.get(), profile=points) else: outer_points = [(obj.data.vertices[i].co.x, obj.data.vertices[i].co.y) for i in indices["profile"]] outer_points.append(outer_points[0]) @@ -189,8 +190,7 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator): ] for curve in inner_points: curve.append(curve[0]) - profile = ifcopenshell.api.run( - "profile.add_arbitrary_profile_with_voids", + profile = ifcopenshell.api.profile.add_arbitrary_profile_with_voids( tool.Ifc.get(), outer_profile=outer_points, inner_profiles=inner_points, diff --git a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py index 3cbab7f38d..ff0e2b11b7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py +++ b/src/ifcopenshell-python/ifcopenshell/api/attribute/edit_attributes.py @@ -39,11 +39,8 @@ def edit_attributes(file: ifcopenshell.file, product: ifcopenshell.entity_instan :param product: The product you want to edit. This may be any rooted IFC entity. - :type product: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py b/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py index d77776f04a..b9a2630442 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py +++ b/src/ifcopenshell-python/ifcopenshell/api/boundary/assign_connection_geometry.py @@ -17,17 +17,20 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit +import numpy as np +import numpy.typing as npt from typing import Optional +from ifcopenshell.util.shape_builder import SequenceOfVectors, V, ifc_safe_vector_type def assign_connection_geometry( file: ifcopenshell.file, rel_space_boundary: ifcopenshell.entity_instance, - outer_boundary: list[tuple[float, float]], + outer_boundary: SequenceOfVectors, location: tuple[float, float, float], axis: tuple[float, float, float], ref_direction: tuple[float, float, float], - inner_boundaries: Optional[list[list[tuple[float, float]]]] = None, + inner_boundaries: Optional[SequenceOfVectors] = None, unit_scale: Optional[float] = None, ) -> None: """Create and assign a connection geometry to a space boundary relationship @@ -40,35 +43,28 @@ def assign_connection_geometry( :param rel_space_boundary: The space boundary relationship to assign the connection geometry to. - :type rel_space_boundary: ifcopenshell.entity_instance :param outer_boundary: A list of 2D points representing an open polyline. The last point will connect to the first point. Each point is represented by an interable of 2 floats. The coordinates of the points are relative to the positional matrix arguments. - :type outer_boundary: list[tuple[float, float]] :param inner_boundaries: A list of zero or more inner boundaries to use for the plane. Each boundary is represented by an open polyline, as defined by the outer_boundary argument. - :type inner_boundaries: list[list[tuple[float, float]]], optional :param location: The local origin of the connection geometry, defined as an XYZ coordinate relative to the placement of the space that is being bounded. - :type location: tuple[float, float, float] :param axis: The local X axis of the connection geometry, defined as an XYZ vector relative to the placement of the space that is being bounded. - :type axis: tuple[float, float, float] :param ref_direction: The local Z axis of the connection geometry, defined as an XYZ vector relative to the placement of the space that is being bounded. The Y vector is automatically derived using the right hand rule. - :type ref_direction: tuple[float, float, float] :param unit_scale: The unit scale as calculated by ifcopenshell.util.unit.calculate_unit_scale. If not provided, it will be automatically calculated for you. :type unit_scale: float, optional :return: None - :rtype: None Example: @@ -83,19 +79,26 @@ def assign_connection_geometry( usecase = Usecase() usecase.file = file usecase.rel_space_boundary = rel_space_boundary - usecase.outer_boundary = outer_boundary - usecase.inner_boundaries = inner_boundaries or () - usecase.location = location - usecase.axis = axis - usecase.ref_direction = ref_direction - usecase.unit_scale = unit_scale + usecase.outer_boundary = V(outer_boundary) + usecase.inner_boundaries = V(inner_boundaries or []) + usecase.location = V(location) + usecase.axis = V(axis) + usecase.ref_direction = V(ref_direction) + usecase.unit_scale = unit_scale if unit_scale is not None else ifcopenshell.util.unit.calculate_unit_scale(file) return usecase.execute() class Usecase: + file: ifcopenshell.file + rel_space_boundary: ifcopenshell.entity_instance + outer_boundary: npt.NDArray + inner_boundaries: npt.NDArray + location: npt.NDArray + axis: npt.NDArray + ref_direction: npt.NDArray + unit_scale: float + def execute(self): - if self.unit_scale is None: - self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) outer_boundary = self.create_polyline(self.outer_boundary) inner_boundaries = tuple(self.create_polyline(boundary) for boundary in self.inner_boundaries) plane = self.create_plane(self.location, self.axis, self.ref_direction) @@ -103,18 +106,23 @@ class Usecase: connection_geometry = self.file.createIfcConnectionSurfaceGeometry(curve_bounded_plane) self.rel_space_boundary.ConnectionGeometry = connection_geometry - def create_point(self, point): - return self.file.createIfcCartesianPoint(point / self.unit_scale) + def create_point(self, point: npt.NDArray) -> ifcopenshell.entity_instance: + return self.file.create_enitty("IfcCartesianPoint", ifc_safe_vector_type(point / self.unit_scale)) - def close_polyline(self, points): + def close_polyline( + self, points: tuple[ifcopenshell.entity_instance, ...] + ) -> tuple[ifcopenshell.entity_instance, ...]: return points + (points[0],) - def create_polyline(self, points): - if points[0] == points[-1]: + def create_polyline(self, points: npt.NDArray) -> ifcopenshell.entity_instance: + if np.allclose(points[0], points[-1]): points = points[0 : len(points) - 1] - return self.file.createIfcPolyline(self.close_polyline(tuple(self.create_point(point) for point in points))) + ifc_points = tuple(self.create_point(point) for point in points) + return self.file.createIfcPolyline(self.close_polyline(ifc_points)) - def create_plane(self, location, axis, ref_direction): + def create_plane( + self, location: npt.NDArray, axis: npt.NDArray, ref_direction: npt.NDArray + ) -> ifcopenshell.entity_instance: return self.file.createIfcPlane( self.file.createIfcAxis2Placement3D( self.create_point(location), diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py index af886a0014..2932a5bb93 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_classification.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.guid import ifcopenshell.util.schema import ifcopenshell.util.date -from typing import Union +from typing import Union, Any def add_classification( @@ -61,9 +61,7 @@ def add_classification( classification library. The latter approach is preferred if you are using a commonly known system such as Uniclass, as this will ensure all metadata is added correctly. - :type classification: str,ifcopenshell.entity_instance :return: The added IfcClassification element - :rtype: ifcopenshell.entity_instance Example: @@ -81,28 +79,28 @@ def add_classification( """ usecase = Usecase() usecase.file = file - usecase.settings = { - "classification": classification, - } - return usecase.execute() + return usecase.execute(classification) class Usecase: - def execute(self): - if isinstance(self.settings["classification"], str): - classification = self.file.createIfcClassification(Name=self.settings["classification"]) + file: ifcopenshell.file + + def execute(self, classification: Union[str, ifcopenshell.entity_instance]) -> ifcopenshell.entity_instance: + self.classification = classification + if isinstance(self.classification, str): + classification = self.file.create_entity("IfcClassification", Name=self.classification) self.relate_to_project(classification) return classification return self.add_from_library() - def add_from_library(self): + def add_from_library(self) -> ifcopenshell.entity_instance: edition_date = None - if self.settings["classification"].EditionDate: - edition_date = ifcopenshell.util.date.ifc2datetime(self.settings["classification"].EditionDate) - self.settings["classification"].EditionDate = None + if self.classification.EditionDate: + edition_date = ifcopenshell.util.date.ifc2datetime(self.classification.EditionDate) + self.classification.EditionDate = None migrator = ifcopenshell.util.schema.Migrator() - result = migrator.migrate(self.settings["classification"], self.file) + result = migrator.migrate(self.classification, self.file) # TODO: should auto date migration be part of the migrator? if self.file.schema == "IFC2X3" and edition_date: @@ -118,7 +116,7 @@ class Usecase: return result - def relate_to_project(self, classification): + def relate_to_project(self, classification: ifcopenshell.entity_instance) -> None: self.file.create_entity( "IfcRelAssociatesClassification", GlobalId=ifcopenshell.guid.new(), diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py index 1e0ae329bf..817d15b5cf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/add_reference.py @@ -21,7 +21,7 @@ import ifcopenshell.api.owner import ifcopenshell.guid import ifcopenshell.util.element import ifcopenshell.util.schema -from typing import Optional, Union +from typing import Optional, Union, Any def add_reference( @@ -65,23 +65,18 @@ def add_reference( :param product: The list of IFC objects, properties, or resources you want to associate the classification reference to. - :type product: list[ifcopenshell.entity_instance] :param reference: The classification reference entity taken from an IFC classification library. If you supply this parameter, you will use option 2. - :type reference: ifcopenshell.entity_instance, optional :param identification: If you choose option 1 and do not specify a reference, you may manually specify an identification code. The code is typically a short identifier and may have punctuation to separate the levels of hierarchy in the classificaion (e.g. Pr_12_23_34). - :type identification: str, optional :param name: If you choose option 1 and do not specify a reference, you may manually specify a name. The name is typically human readable. - :type name: str, optional :param classification: The IfcClassification entity in your IFC model (not the library, if you are doing option 2) that the reference is part of. - :type classification: ifcopenshell.entity_instance :param is_lightweight: If you are doing option 2, choose whether or not to only add that particular reference (lighweight) or also add all of its parent references in the classification hierarchy (not @@ -91,13 +86,11 @@ def add_reference( references merely help describe the "tree" of classifications, but is generally unnecessary. Using lightweight classifications are recommended and is the default. - :type is_lightweight: bool, optional :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements. :return: The newly added IfcClassificationReference or `None` if `products` was empty list. - :rtype: Union[ifcopenshell.entity_instance, None] Example: @@ -136,6 +129,9 @@ def add_reference( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if not self.settings["products"]: return diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py index d6c8ef8bc4..b2091c0eca 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_classification.py @@ -28,11 +28,8 @@ def edit_classification( IfcClassification, consult the IFC documentation. :param classification: The IfcClassification entity you want to edit - :type classification: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -43,7 +40,5 @@ def edit_classification( ifcopenshell.api.classification.edit_classification(model, classification=classification, attributes={"Name": "Foo"}) """ - settings = {"classification": classification, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["classification"], name, value) + for name, value in attributes.items(): + setattr(classification, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py index 80d978e7d3..f5a85d40f9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/edit_reference.py @@ -28,11 +28,8 @@ def edit_reference( IfcClassificationReference, consult the IFC documentation. :param reference: The IfcClassificationReference entity you want to edit - :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -43,7 +40,5 @@ def edit_reference( ifcopenshell.api.classification.edit_reference(model, reference=reference, attributes={"Name": "Foo"}) """ - settings = {"reference": reference, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["reference"], name, value) + for name, value in attributes.items(): + setattr(reference, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py index 90e6abbbe7..38382eda91 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_classification.py @@ -28,9 +28,7 @@ def remove_classification(file: ifcopenshell.file, classification: ifcopenshell. removed from a project. :param classification: The IfcClassification entity you want to remove - :type classification: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -42,16 +40,17 @@ def remove_classification(file: ifcopenshell.file, classification: ifcopenshell. """ usecase = Usecase() usecase.file = file - usecase.settings = {"classification": classification} - return usecase.execute() + return usecase.execute(classification) class Usecase: - def execute(self): - references = self.get_references(self.settings["classification"]) + file: ifcopenshell.file + + def execute(self, classification: ifcopenshell.entity_instance) -> None: + references = self.get_references(classification) for reference in references: self.file.remove(reference) - self.file.remove(self.settings["classification"]) + self.file.remove(classification) for rel in self.file.by_type("IfcRelAssociatesClassification"): if not rel.RelatingClassification: history = rel.OwnerHistory diff --git a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py index 0bdd347bfa..55dfd11b08 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/classification/remove_reference.py @@ -33,15 +33,12 @@ def remove_reference( :param reference: The IfcClassificationReference entity of the relationship you want to remove. - :type reference: ifcopenshell.entity_instance :param product: The list fo object entities of the relationship you want to remove. - :type product: list[ifcopenshell.entity_instance] :raises TypeError: If file is IFC2X3 and `products` has non-IfcRoot elements. :return: None - :rtype: None Example: @@ -56,20 +53,18 @@ def remove_reference( ifcopenshell.api.classification.remove_reference(model, reference=reference, products=[wall_type]) """ - settings = {"reference": reference, "products": products} - is_ifc2x3 = file.schema == "IFC2X3" - products = set(settings["products"]) - referenced = ifcopenshell.util.element.get_referenced_elements(settings["reference"]) - products -= products.difference(referenced) + products_set = set(products) + referenced = ifcopenshell.util.element.get_referenced_elements(reference) + products_set -= products_set.difference(referenced) # all products are already unassigned from a reference - if not products: + if not products_set: return rooted_products: set[ifcopenshell.entity_instance] = set() non_rooted_products: set[ifcopenshell.entity_instance] = set() - for product in settings["products"]: + for product in products: if product.is_a("IfcRoot"): rooted_products.add(product) else: @@ -86,7 +81,7 @@ def remove_reference( reference_rels = { rel for rel in reference_rels - if rel.is_a("IfcRelAssociatesClassification") and rel.RelatingClassification == settings["reference"] + if rel.is_a("IfcRelAssociatesClassification") and rel.RelatingClassification == reference } for rel in reference_rels: @@ -108,7 +103,7 @@ def remove_reference( rels = getattr(product, "HasExternalReference", []) reference_rels.update(rels) - reference_rels = {rel for rel in reference_rels if rel.RelatingReference == settings["reference"]} + reference_rels = {rel for rel in reference_rels if rel.RelatingReference == reference} for rel in reference_rels: related_objects = set(rel.RelatedResourceObjects) - non_rooted_products if related_objects: @@ -117,6 +112,6 @@ def remove_reference( file.remove(rel) # TODO: we only handle lightweight classifications here - referenced_elements = ifcopenshell.util.element.get_referenced_elements(settings["reference"]) + referenced_elements = ifcopenshell.util.element.get_referenced_elements(reference) if not referenced_elements: - file.remove(settings["reference"]) + file.remove(reference) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py index fdcdf50f6f..63ecfeb3c0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric_reference.py @@ -26,16 +26,14 @@ def add_metric_reference( Adds a chain of references to a metric. The reference path is a string of the form "attribute.attribute.attribute" Used to reference a value of an attribute of an instance through a metric objective entity. """ - settings = {"metric": metric, "reference_path": reference_path} - references_created = [] - if settings["reference_path"]: - attributes = settings["reference_path"].split(".") + if reference_path: + attributes = reference_path.split(".") for i in range(len(attributes)): if i == 0: reference = file.create_entity("IfcReference") reference.AttributeIdentifier = attributes[i] - settings["metric"].ReferencePath = reference + metric.ReferencePath = reference references_created.append(reference) else: reference = file.create_entity("IfcReference") diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py index 74bf3d9a04..f4daad0abb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/assign_constraint.py @@ -39,36 +39,29 @@ def assign_constraint( :param products: The list of products the constraint applies to. This is anything which can have properties or quantities. - :type products: list[ifcopenshell.entity_instance] :param constraint: The IfcObjective constraint - :type constraint: ifcopenshell.entity_instance :return: The new or updated IfcRelAssociatesConstraint relationship or `None` if `products` was an empty list. - :rtype: ifcopenshell.entity_instance """ usecase = Usecase() usecase.file = file - usecase.settings = { - "products": products, - "constraint": constraint, - } - return usecase.execute() + return usecase.execute(products, constraint) class Usecase: - def execute(self): - products = set(self.settings["products"]) + file: ifcopenshell.file + + def execute(self, products: list[ifcopenshell.entity_instance], constraint: ifcopenshell.entity_instance): if not products: return + products_set = set(products) - self.constraint = self.settings["constraint"] - - rels = self.get_constraint_rels() + rels = self.get_constraint_rels(constraint) related_objects = set() for rel in rels: related_objects.update(rel.RelatedObjects) - products_to_assign = products - related_objects + products_to_assign = products_set - related_objects if not products_to_assign: return rels[0] @@ -85,14 +78,14 @@ class Usecase: **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(self.file), - "RelatingConstraint": self.constraint, + "RelatingConstraint": constraint, "RelatedObjects": list(products_to_assign), } ) - def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]: + def get_constraint_rels(self, constraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: rels = [] - for rel in self.file.get_inverse(self.constraint): + for rel in self.file.get_inverse(constraint): if rel.is_a("IfcRelAssociatesConstraint"): rels.append(rel) return rels diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py index 89a0cbef7d..3967846759 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_metric.py @@ -26,11 +26,8 @@ def edit_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance, a IfcMetric, consult the IFC documentation. :param metric: The IfcMetric you want to edit. - :type metric: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance, a ifcopenshell.api.constraint.edit_metric(model, metric=metric, attributes={"ConstraintGrade": "HARD"}) """ - settings = {"metric": metric, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["metric"], name, value) + for name, value in attributes.items(): + setattr(metric, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py index ce4e76ba9e..3cd75356a7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/edit_objective.py @@ -28,11 +28,8 @@ def edit_objective( IfcObjective, consult the IFC documentation. :param objective: The IfcObjective you want to edit. - :type objective: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_objective( ifcopenshell.api.constraint.edit_objective(model, objective=objective, attributes={"ConstraintGrade": "HARD"}) """ - settings = {"objective": objective, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["objective"], name, value) + for name, value in attributes.items(): + setattr(objective, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py index f700a2c150..76668f714d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_metric.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.util.element def remove_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance) -> None: @@ -41,17 +42,18 @@ def remove_metric(file: ifcopenshell.file, metric: ifcopenshell.entity_instance) """ usecase = Usecase() usecase.file = file - usecase.settings = {"metric": metric} - return usecase.execute() + return usecase.execute(metric) class Usecase: - def execute(self): - if self.settings["metric"].ReferencePath: - reference = self.settings["metric"].ReferencePath + file: ifcopenshell.file + + def execute(self, metric: ifcopenshell.entity_instance) -> None: + if metric.ReferencePath: + reference = metric.ReferencePath self.delete_reference(reference) - self.file.remove(self.settings["metric"]) + self.file.remove(metric) for rel in self.file.by_type("IfcRelAssociatesConstraint"): if not rel.RelatingConstraint: history = rel.OwnerHistory @@ -62,7 +64,7 @@ class Usecase: if not resource_rel.RelatingConstraint: self.file.remove(resource_rel) - def delete_reference(self, reference): + def delete_reference(self, reference: ifcopenshell.entity_instance) -> None: if reference.InnerReference: self.delete_reference(reference.InnerReference) self.file.remove(reference) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py index cff9716809..d9e84cc826 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/unassign_constraint.py @@ -32,41 +32,35 @@ def unassign_constraint( other products. :param products: The list of products the constraint applies to. - :type products: list[ifcopenshell.entity_instance] :param constraint: The IfcObjective constraint - :type constraint: ifcopenshell.entity_instance :return: None - :rtype: None """ usecase = Usecase() usecase.file = file - usecase.settings = { - "products": products, - "constraint": constraint, - } - return usecase.execute() + return usecase.execute(products, constraint) class Usecase: - def execute(self): - products = set(self.settings["products"]) - if not products: - return + file: ifcopenshell.file - self.constraint = self.settings["constraint"] - rels = self.get_constraint_rels() + def execute(self, products_: list[ifcopenshell.entity_instance], constraint: ifcopenshell.entity_instance): + if not products_: + return + products_set = set(products_) + + rels = self.get_constraint_rels(constraint) related_objects = set() for rel in rels: related_objects.update(rel.RelatedObjects) - if not related_objects.intersection(products): + if not related_objects.intersection(products_set): return for rel in rels: related_objects = set(rel.RelatedObjects) - if not related_objects.intersection(products): + if not related_objects.intersection(products_set): continue - related_objects -= products + related_objects -= products_set if related_objects: rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(self.file, **{"element": rel}) @@ -77,9 +71,9 @@ class Usecase: if history: ifcopenshell.util.element.remove_deep2(self.file, history) - def get_constraint_rels(self) -> list[ifcopenshell.entity_instance]: + def get_constraint_rels(self, cosntraint: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: rels = [] - for rel in self.file.get_inverse(self.constraint): + for rel in self.file.get_inverse(cosntraint): if rel.is_a("IfcRelAssociatesConstraint"): rels.append(rel) return rels diff --git a/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py b/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py index ba52e83a9e..1a7d567c58 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py +++ b/src/ifcopenshell-python/ifcopenshell/api/context/edit_context.py @@ -27,11 +27,8 @@ def edit_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance, IfcGeometricRepresentationContext, consult the IFC documentation. :param context: The IfcGeometricRepresentationContext entity you want to edit - :type context: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -47,7 +44,5 @@ def edit_context(file: ifcopenshell.file, context: ifcopenshell.entity_instance, ifcopenshell.api.context.edit_context(model, context=body, attributes={"ContextIdentifier": "Body"}) """ - settings = {"context": context, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["context"], name, value) + for name, value in attributes.items(): + setattr(context, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py index ac6b563960..fcf298ff7c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item.py @@ -25,8 +25,6 @@ from typing import Union def copy_cost_item( file: ifcopenshell.file, cost_item: ifcopenshell.entity_instance ) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: - # TODO: currently it never returns list of duplicated cost items - # though it is stated in the docs """Copies all cost items and related relationships The following relationships are also duplicated: @@ -36,9 +34,7 @@ def copy_cost_item( * The copy will have duplicated nested cost items :param cost_item: The cost item to be duplicated - :type cost_item: ifcopenshell.entity_instance :return: The duplicated cost item or the list of duplicated cost items if the latter has children - :rtype: ifcopenshell.entity_instance or list[ifcopenshell.entity_instance] Example: .. code:: python @@ -53,22 +49,29 @@ def copy_cost_item( """ usecase = Usecase() usecase.file = file - usecase.settings = {"cost_item": cost_item} - return usecase.execute() + return usecase.execute(cost_item) class Usecase: - def execute(self): - self.new_cost_items = [] - return self.duplicate_cost_item(self.settings["cost_item"]) + file: ifcopenshell.file + new_cost_items: list[ifcopenshell.entity_instance] - def duplicate_cost_item(self, cost_item): + def execute( + self, cost_item: ifcopenshell.entity_instance + ) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: + self.new_cost_items = [] + self.duplicate_cost_item(cost_item) + return self.new_cost_items[0] if len(self.new_cost_items) == 1 else self.new_cost_items + + def duplicate_cost_item(self, cost_item: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: new_cost_item = ifcopenshell.util.element.copy_deep(self.file, cost_item) self.new_cost_items.append(new_cost_item) self.copy_indirect_attributes(cost_item, new_cost_item) return new_cost_item - def copy_indirect_attributes(self, from_element, to_element): + def copy_indirect_attributes( + self, from_element: ifcopenshell.entity_instance, to_element: ifcopenshell.entity_instance + ) -> None: for inverse in self.file.get_inverse(from_element): if inverse.is_a("IfcRelDefinesByProperties"): inverse = ifcopenshell.util.element.copy(self.file, inverse) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py index 2417c9ac65..813e306bc8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item.py @@ -28,11 +28,8 @@ def edit_cost_item( IfcCostItem, consult the IFC documentation. :param cost_item: The IfcCostItem entity you want to edit - :type cost_item: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_cost_item( item = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=schedule) ifcopenshell.api.cost.edit_cost_item(model, cost_item=item, attributes={"Name": "Foo"}) """ - settings = {"cost_item": cost_item, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["cost_item"], name, value) + for name, value in attributes.items(): + setattr(cost_item, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py index cb265318ac..697d028a37 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_item_quantity.py @@ -28,11 +28,8 @@ def edit_cost_item_quantity( IfcPhysicalQuantity, consult the IFC documentation. :param physical_quantity: The IfcPhysicalQuantity entity you want to edit - :type physical_quantity: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -50,7 +47,5 @@ def edit_cost_item_quantity( ifcopenshell.api.cost.edit_cost_item_quantity(model, physical_quantity=quantity, "attributes": {"VolumeValue": 3.0}) """ - settings = {"physical_quantity": physical_quantity, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["physical_quantity"], name, value) + for name, value in attributes.items(): + setattr(physical_quantity, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py index 1c6f42b123..d55e93330e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_schedule.py @@ -28,11 +28,8 @@ def edit_cost_schedule( IfcCostSchedule, consult the IFC documentation. :param cost_schedule: The IfcCostSchedule entity you want to edit - :type cost_schedule: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,8 +39,5 @@ def edit_cost_schedule( ifcopenshell.api.cost.edit_cost_schedule(model, cost_schedule=schedule, attributes={"Name": "Foo"}) """ - - settings = {"cost_schedule": cost_schedule, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["cost_schedule"], name, value) + for name, value in attributes.items(): + setattr(cost_schedule, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py index f3e4e01acd..5812cbcf75 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py @@ -31,11 +31,8 @@ def edit_cost_value( IfcCostValue, consult the IFC documentation. :param cost_value: The IfcCostValue entity you want to edit - :type cost_value: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -49,14 +46,12 @@ def edit_cost_value( ifcopenshell.api.cost.edit_cost_value(model, cost_value=value, attributes={"AppliedValue": 42.0}) """ - settings = {"cost_value": cost_value, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if name == "AppliedValue" and value is not None: # TODO: support all applied value select types value = file.createIfcMonetaryMeasure(value) elif name == "UnitBasis": - old_unit_basis = settings["cost_value"].UnitBasis + old_unit_basis = cost_value.UnitBasis if value: value_component = file.create_entity( ifcopenshell.util.unit.get_unit_measure_class(value["UnitComponent"].UnitType), @@ -65,4 +60,4 @@ def edit_cost_value( value = file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"]) if old_unit_basis and len(file.get_inverse(old_unit_basis)) == 0: ifcopenshell.util.element.remove_deep(file, old_unit_basis) - setattr(settings["cost_value"], name, value) + setattr(cost_value, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py index bb815647fd..de180d65ef 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/unassign_cost_item_quantity.py @@ -30,12 +30,9 @@ def unassign_cost_item_quantity( have any impact on the cost item. :param cost_item: The IfcCostItem to remove quantities from - :type cost_item: ifcopenshell.entity_instance :param products: A list of IfcProducts that may have parametrically connected quantities to the cost item - :type products: list[ifcopenshell.entity_instance] :return: None - :rtype: None Example: @@ -69,38 +66,39 @@ def unassign_cost_item_quantity( """ usecase = Usecase() usecase.file = file - usecase.settings = {"cost_item": cost_item, "products": products or []} - return usecase.execute() + return usecase.execute(cost_item, products or []) class Usecase: - def execute(self): - self.quantities = set(self.settings["cost_item"].CostQuantities or []) - for quantity in self.settings["cost_item"].CostQuantities or []: + file: ifcopenshell.file + + def execute(self, cost_item: ifcopenshell.entity_instance, products: list[ifcopenshell.entity_instance]) -> None: + quantities = set(cost_item.CostQuantities or []) + for quantity in cost_item.CostQuantities or []: for inverse in self.file.get_inverse(quantity): if not inverse.is_a("IfcElementQuantity"): continue for rel in inverse.DefinesOccurrence or []: for related_object in rel.RelatedObjects: - if related_object in self.settings["products"]: - self.quantities.remove(quantity) - self.settings["cost_item"].CostQuantities = list(self.quantities) - for product in self.settings["products"]: + if related_object in products: + quantities.remove(quantity) + cost_item.CostQuantities = list(quantities) + for product in products: ifcopenshell.api.control.unassign_control( self.file, related_object=product, - relating_control=self.settings["cost_item"], + relating_control=cost_item, ) - self.update_cost_item_count() + self.update_cost_item_count(cost_item) - def update_cost_item_count(self): + def update_cost_item_count(self, cost_item: ifcopenshell.entity_instance) -> None: # This is a bold assumption # https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564 - if len(self.settings["cost_item"].CostQuantities) == 1: - quantity = self.settings["cost_item"].CostQuantities[0] + if len(cost_item.CostQuantities) == 1: + quantity = cost_item.CostQuantities[0] if quantity.is_a("IfcQuantityCount"): count = 0 - for rel in self.settings["cost_item"].Controls: + for rel in cost_item.Controls: count += len(rel.RelatedObjects) if count: quantity[3] = count diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py index 5627cc1c84..a86a178e86 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/assign_document.py @@ -41,15 +41,12 @@ def assign_document( :param product: The list of objects to associate the document to. This could be almost any sensible object in IFC. - :type product: list[ifcopenshell.entity_instance] :param document: The IfcDocumentReference to associate to, or alternatively an IfcDocumentInformation, though this is not recommended. - :type document: ifcopenshell.entity_instance :return: The IfcRelAssociatesDocument relationship or `None` if `products` was an empty list or all products were already assigned to the `document`. - :rtype: ifcopenshell.entity_instance Example: @@ -65,43 +62,41 @@ def assign_document( # Let's imagine storey represents an IfcBuildingStorey for the ground floor ifcopenshell.api.document.assign_document(model, products=[storey], document=reference) """ - settings = { - "products": products, - "document": document, - } # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? # NOTE: reuses code from `library.assign_reference` - referenced_elements = ifcopenshell.util.element.get_referenced_elements(settings["document"]) - products: set[ifcopenshell.entity_instance] = set(settings["products"]) - products = products - referenced_elements + referenced_elements = ifcopenshell.util.element.get_referenced_elements(document) + products_set: set[ifcopenshell.entity_instance] = set(products) + products_set = products_set - referenced_elements - if not products: + if not products_set: return if file.schema == "IFC2X3": rel = next( - (r for r in file.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == settings["document"]), + (r for r in file.by_type("IfcRelAssociatesDocument") if r.RelatingDocument == document), None, ) else: - ifc_class = settings["document"].is_a() + ifc_class = document.is_a() if ifc_class == "IfcDocumentReference": - rel = next(iter(settings["document"].DocumentRefForObjects), None) + rel = next(iter(document.DocumentRefForObjects), None) elif ifc_class == "IfcDocumentInformation": - rel = next(iter(settings["document"].DocumentInfoForObjects), None) + rel = next(iter(document.DocumentInfoForObjects), None) + else: + assert False, f"Unexpected document type: {ifc_class}" if not rel: return file.create_entity( "IfcRelAssociatesDocument", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.owner.create_owner_history(file), - RelatedObjects=list(products), - RelatingDocument=settings["document"], + RelatedObjects=list(products_set), + RelatingDocument=document, ) - related_objects = set(rel.RelatedObjects) | products + related_objects = set(rel.RelatedObjects) | products_set rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(file, element=rel) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py b/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py index 09d697f7db..56c1d40df4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/edit_information.py @@ -30,11 +30,8 @@ def edit_information( IfcDocumentInformation, consult the IFC documentation. :param reference: The IfcDocumentInformation entity you want to edit - :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -46,7 +43,5 @@ def edit_information( attributes={"Identification": "A-GA-6100", "Name": "Overall Plan", "Location": "A-GA-6100 - Overall Plan.pdf"}) """ - settings = {"information": information, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["information"], name, value) + for name, value in attributes.items(): + setattr(information, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py b/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py index 5a00827305..51ad195bf3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/edit_reference.py @@ -30,11 +30,8 @@ def edit_reference( IfcDocumentReference, consult the IFC documentation. :param reference: The IfcDocumentReference entity you want to edit - :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -49,7 +46,5 @@ def edit_reference( ifcopenshell.api.document.edit_reference(model, reference=reference, attributes={"Identification": "2.1.15"}) """ - settings = {"reference": reference, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["reference"], name, value) + for name, value in attributes.items(): + setattr(reference, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py b/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py index 1f7356e989..c083960e59 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py +++ b/src/ifcopenshell-python/ifcopenshell/api/drawing/edit_text_literal.py @@ -28,11 +28,8 @@ def edit_text_literal( IfcTextLiteral, consult the IFC documentation. :param reference: The IfcTextLiteral entity you want to edit - :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_text_literal( ifcopenshell.api.drawing.edit_text_literal(model, text_literal=text, attributes={"Literal": "MY ANNOTATION"}) """ - settings = {"text_literal": text_literal, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["text_literal"], name, value) + for name, value in attributes.items(): + setattr(text_literal, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py index 1883374b97..174c475f92 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py @@ -17,7 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit -from typing import Union +from typing import Union, Any COORD = Union[tuple[float, float], tuple[float, float, float]] @@ -82,6 +82,9 @@ def add_axis_representation( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) is_2d = len(self.settings["axis"][0]) == 2 diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py index 6b48845e4e..9d5fe9f20b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_footprint_representation.py @@ -26,14 +26,9 @@ def add_footprint_representation( # A list of IFC curves to include in the curve set curves: list[ifcopenshell.entity_instance], ) -> ifcopenshell.entity_instance: - settings = { - "context": context, - "curves": curves, - } - return file.createIfcShapeRepresentation( - settings["context"], - settings["context"].ContextIdentifier, + context, + context.ContextIdentifier, "GeometricCurveSet", - [file.createIfcGeometricCurveSet(settings["curves"])], + [file.createIfcGeometricCurveSet(curves)], ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py index efe6aa5c89..940315621a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_mesh_representation.py @@ -17,7 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit -from typing import Optional +from typing import Optional, Any COORD_3D = tuple[float, float, float] @@ -60,6 +60,9 @@ def add_mesh_representation( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if self.settings["unit_scale"] is None: self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py index 17cdec58c8..77bb515996 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py @@ -52,6 +52,9 @@ def add_profile_representation( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]] diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py index 22fb99d583..fdc457367b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py @@ -426,6 +426,9 @@ def add_window_representation( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): builder = ShapeBuilder(self.file) np_X, np_Y, np_Z = 0, 1, 2 diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py index 571c5212ba..81fa276a2c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_path.py @@ -31,53 +31,33 @@ def connect_path( related_connection: str = "NOTDEFINED", description: Optional[str] = None, ) -> ifcopenshell.entity_instance: - settings = { - "relating_element": relating_element, - "related_element": related_element, - "relating_connection": relating_connection, - "related_connection": related_connection, - "description": description, - } - - incompatible_connections = [] - for rel in settings["relating_element"].ConnectedTo: + incompatible_connections: list[ifcopenshell.entity_instance] = [] + for rel in relating_element.ConnectedTo: if not rel.is_a("IfcRelConnectsPathElements"): continue - if rel.RelatedElement == settings["related_element"]: + if rel.RelatedElement == related_element: incompatible_connections.append(rel) - elif ( - rel.RelatingConnectionType in ["ATSTART", "ATEND"] - and rel.RelatingConnectionType == settings["relating_connection"] - ): + elif rel.RelatingConnectionType in ["ATSTART", "ATEND"] and rel.RelatingConnectionType == relating_connection: incompatible_connections.append(rel) - for rel in settings["relating_element"].ConnectedFrom: + for rel in relating_element.ConnectedFrom: if not rel.is_a("IfcRelConnectsPathElements"): continue - if ( - rel.RelatedConnectionType in ["ATSTART", "ATEND"] - and rel.RelatedConnectionType == settings["relating_connection"] - ): + if rel.RelatedConnectionType in ["ATSTART", "ATEND"] and rel.RelatedConnectionType == relating_connection: incompatible_connections.append(rel) - for rel in settings["related_element"].ConnectedFrom: + for rel in related_element.ConnectedFrom: if not rel.is_a("IfcRelConnectsPathElements"): continue - if ( - rel.RelatedConnectionType in ["ATSTART", "ATEND"] - and rel.RelatedConnectionType == settings["related_connection"] - ): + if rel.RelatedConnectionType in ["ATSTART", "ATEND"] and rel.RelatedConnectionType == related_connection: incompatible_connections.append(rel) - for rel in settings["related_element"].ConnectedTo: + for rel in related_element.ConnectedTo: if not rel.is_a("IfcRelConnectsPathElements"): continue - if rel.RelatedElement == settings["relating_element"]: + if rel.RelatedElement == relating_element: incompatible_connections.append(rel) - elif ( - rel.RelatingConnectionType in ["ATSTART", "ATEND"] - and rel.RelatingConnectionType == settings["related_connection"] - ): + elif rel.RelatingConnectionType in ["ATSTART", "ATEND"] and rel.RelatingConnectionType == related_connection: incompatible_connections.append(rel) if incompatible_connections: @@ -87,14 +67,15 @@ def connect_path( if history: ifcopenshell.util.element.remove_deep2(file, history) - return file.createIfcRelConnectsPathElements( + return file.create_entity( + "IfcRelConnectsPathElements", ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.owner.create_owner_history(file), - Description=settings["description"], - RelatingElement=settings["relating_element"], - RelatedElement=settings["related_element"], - RelatingConnectionType=settings["relating_connection"], - RelatedConnectionType=settings["related_connection"], + Description=description, + RelatingElement=relating_element, + RelatedElement=related_element, + RelatingConnectionType=relating_connection, + RelatedConnectionType=related_connection, RelatingPriorities=[], RelatedPriorities=[], ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py index 64a4f571dc..4972058fb4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py @@ -46,29 +46,25 @@ def assign_group( ifcopenshell.api.group.assign_group(model, products=model.by_type("IfcFurniture"), group=group) """ - settings = { - "products": products, - "group": group, - } - - if not settings["products"]: + if not products: return - if not settings["group"].IsGroupedBy: + is_grouped_by: tuple[ifcopenshell.entity_instance, ...] + if not (is_grouped_by := group.IsGroupedBy): return file.create_entity( "IfcRelAssignsToGroup", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": settings["products"], - "RelatingGroup": settings["group"], - } + "RelatedObjects": products, + "RelatingGroup": group, + }, ) - rel = settings["group"].IsGroupedBy[0] + rel = is_grouped_by[0] related_objects = set(rel.RelatedObjects) or set() - products = set(settings["products"]) - if products.issubset(related_objects): + products_set = set(products) + if products_set.issubset(related_objects): return rel - rel.RelatedObjects = list(related_objects | products) + rel.RelatedObjects = list(related_objects | products_set) ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py index a404ef0f88..5a7a2b2018 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/edit_group.py @@ -26,11 +26,8 @@ def edit_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance, att IfcGroup, consult the IFC documentation. :param group: The IfcGroup entity you want to edit - :type group: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -40,7 +37,5 @@ def edit_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance, att ifcopenshell.api.group.edit_group(model, group=group, attributes={"Description": "All furniture and joinery included in the unit"}) """ - settings = {"group": group, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["group"], name, value) + for name, value in attributes.items(): + setattr(group, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py index ee31336eec..cac87106aa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/unassign_group.py @@ -46,17 +46,12 @@ def unassign_group( bad_furniture = furniture[0] ifcopenshell.api.group.unassign_group(model, products=[bad_furniture], group=group) """ - settings = { - "products": products, - "group": group, - } - - if not settings["group"].IsGroupedBy: + if not group.IsGroupedBy: return - rel = settings["group"].IsGroupedBy[0] + rel = group.IsGroupedBy[0] related_objects = set(rel.RelatedObjects) or set() - products = set(settings["products"]) - related_objects -= products + products_set = set(products) + related_objects -= products_set if related_objects: rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py b/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py index 61b3ec7657..468c193ff7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/update_group_products.py @@ -45,24 +45,19 @@ def update_group_products( ifcopenshell.api.group.update_group_products(model, products=model.by_type("IfcFurniture"), group=group) """ - settings = { - "group": group, - "products": products, - } - - if not settings["group"].IsGroupedBy: + if not group.IsGroupedBy: return file.create_entity( "IfcRelAssignsToGroup", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": settings["products"], - "RelatingGroup": settings["group"], + "RelatedObjects": products, + "RelatingGroup": group, } ) else: - rels = settings["group"].IsGroupedBy - objects = set(settings["products"]) + rels = group.IsGroupedBy + objects = set(products) for rel in rels: objects.update([g for g in rel.RelatedObjects if g.is_a("IfcGroup")]) to_purge = rels[1:] diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py index 6f55f5da1d..7203fabb0c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/layer/edit_layer.py @@ -37,7 +37,5 @@ def edit_layer(file: ifcopenshell.file, layer: ifcopenshell.entity_instance, att ifcopenshell.api.layer.edit_layer(model, layer=layer, attributes={"Description": "All walls, based on the AIA standard."}) """ - settings = {"layer": layer, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["layer"], name, value) + for name, value in attributes.items(): + setattr(layer, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py index 97ae094a50..3eb0ed8fa1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/layer/unassign_layer.py @@ -61,17 +61,11 @@ def unassign_layer( # Let's undo it! ifcopenshell.api.layer.unassign_layer(model, items=[representation.Items[0]], layer=layer) """ - settings = { - "items": items, - "layer": layer, - } - - layer = settings["layer"] assigned_items = set(layer.AssignedItems) or set() - items = set(settings["items"]) - if not items.issubset(assigned_items): + items_set = set(items) + if not items_set.issubset(assigned_items): return - assigned_items = list(assigned_items - items) + assigned_items = list(assigned_items - items_set) # keep IFC valid in case if there are no items left if assigned_items: diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py b/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py index b3bc87b1c3..4e12195bda 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/edit_library.py @@ -28,11 +28,8 @@ def edit_library(file: ifcopenshell.file, library: ifcopenshell.entity_instance, IfcLibraryInformation, consult the IFC documentation. :param library: The IfcLibraryInformation entity you want to edit - :type library: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py index f3016fd9cf..e6acacbd8f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/edit_reference.py @@ -28,11 +28,8 @@ def edit_reference( IfcLibraryReference, consult the IFC documentation. :param reference: The IfcLibraryReference entity you want to edit - :type reference: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -44,7 +41,5 @@ def edit_reference( ifcopenshell.api.library.edit_reference(model, reference=reference, attributes={"Identification": "http://example.org/digitaltwin#AHU01"}) """ - settings = {"reference": reference, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["reference"], name, value) + for name, value in attributes.items(): + setattr(reference, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py index 7719fd86c8..24fa93487b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.representation +from typing import Any def assign_profile( @@ -97,6 +98,7 @@ def assign_profile( class Usecase: file: ifcopenshell.file + settings: dict[str, Any] def execute(self) -> None: # TODO: handle composite profiles diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py index 939e995a06..9a01322743 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_assigned_material.py @@ -28,11 +28,8 @@ def edit_assigned_material( IfcMaterial, consult the IFC documentation. :param element: The IfcMaterial entity you want to edit - :type element: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_assigned_material( ifcopenshell.api.material.edit_assigned_material(model, element=concrete, attributes={"Description": "40MPA concrete with broom finish"}) """ - settings = {"element": element, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["element"], name, value) + for name, value in attributes.items(): + setattr(element, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py index eda33d349b..a7398ca5f0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_constituent.py @@ -31,13 +31,9 @@ def edit_constituent( IfcMaterialConstituent, consult the IFC documentation. :param constituent: The IfcMaterialConstituent entity you want to edit - :type constituent: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional :param material: The IfcMaterial entity you want to change the constituent to - :type material: ifcopenshell.entity_instance, optional :return: None - :rtype: None Example: @@ -65,8 +61,6 @@ def edit_constituent( ifcopenshell.api.material.edit_constituent(model, constituent=constituent, attributes={"Name": "Glazing"}) """ - settings = {"constituent": constituent, "attributes": attributes or {}, "material": material} - - for name, value in settings["attributes"].items(): - setattr(settings["constituent"], name, value) - settings["constituent"].Material = settings["material"] + for name, value in (attributes or {}).items(): + setattr(constituent, name, value) + constituent.Material = material diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py index d691d6b154..68f3c6d832 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer.py @@ -31,14 +31,10 @@ def edit_layer( IfcMaterialLayer, consult the IFC documentation. :param layer: The IfcMaterialLayer entity you want to edit - :type layer: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional :param material: The IfcMaterial entity you want the layer to be made from. - :type material: ifcopenshell.entity_instance, optional :return: None - :rtype: None Example: @@ -63,9 +59,7 @@ def edit_layer( layer = ifcopenshell.api.material.add_layer(model, layer_set=material_set, material=gypsum) ifcopenshell.api.material.edit_layer(model, layer=layer, attributes={"LayerThickness": 13}) """ - settings = {"layer": layer, "attributes": attributes or {}, "material": material} - - for name, value in settings["attributes"].items(): - setattr(settings["layer"], name, value) - if settings["material"]: - settings["layer"].Material = settings["material"] + for name, value in (attributes or {}).items(): + setattr(layer, name, value) + if material: + layer.Material = material diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py index 6c581c1374..9d1ffb14ec 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_layer_usage.py @@ -29,11 +29,8 @@ def edit_layer_usage(file: ifcopenshell.file, usage: ifcopenshell.entity_instanc IfcMaterialLayerSetUsage, consult the IFC documentation. :param usage: The IfcMaterialLayerSetUsage entity you want to edit - :type usage: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -75,7 +72,5 @@ def edit_layer_usage(file: ifcopenshell.file, usage: ifcopenshell.entity_instanc ifcopenshell.api.material.edit_layer_usage(model, usage=rel.RelatingMaterial, attributes={"OffsetFromReferenceLine": 200}) """ - settings = {"usage": usage, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["usage"], name, value) + for name, value in attributes.items(): + setattr(usage, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py index 17f925997e..c35755a960 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py @@ -34,17 +34,12 @@ def edit_profile( IfcMaterialProfile, consult the IFC documentation. :param profile: The IfcMaterialProfile entity you want to edit - :type profile: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional :param profile_def: The IfcProfileDef entity the profile curve should be extruded from. - :type profile_def: ifcopenshell.entity_instance, optional :param material: The IfcMaterial entity you want to change the profile to be made from. - :type material: ifcopenshell.entity_instance, optional :return: None - :rtype: None Example: @@ -80,16 +75,9 @@ def edit_profile( ifcopenshell.api.material.edit_profile(model, profile=profile_item, profile_def=hea200, material=steel2) """ - settings = { - "profile": profile, - "attributes": attributes or {}, - "profile_def": profile_def, - "material": material, - } - - for name, value in settings["attributes"].items(): - setattr(settings["profile"], name, value) - if settings["material"]: - settings["profile"].Material = settings["material"] - if settings["profile_def"]: - settings["profile"].Profile = settings["profile_def"] + for name, value in (attributes or {}).items(): + setattr(profile, name, value) + if material: + profile.Material = material + if profile_def: + profile.Profile = profile_def diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index f9894db5b3..4a659f251c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.geom import ifcopenshell.util.representation +from ifcopenshell.geom import ShapeType from typing import Any @@ -34,11 +35,8 @@ def edit_profile_usage( IfcMaterialProfileSetUsage, consult the IFC documentation. :param usage: The IfcMaterialProfileSetUsage entity you want to edit - :type usage: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -98,16 +96,19 @@ def edit_profile_usage( class Usecase: - def execute(self): - self.cardinal_point = self.settings["attributes"].get("CardinalPoint") - if self.cardinal_point and self.cardinal_point != self.settings["usage"].CardinalPoint: + file: ifcopenshell.file + + def execute(self, usage: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: + self.attributes = attributes + self.cardinal_point = attributes.get("CardinalPoint") + if self.cardinal_point and self.cardinal_point != usage.CardinalPoint: self.update_cardinal_point() - for name, value in self.settings["attributes"].items(): - setattr(self.settings["usage"], name, value) + for name, value in attributes.items(): + setattr(usage, name, value) def update_cardinal_point(self): - material_set = self.settings["usage"].ForProfileSet + material_set = self.attributes["usage"].ForProfileSet self.profile = material_set.CompositeProfile if not self.profile and material_set.MaterialProfiles: self.profile = material_set.MaterialProfiles[0].Profile @@ -117,13 +118,13 @@ class Usecase: self.position = self.calculate_position() if self.file.schema == "IFC2X3": - for rel in self.file.get_inverse(self.settings["usage"]): + for rel in self.file.get_inverse(self.attributes["usage"]): if not rel.is_a("IfcRelAssociatesMaterial"): continue for element in rel.RelatedObjects: self.update_representation(element) else: - for rel in self.settings["usage"].AssociatedTo: + for rel in self.attributes["usage"].AssociatedTo: for element in rel.RelatedObjects: self.update_representation(element) @@ -166,7 +167,7 @@ class Usecase: elif self.cardinal_point == 9: return self.get_top_right(shape) - def get_bottom_left(self, shape): + def get_bottom_left(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] y = [v[i + 1] for i in range(0, len(v), 3)] @@ -174,13 +175,13 @@ class Usecase: height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, height / 2, 0.0))) - def get_bottom_centre(self, shape): + def get_bottom_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts y = [v[i + 1] for i in range(0, len(v), 3)] height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, height / 2, 0.0))) - def get_bottom_right(self, shape): + def get_bottom_right(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] y = [v[i + 1] for i in range(0, len(v), 3)] @@ -188,22 +189,22 @@ class Usecase: height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, height / 2, 0.0))) - def get_mid_depth_left(self, shape): + def get_mid_depth_left(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] width = max(x) - min(x) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, 0.0, 0.0))) - def get_mid_depth_centre(self, shape): + def get_mid_depth_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance: return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))) - def get_mid_depth_right(self, shape): + def get_mid_depth_right(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] width = max(x) - min(x) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, 0.0, 0.0))) - def get_top_left(self, shape): + def get_top_left(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] y = [v[i + 1] for i in range(0, len(v), 3)] @@ -211,13 +212,13 @@ class Usecase: height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, -height / 2, 0.0))) - def get_top_centre(self, shape): + def get_top_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts y = [v[i + 1] for i in range(0, len(v), 3)] height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, -height / 2, 0.0))) - def get_top_right(self, shape): + def get_top_right(self, shape: ShapeType) -> ifcopenshell.entity_instance: v = shape.verts x = [v[i] for i in range(0, len(v), 3)] y = [v[i + 1] for i in range(0, len(v), 3)] @@ -225,7 +226,7 @@ class Usecase: height = max(y) - min(y) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, -height / 2, 0.0))) - def update_representation(self, element): + def update_representation(self, element: ifcopenshell.entity_instance) -> None: representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not representation: return @@ -234,5 +235,5 @@ class Usecase: if subelement.is_a("IfcSweptAreaSolid") and subelement.SweptArea == self.profile: self.update_swept_area_solid(subelement) - def update_swept_area_solid(self, element): + def update_swept_area_solid(self, element: ifcopenshell.entity_instance) -> None: element.Position = self.position diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py index 7521c48a15..d9401cfa6d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.util.element def remove_constituent( diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py index cd4f2f1cd6..6af15a7d3a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_list_item.py @@ -54,8 +54,6 @@ def remove_list_item( # Let's remove the glass ifcopenshell.api.material.remove_list_item(model, material_list=material_set, material_index=1) """ - settings = {"material_list": material_list, "material_index": material_index} - - materials = list(settings["material_list"].Materials) - materials.pop(settings["material_index"]) - settings["material_list"].Materials = materials + materials = list(material_list.Materials) + materials.pop(material_index) + material_list.Materials = materials diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py b/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py index 0015d3558a..aa06433e66 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/reorder_set_item.py @@ -57,16 +57,17 @@ def reorder_set_item( ifcopenshell.api.material.reorder_set_item(model, material_set=material_set, old_index=0, new_index=1) """ - settings = {"material_set": material_set, "old_index": old_index, "new_index": new_index} - - if settings["material_set"].is_a("IfcMaterialConstituentSet"): + if material_set.is_a("IfcMaterialConstituentSet"): set_name = "MaterialConstituents" - elif settings["material_set"].is_a("IfcMaterialLayerSet"): + elif material_set.is_a("IfcMaterialLayerSet"): set_name = "MaterialLayers" - elif settings["material_set"].is_a("IfcMaterialProfileSet"): + elif material_set.is_a("IfcMaterialProfileSet"): set_name = "MaterialProfiles" - elif settings["material_set"].is_a("IfcMaterialList"): + elif material_set.is_a("IfcMaterialList"): set_name = "Materials" - items = list(getattr(settings["material_set"], set_name) or []) - items.insert(settings["new_index"], items.pop(settings["old_index"])) - setattr(settings["material_set"], set_name, items) + else: + raise ValueError(f"Unexpected material set type: '{material_set.is_a()}'.") + + items = list(getattr(material_set, set_name) or []) + items.insert(new_index, items.pop(old_index)) + setattr(material_set, set_name, items) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py index 8db2060df0..f8386490c7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/unassign_material.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.util.element +from typing import Any def unassign_material(file: ifcopenshell.file, products: list[ifcopenshell.entity_instance]) -> None: @@ -58,6 +59,9 @@ def unassign_material(file: ifcopenshell.file, products: list[ifcopenshell.entit class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.products = set(self.settings["products"]) if not self.products: diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py index 991006be01..3f18754287 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_application.py @@ -17,7 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.api -from typing import Optional +from typing import Optional, Any def add_application( @@ -68,6 +68,9 @@ def add_application( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if not self.settings["application_developer"]: self.settings["application_developer"] = self.create_application_organisation() diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py index 7105e5e78d..75bb5eb517 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_role.py @@ -48,16 +48,14 @@ def add_role( identification="AWB", name="Architects Without Ballpens") ifcopenshell.api.owner.add_role(model, assigned_object=organisation, role="ARCHITECT") """ - settings = {"assigned_object": assigned_object, "role": role} - - element = file.createIfcActorRole("ARCHITECT") - if settings["role"]: + element = file.create_entity("IfcActorRole", Role="ARCHITECT") + if role: try: - element.Role = settings["role"] + element.Role = role except: element.Role = "USERDEFINED" - element.UserDefinedRole = settings["role"] - roles = list(settings["assigned_object"].Roles) if settings["assigned_object"].Roles else [] + element.UserDefinedRole = role + roles = list(assigned_object.Roles) if assigned_object.Roles else [] roles.append(element) - settings["assigned_object"].Roles = roles + assigned_object.Roles = roles return element diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py index 799b37462c..8ad238f994 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_actor.py @@ -26,11 +26,8 @@ def edit_actor(file: ifcopenshell.file, actor: ifcopenshell.entity_instance, att IfcActor, consult the IFC documentation. :param actor: The IfcActor entity you want to edit - :type actor: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -49,7 +46,5 @@ def edit_actor(file: ifcopenshell.file, actor: ifcopenshell.entity_instance, att ifcopenshell.api.actor.edit_actor(model, actor=actor, attributes={"Description": "Responsible for buildings A, B, and C."}) """ - settings = {"actor": actor, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["actor"], name, value) + for name, value in attributes.items(): + setattr(actor, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py index 43776fa711..52e80d6ef1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_address.py @@ -26,11 +26,8 @@ def edit_address(file: ifcopenshell.file, address: ifcopenshell.entity_instance, IfcAddress, consult the IFC documentation. :param address: The IfcAddress entity you want to edit - :type address: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -51,7 +48,5 @@ def edit_address(file: ifcopenshell.file, address: ifcopenshell.entity_instance, "ElectronicMailAddresses": ["bobthebuilder@example.com"], "WWWHomePageURL": "https://thinkmoult.com"}) """ - settings = {"address": address, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["address"], name, value) + for name, value in attributes.items(): + setattr(address, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py index 9cfbfff48d..a2bb5e3eb6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_organisation.py @@ -28,11 +28,8 @@ def edit_organisation( IfcOrganization, consult the IFC documentation. :param organisation: The IfcOrganization entity you want to edit - :type organisation: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -43,7 +40,5 @@ def edit_organisation( ifcopenshell.api.owner.edit_organisation(model, organisation=organisation, attributes={"name": "Architects Without Ballpens"}) """ - settings = {"organisation": organisation, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["organisation"], name, value) + for name, value in attributes.items(): + setattr(organisation, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py index 0e4bd15293..6b697bd674 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_person.py @@ -26,11 +26,8 @@ def edit_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance, a IfcPerson, consult the IFC documentation. :param person: The IfcPerson entity you want to edit - :type person: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -41,7 +38,5 @@ def edit_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance, a ifcopenshell.api.owner.edit_person(model, person=person, attributes={"MiddleNames": ["The"], "FamilyName": "Builder"}) """ - settings = {"person": person, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["person"], name, value) + for name, value in attributes.items(): + setattr(person, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py index 885d4b9b9b..fca80a16b0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/edit_role.py @@ -26,11 +26,8 @@ def edit_role(file: ifcopenshell.file, role: ifcopenshell.entity_instance, attri IfcActorRole, consult the IFC documentation. :param role: The IfcActorRole entity you want to edit - :type role: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -45,7 +42,5 @@ def edit_role(file: ifcopenshell.file, role: ifcopenshell.entity_instance, attri # But Bob is not an architect ifcopenshell.api.owner.edit_role(model, role=role, attributes={"Role": "CONSTRUCTIONMANAGER"}) """ - settings = {"role": role, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["role"], name, value) + for name, value in attributes.items(): + setattr(role, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py index 08892a555b..80f3b8cf71 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile.py @@ -16,12 +16,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import numpy.typing as npt import ifcopenshell.util.unit -from typing import Optional +from ifcopenshell.util.shape_builder import V, SequenceOfVectors, ifc_safe_vector_type +from typing import Optional, Union def add_arbitrary_profile( - file: ifcopenshell.file, profile: list[tuple[float, float]], name: Optional[str] = None + file: ifcopenshell.file, profile: SequenceOfVectors, name: Optional[str] = None ) -> ifcopenshell.entity_instance: """Adds a new arbitrary polyline-based profile @@ -33,13 +35,10 @@ def add_arbitrary_profile( identical. :param profile: A list of coordinates - :type profile: list[tuple[float, float]] :param name: If the profile is semantically significant (i.e. to be managed and reused by the user) then it must be named. Otherwise, this may be left as none. - :type name: str, optional :return: The newly created IfcArbitraryClosedProfileDef - :rtype: ifcopenshell.entity_instance Example: @@ -53,26 +52,30 @@ def add_arbitrary_profile( """ usecase = Usecase() usecase.file = file - usecase.settings = {"profile": profile, "name": name} - return usecase.execute() + return usecase.execute(V(profile), name) class Usecase: - def execute(self): - self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - points = [self.convert_si_to_unit(p) for p in self.settings["profile"]] - if self.file.schema == "IFC2X3": - curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points]) - else: - dimensions = len(points[0]) - if dimensions == 2: - ifc_points = self.file.createIfcCartesianPointList2D(points) - elif dimensions == 3: - ifc_points = self.file.createIfcCartesianPointList3D(points) - curve = self.file.createIfcIndexedPolyCurve(ifc_points) - return self.file.createIfcArbitraryClosedProfileDef("AREA", self.settings["name"], curve) + file: ifcopenshell.file - def convert_si_to_unit(self, co): - if isinstance(co, (tuple, list)): - return [self.convert_si_to_unit(o) for o in co] - return co / self.settings["unit_scale"] + def execute(self, profile: npt.NDArray, name: Union[str, None]): + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) + points = self.convert_si_to_unit(profile) + if self.file.schema == "IFC2X3": + curve = self.file.create_entity( + "IfcPolyline", + [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(p)) for p in points], + ) + else: + dimensions = points.shape[1] + if dimensions == 2: + ifc_points = self.file.create_entity("IfcCartesianPointList2D", ifc_safe_vector_type(points)) + elif dimensions == 3: + ifc_points = self.file.create_entity("IfcCartesianPointList3D", ifc_safe_vector_type(points)) + else: + assert False, f"Invalid dimensions: {dimensions}." + curve = self.file.create_entity("IfcIndexedPolyCurve", ifc_points) + return self.file.create_entity("IfcArbitraryClosedProfileDef", "AREA", name, curve) + + def convert_si_to_unit(self, co: npt.NDArray) -> npt.NDArray: + return co / self.unit_scale diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py index 8412d40e23..b813a3c65f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_arbitrary_profile_with_voids.py @@ -17,13 +17,15 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.util.unit -from typing import Optional +import numpy.typing as npt +from ifcopenshell.util.shape_builder import SequenceOfVectors, ifc_safe_vector_type, V +from typing import Optional, Union def add_arbitrary_profile_with_voids( file: ifcopenshell.file, - outer_profile: list[tuple[float, float]], - inner_profiles: list[list[tuple[float, float]]], + outer_profile: SequenceOfVectors, + inner_profiles: list[SequenceOfVectors], name: Optional[str] = None, ) -> ifcopenshell.entity_instance: """Adds a new arbitrary polyline-based profile with voids @@ -41,15 +43,11 @@ def add_arbitrary_profile_with_voids( provided in SI meters. :param outer_profile: A list of coordinates - :type profile: list[tuple[float, float]] :param inner_profiles: A list of polylines - :type profile: list[list[tuple[float, float]]] :param name: If the profile is semantically significant (i.e. to be managed and reused by the user) then it must be named. Otherwise, this may be left as none. - :type name: str, optional :return: The newly created IfcArbitraryProfileDefWithVoids - :rtype: ifcopenshell.entity_instance Example: @@ -63,37 +61,52 @@ def add_arbitrary_profile_with_voids( """ usecase = Usecase() usecase.file = file - usecase.settings = {"outer_profile": outer_profile, "inner_profiles": inner_profiles, "name": name} - return usecase.execute() + return usecase.execute(V(outer_profile), [V(p) for p in inner_profiles], name) class Usecase: - def execute(self): - self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - outer_points = [self.convert_si_to_unit(p) for p in self.settings["outer_profile"]] - inner_points = [] - for inner_profile in self.settings["inner_profiles"]: - inner_points.append([self.convert_si_to_unit(p) for p in inner_profile]) + file: ifcopenshell.file + + def execute( + self, + outer_profile: npt.NDArray, + inner_profiles: list[npt.NDArray], + name: Union[str, None], + ): + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) + outer_points = self.convert_si_to_unit(outer_profile) + inner_points: list[npt.NDArray] = [] + for inner_profile in inner_profiles: + inner_points.append(self.convert_si_to_unit(inner_profile)) + + inner_curves: list[ifcopenshell.entity_instance] = [] if self.file.schema == "IFC2X3": - outer_curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in outer_points]) - inner_curves = [] + outer_curve = self.file.create_entity( + "IfcPolyline", + [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(p)) for p in outer_points], + ) for inner_point in inner_points: inner_curves.append( - self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in inner_point]) + self.file.create_entity( + "IfcPolyline", + [self.file.create_entity("IfcCartesianPoint", ifc_safe_vector_type(p)) for p in inner_point], + ) ) else: - outer_curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList3D(outer_points)) - inner_curves = [] + outer_curve = self.file.create_entity( + "IfcIndexedPolyCurve", + (self.file.create_entity("IfcCartesianPointList3D", ifc_safe_vector_type(outer_points))), + ) for inner_point in inner_points: - dimensions = len(inner_point[0]) + dimensions = inner_point.shape[1] if dimensions == 2: - ifc_points = self.file.createIfcCartesianPointList2D(inner_point) + ifc_points = self.file.create_entity("IfcCartesianPointList2D", ifc_safe_vector_type(inner_point)) elif dimensions == 3: - ifc_points = self.file.createIfcCartesianPointList3D(inner_point) - inner_curves.append(self.file.createIfcIndexedPolyCurve(ifc_points)) - return self.file.createIfcArbitraryProfileDefWithVoids("AREA", self.settings["name"], outer_curve, inner_curves) + ifc_points = self.file.create_entity("IfcCartesianPointList3D", ifc_safe_vector_type(inner_point)) + else: + assert False, f"Invalid dimensions: {dimensions}." + inner_curves.append(self.file.create_entity("IfcIndexedPolyCurve", ifc_points)) + return self.file.create_entity("IfcArbitraryProfileDefWithVoids", "AREA", name, outer_curve, inner_curves) - def convert_si_to_unit(self, co): - if isinstance(co, (tuple, list)): - return [self.convert_si_to_unit(o) for o in co] - return co / self.settings["unit_scale"] + def convert_si_to_unit(self, co: npt.NDArray) -> npt.NDArray: + return co / self.unit_scale diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py index b1fda065f0..ba7bc3cfeb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/edit_profile.py @@ -26,11 +26,8 @@ def edit_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instance, IfcProfileDef, consult the IFC documentation. :param profile: The IfcProfileDef entity you want to edit - :type profile: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -43,7 +40,5 @@ def edit_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instance, ifcopenshell.api.profile.edit_profile(model, profile=circle, attributes={"ProfileName": "1000mm Dia"}) """ - settings = {"profile": profile, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["profile"], name, value) + for name, value in attributes.items(): + setattr(profile, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py index ffa31faa8e..db585c3276 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_pset.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.api.pset import ifcopenshell.guid -from typing import Optional +from typing import Optional, Any def add_pset( @@ -92,12 +92,11 @@ def add_pset( # Add a fire rating property standardised by buildingSMART. ifcopenshell.api.pset.edit_pset(model, pset=pset, properties={"FireRating": "2HR"}) """ - settings = {"product": product, "name": name} is_ifc2x3 = file.schema == "IFC2X3" - if settings["product"].is_a("IfcObject") or settings["product"].is_a("IfcContext"): - for rel in settings["product"].IsDefinedBy or []: - if rel.is_a("IfcRelDefinesByProperties") and rel.RelatingPropertyDefinition.Name == settings["name"]: + if product.is_a("IfcObject") or product.is_a("IfcContext"): + for rel in product.IsDefinedBy or []: + if rel.is_a("IfcRelDefinesByProperties") and rel.RelatingPropertyDefinition.Name == name: return rel.RelatingPropertyDefinition pset = file.create_entity( @@ -105,15 +104,15 @@ def add_pset( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "Name": settings["name"], + "Name": name, }, ) - ifcopenshell.api.pset.assign_pset(file, [settings["product"]], pset) + ifcopenshell.api.pset.assign_pset(file, [product], pset) return pset - elif settings["product"].is_a("IfcTypeObject"): - for definition in settings["product"].HasPropertySets or []: - if definition.Name == settings["name"]: + elif product.is_a("IfcTypeObject"): + for definition in product.HasPropertySets or []: + if definition.Name == name: return definition pset = file.create_entity( @@ -121,42 +120,43 @@ def add_pset( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "Name": settings["name"], + "Name": name, }, ) - ifcopenshell.api.pset.assign_pset(file, [settings["product"]], pset) + ifcopenshell.api.pset.assign_pset(file, [product], pset) return pset # in IFC2X3 IfcMaterialDefinition not yet existed - elif settings["product"].is_a("IfcMaterialDefinition") or settings["product"].is_a("IfcMaterial"): - kwargs = {"Material": settings["product"]} + elif product.is_a("IfcMaterialDefinition") or product.is_a("IfcMaterial"): + kwargs: dict[str, Any] + kwargs = {"Material": product} if file.schema == "IFC2X3": ifc_class = ifc2x3_subclass or "IfcExtendedMaterialProperties" - definitions = (d for d in file.by_type("IfcMaterialProperties") if d.Material == settings["product"]) + definitions = (d for d in file.by_type("IfcMaterialProperties") if d.Material == product) if ifc_class == "IfcExtendedMaterialProperties": - kwargs["Name"] = settings["name"] + kwargs["Name"] = name else: ifc_class = "IfcMaterialProperties" - definitions = settings["product"].HasProperties - kwargs["Name"] = settings["name"] + definitions = product.HasProperties + kwargs["Name"] = name for definition in definitions: # In IFC2X3 not all IfcMaterialProperties has Name - if getattr(definition, "Name", None) == settings["name"]: + if getattr(definition, "Name", None) == name: return definition return file.create_entity(ifc_class, **kwargs) - elif settings["product"].is_a("IfcProfileDef"): + elif product.is_a("IfcProfileDef"): # in IFC2X3 IfcProfileProperties doesn't have Name and we cannot identify them if file.schema != "IFC2X3": - for definition in settings["product"].HasProperties or []: - if definition.Name == settings["name"]: + for definition in product.HasProperties or []: + if definition.Name == name: return definition kwargs = {} - kwargs["ProfileDefinition"] = settings["product"] + kwargs["ProfileDefinition"] = product if file.schema != "IFC2X3": - kwargs["Name"] = settings["name"] + kwargs["Name"] = name if is_ifc2x3: ifc_class = ifc2x3_subclass or "IfcGeneralProfileProperties" @@ -164,4 +164,4 @@ def add_pset( ifc_class = "IfcProfileProperties" return file.create_entity(ifc_class, **kwargs) - raise TypeError(f"Class '{settings['product'].is_a(True)}' doesn't support adding a property set.") + raise TypeError(f"Class '{product.is_a(True)}' doesn't support adding a property set.") diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py b/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py index 15646fe9c7..415b8ec187 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/add_qto.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.guid +from typing import Any def add_qto(file: ifcopenshell.file, product: ifcopenshell.entity_instance, name: str) -> ifcopenshell.entity_instance: @@ -83,9 +84,15 @@ def add_qto(file: ifcopenshell.file, product: ifcopenshell.entity_instance, name class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): - if self.settings["product"].is_a("IfcObject") or self.settings["product"].is_a("IfcContext"): - for rel in self.settings["product"].IsDefinedBy or []: + product: ifcopenshell.entity_instance = self.settings["product"] + name: str = self.settings["name"] + + if product.is_a("IfcObject") or product.is_a("IfcContext"): + for rel in product.IsDefinedBy or []: if ( rel.is_a("IfcRelDefinesByProperties") and rel.RelatingPropertyDefinition.Name == self.settings["name"] @@ -103,14 +110,14 @@ class Usecase: } ) return qto - elif self.settings["product"].is_a("IfcTypeObject"): - for definition in self.settings["product"].HasPropertySets or []: - if definition.Name == self.settings["name"]: + elif product.is_a("IfcTypeObject"): + for definition in product.HasPropertySets or []: + if definition.Name == name: return definition qto = self.create_qto() - has_property_sets = list(self.settings["product"].HasPropertySets or []) + has_property_sets = list(product.HasPropertySets or []) has_property_sets.append(qto) - self.settings["product"].HasPropertySets = has_property_sets + product.HasPropertySets = has_property_sets return qto def create_qto(self): diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py index 1984506c30..3677a8e63e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_prop_template.py @@ -28,11 +28,8 @@ def edit_prop_template( IfcSimplePropertyTemplate, consult the IFC documentation. :param prop_template: The IfcSimplePropertyTemplate entity you want to edit - :type prop_template: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py index 1682fa150d..d143b5554e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/edit_pset_template.py @@ -28,11 +28,8 @@ def edit_pset_template( IfcPropertySetTemplate, consult the IFC documentation. :param pset_template: The IfcPropertySetTemplate entity you want to edit - :type pset_template: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -45,7 +42,5 @@ def edit_pset_template( ifcopenshell.api.pset_template.edit_pset_template(model, pset_template=template, attributes={"Name": "ABC_RiskFactors"}) """ - settings = {"pset_template": pset_template, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["pset_template"], name, value) + for name, value in attributes.items(): + setattr(pset_template, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py index b445c1d1ad..fc67a8d3c7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource_quantity.py @@ -36,15 +36,12 @@ def add_resource_quantity( This base quantity is then used in other calculations. :param resource: The IfcConstructionResource to add a quantity to. - :type resource: ifcopenshell.entity_instance :param ifc_class: The type of quantity to add, chosen from IfcQuantityArea (for material), IfcQuantityCount (for products), IfcQuantityLength (for material), IfcQuantityTime (for equipment or labour), IfcQuantityVolume (for material), and IfcQuantityWeight (for material). - :type ifc_class: str,optional :return: The newly created quantity depending on the IFC class - :rtype: ifcopenshell.entity_instance Example: @@ -65,8 +62,6 @@ def add_resource_quantity( ifcopenshell.api.resource.edit_resource_quantity(model, physical_quantity=quantity, attributes={"TimeValue": 8.0}) """ - settings = {"resource": resource, "ifc_class": ifc_class} - resource_type = resource.is_a() supported_quantities = ifcopenshell.util.resource.RESOURCES_TO_QUANTITIES[resource_type] if ifc_class not in supported_quantities: @@ -75,14 +70,14 @@ def add_resource_quantity( f"Supported quantities: {','.join(supported_quantities)}" ) - quantity = file.create_entity(settings["ifc_class"], Name="Unnamed") + quantity = file.create_entity(ifc_class, Name="Unnamed") # 3 IfcPhysicalSimpleQuantity Value - if settings["ifc_class"] == "IfcQuantityCount": + if ifc_class == "IfcQuantityCount": quantity[3] = 0 else: quantity[3] = 0.0 - old_quantity = settings["resource"].BaseQuantity - settings["resource"].BaseQuantity = quantity + old_quantity = resource.BaseQuantity + resource.BaseQuantity = quantity if old_quantity: ifcopenshell.util.element.remove_deep(file, old_quantity) return quantity diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py index b5bb899ead..3476032c1a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource.py @@ -26,11 +26,8 @@ def edit_resource(file: ifcopenshell.file, resource: ifcopenshell.entity_instanc IfcResource, consult the IFC documentation. :param resource: The IfcResource entity you want to edit - :type resource: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,7 +39,5 @@ def edit_resource(file: ifcopenshell.file, resource: ifcopenshell.entity_instanc # Change the name of the resource to "Zone A Crew" ifcopenshell.api.resource.edit_resource(model, resource=resource, attributes={"Name": "Foo"}) """ - settings = {"resource": resource, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["resource"], name, value) + for name, value in attributes.items(): + setattr(resource, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py index af8f55f94f..87a14774d8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_quantity.py @@ -28,11 +28,8 @@ def edit_resource_quantity( IfC quantity, consult the IFC documentation. :param physical_quantity: The IfC quantity entity you want to edit - :type physical_quantity: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -53,10 +50,5 @@ def edit_resource_quantity( ifcopenshell.api.resource.edit_resource_quantity(model, physical_quantity=time, attributes={"TimeValue": 8.0}) """ - settings = { - "physical_quantity": physical_quantity, - "attributes": attributes, - } - - for name, value in settings["attributes"].items(): - setattr(settings["physical_quantity"], name, value) + for name, value in attributes.items(): + setattr(physical_quantity, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py index 687fe0490d..7326a5fd90 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/edit_resource_time.py @@ -18,6 +18,9 @@ import ifcopenshell import ifcopenshell.api.sequence +import ifcopenshell.util.constraint +import ifcopenshell.util.date +import ifcopenshell.util.resource from typing import Any @@ -30,11 +33,8 @@ def edit_resource_time( IfcResourceTime, consult the IFC documentation. :param resource_time: The IfcResourceTime entity you want to edit - :type resource_time: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -62,25 +62,23 @@ def edit_resource_time( """ usecase = Usecase() usecase.file = file - usecase.settings = {"resource_time": resource_time, "attributes": attributes} - return usecase.execute() + return usecase.execute(resource_time, attributes) class Usecase: - def execute(self): - self.resource = self.get_resource() + file: ifcopenshell.file + + def execute(self, resource_time: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: + resource = self.get_resource(resource_time) # If the user specifies both an end date and a duration, the duration takes priority - if ( - self.settings["attributes"].get("ScheduleWork", None) - and "ScheduleFinish" in self.settings["attributes"].keys() - ): - del self.settings["attributes"]["ScheduleFinish"] - if self.settings["attributes"].get("ActualWork", None) and "ActualFinish" in self.settings["attributes"].keys(): - del self.settings["attributes"]["ActualFinish"] + if attributes.get("ScheduleWork", None) and "ScheduleFinish" in attributes.keys(): + del attributes["ScheduleFinish"] + if attributes.get("ActualWork", None) and "ActualFinish" in attributes.keys(): + del attributes["ActualFinish"] - for name, value in self.settings["attributes"].items(): - metrics = ifcopenshell.util.constraint.get_metric_constraints(self.resource, "Usage." + name) + for name, value in attributes.items(): + metrics = ifcopenshell.util.constraint.get_metric_constraints(resource, "Usage." + name) if metrics and ifcopenshell.util.constraint.is_hard_constraint(metrics[0]): continue if value: @@ -88,13 +86,13 @@ class Usecase: value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") elif name == "ScheduleWork" or name == "ActualWork" or name == "RemainingTime": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") - setattr(self.settings["resource_time"], name, value) + setattr(resource_time, name, value) if name == "ScheduleUsage" and ifcopenshell.util.constraint.get_metric_constraints( - self.resource, "Usage.ScheduleWork" + resource, "Usage.ScheduleWork" ): - task = ifcopenshell.util.resource.get_task_assignments(self.resource) + task = ifcopenshell.util.resource.get_task_assignments(resource) if task: ifcopenshell.api.sequence.calculate_task_duration(self.file, task=task) - def get_resource(self): - return [e for e in self.file.get_inverse(self.settings["resource_time"]) if e.is_a("IfcResource")][0] + def get_resource(self, resource_time: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + return next(e for e in self.file.get_inverse(resource_time) if e.is_a("IfcResource")) diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py index a55c711e53..6f02a4531d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py @@ -23,6 +23,7 @@ import ifcopenshell.api.geometry import ifcopenshell.util.system import ifcopenshell.util.element import ifcopenshell.util.placement +from typing import Any def copy_class(file: ifcopenshell.file, product: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: @@ -74,6 +75,9 @@ def copy_class(file: ifcopenshell.file, product: ifcopenshell.entity_instance) - class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): result = ifcopenshell.util.element.copy(self.file, self.settings["product"]) self.copy_direct_attributes(result) diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py b/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py index a31dc4c00a..67d67ed7f2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py @@ -19,7 +19,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.guid -from typing import Optional +from typing import Optional, Any def create_entity( @@ -80,6 +80,9 @@ def create_entity( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): element = self.file.create_entity( self.settings["ifc_class"], diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py index 3e4cd3faf4..7682856864 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py @@ -27,7 +27,7 @@ import ifcopenshell.util.representation import ifcopenshell.util.type import ifcopenshell.util.schema import ifcopenshell.util.element -from typing import Optional, Union, Literal +from typing import Optional, Union, Literal, Any def reassign_class( @@ -87,6 +87,7 @@ def reassign_class( class Usecase: file: ifcopenshell.file + settings: dict[str, Any] def execute(self): ifc_class: str = self.settings["ifc_class"] diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py index 89037d4eb0..38d64aa1bc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task_time.py @@ -29,11 +29,8 @@ def add_task_time( (especially for maintenance tasks). :param task: The task to add time data to. - :type task: ifcopenshell.entity_instance :param is_recurring: Whether or not the time should recur. - :type is_recurring: bool :return: The newly created IfcTaskTime. - :rtype: ifcopenshell.entity_instance Example: @@ -61,11 +58,9 @@ def add_task_time( ifcopenshell.api.sequence.edit_task_time(model, task_time=time, attributes={"ScheduleStart": "2000-01-01", "ScheduleDuration": "P2D"}) """ - settings = {"task": task, "is_recurring": is_recurring} - - if settings["is_recurring"]: + if is_recurring: task_time = file.create_entity("IfcTaskTimeRecurring") else: task_time = file.create_entity("IfcTaskTime") - settings["task"].TaskTime = task_time + task.TaskTime = task_time return task_time diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py index 65ea1db9bd..a5d813756e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py @@ -34,17 +34,13 @@ def assign_lag_time( are allowed. :param rel_sequence: The IfcRelSequence to assign the lag time to. - :type rel_sequence: ifcopenshell.entity_instance :param lag_value: An ISO standardised duration string. - :type lag_value: str :param duration_type: Choose from WORKTIME for the associated calendar-based lag times (this is the most common scenario and is recommended as a default), or ELAPSEDTIME to not follow the calendar. You may also choose NOTDEFINED but the behaviour of this is unclear. - :type duration_type: str :return: The newly created IfcLagTime - :rtype: ifcopenshell.entity_instance Example: @@ -84,16 +80,10 @@ def assign_lag_time( # for whatever reason. ifcopenshell.api.sequence.assign_lag_time(model, rel_sequence=sequence, lag_value="P1D") """ - settings = { - "rel_sequence": rel_sequence, - "lag_value": lag_value, - "duration_type": duration_type, - } - - lag_value = file.createIfcDuration(ifcopenshell.util.date.datetime2ifc(settings["lag_value"], "IfcDuration")) - lag_time = file.create_entity("IfcLagTime", DurationType=settings["duration_type"], LagValue=lag_value) - if settings["rel_sequence"].is_a("IfcRelSequence"): - if settings["rel_sequence"].TimeLag and len(file.get_inverse(settings["rel_sequence"].TimeLag)) == 1: - file.remove(settings["rel_sequence"].TimeLag) - settings["rel_sequence"].TimeLag = lag_time + lag_value = file.create_entity("IfcDuration", ifcopenshell.util.date.datetime2ifc(lag_value, "IfcDuration")) + lag_time = file.create_entity("IfcLagTime", DurationType=duration_type, LagValue=lag_value) + if rel_sequence.is_a("IfcRelSequence"): + if rel_sequence.TimeLag and len(file.get_inverse(rel_sequence.TimeLag)) == 1: + file.remove(rel_sequence.TimeLag) + rel_sequence.TimeLag = lag_time return lag_time diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py index 0c64fb09e2..333aefb418 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py @@ -65,11 +65,8 @@ def assign_recurrence_pattern( :param parent: Either an IfcTaskTimeRecurring if you are defining a recurring schedule for a task, or IfcWorkTime if you are defining a recurring pattern for a workdays or holidays in a calendar. - :type parent: ifcopenshell.entity_instance :param recurrence_type: One of the types of recurrences. - :type recurrence_type: str :return: The newly created IfcRecurrencePattern - :rtype: ifcopenshell.entity_instance Example: @@ -108,16 +105,14 @@ def assign_recurrence_pattern( ifcopenshell.api.sequence.edit_recurrence_pattern(model, recurrence_pattern=pattern, attributes={"DayComponent": [1], "Interval": 6}) """ - settings = {"parent": parent, "recurrence_type": recurrence_type} + recurrence = file.create_entity("IfcRecurrencePattern", recurrence_type) - recurrence = file.createIfcRecurrencePattern(settings["recurrence_type"]) - - if settings["parent"].is_a("IfcWorkTime"): - if settings["parent"].RecurrencePattern and len(file.get_inverse(settings["parent"].RecurrencePattern)) == 1: - file.remove(settings["parent"].RecurrencePattern) - settings["parent"].RecurrencePattern = recurrence - elif settings["parent"].is_a("IfcTaskTimeRecurring"): - if recurrence_old := settings["parent"].Recurrence and len(file.get_inverse(recurrence_old)) == 1: + if parent.is_a("IfcWorkTime"): + if parent.RecurrencePattern and len(file.get_inverse(parent.RecurrencePattern)) == 1: + file.remove(parent.RecurrencePattern) + parent.RecurrencePattern = recurrence + elif parent.is_a("IfcTaskTimeRecurring"): + if (recurrence_old := parent.Recurrence) and len(file.get_inverse(recurrence_old)) == 1: file.remove(recurrence_old) - settings["parent"].Recurrence = recurrence + parent.Recurrence = recurrence return recurrence diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py index 664d5780ec..65491604aa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_sequence.py @@ -51,13 +51,10 @@ def assign_sequence( predecessor and successor tasks in the planning profession. :param relating_process: The previous / predecessor task. - :type relating_process: ifcopenshell.entity_instance :param related_process: The next / successor task. - :type related_process: ifcopenshell.entity_instance :param sequence_type: Choose from FINISH_START, FINISH_FINISH, START_START, or START_FINISH. :return: The newly created IfcRelSequence - :rtype: ifcopenshell.entity_instance Example: @@ -109,24 +106,18 @@ def assign_sequence( # to be 2000-01-05. ifcopenshell.api.sequence.cascade_schedule(model, task=formwork) """ - settings = { - "relating_process": relating_process, - "related_process": related_process, - "sequence_type": sequence_type, - } - - for rel in settings["related_process"].IsSuccessorFrom or []: - if rel.RelatingProcess == settings["relating_process"]: + for rel in related_process.IsSuccessorFrom or []: + if rel.RelatingProcess == relating_process: return rel rel = file.create_entity( "IfcRelSequence", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatingProcess": settings["relating_process"], - "RelatedProcess": settings["related_process"], - "SequenceType": settings["sequence_type"], + "RelatingProcess": relating_process, + "RelatedProcess": related_process, + "SequenceType": sequence_type, } ) - ifcopenshell.api.sequence.cascade_schedule(file, task=settings["relating_process"]) + ifcopenshell.api.sequence.cascade_schedule(file, task=relating_process) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py index 7c3f821e7e..b8b4d857b6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py @@ -20,6 +20,7 @@ import math import ifcopenshell.api.sequence import ifcopenshell.util.date import ifcopenshell.util.element +from typing import Union def calculate_task_duration(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> None: @@ -35,9 +36,7 @@ def calculate_task_duration(file: ifcopenshell.file, task: ifcopenshell.entity_i then nothing happens. :param task: The IfcTask to calculate the duration for. - :type task: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -82,18 +81,20 @@ def calculate_task_duration(file: ifcopenshell.file, task: ifcopenshell.entity_i """ usecase = Usecase() usecase.file = file - usecase.settings = {"task": task} - return usecase.execute() + return usecase.execute(task) class Usecase: - def execute(self): + file: ifcopenshell.file + + def execute(self, task: ifcopenshell.entity_instance) -> None: + self.task = task self.seconds_per_workday = self.calculate_seconds_per_workday() duration = self.calculate_max_resource_usage_duration() if duration: self.set_task_duration(duration) - def calculate_seconds_per_workday(self): + def calculate_seconds_per_workday(self) -> float: def get_work_schedule(task): for rel in task.HasAssignments or []: if rel.is_a("IfcRelAssignsToControl") and rel.RelatingControl.is_a("IfcWorkSchedule"): @@ -102,7 +103,7 @@ class Usecase: return get_work_schedule(rel.RelatingObject) default_seconds_per_workday = 8 * 60 * 60 - work_schedule = get_work_schedule(self.settings["task"]) + work_schedule = get_work_schedule(self.task) if not work_schedule: return default_seconds_per_workday psets = ifcopenshell.util.element.get_psets(work_schedule) @@ -115,9 +116,9 @@ class Usecase: work_day_duration = ifcopenshell.util.date.ifc2datetime(psets["Pset_WorkControlCommon"]["WorkDayDuration"]) return work_day_duration.seconds - def calculate_max_resource_usage_duration(self): + def calculate_max_resource_usage_duration(self) -> float: max_duration = 0 - for rel in self.settings["task"].OperatesOn or []: + for rel in self.task.OperatesOn or []: for related_object in rel.RelatedObjects: if related_object.is_a("IfcConstructionResource"): duration = self.calculate_duration_in_days(related_object) @@ -125,7 +126,7 @@ class Usecase: max_duration = duration return max_duration - def calculate_duration_in_days(self, resource): + def calculate_duration_in_days(self, resource: ifcopenshell.entity_instance) -> Union[float, None]: def is_hourly_work(schedule_work): return "T" in schedule_work @@ -140,7 +141,7 @@ class Usecase: schedule_seconds = (schedule_duration.days + partial_days) * self.seconds_per_workday return math.ceil((schedule_seconds / self.seconds_per_workday) / schedule_usage) - def set_task_duration(self, duration): - if not self.settings["task"].TaskTime: - ifcopenshell.api.sequence.add_task_time(self.file, task=self.settings["task"]) - self.settings["task"].TaskTime.ScheduleDuration = f"P{duration}D" + def set_task_duration(self, duration: float) -> None: + if not (task_time := self.task.TaskTime): + ifcopenshell.api.sequence.add_task_time(self.file, task=self.task) + task_time.ScheduleDuration = f"P{duration}D" diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py index d0670b76e8..a6637c7a88 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py @@ -24,7 +24,7 @@ import ifcopenshell.guid import ifcopenshell.util.element import ifcopenshell.util.sequence import ifcopenshell.util.system -from typing import Optional +from typing import Optional, Union def create_baseline( @@ -44,11 +44,8 @@ def create_baseline( * Same Resource Relationships :param work_schedule: The planned work_schedule to baseline - :type work_schedule: ifcopenshell.entity_instance :param name: baseline work schedule name - :type name: str, optional :return: The baseline work_schedule - :rtype: ifcopenshell.entity_instance Example: @@ -62,23 +59,20 @@ def create_baseline( """ usecase = Usecase() usecase.file = file - usecase.settings = {"work_schedule": work_schedule, "name": name} - return usecase.execute() + return usecase.execute(work_schedule, name) class Usecase: - def execute(self): - result = self.create_baseline_work_schedule(self.settings["work_schedule"]) - return result + file: ifcopenshell.file - def create_baseline_work_schedule(self, work_schedule): + def execute(self, work_schedule: ifcopenshell.entity_instance, name: Union[str, None]) -> None: # create work schedule if not work_schedule.PredefinedType == "PLANNED": return baseline_work_schedule = ifcopenshell.api.sequence.add_work_schedule( self.file, name=work_schedule.Name, predefined_type="BASELINE" ) - baseline_work_schedule.Name = self.settings["name"] + baseline_work_schedule.Name = name self.create_baseline_reference(work_schedule, baseline_work_schedule) for summary_task in ifcopenshell.util.sequence.get_root_tasks(work_schedule): current, duplicate = ifcopenshell.api.sequence.duplicate_task(self.file, task=summary_task) @@ -88,7 +82,9 @@ class Usecase: for i, task in enumerate(current): self.create_baseline_reference(task, duplicate[i]) - def create_baseline_reference(self, relating_object, related_object): + def create_baseline_reference( + self, relating_object: ifcopenshell.entity_instance, related_object: ifcopenshell.entity_instance + ) -> ifcopenshell.entity_instance: referenced_by = None if relating_object.Declares: referenced_by = relating_object.Declares[0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py index db47ccd5ec..7f974bb84e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py @@ -23,9 +23,12 @@ import ifcopenshell.api.owner import ifcopenshell.api.sequence import ifcopenshell.util.element import ifcopenshell.util.sequence +from typing import Union, Any -def duplicate_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: +def duplicate_task( + file: ifcopenshell.file, task: ifcopenshell.entity_instance +) -> Union[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]]: """Duplicates a task in the project The following relationships are also duplicated: @@ -35,9 +38,7 @@ def duplicate_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) * The copy will have duplicated nested tasks :param task: The task to be duplicated - :type task: ifcopenshell.entity_instance :return: The duplicated task or the list of duplicated tasks if the latter has children - :rtype: ifcopenshell.entity_instance or list of ifcopenshell.entity_instance Example: .. code:: python @@ -55,6 +56,9 @@ def duplicate_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.tracker = {"current": [], "duplicate": []} self.duplicate_task(self.settings["task"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py index d3c8a4418a..d55bf0e4b5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_lag_time.py @@ -28,11 +28,8 @@ def edit_lag_time(file: ifcopenshell.file, lag_time: ifcopenshell.entity_instanc IfcLagTime, consult the IFC documentation. :param lag_time: The IfcLagTime entity you want to edit - :type lag_time: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -75,14 +72,12 @@ def edit_lag_time(file: ifcopenshell.file, lag_time: ifcopenshell.entity_instanc # Or, let's make it 2 days instead. ifcopenshell.api.sequence.edit_lag_time(model, lag_time=lag, attributes={"LagValue": "P2D"}) """ - settings = {"lag_time": lag_time, "attributes": attributes} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if name == "LagValue" and value is not None: if isinstance(value, float): value = file.createIfcRatioMeasure(value) else: value = file.createIfcDuration(ifcopenshell.util.date.datetime2ifc(value, "IfcDuration")) - setattr(settings["lag_time"], name, value) - for rel in [r for r in file.get_inverse(settings["lag_time"]) if r.is_a("IfcRelSequence")]: + setattr(lag_time, name, value) + for rel in [r for r in file.get_inverse(lag_time) if r.is_a("IfcRelSequence")]: ifcopenshell.api.sequence.cascade_schedule(file, task=rel.RelatedProcess) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py index 6b165f716a..519c5cdf25 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_recurrence_pattern.py @@ -30,11 +30,8 @@ def edit_recurrence_pattern( IfcRecurrencePattern, consult the IFC documentation. :param recurrence_pattern: The IfcRecurrencePattern entity you want to edit - :type recurrence_pattern: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -55,13 +52,8 @@ def edit_recurrence_pattern( ifcopenshell.api.sequence.edit_recurrence_pattern(model, recurrence_pattern=pattern, attributes={"WeekdayComponent": [1, 2, 3, 4, 5]}) """ - settings = { - "recurrence_pattern": recurrence_pattern, - "attributes": attributes, - } - - for name, value in settings["attributes"].items(): - setattr(settings["recurrence_pattern"], name, value) + for name, value in attributes.items(): + setattr(recurrence_pattern, name, value) ifcopenshell.util.sequence.is_working_day.cache_clear() ifcopenshell.util.sequence.is_calendar_applicable.cache_clear() diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py index dee8a32e64..2686ec5ace 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_sequence.py @@ -30,11 +30,8 @@ def edit_sequence( IfcRelSequence, consult the IFC documentation. :param rel_sequence: The IfcRelSequence entity you want to edit - :type rel_sequence: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -62,9 +59,7 @@ def edit_sequence( ifcopenshell.api.sequence.edit_sequence(model, rel_sequence=sequence, attributes={"SequenceType": "START_START"}) """ - settings = {"rel_sequence": rel_sequence, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["rel_sequence"], name, value) - if "SequenceType" in settings["attributes"].keys(): - ifcopenshell.api.sequence.cascade_schedule(file, task=settings["rel_sequence"].RelatedProcess) + for name, value in attributes.items(): + setattr(rel_sequence, name, value) + if "SequenceType" in attributes.keys(): + ifcopenshell.api.sequence.cascade_schedule(file, task=rel_sequence.RelatedProcess) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py index dcd218059f..3fa1b88caf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task.py @@ -26,11 +26,8 @@ def edit_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance, attri IfcTask, consult the IFC documentation. :param task: The IfcTask entity you want to edit - :type task: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -48,7 +45,5 @@ def edit_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance, attri # Change the identification ifcopenshell.api.sequence.edit_task(model, task=task, attributes={"Identification": "M"}) """ - settings = {"task": task, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["task"], name, value) + for name, value in attributes.items(): + setattr(task, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py index 6d9d3404d1..119ab82a14 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_task_time.py @@ -36,11 +36,8 @@ def edit_task_time( IfcTaskTime, consult the IFC documentation. :param task_time: The IfcTaskTime entity you want to edit - :type task_time: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -61,94 +58,89 @@ def edit_task_time( """ usecase = Usecase() usecase.file = file - usecase.settings = {"task_time": task_time, "attributes": attributes} - return usecase.execute() + return usecase.execute(task_time, attributes) class Usecase: - def execute(self): + file: ifcopenshell.file + + def execute(self, task_time: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: + self.task_time = task_time self.task = self.get_task() self.calendar = ifcopenshell.util.sequence.derive_calendar(self.task) # If the user specifies both an end date and a duration, the duration takes priority - if ( - self.settings["attributes"].get("ScheduleDuration", None) - and "ScheduleFinish" in self.settings["attributes"].keys() - ): - del self.settings["attributes"]["ScheduleFinish"] + if attributes.get("ScheduleDuration", None) and "ScheduleFinish" in attributes.keys(): + del attributes["ScheduleFinish"] - duration_type = self.settings["attributes"].get("DurationType", self.settings["task_time"].DurationType) - finish = self.settings["attributes"].get("ScheduleFinish", None) + duration_type = attributes.get("DurationType", self.task_time.DurationType) + finish = attributes.get("ScheduleFinish", None) if finish: if isinstance(finish, str): finish = datetime.datetime.fromisoformat(finish) - self.settings["attributes"]["ScheduleFinish"] = datetime.datetime.combine( + attributes["ScheduleFinish"] = datetime.datetime.combine( ifcopenshell.util.sequence.get_soonest_working_day(finish, duration_type, self.calendar), datetime.time(17), ) - start = self.settings["attributes"].get("ScheduleStart", None) + start = attributes.get("ScheduleStart", None) if start: if isinstance(start, str): start = datetime.datetime.fromisoformat(start) - self.settings["attributes"]["ScheduleStart"] = datetime.datetime.combine( + attributes["ScheduleStart"] = datetime.datetime.combine( ifcopenshell.util.sequence.get_soonest_working_day(start, duration_type, self.calendar), datetime.time(9), ) - for name, value in self.settings["attributes"].items(): + for name, value in attributes.items(): if value is not None: if "Start" in name or "Finish" in name or name == "StatusTime": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") elif name == "ScheduleDuration" or name == "ActualDuration" or name == "RemainingTime": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") - setattr(self.settings["task_time"], name, value) + setattr(self.task_time, name, value) - if ( - "ScheduleDuration" in self.settings["attributes"].keys() - and self.settings["task_time"].ScheduleDuration - and self.settings["task_time"].ScheduleStart - ): + if "ScheduleDuration" in attributes.keys() and task_time.ScheduleDuration and task_time.ScheduleStart: self.calculate_finish() - elif self.settings["attributes"].get("ScheduleStart", None) and self.settings["task_time"].ScheduleDuration: + elif attributes.get("ScheduleStart", None) and task_time.ScheduleDuration: self.calculate_finish() - elif self.settings["attributes"].get("ScheduleFinish", None) and self.settings["task_time"].ScheduleStart: + elif attributes.get("ScheduleFinish", None) and task_time.ScheduleStart: self.calculate_duration() - if self.settings["task_time"].ScheduleDuration and ( - "ScheduleStart" in self.settings["attributes"].keys() - or "ScheduleFinish" in self.settings["attributes"].keys() - or "ScheduleDuration" in self.settings["attributes"].keys() + if task_time.ScheduleDuration and ( + "ScheduleStart" in attributes.keys() + or "ScheduleFinish" in attributes.keys() + or "ScheduleDuration" in attributes.keys() ): ifcopenshell.api.sequence.cascade_schedule(self.file, task=self.task) - if self.settings["task_time"].ScheduleDuration: + if task_time.ScheduleDuration: self.handle_resource_calculation() def calculate_finish(self): finish = ifcopenshell.util.sequence.get_start_or_finish_date( - ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart), - ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleDuration), - self.settings["task_time"].DurationType, + ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleStart), + ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleDuration), + self.task_time.DurationType, self.calendar, date_type="FINISH", ) - self.settings["task_time"].ScheduleFinish = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime") + self.task_time.ScheduleFinish = ifcopenshell.util.date.datetime2ifc(finish, "IfcDateTime") def calculate_duration(self): - start = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleStart) - finish = ifcopenshell.util.date.ifc2datetime(self.settings["task_time"].ScheduleFinish) + start = ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleStart) + finish = ifcopenshell.util.date.ifc2datetime(self.task_time.ScheduleFinish) current_date = datetime.date(start.year, start.month, start.day) finish_date = datetime.date(finish.year, finish.month, finish.day) duration = datetime.timedelta(days=1) while current_date < finish_date: - if self.settings["task_time"].DurationType == "ELAPSEDTIME" or not self.calendar: + if self.task_time.DurationType == "ELAPSEDTIME" or not self.calendar: duration += datetime.timedelta(days=1) elif ifcopenshell.util.sequence.is_working_day(current_date, self.calendar): duration += datetime.timedelta(days=1) current_date += datetime.timedelta(days=1) - self.settings["task_time"].ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcDuration") + self.task_time.ScheduleDuration = ifcopenshell.util.date.datetime2ifc(duration, "IfcDuration") def get_task(self) -> ifcopenshell.entity_instance: - return next(e for e in self.file.get_inverse(self.settings["task_time"]) if e.is_a("IfcTask")) + return next(e for e in self.file.get_inverse(self.task_time) if e.is_a("IfcTask")) def handle_resource_calculation(self): resources = ifcopenshell.util.sequence.get_task_resources(self.task, is_deep=False) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py index f82f53e534..83e56a4372 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_calendar.py @@ -28,11 +28,8 @@ def edit_work_calendar( IfcWorkCalendar, consult the IFC documentation. :param work_calendar: The IfcWorkCalendar entity you want to edit - :type work_calendar: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -45,7 +42,5 @@ def edit_work_calendar( ifcopenshell.api.sequence.edit_work_calendar(model, work_calendar=calendar, attributes={"Description": "Monday to Friday 8 hour days"}) """ - settings = {"work_calendar": work_calendar, "attributes": attributes} - - for name, value in settings["attributes"].items(): - setattr(settings["work_calendar"], name, value) + for name, value in attributes.items(): + setattr(work_calendar, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py index 920e2c671e..145460e11c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_plan.py @@ -29,11 +29,8 @@ def edit_work_plan( IfcWorkPlan, consult the IFC documentation. :param work_plan: The IfcWorkPlan entity you want to edit - :type work_plan: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -46,12 +43,10 @@ def edit_work_plan( ifcopenshell.api.sequence.edit_work_plan(model, work_plan=work_plan, attributes={"Description": "Construction of phase 1"}) """ - settings = {"work_plan": work_plan, "attributes": attributes} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if value: if "Date" in name or "Time" in name: value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") elif name == "Duration" or name == "TotalFloat": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") - setattr(settings["work_plan"], name, value) + setattr(work_plan, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py index 5ed392ddff..98905b9a1f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_schedule.py @@ -29,11 +29,8 @@ def edit_work_schedule( IfcWorkSchedule, consult the IFC documentation. :param work_schedule: The IfcWorkSchedule entity you want to edit - :type work_schedule: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -50,12 +47,10 @@ def edit_work_schedule( ifcopenshell.api.sequence.edit_work_schedule(model, work_schedule=work_schedule, attributes={"Description": "3 crane design option"}) """ - settings = {"work_schedule": work_schedule, "attributes": attributes} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if value: if "Date" in name or "Time" in name: value = ifcopenshell.util.date.datetime2ifc(value, "IfcDateTime") elif name == "Duration" or name == "TotalFloat": value = ifcopenshell.util.date.datetime2ifc(value, "IfcDuration") - setattr(settings["work_schedule"], name, value) + setattr(work_schedule, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py index 07c1f818d5..7fae8b4c18 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/edit_work_time.py @@ -31,11 +31,8 @@ def edit_work_time( IfcWorkTime, consult the IFC documentation. :param work_time: The IfcWorkTime entity you want to edit - :type work_time: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -54,16 +51,14 @@ def edit_work_time( ifcopenshell.api.sequence.edit_work_time(model, work_time=work_time, attributes={"StartDate": "2000-01-01", "FinishDate": "2000-01-02"}) """ - settings = {"work_time": work_time, "attributes": attributes} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if name in ("Start", "StartDate"): value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate") # 4 IfcWorktime Start - settings["work_time"][4] = value + work_time[4] = value elif name in ("Finish", "FinishDate"): value = ifcopenshell.util.date.datetime2ifc(value, "IfcDate") # 5 IfcWorktime Finish - settings["work_time"][5] = value + work_time[5] = value else: - setattr(settings["work_time"], name, value) + setattr(work_time, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py index a7fb9d01e6..14e82822e3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/recalculate_schedule.py @@ -35,9 +35,7 @@ def recalculate_schedule(file: ifcopenshell.file, work_schedule: ifcopenshell.en error. :param work_schedule: The IfcWorkSchedule to perform the calculation on. - :type work_schedule: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -50,12 +48,14 @@ def recalculate_schedule(file: ifcopenshell.file, work_schedule: ifcopenshell.en """ usecase = Usecase() usecase.file = file - usecase.settings = {"work_schedule": work_schedule} - return usecase.execute() + return usecase.execute(work_schedule) class Usecase: - def execute(self): + file: ifcopenshell.file + + def execute(self, work_schedule: ifcopenshell.entity_instance) -> None: + self.work_schedule = work_schedule # The method implemented is the same as shown here: # https://www.youtube.com/watch?v=qTErIV6OqLg self.start_dates = [] @@ -88,7 +88,6 @@ class Usecase: if is_cyclic: raise RecursionError("Task graph is cyclic and so critical path method cannot be performed.") - return self.pending_nodes = set(self.g.nodes) while self.pending_nodes: @@ -100,7 +99,7 @@ class Usecase: self.update_task_times() - def build_network_graph(self): + def build_network_graph(self) -> None: self.sequence_type_map = { None: "FS", "START_START": "SS", @@ -114,14 +113,14 @@ class Usecase: self.edges = [] self.g.add_node("start", duration=0, duration_type="ELAPSEDTIME", calendar=None) self.g.add_node("finish", duration=0, duration_type="ELAPSEDTIME", calendar=None) - for rel in self.settings["work_schedule"].Controls: + for rel in self.work_schedule.Controls: for related_object in rel.RelatedObjects: if not related_object.is_a("IfcTask"): continue self.add_node(related_object) self.g.add_edges_from(self.edges) - def add_node(self, task): + def add_node(self, task: ifcopenshell.entity_instance) -> None: if task.IsNestedBy: for rel in task.IsNestedBy: [self.add_node(o) for o in rel.RelatedObjects] @@ -176,7 +175,7 @@ class Usecase: if not successor_types: self.edges.append((task.id(), "finish", {"lag_time": 0, "type": "FF"})) - def update_task_times(self): + def update_task_times(self) -> None: for ifc_definition_id in self.g.nodes: if ifc_definition_id in ("start", "finish"): continue @@ -198,12 +197,12 @@ class Usecase: }, ) - def offset_date(self, date, days, node): + def offset_date(self, date: datetime.datetime, days: int, node: dict) -> datetime.datetime: return ifcopenshell.util.sequence.offset_date( date, datetime.timedelta(days=days), node["duration_type"], node["calendar"] ) - def forward_pass(self, node): + def forward_pass(self, node) -> bool: successors = self.g.successors(node) predecessors = list(self.g.predecessors(node)) data = self.g.nodes[node] @@ -326,7 +325,7 @@ class Usecase: return True - def backward_pass(self, node): + def backward_pass(self, node) -> bool: successors = list(self.g.successors(node)) predecessors = self.g.predecessors(node) data = self.g.nodes[node] @@ -496,12 +495,12 @@ class Usecase: def calculate_free_float( self, - predecessor_date, - successor_date, - lag_time, - predecessor_data, - successor_data, - ): + predecessor_date: datetime.datetime, + successor_date: datetime.datetime, + lag_time: int, + predecessor_data: dict, + successor_data: dict, + ) -> datetime.timedelta: if not lag_time: min_successor_date = successor_date else: diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py index bb31cd7c7d..abd66e7fa8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/dereference_structure.py @@ -29,12 +29,10 @@ def dereference_structure( """Dereferences a list of products and space :param products: The list of physical IfcElements that exists in the space. - :type products: list[ifcopenshell.entity_instance] :param relating_structure: The IfcSpatialStructureElement element, such as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element exists in. :return: None - :rtype: None Example: @@ -68,14 +66,12 @@ def dereference_structure( # Actually, it only goes up to storey 2. ifcopenshell.api.spatial.dereference_structure(model, products=[column], relating_structure=storey3) """ - settings = {"products": products, "relating_structure": relating_structure} - - products = set(settings["products"]) - for rel in settings["relating_structure"].ReferencesElements: + products_set = set(products) + for rel in relating_structure.ReferencesElements: related_elements = set(rel.RelatedElements) - if not related_elements.intersection(products): + if not related_elements.intersection(products_set): continue - related_elements = related_elements - products + related_elements = related_elements - products_set if related_elements: rel.RelatedElements = list(related_elements) ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py index 2897219e68..41452244d1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/reference_structure.py @@ -46,14 +46,11 @@ def reference_structure( spaces simultaneously. :param products: The list of physical IfcElements that exists in the space. - :type products: list[ifcopenshell.entity_instance] :param relating_structure: The IfcSpatialStructureElement element, such as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element exists in. - :type relating_structure: ifcopenshell.entity_instance :return: The IfcRelReferencedInSpatialStructure relationship instance or `None` if `products` was an empty list. - :rtype: Union[ifcopenshell.entity_instance, None] Example: @@ -85,19 +82,16 @@ def reference_structure( model, products=[column], relating_structure=[storey2, storey3] ) """ - settings = { - "products": products, - "relating_structure": relating_structure, - } - structure = settings["relating_structure"] - products = set(settings["products"]) + structure = relating_structure + products_set = set(products) - if not products: + if not products_set: return referenced = ifcopenshell.util.element.get_structure_referenced_elements(structure) - products_to_assign = products - referenced + products_to_assign = products_set - referenced + rel: Union[ifcopenshell.entity_instance, None] rel = next(iter(structure.ReferencesElements), None) if not products_to_assign: diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py index 4b7d94a4a2..65c03eb238 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_analysis_model.py @@ -28,14 +28,8 @@ def edit_structural_analysis_model( IfcStructuralAnalysisModel, consult the IFC documentation. :param structural_analysis_model: The IfcStructuralAnalysisModel entity you want to edit - :type structural_analysis_model: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None """ - settings = {"structural_analysis_model": structural_analysis_model, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["structural_analysis_model"], name, value) - return settings["structural_analysis_model"] + for name, value in attributes.items(): + setattr(structural_analysis_model, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py index 54963415df..3d33e4e8ad 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_boundary_condition.py @@ -28,19 +28,14 @@ def edit_structural_boundary_condition( IfcBoundaryCondition, consult the IFC documentation. :param condition: The IfcBoundaryCondition entity you want to edit - :type condition: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None """ - settings = {"condition": condition, "attributes": attributes} - - for name, data in settings["attributes"].items(): + for name, data in attributes.items(): if data["type"] == "string" or data["type"] == "null": value = data["value"] elif data["type"] == "IfcBoolean": value = file.createIfcBoolean(data["value"]) else: value = file.create_entity(data["type"], data["value"]) - setattr(settings["condition"], name, value) + setattr(condition, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py index 8b90457a47..1b7105d95d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py @@ -16,25 +16,21 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell +from ifcopenshell.util.shape_builder import VectorType, ifc_safe_vector_type def edit_structural_item_axis( file: ifcopenshell.file, structural_item: ifcopenshell.entity_instance, - axis: tuple[float, float, float] = (0.0, 0.0, 1.0), + axis: VectorType = (0.0, 0.0, 1.0), ) -> None: """Edits the coordinate system of a structural connection :param structural_item: The IfcStructuralItem you want to modify. - :type structural_item: ifcopenshell.entity_instance :param axis: The unit Z axis vector defined as a list of 3 floats. Defaults to (0., 0., 1.). - :type axis: tuple[float, float, float] :return: None - :rtype: None """ - settings = {"structural_item": structural_item, "axis": axis} - - if len(file.get_inverse(settings["structural_item"].Axis)) == 1: - file.remove(settings["structural_item"].Axis) - settings["structural_item"].Axis = file.createIfcDirection(settings["axis"]) + if len(file.get_inverse(axis_dir := structural_item.Axis)) == 1: + file.remove(axis_dir) + structural_item.Axis = file.create_entity("IfcDirection", ifc_safe_vector_type(axis)) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py index 20298d1c0f..faa218a7a3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load.py @@ -28,13 +28,8 @@ def edit_structural_load( IfcStructuralLoad, consult the IFC documentation. :param structural_load: The IfcStructuralLoad entity you want to edit - :type structural_load: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None """ - settings = {"structural_load": structural_load, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["structural_load"], name, value) + for name, value in attributes.items(): + setattr(structural_load, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py index 59231fb9e4..02948c637d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_load_case.py @@ -28,13 +28,8 @@ def edit_structural_load_case( IfcStructuralLoadCase, consult the IFC documentation. :param load_case: The IfcStructuralLoadCase entity you want to edit - :type load_case: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None """ - settings = {"load_case": load_case, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["load_case"], name, value) + for name, value in attributes.items(): + setattr(load_case, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py index 408f8e7906..64d38cfd2f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py @@ -82,17 +82,13 @@ def add_surface_style( :param style: The IfcSurfaceStyle you want to add to presentation item to. See ifcopenshell.api.style.add_style. - :type style: ifcopenshell.entity_instance :param ifc_class: Choose from IfcSurfaceStyleShading, IfcSurfaceStyleRendering, IfcSurfaceStyleWithTextures, IfcSurfaceStyleLighting, IfcSurfaceStyleReflectance, or IfcExternallyDefinedSurfaceStyle. - :type ifc_class: str :param attributes: a dictionary of attribute names and values. - :type attributes: dict, optional :return: The newly created presentation item based on the provided ifc_class. - :rtype: ifcopenshell.entity_instance Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py index 3ecc91177f..f870a3006e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.api.style import ifcopenshell.util.element +from typing import Any def assign_material_style( @@ -115,6 +116,9 @@ def assign_material_style( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): self.style = self.settings["style"] if self.file.schema == "IFC2X3" or self.settings["should_use_presentation_style_assignment"]: diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py index d1a42e5d9e..7a39629528 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/edit_presentation_style.py @@ -28,11 +28,8 @@ def edit_presentation_style( IfcPresentationStyle, consult the IFC documentation. :param style: The IfcPresentationStyle entity you want to edit - :type style: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -44,7 +41,5 @@ def edit_presentation_style( # Change the name of the style to "Foo" ifcopenshell.api.style.edit_presentation_style(model, style=style, attributes={"Name": "Foo"}) """ - settings = {"style": style, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["style"], name, value) + for name, value in attributes.items(): + setattr(style, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py index c5be1d757f..40f4fcedf4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell -from typing import Any +from typing import Any, Union def edit_surface_style( @@ -36,11 +36,8 @@ def edit_surface_style( example below. :param style: The IfcPresentationStyle entity you want to edit - :type style: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -74,14 +71,18 @@ def edit_surface_style( """ usecase = Usecase() usecase.file = file - usecase.settings = {"style": style, "attributes": attributes or {}} - return usecase.execute() + return usecase.execute(style, attributes) class Usecase: - def execute(self): + file: ifcopenshell.file + settings: dict[str, Any] + + def execute(self, style: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: + self.style = style + attributes = {} - for attribute in self.settings["style"].wrapped_data.declaration().as_entity().all_attributes(): + for attribute in style.wrapped_data.declaration().as_entity().all_attributes(): attribute_type = attribute.type_of_attribute() if attribute_type.as_aggregation_type() is None: attribute_type = attribute_type.declared_type().name() @@ -90,7 +91,7 @@ class Usecase: attribute_type = attribute_type.type_of_element() attributes[attribute.name()] = attribute_type - for key, value in self.settings["attributes"].items(): + for key, value in attributes.items(): attribute_class = attributes.get(key) if attribute_class == "IfcColourRgb": self.edit_colour_rgb(key, value) @@ -101,39 +102,37 @@ class Usecase: else: setattr(self.settings["style"], key, value) - def edit_colour_rgb(self, name, value: dict): - if (attribute := getattr(self.settings["style"], name)) is None: + def edit_colour_rgb(self, name: str, value: dict[str, Any]): + if (attribute := getattr(self.style, name)) is None: attribute = self.file.createIfcColourRgb() - setattr(self.settings["style"], name, attribute) + setattr(self.style, name, attribute) attribute.Name = value.get("Name", None) attribute.Red = value["Red"] attribute.Green = value["Green"] attribute.Blue = value["Blue"] - def edit_colour_or_factor(self, name, value): + def edit_colour_or_factor(self, name: str, value: Union[dict[str, Any], ifcopenshell.entity_instance, None]): if isinstance(value, dict): - attribute = getattr(self.settings["style"], name) + attribute = getattr(self.style, name) if not attribute or not attribute.is_a("IfcColourRgb"): colour = self.file.createIfcColourRgb(None, 0, 0, 0) - setattr(self.settings["style"], name, colour) - attribute = getattr(self.settings["style"], name) + setattr(self.style, name, colour) + attribute = getattr(self.style, name) attribute[1] = value["Red"] attribute[2] = value["Green"] attribute[3] = value["Blue"] else: # assume it's float value for IfcNormalisedRatioMeasure or None - existing_value = getattr(self.settings["style"], name) + existing_value = getattr(self.style, name) if existing_value and existing_value.id(): self.file.remove(existing_value) if value is not None: - value = self.file.createIfcNormalisedRatioMeasure(value) - setattr(self.settings["style"], name, value) + value = self.file.create_entity("IfcNormalisedRatioMeasure", value) + setattr(self.style, name, value) - def edit_specular_highlight(self, value): + def edit_specular_highlight(self, value: Union[dict[str, Any], None]) -> None: if value is None: - self.settings["style"].SpecularHighlight = None + self.style.SpecularHighlight = None elif value.get("IfcSpecularExponent", None): - self.settings["style"].SpecularHighlight = self.file.createIfcSpecularExponent(value["IfcSpecularExponent"]) + self.style.SpecularHighlight = self.file.createIfcSpecularExponent(value["IfcSpecularExponent"]) elif value.get("IfcSpecularRoughness", None): - self.settings["style"].SpecularHighlight = self.file.createIfcSpecularRoughness( - value["IfcSpecularRoughness"] - ) + self.style.SpecularHighlight = self.file.createIfcSpecularRoughness(value["IfcSpecularRoughness"]) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py index 3ee4cbf6d5..c72cca657e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/remove_style.py @@ -41,7 +41,6 @@ def remove_style(file: ifcopenshell.file, style: ifcopenshell.entity_instance) - """ usecase = Usecase() usecase.file = file - usecase.settings = {"style": style} return usecase.execute(style) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py index bad29c878f..19bb9e1b79 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/remove_surface_style.py @@ -24,9 +24,7 @@ def remove_surface_style(file: ifcopenshell.file, style: ifcopenshell.entity_ins """Removes a presentation item from a presentation style :param style: The IfcPresentationItem to remove. - :type style: ifcopenshell.entity_instance :return: None - :rtype: None Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py b/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py index f56fad763c..4141adbacc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/unassign_representation_styles.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell +from typing import Any def unassign_representation_styles( @@ -63,6 +64,9 @@ def unassign_representation_styles( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if not self.settings["styles"]: return [] diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py index 9a1bc76f43..b0b39cfd2b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/connect_port.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.guid import ifcopenshell.util.element -from typing import Optional +from typing import Optional, Any def connect_port( @@ -110,6 +110,9 @@ def connect_port( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): # Note: there are a number of ambiguities with port connectivity. We # assume system topology is represented by a directed graph. In other diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py index 00311b7ac0..c90894ec7f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/edit_system.py @@ -26,11 +26,8 @@ def edit_system(file: ifcopenshell.file, system: ifcopenshell.entity_instance, a IfcSystem, consult the IFC documentation. :param system: The IfcSystem entity you want to edit - :type system: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -42,8 +39,5 @@ def edit_system(file: ifcopenshell.file, system: ifcopenshell.entity_instance, a # Change the name of the system to "HW" for Hot Water ifcopenshell.api.system.edit_system(model, system=system, attributes={"Name": "HW"}) """ - - settings = {"system": system, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["system"], name, value) + for name, value in attributes.items(): + setattr(system, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py index 1bf459920f..b5b69e276c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.util.element +from typing import Any def unassign_port( @@ -62,6 +63,9 @@ def unassign_port( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if self.file.schema == "IFC2X3": return self.execute_ifc2x3() diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py index 2a20f57f2f..f4bfae022e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py @@ -22,7 +22,7 @@ import ifcopenshell.api.owner import ifcopenshell.api.material import ifcopenshell.guid import ifcopenshell.util.element -from typing import Union, Iterable +from typing import Union, Iterable, Any def assign_type( @@ -186,6 +186,9 @@ def assign_type( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): if not self.settings["related_objects"]: return diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py index 8a68e5727f..0fba187c75 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_derived_unit.py @@ -46,11 +46,8 @@ def add_derived_unit( :type unit_type: str :param userdefinedtype: The user defined type in case of choosing USERDEFINED, or None for no user defined type. - :type userdefinedtype: str or None :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: The newly created IfcDerivedUnit - :rtype: ifcopenshell.entity_instance Example: @@ -69,15 +66,13 @@ def add_derived_unit( #12=IfcDerivedUnit((#10,#11),.LINEARVELOCITY.,$) """ - settings = {"unit_type": unit_type, "attributes": attributes} - derive_unit_elements = [] - for named_unit in settings["attributes"]: + for named_unit in attributes: derive_unit_elements.append( - file.create_entity("IfcDerivedUnitElement", Unit=named_unit, Exponent=settings["attributes"][named_unit]) + file.create_entity("IfcDerivedUnitElement", Unit=named_unit, Exponent=attributes[named_unit]) ) return file.create_entity( - "IfcDerivedUnit", Elements=derive_unit_elements, UnitType=settings["unit_type"], UserDefinedType=userdefinedtype + "IfcDerivedUnit", Elements=derive_unit_elements, UnitType=unit_type, UserDefinedType=userdefinedtype ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py index 4e2dc3f848..d57acbd435 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/assign_unit.py @@ -18,7 +18,7 @@ import ifcopenshell import ifcopenshell.util.unit -from typing import Optional +from typing import Optional, Any def assign_unit( @@ -75,6 +75,9 @@ def assign_unit( class Usecase: + file: ifcopenshell.file + settings: dict[str, Any] + def execute(self): # We're going to refactor this to split unit creation and assignment if self.settings["units"]: diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py index a8b3316dbd..5bc9bbd316 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_derived_unit.py @@ -26,13 +26,8 @@ def edit_derived_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instanc IfcDerivedUnit, consult the IFC documentation. :param unit: The IfcDerivedUnit entity you want to edit - :type unit: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None """ - settings = {"unit": unit, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["unit"], name, value) + for name, value in attributes.items(): + setattr(unit, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py index 4fdfe6ec29..bccc61afc0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_monetary_unit.py @@ -26,11 +26,8 @@ def edit_monetary_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instan IfcMonetaryUnit, consult the IFC documentation. :param unit: The IfcMonetaryUnit entity you want to edit - :type unit: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -43,7 +40,5 @@ def edit_monetary_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instan # Ah who are we kidding ifcopenshell.api.unit.edit_monetary_unit(model, unit=zwl, attributes={"Currency": "USD"}) """ - settings = {"unit": unit, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): - setattr(settings["unit"], name, value) + for name, value in attributes.items(): + setattr(unit, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py index e439e2c476..ccd0e96d54 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py @@ -29,11 +29,8 @@ def edit_named_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance, IfcNamedUnit, consult the IFC documentation. :param unit: The IfcNamedUnit entity you want to edit - :type unit: ifcopenshell.entity_instance :param attributes: a dictionary of attribute names and values. - :type attributes: dict :return: None - :rtype: None Example: @@ -45,15 +42,13 @@ def edit_named_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance, # Uh, crates? Boxes? Whatever. ifcopenshell.api.unit.edit_named_unit(model, unit=unit, attibutes={"Name": "CRATES"}) """ - settings = {"unit": unit, "attributes": attributes or {}} - - for name, value in settings["attributes"].items(): + for name, value in attributes.items(): if name == "Dimensions": - dimensions = settings["unit"].Dimensions + dimensions = unit.Dimensions if len(file.get_inverse(dimensions)) > 1: - settings["unit"].Dimensions = file.createIfcDimensionalExponents(*value) + unit.Dimensions = file.createIfcDimensionalExponents(*value) else: for i, exponent in enumerate(value): dimensions[i] = exponent continue - setattr(settings["unit"], name, value) + setattr(unit, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py index 17eee6d4be..8e0335b77e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py @@ -23,9 +23,7 @@ def unassign_unit(file: ifcopenshell.file, units: Optional[list[ifcopenshell.ent """Unassigns units as default units for the project :param units: A list of units to assign as project defaults. - :type units: list[ifcopenshell.entity_instance],optional :return: None - :rtype: None Example: @@ -44,15 +42,12 @@ def unassign_unit(file: ifcopenshell.file, units: Optional[list[ifcopenshell.ent # Actually, we don't need areas. ifcopenshell.api.unit.unassign_unit(model, units=[area]) """ - settings = {"units": units} - - unit_assignment = file.by_type("IfcUnitAssignment") - if not unit_assignment: + unit_assignments = file.by_type("IfcUnitAssignment") + if not unit_assignments: return - unit_assignment = unit_assignment[0] - units = set(unit_assignment.Units or []) - units = units - set(settings["units"]) - if units: - unit_assignment.Units = list(units) - return unit_assignment + unit_assignment = unit_assignments[0] + units_set = set(unit_assignment.Units or []) + units_set = units_set - set(units or []) + if units_set: + unit_assignment.Units = list(units_set) file.remove(unit_assignment) diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index 04c6c2ce50..d3c94b616d 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -24,6 +24,7 @@ import zipfile import functools import ifcopenshell from pathlib import Path +from typing import Any from typing import Callable from typing import Generator from typing import Optional diff --git a/src/ifcopenshell-python/ifcopenshell/util/placement.py b/src/ifcopenshell-python/ifcopenshell/util/placement.py index d6caa1b657..5f8ad154b6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/placement.py +++ b/src/ifcopenshell-python/ifcopenshell/util/placement.py @@ -137,9 +137,7 @@ def get_cartesiantransformationoperator3d(inst: ifcopenshell.entity_instance) -> ``get_mappeditem_transformation`` instead. :param item: The IfcCartesianTransformationOperator entity - :type item: ifcopenshell.entity_instance :return: A 4x4 numpy transformation matrix - :rtype: MatrixType """ origin = np.array(inst.LocalOrigin.Coordinates) axis1 = np.array((1.0, 0.0, 0.0)) From a68cb746996827468683c71541bed835d65e4f96 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 18 Feb 2025 15:18:18 +0500 Subject: [PATCH 058/476] ifcopenshell.alignment - fix error in Python 3.9 match statement was added only in Python 3.10 --- .../ifcopenshell/alignment.py | 109 +++++++++--------- 1 file changed, 52 insertions(+), 57 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/alignment.py b/src/ifcopenshell-python/ifcopenshell/alignment.py index a40151068d..d0e46fe16f 100644 --- a/src/ifcopenshell-python/ifcopenshell/alignment.py +++ b/src/ifcopenshell-python/ifcopenshell/alignment.py @@ -203,67 +203,62 @@ class IfcAlignmentHelper: else: transition = "CONTSAMEGRADIENTSAMECURVATURE" - match _type: - case "LINE": - parent_curve = self._file.create_entity( - type="IfcLine", - Pnt=self._file.create_entity( - type="IfcCartesianPoint", - Coordinates=(0.0, 0.0), + if _type == "LINE": + parent_curve = self._file.create_entity( + type="IfcLine", + Pnt=self._file.create_entity( + type="IfcCartesianPoint", + Coordinates=(0.0, 0.0), + ), + Dir=self._file.create_entity( + type="IfcVector", + Orientation=self._file.create_entity( + type="IfcDirection", + DirectionRatios=(1.0, 0.0), ), - Dir=self._file.create_entity( - type="IfcVector", - Orientation=self._file.create_entity( - type="IfcDirection", - DirectionRatios=(1.0, 0.0), - ), - Magnitude=1.0, + Magnitude=1.0, + ), + ) + curve_segment = self._file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=self._file.createIfcDirection( + (math.cos(start_direction), math.sin(start_direction)), ), - ) - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=start_point, - RefDirection=self._file.createIfcDirection( - (math.cos(start_direction), math.sin(start_direction)), - ), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(length), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - case "CIRCULARARC": - parent_curve = self._file.createIfcCircle( - Position=self._file.createIfcAxis2Placement2D( - Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)), - RefDirection=self._file.createIfcDirection( - (math.cos(start_direction), math.sin(start_direction)) - ), - ), - Radius=abs(start_radius), - ) + ), + SegmentStart=self._file.createIfcLengthMeasure(0.0), + SegmentLength=self._file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + result = (curve_segment, None) + elif _type == "CIRCULARARC": + parent_curve = self._file.createIfcCircle( + Position=self._file.createIfcAxis2Placement2D( + Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)), + RefDirection=self._file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + Radius=abs(start_radius), + ) - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=start_point, - RefDirection=self._file.createIfcDirection( - (math.cos(start_direction), math.sin(start_direction)) - ), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(length * start_radius / abs(start_radius)), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) + curve_segment = self._file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=self._file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=self._file.createIfcLengthMeasure(0.0), + SegmentLength=self._file.createIfcLengthMeasure(length * start_radius / abs(start_radius)), + ParentCurve=parent_curve, + ) + result = (curve_segment, None) - case _: - result = (None, None) + else: + result = (None, None) return result From 8ea31424517c0078a3e2e7e1822809267ca9771e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 18 Feb 2025 16:06:51 +0500 Subject: [PATCH 059/476] Fix errors removing layers/constituents after eeac0a2 --- .../ifcopenshell/api/material/remove_constituent.py | 4 ++-- .../ifcopenshell/api/material/remove_layer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py index d9401cfa6d..8367f24d0e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_constituent.py @@ -52,7 +52,7 @@ def remove_constituent( # invalid. ifcopenshell.api.material.remove_constituent(model, constituent=glazing) """ - material = layer.Material + material = constituent.Material file.remove(constituent) if material and should_remove_material: - ifcopenshell.util.element.remove_deep2(file, subelement) + ifcopenshell.util.element.remove_deep2(file, material) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py index 7055beaf26..443ad122c0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_layer.py @@ -57,4 +57,4 @@ def remove_layer( material = layer.Material file.remove(layer) if material and should_remove_material: - ifcopenshell.util.element.remove_deep2(file, subelement) + ifcopenshell.util.element.remove_deep2(file, material) From 527e0f3dbfcf0b797b074babf1d5fe49e3d6255c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 18 Feb 2025 16:45:21 +0500 Subject: [PATCH 060/476] set_element_value - fix error for Python 3.9 types.EllipsisType was added only in 3.10 --- src/ifcopenshell-python/ifcopenshell/util/selector.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index 37919b9408..f0a1025583 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -17,7 +17,7 @@ # along with IfcOpenShell. If not, see . import re -import types +import sys import lark import numpy as np import ifcopenshell.api.pset @@ -37,6 +37,11 @@ import ifcopenshell.util.unit from decimal import Decimal from typing import Optional, Any, Union, Iterable +if sys.version_info >= (3, 10): + from types import EllipsisType +else: + EllipsisType = type(...) + filter_elements_grammar = lark.Lark( """start: filter_group @@ -659,7 +664,7 @@ def set_element_value( def process_pset_prop_value( pset: ifcopenshell.entity_instance, prop: str, value: Any - ) -> Union[Any, types.EllipsisType]: + ) -> Union[Any, EllipsisType]: """Try to process value for edit_pset. `edit_pset` is expecting a sequence of values @@ -672,7 +677,7 @@ def set_element_value( current_value = element.get(key, ...) # Check if previous value is a list as a fast way to identify enum properties. - if not isinstance(current_value, (types.EllipsisType, list)): + if not isinstance(current_value, (EllipsisType, list)): return value if isinstance(current_value, list): From 4efb79e472b0e303c46aaf5cdd3e5cd06d6af085 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 18 Feb 2025 17:19:02 +0500 Subject: [PATCH 061/476] fix missing get_spline_points in add_representation --- .../api/geometry/add_representation.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py index 13d06e59c8..8244c3e434 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_representation.py @@ -111,10 +111,12 @@ class Usecase: settings: dict[str, Any] ifc_vertices: list[ifcopenshell.entity_instance] coordinate_offset: Union[npt.NDArray[np.float64], None] + geometry: Union[bpy.types.Mesh, bpy.types.Curve] def execute(self) -> Union[ifcopenshell.entity_instance, None]: self.is_manifold = None self.coordinate_offset = self.settings["coordinate_offset"] + self.geometry = self.settings["geometry"] if ( isinstance(self.settings["geometry"], bpy.types.Mesh) and self.settings["geometry"] == self.settings["blender_object"].data @@ -636,9 +638,7 @@ class Usecase: dim = (lambda v: v.xy) if is_2d else (lambda v: v.xyz) results = [] for spline in curve_object_data.splines: - points = spline.bezier_points[:] + spline.points[:] - if spline.use_cyclic_u: - points.append(points[0]) + points = self.get_spline_points(spline) ifc_points = [self.create_cartesian_point(*dim(point.co)) for point in points] results.append(self.file.createIfcPolyline(ifc_points)) return results @@ -981,12 +981,12 @@ class Usecase: ) def create_structural_reference_representation(self) -> ifcopenshell.entity_instance: - if len(self.settings["geometry"].vertices) == 1: + if isinstance(self.geometry, bpy.types.Mesh) and len(self.geometry.vertices) == 1: return self.file.createIfcTopologyRepresentation( self.settings["context"], self.settings["context"].ContextIdentifier, "Vertex", - [self.create_vertex_point(self.settings["geometry"].vertices[0].co)], + [self.create_vertex_point(self.geometry.vertices[0].co)], ) return self.file.createIfcTopologyRepresentation( self.settings["context"], @@ -998,11 +998,22 @@ class Usecase: def create_vertex_point(self, point: Vector) -> ifcopenshell.entity_instance: return self.file.createIfcVertexPoint(self.create_cartesian_point(point.x, point.y, point.z)) + def get_spline_points( + self, spline: bpy.types.Spline + ) -> list[Union[bpy.types.SplinePoint, bpy.types.BezierSplinePoint]]: + points = spline.bezier_points[:] + spline.points[:] + if spline.use_cyclic_u: + points.append(points[0]) + return points + def create_edge(self) -> Union[ifcopenshell.entity_instance, None]: - if hasattr(self.settings["geometry"], "splines"): - points = self.get_spline_points(self.settings["geometry"].splines[0]) + geometry = self.geometry + if isinstance(geometry, bpy.types.Curve): + points = self.get_spline_points(geometry.splines[0]) + elif isinstance(geometry, bpy.types.Mesh): + points = geometry.vertices else: - points = self.settings["geometry"].vertices + assert False, type(geometry) if not points: return return self.file.createIfcEdge(self.create_vertex_point(points[0].co), self.create_vertex_point(points[1].co)) From bb1e00e474a38dc609abb7ff4e4f637cbd9b3754 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:43:11 -0800 Subject: [PATCH 062/476] Fixes problems mapping vertical alignment segments business logic to geometry --- src/ifcparse/IfcAlignmentHelper.cpp | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/ifcparse/IfcAlignmentHelper.cpp b/src/ifcparse/IfcAlignmentHelper.cpp index 8e26375ab5..f60feacc3e 100644 --- a/src/ifcparse/IfcAlignmentHelper.cpp +++ b/src/ifcparse/IfcAlignmentHelper.cpp @@ -30,6 +30,7 @@ // @todo use std::numbers::pi when upgrading to C++ 20 static const double PI = boost::math::constants::pi(); +#include #ifdef HAS_SCHEMA_4x3_add2 @@ -243,14 +244,14 @@ std::tuple::ptr, typenam // back gradient auto dxBG = xPVI - xPBG; auto dyBG = yPVI - yPBG; - auto start_slope = atan2(dyBG, dxBG); + auto start_slope = tan(atan2(dyBG,dxBG)); // forward gradient point_iter++; std::tie(xPFG, yPFG) = *point_iter; auto dxFG = xPFG - xPVI; auto dyFG = yPFG - yPVI; - auto end_slope = atan2(dyFG, dxFG); + auto end_slope = tan(atan2(dyFG,dxFG)); double xEVC = xPVI + length / 2; double yEVC = yPVI + end_slope * length / 2; @@ -292,8 +293,9 @@ std::tuple::ptr, typenam // create last tangent run auto dx = xPVI - xPBG; auto dy = yPVI - yPBG; - auto slope = atan2(dy, dx); - auto gradient_length = sqrt(dx * dx + dy * dy); + auto slope = tan(atan2(dy,dx)); + auto gradient_length = dx; + file.addDoublet(xPBG, yPBG); auto design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xPBG, gradient_length, yPBG, slope, slope, boost::none, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT); auto alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters); @@ -688,13 +690,18 @@ std::pair mapAlign new Ifc4x3_add2::IfcCartesianPoint(std::vector({0, 0})), new Ifc4x3_add2::IfcVector(new Ifc4x3_add2::IfcDirection(std::vector{1, 0}), 1.0)); + // IfcCurveSegment.SegmentLength is the length of the curve segment, not the horizontal length. + auto dx = cos(atan(start_gradient)); + auto dy = sin(atan(start_gradient)); + auto segment_curve_length = horizontal_length / dx; + auto curve_segment = new Ifc4x3_add2::IfcCurveSegment( Ifc4x3_add2::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT, new Ifc4x3_add2::IfcAxis2Placement2D( new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}), - new Ifc4x3_add2::IfcDirection({sqrt(1.0 - start_gradient * start_gradient), start_gradient})), + new Ifc4x3_add2::IfcDirection({dx,dy})), new Ifc4x3_add2::IfcLengthMeasure(0.0), // start - new Ifc4x3_add2::IfcLengthMeasure(horizontal_length), + new Ifc4x3_add2::IfcLengthMeasure(segment_curve_length), parent_curve); result.first = curve_segment; @@ -710,11 +717,22 @@ std::pair mapAlign std::vector{A, B, C}, boost::none); + // IfcCurveSegment.SegmentLength is the length of the curve segment, not the horizontal length. + // The curve length is calculated by integrating the differential curve length equation sqrt(1 + (dy/dx)^2) from 0 to horizontal_length. + // y = A + Bx + Cx^2 + // dy/dx = B + 2Cx + auto dx = cos(atan(start_gradient)); + auto dy = sin(atan(start_gradient)); + auto curve_length_fn = [B, C](double x) { return sqrt(1 + pow(B + C * x, 2)); }; + auto segment_curve_length = boost::math::quadrature::trapezoidal(curve_length_fn, 0.0, horizontal_length); + auto curve_segment = new Ifc4x3_add2::IfcCurveSegment( Ifc4x3_add2::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT, - new Ifc4x3_add2::IfcAxis2Placement2D(new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}), new Ifc4x3_add2::IfcDirection({sqrt(1.0 - start_gradient * start_gradient), start_gradient})), + new Ifc4x3_add2::IfcAxis2Placement2D( + new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}), + new Ifc4x3_add2::IfcDirection({dx,dy})), new Ifc4x3_add2::IfcLengthMeasure(0.0), - new Ifc4x3_add2::IfcLengthMeasure(horizontal_length), + new Ifc4x3_add2::IfcLengthMeasure(segment_curve_length), parent_curve); result.first = curve_segment; @@ -735,11 +753,14 @@ std::pair mapAlign new Ifc4x3_add2::IfcDirection(std::vector{1, 0})), radius); + + auto segment_curve_length = radius * fabs(end_angle - start_angle); + Ifc4x3_add2::IfcCurveSegment* curve_segment = new Ifc4x3_add2::IfcCurveSegment( Ifc4x3_add2::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT, new Ifc4x3_add2::IfcAxis2Placement2D(new Ifc4x3_add2::IfcCartesianPoint({start_distance_along, start_height}), new Ifc4x3_add2::IfcDirection({1.0, 0.0})), new Ifc4x3_add2::IfcLengthMeasure(0.0), - new Ifc4x3_add2::IfcLengthMeasure(horizontal_length), + new Ifc4x3_add2::IfcLengthMeasure(segment_curve_length), parent_curve); result.first = curve_segment; From f11e09c7d88200ffc9815fa3bd31274c21061447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 18 Feb 2025 19:36:14 -0300 Subject: [PATCH 063/476] See #6121. Area measurement tool now uses IFC area unit. --- .../bonsai/bim/module/drawing/helper.py | 43 ++++++++++++++++++- src/bonsai/bonsai/tool/polyline.py | 6 ++- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index 5bd1661bfa..720459f85b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -19,6 +19,7 @@ import bpy import math import mathutils.geometry +import ifcopenshell import bonsai.tool as tool from mathutils import Vector @@ -135,6 +136,7 @@ def format_distance( scaleFactor = bpy.context.scene.unit_settings.scale_length unit_system = bpy.context.scene.unit_settings.system unit_length = bpy.context.scene.unit_settings.length_unit + area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT")) value *= scaleFactor @@ -225,7 +227,24 @@ def format_distance( if add_inches or frac: tx_dist += '"' else: - tx_dist = str("%1.3f" % (value * toInches / inPerFoot)) + " sq. ft." + fmt = "%1.3f" + sq_feet = round(value * toInches / inPerFoot, 4) + tx_dist = "" + if area_unit_symbol == " ft2": + fmt += area_unit_symbol + tx_dist = fmt % sq_feet + if area_unit_symbol == " in2": + sq_inch = sq_feet * 144 + fmt += area_unit_symbol + tx_dist = fmt % sq_inch + if area_unit_symbol == " yd2": + sq_yard = sq_feet / 9 + fmt += area_unit_symbol + tx_dist = fmt % sq_yard + if area_unit_symbol == " mi2": + sq_mile = sq_feet / 27878400 + fmt += area_unit_symbol + tx_dist = fmt % sq_mile # METRIC FORMATTING elif unit_system == "METRIC": @@ -287,7 +306,27 @@ def format_distance( d_mm = value * (1000) tx_dist = fmt % d_mm if isArea: - tx_dist += s_code + if area_unit_symbol == " m2": + if decimal_places is None: + fmt = "%1.3f" + if hide_units is False: + fmt += area_unit_symbol + tx_dist = fmt % value + if area_unit_symbol == " cm2": + if decimal_places is None: + fmt = "%1.1f" + if hide_units is False: + fmt += area_unit_symbol + d_cm = value * (10000) + tx_dist = fmt % d_cm + if area_unit_symbol == " mm2": + if decimal_places is None: + fmt = "%1.0f" + if hide_units is False: + fmt += area_unit_symbol + d_cm = value * (1000000) + tx_dist = fmt % d_cm + else: tx_dist = fmt % value diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 00fbad7483..8554e182f1 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -453,12 +453,14 @@ class Polyline(bonsai.core.tool.Polyline): unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if bpy.context.scene.unit_settings.system == "IMPERIAL": precision = bpy.context.scene.DocProperties.imperial_precision + if is_area: + area_unit = bpy.context.scene.BIMProperties.area_unit + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), unit_type=area_unit) else: precision = None - value = value if is_area else value / unit_scale return format_distance( - value, + value / unit_scale, precision=precision, hide_units=False, isArea=is_area, From 02f0c92a41164147def85f4ad9446b67a0c452fc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 19 Feb 2025 16:39:24 +1100 Subject: [PATCH 064/476] Fix bug where updating the null value on an enum property would lead to infinite recursion --- src/bonsai/bonsai/bim/prop.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 8c790444f1..8e855a1139 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -230,15 +230,11 @@ def update_attribute_value(self: "Attribute", context: bpy.types.Context) -> Non def update_is_null(self: "Attribute", context: bpy.types.Context) -> None: - if not self.is_null: - return - self.string_value = "" - self.int_value = 0 - self.float_value = 0 - self.length_value = 0 - self.bool_value = False - if self.is_null is not True: - self.is_null = True + if self.is_null: + if self.data_type != "enum" and self.get_value() != (default := self.get_value_default()): + self.set_value(default) + if self.is_null is not True: + self.is_null = True def set_int_value(self: "Attribute", new_value: int) -> None: From a8efd53d6b0843b940b043e3eef654fef13e65e4 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 19 Feb 2025 16:41:20 +1100 Subject: [PATCH 065/476] Fix #6182. For convenience, precalculate scale value for projected CRS map unit map conversion --- src/bonsai/bonsai/bim/prop.py | 6 ++++++ src/bonsai/bonsai/tool/georeference.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 8e855a1139..2f095cbb2c 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -235,6 +235,11 @@ def update_is_null(self: "Attribute", context: bpy.types.Context) -> None: self.set_value(default) if self.is_null is not True: self.is_null = True + if self.update: + update = globals() + for name in self.update.split("."): + update = update[name] if isinstance(update, dict) else getattr(update, name) + update(self, context) def set_int_value(self: "Attribute", new_value: int) -> None: @@ -322,6 +327,7 @@ class Attribute(PropertyGroup): value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound") special_type: StringProperty(name="Special Value Type", default="") metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute") + update: StringProperty(name="Update", description="Custom update function to be executed") if TYPE_CHECKING: name: str diff --git a/src/bonsai/bonsai/tool/georeference.py b/src/bonsai/bonsai/tool/georeference.py index 9cd80ce993..f2a0a4d866 100644 --- a/src/bonsai/bonsai/tool/georeference.py +++ b/src/bonsai/bonsai/tool/georeference.py @@ -58,6 +58,7 @@ class Georeference(bonsai.core.tool.Georeference): ) if data["MapUnit"]: new.enum_value = str(data["MapUnit"].id()) + new.update = "tool.Georeference.update_map_unit" return True props = bpy.context.scene.BIMGeoreferenceProperties @@ -72,6 +73,21 @@ class Georeference(bonsai.core.tool.Georeference): bonsai.bim.helper.import_attributes2(projected_crs, props.projected_crs, callback=callback) return + @classmethod + def update_map_unit(cls, self, context) -> None: + if unit_id := self.get_value(): + map_unit = tool.Ifc.get().by_id(int(unit_id)) + project_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT") + if map_unit and project_unit: + result = ifcopenshell.util.unit.convert_unit(1, project_unit, map_unit) + else: + result = 1.0 + else: + result = 1.0 + for attribute in bpy.context.scene.BIMGeoreferenceProperties.coordinate_operation: + if attribute.name == "Scale": + attribute.set_value(str(result)) + @classmethod def import_coordinate_operation(cls) -> None: def callback(name, prop, data): From ee0599e263cbd3388addbbba8ab78af0cf8f1f7c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 19 Feb 2025 17:44:48 +1100 Subject: [PATCH 066/476] Fix #6136. Allow setting of custom tmp dir, useful for packaged envs like flatpak --- src/bonsai/bonsai/bim/__init__.py | 4 +- .../bonsai/bim/module/augin/operator.py | 2 +- .../bonsai/bim/module/project/operator.py | 8 ++- .../bonsai/bim/module/tester/operator.py | 2 +- src/bonsai/bonsai/bim/operator.py | 58 +++++++------------ src/bonsai/bonsai/bim/ui.py | 12 +++- 6 files changed, 39 insertions(+), 47 deletions(-) diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index 3755a8d12a..5985459e2a 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -115,10 +115,8 @@ classes = [ operator.ReloadIfcFile, operator.RemoveIfcFile, operator.RevertClippingPlaneCut, - operator.SelectDataDir, - operator.SelectCacheDir, + operator.SelectDir, operator.SelectIfcFile, - operator.SelectSchemaDir, operator.SelectURIAttribute, operator.SetTab, operator.SwitchTab, diff --git a/src/bonsai/bonsai/bim/module/augin/operator.py b/src/bonsai/bonsai/bim/module/augin/operator.py index 965279e9b7..c4999ab08f 100644 --- a/src/bonsai/bonsai/bim/module/augin/operator.py +++ b/src/bonsai/bonsai/bim/module/augin/operator.py @@ -113,7 +113,7 @@ class AuginCreateNewModel(bpy.types.Operator): context.scene.collection.objects.link(cam_obj) context.scene.camera = cam_obj - tmpdir = tempfile.mkdtemp() + tmpdir = tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None) thumb_path = os.path.join(tmpdir, "thumb.png") context.scene.render.image_settings.file_format = "PNG" context.scene.render.filepath = thumb_path diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index aa1c8b5403..9e40f982fb 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1057,7 +1057,9 @@ class LoadProjectElements(bpy.types.Operator): logger = logging.getLogger("ImportIFC") path_log = tool.Blender.get_data_dir_path("process.log") if not os.access(path_log.parent, os.W_OK): - path_log = os.path.join(tempfile.mkdtemp(), "process.log") + path_log = os.path.join( + tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None), "process.log" + ) logging.basicConfig( filename=path_log, filemode="a", @@ -1579,7 +1581,9 @@ class ExportIFC(bpy.types.Operator): logger = logging.getLogger("ExportIFC") path_log = tool.Blender.get_data_dir_path("process.log") if not os.access(path_log.parent, os.W_OK): - path_log = os.path.join(tempfile.mkdtemp(), "process.log") + path_log = os.path.join( + tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None), "process.log" + ) logging.basicConfig( filename=path_log, filemode="a", diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index 652d0ff61a..b16abd3fcd 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -72,7 +72,7 @@ class ExecuteIfcTester(bpy.types.Operator): # No need for if-statement, just postponing lots of diffs. if True: - dirpath = tempfile.mkdtemp() + dirpath = tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None) start = time.time() output = Path(os.path.join(dirpath, "{}_{}.html".format(Path(ifc_path).name, Path(specs_path).name))) diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 5ba1596203..be0d07102d 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -226,47 +226,27 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector): return {"RUNNING_MODAL"} -class SelectDataDir(bpy.types.Operator): - bl_idname = "bim.select_data_dir" - bl_label = "Select Data Directory" +class SelectDir(bpy.types.Operator): + bl_idname = "bim.select_dir" + bl_label = "Select Directory" bl_options = {"REGISTER", "UNDO"} - bl_description = "Select the directory that contains all IFC data es. PSet, styles, etc..." + bl_description = "Open a file browser to choose the directory" filepath: bpy.props.StringProperty(subtype="FILE_PATH") + data_path: bpy.props.StringProperty(name="Data Path") def execute(self, context): - context.scene.BIMProperties.data_dir = os.path.dirname(self.filepath) - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class SelectCacheDir(bpy.types.Operator): - bl_idname = "bim.select_cache_dir" - bl_label = "Select Cache Directory" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Select the directory that contains HDF5 cache files" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - context.scene.BIMProperties.cache_dir = os.path.dirname(self.filepath) - return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - - -class SelectSchemaDir(bpy.types.Operator): - bl_idname = "bim.select_schema_dir" - bl_label = "Select Schema Directory" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Select the directory containing the IFC schema specification" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def execute(self, context): - context.scene.BIMProperties.schema_dir = os.path.dirname(self.filepath) + crumbs = self.data_path.split(".") + if crumbs[0] == "preferences": + crumbs.pop(0) + data = tool.Blender.get_addon_preferences() + else: + data = context + while crumbs: + crumb = crumbs.pop(0) + if crumbs: + data = getattr(data, crumb) + else: + setattr(data, crumb, os.path.dirname(self.filepath)) return {"FINISHED"} def invoke(self, context, event): @@ -816,7 +796,9 @@ class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator): logger = logging.getLogger("ImportIFC") path_log = tool.Blender.get_data_dir_path("process.log") if not os.access(path_log.parent, os.W_OK): - path_log = os.path.join(tempfile.mkdtemp(), "process.log") + path_log = os.path.join( + tempfile.mkdtemp(dir=tool.Blender.get_addon_preferences().tmp_dir or None), "process.log" + ) logging.basicConfig( filename=path_log, filemode="a", diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index a666d3fd94..fe300719af 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -245,6 +245,10 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): description="If disabled, the toolbar will only load when an IFC model is active", ) should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False) + tmp_dir: StringProperty( + name="Temporary Directory", + description='Path to create and store temporary files. If left blank, a system default will be used.', + ) spatial_elements_unselectable: BoolProperty( name="Make Spatial Elements Unselectable By Default", default=True, @@ -356,11 +360,15 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): def draw_directories(self, layout, context): row = layout.row(align=True) row.prop(context.scene.BIMProperties, "data_dir") - row.operator("bim.select_data_dir", icon="FILE_FOLDER", text="") + row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.data_dir" row = layout.row(align=True) row.prop(context.scene.BIMProperties, "cache_dir") - row.operator("bim.select_cache_dir", icon="FILE_FOLDER", text="") + row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.cache_dir" + + row = layout.row(align=True) + row.prop(self, "tmp_dir") + row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.tmp_dir" def draw_drawing_settings(self, layout, context): layout.prop(context.scene.BIMProperties, "pset_dir") From deb0a46acbe4a6a62de4b6cdbd5febb28ff69ab2 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 19 Feb 2025 17:52:34 +1100 Subject: [PATCH 067/476] Fix #6186. Don't hide filter UI if there are no results. --- src/bonsai/bonsai/bim/module/model/ui.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index b0a59a3916..297ce5a2d7 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -117,9 +117,8 @@ class LaunchTypeManager(bpy.types.Operator): op.ifc_product = "IfcElementType" op.ifc_class = AuthoringData.data["ifc_element_type"] or props.ifc_class or "" - if AuthoringData.data["total_types"]: - row = self.layout.row(align=True) - row.prop(props, "search_name", icon="FILTER", text="") + row = self.layout.row(align=True) + row.prop(props, "search_name", icon="FILTER", text="") columns = self.layout.column_flow(columns=3) if AuthoringData.data["total_pages"] > 0: From 6c511f6b1b43998981f972477c6000317dbf47a9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Feb 2025 17:26:34 +0500 Subject: [PATCH 068/476] Project library UI - fix missing elements added to parent libraries Previously if there was library A and it had sublibrary B and then some element were directly assigned to library A, UI still wouldn't show this element and would show only elements assigned to the B, the last library in the hierarchy --- src/bonsai/bonsai/bim/module/project/operator.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 9e40f982fb..8510a7d125 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -304,13 +304,14 @@ class ChangeLibraryElement(bpy.types.Operator): if self.breadcrumb_type == "LIBRARY": hierarchy = tool.Project.get_project_hierarchy(library_file) assert active_project_library is not None - if active_project_library == "NO_LIBRARY" or not hierarchy[active_project_library]: - for appendable_type in sorted(tool.Project.get_appendable_asset_types()): - elements = library_file.by_type(appendable_type) - if elements := filter_elements(elements): - self.props.add_library_asset_class(appendable_type, len(elements)) - else: + if active_project_library != "NO_LIBRARY" and hierarchy[active_project_library]: tool.Project.load_project_libraries_to_ui(active_project_library, hierarchy) + + for appendable_type in sorted(tool.Project.get_appendable_asset_types()): + elements = library_file.by_type(appendable_type) + if elements := filter_elements(elements): + self.props.add_library_asset_class(appendable_type, len(elements)) + else: # breadcrumb_type CLASS. elements = self.library_file.by_type(self.element_name) elements = list(filter_elements(elements)) From 67bac441ad9e6c1538fb0aecfc7f1f6e49f75f79 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Feb 2025 11:06:03 +0500 Subject: [PATCH 069/476] typing --- src/bonsai/bonsai/bim/export_ifc.py | 3 +- src/bonsai/bonsai/bim/handler.py | 5 +- src/bonsai/bonsai/bim/ifc.py | 10 +- src/bonsai/bonsai/bim/import_ifc.py | 7 +- src/bonsai/bonsai/bim/module/bcf/operator.py | 2 +- .../bonsai/bim/module/boundary/operator.py | 2 +- src/bonsai/bonsai/bim/module/cad/workspace.py | 33 ++- .../bim/module/classification/operator.py | 3 +- .../bonsai/bim/module/debug/operator.py | 26 +- src/bonsai/bonsai/bim/module/debug/prop.py | 25 +- src/bonsai/bonsai/bim/module/debug/ui.py | 19 +- src/bonsai/bonsai/bim/module/drawing/data.py | 13 +- .../bonsai/bim/module/drawing/decoration.py | 6 +- .../bonsai/bim/module/drawing/gizmos.py | 38 +-- .../bonsai/bim/module/drawing/handler.py | 11 +- .../bonsai/bim/module/drawing/helper.py | 13 +- .../bonsai/bim/module/drawing/operator.py | 150 ++++++----- src/bonsai/bonsai/bim/module/drawing/prop.py | 10 +- .../bonsai/bim/module/drawing/scheduler.py | 17 +- src/bonsai/bonsai/bim/module/drawing/ui.py | 14 +- .../bonsai/bim/module/drawing/workspace.py | 5 +- .../bonsai/bim/module/geometry/__init__.py | 6 +- src/bonsai/bonsai/bim/module/geometry/data.py | 22 +- .../bonsai/bim/module/geometry/decorator.py | 9 +- .../bonsai/bim/module/geometry/operator.py | 243 +++++++++++------- src/bonsai/bonsai/bim/module/geometry/prop.py | 7 +- src/bonsai/bonsai/bim/module/geometry/ui.py | 38 +-- .../bonsai/bim/module/georeference/data.py | 4 +- .../bim/module/georeference/decorator.py | 10 +- .../bonsai/bim/module/georeference/prop.py | 69 ++++- .../bonsai/bim/module/georeference/ui.py | 18 +- .../bonsai/bim/module/layer/operator.py | 15 +- src/bonsai/bonsai/bim/module/layer/ui.py | 1 - src/bonsai/bonsai/bim/module/material/data.py | 3 +- src/bonsai/bonsai/bim/module/material/ui.py | 3 +- src/bonsai/bonsai/bim/module/misc/operator.py | 2 +- src/bonsai/bonsai/bim/module/model/opening.py | 33 ++- src/bonsai/bonsai/bim/module/model/profile.py | 15 +- src/bonsai/bonsai/bim/module/model/slab.py | 2 +- src/bonsai/bonsai/bim/module/model/wall.py | 18 +- .../bonsai/bim/module/model/workspace.py | 12 +- src/bonsai/bonsai/bim/module/profile/data.py | 6 +- .../bonsai/bim/module/profile/operator.py | 7 +- .../bonsai/bim/module/project/decorator.py | 8 +- src/bonsai/bonsai/bim/module/project/gizmo.py | 4 +- .../bonsai/bim/module/project/operator.py | 152 ++++++----- .../bonsai/bim/module/qto/calculator.py | 3 +- .../bonsai/bim/module/search/operator.py | 4 +- .../bonsai/bim/module/style/operator.py | 14 +- src/bonsai/bonsai/bim/module/void/data.py | 16 +- src/bonsai/bonsai/bim/module/void/operator.py | 21 +- src/bonsai/bonsai/bim/module/void/prop.py | 31 ++- src/bonsai/bonsai/bim/module/void/ui.py | 19 +- src/bonsai/bonsai/bim/operator.py | 19 +- src/bonsai/bonsai/bim/prop.py | 57 +++- src/bonsai/bonsai/bim/ui.py | 92 ++++--- src/bonsai/bonsai/tool/blender.py | 2 +- src/bonsai/bonsai/tool/debug.py | 10 +- src/bonsai/bonsai/tool/drawing.py | 61 +++-- src/bonsai/bonsai/tool/feature.py | 10 +- src/bonsai/bonsai/tool/geometry.py | 149 +++++++---- src/bonsai/bonsai/tool/georeference.py | 71 +++-- src/bonsai/bonsai/tool/ifc.py | 8 +- src/bonsai/bonsai/tool/loader.py | 10 +- src/bonsai/bonsai/tool/misc.py | 5 +- src/bonsai/bonsai/tool/model.py | 19 +- src/bonsai/bonsai/tool/polyline.py | 4 +- src/bonsai/bonsai/tool/project.py | 4 +- src/bonsai/bonsai/tool/root.py | 19 +- src/bonsai/bonsai/tool/snap.py | 9 +- src/bonsai/bonsai/tool/spatial.py | 5 +- src/bonsai/bonsai/tool/surveyor.py | 2 +- src/bonsai/scripts/headless_import.py | 5 +- src/bonsai/test/bim/bootstrap.py | 4 +- src/bonsai/test/bim/test_feature.py | 12 +- src/bonsai/test/tool/test_drawing.py | 69 ++--- src/bonsai/test/tool/test_geometry.py | 25 +- src/bonsai/test/tool/test_georeference.py | 48 ++-- src/bonsai/test/tool/test_ifc.py | 2 +- src/bonsai/test/tool/test_loader.py | 3 +- src/bonsai/test/tool/test_model.py | 5 +- src/bonsai/test/tool/test_project.py | 22 +- src/bonsai/test/tool/test_root.py | 6 +- src/bonsai/test/tool/test_surveyor.py | 5 +- .../api/cost/copy_cost_item_values.py | 11 +- .../ifcopenshell/api/layer/assign_layer.py | 13 +- .../api/material/edit_profile_usage.py | 3 +- .../ifcopenshell/api/owner/settings.py | 12 +- .../api/sequence/cascade_schedule.py | 34 ++- .../api/sequence/create_baseline.py | 4 +- .../edit_structural_connection_cs.py | 29 +-- src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py | 3 +- 92 files changed, 1261 insertions(+), 837 deletions(-) diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py index deafc5b3ff..84173a9ec5 100644 --- a/src/bonsai/bonsai/bim/export_ifc.py +++ b/src/bonsai/bonsai/bim/export_ifc.py @@ -132,7 +132,8 @@ class IfcExporter: bpy.ops.bim.update_representation(obj=obj.name) def has_changed_materials(self, obj: bpy.types.Object) -> bool: - checksum = obj.data.BIMMeshProperties.material_checksum + mprops = tool.Geometry.get_mesh_props(obj.data) + checksum = mprops.material_checksum return checksum != tool.Geometry.get_material_checksum(obj) def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index d65017243f..572cfaf117 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -221,7 +221,8 @@ def refresh_ui_data(): if isinstance(tool.Ifc.get(), ifcopenshell.sqlite): tool.Ifc.get().clear_cache() - bpy.context.scene.DocProperties.should_draw_decorations = bpy.context.scene.DocProperties.should_draw_decorations + props = tool.Drawing.get_document_props() + props.should_draw_decorations = props.should_draw_decorations if bpy.context.scene.WebProperties.is_connected: tool.Web.send_webui_data() @@ -343,7 +344,7 @@ def load_post(scene): bpy.context.scene.BIMProperties.has_blend_warning = True # Bonsai overlays - georeference_props = bpy.context.scene.BIMGeoreferenceProperties + georeference_props = tool.Georeference.get_georeference_props() aggregate_props = bpy.context.scene.BIMAggregateProperties nest_props = bpy.context.scene.BIMNestProperties model_props = tool.Model.get_model_props() diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 1c5d7800b1..044bf74fd2 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -178,6 +178,7 @@ class IfcStore: if not os.path.isfile(path): return extension = path.split(".")[-1] + props = tool.Project.get_project_props() if extension.lower() == "ifczip": with tempfile.TemporaryDirectory() as unzipped_path: with zipfile.ZipFile(path, "r") as zip_ref: @@ -187,7 +188,7 @@ class IfcStore: return elif extension.lower() == "ifcxml": IfcStore.file = ifcopenshell.file(ifcopenshell.ifcopenshell_wrapper.parse_ifcxml(path)) - elif bpy.context.scene.BIMProjectProperties.should_stream: + elif props.should_stream: IfcStore.file = ifcopenshell.open(path, should_stream=True) else: IfcStore.file = ifcopenshell.open(path) @@ -195,9 +196,8 @@ class IfcStore: @staticmethod def get_schema() -> ifcopenshell.ifcopenshell_wrapper.schema_definition: if IfcStore.file is None: - IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name( - bpy.context.scene.BIMProjectProperties.export_schema - ) + props = tool.Project.get_project_props() + IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(props.export_schema) elif IfcStore.schema is None: IfcStore.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(IfcStore.file.schema_identifier) return IfcStore.schema @@ -247,7 +247,7 @@ class IfcStore: # refactor this class and deprecate usage of IfcStore in favour of # tools. if not isinstance(obj, (bpy.types.Object, bpy.types.Material)): - obj.BIMMeshProperties.ifc_definition_id = element.id() + tool.Geometry.get_mesh_props(obj).ifc_definition_id = element.id() return existing_obj = IfcStore.id_map.get(element.id(), None) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index d2bdc2ed70..13fce33a93 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -90,7 +90,8 @@ class MaterialCreator: return # Already has materials assign to the representation itself # Otherwise, we need to check for material styles on the element, since # create_shape on types only works on representations. - context = tool.Ifc.get().by_id(self.mesh.BIMMeshProperties.ifc_definition_id).ContextOfItems + mprops = tool.Geometry.get_mesh_props(self.mesh) + context = tool.Ifc.get().by_id(mprops.ifc_definition_id).ContextOfItems for material in ifcopenshell.util.element.get_materials(element): if style := ifcopenshell.util.representation.get_material_style(material, context): self.mesh["ios_materials"] = (style.id(),) @@ -218,7 +219,7 @@ class IfcImporter: self.progress = 0 self.material_creator = MaterialCreator(ifc_import_settings, self) - classes_to_wireframe_str = bpy.context.scene.DocProperties.classes_to_wireframe + classes_to_wireframe_str = tool.Drawing.get_document_props().classes_to_wireframe self.classes_to_wireframe_list = [word.strip() for word in classes_to_wireframe_str.split(",")] def profile_code(self, message: str) -> None: @@ -434,7 +435,7 @@ class IfcImporter: return False def calculate_model_offset(self) -> None: - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if self.ifc_import_settings.false_origin_mode == "MANUAL": tool.Loader.set_manual_blender_offset(self.file) elif self.ifc_import_settings.false_origin_mode == "AUTOMATIC": diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index bfcc40b8a9..aed65de52c 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -1316,7 +1316,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): [0, 0, 0, 1], ) ) - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) matrix = ifcopenshell.util.geolocation.global2local( diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py index 5d83a88597..75cf32cb66 100644 --- a/src/bonsai/bonsai/bim/module/boundary/operator.py +++ b/src/bonsai/bonsai/bim/module/boundary/operator.py @@ -114,7 +114,7 @@ class Loader: bm.edges.new((verts[-1], verts[0])) bm.to_mesh(mesh) bm.free() - mesh.BIMMeshProperties.ifc_definition_id = surface.id() + tool.Ifc.link(surface, mesh) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) matrix = mathutils.Matrix( ifcopenshell.util.placement.get_axis2placement(surface.BasisSurface.Position).tolist() diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 854b34bb7d..4b70d04df1 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -69,10 +69,12 @@ class CadTool(WorkSpaceTool): ("bim.cad_hotkey", {"type": "X", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_X")]}), ) - def draw_settings(context, layout, workspace_tool): + def draw_settings( + context: bpy.types.Context, layout: bpy.types.UILayout, workspace_tool: bpy.types.WorkSpaceTool + ) -> None: ui_context = str(context.region.type) obj = context.active_object - if not obj or not obj.data: + if not obj or not (data := obj.data): return is_profile = tool.Geometry.is_profile_object_active() if is_profile: @@ -122,7 +124,10 @@ class CadTool(WorkSpaceTool): row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator(row, "Reset Vertex", "S_X", bpy.ops.bim.reset_vertex.__doc__, ui_context) - elif hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.subshape_type == "AXIS": + elif ( + isinstance(data, tool.Geometry.TYPES_WITH_MESH_PROPERTIES) + and tool.Geometry.get_mesh_props(data).subshape_type == "AXIS" + ): add_header_apply_button( layout, "Edit Axis", "bim.edit_extrusion_axis", "bim.disable_editing_extrusion_axis", ui_context ) @@ -141,7 +146,7 @@ class CadTool(WorkSpaceTool): if ( (RailingData.is_loaded or not RailingData.load()) and RailingData.data["pset_data"] - and context.active_object.BIMRailingProperties.is_editing_path + and obj.BIMRailingProperties.is_editing_path ): add_header_apply_button( layout, @@ -154,7 +159,7 @@ class CadTool(WorkSpaceTool): elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] - and context.active_object.BIMRoofProperties.is_editing_path + and obj.BIMRoofProperties.is_editing_path ): add_header_apply_button( layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context @@ -252,15 +257,27 @@ class CadHotkey(bpy.types.Operator): bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion) def hotkey_S_Q(self): - element = tool.Ifc.get_entity(bpy.context.active_object) - if bpy.context.active_object.data.BIMMeshProperties.subshape_type == "PROFILE": + obj = bpy.context.active_object + + if not obj: + return + + if not tool.Geometry.has_mesh_properties(data := obj.data): + return + + element = tool.Ifc.get_entity(obj) + if not element: + return + + mprops = tool.Geometry.get_mesh_props(data) + if mprops.subshape_type == "PROFILE": if element.is_a("IfcProfileDef"): bpy.ops.bim.edit_arbitrary_profile() elif element.is_a("IfcRelSpaceBoundary"): bpy.ops.bim.edit_boundary_geometry() else: bpy.ops.bim.edit_extrusion_profile() - elif bpy.context.active_object.data.BIMMeshProperties.subshape_type == "AXIS": + elif mprops.subshape_type == "AXIS": bpy.ops.bim.edit_extrusion_axis() def hotkey_S_R(self): diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index 199fa2d61b..acdffae946 100644 --- a/src/bonsai/bonsai/bim/module/classification/operator.py +++ b/src/bonsai/bonsai/bim/module/classification/operator.py @@ -201,7 +201,8 @@ class EnableEditingClassification(bpy.types.Operator): def execute(self, context): def callback(name, prop, data): if name == "ReferenceTokens": - new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add() + geo_props = tool.Georeference.get_georeference_props() + new = geo_props.projected_crs.add() new.name = name new.data_type = "string" new.is_null = data[name] is None diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 33100956ef..bb949b8fd0 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -101,13 +101,13 @@ class ConvertToBlender(bpy.types.Operator): if tool.Geometry.has_mesh_properties(data): if data.library: continue - data.BIMMeshProperties.ifc_definition_id = 0 + tool.Geometry.get_mesh_props(data).ifc_definition_id = 0 for material in bpy.data.materials: if material.library: continue tool.Ifc.unlink(obj=material) context.scene.BIMProperties.ifc_file = "" - context.scene.BIMDebugProperties.attributes.clear() + tool.Debug.get_debug_props().attributes.clear() IfcStore.purge() bonsai.bim.handler.refresh_ui_data() return {"FINISHED"} @@ -256,7 +256,7 @@ class CreateShapeFromStepId(bpy.types.Operator): logger = logging.getLogger("ImportIFC") self.ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) self.file = tool.Ifc.get() - element = self.file.by_id(self.step_id or int(context.scene.BIMDebugProperties.step_id)) + element = self.file.by_id(self.step_id or int(tool.Debug.get_debug_props().step_id)) settings = ifcopenshell.geom.settings() settings.set("keep-bounding-boxes", True) if self.should_include_curves: @@ -309,7 +309,7 @@ class RewindInspector(bpy.types.Operator): bl_description = "Rewind the Inspector to the previously inspected element" def execute(self, context): - props = context.scene.BIMDebugProperties + props = tool.Debug.get_debug_props() total_breadcrumbs = len(props.step_id_breadcrumb) if total_breadcrumbs < 2: return {"FINISHED"} @@ -332,9 +332,9 @@ class InspectFromStepId(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() - debug_props = context.scene.BIMDebugProperties + debug_props = tool.Debug.get_debug_props() debug_props.active_step_id = self.step_id - crumb = context.scene.BIMDebugProperties.step_id_breadcrumb.add() + crumb = debug_props.step_id_breadcrumb.add() crumb.name = str(self.step_id) element = self.file.by_id(self.step_id) debug_props.attributes.clear() @@ -385,7 +385,7 @@ class InspectFromObject(bpy.types.Operator): if ( (data := obj.data) and tool.Geometry.has_mesh_properties(data) - and (ifc_id := data.BIMMeshProperties.ifc_definition_id) + and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id) ): return ifc_id @@ -438,7 +438,8 @@ class ParseExpress(bpy.types.Operator): bl_label = "Parse Express" def execute(self, context): - core.parse_express(tool.Debug, context.scene.BIMDebugProperties.express_file) + props = tool.Debug.get_debug_props() + core.parse_express(tool.Debug, props.express_file) bonsai.bim.handler.refresh_ui_data() return {"FINISHED"} @@ -452,8 +453,9 @@ class SelectExpressFile(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"}) def execute(self, context): + props = tool.Debug.get_debug_props() if os.path.exists(self.filepath) and "exp" in os.path.splitext(self.filepath)[1]: - context.scene.BIMDebugProperties.express_file = self.filepath + props.express_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -497,7 +499,7 @@ class PrintUnusedElementStats(bpy.types.Operator): ignore_styled_items: bpy.props.BoolProperty(name="Ignore Styled Items", default=True) def execute(self, context): - props = context.scene.BIMDebugProperties + props = tool.Debug.get_debug_props() # ignore some classes that could have zero 0 inverse references by their nature ignore_classes = [] if self.ignore_contexts: @@ -543,7 +545,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator): return True def _execute(self, context): - props = context.scene.BIMDebugProperties + props = tool.Debug.get_debug_props() if props.ifc_class_purge: purged_elements = core.purge_unused_elements(tool.Ifc, tool.Debug, props.ifc_class_purge) self.report({"INFO"}, f"{purged_elements} unused elements found and removed.") @@ -803,7 +805,7 @@ class DebugActiveDrawing(bpy.types.Operator): ) def execute(self, context: bpy.types.Context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() drawing_item = props.drawings[props.active_drawing_index] drawing = tool.Ifc.get().by_id(drawing_item.ifc_definition_id) diff --git a/src/bonsai/bonsai/bim/module/debug/prop.py b/src/bonsai/bonsai/bim/module/debug/prop.py index 01076ccde3..5a28b22f79 100644 --- a/src/bonsai/bonsai/bim/module/debug/prop.py +++ b/src/bonsai/bonsai/bim/module/debug/prop.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import bpy from bonsai.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( @@ -28,6 +29,9 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Literal, get_args + +DisplayType = Literal["BOUNDS", "WIRE", "SOLID", "TEXTURED"] class BIMDebugProperties(PropertyGroup): @@ -41,14 +45,23 @@ class BIMDebugProperties(PropertyGroup): inverse_references: CollectionProperty(name="Inverse References", type=Attribute) express_file: StringProperty(name="Express File") display_type: EnumProperty( - items=[ - ("BOUNDS", "Bounds", ""), - ("WIRE", "Wire", ""), - ("SOLID", "Solid", ""), - ("TEXTURED", "Textured", ""), - ], + items=[(display_type, display_type.capitalize(), "") for display_type in get_args(DisplayType)], name="Display Type", default="BOUNDS", ) ifc_class_purge: StringProperty(name="Unused Elements IFC Class", default="") package_name: StringProperty(name="Package Name", default="") + + if TYPE_CHECKING: + step_id: int + number_of_polygons: int + percentile_of_polygons: int + active_step_id: int + step_id_breadcrumb: bpy.types.bpy_prop_collection_idprop[StrProperty] + attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + inverse_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + inverse_references: bpy.types.bpy_prop_collection_idprop[Attribute] + express_file: str + display_type: str + ifc_class_purge: str + package_name: str diff --git a/src/bonsai/bonsai/bim/module/debug/ui.py b/src/bonsai/bonsai/bim/module/debug/ui.py index 3611920095..4fbd5c8ea6 100644 --- a/src/bonsai/bonsai/bim/module/debug/ui.py +++ b/src/bonsai/bonsai/bim/module/debug/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bpy.types import Panel @@ -32,7 +33,7 @@ class BIM_PT_debug(Panel): def draw(self, context): layout = self.layout - props = context.scene.BIMDebugProperties + props = tool.Debug.get_debug_props() row = self.layout.row(align=True) row.prop(context.scene.BIMProperties, "ifc_file", text="") @@ -87,27 +88,25 @@ class BIM_PT_debug(Panel): row.prop(props, "step_id", text="") row = layout.split(factor=0.7, align=True) - row.operator("bim.select_high_polygon_meshes").threshold = context.scene.BIMDebugProperties.number_of_polygons + row.operator("bim.select_high_polygon_meshes").threshold = props.number_of_polygons row.prop(props, "number_of_polygons", text="") row = layout.split(factor=0.7, align=True) - row.operator("bim.select_highest_polygon_meshes").percentile = ( - context.scene.BIMDebugProperties.percentile_of_polygons - ) + row.operator("bim.select_highest_polygon_meshes").percentile = props.percentile_of_polygons row.prop(props, "percentile_of_polygons", text="") row = layout.split(factor=0.5, align=True) row.prop(props, "display_type", text="") - row.operator("bim.override_display_type").display = context.scene.BIMDebugProperties.display_type + row.operator("bim.override_display_type").display = props.display_type layout.operator("bim.purge_unused_representations") row = layout.row(align=True) - row.prop(context.scene.BIMDebugProperties, "ifc_class_purge", text="") + row.prop(props, "ifc_class_purge", text="") row.operator("bim.purge_unused_elements_by_class", text="Purge Orphaned", icon="TRASH") row.operator("bim.print_unused_elements_stats", text="", icon="INFO") - if context.active_object and context.active_object.data: - mprops = context.active_object.data.BIMMeshProperties + if context.active_object and (data := context.active_object.data): + mprops = tool.Geometry.get_mesh_props(data) row = layout.row() row.operator("bim.get_representation_ifc_parameters") for index, ifc_parameter in enumerate(mprops.ifc_parameters): @@ -123,7 +122,7 @@ class BIM_PT_debug(Panel): row.operator("bim.rewind_inspector", icon="FRAME_PREV", text="") row.prop(props, "active_step_id", text="") row = layout.row(align=True) - row.operator("bim.inspect_from_step_id").step_id = context.scene.BIMDebugProperties.active_step_id + row.operator("bim.inspect_from_step_id").step_id = props.active_step_id row.operator("bim.inspect_from_object") if props.attributes: diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index be252a29c9..d781b8bd30 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -88,7 +88,8 @@ class SheetsData: project = tool.Ifc.get().by_type("IfcProject")[0] titleblocks_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") if not titleblocks_dir: - titleblocks_dir = bpy.context.scene.DocProperties.titleblocks_dir + props = tool.Drawing.get_document_props() + titleblocks_dir = props.titleblocks_dir titleblocks_dir = tool.Ifc.resolve_uri(titleblocks_dir) if os.path.exists(titleblocks_dir): files.extend([str(f.stem) for f in Path(titleblocks_dir).glob("*.svg")]) @@ -120,23 +121,25 @@ class DrawingsData: @classmethod def location_hint(cls): - if bpy.context.scene.DocProperties.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]: + props = tool.Drawing.get_document_props() + if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]: results = [("0", "Origin", "")] results.extend( [(str(s.id()), s.Name or "Unnamed", "") for s in tool.Ifc.get().by_type("IfcBuildingStorey")] ) return results - elif bpy.context.scene.DocProperties.target_view in ["MODEL_VIEW"]: + elif props.target_view in ["MODEL_VIEW"]: return [(h.upper(), h, "") for h in ["Orthographic", "Perspective"]] return [(h.upper(), h, "") for h in ["North", "South", "East", "West"]] @classmethod def active_drawing_pset_data(cls): ifc_file = tool.Ifc.get() - drawing_id = bpy.context.scene.DocProperties.active_drawing_id + props = tool.Drawing.get_document_props() + drawing_id = props.active_drawing_id if drawing_id == 0: return {} - drawing = ifc_file.by_id(bpy.context.scene.DocProperties.active_drawing_id) + drawing = ifc_file.by_id(drawing_id) return ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index e8fba1914d..0bf8ab00b8 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -423,7 +423,8 @@ class BaseDecorator: # font_size = 16 <-- this is a good default # TODO: need to synchronize it better with svg - magic_font_scale = bpy.context.scene.DocProperties.magic_font_scale + props = tool.Drawing.get_document_props() + magic_font_scale = props.magic_font_scale font_size_px = int(magic_font_scale * mm_to_px) * font_size_mm / 2.5 pos = pos - line_no * font_size_px * rotation_matrix[1] @@ -2022,7 +2023,8 @@ class DecorationsHandler: for object_type in ("SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"): self.decorators[object_type] = self.decorators["FALL"] self.decorators["MULTI_SYMBOL"] = self.decorators["SYMBOL"] - if drawing_font := bpy.context.scene.DocProperties.drawing_font: + props = tool.Drawing.get_document_props() + if drawing_font := props.drawing_font: drawing_font_path = tool.Blender.get_data_dir_path(Path("fonts") / drawing_font) if drawing_font_path.is_file(): font_id = blf.load(drawing_font_path.__str__()) diff --git a/src/bonsai/bonsai/bim/module/drawing/gizmos.py b/src/bonsai/bonsai/bim/module/drawing/gizmos.py index 8e4f9bb97e..952ea0cb96 100644 --- a/src/bonsai/bonsai/bim/module/drawing/gizmos.py +++ b/src/bonsai/bonsai/bim/module/drawing/gizmos.py @@ -19,6 +19,7 @@ import bpy import blf import gpu +import bonsai.tool as tool from bpy import types from mathutils import Vector from mathutils import geometry @@ -478,21 +479,25 @@ class ExtrusionWidget(types.GizmoGroup): bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"} @classmethod - def poll(cls, ctx): - obj = ctx.object + def poll(cls, context): + obj = context.active_object return ( obj - and obj.type == "MESH" - and obj.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None + and (data := obj.data) + and isinstance(data, bpy.types.Mesh) + and tool.Geometry.get_mesh_props(data).ifc_parameters.get("IfcExtrudedAreaSolid/Depth") is not None ) - def setup(self, ctx): - target = ctx.object - prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") + def setup(self, context: bpy.types.Context) -> None: + target = context.object + assert target + mesh = target.data + assert isinstance(mesh, bpy.types.Mesh) + prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth") basis = target.matrix_world.normalized() - theme = ctx.preferences.themes[0].user_interface - scale_value = self.get_scale_value(ctx.scene.unit_settings.system, ctx.scene.unit_settings.length_unit) + theme = context.preferences.themes[0].user_interface + scale_value = self.get_scale_value(context.scene.unit_settings.system, context.scene.unit_settings.length_unit) # setup handle gz = self.handle = self.gizmos.new("BIM_GT_uglydot_3d") @@ -521,23 +526,26 @@ class ExtrusionWidget(types.GizmoGroup): # gz.use_draw_modal = True # gz.target_set_prop('value', target.demo, 'depth') - def refresh(self, ctx): + def refresh(self, context: bpy.types.Context) -> None: """updating gizmos""" - target = ctx.object + target = context.active_object basis = target.matrix_world.normalized() self.handle.matrix_basis = basis self.guides.matrix_basis = basis - def update(self, ctx): + def update(self, context: bpy.types.Context) -> None: """updating object""" bpy.ops.bim.update_parametric_representation() - target = ctx.object - prop = target.data.BIMMeshProperties.ifc_parameters.get("IfcExtrudedAreaSolid/Depth") + target = context.active_object + assert target + mesh = target.data + assert isinstance(mesh, bpy.types.Mesh) + prop = tool.Geometry.get_mesh_props(mesh).ifc_parameters.get("IfcExtrudedAreaSolid/Depth") self.handle.target_set_prop("offset", prop, "value") self.guides.target_set_prop("depth", prop, "value") @staticmethod - def get_scale_value(system, length_unit): + def get_scale_value(system: str, length_unit: str) -> float: scale_value = 1 if system == "METRIC": if length_unit == "KILOMETERS": diff --git a/src/bonsai/bonsai/bim/module/drawing/handler.py b/src/bonsai/bonsai/bim/module/drawing/handler.py index abfc8bb261..d1622d1908 100644 --- a/src/bonsai/bonsai/bim/module/drawing/handler.py +++ b/src/bonsai/bonsai/bim/module/drawing/handler.py @@ -24,7 +24,8 @@ from bpy.app.handlers import persistent @persistent def load_post(*args): - if bpy.context.scene.DocProperties.should_draw_decorations: + props = tool.Drawing.get_document_props() + if props.should_draw_decorations: decoration.DecorationsHandler.install(bpy.context) else: decoration.DecorationsHandler.uninstall() @@ -35,9 +36,11 @@ def depsgraph_update_pre_handler(scene): set_active_camera_resolution(scene) -def set_active_camera_resolution(scene): - if not scene.camera or "/" not in scene.camera.name or not scene.DocProperties.drawings: +def set_active_camera_resolution(scene: bpy.types.Scene) -> None: + props = tool.Drawing.get_document_props() + if not scene.camera or "/" not in scene.camera.name or not props.drawings: return + assert isinstance(scene.camera.data, bpy.types.Camera) props = scene.camera.data.BIMCameraProperties ortho_scale = max((props.width, props.height)) aspect_ratio = props.width / props.height @@ -60,5 +63,3 @@ def set_active_camera_resolution(scene): scene.render.resolution_x = scene.camera.data.BIMCameraProperties.raster_x = int(raster_x) scene.render.resolution_y = scene.camera.data.BIMCameraProperties.raster_y = int(raster_y) - - current_drawing = scene.DocProperties.drawings[scene.DocProperties.current_drawing_index] diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index 720459f85b..c63fd3d910 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -22,6 +22,7 @@ import mathutils.geometry import ifcopenshell import bonsai.tool as tool from mathutils import Vector +from typing import Union # Code taken and updated from https://blenderartists.org/t/detecting-intersection-of-bounding-boxes/457520/2 @@ -136,7 +137,9 @@ def format_distance( scaleFactor = bpy.context.scene.unit_settings.scale_length unit_system = bpy.context.scene.unit_settings.system unit_length = bpy.context.scene.unit_settings.length_unit - area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT")) + area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol( + ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT") + ) value *= scaleFactor @@ -326,16 +329,18 @@ def format_distance( fmt += area_unit_symbol d_cm = value * (1000000) tx_dist = fmt % d_cm - + else: tx_dist = fmt % value return tx_dist -def get_active_drawing(scene): +def get_active_drawing( + scene: bpy.types.Scene, +) -> Union[tuple[bpy.types.Collection, bpy.types.Camera], tuple[None, None]]: """Get active drawing collection and camera""" - props = scene.DocProperties + props = tool.Drawing.get_document_props() try: camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id)) return camera.BIMObjectProperties.collection, camera diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 950d701e4a..6bac483f21 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -134,19 +134,19 @@ class AddDrawing(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Add a drawing view to the IFC project" def _execute(self, context): - self.props = context.scene.DocProperties - hint = self.props.location_hint - if self.props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]: + props = tool.Drawing.get_document_props() + hint = props.location_hint + if props.target_view in ["PLAN_VIEW", "REFLECTED_PLAN_VIEW"]: hint = int(hint) core.add_drawing( tool.Ifc, tool.Collector, tool.Drawing, - target_view=self.props.target_view, + target_view=props.target_view, location_hint=hint, ) try: - drawing = tool.Ifc.get().by_id(self.props.active_drawing_id) + drawing = tool.Ifc.get().by_id(props.active_drawing_id) core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing) except: pass @@ -162,7 +162,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -176,7 +176,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator): row.prop(self, "should_duplicate_annotations") def _execute(self, context): - self.props = context.scene.DocProperties + props = tool.Drawing.get_document_props() core.duplicate_drawing( tool.Ifc, tool.Drawing, @@ -184,7 +184,7 @@ class DuplicateDrawing(bpy.types.Operator, tool.Ifc.Operator): should_duplicate_annotations=self.should_duplicate_annotations, ) try: - drawing = tool.Ifc.get().by_id(self.props.active_drawing_id) + drawing = tool.Ifc.get().by_id(props.active_drawing_id) core.sync_references(tool.Ifc, tool.Collector, tool.Drawing, drawing=drawing) except: pass @@ -244,7 +244,7 @@ class CreateDrawing(bpy.types.Operator): return self.execute(context) def execute(self, context): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id if self.print_all: @@ -385,7 +385,7 @@ class CreateDrawing(bpy.types.Operator): obj.hide_render = obj.name not in visible_object_names context.scene.render.filepath = str(Path(svg_path).with_suffix(".png")) - drawing_style = context.scene.DocProperties.drawing_styles[self.cprops.active_drawing_style_index] + drawing_style = self.props.drawing_styles[self.cprops.active_drawing_style_index] if drawing_style.render_type == "DEFAULT": bpy.ops.render.render(write_still=True) @@ -714,7 +714,8 @@ class CreateDrawing(bpy.types.Operator): files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()} - for link in context.scene.BIMProjectProperties.links: + props = tool.Project.get_project_props() + for link in props.links: if link.name not in IfcStore.session_files: IfcStore.session_files[link.name] = ifcopenshell.open(link.name) files[link.name] = IfcStore.session_files[link.name] @@ -1141,7 +1142,8 @@ class CreateDrawing(bpy.types.Operator): try: return tool.Ifc.get().by_guid(guid) except: - for link in bpy.context.scene.BIMProjectProperties.links: + props = tool.Project.get_project_props() + for link in props.links: if link.name not in IfcStore.session_files: IfcStore.session_files[link.name] = ifcopenshell.open(link.name) try: @@ -1458,7 +1460,8 @@ class AddSheet(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Add a sheet to the project" def _execute(self, context): - core.add_sheet(tool.Ifc, tool.Drawing, titleblock=context.scene.DocProperties.titleblock) + props = tool.Drawing.get_document_props() + core.add_sheet(tool.Ifc, tool.Drawing, titleblock=props.titleblock) class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator): @@ -1474,7 +1477,7 @@ class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator): cls.poll_message_set("Not implemented yet.") return False - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -1483,7 +1486,7 @@ class DuplicateSheet(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): pass """ - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() core.duplicate_sheet( tool.Ifc, tool.Drawing, @@ -1508,7 +1511,7 @@ class OpenLayout(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id) sheet_builder = sheeter.SheetBuilder() sheet_builder.update_sheet_drawing_sizes(sheet) @@ -1530,7 +1533,8 @@ class SelectAllSheets(bpy.types.Operator): return self.execute(context) def execute(self, context): - for sheet in context.scene.DocProperties.sheets: + props = tool.Drawing.get_document_props() + for sheet in props.sheets: if sheet.is_selected != self.select_all: sheet.is_selected = self.select_all return {"FINISHED"} @@ -1550,7 +1554,7 @@ class OpenSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_sheet_item(is_sheet=True): cls.poll_message_set("No sheet selected.") return False @@ -1564,7 +1568,7 @@ class OpenSheet(bpy.types.Operator, tool.Ifc.Operator): return self.execute(context) def execute(self, context): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command if self.open_all: @@ -1612,7 +1616,7 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() # Won't be visible in UI anyway. if not props.sheets or not context.scene.BIMProperties.data_dir: return False @@ -1622,8 +1626,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): return True def _execute(self, context): - props = context.scene.DocProperties - active_drawing = tool.Drawing.get_active_drawing_item() + props = tool.Drawing.get_document_props() + active_drawing = props.drawings[props.active_drawing_index] assert active_drawing active_sheet = tool.Drawing.get_active_sheet(context) @@ -1680,7 +1684,7 @@ class RemoveDrawingFromSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() active_item = tool.Drawing.get_active_sheet_item() if active_item is None: return False @@ -1714,7 +1718,7 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_sheet_item(is_sheet=True): cls.poll_message_set("No sheet selected.") return False @@ -1731,7 +1735,7 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): scene = context.scene - props = scene.DocProperties + props = tool.Drawing.get_document_props() svg2pdf_command = tool.Blender.get_addon_preferences().svg2pdf_command svg2dxf_command = tool.Blender.get_addon_preferences().svg2dxf_command @@ -1838,7 +1842,8 @@ class SelectAllDrawings(bpy.types.Operator): return self.execute(context) def execute(self, context): - for drawing in context.scene.DocProperties.drawings: + props = tool.Drawing.get_document_props() + for drawing in props.drawings: if drawing.is_selected != self.select_all: drawing.is_selected = self.select_all return {"FINISHED"} @@ -1857,7 +1862,7 @@ class OpenDrawing(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -1871,7 +1876,7 @@ class OpenDrawing(bpy.types.Operator): return self.execute(context) def execute(self, context): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if self.open_all: drawings = [ tool.Ifc.get().by_id(d.ifc_definition_id) for d in self.props.drawings if d.is_drawing and d.is_selected @@ -1907,7 +1912,7 @@ class ActivateModel(bpy.types.Operator): bl_description = "Activate the model view, hide all annotations" def execute(self, context): - dprops = bpy.context.scene.DocProperties + dprops = tool.Drawing.get_document_props() dprops.active_drawing_id = 0 CutDecorator.uninstall() @@ -1971,11 +1976,12 @@ class ActivateDrawingBase: return self.execute(context) def execute(self, context): - if bpy.context.scene.DocProperties.is_editing_drawings == False: + props = tool.Drawing.get_document_props() + if props.is_editing_drawings == False: bpy.ops.bim.load_drawings() drawing = tool.Ifc.get().by_id(self.drawing) - dprops = bpy.context.scene.DocProperties + dprops = tool.Drawing.get_document_props() if self.use_quick_preview: tool.Blender.activate_camera(tool.Drawing.import_temporary_drawing_camera(drawing)) @@ -2027,7 +2033,7 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -2050,7 +2056,7 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_sheet_item(reference_type="DRAWING"): cls.poll_message_set("No drawing selected.") return False @@ -2066,7 +2072,8 @@ class SelectDocIfcFile(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - context.scene.DocProperties.ifc_files[self.index].name = self.filepath + props = tool.Drawing.get_document_props() + props.ifc_files[self.index].name = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -2098,7 +2105,7 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -2112,12 +2119,10 @@ class RemoveDrawing(bpy.types.Operator, tool.Ifc.Operator): return self.execute(context) def _execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if self.remove_all: drawings = [ - tool.Ifc.get().by_id(d.ifc_definition_id) - for d in context.scene.DocProperties.drawings - if d.is_drawing and d.is_selected + tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected ] else: if not self.drawing: @@ -2187,7 +2192,8 @@ class ReloadDrawingStyles(bpy.types.Operator): with open(json_path, "r") as fi: shading_styles_json = json.load(fi) - drawing_styles = context.scene.DocProperties.drawing_styles + props = tool.Drawing.get_document_props() + drawing_styles = props.drawing_styles drawing_styles.clear() styles = [style for style in shading_styles_json] for style_name in styles: @@ -2212,7 +2218,8 @@ class AddDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - drawing_styles = context.scene.DocProperties.drawing_styles + props = tool.Drawing.get_document_props() + drawing_styles = props.drawing_styles new = drawing_styles.add() # drawing style is saved to ifc on rename new.name = tool.Blender.ensure_unique_name("New Drawing Style", drawing_styles) @@ -2227,7 +2234,8 @@ class RemoveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): index: bpy.props.IntProperty() def execute(self, context): - context.scene.DocProperties.drawing_styles.remove(self.index) + props = tool.Drawing.get_document_props() + props.drawing_styles.remove(self.index) context.scene.camera.data.BIMCameraProperties.active_drawing_style_index = max(self.index - 1, 0) bpy.ops.bim.save_drawing_styles_data() return {"FINISHED"} @@ -2283,7 +2291,8 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): index = int(self.index) else: index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index - scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style) + props = tool.Drawing.get_document_props() + props.drawing_styles[index].raster_style = json.dumps(style) bpy.ops.bim.save_drawing_styles_data() return {"FINISHED"} @@ -2309,7 +2318,8 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator): if not DrawingsData.is_loaded: DrawingsData.load() drawing_pset_data = DrawingsData.data["active_drawing_pset_data"] - drawing_styles = context.scene.DocProperties.drawing_styles + props = tool.Drawing.get_document_props() + drawing_styles = props.drawing_styles rel_path = drawing_pset_data["ShadingStyles"] current_style = drawing_pset_data.get("CurrentShadingStyle", None) @@ -2338,7 +2348,7 @@ class SaveDrawingStylesData(bpy.types.Operator, tool.Ifc.Operator): new_style_name = None ifc_file = tool.Ifc.get() - drawing = ifc_file.by_id(context.scene.DocProperties.active_drawing_id) + drawing = ifc_file.by_id(props.active_drawing_id) pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing") ifcopenshell.api.run( "pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": new_style_name} @@ -2357,17 +2367,18 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): scene = context.scene ifc_file = tool.Ifc.get() active_drawing_style_index = scene.camera.data.BIMCameraProperties.active_drawing_style_index + props = tool.Drawing.get_document_props() - if active_drawing_style_index >= len(scene.DocProperties.drawing_styles): + if active_drawing_style_index >= len(props.drawing_styles): self.report({"ERROR"}, "Could not find active drawing style") return {"CANCELLED"} - self.drawing_style = scene.DocProperties.drawing_styles[active_drawing_style_index] + self.drawing_style = props.drawing_styles[active_drawing_style_index] self.set_raster_style(context) self.set_query(context) - drawing = ifc_file.by_id(scene.DocProperties.active_drawing_id) + drawing = ifc_file.by_id(props.active_drawing_id) pset = tool.Pset.get_element_pset(drawing, "EPset_Drawing") ifcopenshell.api.run( "pset.edit_pset", ifc_file, pset=pset, properties={"CurrentShadingStyle": self.drawing_style.name} @@ -2392,7 +2403,8 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): def set_query(self, context: bpy.types.Context) -> None: self.include_global_ids = [] self.exclude_global_ids = [] - for ifc_file in context.scene.DocProperties.ifc_files: + props = tool.Drawing.get_document_props() + for ifc_file in props.ifc_files: try: ifc = ifcopenshell.open(ifc_file.name) except: @@ -2507,14 +2519,14 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not props.schedules: cls.poll_message_set("No schedule selected.") return False return props.schedules and props.sheets and context.scene.BIMProperties.data_dir def _execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() active_schedule = props.schedules[props.active_schedule_index] active_sheet = tool.Drawing.get_active_sheet(context) schedule = tool.Ifc.get().by_id(active_schedule.ifc_definition_id) @@ -2573,14 +2585,14 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not props.references: cls.poll_message_set("No reference selected.") return False return props.references and props.sheets and context.scene.BIMProperties.data_dir def _execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() active_reference = props.references[props.active_reference_index] active_sheet = tool.Drawing.get_active_sheet(context) extref = tool.Ifc.get().by_id(active_reference.ifc_definition_id) @@ -2682,7 +2694,8 @@ class AddDrawingStyleAttribute(bpy.types.Operator): def execute(self, context): props = context.scene.camera.data.BIMCameraProperties - context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.add() + dprops = tool.Drawing.get_document_props() + dprops.drawing_styles[props.active_drawing_style_index].attributes.add() return {"FINISHED"} @@ -2695,7 +2708,8 @@ class RemoveDrawingStyleAttribute(bpy.types.Operator): def execute(self, context): props = context.scene.camera.data.BIMCameraProperties - context.scene.DocProperties.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index) + dprops = tool.Drawing.get_document_props() + dprops.drawing_styles[props.active_drawing_style_index].attributes.remove(self.index) return {"FINISHED"} @@ -2979,7 +2993,7 @@ class LoadSheets(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): core.load_sheets(tool.Drawing) - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() sheets_not_found = [] for sheet_prop in props.sheets: if not sheet_prop.is_sheet: @@ -3009,7 +3023,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator): document_type: Literal["SHEET", "TITLEBLOCK", "EMBEDDED"] def invoke(self, context, event): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id) if sheet.is_a("IfcDocumentInformation"): self.document_type = "SHEET" @@ -3023,6 +3037,7 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator): return context.window_manager.invoke_props_dialog(self) def draw(self, context): + props = tool.Drawing.get_document_props() if self.document_type == "SHEET": row = self.layout.row() row.prop(self, "identification", text="Identification") @@ -3030,13 +3045,13 @@ class EditSheet(bpy.types.Operator, tool.Ifc.Operator): row.prop(self, "name", text="Name") elif self.document_type == "TITLEBLOCK": row = self.layout.row() - row.prop(context.scene.DocProperties, "titleblock", text="Titleblock") + row.prop(props, "titleblock", text="Titleblock") elif self.document_type == "EMBEDDED": row = self.layout.row() row.prop(self, "identification", text="Identification") def _execute(self, context): - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() sheet = tool.Ifc.get().by_id(self.props.sheets[self.props.active_sheet_index].ifc_definition_id) if self.document_type == "SHEET": core.rename_sheet(tool.Ifc, tool.Drawing, sheet=sheet, identification=self.identification, name=self.name) @@ -3134,7 +3149,7 @@ class ExpandTargetView(bpy.types.Operator): target_view: bpy.props.StringProperty() def execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() for drawing in [d for d in props.drawings if d.target_view == self.target_view]: drawing.is_expanded = True core.load_drawings(tool.Drawing) @@ -3150,7 +3165,7 @@ class ContractTargetView(bpy.types.Operator): target_view: bpy.props.StringProperty() def execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() for drawing in [d for d in props.drawings if d.target_view == self.target_view]: drawing.is_expanded = False core.load_drawings(tool.Drawing) @@ -3166,7 +3181,7 @@ class ExpandSheet(bpy.types.Operator): sheet: bpy.props.IntProperty() def execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]: sheet.is_expanded = True core.load_sheets(tool.Drawing) @@ -3182,7 +3197,7 @@ class ContractSheet(bpy.types.Operator): sheet: bpy.props.IntProperty() def execute(self, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]: sheet.is_expanded = False core.load_sheets(tool.Drawing) @@ -3373,7 +3388,7 @@ class ConvertSVGToDXF(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.DocProperties + props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False @@ -3387,14 +3402,13 @@ class ConvertSVGToDXF(bpy.types.Operator): return self.execute(context) def execute(self, context): + props = tool.Drawing.get_document_props() if self.convert_all: drawings = [ - tool.Ifc.get().by_id(d.ifc_definition_id) - for d in context.scene.DocProperties.drawings - if d.is_drawing and d.is_selected + tool.Ifc.get().by_id(d.ifc_definition_id) for d in props.drawings if d.is_drawing and d.is_selected ] else: - drawings = [tool.Ifc.get().by_id(context.scene.DocProperties.drawings.get(self.view).ifc_definition_id)] + drawings = [tool.Ifc.get().by_id(props.drawings.get(self.view).ifc_definition_id)] drawing_uris: list[Path] = [] drawings_not_found: list[str] = [] diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 02ce195100..6c875d1cfe 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -76,7 +76,7 @@ def update_diagram_scale(self, context): try: element = ( tool.Ifc.get() - .by_id(self.id_data.BIMMeshProperties.ifc_definition_id) + .by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id) .OfProductRepresentation[0] .ShapeOfProduct[0] ) @@ -93,7 +93,7 @@ def update_diagram_scale(self, context): ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties=diagram_scale) -def update_is_nts(self, context): +def update_is_nts(self: "BIMCameraProperties", context: bpy.types.Context) -> None: if not self.update_props: return if not context.scene.camera or context.scene.camera.data != self.id_data: @@ -104,7 +104,7 @@ def update_is_nts(self, context): try: element = ( tool.Ifc.get() - .by_id(self.id_data.BIMMeshProperties.ifc_definition_id) + .by_id(tool.Geometry.get_mesh_props(self.id_data).ifc_definition_id) .OfProductRepresentation[0] .ShapeOfProduct[0] ) @@ -192,8 +192,8 @@ def get_drawing_style_name(self: "DrawingStyle"): def set_drawing_style_name(self: "DrawingStyle", new_value: str) -> None: """ensure the name is unique""" - scene = bpy.context.scene - drawing_styles = [s.name for s in scene.DocProperties.drawing_styles if s.name != self.name] + props = tool.Drawing.get_document_props() + drawing_styles = [s.name for s in props.drawing_styles if s.name != self.name] new_value = tool.Blender.ensure_unique_name(new_value, drawing_styles) old_value = self.name self["name"] = new_value diff --git a/src/bonsai/bonsai/bim/module/drawing/scheduler.py b/src/bonsai/bonsai/bim/module/drawing/scheduler.py index d00fd1d94f..00ce2c1612 100644 --- a/src/bonsai/bonsai/bim/module/drawing/scheduler.py +++ b/src/bonsai/bonsai/bim/module/drawing/scheduler.py @@ -56,7 +56,7 @@ def a1_to_rc(cell): class Scheduler: - def schedule(self, infile, outfile): + def schedule(self, infile: str, outfile: str) -> None: self.svg = svgwrite.Drawing( outfile, debug=False, @@ -71,11 +71,12 @@ class Scheduler: elif infile.endswith("xlsx"): self.schedule_xlsx(infile, outfile) - def parse_css(self, infile): + def parse_css(self, infile: str) -> None: + props = tool.Drawing.get_document_props() stylesheet_path = os.path.splitext(infile)[0] + ".css" if not os.path.exists(stylesheet_path): - stylesheet_rel_path = getattr(bpy.context.scene.DocProperties, "schedules_stylesheet_path") - ifc_file_path = os.path.dirname(IfcStore.path) + stylesheet_rel_path = props.schedules_stylesheet_path + ifc_file_path = os.path.dirname(tool.Ifc.get_path()) stylesheet_path = ifc_file_path + "\\" + stylesheet_rel_path if not os.path.exists(stylesheet_path): stylesheet_path = tool.Blender.get_data_dir_path(Path("assets") / "schedule.css") @@ -91,7 +92,7 @@ class Scheduler: self.svg.defs.add(self.svg.style(css)) - def schedule_xlsx(self, infile, outfile): + def schedule_xlsx(self, infile: str, outfile: str) -> None: workbook = openpyxl.open(infile, data_only=True) sheet = workbook.active @@ -236,7 +237,7 @@ class Scheduler: self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height) self.svg.save(pretty=True) - def schedule_ods(self, infile, outfile): + def schedule_ods(self, infile: str, outfile: str) -> None: doc = load_ods(infile) # useful for debugging ods @@ -495,11 +496,11 @@ class Scheduler: self.svg["viewBox"] = "0 0 {} {}".format(total_width, total_height) self.svg.save(pretty=True) - def get_style(self, style_name, styles): + def get_style(self, style_name: str, styles: dict) -> dict: style = styles[style_name] if style_name else {} return style - def get_box_alignment(self, style): + def get_box_alignment(self, style: dict) -> str: if style and "vertical-align" in style and style["vertical-align"] != "automatic": vertical_align = style["vertical-align"] else: diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index f35fc55325..6bf62ddec2 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -49,7 +49,7 @@ class BIM_PT_camera(Panel): return self.layout.use_property_split = True - dprops = context.scene.DocProperties + dprops = tool.Drawing.get_document_props() props = context.scene.camera.data.BIMCameraProperties col = self.layout.column(align=True) @@ -161,7 +161,7 @@ class BIM_PT_drawing_underlay(Panel): layout.use_property_split = True camera = context.scene.camera assert camera - dprops = context.scene.DocProperties + dprops = tool.Drawing.get_document_props() props = camera.data.BIMCameraProperties drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles) @@ -229,7 +229,7 @@ class BIM_PT_drawings(Panel): draw_project_not_saved_ui(self) return - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if not self.props.is_editing_drawings: row = self.layout.row(align=True) @@ -302,7 +302,7 @@ class BIM_PT_schedules(Panel): draw_project_not_saved_ui(self) return - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if not self.props.is_editing_schedules: row = self.layout.row(align=True) @@ -352,7 +352,7 @@ class BIM_PT_references(Panel): draw_project_not_saved_ui(self) return - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if not self.props.is_editing_references: row = self.layout.row(align=True) @@ -394,7 +394,7 @@ class BIM_PT_sheets(Panel): draw_project_not_saved_ui(self) return - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if not self.props.is_editing_sheets: row = self.layout.row(align=True) @@ -601,7 +601,7 @@ class BIM_UL_drawinglist(bpy.types.UIList): selected_icon = "CHECKBOX_HLT" if item.is_selected else "CHECKBOX_DEHLT" row.prop(item, "is_selected", text="", icon=selected_icon, emboss=False) row.prop(item, "name", text="", emboss=False) - self.props = context.scene.DocProperties + self.props = tool.Drawing.get_document_props() if ( self.props.drawings and self.props.active_drawing_id diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py index 30d57bf3db..bd679cb065 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -197,6 +197,8 @@ def create_annotation_occurrence(context): class AnnotationToolUI: + layout: bpy.types.UILayout + @classmethod def draw(cls, context, layout): cls.layout = layout @@ -224,7 +226,8 @@ class AnnotationToolUI: @classmethod def draw_create_object_interface(cls): row = cls.layout.row(align=True) - row.prop(bpy.context.scene.DocProperties, "should_draw_decorations", text="Viewport Annotations") + props = tool.Drawing.get_document_props() + row.prop(props, "should_draw_decorations", text="Viewport Annotations") @classmethod def draw_edit_object_interface(cls, context): diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index 533cdcd6bf..4e9eb9cf6f 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -98,12 +98,14 @@ addon_keymaps = [] @persistent -def block_scale(scene): +def block_scale(scene: bpy.types.Scene) -> None: + import bonsai.tool as tool + if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active): if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id: if obj.scale != (1, 1, 1): obj.scale = (1, 1, 1) - elif isinstance(obj, bpy.types.Mesh) and obj.BIMMeshProperties.ifc_definition_id: + elif isinstance(obj, bpy.types.Mesh) and tool.Geometry.get_mesh_props(obj).ifc_definition_id: if obj.scale != (1, 1, 1): obj.scale = (1, 1, 1) diff --git a/src/bonsai/bonsai/bim/module/geometry/data.py b/src/bonsai/bonsai/bim/module/geometry/data.py index a719d27893..2ba25df702 100644 --- a/src/bonsai/bonsai/bim/module/geometry/data.py +++ b/src/bonsai/bonsai/bim/module/geometry/data.py @@ -53,15 +53,16 @@ class ViewportData: obj = bpy.context.active_object element = tool.Ifc.get_entity(obj) - modes = [obj_mode] - - if bpy.context.scene.BIMGeometryProperties.representation_obj: + modes: list[tuple[str, str, str, str, int]] = [obj_mode] + gprops = tool.Geometry.get_geometry_props() + if gprops.representation_obj: modes.append(item_mode) if not obj: return modes - if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs: + pprops = tool.Project.get_project_props() + if obj in pprops.clipping_planes_objs: pass elif element: if tool.Geometry.is_locked(element): @@ -107,8 +108,9 @@ class RepresentationsData: element = tool.Ifc.get_entity(obj) active_representation_id = None - if obj.data and hasattr(obj.data, "BIMMeshProperties"): - active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id + active_representation = tool.Geometry.get_active_representation(obj) + if active_representation: + active_representation_id = active_representation.id() for representation in tool.Geometry.get_representations_iter(element): representation_type = representation.RepresentationType @@ -158,9 +160,9 @@ class RepresentationsData: if not obj.data: return [] element = tool.Ifc.get_entity(obj) - if not (active_representation_id := obj.data.BIMMeshProperties.ifc_definition_id): + base_representation = tool.Geometry.get_active_representation(obj) + if not base_representation: return [] # Maybe in profile editing mode - base_representation = tool.Ifc.get().by_id(active_representation_id) # shape aspects matching context of the active representation matching_shape_aspects = [] @@ -390,7 +392,7 @@ class PlacementData: def load(cls): cls.data = {"has_placement": cls.has_placement()} - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() obj = bpy.context.active_object if obj and props.has_blender_offset: xyz = cls.original_xyz(obj) @@ -413,7 +415,7 @@ class PlacementData: @classmethod def original_xyz(cls, obj): unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() xyz = ifcopenshell.util.geolocation.xyz2enh( obj.matrix_world[0][3], obj.matrix_world[1][3], diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py index 2be10a27f0..5993cc4469 100644 --- a/src/bonsai/bonsai/bim/module/geometry/decorator.py +++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py @@ -46,12 +46,14 @@ class ItemDecorator: obj_is_boolean: dict[str, list[ifcopenshell.entity_instance]] = {} objs: dict[str, dict[str, list]] = {} obj_matrix: dict[str, Matrix] = {} - for item_obj in context.scene.BIMGeometryProperties.item_objs: + props = tool.Geometry.get_geometry_props() + for item_obj in props.item_objs: if obj := item_obj.obj: obj: bpy.types.Object objs[obj.name] = cls.get_obj_data(obj) obj_is_selected[obj.name] = obj.select_get() - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + item = tool.Geometry.get_active_representation(obj) + assert item obj_is_boolean[obj.name] = [i for i in tool.Ifc.get().get_inverse(item) if i.is_a("IfcBooleanResult")] obj_matrix[obj.name] = obj.matrix_world.copy() @@ -143,7 +145,8 @@ class ItemDecorator: color = selected_elements_color blf.color(font_id, *color) - for item in context.scene.BIMGeometryProperties.item_objs: + props = tool.Geometry.get_geometry_props() + for item in props.item_objs: if (obj := item.obj) and obj.hide_get() == False: if obj.select_get(): centroid = obj.matrix_world @ Vector(obj.bound_box[0]).lerp(Vector(obj.bound_box[6]), 0.5) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 6a234800bf..1d43e584d7 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -23,6 +23,7 @@ import numpy as np import numpy.typing as npt import ifcopenshell import ifcopenshell.api.layer +import ifcopenshell.api.style import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation @@ -78,7 +79,8 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator): self.separate_element(element) def separate_item(self, context, obj): - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + item = tool.Geometry.get_active_representation(obj) + assert item if tool.Geometry.is_meshlike_item(item): previous_selected_objects = context.selected_objects bpy.ops.mesh.separate(type=self.type) @@ -86,7 +88,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator): if obj in previous_selected_objects: continue self.add_meshlike_item(obj) - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(tool.Geometry.get_geometry_props().representation_obj) else: self.report({"INFO"}, f"Separating an {item.is_a()} is not supported") @@ -121,7 +123,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator): representation.Items = list(representation.Items) + [item] obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Ifc.link(item, obj) props.add_item_object(obj, item) def separate_element(self, element): @@ -302,11 +304,12 @@ class AddRepresentation(bpy.types.Operator, tool.Ifc.Operator): return context.window_manager.invoke_props_dialog(self) def draw(self, context): + props = tool.Geometry.get_geometry_props() row = self.layout.row() row.prop(self, "representation_conversion_method", text="") if self.representation_conversion_method == "OBJECT": row = self.layout.row() - row.prop(context.scene.BIMGeometryProperties, "representation_from_object", text="") + row.prop(props, "representation_from_object", text="") class SelectConnection(bpy.types.Operator, tool.Ifc.Operator): @@ -457,13 +460,17 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} def update_obj_mesh_representation(self, context: bpy.types.Context, obj: bpy.types.Object) -> None: + data = obj.data + assert tool.Geometry.is_data_supported_for_adding_representation(data) + mprops = tool.Geometry.get_mesh_props(data) + product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) material = ifcopenshell.util.element.get_material(product, should_skip_usage=True) # NOTE: Currently iterator doesn't detect whether opening is actually affected the representation # or it's just present on the element. In theory, we can also allow editing representations # if we know that representation wasn't affected by existing openings. - has_openings = tool.Geometry.has_openings(product) and obj.data.BIMMeshProperties.has_openings_applied + has_openings = tool.Geometry.has_openings(product) and tool.Geometry.get_mesh_props(data).has_openings_applied if has_openings and not self.apply_openings: # Meshlike things with openings can only be updated without openings applied. if self.from_ui: @@ -486,7 +493,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): if tool.Ifc.is_moved(obj) or tool.Geometry.is_scaled(obj): core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) - old_representation = self.file.by_id(obj.data.BIMMeshProperties.ifc_definition_id) + old_representation = tool.Geometry.get_active_representation(obj) + assert old_representation if material and material.is_a() in ["IfcMaterialProfileSet", "IfcMaterialLayerSet"]: if self.ifc_representation_class == "IfcTessellatedFaceSet": # We are explicitly casting to a tessellation, so remove all parametric materials. @@ -545,12 +553,12 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): tool.Geometry.run_style_add_style(obj=mat) for mat in tool.Geometry.get_object_materials_without_styles(obj) ] - ifcopenshell.api.run( - "style.assign_representation_styles", + props = tool.Geometry.get_geometry_props() + ifcopenshell.api.style.assign_representation_styles( self.file, shape_representation=new_representation, styles=tool.Geometry.get_styles(obj, only_assigned_to_faces=True), - should_use_presentation_style_assignment=context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, + should_use_presentation_style_assignment=props.should_use_presentation_style_assignment, ) tool.Geometry.record_object_materials(obj) @@ -569,8 +577,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): continue representation.RepresentationIdentifier = "Reference" - obj.data.BIMMeshProperties.ifc_definition_id = int(new_representation.id()) - obj.data.name = f"{old_representation.ContextOfItems.id()}/{new_representation.id()}" + tool.Ifc.link(new_representation, data) + data.name = tool.Loader.get_mesh_name(new_representation) # TODO: In simple scenarios, a type has a ShapeRepresentation of ID # 123. This is then mapped through mapped representations by @@ -586,7 +594,7 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): # transformations. core.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_representation) - if obj.data.BIMMeshProperties.ifc_parameters: + if mprops.ifc_parameters: core.get_representation_ifc_parameters(tool.Geometry, obj=obj) @@ -598,12 +606,13 @@ class UpdateParametricRepresentation(bpy.types.Operator): @classmethod def poll(cls, context): - return context.active_object and context.active_object.mode == "OBJECT" + return (obj := context.active_object) and obj.mode == "OBJECT" and tool.Geometry.has_mesh_properties(obj.data) def execute(self, context): self.file = IfcStore.get_file() obj = context.active_object - props = obj.data.BIMMeshProperties + assert obj and tool.Geometry.has_mesh_properties(obj.data) + props = tool.Geometry.get_mesh_props(obj.data) parameter = props.ifc_parameters[self.index] self.file.by_id(parameter.step_id)[parameter.index] = parameter.value show_representation_parameters = bool(props.ifc_parameters) @@ -627,8 +636,10 @@ class GetRepresentationIfcParameters(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - core.get_representation_ifc_parameters(tool.Geometry, obj=context.active_object) - parameters = context.active_object.data.BIMMeshProperties.ifc_parameters + obj = context.active_object + assert obj and tool.Geometry.has_mesh_properties((data := obj.data)) + core.get_representation_ifc_parameters(tool.Geometry, obj=obj) + parameters = tool.Geometry.get_mesh_props(data).ifc_parameters self.report({"INFO"}, f"{len(parameters)} parameters found.") @@ -721,7 +732,7 @@ class OverrideDelete(bpy.types.Operator): row = self.layout.row() row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR") - def _execute(self, context): + def _execute(self, context: bpy.types.Context): start_time = time() if self.is_batch: @@ -730,9 +741,7 @@ class OverrideDelete(bpy.types.Operator): self.process_arrays(context) clear_active_object = True for obj in context.selected_objects: - try: - obj.name - except: + if not tool.Blender.is_valid_data_block(obj): continue element = tool.Ifc.get_entity(obj) if element: @@ -782,7 +791,7 @@ class OverrideDelete(bpy.types.Operator): data["old_file"].redo() tool.Ifc.set(data["new_file"]) - def process_arrays(self, context): + def process_arrays(self, context: bpy.types.Context) -> None: selected_objects = set(context.selected_objects) array_parents = set() for obj in context.selected_objects: @@ -1030,7 +1039,8 @@ class OverrideDuplicateMove(bpy.types.Operator): # Unlink from previous boolean element # and keep object tracked for decorations. if is_tracked_opening: - new_obj.data.BIMMeshProperties.ifc_boolean_id = 0 + mprops = tool.Geometry.get_mesh_props(new_obj.data) + mprops.ifc_boolean_id = 0 tool.Root.add_tracked_opening(new_obj, tracked_opening_type) if obj == context.active_object: @@ -1054,7 +1064,7 @@ class OverrideDuplicateMove(bpy.types.Operator): if new.is_a("IfcRelSpaceBoundary"): surface = new.ConnectionGeometry.SurfaceOnRelatingElement temp_data.name = f"0/{surface.id()}" - temp_data.BIMMeshProperties.ifc_definition_id = surface.id() + tool.Ifc.link(surface, temp_data) else: tool.Blender.remove_data_block(temp_data) @@ -1090,12 +1100,14 @@ class OverrideDuplicateMove(bpy.types.Operator): @staticmethod def duplicate_item(obj: bpy.types.Object) -> None: props = tool.Geometry.get_geometry_props() - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + item = tool.Geometry.get_active_representation(obj) + assert item new_item = ifcopenshell.util.element.copy_deep(tool.Ifc.get(), item) new_obj = obj.copy() + assert tool.Geometry.has_mesh_properties(obj.data) temp_data = obj.data.copy() new_obj.data = temp_data - new_obj.data.BIMMeshProperties.ifc_definition_id = new_item.id() + tool.Ifc.link(new_item, temp_data) new_obj.name = obj.data.name = f"Item/{new_item.is_a()}/{new_item.id()}" props.add_item_object(new_obj, new_item) @@ -1671,7 +1683,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator): def join_item(self) -> None: props = tool.Geometry.get_geometry_props() ifc_file = tool.Ifc.get() - item = tool.Ifc.get().by_id(self.target.data.BIMMeshProperties.ifc_definition_id) + item = tool.Geometry.get_active_representation(self.target) + assert item if tool.Geometry.is_meshlike_item(item): tool.Geometry.dissolve_triangulated_edges(self.target) item_objs = [i.obj for i in props.item_objs if i.obj] @@ -1694,7 +1707,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator): for item_data in items_data: props.add_item_object(item_data["obj"], ifc_file.by_id(item_data["ifc_definition_id"])) - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) bpy.context.view_layer.update() tool.Root.reload_item_decorator() @@ -1702,7 +1715,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator): ifc_file = tool.Ifc.get() builder = ShapeBuilder(ifc_file) si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) - representation = ifc_file.by_id(self.target.data.BIMMeshProperties.ifc_definition_id) + representation = tool.Geometry.get_active_representation(self.target) + assert representation representation_type = representation.RepresentationType if representation_type in ("Tessellation", "Brep"): for obj in bpy.context.selected_objects: @@ -1741,7 +1755,8 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator): continue # Only objects of the same representation type can be joined - obj_rep = ifc_file.by_id(obj.data.BIMMeshProperties.ifc_definition_id) + obj_rep = tool.Geometry.get_active_representation(obj) + assert obj_rep if obj_rep.RepresentationType != representation_type: obj.select_set(False) self.report( @@ -1892,9 +1907,10 @@ class OverrideEscape(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - if context.scene.BIMGeometryProperties.mode == "ITEM": + props = tool.Geometry.get_geometry_props() + if props.mode == "ITEM": tool.Geometry.disable_item_mode() - elif context.scene.BIMGeometryProperties.mode == "EDIT": + elif props.mode == "EDIT": bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT", should_save=False) tool.Geometry.disable_item_mode() elif tool.Model.get_model_props().openings: @@ -1950,6 +1966,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): def handle_single_object(self, context: bpy.types.Context, obj: bpy.types.Object) -> None: element = tool.Ifc.get_entity(obj) props = tool.Geometry.get_geometry_props() + pprops = tool.Project.get_project_props() if obj == props.representation_obj: self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly") elif obj in [o.obj for o in context.scene.BIMAggregateProperties.not_editing_objects]: @@ -1957,7 +1974,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): self.report( {"ERROR"}, f"Element '{obj.name}' does not belong to this aggregate and cannot be edited directly" ) - elif obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs: + elif obj in pprops.clipping_planes_objs: self.report({"ERROR"}, "Clipping planes cannot be edited") elif element: if not obj.data: @@ -2005,12 +2022,15 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): self.handle_single_object(context, obj) def enable_editing_representation_item(self, context: bpy.types.Context, obj: bpy.types.Object) -> None: - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) - element = tool.Ifc.get_entity(context.scene.BIMGeometryProperties.representation_obj) + item = tool.Geometry.get_active_representation(obj) + assert item + element = tool.Ifc.get_entity(tool.Geometry.get_geometry_props().representation_obj) if tool.Geometry.is_meshlike_item(item): tool.Geometry.dissolve_triangulated_edges(obj) tool.Blender.select_and_activate_single_object(context, obj) - obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data) + assert isinstance(mesh := obj.data, bpy.types.Mesh) + props = tool.Geometry.get_mesh_props(mesh) + props.mesh_checksum = tool.Geometry.get_mesh_checksum(mesh) self.enable_edit_mode(context) elif ( item.is_a("IfcSweptAreaSolid") @@ -2027,21 +2047,21 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): f"Couldn't import profile, editing it directly is not yet supported. Failing profile: {profile}.", ) return - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Ifc.link(item, obj.data) self.enable_edit_mode(context) ProfileDecorator.install(context) if not bpy.app.background: tool.Blender.set_viewport_tool("bim.cad_tool") elif item.is_a("IfcAnnotationFillArea"): tool.Model.import_annotation_fill_area(item, obj=obj) - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Ifc.link(item, obj.data) self.enable_edit_mode(context) ProfileDecorator.install(context) if not bpy.app.background: tool.Blender.set_viewport_tool("bim.cad_tool") elif tool.Geometry.is_curvelike_item(item): tool.Model.import_curve(item, obj=obj) - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Ifc.link(item, obj.data) self.enable_edit_mode(context) ProfileDecorator.install(context) if not bpy.app.background: @@ -2052,10 +2072,11 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator): def enable_edit_mode(self, context: bpy.types.Context) -> Union[None, set[str]]: if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}: return {"CANCELLED"} - context.scene.BIMGeometryProperties.is_changing_mode = True - if context.scene.BIMGeometryProperties.mode != "EDIT": - context.scene.BIMGeometryProperties.mode = "EDIT" - context.scene.BIMGeometryProperties.is_changing_mode = False + props = tool.Geometry.get_geometry_props() + props.is_changing_mode = True + if props.mode != "EDIT": + props.mode = "EDIT" + props.is_changing_mode = False def has_aggregates(self, objs): for obj in objs: @@ -2102,14 +2123,15 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): tool.Blender.toggle_edit_mode(context) - context.scene.BIMGeometryProperties.is_changing_mode = True - if context.scene.BIMGeometryProperties.representation_obj: - if context.scene.BIMGeometryProperties.mode != "ITEM": - context.scene.BIMGeometryProperties.mode = "ITEM" + props = tool.Geometry.get_geometry_props() + props.is_changing_mode = True + if props.representation_obj: + if props.mode != "ITEM": + props.mode = "ITEM" else: - if context.scene.BIMGeometryProperties.mode != "OBJECT": - context.scene.BIMGeometryProperties.mode = "OBJECT" - context.scene.BIMGeometryProperties.is_changing_mode = False + if props.mode != "OBJECT": + props.mode = "OBJECT" + props.is_changing_mode = False if context.active_object and self.should_save: element = tool.Ifc.get_entity(context.active_object) @@ -2155,24 +2177,27 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): else: bpy.ops.bim.edit_extrusion_profile() return self.execute(context) - elif obj.data.BIMMeshProperties.ifc_definition_id: - if not tool.Geometry.has_geometric_data(obj): + elif representation := tool.Geometry.get_active_representation(obj): + if not tool.Geometry.is_geometric_data(obj.data): self.is_valid = False self.should_save = False - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + assert tool.Geometry.has_mesh_properties(obj.data) + mesh_props = tool.Geometry.get_mesh_props(obj.data) if tool.Geometry.is_meshlike( representation - ) and obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data): + ) and mesh_props.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data): self.edited_objs.append(obj) elif getattr(element, "HasOpenings", None): self.unchanged_objs_with_openings.append(obj) else: tool.Ifc.finish_edit(obj) elif element.is_a("IfcGridAxis"): - if not tool.Geometry.has_geometric_data(obj): + if not tool.Geometry.is_geometric_data(obj.data): self.is_valid = False self.should_save = False - if obj.data.BIMMeshProperties.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data): + assert tool.Geometry.has_mesh_properties(obj.data) + mesh_props = tool.Geometry.get_mesh_props(obj.data) + if mesh_props.mesh_checksum != tool.Geometry.get_mesh_checksum(obj.data): self.edited_objs.append(obj) else: tool.Ifc.finish_edit(obj) @@ -2197,9 +2222,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} def edit_representation_item(self, obj: bpy.types.Object) -> None: - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + props = tool.Geometry.get_geometry_props() + item = tool.Geometry.get_active_representation(obj) + assert item if tool.Geometry.is_meshlike_item(item): - if tool.Geometry.has_geometric_data(obj) and obj.data.polygons: + if tool.Geometry.is_geometric_data(obj.data) and obj.data.polygons: tool.Geometry.edit_meshlike_item(obj) else: tool.Geometry.import_item(obj) @@ -2220,11 +2247,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile) - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) tool.Geometry.import_item(obj) tool.Geometry.import_item_attributes(obj) - element = tool.Ifc.get_entity(bpy.context.scene.BIMGeometryProperties.representation_obj) + element = tool.Ifc.get_entity(props.representation_obj) # Only certain classes should have a footprint if element.is_a() in ("IfcSlab", "IfcRamp"): footprint_context = ifcopenshell.util.representation.get_context( @@ -2277,9 +2304,9 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): for inverse in tool.Ifc.get().get_inverse(item): ifcopenshell.util.element.replace_attribute(inverse, item, profile) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item) - obj.data.BIMMeshProperties.ifc_definition_id = profile.id() + tool.Ifc.link(profile, obj.data) - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) tool.Geometry.import_item(obj) tool.Geometry.import_item_attributes(obj) elif tool.Geometry.is_curvelike_item(item): @@ -2305,10 +2332,9 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.util.element.replace_attribute(inverse, item, new) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item) - obj.data.BIMMeshProperties.ifc_definition_id = new.id() + tool.Ifc.link(new, obj.data) tool.Geometry.import_item(obj) - props = tool.Geometry.get_geometry_props() for item in additional_curves: representation = tool.Geometry.get_active_representation(props.representation_obj) representation = ifcopenshell.util.representation.resolve_representation(representation) @@ -2317,7 +2343,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): name = f"Item/{item.is_a()}/{item.id()}" mesh = bpy.data.meshes.new(name) new_obj = bpy.data.objects.new(name, mesh) - new_obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Ifc.link(item, new_obj.data) bpy.context.collection.objects.link(new_obj) props.add_item_object(new_obj, item) new_obj.matrix_world = obj.matrix_world @@ -2330,10 +2356,11 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): def enable_edit_mode(self, context): if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}: return {"CANCELLED"} - context.scene.BIMGeometryProperties.is_changing_mode = True - if context.scene.BIMGeometryProperties.mode != "EDIT": - context.scene.BIMGeometryProperties.mode = "EDIT" - context.scene.BIMGeometryProperties.is_changing_mode = False + props = tool.Geometry.get_geometry_props() + props.is_changing_mode = True + if props.mode != "EDIT": + props.mode = "EDIT" + props.is_changing_mode = False class FlipObject(bpy.types.Operator): @@ -2370,12 +2397,13 @@ class EnableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): item.tags += "," item.tags += tag - if obj.data and hasattr(obj.data, "BIMMeshProperties"): - active_representation_id = obj.data.BIMMeshProperties.ifc_definition_id - representation = tool.Ifc.get().by_id(active_representation_id) + if tool.Geometry.has_mesh_properties((data := obj.data)): + representation = tool.Geometry.get_data_representation(data) + assert representation # Shape aspects must be considered from the PartOfProductDefinitionShape level element = tool.Ifc.get_entity(obj) + assert element product_reps = [] if element.is_a("IfcProduct"): product_reps = [element.Representation] @@ -2440,7 +2468,8 @@ class DisableEditingRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - obj.BIMGeometryProperties.is_editing = False + assert obj + tool.Geometry.get_object_geometry_props(obj).is_editing = False class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator): @@ -2451,9 +2480,12 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - if context.scene.BIMGeometryProperties.representation_obj: + if tool.Geometry.get_geometry_props().representation_obj: return False # Artificial restriction for now to prevent removing when in item mode - if not (obj := tool.Geometry.get_active_or_representation_obj()) or len(obj.BIMGeometryProperties.items) <= 1: + if ( + not (obj := tool.Geometry.get_active_or_representation_obj()) + or len(tool.Geometry.get_object_geometry_props(obj).items) <= 1 + ): cls.poll_message_set( "Active object need to have more than 1 representation items to keep representation valid" ) @@ -2488,13 +2520,17 @@ class SelectRepresentationItem(bpy.types.Operator): def execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - item = tool.Ifc.get().by_id(obj.BIMGeometryProperties.active_item.ifc_definition_id) + obj_props = tool.Geometry.get_object_geometry_props(obj) + assert obj_props.active_item + item = tool.Ifc.get().by_id(obj_props.active_item.ifc_definition_id) item_ids = self.get_nested_item_ids(item) props = tool.Geometry.get_geometry_props() for item_obj in props.item_objs: - if item_obj.obj.data.BIMMeshProperties.ifc_definition_id in item_ids: - tool.Blender.select_object(item_obj.obj) + obj_ = item_obj.obj + props = tool.Geometry.get_mesh_props(obj_.data) + if props.ifc_definition_id in item_ids: + tool.Blender.select_object(obj_) return {"FINISHED"} def get_nested_item_ids(self, item): @@ -2512,7 +2548,7 @@ class SelectRepresentationItem(bpy.types.Operator): def poll_editing_representation_item_style(cls, context): if not (obj := tool.Geometry.get_active_or_representation_obj()): return False - props = obj.BIMGeometryProperties + props = tool.Geometry.get_object_geometry_props(obj) if not props.is_editing: return False if not (item := props.active_item): @@ -2547,7 +2583,8 @@ class EnableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + assert obj + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_style = True ifc_file = tool.Ifc.get() @@ -2566,7 +2603,8 @@ class EditRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + assert obj + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_style = False ifc_file = tool.Ifc.get() @@ -2588,7 +2626,7 @@ class DisableEditingRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operato def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_style = False @@ -2604,7 +2642,8 @@ class UnassignRepresentationItemStyle(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): active_obj = tool.Geometry.get_active_or_representation_obj() - active_props = active_obj.BIMGeometryProperties + assert active_obj + active_props = tool.Geometry.get_object_geometry_props(active_obj) active_props.is_editing_item_style = False # Get active representation item @@ -2671,7 +2710,8 @@ class EnableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Op def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + assert obj + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_shape_aspect = True # set dropdown to currently active shape aspect @@ -2687,11 +2727,13 @@ class EditRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMGeometryProperties + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_shape_aspect = False ifc_file = tool.Ifc.get() + assert props.active_item representation_item_id = props.active_item.ifc_definition_id representation_item = ifc_file.by_id(representation_item_id) @@ -2744,7 +2786,8 @@ class DisableEditingRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.O def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() - props = obj.BIMGeometryProperties + assert obj + props = tool.Geometry.get_object_geometry_props(obj) props.is_editing_item_shape_aspect = False @@ -2755,10 +2798,12 @@ class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Opera def _execute(self, context): obj = tool.Geometry.get_active_or_representation_obj() + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMGeometryProperties + props = tool.Geometry.get_object_geometry_props(obj) ifc_file = tool.Ifc.get() + assert props.active_item representation_item_id = props.active_item.ifc_definition_id representation_item = ifc_file.by_id(representation_item_id) shape_aspect = ifc_file.by_id(props.active_item.shape_aspect_id) @@ -2841,7 +2886,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): boolean_ids.add(item.SecondOperand.id()) continue item_mesh = bpy.data.meshes.new(f"Item/{item.is_a()}/{item_id}") - item_mesh.BIMMeshProperties.ifc_definition_id = item_id + tool.Ifc.link(item, item_mesh) item_obj = bpy.data.objects.new(f"Item/{item.is_a()}/{item_id}", item_mesh) item_obj.matrix_world = obj.matrix_world @@ -2871,7 +2916,7 @@ class UpdateItemAttributes(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object tool.Geometry.sync_item_positions() tool.Geometry.update_item_attributes(obj) - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(tool.Geometry.get_geometry_props().representation_obj) tool.Geometry.import_item(obj) tool.Root.reload_item_decorator() @@ -2888,6 +2933,10 @@ class NameProfile(bpy.types.Operator, tool.Ifc.Operator): options={"SKIP_SAVE"}, ) + if TYPE_CHECKING: + extrusion_item_obj: str + profile_name: str + def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) @@ -2902,7 +2951,7 @@ class NameProfile(bpy.types.Operator, tool.Ifc.Operator): ifc_file = tool.Ifc.get() extrusion_item_obj = bpy.data.objects[self.extrusion_item_obj] - mesh_props = extrusion_item_obj.data.BIMMeshProperties + mesh_props = tool.Geometry.get_mesh_props(extrusion_item_obj.data) extrusion = ifc_file.by_id(mesh_props.ifc_definition_id) assert extrusion.is_a("IfcSweptAreaSolid") profile = extrusion.SweptArea @@ -2972,9 +3021,9 @@ class AddMeshlikeItem(bpy.types.Operator, tool.Ifc.Operator): props.add_item_object(obj, item) representation.Items = list(representation.Items) + [item] - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id() tool.Root.reload_item_decorator() @@ -3020,10 +3069,10 @@ class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator): props.add_item_object(obj, item) representation.Items = list(representation.Items) + [item] - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id() tool.Geometry.import_item(obj) tool.Geometry.import_item_attributes(obj) tool.Root.reload_item_decorator() @@ -3091,10 +3140,10 @@ class AddCurvelikeItem(bpy.types.Operator, tool.Ifc.Operator): props.add_item_object(obj, item) representation.Items = list(representation.Items) + [item] - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id() tool.Geometry.import_item(obj) tool.Geometry.import_item_attributes(obj) @@ -3140,10 +3189,10 @@ class AddHalfSpaceSolidItem(bpy.types.Operator, tool.Ifc.Operator): representation = ifcopenshell.util.representation.resolve_representation(representation) representation.Items = list(representation.Items) + [item] - tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Geometry.reload_representation(props.representation_obj) obj.name = obj.data.name = f"Item/{item.is_a()}/{item.id()}" - obj.data.BIMMeshProperties.ifc_definition_id = item.id() + tool.Geometry.get_mesh_props(obj.data).ifc_definition_id = item.id() tool.Geometry.import_item(obj) # TODO refactor to core and not rely on selection diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py index 2ab4ec5efe..7b80e9b040 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -32,7 +32,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) -from typing import Optional, TYPE_CHECKING, Union +from typing import Optional, TYPE_CHECKING, Union, Literal def get_contexts(self, context): @@ -270,6 +270,9 @@ class BIMObjectGeometryProperties(PropertyGroup): representation_item_layer: str +GeometryMode = Literal["OBJECT", "ITEM", "EDIT"] + + class BIMGeometryProperties(PropertyGroup): # Revit workaround should_use_presentation_style_assignment: BoolProperty(name="Force Presentation Style Assignment", default=False) @@ -308,7 +311,7 @@ class BIMGeometryProperties(PropertyGroup): should_force_faceted_brep: bool should_force_triangulation: bool is_changing_mode: bool - mode: str + mode: GeometryMode representation_obj: Union[bpy.types.Object, None] item_objs: bpy.types.bpy_prop_collection_idprop[RepresentationItemObject] representation_from_object: Union[bpy.types.Object, None] diff --git a/src/bonsai/bonsai/bim/module/geometry/ui.py b/src/bonsai/bonsai/bim/module/geometry/ui.py index 01d0a8ae0e..b0ad2ea47d 100644 --- a/src/bonsai/bonsai/bim/module/geometry/ui.py +++ b/src/bonsai/bonsai/bim/module/geometry/ui.py @@ -53,9 +53,10 @@ def mode_menu(self, context): UIData.load() ifc_icon = f"{UIData.data['menu_icon_color_mode']}_ifc" row = self.layout.row(align=True) - if context.scene.BIMGeometryProperties.mode == "EDIT": + props = tool.Geometry.get_geometry_props() + if props.mode == "EDIT": row.operator("bim.override_mode_set_object", icon="CANCEL", text="Discard Changes").should_save = False - row.prop(context.scene.BIMGeometryProperties, "mode", text="", icon_value=bonsai.bim.icons[ifc_icon].icon_id) + row.prop(props, "mode", text="", icon_value=bonsai.bim.icons[ifc_icon].icon_id) def object_menu(self, context): @@ -411,15 +412,18 @@ class BIM_PT_mesh(Panel): @classmethod def poll(cls, context): return ( - context.active_object is not None - and context.active_object.type == "MESH" - and hasattr(context.active_object.data, "BIMMeshProperties") - and context.active_object.data.BIMMeshProperties.ifc_definition_id + (obj := context.active_object) is not None + and (mesh := obj.data) + and isinstance(mesh, bpy.types.Mesh) + and tool.Geometry.get_mesh_props(mesh).ifc_definition_id ) def draw(self, context): - if not context.active_object.data: - return + obj = context.active_object + assert obj + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + row = self.layout.row() row.label(text="Advanced Users Only", icon="ERROR") @@ -427,7 +431,7 @@ class BIM_PT_mesh(Panel): row = layout.row() text = "Manually Save Representation" - if tool.Ifc.is_edited(context.active_object): + if tool.Ifc.is_edited(obj): text += "*" row.operator("bim.update_representation", text=text) @@ -454,8 +458,8 @@ class BIM_PT_mesh(Panel): op = row.operator("bim.update_representation", text="Convert To Arbitrary Extrusion With Voids") op.ifc_representation_class = "IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" - if context.active_object and context.active_object.data: - mprops = context.active_object.data.BIMMeshProperties + if True: + mprops = tool.Geometry.get_mesh_props(mesh) row = layout.row() row.operator("bim.get_representation_ifc_parameters") for index, ifc_parameter in enumerate(mprops.ifc_parameters): @@ -477,7 +481,7 @@ class BIM_PT_placement(Panel): @classmethod def poll(cls, context): - return context.active_object and context.active_object.BIMObjectProperties.ifc_definition_id + return (obj := context.active_object) and obj.BIMObjectProperties.ifc_definition_id def draw(self, context): if not PlacementData.is_loaded: @@ -571,10 +575,10 @@ class BIM_PT_workarounds(Panel): @classmethod def poll(cls, context): return ( - context.active_object is not None - and context.active_object.type == "MESH" - and hasattr(context.active_object.data, "BIMMeshProperties") - and context.active_object.data.BIMMeshProperties.ifc_definition_id + (obj := context.active_object) is not None + and (mesh := obj.data) + and isinstance(mesh, bpy.types.Mesh) + and tool.Geometry.get_mesh_props(mesh).ifc_definition_id ) def draw(self, context): @@ -588,7 +592,7 @@ class BIM_PT_workarounds(Panel): class BIM_UL_representation_items(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item(self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_propname): if item: icon = "MATERIAL" if item.surface_style else "MESH_UVSPHERE" row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/georeference/data.py b/src/bonsai/bonsai/bim/module/georeference/data.py index 314de226ff..b64ef55c39 100644 --- a/src/bonsai/bonsai/bim/module/georeference/data.py +++ b/src/bonsai/bonsai/bim/module/georeference/data.py @@ -167,7 +167,7 @@ class GeoreferenceData: result["rotation"] = str(round(ifcopenshell.util.geolocation.yaxis2angle(*wcs[:, 1][:2]), 3)) result["x"], result["y"], result["z"] = wcs[:, 3][:3] - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: blender_xyz = ifcopenshell.util.geolocation.enh2xyz( result["x"], @@ -193,7 +193,7 @@ class GeoreferenceData: @classmethod def local_origin(cls): - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if not props.has_blender_offset: return unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) diff --git a/src/bonsai/bonsai/bim/module/georeference/decorator.py b/src/bonsai/bonsai/bim/module/georeference/decorator.py index bd5a9b97e8..afd9dfba27 100644 --- a/src/bonsai/bonsai/bim/module/georeference/decorator.py +++ b/src/bonsai/bonsai/bim/module/georeference/decorator.py @@ -21,6 +21,7 @@ import blf import gpu import bmesh import ifcopenshell +import ifcopenshell.util.geolocation import bonsai.tool as tool from math import radians from bpy.types import SpaceView3D @@ -53,7 +54,8 @@ class GeoreferenceDecorator: cls.is_installed = False def draw_batch(self, shader_type, content_pos, color, indices=None): - self.scale = bpy.context.scene.BIMGeoreferenceProperties.visualization_scale + props = tool.Georeference.get_georeference_props() + self.scale = props.visualization_scale content_pos = [v * self.scale for v in content_pos] shader = self.line_shader if shader_type == "LINES" else self.shader batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) @@ -64,7 +66,7 @@ class GeoreferenceDecorator: if not GeoreferenceData.is_loaded: GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if not props.model_origin: # If this is empty, no georeferencing data has been loaded. return @@ -177,7 +179,7 @@ class GeoreferenceDecorator: if not GeoreferenceData.is_loaded: GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if not props.model_origin: # If this is empty, no georeferencing data has been loaded. return @@ -350,7 +352,7 @@ class GeoreferenceDecorator: self.gn_angle = float(GeoreferenceData.data["map_derived_angle"] or 0) self.tn_angle = float(GeoreferenceData.data["true_derived_angle"] or 0) - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: blender_angle = ifcopenshell.util.geolocation.xaxis2angle( float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate) diff --git a/src/bonsai/bonsai/bim/module/georeference/prop.py b/src/bonsai/bonsai/bim/module/georeference/prop.py index f0458b2045..58a3088b86 100644 --- a/src/bonsai/bonsai/bim/module/georeference/prop.py +++ b/src/bonsai/bonsai/bim/module/georeference/prop.py @@ -33,15 +33,18 @@ from bpy.props import ( ) from bonsai.bim.module.georeference.data import GeoreferenceData from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator +from typing import TYPE_CHECKING -def get_coordinate_operation_class(self, context): +def get_coordinate_operation_class( + self: "BIMGeoreferenceProperties", context: bpy.types.Context +) -> list[tuple[str, str, str]]: if not GeoreferenceData.is_loaded: GeoreferenceData.load() return GeoreferenceData.data["coordinate_operation_class"] -def update_true_north_angle(self, context): +def update_true_north_angle(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: if self.is_changing_angle: return self.is_changing_angle = True @@ -54,7 +57,7 @@ def update_true_north_angle(self, context): self.is_changing_angle = False -def update_true_north_vector(self, context): +def update_true_north_vector(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: if self.is_changing_angle: return self.is_changing_angle = True @@ -67,7 +70,7 @@ def update_true_north_vector(self, context): self.is_changing_angle = False -def update_grid_north_angle(self, context): +def update_grid_north_angle(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: if self.is_changing_angle: return self.is_changing_angle = True @@ -81,7 +84,7 @@ def update_grid_north_angle(self, context): self.is_changing_angle = False -def update_grid_north_vector(self, context): +def update_grid_north_vector(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: if self.is_changing_angle: return self.is_changing_angle = True @@ -95,15 +98,15 @@ def update_grid_north_vector(self, context): self.is_changing_angle = False -def update_should_visualise(self, context): +def update_should_visualise(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: if self.should_visualise: GeoreferenceDecorator.install(bpy.context) else: GeoreferenceDecorator.uninstall() -def update_blender_coordinates(self, context): - props = bpy.context.scene.BIMGeoreferenceProperties +def update_blender_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: + props = self if props.is_updating_coordinates: return props.is_updating_coordinates = True @@ -123,8 +126,8 @@ def update_blender_coordinates(self, context): props.is_updating_coordinates = False -def update_local_coordinates(self, context): - props = bpy.context.scene.BIMGeoreferenceProperties +def update_local_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: + props = self if props.is_updating_coordinates: return props.is_updating_coordinates = True @@ -147,8 +150,8 @@ def update_local_coordinates(self, context): props.is_updating_coordinates = False -def update_map_coordinates(self, context): - props = bpy.context.scene.BIMGeoreferenceProperties +def update_map_coordinates(self: "BIMGeoreferenceProperties", context: bpy.types.Context) -> None: + props = self if props.is_updating_coordinates: return props.is_updating_coordinates = True @@ -248,3 +251,45 @@ class BIMGeoreferenceProperties(PropertyGroup): wcs_y: StringProperty(name="WCS Y", default="0") wcs_z: StringProperty(name="WCS Z", default="0") wcs_rotation: StringProperty(name="WCS Rotation", default="0") + + if TYPE_CHECKING: + coordinate_operation_class: str + is_changing_angle: bool + is_editing: bool + is_editing_wcs: bool + is_editing_true_north: bool + coordinate_operation: bpy.types.bpy_prop_collection_idprop[Attribute] + projected_crs: bpy.types.bpy_prop_collection_idprop[Attribute] + is_updating_coordinates: bool + blender_coordinates: str + local_coordinates: str + map_coordinates: str + should_visualise: bool + visualization_scale: float + grid_north_angle: str + x_axis_abscissa: str + x_axis_ordinate: str + x_axis_is_null: bool + + host_model_origin: str + host_model_origin_si: str + host_model_project_north: str + + model_origin: str + model_origin_si: str + model_project_north: str + + has_blender_offset: bool + blender_offset_x: str + blender_offset_y: str + blender_offset_z: str + blender_x_axis_abscissa: str + blender_x_axis_ordinate: str + + true_north_angle: str + true_north_abscissa: str + true_north_ordinate: str + wcs_x: str + wcs_y: str + wcs_z: str + wcs_rotation: str diff --git a/src/bonsai/bonsai/bim/module/georeference/ui.py b/src/bonsai/bonsai/bim/module/georeference/ui.py index 8b2dfd43e0..3dfec3a9f5 100644 --- a/src/bonsai/bonsai/bim/module/georeference/ui.py +++ b/src/bonsai/bonsai/bim/module/georeference/ui.py @@ -32,7 +32,7 @@ class BIM_PT_gis(Panel): bl_parent_id = "BIM_PT_tab_geometry" def draw_header(self, context): - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() row = self.layout.row(align=True) icon = "HIDE_OFF" if props.should_visualise else "HIDE_ON" row.label(text="") # empty text occupies the left of the row @@ -43,7 +43,7 @@ class BIM_PT_gis(Panel): def draw(self, context): self.layout.use_property_split = True self.layout.use_property_decorate = False - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if not GeoreferenceData.is_loaded: GeoreferenceData.load() @@ -54,7 +54,7 @@ class BIM_PT_gis(Panel): self.draw_ui(context) def draw_editable_ui(self, context): - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() row = self.layout.row(align=True) row.label(text="Projected CRS", icon="WORLD") row.operator("bim.edit_georeferencing", icon="CHECKMARK", text="") @@ -87,7 +87,7 @@ class BIM_PT_gis(Panel): draw_attribute(attribute, self.layout.row()) def draw_ui(self, context): - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if tool.Ifc.get_schema() == "IFC2X3": row = self.layout.row() @@ -149,7 +149,7 @@ class BIM_PT_gis_true_north(Panel): if not GeoreferenceData.is_loaded: GeoreferenceData.load() - self.props = context.scene.BIMGeoreferenceProperties + self.props = tool.Georeference.get_georeference_props() if self.props.is_editing_true_north: self.draw_editable_ui(context) @@ -200,7 +200,7 @@ class BIM_PT_gis_blender(Panel): if not GeoreferenceData.is_loaded: GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: row = self.layout.row() @@ -233,7 +233,7 @@ class BIM_PT_gis_wcs(Panel): if not GeoreferenceData.is_loaded: GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.is_editing_wcs: self.draw_editable_ui(context) @@ -263,7 +263,7 @@ class BIM_PT_gis_wcs(Panel): row.operator("bim.enable_editing_wcs", icon="GREASEPENCIL", text="") def draw_editable_ui(self, context): - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() row = self.layout.row(align=True) row.label(text="World Coordinate System", icon="EMPTY_ARROWS") @@ -293,7 +293,7 @@ class BIM_PT_gis_calculator(Panel): if not GeoreferenceData.is_loaded: GeoreferenceData.load() - props = context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: row = self.layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/layer/operator.py b/src/bonsai/bonsai/bim/module/layer/operator.py index 9c2f4a2249..13b5d79a56 100644 --- a/src/bonsai/bonsai/bim/module/layer/operator.py +++ b/src/bonsai/bonsai/bim/module/layer/operator.py @@ -147,7 +147,7 @@ class AssignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator): "layer.assign_layer", self.file, **{ - "items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)], + "items": [self.file.by_id(tool.Geometry.get_mesh_props(item).ifc_definition_id)], "layer": self.file.by_id(self.layer), }, ) @@ -164,15 +164,10 @@ class UnassignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): item = bpy.data.meshes.get(self.item) if self.item else context.active_object.data - self.file = IfcStore.get_file() - ifcopenshell.api.run( - "layer.unassign_layer", - self.file, - **{ - "items": [self.file.by_id(item.BIMMeshProperties.ifc_definition_id)], - "layer": self.file.by_id(self.layer), - }, - ) + ifc_file = tool.Ifc.get() + representation = tool.Geometry.get_data_representation(item) + assert representation + ifcopenshell.api.layer.unassign_layer(ifc_file, items=[representation], layer=ifc_file.by_id(self.layer)) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/layer/ui.py b/src/bonsai/bonsai/bim/module/layer/ui.py index 8c3b77d30a..5fb8db827d 100644 --- a/src/bonsai/bonsai/bim/module/layer/ui.py +++ b/src/bonsai/bonsai/bim/module/layer/ui.py @@ -77,7 +77,6 @@ class BIM_UL_layers(UIList): row.label(text=item.name) if context.active_object and isinstance(context.active_object.data, Mesh): - mprops = context.active_object.data.BIMMeshProperties if item.ifc_definition_id in LayersData.data["active_layers"]: op = row.operator("bim.unassign_presentation_layer", text="", icon="KEYFRAME_HLT", emboss=False) op.layer = item.ifc_definition_id diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index 50a03603df..a332a89804 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -282,8 +282,9 @@ class ObjectMaterialData: if item.is_a("IfcMaterialLayer"): total_thickness = item.LayerThickness unit_system = bpy.context.scene.unit_settings.system + props = tool.Drawing.get_document_props() if unit_system == "IMPERIAL": - precision = bpy.context.scene.DocProperties.imperial_precision + precision = props.imperial_precision else: precision = None formatted_thickness = format_distance( diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index 19865be4e8..9ddd5ccd9d 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -351,9 +351,10 @@ class BIM_PT_object_material(Panel): if ObjectMaterialData.data["total_thickness"]: total_thickness = ObjectMaterialData.data["total_thickness"] unit_system = bpy.context.scene.unit_settings.system + props = tool.Drawing.get_document_props() if unit_system == "IMPERIAL": - precision = bpy.context.scene.DocProperties.imperial_precision + precision = props.imperial_precision else: precision = None formatted_thickness = format_distance( diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index 198408eeb5..20bd454d35 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -299,7 +299,7 @@ class DrawSystemArrows(bpy.types.Operator, tool.Ifc.Operator): tool.Blender.select_and_activate_single_object(context, curve) def get_absolute_matrix(self, matrix): - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: matrix = np.array( ifcopenshell.util.geolocation.global2local( diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index 0e73e71a91..d9da756705 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -236,9 +236,9 @@ class FilledOpeningGenerator: voided_elements = ifcopenshell.util.element.get_parts(voided_element) or [voided_element] for voided_element in voided_elements: voided_obj = tool.Ifc.get_object(voided_element) - if not voided_obj.data: + representation = tool.Geometry.get_active_representation(voided_obj) + if not representation: continue - representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) bonsai.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -465,10 +465,13 @@ class AddBoolean(Operator, tool.Ifc.Operator): self.report({"INFO"}, "At least two representation items must be selected to add a boolean.") return {"CANCELLED"} - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() - first_item = tool.Ifc.get().by_id(first_obj.data.BIMMeshProperties.ifc_definition_id) - second_items = [tool.Ifc.get().by_id(o.data.BIMMeshProperties.ifc_definition_id) for o in second_objs] + first_item = tool.Geometry.get_active_representation(first_obj) + assert first_item + second_items = [ + representation for o in second_objs if (representation := tool.Geometry.get_active_representation(o)) + ] booleans = ifcopenshell.api.geometry.add_boolean(tool.Ifc.get(), first_item, second_items, props.operator) rep_obj = tool.Geometry.get_geometry_props().representation_obj @@ -664,6 +667,7 @@ class EditOpenings(Operator, tool.Ifc.Operator): def edit_openings( self, building_objs: set[bpy.types.Object], opening_elements: set[ifcopenshell.entity_instance] ) -> None: + props = tool.Geometry.get_geometry_props() objects_to_remove: set[bpy.types.Object] = set() for opening_element in opening_elements: opening_obj = tool.Ifc.get_object(opening_element) @@ -690,8 +694,8 @@ class EditOpenings(Operator, tool.Ifc.Operator): self.get_all_building_objects_of_similar_openings(opening_element) ) # NB this has nothing to do with clone similar_opening tool.Ifc.unlink(element=opening_element) - if bpy.context.scene.BIMGeometryProperties.representation_obj == opening_obj: - bpy.context.scene.BIMGeometryProperties.representation_obj = None + if props.representation_obj == opening_obj: + props.representation_obj = None objects_to_remove.add(opening_obj) tool.Blender.remove_data_blocks(objects_to_remove, remove_unused_data=True) @@ -817,11 +821,11 @@ class RemoveBoolean(Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() return props.active_boolean def _execute(self, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() ifcopenshell.api.geometry.remove_boolean( tool.Ifc.get(), tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id) ) @@ -841,7 +845,7 @@ class SelectBoolean(Operator): @classmethod def poll(cls, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() return props.active_boolean def invoke(self, context, event): @@ -850,9 +854,9 @@ class SelectBoolean(Operator): return self.execute(context) def execute(self, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() queue = [tool.Ifc.get().by_id(props.active_boolean.ifc_definition_id)] - items = {i.ifc_definition_id: i.obj for i in context.scene.BIMGeometryProperties.item_objs} + items = {i.ifc_definition_id: i.obj for i in tool.Geometry.get_geometry_props().item_objs} while queue: item = queue.pop() if item.is_a("IfcBooleanResult"): @@ -911,11 +915,12 @@ class DecorationsHandler: gpu.state.point_size_set(6) gpu.state.blend_set("ALPHA") + gprops = tool.Geometry.get_geometry_props() for opening in props.openings: obj = opening.obj - if context.scene.BIMGeometryProperties.representation_obj == obj: + if gprops.representation_obj == obj: # We are editing the representation of the opening : - for item in context.scene.BIMGeometryProperties.item_objs: + for item in gprops.item_objs: if item.obj.mode == "EDIT": obj = item.obj break diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 6286422f99..d96e7d0b51 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -22,6 +22,7 @@ import bmesh import mathutils.geometry import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.geometry import ifcopenshell.util.type import ifcopenshell.util.unit import ifcopenshell.util.element @@ -265,7 +266,7 @@ class DumbProfileRegenerator: def _regenerate_from_type(self, related_object: ifcopenshell.entity_instance) -> None: obj = tool.Ifc.get_object(related_object) - if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: + if not obj or not tool.Geometry.get_active_representation(obj): return DumbProfileRecalculator().recalculate([obj]) @@ -501,7 +502,9 @@ class DumbProfileJoiner: "geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_axis ) - def get_placement_axes(body_representation): + def get_placement_axes( + body_representation: Union[ifcopenshell.entity_instance, None], + ) -> Union[tuple[tuple[float, float, float], tuple[float, float, float]], tuple[None, None]]: if not body_representation: return None, None extrusion = tool.Model.get_extrusion(body_representation) @@ -513,8 +516,7 @@ class DumbProfileJoiner: return ((0.0, 0.0, 1.0), (1.0, 0.0, 0.0)) old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") - new_body = ifcopenshell.api.run( - "geometry.add_profile_representation", + new_body = ifcopenshell.api.geometry.add_profile_representation( tool.Ifc.get(), context=self.body_context, profile=self.profile, @@ -527,8 +529,9 @@ class DumbProfileJoiner: if old_body: for inverse in tool.Ifc.get().get_inverse(old_body): ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body) - obj.data.BIMMeshProperties.ifc_definition_id = int(new_body.id()) - obj.data.name = f"{self.body_context.id()}/{new_body.id()}" + assert isinstance(mesh := obj.data, bpy.types.Mesh) + tool.Ifc.link(new_body, mesh) + mesh.name = tool.Loader.get_mesh_name(new_body) bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body) else: ifcopenshell.api.run( diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index de3078819d..328eb34090 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -259,7 +259,7 @@ class DumbSlabPlaner: self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str], new_thickness: float ) -> None: obj = tool.Ifc.get_object(related_object) - if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: + if not obj or not tool.Geometry.get_active_representation(obj): return material = ifcopenshell.util.element.get_material(related_object) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 5a94be7528..d6e6ec7a31 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -908,7 +908,7 @@ class DumbWallPlaner: self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str] ) -> None: obj = tool.Ifc.get_object(related_object) - if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id: + if not obj or not tool.Geometry.get_active_representation(obj): return material = ifcopenshell.util.element.get_material(related_object) @@ -1331,7 +1331,9 @@ class DumbWallJoiner: axis = body = tool.Model.get_wall_axis(obj)["reference"] self.axis = copy.deepcopy(axis) self.body = copy.deepcopy(body) - extrusion_data = self.get_extrusion_data(tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)) + representation = tool.Geometry.get_active_representation(obj) + assert representation + extrusion_data = self.get_extrusion_data(representation) height = extrusion_data["height"] x_angle = extrusion_data["x_angle"] self.clippings = [] @@ -1410,8 +1412,9 @@ class DumbWallJoiner: if old_body: for inverse in tool.Ifc.get().get_inverse(old_body): ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body) - obj.data.BIMMeshProperties.ifc_definition_id = int(new_body.id()) - obj.data.name = f"{self.body_context.id()}/{new_body.id()}" + assert isinstance(mesh := obj.data, bpy.types.Mesh) + tool.Ifc.link(new_body, mesh) + mesh.name = tool.Loader.get_mesh_name(new_body) bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body) else: ifcopenshell.api.run( @@ -1708,10 +1711,11 @@ class DumbWallJoiner: return True - def clip(self, wall1, slab2): + def clip(self, wall1: bpy.types.Object, slab2: bpy.types.Object) -> float: """returns height of the clipped wall, adds clipping plane to `clippings`""" element1 = tool.Ifc.get_entity(wall1) element2 = tool.Ifc.get_entity(slab2) + assert element1 and element2 layers1 = tool.Model.get_material_layer_parameters(element1) axis1 = tool.Model.get_wall_axis(wall1, layers1) @@ -1719,7 +1723,9 @@ class DumbWallJoiner: bases = [axis1["base"][0].to_3d(), axis1["base"][1].to_3d(), axis1["side"][0].to_3d(), axis1["side"][1].to_3d()] bases = [Vector((v[0], v[1], wall1.matrix_world.translation.z)) for v in bases] # add wall Z location - extrusion = self.get_extrusion_data(tool.Ifc.get().by_id(wall1.data.BIMMeshProperties.ifc_definition_id)) + representation = tool.Geometry.get_active_representation(wall1) + assert representation + extrusion = self.get_extrusion_data(representation) wall_dir = wall1.matrix_world.to_quaternion() @ extrusion["direction"] slab_pt = slab2.matrix_world @ Vector((0, 0, 0)) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index ad4446b9b6..11f4fe73c5 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -96,7 +96,8 @@ class BimTool(WorkSpaceTool): def draw_settings( cls, context: bpy.types.Context, layout: bpy.types.UILayout, ws_tool: bpy.types.WorkSpaceTool ) -> None: - if context.scene.BIMGeometryProperties.mode == "ITEM": + props = tool.Geometry.get_geometry_props() + if props.mode == "ITEM": EditItemUI.draw(context, layout) elif ( active_ifc_object := (context.active_object and tool.Ifc.get_entity(context.active_object)) @@ -430,7 +431,7 @@ class EditItemUI: obj = context.active_object assert obj - mesh_props = obj.data.BIMMeshProperties + mesh_props = tool.Geometry.get_mesh_props(obj.data) if AuthoringData.data["is_representation_item_swept_solid"]: # TODO: support EndSweptArea for IfcRevolvedAreaSolidTapered, # will need to add second attribute for this. @@ -440,10 +441,10 @@ class EditItemUI: op = row.operator("bim.name_profile", text="", icon="TAG") op.extrusion_item_obj = obj.name - for item_attribute in obj.data.BIMMeshProperties.item_attributes: + for item_attribute in mesh_props.item_attributes: row = cls.layout.row() draw_attribute(item_attribute, cls.layout) - if len(obj.data.BIMMeshProperties.item_attributes) or AuthoringData.data["is_representation_item_swept_solid"]: + if len(mesh_props.item_attributes) or AuthoringData.data["is_representation_item_swept_solid"]: row = cls.layout.row() row.operator("bim.update_item_attributes", icon="FILE_REFRESH", text="") @@ -1136,7 +1137,8 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): row.prop(self, "z") def hotkey_S_A(self): - if bpy.context.scene.BIMGeometryProperties.mode == "ITEM": + gprops = tool.Geometry.get_geometry_props() + if gprops.mode == "ITEM": bpy.ops.wm.call_menu(name="BIM_MT_add_representation_item") return diff --git a/src/bonsai/bonsai/bim/module/profile/data.py b/src/bonsai/bonsai/bim/module/profile/data.py index 88bd85197c..38a32d34f7 100644 --- a/src/bonsai/bonsai/bim/module/profile/data.py +++ b/src/bonsai/bonsai/bim/module/profile/data.py @@ -94,7 +94,7 @@ class ProfileData: obj = bpy.context.active_object return ( obj - and obj.data - and hasattr(obj.data, "BIMMeshProperties") - and obj.data.BIMMeshProperties.subshape_type == "PROFILE" + and (data := obj.data) + and isinstance(data, bpy.types.Mesh) + and tool.Geometry.get_mesh_props(data).subshape_type == "PROFILE" ) diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index 1a330917db..eab0db8ef2 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -248,7 +248,12 @@ class EnableEditingArbitraryProfile(bpy.types.Operator): def disable_editing_arbitrary_profile(context): obj = context.active_object - if obj and obj.type == "MESH" and obj.data and obj.data.BIMMeshProperties.subshape_type == "PROFILE": + if ( + obj + and (mesh := obj.data) + and isinstance(mesh, bpy.types.Mesh) + and tool.Geometry.get_mesh_props(mesh).subshape_type == "PROFILE" + ): ProfileDecorator.uninstall() bpy.ops.object.mode_set(mode="OBJECT") profile_mesh = obj.data diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py index cd00264224..1f738dbc90 100644 --- a/src/bonsai/bonsai/bim/module/project/decorator.py +++ b/src/bonsai/bonsai/bim/module/project/decorator.py @@ -31,7 +31,8 @@ from typing import Union @persistent def toggle_decorations_on_load(*args): - if bpy.context.scene.BIMProjectProperties.clipping_planes: + props = tool.Project.get_project_props() + if props.clipping_planes: ClippingPlaneDecorator.install(bpy.context) else: ClippingPlaneDecorator.uninstall() @@ -99,7 +100,7 @@ class ProjectDecorator: selected_edges = [] selected_tris = [] - props = context.scene.BIMProjectProperties + props = tool.Project.get_project_props() try: obj = props.queried_obj selected_vertices = obj["selected_vertices"] @@ -171,7 +172,8 @@ class ClippingPlaneDecorator: unselected_edges = [] unselected_tris = [] - for clipping_plane in context.scene.BIMProjectProperties.clipping_planes: + props = tool.Project.get_project_props() + for clipping_plane in props.clipping_planes: obj = clipping_plane.obj if not obj or not obj.data: continue diff --git a/src/bonsai/bonsai/bim/module/project/gizmo.py b/src/bonsai/bonsai/bim/module/project/gizmo.py index e5da7e3d52..bb372b9cb7 100644 --- a/src/bonsai/bonsai/bim/module/project/gizmo.py +++ b/src/bonsai/bonsai/bim/module/project/gizmo.py @@ -18,6 +18,7 @@ import bpy +import bonsai.tool as tool from bpy.types import GizmoGroup from mathutils import Matrix @@ -32,11 +33,12 @@ class ClippingPlane(GizmoGroup): @classmethod def poll(cls, context): obj = context.object + props = tool.Project.get_project_props() return ( context.selected_objects and obj and obj.name.startswith("ClippingPlane") - and obj in [sp.obj for sp in context.scene.BIMProjectProperties.clipping_planes] + and obj in [sp.obj for sp in props.clipping_planes] ) def setup(self, context): diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 8510a7d125..0aea8d00a3 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -75,35 +75,36 @@ class NewProject(bpy.types.Operator): def execute(self, context): bpy.ops.wm.read_homefile() + pprops = tool.Project.get_project_props() if self.preset == "metric_m": - bpy.context.scene.BIMProjectProperties.export_schema = "IFC4" + pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "METERS" bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" - bpy.context.scene.BIMProjectProperties.template_file = "0" + pprops.template_file = "0" elif self.preset == "metric_mm": - bpy.context.scene.BIMProjectProperties.export_schema = "IFC4" + pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" - bpy.context.scene.BIMProjectProperties.template_file = "0" + pprops.template_file = "0" elif self.preset == "imperial_ft": - bpy.context.scene.BIMProjectProperties.export_schema = "IFC4" + pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "IMPERIAL" bpy.context.scene.unit_settings.length_unit = "FEET" bpy.context.scene.BIMProperties.area_unit = "square foot" bpy.context.scene.BIMProperties.volume_unit = "cubic foot" - bpy.context.scene.BIMProjectProperties.template_file = "0" + pprops.template_file = "0" elif self.preset == "demo": - bpy.context.scene.BIMProjectProperties.export_schema = "IFC4" + pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" - bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Template.ifc" + pprops.template_file = "IFC4 Demo Template.ifc" if self.preset != "wizard": bpy.ops.bim.create_project() @@ -126,7 +127,7 @@ class CreateProject(bpy.types.Operator): return {"FINISHED"} def _execute(self, context): - props = context.scene.BIMProjectProperties + props = tool.Project.get_project_props() template = None if props.template_file == "0" else props.template_file if tool.Blender.is_default_scene(): for obj in bpy.data.objects: @@ -592,7 +593,8 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): self.import_presentation_style_from_ifc(element, context) try: - context.scene.BIMProjectProperties.library_elements[self.prop_index].is_appended = True + props = tool.Project.get_project_props() + props.library_elements[self.prop_index].is_appended = True except: # TODO Remove this terrible code when I refactor this into the core pass @@ -771,8 +773,8 @@ class EnableEditingHeader(bpy.types.Operator): return IfcStore.get_file() def execute(self, context): - self.file = IfcStore.get_file() - props = context.scene.BIMProjectProperties + self.file = tool.Ifc.get() + props = tool.Project.get_project_props() props.is_editing = True mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description) @@ -818,8 +820,8 @@ class EditHeader(bpy.types.Operator): return result def _execute(self, context): - self.file = IfcStore.get_file() - props = context.scene.BIMProjectProperties + self.file = tool.Ifc.get() + props = tool.Project.get_project_props() props.is_editing = True self.file.wrapped_data.header.file_description.description = (f"ViewDefinition[{props.mvd}]",) @@ -860,7 +862,8 @@ class DisableEditingHeader(bpy.types.Operator): bl_description = "Cancel unsaved header information" def execute(self, context): - context.scene.BIMProjectProperties.is_editing = False + props = tool.Project.get_project_props() + props.is_editing = False return {"FINISHED"} @@ -983,9 +986,10 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): f"Error loading IFC file from filepath '{filepath}'. See logs above in the system console for the details.", ) return {"CANCELLED"} - context.scene.BIMProjectProperties.is_loading = True - context.scene.BIMProjectProperties.total_elements = len(tool.Ifc.get().by_type("IfcElement")) - context.scene.BIMProjectProperties.use_relative_project_path = self.use_relative_path + props = tool.Project.get_project_props() + props.is_loading = True + props.total_elements = len(tool.Ifc.get().by_type("IfcElement")) + props.use_relative_project_path = self.use_relative_path tool.Blender.register_toolbar() tool.Project.add_recent_ifc_project(self.get_filepath_abs()) @@ -1051,8 +1055,8 @@ class LoadProjectElements(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - self.props = context.scene.BIMProjectProperties - self.file = IfcStore.get_file() + self.props = tool.Project.get_project_props() + self.file = tool.Ifc.get() bonsai.bim.schema.reload(self.file.schema_identifier) start = time.time() logger = logging.getLogger("ImportIFC") @@ -1084,7 +1088,8 @@ class LoadProjectElements(bpy.types.Operator): ifc_importer.execute() settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start)) print("Import finished in {:.2f} seconds".format(time.time() - start)) - context.scene.BIMProjectProperties.is_loading = False + props = tool.Project.get_project_props() + props.is_loading = False tool.Project.load_pset_templates() tool.Project.load_default_thumbnails() @@ -1156,7 +1161,8 @@ class ToggleFilterCategories(bpy.types.Operator): should_select: bpy.props.BoolProperty(name="Should Select", default=True) def execute(self, context): - for filter_category in context.scene.BIMProjectProperties.filter_categories: + props = tool.Project.get_project_props() + for filter_category in props.filter_categories: filter_category.is_selected = self.should_select return {"FINISHED"} @@ -1183,7 +1189,7 @@ class LinkIfc(bpy.types.Operator): directory: str def draw(self, context): - pprops = context.scene.BIMProjectProperties + pprops = tool.Project.get_project_props() row = self.layout.row() row.prop(self, "use_relative_path") row = self.layout.row() @@ -1204,7 +1210,8 @@ class LinkIfc(bpy.types.Operator): if bpy.data.filepath and filepath.samefile(bpy.data.filepath): self.report({"INFO"}, "Can't link the current .blend file") continue - new = context.scene.BIMProjectProperties.links.add() + props = tool.Project.get_project_props() + new = props.links.add() filepath = tool.Ifc.get_uri(filepath, use_relative_path=self.use_relative_path) new.name = filepath status = bpy.ops.bim.load_link(filepath=filepath, use_cache=self.use_cache) @@ -1235,9 +1242,10 @@ class UnlinkIfc(bpy.types.Operator): def execute(self, context): filepath = Path(self.filepath).as_posix() bpy.ops.bim.unload_link(filepath=filepath) - index = context.scene.BIMProjectProperties.links.find(filepath) + props = tool.Project.get_project_props() + index = props.links.find(filepath) if index != -1: - context.scene.BIMProjectProperties.links.remove(index) + props.links.remove(index) return {"FINISHED"} @@ -1257,8 +1265,9 @@ class UnloadLink(bpy.types.Operator): if tool.Blender.ensure_blender_path_is_abs(Path(library.filepath)) == filepath: bpy.data.libraries.remove(library) - links = context.scene.BIMProjectProperties.links - link = links.get(self.filepath) + props = tool.Project.get_project_props() + links = props.links + link = links[self.filepath] # Let's assume that user might delete it. if empty_handle := link.empty_handle: bpy.data.objects.remove(empty_handle) @@ -1267,7 +1276,7 @@ class UnloadLink(bpy.types.Operator): if not any([l.is_loaded for l in links]): ProjectDecorator.uninstall() # we make sure we don't draw queried object from the file that was just unlinked - elif queried_obj := context.scene.BIMProjectProperties.queried_obj: + elif queried_obj := props.queried_obj: queried_filepath = Path(queried_obj["ifc_filepath"]) if queried_filepath == filepath: ProjectDecorator.uninstall() @@ -1299,7 +1308,7 @@ class LoadLink(bpy.types.Operator): def link_blend(self, filepath: Path) -> None: with bpy.data.libraries.load(str(filepath), link=True) as (data_from, data_to): data_to.scenes = data_from.scenes - link = bpy.context.scene.BIMProjectProperties.links[self.filepath] + link = tool.Project.get_project_props().links[self.filepath] for scene in bpy.data.scenes: if not scene.library or Path(scene.library.filepath) != filepath: continue @@ -1325,13 +1334,13 @@ class LoadLink(bpy.types.Operator): if not blend_filepath.exists(): pprops = tool.Project.get_project_props() - gprops = bpy.context.scene.BIMGeoreferenceProperties + gprops = tool.Georeference.get_georeference_props() code = f""" import bpy def run(): - gprops = bpy.context.scene.BIMGeoreferenceProperties + gprops = tool.Georeference.get_georeference_props() # Our model origin becomes their host model origin gprops.host_model_origin = "{gprops.model_origin}" gprops.host_model_origin_si = "{gprops.model_origin_si}" @@ -1342,7 +1351,7 @@ def run(): gprops.blender_offset_z = "{gprops.blender_offset_z}" gprops.blender_x_axis_abscissa = "{gprops.blender_x_axis_abscissa}" gprops.blender_x_axis_ordinate = "{gprops.blender_x_axis_ordinate}" - pprops = bpy.context.scene.BIMProjectProperties + pprops = tool.Project.get_project_props() pprops.distance_limit = {pprops.distance_limit} pprops.false_origin_mode = "{pprops.false_origin_mode}" pprops.false_origin = "{pprops.false_origin}" @@ -1397,7 +1406,7 @@ except Exception as e: with open(json_filepath, "r") as f: data = json.load(f) - gprops = bpy.context.scene.BIMGeoreferenceProperties + gprops = tool.Georeference.get_georeference_props() for prop in ("model_origin", "model_origin_si", "model_project_north"): if (value := data.get(prop, None)) is not None: setattr(gprops, prop, value) @@ -1433,8 +1442,8 @@ class ToggleLinkSelectability(bpy.types.Operator): link: bpy.props.StringProperty(name="Linked IFC Filepath") def execute(self, context): - props = context.scene.BIMProjectProperties - link = props.links.get(self.link) + props = tool.Project.get_project_props() + link = props.links[self.link] self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend")) link.is_selectable = (is_selectable := not link.is_selectable) for collection in self.get_linked_collections(): @@ -1460,8 +1469,8 @@ class ToggleLinkVisibility(bpy.types.Operator): mode: bpy.props.EnumProperty(name="Visibility Mode", items=((i, i, "") for i in ("WIREFRAME", "VISIBLE"))) def execute(self, context): - props = context.scene.BIMProjectProperties - link = props.links.get(self.link) + props = tool.Project.get_project_props() + link = props.links[self.link] self.library_filepath = tool.Blender.ensure_blender_path_is_abs(Path(self.link).with_suffix(".ifc.cache.blend")) if self.mode == "WIREFRAME": self.toggle_wireframe(link) @@ -1507,7 +1516,7 @@ class SelectLinkHandle(bpy.types.Operator): index: bpy.props.IntProperty(name="Link Index") def execute(self, context): - props = context.scene.BIMProjectProperties + props = tool.Project.get_project_props() link = props.links[self.index] handle = link.empty_handle if not handle: @@ -1549,7 +1558,7 @@ class ExportIFC(bpy.types.Operator): bpy.ops.wm.save_mainfile("INVOKE_DEFAULT") return {"FINISHED"} - self.use_relative_path = context.scene.BIMProjectProperties.use_relative_project_path + self.use_relative_path = tool.Project.get_project_props().use_relative_project_path if (filepath := context.scene.BIMProperties.ifc_file) and not self.should_save_as: self.filepath = str(tool.Blender.ensure_blender_path_is_abs(Path(filepath))) return self.execute(context) @@ -1564,7 +1573,7 @@ class ExportIFC(bpy.types.Operator): return {"RUNNING_MODAL"} def execute(self, context): - project_props = context.scene.BIMProjectProperties + project_props = tool.Project.get_project_props() project_props.use_relative_project_path = self.use_relative_path if project_props.should_disable_undo_on_save: old_history_size = tool.Ifc.get().history_size @@ -1612,10 +1621,12 @@ class ExportIFC(bpy.types.Operator): # New project created in Bonsai should be in recent projects too. tool.Project.add_recent_ifc_project(Path(output_file)) scene = context.scene - if not scene.DocProperties.ifc_files: - new = scene.DocProperties.ifc_files.add() + props = tool.Drawing.get_document_props() + if not props.ifc_files: + new = props.ifc_files.add() new.name = output_file - if context.scene.BIMProjectProperties.use_relative_project_path and bpy.data.is_saved: + props = tool.Project.get_project_props() + if props.use_relative_project_path and bpy.data.is_saved: output_file = os.path.relpath(output_file, bpy.path.abspath("//")) if scene.BIMProperties.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): scene.BIMProperties.ifc_file = output_file @@ -1660,8 +1671,8 @@ class LoadLinkedProject(bpy.types.Operator): start = time.time() - pprops = bpy.context.scene.BIMProjectProperties - gprops = bpy.context.scene.BIMGeoreferenceProperties + pprops = tool.Project.get_project_props() + gprops = tool.Georeference.get_georeference_props() self.filepath = Path(self.filepath).as_posix() print("Processing", self.filepath) @@ -1875,7 +1886,7 @@ class LoadLinkedProject(bpy.types.Operator): mesh = bpy.data.meshes.new("Mesh") geometry = shape.geometry - gprops = bpy.context.scene.BIMGeoreferenceProperties + gprops = tool.Georeference.get_georeference_props() if ( gprops.has_blender_offset and geometry.verts @@ -1981,9 +1992,8 @@ class QueryLinkedElement(bpy.types.Operator): from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d LinksData.linked_data = {} - props = context.scene.BIMProjectProperties + props = tool.Project.get_project_props() props.queried_obj = None - props.quried_obj_root = None for area in bpy.context.screen.areas: if area.type == "PROPERTIES": @@ -2119,6 +2129,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement): def _execute(self, context): from bonsai.bim.module.project.data import LinksData + props = tool.Project.get_project_props() if not LinksData.linked_data: self.report({"INFO"}, "No linked element found.") return {"CANCELLED"} @@ -2128,7 +2139,7 @@ class AppendInspectedLinkedElement(AppendLibraryElement): self.report({"INFO"}, "Cannot find Global Id for element.") return {"CANCELLED"} - queried_obj = context.scene.BIMProjectProperties.queried_obj + queried_obj = props.queried_obj ifc_file = tool.Ifc.get() linked_ifc_file: ifcopenshell.file @@ -2275,34 +2286,36 @@ class RefreshClippingPlanes(bpy.types.Operator): def modal(self, context, event): should_refresh = False + props = tool.Project.get_project_props() self.clean_deleted_planes(context) - for clipping_plane in context.scene.BIMProjectProperties.clipping_planes: + for clipping_plane in props.clipping_planes: if clipping_plane.obj and self.is_moved(clipping_plane.obj): should_refresh = True break - total_planes = len(context.scene.BIMProjectProperties.clipping_planes) + total_planes = len(props.clipping_planes) if should_refresh or total_planes != self.total_planes: self.refresh_clipping_planes(context) - for clipping_plane in context.scene.BIMProjectProperties.clipping_planes: + for clipping_plane in props.clipping_planes: if clipping_plane.obj: tool.Geometry.record_object_position(clipping_plane.obj) self.total_planes = total_planes return {"PASS_THROUGH"} - def clean_deleted_planes(self, context): + def clean_deleted_planes(self, context: bpy.types.Context) -> None: + props = tool.Project.get_project_props() while True: - for i, clipping_plane in enumerate(context.scene.BIMProjectProperties.clipping_planes): + for i, clipping_plane in enumerate(props.clipping_planes): if clipping_plane.obj: try: clipping_plane.obj.name except: - context.scene.BIMProjectProperties.clipping_planes.remove(i) + props.clipping_planes.remove(i) break else: - context.scene.BIMProjectProperties.clipping_planes.remove(i) + props.clipping_planes.remove(i) break else: break @@ -2326,14 +2339,15 @@ class RefreshClippingPlanes(bpy.types.Operator): region = next(r for r in area.regions if r.type == "WINDOW") data = region.data - if not len(context.scene.BIMProjectProperties.clipping_planes): + props = tool.Project.get_project_props() + if not len(props.clipping_planes): data.use_clip_planes = False else: with bpy.context.temp_override(area=area, region=region): bpy.ops.view3d.clip_border() clip_planes = [] - for clipping_plane in bpy.context.scene.BIMProjectProperties.clipping_planes: + for clipping_plane in tool.Project.get_project_props().clipping_planes: obj = clipping_plane.obj if not obj: continue @@ -2372,8 +2386,8 @@ class CreateClippingPlane(bpy.types.Operator): from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d # Clean up deleted planes - - if len(context.scene.BIMProjectProperties.clipping_planes) > 5: + props = tool.Project.get_project_props() + if len(props.clipping_planes) > 5: self.report({"INFO"}, "Maximum of six clipping planes allowed.") return {"FINISHED"} @@ -2413,7 +2427,7 @@ class CreateClippingPlane(bpy.types.Operator): context.scene.cursor.location = location - new = context.scene.BIMProjectProperties.clipping_planes.add() + new = tool.Project.get_project_props().clipping_planes.add() new.obj = plane_obj tool.Blender.set_active_object(plane_obj) @@ -2444,7 +2458,7 @@ class FlipClippingPlane(bpy.types.Operator): def execute(self, context): obj = context.active_object - if obj in context.scene.BIMProjectProperties.clipping_planes_objs: + if obj in tool.Project.get_project_props().clipping_planes_objs: obj.rotation_euler[0] += radians(180) context.view_layer.update() return {"FINISHED"} @@ -2462,14 +2476,15 @@ class BIM_OT_save_clipping_planes(bpy.types.Operator): @classmethod def poll(cls, context): if IfcStore.path: - return context.scene.BIMProjectProperties.clipping_planes + return tool.Project.get_project_props().clipping_planes cls.poll_message_set("Please Save The IFC File") def execute(self, context): clipping_planes_to_serialize = defaultdict(dict) - clipping_planes = context.scene.BIMProjectProperties.clipping_planes + clipping_planes = tool.Project.get_project_props().clipping_planes for clipping_plane in clipping_planes: obj = clipping_plane.obj + assert obj name = obj.name clipping_planes_to_serialize[name]["location"] = obj.location[0:3] clipping_planes_to_serialize[name]["rotation"] = obj.rotation_euler[0:3] @@ -2495,13 +2510,14 @@ class BIM_OT_load_clipping_planes(bpy.types.Operator): cls.poll_message_set("Please Save The IFC File") def execute(self, context): - bpy.data.batch_remove(context.scene.BIMProjectProperties.clipping_planes_objs) - context.scene.BIMProjectProperties.clipping_planes.clear() + props = tool.Project.get_project_props() + bpy.data.batch_remove(props.clipping_planes_objs) + props.clipping_planes.clear() with open(Path(IfcStore.path).with_name(CLIPPING_PLANES_FILE_NAME), "r") as file: clipping_planes_dict = json.load(file) for name, values in clipping_planes_dict.items(): bpy.ops.bim.create_clipping_plane() - obj = context.scene.BIMProjectProperties.clipping_planes_objs[-1] + obj = props.clipping_planes_objs[-1] obj.name = name obj.location = values["location"] obj.rotation_euler = values["rotation"] diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index e4ba0580a7..871b128db6 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -451,7 +451,8 @@ def get_side_area(o: bpy.types.Object) -> float: def get_cross_section_area(obj: bpy.types.Object) -> float: - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + representation = tool.Geometry.get_active_representation(obj) + assert representation item = representation.Items[0] while True: if item.is_a("IfcExtrudedAreaSolid"): diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 5083fb2445..4022c1d5ff 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -711,14 +711,14 @@ class SelectSimilar(Operator, tool.Ifc.Operator): return self.execute(context) def _execute(self, context): - props = context.scene.BIMSearchProperties obj = context.active_object element = tool.Ifc.get_entity(obj) key = self.key if key == "PredefinedType": key = "predefined_type" value = ifcopenshell.util.selector.get_element_value(element, key) - tolerance = bpy.context.scene.DocProperties.tolerance + dprops = tool.Drawing.get_document_props() + tolerance = dprops.tolerance # Determine the number of decimal places based on the magnitude of the rounding value if tolerance < 1: diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index ef7914c69e..5f2d6d2a52 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -155,10 +155,10 @@ class UnlinkStyle(bpy.types.Operator, tool.Ifc.Operator): # for unlinked blender material. updated_meshes = set() for obj in bpy.data.objects: - mesh = obj.data - if not isinstance(mesh, bpy.types.Mesh): + if not (mesh := obj.data) or not isinstance(mesh, bpy.types.Mesh): continue - if not mesh.BIMMeshProperties.ifc_definition_id: + representation = tool.Geometry.get_data_representation(mesh) + if not representation: continue if mesh in updated_meshes: continue @@ -1137,13 +1137,16 @@ class AssignStyleToSelected(bpy.types.Operator, tool.Ifc.Operator): ifc_file = tool.Ifc.get() style = ifc_file.by_id(self.style_id) material = tool.Ifc.get_object(style) + assert isinstance(material, bpy.types.Material) has_items = False representations: dict[ifcopenshell.entity_instance, bpy.types.Object] = {} for obj in context.selected_objects: if tool.Geometry.is_representation_item(obj): has_items = True - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + assert isinstance(obj.data, bpy.types.Mesh) + item = tool.Geometry.get_active_representation(obj) + assert item tool.Style.assign_style_to_representation_item(item, style) obj.data.materials.clear() obj.data.materials.append(material) @@ -1156,7 +1159,8 @@ class AssignStyleToSelected(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} if has_items: - tool.Geometry.reload_representation(context.scene.BIMGeometryProperties.representation_obj) + gprops = tool.Geometry.get_geometry_props() + tool.Geometry.reload_representation(gprops.representation_obj) bpy.ops.bim.disable_editing_representation_items() bpy.ops.bim.enable_editing_representation_items() diff --git a/src/bonsai/bonsai/bim/module/void/data.py b/src/bonsai/bonsai/bim/module/void/data.py index fe387b6f81..133d3e15f5 100644 --- a/src/bonsai/bonsai/bim/module/void/data.py +++ b/src/bonsai/bonsai/bim/module/void/data.py @@ -127,26 +127,14 @@ class BooleansData: def booleans(cls): props = tool.Geometry.get_geometry_props() obj = props.representation_obj or bpy.context.active_object - if ( - not obj.data - or not hasattr(obj.data, "BIMMeshProperties") - or not obj.data.BIMMeshProperties.ifc_definition_id - ): + if not (representation := tool.Geometry.get_active_representation(obj)): return [] - - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) return tool.Model.get_booleans(representation=representation) @classmethod def manual_booleans(cls): props = tool.Geometry.get_geometry_props() obj = props.representation_obj or bpy.context.active_object - if ( - not obj.data - or not hasattr(obj.data, "BIMMeshProperties") - or not obj.data.BIMMeshProperties.ifc_definition_id - ): + if not (representation := tool.Geometry.get_active_representation(obj)): return [] - - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) return tool.Model.get_manual_booleans(tool.Ifc.get_entity(obj), representation=representation) diff --git a/src/bonsai/bonsai/bim/module/void/operator.py b/src/bonsai/bonsai/bim/module/void/operator.py index 5867ed9950..a1493f2711 100644 --- a/src/bonsai/bonsai/bim/module/void/operator.py +++ b/src/bonsai/bonsai/bim/module/void/operator.py @@ -139,7 +139,8 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator): tool.Ifc, tool.Geometry, tool.Surveyor, obj=voided_obj ) - representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) + representation = tool.Geometry.get_active_representation(voided_obj) + assert representation bonsai.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -269,20 +270,18 @@ class BooleansMarkAsManual(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): obj = context.active_object - if ( - obj - and tool.Ifc.get_entity(obj) - and hasattr(obj.data, "BIMMeshProperties") - and obj.data.BIMMeshProperties.ifc_definition_id - ): + if obj and tool.Ifc.get_entity(obj) and tool.Geometry.get_active_representation(obj): return True cls.poll_message_set("Need to select IFC element with representation") return False def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + assert element + representation = tool.Geometry.get_active_representation(obj) + assert representation booleans = tool.Model.get_booleans(representation=representation) if self.mark_as_manual: @@ -304,13 +303,13 @@ class EnableEditingBooleans(bpy.types.Operator): @classmethod def poll(cls, context): - if not bpy.context.scene.BIMGeometryProperties.representation_obj: + if not tool.Geometry.get_geometry_props().representation_obj: cls.poll_message_set("To enable editing booleans object should be in item mode.") return False return True def execute(self, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() gprops = tool.Geometry.get_geometry_props() rep_obj = gprops.representation_obj assert rep_obj @@ -344,6 +343,6 @@ class DisableEditingBooleans(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() props.is_editing = False return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/void/prop.py b/src/bonsai/bonsai/bim/module/void/prop.py index 8c6bbaf4e6..0170dbd606 100644 --- a/src/bonsai/bonsai/bim/module/void/prop.py +++ b/src/bonsai/bonsai/bim/module/void/prop.py @@ -19,34 +19,51 @@ import bpy from bpy.types import PropertyGroup from bpy.props import PointerProperty, StringProperty, IntProperty, BoolProperty, CollectionProperty, EnumProperty -from typing import Union +from typing import Union, TYPE_CHECKING, Literal, get_args + +OperatorType = Literal["DIFFERENCE", "INTERSECTION", "UNION"] class Boolean(PropertyGroup): name: StringProperty(name="Name") - operator: StringProperty(name="Operator") + operator: EnumProperty( + items=[(i, i, "") for i in get_args(OperatorType)], + name="Operator", + default="DIFFERENCE", + ) ifc_definition_id: IntProperty(name="IFC Definition ID") level: IntProperty(name="Level") + if TYPE_CHECKING: + operator: OperatorType + name: str + ifc_definition_id: int + level: int + class VoidProperties(PropertyGroup): desired_opening: PointerProperty(name="Desired Opening To Fill", type=bpy.types.Object) + if TYPE_CHECKING: + desired_opening: Union[bpy.types.Object, None] + class BIMBooleanProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing", default=False) booleans: CollectionProperty(name="Booleans", type=Boolean) active_boolean_index: IntProperty(name="Active Boolean Index") operator: EnumProperty( - items=[ - ("DIFFERENCE", "DIFFERENCE", ""), - ("INTERSECTION", "INTERSECTION", ""), - ("UNION", "UNION", ""), - ], + items=[(i, i, "") for i in get_args(OperatorType)], name="Operator", default="DIFFERENCE", ) + if TYPE_CHECKING: + is_editing: bool + booleans: bpy.types.bpy_prop_collection_idprop[Boolean] + active_boolean_index: int + operator: OperatorType + @property def active_boolean(self) -> Union[Boolean, None]: if self.booleans and 0 <= self.active_boolean_index < len(self.booleans): diff --git a/src/bonsai/bonsai/bim/module/void/ui.py b/src/bonsai/bonsai/bim/module/void/ui.py index d07266a56f..69b34a40e1 100644 --- a/src/bonsai/bonsai/bim/module/void/ui.py +++ b/src/bonsai/bonsai/bim/module/void/ui.py @@ -126,13 +126,10 @@ class BIM_PT_booleans(Panel): @classmethod def poll(cls, context): return ( - context.active_object is not None - and context.active_object.type == "MESH" - and hasattr(context.active_object.data, "BIMMeshProperties") - and ( - context.active_object.data.BIMMeshProperties.ifc_definition_id - or context.active_object.data.BIMMeshProperties.ifc_boolean_id - ) + (obj := context.active_object) is not None + and isinstance(data := obj.data, bpy.types.Mesh) + and (mesh_props := tool.Geometry.get_mesh_props(data)) + and (mesh_props.ifc_definition_id or mesh_props.ifc_boolean_id) ) def draw(self, context): @@ -141,13 +138,13 @@ class BIM_PT_booleans(Panel): obj = context.active_object assert obj + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) - if not context.active_object.data: - return layout = self.layout - props = context.scene.BIMBooleanProperties + props = tool.Feature.get_boolean_props() - if context.active_object.data.BIMMeshProperties.ifc_definition_id: + if tool.Geometry.get_mesh_props(mesh).ifc_definition_id: row = layout.row(align=True) total_booleans = BooleansData.data["total_booleans"] manual_booleans = BooleansData.data["manual_booleans"] diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index be0d07102d..2bd6f445f4 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -828,7 +828,8 @@ class AddIfcFile(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.DocProperties.ifc_files.add() + props = tool.Drawing.get_document_props() + props.ifc_files.add() return {"FINISHED"} @@ -839,7 +840,8 @@ class RemoveIfcFile(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.DocProperties.ifc_files.remove(self.index) + props = tool.Drawing.get_document_props() + props.ifc_files.remove(self.index) return {"FINISHED"} @@ -1060,7 +1062,8 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - cutting_planes = [p.obj for p in context.scene.BIMProjectProperties.clipping_planes] + props = tool.Project.get_project_props() + cutting_planes = [obj for p in props.clipping_planes if (obj := p.obj)] if not cutting_planes: self.report({"INFO"}, "No cutting planes found.") return {"FINISHED"} @@ -1070,7 +1073,7 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator): objects_processed, t0 = 0, time.time() wm.progress_begin(0, len(context.selected_objects)) for obj_i, obj in enumerate(context.selected_objects): - if obj.type != "MESH": + if not isinstance((mesh := obj.data), bpy.types.Mesh): continue if obj in cutting_planes: @@ -1082,7 +1085,6 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator): ws_to_ls = obj.matrix_world.inverted() rotation = ws_to_ls.to_quaternion() - mesh = obj.data bm = tool.Blender.get_bmesh_for_mesh(mesh) object_changed = False @@ -1106,7 +1108,7 @@ class ClippingPlaneCutWithCappings(bpy.types.Operator): # don't swap mesh if it wasn't affected by any of the cutting planes if object_changed: temp_mesh = bpy.data.meshes.new("temp_cut") - temp_mesh.BIMMeshProperties.replaced_mesh = mesh + tool.Geometry.get_mesh_props(temp_mesh).replaced_mesh = mesh for material in mesh.materials: temp_mesh.materials.append(material) obj.data = temp_mesh @@ -1163,9 +1165,10 @@ class RevertClippingPlaneCut(bpy.types.Operator): self.report({"INFO"}, f"{objects_processed} processed - {time.time()-t0:.3f} sec") return {"FINISHED"} - def revert_object_mesh(self, obj): + def revert_object_mesh(self, obj: bpy.types.Object) -> None: mesh = obj.data - replaced_mesh = mesh.BIMMeshProperties.replaced_mesh + assert isinstance(mesh, bpy.types.Mesh) + replaced_mesh = tool.Geometry.get_mesh_props(mesh).replaced_mesh if replaced_mesh: obj.data = replaced_mesh tool.Blender.remove_data_block(mesh, do_unlink=False) diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 2f095cbb2c..c9b8c759d8 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -525,6 +525,13 @@ class IfcParameter(PropertyGroup): value: FloatProperty(name="Value") # For now, only floats type: StringProperty(name="Type") + if TYPE_CHECKING: + name: str + step_id: int + index: int + value: float + type: str + class PsetQto(PropertyGroup): name: StringProperty(name="Name") @@ -532,6 +539,11 @@ class PsetQto(PropertyGroup): is_expanded: BoolProperty(name="Is Expanded", default=True) is_editable: BoolProperty(name="Is Editable") + if TYPE_CHECKING: + properties: bpy.types.bpy_prop_collection_idprop[Attribute] + is_expanded: bool + is_editable: bool + class GlobalId(PropertyGroup): name: StringProperty(name="Name") @@ -540,6 +552,9 @@ class GlobalId(PropertyGroup): class BIMCollectionProperties(PropertyGroup): obj: PointerProperty(type=bpy.types.Object) + if TYPE_CHECKING: + obj: Union[bpy.types.Object, None] + class BIMObjectProperties(PropertyGroup): collection: PointerProperty(type=bpy.types.Collection) @@ -564,6 +579,9 @@ def get_profiles(self: "BIMMeshProperties", context: bpy.types.Context): return ItemData.data["profiles_enum"] +SubshapeType = Literal["-", "PROFILE", "AXIS"] + + class BIMMeshProperties(PropertyGroup): ifc_definition_id: IntProperty(name="IFC Definition ID") ifc_boolean_id: IntProperty(name="IFC Boolean ID") @@ -572,7 +590,7 @@ class BIMMeshProperties(PropertyGroup): is_native: BoolProperty(name="Is Native", default=False) is_swept_solid: BoolProperty(name="Is Swept Solid") is_parametric: BoolProperty(name="Is Parametric", default=False) - subshape_type: EnumProperty(name="Subshape Type", items=[(i, i, "") for i in ("-", "PROFILE", "AXIS")]) + subshape_type: EnumProperty(name="Subshape Type", items=[(i, i, "") for i in get_args(SubshapeType)]) ifc_parameters: CollectionProperty(name="IFC Parameters", type=IfcParameter) item_attributes: CollectionProperty(name="Item Attributes", type=Attribute) item_profile: EnumProperty(name="Item Profile", items=get_profiles) @@ -580,6 +598,22 @@ class BIMMeshProperties(PropertyGroup): mesh_checksum: StringProperty(name="Mesh Checksum", default="") replaced_mesh: PointerProperty(type=bpy.types.Mesh, description="Original mesh to revert section cutaway") + if TYPE_CHECKING: + ifc_definition_id: int + ifc_boolean_id: int + obj: Union[bpy.types.Object, None] + has_openings_applied: bool + is_native: bool + is_swept_solid: bool + is_parametric: bool + subshape_type: SubshapeType + ifc_parameters: bpy.types.bpy_prop_collection_idprop[IfcParameter] + item_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + item_profile: str + material_checksum: str + mesh_checksum: str + replaced_mesh: Union[bpy.types.Mesh, None] + class BIMFacet(PropertyGroup): name: StringProperty(name="Name") @@ -599,16 +633,30 @@ class BIMFacet(PropertyGroup): ], ) + if TYPE_CHECKING: + pset: str + value: str + type: str + comparison: Literal["=", "!=", ">=", "<=", ">", "<", "*=", "!*="] + class BIMFilterGroup(PropertyGroup): filters: CollectionProperty(type=BIMFacet, name="filters") + if TYPE_CHECKING: + filters: bpy.types.bpy_prop_collection_idprop[BIMFacet] + class BIMSnapGroups(PropertyGroup): object: BoolProperty(name="Object", default=True) polyline: BoolProperty(name="Polyline", default=True) measure: BoolProperty(name="Measure", default=True) + if TYPE_CHECKING: + object: bool + polyline: bool + measure: bool + class BIMSnapProperties(PropertyGroup): vertex: BoolProperty(name="Vertex", default=True) @@ -616,3 +664,10 @@ class BIMSnapProperties(PropertyGroup): edge_center: BoolProperty(name="Edge Center", default=True) edge_intersection: BoolProperty(name="Edge Intersection", default=True) face: BoolProperty(name="Face", default=True) + + if TYPE_CHECKING: + vertex: bool + edge: bool + edge_center: bool + edge_intersection: bool + face: bool diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index fe300719af..a77650a72b 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -19,6 +19,7 @@ import os import bpy import platform +import bonsai.bim.helper from pathlib import Path from bpy.types import Panel from bpy.props import StringProperty, IntProperty, BoolProperty @@ -34,7 +35,7 @@ import bonsai.bim import bonsai.tool as tool from ifcopenshell.util.file import IfcHeaderExtractor from bonsai.bim.prop import Attribute -from typing import Optional +from typing import Optional, TYPE_CHECKING class IFCFileSelector: @@ -147,7 +148,7 @@ class BIM_PT_section_with_cappings(Panel): row.operator("bim.clipping_plane_cut_with_cappings", icon="XRAY", text="Cut") row.operator("bim.revert_clipping_plane_cut", icon="FILE_REFRESH", text="Revert Cut") - props = context.scene.BIMProjectProperties + props = tool.Project.get_project_props() box = layout.box() header = box.row(align=True) header.label(text="Clipping Planes") @@ -247,7 +248,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): should_play_chaching_sound: BoolProperty(name="Play A Cha-Ching Sound When Project Costs Updates", default=False) tmp_dir: StringProperty( name="Temporary Directory", - description='Path to create and store temporary files. If left blank, a system default will be used.', + description="Path to create and store temporary files. If left blank, a system default will be used.", ) spatial_elements_unselectable: BoolProperty( name="Make Spatial Elements Unselectable By Default", @@ -302,7 +303,6 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): size=4, description="Color of background overlays", ) - opening_focus_opacity: bpy.props.IntProperty( default=100, min=0, @@ -312,7 +312,29 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): description="When modifying openings, other elements of the model will display with some transparency.\n0 is fully transparent and 100 is fully opaque", ) - def draw(self, context): + if TYPE_CHECKING: + svg2pdf_command: str + svg2dxf_command: str + svg_command: str + layout_svg_command: str + pdf_command: str + spreadsheet_command: str + should_hide_empty_props: bool + should_setup_workspace: bool + activate_workspace: bool + should_setup_toolbar: bool + should_play_chaching_sound: bool + spatial_elements_unselectable: bool + tmp_dir: str + decorations_colour: tuple[float, float, float, float] + decorator_color_selected: tuple[float, float, float, float] + decorator_color_unselected: tuple[float, float, float, float] + decorator_color_special: tuple[float, float, float, float] + decorator_color_error: tuple[float, float, float, float] + decorator_color_background: tuple[float, float, float, float] + opening_focus_opacity: int + + def draw(self, context: bpy.types.Context) -> None: layout = self.layout row = layout.row() @@ -333,7 +355,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Drawing", self.draw_drawing_settings) bonsai.bim.helper.draw_expandable_panel(self.layout, context, "Openings", self.draw_openings_settings) - def draw_commands(self, layout, context): + def draw_commands(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "svg2pdf_command") layout.prop(self, "svg2dxf_command") layout.prop(self, "svg_command") @@ -341,15 +363,16 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): layout.prop(self, "pdf_command") layout.prop(self, "spreadsheet_command") - def draw_misc_settings(self, layout, context): + def draw_misc_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(self, "should_hide_empty_props") layout.prop(self, "should_setup_workspace") layout.prop(self, "activate_workspace") layout.prop(self, "should_setup_toolbar") layout.prop(self, "should_play_chaching_sound") layout.prop(self, "spatial_elements_unselectable") - layout.prop(context.scene.BIMProjectProperties, "should_disable_undo_on_save") - layout.prop(context.scene.BIMProjectProperties, "should_stream") + props = tool.Project.get_project_props() + layout.prop(props, "should_disable_undo_on_save") + layout.prop(props, "should_stream") def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: props = tool.Model.get_model_props() @@ -357,7 +380,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): if props.occurrence_name_style == "CUSTOM": layout.prop(props, "occurrence_name_function") - def draw_directories(self, layout, context): + def draw_directories(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: row = layout.row(align=True) row.prop(context.scene.BIMProperties, "data_dir") row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.data_dir" @@ -370,25 +393,26 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): row.prop(self, "tmp_dir") row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.tmp_dir" - def draw_drawing_settings(self, layout, context): + def draw_drawing_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: layout.prop(context.scene.BIMProperties, "pset_dir") - layout.prop(context.scene.DocProperties, "sheets_dir") - layout.prop(context.scene.DocProperties, "layouts_dir") - layout.prop(context.scene.DocProperties, "titleblocks_dir") - layout.prop(context.scene.DocProperties, "drawings_dir") - layout.prop(context.scene.DocProperties, "stylesheet_path") - layout.prop(context.scene.DocProperties, "schedules_stylesheet_path") - layout.prop(context.scene.DocProperties, "markers_path") - layout.prop(context.scene.DocProperties, "symbols_path") - layout.prop(context.scene.DocProperties, "patterns_path") - layout.prop(context.scene.DocProperties, "shadingstyles_path") - layout.prop(context.scene.DocProperties, "shadingstyle_default") + dprops = tool.Drawing.get_document_props() + layout.prop(dprops, "sheets_dir") + layout.prop(dprops, "layouts_dir") + layout.prop(dprops, "titleblocks_dir") + layout.prop(dprops, "drawings_dir") + layout.prop(dprops, "stylesheet_path") + layout.prop(dprops, "schedules_stylesheet_path") + layout.prop(dprops, "markers_path") + layout.prop(dprops, "symbols_path") + layout.prop(dprops, "patterns_path") + layout.prop(dprops, "shadingstyles_path") + layout.prop(dprops, "shadingstyle_default") row = layout.row() - row.prop(context.scene.DocProperties, "drawing_font") - row.prop(context.scene.DocProperties, "magic_font_scale") - layout.prop(context.scene.DocProperties, "imperial_precision") - layout.prop(context.scene.DocProperties, "tolerance") - layout.prop(context.scene.DocProperties, "classes_to_wireframe") + row.prop(dprops, "drawing_font") + row.prop(dprops, "magic_font_scale") + layout.prop(dprops, "imperial_precision") + layout.prop(dprops, "tolerance") + layout.prop(dprops, "classes_to_wireframe") def draw_decorator_colors(self, layout, context): layout.row().prop(self, "decorations_colour") @@ -496,9 +520,10 @@ class BIM_PT_tabs(Panel): op.uri = "https://docs.bonsaibim.org/guides/troubleshooting.html#saving-and-loading-blend-files" row.operator("bim.close_blend_warning", text="", icon="CANCEL") - if context.mode == "OBJECT" and context.scene.BIMGeometryProperties.mode in ("OBJECT", "ITEM"): + gprops = tool.Geometry.get_geometry_props() + if context.mode == "OBJECT" and gprops.mode in ("OBJECT", "ITEM"): pass - elif context.mode.startswith("EDIT") and context.scene.BIMGeometryProperties.mode == "EDIT": + elif context.mode.startswith("EDIT") and gprops.mode == "EDIT": pass else: box = self.layout.box() @@ -533,7 +558,7 @@ class BIM_PT_tab_new_project_wizard(Panel): if not tool.Blender.is_tab(context, "PROJECT"): return False props = context.scene.BIMProperties - pprops = context.scene.BIMProjectProperties + pprops = tool.Project.get_project_props() if pprops.is_loading: return False elif tool.Ifc.get() or props.ifc_file: @@ -555,7 +580,7 @@ class BIM_PT_tab_project_info(Panel): if not tool.Blender.is_tab(context, "PROJECT"): return False props = context.scene.BIMProperties - pprops = context.scene.BIMProjectProperties + pprops = tool.Project.get_project_props() if pprops.is_loading: return True elif tool.Ifc.get() or props.ifc_file: @@ -854,6 +879,7 @@ class BIM_PT_tab_object_metadata(Panel): @classmethod def poll(cls, context): + props = tool.Project.get_project_props() return ( tool.Blender.is_tab(context, "OBJECT") and tool.Ifc.get() @@ -862,7 +888,7 @@ class BIM_PT_tab_object_metadata(Panel): and ( obj.type != "EMPTY" or not obj.instance_collection - or not any(l.empty_handle == obj for l in context.scene.BIMProjectProperties.links) + or not any(l.empty_handle == obj for l in props.links) ) ) @@ -1231,7 +1257,7 @@ class BIM_PT_decorators_overlay(Panel): view = context.space_data overlay = view.overlay - georeference_props = bpy.context.scene.BIMGeoreferenceProperties + georeference_props = tool.Georeference.get_georeference_props() aggregate_props = bpy.context.scene.BIMAggregateProperties nest_props = bpy.context.scene.BIMNestProperties model_props = tool.Model.get_model_props() diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index ad62b1fabc..b9699de172 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -913,7 +913,7 @@ class Blender(bonsai.core.tool.Blender): return False if not (element := tool.Ifc.get_entity(obj)): return True - if obj in bpy.context.scene.BIMProjectProperties.clipping_planes_objs: + if obj in tool.Project.get_project_props().clipping_planes_objs: return False usage_type = tool.Model.get_usage_type(element) if usage_type in ("LAYER1", "LAYER2"): diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 0dd9b6c855..ec4490b2c3 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import os import json import bpy @@ -30,10 +31,17 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore from mathutils import Vector from collections import defaultdict -from typing import Iterable, Literal +from typing import Iterable, Literal, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.debug.prop import BIMDebugProperties class Debug(bonsai.core.tool.Debug): + @classmethod + def get_debug_props(cls) -> BIMDebugProperties: + return bpy.context.scene.BIMDebugProperties + @classmethod def add_schema_identifier(cls, schema: W.schema_definition) -> None: IfcStore.schema_identifiers.append(schema.name()) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 134f3bcf5f..088e5b7025 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -346,19 +346,23 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def disable_editing_drawings(cls) -> None: - bpy.context.scene.DocProperties.is_editing_drawings = False + props = tool.Drawing.get_document_props() + props.is_editing_drawings = False @classmethod def disable_editing_schedules(cls) -> None: - bpy.context.scene.DocProperties.is_editing_schedules = False + props = tool.Drawing.get_document_props() + props.is_editing_schedules = False @classmethod def disable_editing_references(cls) -> None: - bpy.context.scene.DocProperties.is_editing_references = False + props = tool.Drawing.get_document_props() + props.is_editing_references = False @classmethod def disable_editing_sheets(cls) -> None: - bpy.context.scene.DocProperties.is_editing_sheets = False + props = tool.Drawing.get_document_props() + props.is_editing_sheets = False @classmethod def disable_editing_text(cls, obj: bpy.types.Object) -> None: @@ -383,19 +387,23 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def enable_editing_drawings(cls) -> None: - bpy.context.scene.DocProperties.is_editing_drawings = True + props = tool.Drawing.get_document_props() + props.is_editing_drawings = True @classmethod def enable_editing_schedules(cls) -> None: - bpy.context.scene.DocProperties.is_editing_schedules = True + props = tool.Drawing.get_document_props() + props.is_editing_schedules = True @classmethod def enable_editing_references(cls) -> None: - bpy.context.scene.DocProperties.is_editing_references = True + props = tool.Drawing.get_document_props() + props.is_editing_references = True @classmethod def enable_editing_sheets(cls) -> None: - bpy.context.scene.DocProperties.is_editing_sheets = True + props = tool.Drawing.get_document_props() + props.is_editing_sheets = True @classmethod def enable_editing_text(cls, obj: bpy.types.Object) -> None: @@ -616,7 +624,8 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def is_editing_sheets(cls) -> bool: - return bpy.context.scene.DocProperties.is_editing_sheets + props = tool.Drawing.get_document_props() + return props.is_editing_sheets @classmethod def remove_literal_from_annotation(cls, obj: bpy.types.Object, literal: ifcopenshell.entity_instance) -> None: @@ -810,7 +819,7 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def import_drawings(cls) -> None: - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() expanded_target_views = {d.target_view for d in props.drawings if d.is_expanded} if not hasattr(cls, "drawing_selected_states"): cls.drawing_selected_states = {} @@ -1048,7 +1057,8 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def show_decorations(cls) -> None: - bpy.context.scene.DocProperties.should_draw_decorations = True + props = tool.Drawing.get_document_props() + props.should_draw_decorations = True @classmethod def update_text_value(cls, obj: bpy.types.Object) -> None: @@ -1147,36 +1157,33 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def get_default_layout_path(cls, identification: str, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] + props = tool.Drawing.get_document_props() layouts_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir") - or bpy.context.scene.DocProperties.layouts_dir + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "LayoutsDir") or props.layouts_dir ) return os.path.join(layouts_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/") @classmethod def get_default_sheet_path(cls, identification: str, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] - sheets_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir") - or bpy.context.scene.DocProperties.sheets_dir - ) + props = tool.Drawing.get_document_props() + sheets_dir = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "SheetsDir") or props.sheets_dir return os.path.join(sheets_dir, cls.sanitise_filename(f"{identification} - {name}.svg")).replace("\\", "/") @classmethod def get_default_titleblock_path(cls, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] titleblocks_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") - or bpy.context.scene.DocProperties.titleblocks_dir + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") or props.titleblocks_dir ) return os.path.join(titleblocks_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/") @classmethod def get_default_drawing_path(cls, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] + props = tool.Drawing.get_document_props() drawings_dir = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir") - or bpy.context.scene.DocProperties.drawings_dir + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "DrawingsDir") or props.drawings_dir ) return os.path.join(drawings_dir, cls.sanitise_filename(f"{name}.svg")).replace("\\", "/") @@ -1187,15 +1194,16 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def get_default_drawing_resource_path(cls, resource: str) -> Union[str, None]: project = tool.Ifc.get().by_type("IfcProject")[0] - resource_path = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or getattr( - bpy.context.scene.DocProperties, f"{resource.lower()}_path" + props = tool.Drawing.get_document_props() + resource_path = ( + ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or props.resource_path ) if resource_path: return resource_path.replace("\\", "/") @classmethod def get_default_shading_style(cls) -> str: - dprops = bpy.context.scene.DocProperties + dprops = tool.Drawing.get_document_props() return dprops.shadingstyle_default @classmethod @@ -1501,7 +1509,7 @@ class Drawing(bonsai.core.tool.Drawing): dst.data = dst.data.copy() dst.name = dst.name.replace("IfcGridAxis/", "") dst.BIMObjectProperties.ifc_definition_id = 0 - dst.data.BIMMeshProperties.ifc_definition_id = 0 + tool.Geometry.get_geometry_props(dst).ifc_definition_id = 0 return dst def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]: @@ -1883,7 +1891,8 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def is_active_drawing(cls, drawing: ifcopenshell.entity_instance) -> bool: - return drawing.id() == bpy.context.scene.DocProperties.active_drawing_id + props = tool.Drawing.get_document_props() + return drawing.id() == props.active_drawing_id @classmethod def run_drawing_activate_model(cls) -> None: diff --git a/src/bonsai/bonsai/tool/feature.py b/src/bonsai/bonsai/tool/feature.py index d8fd8921eb..0990aef008 100644 --- a/src/bonsai/bonsai/tool/feature.py +++ b/src/bonsai/bonsai/tool/feature.py @@ -16,17 +16,25 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import bonsai.core.tool import bonsai.tool as tool import bonsai.bim.helper import ifcopenshell -from typing import Iterable +from typing import Iterable, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.void.prop import BIMBooleanProperties class Feature(bonsai.core.tool.Feature): # TODO: consolidate module/model/opening and module/void into new module/feature + @classmethod + def get_boolean_props(cls) -> BIMBooleanProperties: + return bpy.context.scene.BIMBooleanProperties + @classmethod def add_feature(cls, featured_obj: bpy.types.Object, feature_objs: Iterable[bpy.types.Object]) -> None: featured_element = tool.Ifc.get_entity(featured_obj) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index fca6541088..6fb121766c 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -27,6 +27,7 @@ import numpy.typing as npt import multiprocessing import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.geometry import ifcopenshell.api.grid import ifcopenshell.api.profile import ifcopenshell.api.style @@ -53,11 +54,23 @@ from math import radians, pi from mathutils import Vector, Matrix from mathutils.bvhtree import BVHTree from bonsai.bim.ifc import IfcStore -from typing import Union, Iterable, Optional, Literal, Iterator, List, TYPE_CHECKING, get_args, Generator, cast +from typing import ( + Union, + Iterable, + Optional, + Literal, + Iterator, + List, + TYPE_CHECKING, + get_args, + Generator, + cast, + TypeGuard, +) from typing_extensions import TypeIs if TYPE_CHECKING: - from bonsai.bim.prop import Attribute + from bonsai.bim.prop import Attribute, BIMMeshProperties from bonsai.bim.module.geometry.prop import BIMObjectGeometryProperties, BIMGeometryProperties @@ -70,6 +83,10 @@ class Geometry(bonsai.core.tool.Geometry): def get_object_geometry_props(cls, object: bpy.types.Object) -> BIMObjectGeometryProperties: return object.BIMGeometryProperties + @classmethod + def get_mesh_props(cls, mesh: TYPES_WITH_MESH_PROPERTIES) -> BIMMeshProperties: + return mesh.BIMMeshProperties + @classmethod def change_object_data(cls, obj: bpy.types.Object, data: bpy.types.ID, is_global: bool = False) -> None: if is_global: @@ -182,7 +199,9 @@ class Geometry(bonsai.core.tool.Geometry): if item_obj.obj == obj: props.item_objs.remove(i) break - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + item = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id) cls.remove_representation_item(item) cls.reload_representation(props.representation_obj) bpy.data.objects.remove(obj) @@ -249,17 +268,18 @@ class Geometry(bonsai.core.tool.Geometry): bonsai.core.system.remove_port(tool.Ifc, tool.System, port=port) ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element) - if isinstance(obj.data, bpy.types.Mesh) and not tool.Ifc.get_entity_by_id( - obj.data.BIMMeshProperties.ifc_definition_id - ): - tool.Blender.remove_data_block(obj.data) + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + if not tool.Ifc.get_entity_by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id): + tool.Blender.remove_data_block(mesh) if is_spatial: bonsai.core.spatial.import_spatial_decomposition(tool.Spatial) try: obj.name - if bpy.context.scene.BIMGeometryProperties.representation_obj == obj: - bpy.context.scene.BIMGeometryProperties.representation_obj = None + props = tool.Geometry.get_geometry_props() + if props.representation_obj == obj: + props.representation_obj = None bpy.data.objects.remove(obj) except: pass @@ -268,7 +288,9 @@ class Geometry(bonsai.core.tool.Geometry): def dissolve_triangulated_edges(cls, obj: bpy.types.Object) -> None: # AdvancedBreps may contain non-faceted, curved faces (e.g. as part of # a cylinder) so dissolving edges should not be allowed. - mesh_element = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + mesh = obj.data + assert isinstance(mesh, Geometry.TYPES_WITH_MESH_PROPERTIES) + mesh_element = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id) if ( ( mesh_element.is_a("IfcShapeRepresentation") @@ -279,26 +301,30 @@ class Geometry(bonsai.core.tool.Geometry): or not obj.data ): return - if hasattr(obj.data, "attributes") and (ios_edges_attribute := obj.data.attributes.get("ios_edges")): + + if not isinstance(mesh, bpy.types.Mesh): + return + + if hasattr(mesh, "attributes") and (ios_edges_attribute := mesh.attributes.get("ios_edges")): # Edges from a forced triangulation are stored as True in a boolean attribute on the mesh bm = bmesh.new() - bm.from_mesh(obj.data) + bm.from_mesh(mesh) edges_to_dissolve = [e for i, e in enumerate(bm.edges) if not ios_edges_attribute.data[i].value] bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve) - bm.to_mesh(obj.data) + bm.to_mesh(mesh) bm.free() - elif "ios_edges" in obj.data: + elif "ios_edges" in mesh: bm = bmesh.new() - bm.from_mesh(obj.data) - edges_to_keep = set(map(frozenset, obj.data["ios_edges"])) + bm.from_mesh(mesh) + edges_to_keep = set(map(frozenset, mesh["ios_edges"])) edges_to_dissolve = [] for edge in bm.edges: if frozenset([vert.index for vert in edge.verts]) not in edges_to_keep: edges_to_dissolve.append(edge) bmesh.ops.dissolve_edges(bm, edges=edges_to_dissolve) - bm.to_mesh(obj.data) + bm.to_mesh(mesh) bm.free() - del obj.data["ios_edges"] + del mesh["ios_edges"] @classmethod def apply_item_ids_as_vertex_groups(cls, obj: bpy.types.Object) -> None: @@ -486,13 +512,19 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def get_active_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: """:return: IfcRepresentation/IfcRepresentationItem or None""" - if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id: - return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + if ( + (data := obj.data) + and isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES) + and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id) + ): + return tool.Ifc.get().by_id(ifc_id) @classmethod - def get_data_representation(cls, data: bpy.types.Mesh) -> ifcopenshell.entity_instance | None: - if hasattr(data, "BIMMeshProperties") and data.BIMMeshProperties.ifc_definition_id: - return tool.Ifc.get().by_id(data.BIMMeshProperties.ifc_definition_id) + def get_data_representation(cls, data: bpy.types.ID) -> ifcopenshell.entity_instance | None: + if isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES) and ( + ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id + ): + return tool.Ifc.get().by_id(ifc_id) @classmethod def get_active_representation_context(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance: @@ -670,13 +702,13 @@ class Geometry(bonsai.core.tool.Geometry): return data.users != 0 @classmethod - def has_geometric_data(cls, obj: bpy.types.Object) -> bool: - if not obj.data: + def is_geometric_data(cls, data: Union[bpy.types.ID, None]) -> TypeGuard[Union[bpy.types.Mesh, bpy.types.Curve]]: + if not data: return False - if isinstance(obj.data, bpy.types.Mesh): - return bool(obj.data.vertices) - elif isinstance(obj.data, bpy.types.Curve): - return bool(obj.data.splines) + if isinstance(data, bpy.types.Mesh): + return bool(data.vertices) + elif isinstance(data, bpy.types.Curve): + return bool(data.splines) return False @classmethod @@ -826,7 +858,8 @@ class Geometry(bonsai.core.tool.Geometry): ifc_importer.material_creator.load_existing_materials() shape_has_openings = cls.does_shape_has_openings(shape) ifc_importer.material_creator.create(element, obj, mesh, shape_has_openings) - mesh.BIMMeshProperties.has_openings_applied = apply_openings + mprops = tool.Geometry.get_mesh_props(mesh) + mprops.has_openings_applied = apply_openings if not shape_has_openings: tool.Loader.load_indexed_colour_map(representation, mesh) tool.Loader.link_mesh(shape, mesh) @@ -852,7 +885,8 @@ class Geometry(bonsai.core.tool.Geometry): ifc_importer.material_creator.load_existing_materials() shape_has_openings = False ifc_importer.material_creator.create(element, obj, mesh, shape_has_openings) - mesh.BIMMeshProperties.has_openings_applied = apply_openings + mprops = tool.Geometry.get_mesh_props(mesh) + mprops.has_openings_applied = apply_openings if not shape_has_openings: tool.Loader.load_indexed_colour_map(representation, mesh) meshes[mesh_name] = mesh @@ -867,7 +901,7 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def import_representation_parameters(cls, data: bpy.types.Mesh) -> None: - props = data.BIMMeshProperties + props = tool.Geometry.get_mesh_props(data) elements = tool.Ifc.get().traverse(tool.Ifc.get().by_id(props.ifc_definition_id)) props.ifc_parameters.clear() for element in elements: @@ -974,7 +1008,8 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def is_profile_based(cls, data: bpy.types.Mesh) -> bool: - return data.BIMMeshProperties.subshape_type == "PROFILE" + props = tool.Geometry.get_mesh_props(data) + return props.subshape_type == "PROFILE" @classmethod def is_profile_object_active(cls) -> bool: @@ -992,7 +1027,7 @@ class Geometry(bonsai.core.tool.Geometry): data = obj.data if ( isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES) - and (ifc_id := data.BIMMeshProperties.ifc_definition_id) + and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id) and ((item := tool.Ifc.get().by_id(ifc_id)).is_a("IfcRepresentationItem")) ): return item @@ -1008,14 +1043,14 @@ class Geometry(bonsai.core.tool.Geometry): if tool.Ifc.get_entity(obj): return obj elif tool.Geometry.is_representation_item(obj): - return bpy.context.scene.BIMGeometryProperties.representation_obj + return tool.Geometry.get_geometry_props().representation_obj @classmethod def is_boolean_operand(cls, obj: bpy.types.Object) -> bool: return bool( (data := obj.data) and isinstance(data, Geometry.TYPES_WITH_MESH_PROPERTIES) - and (ifc_id := data.BIMMeshProperties.ifc_definition_id) + and (ifc_id := tool.Geometry.get_mesh_props(data).ifc_definition_id) and (item := tool.Ifc.get().by_id(ifc_id)) and ( item.is_a("IfcBooleanResult") @@ -1041,7 +1076,8 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def record_object_materials(cls, obj: bpy.types.Object) -> None: - obj.data.BIMMeshProperties.material_checksum = cls.get_material_checksum(obj) + props = tool.Geometry.get_mesh_props(obj.data) + props.material_checksum = cls.get_material_checksum(obj) @classmethod def record_object_position(cls, obj: bpy.types.Object) -> None: @@ -1146,11 +1182,13 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def should_force_faceted_brep(cls) -> bool: - return bpy.context.scene.BIMGeometryProperties.should_force_faceted_brep + props = tool.Geometry.get_geometry_props() + return props.should_force_faceted_brep @classmethod def should_force_triangulation(cls) -> bool: - return bpy.context.scene.BIMGeometryProperties.should_force_triangulation + props = tool.Geometry.get_geometry_props() + return props.should_force_triangulation @classmethod def should_generate_uvs(cls, obj: bpy.types.Object) -> bool: @@ -1167,7 +1205,8 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def should_use_presentation_style_assignment(cls) -> bool: - return bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment + props = tool.Geometry.get_geometry_props() + return props.should_use_presentation_style_assignment @classmethod def get_model_representations(cls) -> list[ifcopenshell.entity_instance]: @@ -1238,7 +1277,8 @@ class Geometry(bonsai.core.tool.Geometry): In the most cases just use reload_representation as it will handle those complications by itself. """ - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + representation = cls.get_active_representation(obj) + assert representation bonsai.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -1526,7 +1566,7 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def get_blender_offset_type(cls, obj: bpy.types.Object) -> Optional[str]: - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: if (result := obj.BIMObjectProperties.blender_offset_type) == "NONE": result = obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" @@ -1641,7 +1681,8 @@ class Geometry(bonsai.core.tool.Geometry): for item_obj in props.item_objs: if not (obj := item_obj.obj) or not tool.Ifc.is_moved(obj): continue - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + item = cls.get_active_representation(obj) + assert item if item.is_a("IfcSweptAreaSolid"): has_changed = True old_position = item.Position @@ -1683,7 +1724,7 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def import_item_attributes(cls, obj: bpy.types.Object) -> None: - props = obj.data.BIMMeshProperties + props = tool.Geometry.get_mesh_props(obj.data) props.item_attributes.clear() item = tool.Ifc.get().by_id(props.ifc_definition_id) allowed_attributes = [ @@ -1710,10 +1751,10 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def update_item_attributes(cls, obj: bpy.types.Object) -> None: - props = obj.data.BIMMeshProperties + props = tool.Geometry.get_mesh_props(obj.data) ifc_file = tool.Ifc.get() - item = tool.Ifc.get().by_id(props.ifc_definition_id) + item = ifc_file.by_id(props.ifc_definition_id) for attribute in props.item_attributes: setattr(item, attribute.name, attribute.get_value()) @@ -1738,7 +1779,9 @@ class Geometry(bonsai.core.tool.Geometry): tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(tool.Ifc.get()) tool.Loader.settings.context_settings = tool.Loader.create_settings() tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True) - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + assert isinstance(obj.data, bpy.types.Mesh) + item = tool.Geometry.get_active_representation(obj) + assert item obj.data.clear_geometry() if item.is_a("IfcHalfSpaceSolid"): @@ -1802,12 +1845,14 @@ class Geometry(bonsai.core.tool.Geometry): props.mode = "OBJECT" props.is_changing_mode = False props.representation_obj = None - bpy.context.scene.BIMBooleanProperties.is_editing = False + tool.Feature.get_boolean_props().is_editing = False @classmethod def edit_meshlike_item(cls, obj: bpy.types.Object) -> None: - item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) - if obj.data.BIMMeshProperties.mesh_checksum == cls.get_mesh_checksum(obj.data): + item = tool.Geometry.get_active_representation(obj) + assert item + mprops = tool.Geometry.get_mesh_props(obj.data) + if mprops.mesh_checksum == cls.get_mesh_checksum(obj.data): return builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) @@ -1830,8 +1875,8 @@ class Geometry(bonsai.core.tool.Geometry): for inverse in tool.Ifc.get().get_inverse(item): ifcopenshell.util.element.replace_attribute(inverse, item, new_item) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item) - obj.data.BIMMeshProperties.ifc_definition_id = new_item.id() - cls.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj) + tool.Ifc.link(new_item, obj.data) + cls.reload_representation(props.representation_obj) @classmethod def split_by_loose_parts(cls, obj: bpy.types.Object) -> List[bpy.types.Mesh]: diff --git a/src/bonsai/bonsai/tool/georeference.py b/src/bonsai/bonsai/tool/georeference.py index f2a0a4d866..7b8ee46d24 100644 --- a/src/bonsai/bonsai/tool/georeference.py +++ b/src/bonsai/bonsai/tool/georeference.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import json import numpy as np @@ -27,24 +28,34 @@ import ifcopenshell.util.unit import bonsai.core.tool import bonsai.tool as tool import bonsai.bim.helper -from typing import Any, Union, Literal +from typing import Any, Union, Literal, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.georeference.prop import BIMGeoreferenceProperties class Georeference(bonsai.core.tool.Georeference): COORDINATE_TYPE = Literal["blender", "local", "map"] + @classmethod + def get_georeference_props(cls) -> BIMGeoreferenceProperties: + return bpy.context.scene.BIMGeoreferenceProperties + @classmethod def add_georeferencing(cls) -> None: + props = cls.get_georeference_props() tool.Ifc.run( "georeference.add_georeferencing", - ifc_class=bpy.context.scene.BIMGeoreferenceProperties.coordinate_operation_class, + ifc_class=props.coordinate_operation_class, ) @classmethod def import_projected_crs(cls) -> None: + props = tool.Georeference.get_georeference_props() + def callback(name, prop, data): if name == "MapUnit": - new = bpy.context.scene.BIMGeoreferenceProperties.projected_crs.add() + new = props.projected_crs.add() new.name = name new.data_type = "enum" new.is_null = data[name] is None @@ -61,7 +72,6 @@ class Georeference(bonsai.core.tool.Georeference): new.update = "tool.Georeference.update_map_unit" return True - props = bpy.context.scene.BIMGeoreferenceProperties props.projected_crs.clear() if tool.Ifc.get_schema() == "IFC2X3": @@ -84,7 +94,8 @@ class Georeference(bonsai.core.tool.Georeference): result = 1.0 else: result = 1.0 - for attribute in bpy.context.scene.BIMGeoreferenceProperties.coordinate_operation: + props = cls.get_georeference_props() + for attribute in props.coordinate_operation: if attribute.name == "Scale": attribute.set_value(str(result)) @@ -92,7 +103,7 @@ class Georeference(bonsai.core.tool.Georeference): def import_coordinate_operation(cls) -> None: def callback(name, prop, data): if name in ("FirstCoordinate", "SecondCoordinate"): - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() if name == "FirstCoordinate": new = props.coordinate_operation.add() new.name = "Measure Type" @@ -110,7 +121,7 @@ class Georeference(bonsai.core.tool.Georeference): prop.string_value = "" if prop.is_null else str(data[name].wrappedValue) return True elif name == "XAxisAbscissa": - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() props.is_changing_angle = True if data["XAxisAbscissa"] is None or data["XAxisOrdinate"] is None: props.x_axis_is_null = True @@ -132,7 +143,7 @@ class Georeference(bonsai.core.tool.Georeference): prop.string_value = "" if prop.is_null else str(data[name]) return True - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() props.coordinate_operation.clear() if tool.Ifc.get_schema() == "IFC2X3": @@ -151,7 +162,7 @@ class Georeference(bonsai.core.tool.Georeference): if tool.Ifc.get_schema() == "IFC2X3": return - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() props.is_changing_angle = True props.true_north_abscissa = "0" props.true_north_ordinate = "1" @@ -175,7 +186,7 @@ class Georeference(bonsai.core.tool.Georeference): attributes[prop.name] = tool.Ifc.get().by_id(int(prop.enum_value)) return True - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() return bonsai.bim.helper.export_attributes(props.projected_crs, callback=callback) @classmethod @@ -191,7 +202,7 @@ class Georeference(bonsai.core.tool.Georeference): attributes[prop.name] = tool.Ifc.get().create_entity(measure_type, float(prop.string_value)) return True elif prop.name == "XAxisAbscissa": - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() if props.x_axis_is_null: attributes["XAxisAbscissa"] = None attributes["XAxisOrdinate"] = None @@ -206,12 +217,12 @@ class Georeference(bonsai.core.tool.Georeference): attributes[prop.name] = float(prop.string_value) return True - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() return bonsai.bim.helper.export_attributes(props.coordinate_operation, callback=callback) @classmethod def get_true_north_attributes(cls) -> Union[list[float], None]: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() try: return [float(props.true_north_abscissa), float(props.true_north_ordinate)] except ValueError: @@ -219,36 +230,42 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def enable_editing(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing = True + props = cls.get_georeference_props() + props.is_editing = True @classmethod def disable_editing(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing = False + props = cls.get_georeference_props() + props.is_editing = False @classmethod def enable_editing_wcs(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing_wcs = True + props = cls.get_georeference_props() + props.is_editing_wcs = True @classmethod def disable_editing_wcs(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing_wcs = False + props = cls.get_georeference_props() + props.is_editing_wcs = False @classmethod def enable_editing_true_north(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing_true_north = True + props = cls.get_georeference_props() + props.is_editing_true_north = True @classmethod def disable_editing_true_north(cls) -> None: - bpy.context.scene.BIMGeoreferenceProperties.is_editing_true_north = False + props = cls.get_georeference_props() + props.is_editing_true_north = False @classmethod def set_coordinates(cls, io: COORDINATE_TYPE, coordinates: list[float]) -> None: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() setattr(props, f"{io}_coordinates", ",".join([str(o) for o in coordinates])) @classmethod def get_coordinates(cls, io: COORDINATE_TYPE) -> list[float]: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() return [float(co) for co in getattr(props, f"{io}_coordinates").split(",")] @classmethod @@ -260,7 +277,7 @@ class Georeference(bonsai.core.tool.Georeference): def xyz2enh( cls, coordinates: tuple[float, float, float], should_return_in_map_units: bool = True ) -> tuple[float, float, float]: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() if props.has_blender_offset: coordinates = ifcopenshell.util.geolocation.xyz2enh( coordinates[0], @@ -279,7 +296,7 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def enh2xyz(cls, coordinates: tuple[float, float, float]) -> tuple[float, float, float]: coordinates = ifcopenshell.util.geolocation.auto_enh2xyz(tool.Ifc.get(), *coordinates) - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() if props.has_blender_offset: coordinates = ifcopenshell.util.geolocation.enh2xyz( coordinates[0], @@ -329,7 +346,7 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def import_wcs(cls) -> None: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() wcs = None for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): wcs = context.WorldCoordinateSystem @@ -346,7 +363,7 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def export_wcs(cls) -> dict[str, float]: - props = bpy.context.scene.BIMGeoreferenceProperties + props = cls.get_georeference_props() return { "x": float(props.wcs_x), "y": float(props.wcs_y), @@ -361,7 +378,7 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def set_model_origin(cls) -> None: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - gprops = bpy.context.scene.BIMGeoreferenceProperties + gprops = tool.Georeference.get_georeference_props() e, n, h = cls.xyz2enh((0, 0, 0), should_return_in_map_units=False) gprops.model_origin = f"{e},{n},{h}" gprops.model_origin_si = f"{e * unit_scale},{n * unit_scale},{h * unit_scale}" @@ -375,4 +392,4 @@ class Georeference(bonsai.core.tool.Georeference): @classmethod def has_blender_offset(cls) -> bool: - return bpy.context.scene.BIMGeoreferenceProperties.has_blender_offset + return tool.Georeference.get_georeference_props().has_blender_offset diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index 5b3f8bb48a..c82ffa80a5 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -111,7 +111,7 @@ class Ifc(bonsai.core.tool.Ifc): elif isinstance(obj, bpy.types.Material): props = obj.BIMStyleProperties else: - props = obj.BIMMeshProperties + props = tool.Geometry.get_mesh_props(obj) if props and (ifc_definition_id := props.ifc_definition_id): try: @@ -180,7 +180,7 @@ class Ifc(bonsai.core.tool.Ifc): cls.setup_listeners(obj) IfcStore.edited_objs = set() - edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs + edited_objs = tool.Project.get_project_props().edited_objs for i in range(len(edited_objs))[::-1]: obj = edited_objs[i].obj if obj: @@ -220,7 +220,7 @@ class Ifc(bonsai.core.tool.Ifc): """ if obj in IfcStore.edited_objs: return - edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs + edited_objs = tool.Project.get_project_props().edited_objs edited_objs.add().obj = obj IfcStore.edited_objs.add(obj) IfcStore.history_edit_object(obj, finish_editing=False) @@ -233,7 +233,7 @@ class Ifc(bonsai.core.tool.Ifc): """ if obj not in IfcStore.edited_objs: return - edited_objs = bpy.context.scene.BIMProjectProperties.edited_objs + edited_objs = tool.Project.get_project_props().edited_objs edited_objs.remove(next(i for i, o in enumerate(edited_objs) if o.obj == obj)) IfcStore.edited_objs.discard(obj) IfcStore.history_edit_object(obj, finish_editing=True) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index f79f4b4509..17e8764fbf 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -89,7 +89,7 @@ class Loader(bonsai.core.tool.Loader): @classmethod def get_mesh_name(cls, representation: ifcopenshell.entity_instance) -> str: - context_id = representation.ContextOfItems.id() if hasattr(representation, "ContextOfItems") else 0 + context_id = context.id() if (context := getattr(representation, "ContextOfItems", None)) else 0 return "{}/{}".format(context_id, representation.id()) @classmethod @@ -105,7 +105,7 @@ class Loader(bonsai.core.tool.Loader): mesh: tool.Geometry.TYPES_WITH_MESH_PROPERTIES, ) -> None: geometry = shape.geometry if hasattr(shape, "geometry") else shape - mesh.BIMMeshProperties.ifc_definition_id = int(geometry.id.split("-")[0]) + tool.Geometry.get_mesh_props(mesh).ifc_definition_id = int(geometry.id.split("-")[0]) @classmethod def create_surface_style_shading( @@ -698,7 +698,7 @@ class Loader(bonsai.core.tool.Loader): project_north = 0 if has_offset or has_rotation: - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.blender_offset_x = str(model_offset[0]) props.blender_offset_y = str(model_offset[1]) props.blender_offset_z = str(model_offset[2]) @@ -747,7 +747,7 @@ class Loader(bonsai.core.tool.Loader): cls, element: ifcopenshell.entity_instance, is_gross: bool = False ) -> Union[ifcopenshell.geom.ShapeElementType, None]: context_settings = cls.settings.gross_context_settings if is_gross else cls.settings.context_settings - geometry_library = bpy.context.scene.BIMProjectProperties.geometry_library + geometry_library = tool.Project.get_project_props().geometry_library for settings in context_settings: try: result = ifcopenshell.geom.create_shape(settings, element, geometry_library=geometry_library) @@ -952,7 +952,7 @@ class Loader(bonsai.core.tool.Loader): matrix[1][3] = offset_xyz[1] matrix[2][3] = offset_xyz[2] - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset: if obj.BIMObjectProperties.blender_offset_type == "NONE": obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" diff --git a/src/bonsai/bonsai/tool/misc.py b/src/bonsai/bonsai/tool/misc.py index 87d367895b..db43a14c5d 100644 --- a/src/bonsai/bonsai/tool/misc.py +++ b/src/bonsai/bonsai/tool/misc.py @@ -113,10 +113,11 @@ class Misc(bonsai.core.tool.Misc): new_objs = [] for obj in objs: - if obj.type != "MESH" or obj == cutter: + mesh = obj.data + if not isinstance(mesh, bpy.types.Mesh) or obj == cutter: continue new_obj = obj.copy() - new_obj.data = obj.data.copy() + new_obj.data = mesh.copy() for collection in obj.users_collection: collection.objects.link(new_obj) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 6db9718d4d..e55798602f 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -277,7 +277,7 @@ class Model(bonsai.core.tool.Model): mesh = bpy.data.meshes.new("Axis") mesh.from_pydata(cls.vertices, cls.edges, []) - mesh.BIMMeshProperties.subshape_type = "AXIS" + tool.Geometry.get_mesh_props(mesh).subshape_type = "AXIS" if obj is None: obj = bpy.data.objects.new("Axis", mesh) @@ -334,7 +334,7 @@ class Model(bonsai.core.tool.Model): mesh = bpy.data.meshes.new("Profile") mesh.from_pydata(cls.vertices, cls.edges, []) - mesh.BIMMeshProperties.subshape_type = "PROFILE" + tool.Geometry.get_mesh_props(mesh).subshape_type = "PROFILE" if obj is None: obj = bpy.data.objects.new("Profile", mesh) @@ -376,7 +376,7 @@ class Model(bonsai.core.tool.Model): mesh = bpy.data.meshes.new("Curve") mesh.from_pydata(cls.vertices, cls.edges, []) - mesh.BIMMeshProperties.subshape_type = "PROFILE" + tool.Geometry.get_mesh_props(mesh).subshape_type = "PROFILE" if obj is None: obj = bpy.data.objects.new("Curve", mesh) @@ -417,7 +417,7 @@ class Model(bonsai.core.tool.Model): mesh = bpy.data.meshes.new("Surface") mesh.from_pydata(cls.vertices, cls.edges, []) - mesh.BIMMeshProperties.subshape_type = "PROFILE" + tool.Geometry.get_mesh_props(mesh).subshape_type = "PROFILE" if obj is None: obj = bpy.data.objects.new("Surface", mesh) @@ -569,7 +569,10 @@ class Model(bonsai.core.tool.Model): element: Optional[ifcopenshell.entity_instance] = None, representation: Optional[ifcopenshell.entity_instance] = None, ) -> list[ifcopenshell.entity_instance]: + """Either element or representation must be provided.""" + assert element or representation, "Either element or representation must be provided." if representation is None: + assert element representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not representation: return [] @@ -1461,9 +1464,9 @@ class Model(bonsai.core.tool.Model): after material assignment or material unassignment. """ for element in elements: - if not (obj := tool.Ifc.get_object(element)) or not obj.data: + if not (obj := tool.Ifc.get_object(element)) or not (data := obj.data): continue - representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + representation = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(data).ifc_definition_id) bonsai.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -1931,7 +1934,9 @@ class Model(bonsai.core.tool.Model): or it's not referring to an object (e.g. potential boolean object).""" if obj.type != "MESH": return - return obj.data.BIMMeshProperties.obj + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + return tool.Geometry.get_mesh_props(mesh).obj @classmethod def get_tracked_opening_type(cls, obj: bpy.types.Object) -> Union[Literal["OPENING", "BOOLEAN"], None]: diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 8554e182f1..db8f4e7922 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -20,6 +20,7 @@ import bpy import bmesh import math import ifcopenshell +import ifcopenshell.util.unit import bonsai.core.tool import bonsai.tool as tool from bonsai.bim.module.drawing.helper import format_distance @@ -452,7 +453,8 @@ class Polyline(bonsai.core.tool.Polyline): def format_input_ui_units(cls, value: float, is_area: bool = False) -> str: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if bpy.context.scene.unit_settings.system == "IMPERIAL": - precision = bpy.context.scene.DocProperties.imperial_precision + dprops = tool.Drawing.get_document_props() + precision = dprops.imperial_precision if is_area: area_unit = bpy.context.scene.BIMProperties.area_unit unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), unit_type=area_unit) diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 7b529cdea9..98f4f76799 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -235,7 +235,7 @@ class Project(bonsai.core.tool.Project): @classmethod def load_linked_models_from_ifc(cls) -> None: - links = bpy.context.scene.BIMProjectProperties.links + links = tool.Project.get_project_props().links links.clear() links_document = cls.get_linked_models_document() if not links_document: @@ -252,7 +252,7 @@ class Project(bonsai.core.tool.Project): @classmethod def save_linked_models_to_ifc(cls) -> None: ifc_file = tool.Ifc.get() - links = bpy.context.scene.BIMProjectProperties.links + links = tool.Project.get_project_props().links filepaths: set[Path] = set() for link in links: filepaths.add(Path(link.name)) diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index b18e649338..ac763cdbf4 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -19,6 +19,7 @@ import bpy import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.style import ifcopenshell.util.representation import ifcopenshell.util.element import ifcopenshell.util.placement @@ -49,12 +50,12 @@ class Root(bonsai.core.tool.Root): tool.Geometry.run_style_add_style(obj=mat) for mat in tool.Geometry.get_object_materials_without_styles(obj) ] - ifcopenshell.api.run( - "style.assign_representation_styles", + props = tool.Geometry.get_geometry_props() + ifcopenshell.api.style.assign_representation_styles( tool.Ifc.get(), shape_representation=body, styles=tool.Geometry.get_styles(obj), - should_use_presentation_style_assignment=bpy.context.scene.BIMGeometryProperties.should_use_presentation_style_assignment, + should_use_presentation_style_assignment=props.should_use_presentation_style_assignment, ) @classmethod @@ -169,8 +170,8 @@ class Root(bonsai.core.tool.Root): @classmethod def get_object_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: - if obj.data and obj.data.BIMMeshProperties.ifc_definition_id: - return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id) + if obj.data and (mesh_props := tool.Geometry.get_mesh_props(obj.data)).ifc_definition_id: + return tool.Ifc.get().by_id(mesh_props.ifc_definition_id) element = tool.Ifc.get_entity(obj) if element.is_a("IfcTypeProduct"): if element.RepresentationMaps: @@ -302,8 +303,8 @@ class Root(bonsai.core.tool.Root): voided_objs.append(subobj) for voided_obj in voided_objs: - if voided_obj.data: - representation = tool.Ifc.get().by_id(voided_obj.data.BIMMeshProperties.ifc_definition_id) + if data := voided_obj.data: + representation = tool.Ifc.get().by_id(tool.Geometry.get_mesh_props(data).ifc_definition_id) bonsai.core.geometry.switch_representation( tool.Ifc, tool.Geometry, @@ -421,8 +422,8 @@ class Root(bonsai.core.tool.Root): to unlink them. """ tool.Ifc.unlink(obj=obj) - if hasattr(obj.data, "BIMMeshProperties"): - obj.data.BIMMeshProperties.ifc_definition_id = 0 + if tool.Geometry.has_mesh_properties((data := obj.data)): + tool.Geometry.get_mesh_props(mesh).ifc_definition_id = 0 for material_slot in obj.material_slots: if material := material_slot.material: tool.Ifc.unlink(obj=material) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index b06d9a65bb..10636f031a 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -25,6 +25,7 @@ import math import mathutils from mathutils import Matrix, Vector from lark import Lark, Transformer +from typing import Union class Snap(bonsai.core.tool.Snap): @@ -313,7 +314,9 @@ class Snap(bonsai.core.tool.Snap): plane_normal = tool.Polyline.use_transform_orientations(plane_normal) return plane_origin, plane_normal - def cast_rays_to_single_object(obj, mouse_pos): + def cast_rays_to_single_object( + obj: bpy.types.Object, mouse_pos: tuple[int, int] + ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: if obj.type != "MESH": return None, None, None hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj) @@ -332,7 +335,9 @@ class Snap(bonsai.core.tool.Snap): else: return None, None, None - def cast_rays_and_get_best_object(objs_to_raycast, mouse_pos): + def cast_rays_and_get_best_object( + objs_to_raycast: list[bpy.types.Object], mouse_pos: tuple[int, int] + ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: best_length_squared = 1.0 best_obj = None best_hit = None diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index f2c784c464..2f59e0b3da 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -699,9 +699,10 @@ class Spatial(bonsai.core.tool.Spatial): for obj in bpy.context.visible_objects: visible_element = tool.Ifc.get_entity(obj) + old_mesh = obj.data if ( not visible_element - or obj.type != "MESH" + or not isinstance(old_mesh, bpy.types.Mesh) or not cls.is_bounding_class(visible_element) or not tool.Drawing.is_intersecting_plane(obj, cut_point, cut_normal) ): @@ -959,7 +960,7 @@ class Spatial(bonsai.core.tool.Spatial): old_mesh = active_obj.data old_mesh_name = old_mesh.name assert active_obj and isinstance(old_mesh, bpy.types.Mesh) - mesh.BIMMeshProperties.ifc_definition_id = old_mesh.BIMMeshProperties.ifc_definition_id + tool.Geometry.get_mesh_props(mesh).ifc_definition_id = tool.Geometry.get_mesh_props(old_mesh).ifc_definition_id tool.Geometry.change_object_data(active_obj, mesh, is_global=True) tool.Ifc.edit(active_obj) tool.Blender.remove_data_block(old_mesh) diff --git a/src/bonsai/bonsai/tool/surveyor.py b/src/bonsai/bonsai/tool/surveyor.py index 80aa96a341..28856a19f8 100644 --- a/src/bonsai/bonsai/tool/surveyor.py +++ b/src/bonsai/bonsai/tool/surveyor.py @@ -32,7 +32,7 @@ class Surveyor(bonsai.core.tool.Surveyor): def get_absolute_matrix(cls, obj: bpy.types.Object) -> npt.NDArray[np.float64]: M_TRANSLATION = (slice(0, 3), 3) matrix = np.array(obj.matrix_world) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE": unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) coordinate_offset = tool.Geometry.get_cartesian_point_offset(obj) diff --git a/src/bonsai/scripts/headless_import.py b/src/bonsai/scripts/headless_import.py index cda13e3878..6ce5f1f1ad 100644 --- a/src/bonsai/scripts/headless_import.py +++ b/src/bonsai/scripts/headless_import.py @@ -1,12 +1,13 @@ # This can be run using `blender -b -P headless_import.py` import bpy +import bonsai.tool as tool from bonsai.bim.ifc import IfcStore # When federating, you may wish to manually specify the origin to ensure models # with different or arbitrary origin conventions will turn up in the right spot. -props = bpy.context.scene.BIMGeoreferenceProperties +props = tool.Georeference.get_georeference_props() # A good idea it to test import a portion of the model (or grids only) and check # georeferencing coordinates in the IFC Georeferencing panel before filling out @@ -19,7 +20,7 @@ props = bpy.context.scene.BIMGeoreferenceProperties # props.blender_x_axis_ordinate = '0.989063862448262' # props.has_blender_offset = True -props = bpy.context.scene.BIMProjectProperties +props = tool.Project.get_project_props() # Generally recommended to disable caching for stability right now props.should_cache = False diff --git a/src/bonsai/test/bim/bootstrap.py b/src/bonsai/test/bim/bootstrap.py index bea49df413..a973c8a7dc 100644 --- a/src/bonsai/test/bim/bootstrap.py +++ b/src/bonsai/test/bim/bootstrap.py @@ -23,6 +23,7 @@ import bpy import pytest import webbrowser import bonsai.bim.handler +import bonsai.tool as tool import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.representation @@ -66,7 +67,8 @@ class NewIfc4X3: bpy.data.batch_remove(bpy.data.objects) bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) bonsai.bim.handler.load_post(None) - bpy.context.scene.BIMProjectProperties.export_schema = "IFC4X3_ADD2" + props = tool.Project.get_project_props() + props.export_schema = "IFC4X3_ADD2" bpy.ops.bim.create_project() diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 95945ced81..50598d7be1 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -39,7 +39,7 @@ scenarios("feature") variables = { "cwd": Path.cwd().as_posix(), - "ifc": "IfcStore.get_file()", + "ifc": "tool.Ifc.get()", "pset_ifc": "IfcStore.pset_template_file", "classification_ifc": "IfcStore.classification_file", } @@ -190,7 +190,8 @@ def an_empty_blender_session(): # default project settings bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" - bpy.context.scene.BIMProjectProperties.template_file = "0" + props = tool.Project.get_project_props() + props.template_file = "0" tool.Blender.get_addon_preferences().should_play_chaching_sound = False @@ -203,7 +204,8 @@ def an_empty_ifc_project(): @given("an empty IFC2X3 project") def an_empty_ifc_2x3_project(): an_empty_blender_session() - bpy.context.scene.BIMProjectProperties.export_schema = "IFC2X3" + props = tool.Project.get_project_props() + props.export_schema = "IFC2X3" bpy.ops.bim.create_project() @@ -742,7 +744,7 @@ def the_object_name_has_a_representation_type_of_context(name, type, context): def the_object_name_data_is_a_type_representation_of_context(name, type, context): ifc = an_ifc_file_exists() context, subcontext, target_view = context.split("/") - rep = ifc.by_id(the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id) + rep = ifc.by_id(tool.Geometry.get_mesh_props(the_object_name_exists(name).data).ifc_definition_id) assert rep assert rep.RepresentationType == type, f"The object {name} is not a {type} representation" assert rep.ContextOfItems.ContextType == context @@ -888,7 +890,7 @@ def the_object_name_has_no_data(name): @then(parsers.parse('the object "{name}" has data which is an IFC representation')) def the_object_name_has_ifc_representation_data(name): - id = the_object_name_exists(name).data.BIMMeshProperties.ifc_definition_id + id = tool.Geometry.get_mesh_props(the_object_name_exists(name).data).ifc_definition_id assert id != 0, f"The ID is {id}" diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 5e3c239e03..902e51e75d 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -113,30 +113,34 @@ class TestDeleteDrawingElements(NewFile): class TestDisableEditingDrawings(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_drawings = True + props = tool.Drawing.get_document_props() + props.is_editing_drawings = True subject.disable_editing_drawings() - assert bpy.context.scene.DocProperties.is_editing_drawings == False + assert props.is_editing_drawings == False class TestDisableEditingSchedules(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_schedules = True + props = tool.Drawing.get_document_props() + props.is_editing_schedules = True subject.disable_editing_schedules() - assert bpy.context.scene.DocProperties.is_editing_schedules == False + assert props.is_editing_schedules == False class TestDisableEditingReferences(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_references = True + props = tool.Drawing.get_document_props() + props.is_editing_references = True subject.disable_editing_references() - assert bpy.context.scene.DocProperties.is_editing_references == False + assert props.is_editing_references == False class TestDisableEditingSheets(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_sheets = True + props = tool.Drawing.get_document_props() + props.is_editing_sheets = True subject.disable_editing_sheets() - assert bpy.context.scene.DocProperties.is_editing_sheets == False + assert props.is_editing_sheets == False class TestDisableEditingText(NewFile): @@ -166,30 +170,34 @@ class TestEnableEditing(NewFile): class TestEnableEditingDrawings(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_drawings = False + props = tool.Drawing.get_document_props() + props.is_editing_drawings = False subject.enable_editing_drawings() - assert bpy.context.scene.DocProperties.is_editing_drawings == True + assert props.is_editing_drawings == True class TestEnableEditingSchedules(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_schedules = False + props = tool.Drawing.get_document_props() + props.is_editing_schedules = False subject.enable_editing_schedules() - assert bpy.context.scene.DocProperties.is_editing_schedules == True + assert props.is_editing_schedules == True class TestEnableEditingReferences(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_references = False + props = tool.Drawing.get_document_props() + props.is_editing_references = False subject.enable_editing_references() - assert bpy.context.scene.DocProperties.is_editing_references == True + assert props.is_editing_references == True class TestEnableEditingSheets(NewFile): def test_run(self): - bpy.context.scene.DocProperties.is_editing_sheets = False + props = tool.Drawing.get_document_props() + props.is_editing_sheets = False subject.enable_editing_sheets() - assert bpy.context.scene.DocProperties.is_editing_sheets == True + assert props.is_editing_sheets == True class TestEnableEditingText(NewFile): @@ -492,7 +500,7 @@ class TestImportDrawings(NewFile): pset = ifcopenshell.api.run("pset.add_pset", ifc, product=drawing, name="EPset_Drawing") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={"TargetView": "PLAN_VIEW"}) subject.import_drawings() - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() for d in props.drawings: d.is_expanded = True subject.import_drawings() @@ -508,7 +516,7 @@ class TestImportSchedules(NewFile): ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SCHEDULE") subject.import_documents("SCHEDULE") - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.schedules[0].ifc_definition_id == document.id() assert props.schedules[0].identification == "X" assert props.schedules[0].name == "FOOBAR" @@ -519,7 +527,7 @@ class TestImportSchedules(NewFile): ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="SCHEDULE") subject.import_documents("SCHEDULE") - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.schedules[0].ifc_definition_id == document.id() assert props.schedules[0].identification == "X" assert props.schedules[0].name == "FOOBAR" @@ -532,7 +540,7 @@ class TestImportReferences(NewFile): ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="REFERENCE") subject.import_documents("REFERENCE") - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.references[0].ifc_definition_id == document.id() assert props.references[0].identification == "X" assert props.references[0].name == "FOOBAR" @@ -543,7 +551,7 @@ class TestImportReferences(NewFile): ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="REFERENCE") subject.import_documents("REFERENCE") - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.references[0].ifc_definition_id == document.id() assert props.references[0].identification == "X" assert props.references[0].name == "FOOBAR" @@ -556,7 +564,7 @@ class TestImportSheets(NewFile): ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SHEET") subject.import_sheets() - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.sheets[0].ifc_definition_id == document.id() assert props.sheets[0].identification == "X" assert props.sheets[0].name == "FOOBAR" @@ -567,7 +575,7 @@ class TestImportSheets(NewFile): ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ") document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="SHEET") subject.import_sheets() - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() assert props.sheets[0].ifc_definition_id == document.id() assert props.sheets[0].identification == "X" assert props.sheets[0].name == "FOOBAR" @@ -657,9 +665,10 @@ class TestSetName(NewFile): class TestShowDecorations(NewFile): def test_run(self): - bpy.context.scene.DocProperties.should_draw_decorations = False + props = tool.Drawing.get_document_props() + props.should_draw_decorations = False subject.show_decorations() - assert bpy.context.scene.DocProperties.should_draw_decorations is True + assert props.should_draw_decorations is True class TestDrawingMaintainingSheetPosition(NewFile): @@ -680,7 +689,7 @@ class TestDrawingMaintainingSheetPosition(NewFile): return drawing_data def test_run(self): - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() bpy.ops.bim.create_project() ifc = tool.Ifc.get() sheet_path = Path.cwd() / "layouts" / "A00 - UNTITLED.svg" @@ -845,10 +854,11 @@ class TestDrawingStyles(NewFile): ifc = tool.Ifc.get() drawing = ifc.by_type("IfcAnnotation")[0] bpy.ops.bim.expand_target_view(target_view="PLAN_VIEW") - props = bpy.context.scene.DocProperties + props = tool.Drawing.get_document_props() props.active_drawing_index = 2 bpy.ops.bim.activate_drawing(drawing=drawing.id()) - self.drawing_styles = bpy.context.scene.DocProperties.drawing_styles + props = tool.Drawing.get_document_props() + self.drawing_styles = props.drawing_styles def test_drawing_styles_not_loaded_if_underlay_is_inactive(self): self.setup_project_with_drawing() @@ -867,7 +877,8 @@ class TestDrawingStyles(NewFile): class TestAddReferenceImage(NewFile): def test_run(self): - bpy.context.scene.BIMProjectProperties.template_file = "0" + props = tool.Project.get_project_props() + props.template_file = "0" bpy.ops.bim.create_project() ifc_path = Path("test/files/temp/test.ifc").absolute() bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True) diff --git a/src/bonsai/test/tool/test_geometry.py b/src/bonsai/test/tool/test_geometry.py index f1ca935f30..63721bcdb7 100644 --- a/src/bonsai/test/tool/test_geometry.py +++ b/src/bonsai/test/tool/test_geometry.py @@ -171,14 +171,14 @@ class TestGetCartesianPointCoordinateOffset(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True obj.BIMObjectProperties.cartesian_point_offset = "1,2,3" assert np.allclose(subject.get_cartesian_point_offset(obj), np.array((1.0, 2.0, 3.0))) def test_get_null_if_not_a_cartesian_point_offset_type(self): obj = bpy.data.objects.new("Object", None) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True obj.BIMObjectProperties.cartesian_point_offset = "1,2,3" assert subject.get_cartesian_point_offset(obj) is None @@ -186,7 +186,7 @@ class TestGetCartesianPointCoordinateOffset(NewFile): def test_get_null_if_no_blender_offset(self): obj = bpy.data.objects.new("Object", None) obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = False assert subject.get_cartesian_point_offset(obj) is None @@ -237,14 +237,15 @@ class TestImportRepresentationParameters(NewFile): item = ifc.createIfcExtrudedAreaSolid(SweptArea=swept_area, Depth=2) representation = ifc.createIfcShapeRepresentation(Items=[item]) data = bpy.data.meshes.new("Mesh") - data.BIMMeshProperties.ifc_definition_id = representation.id() + mprops = tool.Geometry.get_mesh_props(data) + mprops.ifc_definition_id = representation.id() subject.import_representation_parameters(data) - assert data.BIMMeshProperties.ifc_parameters[0].name == "IfcExtrudedAreaSolid/Depth" - assert data.BIMMeshProperties.ifc_parameters[0].step_id == item.id() - assert data.BIMMeshProperties.ifc_parameters[0].index == 3 - assert data.BIMMeshProperties.ifc_parameters[1].name == "IfcCircleProfileDef/Radius" - assert data.BIMMeshProperties.ifc_parameters[1].step_id == swept_area.id() - assert data.BIMMeshProperties.ifc_parameters[1].index == 3 + assert mprops.ifc_parameters[0].name == "IfcExtrudedAreaSolid/Depth" + assert mprops.ifc_parameters[0].step_id == item.id() + assert mprops.ifc_parameters[0].index == 3 + assert mprops.ifc_parameters[1].name == "IfcCircleProfileDef/Radius" + assert mprops.ifc_parameters[1].step_id == swept_area.id() + assert mprops.ifc_parameters[1].index == 3 class TestIsBodyRepresentation(NewFile): @@ -293,7 +294,7 @@ class TestLink(NewFile): element = ifc.createIfcShapeRepresentation() obj = bpy.data.meshes.new("Mesh") subject.link(element, obj) - assert obj.BIMMeshProperties.ifc_definition_id == element.id() + assert tool.Geometry.get_mesh_props(obj).ifc_definition_id == element.id() class TestRecordObjectMaterials(NewFile): @@ -306,7 +307,7 @@ class TestRecordObjectMaterials(NewFile): material.BIMStyleProperties.ifc_definition_id = style.id() obj.data.materials.append(material) subject.record_object_materials(obj) - assert obj.data.BIMMeshProperties.material_checksum == str([style.id()]) + assert tool.Geometry.get_mesh_props(obj).material_checksum == str([style.id()]) class TestRecordObjectPosition(NewFile): diff --git a/src/bonsai/test/tool/test_georeference.py b/src/bonsai/test/tool/test_georeference.py index 4b22a84781..96e498482c 100644 --- a/src/bonsai/test/tool/test_georeference.py +++ b/src/bonsai/test/tool/test_georeference.py @@ -39,7 +39,7 @@ class TestImportProjectedCRS(NewFile): ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") ifcopenshell.api.run("context.add_context", ifc, context_type="Model") subject.import_projected_crs() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert len(props.projected_crs) == 0 def test_importing_projected_crs(self): @@ -58,7 +58,7 @@ class TestImportProjectedCRS(NewFile): unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT") projected_crs.MapUnit = unit subject.import_projected_crs() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert props.projected_crs.get("Name").string_value == "Name" assert props.projected_crs.get("Description").string_value == "Description" assert props.projected_crs.get("GeodeticDatum").string_value == "GeodeticDatum" @@ -71,7 +71,7 @@ class TestImportProjectedCRS(NewFile): ifc = ifcopenshell.file(schema="IFC2X3") tool.Ifc.set(ifc) subject.import_projected_crs() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert len(props.projected_crs) == 0 @@ -82,7 +82,7 @@ class TestImportCoordinateOperation(NewFile): ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") ifcopenshell.api.run("context.add_context", ifc, context_type="Model") subject.import_coordinate_operation() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert len(props.coordinate_operation) == 0 def test_importing_coordinate_operation(self): @@ -99,7 +99,7 @@ class TestImportCoordinateOperation(NewFile): map_conversion.XAxisOrdinate = 5 map_conversion.Scale = 6 subject.import_coordinate_operation() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert props.coordinate_operation.get("Eastings").string_value == "1.0" assert props.coordinate_operation.get("Northings").string_value == "2.0" assert props.coordinate_operation.get("OrthogonalHeight").string_value == "3.0" @@ -112,7 +112,7 @@ class TestImportCoordinateOperation(NewFile): ifc = ifcopenshell.file(schema="IFC2X3") tool.Ifc.set(ifc) subject.import_coordinate_operation() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert len(props.coordinate_operation) == 0 @@ -123,7 +123,7 @@ class TestImportTrueNorth(NewFile): ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") ifcopenshell.api.run("context.add_context", ifc, context_type="Model") subject.import_true_north() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert props.true_north_abscissa == "0" assert props.true_north_ordinate == "1" assert props.true_north_angle == "0" @@ -135,7 +135,7 @@ class TestImportTrueNorth(NewFile): context = ifcopenshell.api.run("context.add_context", ifc, context_type="Model") context.TrueNorth = ifc.createIfcDirection((1.0, 2.0, 0.0)) subject.import_true_north() - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() assert props.true_north_abscissa == "1.0" assert props.true_north_ordinate == "2.0" assert props.true_north_angle == "-26.5650512" @@ -176,35 +176,39 @@ class TestGetTrueNorthAttributes(NewFile): class TestEnableEditing(NewFile): def test_run(self): - bpy.context.scene.BIMGeoreferenceProperties.is_editing = False + props = tool.Georeference.get_georeference_props() + props.is_editing = False subject.enable_editing() - assert bpy.context.scene.BIMGeoreferenceProperties.is_editing is True + assert props.is_editing is True class TestDisableEditing(NewFile): def test_run(self): - bpy.context.scene.BIMGeoreferenceProperties.is_editing = True + props = tool.Georeference.get_georeference_props() + props.is_editing = True subject.disable_editing() - assert bpy.context.scene.BIMGeoreferenceProperties.is_editing is False + assert props.is_editing is False class TestSetCoordinates(NewFile): def test_run(self): + props = tool.Georeference.get_georeference_props() subject.set_coordinates("local", [1.0, 2.0, 3.0]) - assert bpy.context.scene.BIMGeoreferenceProperties.local_coordinates == "1.0,2.0,3.0" + assert props.local_coordinates == "1.0,2.0,3.0" subject.set_coordinates("blender", [4.0, 5.0, 6.0]) - assert bpy.context.scene.BIMGeoreferenceProperties.blender_coordinates == "4.0,5.0,6.0" + assert props.blender_coordinates == "4.0,5.0,6.0" subject.set_coordinates("map", [7.0, 8.0, 9.0]) - assert bpy.context.scene.BIMGeoreferenceProperties.map_coordinates == "7.0,8.0,9.0" + assert props.map_coordinates == "7.0,8.0,9.0" class TestGetCoordinates(NewFile): def test_run(self): - bpy.context.scene.BIMGeoreferenceProperties.local_coordinates = "1.0,2.0,3.0" + props = tool.Georeference.get_georeference_props() + props.local_coordinates = "1.0,2.0,3.0" assert subject.get_coordinates("local") == [1.0, 2.0, 3.0] - bpy.context.scene.BIMGeoreferenceProperties.blender_coordinates = "4.0,5.0,6.0" + props.blender_coordinates = "4.0,5.0,6.0" assert subject.get_coordinates("blender") == [4.0, 5.0, 6.0] - bpy.context.scene.BIMGeoreferenceProperties.map_coordinates = "7.0,8.0,9.0" + props.map_coordinates = "7.0,8.0,9.0" assert subject.get_coordinates("map") == [7.0, 8.0, 9.0] @@ -231,7 +235,7 @@ class TestXyz2Enh(NewFile): ifc = ifcopenshell.file() ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") tool.Ifc.set(ifc) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True props.blender_offset_x = "1.0" assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0) @@ -247,7 +251,7 @@ class TestXyz2Enh(NewFile): assert subject.xyz2enh([0.0, 0.0, 0.0]) == (1.0, 0.0, 0.0) def test_applying_both_blender_offset_and_map_conversion(self): - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True props.blender_offset_x = "1.0" ifc = ifcopenshell.file() @@ -271,7 +275,7 @@ class TestEnh2Xyz(NewFile): ifc = ifcopenshell.file() ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcProject") tool.Ifc.set(ifc) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True props.blender_offset_x = "1.0" assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0) @@ -287,7 +291,7 @@ class TestEnh2Xyz(NewFile): assert subject.enh2xyz([0.0, 0.0, 0.0]) == (-1.0, 0.0, 0.0) def test_applying_both_blender_offset_and_map_conversion(self): - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True props.blender_offset_x = "1.0" ifc = ifcopenshell.file() diff --git a/src/bonsai/test/tool/test_ifc.py b/src/bonsai/test/tool/test_ifc.py index 429dd76212..641a8adfa0 100644 --- a/src/bonsai/test/tool/test_ifc.py +++ b/src/bonsai/test/tool/test_ifc.py @@ -189,7 +189,7 @@ class TestLink(test.bim.bootstrap.NewFile): element = ifc.create_entity("IfcShapeRepresentation") obj = bpy.data.meshes.new("Material") subject.link(element, obj) - assert obj.BIMMeshProperties.ifc_definition_id == element.id() + assert tool.Geometry.get_mesh_props(obj).ifc_definition_id == element.id() class TestUnlink(test.bim.bootstrap.NewFile): diff --git a/src/bonsai/test/tool/test_loader.py b/src/bonsai/test/tool/test_loader.py index c8235ed52b..30ef9e22b9 100644 --- a/src/bonsai/test/tool/test_loader.py +++ b/src/bonsai/test/tool/test_loader.py @@ -520,7 +520,8 @@ class TestLoadingIndexedMap(NewFile): class TestSetupActiveBsddClassification(NewFile): def run_test(self, schema: ifcopenshell.util.schema.IFC_SCHEMA) -> None: schema_ = "IFC4X3_ADD2" if schema == "IFC4X3" else schema - bpy.context.scene.BIMProjectProperties.export_schema = schema_ + props = tool.Project.get_project_props() + props.export_schema = schema_ bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() name = "CCI Construction" diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index 13a847f94a..a77d90ab92 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -378,7 +378,7 @@ class TestGenerateStair2DProfile(NewFile): class TestUsingArrays(NewFile): def setup_array(self, add_second_layer=False, sync_children=False): - bpy.context.scene.BIMProjectProperties.template_file = "0" + tool.Project.get_project_props().template_file = "0" bpy.ops.bim.create_project() bpy.ops.mesh.primitive_cube_add() @@ -467,7 +467,8 @@ class TestApplyIfcMaterialChanges(NewFile): return mesh def setup_test(self, and_elements: bool = True) -> None: - bpy.context.scene.BIMProjectProperties.template_file = "0" + props = tool.Project.get_project_props() + props.template_file = "0" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py index 4af7494a85..a5f94dbe4b 100644 --- a/src/bonsai/test/tool/test_project.py +++ b/src/bonsai/test/tool/test_project.py @@ -244,25 +244,25 @@ class TestLoadProject(NewFile): class TestLoadLinkedModels(NewFile): def test_load_linked_models_no_document(self): - links = bpy.context.scene.BIMProjectProperties.links + props = tool.Project.get_project_props() ifc = ifcopenshell.file() tool.Ifc.set(ifc) subject.load_linked_models_from_ifc() - assert len(links) == 0 + assert len(props.links) == 0 def test_load_linked_models_document_no_references(self): ifc = ifcopenshell.file() - links = bpy.context.scene.BIMProjectProperties.links + props = tool.Project.get_project_props() ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) document.Name = "BBIM_Linked_Models" tool.Ifc.set(ifc) subject.load_linked_models_from_ifc() - assert len(links) == 0 + assert len(props.links) == 0 def test_load_linked_models_document_with_references(self): ifc = ifcopenshell.file() - links = bpy.context.scene.BIMProjectProperties.links + props = tool.Project.get_project_props() ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) document.Name = "BBIM_Linked_Models" @@ -271,8 +271,8 @@ class TestLoadLinkedModels(NewFile): reference.Location = linked_model_path tool.Ifc.set(ifc) subject.load_linked_models_from_ifc() - assert len(links) == 1 - assert links[0].name == linked_model_path + assert len(props.links) == 1 + assert props.links[0].name == linked_model_path class TestSaveLinkedModelsToIfc(NewFile): @@ -286,8 +286,8 @@ class TestSaveLinkedModelsToIfc(NewFile): def test_save_linked_models_to_ifc_paths_to_add(self): ifc = ifcopenshell.file() ifcopenshell.api.root.create_entity(ifc, "IfcProject") - links = bpy.context.scene.BIMProjectProperties.links - link = links.add() + props = tool.Project.get_project_props() + link = props.links.add() linked_model_path = "test.ifc" link.name = linked_model_path tool.Ifc.set(ifc) @@ -299,7 +299,7 @@ class TestSaveLinkedModelsToIfc(NewFile): def test_save_linked_models_to_ifc_already_created_references(self): ifc = ifcopenshell.file() - links = bpy.context.scene.BIMProjectProperties.links + links = tool.Project.get_project_props().links ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) @@ -326,7 +326,7 @@ class TestSaveLinkedModelsToIfc(NewFile): def test_save_linked_models_to_ifc_references_to_remove(self): ifc = ifcopenshell.file() - links = bpy.context.scene.BIMProjectProperties.links + links = tool.Project.get_project_props().links ifcopenshell.api.root.create_entity(ifc, "IfcProject") document = ifcopenshell.api.document.add_information(ifc) diff --git a/src/bonsai/test/tool/test_root.py b/src/bonsai/test/tool/test_root.py index 19cba02029..e87eb312a7 100644 --- a/src/bonsai/test/tool/test_root.py +++ b/src/bonsai/test/tool/test_root.py @@ -120,8 +120,8 @@ class TestGetObjectRepresentation(NewFile): ifc = ifcopenshell.file() tool.Ifc.set(ifc) representation = ifc.createIfcShapeRepresentation() - obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) - obj.data.BIMMeshProperties.ifc_definition_id = representation.id() + obj = bpy.data.objects.new("Object", (mesh := bpy.data.meshes.new("Mesh"))) + tool.Geometry.get_mesh_props(mesh).ifc_definition_id = representation.id() assert subject.get_object_representation(obj) == representation @@ -175,7 +175,7 @@ class TestSetObjectName(NewFile): class TestReassignClass(NewFile): def test_reassigning_multiple_occurrences_of_the_same_type(self): - bpy.context.scene.BIMProjectProperties.template_file = "IFC4 Demo Template.ifc" + tool.Project.get_project_props().template_file = "IFC4 Demo Template.ifc" bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() context = bpy.context diff --git a/src/bonsai/test/tool/test_surveyor.py b/src/bonsai/test/tool/test_surveyor.py index 0a12bd3903..bfba0cb480 100644 --- a/src/bonsai/test/tool/test_surveyor.py +++ b/src/bonsai/test/tool/test_surveyor.py @@ -20,6 +20,7 @@ import bpy import numpy as np import ifcopenshell import ifcopenshell.api +import ifcopenshell.util.geolocation import test.bim.bootstrap import bonsai.core.tool @@ -34,7 +35,7 @@ class TestImplementsTool(test.bim.bootstrap.NewFile): class TestGetGlobalMatrix(test.bim.bootstrap.NewFile): def test_getting_an_absolute_matrix_if_no_blender_offset(self): - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = False obj = bpy.data.objects.new("Object", None) assert (subject.get_absolute_matrix(obj) == np.array(obj.matrix_world)).all() @@ -45,7 +46,7 @@ class TestGetGlobalMatrix(test.bim.bootstrap.NewFile): unit = ifcopenshell.api.run("unit.add_si_unit", ifc, unit_type="LENGTHUNIT", prefix="MILLI") ifcopenshell.api.run("unit.assign_unit", ifc, units=[unit]) tool.Ifc.set(ifc) - props = bpy.context.scene.BIMGeoreferenceProperties + props = tool.Georeference.get_georeference_props() props.has_blender_offset = True props.blender_offset_x = "1000" props.blender_offset_y = "2000" diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py index 6903f41bfe..e62b105c50 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/copy_cost_item_values.py @@ -30,11 +30,8 @@ def copy_cost_item_values( parametrically linked, so if one value changes, the other will not. :param source: The IfcCostItem to copy cost values from - :type source: ifcopenshell.entity_instance :param destination: The IfcCostItem to copy cost values from - :type destination: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -53,11 +50,9 @@ def copy_cost_item_values( # Let's copy the value from one item to another ifcopenshell.api.cost.copy_cost_item_values(model, source=item1, destination=item2) """ - settings = {"source": source, "destination": destination} - - for cost_value in settings["destination"].CostValues or []: + for cost_value in destination.CostValues or []: ifcopenshell.api.cost.remove_cost_item_value(file, cost_value=cost_value) copied_cost_values = [] - for cost_value in settings["source"].CostValues or []: + for cost_value in source.CostValues or []: copied_cost_values.append(ifcopenshell.util.element.copy_deep(file, cost_value)) - settings["destination"].CostValues = copied_cost_values + destination.CostValues = copied_cost_values diff --git a/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py b/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py index fa57e526cc..57117b6625 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py +++ b/src/ifcopenshell-python/ifcopenshell/api/layer/assign_layer.py @@ -62,15 +62,10 @@ def assign_layer( # only one item) to the layer. ifcopenshell.api.layer.assign_layer(model, items=[representation.Items[0]], layer=layer) """ - settings = { - "items": items, - "layer": layer, - } - # support AssignedItems == None since layer might just got created - layer = settings["layer"] + assigned_items: set[ifcopenshell.entity_instance] assigned_items = set(layer.AssignedItems or []) - items = set(settings["items"]) - if items.issubset(assigned_items): + items_set = set(items) + if items_set.issubset(assigned_items): return - layer.AssignedItems = list(assigned_items | items) + layer.AssignedItems = list(assigned_items | items_set) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index 4a659f251c..8a583d64b4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -91,8 +91,7 @@ def edit_profile_usage( usecase = Usecase() usecase.file = file - usecase.settings = {"usage": usage, "attributes": attributes} - return usecase.execute() + return usecase.execute(usage, attributes) class Usecase: diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py index 493f4077f4..b5dedfff49 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/settings.py @@ -30,17 +30,15 @@ def get_application(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instanc IfcApplication. See ifcopenshell.api.owner.create_owner_history for details. :param ifc: The IFC file object that is being edited. - :type ifc: ifcopenshell.file :return: The IfcApplication with metadata of the authoring software. - :rtype: ifcopenshell.entity_instance """ - app = ifc.by_type("IfcApplication") + app = next(iter(ifc.by_type("IfcApplication")), None) if not app and ifc.schema == "IFC2X3": raise Exception( "Please create an application to continue. See the owner.create_owner_history docs for more info." "https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" ) - return (app or [None])[0] + return app def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]: @@ -50,17 +48,15 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None IfcApplication. See ifcopenshell.api.owner.create_owner_history for details. :param ifc: The IFC file object that is being edited. - :type ifc: ifcopenshell.file :return: The IfcPersonAndOrganization with metadata of the authoring user. - :rtype: ifcopenshell.entity_instance """ - pao = ifc.by_type("IfcPersonAndOrganization") + pao = next(iter(ifc.by_type("IfcPersonAndOrganization")), None) if not pao and ifc.schema == "IFC2X3": raise Exception( "Please create a user to continue. See the owner.create_owner_history docs for more info." "https://docs.ifcopenshell.org/autoapi/ifcopenshell/api/owner/create_owner_history/index.html" ) - return (pao or [None])[0] + return pao get_application_factory = get_application diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py index 8a989ecb55..65cbd5a58a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/cascade_schedule.py @@ -19,6 +19,8 @@ import datetime import ifcopenshell.util.date import ifcopenshell.util.sequence +from ifcopenshell.util.sequence import DURATION_TYPE +from typing import Union, Optional def cascade_schedule(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> None: @@ -41,9 +43,7 @@ def cascade_schedule(file: ifcopenshell.file, task: ifcopenshell.entity_instance be equivalent to be Tuesday 8am, for instance. :param task: The start task to begin cascading from. - :type task: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -103,16 +103,22 @@ def cascade_schedule(file: ifcopenshell.file, task: ifcopenshell.entity_instance """ usecase = Usecase() usecase.file = file - usecase.settings = {"task": task} - return usecase.execute() + return usecase.execute(task) class Usecase: - def execute(self): - self.calendar_cache = {} - self.cascade_task(self.settings["task"], is_first_task=True) + file: ifcopenshell.file - def cascade_task(self, task, is_first_task=False, task_sequence=None): + def execute(self, task: ifcopenshell.entity_instance): + self.calendar_cache = {} + self.cascade_task(task, is_first_task=True) + + def cascade_task( + self, + task: ifcopenshell.entity_instance, + is_first_task: bool = False, + task_sequence: Optional[list[ifcopenshell.entity_instance]] = None, + ) -> None: if task_sequence is None: task_sequence = [] @@ -316,18 +322,22 @@ class Usecase: for nested_task in rel.RelatedObjects or [] ] - def get_lag_time_days(self, lag_time): + def get_lag_time_days(self, lag_time: ifcopenshell.entity_instance) -> int: return ifcopenshell.util.date.ifc2datetime(lag_time.LagValue.wrappedValue).days - def get_calendar(self, task): + def get_calendar(self, task: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: if task.id() not in self.calendar_cache: self.calendar_cache[task.id()] = ifcopenshell.util.sequence.derive_calendar(task) return self.calendar_cache[task.id()] - def offset_date(self, date, days, duration_type, calendar): + def offset_date( + self, date: datetime.datetime, days: int, duration_type: DURATION_TYPE, calendar: ifcopenshell.entity_instance + ) -> datetime.datetime: return ifcopenshell.util.sequence.offset_date(date, datetime.timedelta(days=days), duration_type, calendar) - def get_task_time_attribute(self, task, attribute): + def get_task_time_attribute( + self, task: ifcopenshell.entity_instance, attribute: str + ) -> Union[datetime.datetime, None]: if task.TaskTime: value = getattr(task.TaskTime, attribute) if value: diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py index a6637c7a88..ab1035bbab 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/create_baseline.py @@ -75,7 +75,9 @@ class Usecase: baseline_work_schedule.Name = name self.create_baseline_reference(work_schedule, baseline_work_schedule) for summary_task in ifcopenshell.util.sequence.get_root_tasks(work_schedule): - current, duplicate = ifcopenshell.api.sequence.duplicate_task(self.file, task=summary_task) + res = ifcopenshell.api.sequence.duplicate_task(self.file, task=summary_task) + assert isinstance(res, list) + current, duplicate = res ifcopenshell.api.control.assign_control( self.file, relating_control=baseline_work_schedule, related_object=duplicate[0] ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py index 0d569c5f56..e9fcc0d660 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py @@ -16,42 +16,33 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import ifcopenshell +from ifcopenshell.util.shape_builder import VectorType, ifc_safe_vector_type def edit_structural_connection_cs( file: ifcopenshell.file, structural_item: ifcopenshell.entity_instance, - axis: tuple[float, float, float] = (0.0, 0.0, 1.0), - ref_direction: tuple[float, float, float] = (1.0, 0.0, 0.0), + axis: VectorType = (0.0, 0.0, 1.0), + ref_direction: VectorType = (1.0, 0.0, 0.0), ) -> None: """Edits the coordinate system of a structural connection :param structural_item: The IfcStructuralItem you want to modify. - :type structural_item: ifcopenshell.entity_instance :param axis: The unit Z axis vector defined as a list of 3 floats. Defaults to (0., 0., 1.). - :type axis: tuple[float, float, float] :param ref_direction: The unit X axis vector defined as a list of 3 floats. Defaults to (1., 0., 0.). - :type ref_direction: tuple[float, float, float] :return: None - :rtype: None """ - settings = { - "structural_item": structural_item, - "axis": axis, - "ref_direction": ref_direction, - } - - if settings["structural_item"].ConditionCoordinateSystem is None: + if structural_item.ConditionCoordinateSystem is None: point = file.createIfcCartesianPoint((0.0, 0.0, 0.0)) ccs = file.createIfcAxis2Placement3D(point, None, None) - settings["structural_item"].ConditionCoordinateSystem = ccs + structural_item.ConditionCoordinateSystem = ccs - ccs = settings["structural_item"].ConditionCoordinateSystem + ccs = structural_item.ConditionCoordinateSystem if ccs.Axis and len(file.get_inverse(ccs.Axis)) == 1: file.remove(ccs.Axis) - ccs.Axis = file.createIfcDirection(settings["axis"]) - if ccs.RefDirection and len(file.get_inverse(ccs.RefDirection)) == 1: - file.remove(ccs.RefDirection) - ccs.RefDirection = file.createIfcDirection(settings["ref_direction"]) + ccs.Axis = file.create_entity("IfcDirection", ifc_safe_vector_type(axis)) + if (prev_ref_direction := ccs.RefDirection) and len(file.get_inverse(prev_ref_direction)) == 1: + file.remove(prev_ref_direction) + ccs.RefDirection = file.create_entity("IfcDirection", ifc_safe_vector_type(ref_direction)) diff --git a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py index 07184d1cb2..6a52abd742 100644 --- a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py +++ b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py @@ -84,7 +84,8 @@ class Patcher: import bonsai.tool as tool from math import degrees - bpy.context.scene.BIMProjectProperties.should_use_native_meshes = True + props = tool.Project.get_project_props() + props.should_use_native_meshes = True bpy.ops.bim.load_project(filepath=self.filepath) old_history_size = tool.Ifc.get().history_size From 43cc7a0304e4b813f7465b958502f183e5a9b0f1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Feb 2025 12:15:39 +0500 Subject: [PATCH 070/476] Replace direct access to IfcStore with tool.Ifc --- src/bonsai/bonsai/bim/handler.py | 8 +-- src/bonsai/bonsai/bim/helper.py | 4 +- src/bonsai/bonsai/bim/import_ifc.py | 2 +- .../bonsai/bim/module/aggregate/operator.py | 1 - src/bonsai/bonsai/bim/module/aggregate/ui.py | 6 +- .../bonsai/bim/module/attribute/operator.py | 3 +- .../bonsai/bim/module/attribute/prop.py | 1 - src/bonsai/bonsai/bim/module/attribute/ui.py | 1 - src/bonsai/bonsai/bim/module/bcf/operator.py | 9 ++- src/bonsai/bonsai/bim/module/boundary/ui.py | 9 ++- src/bonsai/bonsai/bim/module/bsdd/ui.py | 1 - .../bonsai/bim/module/clash/operator.py | 4 +- .../bim/module/classification/operator.py | 2 +- .../bonsai/bim/module/classification/ui.py | 3 +- .../bonsai/bim/module/constraint/operator.py | 5 +- src/bonsai/bonsai/bim/module/constraint/ui.py | 8 +-- src/bonsai/bonsai/bim/module/cost/prop.py | 5 +- src/bonsai/bonsai/bim/module/cost/ui.py | 3 +- .../bonsai/bim/module/covetool/operator.py | 4 +- src/bonsai/bonsai/bim/module/csv/operator.py | 5 +- src/bonsai/bonsai/bim/module/csv/ui.py | 6 +- .../bonsai/bim/module/debug/operator.py | 14 ++--- src/bonsai/bonsai/bim/module/diff/ui.py | 1 - .../bonsai/bim/module/document/operator.py | 1 - src/bonsai/bonsai/bim/module/document/ui.py | 8 +-- .../bonsai/bim/module/drawing/operator.py | 2 +- src/bonsai/bonsai/bim/module/fm/ui.py | 5 +- .../bonsai/bim/module/geometry/operator.py | 8 +-- src/bonsai/bonsai/bim/module/geometry/ui.py | 5 +- .../bonsai/bim/module/group/operator.py | 4 +- src/bonsai/bonsai/bim/module/group/ui.py | 6 +- .../bonsai/bim/module/layer/operator.py | 7 +-- src/bonsai/bonsai/bim/module/layer/ui.py | 4 +- .../bonsai/bim/module/material/operator.py | 19 +++--- src/bonsai/bonsai/bim/module/material/prop.py | 1 - src/bonsai/bonsai/bim/module/material/ui.py | 9 ++- src/bonsai/bonsai/bim/module/model/handler.py | 1 - src/bonsai/bonsai/bim/module/model/opening.py | 1 - .../bonsai/bim/module/model/polyline.py | 1 - src/bonsai/bonsai/bim/module/model/product.py | 2 +- src/bonsai/bonsai/bim/module/model/slab.py | 2 +- src/bonsai/bonsai/bim/module/model/wall.py | 4 +- src/bonsai/bonsai/bim/module/nest/operator.py | 5 +- src/bonsai/bonsai/bim/module/nest/ui.py | 6 +- src/bonsai/bonsai/bim/module/profile/prop.py | 1 - .../bonsai/bim/module/project/operator.py | 34 +++++----- src/bonsai/bonsai/bim/module/project/prop.py | 2 +- src/bonsai/bonsai/bim/module/project/ui.py | 10 +-- src/bonsai/bonsai/bim/module/pset/operator.py | 10 +-- src/bonsai/bonsai/bim/module/pset/prop.py | 10 +-- src/bonsai/bonsai/bim/module/pset/ui.py | 5 +- .../bonsai/bim/module/pset_template/prop.py | 3 +- src/bonsai/bonsai/bim/module/qto/operator.py | 1 - src/bonsai/bonsai/bim/module/resource/prop.py | 1 - src/bonsai/bonsai/bim/module/resource/ui.py | 4 +- src/bonsai/bonsai/bim/module/root/operator.py | 3 +- src/bonsai/bonsai/bim/module/root/ui.py | 4 +- .../bonsai/bim/module/sequence/operator.py | 13 ++-- src/bonsai/bonsai/bim/module/sequence/prop.py | 1 - src/bonsai/bonsai/bim/module/sequence/ui.py | 11 ++-- .../module/structural/load_decoration_data.py | 9 ++- .../bonsai/bim/module/structural/operator.py | 63 ++++++++++--------- .../bonsai/bim/module/structural/prop.py | 1 - src/bonsai/bonsai/bim/module/structural/ui.py | 28 ++++----- src/bonsai/bonsai/bim/module/style/ui.py | 4 +- .../bonsai/bim/module/system/operator.py | 2 - src/bonsai/bonsai/bim/module/type/operator.py | 7 +-- src/bonsai/bonsai/bim/module/type/prop.py | 2 - src/bonsai/bonsai/bim/module/type/ui.py | 1 - src/bonsai/bonsai/bim/module/unit/prop.py | 1 - src/bonsai/bonsai/bim/module/unit/ui.py | 4 +- src/bonsai/bonsai/bim/module/void/operator.py | 5 +- src/bonsai/bonsai/tool/ifc.py | 6 +- src/bonsai/bonsai/tool/structural.py | 5 +- .../docs/guides/development/undo_system.rst | 2 +- src/bonsai/scripts/headless_import.py | 1 - src/bonsai/test/bim/bootstrap.py | 18 +++--- src/bonsai/test/bim/test_feature.py | 14 ++--- src/bonsai/test/tool/test_drawing.py | 1 - src/bonsai/test/tool/test_misc.py | 1 - .../recipes/FixArchiCADToRevitSpaces.py | 5 +- 81 files changed, 228 insertions(+), 271 deletions(-) diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index 572cfaf117..287e0bb09d 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -60,7 +60,7 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) - if props.is_renaming: props.is_renmaing = False return - IfcStore.get_file().by_id(ifc_definition_id).Name = obj.name + tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name refresh_ui_data() return @@ -71,7 +71,7 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) - obj.BIMObjectProperties.is_renaming = False return - element = IfcStore.get_file().by_id(obj.BIMObjectProperties.ifc_definition_id) + element = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id) if "/" in obj.name: object_name = obj.name element_name = obj.name.split("/", 1)[1] @@ -231,9 +231,9 @@ def refresh_ui_data(): def loadIfcStore(scene): IfcStore.purge() refresh_ui_data() - if not IfcStore.get_file(): + if not tool.Ifc.get(): return - IfcStore.get_schema() + tool.Ifc.schema() IfcStore.relink_all_objects() diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index a8b25bca1a..2d19fa6e1f 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -27,7 +27,6 @@ import ifcopenshell.util.element import ifcopenshell.util.unit from ifcopenshell.util.doc import get_attribute_doc, get_predefined_type_doc, get_property_doc import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING if TYPE_CHECKING: @@ -119,7 +118,8 @@ def import_attributes( data: dict[str, Any], callback: Optional[ImportCallback] = None, ) -> None: - for attribute in IfcStore.get_schema().declaration_by_name(ifc_class).all_attributes(): + schema = tool.Ifc.schema() + for attribute in schema.declaration_by_name(ifc_class).all_attributes(): import_attribute(attribute, props, data, callback=callback) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 13fce33a93..5dd519de28 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -881,7 +881,7 @@ class IfcImporter: self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file) if not bpy.context.scene.BIMProperties.ifc_file: bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() def calculate_unit_scale(self): self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py index c2e0cb16c2..c324037b89 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/operator.py +++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py @@ -23,7 +23,6 @@ import ifcopenshell.util.element import bonsai.tool as tool import bonsai.core.aggregate as core import bonsai.core.spatial -from bonsai.bim.ifc import IfcStore class BIM_OT_aggregate_assign_object(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/aggregate/ui.py b/src/bonsai/bonsai/bim/module/aggregate/ui.py index 7b9503f530..74389c723d 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/ui.py +++ b/src/bonsai/bonsai/bim/module/aggregate/ui.py @@ -39,7 +39,7 @@ class BIM_PT_aggregate(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): return False @@ -120,9 +120,9 @@ class BIM_PT_linked_aggregate(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): return False return True diff --git a/src/bonsai/bonsai/bim/module/attribute/operator.py b/src/bonsai/bonsai/bim/module/attribute/operator.py index 3282c66ec5..2b279895e3 100644 --- a/src/bonsai/bonsai/bim/module/attribute/operator.py +++ b/src/bonsai/bonsai/bim/module/attribute/operator.py @@ -26,7 +26,6 @@ import bonsai.bim.helper import bonsai.tool as tool import bonsai.core.attribute as core import bonsai.core.spatial -from bonsai.bim.ifc import IfcStore def get_objs_for_operation(operator_properties, context): @@ -117,7 +116,7 @@ class EditAttributes(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() obj = tool.Blender.get_active_object(is_selected=False) if not (element := tool.Ifc.get_entity(obj)): return diff --git a/src/bonsai/bonsai/bim/module/attribute/prop.py b/src/bonsai/bonsai/bim/module/attribute/prop.py index 207fd927fd..fd2dab7cd2 100644 --- a/src/bonsai/bonsai/bim/module/attribute/prop.py +++ b/src/bonsai/bonsai/bim/module/attribute/prop.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import bpy -from bonsai.bim.ifc import IfcStore from bonsai.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( diff --git a/src/bonsai/bonsai/bim/module/attribute/ui.py b/src/bonsai/bonsai/bim/module/attribute/ui.py index 6f0f5a5599..0fce2b07d4 100644 --- a/src/bonsai/bonsai/bim/module/attribute/ui.py +++ b/src/bonsai/bonsai/bim/module/attribute/ui.py @@ -18,7 +18,6 @@ import bonsai.bim.helper from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.attribute.data import AttributesData import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index aed65de52c..0c145d98f8 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -43,7 +43,6 @@ import bonsai.tool as tool import bonsai.bim.module.bcf.prop as bcf_prop import bonsai.bim.module.bcf.bcfstore as bcfstore from pathlib import Path -from bonsai.bim.ifc import IfcStore from math import radians, degrees, atan, tan, cos, sin from mathutils import Vector, Matrix, Euler, geometry from xsdata.models.datatype import XmlDateTime @@ -1206,7 +1205,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): return True def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() bcfxml = bcfstore.BcfStore.get_bcfxml() assert bcfxml @@ -1355,7 +1354,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): objs: list[bpy.types.Object] = [] for global_id in exception_global_ids: - obj = IfcStore.get_element(global_id) + obj = tool.Ifc.get_object_by_identifier(global_id) if obj and context.view_layer.objects.get(obj.name): assert isinstance(obj, bpy.types.Object) objs.append(obj) @@ -1414,7 +1413,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): return bpy.ops.object.select_all(action="DESELECT") for global_id in selected_global_ids: - obj = IfcStore.get_element(global_id) + obj = tool.Ifc.get_object_by_identifier(global_id) if obj: obj.select_set(True) obj.hide_set(False) @@ -1427,7 +1426,7 @@ class ActivateBcfViewpoint(bpy.types.Operator): for acomponent in acoloring.component: global_id_colours.setdefault(acomponent.ifc_guid, acoloring.color) for global_id, color in global_id_colours.items(): - obj = IfcStore.get_element(global_id) + obj = tool.Ifc.get_object_by_identifier(global_id) if obj: obj.color = self.hex_to_rgb(color) diff --git a/src/bonsai/bonsai/bim/module/boundary/ui.py b/src/bonsai/bonsai/bim/module/boundary/ui.py index e3c7d8936f..e7332d50c6 100644 --- a/src/bonsai/bonsai/bim/module/boundary/ui.py +++ b/src/bonsai/bonsai/bim/module/boundary/ui.py @@ -18,7 +18,6 @@ import bpy from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore import bonsai.tool as tool from bonsai.bim.module.boundary.data import SpaceBoundariesData @@ -34,7 +33,7 @@ class BIM_PT_SceneBoundaries(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): row = self.layout.row(align=True) @@ -58,9 +57,9 @@ class BIM_PT_Boundary(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - entity = IfcStore.get_file().by_id(props.ifc_definition_id) + entity = tool.Ifc.get().by_id(props.ifc_definition_id) return entity.is_a("IfcRelSpaceBoundary") def draw(self, context): @@ -134,7 +133,7 @@ class BIM_PT_SpaceBoundaries(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False element = tool.Ifc.get_entity(context.active_object) for ifc_class in ("IfcSpace", "IfcExternalSpatialElement"): diff --git a/src/bonsai/bonsai/bim/module/bsdd/ui.py b/src/bonsai/bonsai/bim/module/bsdd/ui.py index ac3e633d8a..046bc46fec 100644 --- a/src/bonsai/bonsai/bim/module/bsdd/ui.py +++ b/src/bonsai/bonsai/bim/module/bsdd/ui.py @@ -18,7 +18,6 @@ import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore class BIM_PT_bsdd(Panel): diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index 020aeba55e..d8eb640551 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -300,7 +300,7 @@ class SelectIfcClashResults(bpy.types.Operator): def execute(self, context): # TODO refactor into new clash results system - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.filepath = bpy.path.ensure_ext(self.filepath, ".json") with open(self.filepath) as f: clash_sets = json.load(f) @@ -478,7 +478,7 @@ class SelectSmartGroup(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group + return tool.Ifc.get() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group def execute(self, context): selected_smart_group = context.scene.BIMClashProperties.active_smart_group diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index acdffae946..fa5e914880 100644 --- a/src/bonsai/bonsai/bim/module/classification/operator.py +++ b/src/bonsai/bonsai/bim/module/classification/operator.py @@ -240,7 +240,7 @@ class RemoveClassification(bpy.types.Operator, tool.Ifc.Operator): classification: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "classification.remove_classification", tool.Ifc.get(), diff --git a/src/bonsai/bonsai/bim/module/classification/ui.py b/src/bonsai/bonsai/bim/module/classification/ui.py index 87257fbaaa..c171ff3cd6 100644 --- a/src/bonsai/bonsai/bim/module/classification/ui.py +++ b/src/bonsai/bonsai/bim/module/classification/ui.py @@ -21,7 +21,6 @@ import bonsai.bim.helper import bonsai.tool as tool import bonsai.bim.module.classification.prop as classification_prop from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.classification.data import ( ClassificationsData, ClassificationReferencesData, @@ -124,7 +123,7 @@ class ReferenceUI: self.sprops = context.scene.BIMClassificationProperties self.bprops = context.scene.BIMBSDDProperties self.props = context.scene.BIMClassificationReferenceProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.draw_add_ui(context) diff --git a/src/bonsai/bonsai/bim/module/constraint/operator.py b/src/bonsai/bonsai/bim/module/constraint/operator.py index deed4d6335..3a66b074c7 100644 --- a/src/bonsai/bonsai/bim/module/constraint/operator.py +++ b/src/bonsai/bonsai/bim/module/constraint/operator.py @@ -23,7 +23,6 @@ import ifcopenshell.api.constraint import ifcopenshell.util.attribute import bonsai.bim.helper import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore class LoadObjectives(bpy.types.Operator): @@ -84,7 +83,7 @@ class AddObjective(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - result = ifcopenshell.api.run("constraint.add_objective", IfcStore.get_file()) + result = ifcopenshell.api.run("constraint.add_objective", tool.Ifc.get()) bpy.ops.bim.load_objectives() bpy.ops.bim.enable_editing_constraint(constraint=result.id()) return {"FINISHED"} @@ -116,7 +115,7 @@ class RemoveConstraint(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMConstraintProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "constraint.remove_constraint", self.file, **{"constraint": self.file.by_id(self.constraint)} ) diff --git a/src/bonsai/bonsai/bim/module/constraint/ui.py b/src/bonsai/bonsai/bim/module/constraint/ui.py index f7d530a757..76f4c4a1bb 100644 --- a/src/bonsai/bonsai/bim/module/constraint/ui.py +++ b/src/bonsai/bonsai/bim/module/constraint/ui.py @@ -16,8 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.module.constraint.data import ConstraintsData, ObjectConstraintsData @@ -33,7 +33,7 @@ class BIM_PT_constraints(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not ConstraintsData.is_loaded: @@ -81,7 +81,7 @@ class BIM_PT_object_constraints(Panel): def poll(cls, context): if not context.active_object: return False - if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): return False return bool(context.active_object.BIMObjectProperties.ifc_definition_id) @@ -93,7 +93,7 @@ class BIM_PT_object_constraints(Panel): self.oprops = obj.BIMObjectProperties self.sprops = context.scene.BIMConstraintProperties self.props = obj.BIMObjectConstraintProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.draw_add_ui() diff --git a/src/bonsai/bonsai/bim/module/cost/prop.py b/src/bonsai/bonsai/bim/module/cost/prop.py index 092f429121..abe035fdb6 100644 --- a/src/bonsai/bonsai/bim/module/cost/prop.py +++ b/src/bonsai/bonsai/bim/module/cost/prop.py @@ -19,7 +19,6 @@ import bpy import ifcopenshell.api import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.classification.data import CostClassificationsData from bonsai.bim.module.cost.data import CostSchedulesData, CostItemRatesData, CostItemQuantitiesData from bonsai.bim.prop import StrProperty, Attribute @@ -83,7 +82,7 @@ def update_cost_item_identification(self, context): props = context.scene.BIMCostProperties if not props.is_cost_update_enabled or self.identification == "XXX": return - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "cost.edit_cost_item", self.file, @@ -98,7 +97,7 @@ def update_cost_item_name(self, context): props = context.scene.BIMCostProperties if not props.is_cost_update_enabled or self.name == "Unnamed": return - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "cost.edit_cost_item", self.file, diff --git a/src/bonsai/bonsai/bim/module/cost/ui.py b/src/bonsai/bonsai/bim/module/cost/ui.py index 67d14437ed..387998e9ac 100644 --- a/src/bonsai/bonsai/bim/module/cost/ui.py +++ b/src/bonsai/bonsai/bim/module/cost/ui.py @@ -21,7 +21,6 @@ import bonsai.bim.helper import bonsai.bim.module.cost.prop as CostProp import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.cost.data import CostSchedulesData from typing import Any @@ -37,7 +36,7 @@ class BIM_PT_cost_schedules(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/covetool/operator.py b/src/bonsai/bonsai/bim/module/covetool/operator.py index 0bafcafa4f..1d64dd958e 100644 --- a/src/bonsai/bonsai/bim/module/covetool/operator.py +++ b/src/bonsai/bonsai/bim/module/covetool/operator.py @@ -20,8 +20,8 @@ import bpy import json import ifcopenshell import ifcopenshell.util.element +import bonsai.tool as tool from math import degrees, atan2 -from bonsai.bim.ifc import IfcStore from .api import Api @@ -92,7 +92,7 @@ class RunAnalysis(bpy.types.Operator): bl_label = "Run Analysis" def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.inputs = { "floors": [], "walls": [], diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index 7d59bc5a92..cd9e784bf2 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -27,7 +27,6 @@ import ifcopenshell import ifcopenshell.util.selector import bonsai.tool as tool import bonsai.bim.module.drawing.scheduler as scheduler -from bonsai.bim.ifc import IfcStore from bonsai.bim.handler import refresh_ui_data from typing import TYPE_CHECKING from collections import Counter @@ -212,7 +211,7 @@ class ExportIfcCsv(bpy.types.Operator): props = context.scene.CsvProperties self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}") if props.should_load_from_memory: - ifc_file = IfcStore.get_file() + ifc_file = tool.Ifc.get() else: ifc_file = ifcopenshell.open(props.csv_ifc_file) results = ifcopenshell.util.selector.filter_elements( @@ -314,7 +313,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): props = context.scene.CsvProperties if props.should_load_from_memory: - ifc_file = IfcStore.get_file() + ifc_file = tool.Ifc.get() else: ifc_file = ifcopenshell.open(props.csv_ifc_file) ifc_csv = ifccsv.IfcCsv() diff --git a/src/bonsai/bonsai/bim/module/csv/ui.py b/src/bonsai/bonsai/bim/module/csv/ui.py index 0d4d6ef11e..6aa41177b0 100644 --- a/src/bonsai/bonsai/bim/module/csv/ui.py +++ b/src/bonsai/bonsai/bim/module/csv/ui.py @@ -17,8 +17,8 @@ # along with Bonsai. If not, see . import bonsai.bim.helper +import bonsai.tool as tool from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.search.data import SearchData @@ -37,7 +37,7 @@ class BIM_PT_ifccsv(Panel): scene = context.scene props = scene.CsvProperties - if IfcStore.get_file(): + if tool.Ifc.get(): row = layout.row(align=True) row.prop(props, "should_load_from_memory") row.operator("bim.import_csv_attributes", icon="IMPORT", text="") @@ -49,7 +49,7 @@ class BIM_PT_ifccsv(Panel): row.operator("bim.export_csv_attributes", icon="EXPORT", text="") row.prop(props, "should_show_settings", icon="PREFERENCES", text="") - if not IfcStore.get_file() or not props.should_load_from_memory: + if not tool.Ifc.get() or not props.should_load_from_memory: row = layout.row(align=True) row.prop(props, "csv_ifc_file") row.operator("bim.select_csv_ifc_file", icon="FILE_FOLDER", text="") diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index bb949b8fd0..68e3525a87 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -78,10 +78,10 @@ class PrintIfcFile(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def execute(self, context): - print(IfcStore.get_file().wrapped_data.to_string()) + print(tool.Ifc.get().wrapped_data.to_string()) return {"FINISHED"} @@ -184,10 +184,10 @@ class CreateAllShapes(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() elements = self.file.by_type("IfcElement") + self.file.by_type("IfcSpace") total = len(elements) @@ -328,10 +328,10 @@ class InspectFromStepId(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() debug_props = tool.Debug.get_debug_props() debug_props.active_step_id = self.step_id crumb = debug_props.step_id_breadcrumb.add() @@ -422,7 +422,7 @@ class PrintObjectPlacement(bpy.types.Operator): return self.execute(context) def execute(self, context): - placement = ifcopenshell.util.placement.get_local_placement(IfcStore.get_file().by_id(self.step_id)) + placement = ifcopenshell.util.placement.get_local_placement(tool.Ifc.get().by_id(self.step_id)) if self.create_empty_object: bpy.ops.object.empty_add(type="ARROWS") si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) diff --git a/src/bonsai/bonsai/bim/module/diff/ui.py b/src/bonsai/bonsai/bim/module/diff/ui.py index b147f884d1..34e66df0f5 100644 --- a/src/bonsai/bonsai/bim/module/diff/ui.py +++ b/src/bonsai/bonsai/bim/module/diff/ui.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.diff.data import DiffData import bonsai.bim.helper import bonsai.tool as tool diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index 70c3401fb0..13c494d092 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -24,7 +24,6 @@ import ifcopenshell.util.element import bonsai.bim.handler import bonsai.tool as tool import bonsai.core.document as core -from bonsai.bim.ifc import IfcStore class LoadProjectDocuments(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index d2cf2e7ab6..c2aff57ed4 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -16,8 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.module.document.data import DocumentData, ObjectDocumentData @@ -33,7 +33,7 @@ class BIM_PT_documents(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not DocumentData.is_loaded: @@ -92,7 +92,7 @@ class BIM_PT_object_documents(Panel): def poll(cls, context): if not context.active_object: return False - if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): return False return bool(context.active_object.BIMObjectProperties.ifc_definition_id) @@ -103,7 +103,7 @@ class BIM_PT_object_documents(Panel): obj = context.active_object self.oprops = obj.BIMObjectProperties self.props = context.scene.BIMDocumentProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.draw_add_ui() diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 6bac483f21..3d1c3f46ec 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -261,7 +261,7 @@ class CreateDrawing(bpy.types.Operator): self.camera = context.scene.camera self.camera_element = tool.Ifc.get_entity(self.camera) self.camera_document = tool.Drawing.get_drawing_document(self.camera_element) - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() with profile("Drawing generation process"): with profile("Initialize drawing generation process"): diff --git a/src/bonsai/bonsai/bim/module/fm/ui.py b/src/bonsai/bonsai/bim/module/fm/ui.py index 1e76ec61c3..89cd764adf 100644 --- a/src/bonsai/bonsai/bim/module/fm/ui.py +++ b/src/bonsai/bonsai/bim/module/fm/ui.py @@ -18,7 +18,6 @@ import bonsai.tool as tool from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.fm.data import FMData @@ -40,11 +39,11 @@ class BIM_PT_fm(Panel): scene = context.scene props = scene.BIMFMProperties - if IfcStore.get_file(): + if tool.Ifc.get(): row = layout.row() row.prop(props, "should_load_from_memory") - if not IfcStore.get_file() or not props.should_load_from_memory: + if not tool.Ifc.get() or not props.should_load_from_memory: row = layout.row() props.ifc_files.layout_file_select(row, "*.ifc;*.ifczip;*.ifcxml", "IFC File(s)") diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 1d43e584d7..3dd372d6cd 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -609,7 +609,7 @@ class UpdateParametricRepresentation(bpy.types.Operator): return (obj := context.active_object) and obj.mode == "OBJECT" and tool.Geometry.has_mesh_properties(obj.data) def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() obj = context.active_object assert obj and tool.Geometry.has_mesh_properties(obj.data) props = tool.Geometry.get_mesh_props(obj.data) @@ -834,7 +834,7 @@ class OverrideOutlinerDelete(bpy.types.Operator): # unintended IFC spatial modifications. To make life less confusing for # the user, Delete means Delete. End of story. # Deep magick from the dawn of time - if IfcStore.get_file(): + if tool.Ifc.get(): return IfcStore.execute_ifc_operator(self, context) # https://blender.stackexchange.com/questions/203729/python-get-selected-objects-in-outliner objects_to_delete = set() @@ -958,7 +958,7 @@ class OverrideDuplicateMove(bpy.types.Operator): @staticmethod def execute_duplicate_operator(self, context, linked=False): # Deep magick from the dawn of time - if IfcStore.get_file(): + if tool.Ifc.get(): IfcStore.execute_ifc_operator(self, context) if self.new_active_obj: context.view_layer.objects.active = self.new_active_obj @@ -3218,7 +3218,7 @@ class OverrideMove(bpy.types.Operator): def execute(self, context): # Deep magick from the dawn of time - if IfcStore.get_file(): + if tool.Ifc.get(): IfcStore.execute_ifc_operator(self, context) if self.new_active_obj: context.view_layer.objects.active = self.new_active_obj diff --git a/src/bonsai/bonsai/bim/module/geometry/ui.py b/src/bonsai/bonsai/bim/module/geometry/ui.py index b0ad2ea47d..8ba3958222 100644 --- a/src/bonsai/bonsai/bim/module/geometry/ui.py +++ b/src/bonsai/bonsai/bim/module/geometry/ui.py @@ -20,7 +20,6 @@ import bpy import bonsai.bim import bonsai.tool as tool from bpy.types import Panel, Menu, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import prop_with_search from bonsai.bim.module.geometry.data import ( RepresentationsData, @@ -337,9 +336,9 @@ class BIM_PT_connections(Panel): def poll(cls, context): if not context.active_object: return False - if not IfcStore.get_element(context.active_object.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): return False - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not ConnectionsData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index e991dcf4f1..9a9124ebfa 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -22,7 +22,6 @@ import ifcopenshell.api.group import ifcopenshell.util.attribute import bonsai.bim.helper import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore import json @@ -129,8 +128,7 @@ class RemoveGroup(bpy.types.Operator, tool.Ifc.Operator): group: bpy.props.IntProperty() def _execute(self, context): - props = context.scene.BIMGroupProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run("group.remove_group", self.file, **{"group": self.file.by_id(self.group)}) bpy.ops.bim.load_groups() return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/group/ui.py b/src/bonsai/bonsai/bim/module/group/ui.py index 88077f99eb..8cd7d1fe2a 100644 --- a/src/bonsai/bonsai/bim/module/group/ui.py +++ b/src/bonsai/bonsai/bim/module/group/ui.py @@ -17,8 +17,8 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.module.group.data import GroupsData, ObjectGroupsData @@ -34,7 +34,7 @@ class BIM_PT_groups(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not GroupsData.is_loaded: @@ -93,7 +93,7 @@ class BIM_PT_object_groups(Panel): def poll(cls, context): if not context.active_object: return False - return IfcStore.get_file() and context.active_object.BIMObjectProperties.ifc_definition_id + return tool.Ifc.get() and context.active_object.BIMObjectProperties.ifc_definition_id def draw(self, context): if not ObjectGroupsData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/layer/operator.py b/src/bonsai/bonsai/bim/module/layer/operator.py index 13b5d79a56..5480c86034 100644 --- a/src/bonsai/bonsai/bim/module/layer/operator.py +++ b/src/bonsai/bonsai/bim/module/layer/operator.py @@ -24,7 +24,6 @@ import ifcopenshell.util.element import ifcopenshell.util.attribute import bonsai.bim.helper import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore class LoadLayers(bpy.types.Operator): @@ -33,7 +32,7 @@ class LoadLayers(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = context.scene.BIMLayerProperties props.layers.clear() for layer in tool.Ifc.get().by_type("IfcPresentationLayerAssignment"): @@ -126,7 +125,7 @@ class RemovePresentationLayer(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMLayerProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run("layer.remove_layer", self.file, **{"layer": self.file.by_id(self.layer)}) bpy.ops.bim.load_layers() return {"FINISHED"} @@ -142,7 +141,7 @@ class AssignPresentationLayer(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): item = bpy.data.meshes.get(self.item) if self.item else context.active_object.data - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "layer.assign_layer", self.file, diff --git a/src/bonsai/bonsai/bim/module/layer/ui.py b/src/bonsai/bonsai/bim/module/layer/ui.py index 5fb8db827d..609cb62815 100644 --- a/src/bonsai/bonsai/bim/module/layer/ui.py +++ b/src/bonsai/bonsai/bim/module/layer/ui.py @@ -17,8 +17,8 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bpy.types import Panel, UIList, Mesh -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.module.layer.data import LayersData @@ -34,7 +34,7 @@ class BIM_PT_layers(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not LayersData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 820ac5359e..3e9ff20924 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -29,7 +29,6 @@ import bonsai.tool as tool import bonsai.core.style import bonsai.core.material as core import bonsai.bim.module.model.profile as model_profile -from bonsai.bim.ifc import IfcStore from typing import Any, Union, TYPE_CHECKING from bonsai.bim.module.model import wall, slab @@ -118,7 +117,7 @@ class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() profile = ifcopenshell.api.run( "profile.add_parameterized_profile", self.file, @@ -292,7 +291,7 @@ class AddConstituent(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "material.add_constituent", self.file, @@ -328,7 +327,7 @@ class AddProfile(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = tool.Material.get_material_props() ifcopenshell.api.run( "material.add_profile", @@ -387,7 +386,7 @@ class ReorderMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() material_set = self.file.by_id(self.material_set) ifcopenshell.api.run( "material.reorder_set_item", @@ -443,7 +442,7 @@ class AddListItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "material.add_list_item", self.file, @@ -463,7 +462,7 @@ class RemoveListItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "material.remove_list_item", self.file, @@ -557,7 +556,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator): material_set_usage: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() active_obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object props = active_obj.BIMObjectMaterialProperties element = tool.Ifc.get_entity(active_obj) @@ -663,7 +662,7 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): material_set_item: bpy.props.IntProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.mprops = tool.Material.get_material_props() self.props = obj.BIMObjectMaterialProperties @@ -707,7 +706,7 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = obj.BIMObjectMaterialProperties mprops = tool.Material.get_material_props() element = tool.Ifc.get_entity(obj) diff --git a/src/bonsai/bonsai/bim/module/material/prop.py b/src/bonsai/bonsai/bim/module/material/prop.py index f21a34a702..04a596e73f 100644 --- a/src/bonsai/bonsai/bim/module/material/prop.py +++ b/src/bonsai/bonsai/bim/module/material/prop.py @@ -22,7 +22,6 @@ from ifcopenshell.util.doc import get_entity_doc import bonsai.tool as tool from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData from bonsai.bim.module.profile.data import ProfileData -from bonsai.bim.ifc import IfcStore from bonsai.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup from bpy.props import ( diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index 9ddd5ccd9d..fe0fce2413 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -21,7 +21,6 @@ import bonsai.bim.helper import bonsai.tool as tool import bpy from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.helper import prop_with_search from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData @@ -43,7 +42,7 @@ class BIM_PT_materials(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not MaterialsData.is_loaded: @@ -143,9 +142,9 @@ class BIM_PT_object_material(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not hasattr(IfcStore.get_file().by_id(props.ifc_definition_id), "HasAssociations"): + if not hasattr(tool.Ifc.get().by_id(props.ifc_definition_id), "HasAssociations"): return False return True @@ -153,7 +152,7 @@ class BIM_PT_object_material(Panel): if not ObjectMaterialData.is_loaded: ObjectMaterialData.load() - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.oprops = context.active_object.BIMObjectProperties self.props = context.active_object.BIMObjectMaterialProperties self.mprops = tool.Material.get_material_props() diff --git a/src/bonsai/bonsai/bim/module/model/handler.py b/src/bonsai/bonsai/bim/module/model/handler.py index 2b30a1238c..b69b595c34 100644 --- a/src/bonsai/bonsai/bim/module/model/handler.py +++ b/src/bonsai/bonsai/bim/module/model/handler.py @@ -20,7 +20,6 @@ import bpy import ifcopenshell import ifcopenshell.api from bonsai.bim.module.model import product, wall, slab, profile, opening, task -from bonsai.bim.ifc import IfcStore from bpy.app.handlers import persistent diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index d9da756705..319ffdd345 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -38,7 +38,6 @@ import bonsai.tool as tool import bonsai.core.geometry import bonsai.bim.import_ifc as import_ifc from collections import defaultdict -from bonsai.bim.ifc import IfcStore from math import pi, radians from mathutils import Vector, Matrix from bpy.types import Operator diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 2f780015a1..d3c014586a 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -35,7 +35,6 @@ import bonsai.core.root import bonsai.core.geometry import bonsai.core.model as core import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from math import pi, sin, cos, degrees, tan, radians from mathutils import Vector, Matrix, Quaternion from bonsai.bim.module.model.opening import FilledOpeningGenerator diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index ddc9eb5fea..ef00ea7b21 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -757,7 +757,7 @@ def regenerate_profile_usage(usecase_path, ifc_file, settings): elements.append(element) for element in elements: - obj = IfcStore.get_element(element.id()) + obj = tool.Ifc.get_object_by_identifier(element.id()) if not obj: continue representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 328eb34090..2ccd20a3e5 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -889,7 +889,7 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): model_props = tool.Model.get_model_props() direction_sense = model_props.direction_sense offset = model_props.offset - model = IfcStore.get_file() + model = tool.Ifc.get() element = tool.Ifc.get_entity(slab) material = ifcopenshell.util.element.get_material(element) material_set_usage = model.by_id(material.id()) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index d6e6ec7a31..daf9d9a43b 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -377,7 +377,7 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): walls, is_polyline_closed = DumbWallGenerator(self.relating_type).generate("POLYLINE") for wall in walls: - model = IfcStore.get_file() + model = tool.Ifc.get() element = tool.Ifc.get_entity(wall["obj"]) material = ifcopenshell.util.element.get_material(element) material_set_usage = model.by_id(material.id()) @@ -598,7 +598,7 @@ class DumbWallGenerator: self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) def generate(self, insertion_type="CURSOR"): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.layers = tool.Model.get_material_layer_parameters(self.relating_type) if not self.layers["thickness"]: return diff --git a/src/bonsai/bonsai/bim/module/nest/operator.py b/src/bonsai/bonsai/bim/module/nest/operator.py index 00b1f15f33..f3247817ec 100644 --- a/src/bonsai/bonsai/bim/module/nest/operator.py +++ b/src/bonsai/bonsai/bim/module/nest/operator.py @@ -21,7 +21,6 @@ import ifcopenshell import ifcopenshell.util.element import bonsai.tool as tool import bonsai.core.nest as core -from bonsai.bim.ifc import IfcStore class BIM_OT_nest_assign_object(bpy.types.Operator, tool.Ifc.Operator): @@ -113,7 +112,7 @@ class BIM_OT_select_components(bpy.types.Operator): obj: bpy.props.StringProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() obj = bpy.data.objects.get(self.obj) or context.active_object components = ifcopenshell.util.element.get_components(tool.Ifc.get_entity(obj)) component_objs = set(tool.Ifc.get_object(c) for c in components) @@ -132,7 +131,7 @@ class BIM_OT_select_nest(bpy.types.Operator): obj: bpy.props.StringProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() obj = bpy.data.objects.get(self.obj) or context.active_object nest = ifcopenshell.util.element.get_nest(tool.Ifc.get_entity(obj)) nest_obj = tool.Ifc.get_object(nest) diff --git a/src/bonsai/bonsai/bim/module/nest/ui.py b/src/bonsai/bonsai/bim/module/nest/ui.py index b1983a2411..1fd8f5ca4a 100644 --- a/src/bonsai/bonsai/bim/module/nest/ui.py +++ b/src/bonsai/bonsai/bim/module/nest/ui.py @@ -16,9 +16,9 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +import bonsai.tool as tool from bpy.types import Panel from bonsai.bim.module.nest.data import NestData -from bonsai.bim.ifc import IfcStore class BIM_PT_nest(Panel): @@ -37,9 +37,9 @@ class BIM_PT_nest(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): return False return True diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py index 9a2da938e6..3f25c5f4e4 100644 --- a/src/bonsai/bonsai/bim/module/profile/prop.py +++ b/src/bonsai/bonsai/bim/module/profile/prop.py @@ -21,7 +21,6 @@ import ifcopenshell import ifcopenshell.util.schema import ifcopenshell.util.attribute import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from bonsai.bim.prop import StrProperty, Attribute from bonsai.bim.module.profile.data import ProfileData from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 0aea8d00a3..02ee383a6a 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -503,10 +503,10 @@ class AppendEntireLibrary(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.library = IfcStore.library_file query = ", ".join(tool.Project.get_appendable_asset_types()) @@ -523,10 +523,10 @@ class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.library = IfcStore.library_file for element in ifcopenshell.util.selector.filter_elements(self.library, self.query): @@ -556,7 +556,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - poll = bool(IfcStore.get_file()) + poll = bool(tool.Ifc.get()) if not poll: cls.poll_message_set("Please create or load a project first.") return poll @@ -602,7 +602,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} def import_material_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) ifc_importer = import_ifc.IfcImporter(ifc_import_settings) @@ -620,7 +620,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): ifc_importer.create_style(style) def import_product_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) ifc_importer = import_ifc.IfcImporter(ifc_import_settings) @@ -633,7 +633,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): ifc_importer.place_objects_in_collections() def import_type_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None: - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(context, IfcStore.path, logger) @@ -648,7 +648,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): def import_materials(self, element: ifcopenshell.entity_instance, ifc_importer: import_ifc.IfcImporter) -> None: for material in ifcopenshell.util.element.get_materials(element): - if IfcStore.get_element(material.id()): + if tool.Ifc.get_object_by_identifier(material.id()): continue self.import_material_styles(material, ifc_importer) @@ -662,7 +662,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): if not element.is_a("IfcRepresentationItem") or not element.StyledByItem: continue for element2 in self.file.traverse(element.StyledByItem[0]): - if element2.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element2.id()): + if element2.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element2.id()): ifc_importer.create_style(element2) def import_material_styles( @@ -673,7 +673,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator): if not material.HasRepresentation: return for element in self.file.traverse(material.HasRepresentation[0]): - if element.is_a("IfcSurfaceStyle") and not IfcStore.get_element(element.id()): + if element.is_a("IfcSurfaceStyle") and not tool.Ifc.get_object_by_identifier(element.id()): ifc_importer.create_style(element) @@ -770,14 +770,14 @@ class EnableEditingHeader(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def execute(self, context): self.file = tool.Ifc.get() props = tool.Project.get_project_props() props.is_editing = True - mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description) + mvd = "".join(tool.Ifc.get().wrapped_data.header.file_description.description) if "[" in mvd: props.mvd = mvd.split("[")[1][0:-1] else: @@ -807,7 +807,7 @@ class EditHeader(bpy.types.Operator): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def execute(self, context): IfcStore.begin_transaction(self) @@ -832,7 +832,7 @@ class EditHeader(bpy.types.Operator): return {"FINISHED"} def record_state(self): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() return { "description": self.file.wrapped_data.header.file_description.description, "author": self.file.wrapped_data.header.file_name.author, @@ -841,14 +841,14 @@ class EditHeader(bpy.types.Operator): } def rollback(self, data): - file = IfcStore.get_file() + file = tool.Ifc.get() file.wrapped_data.header.file_description.description = data["old"]["description"] file.wrapped_data.header.file_name.author = data["old"]["author"] file.wrapped_data.header.file_name.organization = data["old"]["organisation"] file.wrapped_data.header.file_name.authorization = data["old"]["authorisation"] def commit(self, data): - file = IfcStore.get_file() + file = tool.Ifc.get() file.wrapped_data.header.file_description.description = data["new"]["description"] file.wrapped_data.header.file_name.author = data["new"]["author"] file.wrapped_data.header.file_name.organization = data["new"]["organisation"] diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index e56826be57..3d6f0460bc 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -121,7 +121,7 @@ def update_filter_mode(self: "BIMProjectProperties", context: bpy.types.Context) self.filter_categories.clear() if self.filter_mode == "NONE": return - file = IfcStore.get_file() + file = tool.Ifc.get() if self.filter_mode == "DECOMPOSITION": if file.schema == "IFC2X3": elements = file.by_type("IfcSpatialStructureElement") diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index cd0c69512f..b0950b2f15 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -153,7 +153,7 @@ class BIM_PT_project(Panel): self.layout.use_property_split = True props = context.scene.BIMProperties pprops = self.props = tool.Project.get_project_props() - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() if pprops.is_loading: self.draw_advanced_loading_ui(context) elif self.file or props.ifc_file: @@ -247,7 +247,7 @@ class BIM_PT_project(Panel): def draw_editing_buttons(self, context, row): pprops = self.props - if IfcStore.get_file(): + if tool.Ifc.get(): if pprops.is_editing: row.operator("bim.edit_header", icon="CHECKMARK", text="") row.operator("bim.disable_editing_header", icon="CANCEL", text="") @@ -257,10 +257,10 @@ class BIM_PT_project(Panel): def draw_editable_file_info(self, context): pprops = self.props - if IfcStore.get_file(): + if tool.Ifc.get(): row = self.layout.row(align=True) row.label(text="IFC Schema", icon="FILE_CACHE") - row.label(text=IfcStore.get_file().schema) + row.label(text=tool.Ifc.get().schema) if pprops.is_editing: row = self.layout.row(align=True) @@ -281,7 +281,7 @@ class BIM_PT_project(Panel): else: row = self.layout.row(align=True) row.label(text="IFC MVD", icon="FILE_HIDDEN") - mvd = "".join(IfcStore.get_file().wrapped_data.header.file_description.description) + mvd = "".join(tool.Ifc.get().wrapped_data.header.file_description.description) if "[" in mvd: mvd = mvd.split("[")[1][0:-1] row.label(text=mvd) diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py index 4165951a53..691d2e6930 100644 --- a/src/bonsai/bonsai/bim/module/pset/operator.py +++ b/src/bonsai/bonsai/bim/module/pset/operator.py @@ -94,7 +94,7 @@ class EditPset(bpy.types.Operator, tool.Ifc.Operator): properties: bpy.props.StringProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = tool.Pset.get_pset_props(self.obj, self.obj_type) ifc_definition_id = tool.Blender.get_obj_ifc_definition_id(self.obj, self.obj_type, context) element = tool.Ifc.get().by_id(ifc_definition_id) @@ -226,7 +226,7 @@ class AddQto(bpy.types.Operator, tool.Ifc.Operator): obj_type: bpy.props.StringProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() qto_name = tool.Pset.get_pset_name(self.obj, self.obj_type, pset_type="QTO") bpy.ops.bim.enable_pset_editing( pset_id=0, pset_name=qto_name, pset_type="QTO", obj=self.obj, obj_type=self.obj_type @@ -314,7 +314,7 @@ class BIM_OT_rename_parameters(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props_to_map = context.scene.RenameProperties - ifc_file = IfcStore.get_file() + ifc_file = tool.Ifc.get() all_ifc_elements = ifc_file.by_type("IfcElement") for ifc_element in all_ifc_elements: @@ -347,7 +347,7 @@ class BIM_OT_add_edit_custom_property(bpy.types.Operator, tool.Ifc.Operator): index: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = context.scene.AddEditProperties for obj in tool.Blender.get_selected_objects(): @@ -402,7 +402,7 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator): index: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = context.scene.DeletePsets for obj in tool.Blender.get_selected_objects(): diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py index 86a3363557..6a1b68841f 100644 --- a/src/bonsai/bonsai/bim/module/pset/prop.py +++ b/src/bonsai/bonsai/bim/module/pset/prop.py @@ -26,7 +26,6 @@ import bonsai.tool as tool from bonsai.bim.prop import Attribute, StrProperty from bonsai.bim.module.pset.data import AddEditCustomPropertiesData, ObjectPsetsData, MaterialPsetsData from bonsai.bim.module.material.data import ObjectMaterialData -from bonsai.bim.ifc import IfcStore from bpy.types import PropertyGroup from bpy.props import ( PointerProperty, @@ -135,7 +134,7 @@ def get_resource_pset_names(self, context): global psetnames rprops = context.scene.BIMResourceProperties rtprops = context.scene.BIMResourceTreeProperties - ifc_class = IfcStore.get_file().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a() + ifc_class = tool.Ifc.get().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a() if ifc_class not in psetnames: psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema()) psetnames[ifc_class] = blender_formatted_enum_from_psets(psets) @@ -146,7 +145,7 @@ def get_resource_qto_names(self, context): global qtonames rprops = context.scene.BIMResourceProperties rtprops = context.scene.BIMResourceTreeProperties - ifc_class = IfcStore.get_file().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a() + ifc_class = tool.Ifc.get().by_id(rtprops.resources[rprops.active_resource_index].ifc_definition_id).is_a() if ifc_class not in qtonames: psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, qto_only=True, schema=tool.Ifc.get_schema()) qtonames[ifc_class] = blender_formatted_enum_from_psets(psets) @@ -174,7 +173,7 @@ def get_group_qto_names(self, context): def get_profile_pset_names(self, context): global psetnames pprops = tool.Profile.get_profile_props() - ifc_class = IfcStore.get_file().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a() + ifc_class = tool.Ifc.get().by_id(pprops.profiles[pprops.active_profile_index].ifc_definition_id).is_a() if ifc_class not in psetnames: psets = bonsai.bim.schema.ifc.psetqto.get_applicable(ifc_class, pset_only=True, schema=tool.Ifc.get_schema()) psetnames[ifc_class] = blender_formatted_enum_from_psets(psets) @@ -290,7 +289,8 @@ class AddEditProperties(PropertyGroup): enum_values: CollectionProperty(name="Enum Values", type=Attribute) def get_value_name(self) -> Union[Literal["string_value", "bool_value", "int_value", "float_value"], None]: - ifc_data_type = IfcStore.get_schema().declaration_by_name(self.primary_measure_type) + schema = tool.Ifc.schema() + ifc_data_type = schema.declaration_by_name(self.primary_measure_type) data_type = ifcopenshell.util.attribute.get_primitive_type(ifc_data_type) if data_type == "string": return "string_value" diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index 21f3c1b083..4adda725dc 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -20,7 +20,6 @@ from __future__ import annotations import bpy import bonsai.tool as tool from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import prop_with_search, get_display_value from bonsai.bim.module.pset.data import ( ObjectPsetsData, @@ -247,7 +246,7 @@ class BIM_PT_object_psets(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False return True @@ -325,7 +324,7 @@ class BIM_PT_object_qtos(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False return True diff --git a/src/bonsai/bonsai/bim/module/pset_template/prop.py b/src/bonsai/bonsai/bim/module/pset_template/prop.py index 66cd7e3fea..122a09128c 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/prop.py +++ b/src/bonsai/bonsai/bim/module/pset_template/prop.py @@ -183,7 +183,8 @@ class PropTemplate(PropertyGroup): def get_value_name(self) -> str: if self.primary_measure_type == "-": return "string_value" - ifc_data_type = IfcStore.get_schema().declaration_by_name(self.primary_measure_type) + schema = tool.Ifc.schema() + ifc_data_type = schema.declaration_by_name(self.primary_measure_type) data_type = ifcopenshell.util.attribute.get_primitive_type(ifc_data_type) if data_type == "string": return "string_value" diff --git a/src/bonsai/bonsai/bim/module/qto/operator.py b/src/bonsai/bonsai/bim/module/qto/operator.py index fe1574d1fa..63d16335a1 100644 --- a/src/bonsai/bonsai/bim/module/qto/operator.py +++ b/src/bonsai/bonsai/bim/module/qto/operator.py @@ -21,7 +21,6 @@ import ifcopenshell import ifcopenshell.api import bonsai.tool as tool import bonsai.core.qto as core -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.qto import helper diff --git a/src/bonsai/bonsai/bim/module/resource/prop.py b/src/bonsai/bonsai/bim/module/resource/prop.py index 8613f44d1a..b5f1360c4e 100644 --- a/src/bonsai/bonsai/bim/module/resource/prop.py +++ b/src/bonsai/bonsai/bim/module/resource/prop.py @@ -19,7 +19,6 @@ import bpy import ifcopenshell.api import ifcopenshell.util.resource -from bonsai.bim.ifc import IfcStore import bonsai.tool as tool import bonsai.bim.module.pset.data import bonsai.bim.module.resource.data diff --git a/src/bonsai/bonsai/bim/module/resource/ui.py b/src/bonsai/bonsai/bim/module/resource/ui.py index 5ffdac8881..898749ff67 100644 --- a/src/bonsai/bonsai/bim/module/resource/ui.py +++ b/src/bonsai/bonsai/bim/module/resource/ui.py @@ -17,9 +17,9 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import bonsai.bim.helper from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.resource.data import ResourceData from typing import Any @@ -35,7 +35,7 @@ class BIM_PT_resources(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 4801d3f999..0d6a7b4d0f 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -57,8 +57,9 @@ class EnableReassignClass(bpy.types.Operator): "IfcAnnotation", "IfcRelSpaceBoundary", ] + schema = tool.Ifc.schema() for ifc_product in ifc_products: - if ifcopenshell.util.schema.is_a(IfcStore.get_schema().declaration_by_name(ifc_class), ifc_product): + if schema.declaration_by_name(ifc_class).is_a(ifc_product): context.scene.BIMRootProperties.ifc_product = ifc_product element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) context.scene.BIMRootProperties.ifc_class = element.is_a() diff --git a/src/bonsai/bonsai/bim/module/root/ui.py b/src/bonsai/bonsai/bim/module/root/ui.py index 809230656f..b1a2b52637 100644 --- a/src/bonsai/bonsai/bim/module/root/ui.py +++ b/src/bonsai/bonsai/bim/module/root/ui.py @@ -17,9 +17,9 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import bonsai.bim.module.root.prop as root_prop from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import prop_with_search from bonsai.bim.module.root.data import IfcClassData from bonsai.bim.module.model.data import AuthoringData @@ -38,7 +38,7 @@ class BIM_PT_class(Panel): def poll(cls, context): if not context.active_object: return False - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not IfcClassData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 6730353b00..5f5854edd2 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -31,7 +31,6 @@ import ifcopenshell.util.sequence import ifcopenshell.util.selector from datetime import datetime from dateutil import parser, relativedelta -from bonsai.bim.ifc import IfcStore from bpy_extras.io_utils import ImportHelper from typing import get_args, TYPE_CHECKING from typing_extensions import assert_never @@ -700,7 +699,7 @@ class ImportP6(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): def _execute(self, context): from ifc4d.p62ifc import P62Ifc - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() p62ifc = P62Ifc() p62ifc.xml = self.filepath @@ -728,7 +727,7 @@ class ImportP6XER(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): def _execute(self, context): from ifc4d.p6xer2ifc import P6XER2Ifc - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() p6xer2ifc = P6XER2Ifc() p6xer2ifc.xer = self.filepath @@ -756,7 +755,7 @@ class ImportPP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): def _execute(self, context): from ifc4d.pp2ifc import PP2Ifc - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() pp2ifc = PP2Ifc() pp2ifc.pp = self.filepath @@ -784,7 +783,7 @@ class ImportMSP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): def _execute(self, context): from ifc4d.msp2ifc import MSP2Ifc - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() msp2ifc = MSP2Ifc() msp2ifc.xml = self.filepath @@ -814,7 +813,7 @@ class ExportMSP(bpy.types.Operator, ImportHelper): def execute(self, context): from ifc4d.ifc2msp import Ifc2Msp - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() ifc2msp = Ifc2Msp() ifc2msp.work_schedule = self.file.by_type("IfcWorkSchedule")[0] @@ -847,7 +846,7 @@ class ExportP6(bpy.types.Operator, ImportHelper): def execute(self, context): from ifc4d.ifc2p6 import Ifc2P6 - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() start = time.time() ifc2p6 = Ifc2P6() ifc2p6.xml = bpy.path.ensure_ext(self.filepath, ".xml") diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py index d2e550138d..ce0e307076 100644 --- a/src/bonsai/bonsai/bim/module/sequence/prop.py +++ b/src/bonsai/bonsai/bim/module/sequence/prop.py @@ -23,7 +23,6 @@ import ifcopenshell.util.attribute import ifcopenshell.util.date import bonsai.tool as tool import bonsai.core.sequence as core -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.sequence.data import SequenceData, AnimationColorSchemeData, refresh as refresh_sequence_data import bonsai.bim.module.resource.data import bonsai.bim.module.pset.data diff --git a/src/bonsai/bonsai/bim/module/sequence/ui.py b/src/bonsai/bonsai/bim/module/sequence/ui.py index 2320bb80b4..21ed962852 100644 --- a/src/bonsai/bonsai/bim/module/sequence/ui.py +++ b/src/bonsai/bonsai/bim/module/sequence/ui.py @@ -22,7 +22,6 @@ import isodate import bonsai.tool as tool import bonsai.bim.helper from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes from bonsai.bim.module.sequence.data import ( WorkPlansData, @@ -45,7 +44,7 @@ class BIM_PT_status(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): self.props = context.scene.BIMStatusProperties @@ -77,7 +76,7 @@ class BIM_PT_work_plans(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and file.schema != "IFC2X3" def draw(self, context): @@ -147,7 +146,7 @@ class BIM_PT_work_schedules(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -592,7 +591,7 @@ class BIM_PT_animation_Color_Scheme(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -980,7 +979,7 @@ class BIM_PT_work_calendars(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py b/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py index 8230c55c66..a91a7947c5 100644 --- a/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py +++ b/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py @@ -26,7 +26,6 @@ import ifcopenshell.api import ifcopenshell.util.attribute import ifcopenshell.util.unit as ifcunit import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.structural.shader import DecorationShader from typing import Literal, TypedDict, Iterable @@ -307,7 +306,7 @@ class ShaderInfo: props = bpy.context.scene.BIMStructuralProperties group_definition_id = int(props.load_group_to_show) - file = IfcStore.get_file() + file = tool.Ifc.get() groups = [file.by_id(group_definition_id)] recursive_subgroups(groups, 10, props.activity_type) @@ -333,7 +332,7 @@ class ShaderInfo: orientation = np.eye(3) if reference_frame == "LOCAL_COORDS": orientation = rotation - blender_object: bpy.types.Object = IfcStore.get_element(getattr(surf, "GlobalId", None)) + blender_object: bpy.types.Object = tool.Ifc.get_object_by_identifier(getattr(surf, "GlobalId", None)) mat = blender_object.matrix_world mesh: bpy.types.Mesh = blender_object.data @@ -454,7 +453,7 @@ class ShaderInfo: activity_list = value["activities"] if len(activity_list) == 0: continue - blender_object = IfcStore.get_element(getattr(conn, "GlobalId", None)) + blender_object = tool.Ifc.get_object_by_identifier(getattr(conn, "GlobalId", None)) if blender_object.type == "MESH": conn_location = blender_object.matrix_world @ blender_object.data.vertices[0].co rotation = self.get_point_connection_rotation(conn) @@ -580,7 +579,7 @@ class ShaderInfo: if len(activity_list) == 0: continue - blender_object = IfcStore.get_element(getattr(member, "GlobalId", None)) + blender_object = tool.Ifc.get_object_by_identifier(getattr(member, "GlobalId", None)) start_co = blender_object.matrix_world @ blender_object.data.vertices[0].co end_co = blender_object.matrix_world @ blender_object.data.vertices[1].co diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index 646e5ad294..47d74515f8 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -20,13 +20,13 @@ import bpy import json import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.structural import ifcopenshell.util.attribute import bonsai.bim.helper import bonsai.core.structural as core import bonsai.tool as tool from math import degrees from mathutils import Vector, Matrix -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.structural.decorator import LoadsDecorator @@ -77,13 +77,12 @@ class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties - file = IfcStore.get_file() + file = tool.Ifc.get() related_structural_connection = file.by_id(oprops.ifc_definition_id) relating_structural_member = file.by_id(props.relating_structural_member.BIMObjectProperties.ifc_definition_id) if not relating_structural_member.is_a("IfcStructuralMember"): return {"FINISHED"} - ifcopenshell.api.run( - "structural.add_structural_member_connection", + ifcopenshell.api.structural.add_structural_member_connection( file, relating_structural_member=relating_structural_member, related_structural_connection=related_structural_connection, @@ -125,7 +124,7 @@ class RemoveStructuralConnectionCondition(bpy.types.Operator, tool.Ifc.Operator) connects_structural_member: bpy.props.IntProperty() def _execute(self, context): - file = IfcStore.get_file() + file = tool.Ifc.get() relation = file.by_id(self.connects_structural_member) connection = relation.RelatedStructuralConnection ifcopenshell.api.run("structural.remove_structural_connection_condition", file, **{"relation": relation}) @@ -139,7 +138,7 @@ class AddStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): connection: bpy.props.IntProperty() def _execute(self, context): - file = IfcStore.get_file() + file = tool.Ifc.get() connection = file.by_id(self.connection) ifcopenshell.api.run("structural.add_structural_boundary_condition", file, **{"connection": connection}) return {"FINISHED"} @@ -152,7 +151,7 @@ class RemoveStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): connection: bpy.props.IntProperty() def _execute(self, context): - file = IfcStore.get_file() + file = tool.Ifc.get() connection = file.by_id(self.connection) ifcopenshell.api.run("structural.remove_structural_boundary_condition", file, **{"connection": connection}) return {"FINISHED"} @@ -170,8 +169,9 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator): props.boundary_condition_attributes.clear() condition = tool.Ifc.get().by_id(self.boundary_condition) + schema = tool.Ifc.schema() - for attribute in IfcStore.get_schema().declaration_by_name(condition.is_a()).all_attributes(): + for attribute in schema.declaration_by_name(condition.is_a()).all_attributes(): value = getattr(condition, attribute.name(), None) data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) new = props.boundary_condition_attributes.add() @@ -207,7 +207,7 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object props = obj.BIMStructuralProperties - file = IfcStore.get_file() + file = tool.Ifc.get() connection = file.by_id(self.connection) condition = connection.AppliedCondition @@ -353,7 +353,7 @@ class EnableEditingStructuralItemAxis(bpy.types.Operator): oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() item = self.file.by_id(oprops.ifc_definition_id) z_axis = Vector(item.Axis.DirectionRatios).normalized() @ obj.matrix_world if item.Axis else None x_axis = (obj.data.vertices[1].co - obj.data.vertices[0].co).normalized() @@ -407,7 +407,7 @@ class EditStructuralItemAxis(bpy.types.Operator, tool.Ifc.Operator): props = obj.BIMStructuralProperties relative_matrix = props.axis_empty.matrix_world @ obj.matrix_world.inverted() z_axis = relative_matrix.col[2][0:3] - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.edit_structural_item_axis", self.file, @@ -428,7 +428,7 @@ class EnableEditingStructuralConnectionCS(bpy.types.Operator): oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() item = self.file.by_id(oprops.ifc_definition_id) location = obj.data.vertices[0].co @@ -496,7 +496,7 @@ class EditStructuralConnectionCS(bpy.types.Operator, tool.Ifc.Operator): relative_matrix = props.ccs_empty.matrix_world @ obj.matrix_world.inverted() x_axis = relative_matrix.col[0][0:3] z_axis = relative_matrix.col[2][0:3] - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.edit_structural_connection_cs", self.file, @@ -515,7 +515,7 @@ class AssignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): load_case: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "aggregate.assign_object", self.file, @@ -534,7 +534,7 @@ class UnassignStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): load_case: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "aggregate.unassign_object", self.file, @@ -552,7 +552,7 @@ class AddStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - ifcopenshell.api.run("structural.add_structural_load_case", IfcStore.get_file()) + ifcopenshell.api.run("structural.add_structural_load_case", tool.Ifc.get()) return {"FINISHED"} @@ -564,7 +564,7 @@ class EditStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties attributes = bonsai.bim.helper.export_attributes(props.load_case_attributes) - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.edit_structural_load_case", self.file, @@ -581,7 +581,7 @@ class RemoveStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): load_case: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.remove_structural_load_case", self.file, load_case=self.file.by_id(self.load_case) ) @@ -639,7 +639,7 @@ class AddStructuralLoadGroup(bpy.types.Operator, tool.Ifc.Operator): load_case: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() load_group = ifcopenshell.api.run("structural.add_structural_load_group", self.file) ifcopenshell.api.run( "group.assign_group", self.file, products=[load_group], group=self.file.by_id(self.load_case) @@ -654,7 +654,7 @@ class RemoveStructuralLoadGroup(bpy.types.Operator, tool.Ifc.Operator): load_group: bpy.props.IntProperty() def _execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.remove_structural_load_group", self.file, load_group=self.file.by_id(self.load_group) ) @@ -668,7 +668,7 @@ class EnableEditingStructuralLoadGroupActivities(bpy.types.Operator): load_group: bpy.props.IntProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() self.props = context.scene.BIMStructuralProperties self.props.active_load_group_id = self.load_group self.props.load_group_editing_type = "ACTIVITY" @@ -693,7 +693,7 @@ class AddStructuralActivity(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.props = context.scene.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() for obj in context.selected_objects: if not obj.BIMObjectProperties.ifc_definition_id: continue @@ -741,7 +741,7 @@ class LoadStructuralLoads(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = context.scene.BIMStructuralProperties props.structural_loads.clear() loads = tool.Ifc.get().by_type("IfcStructuralLoad") @@ -787,7 +787,7 @@ class AddStructuralLoad(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): result = ifcopenshell.api.run( - "structural.add_structural_load", IfcStore.get_file(), name="New Load", ifc_class=self.ifc_class + "structural.add_structural_load", tool.Ifc.get(), name="New Load", ifc_class=self.ifc_class ) bpy.ops.bim.load_structural_loads() bpy.ops.bim.enable_editing_structural_load(structural_load=result.id()) @@ -828,7 +828,7 @@ class RemoveStructuralLoad(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.remove_structural_load", self.file, @@ -846,7 +846,7 @@ class EditStructuralLoad(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties attributes = bonsai.bim.helper.export_attributes(props.structural_load_attributes) - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.edit_structural_load", self.file, @@ -877,7 +877,7 @@ class LoadBoundaryConditions(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() props = context.scene.BIMStructuralProperties props.boundary_conditions.clear() conditions = tool.Ifc.get().by_type("IfcBoundaryCondition") @@ -936,7 +936,7 @@ class AddBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): result = ifcopenshell.api.run( "structural.add_structural_boundary_condition", - IfcStore.get_file(), + tool.Ifc.get(), name="New Load", ifc_class=self.ifc_class, ) @@ -957,7 +957,8 @@ class EnableEditingBoundaryCondition(bpy.types.Operator): boundary_condition = tool.Ifc.get().by_id(self.boundary_condition) # bonsai.bim.helper.import_attributes(data["type"], props.boundary_condition_attributes, data) - for attribute in IfcStore.get_schema().declaration_by_name(boundary_condition.is_a()).all_attributes(): + schema = tool.Ifc.schema() + for attribute in schema.declaration_by_name(boundary_condition.is_a()).all_attributes(): value = getattr(boundary_condition, attribute.name(), None) data_type = ifcopenshell.util.attribute.get_primitive_type(attribute) new = props.boundary_condition_attributes.add() @@ -1000,7 +1001,7 @@ class RemoveBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() ifcopenshell.api.run( "structural.remove_structural_boundary_condition", self.file, @@ -1017,7 +1018,7 @@ class EditBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = context.scene.BIMStructuralProperties - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() # attributes = bonsai.bim.helper.export_attributes(props.boundary_condition_attributes) attributes = {} for attribute in props.boundary_condition_attributes: diff --git a/src/bonsai/bonsai/bim/module/structural/prop.py b/src/bonsai/bonsai/bim/module/structural/prop.py index 740c97f1e6..2ecc29834a 100644 --- a/src/bonsai/bonsai/bim/module/structural/prop.py +++ b/src/bonsai/bonsai/bim/module/structural/prop.py @@ -19,7 +19,6 @@ from math import radians import bpy import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore from bonsai.bim.prop import StrProperty, Attribute from bonsai.bim.module.structural.data import ( StructuralLoadCasesData, diff --git a/src/bonsai/bonsai/bim/module/structural/ui.py b/src/bonsai/bonsai/bim/module/structural/ui.py index 767724bdd5..d8f69e232e 100644 --- a/src/bonsai/bonsai/bim/module/structural/ui.py +++ b/src/bonsai/bonsai/bim/module/structural/ui.py @@ -17,9 +17,9 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import bonsai.bim.helper from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import draw_attributes, prop_with_search from bonsai.bim.module.structural.data import ( StructuralBoundaryConditionsData, @@ -90,9 +90,9 @@ class BIM_PT_structural_boundary_conditions(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): return False return True @@ -125,9 +125,9 @@ class BIM_PT_connected_structural_members(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): return False return True @@ -178,9 +178,9 @@ class BIM_PT_structural_member(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralMember"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralMember"): return False return True @@ -221,9 +221,9 @@ class BIM_PT_structural_connection(Panel): props = context.active_object.BIMObjectProperties if not props.ifc_definition_id: return False - if not IfcStore.get_element(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): return False - if not IfcStore.get_file().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): return False return True @@ -274,7 +274,7 @@ class BIM_PT_structural_analysis_models(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -348,7 +348,7 @@ class BIM_PT_structural_load_cases(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -443,7 +443,7 @@ class BIM_PT_show_structural_activities(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -474,7 +474,7 @@ class BIM_PT_structural_loads(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): @@ -543,7 +543,7 @@ class BIM_PT_boundary_conditions(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/style/ui.py b/src/bonsai/bonsai/bim/module/style/ui.py index 2486ee91e8..2b7eaad760 100644 --- a/src/bonsai/bonsai/bim/module/style/ui.py +++ b/src/bonsai/bonsai/bim/module/style/ui.py @@ -20,9 +20,7 @@ import bpy import bonsai.bim.helper import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.style.data import StylesData, BlenderMaterialStyleData -from typing import Union class BIM_PT_styles(Panel): @@ -36,7 +34,7 @@ class BIM_PT_styles(Panel): @classmethod def poll(cls, context): - return IfcStore.get_file() + return tool.Ifc.get() def draw(self, context): if not StylesData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 4ef3c255f7..5c9750dc23 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -21,9 +21,7 @@ import ifcopenshell.api import bonsai.tool as tool import bonsai.core.system as core import bonsai.bim.helper -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.system.data import PortData -from mathutils import Matrix class LoadSystems(bpy.types.Operator): diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index 866407be3e..ee370e5ae5 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -29,7 +29,6 @@ import bonsai.tool as tool import bonsai.core.geometry import bonsai.core.type as core import bonsai.core.root -from bonsai.bim.ifc import IfcStore class AssignType(bpy.types.Operator, tool.Ifc.Operator): @@ -66,7 +65,7 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator): def exclude_callback(attribute): return attribute.is_a("IfcProfileDef") and attribute.ProfileName - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() objs = [bpy.data.objects.get(self.related_object)] if self.related_object else context.selected_objects for obj in objs: element = tool.Ifc.get_entity(obj) @@ -187,7 +186,7 @@ class SelectSimilarType(bpy.types.Operator): related_object: bpy.props.StringProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() objects = bpy.context.selected_objects # store relating types to avoid selecting same elements multiple times @@ -229,7 +228,7 @@ class SelectTypeObjects(bpy.types.Operator): relating_type: bpy.props.StringProperty() def execute(self, context): - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() relating_type = bpy.data.objects.get(self.relating_type) if self.relating_type else context.active_object at_least_one_selectable_typed_object = False for element in ifcopenshell.util.element.get_types(tool.Ifc.get_entity(relating_type)): diff --git a/src/bonsai/bonsai/bim/module/type/prop.py b/src/bonsai/bonsai/bim/module/type/prop.py index ac7986fe6b..aedb5c1993 100644 --- a/src/bonsai/bonsai/bim/module/type/prop.py +++ b/src/bonsai/bonsai/bim/module/type/prop.py @@ -20,8 +20,6 @@ import bpy import ifcopenshell.util.element import ifcopenshell.util.type from bonsai.bim.module.type.data import TypeData -from bonsai.bim.prop import StrProperty, Attribute -from bonsai.bim.ifc import IfcStore import bonsai.tool as tool from bpy.types import PropertyGroup from bpy.props import ( diff --git a/src/bonsai/bonsai/bim/module/type/ui.py b/src/bonsai/bonsai/bim/module/type/ui.py index 4bfca516e3..405507f168 100644 --- a/src/bonsai/bonsai/bim/module/type/ui.py +++ b/src/bonsai/bonsai/bim/module/type/ui.py @@ -19,7 +19,6 @@ import bonsai.tool as tool import bonsai.bim.module.type.prop as type_prop from bpy.types import Panel -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import prop_with_search from bonsai.bim.module.type.data import TypeData diff --git a/src/bonsai/bonsai/bim/module/unit/prop.py b/src/bonsai/bonsai/bim/module/unit/prop.py index edd23f2ce4..b01fab4ddf 100644 --- a/src/bonsai/bonsai/bim/module/unit/prop.py +++ b/src/bonsai/bonsai/bim/module/unit/prop.py @@ -17,7 +17,6 @@ # along with Bonsai. If not, see . import bpy -from bonsai.bim.ifc import IfcStore from bonsai.bim.prop import StrProperty, Attribute from bonsai.bim.module.unit.data import UnitsData from bpy.types import PropertyGroup diff --git a/src/bonsai/bonsai/bim/module/unit/ui.py b/src/bonsai/bonsai/bim/module/unit/ui.py index 77e0117699..83637845f7 100644 --- a/src/bonsai/bonsai/bim/module/unit/ui.py +++ b/src/bonsai/bonsai/bim/module/unit/ui.py @@ -17,8 +17,8 @@ # along with Bonsai. If not, see . import bonsai.bim.helper +import bonsai.tool as tool from bpy.types import Panel, UIList -from bonsai.bim.ifc import IfcStore from bonsai.bim.helper import prop_with_search from bonsai.bim.module.unit.data import UnitsData @@ -34,7 +34,7 @@ class BIM_PT_units(Panel): @classmethod def poll(cls, context): - file = IfcStore.get_file() + file = tool.Ifc.get() return file def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/void/operator.py b/src/bonsai/bonsai/bim/module/void/operator.py index a1493f2711..91537ed528 100644 --- a/src/bonsai/bonsai/bim/module/void/operator.py +++ b/src/bonsai/bonsai/bim/module/void/operator.py @@ -24,7 +24,6 @@ import bonsai.tool as tool import bonsai.core.geometry import bonsai.core.root import bonsai.bim.handler -from bonsai.bim.ifc import IfcStore from bonsai.bim.module.model.opening import FilledOpeningGenerator @@ -98,7 +97,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator): break element_had_openings = tool.Geometry.has_openings(voided_element) - body_context = ifcopenshell.util.representation.get_context(IfcStore.get_file(), "Model", "Body") + body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body") if not element2: element2 = bonsai.core.root.assign_class( tool.Ifc, @@ -215,7 +214,7 @@ class AddFilling(bpy.types.Operator, tool.Ifc.Operator): opening = context.scene.objects.get(self.opening, context.scene.VoidProperties.desired_opening) if opening is None: return {"FINISHED"} - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() element_id = obj.BIMObjectProperties.ifc_definition_id opening_id = opening.BIMObjectProperties.ifc_definition_id if not element_id or not opening_id or element_id == opening_id: diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index c82ffa80a5..ce9f856f80 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -129,9 +129,13 @@ class Ifc(bonsai.core.tool.Ifc): return None @classmethod - def get_object(cls, element: ifcopenshell.entity_instance) -> IFC_CONNECTED_TYPE: + def get_object(cls, element: ifcopenshell.entity_instance) -> Union[IFC_CONNECTED_TYPE, None]: return IfcStore.get_element(element.id()) + @classmethod + def get_object_by_identifier(cls, id_or_guid: Union[int, str]) -> Union[IFC_CONNECTED_TYPE, None]: + return IfcStore.get_element(id_or_guid) + @classmethod def rebuild_element_maps(cls) -> None: """Rebuilds the id_map and guid_map diff --git a/src/bonsai/bonsai/tool/structural.py b/src/bonsai/bonsai/tool/structural.py index 7dfa3dbdd0..30cbb00191 100644 --- a/src/bonsai/bonsai/tool/structural.py +++ b/src/bonsai/bonsai/tool/structural.py @@ -23,8 +23,6 @@ import json import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool -from bonsai.bim.ifc import IfcStore -from pprint import pprint from typing import Union, Any @@ -129,7 +127,8 @@ class Structural(bonsai.core.tool.Structural): def load_structural_analysis_model_attributes(cls, data: dict[str, Any]) -> None: props = bpy.context.scene.BIMStructuralProperties props.structural_analysis_model_attributes.clear() - for attribute in IfcStore.get_schema().declaration_by_name("IfcStructuralAnalysisModel").all_attributes(): + schema = tool.Ifc.schema() + for attribute in schema.declaration_by_name("IfcStructuralAnalysisModel").all_attributes(): data_type = str(attribute.type_of_attribute) if " 0: @@ -114,8 +113,8 @@ class Patcher: bpy.ops.bim.update_representation( ifc_representation_class="IfcExtrudedAreaSolid/IfcArbitraryProfileDefWithVoids" ) - for context in IfcStore.get_file().by_type("IfcGeometricRepresentationContext", include_subtypes=False): + for context in tool.Ifc.get().by_type("IfcGeometricRepresentationContext", include_subtypes=False): if context.Precision: context.Precision = 10 - self.file = IfcStore.get_file() + self.file = tool.Ifc.get() From 8b92b28dfcd033ec2557b4cb9eb2b6818211058c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Feb 2025 17:45:05 +0500 Subject: [PATCH 071/476] should_clean_mesh - add description --- src/bonsai/bonsai/bim/module/project/prop.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index 3d6f0460bc..900d92e81f 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -299,7 +299,14 @@ class BIMProjectProperties(PropertyGroup): should_merge_materials_by_colour: BoolProperty(name="Merge Materials by Colour", default=False) should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False) should_load_geometry: BoolProperty(name="Load Geometry", default=True) - should_clean_mesh: BoolProperty(name="Clean Meshes", default=False) + should_clean_mesh: BoolProperty( + name="Clean Meshes", + description=( + "Convert all triangles to quads for meshes. " + "By default Bonsai is importing meshes triangulated (even if they are not stored as triangulated in IFC)." + ), + default=False, + ) should_cache: BoolProperty(name="Cache", default=False) deflection_tolerance: FloatProperty(name="Deflection Tolerance", default=0.001) angular_tolerance: FloatProperty(name="Angular Tolerance", default=0.5) From 4bb32b252df74ea99cb3b0d361bb1c802f124a6f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Feb 2025 18:31:27 +0500 Subject: [PATCH 072/476] IFC Delete - add logs to system console In some cases deletion can take ages, so there should be at least some way for user to find out how it's going. --- .../bonsai/bim/module/geometry/operator.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 3dd372d6cd..a2fda56b89 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -731,6 +731,7 @@ class OverrideDelete(bpy.types.Operator): if self.is_batch: row = self.layout.row() row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR") + row.label(text="See system console for deletion progress.") def _execute(self, context: bpy.types.Context): start_time = time() @@ -740,8 +741,19 @@ class OverrideDelete(bpy.types.Operator): self.process_arrays(context) clear_active_object = True - for obj in context.selected_objects: - if not tool.Blender.is_valid_data_block(obj): + objects_to_remove = context.selectable_objects + for i, obj in enumerate(objects_to_remove): + # Log time. + time_since_start = time() - start_time + is_valid_data_block = tool.Blender.is_valid_data_block(obj) + if time_since_start > 10: + obj_name = f" ({obj.name})" if is_valid_data_block else "" + print( + f"Removing object {i}/{len(objects_to_remove)}{obj_name}. " + f"Time since start: {time_since_start:.2f} seconds." + ) + + if not is_valid_data_block: continue element = tool.Ifc.get_entity(obj) if element: From f4d101788d116f92bdc47e4c4204d09906e5e170 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 20 Feb 2025 19:18:35 +1100 Subject: [PATCH 073/476] Fix insane bug when replacing element that looped through every single element. --- src/ifcopenshell-python/ifcopenshell/util/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index b8ef0711d5..524f86b8ac 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -1333,7 +1333,7 @@ def get_referenced_elements(reference: ifcopenshell.entity_instance) -> set[ifco def replace_element(element: ifcopenshell.entity_instance, replacement: ifcopenshell.entity_instance) -> None: - for inverse in element.file: + for inverse in element.file.get_inverse(element): replace_attribute(inverse, element, replacement) From 04a20b5c802943ec70cc9c1c4f1ab78514c9160d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 20 Feb 2025 10:55:32 +0500 Subject: [PATCH 074/476] small fix for 4bb32b252d --- src/bonsai/bonsai/bim/module/geometry/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index a2fda56b89..724603b976 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -742,7 +742,7 @@ class OverrideDelete(bpy.types.Operator): self.process_arrays(context) clear_active_object = True objects_to_remove = context.selectable_objects - for i, obj in enumerate(objects_to_remove): + for i, obj in enumerate(objects_to_remove, 1): # Log time. time_since_start = time() - start_time is_valid_data_block = tool.Blender.is_valid_data_block(obj) From 79abbad796e542fbd495eee7f20b159b4454ed10 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 20 Feb 2025 11:13:15 +0500 Subject: [PATCH 075/476] Project Library UI - fix library asset count not considering assets from sublibraries As a result if all elements were assigned to sublibrary, it was showing that library itself has 0 assets and wasn't allowing to expand to see the sublibraries. --- src/bonsai/bonsai/tool/project.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 98f4f76799..53bfe427c5 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -372,6 +372,10 @@ class Project(bonsai.core.tool.Project): props = cls.get_project_props() for project_library in libraries: library_elements = tool.Project.get_project_library_elements(project_library) + subhierarchy = libraries[project_library] + for sublibrary in subhierarchy: + sublibrary_elements = tool.Project.get_project_library_elements(sublibrary) + library_elements.update(sublibrary_elements) props.add_library_project_library( project_library.Name or "Unnamed", len(library_elements), project_library.id() ) From a82611580382016f80551ca82c038ad9aa88b4b2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 20 Feb 2025 11:29:17 +0500 Subject: [PATCH 076/476] Project Library UI - allow expanding libraries hierarchy even if there are no assets yet --- src/bonsai/bonsai/bim/module/project/operator.py | 2 +- src/bonsai/bonsai/bim/module/project/prop.py | 8 +++++++- src/bonsai/bonsai/bim/module/project/ui.py | 2 +- src/bonsai/bonsai/tool/project.py | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 02ee383a6a..4e68b56913 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -237,7 +237,7 @@ class RefreshLibrary(bpy.types.Operator): elements.update(library_file.by_type(importable_type)) rels = tool.Project.get_project_library_rels(library_file) elements = {e for e in elements if not tool.Project.is_element_assigned_to_project_library(e, rels)} - self.props.add_library_project_library("Unassigned", len(elements), 0) + self.props.add_library_project_library("Unassigned", len(elements), 0, False) ifc_project = library_file.by_type("IfcProject")[0] hierarchy = tool.Project.get_project_hierarchy(library_file) diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index 900d92e81f..a570724fee 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -180,6 +180,8 @@ class LibraryElement(PropertyGroup): element_type: EnumProperty(items=[(i, i, "") for i in get_args(LibraryElementType)], name="Element Type") # Asset group. asset_count: IntProperty(name="Asset Count") + # Asset library. + has_sublibraries: BoolProperty(name="Has Sublibraries", default=False) # Asset. ifc_definition_id: IntProperty(name="IFC Definition ID") is_declared: BoolProperty(name="Is Declared", default=False) @@ -194,6 +196,7 @@ class LibraryElement(PropertyGroup): name: str element_type: LibraryElementType asset_count: int + has_sublibraries: bool ifc_definition_id: int is_declared: bool is_appended: bool @@ -402,12 +405,15 @@ class BIMProjectProperties(PropertyGroup): def clipping_planes_objs(self) -> list[bpy.types.Object]: return list({cp.obj for cp in self.clipping_planes if cp.obj}) - def add_library_project_library(self, name: str, asset_count: int, ifc_definition_id: int) -> LibraryElement: + def add_library_project_library( + self, name: str, asset_count: int, ifc_definition_id: int, has_sublibraries: bool + ) -> LibraryElement: new = self.library_elements.add() new["name"] = name new.asset_count = asset_count new.element_type = "LIBRARY" new.ifc_definition_id = ifc_definition_id + new.has_sublibraries = has_sublibraries return new def add_library_asset_class(self, name: str, asset_count: int) -> LibraryElement: diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index b0950b2f15..e49cdb8910 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -512,7 +512,7 @@ class BIM_UL_library(UIList): ): if item: row = layout.row(align=True) - if item.element_type != "ASSET" and item.asset_count > 0: + if item.element_type != "ASSET" and (item.asset_count > 0 or item.has_sublibraries): op = row.operator("bim.change_library_element", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False) op.element_name = item.name op.breadcrumb_type = item.element_type diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 53bfe427c5..6a2bbb0140 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -377,7 +377,7 @@ class Project(bonsai.core.tool.Project): sublibrary_elements = tool.Project.get_project_library_elements(sublibrary) library_elements.update(sublibrary_elements) props.add_library_project_library( - project_library.Name or "Unnamed", len(library_elements), project_library.id() + project_library.Name or "Unnamed", len(library_elements), project_library.id(), bool(subhierarchy) ) @classmethod From 174f909137216b5092406170576b79fcf80a1907 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 20 Feb 2025 16:59:20 +0500 Subject: [PATCH 077/476] Fix #6192 --- .../ifcopenshell/api/style/edit_surface_style.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py index 40f4fcedf4..28419b8d86 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py @@ -81,7 +81,7 @@ class Usecase: def execute(self, style: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: self.style = style - attributes = {} + attribute_types = {} for attribute in style.wrapped_data.declaration().as_entity().all_attributes(): attribute_type = attribute.type_of_attribute() if attribute_type.as_aggregation_type() is None: @@ -89,10 +89,10 @@ class Usecase: else: # doesn't have .declared_type() attribute_type = attribute_type.type_of_element() - attributes[attribute.name()] = attribute_type + attribute_types[attribute.name()] = attribute_type for key, value in attributes.items(): - attribute_class = attributes.get(key) + attribute_class = attribute_types.get(key) if attribute_class == "IfcColourRgb": self.edit_colour_rgb(key, value) elif key == "SpecularHighlight": From abddc739fe64f6c6d0cee549b772002dd95b0379 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 20 Feb 2025 17:19:24 +0500 Subject: [PATCH 078/476] Fix UI for 4bb32b252d --- src/bonsai/bonsai/bim/module/geometry/operator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 724603b976..e3f488284e 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -731,6 +731,7 @@ class OverrideDelete(bpy.types.Operator): if self.is_batch: row = self.layout.row() row.label(text="Warning: Faster deletion will use more memory.", icon="ERROR") + row = self.layout.row() row.label(text="See system console for deletion progress.") def _execute(self, context: bpy.types.Context): From b19ece18c63478491ad19d93d0a130ab82a8fcb6 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 20 Feb 2025 23:21:59 +1100 Subject: [PATCH 079/476] Fix minor regression in linking models due to new tool call. --- src/bonsai/bonsai/bim/module/project/operator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 4e68b56913..3408bd8baa 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1340,6 +1340,7 @@ class LoadLink(bpy.types.Operator): import bpy def run(): + import bonsai.tool as tool gprops = tool.Georeference.get_georeference_props() # Our model origin becomes their host model origin gprops.host_model_origin = "{gprops.model_origin}" From 331f491d5080a71cd4437ab375d95328ee7e4b85 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 20 Feb 2025 14:50:55 +0500 Subject: [PATCH 080/476] typing --- src/bonsai/bonsai/bim/handler.py | 25 +++--- src/bonsai/bonsai/bim/helper.py | 10 ++- src/bonsai/bonsai/bim/ifc.py | 17 ++-- src/bonsai/bonsai/bim/import_ifc.py | 16 ++-- .../bonsai/bim/module/augin/operator.py | 7 +- src/bonsai/bonsai/bim/module/augin/ui.py | 4 +- .../bonsai/bim/module/clash/operator.py | 5 +- .../bonsai/bim/module/debug/operator.py | 6 +- src/bonsai/bonsai/bim/module/debug/ui.py | 3 +- src/bonsai/bonsai/bim/module/diff/operator.py | 10 ++- src/bonsai/bonsai/bim/module/document/data.py | 2 +- .../bonsai/bim/module/document/operator.py | 2 +- src/bonsai/bonsai/bim/module/document/prop.py | 14 +++ src/bonsai/bonsai/bim/module/document/ui.py | 4 +- .../bonsai/bim/module/drawing/operator.py | 17 ++-- .../bonsai/bim/module/project/operator.py | 40 +++++---- src/bonsai/bonsai/bim/module/project/ui.py | 33 +++++-- .../bonsai/bim/module/search/operator.py | 56 +++++++----- src/bonsai/bonsai/bim/module/search/prop.py | 56 ++++++++++-- src/bonsai/bonsai/bim/module/search/ui.py | 7 +- src/bonsai/bonsai/bim/module/unit/prop.py | 23 ++++- src/bonsai/bonsai/bim/module/unit/ui.py | 4 +- src/bonsai/bonsai/bim/module/web/data.py | 6 +- src/bonsai/bonsai/bim/operator.py | 21 +++-- src/bonsai/bonsai/bim/prop.py | 76 ++++++++++++++-- src/bonsai/bonsai/bim/schema.py | 8 ++ src/bonsai/bonsai/bim/ui.py | 22 ++--- src/bonsai/bonsai/tool/blender.py | 17 ++-- src/bonsai/bonsai/tool/brick.py | 3 +- src/bonsai/bonsai/tool/debug.py | 3 +- src/bonsai/bonsai/tool/document.py | 43 +++++---- src/bonsai/bonsai/tool/ifcgit.py | 4 +- src/bonsai/bonsai/tool/polyline.py | 3 +- src/bonsai/bonsai/tool/project.py | 3 +- src/bonsai/bonsai/tool/pset_template.py | 3 +- src/bonsai/bonsai/tool/search.py | 7 +- src/bonsai/bonsai/tool/unit.py | 41 ++++++--- src/bonsai/bonsai/tool/web.py | 3 +- src/bonsai/test/tool/test_brick.py | 5 +- src/bonsai/test/tool/test_debug.py | 3 +- src/bonsai/test/tool/test_document.py | 74 ++++++++-------- src/bonsai/test/tool/test_ifc.py | 6 +- src/bonsai/test/tool/test_unit.py | 87 ++++++++++--------- .../ifcopenshell/api/root/remove_product.py | 74 ++++++++-------- .../api/style/edit_surface_style.py | 2 +- .../ifcopenshell/entity_instance.py | 15 +--- src/ifcopenshell-python/ifcopenshell/file.py | 17 ---- 47 files changed, 577 insertions(+), 330 deletions(-) diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index 287e0bb09d..4ed12d74f0 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -228,7 +228,7 @@ def refresh_ui_data(): @persistent -def loadIfcStore(scene): +def loadIfcStore(scene: bpy.types.Scene) -> None: IfcStore.purge() refresh_ui_data() if not tool.Ifc.get(): @@ -238,19 +238,21 @@ def loadIfcStore(scene): @persistent -def undo_post(scene): - if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction: - IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction - IfcStore.undo(until_key=bpy.context.scene.BIMProperties.last_transaction) +def undo_post(scene: bpy.types.Scene) -> None: + props = tool.Blender.get_bim_props() + if IfcStore.last_transaction != props.last_transaction: + IfcStore.last_transaction = props.last_transaction + IfcStore.undo(until_key=props.last_transaction) refresh_ui_data() tool.Ifc.rebuild_element_maps() @persistent -def redo_post(scene): - if IfcStore.last_transaction != bpy.context.scene.BIMProperties.last_transaction: - IfcStore.last_transaction = bpy.context.scene.BIMProperties.last_transaction - IfcStore.redo(until_key=bpy.context.scene.BIMProperties.last_transaction) +def redo_post(scene: bpy.types.Scene) -> None: + props = tool.Blender.get_bim_props() + if IfcStore.last_transaction != props.last_transaction: + IfcStore.last_transaction = props.last_transaction + IfcStore.redo(until_key=props.last_transaction) refresh_ui_data() tool.Ifc.rebuild_element_maps() @@ -283,7 +285,7 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None return pao -def viewport_shading_changed_callback(area): +def viewport_shading_changed_callback(area: bpy.types.Area) -> None: shading = area.spaces.active.shading.type if shading == "RENDERED": bpy.context.scene.BIMStylesProperties.active_style_type = "External" @@ -341,7 +343,8 @@ def load_post(scene): tool.Blender.setup_tabs() if tool.Ifc.get() and bpy.data.is_saved: - bpy.context.scene.BIMProperties.has_blend_warning = True + props = tool.Blender.get_bim_props() + props.has_blend_warning = True # Bonsai overlays georeference_props = tool.Georeference.get_georeference_props() diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 2d19fa6e1f..3ac994739a 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -32,6 +32,7 @@ from typing import Optional, Callable, Any, Union, Iterable, TYPE_CHECKING if TYPE_CHECKING: import bonsai.bim.prop from bonsai.bim.prop import Attribute + from bonsai.bim.module.search.prop import BIMFilterGroup # ImportCallback return values: # - None - property should be imported by default workflow @@ -372,11 +373,16 @@ def convert_property_group_from_si(property_group: bpy.types.PropertyGroup, skip setattr(property_group, prop_name, prop_value) -def draw_filter(layout: bpy.types.UILayout, filter_groups, data, module: str) -> None: +def draw_filter( + layout: bpy.types.UILayout, + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup], + data, + module: str, +) -> None: if not data.is_loaded: data.load() - sprops = bpy.context.scene.BIMSearchProperties + sprops = tool.Search.get_search_props() if tool.Ifc.get(): row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 044bf74fd2..35b40b7b3c 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -105,7 +105,8 @@ class IfcStore: @staticmethod def get_file(): if IfcStore.file is None: - IfcStore.path = cast(str, bpy.context.scene.BIMProperties.ifc_file) + props = tool.Blender.get_bim_props() + IfcStore.path = props.ifc_file # Interpret relative paths as relative to .blend file. if IfcStore.path and not os.path.isabs(IfcStore.path): IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path)) @@ -119,10 +120,11 @@ class IfcStore: @staticmethod def get_cache(): if IfcStore.cache is None and IfcStore.path: + props = tool.Blender.get_bim_props() ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest() - os.makedirs(bpy.context.scene.BIMProperties.cache_dir, exist_ok=True) - IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5") + os.makedirs(props.cache_dir, exist_ok=True) + IfcStore.cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5") cache_path = Path(IfcStore.cache_path) cache_settings = ifcopenshell.geom.settings() serializer_settings = ifcopenshell.geom.serializer_settings() @@ -162,7 +164,8 @@ class IfcStore: assert IfcStore.file ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest() - new_cache_path = os.path.join(bpy.context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5") + props = tool.Blender.get_bim_props() + new_cache_path = os.path.join(props.cache_dir, f"{ifc_hash}.h5") IfcStore.cache = None try: shutil.move(IfcStore.cache_path, new_cache_path) @@ -414,7 +417,8 @@ class IfcStore: method: Literal["EXECUTE", "INVOKE", "MODAL"] = "EXECUTE", ) -> set[str]: bonsai.last_actions.append({"type": "operator", "name": operator.bl_idname}) - bpy.context.scene.BIMProperties.is_dirty = True + props = tool.Blender.get_bim_props() + props.is_dirty = True # Modals don't nest, and Blender handles the loop that continuously calls modal() is_top_level_operator = not bool(IfcStore.current_transaction) or (method == "MODAL") @@ -493,7 +497,8 @@ class IfcStore: ) -> None: key = getattr(operator, "transaction_key", None) data = getattr(operator, "transaction_data", None) - bpy.context.scene.BIMProperties.last_transaction = key + props = tool.Blender.get_bim_props() + props.last_transaction = key IfcStore.last_transaction = key rollback = rollback or getattr(operator, "rollback", lambda data: True) commit = commit or getattr(operator, "commit", lambda data: True) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 5dd519de28..21c8d16e3e 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -879,17 +879,19 @@ class IfcImporter: def load_file(self): self.ifc_import_settings.logger.info("loading file %s", self.ifc_import_settings.input_file) - if not bpy.context.scene.BIMProperties.ifc_file: - bpy.context.scene.BIMProperties.ifc_file = self.ifc_import_settings.input_file + props = tool.Blender.get_bim_props() + if not props.ifc_file: + props.ifc_file = self.ifc_import_settings.input_file self.file = tool.Ifc.get() def calculate_unit_scale(self): self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) tool.Loader.set_unit_scale(self.unit_scale) - def set_units(self): + def set_units(self) -> None: if not (assignment := self.file.by_type("IfcProject")[0].UnitsInContext): return # Geometry is optional in IFC + props = tool.Blender.get_bim_props() for unit in assignment.Units: if unit.is_a("IfcNamedUnit") and unit.UnitType == "LENGTHUNIT": if unit.is_a("IfcSIUnit"): @@ -909,19 +911,19 @@ class IfcImporter: elif unit.is_a("IfcNamedUnit") and unit.UnitType == "AREAUNIT": name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower() try: - bpy.context.scene.BIMProperties.area_unit = "{}{}".format( + props.area_unit = "{}{}".format( unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name ) except: # Probably an invalid unit. - bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" + props.area_unit = "SQUARE_METRE" elif unit.is_a("IfcNamedUnit") and unit.UnitType == "VOLUMEUNIT": name = unit.Name if unit.is_a("IfcSIUnit") else unit.Name.lower() try: - bpy.context.scene.BIMProperties.volume_unit = "{}{}".format( + props.volume_unit = "{}{}".format( unit.Prefix + "/" if hasattr(unit, "Prefix") and unit.Prefix else "", name ) except: # Probably an invalid unit. - bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" + props.volume_unit = "CUBIC_METRE" def create_project(self): project = self.file.by_type("IfcProject")[0] diff --git a/src/bonsai/bonsai/bim/module/augin/operator.py b/src/bonsai/bonsai/bim/module/augin/operator.py index c4999ab08f..b578beb554 100644 --- a/src/bonsai/bonsai/bim/module/augin/operator.py +++ b/src/bonsai/bonsai/bim/module/augin/operator.py @@ -122,15 +122,16 @@ class AuginCreateNewModel(bpy.types.Operator): context.scene.render.image_settings.file_format = old_file_format context.scene.render.filepath = old_filepath - client.upload_file(context.scene.BIMProperties.ifc_file, result["s3_bucket"], result["model_path"]) + bim_props = tool.Blender.get_bim_props() + client.upload_file(bim_props.ifc_file, result["s3_bucket"], result["model_path"]) client.upload_file(thumb_path, result["s3_bucket"], result["thumb_path"]) # Notify done url = "https://server.auge.pro.br/API/v3/augin_rest.php/files_uploaded" payload = { "user_token": props.token, - "ifc_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file), - "model_filesize": os.path.getsize(context.scene.BIMProperties.ifc_file), + "ifc_filesize": os.path.getsize(bim_props.ifc_file), + "model_filesize": os.path.getsize(bim_props.ifc_file), "thumb_filesize": os.path.getsize(thumb_path), "model_upload_path": result["model_path"], "thumb_upload_path": result["thumb_path"], diff --git a/src/bonsai/bonsai/bim/module/augin/ui.py b/src/bonsai/bonsai/bim/module/augin/ui.py index 0da722f7a6..65ca3ac7f9 100644 --- a/src/bonsai/bonsai/bim/module/augin/ui.py +++ b/src/bonsai/bonsai/bim/module/augin/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy.types +import bonsai.tool as tool class BIM_PT_augin(bpy.types.Panel): @@ -47,7 +48,8 @@ class BIM_PT_augin(bpy.types.Panel): row = layout.row() row.label(text="Logged in as " + props.username) - if not context.scene.BIMProperties.ifc_file: + bim_props = tool.Blender.get_bim_props() + if not bim_props.ifc_file: row = layout.row() row.label(text="No IFC Found") return diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index d8eb640551..c3d87e4759 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -321,8 +321,9 @@ class SelectIfcClashResults(bpy.types.Operator): ifc_file = "" for scene in obj.users_scene: - if scene.BIMProperties.ifc_file: - ifc_file = scene.BIMProperties.ifc_file + bim_props = tool.Blender.get_bim_props(scene) + if bim_props.ifc_file: + ifc_file = bim_props.ifc_file if scene.library: break diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 68e3525a87..4b16c3fb12 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -106,7 +106,8 @@ class ConvertToBlender(bpy.types.Operator): if material.library: continue tool.Ifc.unlink(obj=material) - context.scene.BIMProperties.ifc_file = "" + bim_props = tool.Blender.get_bim_props() + bim_props.ifc_file = "" tool.Debug.get_debug_props().attributes.clear() IfcStore.purge() bonsai.bim.handler.refresh_ui_data() @@ -160,7 +161,8 @@ class ProfileImportIFC(bpy.types.Operator): if not tool.Ifc.get(): cls.poll_message_set("No IFC file loaded.") return False - if not context.scene.BIMProperties.ifc_file: + bim_props = tool.Blender.get_bim_props() + if not bim_props.ifc_file: cls.poll_message_set("Current IFC file is not saved.") return False return True diff --git a/src/bonsai/bonsai/bim/module/debug/ui.py b/src/bonsai/bonsai/bim/module/debug/ui.py index 4fbd5c8ea6..c2c43bd4bb 100644 --- a/src/bonsai/bonsai/bim/module/debug/ui.py +++ b/src/bonsai/bonsai/bim/module/debug/ui.py @@ -34,9 +34,10 @@ class BIM_PT_debug(Panel): layout = self.layout props = tool.Debug.get_debug_props() + bim_props = tool.Blender.get_bim_props() row = self.layout.row(align=True) - row.prop(context.scene.BIMProperties, "ifc_file", text="") + row.prop(bim_props, "ifc_file", text="") row.operator("bim.validate_ifc_file", icon="CHECKMARK", text="") row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="") diff --git a/src/bonsai/bonsai/bim/module/diff/operator.py b/src/bonsai/bonsai/bim/module/diff/operator.py index ead5dd6e04..dc9e981b19 100644 --- a/src/bonsai/bonsai/bim/module/diff/operator.py +++ b/src/bonsai/bonsai/bim/module/diff/operator.py @@ -70,8 +70,9 @@ class VisualiseDiff(bpy.types.Operator): ifc_file = "" for scene in obj.users_scene: - if scene.BIMProperties.ifc_file: - ifc_file = scene.BIMProperties.ifc_file + bim_props = tool.Blender.get_bim_props(scene) + if bim_props.ifc_file: + ifc_file = bim_props.ifc_file if scene.library: break @@ -257,8 +258,9 @@ class SelectDiffObjects(bpy.types.Operator): ifc_file = "" for scene in obj.users_scene: - if scene.BIMProperties.ifc_file: - ifc_file = scene.BIMProperties.ifc_file + bim_props = tool.Blender.get_bim_props(scene) + if bim_props.ifc_file: + ifc_file = bim_props.ifc_file if scene.library: break diff --git a/src/bonsai/bonsai/bim/module/document/data.py b/src/bonsai/bonsai/bim/module/document/data.py index 0a75605b5c..5cde82e499 100644 --- a/src/bonsai/bonsai/bim/module/document/data.py +++ b/src/bonsai/bonsai/bim/module/document/data.py @@ -52,7 +52,7 @@ class DocumentData: @classmethod def parent_document(cls): - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() if len(props.breadcrumbs): parent = tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name)) if tool.Ifc.get_schema() == "IFC2X3": diff --git a/src/bonsai/bonsai/bim/module/document/operator.py b/src/bonsai/bonsai/bim/module/document/operator.py index 13c494d092..2ed1a0a8d5 100644 --- a/src/bonsai/bonsai/bim/module/document/operator.py +++ b/src/bonsai/bonsai/bim/module/document/operator.py @@ -115,7 +115,7 @@ class EditDocument(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() core.edit_document(tool.Ifc, tool.Document, document=tool.Ifc.get().by_id(props.active_document_id)) diff --git a/src/bonsai/bonsai/bim/module/document/prop.py b/src/bonsai/bonsai/bim/module/document/prop.py index 2f8d32408f..ef4fb29ab9 100644 --- a/src/bonsai/bonsai/bim/module/document/prop.py +++ b/src/bonsai/bonsai/bim/module/document/prop.py @@ -30,6 +30,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING def update_document_name(self: "Document", context: bpy.types.Context) -> None: @@ -57,6 +58,11 @@ class Document(PropertyGroup): ) ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + identification: str + is_information: bool + ifc_definition_id: int + class BIMDocumentProperties(PropertyGroup): document_attributes: CollectionProperty(name="Document Attributes", type=Attribute) @@ -65,3 +71,11 @@ class BIMDocumentProperties(PropertyGroup): breadcrumbs: CollectionProperty(name="Breadcrumbs", type=StrProperty) active_document_index: IntProperty(name="Active Document Index") is_editing: BoolProperty(name="Is Editing", default=False) + + if TYPE_CHECKING: + document_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + active_document_id: int + documents: bpy.types.bpy_prop_collection_idprop[Document] + breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty] + active_document_index: int + is_editing: bool diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index c2aff57ed4..4bd935c8f6 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -39,7 +39,7 @@ class BIM_PT_documents(Panel): if not DocumentData.is_loaded: DocumentData.load() - self.props = context.scene.BIMDocumentProperties + self.props = tool.Document.get_document_props() row = self.layout.row(align=True) row.label(text="{} Documents Found".format(DocumentData.data["total_information"]), icon="FILE") @@ -102,7 +102,7 @@ class BIM_PT_object_documents(Panel): obj = context.active_object self.oprops = obj.BIMObjectProperties - self.props = context.scene.BIMDocumentProperties + self.props = tool.Document.get_document_props() self.file = tool.Ifc.get() self.draw_add_ui() diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 3d1c3f46ec..6014015995 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -712,7 +712,8 @@ class CreateDrawing(bpy.types.Operator): } cached_linework -= edited_guids - files = {context.scene.BIMProperties.ifc_file: tool.Ifc.get()} + bim_props = tool.Blender.get_bim_props() + files = {bim_props.ifc_file: tool.Ifc.get()} props = tool.Project.get_project_props() for link in props.links: @@ -730,7 +731,7 @@ class CreateDrawing(bpy.types.Operator): # Don't use draw.main() just whilst we're prototyping and experimenting # TODO: hash paths are never used ifc_hash = hashlib.md5(ifc_path.encode("utf-8")).hexdigest() - ifc_cache_path = os.path.join(context.scene.BIMProperties.cache_dir, f"{ifc_hash}.h5") + ifc_cache_path = os.path.join(bim_props.cache_dir, f"{ifc_hash}.h5") self.serialiser.setFile(ifc) drawing_elements = tool.Drawing.get_drawing_elements(self.camera_element, ifc_file=ifc) @@ -1618,7 +1619,8 @@ class AddDrawingToSheet(bpy.types.Operator, tool.Ifc.Operator): def poll(cls, context): props = tool.Drawing.get_document_props() # Won't be visible in UI anyway. - if not props.sheets or not context.scene.BIMProperties.data_dir: + bim_props = tool.Blender.get_bim_props() + if not props.sheets or not bim_props.data_dir: return False if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") @@ -1722,7 +1724,8 @@ class CreateSheets(bpy.types.Operator, tool.Ifc.Operator): if not tool.Drawing.get_active_sheet_item(is_sheet=True): cls.poll_message_set("No sheet selected.") return False - return props.sheets and context.scene.BIMProperties.data_dir + bim_props = tool.Blender.get_bim_props() + return props.sheets and bim_props.data_dir def invoke(self, context, event): # opening all sheets on shift+click @@ -2523,7 +2526,8 @@ class AddScheduleToSheet(bpy.types.Operator, tool.Ifc.Operator): if not props.schedules: cls.poll_message_set("No schedule selected.") return False - return props.schedules and props.sheets and context.scene.BIMProperties.data_dir + bim_props = tool.Blender.get_bim_props() + return props.schedules and props.sheets and bim_props.data_dir def _execute(self, context): props = tool.Drawing.get_document_props() @@ -2589,7 +2593,8 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator): if not props.references: cls.poll_message_set("No reference selected.") return False - return props.references and props.sheets and context.scene.BIMProperties.data_dir + bim_props = tool.Blender.get_bim_props() + return props.references and props.sheets and bim_props.data_dir def _execute(self, context): props = tool.Drawing.get_document_props() diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 3408bd8baa..b91e0d8158 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -76,34 +76,35 @@ class NewProject(bpy.types.Operator): def execute(self, context): bpy.ops.wm.read_homefile() pprops = tool.Project.get_project_props() + bim_props = tool.Blender.get_bim_props() if self.preset == "metric_m": pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "METERS" - bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" - bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" + bim_props.area_unit = "SQUARE_METRE" + bim_props.volume_unit = "CUBIC_METRE" pprops.template_file = "0" elif self.preset == "metric_mm": pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" - bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" - bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" + bim_props.area_unit = "SQUARE_METRE" + bim_props.volume_unit = "CUBIC_METRE" pprops.template_file = "0" elif self.preset == "imperial_ft": pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "IMPERIAL" bpy.context.scene.unit_settings.length_unit = "FEET" - bpy.context.scene.BIMProperties.area_unit = "square foot" - bpy.context.scene.BIMProperties.volume_unit = "cubic foot" + bim_props.area_unit = "square foot" + bim_props.volume_unit = "cubic foot" pprops.template_file = "0" elif self.preset == "demo": pprops.export_schema = "IFC4" bpy.context.scene.unit_settings.system = "METRIC" bpy.context.scene.unit_settings.length_unit = "MILLIMETERS" - bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" - bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" + bim_props.area_unit = "SQUARE_METRE" + bim_props.volume_unit = "CUBIC_METRE" pprops.template_file = "IFC4 Demo Template.ifc" if self.preset != "wizard": @@ -979,7 +980,8 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): if not self.is_advanced and not self.should_start_fresh_session: bpy.ops.bim.convert_to_blender() - context.scene.BIMProperties.ifc_file = filepath + bim_props = tool.Blender.get_bim_props() + bim_props.ifc_file = filepath if not tool.Ifc.get(): self.report( {"ERROR"}, @@ -1039,13 +1041,15 @@ class RevertProject(bpy.types.Operator, IFCFileSelector): @classmethod def poll(cls, context): - if not context.scene.BIMProperties.ifc_file: + props = tool.Blender.get_bim_props() + if not props.ifc_file: cls.poll_message_set("IFC project need to be loaded and saved on the disk.") return False return True def execute(self, context): - bpy.ops.bim.load_project(should_start_fresh_session=True, filepath=context.scene.BIMProperties.ifc_file) + props = tool.Blender.get_bim_props() + bpy.ops.bim.load_project(should_start_fresh_session=True, filepath=props.ifc_file) return {"FINISHED"} @@ -1070,7 +1074,8 @@ class LoadProjectElements(bpy.types.Operator): filemode="a", level=logging.DEBUG, ) - settings = import_ifc.IfcImportSettings.factory(context, context.scene.BIMProperties.ifc_file, logger) + props = tool.Blender.get_bim_props() + settings = import_ifc.IfcImportSettings.factory(context, props.ifc_file, logger) settings.has_filter = self.props.filter_mode != "NONE" settings.should_filter_spatial_elements = self.props.should_filter_spatial_elements if self.props.filter_mode == "DECOMPOSITION": @@ -1560,7 +1565,8 @@ class ExportIFC(bpy.types.Operator): return {"FINISHED"} self.use_relative_path = tool.Project.get_project_props().use_relative_project_path - if (filepath := context.scene.BIMProperties.ifc_file) and not self.should_save_as: + props = tool.Blender.get_bim_props() + if (filepath := props.ifc_file) and not self.should_save_as: self.filepath = str(tool.Blender.ensure_blender_path_is_abs(Path(filepath))) return self.execute(context) if not self.filepath: @@ -1621,7 +1627,6 @@ class ExportIFC(bpy.types.Operator): print("Export finished in {:.2f} seconds".format(time.time() - start)) # New project created in Bonsai should be in recent projects too. tool.Project.add_recent_ifc_project(Path(output_file)) - scene = context.scene props = tool.Drawing.get_document_props() if not props.ifc_files: new = props.ifc_files.add() @@ -1629,12 +1634,13 @@ class ExportIFC(bpy.types.Operator): props = tool.Project.get_project_props() if props.use_relative_project_path and bpy.data.is_saved: output_file = os.path.relpath(output_file, bpy.path.abspath("//")) - if scene.BIMProperties.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): - scene.BIMProperties.ifc_file = output_file + bim_props = tool.Blender.get_bim_props() + if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): + bim_props.ifc_file = output_file save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath) if save_blend_file: bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath) - bpy.context.scene.BIMProperties.is_dirty = False + bim_props.is_dirty = False bonsai.bim.handler.refresh_ui_data() self.report( {"INFO"}, diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index e49cdb8910..c5d390c780 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -28,7 +28,7 @@ from bonsai.bim.module.project.data import ProjectData, LinksData from typing import TYPE_CHECKING if TYPE_CHECKING: - from bonsai.bim.module.project.prop import LibraryElement, BIMProjectProperties + from bonsai.bim.module.project.prop import LibraryElement, BIMProjectProperties, FilterCategory, Link def file_import_menu(self, context): @@ -151,7 +151,7 @@ class BIM_PT_project(Panel): self.layout.use_property_decorate = False self.layout.use_property_split = True - props = context.scene.BIMProperties + props = tool.Blender.get_bim_props() pprops = self.props = tool.Project.get_project_props() self.file = tool.Ifc.get() if pprops.is_loading: @@ -305,7 +305,7 @@ class BIM_PT_project(Panel): def draw_loaded_project_ui(self, context): # file name row - props = context.scene.BIMProperties + props = tool.Blender.get_bim_props() file_name_row = self.layout.row(align=True) file_name_row.label(text=os.path.basename(props.ifc_file), icon="FILE") self.draw_editing_buttons(context, file_name_row) @@ -315,7 +315,7 @@ class BIM_PT_project(Panel): # file path row and actions section row = self.layout.row(align=True) - if context.scene.BIMProperties.is_dirty: + if props.is_dirty: row.label(text="Saved*", icon="EXPORT") else: row.label(text="Saved", icon="EXPORT") @@ -339,7 +339,7 @@ class BIM_PT_new_project_wizard(Panel): self.layout.use_property_decorate = False self.layout.use_property_split = True - props = context.scene.BIMProperties + props = tool.Blender.get_bim_props() pprops = tool.Project.get_project_props() prop_with_search(self.layout, pprops, "export_schema") row = self.layout.row() @@ -542,7 +542,16 @@ class BIM_UL_library(UIList): class BIM_UL_filter_categories(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMProjectProperties, + item: FilterCategory, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) row.label(text=f"{item.name} ({item.total_elements})") @@ -556,7 +565,17 @@ class BIM_UL_filter_categories(UIList): class BIM_UL_links(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMProjectProperties, + item: Link, + icon, + active_data, + active_propname, + index, + ): if item: row = layout.row(align=True) if item.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 4022c1d5ff..32d3729d58 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -39,6 +39,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Literal, get_args class AddFilterGroup(Operator): @@ -144,13 +145,19 @@ class Search(Operator): bl_idname = "bim.search" bl_label = "Search" - property_group: bpy.props.StringProperty(name="Property Group", default="") + PropertyGroupType = Literal["CsvProperties", "BIMSearchProperties"] + property_group: bpy.props.EnumProperty( + name="Property Group", items=[(i, i, "") for i in get_args(PropertyGroupType)] + ) + + if TYPE_CHECKING: + property_group: PropertyGroupType def execute(self, context): if self.property_group == "CsvProperties": props = context.scene.CsvProperties elif self.property_group == "BIMSearchProperties": - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() else: raise Exception(f"bim.search - unexpected property group name '{self.property_group}'.") @@ -213,11 +220,12 @@ class LoadSearch(Operator, tool.Ifc.Operator): def _execute(self, context): filter_groups = tool.Search.get_filter_groups(self.module) - group = tool.Ifc.get().by_id(int(context.scene.BIMSearchProperties.saved_searches)) + props = tool.Search.get_search_props() + group = tool.Ifc.get().by_id(int(props.saved_searches)) tool.Search.import_filter_query(tool.Search.get_group_query(group), filter_groups) def draw(self, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() row = self.layout.row() row.prop(props, "saved_searches", text="") @@ -239,7 +247,7 @@ class ColourByProperty(Operator): return result def _execute(self, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() query = props.colourscheme_query if props.colourscheme_key == "QUERY" else props.colourscheme_key if not query: @@ -358,11 +366,11 @@ class SelectByProperty(Operator): @classmethod def poll(cls, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() return props.active_colourscheme_index < len(props.colourscheme) def execute(self, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() query = props.colourscheme_query if props.colourscheme_key == "QUERY" else props.colourscheme_key if not query: @@ -420,7 +428,7 @@ class SaveColourscheme(Operator, tool.Ifc.Operator): if not self.name: return - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() query = props.colourscheme_query group = [g for g in tool.Ifc.get().by_type("IfcGroup") if g.Name == self.name] @@ -446,7 +454,7 @@ class LoadColourscheme(Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() group = tool.Ifc.get().by_id(int(props.saved_colourschemes)) description = json.loads(group.Description) props.colourscheme_query = description.get("colourscheme_query") @@ -458,7 +466,7 @@ class LoadColourscheme(Operator, tool.Ifc.Operator): new.colour = data["colour"] def draw(self, context): - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() row = self.layout.row() row.prop(props, "saved_colourschemes", text="") @@ -549,7 +557,7 @@ class ResetObjectColours(Operator): def execute(self, context): for obj in context.visible_objects: obj.color = (1, 1, 1, 1) - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() props.colourscheme.clear() return {"FINISHED"} @@ -562,7 +570,7 @@ class ToggleFilterSelection(Operator): action: EnumProperty(items=(("SELECT", "Select", ""), ("DESELECT", "Deselect", ""))) def execute(self, context): - props = bpy.context.scene.BIMSearchProperties + props = tool.Search.get_search_props() self.selecting_actionbool = self.action == "SELECT" if props.filter_type == "CLASSES": for ifc_class in props.filter_classes: @@ -587,7 +595,7 @@ class ActivateIfcClassFilter(Operator): return True def invoke(self, context, event): - props = bpy.context.scene.BIMSearchProperties + props = tool.Search.get_search_props() props.filter_classes.clear() ifc_types = {} for obj in context.selected_objects: @@ -606,18 +614,20 @@ class ActivateIfcClassFilter(Operator): return context.window_manager.invoke_props_dialog(self, width=250) def execute(self, context): - bpy.context.scene.BIMSearchProperties.filter_classes.clear() + props = tool.Search.get_search_props() + props.filter_classes.clear() return {"FINISHED"} def draw(self, context): + props = tool.Search.get_search_props() self.layout.template_list( "BIM_UL_ifc_class_filter", "", - context.scene.BIMSearchProperties, + props, "filter_classes", - context.scene.BIMSearchProperties, + props, "filter_classes_index", - rows=min(len(bpy.context.scene.BIMSearchProperties.filter_classes), 20), + rows=min(len(props.filter_classes), 20), ) row = self.layout.row(align=True) row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT" @@ -638,7 +648,7 @@ class ActivateContainerFilter(Operator): return True def invoke(self, context, event): - props = bpy.context.scene.BIMSearchProperties + props = tool.Search.get_search_props() props.filter_container.clear() containers = {} @@ -661,18 +671,20 @@ class ActivateContainerFilter(Operator): return context.window_manager.invoke_props_dialog(self, width=250) def execute(self, context): - bpy.context.scene.BIMSearchProperties.filter_container.clear() + props = tool.Search.get_search_props() + props.filter_container.clear() return {"FINISHED"} def draw(self, context): + props = tool.Search.get_search_props() self.layout.template_list( "BIM_UL_ifc_building_storey_filter", "", - context.scene.BIMSearchProperties, + props, "filter_container", - context.scene.BIMSearchProperties, + props, "filter_container_index", - rows=min(len(bpy.context.scene.BIMSearchProperties.filter_container), 20), + rows=min(len(props.filter_container), 20), ) row = self.layout.row(align=True) row.operator("bim.toggle_filter_selection", text="Select All").action = "SELECT" diff --git a/src/bonsai/bonsai/bim/module/search/prop.py b/src/bonsai/bonsai/bim/module/search/prop.py index 08cfb46d8f..61cff222d3 100644 --- a/src/bonsai/bonsai/bim/module/search/prop.py +++ b/src/bonsai/bonsai/bim/module/search/prop.py @@ -33,33 +33,34 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING -def get_element_key(self, context): +def get_element_key(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not SelectSimilarData.is_loaded: SelectSimilarData.load() return SelectSimilarData.data["element_key"] -def get_colourscheme_key(self, context): +def get_colourscheme_key(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not ColourByPropertyData.is_loaded: ColourByPropertyData.load() return ColourByPropertyData.data["colourscheme_key"] -def get_saved_searches(self, context): +def get_saved_searches(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not SearchData.is_loaded: SearchData.load() return SearchData.data["saved_searches"] -def get_saved_colourschemes(self, context): +def get_saved_colourschemes(self: "BIMSearchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not ColourByPropertyData.is_loaded: ColourByPropertyData.load() return ColourByPropertyData.data["saved_colourschemes"] -def update_is_class_selected(self, context): +def update_is_class_selected(self: "BIMFilterClasses", context: bpy.types.Context) -> None: if self.is_selected: for obj in self.unselected_objects: obj.obj.select_set(True) @@ -73,7 +74,7 @@ def update_is_class_selected(self, context): new.obj = obj -def update_is_container_selected(self, context): +def update_is_container_selected(self: "BIMFilterBuildingStoreys", context: bpy.types.Context) -> None: if self.is_selected: for obj in self.unselected_objects: obj.obj.select_set(True) @@ -87,15 +88,15 @@ def update_is_container_selected(self, context): new.obj = obj -def update_show_flat_colours(self, context): +def update_show_flat_colours(self: "BIMSearchProperties", context: bpy.types.Context) -> None: + space = tool.Blender.get_view3d_space() + assert space if self.show_flat_colours: - space = tool.Blender.get_view3d_space() space.shading.light = "FLAT" space.shading.color_type = "OBJECT" space.shading.show_object_outline = True space.shading.show_cavity = True else: - space = tool.Blender.get_view3d_space() space.shading.type = "SOLID" space.shading.light = "STUDIO" space.shading.show_object_outline = True @@ -108,6 +109,11 @@ class BIMFilterClasses(PropertyGroup): total: IntProperty(name="Total") unselected_objects: CollectionProperty(type=ObjProperty, name="Unfiltered Objects") + if TYPE_CHECKING: + is_selected: bool + total: int + unselected_objects: bpy.types.bpy_prop_collection_idprop[ObjProperty] + class BIMFilterBuildingStoreys(PropertyGroup): name: StringProperty(name="Name") @@ -115,12 +121,21 @@ class BIMFilterBuildingStoreys(PropertyGroup): total: IntProperty(name="Total") unselected_objects: CollectionProperty(type=ObjProperty, name="Unfiltered Objects") + if TYPE_CHECKING: + is_selected: bool + total: int + unselected_objects: bpy.types.bpy_prop_collection_idprop[ObjProperty] + class BIMColour(PropertyGroup): name: StringProperty(name="Name") total: IntProperty(name="Total") colour: FloatVectorProperty(name="Colour", subtype="COLOR", default=(1, 0, 0), min=0.0, max=1.0) + if TYPE_CHECKING: + total: int + colour: tuple[float, float, float] + class BIMSearchProperties(PropertyGroup): element_key: EnumProperty(items=get_element_key, name="Element Key") @@ -189,6 +204,29 @@ class BIMSearchProperties(PropertyGroup): filter_container_index: IntProperty(name="Filter Level Index") show_flat_colours: BoolProperty(name="Flat Colours", default=False, update=update_show_flat_colours) + if TYPE_CHECKING: + element_key: str + filter_query: str + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + facet: str + saved_searches: str + saved_colourschemes: str + colourscheme_key: str + colourscheme_query: str + palette: str + min_mode: Literal["AUTO", "MANUAL"] + max_mode: Literal["AUTO", "MANUAL"] + min_value: float + max_value: float + colourscheme: bpy.types.bpy_prop_collection_idprop[BIMColour] + active_colourscheme_index: int + filter_type: str + filter_classes: bpy.types.bpy_prop_collection_idprop[BIMFilterClasses] + filter_classes_index: int + filter_container: bpy.types.bpy_prop_collection_idprop[BIMFilterBuildingStoreys] + filter_container_index: int + show_flat_colours: bool + def get_classes(self, ifc_product): declaration = tool.Ifc.schema().declaration_by_name(ifc_product) diff --git a/src/bonsai/bonsai/bim/module/search/ui.py b/src/bonsai/bonsai/bim/module/search/ui.py index ce0b346027..a9254415e7 100644 --- a/src/bonsai/bonsai/bim/module/search/ui.py +++ b/src/bonsai/bonsai/bim/module/search/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import bonsai.bim.helper from bpy.types import Panel from bonsai.bim.module.search.data import SearchData, ColourByPropertyData, SelectSimilarData @@ -34,7 +35,7 @@ class BIM_PT_search(Panel): if not SearchData.is_loaded: SearchData.load() - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() bonsai.bim.helper.draw_filter(self.layout, props.filter_groups, SearchData, "search") @@ -73,7 +74,7 @@ class BIM_PT_colour_by_property(Panel): if not ColourByPropertyData.is_loaded: ColourByPropertyData.load() - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() row = self.layout.row(align=True) row.label(text=f"{len(ColourByPropertyData.data['saved_colourschemes'])} Saved Colourschemes") @@ -127,7 +128,7 @@ class BIM_PT_select_similar(Panel): if not SelectSimilarData.is_loaded: SelectSimilarData.load() - props = context.scene.BIMSearchProperties + props = tool.Search.get_search_props() if SelectSimilarData.data["element_key"]: row = self.layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/unit/prop.py b/src/bonsai/bonsai/bim/module/unit/prop.py index b01fab4ddf..f0f04e16ae 100644 --- a/src/bonsai/bonsai/bim/module/unit/prop.py +++ b/src/bonsai/bonsai/bim/module/unit/prop.py @@ -30,21 +30,22 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING -def get_unit_classes(self, context): +def get_unit_classes(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not UnitsData.is_loaded: UnitsData.load() return UnitsData.data["unit_classes"] -def get_conversion_unit_types(self, context): +def get_conversion_unit_types(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not UnitsData.is_loaded: UnitsData.load() return UnitsData.data["conversion_unit_types"] -def get_named_unit_types(self, context): +def get_named_unit_types(self: "BIMUnitProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not UnitsData.is_loaded: UnitsData.load() return UnitsData.data["named_unit_types"] @@ -57,6 +58,12 @@ class Unit(PropertyGroup): ifc_class: StringProperty(name="IFC Class") ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + unit_type: str + is_assigned: bool + ifc_class: str + ifc_definition_id: int + class BIMUnitProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing") @@ -67,3 +74,13 @@ class BIMUnitProperties(PropertyGroup): conversion_unit_types: EnumProperty(items=get_conversion_unit_types, name="Conversion Unit Types") named_unit_types: EnumProperty(items=get_named_unit_types, name="Named Unit Types") unit_attributes: CollectionProperty(name="Unit Attributes", type=Attribute) + + if TYPE_CHECKING: + is_editing: bool + units: bpy.types.bpy_prop_collection_idprop[Unit] + active_unit_index: int + active_unit_id: int + unit_classes: str + conversion_unit_types: str + named_unit_types: str + unit_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] diff --git a/src/bonsai/bonsai/bim/module/unit/ui.py b/src/bonsai/bonsai/bim/module/unit/ui.py index 83637845f7..f7b55a96e3 100644 --- a/src/bonsai/bonsai/bim/module/unit/ui.py +++ b/src/bonsai/bonsai/bim/module/unit/ui.py @@ -41,7 +41,7 @@ class BIM_PT_units(Panel): if not UnitsData.is_loaded: UnitsData.load() - self.props = context.scene.BIMUnitProperties + self.props = tool.Unit.get_unit_props() row = self.layout.row(align=True) row.label(text="{} Units Found".format(UnitsData.data["total_units"]), icon="SNAP_GRID") @@ -103,7 +103,7 @@ class BIM_PT_units(Panel): class BIM_UL_units(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - props = context.scene.BIMUnitProperties + props = tool.Unit.get_unit_props() if item: icon = "MOD_MESHDEFORM" if item.ifc_class == "IfcSIUnit": diff --git a/src/bonsai/bonsai/bim/module/web/data.py b/src/bonsai/bonsai/bim/module/web/data.py index fa94ad26d8..db939cadfa 100644 --- a/src/bonsai/bonsai/bim/module/web/data.py +++ b/src/bonsai/bonsai/bim/module/web/data.py @@ -39,9 +39,11 @@ class WebData: @classmethod def get_ifc_file_name(cls): - filename = os.path.basename(bpy.context.scene.BIMProperties.ifc_file) + props = tool.Blender.get_bim_props() + filename = os.path.basename(props.ifc_file) return filename @classmethod def get_is_dirty(cls): - return bpy.context.scene.BIMProperties.is_dirty + props = tool.Blender.get_bim_props() + return props.is_dirty diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 2bd6f445f4..f32cd2ef65 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -130,7 +130,8 @@ class CloseBlendWarning(bpy.types.Operator): bl_label = "Close Blend Warning" def execute(self, context): - bpy.context.scene.BIMProperties.has_blend_warning = False + props = tool.Blender.get_bim_props() + props.has_blend_warning = False return {"FINISHED"} def draw(self, context): @@ -213,11 +214,13 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector): def execute(self, context): if self.is_existing_ifc_file(): - context.scene.BIMProperties.ifc_file = self.get_filepath() + props = tool.Blender.get_bim_props() + props.ifc_file = self.get_filepath() return {"FINISHED"} def invoke(self, context, event): - filepath = Path(context.scene.BIMProperties.ifc_file) + props = tool.Blender.get_bim_props() + filepath = Path(props.ifc_file) res = tool.Blender.operator_invoke_filepath_hotkeys(self, context, event, filepath) if res is not None: return res @@ -575,7 +578,8 @@ class BIM_OT_add_section_plane(bpy.types.Operator): backfacing.location = mix_backfacing.location + Vector((-200, 200)) emission = nodes.new(type="ShaderNodeEmission") - emission.inputs[0].default_value = list(context.scene.BIMProperties.section_plane_colour) + [1] + props = tool.Blender.get_bim_props() + emission.inputs[0].default_value = list(props.section_plane_colour) + [1] emission.location = mix_backfacing.location - Vector((200, 150)) cut_obj = nodes.new(type="ShaderNodeTexCoord") @@ -639,7 +643,8 @@ class BIM_OT_add_section_plane(bpy.types.Operator): material = bpy.data.materials.new("Section Override") material.use_nodes = True - if context.scene.BIMProperties.should_section_selected_objects: + props = tool.Blender.get_bim_props() + if props.should_section_selected_objects: objects = list(context.selected_objects) else: objects = list(context.visible_objects) @@ -814,7 +819,8 @@ class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator): settings.logger.info("Import finished in {:.2f} seconds".format(time.time() - start)) print("Import finished in {:.2f} seconds".format(time.time() - start)) - context.scene.BIMProperties.ifc_file = self.filepath + bim_props = tool.Blender.get_bim_props() + bim_props.ifc_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -852,7 +858,8 @@ class FetchObjectPassport(bpy.types.Operator): def execute(self, context): # TODO: this is dead code, awaiting reimplementation. See #1222. for reference in context.active_object.BIMObjectProperties.document_references: - reference = context.scene.BIMProperties.document_references[reference.name] + bim_props = tool.Blender.get_bim_props() + reference = bim_props.document_references[reference.name] if reference.location[-6:] == ".blend": self.fetch_blender(reference, context) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index c9b8c759d8..e335af9a0e 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -119,23 +119,27 @@ def get_attribute_enum_values(prop: "Attribute", context: bpy.types.Context) -> def update_schema_dir(self: "BIMProperties", context: bpy.types.Context) -> None: import bonsai.bim.schema - bonsai.bim.schema.ifc.schema_dir = context.scene.BIMProperties.schema_dir + bim_props = tool.Blender.get_bim_props() + bonsai.bim.schema.ifc.schema_dir = bim_props.schema_dir def update_data_dir(self: "BIMProperties", context: bpy.types.Context) -> None: import bonsai.bim.schema - bonsai.bim.schema.ifc.data_dir = context.scene.BIMProperties.data_dir + bim_props = tool.Blender.get_bim_props() + bonsai.bim.schema.ifc.data_dir = bim_props.data_dir def update_cache_dir(self: "BIMProperties", context: bpy.types.Context) -> None: import bonsai.bim.schema - bonsai.bim.schema.ifc.cache_dir = context.scene.BIMProperties.cache_dir + bim_props = tool.Blender.get_bim_props() + bonsai.bim.schema.ifc.cache_dir = bim_props.cache_dir def update_ifc_file(self: "BIMProperties", context: bpy.types.Context) -> None: - if context.scene.BIMProperties.ifc_file: + bim_props = tool.Blender.get_bim_props() + if bim_props.ifc_file: bonsai.bim.handler.loadIfcStore(context.scene) @@ -331,10 +335,31 @@ class Attribute(PropertyGroup): if TYPE_CHECKING: name: str + display_name: str description: str ifc_class: str data_type: AttributeDataType special_type: AttributeSpecialType + string_value: str + bool_value: bool + int_value: int + float_value: float + length_value: float + enum_items: str + enum_descriptions: bpy.types.bpy_prop_collection_idprop[StrProperty] + enum_value: str + filepath_value: MultipleFileSelect + filter_glob: str + is_null: bool + is_optional: bool + is_uri: bool + is_selected: bool + value_min: float + value_min_constraint: bool + value_max: float + value_max_constraint: bool + metadata: str + update: str def get_value(self) -> Union[str, float, int, bool, None]: if self.is_optional and self.is_null: @@ -439,6 +464,13 @@ class BIMAreaProperties(PropertyGroup): active_tab: BoolProperty(default=True, name="Active Tab") inactive_tab: BoolProperty(default=False, name="Inactive Tab") + if TYPE_CHECKING: + tab: str + previous_tab: str + alt_tab: str + active_tab: bool + inactive_tab: bool + # BIMAreaProperties exists per area and is setup on load post. However, for new # or temporary screens, they may not be setup yet, so this global tab @@ -449,6 +481,11 @@ class BIMTabProperties(PropertyGroup): active_tab: BoolProperty(default=True, name="Active Tab") inactive_tab: BoolProperty(default=False, name="Inactive Tab") + if TYPE_CHECKING: + tab: str + active_tab: bool + inactive_tab: bool + class BIMProperties(PropertyGroup): is_dirty: BoolProperty(name="Is Dirty", default=False) @@ -517,6 +554,21 @@ class BIMProperties(PropertyGroup): name="IFC Volume Unit", ) + if TYPE_CHECKING: + is_dirty: bool + schema_dir: str + data_dir: str + cache_dir: str + has_blend_warning: bool + pset_dir: str + ifc_file: str + last_transaction: str + should_section_selected_objects: bool + section_plane_colour: tuple[float, float, float] + section_line_decorator_width: float + area_unit: str + volume_unit: str + class IfcParameter(PropertyGroup): name: StringProperty(name="Name") @@ -547,6 +599,7 @@ class PsetQto(PropertyGroup): class GlobalId(PropertyGroup): name: StringProperty(name="Name") + ifc_definition_id: IntProperty(name="IFC Definition ID") class BIMCollectionProperties(PropertyGroup): @@ -556,11 +609,14 @@ class BIMCollectionProperties(PropertyGroup): obj: Union[bpy.types.Object, None] +BlenderOffsetType = Literal["NONE", "OBJECT_PLACEMENT", "CARTESIAN_POINT", "NOT_APPLICABLE"] + + class BIMObjectProperties(PropertyGroup): collection: PointerProperty(type=bpy.types.Collection) ifc_definition_id: IntProperty(name="IFC Definition ID") blender_offset_type: EnumProperty( - items=[(o, o, "") for o in ["NONE", "OBJECT_PLACEMENT", "CARTESIAN_POINT", "NOT_APPLICABLE"]], + items=[(o, o, "") for o in get_args(BlenderOffsetType)], name="Blender Offset", default="NONE", ) @@ -570,6 +626,16 @@ class BIMObjectProperties(PropertyGroup): location_checksum: StringProperty(name="Location Checksum") rotation_checksum: StringProperty(name="Rotation Checksum") + if TYPE_CHECKING: + collection: Union[bpy.types.Collection, None] + ifc_definition_id: int + blender_offset_type: BlenderOffsetType + cartesian_point_offset: str + is_reassigning_class: bool + is_renaming: bool + location_checksum: str + rotation_checksum: str + def get_profiles(self: "BIMMeshProperties", context: bpy.types.Context): from bonsai.bim.module.model.data import ItemData diff --git a/src/bonsai/bonsai/bim/schema.py b/src/bonsai/bonsai/bim/schema.py index 6d67d2e20a..cae1e32146 100644 --- a/src/bonsai/bonsai/bim/schema.py +++ b/src/bonsai/bonsai/bim/schema.py @@ -19,6 +19,8 @@ import ifcopenshell import ifcopenshell.util.pset import bonsai.tool as tool +import bpy +import bpy_restrict_state class IfcSchema: @@ -47,10 +49,16 @@ class IfcSchema: self.psetqto.get_applicable.cache_clear() self.psetqto.get_applicable_names.cache_clear() self.psetqto.get_by_name.cache_clear() + + # During register we cannot access the context either way. + if isinstance(bpy.context, bpy_restrict_state._RestrictContext): + return for path in tool.Blender.get_data_dir_paths("pset", "*.ifc"): self.psetqto.templates.append(ifcopenshell.open(path)) +# TODO: do we really need to load it on module import? +# Loading it on IFC load should be enough. ifc = IfcSchema() diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index a77650a72b..4223706b8a 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -121,7 +121,7 @@ class BIM_PT_section_plane(Panel): def draw(self, context): layout = self.layout layout.use_property_split = True - props = context.scene.BIMProperties + props = tool.Blender.get_bim_props() layout.prop(props, "should_section_selected_objects") layout.prop(props, "section_plane_colour") @@ -381,12 +381,13 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): layout.prop(props, "occurrence_name_function") def draw_directories(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: + props = tool.Blender.get_bim_props() row = layout.row(align=True) - row.prop(context.scene.BIMProperties, "data_dir") + row.prop(props, "data_dir") row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.data_dir" row = layout.row(align=True) - row.prop(context.scene.BIMProperties, "cache_dir") + row.prop(props, "cache_dir") row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "scene.BIMProperties.cache_dir" row = layout.row(align=True) @@ -394,7 +395,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences): row.operator("bim.select_dir", icon="FILE_FOLDER", text="").data_path = "preferences.tmp_dir" def draw_drawing_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: - layout.prop(context.scene.BIMProperties, "pset_dir") + props = tool.Blender.get_bim_props() + layout.prop(props, "pset_dir") dprops = tool.Drawing.get_document_props() layout.prop(dprops, "sheets_dir") layout.prop(dprops, "layouts_dir") @@ -510,8 +512,8 @@ class BIM_PT_tabs(Panel): if not tool.Ifc.get(): return - props = context.scene.BIMProperties - if props.has_blend_warning: + bim_props = tool.Blender.get_bim_props() + if bim_props.has_blend_warning: box = self.layout.box() box.alert = True row = box.row(align=True) @@ -557,11 +559,11 @@ class BIM_PT_tab_new_project_wizard(Panel): def poll(cls, context): if not tool.Blender.is_tab(context, "PROJECT"): return False - props = context.scene.BIMProperties + bim_props = tool.Blender.get_bim_props() pprops = tool.Project.get_project_props() if pprops.is_loading: return False - elif tool.Ifc.get() or props.ifc_file: + elif tool.Ifc.get() or bim_props.ifc_file: return False return True @@ -579,11 +581,11 @@ class BIM_PT_tab_project_info(Panel): def poll(cls, context): if not tool.Blender.is_tab(context, "PROJECT"): return False - props = context.scene.BIMProperties + bim_props = tool.Blender.get_bim_props() pprops = tool.Project.get_project_props() if pprops.is_loading: return True - elif tool.Ifc.get() or props.ifc_file: + elif tool.Ifc.get() or bim_props.ifc_file: return True return False diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index b9699de172..a4a2ed4558 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -37,9 +37,12 @@ from mathutils import Vector from pathlib import Path from functools import lru_cache from bonsai.bim.ifc import IFC_CONNECTED_TYPE -from typing import Any, Optional, Union, Literal, Iterable, Callable, TypeVar, Generator +from typing import Any, Optional, Union, Literal, Iterable, Callable, TypeVar, Generator, TYPE_CHECKING from typing_extensions import assert_never +if TYPE_CHECKING: + from bonsai.bim.prop import BIMProperties + VIEWPORT_ATTRIBUTES = [ "view_matrix", @@ -1477,10 +1480,8 @@ class Blender(bonsai.core.tool.Blender): @classmethod def get_user_data_dir(cls) -> Path: - try: - return Path(bpy.context.scene.BIMProperties.data_dir) - except AttributeError: - return Path() + props = tool.Blender.get_bim_props() + return Path(props.data_dir) @classmethod def get_data_dir_path(cls, relative_path: Union[str, Path]) -> Path: @@ -1538,3 +1539,9 @@ class Blender(bonsai.core.tool.Blender): dct = {cls.bl_idname: cls.ifc_element_type for cls in (BimTool.__subclasses__())} return types.MappingProxyType(dct) + + @classmethod + def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties: + if scene is None: + scene = bpy.context.scene + return scene.BIMProperties diff --git a/src/bonsai/bonsai/tool/brick.py b/src/bonsai/bonsai/tool/brick.py index 4b034f2160..acd8d50b01 100644 --- a/src/bonsai/bonsai/tool/brick.py +++ b/src/bonsai/bonsai/tool/brick.py @@ -86,10 +86,11 @@ class Brick(bonsai.core.tool.Brick): project = tool.Ifc.get().by_type("IfcProject")[0] ns = Namespace(namespace) brick_project = ns[project.GlobalId] + props = tool.Blender.get_bim_props() with BrickStore.new_changeset() as cs: cs.add((brick_project, A, REF.ifcProject)) cs.add((brick_project, REF.ifcProjectID, Literal(project.GlobalId))) - cs.add((brick_project, REF.ifcFileLocation, Literal(bpy.context.scene.BIMProperties.ifc_file))) + cs.add((brick_project, REF.ifcFileLocation, Literal(props.ifc_file))) if project.Name: cs.add((brick_project, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal(project.Name))) return str(brick_project) diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index ec4490b2c3..7752423ce4 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -54,7 +54,8 @@ class Debug(bonsai.core.tool.Debug): @classmethod def purge_hdf5_cache(cls) -> None: - cache_dir = bpy.context.scene.BIMProperties.cache_dir + props = tool.Blender.get_bim_props() + cache_dir = props.cache_dir filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")] for f in filelist: try: diff --git a/src/bonsai/bonsai/tool/document.py b/src/bonsai/bonsai/tool/document.py index 25799948fc..5d1e5eee54 100644 --- a/src/bonsai/bonsai/tool/document.py +++ b/src/bonsai/bonsai/tool/document.py @@ -16,56 +16,68 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import ifcopenshell.util.system import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool -from typing import Any, Union, Sequence +from typing import Any, Union, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.document.prop import BIMDocumentProperties class Document(bonsai.core.tool.Document): + @classmethod + def get_document_props(cls) -> BIMDocumentProperties: + return bpy.context.scene.BIMDocumentProperties + @classmethod def add_breadcrumb(cls, document: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() new = props.breadcrumbs.add() new.name = str(document.id()) @classmethod def clear_breadcrumbs(cls) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() props.breadcrumbs.clear() @classmethod def clear_document_tree(cls) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() props.documents.clear() @classmethod def disable_editing_document(cls) -> None: - bpy.context.scene.BIMDocumentProperties.active_document_id = 0 + props = cls.get_document_props() + props.active_document_id = 0 @classmethod def disable_editing_ui(cls) -> None: - bpy.context.scene.BIMDocumentProperties.is_editing = False + props = cls.get_document_props() + props.is_editing = False @classmethod def enable_editing_ui(cls) -> None: - bpy.context.scene.BIMDocumentProperties.is_editing = True + props = cls.get_document_props() + props.is_editing = True @classmethod def export_document_attributes(cls) -> dict[str, Any]: - return bonsai.bim.helper.export_attributes(bpy.context.scene.BIMDocumentProperties.document_attributes) + props = cls.get_document_props() + return bonsai.bim.helper.export_attributes(props.document_attributes) @classmethod def get_active_breadcrumb(cls) -> Union[ifcopenshell.entity_instance, None]: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() if len(props.breadcrumbs): return tool.Ifc.get().by_id(int(props.breadcrumbs[-1].name)) @classmethod def import_document_attributes(cls, document: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() props.document_attributes.clear() def callback(attr_name: str, _, data: dict[str, Any]) -> Union[bool, None]: @@ -86,7 +98,7 @@ class Document(bonsai.core.tool.Document): @classmethod def import_project_documents(cls) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() props.documents.clear() project = tool.Ifc.get().by_type("IfcProject")[0] for rel in project.HasAssociations or []: @@ -100,7 +112,7 @@ class Document(bonsai.core.tool.Document): @classmethod def import_references(cls, document: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() is_ifc2x3 = tool.Ifc.get_schema() == "IFC2X3" references = cls.get_document_references(document) for element in references: @@ -115,7 +127,7 @@ class Document(bonsai.core.tool.Document): @classmethod def import_subdocuments(cls, document: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() if document.IsPointer: for element in document.IsPointer[0].RelatedDocuments or []: new = props.documents.add() @@ -130,13 +142,14 @@ class Document(bonsai.core.tool.Document): @classmethod def remove_latest_breadcrumb(cls) -> None: - props = bpy.context.scene.BIMDocumentProperties + props = cls.get_document_props() if len(props.breadcrumbs): props.breadcrumbs.remove(len(props.breadcrumbs) - 1) @classmethod def set_active_document(cls, document: ifcopenshell.entity_instance) -> None: - bpy.context.scene.BIMDocumentProperties.active_document_id = document.id() + props = cls.get_document_props() + props.active_document_id = document.id() @classmethod def get_document_information_id(cls, document: ifcopenshell.entity_instance) -> Union[str, None]: diff --git a/src/bonsai/bonsai/tool/ifcgit.py b/src/bonsai/bonsai/tool/ifcgit.py index 5b0f9b88e5..93475c090c 100644 --- a/src/bonsai/bonsai/tool/ifcgit.py +++ b/src/bonsai/bonsai/tool/ifcgit.py @@ -340,8 +340,8 @@ class IfcGit: @classmethod def get_revisions_step_ids(cls) -> Union[STEP_IDS, None]: - - path_ifc = bpy.data.scenes["Scene"].BIMProperties.ifc_file + props = tool.Blender.get_bim_props() + path_ifc = tool.Blender.get_bim_props().ifc_file props = bpy.context.scene.IfcGitProperties repo = IfcGitRepo.repo item = props.ifcgit_commits[props.commit_index] diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index db8f4e7922..56eab4a288 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -456,7 +456,8 @@ class Polyline(bonsai.core.tool.Polyline): dprops = tool.Drawing.get_document_props() precision = dprops.imperial_precision if is_area: - area_unit = bpy.context.scene.BIMProperties.area_unit + props = tool.Blender.get_bim_props() + area_unit = props.area_unit unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), unit_type=area_unit) else: precision = None diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 6a2bbb0140..3f4d77f0d7 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -70,7 +70,8 @@ class Project(bonsai.core.tool.Project): @classmethod def load_pset_templates(cls): - pset_dir = tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir) + props = tool.Blender.get_bim_props() + pset_dir = tool.Ifc.resolve_uri(props.pset_dir) if os.path.isdir(pset_dir): for path in Path(pset_dir).glob("*.ifc"): bonsai.bim.schema.ifc.psetqto.templates.append(ifcopenshell.open(path)) diff --git a/src/bonsai/bonsai/tool/pset_template.py b/src/bonsai/bonsai/tool/pset_template.py index e2b69705b6..8977d1e32c 100644 --- a/src/bonsai/bonsai/tool/pset_template.py +++ b/src/bonsai/bonsai/tool/pset_template.py @@ -133,7 +133,8 @@ class PsetTemplate(bonsai.core.tool.PsetTemplate): for f in tool.Blender.get_data_dir_paths("pset", "*.ifc"): paths.append((f, "Global Pset Template")) - pset_dir = Path(tool.Ifc.resolve_uri(bpy.context.scene.BIMProperties.pset_dir)) + props = tool.Blender.get_bim_props() + pset_dir = Path(tool.Ifc.resolve_uri(props.pset_dir)) if pset_dir.is_dir(): for path in Path(pset_dir).glob("*.ifc"): paths.append((path, "Project Pset Template")) diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index 3b90961559..a48e1404b6 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -30,9 +30,14 @@ from typing import Union, Literal, TYPE_CHECKING if TYPE_CHECKING: from bonsai.bim.prop import BIMFilterGroup + from bonsai.bim.module.search.prop import BIMSearchProperties class Search(bonsai.core.tool.Search): + @classmethod + def get_search_props(cls) -> BIMSearchProperties: + return bpy.context.scene.BIMSearchProperties + @classmethod def get_group_query(cls, group: ifcopenshell.entity_instance) -> str: return json.loads(group.Description)["query"] @@ -40,7 +45,7 @@ class Search(bonsai.core.tool.Search): @classmethod def get_filter_groups(cls, module: str) -> bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]: if module == "search": - return bpy.context.scene.BIMSearchProperties.filter_groups + return cls.get_search_props().filter_groups elif module == "csv": return bpy.context.scene.CsvProperties.filter_groups elif module == "diff": diff --git a/src/bonsai/bonsai/tool/unit.py b/src/bonsai/bonsai/tool/unit.py index 37597cf95c..808ea5ead0 100644 --- a/src/bonsai/bonsai/tool/unit.py +++ b/src/bonsai/bonsai/tool/unit.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import json import math @@ -23,24 +24,34 @@ import ifcopenshell import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool -from typing import Union, Literal, Any +from typing import Union, Literal, Any, TYPE_CHECKING from typing_extensions import assert_never +if TYPE_CHECKING: + from bonsai.bim.module.unit.prop import BIMUnitProperties + class Unit(bonsai.core.tool.Unit): UNIT_TYPE = Literal["LENGTHUNIT", "AREAUNIT", "VOLUMEUNIT"] + @classmethod + def get_unit_props(cls) -> BIMUnitProperties: + return bpy.context.scene.BIMUnitProperties + @classmethod def clear_active_unit(cls) -> None: - bpy.context.scene.BIMUnitProperties.active_unit_id = 0 + props = cls.get_unit_props() + props.active_unit_id = 0 @classmethod def disable_editing_units(cls) -> None: - bpy.context.scene.BIMUnitProperties.is_editing = False + props = cls.get_unit_props() + props.is_editing = False @classmethod def enable_editing_units(cls) -> None: - bpy.context.scene.BIMUnitProperties.is_editing = True + props = cls.get_unit_props() + props.is_editing = True @classmethod def export_unit_attributes(cls) -> dict[str, Any]: @@ -52,11 +63,12 @@ class Unit(bonsai.core.tool.Unit): attributes[prop.name] = (0, 0, 0, 0, 0, 0, 0) return True - props = bpy.context.scene.BIMUnitProperties + props = cls.get_unit_props() return bonsai.bim.helper.export_attributes(props.unit_attributes, callback=callback) @classmethod def get_scene_unit_name(cls, unit_type: UNIT_TYPE) -> str: + bim_props = tool.Blender.get_bim_props() if unit_type == "LENGTHUNIT": props = bpy.context.scene.unit_settings if props.length_unit == "MILES": @@ -69,23 +81,24 @@ class Unit(bonsai.core.tool.Unit): return "thou" return "foot" elif unit_type == "AREAUNIT": - return bpy.context.scene.BIMProperties.area_unit + return bim_props.area_unit elif unit_type == "VOLUMEUNIT": - return bpy.context.scene.BIMProperties.volume_unit + return bim_props.volume_unit else: assert_never() @classmethod def get_scene_unit_si_prefix(cls, unit_type: UNIT_TYPE) -> Union[str, None]: + bim_props = tool.Blender.get_bim_props() if unit_type == "LENGTHUNIT": props = bpy.context.scene.unit_settings if props.length_unit == "ADAPTIVE" or props.length_unit == "METERS": return return props.length_unit.replace("METERS", "") elif unit_type == "AREAUNIT": - unit = bpy.context.scene.BIMProperties.area_unit + unit = bim_props.area_unit elif unit_type == "VOLUMEUNIT": - unit = bpy.context.scene.BIMProperties.volume_unit + unit = bim_props.volume_unit else: assert_never(unit_type) if "/" in unit: @@ -93,9 +106,11 @@ class Unit(bonsai.core.tool.Unit): @classmethod def import_unit_attributes(cls, unit: ifcopenshell.entity_instance) -> None: + props = cls.get_unit_props() + def callback(name, prop, data): if name == "Dimensions" and data["type"] != "IfcSIUnit": - new = bpy.context.scene.BIMUnitProperties.unit_attributes.add() + new = props.unit_attributes.add() new.name = name new.is_null = data[name] is None new.is_optional = False @@ -103,13 +118,12 @@ class Unit(bonsai.core.tool.Unit): new.string_value = json.dumps([e for e in tool.Ifc.get().by_id(data["id"]).Dimensions]) return True - props = bpy.context.scene.BIMUnitProperties props.unit_attributes.clear() bonsai.bim.helper.import_attributes2(unit, props.unit_attributes, callback=callback) @classmethod def import_units(cls) -> None: - props = bpy.context.scene.BIMUnitProperties + props = tool.Unit.get_unit_props() props.units.clear() units = [] @@ -158,7 +172,8 @@ class Unit(bonsai.core.tool.Unit): @classmethod def set_active_unit(cls, unit: ifcopenshell.entity_instance) -> None: - bpy.context.scene.BIMUnitProperties.active_unit_id = unit.id() + props = cls.get_unit_props() + props.active_unit_id = unit.id() @classmethod def get_project_currency_unit(cls) -> Union[ifcopenshell.entity_instance, None]: diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index 13f5ff7897..e37911b75e 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -730,7 +730,8 @@ class Web(bonsai.core.tool.Web): if operator_data["type"] == "getDrawings": drawings_data = [] sheets_data = [] - ifc_file_dir = os.path.dirname(bpy.context.scene.BIMProperties.ifc_file) + props = tool.Blender.get_bim_props() + ifc_file_dir = os.path.dirname(props.ifc_file) sheets = [d for d in tool.Ifc.get().by_type("IfcDocumentInformation") if d.Scope == "SHEET"] for sheet in sorted(sheets, key=lambda s: getattr(s, "Identification", getattr(s, "DocumentId", None))): diff --git a/src/bonsai/test/tool/test_brick.py b/src/bonsai/test/tool/test_brick.py index c2d10d699f..b26a2acd43 100644 --- a/src/bonsai/test/tool/test_brick.py +++ b/src/bonsai/test/tool/test_brick.py @@ -122,11 +122,10 @@ class TestAddBrickifcProject(NewFile): result = subject.add_brickifc_project("http://example.org/digitaltwin#") assert result == f"http://example.org/digitaltwin#{project.GlobalId}" brick = URIRef(result) + props = tool.Blender.get_bim_props() assert list(BrickStore.graph.triples((brick, A, REF.ifcProject))) assert list(BrickStore.graph.triples((brick, REF.ifcProjectID, Literal(project.GlobalId)))) - assert list( - BrickStore.graph.triples((brick, REF.ifcFileLocation, Literal(bpy.context.scene.BIMProperties.ifc_file))) - ) + assert list(BrickStore.graph.triples((brick, REF.ifcFileLocation, Literal(props.ifc_file)))) assert list( BrickStore.graph.triples( (brick, URIRef("http://www.w3.org/2000/01/rdf-schema#label"), Literal("My Project")) diff --git a/src/bonsai/test/tool/test_debug.py b/src/bonsai/test/tool/test_debug.py index d7d6a95c2c..888a717f4d 100644 --- a/src/bonsai/test/tool/test_debug.py +++ b/src/bonsai/test/tool/test_debug.py @@ -55,7 +55,8 @@ class TestLoadExpress(NewFile): class TestPurgeHdf5Cache(NewFile): def test_run(self): - cache_dir = Path(bpy.context.scene.BIMProperties.cache_dir) + props = tool.Blender.get_bim_props() + cache_dir = Path(props.cache_dir) test_file = cache_dir / "test.h5" test_file.parent.mkdir(parents=True, exist_ok=True) test_file.touch() diff --git a/src/bonsai/test/tool/test_document.py b/src/bonsai/test/tool/test_document.py index 30ce8b5721..d60b9fbe31 100644 --- a/src/bonsai/test/tool/test_document.py +++ b/src/bonsai/test/tool/test_document.py @@ -36,13 +36,13 @@ class TestAddBreadcrumb(NewFile): tool.Ifc().set(ifc) document = ifc.createIfcDocumentInformation() subject.add_breadcrumb(document) - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() assert props.breadcrumbs[0].name == str(document.id()) class TestClearBreadcrumbs(NewFile): def test_run(self): - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() props.breadcrumbs.add() subject.clear_breadcrumbs() assert len(props.breadcrumbs) == 0 @@ -50,7 +50,7 @@ class TestClearBreadcrumbs(NewFile): class TestClearDocumentTree(NewFile): def test_run(self): - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() new = props.documents.add() subject.clear_document_tree() assert len(props.documents) == 0 @@ -58,23 +58,26 @@ class TestClearDocumentTree(NewFile): class TestDisableEditingDocument(NewFile): def test_run(self): - bpy.context.scene.BIMDocumentProperties.active_document_id = 1 + props = tool.Document.get_document_props() + props.active_document_id = 1 subject.disable_editing_document() - assert bpy.context.scene.BIMDocumentProperties.active_document_id == 0 + assert props.active_document_id == 0 class TestDisableEditingUI(NewFile): def test_run(self): - bpy.context.scene.BIMDocumentProperties.is_editing = True + props = tool.Document.get_document_props() + props.is_editing = True subject.disable_editing_ui() - assert bpy.context.scene.BIMDocumentProperties.is_editing == False + assert props.is_editing == False class TestEnableEditingUI(NewFile): def test_run(self): - bpy.context.scene.BIMDocumentProperties.is_editing = False + props = tool.Document.get_document_props() + props.is_editing = False subject.enable_editing_ui() - assert bpy.context.scene.BIMDocumentProperties.is_editing == True + assert props.is_editing == True class TestExportDocumentAttributes(NewFile): @@ -129,22 +132,22 @@ class TestImportDocumentAttributes(NewFile): document.Confidentiality = "CONFIDENTIAL" document.Status = "DRAFT" subject().import_document_attributes(document) - props = bpy.context.scene.BIMDocumentProperties - assert props.document_attributes.get("Identification").string_value == "Identification" - assert props.document_attributes.get("Name").string_value == "Name" - assert props.document_attributes.get("Description").string_value == "Description" - assert props.document_attributes.get("Location").string_value == "Location" - assert props.document_attributes.get("Purpose").string_value == "Purpose" - assert props.document_attributes.get("IntendedUse").string_value == "IntendedUse" - assert props.document_attributes.get("Scope").string_value == "Scope" - assert props.document_attributes.get("Revision").string_value == "Revision" - assert props.document_attributes.get("CreationTime").string_value == "CreationTime" - assert props.document_attributes.get("LastRevisionTime").string_value == "LastRevisionTime" - assert props.document_attributes.get("ElectronicFormat").string_value == "ElectronicFormat" - assert props.document_attributes.get("ValidFrom").string_value == "ValidFrom" - assert props.document_attributes.get("ValidUntil").string_value == "ValidUntil" - assert props.document_attributes.get("Confidentiality").enum_value == "CONFIDENTIAL" - assert props.document_attributes.get("Status").enum_value == "DRAFT" + props = tool.Document.get_document_props() + assert props.document_attributes["Identification"].string_value == "Identification" + assert props.document_attributes["Name"].string_value == "Name" + assert props.document_attributes["Description"].string_value == "Description" + assert props.document_attributes["Location"].string_value == "Location" + assert props.document_attributes["Purpose"].string_value == "Purpose" + assert props.document_attributes["IntendedUse"].string_value == "IntendedUse" + assert props.document_attributes["Scope"].string_value == "Scope" + assert props.document_attributes["Revision"].string_value == "Revision" + assert props.document_attributes["CreationTime"].string_value == "CreationTime" + assert props.document_attributes["LastRevisionTime"].string_value == "LastRevisionTime" + assert props.document_attributes["ElectronicFormat"].string_value == "ElectronicFormat" + assert props.document_attributes["ValidFrom"].string_value == "ValidFrom" + assert props.document_attributes["ValidUntil"].string_value == "ValidUntil" + assert props.document_attributes["Confidentiality"].enum_value == "CONFIDENTIAL" + assert props.document_attributes["Status"].enum_value == "DRAFT" def test_importing_reference(self): ifc = ifcopenshell.file() @@ -155,11 +158,11 @@ class TestImportDocumentAttributes(NewFile): document.Name = "Name" document.Description = "Description" subject().import_document_attributes(document) - props = bpy.context.scene.BIMDocumentProperties - assert props.document_attributes.get("Location").string_value == "Location" - assert props.document_attributes.get("Identification").string_value == "Identification" - assert props.document_attributes.get("Name").string_value == "Name" - assert props.document_attributes.get("Description").string_value == "Description" + props = tool.Document.get_document_props() + assert props.document_attributes["Location"].string_value == "Location" + assert props.document_attributes["Identification"].string_value == "Identification" + assert props.document_attributes["Name"].string_value == "Name" + assert props.document_attributes["Description"].string_value == "Description" class TestImportProjectDocuments(NewFile): @@ -169,7 +172,7 @@ class TestImportProjectDocuments(NewFile): ifc.createIfcProject() document = ifcopenshell.api.run("document.add_information", ifc) subject.import_project_documents() - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() assert len(props.documents) == 1 assert props.documents[0].ifc_definition_id == document.id() assert props.documents[0].name == "Unnamed" @@ -185,7 +188,7 @@ class TestImportReferences(NewFile): document = ifcopenshell.api.run("document.add_information", ifc) reference = ifcopenshell.api.run("document.add_reference", ifc, information=document) subject.import_references(document) - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() assert len(props.documents) == 1 assert props.documents[0].ifc_definition_id == reference.id() assert props.documents[0].name == "Unnamed" @@ -201,7 +204,7 @@ class TestImportSubdocuments(NewFile): document = ifcopenshell.api.run("document.add_information", ifc) subdocument = ifcopenshell.api.run("document.add_information", ifc, parent=document) subject.import_subdocuments(document) - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() assert len(props.documents) == 1 assert props.documents[0].ifc_definition_id == subdocument.id() assert props.documents[0].name == "Unnamed" @@ -220,7 +223,7 @@ class TestIsDocumentInformation(NewFile): class TestRemoveLatestBreadcrumb(NewFile): def test_run(self): - props = bpy.context.scene.BIMDocumentProperties + props = tool.Document.get_document_props() props.breadcrumbs.add() props.breadcrumbs.add() subject.remove_latest_breadcrumb() @@ -232,4 +235,5 @@ class TestSetActiveDocument(NewFile): ifc = ifcopenshell.file() document = ifc.createIfcDocumentInformation() subject.set_active_document(document) - assert bpy.context.scene.BIMDocumentProperties.active_document_id == document.id() + props = tool.Document.get_document_props() + assert props.active_document_id == document.id() diff --git a/src/bonsai/test/tool/test_ifc.py b/src/bonsai/test/tool/test_ifc.py index 641a8adfa0..aedcea3465 100644 --- a/src/bonsai/test/tool/test_ifc.py +++ b/src/bonsai/test/tool/test_ifc.py @@ -40,12 +40,14 @@ class TestSet(test.bim.bootstrap.NewFile): class TestGet(test.bim.bootstrap.NewFile): def test_getting_an_ifc_dataset_from_a_ifc_spf_filepath(self): assert subject.get() is None - bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc" + props = tool.Blender.get_bim_props() + props.ifc_file = "test/files/basic.ifc" result = subject.get() assert isinstance(result, ifcopenshell.file) def test_getting_the_active_ifc_dataset_regardless_of_ifc_path(self): - bpy.context.scene.BIMProperties.ifc_file = "test/files/basic.ifc" + props = tool.Blender.get_bim_props() + props.ifc_file = "test/files/basic.ifc" ifc = ifcopenshell.file() subject.set(ifc) assert subject.get() == ifc diff --git a/src/bonsai/test/tool/test_unit.py b/src/bonsai/test/tool/test_unit.py index 4a8512c360..a3bad12ab3 100644 --- a/src/bonsai/test/tool/test_unit.py +++ b/src/bonsai/test/tool/test_unit.py @@ -18,6 +18,9 @@ import bpy import ifcopenshell +import ifcopenshell.api.project +import ifcopenshell.api.root +import ifcopenshell.api.unit import bonsai.core.tool import bonsai.tool as tool from test.bim.bootstrap import NewFile @@ -31,23 +34,26 @@ class TestImplementsTool(NewFile): class TestClearActiveUnit(NewFile): def test_run(self): - bpy.context.scene.BIMUnitProperties.active_unit_id = 1 + props = tool.Unit.get_unit_props() + props.active_unit_id = 1 subject.clear_active_unit() - assert bpy.context.scene.BIMUnitProperties.active_unit_id == 0 + assert props.active_unit_id == 0 class TestDisableEditingUnits(NewFile): def test_run(self): - bpy.context.scene.BIMUnitProperties.is_editing = True + props = tool.Unit.get_unit_props() + props.is_editing = True subject.disable_editing_units() - assert bpy.context.scene.BIMUnitProperties.is_editing == False + assert props.is_editing == False class TestEnableEditingUnits(NewFile): def test_run(self): - bpy.context.scene.BIMUnitProperties.is_editing = False + props = tool.Unit.get_unit_props() + props.is_editing = False subject.enable_editing_units() - assert bpy.context.scene.BIMUnitProperties.is_editing == True + assert props.is_editing == True class TestExportUnitAttributes(NewFile): @@ -98,10 +104,11 @@ class TestExportUnitAttributes(NewFile): class TestGetSceneUnitName(NewFile): def test_getting_an_imperial_name(self): + props = tool.Blender.get_bim_props() bpy.context.scene.unit_settings.system = "IMPERIAL" bpy.context.scene.unit_settings.length_unit = "MILES" - bpy.context.scene.BIMProperties.area_unit = "square foot" - bpy.context.scene.BIMProperties.volume_unit = "cubic inch" + props.area_unit = "square foot" + props.volume_unit = "cubic inch" assert subject.get_scene_unit_name("LENGTHUNIT") == "mile" assert subject.get_scene_unit_name("AREAUNIT") == "square foot" assert subject.get_scene_unit_name("VOLUMEUNIT") == "cubic inch" @@ -142,13 +149,14 @@ class TestGetSceneUnitSIPrefix: assert subject.get_scene_unit_si_prefix("LENGTHUNIT") == "KILO" bpy.context.scene.unit_settings.length_unit = "ADAPTIVE" assert subject.get_scene_unit_si_prefix("LENGTHUNIT") is None - bpy.context.scene.BIMProperties.area_unit = "SQUARE_METRE" + props = tool.Blender.get_bim_props() + props.area_unit = "SQUARE_METRE" assert subject.get_scene_unit_si_prefix("AREAUNIT") is None - bpy.context.scene.BIMProperties.area_unit = "MILLI/SQUARE_METRE" + props.area_unit = "MILLI/SQUARE_METRE" assert subject.get_scene_unit_si_prefix("AREAUNIT") == "MILLI" - bpy.context.scene.BIMProperties.volume_unit = "CUBIC_METRE" + props.volume_unit = "CUBIC_METRE" assert subject.get_scene_unit_si_prefix("VOLUMEUNIT") is None - bpy.context.scene.BIMProperties.volume_unit = "MILLI/CUBIC_METRE" + props.volume_unit = "MILLI/CUBIC_METRE" assert subject.get_scene_unit_si_prefix("VOLUMEUNIT") == "MILLI" @@ -159,25 +167,25 @@ class TestImportUnitAttributes(NewFile): unit.UnitType = "ANGULARVELOCITYUNIT" unit.UserDefinedType = "UserDefinedType" subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("UnitType").enum_value == "ANGULARVELOCITYUNIT" - assert props.unit_attributes.get("UserDefinedType").string_value == "UserDefinedType" + props = tool.Unit.get_unit_props() + assert props.unit_attributes["UnitType"].enum_value == "ANGULARVELOCITYUNIT" + assert props.unit_attributes["UserDefinedType"].string_value == "UserDefinedType" def test_importing_monetary_units(self): tool.Ifc.set(ifc := ifcopenshell.file()) unit = ifc.createIfcMonetaryUnit() unit.Currency = "Currency" subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("Currency").string_value == "Currency" + props = tool.Unit.get_unit_props() + assert props.unit_attributes["Currency"].string_value == "Currency" def test_importing_monetary_units_ifc2x3(self): tool.Ifc.set(ifc := ifcopenshell.file(schema="IFC2X3")) unit = ifc.createIfcMonetaryUnit() unit.Currency = "USD" subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("Currency").enum_value == "USD" + props = tool.Unit.get_unit_props() + assert props.unit_attributes["Currency"].enum_value == "USD" def test_importing_context_dependent_units(self): ifc = ifcopenshell.file() @@ -187,10 +195,10 @@ class TestImportUnitAttributes(NewFile): unit.Name = "Name" unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7) subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" - assert props.unit_attributes.get("Name").string_value == "Name" - assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]" + props = tool.Unit.get_unit_props() + assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes["Name"].string_value == "Name" + assert props.unit_attributes["Dimensions"].string_value == "[1, 2, 3, 4, 5, 6, 7]" def test_importing_conversion_based_units(self): ifc = ifcopenshell.file() @@ -200,10 +208,10 @@ class TestImportUnitAttributes(NewFile): unit.Name = "Name" unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7) subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" - assert props.unit_attributes.get("Name").string_value == "Name" - assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]" + props = tool.Unit.get_unit_props() + assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes["Name"].string_value == "Name" + assert props.unit_attributes["Dimensions"].string_value == "[1, 2, 3, 4, 5, 6, 7]" def test_importing_conversion_based_with_offset_units(self): ifc = ifcopenshell.file() @@ -214,11 +222,11 @@ class TestImportUnitAttributes(NewFile): unit.Dimensions = ifc.createIfcDimensionalExponents(1, 2, 3, 4, 5, 6, 7) unit.ConversionOffset = 1 subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" - assert props.unit_attributes.get("Name").string_value == "Name" - assert props.unit_attributes.get("Dimensions").string_value == "[1, 2, 3, 4, 5, 6, 7]" - assert props.unit_attributes.get("ConversionOffset").float_value == 1 + props = tool.Unit.get_unit_props() + assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes["Name"].string_value == "Name" + assert props.unit_attributes["Dimensions"].string_value == "[1, 2, 3, 4, 5, 6, 7]" + assert props.unit_attributes["ConversionOffset"].float_value == 1 def test_importing_si_units(self): ifc = ifcopenshell.file() @@ -228,11 +236,11 @@ class TestImportUnitAttributes(NewFile): unit.Prefix = "EXA" unit.Name = "AMPERE" subject.import_unit_attributes(unit) - props = bpy.context.scene.BIMUnitProperties - assert props.unit_attributes.get("UnitType").enum_value == "ABSORBEDDOSEUNIT" - assert props.unit_attributes.get("Prefix").enum_value == "EXA" - assert props.unit_attributes.get("Name").enum_value == "AMPERE" - assert props.unit_attributes.get("Dimensions") is None + props = tool.Unit.get_unit_props() + assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT" + assert props.unit_attributes["Prefix"].enum_value == "EXA" + assert props.unit_attributes["Name"].enum_value == "AMPERE" + assert props.unit_attributes["Dimensions"] is None class TestImportUnits(NewFile): @@ -248,7 +256,7 @@ class TestImportUnits(NewFile): ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject") ifcopenshell.api.unit.assign_unit(ifc, units=[unit2]) subject.import_units() - props = bpy.context.scene.BIMUnitProperties + props = tool.Unit.get_unit_props() assert len(props.units) == 6 assert props.units[0].ifc_definition_id == unit1.id() @@ -311,4 +319,5 @@ class TestSetActiveUnit(NewFile): ifc = ifcopenshell.file() unit = ifc.createIfcSIUnit() subject.set_active_unit(unit) - assert bpy.context.scene.BIMUnitProperties.active_unit_id == unit.id() + props = tool.Unit.get_unit_props() + assert props.active_unit_id == unit.id() diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py index 2df1b4eaa5..1ab4bc432e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/remove_product.py @@ -62,42 +62,38 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc # No we don't. ifcopenshell.api.root.remove_product(model, product=wall) """ - settings = {"product": product} - - representations = [] - if settings["product"].is_a("IfcProduct"): - if settings["product"].Representation: - representations = settings["product"].Representation.Representations or [] + representations: list[ifcopenshell.entity_instance] = [] + if product.is_a("IfcProduct"): + if product.Representation: + representations = product.Representation.Representations or [] else: representations = [] # remove object placements - object_placement = settings["product"].ObjectPlacement + object_placement = product.ObjectPlacement if object_placement: if file.get_total_inverses(object_placement) == 1: - settings["product"].ObjectPlacement = None # remove the inverse for remove_deep2 to work + product.ObjectPlacement = None # remove the inverse for remove_deep2 to work ifcopenshell.util.element.remove_deep2(file, object_placement) - elif settings["product"].is_a("IfcTypeProduct"): - representations = [rm.MappedRepresentation for rm in settings["product"].RepresentationMaps or []] + elif product.is_a("IfcTypeProduct"): + representations = [rm.MappedRepresentation for rm in product.RepresentationMaps or []] # remove psets - psets = settings["product"].HasPropertySets or [] + psets = product.HasPropertySets or [] for pset in psets: if file.get_total_inverses(pset) != 1: continue - ifcopenshell.api.pset.remove_pset(file, product=settings["product"], pset=pset) + ifcopenshell.api.pset.remove_pset(file, product=product, pset=pset) for representation in representations: - ifcopenshell.api.geometry.unassign_representation( - file, product=settings["product"], representation=representation - ) - ifcopenshell.api.geometry.remove_representation(file, **{"representation": representation}) - for opening in getattr(settings["product"], "HasOpenings", []) or []: + ifcopenshell.api.geometry.unassign_representation(file, product=product, representation=representation) + ifcopenshell.api.geometry.remove_representation(file, representation=representation) + for opening in getattr(product, "HasOpenings", []) or []: ifcopenshell.api.feature.remove_feature(file, feature=opening.RelatedOpeningElement) - if settings["product"].is_a("IfcGrid"): - for axis in settings["product"].UAxes + settings["product"].VAxes + (settings["product"].WAxes or ()): + if product.is_a("IfcGrid"): + for axis in product.UAxes + product.VAxes + (product.WAxes or ()): ifcopenshell.api.grid.remove_grid_axis(file, axis=axis) def element_exists(element_id): @@ -108,22 +104,20 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc return False # TODO: remove object placement and other relationships - for inverse_id in [i.id() for i in file.get_inverse(settings["product"])]: + for inverse_id in [i.id() for i in file.get_inverse(product)]: try: inverse = file.by_id(inverse_id) except: continue if inverse.is_a("IfcRelDefinesByProperties"): - ifcopenshell.api.pset.remove_pset( - file, product=settings["product"], pset=inverse.RelatingPropertyDefinition - ) + ifcopenshell.api.pset.remove_pset(file, product=product, pset=inverse.RelatingPropertyDefinition) elif inverse.is_a("IfcRelAssociatesMaterial"): - ifcopenshell.api.material.unassign_material(file, products=[settings["product"]]) + ifcopenshell.api.material.unassign_material(file, products=[product]) elif inverse.is_a("IfcRelDefinesByType"): - if inverse.RelatingType == settings["product"]: + if inverse.RelatingType == product: ifcopenshell.api.type.unassign_type(file, related_objects=inverse.RelatedObjects) else: - ifcopenshell.api.type.unassign_type(file, related_objects=[settings["product"]]) + ifcopenshell.api.type.unassign_type(file, related_objects=[product]) elif inverse.is_a("IfcRelSpaceBoundary"): ifcopenshell.api.boundary.remove_boundary(file, boundary=inverse) elif inverse.is_a("IfcRelFillsElement"): @@ -142,7 +136,7 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelNests"): - if inverse.RelatingObject == settings["product"]: + if inverse.RelatingObject == product: inverse_id = inverse.id() for subelement in inverse.RelatedObjects: if subelement.is_a("IfcDistributionPort"): @@ -153,27 +147,27 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - elif inverse.RelatedObjects == (settings["product"],): + elif inverse.RelatedObjects == (product,): history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelAggregates"): - if inverse.RelatingObject == settings["product"] or len(inverse.RelatedObjects) == 1: + if inverse.RelatingObject == product or len(inverse.RelatedObjects) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelContainedInSpatialStructure"): - if inverse.RelatingStructure == settings["product"] or len(inverse.RelatedElements) == 1: + if inverse.RelatingStructure == product or len(inverse.RelatedElements) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelConnectsElements"): if inverse.is_a("IfcRelConnectsWithRealizingElements"): - if settings["product"] not in (inverse.RelatingElement, inverse.RelatedElement) and any( - el for el in inverse.RealizingElements if el != settings["product"] + if product not in (inverse.RelatingElement, inverse.RelatedElement) and any( + el for el in inverse.RealizingElements if el != product ): continue history = inverse.OwnerHistory @@ -181,15 +175,15 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelConnectsPortToElement"): - if inverse.RelatedElement == settings["product"]: + if inverse.RelatedElement == product: ifcopenshell.api.root.remove_product(file, product=inverse.RelatingPort) - elif inverse.RelatingPort == settings["product"]: + elif inverse.RelatingPort == product: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelConnectsPorts"): - if settings["product"] not in (inverse.RelatingPort, inverse.RelatedPort): + if product not in (inverse.RelatingPort, inverse.RelatedPort): # if it's not RelatingPort/RelatedPort then it's optional RealizingElement # so we keep the relationship continue @@ -204,7 +198,7 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelAssignsToProduct"): - if inverse.RelatingProduct == settings["product"]: + if inverse.RelatingProduct == product: history = inverse.OwnerHistory file.remove(inverse) if history: @@ -215,17 +209,17 @@ def remove_product(file: ifcopenshell.file, product: ifcopenshell.entity_instanc if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelFlowControlElements"): - if inverse.RelatingFlowElement == settings["product"]: + if inverse.RelatingFlowElement == product: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - elif inverse.RelatedControlElements == (settings["product"],): + elif inverse.RelatedControlElements == (product,): history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["product"].OwnerHistory - file.remove(settings["product"]) + history = product.OwnerHistory + file.remove(product) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py index 28419b8d86..dba4dd48ea 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py @@ -81,7 +81,7 @@ class Usecase: def execute(self, style: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: self.style = style - attribute_types = {} + attribute_types: dict[str, str] = {} for attribute in style.wrapped_data.declaration().as_entity().all_attributes(): attribute_type = attribute.type_of_attribute() if attribute_type.as_aggregation_type() is None: diff --git a/src/ifcopenshell-python/ifcopenshell/entity_instance.py b/src/ifcopenshell-python/ifcopenshell/entity_instance.py index dcd3b74cb0..15e5980d41 100644 --- a/src/ifcopenshell-python/ifcopenshell/entity_instance.py +++ b/src/ifcopenshell-python/ifcopenshell/entity_instance.py @@ -248,14 +248,10 @@ class entity_instance: :param f: A callable that takes a single argument and returns a boolean value. It represents the condition. - :type f: Callable :param g: A callable that takes a single argument and returns a transformed value. It represents the transformation. - :type g: Callable :param value: Any object, the input value to be processed - :type value: Any :return: Transformed value - :rtype: Any Example: @@ -304,8 +300,6 @@ class entity_instance: """Return the data type of a positional attribute of the element :param attr: The index or name of the attribute - :type attr: Union[int, str] - :rtype: string """ attr_idx = attr if isinstance(attr, numbers.Integral) else self.wrapped_data.get_argument_index(attr) return self.wrapped_data.get_argument_type(attr_idx) @@ -314,8 +308,6 @@ class entity_instance: """Return the name of a positional attribute of the element :param attr_idx: The index of the attribute - :type attr_idx: int - :rtype: string """ return self.wrapped_data.get_argument_name(attr_idx) @@ -405,9 +397,7 @@ class entity_instance: returned IFC class name should include schema name (e.g. "IFC4.IfcWall" if `True` and "IfcWall" if `False`). If omitted will act as `False`. - :type args: Union[str, bool] :returns: Either the name of the class, or a boolean if it passes the check - :rtype: Union[str, bool] Example: @@ -423,10 +413,7 @@ class entity_instance: return self.wrapped_data.is_a(*args) def id(self) -> int: - """Return the STEP numerical identifier - - :rtype: int - """ + """Return the STEP numerical identifier""" return self.wrapped_data.id() def __eq__(self, other: "entity_instance") -> bool: diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index d3c94b616d..328d98b27f 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -359,7 +359,6 @@ class file: of those methods. :param type: Case insensitive name of the IFC class - :type type: string :param args: The positional arguments of the IFC class :param kwargs: The keyword arguments of the IFC class :returns: An entity instance @@ -482,12 +481,10 @@ class file: """Return an IFC entity instance filtered by IFC ID. :param id: STEP numerical identifier - :type id: int :raises RuntimeError: If `id` is not found. :returns: An ifcopenshell.entity_instance - :rtype: ifcopenshell.entity_instance """ return self[id] @@ -495,13 +492,10 @@ class file: """Return an IFC entity instance filtered by IFC GUID. :param guid: GlobalId value in 22-character encoded form - :type guid: string :raises RuntimeError: If `guid` is not found. :returns: An ifcopenshell.entity_instance - :rtype: ifcopenshell.entity_instance - """ return self[guid] @@ -510,9 +504,7 @@ class file: If the entity already exists, it is not re-added. Existence of entity is checked by it's `.identity()`. :param inst: The entity instance to add - :type inst: ifcopenshell.entity_instance :returns: An ifcopenshell.entity_instance - :rtype: ifcopenshell.entity_instance """ if self.transaction: @@ -530,14 +522,11 @@ class file: If an IFC type class has subclasses, all entities of those subclasses are also returned. :param type: The case insensitive type of IFC class to return. - :type type: string :param include_subtypes: Whether or not to return subtypes of the IFC class - :type include_subtypes: bool :raises RuntimeError: If `type` is not found in IFC schema. :returns: A list of ifcopenshell.entity_instance objects - :rtype: list[ifcopenshell.entity_instance] """ if include_subtypes: return [entity_instance(e, self) for e in self.wrapped_data.by_type(type)] @@ -625,9 +614,7 @@ class file: significantly faster. :param inst: The entity instance to get inverse relationships - :type inst: ifcopenshell.entity_instance :returns: The total number of references - :rtype: int """ return self.wrapped_data.get_total_inverses(inst.wrapped_data) @@ -639,8 +626,6 @@ class file: the reference to the deleted will be removed from the aggregate. :param inst: The entity instance to delete - :type inst: ifcopenshell.entity_instance - :rtype: None """ if self.transaction: self.transaction.store_delete(inst) @@ -676,9 +661,7 @@ class file: if None. Supported formats : .ifc, .ifcXML, .ifcZIP (equivalent to format=".ifc" with zipped=True) For zipped .ifcXML use format=".ifcXML" with zipped=True - :type format: str :param zipped: zip the file after it is written - :type zipped: bool Example: From 25d8fff5fdd8c282e695feb8275cd72bcae02730 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 20 Feb 2025 17:43:17 +0500 Subject: [PATCH 081/476] remove_representation - small optimizations check schema version instead of accessing attribute directly (hasattr under the hood is just doing getattr and checking whether it returns AttributeError), so one less IFC access Similar thing with element.Item to access IFC just once. --- .../ifcopenshell/api/geometry/remove_representation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py index e8cd98aefc..66b374ca6d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py @@ -37,6 +37,7 @@ def remove_representation( :param should_keep_named_profiles: If true, named profile defs will not be removed as they are assumed to be significant. """ + is_ifc2x3 = file.schema == "IFC2X3" styled_items = set() presentation_layer_assignments = set() textures = set() @@ -46,9 +47,7 @@ def remove_representation( if subelement.is_a("IfcRepresentationItem"): [styled_items.add(s) for s in subelement.StyledByItem or []] # IFC2X3 is using LayerAssignments - for s in ( - subelement.LayerAssignment if hasattr(subelement, "LayerAssignment") else subelement.LayerAssignments - ): + for s in subelement.LayerAssignment if not is_ifc2x3 else subelement.LayerAssignments: presentation_layer_assignments.add(s) # IfcTessellatedFaceSet inverses [textures.add(t) for t in getattr(subelement, "HasTextures", []) or []] @@ -77,7 +76,8 @@ def remove_representation( to_delete = file.to_delete or set() for element in styled_items: - if not element.Item or element.Item in to_delete: + item = element.Item + if not item or item in to_delete: file.remove(element) for element in presentation_layer_assignments: if all(item in to_delete for item in element.AssignedItems): From ae4f1253711edd2e5990cf82e0c4c6e49e53e5a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 19 Feb 2025 16:13:35 -0300 Subject: [PATCH 082/476] Fix auto-angle snapping in polyline tools. When locked to a plane, the mouse will be loosely locked to and angle that is divided by 15 degrees. This was not working properly for "XZ" and "YZ" planes. --- src/bonsai/bonsai/tool/snap.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 10636f031a..249d856579 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -193,7 +193,7 @@ class Snap(bonsai.core.tool.Snap): rot_intersection = rot_mat @ translated_intersection proximity = rot_intersection.y if tool_state.plane_method == "XZ": - proximity = rot_intersection.z + proximity = rot_intersection.x is_on_rot_axis = abs(proximity) <= stick_factor if is_on_rot_axis: @@ -208,10 +208,14 @@ class Snap(bonsai.core.tool.Snap): # If lock axis is on it will use the snap angle so there is no need to search for eligible axis if elegible_axis or tool_state.lock_axis: # Adapt axis to make snap angle work with other plane method - if tool_state.plane_method == "XZ": - axis = -axis - if tool_state.plane_method == "YZ": - axis = 90 - (axis * -1) + if elegible_axis: + if tool_state.plane_method == "XZ": + axis = 90 - (axis * -1) + else: + if tool_state.plane_method == "XZ": + axis = -axis + if tool_state.plane_method == "YZ": + axis = 90 - (axis * -1) rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis) rot_mat = tool.Polyline.use_transform_orientations(rot_mat) rot_intersection = rot_mat @ translated_intersection From d37baa4e0cb3b10896f03c581944c02f028aac73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 19 Feb 2025 16:18:22 -0300 Subject: [PATCH 083/476] Fix calculation for polyline tool when working with X, Y and Z. --- src/bonsai/bonsai/bim/module/model/polyline.py | 2 +- src/bonsai/bonsai/tool/polyline.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index d3c014586a..7dd7382127 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -597,7 +597,7 @@ class PolylineOperator: self.report({"WARNING"}, "The number typed is not valid.") return is_valid else: - if self.input_type in {"X", "Y"}: + if self.input_type in {"X", "Y", "Z"}: tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) elif self.input_type in {"D", "A"}: tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 56eab4a288..4442bab9c0 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -267,10 +267,20 @@ class Polyline(bonsai.core.tool.Polyline): snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0] snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) - if tool_state.use_default_container: - snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) + if tool_state.is_input_on: + if tool_state.use_default_container: + mouse_vector = Vector( + (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), default_container_elevation) + ) + else: + mouse_vector = Vector( + (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), input_ui.get_number_value("Z")) + ) else: - snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) + if tool_state.use_default_container: + snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) + else: + snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) if len(polyline_points) > 1: second_to_last_point_data = polyline_points[len(polyline_points) - 2] From 9f0d408e79cd3fcdb0ff5b08c65c407fd3e6319c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 20 Feb 2025 18:46:23 -0300 Subject: [PATCH 084/476] See #6190. Polyline tool support for XZ and YZ planes for profiles. --- .../bonsai/bim/module/model/polyline.py | 137 +++++++++--------- src/bonsai/bonsai/bim/module/model/profile.py | 35 +++-- 2 files changed, 94 insertions(+), 78 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 7dd7382127..d8ea5c0eda 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -429,71 +429,78 @@ def get_horizontal_profile_preview_data(context, relating_type): case "9": grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts] - # Create profile curve - scale_mat = Matrix.Scale(-1, 4, (1.0, 0.0, 0.0)) - grouped_verts = [scale_mat @ Vector(v) for v in grouped_verts] - profile_curve = bpy.data.curves.new("Profile", type="CURVE") - profile_curve.dimensions = "2D" - profile_curve.splines.new("POLY") - profile_curve.splines[0].points.add(len(grouped_verts)) - - for i, point in enumerate(profile_curve.splines[0].points): - if i == len(grouped_verts): # Close curve - point.co = Vector((*grouped_verts[0], 0)) - continue - point.co = Vector((*grouped_verts[i], 0)) - profile_obj = bpy.data.objects.new("Profile", profile_curve) - - # Create path curve with profile object as bevel - path_curve = bpy.data.curves.new("Polyline", type="CURVE") - path_curve.dimensions = "2D" - path_curve.splines.new("POLY") - path_curve.splines[0].points.add(len(polyline_verts) - 1) - for i, point in enumerate(path_curve.splines[0].points): - point.co = Vector((*polyline_verts[i], 0)) - path_curve.splines[0].use_smooth = False - path_curve.bevel_mode = "OBJECT" - path_curve.bevel_object = profile_obj - - # Convert path curve to mesh - # This operation throws a warning when done during gpu drawing, so it was removed from the decorator file to be handled here - path_obj = bpy.data.objects.new("Preview", path_curve) - context.scene.collection.objects.link(path_obj) - bpy.context.view_layer.objects.active = path_obj - dg = context.evaluated_depsgraph_get() - path_obj = path_obj.evaluated_get(dg) - me = path_obj.to_mesh() - - # Create bmesh from path mesh - bm = bmesh.new() - new_verts = [bm.verts.new(v.co) for v in me.vertices] - index = [[v for v in edge.vertices] for edge in me.edges] - new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index] - for face in me.polygons: - verts = [new_verts[i] for i in face.vertices] - bm.faces.new(verts) - bm.verts.index_update() - bm.edges.index_update() - tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] - - bpy.data.objects.remove(bpy.data.objects[path_obj.name], do_unlink=True) - bpy.data.objects.remove(bpy.data.objects[profile_obj.name], do_unlink=True) - try: - bpy.data.curves.remove(profile_obj.data, do_unlink=True) - except: - pass - try: - bpy.data.curves.remove(path_obj.data, do_unlink=True) - except: - pass - data = {} - data["verts"] = [tuple(v.co) for v in bm.verts] - data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges] + data["verts"] = [] + data["edges"] = [] + data["tris"] = [] + + grouped_verts = [(v) for v in grouped_verts] + + all_bm = bmesh.new() + for i in range(len(polyline_verts) - 1): + mesh = bpy.data.meshes.new("TempMesh") + # Create the initial mesh from the profile verts + bm = create_bmesh_from_vertices(grouped_verts, is_closed=True) + bm.verts.ensure_lookup_table() + # Creates the clipping plane formed by two segments. + # The first one is for the profile start, based on the current and previous segment of the polyline. + # The second is for the profile end, based on the current and the next segment. + if i == 0: + d = (polyline_verts[i+1] - polyline_verts[i]).normalized() + clip_start = d + else: + d1 = (polyline_verts[i] - polyline_verts[i-1]).normalized() + d2 = (polyline_verts[i] - polyline_verts[i+1]).normalized() + clip_start = (d1-d2).normalized() + + if i == len(polyline_verts) - 2: + d = (polyline_verts[i+1] - polyline_verts[i]).normalized() + clip_end = d + else: + d1 = (polyline_verts[i+1] - polyline_verts[i]).normalized() + d2 = (polyline_verts[i+1] - polyline_verts[i+2]).normalized() + clip_end = (d1-d2).normalized() + + # Rotates the profile face to the right direction + direction = polyline_verts[i+1] - polyline_verts[i] + position = polyline_verts[i] + rotation_matrix = direction.to_track_quat('Z', 'Y').to_matrix().to_4x4() + bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix) + bmesh.ops.translate(bm, verts=bm.verts, vec=position) + bmesh.ops.translate(bm, verts=bm.verts, vec=-direction) + + # Extrude and move the new face + last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:]) + new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)] + bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3) + # Apply the cutting planes + cut = bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], plane_co=polyline_verts[i], plane_no=clip_start, clear_inner=True) + bm.verts.index_update() + bm.edges.index_update() + cut = bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], plane_co=polyline_verts[i+1], plane_no=clip_end, clear_outer=True) + + bm.to_mesh(mesh) + bm.free() + mesh.update() + all_bm.from_mesh(mesh) + bpy.data.meshes.remove(bpy.data.meshes["TempMesh"]) + + # It's necessary to add the mesh to an object to get the expected result. + mesh = bpy.data.meshes.new("TempMesh2") + all_bm.to_mesh(mesh) + all_bm.free() + obj = bpy.data.objects.new('TempObj', mesh) + bm = bmesh.new() + bm.from_mesh(obj.data) + bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"]) + + verts = [tuple(v.co) for v in bm.verts] + edges = [[v.index for v in e.verts] for e in bm.edges] + tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] + data["verts"] = verts + data["edges"] = edges data["tris"] = tris - bm.free() - return data @@ -632,21 +639,21 @@ class PolylineOperator: if x: if event.shift and event.value == "PRESS" and event.type == "X": self.tool_state.use_default_container = False - self.tool_state.plane_method = "YZ" + self.tool_state.plane_method = "YZ" if self.tool_state.plane_method !="YZ" else None self.tool_state.axis_method = None tool.Blender.update_viewport() if y: if event.shift and event.value == "PRESS" and event.type == "Y": self.tool_state.use_default_container = False - self.tool_state.plane_method = "XZ" + self.tool_state.plane_method = "XZ" if self.tool_state.plane_method !="XZ" else None self.tool_state.axis_method = None tool.Blender.update_viewport() if z: if event.shift and event.value == "PRESS" and event.type == "Z": self.tool_state.use_default_container = False - self.tool_state.plane_method = "XY" + self.tool_state.plane_method = "XY" if self.tool_state.plane_method !="XY" else None self.tool_state.axis_method = None tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index d96e7d0b51..8448d4d857 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -48,6 +48,7 @@ class DumbProfileGenerator: self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) def generate(self, insertion_type="CURSOR"): + self.insertion_type = insertion_type self.file = tool.Ifc.get() self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) material = ifcopenshell.util.element.get_material(self.relating_type) @@ -70,9 +71,9 @@ class DumbProfileGenerator: self.rotation = 0 self.location = Vector((0, 0, 0)) self.cardinal_point = int(props.cardinal_point) - if insertion_type == "POLYLINE": + if self.insertion_type == "POLYLINE": return self.derive_from_polyline() - elif insertion_type == "CURSOR": + elif self.insertion_type == "CURSOR": return self.derive_from_cursor() def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]: @@ -107,13 +108,17 @@ class DumbProfileGenerator: matrix_world = Matrix() if self.relating_type.is_a() not in ("IfcColumnType", "IfcPileType"): - matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world - matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world + if self.insertion_type not in {"POLYLINE"}: + matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world + matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world + else: + rotation_matrix = self.direction.to_track_quat('Z', 'Y') + matrix_world = rotation_matrix.to_matrix().to_4x4() @ matrix_world + matrix_world.translation = self.location - if self.container_obj: + if self.insertion_type not in {"POLYLINE"} and self.container_obj: matrix_world.translation.z = self.container_obj.location.z - element = bonsai.core.root.assign_class( tool.Ifc, tool.Collector, @@ -171,19 +176,19 @@ class DumbProfileGenerator: return obj def create_profile_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]: - direction = coords[1] - coords[0] - length = direction.length + self.direction = coords[1] - coords[0] + length = self.direction.length if round(length, 4) < 0.1: return data = {"coords": coords} self.depth = length - self.rotation = atan2(direction[1], direction[0]) + self.rotation = atan2(self.direction[1], self.direction[0]) if should_round: # Round to nearest 50mm (yes, metric for now) self.length = 0.05 * round(length / 0.05) # Round to nearest 5 degrees - nearest_degree = (math.pi / 180) * 5 + nearest_degree = (pi / 180) * 5 self.rotation = nearest_degree * round(self.rotation / nearest_degree) self.location = coords[0] data["obj"] = self.create_profile() @@ -1122,6 +1127,8 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato def __init__(self): super().__init__() + self.input_ui = tool.Polyline.create_input_ui(init_z=True) + self.input_options = ["D", "A", "X", "Y", "Z"] self.relating_type = None props = tool.Model.get_model_props() relating_type_id = props.relating_type_id @@ -1166,7 +1173,9 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato self.handle_mouse_move(context, event, should_round=True) - self.choose_axis(event) + self.choose_axis(event, z=True) + + self.choose_plane(event) self.handle_snap_selection(context, event) @@ -1202,6 +1211,6 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato def _invoke(self, context, event): super().invoke(context, event) ProductDecorator.install(context) - self.tool_state.use_default_container = True - self.tool_state.plane_method = "XY" + self.tool_state.use_default_container = False + self.tool_state.plane_method = None return {"RUNNING_MODAL"} From c11060b46aa1754162e7c117eaa64613c85a61ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 20 Feb 2025 21:13:39 -0300 Subject: [PATCH 085/476] Fix #6185 issue when trying to create a slab with only two points. --- src/bonsai/bonsai/bim/module/model/slab.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 2ccd20a3e5..1010217cae 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -885,6 +885,8 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): return {"FINISHED"} slab = DumbSlabGenerator(self.relating_type).generate("POLYLINE") + if not slab: + return model_props = tool.Model.get_model_props() direction_sense = model_props.direction_sense From 0f92c2d26b296c49c127cc8a532fcf7d6e509f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 20 Feb 2025 22:21:44 -0300 Subject: [PATCH 086/476] Fix #6161 Error - cannot access local variable 'snap_point' --- src/bonsai/bonsai/tool/snap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 249d856579..8f89088e63 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -577,7 +577,7 @@ class Snap(bonsai.core.tool.Snap): "object": obj, } snaps_by_type.insert(0, snap_point) - cls.update_snapping_point(snap_point["point"], snap_point["type"]) + cls.update_snapping_point(snap_point["point"], snap_point["type"]) return snaps_by_type cls.update_snapping_point(point["point"], point["type"]) return snaps_by_type From fed8c2162583f25ea54972cfdcabb66df2153312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 20 Feb 2025 22:52:42 -0300 Subject: [PATCH 087/476] Polyline tool - Improves the preview fix for #6083 by avoiding overlapping edges. --- src/bonsai/bonsai/tool/polyline.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 4442bab9c0..9e48c9ae73 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -521,9 +521,14 @@ class Polyline(bonsai.core.tool.Polyline): for point in polyline_points[1:]: # The first can be repeated to form a wall loop if (x, y, z) == (point.x, point.y, point.z): return "Cannot create two points at the same location" - # Avoids duplicating an edge + # Avoids creating overlapping edges if len(polyline_points) > 1: - if Vector((x, y, z)) == Vector((polyline_points[-2].x, polyline_points[-2].y, polyline_points[-2].z)): + v1 = Vector((x, y, z)) + v2 = Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z)) + v3 = Vector((polyline_points[-2].x, polyline_points[-2].y, polyline_points[-2].z)) + angle = tool.Cad.angle_3_vectors(v1, v2, v3, new_angle=None, degrees=True + ) + if tool.Cad.is_x(angle, 0): return # TODO move this limitation to be Wall tool specific. Right now it also affects Measure tool # Avoids creating segments smaller then 0.1. This is a limitation from create_wall_from_2_points From 69535c53e096cf06c733dbe096d3cae5646db357 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Feb 2025 17:31:27 +0500 Subject: [PATCH 088/476] Fix typo in 4bb32b252d #6199 ahh --- src/bonsai/bonsai/bim/module/geometry/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index e3f488284e..fbccff9266 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -742,7 +742,7 @@ class OverrideDelete(bpy.types.Operator): self.process_arrays(context) clear_active_object = True - objects_to_remove = context.selectable_objects + objects_to_remove = context.selected_objects for i, obj in enumerate(objects_to_remove, 1): # Log time. time_since_start = time() - start_time From cf1647a45114f95a871e5f4212870b68bb825cc0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Feb 2025 14:40:41 +0500 Subject: [PATCH 089/476] Fix bugs in 17642ca4e9 --- .../ifcopenshell/api/sequence/calculate_task_duration.py | 2 +- .../ifcopenshell/api/style/edit_surface_style.py | 3 +-- src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py | 1 + .../test/api/structural/test_edit_structural_analysis_model.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py index b8b4d857b6..d5f0e5947e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/calculate_task_duration.py @@ -143,5 +143,5 @@ class Usecase: def set_task_duration(self, duration: float) -> None: if not (task_time := self.task.TaskTime): - ifcopenshell.api.sequence.add_task_time(self.file, task=self.task) + task_time = ifcopenshell.api.sequence.add_task_time(self.file, task=self.task) task_time.ScheduleDuration = f"P{duration}D" diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py index dba4dd48ea..7f7f05948d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/edit_surface_style.py @@ -76,7 +76,6 @@ def edit_surface_style( class Usecase: file: ifcopenshell.file - settings: dict[str, Any] def execute(self, style: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: self.style = style @@ -100,7 +99,7 @@ class Usecase: elif attribute_class == "IfcColourOrFactor": self.edit_colour_or_factor(key, value) else: - setattr(self.settings["style"], key, value) + setattr(style, key, value) def edit_colour_rgb(self, name: str, value: dict[str, Any]): if (attribute := getattr(self.style, name)) is None: diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py index 8e0335b77e..9651bfa87e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/unassign_unit.py @@ -50,4 +50,5 @@ def unassign_unit(file: ifcopenshell.file, units: Optional[list[ifcopenshell.ent units_set = units_set - set(units or []) if units_set: unit_assignment.Units = list(units_set) + return file.remove(unit_assignment) diff --git a/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py b/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py index 18f389f488..fddb8a9993 100644 --- a/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py +++ b/src/ifcopenshell-python/test/api/structural/test_edit_structural_analysis_model.py @@ -23,7 +23,7 @@ import ifcopenshell.api.structural class TestEditStructuralAnalysisModel(test.bootstrap.IFC4): def test_editing_a_structural_analysis_model(self): subject = ifcopenshell.api.structural.add_structural_analysis_model(self.file) - subject = ifcopenshell.api.structural.edit_structural_analysis_model( + ifcopenshell.api.structural.edit_structural_analysis_model( self.file, structural_analysis_model=subject, attributes={"Name": "My edited model", "Description": "Description of my model"}, From e6d3396630051c484e7ac1c6efbd844857d37406 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Feb 2025 14:45:17 +0500 Subject: [PATCH 090/476] typing --- src/bonsai/bonsai/bim/import_ifc.py | 2 +- .../bonsai/bim/module/drawing/annotation.py | 5 +- .../bonsai/bim/module/geometry/operator.py | 2 +- src/bonsai/bonsai/tool/drawing.py | 49 +++++++++++-------- .../api/style/add_surface_style.py | 18 +++---- .../ifcopenshell/util/element.py | 2 +- 6 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 21c8d16e3e..256bd8ff85 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -970,7 +970,7 @@ class IfcImporter: object_type = element.ObjectType return ( object_type in tool.Drawing.ANNOTATION_TYPES_DATA - and tool.Drawing.ANNOTATION_TYPES_DATA[object_type][3] == "curve" + and tool.Drawing.ANNOTATION_TYPES_DATA[object_type].data_type == "curve" ) def get_drawing_group(self, element): diff --git a/src/bonsai/bonsai/bim/module/drawing/annotation.py b/src/bonsai/bonsai/bim/module/drawing/annotation.py index 8bc9ee46d5..6acf5a03f7 100644 --- a/src/bonsai/bonsai/bim/module/drawing/annotation.py +++ b/src/bonsai/bonsai/bim/module/drawing/annotation.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import os import bpy import math @@ -125,7 +126,9 @@ class Annotator: return obj @staticmethod - def get_annotation_obj(drawing: ifcopenshell.entity_instance, object_type: str, data_type: str) -> bpy.types.Object: + def get_annotation_obj( + drawing: ifcopenshell.entity_instance, object_type: str, data_type: tool.Drawing.ANNOTATION_DATA_TYPE + ) -> bpy.types.Object: camera = tool.Ifc.get_object(drawing) co1, _, _, _ = Annotator.get_placeholder_coords(camera) matrix_world = tool.Drawing.get_camera_matrix(camera) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index fbccff9266..0763bd4963 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -683,7 +683,7 @@ class CopyRepresentation(bpy.types.Operator, tool.Ifc.Operator): ) -def lock_error_message(name): +def lock_error_message(name: str) -> str: return f"'{name}' is locked. Unlock it via the Spatial panel in the Project Overview tab." diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 088e5b7025..07e274b011 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -51,7 +51,7 @@ from shapely.ops import unary_union from lxml import etree from mathutils import Vector, Matrix from fractions import Fraction -from typing import Optional, Union, Iterable, Any, Literal, Sequence, TYPE_CHECKING +from typing import Optional, Union, Iterable, Any, Literal, Sequence, TYPE_CHECKING, NamedTuple from pathlib import Path if TYPE_CHECKING: @@ -65,25 +65,32 @@ class Drawing(bonsai.core.tool.Drawing): # ObjectType: annotation_name, description, icon, data_type # fmt: off - ANNOTATION_TYPES_DATA = { - "DIMENSION": ("Dimension", "Add dimensions annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "FIXED_SIZE", "curve"), - "ANGLE": ("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"), - "RADIUS": ("Radius", "", "FORWARD", "curve"), - "DIAMETER": ("Diameter", "Add diameter annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "ARROW_LEFTRIGHT", "curve"), - "TEXT": ("Text", "", "SMALL_CAPS", "empty"), - "TEXT_LEADER": ("Leader", "", "TRACKING_BACKWARDS", "curve"), - "STAIR_ARROW": ("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"), - "PLAN_LEVEL": ("Level (Plan)", "", "SORTBYEXT", "curve"), - "SECTION_LEVEL": ("Level (Section)", "", "TRIA_DOWN", "curve"), - "BREAKLINE": ("Breakline", "", "FCURVE", "mesh"), - "SYMBOL": ("Symbol", "", "KEYFRAME", "empty"), - "MULTI_SYMBOL": ("Multi-Symbol", "", "OUTLINER_DATA_POINTCLOUD", "mesh"), - "LINEWORK": ("Line", "", "SNAP_MIDPOINT", "mesh"), - "BATTING": ("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"), - "REVISION_CLOUD":("Revision Cloud", "Add revision cloud", "VOLUME_DATA", "mesh"), - "FILL_AREA": ("Fill Area", "", "NODE_TEXTURE", "mesh"), - "FALL": ("Fall", "", "SORT_ASC", "curve"), - "IMAGE": ("Image", "Add reference image attached to the drawing", "TEXTURE", "mesh"), + + class AnnotationObjectType(NamedTuple): + annotation_name: str + description: str + icon: str + data_type: Drawing.ANNOTATION_DATA_TYPE + + ANNOTATION_TYPES_DATA: dict[str, AnnotationObjectType] = { + "DIMENSION": AnnotationObjectType("Dimension", "Add dimensions annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "FIXED_SIZE", "curve"), + "ANGLE": AnnotationObjectType("Angle", "", "DRIVER_ROTATIONAL_DIFFERENCE", "curve"), + "RADIUS": AnnotationObjectType("Radius", "", "FORWARD", "curve"), + "DIAMETER": AnnotationObjectType("Diameter", "Add diameter annotation.\nMeasurement values can be hidden through ShowDescriptionOnly property\nof BBIM_Dimension property set", "ARROW_LEFTRIGHT", "curve"), + "TEXT": AnnotationObjectType("Text", "", "SMALL_CAPS", "empty"), + "TEXT_LEADER": AnnotationObjectType("Leader", "", "TRACKING_BACKWARDS", "curve"), + "STAIR_ARROW": AnnotationObjectType("Stair Arrow", "Add stair arrow annotation.\nIf you have IfcStairFlight object selected, it will be used as a reference for the annotation", "SCREEN_BACK", "curve"), + "PLAN_LEVEL": AnnotationObjectType("Level (Plan)", "", "SORTBYEXT", "curve"), + "SECTION_LEVEL": AnnotationObjectType("Level (Section)", "", "TRIA_DOWN", "curve"), + "BREAKLINE": AnnotationObjectType("Breakline", "", "FCURVE", "mesh"), + "SYMBOL": AnnotationObjectType("Symbol", "", "KEYFRAME", "empty"), + "MULTI_SYMBOL": AnnotationObjectType("Multi-Symbol", "", "OUTLINER_DATA_POINTCLOUD", "mesh"), + "LINEWORK": AnnotationObjectType("Line", "", "SNAP_MIDPOINT", "mesh"), + "BATTING": AnnotationObjectType("Batting", "Add batting annotation.\nThickness could be changed through Thickness property of BBIM_Batting property set", "FORCE_FORCE", "mesh"), + "REVISION_CLOUD":AnnotationObjectType("Revision Cloud", "Add revision cloud", "VOLUME_DATA", "mesh"), + "FILL_AREA": AnnotationObjectType("Fill Area", "", "NODE_TEXTURE", "mesh"), + "FALL": AnnotationObjectType("Fall", "", "SORT_ASC", "curve"), + "IMAGE": AnnotationObjectType("Image", "Add reference image attached to the drawing", "TEXTURE", "mesh"), } # fmt: on @@ -112,7 +119,7 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def get_annotation_data_type(cls, object_type: str) -> ANNOTATION_DATA_TYPE: - return cls.ANNOTATION_TYPES_DATA[object_type][3] + return cls.ANNOTATION_TYPES_DATA[object_type].data_type @classmethod def create_annotation_object(cls, drawing: ifcopenshell.entity_instance, object_type: str) -> bpy.types.Object: diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py index 64d38cfd2f..944240e618 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py @@ -35,7 +35,7 @@ def add_surface_style( style: ifcopenshell.entity_instance, ifc_class: SURFACE_STYLE_TYPES = "IfcSurfaceStyleShading", attributes: Optional[dict[str, Any]] = None, -) -> None: +) -> ifcopenshell.entity_instance: """Adds a new presentation item to a surface style A surface style can have multiple different types of presentation items @@ -123,20 +123,20 @@ def add_surface_style( "SpecularHighlight": {"SpecularRoughness": 0.5}, # Roughness factor }) """ - settings = {"style": style, "ifc_class": ifc_class, "attributes": attributes or {}} + attributes = attributes or {} + style_item = file.create_entity(ifc_class) + ifcopenshell.api.style.edit_surface_style(file, style=style_item, attributes=attributes) + styles: list[ifcopenshell.entity_instance] + styles = list(style.Styles or []) - style_item = file.create_entity(settings["ifc_class"]) - ifcopenshell.api.style.edit_surface_style(file, style=style_item, attributes=settings["attributes"]) - styles = list(settings["style"].Styles or []) - - select_class = settings["ifc_class"] + select_class = ifc_class if select_class == "IfcSurfaceStyleRendering": select_class = "IfcSurfaceStyleShading" duplicate_items = [s for s in styles if s.is_a(select_class)] for duplicate_item in duplicate_items: ifcopenshell.api.style.remove_surface_style(file, style=duplicate_item) - styles = list(settings["style"].Styles or []) + styles = list(style.Styles or []) styles.append(style_item) - settings["style"].Styles = styles + style.Styles = styles return style_item diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 524f86b8ac..f3865bdc7e 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -454,7 +454,7 @@ def get_elements_by_pset(pset: ifcopenshell.entity_instance) -> set[ifcopenshell return elements -def get_predefined_type(element: ifcopenshell.entity_instance) -> str: +def get_predefined_type(element: ifcopenshell.entity_instance) -> Union[str, None]: """Retrieves the PrefefinedType attribute of an element. If the predefined type is user defined, the custom type (such as object From 6e20ee9feaa491119443c947aae37dd70b140e69 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Feb 2025 15:31:19 +0500 Subject: [PATCH 091/476] remove_deep2 - also_considered_inverses optimization 1) replaced walk with traverse(max_levels=1) 2) early return if there total_inverses == 0 3) early return if also_considered_inverses is enough to cover total_inverses --- .../ifcopenshell/util/element.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index f3865bdc7e..5e011b525f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -1466,18 +1466,22 @@ def remove_deep2( :param element: The starting element that defines the subgraph """ # ifc_file.batch() - also_considered_inverses = 0 + total_inverses = ifc_file.get_total_inverses(element) + if total_inverses > 0: - def increment_considered_inverses(_): - nonlocal also_considered_inverses - also_considered_inverses += 1 + def are_inverses_contained() -> bool: + also_considered_inverses = 0 - for considered_element in also_consider: - for attribute in considered_element: - considered_element.walk(lambda x: x == element, increment_considered_inverses, attribute) + for considered_element in also_consider: + traverse = ifc_file.traverse(considered_element, max_levels=1) + if element in traverse: + also_considered_inverses += 1 + if total_inverses == also_considered_inverses: + return True + return False - if ifc_file.get_total_inverses(element) > 0 + also_considered_inverses: - return + if not are_inverses_contained(): + return to_delete = set() subgraph = list(ifc_file.traverse(element, breadth_first=True)) From f7efbe59246d5e759c721aca6e7c99ccc97aa180 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Feb 2025 15:54:58 +0500 Subject: [PATCH 092/476] remove_representation - optimizations 1) `do_not_delete` performs best when it's set 2) also_consider when `element` related elements go first, so there will be no need to traverse all other elements to see if they cover `element`'s inverses. --- .../api/geometry/remove_representation.py | 17 ++++++++++++----- .../ifcopenshell/util/element.py | 4 +++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py index 66b374ca6d..0618b407ec 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/remove_representation.py @@ -39,7 +39,8 @@ def remove_representation( """ is_ifc2x3 = file.schema == "IFC2X3" styled_items = set() - presentation_layer_assignments = set() + presentation_layer_assignments_items: set[ifcopenshell.entity_instance] = set() + presentation_layer_assignments_reps: set[ifcopenshell.entity_instance] = set() textures = set() colours = set() named_profiles = set() @@ -48,13 +49,13 @@ def remove_representation( [styled_items.add(s) for s in subelement.StyledByItem or []] # IFC2X3 is using LayerAssignments for s in subelement.LayerAssignment if not is_ifc2x3 else subelement.LayerAssignments: - presentation_layer_assignments.add(s) + presentation_layer_assignments_items.add(s) # IfcTessellatedFaceSet inverses [textures.add(t) for t in getattr(subelement, "HasTextures", []) or []] [colours.add(t) for t in getattr(subelement, "HasColours", []) or []] elif subelement.is_a("IfcRepresentation"): for layer in subelement.LayerAssignments: - presentation_layer_assignments.add(layer) + presentation_layer_assignments_reps.add(layer) elif subelement.is_a("IfcProfileDef") and subelement.ProfileName: named_profiles.add(subelement) @@ -62,11 +63,16 @@ def remove_representation( if should_keep_named_profiles: do_not_delete += named_profiles + # Order matters - layer assignments may reference representation directly. + also_consider = list(presentation_layer_assignments_reps) + also_consider.extend(presentation_layer_assignments_items - presentation_layer_assignments_reps) + also_consider.extend(styled_items) + also_consider.extend(textures) ifcopenshell.util.element.remove_deep2( file, representation, - also_consider=list(styled_items | presentation_layer_assignments | colours), - do_not_delete=do_not_delete, + also_consider=also_consider, + do_not_delete=set(do_not_delete), ) for texture in textures: @@ -79,6 +85,7 @@ def remove_representation( item = element.Item if not item or item in to_delete: file.remove(element) + presentation_layer_assignments = presentation_layer_assignments_reps | presentation_layer_assignments_items for element in presentation_layer_assignments: if all(item in to_delete for item in element.AssignedItems): file.remove(element) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 5e011b525f..fd86a09158 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -1434,7 +1434,7 @@ def remove_deep2( ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance, also_consider: list[ifcopenshell.entity_instance] = [], - do_not_delete: list[ifcopenshell.entity_instance] = [], + do_not_delete: set[ifcopenshell.entity_instance] = set(), ) -> None: """Recursively purges a subgraph safely, starting at an element @@ -1462,6 +1462,8 @@ def remove_deep2( :param ifc_file: The IFC file object :param also_consider: elements to also consider as a part of a subgraph + Order could matter for perfomance - elements that reference `element` + directly should go first for the better performance. :param do_not_delete: elements to protect from deletion :param element: The starting element that defines the subgraph """ From ee2fa739bbae9da7afc00de73db547fb6cd91151 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Feb 2025 16:18:36 +0500 Subject: [PATCH 093/476] remove_deep2 - fix traversing same element twice (I hope I don't miss anything, no tests seem to fail and performance is increased significantly) but because `subelement_queue` initiated with the `traverse` all `element`'s subelements will be traversed inside `while` loop twice - once as a part of initial queue and another time when `element` is traversed inside the loop and all those elements added to the queue again. Now we just initiate the `queue` with the `element` and it will be traversed inside the loop like any other element. --- src/ifcopenshell-python/ifcopenshell/util/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index fd86a09158..9034e382be 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -1489,7 +1489,7 @@ def remove_deep2( subgraph = list(ifc_file.traverse(element, breadth_first=True)) subgraph.extend(also_consider) subgraph_set = set(subgraph) - subelement_queue = ifc_file.traverse(element, max_levels=1) + subelement_queue = [element] while subelement_queue: subelement = subelement_queue.pop(0) if ( From 92c2fc0b7fb46706abac123de3b6dafc6a92ce66 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Feb 2025 18:38:31 +0500 Subject: [PATCH 094/476] Fix errors regarding added PredefinedType to IfcAnnotations in IFC4X3 #6200 In IFC4X3 IfcAnnotation now has PredefinedType and checking ObjectType isn't enough. --- src/bonsai/bonsai/bim/import_ifc.py | 2 +- .../bonsai/bim/module/drawing/annotation.py | 6 ++- src/bonsai/bonsai/bim/module/drawing/data.py | 6 ++- .../bonsai/bim/module/drawing/decoration.py | 10 +++-- .../bonsai/bim/module/drawing/operator.py | 6 ++- .../bonsai/bim/module/drawing/svgwriter.py | 37 ++++++++++--------- .../bonsai/bim/module/drawing/workspace.py | 3 +- src/bonsai/bonsai/tool/drawing.py | 2 +- 8 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 256bd8ff85..318f4834e2 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -967,7 +967,7 @@ class IfcImporter: tool.Collector.assign(obj, should_clean_users_collection=False) def is_curve_annotation(self, element: ifcopenshell.entity_instance) -> bool: - object_type = element.ObjectType + object_type = ifcopenshell.util.element.get_predefined_type(element) return ( object_type in tool.Drawing.ANNOTATION_TYPES_DATA and tool.Drawing.ANNOTATION_TYPES_DATA[object_type].data_type == "curve" diff --git a/src/bonsai/bonsai/bim/module/drawing/annotation.py b/src/bonsai/bonsai/bim/module/drawing/annotation.py index 6acf5a03f7..50d987f30c 100644 --- a/src/bonsai/bonsai/bim/module/drawing/annotation.py +++ b/src/bonsai/bonsai/bim/module/drawing/annotation.py @@ -153,7 +153,11 @@ class Annotator: if object_type != "ANGLE": for obj in collection.objects: element = tool.Ifc.get_entity(obj) - if element and element.ObjectType == object_type and obj.type == object_type.upper(): + if ( + element + and ifcopenshell.util.element.get_predefined_type(element) == object_type + and obj.type == object_type.upper() + ): return obj if data_type == "mesh": diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index d781b8bd30..30fda54828 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -364,7 +364,11 @@ class DecoratorData: element = tool.Ifc.get_entity(obj) supported_object_types = ("DIMENSION", "DIAMETER", "SECTION_LEVEL", "PLAN_LEVEL", "RADIUS") - if not element or not element.is_a("IfcAnnotation") or element.ObjectType not in supported_object_types: + if ( + not element + or not element.is_a("IfcAnnotation") + or ifcopenshell.util.element.get_predefined_type(element) not in supported_object_types + ): return None dimension_style = "arrow" diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 0bf8ab00b8..9c26c2b1c5 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -977,6 +977,7 @@ class FallDecorator(BaseDecorator): # same function as in svgwriter.py def get_label_text(): element = tool.Ifc.get_entity(obj) + assert element B, A = [v.co.xyz for v in spline_points[:2]] rise = abs(A.z - B.z) O = A.copy() @@ -989,13 +990,14 @@ class FallDecorator(BaseDecorator): angle = 90 # ues SLOPE_ANGLE as default - if element.ObjectType in ("FALL", "SLOPE_ANGLE"): + object_type = ifcopenshell.util.element.get_predefined_type(element) + if object_type in ("FALL", "SLOPE_ANGLE"): return f"{angle}°" - elif element.ObjectType == "SLOPE_FRACTION": + elif object_type == "SLOPE_FRACTION": if angle == 90: return "-" return f"{self.format_value(context, rise)} / {self.format_value(context, run)}" - elif element.ObjectType == "SLOPE_PERCENT": + elif object_type == "SLOPE_PERCENT": if angle == 90: return "-" return f"{round(angle_tg * 100)} %" @@ -2047,7 +2049,7 @@ class DecorationsHandler: if not element.is_a("IfcAnnotation"): continue - object_type: Union[str, None] = element.ObjectType + object_type: Union[str, None] = ifcopenshell.util.element.get_predefined_type(element) if object_type == "DRAWING": continue diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 6014015995..3528e4dc36 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -1370,7 +1370,11 @@ class CreateDrawing(bpy.types.Operator): elements = list(elements | filtered_drawing_annotations) annotations = sorted( - elements, key=lambda a: (tool.Drawing.get_annotation_z_index(a), 1 if a.ObjectType == "TEXT" else 0) + elements, + key=lambda a: ( + tool.Drawing.get_annotation_z_index(a), + 1 if ifcopenshell.util.element.get_predefined_type(a) == "TEXT" else 0, + ), ) precision = ifcopenshell.util.element.get_pset(self.camera_element, "EPset_Drawing", "MetricPrecision") diff --git a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py index e6dd66150a..9a249450f7 100644 --- a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py +++ b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py @@ -181,35 +181,35 @@ class SvgWriter: self.decimal_places = decimal_places for element in annotations: obj = tool.Ifc.get_object(element) - if not obj or element.ObjectType == "DRAWING": + if not obj or (object_type := ifcopenshell.util.element.get_predefined_type(element)) == "DRAWING": continue - elif element.ObjectType == "GRID": + elif object_type == "GRID": self.draw_grid_annotation(obj) - elif element.ObjectType == "TEXT_LEADER": + elif object_type == "TEXT_LEADER": self.draw_leader_annotation(obj) - elif element.ObjectType == "STAIR_ARROW": + elif object_type == "STAIR_ARROW": self.draw_stair_annotation(obj) - elif element.ObjectType == "DIMENSION": + elif object_type == "DIMENSION": self.draw_dimension_annotations(obj) - elif element.ObjectType == "ANGLE": + elif object_type == "ANGLE": self.draw_angle_annotations(obj) - elif element.ObjectType == "RADIUS": + elif object_type == "RADIUS": self.draw_radius_annotations(obj) - elif element.ObjectType == "DIAMETER": + elif object_type == "DIAMETER": self.draw_diameter_annotations(obj) - elif element.ObjectType == "ELEVATION": + elif object_type == "ELEVATION": self.draw_elevation_annotation(obj) - elif element.ObjectType == "SECTION": + elif object_type == "SECTION": self.draw_section_annotation(obj) - elif element.ObjectType == "BREAKLINE": + elif object_type == "BREAKLINE": self.draw_break_annotations(obj) - elif element.ObjectType == "PLAN_LEVEL": + elif object_type == "PLAN_LEVEL": self.draw_plan_level_annotation(obj) - elif element.ObjectType == "SECTION_LEVEL": + elif object_type == "SECTION_LEVEL": self.draw_section_level_annotation(obj) - elif element.ObjectType == "TEXT": + elif object_type == "TEXT": self.draw_text_annotation(obj, obj.location) - elif element.ObjectType in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"): + elif object_type in ("FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT"): self.draw_fall_annotations(obj) else: self.draw_misc_annotation(obj) @@ -1207,13 +1207,14 @@ class SvgWriter: angle = 90 # ues SLOPE_ANGLE as default - if element.ObjectType in ("FALL", "SLOPE_ANGLE"): + object_type = ifcopenshell.util.element.get_predefined_type(element) + if object_type in ("FALL", "SLOPE_ANGLE"): return f"{angle}°" - elif element.ObjectType == "SLOPE_FRACTION": + elif object_type == "SLOPE_FRACTION": if angle == 90: return "-" return f"{helper.format_distance(rise, precision=self.precision, decimal_places=self.decimal_places)} / {helper.format_distance(run, precision=self.precision, decimal_places=self.decimal_places)}" - elif element.ObjectType == "SLOPE_PERCENT": + elif object_type == "SLOPE_PERCENT": if angle == 90: return "-" return f"{round(angle_tg * 100)} %" diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py index bd679cb065..16d26ea7e0 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -19,6 +19,7 @@ import os import bpy +import ifcopenshell.util.element import bonsai.core.type import bonsai.core.drawing as core import bonsai.tool as tool @@ -333,7 +334,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): if not element or not element.is_a("IfcAnnotation"): continue - annotation_type = element.ObjectType + annotation_type = ifcopenshell.util.element.get_predefined_type(element) if annotation_type not in tool.Drawing.ANNOTATION_TYPES_SUPPORT_SETUP: self.report({"ERROR"}, f"Annotation type {annotation_type} is not supported for readjustment.") continue diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 07e274b011..0dd090c617 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -245,7 +245,7 @@ class Drawing(bonsai.core.tool.Drawing): element_type = element.is_a() - if element_type == "IfcAnnotation" and element.ObjectType in object_types: + if element_type == "IfcAnnotation" and ifcopenshell.util.element.get_predefined_type(element) in object_types: return True if element_type == "IfcTypeProduct" and ( From 894c05e13871169d479b961c5e10cd8a08aaa9eb Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Feb 2025 18:43:45 +0500 Subject: [PATCH 095/476] Fix old typo in 686f4a0 --- src/bonsai/bonsai/bim/module/drawing/annotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/annotation.py b/src/bonsai/bonsai/bim/module/drawing/annotation.py index 50d987f30c..a04360f5ed 100644 --- a/src/bonsai/bonsai/bim/module/drawing/annotation.py +++ b/src/bonsai/bonsai/bim/module/drawing/annotation.py @@ -156,7 +156,7 @@ class Annotator: if ( element and ifcopenshell.util.element.get_predefined_type(element) == object_type - and obj.type == object_type.upper() + and obj.type == data_type.upper() ): return obj From c549dea943b509f02e46c3cabf83bc9d572c4cad Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 22 Feb 2025 20:01:12 +1100 Subject: [PATCH 096/476] See #1227. Test script for priority-aware wall layer joins. --- src/bonsai/scripts/waldo.py | 325 ++++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 src/bonsai/scripts/waldo.py diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py new file mode 100644 index 0000000000..3a308109d1 --- /dev/null +++ b/src/bonsai/scripts/waldo.py @@ -0,0 +1,325 @@ +import numpy as np +import ifcopenshell +import ifcopenshell.api.root +import ifcopenshell.api.type +import ifcopenshell.api.unit +import ifcopenshell.api.project +import ifcopenshell.api.context +import ifcopenshell.api.spatial +import ifcopenshell.api.material +import ifcopenshell.api.geometry +import ifcopenshell.util.shape_builder +import ifcopenshell.util.element + +# from ifcopenshell.util.shape_builder import VectorType, SequenceOfVectors +from collections import namedtuple + +f = ifcopenshell.api.project.create_file() + +ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject") +meters = ifcopenshell.api.unit.add_si_unit(f) +ifcopenshell.api.unit.assign_unit(f, units=[meters]) + +model = ifcopenshell.api.context.add_context(f, context_type="Model") +plan = ifcopenshell.api.context.add_context(f, context_type="Plan") +axis = ifcopenshell.api.context.add_context( + f, context_type="Plan", context_identifier="Axis", target_view="GRAPH_VIEW", parent=plan +) +body = ifcopenshell.api.context.add_context( + f, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model +) +concrete = ifcopenshell.api.material.add_material(f, name="concrete", category="concrete") +site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite") +builder = ifcopenshell.util.shape_builder.ShapeBuilder(f) + + +def test_wall(offset, p1, p2, p3, p4): + offset *= 1.5 + wall_type_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="A") + wall_type_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="B") + + set_a = ifcopenshell.api.material.add_material_set(f, set_type="IfcMaterialLayerSet") + structure = ifcopenshell.api.material.add_layer(f, layer_set=set_a, material=concrete, name="structure") + structure.Priority = p1 + structure.LayerThickness = 0.1 + cladding = ifcopenshell.api.material.add_layer(f, layer_set=set_a, material=concrete, name="cladding") + cladding.Priority = p2 + cladding.LayerThickness = 0.05 + + set_b = ifcopenshell.api.material.add_material_set(f, set_type="IfcMaterialLayerSet") + structure = ifcopenshell.api.material.add_layer(f, layer_set=set_b, material=concrete, name="structure") + structure.Priority = p3 + structure.LayerThickness = 0.1 + cladding = ifcopenshell.api.material.add_layer(f, layer_set=set_b, material=concrete, name="cladding") + cladding.Priority = p4 + cladding.LayerThickness = 0.05 + + ifcopenshell.api.material.assign_material(f, products=[wall_type_a], material=set_a) + ifcopenshell.api.material.assign_material(f, products=[wall_type_b], material=set_b) + + for i, rotation in enumerate((-90, -60, -120, 90, 60, 120)): + wall_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"A{p1}{p2}") + wall_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"B{p3}{p4}") + + ifcopenshell.api.spatial.assign_container(f, products=[wall_a, wall_b], relating_structure=site) + + ifcopenshell.api.type.assign_type(f, related_objects=[wall_a], relating_type=wall_type_a) + ifcopenshell.api.type.assign_type(f, related_objects=[wall_b], relating_type=wall_type_b) + + axis_a = builder.polyline(((0.0, 0.0), (1.0, 0.0))) + axis_b = builder.polyline(((0.0, 0.0), (1.0, 0.0))) + rep_a = builder.get_representation(axis, [axis_a]) + rep_b = builder.get_representation(axis, [axis_b]) + + ifcopenshell.api.geometry.assign_representation(f, product=wall_a, representation=rep_a) + ifcopenshell.api.geometry.assign_representation(f, product=wall_b, representation=rep_b) + + x_offset = i * 2 + sign_offset = 0 if rotation < 0 else 1 + matrix_a = np.eye(4) + matrix_a[:, 3][0:3] = (0 + x_offset, 0 + offset + sign_offset, 0) + matrix_b = np.eye(4) + matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b + matrix_b[:, 3][0:3] = (1 + x_offset, 1 + offset - sign_offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b) + + ifcopenshell.api.geometry.connect_path( + f, relating_element=wall_a, related_element=wall_b, relating_connection="ATEND", related_connection="ATEND" + ) + + Foo(f, body).regenerate(wall_a) + Foo(f, body).regenerate(wall_b) + + +PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness") + + +class Foo: + def __init__(self, file, body): + self.file = file + self.body = body + + def regenerate(self, wall): + print("-" * 100) + print(wall) + layers = self.get_layers(wall) + if not layers: + return + axes = self.get_axes(wall, layers) + self.start_points = [] + self.end_points = [] + for rel in wall.ConnectedTo: + if rel.is_a("IfcRelConnectsPathElements"): + wall2 = rel.RelatedElement + layers1 = self.combine_layers(layers.copy(), rel.RelatingPriorities) + layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatedPriorities) + if not layers1 or not layers2: + continue + self.join(wall, wall2, layers1, layers2, rel.RelatingConnectionType, rel.RelatedConnectionType) + + for rel in wall.ConnectedFrom: + if rel.is_a("IfcRelConnectsPathElements"): + wall2 = rel.RelatingElement + layers1 = self.combine_layers(layers.copy(), rel.RelatedPriorities) + layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatingPriorities) + if not layers1 or not layers2: + continue + self.join(wall, wall2, layers1, layers2, rel.RelatedConnectionType, rel.RelatingConnectionType) + + # for rel in wall.ConnectedFrom: + # if rel.is_a("IfcRelConnectsPathElements"): + # connection = rel.RelatedConnectionType + if not self.start_points: + minx = axes[0][0][0] + self.start_points = [ + np.array((minx, axes[0][0][1])), + np.array((minx, axes[-1][0][1])), + ] + if not self.end_points: + maxx = axes[0][1][0] + self.end_points = [ + np.array((maxx, axes[0][0][1])), + np.array((maxx, axes[-1][0][1])), + ] + print("FINISHED") + print(self.start_points) + print(self.end_points) + + points = [] + if self.start_points[0][1] < self.start_points[-1][1]: + points.extend((self.start_points)) + else: + points.extend(reversed(self.start_points)) + if self.end_points[0][1] > self.end_points[-1][1]: + points.extend((self.end_points)) + else: + points.extend(reversed(self.end_points)) + + builder = ifcopenshell.util.shape_builder.ShapeBuilder(wall.file) + item = builder.extrude(builder.polyline(points, closed=True), magnitude=1.0) + rep = builder.get_representation(self.body, items=[item]) + ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) + + def join(self, wall1, wall2, layers1, layers2, connection1, connection2): + if connection1 == "NOTDEFINED" or connection2 == "NOTDEFINED": + return + print("joining", wall1, layers1, connection1) + print("to", wall2, layers2, connection2) + + # axes = self.get_axes(wall2, layers2) + axes1 = self.get_axes(wall1, layers1) + axes2 = self.get_axes(wall2, layers2) + matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)) + matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) + print(axes1) + print(axes2) + + # Convert wall2 axes to wall1 local coordinates + for axis in axes2: + axis[0] = (matrix1i @ matrix2 @ np.concatenate((axis[0], (0, 1))))[:2] + axis[1] = (matrix1i @ matrix2 @ np.concatenate((axis[1], (0, 1))))[:2] + + # Sort axes from interior to exterior + if connection1 == "ATEND": + if axes2[0][0][0] > axes2[-1][0][0]: # We process layers in a +X direction + axes2 = list(reversed(axes2)) + layers2 = list(reversed(layers2)) + elif connection1 == "ATSTART": + if axes2[-1][0][0] > axes2[0][0][0]: # We process layers in a -X direction + axes2 = list(reversed(axes2)) + layers2 = list(reversed(layers2)) + + # wall2_x = matrix2[:,0][:2] + axis2 = axes2[0] # Take an arbitrary axis + if connection2 == "ATSTART": + axis2 = [axis2[1], axis2[0]] # Flip direction so the axis "points" in the direction of join + if axis2[0][1] < axis2[1][1]: # Pointing +Y + if axes1[-1][0][1] < axes1[0][0][1]: # We process layers1 in a +Y direction + axes1 = list(reversed(axes1)) + layers1 = list(reversed(layers1)) + else: # Pointing -Y + if axes1[0][0][1] < axes1[-1][0][1]: # We process layers1 in a -Y direction + axes1 = list(reversed(axes1)) + layers1 = list(reversed(layers1)) + + print("modified") + print(axes1) + print(axes2) + # Checked + + last_y = axes1[-1][0][1] + ys = iter([a[0][1] for a in axes1]) + print("ys are", [a[0][1] for a in axes1]) + + last_axis2 = axes2[-1] + axes2 = iter(axes2) + axis2 = next(axes2) + y = next(ys) + x = self.intersect_axis(*axis2, y=y) + points = [np.array((x, y))] + print("first point", points) + + layers1 = iter(layers1) + layers2 = iter(layers2) + layer1 = next(layers1, None) + layer2 = next(layers2, None) + + while layer1 and layer2: + print("considering", layer1, layer2) + if layer1.priority > layer2.priority: + axis2 = next(axes2) + x = self.intersect_axis(*axis2, y=y) + layer2 = next(layers2, None) + elif layer2.priority > layer1.priority: + y = next(ys) + x = self.intersect_axis(*axis2, y=y) + layer1 = next(layers1, None) + else: + y = next(ys) + x = self.intersect_axis(*next(axes2), y=y) + layer1 = next(layers1, None) + layer2 = next(layers2, None) + points.append(np.array((x, y))) + + print("points", points) + if points[-1][1] != last_y: + points.append(np.array((self.intersect_axis(*last_axis2, y=last_y), last_y))) + print("fpoints", points) + if connection1 == "ATSTART": + self.start_points = points + elif connection1 == "ATEND": + self.end_points = points + + def get_layers(self, wall) -> list: + material = ifcopenshell.util.element.get_material(wall, should_skip_usage=True) + if not material or not material.is_a("IfcMaterialLayerSet"): + return [] + return [PrioritisedLayer(l.Priority or 0, l.LayerThickness) for l in material.MaterialLayers] + + def combine_layers(self, layers, override_priorities): + results = [] + if override_priorities: + for i, priority in enumerate(override_priorities[: len(layers)]): + layers[i][0] = priority + if not layers: + return [] + results = [layers.pop(0)] + for layer in layers: + if not layer.thickness: + continue + if layer.priority == results[-1].priority: + results[-1] = PrioritisedLayer(layer.priority, results[-1].thickness + layer.thickness) + else: + results.append(layer) + return results + + def intersect_axis(self, p1, p2, y=0): + # Assumes lines are horizontal + x1, y1 = p1 + x2, y2 = p2 + t = (y - y1) / (y2 - y1) + return x1 + t * (x2 - x1) + + def get_axes(self, wall, layers: list[PrioritisedLayer]): + # I think it's not actually necessary to get the exact axis line here. + axes = [] + # Start by getting Reference line + if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"): + for item in ifcopenshell.util.representation.resolve_representation(axis).Items: + if item.is_a("IfcPolyline"): + points = item.Points + elif item.is_a("IfcIndexedPolyCurve"): + points = item.Points.CoordList + else: + continue + if points[0][0] < points[1][0]: # An axis always goes in the +X direction + axes.append([np.array(points[0]), np.array(points[1])]) + else: + axes.append([np.array(points[1]), np.array(points[0])]) + break + else: + # TODO: derive from existing geometry + axes.append([np.array((0.0, 0.0)), np.array((1.0, 0.0))]) + + # Apply usage to convert the Reference line into MlsBase + sense_factor = 1 + if (usage := ifcopenshell.util.element.get_material(wall)) and usage.is_a("IfcMaterialLayerSetUage"): + for point in axes[0]: + point[1] += usage.OffsetFromReferenceLine + sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 + + for layer in layers: + axes.append([p.copy() + np.array((0.0, layer.thickness * sense_factor)) for p in axes[-1]]) + return axes + + +test_wall(0, 1, 1, 1, 1) +test_wall(1, 2, 1, 1, 2) +test_wall(2, 2, 1, 1, 1) +test_wall(3, 1, 2, 1, 1) +test_wall(4, 1, 2, 1, 2) +test_wall(5, 3, 1, 2, 4) + + +f.write("/home/dion/wall.ifc") From d38f56e82d42463c359f2a3b25b89c843b2383a4 Mon Sep 17 00:00:00 2001 From: Jonas Frei <53214867+HelloJowet@users.noreply.github.com> Date: Thu, 20 Feb 2025 23:45:32 +0100 Subject: [PATCH 097/476] fix: bug in initialization of gltf serialiser fixed --- .../geometry_processing.rst | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst index df7ae3321e..0c4753807a 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/geometry_processing.rst @@ -306,39 +306,43 @@ In addition to geometry settings, serialisation has its own set of .. code-block:: python - import ifcopenshell - import ifcopenshell.geom - import multiprocessing - - settings = ifcopenshell.geom.settings() - - # Settings for glTF / glb - settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) - # Note that applying default materials is required in glTF serialisation. - settings.set("apply-default-materials", True) - - # Settings for obj - # settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) - # settings.set("apply-default-materials", True) - # settings.set("use-world-coords", True) - - # Serialise to glTF / glb - serialiser = ifcopenshell.geom.serializers.gltf("output.glb", settings) - self.serialiser_settings = ifcopenshell.geom.serializer_settings() - # Setting element GUIDs is optional, but useful to uniquely identify objects in non-semantic formats. - serialiser_settings.set("use-element-guids", True) - - # Serialise to obj - # serialiser = ifcopenshell.geom.serializers.obj('output.obj', 'output.mtl', settings, serialiser_settings) - - serialiser.setFile(self.file) - serialiser.setUnitNameAndMagnitude("METER", 1.0) - serialiser.writeHeader() - - iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count()) - if iterator.initialize(): - while True: - serialiser.write(iterator.get()) - if not iterator.next(): - break - serialiser.finalize() + import multiprocessing + + import ifcopenshell + import ifcopenshell.geom + + ifc_file = ifcopenshell.open("model.ifc") + + settings = ifcopenshell.geom.settings() + + # Settings for glTF / glb + settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) + # Note that applying default materials is required in glTF serialisation. + settings.set("apply-default-materials", True) + + # Settings for obj + # settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) + # settings.set("apply-default-materials", True) + # settings.set("use-world-coords", True) + + serialiser_settings = ifcopenshell.geom.serializer_settings() + # Setting element GUIDs is optional, but useful to uniquely identify objects in non-semantic formats. + serialiser_settings.set("use-element-guids", True) + + # Serialise to glTF / glb + serialiser = ifcopenshell.geom.serializers.gltf("output.glb", settings, serialiser_settings) + + # Serialise to obj + # serialiser = ifcopenshell.geom.serializers.obj('output.obj', 'output.mtl', settings, serialiser_settings) + + serialiser.setFile(ifc_file) + serialiser.setUnitNameAndMagnitude("METER", 1.0) + serialiser.writeHeader() + + iterator = ifcopenshell.geom.iterator(settings, ifc_file, multiprocessing.cpu_count()) + if iterator.initialize(): + while True: + serialiser.write(iterator.get()) + if not iterator.next(): + break + serialiser.finalize() From 6429fe34edfd627b53bbf41433afd9a21eabd42f Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Sat, 22 Feb 2025 14:59:55 -0800 Subject: [PATCH 098/476] Implements IfcHierarchyHelper.addRelatedObject for IfcRelDefinesByType --- src/ifcparse/IfcHierarchyHelper.h | 97 ++++++++++--------------------- 1 file changed, 31 insertions(+), 66 deletions(-) diff --git a/src/ifcparse/IfcHierarchyHelper.h b/src/ifcparse/IfcHierarchyHelper.h index a8fbdf444b..412f3508dc 100644 --- a/src/ifcparse/IfcHierarchyHelper.h +++ b/src/ifcparse/IfcHierarchyHelper.h @@ -451,6 +451,37 @@ class IFC_PARSE_API IfcHierarchyHelper : public IfcParse::IfcFile { } } + template <> + void addRelatedObject(typename Schema::IfcObjectDefinition* relating_type, + typename Schema::IfcObjectDefinition* related_object, + typename Schema::IfcOwnerHistory* owner_hist) { + typename Schema::IfcRelDefinesByType::list::ptr li = instances_by_type(); + bool found = false; + for (typename Schema::IfcRelDefinesByType::list::it i = li->begin(); i != li->end(); ++i) { + typename Schema::IfcRelDefinesByType* rel = *i; + if (rel->RelatingType() == relating_type) { + typename Schema::IfcObject::list::ptr objects = rel->RelatedObjects(); + objects->push((typename Schema::IfcObject*)related_object); + rel->setRelatedObjects(objects); + found = true; + break; + } + } + if (!found) { + if (!owner_hist) { + owner_hist = getSingle(); + } + if (!owner_hist) { + owner_hist = addOwnerHistory(); + } + typename Schema::IfcObject::list::ptr related_objects(new aggregate_of()); + related_objects->push((typename Schema::IfcObject*)related_object); + typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, (typename Schema::IfcTypeObject*)relating_type); + + addEntity(t); + } + } + typename Schema::IfcOwnerHistory* addOwnerHistory(); typename Schema::IfcProject* addProject(typename Schema::IfcOwnerHistory* owner_hist = 0); void relatePlacements(typename Schema::IfcProduct* parent, typename Schema::IfcProduct* product); @@ -589,70 +620,4 @@ IFC_PARSE_API void setSurfaceColour(IfcHierarchyHelper& file, Ifc4x IFC_PARSE_API void setSurfaceColour(IfcHierarchyHelper& file, Ifc4x3_add1::IfcRepresentation* shape, Ifc4x3_add1::IfcPresentationStyle* style); #endif -/* -template <> -inline void IfcHierarchyHelper::addRelatedObject (typename Schema::IfcObjectDefinition* relating_structure, - typename Schema::IfcObjectDefinition* related_object, typename Schema::IfcOwnerHistory* owner_hist) -{ - typename Schema::IfcRelContainedInSpatialStructure::list::ptr li = instances_by_type(); - bool found = false; - for (typename Schema::IfcRelContainedInSpatialStructure::list::it i = li->begin(); i != li->end(); ++i) { - typename Schema::IfcRelContainedInSpatialStructure* rel = *i; - if (rel->RelatingStructure() == relating_structure) { - typename Schema::IfcProduct::list::ptr products = rel->RelatedElements(); - products->push((typename Schema::IfcProduct*)related_object); - rel->setRelatedElements(products); - found = true; - break; - } - } - if (! found) { - if (! owner_hist) { - owner_hist = getSingle(); - } - if (! owner_hist) { - owner_hist = addOwnerHistory(); - } - typename Schema::IfcProduct::list::ptr related_objects (new aggregate_of()); - related_objects->push((typename Schema::IfcProduct*)related_object); - typename Schema::IfcRelContainedInSpatialStructure* t = new typename Schema::IfcRelContainedInSpatialStructure(IfcParse::IfcGlobalId(), owner_hist, - boost::none, boost::none, related_objects, (typename Schema::IfcSpatialStructureElement*)relating_structure); - - addEntity(t); - } -} - -template <> -inline void IfcHierarchyHelper::addRelatedObject (typename Schema::IfcObjectDefinition* relating_type, - typename Schema::IfcObjectDefinition* related_object, typename Schema::IfcOwnerHistory* owner_hist) -{ - typename Schema::IfcRelDefinesByType::list::ptr li = instances_by_type(); - bool found = false; - for (typename Schema::IfcRelDefinesByType::list::it i = li->begin(); i != li->end(); ++i) { - typename Schema::IfcRelDefinesByType* rel = *i; - if (rel->RelatingType() == relating_type) { - typename Schema::IfcObject::list::ptr objects = rel->RelatedObjects(); - objects->push((typename Schema::IfcObject*)related_object); - rel->setRelatedObjects(objects); - found = true; - break; - } - } - if (! found) { - if (! owner_hist) { - owner_hist = getSingle(); - } - if (! owner_hist) { - owner_hist = addOwnerHistory(); - } - typename Schema::IfcObject::list::ptr related_objects (new aggregate_of()); - related_objects->push((typename Schema::IfcObject*)related_object); - typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, - boost::none, boost::none, related_objects, (typename Schema::IfcTypeObject*)relating_type); - - addEntity(t); - } -} -*/ - #endif From 7ffd2742d65779494e4c7a415f04a133c1d0c45e Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Sat, 22 Feb 2025 16:04:01 -0800 Subject: [PATCH 099/476] Fixes bug mapping geometry for IfcSectionedSolidHorizontal that was introduced in 26ba761 --- .../mapping/IfcSectionedSolidHorizontal.cpp | 16 ++++++------ src/ifcgeom/taxonomy.cpp | 15 ++++++----- src/ifcgeom/taxonomy.h | 26 +++++++++---------- src/ifcwrap/IfcPython.i | 2 +- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index 14ee9899ad..52187d7e5e 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -31,10 +31,10 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in std::vector cross_sections; auto dir = map(inst->Directrix()); - auto pwf = taxonomy::dcast(dir); - if (!pwf) { + auto fn = taxonomy::dcast(dir); + if (!fn) { // Only implement on alignment curves - Logger::Warning("IfcSectionedSolidHorizontal is only implemented for piecewise function Directrix curves", inst); + Logger::Warning("IfcSectionedSolidHorizontal is only implemented for Directrix curves based on taxonomy::function_item", inst); return nullptr; } @@ -70,9 +70,6 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in profile_offsets.push_back(po); } -#else - return nullptr; -#endif if (faces.size() != profile_offsets.size()) { Logger::Warning("Expected CrossSections and CrossSectionPositions to be equal length, but got " + std::to_string(faces.size()) + " and " + std::to_string(profile_offsets.size()) + " respectively", inst); return nullptr; @@ -85,9 +82,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in for (size_t i = 0; i < faces.size(); ++i) { cross_sections.push_back({ longitudes[i], faces[i], profile_offsets[i] }); } - } +#else + return nullptr; +#endif + } - return make_loft(settings_, inst, pwf, cross_sections); + return make_loft(settings_, inst, fn, cross_sections); } #endif diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp index b2d48478f3..6fc7288dda 100644 --- a/src/ifcgeom/taxonomy.cpp +++ b/src/ifcgeom/taxonomy.cpp @@ -800,13 +800,14 @@ boost::optional ifcopenshell::geometry::taxonomy::curve_to_face_upgra } -boost::optional ifcopenshell::geometry::taxonomy::loop_to_piecewise_function_upgrade_impl(ptr item) { - boost::optional pwf_; +boost::optional ifcopenshell::geometry::taxonomy::loop_to_function_item_upgrade_impl(ptr item) { + boost::optional fi_; auto loop_ = dcast(item); if (loop_) { - if (loop_->pwf.is_initialized()) { - pwf_ = loop_->pwf; + if (loop_->fi.is_initialized()) { + fi_ = loop_->fi; } else { + // piecewise_function is a specialization of function_item - callers don't need to know this detail piecewise_function::spans_t spans; spans.reserve(loop_->children.size()); for (auto& edge_ : loop_->children) { @@ -828,9 +829,9 @@ boost::optional ifcopenshell::geometry::taxonomy::loop_ }; spans.emplace_back(taxonomy::make(l, fn)); } - pwf_ = make(0.0,spans); - loop_->pwf = pwf_; + fi_ = make(0.0,spans); + loop_->fi = fi_; } } - return pwf_; + return fi_; } diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index 4f73f10a66..703504abc2 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -892,7 +892,7 @@ typedef item const* ptr; DECLARE_PTR(loop) boost::optional external, closed; - boost::optional pwf; + boost::optional fi; bool is_polyhedron() const { for (auto& e : children) { @@ -1374,27 +1374,27 @@ typedef item const* ptr; } }; - boost::optional loop_to_piecewise_function_upgrade_impl(ptr item); + boost::optional loop_to_function_item_upgrade_impl(ptr item); template - class loop_to_piecewise_function_upgrade { + class loop_to_function_item_upgrade { private: - boost::optional pwf_; + boost::optional fi_; public: - loop_to_piecewise_function_upgrade(taxonomy::ptr item) { - if constexpr (std::is_same_v) { - pwf_ = loop_to_piecewise_function_upgrade_impl(item); + loop_to_function_item_upgrade(taxonomy::ptr item) { + if constexpr (std::is_same_v) { + fi_ = loop_to_function_item_upgrade_impl(item); } } operator bool() const { - return pwf_.is_initialized(); + return fi_.is_initialized(); } operator typename T::ptr() const { - if constexpr (std::is_same_v) { - if (pwf_) { - return *pwf_; + if constexpr (std::is_same_v) { + if (fi_) { + return *fi_; } } return nullptr; @@ -1435,7 +1435,7 @@ typedef item const* ptr; } } { - loop_to_piecewise_function_upgrade upg(u); + loop_to_function_item_upgrade upg(u); if (upg) { return upg; } @@ -1479,7 +1479,7 @@ typedef item const* ptr; } } { - loop_to_piecewise_function_upgrade upg(u); + loop_to_function_item_upgrade upg(u); if (upg) { return upg; } diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i index b6f344da1a..ec726fb5f2 100644 --- a/src/ifcwrap/IfcPython.i +++ b/src/ifcwrap/IfcPython.i @@ -86,7 +86,7 @@ %ignore curve_to_loop_upgrade_impl; %ignore edge_to_loop_upgrade_impl; %ignore curve_to_face_upgrade_impl; -%ignore loop_to_piecewise_function_upgrade_impl; +%ignore loop_to_function_item_upgrade_impl; // settings, can this done more generally? %ignore UseElementNames; From 12f8a1f769e6c9caba4371796c9c74f57114b5a3 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Sat, 22 Feb 2025 16:15:00 -0800 Subject: [PATCH 100/476] Adds utility to stringize station values --- .../ifcopenshell/util/stationing.py | 51 +++++++++++++++++++ .../test/util/test_stationing.py | 45 ++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/util/stationing.py create mode 100644 src/ifcopenshell-python/test/util/test_stationing.py diff --git a/src/ifcopenshell-python/ifcopenshell/util/stationing.py b/src/ifcopenshell-python/ifcopenshell/util/stationing.py new file mode 100644 index 0000000000..2990053295 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/util/stationing.py @@ -0,0 +1,51 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Thomas Krijnen +# +# 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 . + +import math + +def station_as_string(station:float,plus_seperator=3,accuracy=3): + """ + Returns a stringized version of a station. Example 100.0 is 1+00.00 as a stationing string + @param station: the station to be stringized + @param plus_seperator: location of the '+' symbol relative to the decimal place (typically 2 for US units and 3 for SI units) + @param accuracy: number of digits following the decimal place + """ + value = math.fabs(station) + + shifter = math.pow(10.0,plus_seperator) + v1 = math.floor(value/shifter) + v2 = value - v1*shifter + + # Check to make sure that v2 is not basically the same as shifter + # If station = 69500.00000, we sometimes get 694+100.00 instead of 695+00.00 + if math.isclose(v2-shifter,5.0*math.pow(10.0,-(accuracy+1))): + v2 = 0.0 + v1 += 1 + + v1 = -1*v1 if station < 0 else v1 + + station_string = "{:d}+{:0{}.{}f}".format(v1,v2,plus_seperator+accuracy+1,accuracy) + + # special case when v1 is 0 and station is negative, the string above doesn't get the leading + # negative sign. this snippet fixes that + if v1 == 0 and station < 0: + station_string = "-" + station_string + + return station_string + + diff --git a/src/ifcopenshell-python/test/util/test_stationing.py b/src/ifcopenshell-python/test/util/test_stationing.py new file mode 100644 index 0000000000..b1e64c64d4 --- /dev/null +++ b/src/ifcopenshell-python/test/util/test_stationing.py @@ -0,0 +1,45 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2021 Dion Moult +# +# 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 . + +import ifcopenshell.util.stationing as sta + +def test_station_as_string(): + # test with a bunch of "random" station values + s = sta.station_as_string(0.0) + assert(s == "0+000.000") + + s = sta.station_as_string(0.0,2,2) + assert(s == "0+00.00") + + s = sta.station_as_string(0.0,2) + assert(s == "0+00.000") + + s = sta.station_as_string(100.00) + assert(s == "0+100.000") + + s = sta.station_as_string(-100.00) + assert(s == "-0+100.000") + + s = sta.station_as_string(123456.789,2,2) + assert(s == "1234+56.79") + + s = sta.station_as_string(-123456.789,2,2) + assert(s == "-1234+56.79") + + s = sta.station_as_string(123456.789,3,4) + assert(s == "123+456.7890") From 8c16f394c14ae5c7317f5fcd2da5e643ecf47d26 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Sat, 22 Feb 2025 16:16:59 -0800 Subject: [PATCH 101/476] Updates alignment api Fixes mapping of vertical alignment parabola business logic to geometry. Adds creation function of h and v alignment. Adds stationing referent at start of alignment. Fixes utility functions --- src/ifcgeom/function_item_evaluator.cpp | 8 + src/ifcgeom/function_item_evaluator.h | 11 + .../ifcopenshell/alignment.py | 515 +++++++++++++++++- src/ifcparse/IfcAlignmentHelper.cpp | 21 +- 4 files changed, 525 insertions(+), 30 deletions(-) diff --git a/src/ifcgeom/function_item_evaluator.cpp b/src/ifcgeom/function_item_evaluator.cpp index 9ca71b3450..1d5c0d186a 100644 --- a/src/ifcgeom/function_item_evaluator.cpp +++ b/src/ifcgeom/function_item_evaluator.cpp @@ -1,8 +1,16 @@ #include "function_item_evaluator.h" #include "profile_helper.h" +#include + using namespace ifcopenshell::geometry; +double ifcopenshell::geometry::polynomial_length(double A, double B, double C, double horizontal_length) { + auto fn = [A, B, C](double x) -> double { return sqrt(pow(B + 2 * C * x, 2.0) + 1.0); }; + auto l = boost::math::quadrature::trapezoidal(fn, 0.0, horizontal_length); + return l; +} + struct functor_fn_evaluator : public fn_evaluator { functor_fn_evaluator(taxonomy::functor_item::const_ptr fn, const ifcopenshell::geometry::Settings& settings) : fn_evaluator(settings), diff --git a/src/ifcgeom/function_item_evaluator.h b/src/ifcgeom/function_item_evaluator.h index 0e29c6149c..3107b615dc 100644 --- a/src/ifcgeom/function_item_evaluator.h +++ b/src/ifcgeom/function_item_evaluator.h @@ -7,6 +7,17 @@ namespace ifcopenshell { namespace geometry { +/// @brief Computes the curve length of a polynomial of the form y = A + Bx + Cx^2 +/// This function is needed on the python side. To do this computation, a large library like scipy +/// is needed. That is too much overhead. For this reason, a simple function is here on the C++ side +/// that the python side can call +/// @param A constant term +/// @param B linear term +/// @param C quadradic term +/// @param horizontal_length length of the polynomal projected onto the horizontal axis +/// @return curve length +double polynomial_length(double A, double B, double C,double horizontal_length); + /// @brief Abstract class for evaluating a function_item. This class is specialized for each of the function_item types. struct fn_evaluator { fn_evaluator(const ifcopenshell::geometry::Settings& settings) : settings_(settings) { diff --git a/src/ifcopenshell-python/ifcopenshell/alignment.py b/src/ifcopenshell-python/ifcopenshell/alignment.py index d0e46fe16f..ace40847f7 100644 --- a/src/ifcopenshell-python/ifcopenshell/alignment.py +++ b/src/ifcopenshell-python/ifcopenshell/alignment.py @@ -28,6 +28,8 @@ import ifcopenshell.guid import ifcopenshell.template from ifcopenshell import entity_instance from ifcopenshell import ifcopenshell_wrapper +import ifcopenshell.util +import ifcopenshell.util.stationing def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np.ndarray: @@ -46,10 +48,10 @@ def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np # TODO: confirm point is not beyond limits of alignment s = ifcopenshell.geom.settings() - piecewise_function = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data) - pwf_evaluator = ifcopenshell_wrapper.piecewise_function_evaluator(piecewise_function, s) + function_item = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data) + evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) - trans_matrix = pwf_evaluator.evaluate(dist_along) + trans_matrix = evaluator.evaluate(dist_along) return np.array(trans_matrix, dtype=np.float64).T @@ -68,10 +70,10 @@ def evaluate_segment(segment: entity_instance, dist_along: float) -> np.ndarray: raise ValueError(f"Provided value {dist_along=} is beyond the end of the segment ({segment.SegmentLength}).") s = ifcopenshell.geom.settings() - piecewise_function = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data) - pwf_evaluator = ifcopenshell_wrapper.piecewise_function_evaluator(piecewise_function, s) + function_item = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data) + evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) - trans_matrix = pwf_evaluator.evaluate(dist_along) + trans_matrix = evaluator.evaluate(dist_along) return np.array(trans_matrix, dtype=np.float64).T @@ -90,7 +92,8 @@ def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0 raise ValueError("Alignment representation not found.") s = ifcopenshell.geom.settings() - s.set("PIECEWISE_STEP_PARAM", distance_interval) + s.set("piecewise-step-type",0) # 0 = step-size is maximum step size, 1 = step-size is mininimum number of steps + s.set("piecewise-step-size", distance_interval) shape = ifcopenshell.geom.create_shape(s, rep_curve) vertices = shape.verts if len(vertices) == 0: @@ -185,6 +188,129 @@ class IfcAlignmentHelper: alignment_segment.ObjectPlacement = global_placement alignment_segment.Representation = product + def _map_alignment_vertical_segment(self, segment: entity_instance) -> Sequence[entity_instance]: + segment_type = segment.is_a().upper() + expected_type = "IFCALIGNMENTVERTICALSEGMENT" + if not segment_type == expected_type: + raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment_type}'.") + + start_distance_along = segment.StartDistAlong + horizontal_length = segment.HorizontalLength + start_height = segment.StartHeight + start_gradient = segment.StartGradient + end_gradient = segment.EndGradient + radius_of_curvature = segment.RadiusOfCurvature + + if math.isclose(horizontal_length, 0): + # set transition value based on whether this is the final zero-length segment + transition = "DISCONTINUOUS" + else: + transition = "CONTSAMEGRADIENTSAMECURVATURE" + + _type = segment.PredefinedType + + match _type: + case "CONSTANTGRADIENT": + parent_curve = self._file.create_entity( + type="IfcLine", + Pnt=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(0.0,0.0),), + Dir=self._file.create_entity(type="IfcVector", + Orientation=self._file.create_entity(type="IfcDirection",DirectionRatios=(1.0,0.0),), + Magnitude=1.0,), + ) + + dx = math.cos(math.atan(start_gradient)) + dy = math.sin(math.atan(start_gradient)) + curve_segment_length = horizontal_length/dx + + curve_segment = self._file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(start_distance_along,start_height)), + RefDirection=self._file.createIfcDirection((dx,dy)), + ), + SegmentStart=self._file.createIfcLengthMeasure(0.0), + SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), + ParentCurve=parent_curve, + ) + result = (curve_segment, None) + + case "PARABOLICARC": + A = start_height + B = start_gradient + C = (end_gradient - start_gradient)/(2.0*horizontal_length) + + parent_curve = self._file.create_entity( + type="IfcPolynomialCurve", + Position=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(0.0,0.0)), + RefDirection=self._file.createIfcDirection((1.0, 0.0),), + ), + CoefficientsX=(0.0,1.0), + CoefficientsY=(A,B,C), + ) + + dx = math.cos(math.atan(start_gradient)) + dy = math.sin(math.atan(start_gradient)) + curve_segment_length = ifcopenshell_wrapper.polynomial_length(A,B,C,horizontal_length) + + curve_segment = self._file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(start_distance_along,start_height)), + RefDirection=self._file.createIfcDirection((dx,dy)), + ), + SegmentStart=self._file.createIfcLengthMeasure(0.0), + SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), + ParentCurve=parent_curve, + ) + result = (curve_segment, None) + + case "CIRCULARARC": + start_angle = math.atan(start_gradient) + end_angle = math.atan(end_gradient) + if start_angle < end_angle: + radius = horizontal_length/(math.sin(end_angle) - math.sin(start_angle)) + else: + radius = horizontal_length/(math.sin(start_angle) - math.sin(end_angle)) + + parent_curve = self._file.create_entity( + type="IfcCircle", + Position=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(0.0,0.0)), + RefDirection=self._file.createIfcDirection((1.0, 0.0),), + ), + Radius=radius, + ) + + segment_curve_length = radius*math.fabs(end_angle - start_angle) + + curve_segment = self._file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=self._file.create_entity( + type="IfcAxis2Placement2D", + Location=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(start_distance_along,start_height)), + RefDirection=self._file.createIfcDirection((1.0,0.0), + ), + ), + SegmentStart=self._file.createIfcLengthMeasure(0.0), + SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), + ParentCurve=parent_curve, + ) + result = (curve_segment, None) + + case _: + result = (None, None) + + return result + def _map_alignment_horizontal_segment(self, segment: entity_instance) -> Sequence[entity_instance]: segment_type = segment.is_a().upper() expected_type = "IFCALIGNMENTHORIZONTALSEGMENT" @@ -550,10 +676,11 @@ class IfcAlignmentHelper: alignment.Representation = product_definition_shape # create referent for start station + start_station_name = "Start Station ({})".format(ifcopenshell.util.stationing.station_as_string(start_station)) start_referent = self._file.createIfcReferent( GlobalId=ifcopenshell.guid.new(), OwnerHistory=None, - Name="Start Station", + Name=start_station_name, Description=None, ObjectType=None, ObjectPlacement=self._file.createIfcLinearPlacement( @@ -571,6 +698,8 @@ class IfcAlignmentHelper: Representation=None, PredefinedType="STATION", ) + pset_stationing = ifcopenshell.api.pset.add_pset(self._file,product=start_referent,name="Pset_Stationing") + ifcopenshell.api.pset.edit_pset(self._file,pset=pset_stationing,properties={"Station":start_station}) # nest the horizontal and the referent under the alignment nesting_of_alignment = self._file.create_entity( @@ -594,12 +723,11 @@ class IfcAlignmentHelper: return alignment - def add_vertical_alignment( + def _create_vertical_alignment( self, - name: str, - description: str, + composite_curve: entity_instance, vpoints: Sequence[Sequence[float]], - vclengths: Sequence[Sequence[float]], + lengths: Sequence[float], include_geometry: bool = True, ): """ @@ -608,12 +736,353 @@ class IfcAlignmentHelper: @param name: value for Name attribute @param description: value for Description attribute @param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. - @param vclengths: radii values to use for transition + @param vclengths: horizontal length of parabolic vertical curves @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic """ - pass + vertical_segments = list() # business logic + vertical_curve_segments = list() # geometry + xPBG, yPBG = vpoints[0] + xPVI, yPVI = vpoints[1] + i = 1 + for length in lengths: + # back gradient + dxBG = xPVI - xPBG + dyBG = yPVI - yPBG + start_slope = math.tan(math.atan2(dyBG,dxBG)) - def add_alignment( + #forward gradient + i += 1 + xPFG, yPFG = vpoints[i] + dxFG = xPFG - xPVI + dyFG = yPFG - yPVI + end_slope = math.tan(math.atan2(dyFG,dxFG)) + + xEVC = xPVI + length/2.0 + yEVC = yPVI + end_slope * length/2.0 + + # create gradient + gradient_length = dxBG - length/2.0 + design_parameters = self._file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xPBG, + HorizontalLength=gradient_length, + StartHeight=yPBG, + StartGradient=start_slope, + EndGradient=start_slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT" + ) + alignment_segment = self._file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + vertical_segments.append(alignment_segment) + + if include_geometry: + vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) + + # create vertical curve + k = (end_slope - start_slope)/length + xBVC = xPVI - length/2.0 + yBVC = yPVI - start_slope*length/2.0 + + design_parameters = self._file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xBVC, + HorizontalLength=length, + StartHeight=yBVC, + StartGradient=start_slope, + EndGradient=end_slope, + RadiusOfCurvature=1/k, + PredefinedType="PARABOLICARC" + ) + alignment_segment = self._file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + vertical_segments.append(alignment_segment) + + if include_geometry: + vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) + + # start of next curve is end of this curve + xPBG = xEVC + yPBG = yEVC + xPVI = xPFG + yPVI = yPFG + + + # create last gradient run + dx = xPVI - xPBG + dy = yPVI - yPBG + slope = math.tan(math.atan2(dy,dx)) + gradient_length = dx + + design_parameters = self._file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xPBG, + HorizontalLength=gradient_length, + StartHeight=yPBG, + StartGradient=slope, + EndGradient=slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT" + ) + alignment_segment = self._file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + vertical_segments.append(alignment_segment) + + if include_geometry: + vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) + + # create zero length terminator segment + design_parameters = self._file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag="VPOE", + EndTag="VPOE", + StartDistAlong=xPVI, + HorizontalLength=0.0, + StartHeight=yPVI, + StartGradient=slope, + EndGradient=slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT" + ) + alignment_segment = self._file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + vertical_segments.append(alignment_segment) + + if include_geometry: + vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) + + if include_geometry: + gradient_curve = self._file.create_entity( + type="IfcGradientCurve", + Segments=vertical_curve_segments, + SelfIntersect=False, + BaseCurve=composite_curve, + EndPoint=None + ) + else: + gradient_curve = None + + return vertical_segments, vertical_curve_segments, gradient_curve + + def create_alignment_by_pi_method( + self, + alignment_name: str, + points: Sequence[Sequence[float]], + radii: Sequence[float], + vpoints: Sequence[Sequence[float]], + lengths: Sequence[float], + alignment_description: str = None, + start_station: float = 1000.0, + include_geometry: bool = True + ): + """ + Create an alignment using the PI layout method for both horizontal and vertical alignments. + + @param alignment_name: value for Name attribute + @param alignment_description: value for Description attribute + @param points: (X,Y) pairs denoting the location of the horizontal PIs, including start and end + @param radii: radii values to use for transition + @param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. + @param lengths: parabolic vertical curve horizontal length values to use for transition + @param start_station: ??? NOT USED AT THIS TIME ??? + @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic + """ + + horizontal_segments, horizontal_curve_segments, composite_curve = self._create_horizontal_alignment(alignment_name,alignment_description,points,radii,include_geometry) + vertical_segments, vertical_curve_segments, gradient_curve = self._create_vertical_alignment(composite_curve,vpoints,lengths) + + name_segments(prefix="H",segments=horizontal_segments) + name_segments(prefix="V",segments=vertical_segments) + + # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments + horizontal_alignment = self._file.create_entity( + type="IfcAlignmentHorizontal", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=f"{alignment_name} - Horizontal", + Description=alignment_description, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + ) + + nests_horizontal_segments = self._file.create_entity( + type="IfcRelNests", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name="Nests horizontal alignment segments under horizontal alignment", + RelatingObject=horizontal_alignment, + RelatedObjects=horizontal_segments, + ) + + # Create the vertical alignment (IfcAlignmentVertical) and nest alignment segments + vertical_alignment = self._file.create_entity( + type="IfcAlignmentVertical", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=f"{alignment_name} - Vertical", + Description=alignment_description, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + ) + + nests_vertical_segments = self._file.create_entity( + type="IfcRelNests", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name="Nests vertical alignment segments under vertical alignment", + RelatingObject=vertical_alignment, + RelatedObjects=vertical_segments, + ) + + # create the alignment + placement = self._file.createIfcLocalPlacement( + PlacementRelTo=None, + RelativePlacement=self._file.createIfcAxis2Placement2D( + Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)) + ), + ) + + alignment = self._file.create_entity( + type="IfcAlignment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=alignment_name, + Description=alignment_description, + ObjectType=None, + ObjectPlacement=placement, + Representation=None, + PredefinedType=None, + ) + + # create referent for start station + start_station_name = "Start Station ({})".format(ifcopenshell.util.stationing.station_as_string(start_station)) + start_referent = self._file.createIfcReferent( + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=start_station_name, + Description=None, + ObjectType=None, + ObjectPlacement=self._file.createIfcLinearPlacement( + RelativePlacement=self._file.createIfcAxis2PlacementLinear( + Location=self._file.createIfcPointByDistanceExpression( + DistanceAlong=self._file.createIfcLengthMeasure(0.0), + OffsetLateral=None, + OffsetVertical=None, + OffsetLongitudinal=None, + BasisCurve=composite_curve, + ), + ), + CartesianPosition=None, + ), + Representation=None, + PredefinedType="STATION", + ) + pset_stationing = ifcopenshell.api.pset.add_pset(self._file,product=start_referent,name="Pset_Stationing") + ifcopenshell.api.pset.edit_pset(self._file,pset=pset_stationing,properties={"Station":start_station}) + + # nest the horizontal, vertical and the referent under the alignment + nesting_of_alignment = self._file.create_entity( + type="IfcRelNests", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name="Nests horizontal alignment, vertical alginment, and referents under overall alignment", + RelatingObject=alignment, + RelatedObjects=(horizontal_alignment, vertical_alignment, start_referent), + ) + + # aggregate the alignment under the project + project = self._file.by_type("IfcProject")[0] + alignment_within_project = self._file.createIfcRelAggregates( + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name="Aggregates alignment under the project", + RelatingObject=project, + RelatedObjects=(alignment,), + ) + + # create geometric representation + if include_geometry: + # create the footprint representation + footprint_shape_representation = self._file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=self._axis_geom_subcontext, + RepresentationIdentifier="FootPrint", + RepresentationType="Curve2D", + Items=(composite_curve,), + ) + + # create the Curve3D representation + axis3d_shape_representation = self._file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=self._axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(gradient_curve,), + ) + + # create the alignment product definition + product_definition_shape = self._file.create_entity( + type="IfcProductDefinitionShape", + Name="Alignment Product Definition Shape", + Description=None, + Representations=(footprint_shape_representation,axis3d_shape_representation,), + ) + + # create representations for each segment + self._create_segment_representations(placement, horizontal_curve_segments, horizontal_segments) + self._create_segment_representations(placement, vertical_curve_segments, vertical_segments) + + # add the representation to the alignment + alignment.Representation = product_definition_shape + + return alignment + + + def create_horizontal_alignment_by_pi_method( self, name: str, hpoints: Sequence[Sequence[float]], @@ -625,7 +1094,7 @@ class IfcAlignmentHelper: """ Create a new alignment with a horizontal alignment using the PI layout method """ - self._add_horizontal_alignment( + return self._add_horizontal_alignment( alignment_name=name, points=hpoints, radii=radii, @@ -642,7 +1111,19 @@ if __name__ == "__main__": import sys from matplotlib import pyplot as plt - f = ifcopenshell.open(sys.argv[1]) + f = ifcopenshell.file(schema="IFC4X3_ADD2") + project = f.create_entity(type="IfcProject",GlobalId=ifcopenshell.guid.new()) + context = f.create_entity(type="IfcGeometricRepresentationContext") + + points=[(0.,0.),(100.,0.),(200.,150.)] + radii=[(50.)] + + helper = IfcAlignmentHelper(f) + helper.create_horizontal_alignment_by_pi_method( + name="MyAlignment",hpoints = points,radii = radii + ) + + #f = ifcopenshell.open(sys.argv[1]) print_structure(f.by_type("IfcAlignment")[0]) al_hor_rep = f.by_type("IfcCompositeCurve")[0] diff --git a/src/ifcparse/IfcAlignmentHelper.cpp b/src/ifcparse/IfcAlignmentHelper.cpp index f60feacc3e..a2953c16c2 100644 --- a/src/ifcparse/IfcAlignmentHelper.cpp +++ b/src/ifcparse/IfcAlignmentHelper.cpp @@ -208,7 +208,7 @@ Ifc4x3_add2::IfcAlignment* addHorizontalAlignment(IfcHierarchyHelper::ptr alignment_representations(new aggregate_of()); alignment_representations->push(footprint_shape_representation); // 2D footprint @@ -258,7 +258,6 @@ std::tuple::ptr, typenam // create gradient { - file.addDoublet(xPBG, yPBG); auto gradient_length = dxBG - length/2; auto design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xPBG, gradient_length, yPBG, start_slope, start_slope, boost::none, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT); auto alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters); @@ -274,8 +273,6 @@ std::tuple::ptr, typenam double xBVC = xPVI - length / 2; double yBVC = yPVI - start_slope * length / 2; - file.addDoublet(xBVC, yBVC); - auto design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xBVC, length, yBVC, start_slope, end_slope, 1 / k, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_PARABOLICARC); auto alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters); vertical_segments->push(alignment_segment); @@ -296,7 +293,6 @@ std::tuple::ptr, typenam auto slope = tan(atan2(dy,dx)); auto gradient_length = dx; - file.addDoublet(xPBG, yPBG); auto design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xPBG, gradient_length, yPBG, slope, slope, boost::none, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT); auto alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters); vertical_segments->push(alignment_segment); @@ -305,7 +301,6 @@ std::tuple::ptr, typenam } // create zero length terminator segment - file.addDoublet(xPVI, yPVI); design_parameters = new Ifc4x3_add2::IfcAlignmentVerticalSegment(boost::none, boost::none, xPVI, 0.0, yPVI, slope, slope, boost::none, Ifc4x3_add2::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT); alignment_segment = new Ifc4x3_add2::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters); vertical_segments->push(alignment_segment); @@ -358,14 +353,14 @@ Ifc4x3_add2::IfcAlignment* addAlignment(IfcHierarchyHelper& file, c typename aggregate_of::ptr alignment_representation_items(new aggregate_of()); alignment_representation_items->push(composite_curve); - // create the footprint representation - auto footprint_shape_representation = new Ifc4x3_add2::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("FootPrint"), std::string("Curve2D"), alignment_representation_items); - file.addEntity(footprint_shape_representation); - // the gradient curve is a representation item typename aggregate_of::ptr profile_representation_items(new aggregate_of()); profile_representation_items->push(gradient_curve); + // create footprint representation + auto footprint_shape_representation = new Ifc4x3_add2::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("FootPrint"), std::string("Curve2D"), alignment_representation_items); + file.addEntity(footprint_shape_representation); + // create the axis representation auto axis3d_shape_representation = new Ifc4x3_add2::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("Axis"), std::string("Curve3D"), profile_representation_items); file.addEntity(axis3d_shape_representation); @@ -375,10 +370,10 @@ Ifc4x3_add2::IfcAlignment* addAlignment(IfcHierarchyHelper& file, c _createSegmentRepresentations(file, placement, axis_model_representation_subcontext, horizontal_curve_segments, horizontal_segments); _createSegmentRepresentations(file, placement, axis_model_representation_subcontext, vertical_curve_segments, vertical_segments); - // the alignment has two representations, a plan view footprint and a 3d curve + // the alignment has a 3d curve representation typename aggregate_of::ptr alignment_representations(new aggregate_of()); - alignment_representations->push(footprint_shape_representation); // 2D footprint - alignment_representations->push(axis3d_shape_representation); // 3D curve + alignment_representations->push(footprint_shape_representation); // 2D curve + alignment_representations->push(axis3d_shape_representation); // 3D curve // create the alignment product definition product_definition_shape = new Ifc4x3_add2::IfcProductDefinitionShape(std::string("Alignment Product Definition Shape"), boost::none, alignment_representations); From 23158b1b7e96a57cc42b759a7630a2c8f7f3b0a0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Feb 2025 12:02:50 +1100 Subject: [PATCH 102/476] Fix #6201. Regression in creating drawings due to typing in 67bac441ad9 --- src/bonsai/bonsai/tool/drawing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 0dd090c617..88f9a9e820 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -1202,8 +1202,8 @@ class Drawing(bonsai.core.tool.Drawing): def get_default_drawing_resource_path(cls, resource: str) -> Union[str, None]: project = tool.Ifc.get().by_type("IfcProject")[0] props = tool.Drawing.get_document_props() - resource_path = ( - ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or props.resource_path + resource_path = ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", f"{resource}Path") or getattr( + props, f"{resource.lower()}_path" ) if resource_path: return resource_path.replace("\\", "/") From a7509dbe292f00a1fb6662cee404da2edabe45c6 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Feb 2025 17:27:32 +1100 Subject: [PATCH 103/476] See #1227. Experimental support for 3D layerset slicing. --- src/bonsai/bonsai/bim/import_ifc.py | 6 ++- src/bonsai/bonsai/tool/loader.py | 68 +++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 318f4834e2..b159020d43 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -69,6 +69,9 @@ class MaterialCreator: if isinstance(mesh, bpy.types.Curve): return + if mesh.get("has_layer_styles", None) == True: + return + self.mesh = mesh self.obj = obj if element.is_a("IfcTypeProduct"): @@ -1059,12 +1062,13 @@ class IfcImporter: else: mesh["has_cartesian_point_offset"] = False - return tool.Loader.convert_geometry_to_mesh( + mesh = tool.Loader.convert_geometry_to_mesh( geometry, mesh, verts=verts, load_indexed_maps=self.ifc_import_settings.load_indexed_maps, ) + return tool.Loader.slice_layerset_mesh(element, mesh) except: self.ifc_import_settings.logger.error("Could not create mesh for %s", element) import traceback diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 17e8764fbf..7fb5fc4788 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1018,6 +1018,74 @@ class Loader(bonsai.core.tool.Loader): mesh["ios_material_ids"] = ifcopenshell.util.shape.get_faces_material_style_ids(geometry).tolist() return mesh + @classmethod + def slice_layerset_mesh(cls, element: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> bpy.types.Mesh: + if True: # This feature is still experimental + return mesh + if not (material := ifcopenshell.util.element.get_material(element)): + return mesh + elif material.is_a("IfcMaterialLayerSetUsage"): + usage = material + layer_set = material.ForLayerSet + offset = usage.OffsetFromReferenceLine * cls.unit_scale + sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 + elif material.is_a("IfcMaterialLayerSet"): + usage = None + layer_set = material + offset = 0 + sense_factor = 1 + else: + return mesh + if len(layer_set.MaterialLayers) == 1: + return mesh + bm = bmesh.new() + bm.from_mesh(mesh) + prev_co = None + co = Vector((0.0, offset, 0.0)) + no = Vector((0.0, 1.0, 0.0)) + # Cache this + body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") + styles = {} + has_layer_styles = False + for i, material in mesh.materials: + if style := tool.Ifc.get_entity(material): + styles[style] = i + for layer in layer_set.MaterialLayers[:-1]: + prev_co = co.copy() + co.y = layer.LayerThickness * cls.unit_scale * sense_factor + bisect_geom = bmesh.ops.bisect_plane( + bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no + ) + bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"]) + if style := ifcopenshell.util.representation.get_material_style(layer.Material, body): + if (material_index := styles.get(style, None)) is None: + material_index = len(mesh.materials) + mesh.materials.append(tool.Ifc.get_object(style)) + for face in bisect_geom["geom"]: + if isinstance(face, bmesh.types.BMFace): + center = face.calc_center_bounds() * sense_factor + if center.y < co.y and center.y > prev_co.y: + face.material_index = material_index + has_layer_styles = True + + # Last layer + layer = layer_set.MaterialLayers[-1] + if style := ifcopenshell.util.representation.get_material_style(layer.Material, body): + if (material_index := styles.get(style, None)) is None: + material_index = len(mesh.materials) + mesh.materials.append(tool.Ifc.get_object(style)) + for face in bisect_geom["geom"]: + if isinstance(face, bmesh.types.BMFace): + center = face.calc_center_bounds() * sense_factor + if center.y > co.y: + face.material_index = material_index + has_layer_styles = True + + bm.to_mesh(mesh) + bm.free() + mesh["has_layer_styles"] = has_layer_styles + return mesh + @classmethod def create_mesh_from_shape( cls, From b42c3ba8c20ceff815e5e10c5c1defedaefa96a9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Feb 2025 17:28:14 +1100 Subject: [PATCH 104/476] See #1227. Wall layer test script now supports atstart/atend combo test cases and materials. --- src/bonsai/scripts/waldo.py | 112 ++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 30 deletions(-) diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index 3a308109d1..b0eac0583c 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -28,10 +28,21 @@ axis = ifcopenshell.api.context.add_context( body = ifcopenshell.api.context.add_context( f, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model ) -concrete = ifcopenshell.api.material.add_material(f, name="concrete", category="concrete") +material1 = ifcopenshell.api.material.add_material(f, name="material1", category="material1") +material2 = ifcopenshell.api.material.add_material(f, name="material2", category="material2") site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite") builder = ifcopenshell.util.shape_builder.ShapeBuilder(f) +style = ifcopenshell.api.style.add_style(f) +attributes = {"SurfaceColour": {"Name": None, "Red": 1.0, "Green": 0.5, "Blue": 0.5}, "Transparency": 0.0} +ifcopenshell.api.style.add_surface_style(f, style=style, ifc_class="IfcSurfaceStyleShading", attributes=attributes) +ifcopenshell.api.style.assign_material_style(f, material=material1, style=style, context=body) + +style = ifcopenshell.api.style.add_style(f) +attributes = {"SurfaceColour": {"Name": None, "Red": 0.5, "Green": 0.5, "Blue": 1.0}, "Transparency": 0.0} +ifcopenshell.api.style.add_surface_style(f, style=style, ifc_class="IfcSurfaceStyleShading", attributes=attributes) +ifcopenshell.api.style.assign_material_style(f, material=material2, style=style, context=body) + def test_wall(offset, p1, p2, p3, p4): offset *= 1.5 @@ -39,18 +50,18 @@ def test_wall(offset, p1, p2, p3, p4): wall_type_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="B") set_a = ifcopenshell.api.material.add_material_set(f, set_type="IfcMaterialLayerSet") - structure = ifcopenshell.api.material.add_layer(f, layer_set=set_a, material=concrete, name="structure") + structure = ifcopenshell.api.material.add_layer(f, layer_set=set_a, material=material1, name="structure") structure.Priority = p1 structure.LayerThickness = 0.1 - cladding = ifcopenshell.api.material.add_layer(f, layer_set=set_a, material=concrete, name="cladding") + cladding = ifcopenshell.api.material.add_layer(f, layer_set=set_a, material=material2, name="cladding") cladding.Priority = p2 cladding.LayerThickness = 0.05 set_b = ifcopenshell.api.material.add_material_set(f, set_type="IfcMaterialLayerSet") - structure = ifcopenshell.api.material.add_layer(f, layer_set=set_b, material=concrete, name="structure") + structure = ifcopenshell.api.material.add_layer(f, layer_set=set_b, material=material1, name="structure") structure.Priority = p3 structure.LayerThickness = 0.1 - cladding = ifcopenshell.api.material.add_layer(f, layer_set=set_b, material=concrete, name="cladding") + cladding = ifcopenshell.api.material.add_layer(f, layer_set=set_b, material=material2, name="cladding") cladding.Priority = p4 cladding.LayerThickness = 0.05 @@ -58,38 +69,79 @@ def test_wall(offset, p1, p2, p3, p4): ifcopenshell.api.material.assign_material(f, products=[wall_type_b], material=set_b) for i, rotation in enumerate((-90, -60, -120, 90, 60, 120)): - wall_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"A{p1}{p2}") - wall_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"B{p3}{p4}") + for i2, connection in enumerate(("ATEND", "ATSTART", "MIX")): + wall_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"A{p1}{p2}") + wall_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"B{p3}{p4}") - ifcopenshell.api.spatial.assign_container(f, products=[wall_a, wall_b], relating_structure=site) + ifcopenshell.api.spatial.assign_container(f, products=[wall_a, wall_b], relating_structure=site) - ifcopenshell.api.type.assign_type(f, related_objects=[wall_a], relating_type=wall_type_a) - ifcopenshell.api.type.assign_type(f, related_objects=[wall_b], relating_type=wall_type_b) + ifcopenshell.api.type.assign_type(f, related_objects=[wall_a], relating_type=wall_type_a) + ifcopenshell.api.type.assign_type(f, related_objects=[wall_b], relating_type=wall_type_b) - axis_a = builder.polyline(((0.0, 0.0), (1.0, 0.0))) - axis_b = builder.polyline(((0.0, 0.0), (1.0, 0.0))) - rep_a = builder.get_representation(axis, [axis_a]) - rep_b = builder.get_representation(axis, [axis_b]) + axis_a = builder.polyline(((0.0, 0.0), (1.0, 0.0))) + axis_b = builder.polyline(((0.0, 0.0), (1.0, 0.0))) + rep_a = builder.get_representation(axis, [axis_a]) + rep_b = builder.get_representation(axis, [axis_b]) - ifcopenshell.api.geometry.assign_representation(f, product=wall_a, representation=rep_a) - ifcopenshell.api.geometry.assign_representation(f, product=wall_b, representation=rep_b) + ifcopenshell.api.geometry.assign_representation(f, product=wall_a, representation=rep_a) + ifcopenshell.api.geometry.assign_representation(f, product=wall_b, representation=rep_b) - x_offset = i * 2 - sign_offset = 0 if rotation < 0 else 1 - matrix_a = np.eye(4) - matrix_a[:, 3][0:3] = (0 + x_offset, 0 + offset + sign_offset, 0) - matrix_b = np.eye(4) - matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b - matrix_b[:, 3][0:3] = (1 + x_offset, 1 + offset - sign_offset, 0) - ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) - ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b) + x_offset = i * 2 + x_offset += i2 * (2 * 6) + if connection == "ATEND": + sign_offset = 0 if rotation < 0 else 1 + matrix_a = np.eye(4) + matrix_a[:, 3][0:3] = (0 + x_offset, 0 + offset + sign_offset, 0) + matrix_b = np.eye(4) + matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b + matrix_b[:, 3][0:3] = (1 + x_offset, 1 + offset - sign_offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b) - ifcopenshell.api.geometry.connect_path( - f, relating_element=wall_a, related_element=wall_b, relating_connection="ATEND", related_connection="ATEND" - ) + ifcopenshell.api.geometry.connect_path( + f, + relating_element=wall_a, + related_element=wall_b, + relating_connection="ATEND", + related_connection="ATEND", + ) + elif connection == "ATSTART": + sign_offset = 0 if rotation < 0 else 1 + matrix_a = np.eye(4) + matrix_a[:, 3][0:3] = (0 + x_offset, 1 + offset - sign_offset, 0) + matrix_b = np.eye(4) + matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b + matrix_b[:, 3][0:3] = (0 + x_offset, 1 + offset - sign_offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b) - Foo(f, body).regenerate(wall_a) - Foo(f, body).regenerate(wall_b) + ifcopenshell.api.geometry.connect_path( + f, + relating_element=wall_a, + related_element=wall_b, + relating_connection="ATSTART", + related_connection="ATSTART", + ) + elif connection == "MIX": + sign_offset = 0 if rotation < 0 else 1 + matrix_a = np.eye(4) + matrix_a[:, 3][0:3] = (0 + x_offset, 1 + offset - sign_offset, 0) + matrix_b = np.eye(4) + matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b + matrix_b[:, 3][0:3] = (1 + x_offset, 1 + offset - sign_offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b) + + ifcopenshell.api.geometry.connect_path( + f, + relating_element=wall_a, + related_element=wall_b, + relating_connection="ATEND", + related_connection="ATSTART", + ) + + Foo(f, body).regenerate(wall_a) + Foo(f, body).regenerate(wall_b) PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness") From 29a890662e06dc492f9ac898a5d84ee654c0a9cb Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Feb 2025 18:30:58 +1100 Subject: [PATCH 105/476] See #6201. Another typing regression fix. --- src/bonsai/bonsai/tool/drawing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 88f9a9e820..9f72fd51ee 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -1180,6 +1180,7 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def get_default_titleblock_path(cls, name: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] + props = tool.Drawing.get_document_props() titleblocks_dir = ( ifcopenshell.util.element.get_pset(project, "BBIM_Documentation", "TitleblocksDir") or props.titleblocks_dir ) From 973c9e2ec9ee0e47544750e4e09dfb1935d52378 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Feb 2025 18:59:17 +1100 Subject: [PATCH 106/476] See #1227. Implement axis updating for new wall join code. --- src/bonsai/scripts/waldo.py | 54 ++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index b0eac0583c..8ed34f9100 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -140,17 +140,18 @@ def test_wall(offset, p1, p2, p3, p4): related_connection="ATSTART", ) - Foo(f, body).regenerate(wall_a) - Foo(f, body).regenerate(wall_b) + Foo(f, body, axis).regenerate(wall_a) + Foo(f, body, axis).regenerate(wall_b) PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness") class Foo: - def __init__(self, file, body): + def __init__(self, file, body, axis): self.file = file self.body = body + self.axis = axis def regenerate(self, wall): print("-" * 100) @@ -158,7 +159,10 @@ class Foo: layers = self.get_layers(wall) if not layers: return - axes = self.get_axes(wall, layers) + reference = self.get_reference_line(wall) + self.reference_p1, self.reference_p2 = reference + axes = self.get_axes(wall, reference, layers) + self.end_point = None self.start_points = [] self.end_points = [] for rel in wall.ConnectedTo: @@ -211,7 +215,18 @@ class Foo: builder = ifcopenshell.util.shape_builder.ShapeBuilder(wall.file) item = builder.extrude(builder.polyline(points, closed=True), magnitude=1.0) rep = builder.get_representation(self.body, items=[item]) - ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) + if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body): + ifcopenshell.util.element.replace_element(old_rep, rep) + else: + ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) + + item = builder.polyline([self.reference_p1, self.reference_p2]) + rep = builder.get_representation(self.axis, items=[item]) + if old_rep := ifcopenshell.util.representation.get_representation(wall, self.axis): + ifcopenshell.util.element.replace_element(old_rep, rep) + else: + ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) + def join(self, wall1, wall2, layers1, layers2, connection1, connection2): if connection1 == "NOTDEFINED" or connection2 == "NOTDEFINED": @@ -220,17 +235,21 @@ class Foo: print("to", wall2, layers2, connection2) # axes = self.get_axes(wall2, layers2) - axes1 = self.get_axes(wall1, layers1) - axes2 = self.get_axes(wall2, layers2) + reference1 = self.get_reference_line(wall1) + reference2 = self.get_reference_line(wall2) + axes1 = self.get_axes(wall1, reference1, layers1) + axes2 = self.get_axes(wall2, reference2, layers2) matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)) matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) print(axes1) print(axes2) - # Convert wall2 axes to wall1 local coordinates + # Convert wall2 data to wall1 local coordinates for axis in axes2: axis[0] = (matrix1i @ matrix2 @ np.concatenate((axis[0], (0, 1))))[:2] axis[1] = (matrix1i @ matrix2 @ np.concatenate((axis[1], (0, 1))))[:2] + reference2[0] = (matrix1i @ matrix2 @ np.concatenate((reference2[0], (0, 1))))[:2] + reference2[1] = (matrix1i @ matrix2 @ np.concatenate((reference2[1], (0, 1))))[:2] # Sort axes from interior to exterior if connection1 == "ATEND": @@ -300,8 +319,10 @@ class Foo: print("fpoints", points) if connection1 == "ATSTART": self.start_points = points + self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) elif connection1 == "ATEND": self.end_points = points + self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) def get_layers(self, wall) -> list: material = ifcopenshell.util.element.get_material(wall, should_skip_usage=True) @@ -333,10 +354,7 @@ class Foo: t = (y - y1) / (y2 - y1) return x1 + t * (x2 - x1) - def get_axes(self, wall, layers: list[PrioritisedLayer]): - # I think it's not actually necessary to get the exact axis line here. - axes = [] - # Start by getting Reference line + def get_reference_line(self, wall): if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"): for item in ifcopenshell.util.representation.resolve_representation(axis).Items: if item.is_a("IfcPolyline"): @@ -346,14 +364,12 @@ class Foo: else: continue if points[0][0] < points[1][0]: # An axis always goes in the +X direction - axes.append([np.array(points[0]), np.array(points[1])]) - else: - axes.append([np.array(points[1]), np.array(points[0])]) - break - else: - # TODO: derive from existing geometry - axes.append([np.array((0.0, 0.0)), np.array((1.0, 0.0))]) + return [np.array(points[0]), np.array(points[1])] + return [np.array(points[1]), np.array(points[0])] + return [np.array((0.0, 0.0)), np.array((1.0, 0.0))] + def get_axes(self, wall, reference, layers: list[PrioritisedLayer]): + axes = [[p.copy() for p in reference]] # Apply usage to convert the Reference line into MlsBase sense_factor = 1 if (usage := ifcopenshell.util.element.get_material(wall)) and usage.is_a("IfcMaterialLayerSetUage"): From ce26af5a7e1f679922d7fa8fe3f09dbfe7dc96e1 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 23 Feb 2025 11:01:04 +0100 Subject: [PATCH 107/476] Use ProfileType in mapping #6175 --- src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp b/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp index 6a51091f99..3cce6f31dc 100644 --- a/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp +++ b/src/ifcgeom/mapping/IfcArbitraryClosedProfileDef.cpp @@ -26,6 +26,10 @@ using namespace ifcopenshell::geometry; taxonomy::ptr mapping::map_impl(const IfcSchema::IfcArbitraryClosedProfileDef* inst) { auto loop = taxonomy::cast(map(inst->OuterCurve())); if (loop) { + if (inst->ProfileType() == IfcSchema::IfcProfileTypeEnum::IfcProfileType_CURVE) { + return loop; + } + auto face = taxonomy::make(); loop->external = true; face->children = { loop }; From 5599e52184da5802597e3b332ea46fe70b54ada0 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 23 Feb 2025 14:46:10 +0100 Subject: [PATCH 108/476] Option to retain original edges in cgal for non-boolean non-face-with-voids inputs #6173 --- src/ifcgeom/ConversionSettings.h | 8 +++- .../kernels/cgal/CgalConversionResult.cpp | 43 +++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/ifcgeom/ConversionSettings.h b/src/ifcgeom/ConversionSettings.h index 65b11ba394..ed172cad69 100644 --- a/src/ifcgeom/ConversionSettings.h +++ b/src/ifcgeom/ConversionSettings.h @@ -407,7 +407,11 @@ namespace ifcopenshell { static constexpr TriangulationMethod defaultvalue = TRIANGLE_MESH; }; - + struct CgalEmitOriginalEdges : public SettingBase { + static constexpr const char* const name = "cgal-original-edges"; + static constexpr const char* const description = "Try to emit original edge face boundary edges instead of recomputed ones based on face normal. Falls back to triangulated data in case of boolean operands and faces with holes."; + static constexpr bool defaultvalue = false; + }; } template @@ -500,7 +504,7 @@ namespace ifcopenshell { }; class IFC_GEOM_API Settings : public SettingsContainer< - std::tuple + std::tuple > {}; } diff --git a/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp b/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp index ac991e40a3..e7a3b2a9ce 100644 --- a/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp +++ b/src/ifcgeom/kernels/cgal/CgalConversionResult.cpp @@ -144,10 +144,15 @@ ifcopenshell::geometry::CgalShape::CgalShape(const cgal_shape_t& shape, bool con } if (shape.size_of_facets() != 1) { - // this is for handling the specical case of storing a single point in a polyhedron, + // the size_of_facets() == 1 check is for handling the specical case of + // storing a single point in a polyhedron as a degenerate triangle + // // @todo come up with a proper variant for storing lower dimensional entities - CGAL::Polygon_mesh_processing::triangulate_faces(*shape_); - CGAL::Polygon_mesh_processing::remove_degenerate_faces(*shape_); + + // @todo we don't have access to settings here so we don't know whether we should triangulate + // remove_degenerate_faces() is also called in the triangulate() call below though... + // CGAL::Polygon_mesh_processing::triangulate_faces(*shape_); + // CGAL::Polygon_mesh_processing::remove_degenerate_faces(*shape_); } } @@ -183,6 +188,15 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett // ... also becuase of transforming the vertex positions, right? cgal_shape_t s = *this; + const bool setting_use_original_edges = settings.get().get(); + + std::set> original_edges; + if (setting_use_original_edges) { + for (auto it = s.edges_begin(); it != s.edges_end(); ++it) { + original_edges.insert({ it->vertex()->point(), it->prev()->vertex()->point() }); + } + } + if (!place.is_identity()) { const auto& m = place.ccomponents(); @@ -199,14 +213,11 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett } if (!std::all_of(s.facets_begin(), s.facets_end(), [](auto f) { return f.is_triangle(); })) { - if (!s.is_valid()) { Logger::Message(Logger::LOG_ERROR, "Invalid Polyhedron_3 in object (before triangulation)"); return; } - CGAL::Polygon_mesh_processing::remove_degenerate_faces(s); - bool success = false; try { success = CGAL::Polygon_mesh_processing::triangulate_faces(s); @@ -215,27 +226,29 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett return; } + CGAL::Polygon_mesh_processing::remove_degenerate_faces(s); + if (!success) { Logger::Message(Logger::LOG_ERROR, "Triangulation failed"); return; } - // std::cout << "Triangulated model: " << s.size_of_facets() << " facets and " << s.size_of_vertices() << " vertices" << std::endl; if (!s.is_valid()) { Logger::Message(Logger::LOG_ERROR, "Invalid Polyhedron_3 in object (after triangulation)"); // return; } - } // Facet -> planar component map for determining which // edges are to be registered. std::vector> components; - partition_coplanar_components(s, components); std::map facet_to_component; - for (auto it = components.begin(); it != components.end(); ++it) { - for (auto& f : *it) { - facet_to_component[f] = it; + if (!setting_use_original_edges) { + partition_coplanar_components(s, components); + for (auto it = components.begin(); it != components.end(); ++it) { + for (auto& f : *it) { + facet_to_component[f] = it; + } } } @@ -305,8 +318,10 @@ void ifcopenshell::geometry::CgalShape::Triangulate(ifcopenshell::geometry::Sett } vertexidx[i] = (int)vidx; - is_face_boundary[i] = facet_to_component[face] != facet_to_component[current_halfedge->opposite()->face()]; - + is_face_boundary[i] = setting_use_original_edges + ? original_edges.find({ current_halfedge->vertex()->point(), current_halfedge->prev()->vertex()->point() }) != original_edges.end() + : facet_to_component[face] != facet_to_component[current_halfedge->opposite()->face()]; + ++i; ++num_vertices; ++current_halfedge; From 9736345d795e36b4762754f71173a4c279f01cd2 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 23 Feb 2025 14:46:31 +0100 Subject: [PATCH 109/476] Test case #6173 --- .../test/geom/original_edges.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/ifcopenshell-python/test/geom/original_edges.py diff --git a/src/ifcopenshell-python/test/geom/original_edges.py b/src/ifcopenshell-python/test/geom/original_edges.py new file mode 100644 index 0000000000..da6d353316 --- /dev/null +++ b/src/ifcopenshell-python/test/geom/original_edges.py @@ -0,0 +1,97 @@ +import ifcopenshell +import ifcopenshell.geom + +contents = """ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1'); +FILE_NAME('untitled.ifc','2025-02-17T08:13:34+11:00',(''),(''),'IfcOpenShell 0.0.0','Bonsai 0.8.1-alpha250208-8f261be','Nobody'); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPROJECT('3IbgqFUY99IejU9tnNSVCj',$,'My Project',$,$,$,$,(#14,#26),#9); +#2=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); +#3=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); +#4=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); +#5=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0); +#6=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.); +#7=IFCMEASUREWITHUNIT(IFCREAL(0.0174532925199433),#6); +#8=IFCCONVERSIONBASEDUNIT(#5,.PLANEANGLEUNIT.,'degree',#7); +#9=IFCUNITASSIGNMENT((#3,#4,#2,#8)); +#10=IFCCARTESIANPOINT((0.,0.,0.)); +#11=IFCDIRECTION((0.,0.,1.)); +#12=IFCDIRECTION((1.,0.,0.)); +#13=IFCAXIS2PLACEMENT3D(#10,#11,#12); +#14=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#13,$); +#15=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$); +#16=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#14,$,.GRAPH_VIEW.,$); +#17=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Box','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$); +#18=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.SECTION_VIEW.,$); +#19=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.ELEVATION_VIEW.,$); +#20=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.MODEL_VIEW.,$); +#21=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Model',*,*,*,*,#14,$,.PLAN_VIEW.,$); +#22=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Profile','Model',*,*,*,*,#14,$,.ELEVATION_VIEW.,$); +#23=IFCCARTESIANPOINT((0.,0.)); +#24=IFCDIRECTION((1.,0.)); +#25=IFCAXIS2PLACEMENT2D(#23,#24); +#26=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',2,1.E-05,#25,$); +#27=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Plan',*,*,*,*,#26,$,.GRAPH_VIEW.,$); +#28=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Plan',*,*,*,*,#26,$,.PLAN_VIEW.,$); +#29=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#26,$,.PLAN_VIEW.,$); +#30=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Annotation','Plan',*,*,*,*,#26,$,.REFLECTED_PLAN_VIEW.,$); +#31=IFCSITE('0RWRVKEyv0w8rD30dnxZgC',$,'My Site',$,$,#54,$,$,$,$,$,$,$,$); +#37=IFCBUILDING('39X8AnBtP4gvIIEtolio1r',$,'My Building',$,$,#60,$,$,$,$,$,$); +#43=IFCBUILDINGSTOREY('3u3kxyF4r8PPWfqNxA77Mg',$,'My Storey',$,$,#66,$,$,$,$); +#49=IFCRELAGGREGATES('254xcZnL5DnQg9572XWoGw',$,$,$,#1,(#31)); +#50=IFCCARTESIANPOINT((0.,0.,0.)); +#51=IFCDIRECTION((0.,0.,1.)); +#52=IFCDIRECTION((1.,0.,0.)); +#53=IFCAXIS2PLACEMENT3D(#50,#51,#52); +#54=IFCLOCALPLACEMENT($,#53); +#55=IFCRELAGGREGATES('3mOs7xn1P8ZutyBJCrBHy6',$,$,$,#31,(#37)); +#56=IFCCARTESIANPOINT((0.,0.,0.)); +#57=IFCDIRECTION((0.,0.,1.)); +#58=IFCDIRECTION((1.,0.,0.)); +#59=IFCAXIS2PLACEMENT3D(#56,#57,#58); +#60=IFCLOCALPLACEMENT(#54,#59); +#61=IFCRELAGGREGATES('061R4J$_v02e5$uJ_Zcpwu',$,$,$,#37,(#43)); +#62=IFCCARTESIANPOINT((0.,0.,0.)); +#63=IFCDIRECTION((0.,0.,1.)); +#64=IFCDIRECTION((1.,0.,0.)); +#65=IFCAXIS2PLACEMENT3D(#62,#63,#64); +#66=IFCLOCALPLACEMENT(#60,#65); +#67=IFCACTUATOR('3XPXM$jnf6gQHxVSs1SU0h',$,'Cube',$,$,#83,#78,$,.ELECTRICACTUATOR.); +#68=IFCRELCONTAINEDINSPATIALSTRUCTURE('2uCE41OAzCIh07v_z205CZ',$,$,$,(#67),#43); +#77=IFCSHAPEREPRESENTATION(#15,'Body','Tessellation',(#95)); +#78=IFCPRODUCTDEFINITIONSHAPE($,$,(#77)); +#79=IFCCARTESIANPOINT((0.,0.,0.)); +#80=IFCDIRECTION((0.,0.,1.)); +#81=IFCDIRECTION((1.,0.,0.)); +#82=IFCAXIS2PLACEMENT3D(#79,#80,#81); +#83=IFCLOCALPLACEMENT(#66,#82); +#84=IFCCARTESIANPOINTLIST3D(((-1.,1.,-1.),(-1.,-1.,-1.),(-1.,-1.,1.),(1.,1.,-1.),(-1.,1.,1.),(1.,-1.,-1.),(1.,1.,1.),(1.,-1.,1.),(1.,1.,2.),(1.,-1.,2.),(-1.,1.,2.),(-1.,-1.,2.))); +#85=IFCINDEXEDPOLYGONALFACE((5,7,4,1)); +#86=IFCINDEXEDPOLYGONALFACE((7,8,6,4)); +#87=IFCINDEXEDPOLYGONALFACE((8,3,2,6)); +#88=IFCINDEXEDPOLYGONALFACE((4,6,2,1)); +#89=IFCINDEXEDPOLYGONALFACE((5,3,12,11)); +#90=IFCINDEXEDPOLYGONALFACE((3,5,1,2)); +#91=IFCINDEXEDPOLYGONALFACE((11,12,10,9)); +#92=IFCINDEXEDPOLYGONALFACE((8,7,9,10)); +#93=IFCINDEXEDPOLYGONALFACE((3,8,10,12)); +#94=IFCINDEXEDPOLYGONALFACE((7,5,11,9)); +#95=IFCPOLYGONALFACESET(#84,$,(#85,#86,#87,#88,#89,#90,#91,#92,#93,#94),$); +ENDSEC; +END-ISO-10303-21; +""" + +def test_original_edges(): + ifc_file = ifcopenshell.file.from_string(contents) + element = ifc_file.by_id(95) + settings = ifcopenshell.geom.settings() + shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="opencascade") + assert (len(shape.edges) // 2) == 20 + shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="cgal") + assert (len(shape.edges) // 2) == 16 + settings.set('cgal-original-edges', True) + shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="cgal") + assert (len(shape.edges) // 2) == 20 From c30b5cbc089535d8068469e7507b2c79aca30cb3 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 23 Feb 2025 15:22:36 +0100 Subject: [PATCH 110/476] Add std::array typemap #6137 #6191 #6196 --- src/ifcwrap/IfcPython.i | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i index ec726fb5f2..196416586c 100644 --- a/src/ifcwrap/IfcPython.i +++ b/src/ifcwrap/IfcPython.i @@ -54,6 +54,11 @@ %include "exception.i" %include "std_shared_ptr.i" +%{ + #include +%} +%template(DoubleArray3) std::array; + %ignore IfcGeom::NumberNativeDouble; %ignore ifcopenshell::geometry::Converter; From 9289e1d0cc21f540b843127eaff765b9e84ff3e9 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sun, 23 Feb 2025 15:50:31 +0100 Subject: [PATCH 111/476] Try catch to keep the people happy #6176 --- src/ifcgeom/mapping/IfcSweptDiskSolid.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp b/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp index 1eec8c6bc7..bd779fd6cd 100644 --- a/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp +++ b/src/ifcgeom/mapping/IfcSweptDiskSolid.cpp @@ -58,8 +58,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSweptDiskSolid* inst) { auto ep = inst->EndParam(); #else boost::optional sp, ep; - sp = inst->StartParam(); - ep = inst->EndParam(); + try { + sp = inst->StartParam(); + ep = inst->EndParam(); + } catch (const IfcParse::IfcException& e) { + Logger::Warning(e); + } #endif const double tol = settings_.get().get(); From 365393aa0747ec503576fe673a914d9d22e2a623 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 14:00:13 +0500 Subject: [PATCH 112/476] Fix #6209 after 67bac44 --- src/bonsai/bonsai/tool/geometry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 6fb121766c..dd14e9791a 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -268,10 +268,9 @@ class Geometry(bonsai.core.tool.Geometry): bonsai.core.system.remove_port(tool.Ifc, tool.System, port=port) ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element) - mesh = obj.data - assert isinstance(mesh, bpy.types.Mesh) - if not tool.Ifc.get_entity_by_id(tool.Geometry.get_mesh_props(mesh).ifc_definition_id): - tool.Blender.remove_data_block(mesh) + data = obj.data + if data and tool.Geometry.get_data_representation(data): + tool.Blender.remove_data_block(data) if is_spatial: bonsai.core.spatial.import_spatial_decomposition(tool.Spatial) From ee8a3927b431371667995d7cd1531c8c61f91000 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 14:19:52 +0500 Subject: [PATCH 113/476] Fix displaying some attr types names after e8cb9e7 #6216 --- src/bonsai/bonsai/bim/prop.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index e335af9a0e..9a69807c23 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -273,12 +273,16 @@ def set_length_value(self: "Attribute", value: float) -> None: def get_display_name(self: "Attribute") -> str: + DISPLAY_UNIT_TYPES = ("AREA", "VOLUME", "FORCE") name = self.name - if not self.special_type or self.special_type == "LENGTH": + if not self.special_type or self.special_type not in DISPLAY_UNIT_TYPES: return name unit_type = f"{self.special_type}UNIT" project_unit = ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), unit_type) + if not project_unit: + return name + unit_symbol = ifcopenshell.util.unit.get_unit_symbol(project_unit) return f"{name}, {unit_symbol}" From a81b4a7dbbd5678b281fcad98fd370591511df98 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Mon, 24 Feb 2025 10:30:49 +0100 Subject: [PATCH 114/476] Create build_all.yml --- .github/workflows/build_all.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/build_all.yml diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml new file mode 100644 index 0000000000..57a4d8f707 --- /dev/null +++ b/.github/workflows/build_all.yml @@ -0,0 +1,21 @@ +name: Dispatch Build IfcOpenShell + +on: + workflow_dispatch: + +jobs: + trigger-workflows: + runs-on: ubuntu-latest + strategy: + matrix: + workflow: + - 'Build IfcOpenShell Linux' + - 'Build IfcOpenShell Linux ARM' + - 'Build IfcOpenShell OSX' + - 'Build IfcOpenShell WASM / Pyodide' + - 'Build IfcOpenShell Windows' + steps: + - name: Trigger binary build workflows + uses: benc-uk/workflow-dispatch@v1 + with: + workflow: ${{ matrix.workflow }} From 76277c35d2fc978fb603d0cf709d49973eb175dd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 15:00:55 +0500 Subject: [PATCH 115/476] Fix for 365393aa07 ahh, forgot that representation is already removed at this point --- src/bonsai/bonsai/tool/geometry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index dd14e9791a..93c6ee7420 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -269,7 +269,9 @@ class Geometry(bonsai.core.tool.Geometry): ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element) data = obj.data - if data and tool.Geometry.get_data_representation(data): + if tool.Geometry.has_mesh_properties(data) and tool.Ifc.get_entity_by_id( + tool.Geometry.get_mesh_props(data).ifc_definition_id + ): tool.Blender.remove_data_block(data) if is_spatial: From 076a6c152dc24abcf0167588069e67d5b46d1b7c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 11:36:13 +0500 Subject: [PATCH 116/476] fix typo --- .../api/structural/remove_structural_boundary_condition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py index bd9018c1e2..63c6331e51 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py @@ -24,7 +24,7 @@ def remove_structural_boundary_condition( connection: Optional[ifcopenshell.entity_instance] = None, boundary_condition: Optional[ifcopenshell.entity_instance] = None, ) -> None: - """Removes a condition from a connection, or an orphased boundary condition + """Removes a condition from a connection, or an orphaned boundary condition :param connection: The IfcStructuralConnection to remove the condition from. If omitted, it is assumed to be an orphaned condition. From e79a85a4fd310c5dbf3eac70006557a7195ca2fa Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 12:28:20 +0500 Subject: [PATCH 117/476] bim.edit_container_attributes - remove deprecated operator Currently you can edit container name directly from spatial manager, so this operator is no longer needed (and it also was missing from UI either way). --- src/bonsai/bonsai/bim/module/spatial/__init__.py | 1 - src/bonsai/bonsai/bim/module/spatial/operator.py | 11 ----------- src/bonsai/bonsai/core/spatial.py | 5 ----- src/bonsai/bonsai/core/tool.py | 1 - src/bonsai/bonsai/tool/spatial.py | 9 --------- 5 files changed, 27 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/__init__.py b/src/bonsai/bonsai/bim/module/spatial/__init__.py index 3194d18f3a..ee40768f08 100644 --- a/src/bonsai/bonsai/bim/module/spatial/__init__.py +++ b/src/bonsai/bonsai/bim/module/spatial/__init__.py @@ -26,7 +26,6 @@ classes = ( operator.DeleteContainer, operator.DereferenceStructure, operator.DisableEditingContainer, - operator.EditContainerAttributes, operator.EnableEditingContainer, operator.ExpandContainer, operator.ImportSpatialDecomposition, diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index dc68d95a8a..9caa736cbe 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -242,17 +242,6 @@ class ImportSpatialDecomposition(bpy.types.Operator): return {"FINISHED"} -class EditContainerAttributes(bpy.types.Operator): - bl_idname = "bim.edit_container_attributes" - bl_label = "Edit container attributes" - bl_options = {"REGISTER", "UNDO"} - container: bpy.props.IntProperty() - - def execute(self, context): - core.edit_container_attributes(tool.Spatial, entity=tool.Ifc.get().by_id(self.container)) - return {"FINISHED"} - - class ContractContainer(bpy.types.Operator): bl_idname = "bim.contract_container" bl_label = "Contract Container" diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index eff50ec2a3..d6097484bf 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -121,11 +121,6 @@ def import_spatial_decomposition(spatial: tool.Spatial) -> None: spatial.import_spatial_decomposition() -def edit_container_attributes(spatial: tool.Spatial, entity: ifcopenshell.entity_instance) -> None: - spatial.edit_container_attributes(entity) - spatial.import_spatial_decomposition() - - def contract_container(spatial: tool.Spatial, container: ifcopenshell.entity_instance) -> None: spatial.contract_container(container) spatial.import_spatial_decomposition() diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index b84e931871..d96b019df2 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -901,7 +901,6 @@ class Spatial: def deselect_objects(cls): pass def disable_editing(cls, obj): pass def duplicate_object_and_data(cls, obj): pass - def edit_container_attributes(cls, entity): pass def edit_container_name(cls, container, name): pass def enable_editing(cls, obj): pass def expand_container(cls, container): pass diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 2f59e0b3da..26ae9db532 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -479,15 +479,6 @@ class Spatial(bonsai.core.tool.Spatial): for child in children or []: cls.import_spatial_element(child, level_index + 1) - @classmethod - def edit_container_attributes(cls, entity: ifcopenshell.entity_instance) -> None: - # TODO - obj = tool.Ifc.get_object(entity) - bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) - name = bpy.context.scene.BIMSpatialDecompositionProperties.container_name - if name != entity.Name: - cls.edit_container_name(entity, name) - @classmethod def edit_container_name(cls, container: ifcopenshell.entity_instance, name: str) -> None: tool.Ifc.run("attribute.edit_attributes", product=container, attributes={"Name": name}) From 1d2ac9426dba2eb992a2a969a7e8ee157b589c24 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 14:51:45 +0500 Subject: [PATCH 118/476] Fix error during class reassignment after 43cc7a0 --- src/bonsai/bonsai/bim/module/root/operator.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 0d6a7b4d0f..f04cde36f1 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -44,7 +44,9 @@ class EnableReassignClass(bpy.types.Operator): def execute(self, context): obj = context.active_object self.file = tool.Ifc.get() - ifc_class = obj.name.split("/")[0] + element = tool.Ifc.get_entity(obj) + assert element + ifc_class = element.is_a() context.active_object.BIMObjectProperties.is_reassigning_class = True ifc_products = [ "IfcElement", @@ -58,9 +60,16 @@ class EnableReassignClass(bpy.types.Operator): "IfcRelSpaceBoundary", ] schema = tool.Ifc.schema() + declaration = schema.declaration_by_name(ifc_class) for ifc_product in ifc_products: - if schema.declaration_by_name(ifc_class).is_a(ifc_product): + if ifcopenshell.util.schema.is_a(declaration, ifc_product): context.scene.BIMRootProperties.ifc_product = ifc_product + break + else: + self.report({"ERROR"}, f"Couldn't find matching IFC product for the selected object: '{element}'.") + obj.BIMObjectProperties.is_reassigning_class = False + return {"CANCELLED"} + element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) context.scene.BIMRootProperties.ifc_class = element.is_a() context.scene.BIMRootProperties.relating_class_object = None From 5fa0aa03e4dc7834454f766ee2b307171af6621c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 14:52:54 +0500 Subject: [PATCH 119/476] util.schema.is_a - use c++ implementation --- src/ifcopenshell-python/ifcopenshell/util/schema.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema.py b/src/ifcopenshell-python/ifcopenshell/util/schema.py index f709af6b5a..70eff36eb9 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/util/schema.py @@ -80,12 +80,7 @@ def is_a(declaration: ifcopenshell.ifcopenshell_wrapper.entity, ifc_class: str) declaration = ifcopenshell.util.schema.get_declaration(wall) ifcopenshell.util.schema.is_a(declaration, "IfcRoot") # True """ - ifc_class = ifc_class.upper() - if declaration.name_uc() == ifc_class: - return True - if declaration.supertype(): - return is_a(declaration.supertype(), ifc_class) - return False + return declaration._is(ifc_class) def get_supertypes( From 57d6e9b544ac89f743b7e740667e7396bd6227e3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 15:10:10 +0500 Subject: [PATCH 120/476] Fix error reassigning class for structural items --- src/bonsai/bonsai/bim/module/root/data.py | 21 +-------------- src/bonsai/bonsai/bim/module/root/operator.py | 12 +-------- src/bonsai/bonsai/tool/root.py | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/root/data.py b/src/bonsai/bonsai/bim/module/root/data.py index 10c50839ec..8a0376eb64 100644 --- a/src/bonsai/bonsai/bim/module/root/data.py +++ b/src/bonsai/bonsai/bim/module/root/data.py @@ -54,27 +54,8 @@ class IfcClassData: @classmethod def ifc_products(cls): - products = [ - "IfcElementType", - "IfcElement", - "IfcFeatureElement", - "IfcSpatialElement", - "IfcSpatialElementType", - "IfcStructuralItem", - "IfcAnnotation", - "IfcRelSpaceBoundary", - ] + products = tool.Root.get_ifc_products() version = tool.Ifc.get_schema() - if version == "IFC2X3": - products = [ - "IfcElementType", - "IfcElement", - "IfcFeatureElement", - "IfcSpatialStructureElement", - "IfcStructuralItem", - "IfcAnnotation", - "IfcRelSpaceBoundary", - ] return [(e, e, (get_entity_doc(version, e) or {}).get("description", "")) for e in products] @classmethod diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index f04cde36f1..77fc3eebb9 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -48,17 +48,7 @@ class EnableReassignClass(bpy.types.Operator): assert element ifc_class = element.is_a() context.active_object.BIMObjectProperties.is_reassigning_class = True - ifc_products = [ - "IfcElement", - "IfcElementType", - "IfcSpatialElement", - "IfcGroup", - "IfcStructural", - "IfcPositioningElement", - "IfcContext", - "IfcAnnotation", - "IfcRelSpaceBoundary", - ] + ifc_products = tool.Root.get_ifc_products() schema = tool.Ifc.schema() declaration = schema.declaration_by_name(ifc_class) for ifc_product in ifc_products: diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index ac763cdbf4..c665e1226c 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -429,3 +429,29 @@ class Root(bonsai.core.tool.Root): tool.Ifc.unlink(obj=material) if "Ifc" in obj.name and "/" in obj.name: obj.name = obj.name.split("/", 1)[1] + + @classmethod + def get_ifc_products(cls) -> tuple[str, ...]: + version = tool.Ifc.get_schema() + if version == "IFC2X3": + products = ( + "IfcElementType", + "IfcElement", + "IfcFeatureElement", + "IfcSpatialStructureElement", + "IfcStructuralItem", + "IfcAnnotation", + "IfcRelSpaceBoundary", + ) + else: + products = ( + "IfcElementType", + "IfcElement", + "IfcFeatureElement", + "IfcSpatialElement", + "IfcSpatialElementType", + "IfcStructuralItem", + "IfcAnnotation", + "IfcRelSpaceBoundary", + ) + return products From e06684e2741ad1e7df3b5ce9ed7aa88c0e1af7dd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 16:44:54 +0500 Subject: [PATCH 121/476] Comment out outdated code #6200 --- .../bonsai/bim/module/drawing/annotation.py | 20 ++++++++++--------- src/bonsai/bonsai/core/drawing.py | 1 + 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/annotation.py b/src/bonsai/bonsai/bim/module/drawing/annotation.py index a04360f5ed..9f02989384 100644 --- a/src/bonsai/bonsai/bim/module/drawing/annotation.py +++ b/src/bonsai/bonsai/bim/module/drawing/annotation.py @@ -150,15 +150,17 @@ class Annotator: collection.objects.link(obj) return obj - if object_type != "ANGLE": - for obj in collection.objects: - element = tool.Ifc.get_entity(obj) - if ( - element - and ifcopenshell.util.element.get_predefined_type(element) == object_type - and obj.type == data_type.upper() - ): - return obj + # TODO: remove as outdated? + # Is reusing the same objects preventing the creation of new annotations. + # if object_type != "ANGLE": + # for obj in collection.objects: + # element = tool.Ifc.get_entity(obj) + # if ( + # element + # and ifcopenshell.util.element.get_predefined_type(element) == object_type + # and obj.type == data_type.upper() + # ): + # return obj if data_type == "mesh": data = bpy.data.meshes.new(object_type) diff --git a/src/bonsai/bonsai/core/drawing.py b/src/bonsai/bonsai/core/drawing.py index e5ae3cd493..c01b70bd55 100644 --- a/src/bonsai/bonsai/core/drawing.py +++ b/src/bonsai/bonsai/core/drawing.py @@ -417,6 +417,7 @@ def add_annotation( drawing_tool.show_decorations() obj = drawing_tool.create_annotation_object(drawing, object_type) element = ifc.get_entity(obj) + # TODO: element is never None? if not element: relating_type_rep = drawing_tool.get_annotation_representation(relating_type) if relating_type else None element = drawing_tool.run_root_assign_class( From 7331f7361f59b3e721b1e19ffb4f475343e80a00 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Mon, 24 Feb 2025 13:30:24 +0100 Subject: [PATCH 122/476] Workaround explicit in-class specialization to if constexpr #6206 --- src/ifcparse/IfcHierarchyHelper.h | 132 +++++++++++++++--------------- 1 file changed, 65 insertions(+), 67 deletions(-) diff --git a/src/ifcparse/IfcHierarchyHelper.h b/src/ifcparse/IfcHierarchyHelper.h index 412f3508dc..1a7e9d3933 100644 --- a/src/ifcparse/IfcHierarchyHelper.h +++ b/src/ifcparse/IfcHierarchyHelper.h @@ -404,81 +404,79 @@ class IFC_PARSE_API IfcHierarchyHelper : public IfcParse::IfcFile { template void addRelatedObject(typename Schema::IfcObjectDefinition* relating_object, typename Schema::IfcObjectDefinition* related_object, - typename Schema::IfcOwnerHistory* owner_hist = 0) { - typename T::list::ptr li = instances_by_type(); - bool found = false; - for (typename T::list::it i = li->begin(); i != li->end(); ++i) { - T* rel = *i; - try { - if (get_parent_of_relation(rel) == relating_object) { - aggregate_of_instance::ptr products = get_children_of_relation(rel); - products->push(related_object); - set_children_of_relation(rel, products); + typename Schema::IfcOwnerHistory* owner_hist = 0) + { + if constexpr (std::is_same_v) { + typename Schema::IfcRelDefinesByType::list::ptr li = instances_by_type(); + bool found = false; + for (typename Schema::IfcRelDefinesByType::list::it i = li->begin(); i != li->end(); ++i) { + typename Schema::IfcRelDefinesByType* rel = *i; + if (rel->RelatingType() == related_object) { + typename Schema::IfcObject::list::ptr objects = rel->RelatedObjects(); + objects->push((typename Schema::IfcObject*)related_object); + rel->setRelatedObjects(objects); found = true; break; } - } catch (std::exception& e) { - Logger::Error(e); - } catch (...) { - Logger::Error("Unknown error in addRelatedObject()"); - } - } - if (!found) { - if (!owner_hist) { - owner_hist = getSingle(); - } - if (!owner_hist) { - owner_hist = addOwnerHistory(); } + if (!found) { + if (!owner_hist) { + owner_hist = getSingle(); + } + if (!owner_hist) { + owner_hist = addOwnerHistory(); + } + typename Schema::IfcObject::list::ptr related_objects(new aggregate_of()); + related_objects->push((typename Schema::IfcObject*)related_object); + typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, related_object->as); - aggregate_of_instance::ptr related_objects(new aggregate_of_instance); - related_objects->push(related_object); - - IfcEntityInstanceData data = IfcEntityInstanceData(storage_t(T::Class().attribute_count())); - data.storage_.set(0, (std::string)IfcParse::IfcGlobalId()); - data.storage_.set(1, owner_hist); - int relating_index = 4; - int related_index = 5; - if (T::Class().name() == "IfcRelContainedInSpatialStructure" || std::is_base_of::value) { - // some classes have attributes reversed. - std::swap(relating_index, related_index); + addEntity(t); } - data.storage_.set(relating_index, relating_object); - data.storage_.set(related_index, related_objects); - - T* t = (T*)Schema::get_schema().instantiate(&T::Class(), std::move(data)); - addEntity(t); - } - } - - template <> - void addRelatedObject(typename Schema::IfcObjectDefinition* relating_type, - typename Schema::IfcObjectDefinition* related_object, - typename Schema::IfcOwnerHistory* owner_hist) { - typename Schema::IfcRelDefinesByType::list::ptr li = instances_by_type(); - bool found = false; - for (typename Schema::IfcRelDefinesByType::list::it i = li->begin(); i != li->end(); ++i) { - typename Schema::IfcRelDefinesByType* rel = *i; - if (rel->RelatingType() == relating_type) { - typename Schema::IfcObject::list::ptr objects = rel->RelatedObjects(); - objects->push((typename Schema::IfcObject*)related_object); - rel->setRelatedObjects(objects); - found = true; - break; + } else { + typename T::list::ptr li = instances_by_type(); + bool found = false; + for (typename T::list::it i = li->begin(); i != li->end(); ++i) { + T* rel = *i; + try { + if (get_parent_of_relation(rel) == relating_object) { + aggregate_of_instance::ptr products = get_children_of_relation(rel); + products->push(related_object); + set_children_of_relation(rel, products); + found = true; + break; + } + } catch (std::exception& e) { + Logger::Error(e); + } catch (...) { + Logger::Error("Unknown error in addRelatedObject()"); + } } - } - if (!found) { - if (!owner_hist) { - owner_hist = getSingle(); - } - if (!owner_hist) { - owner_hist = addOwnerHistory(); - } - typename Schema::IfcObject::list::ptr related_objects(new aggregate_of()); - related_objects->push((typename Schema::IfcObject*)related_object); - typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, (typename Schema::IfcTypeObject*)relating_type); + if (!found) { + if (!owner_hist) { + owner_hist = getSingle(); + } + if (!owner_hist) { + owner_hist = addOwnerHistory(); + } - addEntity(t); + aggregate_of_instance::ptr related_objects(new aggregate_of_instance); + related_objects->push(related_object); + + IfcEntityInstanceData data = IfcEntityInstanceData(storage_t(T::Class().attribute_count())); + data.storage_.set(0, (std::string)IfcParse::IfcGlobalId()); + data.storage_.set(1, owner_hist); + int relating_index = 4; + int related_index = 5; + if (T::Class().name() == "IfcRelContainedInSpatialStructure" || std::is_base_of::value) { + // some classes have attributes reversed. + std::swap(relating_index, related_index); + } + data.storage_.set(relating_index, relating_object); + data.storage_.set(related_index, related_objects); + + T* t = (T*)Schema::get_schema().instantiate(&T::Class(), std::move(data)); + addEntity(t); + } } } From 5510fb1e3cd7f468d2ebad252abbd248c93dc3d5 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Mon, 24 Feb 2025 13:30:45 +0100 Subject: [PATCH 123/476] Add constexpr --- src/serializers/GltfSerializer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/serializers/GltfSerializer.cpp b/src/serializers/GltfSerializer.cpp index 8f694801be..8b1c0ec03b 100644 --- a/src/serializers/GltfSerializer.cpp +++ b/src/serializers/GltfSerializer.cpp @@ -141,13 +141,12 @@ size_t write_accessor(json& j, std::ofstream& ofs, It begin, It end, int bufferV accessor["componentType"] = component_type::value; accessor["count"] = num; - if (N == 1) { + if constexpr (N == 1) { j["bufferViews"].push_back({ {"buffer", 0}, {"byteOffset", (size_t)ofs.tellp()}, { "byteLength", num * 4}, {"target", ELEMENT_ARRAY_BUFFER} }); } else { j["bufferViews"].push_back({ {"buffer", 0}, {"byteStride", 12}, { "byteOffset", (size_t)ofs.tellp()}, { "byteLength", num * 12}, {"target", ARRAY_BUFFER}}); } - std::array min, max; min.fill(std::numeric_limits::max()); max.fill(std::numeric_limits::lowest()); From 0b2321f47890f2c0885968e5ccef7da51512dff0 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Mon, 24 Feb 2025 13:33:53 +0100 Subject: [PATCH 124/476] Add template keyword --- src/ifcparse/IfcHierarchyHelper.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcparse/IfcHierarchyHelper.h b/src/ifcparse/IfcHierarchyHelper.h index 1a7e9d3933..eff35e7259 100644 --- a/src/ifcparse/IfcHierarchyHelper.h +++ b/src/ifcparse/IfcHierarchyHelper.h @@ -428,7 +428,7 @@ class IFC_PARSE_API IfcHierarchyHelper : public IfcParse::IfcFile { } typename Schema::IfcObject::list::ptr related_objects(new aggregate_of()); related_objects->push((typename Schema::IfcObject*)related_object); - typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, related_object->as); + typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, related_object->template as); addEntity(t); } From 3854afb34d04ab4b57f1497123bbe5a47ccc3609 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Mon, 24 Feb 2025 14:01:48 +0100 Subject: [PATCH 125/476] Update build-all.py; --force when fetch tags --- nix/build-all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/build-all.py b/nix/build-all.py index cb821a5d6d..5f2f2f0a79 100644 --- a/nix/build-all.py +++ b/nix/build-all.py @@ -350,7 +350,7 @@ def git_clone_or_pull_repository(clone_url, target_dir, revision=None): run([git, "clone", "--recursive", clone_url, target_dir]) else: logger.info(f"directory '{target_dir}' already cloned. Pulling latest changes.") - run([git, "-C", target_dir, "fetch", "--all", "--tags"]) + run([git, "-C", target_dir, "fetch", "--all", "--tags", "--force"]) # detect whether we are on a branch and pull if run([git, "rev-parse", "--abbrev-ref", "HEAD"], cwd=target_dir) != "HEAD": From 0783f8082eeb8859e42b2bebcf45be366369bf38 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 11:47:19 +0500 Subject: [PATCH 126/476] black . --- .../bonsai/bim/module/model/polyline.py | 46 +++-- src/bonsai/bonsai/bim/module/model/profile.py | 3 +- src/bonsai/bonsai/tool/polyline.py | 7 +- src/bonsai/scripts/waldo.py | 1 - .../ifcopenshell/alignment.py | 162 ++++++++++-------- .../ifcopenshell/util/stationing.py | 19 +- .../test/geom/original_edges.py | 3 +- .../test/util/test_stationing.py | 31 ++-- 8 files changed, 153 insertions(+), 119 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index d8ea5c0eda..d879a5d474 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -446,25 +446,25 @@ def get_horizontal_profile_preview_data(context, relating_type): # The first one is for the profile start, based on the current and previous segment of the polyline. # The second is for the profile end, based on the current and the next segment. if i == 0: - d = (polyline_verts[i+1] - polyline_verts[i]).normalized() + d = (polyline_verts[i + 1] - polyline_verts[i]).normalized() clip_start = d else: - d1 = (polyline_verts[i] - polyline_verts[i-1]).normalized() - d2 = (polyline_verts[i] - polyline_verts[i+1]).normalized() - clip_start = (d1-d2).normalized() - + d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized() + d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized() + clip_start = (d1 - d2).normalized() + if i == len(polyline_verts) - 2: - d = (polyline_verts[i+1] - polyline_verts[i]).normalized() + d = (polyline_verts[i + 1] - polyline_verts[i]).normalized() clip_end = d else: - d1 = (polyline_verts[i+1] - polyline_verts[i]).normalized() - d2 = (polyline_verts[i+1] - polyline_verts[i+2]).normalized() - clip_end = (d1-d2).normalized() + d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized() + d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized() + clip_end = (d1 - d2).normalized() # Rotates the profile face to the right direction - direction = polyline_verts[i+1] - polyline_verts[i] + direction = polyline_verts[i + 1] - polyline_verts[i] position = polyline_verts[i] - rotation_matrix = direction.to_track_quat('Z', 'Y').to_matrix().to_4x4() + rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4() bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix) bmesh.ops.translate(bm, verts=bm.verts, vec=position) bmesh.ops.translate(bm, verts=bm.verts, vec=-direction) @@ -474,10 +474,22 @@ def get_horizontal_profile_preview_data(context, relating_type): new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)] bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3) # Apply the cutting planes - cut = bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], plane_co=polyline_verts[i], plane_no=clip_start, clear_inner=True) + cut = bmesh.ops.bisect_plane( + bm, + geom=bm.verts[:] + bm.edges[:] + bm.faces[:], + plane_co=polyline_verts[i], + plane_no=clip_start, + clear_inner=True, + ) bm.verts.index_update() bm.edges.index_update() - cut = bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], plane_co=polyline_verts[i+1], plane_no=clip_end, clear_outer=True) + cut = bmesh.ops.bisect_plane( + bm, + geom=bm.verts[:] + bm.edges[:] + bm.faces[:], + plane_co=polyline_verts[i + 1], + plane_no=clip_end, + clear_outer=True, + ) bm.to_mesh(mesh) bm.free() @@ -489,7 +501,7 @@ def get_horizontal_profile_preview_data(context, relating_type): mesh = bpy.data.meshes.new("TempMesh2") all_bm.to_mesh(mesh) all_bm.free() - obj = bpy.data.objects.new('TempObj', mesh) + obj = bpy.data.objects.new("TempObj", mesh) bm = bmesh.new() bm.from_mesh(obj.data) bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"]) @@ -639,21 +651,21 @@ class PolylineOperator: if x: if event.shift and event.value == "PRESS" and event.type == "X": self.tool_state.use_default_container = False - self.tool_state.plane_method = "YZ" if self.tool_state.plane_method !="YZ" else None + self.tool_state.plane_method = "YZ" if self.tool_state.plane_method != "YZ" else None self.tool_state.axis_method = None tool.Blender.update_viewport() if y: if event.shift and event.value == "PRESS" and event.type == "Y": self.tool_state.use_default_container = False - self.tool_state.plane_method = "XZ" if self.tool_state.plane_method !="XZ" else None + self.tool_state.plane_method = "XZ" if self.tool_state.plane_method != "XZ" else None self.tool_state.axis_method = None tool.Blender.update_viewport() if z: if event.shift and event.value == "PRESS" and event.type == "Z": self.tool_state.use_default_container = False - self.tool_state.plane_method = "XY" if self.tool_state.plane_method !="XY" else None + self.tool_state.plane_method = "XY" if self.tool_state.plane_method != "XY" else None self.tool_state.axis_method = None tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 8448d4d857..c094da200c 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -112,10 +112,9 @@ class DumbProfileGenerator: matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world else: - rotation_matrix = self.direction.to_track_quat('Z', 'Y') + rotation_matrix = self.direction.to_track_quat("Z", "Y") matrix_world = rotation_matrix.to_matrix().to_4x4() @ matrix_world - matrix_world.translation = self.location if self.insertion_type not in {"POLYLINE"} and self.container_obj: matrix_world.translation.z = self.container_obj.location.z diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 9e48c9ae73..b5c9ad4232 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -523,11 +523,10 @@ class Polyline(bonsai.core.tool.Polyline): return "Cannot create two points at the same location" # Avoids creating overlapping edges if len(polyline_points) > 1: - v1 = Vector((x, y, z)) + v1 = Vector((x, y, z)) v2 = Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z)) - v3 = Vector((polyline_points[-2].x, polyline_points[-2].y, polyline_points[-2].z)) - angle = tool.Cad.angle_3_vectors(v1, v2, v3, new_angle=None, degrees=True - ) + v3 = Vector((polyline_points[-2].x, polyline_points[-2].y, polyline_points[-2].z)) + angle = tool.Cad.angle_3_vectors(v1, v2, v3, new_angle=None, degrees=True) if tool.Cad.is_x(angle, 0): return # TODO move this limitation to be Wall tool specific. Right now it also affects Measure tool diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index 8ed34f9100..18c6267d39 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -227,7 +227,6 @@ class Foo: else: ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) - def join(self, wall1, wall2, layers1, layers2, connection1, connection2): if connection1 == "NOTDEFINED" or connection2 == "NOTDEFINED": return diff --git a/src/ifcopenshell-python/ifcopenshell/alignment.py b/src/ifcopenshell-python/ifcopenshell/alignment.py index ace40847f7..45eb8bc054 100644 --- a/src/ifcopenshell-python/ifcopenshell/alignment.py +++ b/src/ifcopenshell-python/ifcopenshell/alignment.py @@ -92,7 +92,7 @@ def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0 raise ValueError("Alignment representation not found.") s = ifcopenshell.geom.settings() - s.set("piecewise-step-type",0) # 0 = step-size is maximum step size, 1 = step-size is mininimum number of steps + s.set("piecewise-step-type", 0) # 0 = step-size is maximum step size, 1 = step-size is mininimum number of steps s.set("piecewise-step-size", distance_interval) shape = ifcopenshell.geom.create_shape(s, rep_curve) vertices = shape.verts @@ -193,14 +193,14 @@ class IfcAlignmentHelper: expected_type = "IFCALIGNMENTVERTICALSEGMENT" if not segment_type == expected_type: raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment_type}'.") - + start_distance_along = segment.StartDistAlong horizontal_length = segment.HorizontalLength start_height = segment.StartHeight start_gradient = segment.StartGradient end_gradient = segment.EndGradient radius_of_curvature = segment.RadiusOfCurvature - + if math.isclose(horizontal_length, 0): # set transition value based on whether this is the final zero-length segment transition = "DISCONTINUOUS" @@ -213,23 +213,33 @@ class IfcAlignmentHelper: case "CONSTANTGRADIENT": parent_curve = self._file.create_entity( type="IfcLine", - Pnt=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(0.0,0.0),), - Dir=self._file.create_entity(type="IfcVector", - Orientation=self._file.create_entity(type="IfcDirection",DirectionRatios=(1.0,0.0),), - Magnitude=1.0,), - ) - + Pnt=self._file.create_entity( + type="IfcCartesianPoint", + Coordinates=(0.0, 0.0), + ), + Dir=self._file.create_entity( + type="IfcVector", + Orientation=self._file.create_entity( + type="IfcDirection", + DirectionRatios=(1.0, 0.0), + ), + Magnitude=1.0, + ), + ) + dx = math.cos(math.atan(start_gradient)) dy = math.sin(math.atan(start_gradient)) - curve_segment_length = horizontal_length/dx + curve_segment_length = horizontal_length / dx curve_segment = self._file.create_entity( type="IfcCurveSegment", Transition=transition, Placement=self._file.create_entity( type="IfcAxis2Placement2D", - Location=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(start_distance_along,start_height)), - RefDirection=self._file.createIfcDirection((dx,dy)), + Location=self._file.create_entity( + type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) + ), + RefDirection=self._file.createIfcDirection((dx, dy)), ), SegmentStart=self._file.createIfcLengthMeasure(0.0), SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), @@ -240,30 +250,34 @@ class IfcAlignmentHelper: case "PARABOLICARC": A = start_height B = start_gradient - C = (end_gradient - start_gradient)/(2.0*horizontal_length) + C = (end_gradient - start_gradient) / (2.0 * horizontal_length) parent_curve = self._file.create_entity( type="IfcPolynomialCurve", Position=self._file.create_entity( type="IfcAxis2Placement2D", - Location=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(0.0,0.0)), - RefDirection=self._file.createIfcDirection((1.0, 0.0),), + Location=self._file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)), + RefDirection=self._file.createIfcDirection( + (1.0, 0.0), + ), ), - CoefficientsX=(0.0,1.0), - CoefficientsY=(A,B,C), + CoefficientsX=(0.0, 1.0), + CoefficientsY=(A, B, C), ) - + dx = math.cos(math.atan(start_gradient)) dy = math.sin(math.atan(start_gradient)) - curve_segment_length = ifcopenshell_wrapper.polynomial_length(A,B,C,horizontal_length) + curve_segment_length = ifcopenshell_wrapper.polynomial_length(A, B, C, horizontal_length) curve_segment = self._file.create_entity( type="IfcCurveSegment", Transition=transition, Placement=self._file.create_entity( type="IfcAxis2Placement2D", - Location=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(start_distance_along,start_height)), - RefDirection=self._file.createIfcDirection((dx,dy)), + Location=self._file.create_entity( + type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) + ), + RefDirection=self._file.createIfcDirection((dx, dy)), ), SegmentStart=self._file.createIfcLengthMeasure(0.0), SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), @@ -275,29 +289,34 @@ class IfcAlignmentHelper: start_angle = math.atan(start_gradient) end_angle = math.atan(end_gradient) if start_angle < end_angle: - radius = horizontal_length/(math.sin(end_angle) - math.sin(start_angle)) + radius = horizontal_length / (math.sin(end_angle) - math.sin(start_angle)) else: - radius = horizontal_length/(math.sin(start_angle) - math.sin(end_angle)) + radius = horizontal_length / (math.sin(start_angle) - math.sin(end_angle)) parent_curve = self._file.create_entity( type="IfcCircle", Position=self._file.create_entity( type="IfcAxis2Placement2D", - Location=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(0.0,0.0)), - RefDirection=self._file.createIfcDirection((1.0, 0.0),), + Location=self._file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)), + RefDirection=self._file.createIfcDirection( + (1.0, 0.0), + ), ), Radius=radius, ) - segment_curve_length = radius*math.fabs(end_angle - start_angle) + segment_curve_length = radius * math.fabs(end_angle - start_angle) curve_segment = self._file.create_entity( type="IfcCurveSegment", Transition=transition, Placement=self._file.create_entity( type="IfcAxis2Placement2D", - Location=self._file.create_entity(type="IfcCartesianPoint",Coordinates=(start_distance_along,start_height)), - RefDirection=self._file.createIfcDirection((1.0,0.0), + Location=self._file.create_entity( + type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) + ), + RefDirection=self._file.createIfcDirection( + (1.0, 0.0), ), ), SegmentStart=self._file.createIfcLengthMeasure(0.0), @@ -676,7 +695,9 @@ class IfcAlignmentHelper: alignment.Representation = product_definition_shape # create referent for start station - start_station_name = "Start Station ({})".format(ifcopenshell.util.stationing.station_as_string(start_station)) + start_station_name = "Start Station ({})".format( + ifcopenshell.util.stationing.station_as_string(start_station) + ) start_referent = self._file.createIfcReferent( GlobalId=ifcopenshell.guid.new(), OwnerHistory=None, @@ -698,8 +719,8 @@ class IfcAlignmentHelper: Representation=None, PredefinedType="STATION", ) - pset_stationing = ifcopenshell.api.pset.add_pset(self._file,product=start_referent,name="Pset_Stationing") - ifcopenshell.api.pset.edit_pset(self._file,pset=pset_stationing,properties={"Station":start_station}) + pset_stationing = ifcopenshell.api.pset.add_pset(self._file, product=start_referent, name="Pset_Stationing") + ifcopenshell.api.pset.edit_pset(self._file, pset=pset_stationing, properties={"Station": start_station}) # nest the horizontal and the referent under the alignment nesting_of_alignment = self._file.create_entity( @@ -739,8 +760,8 @@ class IfcAlignmentHelper: @param vclengths: horizontal length of parabolic vertical curves @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic """ - vertical_segments = list() # business logic - vertical_curve_segments = list() # geometry + vertical_segments = list() # business logic + vertical_curve_segments = list() # geometry xPBG, yPBG = vpoints[0] xPVI, yPVI = vpoints[1] i = 1 @@ -748,20 +769,20 @@ class IfcAlignmentHelper: # back gradient dxBG = xPVI - xPBG dyBG = yPVI - yPBG - start_slope = math.tan(math.atan2(dyBG,dxBG)) + start_slope = math.tan(math.atan2(dyBG, dxBG)) - #forward gradient + # forward gradient i += 1 xPFG, yPFG = vpoints[i] dxFG = xPFG - xPVI dyFG = yPFG - yPVI - end_slope = math.tan(math.atan2(dyFG,dxFG)) + end_slope = math.tan(math.atan2(dyFG, dxFG)) - xEVC = xPVI + length/2.0 - yEVC = yPVI + end_slope * length/2.0 + xEVC = xPVI + length / 2.0 + yEVC = yPVI + end_slope * length / 2.0 # create gradient - gradient_length = dxBG - length/2.0 + gradient_length = dxBG - length / 2.0 design_parameters = self._file.create_entity( type="IfcAlignmentVerticalSegment", StartTag=None, @@ -772,7 +793,7 @@ class IfcAlignmentHelper: StartGradient=start_slope, EndGradient=start_slope, RadiusOfCurvature=None, - PredefinedType="CONSTANTGRADIENT" + PredefinedType="CONSTANTGRADIENT", ) alignment_segment = self._file.create_entity( type="IfcAlignmentSegment", @@ -791,9 +812,9 @@ class IfcAlignmentHelper: vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) # create vertical curve - k = (end_slope - start_slope)/length - xBVC = xPVI - length/2.0 - yBVC = yPVI - start_slope*length/2.0 + k = (end_slope - start_slope) / length + xBVC = xPVI - length / 2.0 + yBVC = yPVI - start_slope * length / 2.0 design_parameters = self._file.create_entity( type="IfcAlignmentVerticalSegment", @@ -804,8 +825,8 @@ class IfcAlignmentHelper: StartHeight=yBVC, StartGradient=start_slope, EndGradient=end_slope, - RadiusOfCurvature=1/k, - PredefinedType="PARABOLICARC" + RadiusOfCurvature=1 / k, + PredefinedType="PARABOLICARC", ) alignment_segment = self._file.create_entity( type="IfcAlignmentSegment", @@ -821,7 +842,7 @@ class IfcAlignmentHelper: vertical_segments.append(alignment_segment) if include_geometry: - vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) + vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) # start of next curve is end of this curve xPBG = xEVC @@ -829,11 +850,10 @@ class IfcAlignmentHelper: xPVI = xPFG yPVI = yPFG - # create last gradient run dx = xPVI - xPBG dy = yPVI - yPBG - slope = math.tan(math.atan2(dy,dx)) + slope = math.tan(math.atan2(dy, dx)) gradient_length = dx design_parameters = self._file.create_entity( @@ -846,7 +866,7 @@ class IfcAlignmentHelper: StartGradient=slope, EndGradient=slope, RadiusOfCurvature=None, - PredefinedType="CONSTANTGRADIENT" + PredefinedType="CONSTANTGRADIENT", ) alignment_segment = self._file.create_entity( type="IfcAlignmentSegment", @@ -875,7 +895,7 @@ class IfcAlignmentHelper: StartGradient=slope, EndGradient=slope, RadiusOfCurvature=None, - PredefinedType="CONSTANTGRADIENT" + PredefinedType="CONSTANTGRADIENT", ) alignment_segment = self._file.create_entity( type="IfcAlignmentSegment", @@ -899,10 +919,10 @@ class IfcAlignmentHelper: Segments=vertical_curve_segments, SelfIntersect=False, BaseCurve=composite_curve, - EndPoint=None + EndPoint=None, ) else: - gradient_curve = None + gradient_curve = None return vertical_segments, vertical_curve_segments, gradient_curve @@ -915,7 +935,7 @@ class IfcAlignmentHelper: lengths: Sequence[float], alignment_description: str = None, start_station: float = 1000.0, - include_geometry: bool = True + include_geometry: bool = True, ): """ Create an alignment using the PI layout method for both horizontal and vertical alignments. @@ -930,11 +950,15 @@ class IfcAlignmentHelper: @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic """ - horizontal_segments, horizontal_curve_segments, composite_curve = self._create_horizontal_alignment(alignment_name,alignment_description,points,radii,include_geometry) - vertical_segments, vertical_curve_segments, gradient_curve = self._create_vertical_alignment(composite_curve,vpoints,lengths) + horizontal_segments, horizontal_curve_segments, composite_curve = self._create_horizontal_alignment( + alignment_name, alignment_description, points, radii, include_geometry + ) + vertical_segments, vertical_curve_segments, gradient_curve = self._create_vertical_alignment( + composite_curve, vpoints, lengths + ) - name_segments(prefix="H",segments=horizontal_segments) - name_segments(prefix="V",segments=vertical_segments) + name_segments(prefix="H", segments=horizontal_segments) + name_segments(prefix="V", segments=vertical_segments) # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments horizontal_alignment = self._file.create_entity( @@ -1021,8 +1045,8 @@ class IfcAlignmentHelper: Representation=None, PredefinedType="STATION", ) - pset_stationing = ifcopenshell.api.pset.add_pset(self._file,product=start_referent,name="Pset_Stationing") - ifcopenshell.api.pset.edit_pset(self._file,pset=pset_stationing,properties={"Station":start_station}) + pset_stationing = ifcopenshell.api.pset.add_pset(self._file, product=start_referent, name="Pset_Stationing") + ifcopenshell.api.pset.edit_pset(self._file, pset=pset_stationing, properties={"Station": start_station}) # nest the horizontal, vertical and the referent under the alignment nesting_of_alignment = self._file.create_entity( @@ -1069,7 +1093,10 @@ class IfcAlignmentHelper: type="IfcProductDefinitionShape", Name="Alignment Product Definition Shape", Description=None, - Representations=(footprint_shape_representation,axis3d_shape_representation,), + Representations=( + footprint_shape_representation, + axis3d_shape_representation, + ), ) # create representations for each segment @@ -1078,9 +1105,8 @@ class IfcAlignmentHelper: # add the representation to the alignment alignment.Representation = product_definition_shape - - return alignment + return alignment def create_horizontal_alignment_by_pi_method( self, @@ -1112,18 +1138,16 @@ if __name__ == "__main__": from matplotlib import pyplot as plt f = ifcopenshell.file(schema="IFC4X3_ADD2") - project = f.create_entity(type="IfcProject",GlobalId=ifcopenshell.guid.new()) + project = f.create_entity(type="IfcProject", GlobalId=ifcopenshell.guid.new()) context = f.create_entity(type="IfcGeometricRepresentationContext") - points=[(0.,0.),(100.,0.),(200.,150.)] - radii=[(50.)] + points = [(0.0, 0.0), (100.0, 0.0), (200.0, 150.0)] + radii = [50.0] helper = IfcAlignmentHelper(f) - helper.create_horizontal_alignment_by_pi_method( - name="MyAlignment",hpoints = points,radii = radii - ) + helper.create_horizontal_alignment_by_pi_method(name="MyAlignment", hpoints=points, radii=radii) - #f = ifcopenshell.open(sys.argv[1]) + # f = ifcopenshell.open(sys.argv[1]) print_structure(f.by_type("IfcAlignment")[0]) al_hor_rep = f.by_type("IfcCompositeCurve")[0] diff --git a/src/ifcopenshell-python/ifcopenshell/util/stationing.py b/src/ifcopenshell-python/ifcopenshell/util/stationing.py index 2990053295..1e0517f177 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/stationing.py +++ b/src/ifcopenshell-python/ifcopenshell/util/stationing.py @@ -18,7 +18,8 @@ import math -def station_as_string(station:float,plus_seperator=3,accuracy=3): + +def station_as_string(station: float, plus_seperator=3, accuracy=3): """ Returns a stringized version of a station. Example 100.0 is 1+00.00 as a stationing string @param station: the station to be stringized @@ -27,25 +28,23 @@ def station_as_string(station:float,plus_seperator=3,accuracy=3): """ value = math.fabs(station) - shifter = math.pow(10.0,plus_seperator) - v1 = math.floor(value/shifter) - v2 = value - v1*shifter + shifter = math.pow(10.0, plus_seperator) + v1 = math.floor(value / shifter) + v2 = value - v1 * shifter # Check to make sure that v2 is not basically the same as shifter # If station = 69500.00000, we sometimes get 694+100.00 instead of 695+00.00 - if math.isclose(v2-shifter,5.0*math.pow(10.0,-(accuracy+1))): + if math.isclose(v2 - shifter, 5.0 * math.pow(10.0, -(accuracy + 1))): v2 = 0.0 v1 += 1 - v1 = -1*v1 if station < 0 else v1 + v1 = -1 * v1 if station < 0 else v1 - station_string = "{:d}+{:0{}.{}f}".format(v1,v2,plus_seperator+accuracy+1,accuracy) + station_string = "{:d}+{:0{}.{}f}".format(v1, v2, plus_seperator + accuracy + 1, accuracy) - # special case when v1 is 0 and station is negative, the string above doesn't get the leading + # special case when v1 is 0 and station is negative, the string above doesn't get the leading # negative sign. this snippet fixes that if v1 == 0 and station < 0: station_string = "-" + station_string return station_string - - diff --git a/src/ifcopenshell-python/test/geom/original_edges.py b/src/ifcopenshell-python/test/geom/original_edges.py index da6d353316..41fd6b8aff 100644 --- a/src/ifcopenshell-python/test/geom/original_edges.py +++ b/src/ifcopenshell-python/test/geom/original_edges.py @@ -84,6 +84,7 @@ ENDSEC; END-ISO-10303-21; """ + def test_original_edges(): ifc_file = ifcopenshell.file.from_string(contents) element = ifc_file.by_id(95) @@ -92,6 +93,6 @@ def test_original_edges(): assert (len(shape.edges) // 2) == 20 shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="cgal") assert (len(shape.edges) // 2) == 16 - settings.set('cgal-original-edges', True) + settings.set("cgal-original-edges", True) shape = ifcopenshell.geom.create_shape(settings, element, geometry_library="cgal") assert (len(shape.edges) // 2) == 20 diff --git a/src/ifcopenshell-python/test/util/test_stationing.py b/src/ifcopenshell-python/test/util/test_stationing.py index b1e64c64d4..a72a1fb6c1 100644 --- a/src/ifcopenshell-python/test/util/test_stationing.py +++ b/src/ifcopenshell-python/test/util/test_stationing.py @@ -18,28 +18,29 @@ import ifcopenshell.util.stationing as sta + def test_station_as_string(): # test with a bunch of "random" station values s = sta.station_as_string(0.0) - assert(s == "0+000.000") + assert s == "0+000.000" - s = sta.station_as_string(0.0,2,2) - assert(s == "0+00.00") + s = sta.station_as_string(0.0, 2, 2) + assert s == "0+00.00" - s = sta.station_as_string(0.0,2) - assert(s == "0+00.000") + s = sta.station_as_string(0.0, 2) + assert s == "0+00.000" - s = sta.station_as_string(100.00) - assert(s == "0+100.000") + s = sta.station_as_string(100.00) + assert s == "0+100.000" - s = sta.station_as_string(-100.00) - assert(s == "-0+100.000") + s = sta.station_as_string(-100.00) + assert s == "-0+100.000" - s = sta.station_as_string(123456.789,2,2) - assert(s == "1234+56.79") + s = sta.station_as_string(123456.789, 2, 2) + assert s == "1234+56.79" - s = sta.station_as_string(-123456.789,2,2) - assert(s == "-1234+56.79") + s = sta.station_as_string(-123456.789, 2, 2) + assert s == "-1234+56.79" - s = sta.station_as_string(123456.789,3,4) - assert(s == "123+456.7890") + s = sta.station_as_string(123456.789, 3, 4) + assert s == "123+456.7890" From 6eda4e389c16d49a19934059e6d24b4c651d2215 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 11:37:19 +0500 Subject: [PATCH 127/476] typing --- src/bonsai/bonsai/bim/import_ifc.py | 3 +- src/bonsai/bonsai/bim/module/model/data.py | 2 +- src/bonsai/bonsai/bim/module/model/product.py | 7 +- src/bonsai/bonsai/bim/module/root/data.py | 9 +- src/bonsai/bonsai/bim/module/root/operator.py | 29 ++++--- src/bonsai/bonsai/bim/module/root/prop.py | 54 +++++++----- src/bonsai/bonsai/bim/module/root/ui.py | 15 ++-- src/bonsai/bonsai/bim/module/spatial/data.py | 6 +- .../bonsai/bim/module/spatial/decorator.py | 6 +- .../bonsai/bim/module/spatial/operator.py | 2 +- src/bonsai/bonsai/bim/module/spatial/prop.py | 57 ++++++++++++- src/bonsai/bonsai/bim/module/spatial/ui.py | 85 +++++++++++++------ src/bonsai/bonsai/tool/geometry.py | 4 +- src/bonsai/bonsai/tool/project.py | 6 +- src/bonsai/bonsai/tool/root.py | 14 ++- src/bonsai/bonsai/tool/spatial.py | 75 ++++++++++------ src/bonsai/test/tool/test_model.py | 5 +- src/bonsai/test/tool/test_project.py | 6 +- src/bonsai/test/tool/test_root.py | 6 +- src/bonsai/test/tool/test_spatial.py | 6 +- .../api/cost/remove_cost_item_quantity.py | 15 ++-- .../api/cost/remove_cost_value.py | 33 +++---- .../ifcopenshell/api/grid/remove_grid_axis.py | 2 - .../api/material/assign_profile.py | 20 ++--- .../api/owner/remove_organisation.py | 20 ++--- .../ifcopenshell/api/owner/remove_person.py | 19 ++--- .../api/sequence/assign_lag_time.py | 4 +- .../api/sequence/unassign_lag_time.py | 14 +-- .../remove_structural_analysis_model.py | 10 +-- .../remove_structural_boundary_condition.py | 20 ++--- .../api/structural/remove_structural_load.py | 6 +- .../structural/remove_structural_load_case.py | 10 +-- .../remove_structural_load_group.py | 10 +-- .../ifcopenshell/util/unit.py | 1 + 34 files changed, 336 insertions(+), 245 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index b159020d43..2a3a5f97a2 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -1076,9 +1076,10 @@ class IfcImporter: print(traceback.format_exc()) def set_default_context(self): + rprops = tool.Root.get_root_props() for subcontext in self.file.by_type("IfcGeometricRepresentationSubContext"): if subcontext.ContextIdentifier == "Body": - bpy.context.scene.BIMRootProperties.contexts = str(subcontext.id()) + rprops.contexts = str(subcontext.id()) break def link_element(self, element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None: diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py index cc713e5bd7..797f72714f 100644 --- a/src/bonsai/bonsai/bim/module/model/data.py +++ b/src/bonsai/bonsai/bim/module/model/data.py @@ -97,7 +97,7 @@ class AuthoringData: @classmethod def default_container(cls) -> str | None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() if props.default_container: try: return tool.Ifc.get().by_id(props.default_container).Name diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index ef00ea7b21..4e4ebd731d 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -58,7 +58,8 @@ class AddEmptyType(bpy.types.Operator, AddObjectHelper): def execute(self, context): obj = bpy.data.objects.new("TYPEX", None) context.scene.collection.objects.link(obj) - context.scene.BIMRootProperties.ifc_product = "IfcElementType" + rprops = tool.Root.get_root_props() + rprops.ifc_product = "IfcElementType" tool.Blender.select_and_activate_single_object(context, obj) return {"FINISHED"} @@ -71,7 +72,7 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator): ifc_element_type: bpy.props.StringProperty() def _execute(self, context): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() props.ifc_product = "IfcElementType" props.ifc_class = self.ifc_element_type if self.ifc_element_type == "IfcWallType": @@ -363,7 +364,7 @@ class AddConstrTypeInstance(bpy.types.Operator, tool.Ifc.Operator): ) bonsai.core.type.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) - rprops = context.scene.BIMRootProperties + rprops = tool.Root.get_root_props() ifc_context = None if get_enum_items(rprops, "contexts", context): ifc_context = int(rprops.contexts or "0") or None diff --git a/src/bonsai/bonsai/bim/module/root/data.py b/src/bonsai/bonsai/bim/module/root/data.py index 8a0376eb64..64edaa2a9d 100644 --- a/src/bonsai/bonsai/bim/module/root/data.py +++ b/src/bonsai/bonsai/bim/module/root/data.py @@ -60,7 +60,8 @@ class IfcClassData: @classmethod def ifc_classes(cls): - ifc_product = bpy.context.scene.BIMRootProperties.ifc_product + rprops = tool.Root.get_root_props() + ifc_product = rprops.ifc_product declaration = tool.Ifc.schema().declaration_by_name(ifc_product) declarations = ifcopenshell.util.schema.get_subtypes(declaration) names = [d.name() for d in declarations] @@ -80,7 +81,8 @@ class IfcClassData: @classmethod def ifc_predefined_types(cls): types_enum = [] - ifc_class = bpy.context.scene.BIMRootProperties.ifc_class + rprops = tool.Root.get_root_props() + ifc_class = rprops.ifc_class declaration = tool.Ifc.schema().declaration_by_name(ifc_class) version = tool.Ifc.get_schema() for attribute in declaration.attributes(): @@ -114,7 +116,8 @@ class IfcClassData: @classmethod def representation_template(cls): - ifc_class = bpy.context.scene.BIMRootProperties.ifc_class + rprops = tool.Root.get_root_props() + ifc_class = rprops.ifc_class templates = [ ("EMPTY", "No Geometry", "Start with an empty object"), None, diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 77fc3eebb9..a699287365 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -42,6 +42,7 @@ class EnableReassignClass(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): + rprops = tool.Root.get_root_props() obj = context.active_object self.file = tool.Ifc.get() element = tool.Ifc.get_entity(obj) @@ -53,7 +54,7 @@ class EnableReassignClass(bpy.types.Operator): declaration = schema.declaration_by_name(ifc_class) for ifc_product in ifc_products: if ifcopenshell.util.schema.is_a(declaration, ifc_product): - context.scene.BIMRootProperties.ifc_product = ifc_product + rprops.ifc_product = ifc_product break else: self.report({"ERROR"}, f"Couldn't find matching IFC product for the selected object: '{element}'.") @@ -61,13 +62,13 @@ class EnableReassignClass(bpy.types.Operator): return {"CANCELLED"} element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) - context.scene.BIMRootProperties.ifc_class = element.is_a() - context.scene.BIMRootProperties.relating_class_object = None + rprops.ifc_class = element.is_a() + rprops.relating_class_object = None if hasattr(element, "PredefinedType"): if element.PredefinedType: - context.scene.BIMRootProperties.ifc_predefined_type = element.PredefinedType + rprops.ifc_predefined_type = element.PredefinedType userdefined_type = ifcopenshell.util.element.get_predefined_type(element) - context.scene.BIMRootProperties.ifc_userdefined_type = userdefined_type or "" + rprops.ifc_userdefined_type = userdefined_type or "" return {"FINISHED"} @@ -94,9 +95,9 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator): else: objects = set(context.selected_objects + [context.active_object]) self.file = tool.Ifc.get() - root_props = context.scene.BIMRootProperties - ifc_product: str = root_props.ifc_product - ifc_class: str = root_props.ifc_class + root_props = tool.Root.get_root_props() + ifc_product = root_props.ifc_product + ifc_class = root_props.ifc_class type_ifc_class = next(iter(ifcopenshell.util.type.get_applicable_types(ifc_class, self.file.schema)), None) predefined_type = root_props.ifc_predefined_type @@ -176,7 +177,7 @@ class AssignClass(bpy.types.Operator, tool.Ifc.Operator): ifc_representation_class: bpy.props.StringProperty() def _execute(self, context): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() objects: list[bpy.types.Object] = [] if self.obj: objects = [bpy.data.objects[self.obj]] @@ -358,7 +359,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE") def _invoke(self, context, event): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() # For convenience, preselect OBJs if applicable if props.ifc_product == "IfcFeatureElement": if (obj := tool.Blender.get_active_object(is_selected=True)) and obj.type == "MESH": @@ -376,7 +377,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): return context.window_manager.invoke_props_dialog(self) def _execute(self, context): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() predefined_type = ( props.ifc_userdefined_type if props.ifc_predefined_type == "USERDEFINED" else props.ifc_predefined_type ) @@ -500,7 +501,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): else: material = ifcopenshell.api.run("material.add_material", tool.Ifc.get(), name="Unknown") if representation_template == "PROFILESET": - profile_id = tool.Blender.get_enum_safe(context.scene.BIMRootProperties, "profile") + profile_id = tool.Blender.get_enum_safe(props, "profile") if profile_id in ("-", None): profile = next((p for p in ifc_file.by_type("IfcProfileDef") if p.ProfileName), None) if profile is None: @@ -589,7 +590,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): tool.Blender.set_active_object(obj) def draw(self, context): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() self.layout.use_property_split = True self.layout.use_property_decorate = False row = self.layout.row() @@ -600,7 +601,7 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): if not self.ifc_product: prop_with_search(self.layout, props, "ifc_product", text="Definition", should_click_ok=True) prop_with_search(self.layout, props, "ifc_class", should_click_ok=True) - ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context) + ifc_predefined_types = root_prop.get_ifc_predefined_types(props, context) if ifc_predefined_types: prop_with_search(self.layout, props, "ifc_predefined_type", should_click_ok=True) if props.ifc_predefined_type == "USERDEFINED": diff --git a/src/bonsai/bonsai/bim/module/root/prop.py b/src/bonsai/bonsai/bim/module/root/prop.py index 5243d95c03..4c358dfde1 100644 --- a/src/bonsai/bonsai/bim/module/root/prop.py +++ b/src/bonsai/bonsai/bim/module/root/prop.py @@ -34,28 +34,27 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Union -def get_ifc_predefined_types(self, context): +def get_ifc_predefined_types(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["ifc_predefined_types"] -def get_representation_template(self, context): +def get_representation_template(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["representation_template"] -def refresh_classes(self, context): - old_class = context.scene.BIMRootProperties.ifc_class - old_predefined_type = ( - context.scene.BIMRootProperties.ifc_predefined_type if get_ifc_predefined_types(self, context) else "" - ) +def refresh_classes(self: "BIMRootProperties", context: bpy.types.Context) -> None: + old_class = self.ifc_class + old_predefined_type = self.ifc_predefined_type if get_ifc_predefined_types(self, context) else "" enum = get_ifc_classes(self, context) - context.scene.BIMRootProperties.ifc_class = enum[0][0] + self.ifc_class = enum[0][0] IfcClassData.load() if self.ifc_product == "IfcFeatureElement": @@ -81,48 +80,44 @@ def refresh_classes(self, context): self.ifc_predefined_type = old_predefined_type -def refresh_predefined_types(self, context): +def refresh_predefined_types(self: "BIMRootProperties", context: bpy.types.Context) -> None: IfcClassData.load() enum = get_ifc_predefined_types(self, context) if enum: - context.scene.BIMRootProperties.ifc_predefined_type = enum[0][0] + self.ifc_predefined_type = enum[0][0] -def update_class_enum(self, context): - self.ifc_class = self.ifc_class_filter_enum - - -def get_ifc_products(self, context): +def get_ifc_products(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["ifc_products"] -def get_ifc_classes(self, context): +def get_ifc_classes(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["ifc_classes"] -def get_ifc_classes_suggestions(): +def get_ifc_classes_suggestions(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["ifc_classes_suggestions"] -def get_contexts(self, context): +def get_contexts(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["contexts"] -def get_profile(self, context): +def get_profile(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["profile"] -def update_relating_class_from_object(self, context): +def update_relating_class_from_object(self: "BIMRootProperties", context: bpy.types.Context) -> None: if self.relating_class_object is None: return element = tool.Ifc.get_entity(self.relating_class_object) @@ -139,7 +134,7 @@ def update_relating_class_from_object(self, context): bpy.ops.bim.reassign_class() -def is_object_class_applicable(self, obj): +def is_object_class_applicable(self: "BIMRootProperties", obj: bpy.types.Object) -> bool: element = tool.Ifc.get_entity(obj) if not element: return False @@ -149,11 +144,11 @@ def is_object_class_applicable(self, obj): return element.is_a("IfcTypeObject") == active_element.is_a("IfcTypeObject") -def poll_representation_obj(self, obj): +def poll_representation_obj(self: "BIMRootProperties", obj: bpy.types.Object) -> bool: return obj.type == "MESH" and obj.data.polygons -def poll_featured_obj(self, obj): +def poll_featured_obj(self: "BIMRootProperties", obj: bpy.types.Object) -> bool: return tool.Ifc.get_entity(obj) @@ -192,3 +187,16 @@ class BIMRootProperties(PropertyGroup): getter_enum_suggestions = { "ifc_class": get_ifc_classes_suggestions, } + + if TYPE_CHECKING: + contexts: str + description: str + ifc_product: str + ifc_class: str + ifc_predefined_type: str + ifc_userdefined_type: str + featured_obj: Union[bpy.types.Object, None] + representation_template: str + representation_obj: Union[bpy.types.Object, None] + profile: str + relating_class_object: Union[bpy.types.Object, None] diff --git a/src/bonsai/bonsai/bim/module/root/ui.py b/src/bonsai/bonsai/bim/module/root/ui.py index b1a2b52637..6985bf832b 100644 --- a/src/bonsai/bonsai/bim/module/root/ui.py +++ b/src/bonsai/bonsai/bim/module/root/ui.py @@ -44,6 +44,7 @@ class BIM_PT_class(Panel): if not IfcClassData.is_loaded: IfcClassData.load() props = context.active_object.BIMObjectProperties + rprops = tool.Root.get_root_props() if props.ifc_definition_id: if not IfcClassData.data["has_entity"]: row = self.layout.row(align=True) @@ -58,10 +59,10 @@ class BIM_PT_class(Panel): row.operator("bim.disable_reassign_class", icon="CANCEL", text="") self.draw_class_dropdowns( context, - root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context), + root_prop.get_ifc_predefined_types(rprops, context), is_reassigning_class=True, ) - self.layout.prop(context.scene.BIMRootProperties, "relating_class_object", icon="COPYDOWN") + self.layout.prop(rprops, "relating_class_object", icon="COPYDOWN") else: row = self.layout.row(align=True) row.label( @@ -78,16 +79,16 @@ class BIM_PT_class(Panel): if AuthoringData.data["is_representation_item_active"]: return - ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context) + ifc_predefined_types = root_prop.get_ifc_predefined_types(rprops, context) self.draw_class_dropdowns(context, ifc_predefined_types) row = self.layout.row(align=True) op = row.operator("bim.assign_class") - op.ifc_class = context.scene.BIMRootProperties.ifc_class - op.predefined_type = context.scene.BIMRootProperties.ifc_predefined_type if ifc_predefined_types else "" - op.userdefined_type = context.scene.BIMRootProperties.ifc_userdefined_type + op.ifc_class = rprops.ifc_class + op.predefined_type = rprops.ifc_predefined_type if ifc_predefined_types else "" + op.userdefined_type = rprops.ifc_userdefined_type def draw_class_dropdowns(self, context, ifc_predefined_types, is_reassigning_class=False): - props = context.scene.BIMRootProperties + props = tool.Root.get_root_props() layout = self.layout prop_with_search(layout, props, "ifc_product") prop_with_search(layout, props, "ifc_class") diff --git a/src/bonsai/bonsai/bim/module/spatial/data.py b/src/bonsai/bonsai/bim/module/spatial/data.py index eb8e6585b4..b3e863a329 100644 --- a/src/bonsai/bonsai/bim/module/spatial/data.py +++ b/src/bonsai/bonsai/bim/module/spatial/data.py @@ -58,7 +58,7 @@ class SpatialData: @classmethod def default_container(cls) -> str | None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() if props.default_container: try: return tool.Ifc.get().by_id(props.default_container).Name @@ -102,7 +102,7 @@ class SpatialDecompositionData: @classmethod def default_container(cls) -> str | None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() if props.default_container: try: return tool.Ifc.get().by_id(props.default_container).Name @@ -112,7 +112,7 @@ class SpatialDecompositionData: @classmethod def subelement_class(cls) -> list[tuple[str, str, str]]: results = [] - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() if not (container := props.active_container): return results container_class = tool.Ifc.get().by_id(container.ifc_definition_id).is_a() diff --git a/src/bonsai/bonsai/bim/module/spatial/decorator.py b/src/bonsai/bonsai/bim/module/spatial/decorator.py index b977210805..a7c1059fc7 100644 --- a/src/bonsai/bonsai/bim/module/spatial/decorator.py +++ b/src/bonsai/bonsai/bim/module/spatial/decorator.py @@ -66,7 +66,8 @@ class GridDecorator: blf.size(font_id, 12) blf.enable(font_id, blf.SHADOW) - for axis in context.scene.BIMGridProperties.grid_axes: + grid_props = tool.Spatial.get_grid_props() + for axis in grid_props.grid_axes: if not (obj := axis.obj) or obj.hide_get() == True: continue if obj.select_get() and context.mode != "OBJECT": @@ -120,7 +121,8 @@ class GridDecorator: selected_edges = [] unselected_verts = [] unselected_edges = [] - for axis in context.scene.BIMGridProperties.grid_axes: + grid_props = tool.Spatial.get_grid_props() + for axis in grid_props.grid_axes: if (obj := axis.obj) and obj.hide_get() == False: if obj.select_get(): if context.mode != "OBJECT": diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 9caa736cbe..9d134729f3 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -96,7 +96,7 @@ class AssignContainer(bpy.types.Operator, tool.Ifc.Operator): container = tool.Ifc.get().by_id(self.container) elif ( (obj := tool.Blender.get_active_object()) - and (props := obj.BIMObjectSpatialProperties) + and (props := tool.Spatial.get_object_spatial_props(obj)) and (container_obj := props.container_obj) and (container := tool.Ifc.get_entity(container_obj)) ): diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 1017d8f069..910bb2e28c 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -36,15 +36,18 @@ import bonsai.core.geometry import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.unit +from typing import TYPE_CHECKING, Union, Literal -def get_subelement_class(self, context): +def get_subelement_class( + self: "BIMSpatialDecompositionProperties", context: bpy.types.Context +) -> list[tuple[str, str, str]]: if not SpatialDecompositionData.is_loaded: SpatialDecompositionData.load() return SpatialDecompositionData.data["subelement_class"] -def update_elevation(self, context): +def update_elevation(self: "BIMContainer", context: bpy.types.Context) -> None: try: elevation = float(self.elevation) if self.elevation != str(elevation): @@ -91,7 +94,7 @@ def update_element_mode(self: "BIMSpatialDecompositionProperties", context: bpy. tool.Spatial.load_contained_elements() -def update_grid_is_locked(self, context): +def update_grid_is_locked(self: "BIMGridProperties", context: bpy.types.Context) -> None: if not tool.Ifc.get(): return if tool.Ifc.get().schema in ("IFC2X3", "IFC4"): @@ -108,7 +111,7 @@ def update_grid_is_locked(self, context): bonsai.bim.handler.refresh_ui_data() -def update_spatial_is_locked(self, context): +def update_spatial_is_locked(self: "BIMSpatialDecompositionProperties", context: bpy.types.Context) -> None: if not tool.Ifc.get(): return if tool.Ifc.get().schema == "IFC2X3": @@ -150,6 +153,10 @@ class BIMObjectSpatialProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing") container_obj: PointerProperty(type=bpy.types.Object, name="Container", poll=poll_container_obj) + if TYPE_CHECKING: + is_editing: bool + container_obj: Union[bpy.types.Object, None] + class BIMContainer(PropertyGroup): name: StringProperty(name="Name", update=update_name) @@ -162,6 +169,16 @@ class BIMContainer(PropertyGroup): is_expanded: BoolProperty(name="Is Expanded") ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + ifc_class: str + description: str + long_name: str + elevation: str + level_index: int + has_children: bool + is_expanded: bool + ifc_definition_id: int + class Element(PropertyGroup): name: StringProperty(name="Name") @@ -185,6 +202,16 @@ class Element(PropertyGroup): ), ) + if TYPE_CHECKING: + ifc_class: str + identification: str + ifc_definition_id: int + level: int + has_children: bool + total: int + is_expanded: bool + type: Literal["CLASS", "TYPE", "CLASSIFICATION", "OCCURRENCE"] + class BIMSpatialDecompositionProperties(PropertyGroup): is_locked: BoolProperty( @@ -223,6 +250,23 @@ class BIMSpatialDecompositionProperties(PropertyGroup): name="Should Include Children", default=True, update=update_should_include_children ) + if TYPE_CHECKING: + is_locked: bool + is_visible: bool + container_filter: str + containers: bpy.types.bpy_prop_collection_idprop[BIMContainer] + contracted_containers: str + active_container_index: int + element_filter: str + elements: bpy.types.bpy_prop_collection_idprop[Element] + expanded_elements: str + active_element_index: int + total_elements: int + element_mode: Literal["TYPE", "DECOMPOSITION", "CLASSIFICATION"] + subelement_class: str + default_container: int + should_include_children: bool + @property def active_container(self): if self.containers and self.active_container_index < len(self.containers): @@ -248,3 +292,8 @@ class BIMGridProperties(PropertyGroup): update=update_grid_is_visible, ) grid_axes: CollectionProperty(name="Grid Axes", type=ObjProperty) + + if TYPE_CHECKING: + is_locked: bool + is_visible: bool + grid_axes: bpy.types.bpy_prop_collection_idprop[ObjProperty] diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py index 0569504e25..da20a63d7c 100644 --- a/src/bonsai/bonsai/bim/module/spatial/ui.py +++ b/src/bonsai/bonsai/bim/module/spatial/ui.py @@ -16,10 +16,15 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy from bpy.types import Panel, UIList from bonsai.bim.module.spatial.data import SpatialData, SpatialDecompositionData import bonsai.tool as tool +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.spatial.prop import BIMSpatialDecompositionProperties, BIMContainer, Element class BIM_PT_spatial(Panel): @@ -40,7 +45,9 @@ class BIM_PT_spatial(Panel): if not SpatialData.is_loaded: SpatialData.load() - osprops = context.active_object.BIMObjectSpatialProperties + obj = context.active_object + assert obj + osprops = tool.Spatial.get_object_spatial_props(obj) if osprops.is_editing: if SpatialData.data["default_container"]: @@ -103,7 +110,7 @@ class BIM_PT_spatial_decomposition(Panel): return tool.Ifc.get() def draw_header(self, context): - props = context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() row = self.layout.row(align=True) row.label(text="") # empty text occupies the left of the row icon = "HIDE_OFF" if props.is_visible else "HIDE_ON" @@ -114,7 +121,7 @@ class BIM_PT_spatial_decomposition(Panel): def draw(self, context): if not SpatialDecompositionData.is_loaded: SpatialDecompositionData.load() - self.props = context.scene.BIMSpatialDecompositionProperties + self.props = tool.Spatial.get_spatial_props() if SpatialDecompositionData.data["default_container"]: row = self.layout.row(align=True) @@ -233,7 +240,7 @@ class BIM_PT_grids(Panel): self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids") def draw_header(self, context): - props = context.scene.BIMGridProperties + props = tool.Spatial.get_grid_props() row = self.layout.row(align=True) row.label(text="") # empty text occupies the left of the row icon = "HIDE_OFF" if props.is_visible else "HIDE_ON" @@ -243,25 +250,36 @@ class BIM_PT_grids(Panel): class BIM_UL_containers_manager(UIList): + icon_by_class = { + "IfcProject": "FILE", + "IfcSite": "WORLD", + "IfcBuilding": "HOME", + "IfcBuildingStorey": "LINENUMBERS_OFF", + "IfcSpace": "ANTIALIASED", + "IfcFacilityPart": "MOD_FLUID", + "IfcBridgePart": "MOD_FLUID", + "IfcFacilityPartCommon": "MOD_FLUID", + "IfcMarinePart": "MOD_FLUID", + "IfcRailwayPart": "MOD_FLUID", + "IfcRoadPart": "MOD_FLUID", + } + def __init__(self): self.use_filter_show = True - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMSpatialDecompositionProperties, + item: BIMContainer, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) - icon = { - "IfcProject": "FILE", - "IfcSite": "WORLD", - "IfcBuilding": "HOME", - "IfcBuildingStorey": "LINENUMBERS_OFF", - "IfcSpace": "ANTIALIASED", - "IfcFacilityPart": "MOD_FLUID", - "IfcBridgePart": "MOD_FLUID", - "IfcFacilityPartCommon": "MOD_FLUID", - "IfcMarinePart": "MOD_FLUID", - "IfcRailwayPart": "MOD_FLUID", - "IfcRoadPart": "MOD_FLUID", - }.get(item.ifc_class, "META_PLANE") + icon = self.icon_by_class.get(item.ifc_class, "META_PLANE") split = row.split(factor=0.85) if item.long_name: split2 = split.split(factor=0.7) @@ -275,7 +293,7 @@ class BIM_UL_containers_manager(UIList): row.prop(item, "name", emboss=False, text="", icon=icon) split.prop(item, "elevation", emboss=False, text="") - def draw_hierarchy(self, row, item): + def draw_hierarchy(self, row: bpy.types.UILayout, item: BIMContainer) -> None: if item.level_index: for i in range(0, item.level_index - 1): row.label(text="", icon="BLANK1") @@ -293,11 +311,12 @@ class BIM_UL_containers_manager(UIList): def draw_filter(self, context, layout): row = layout.row() - row.prop(context.scene.BIMSpatialDecompositionProperties, "container_filter", text="", icon="VIEWZOOM") + props = tool.Spatial.get_spatial_props() + row.prop(props, "container_filter", text="", icon="VIEWZOOM") - def filter_items(self, context, data, propname): + def filter_items(self, context: bpy.types.Context, data: BIMSpatialDecompositionProperties, propname: str): items = getattr(data, propname) - filter_name = context.scene.BIMSpatialDecompositionProperties.container_filter.lower() + filter_name = data.container_filter.lower() filter_flags = [self.bitflag_filter_item] * len(items) for idx, item in enumerate(items): @@ -313,7 +332,7 @@ class BIM_UL_containers_manager(UIList): return filter_flags, [] items = getattr(data, propname) - filter_name = context.scene.BIMSpatialDecompositionProperties.container_filter + filter_name = data.container_filter filtered = bpy.types.UI_UL_list.filter_items_by_name(filter_name, self.bitflag_filter_item, items, "name") return filtered, [] @@ -326,7 +345,18 @@ class BIM_UL_elements(UIList): icon_id = "DISCLOSURE_TRI_DOWN" if is_expanded else "DISCLOSURE_TRI_RIGHT" row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).element_index = index - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, fit_flag): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMSpatialDecompositionProperties, + item: Element, + icon, + active_data, + active_propname, + index, + fit_flag, + ): if item: row = layout.row(align=True) for _ in range(item.level): @@ -341,11 +371,12 @@ class BIM_UL_elements(UIList): def draw_filter(self, context, layout): row = layout.row() - row.prop(context.scene.BIMSpatialDecompositionProperties, "element_filter", text="", icon="VIEWZOOM") + props = tool.Spatial.get_spatial_props() + row.prop(props, "element_filter", text="", icon="VIEWZOOM") - def filter_items(self, context, data, propname): + def filter_items(self, context: bpy.types.Context, data: BIMSpatialDecompositionProperties, propname: str): items = getattr(data, propname) - filter_name = context.scene.BIMSpatialDecompositionProperties.element_filter + filter_name = data.element_filter filtered = bpy.types.UI_UL_list.filter_items_by_name(filter_name, self.bitflag_filter_item, items, "name") return filtered, [] diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 93c6ee7420..446b51dd7b 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -147,11 +147,11 @@ class Geometry(bonsai.core.tool.Geometry): def is_locked(cls, element: ifcopenshell.entity_instance) -> bool: if element.is_a("IfcProject"): return True - elif tool.Root.is_spatial_element(element) and bpy.context.scene.BIMSpatialDecompositionProperties.is_locked: + elif tool.Root.is_spatial_element(element) and tool.Spatial.get_spatial_props().is_locked: return True elif ( element.is_a("IfcPositioningElement") or element.is_a("IfcGrid") or element.is_a("IfcGridAxis") - ) and bpy.context.scene.BIMGridProperties.is_locked: + ) and tool.Spatial.get_grid_props().is_locked: return True return False diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 3f4d77f0d7..4741e16a59 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -137,13 +137,15 @@ class Project(bonsai.core.tool.Project): @classmethod def set_context(cls, context): bonsai.bim.handler.refresh_ui_data() - bpy.context.scene.BIMRootProperties.contexts = str(context.id()) + rprops = tool.Root.get_root_props() + rprops.contexts = str(context.id()) @classmethod def set_default_context(cls): context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") if context: - bpy.context.scene.BIMRootProperties.contexts = str(context.id()) + rprops = tool.Root.get_root_props() + rprops.contexts = str(context.id()) @classmethod def set_default_modeling_dimensions(cls): diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index c665e1226c..392d53e2fe 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import ifcopenshell import ifcopenshell.api @@ -27,12 +28,19 @@ import bonsai.core.tool import bonsai.core.aggregate import bonsai.core.geometry import bonsai.tool as tool -from typing import Union, Optional, Any, Literal +from typing import Union, Optional, Any, Literal, TYPE_CHECKING from bonsai.bim.module.spatial.decorator import GridDecorator from bonsai.bim.module.geometry.decorator import ItemDecorator +if TYPE_CHECKING: + from bonsai.bim.module.root.prop import BIMRootProperties + class Root(bonsai.core.tool.Root): + @classmethod + def get_root_props(cls) -> BIMRootProperties: + return bpy.context.scene.BIMRootProperties + @classmethod def add_tracked_opening(cls, obj: bpy.types.Object, opening_type: Literal["OPENING", "BOOLEAN"]) -> None: """Add tracked opening or boolean object.""" @@ -112,7 +120,7 @@ class Root(bonsai.core.tool.Root): @classmethod def get_default_container(cls) -> Optional[ifcopenshell.entity_instance]: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = tool.Spatial.get_spatial_props() if container := props.default_container: try: return tool.Ifc.get().by_id(container) @@ -233,7 +241,7 @@ class Root(bonsai.core.tool.Root): @classmethod def reload_grid_decorator(cls) -> None: - axes = bpy.context.scene.BIMGridProperties.grid_axes + axes = tool.Spatial.get_grid_props().grid_axes axes.clear() for axis in tool.Ifc.get().by_type("IfcGridAxis"): if obj := tool.Ifc.get_object(axis): diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 26ae9db532..2d400229c5 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -41,12 +41,31 @@ import json from math import pi from mathutils import Vector, Matrix from shapely import Polygon -from typing import Generator, Optional, Union, Literal, List, Any, Iterable +from typing import Generator, Optional, Union, Literal, List, Any, Iterable, TYPE_CHECKING from collections import defaultdict from natsort import natsorted +if TYPE_CHECKING: + from bonsai.bim.module.spatial.prop import ( + BIMGridProperties, + BIMSpatialDecompositionProperties, + BIMObjectSpatialProperties, + ) + class Spatial(bonsai.core.tool.Spatial): + @classmethod + def get_spatial_props(cls) -> BIMSpatialDecompositionProperties: + return bpy.context.scene.BIMSpatialDecompositionProperties + + @classmethod + def get_object_spatial_props(cls, obj: bpy.types.Object) -> BIMObjectSpatialProperties: + return obj.BIMObjectSpatialProperties + + @classmethod + def get_grid_props(cls) -> BIMGridProperties: + return bpy.context.scene.BIMGridProperties + @classmethod def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool: if not (element := tool.Ifc.get_entity(element_obj)): @@ -81,7 +100,8 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def disable_editing(cls, obj: bpy.types.Object) -> None: - obj.BIMObjectSpatialProperties.is_editing = False + props = cls.get_object_spatial_props(obj) + props.is_editing = False @classmethod def duplicate_object_and_data(cls, obj: bpy.types.Object) -> bpy.types.Object: @@ -92,8 +112,9 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def enable_editing(cls, obj: bpy.types.Object) -> None: - obj.BIMObjectSpatialProperties.is_editing = True - obj.BIMObjectSpatialProperties.relating_container_object = None + props = cls.get_object_spatial_props(obj) + props.is_editing = True + props.relating_container_object = None @classmethod def get_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: @@ -207,7 +228,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def get_container_elements_grouped_by_classification(cls, container: ifcopenshell.entity_instance) -> dict: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() results = {} if props.should_include_children: elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True) @@ -254,7 +275,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def get_container_elements_grouped_by_type(cls, container: ifcopenshell.entity_instance) -> dict: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() results: defaultdict[str, dict[int, Any]] = defaultdict(dict) if props.should_include_children: elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True) @@ -280,7 +301,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def load_contained_elements(cls) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() props.elements.clear() if not (container := props.active_container): return @@ -295,7 +316,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def load_contained_elements_by_type(cls, container: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() results = cls.get_container_elements_grouped_by_type(container) expanded_elements = json.loads(props.expanded_elements) @@ -350,7 +371,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def load_contained_elements_by_decomposition(cls, container: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() expanded_elements = json.loads(props.expanded_elements) expanded_ifc_ids = expanded_elements.get("IFC_ID", []) @@ -381,7 +402,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def load_contained_elements_by_classification(cls, container: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() expanded_elements = json.loads(props.expanded_elements) expanded_classifications = expanded_elements.get("CLASSIFICATION", []) expanded_classifications_r = expanded_elements.get("CLASSIFICATION_R", []) @@ -445,7 +466,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def import_spatial_decomposition(cls) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() previous_container_index = props.active_container_index props.containers.clear() cls.contracted_containers = json.loads(props.contracted_containers) @@ -456,7 +477,7 @@ class Spatial(bonsai.core.tool.Spatial): def import_spatial_element(cls, element: ifcopenshell.entity_instance, level_index: int) -> None: if not element.is_a("IfcProject") and not tool.Root.is_spatial_element(element): return - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() new = props.containers.add() new.ifc_class = element.is_a() new["name"] = element.Name or "Unnamed" @@ -485,28 +506,28 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def get_active_container(cls) -> Union[ifcopenshell.entity_instance, None]: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() if props.active_container_index < len(props.containers): container = tool.Ifc.get().by_id(props.containers[props.active_container_index].ifc_definition_id) return container @classmethod def contract_container(cls, container: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() contracted_containers = json.loads(props.contracted_containers) contracted_containers.append(container.id()) props.contracted_containers = json.dumps(contracted_containers) @classmethod def expand_container(cls, container: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() contracted_containers = json.loads(props.contracted_containers) contracted_containers.remove(container.id()) props.contracted_containers = json.dumps(contracted_containers) @classmethod def toggle_container_element(cls, element_index: int, is_recursive: bool) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() if props.element_mode == "TYPE": cls.toggle_container_element_by_type(element_index, is_recursive) elif props.element_mode == "DECOMPOSITION": @@ -516,7 +537,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def toggle_container_element_by_type(cls, element_index: int, is_recursive: bool) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements) element = props.elements[element_index] @@ -565,7 +586,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def toggle_container_element_by_decomposition(cls, element_index: int, is_recursive: bool) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() element = props.elements[element_index] expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements) expanded_elements_list: list[Union[str, int]] = expanded_elements.setdefault("IFC_ID", []) @@ -598,7 +619,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def toggle_container_element_by_classification(cls, element_index: int, is_recursive: bool) -> None: - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() expanded_elements: dict[str, list[str]] = json.loads(props.expanded_elements) expanded_elements_list: list[str] = expanded_elements.setdefault("CLASSIFICATION", []) @@ -679,7 +700,7 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod def get_boundary_lines_from_context_visible_objects(cls) -> list[shapely.LineString]: - props = props = tool.Model.get_model_props() + props = tool.Model.get_model_props() calculation_rl = props.rl3 container = tool.Root.get_default_container() container_obj = tool.Ifc.get_object(container) @@ -1154,8 +1175,8 @@ class Spatial(bonsai.core.tool.Spatial): def set_default_container(cls, container: ifcopenshell.entity_instance) -> None: from bonsai.bim.module.spatial.data import SpatialDecompositionData - assert bpy.context - bpy.context.scene.BIMSpatialDecompositionProperties.default_container = container.id() + props = cls.get_spatial_props() + props.default_container = container.id() SpatialDecompositionData.data["default_container"] = SpatialDecompositionData.default_container() project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) @@ -1212,16 +1233,16 @@ class Spatial(bonsai.core.tool.Spatial): def set_target_container_as_default(cls) -> None: if ( (container := tool.Root.get_default_container()) - and (obj := tool.Ifc.get_object(container)) - and bpy.context.active_object + and (container_obj := tool.Ifc.get_object(container)) + and (obj := bpy.context.active_object) ): - props = bpy.context.active_object.BIMObjectSpatialProperties - props.container_obj = obj + props = cls.get_object_spatial_props(obj) + props.container_obj = container_obj @classmethod def get_filtered_elements(cls, should_filter: bool = True) -> Iterable[ifcopenshell.entity_instance]: ifc_file = tool.Ifc.get() - props = bpy.context.scene.BIMSpatialDecompositionProperties + props = cls.get_spatial_props() container = ifc_file.by_id(props.active_container.ifc_definition_id) element_filter = props.element_filter active_element = props.active_element diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index a77d90ab92..f01399e88c 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -383,7 +383,8 @@ class TestUsingArrays(NewFile): bpy.ops.mesh.primitive_cube_add() obj = bpy.context.active_object - bpy.context.scene.BIMRootProperties.ifc_product = "IfcElement" + rprops = tool.Root.get_root_props() + rprops.ifc_product = "IfcElement" bpy.ops.bim.assign_class(ifc_class="IfcActuator", predefined_type="ELECTRICACTUATOR", userdefined_type="") bpy.ops.bim.add_array() @@ -520,7 +521,7 @@ class TestApplyIfcMaterialChanges(NewFile): bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) with_opening = bpy.context.active_object with_opening.name = "With Opening" - props = bpy.context.scene.BIMRootProperties + props = tool.Root.get_root_props() props.representation_obj = with_opening bpy.ops.bim.add_element(ifc_product="IfcFeatureElement", ifc_class="IfcOpeningElement") diff --git a/src/bonsai/test/tool/test_project.py b/src/bonsai/test/tool/test_project.py index a5f94dbe4b..a87b00f753 100644 --- a/src/bonsai/test/tool/test_project.py +++ b/src/bonsai/test/tool/test_project.py @@ -97,7 +97,8 @@ class TestSetContext(NewFile): tool.Ifc.set(ifc) context = ifc.createIfcGeometricRepresentationContext() subject.set_context(context) - assert bpy.context.scene.BIMRootProperties.contexts == str(context.id()) + rprops = tool.Root.get_root_props() + assert rprops.contexts == str(context.id()) class TestSetDefaultContext(NewFile): @@ -115,7 +116,8 @@ class TestSetDefaultContext(NewFile): target_view="MODEL_VIEW", ) subject.set_default_context() - assert bpy.context.scene.BIMRootProperties.contexts == str(body.id()) + rprops = tool.Root.get_root_props() + assert rprops.contexts == str(body.id()) class TestSetDefaultModelingDimensions(NewFile): diff --git a/src/bonsai/test/tool/test_root.py b/src/bonsai/test/tool/test_root.py index e87eb312a7..2c89a4e5fa 100644 --- a/src/bonsai/test/tool/test_root.py +++ b/src/bonsai/test/tool/test_root.py @@ -191,8 +191,10 @@ class TestReassignClass(NewFile): slabs = [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSlab")] assert len(slabs) == 3 tool.Blender.set_objects_selection(context, slabs[0], (slabs[1],)) - context.scene.BIMRootProperties.ifc_product = "IfcElement" - context.scene.BIMRootProperties.ifc_class = "IfcWall" + + props = tool.Root.get_root_props() + props.ifc_product = "IfcElement" + props.ifc_class = "IfcWall" bpy.ops.bim.reassign_class() assert len(ifc_file.by_type("IfcWall")) == 3 diff --git a/src/bonsai/test/tool/test_spatial.py b/src/bonsai/test/tool/test_spatial.py index 2985b458d1..9bcc84c6dc 100644 --- a/src/bonsai/test/tool/test_spatial.py +++ b/src/bonsai/test/tool/test_spatial.py @@ -129,7 +129,8 @@ class TestDisableEditing(NewFile): obj = bpy.data.objects.new("Object", None) subject.enable_editing(obj) subject.disable_editing(obj) - assert obj.BIMObjectSpatialProperties.is_editing is False + props = tool.Spatial.get_object_spatial_props(obj) + assert props.is_editing is False class TestDuplicateObjectAndData(NewFile): @@ -148,7 +149,8 @@ class TestEnableEditing(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) subject.enable_editing(obj) - assert obj.BIMObjectSpatialProperties.is_editing is True + props = tool.Spatial.get_object_spatial_props(obj) + assert props.is_editing is True class TestGetContainer(NewFile): diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py index c9ef89455e..48ac816f18 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py @@ -28,11 +28,8 @@ def remove_cost_item_quantity( removed. :param cost_item: The IfcCostItem that the quantity is assigned to - :type cost_item: ifcopenshell.entity_instance :param physical_quantity: The IfcPhysicalQuantity to remove - :type physical_quantity: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -46,11 +43,9 @@ def remove_cost_item_quantity( ifcopenshell.api.cost.remove_cost_item(model, cost_item=item, physical_quantity=quantity) """ - settings = {"cost_item": cost_item, "physical_quantity": physical_quantity} - - if len(file.get_inverse(settings["physical_quantity"])) == 1: - file.remove(settings["physical_quantity"]) + if len(file.get_inverse(physical_quantity)) == 1: + file.remove(physical_quantity) return - quantities = list(settings["cost_item"].CostQuantities or []) - quantities.remove(settings["physical_quantity"]) - settings["cost_item"].CostQuantities = quantities + quantities = list(cost_item.CostQuantities or []) + quantities.remove(physical_quantity) + cost_item.CostQuantities = quantities diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py index 2c7745d7f9..a99417f4df 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py @@ -28,11 +28,8 @@ def remove_cost_value( :param parent: The IfcCostItem, IfcConstructionResource, or IfcCostValue that the IfcCostValue is assigned to. - :type parent: ifcopenshell.entity_instance :param cost_value: The IfcCostValue that you want to remove - :type parent: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -48,20 +45,18 @@ def remove_cost_value( ifcopenshell.api.cost.remove_cost_value(model, parent=item, cost_value=value) """ - settings = {"parent": parent, "cost_value": cost_value} - - if len(file.get_inverse(settings["cost_value"])) == 1: - file.remove(settings["cost_value"]) + if len(file.get_inverse(cost_value)) == 1: + file.remove(cost_value) # TODO deep purge - elif settings["parent"].is_a("IfcCostItem"): - values = list(settings["parent"].CostValues) - values.remove(settings["cost_value"]) - settings["parent"].CostValues = values if values else None - elif settings["parent"].is_a("IfcConstructionResource"): - values = list(settings["parent"].BaseCosts) - values.remove(settings["cost_value"]) - settings["parent"].BaseCosts = values if values else None - elif settings["parent"].is_a("IfcCostValue"): - components = list(settings["parent"].Components) - components.remove(settings["cost_value"]) - settings["parent"].Components = components if components else None + elif parent.is_a("IfcCostItem"): + values = list(parent.CostValues) + values.remove(cost_value) + parent.CostValues = values if values else None + elif parent.is_a("IfcConstructionResource"): + values = list(parent.BaseCosts) + values.remove(cost_value) + parent.BaseCosts = values if values else None + elif parent.is_a("IfcCostValue"): + components = list(parent.Components) + components.remove(cost_value) + parent.Components = components if components else None diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py index 8239ab9614..c47165643f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py @@ -23,9 +23,7 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance """Removes a grid axis from a grid :param axis: The IfcGridAxis you want to remove. - :type axis: ifcopenshell.entity_instance :return: None - :rtype: None Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py index 24fa93487b..a749ad11c4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py @@ -92,19 +92,17 @@ def assign_profile( """ usecase = Usecase() usecase.file = file - usecase.settings = {"material_profile": material_profile, "profile": profile} - return usecase.execute() + return usecase.execute(material_profile, profile) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] - def execute(self) -> None: + def execute(self, material_profile: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance) -> None: # TODO: handle composite profiles - old_profile = self.settings["material_profile"].Profile - self.settings["material_profile"].Profile = self.settings["profile"] - for profile_set in self.settings["material_profile"].ToMaterialProfileSet: + old_profile = material_profile.Profile + material_profile.Profile = profile + for profile_set in material_profile.ToMaterialProfileSet: for inverse in self.file.get_inverse(profile_set): if not inverse.is_a("IfcMaterialProfileSetUsage"): continue @@ -113,20 +111,20 @@ class Usecase: if not rel.is_a("IfcRelAssociatesMaterial"): continue for element in rel.RelatedObjects: - self.change_profile(element) + self.change_profile(element, profile) else: for rel in inverse.AssociatedTo: for element in rel.RelatedObjects: - self.change_profile(element) + self.change_profile(element, profile) if old_profile and len(self.file.get_inverse(old_profile)) == 0: # TODO: check remove deep self.file.remove(old_profile) - def change_profile(self, element: ifcopenshell.entity_instance) -> None: + def change_profile(self, element: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance) -> None: representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if not representation: return for subelement in self.file.traverse(representation): if subelement.is_a("IfcSweptAreaSolid"): - subelement.SweptArea = self.settings["profile"] + subelement.SweptArea = profile diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py index fbc25adcbe..de1efef20d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py @@ -27,9 +27,7 @@ def remove_organisation(file: ifcopenshell.file, organisation: ifcopenshell.enti removed. :param organisation: The IfcOrganization to remove - :type organisation: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -39,31 +37,29 @@ def remove_organisation(file: ifcopenshell.file, organisation: ifcopenshell.enti identification="AWB", name="Architects Without Ballpens") ifcopenshell.api.owner.remove_organisation(model, organisation=organisation) """ - settings = {"organisation": organisation} - - for role in settings["organisation"].Roles or []: + for role in organisation.Roles or []: if len(file.get_inverse(role)) == 1: ifcopenshell.api.owner.remove_role(file, role=role) - for address in settings["organisation"].Addresses or []: + for address in organisation.Addresses or []: if len(file.get_inverse(address)) == 1: ifcopenshell.api.owner.remove_address(file, address=address) - for inverse in file.get_inverse(settings["organisation"]): + for inverse in file.get_inverse(organisation): if inverse.is_a("IfcOrganizationRelationship"): - if inverse.RelatingOrganization == settings["organisation"]: + if inverse.RelatingOrganization == organisation: file.remove(inverse) - elif inverse.RelatedOrganizations == (settings["organisation"],): + elif inverse.RelatedOrganizations == (organisation,): file.remove(inverse) elif inverse.is_a("IfcDocumentInformation"): - if inverse.Editors == (settings["organisation"],): + if inverse.Editors == (organisation,): inverse.Editors = None elif inverse.is_a("IfcPersonAndOrganization"): ifcopenshell.api.owner.remove_person_and_organisation(file, person_and_organisation=inverse) elif inverse.is_a("IfcActor"): ifcopenshell.api.root.remove_product(file, product=inverse) elif inverse.is_a("IfcResourceLevelRelationship") and not inverse.is_a("IfcOrganizationRelationship"): - if inverse.RelatedResourceObjects == (settings["organisation"],): + if inverse.RelatedResourceObjects == (organisation,): file.remove(inverse) elif inverse.is_a("IfcApplication"): ifcopenshell.api.owner.remove_application(file, application=inverse) - file.remove(settings["organisation"]) + file.remove(organisation) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py index a4f4d64f1f..07508a977e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py @@ -29,9 +29,7 @@ def remove_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance) the only responsile person for them. :param person: The IfcPerson to remove - :type person: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -41,31 +39,30 @@ def remove_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance) identification="bobthebuilder", family_name="Thebuilder", given_name="Bob") ifcopenshell.api.owner.remove_person(model, person=person) """ - settings = {"person": person} - for role in settings["person"].Roles or []: + for role in person.Roles or []: if len(file.get_inverse(role)) == 1: ifcopenshell.api.owner.remove_role(file, role=role) - for address in settings["person"].Addresses or []: + for address in person.Addresses or []: if len(file.get_inverse(address)) == 1: ifcopenshell.api.owner.remove_address(file, address=address) - for inverse in file.get_inverse(settings["person"]): + for inverse in file.get_inverse(person): if inverse.is_a("IfcWorkControl"): - if inverse.Creators == (settings["person"],): + if inverse.Creators == (person,): inverse.Creators = None elif inverse.is_a("IfcInventory"): - if inverse.ResponsiblePersons == (settings["person"],): + if inverse.ResponsiblePersons == (person,): # in IFC2X3 ResponsiblePersons is not optional and without it IfcInventory is not valid if file.schema == "IFC2X3": ifcopenshell.api.root.remove_product(file, product=inverse) elif inverse.is_a("IfcDocumentInformation"): - if inverse.Editors == (settings["person"],): + if inverse.Editors == (person,): inverse.Editors = None elif inverse.is_a("IfcPersonAndOrganization"): ifcopenshell.api.owner.remove_person_and_organisation(file, person_and_organisation=inverse) elif inverse.is_a("IfcActor"): ifcopenshell.api.root.remove_product(file, product=inverse) elif inverse.is_a("IfcResourceLevelRelationship"): - if inverse.RelatedResourceObjects == (settings["person"],): + if inverse.RelatedResourceObjects == (person,): file.remove(inverse) - file.remove(settings["person"]) + file.remove(person) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py index a5d813756e..905602db16 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py @@ -80,8 +80,8 @@ def assign_lag_time( # for whatever reason. ifcopenshell.api.sequence.assign_lag_time(model, rel_sequence=sequence, lag_value="P1D") """ - lag_value = file.create_entity("IfcDuration", ifcopenshell.util.date.datetime2ifc(lag_value, "IfcDuration")) - lag_time = file.create_entity("IfcLagTime", DurationType=duration_type, LagValue=lag_value) + duration = file.create_entity("IfcDuration", ifcopenshell.util.date.datetime2ifc(lag_value, "IfcDuration")) + lag_time = file.create_entity("IfcLagTime", DurationType=duration_type, LagValue=duration) if rel_sequence.is_a("IfcRelSequence"): if rel_sequence.TimeLag and len(file.get_inverse(rel_sequence.TimeLag)) == 1: file.remove(rel_sequence.TimeLag) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py index 08ffcd9f40..fb44184c5f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py @@ -25,9 +25,7 @@ def unassign_lag_time(file: ifcopenshell.file, rel_sequence: ifcopenshell.entity The schedule is cascaded afterwards. :param rel_sequence: The sequence to remove the lag time from. - :type rel_sequence: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -57,12 +55,8 @@ def unassign_lag_time(file: ifcopenshell.file, rel_sequence: ifcopenshell.entity # What if you didn't? ifcopenshell.api.sequence.unassign_lag_time(model, rel_sequence=sequence) """ - settings = { - "rel_sequence": rel_sequence, - } - - if len(file.get_inverse(settings["rel_sequence"].TimeLag)) == 1: - file.remove(settings["rel_sequence"].TimeLag) + if len(file.get_inverse(rel_sequence.TimeLag)) == 1: + file.remove(rel_sequence.TimeLag) else: - settings["rel_sequence"].TimeLag = None - ifcopenshell.api.sequence.cascade_schedule(file, task=settings["rel_sequence"].RelatedProcess) + rel_sequence.TimeLag = None + ifcopenshell.api.sequence.cascade_schedule(file, task=rel_sequence.RelatedProcess) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py index 8135bc0255..144b96f5a9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_analysis_model.py @@ -29,18 +29,14 @@ def remove_structural_analysis_model( :param structural_analysis_model: The IfcStructuralAnalysisModel to remove. - :type structural_analysis_model: ifcopenshell.entity_instance :return: None - :rtype: None """ - settings = {"structural_analysis_model": structural_analysis_model} - - for rel in settings["structural_analysis_model"].IsGroupedBy or []: + for rel in structural_analysis_model.IsGroupedBy or []: history = rel.OwnerHistory file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["structural_analysis_model"].OwnerHistory - file.remove(settings["structural_analysis_model"]) + history = structural_analysis_model.OwnerHistory + file.remove(structural_analysis_model) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py index 63c6331e51..d31811a739 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_boundary_condition.py @@ -28,23 +28,21 @@ def remove_structural_boundary_condition( :param connection: The IfcStructuralConnection to remove the condition from. If omitted, it is assumed to be an orphaned condition. - :type connection: ifcopenshell.entity_instance,optional :param boundary_condition: The IfcBoundaryCondition to remove. - :type boundary_condition: ifcopenshell.entity_instance, optional. :return: None - :rtype: None """ - settings = {"connection": connection, "boundary_condition": boundary_condition} - if settings["connection"]: + if connection: # remove boundary condition from a connection - if not settings["connection"].AppliedCondition: + if not connection.AppliedCondition: return - if len(file.get_inverse(settings["connection"].AppliedCondition)) == 1: - file.remove(settings["connection"].AppliedCondition) - settings["connection"].AppliedCondition = None + applied_condition = connection.AppliedCondition + if file.get_total_inverses(applied_condition) == 1: + file.remove(applied_condition) + connection.AppliedCondition = None else: + assert boundary_condition, "Either connection or boundary_condition must be provided." # remove the boundary condition - for conn in file.get_inverse(settings["boundary_condition"]): + for conn in file.get_inverse(boundary_condition): conn.AppliedCondition = None - file.remove(settings["boundary_condition"]) + file.remove(boundary_condition) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py index 1d406a16e8..0c1a6ad3a1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load.py @@ -22,10 +22,6 @@ def remove_structural_load(file: ifcopenshell.file, structural_load: ifcopenshel """Removes a structural load :param structural_load: The IfcStructuralLoad to remove. - :type structural_load: ifcopenshell.entity_instance :return: None - :rtype: None """ - settings = {"structural_load": structural_load} - - file.remove(settings["structural_load"]) + file.remove(structural_load) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py index 1d9473515a..401db40dbc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_case.py @@ -25,18 +25,14 @@ def remove_structural_load_case(file: ifcopenshell.file, load_case: ifcopenshell """Removes a structural load case :param load_case: The IfcStructuralLoadCase to remove. - :type load_case: ifcopenshell.entity_instance :return: None - :rtype: None """ - settings = {"load_case": load_case} - # TODO: do a deep purge - for rel in settings["load_case"].IsGroupedBy or []: + for rel in load_case.IsGroupedBy or []: history = rel.OwnerHistory file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["load_case"].OwnerHistory - file.remove(settings["load_case"]) + history = load_case.OwnerHistory + file.remove(load_case) ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py index 281630fd7a..25acbbe9cc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_load_group.py @@ -25,20 +25,16 @@ def remove_structural_load_group(file: ifcopenshell.file, load_group: ifcopenshe """Removes a structural load group :param load_group: The IfcStructuralLoadGroup to remove. - :type load_group: ifcopenshell.entity_instance :return: None - :rtype: None """ - settings = {"load_group": load_group} - # TODO: do a deep purge - for inverse in file.get_inverse(settings["load_group"]): + for inverse in file.get_inverse(load_group): if inverse.is_a("IfcRelAssignsToGroup") and len(inverse.RelatedObjects) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["load_group"].OwnerHistory - file.remove(settings["load_group"]) + history = load_group.OwnerHistory + file.remove(load_group) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index b19b8154c9..47adbc284e 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -835,6 +835,7 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = " import ifcopenshell.util.element import ifcopenshell.util.geolocation import ifcopenshell.api.georeference + import ifcopenshell.api.unit prefix = get_prefix(target_units) si_unit = get_unit_name(target_units) From 5eb9cb2ee4b199936ed47f61837e46d7e0806402 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 11:34:47 +0500 Subject: [PATCH 128/476] len(get_inverse) -> get_total_inverses --- src/bonsai/bonsai/bim/module/structural/operator.py | 2 +- .../ifcopenshell/api/cost/edit_cost_value.py | 2 +- .../ifcopenshell/api/cost/remove_cost_item_quantity.py | 2 +- .../ifcopenshell/api/cost/remove_cost_value.py | 2 +- .../ifcopenshell/api/georeference/remove_georeferencing.py | 2 +- .../ifcopenshell/api/grid/remove_grid_axis.py | 2 +- .../ifcopenshell/api/material/assign_profile.py | 2 +- .../ifcopenshell/api/owner/remove_organisation.py | 4 ++-- .../ifcopenshell/api/owner/remove_person.py | 4 ++-- .../ifcopenshell/api/sequence/assign_lag_time.py | 4 ++-- .../ifcopenshell/api/sequence/assign_recurrence_pattern.py | 6 +++--- .../ifcopenshell/api/sequence/unassign_lag_time.py | 4 ++-- .../api/structural/edit_structural_connection_cs.py | 6 +++--- .../api/structural/edit_structural_item_axis.py | 2 +- .../ifcopenshell/api/style/assign_material_style.py | 2 +- .../ifcopenshell/api/unit/edit_named_unit.py | 2 +- .../test/api/geometry/test_add_boolean.py | 4 ++-- 17 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index 47d74515f8..ba08227179 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -750,7 +750,7 @@ class LoadStructuralLoads(bpy.types.Operator): for structural_load in loads: if ( names.count(structural_load.Name or "Unnamed") > 1 - and len(self.file.get_inverse(structural_load)) < 2 + and self.file.get_total_inverses(structural_load) < 2 ): continue new = props.structural_loads.add() diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py index 5812cbcf75..ebc09ed34c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/edit_cost_value.py @@ -58,6 +58,6 @@ def edit_cost_value( value["ValueComponent"], ) value = file.create_entity("IfcMeasureWithUnit", value_component, value["UnitComponent"]) - if old_unit_basis and len(file.get_inverse(old_unit_basis)) == 0: + if old_unit_basis and file.get_total_inverses(old_unit_basis) == 0: ifcopenshell.util.element.remove_deep(file, old_unit_basis) setattr(cost_value, name, value) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py index 48ac816f18..80f330ce0b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item_quantity.py @@ -43,7 +43,7 @@ def remove_cost_item_quantity( ifcopenshell.api.cost.remove_cost_item(model, cost_item=item, physical_quantity=quantity) """ - if len(file.get_inverse(physical_quantity)) == 1: + if file.get_total_inverses(physical_quantity) == 1: file.remove(physical_quantity) return quantities = list(cost_item.CostQuantities or []) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py index a99417f4df..b578cd7134 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_value.py @@ -45,7 +45,7 @@ def remove_cost_value( ifcopenshell.api.cost.remove_cost_value(model, parent=item, cost_value=value) """ - if len(file.get_inverse(cost_value)) == 1: + if file.get_total_inverses(cost_value) == 1: file.remove(cost_value) # TODO deep purge elif parent.is_a("IfcCostItem"): diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py index 63d46b840b..3cbcae5d60 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/remove_georeferencing.py @@ -43,7 +43,7 @@ def remove_georeferencing(file: ifcopenshell.file) -> None: ifcopenshell.api.pset.remove_pset(file, project, file.by_id(pset["id"])) return for projected_crs in file.by_type("IfcProjectedCRS"): - if (unit := projected_crs.MapUnit) and len(file.get_inverse(unit)) == 1: + if (unit := projected_crs.MapUnit) and file.get_total_inverses(unit) == 1: projected_crs.MapUnit = None ifcopenshell.util.element.remove_deep2(file, unit) file.remove(projected_crs) diff --git a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py index c47165643f..652fc89f56 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py +++ b/src/ifcopenshell-python/ifcopenshell/api/grid/remove_grid_axis.py @@ -42,7 +42,7 @@ def remove_grid_axis(file: ifcopenshell.file, axis: ifcopenshell.entity_instance ifcopenshell.api.grid.remove_grid_axis(model, axis=axis_2) """ axis_curve = axis.AxisCurve - if len(file.get_inverse(axis_curve)) == 1: + if file.get_total_inverses(axis_curve) == 1: ifcopenshell.util.element.remove_deep(file, axis_curve) file.remove(axis_curve) file.remove(axis) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py index a749ad11c4..1e7c209e6c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py @@ -117,7 +117,7 @@ class Usecase: for element in rel.RelatedObjects: self.change_profile(element, profile) - if old_profile and len(self.file.get_inverse(old_profile)) == 0: + if old_profile and self.file.get_total_inverses(old_profile) == 0: # TODO: check remove deep self.file.remove(old_profile) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py index de1efef20d..acee11d9f5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_organisation.py @@ -38,10 +38,10 @@ def remove_organisation(file: ifcopenshell.file, organisation: ifcopenshell.enti ifcopenshell.api.owner.remove_organisation(model, organisation=organisation) """ for role in organisation.Roles or []: - if len(file.get_inverse(role)) == 1: + if (file.get_total_inverses(role)) == 1: ifcopenshell.api.owner.remove_role(file, role=role) for address in organisation.Addresses or []: - if len(file.get_inverse(address)) == 1: + if (file.get_total_inverses(address)) == 1: ifcopenshell.api.owner.remove_address(file, address=address) for inverse in file.get_inverse(organisation): if inverse.is_a("IfcOrganizationRelationship"): diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py index 07508a977e..0c618c1e74 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person.py @@ -41,10 +41,10 @@ def remove_person(file: ifcopenshell.file, person: ifcopenshell.entity_instance) """ for role in person.Roles or []: - if len(file.get_inverse(role)) == 1: + if file.get_total_inverses(role) == 1: ifcopenshell.api.owner.remove_role(file, role=role) for address in person.Addresses or []: - if len(file.get_inverse(address)) == 1: + if file.get_total_inverses(address) == 1: ifcopenshell.api.owner.remove_address(file, address=address) for inverse in file.get_inverse(person): if inverse.is_a("IfcWorkControl"): diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py index 905602db16..2c08ab00a3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_lag_time.py @@ -83,7 +83,7 @@ def assign_lag_time( duration = file.create_entity("IfcDuration", ifcopenshell.util.date.datetime2ifc(lag_value, "IfcDuration")) lag_time = file.create_entity("IfcLagTime", DurationType=duration_type, LagValue=duration) if rel_sequence.is_a("IfcRelSequence"): - if rel_sequence.TimeLag and len(file.get_inverse(rel_sequence.TimeLag)) == 1: - file.remove(rel_sequence.TimeLag) + if (current_lag_time := rel_sequence.TimeLag) and file.get_total_inverses(current_lag_time) == 1: + file.remove(current_lag_time) rel_sequence.TimeLag = lag_time return lag_time diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py index 333aefb418..7f74fb300b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_recurrence_pattern.py @@ -108,11 +108,11 @@ def assign_recurrence_pattern( recurrence = file.create_entity("IfcRecurrencePattern", recurrence_type) if parent.is_a("IfcWorkTime"): - if parent.RecurrencePattern and len(file.get_inverse(parent.RecurrencePattern)) == 1: - file.remove(parent.RecurrencePattern) + if (old_recurrence := parent.RecurrencePattern) and file.get_total_inverses(old_recurrence) == 1: + file.remove(old_recurrence) parent.RecurrencePattern = recurrence elif parent.is_a("IfcTaskTimeRecurring"): - if (recurrence_old := parent.Recurrence) and len(file.get_inverse(recurrence_old)) == 1: + if (recurrence_old := parent.Recurrence) and file.get_total_inverses(recurrence_old) == 1: file.remove(recurrence_old) parent.Recurrence = recurrence return recurrence diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py index fb44184c5f..b07a630684 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_lag_time.py @@ -55,8 +55,8 @@ def unassign_lag_time(file: ifcopenshell.file, rel_sequence: ifcopenshell.entity # What if you didn't? ifcopenshell.api.sequence.unassign_lag_time(model, rel_sequence=sequence) """ - if len(file.get_inverse(rel_sequence.TimeLag)) == 1: - file.remove(rel_sequence.TimeLag) + if file.get_total_inverses((current_lag_time := rel_sequence.TimeLag)) == 1: + file.remove(current_lag_time) else: rel_sequence.TimeLag = None ifcopenshell.api.sequence.cascade_schedule(file, task=rel_sequence.RelatedProcess) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py index e9fcc0d660..38befd9c6f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_connection_cs.py @@ -40,9 +40,9 @@ def edit_structural_connection_cs( structural_item.ConditionCoordinateSystem = ccs ccs = structural_item.ConditionCoordinateSystem - if ccs.Axis and len(file.get_inverse(ccs.Axis)) == 1: - file.remove(ccs.Axis) + if (current_axis := ccs.Axis) and file.get_total_inverses(current_axis) == 1: + file.remove(current_axis) ccs.Axis = file.create_entity("IfcDirection", ifc_safe_vector_type(axis)) - if (prev_ref_direction := ccs.RefDirection) and len(file.get_inverse(prev_ref_direction)) == 1: + if (prev_ref_direction := ccs.RefDirection) and file.get_total_inverses(prev_ref_direction) == 1: file.remove(prev_ref_direction) ccs.RefDirection = file.create_entity("IfcDirection", ifc_safe_vector_type(ref_direction)) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py index 1b7105d95d..d3858bec95 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/edit_structural_item_axis.py @@ -31,6 +31,6 @@ def edit_structural_item_axis( Defaults to (0., 0., 1.). :return: None """ - if len(file.get_inverse(axis_dir := structural_item.Axis)) == 1: + if file.get_total_inverses(axis_dir := structural_item.Axis) == 1: file.remove(axis_dir) structural_item.Axis = file.create_entity("IfcDirection", ifc_safe_vector_type(axis)) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py index f870a3006e..fbd706df68 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/assign_material_style.py @@ -174,7 +174,7 @@ class Usecase: new_items.append(self.create_styled_item(item_to_reuse)) representation.Items = new_items for item in same_style_items: - if len(self.file.get_inverse(item)) == 0: + if self.file.get_total_inverses(item) == 0: self.file.remove(item) else: representations = list(definition_representation.Representations) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py index ccd0e96d54..683feb7216 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/edit_named_unit.py @@ -45,7 +45,7 @@ def edit_named_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance, for name, value in attributes.items(): if name == "Dimensions": dimensions = unit.Dimensions - if len(file.get_inverse(dimensions)) > 1: + if file.get_total_inverses(dimensions) > 1: unit.Dimensions = file.createIfcDimensionalExponents(*value) else: for i, exponent in enumerate(value): diff --git a/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py b/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py index ba8385fa3e..07a4400f5a 100644 --- a/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py +++ b/src/ifcopenshell-python/test/api/geometry/test_add_boolean.py @@ -105,7 +105,7 @@ class TestAddBoolean(test.bootstrap.IFC4): assert len(booleans) == 1 assert len(rep.Items) == 2 - assert len(self.file.get_inverse(first1)) == 1 + assert self.file.get_total_inverses(first1) == 1 result = list(self.file.get_inverse(first1))[0] assert result.FirstOperand == first1 assert result.SecondOperand == second1 @@ -114,7 +114,7 @@ class TestAddBoolean(test.bootstrap.IFC4): # Second2 is now used twice. Reusing is OK (albeit confusing), so long as things don't get recursive. assert result2.SecondOperand == second2 - assert len(self.file.get_inverse(first2)) == 1 + assert self.file.get_total_inverses(first2) == 1 result3 = list(self.file.get_inverse(first2))[0] assert result3.FirstOperand == first2 assert result3.SecondOperand == second2 From ce6339869d949f58ed0d3c881df060efd3f96cbd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 16:48:32 +0500 Subject: [PATCH 129/476] Fix some feature tests --- src/bonsai/bonsai/bim/module/pset_template/prop.py | 1 + src/bonsai/bonsai/tool/root.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/pset_template/prop.py b/src/bonsai/bonsai/bim/module/pset_template/prop.py index 122a09128c..fdd6188a76 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/prop.py +++ b/src/bonsai/bonsai/bim/module/pset_template/prop.py @@ -21,6 +21,7 @@ import bpy import ifcopenshell import ifcopenshell.util.attribute from ifcopenshell.util.doc import get_attribute_doc +import bonsai.tool as tool from bonsai.bim.module.pset_template.data import PsetTemplatesData from bonsai.bim.prop import StrProperty, Attribute from bonsai.bim.ifc import IfcStore diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index 392d53e2fe..6e92d8cf5a 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -431,7 +431,7 @@ class Root(bonsai.core.tool.Root): """ tool.Ifc.unlink(obj=obj) if tool.Geometry.has_mesh_properties((data := obj.data)): - tool.Geometry.get_mesh_props(mesh).ifc_definition_id = 0 + tool.Geometry.get_mesh_props(data).ifc_definition_id = 0 for material_slot in obj.material_slots: if material := material_slot.material: tool.Ifc.unlink(obj=material) From 1d815cdb67115232d71e7f3397e368e01961bb42 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 18:22:23 +0500 Subject: [PATCH 130/476] Fix issue reloading AuthoringData from workspace at every draw call If data was already loaded for BIMTool ("all") this ` elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None:` would always fail and this `AuthoringData.data["ifc_element_type"] != ifc_element_type` would always result to True, constantly recalculating data on every draw call. This may uncover some issues that were hidden by the constant update (e.g. the bug fixed in the next commit). Simplified it so now ifc_element_type is almost always referring to either ifc class or None. It's still using "all" in the BIMTool itself but then it's converted to None when passed to draw methods. --- src/bonsai/bonsai/bim/module/model/data.py | 9 +++------ src/bonsai/bonsai/bim/module/model/workspace.py | 17 ++++++----------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py index 797f72714f..5cc0f64699 100644 --- a/src/bonsai/bonsai/bim/module/model/data.py +++ b/src/bonsai/bonsai/bim/module/model/data.py @@ -26,7 +26,7 @@ from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc import bonsai.tool as tool from math import degrees from natsort import natsorted -from typing import Union +from typing import Union, Optional def refresh(): @@ -45,17 +45,14 @@ class AuthoringData: data = {} type_thumbnails = {} types_per_page = 9 - ifc_element_type = None is_loaded = False @classmethod - def load(cls, ifc_element_type=None): + def load(cls, ifc_element_type: Optional[str] = None): cls.is_loaded = True cls.props = tool.Model.get_model_props() - if ifc_element_type: - cls.ifc_element_type = None if ifc_element_type == "all" else ifc_element_type cls.data["default_container"] = cls.default_container() - cls.data["ifc_element_type"] = cls.ifc_element_type + cls.data["ifc_element_type"] = ifc_element_type cls.data["ifc_classes"] = cls.ifc_classes() cls.data["ifc_class_current"] = cls.ifc_class_current() # Make sure .ifc_classes() was run before next lines diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 11f4fe73c5..014ebd013b 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -97,14 +97,15 @@ class BimTool(WorkSpaceTool): cls, context: bpy.types.Context, layout: bpy.types.UILayout, ws_tool: bpy.types.WorkSpaceTool ) -> None: props = tool.Geometry.get_geometry_props() + ifc_element_type = None if cls.ifc_element_type == "all" else cls.ifc_element_type if props.mode == "ITEM": EditItemUI.draw(context, layout) elif ( active_ifc_object := (context.active_object and tool.Ifc.get_entity(context.active_object)) ) and context.selected_objects: - EditObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) + EditObjectUI.draw(context, layout, ifc_element_type=ifc_element_type) else: - CreateObjectUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) + CreateObjectUI.draw(context, layout, ifc_element_type=ifc_element_type) class WallTool(BimTool): @@ -500,9 +501,7 @@ class CreateObjectUI: layout: bpy.types.UILayout @classmethod - def draw( - cls, context: bpy.types.Context, layout: bpy.types.UILayout, ifc_element_type: Optional[str] = None - ) -> None: + def draw(cls, context: bpy.types.Context, layout: bpy.types.UILayout, ifc_element_type: Union[str, None]) -> None: cls.layout = layout cls.props = tool.Model.get_model_props() @@ -516,15 +515,13 @@ class CreateObjectUI: if not AuthoringData.is_loaded: AuthoringData.load(ifc_element_type) - elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None: - AuthoringData.load("all") elif AuthoringData.data["ifc_element_type"] != ifc_element_type: AuthoringData.load(ifc_element_type) - if ifc_element_type and context.region.type == "TOOL_HEADER": + if context.region.type == "TOOL_HEADER": tool_name = ( "Multi Object Tool" - if ifc_element_type == "all" + if ifc_element_type is None else format_ifc_camel_case(ifc_element_type.removesuffix("Type")) + " Tool" ) cls.layout.label(text=tool_name, icon="TOOL_SETTINGS") @@ -747,8 +744,6 @@ class EditObjectUI: if not AuthoringData.is_loaded: AuthoringData.load(ifc_element_type) - elif ifc_element_type == "all" and AuthoringData.data["ifc_element_type"] is not None: - AuthoringData.load("all") elif AuthoringData.data["ifc_element_type"] != ifc_element_type: AuthoringData.load(ifc_element_type) From f6ae711f6c8d867a6f0c215a904ec2ef4b361881 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 18:21:12 +0500 Subject: [PATCH 131/476] Fix feature tests failing due to AuthoringData changes relating_type_id now relies on type_elements which relies on ifc_class_current and they wasn't updated when ifc_class was changed. Typical failing test looked like "TypeError: bpy_struct: item.attr = val: expected a string enum, not int". --- src/bonsai/bonsai/bim/module/model/prop.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 7466a4f70b..4864da1348 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -60,6 +60,8 @@ def get_materials(self, context): def update_ifc_class(self, context): bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class) + AuthoringData.data["ifc_class_current"] = self.ifc_class + AuthoringData.data["type_elements"] = AuthoringData.type_elements() AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id() AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail() if tool.Blender.get_enum_safe(self, "relating_type_id") is None: From 899ada0019334461e47440e1d02221d0fb59932b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 18:50:51 +0500 Subject: [PATCH 132/476] Fix purge unused types test after 3bb4243 --- src/bonsai/test/bim/feature/type.feature | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bonsai/test/bim/feature/type.feature b/src/bonsai/test/bim/feature/type.feature index 6b68fe7566..c32f7a8cfb 100644 --- a/src/bonsai/test/bim/feature/type.feature +++ b/src/bonsai/test/bim/feature/type.feature @@ -202,11 +202,11 @@ Scenario: Select similar type Scenario: Purge unused types Given an empty IFC project And I press "bim.launch_type_manager" - And I set "scene.BIMModelProperties.type_class" to "IfcWallType" - And I set "scene.BIMModelProperties.type_predefined_type" to "SOLIDWALL" - And I set "scene.BIMModelProperties.type_template" to "EMPTY" - When I press "bim.add_type" - Then the object "IfcWallType/TYPEX" is an "IfcWallType" - And the object "IfcWallType/TYPEX" has no data - When I press "bim.purge_unused_types" - Then the object "IfcWallType/TYPEX" does not exist + And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" + And I set "scene.BIMRootProperties.ifc_predefined_type" to "SOLIDWALL" + And I set "scene.BIMRootProperties.representation_template" to "EMPTY" + When I press "bim.add_element" + Then the object "IfcWallType/Unnamed" is an "IfcWallType" + And the object "IfcWallType/Unnamed" has no data + When I press "bim.purge_unused_objects(object_type='TYPE')" + Then the object "IfcWallType/Unnamed" does not exist From 0bd4451ce9ae68ff6a80f2a6f5eef4fa7bdd461f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Feb 2025 19:30:40 +0500 Subject: [PATCH 133/476] Fix issue not cleaning up the mesh after 76277c35d2 --- src/bonsai/bonsai/tool/geometry.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 446b51dd7b..4947666bd6 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -269,8 +269,9 @@ class Geometry(bonsai.core.tool.Geometry): ifcopenshell.api.run("root.remove_product", tool.Ifc.get(), product=element) data = obj.data - if tool.Geometry.has_mesh_properties(data) and tool.Ifc.get_entity_by_id( - tool.Geometry.get_mesh_props(data).ifc_definition_id + if ( + tool.Geometry.has_mesh_properties(data) + and tool.Ifc.get_entity_by_id(tool.Geometry.get_mesh_props(data).ifc_definition_id) is None ): tool.Blender.remove_data_block(data) From 70182d943deb9e9a07a9cf23e781f2a27e22cab3 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Mon, 24 Feb 2025 17:11:01 +0100 Subject: [PATCH 134/476] Update build_osx.yml --- .github/workflows/build_osx.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build_osx.yml b/.github/workflows/build_osx.yml index d70cf0930c..4b968c8516 100644 --- a/.github/workflows/build_osx.yml +++ b/.github/workflows/build_osx.yml @@ -128,10 +128,8 @@ jobs: # # To force the link and overwrite all conflicting files: # brew link --overwrite python@3.13 - brew link --overwrite python@3.12 - brew link --overwrite python@3.13 # https://github.com/rust-lang/rustup/pull/3989/files - brew install --overwrite awscli + brew install --overwrite awscli | true - name: Upload .zip archives to S3 run: | From e46e1512fad705941c2e37d9a157c34181359aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 24 Feb 2025 14:25:24 -0300 Subject: [PATCH 135/476] Fix #6160 where axis snapping in polyline tool were not working consistently --- .../bonsai/bim/module/model/polyline.py | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index d879a5d474..5788bb148f 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -627,25 +627,17 @@ class PolylineOperator: return is_valid def choose_axis(self, event: bpy.types.Event, x: bool = True, y: bool = True, z: bool = False) -> None: - if x: - if not event.shift and event.value == "PRESS" and event.type == "X": - self.tool_state.axis_method = "X" if self.tool_state.axis_method != event.type else None - self.tool_state.lock_axis = False if self.tool_state.lock_axis else True - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() - - if y: - if not event.shift and event.value == "PRESS" and event.type == "Y": - self.tool_state.axis_method = "Y" if self.tool_state.axis_method != event.type else None - self.tool_state.lock_axis = False if self.tool_state.lock_axis else True - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() + options = {"X", "Y"} if z: - if not event.shift and event.value == "PRESS" and event.type == "Z": - self.tool_state.axis_method = "Z" if self.tool_state.axis_method != event.type else None - self.tool_state.lock_axis = False if self.tool_state.lock_axis else True - PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) - tool.Blender.update_viewport() + options = {"X", "Y", "Z"} + if not event.shift and event.value == "PRESS" and event.type in options: + self.tool_state.axis_method = event.type if self.tool_state.axis_method != event.type else None + if self.tool_state.axis_method is not None: + self.tool_state.lock_axis = True + else: + self.tool_state.lock_axis = False + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() def choose_plane(self, event: bpy.types.Event, x: bool = True, y: bool = True, z: bool = True) -> None: if x: From ea274588b03726b0e9a37bc0ee5495df67cce8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 24 Feb 2025 20:28:02 -0300 Subject: [PATCH 136/476] See #6214. Fix how extrusion existing angle is calculated. The issue happen when the layer has a negative direction sense. --- src/bonsai/bonsai/tool/model.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index e55798602f..0596971e53 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1981,7 +1981,18 @@ class Model(bonsai.core.tool.Model): @classmethod def get_existing_x_angle(cls, extrusion: ifcopenshell.entity_instance) -> float: x, y, z = extrusion.ExtrudedDirection.DirectionRatios - x_angle = Vector((0, 1)).angle_signed(Vector((y, z))) + vector = Vector((0, 1)) + x_angle = vector.angle_signed(Vector((y, z))) + + # The extrusion direction is changed by the layer direction change + # So we have to adapt the values of y, z and vector accordingly + if z < 0 and y < 0: + y = abs(y) + z = abs(z) + if z < 0 and y >=0: + vector = Vector((0, -1)) + + x_angle = vector.angle_signed(Vector((y, z))) return x_angle From 6de409031eeda91824c11d59b05c8f9df4f5afb9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 25 Feb 2025 14:16:39 +0500 Subject: [PATCH 137/476] Fix #6222 Mentioned in #6223 --- src/bonsai/bonsai/bim/module/root/data.py | 2 +- src/bonsai/bonsai/bim/module/root/prop.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/root/data.py b/src/bonsai/bonsai/bim/module/root/data.py index 64edaa2a9d..99b17220b5 100644 --- a/src/bonsai/bonsai/bim/module/root/data.py +++ b/src/bonsai/bonsai/bim/module/root/data.py @@ -97,7 +97,7 @@ class IfcClassData: return types_enum @classmethod - def ifc_classes_suggestions(cls): + def ifc_classes_suggestions(cls) -> dict[str, list[dict[str, Union[str, None]]]]: # suggestions : dict[class_name: list[dict[predefined_type, name(optional)]]] suggestions = defaultdict(list) version = tool.Ifc.get_schema() diff --git a/src/bonsai/bonsai/bim/module/root/prop.py b/src/bonsai/bonsai/bim/module/root/prop.py index 4c358dfde1..e2d6cea330 100644 --- a/src/bonsai/bonsai/bim/module/root/prop.py +++ b/src/bonsai/bonsai/bim/module/root/prop.py @@ -99,7 +99,7 @@ def get_ifc_classes(self: "BIMRootProperties", context: bpy.types.Context) -> li return IfcClassData.data["ifc_classes"] -def get_ifc_classes_suggestions(self: "BIMRootProperties", context: bpy.types.Context) -> list[tuple[str, str]]: +def get_ifc_classes_suggestions() -> dict[str, list[dict[str, Union[str, None]]]]: if not IfcClassData.is_loaded: IfcClassData.load() return IfcClassData.data["ifc_classes_suggestions"] From 240c78c12ac161208828209dc1c0ba23e2219d4e Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Tue, 25 Feb 2025 14:18:10 +0100 Subject: [PATCH 138/476] Update IfcHierarchyHelper.h --- src/ifcparse/IfcHierarchyHelper.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcparse/IfcHierarchyHelper.h b/src/ifcparse/IfcHierarchyHelper.h index eff35e7259..4f427a527a 100644 --- a/src/ifcparse/IfcHierarchyHelper.h +++ b/src/ifcparse/IfcHierarchyHelper.h @@ -428,7 +428,7 @@ class IFC_PARSE_API IfcHierarchyHelper : public IfcParse::IfcFile { } typename Schema::IfcObject::list::ptr related_objects(new aggregate_of()); related_objects->push((typename Schema::IfcObject*)related_object); - typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, related_object->template as); + typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, related_object->template as()); addEntity(t); } From dcc681ac74cc4604202b38f198dcfe438fb4a7cf Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 25 Feb 2025 11:16:24 +0500 Subject: [PATCH 139/476] Revert a428169 to avoid constant reload of cached data The code was recalculating type_elements and relating_type_id every time Blender trying to check enum items. Couldn't replicate issue when type duplicated, perhaps it's resolved some other way already. --- src/bonsai/bonsai/bim/module/model/prop.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 4864da1348..c8eca8963f 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -46,9 +46,6 @@ def get_boundary_class(self, context): def get_relating_type_id(self, context): if not AuthoringData.is_loaded: AuthoringData.load() - else: - AuthoringData.data["type_elements"] = AuthoringData.type_elements() - AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id() return AuthoringData.data["relating_type_id"] From b49cc6c5e92dc481b574173062db3bf42924ddea Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 25 Feb 2025 11:18:54 +0500 Subject: [PATCH 140/476] AuthoringData.data["relating_type_data"] Simplify lots of data keys to just one dictionary relating_type_data, so it will be easier to maintain it and update if needed. --- .../bonsai/bim/module/covering/workspace.py | 8 +- src/bonsai/bonsai/bim/module/model/data.py | 83 +++++++------------ src/bonsai/bonsai/bim/module/model/product.py | 4 +- src/bonsai/bonsai/bim/module/model/prop.py | 6 +- src/bonsai/bonsai/bim/module/model/ui.py | 4 +- .../bonsai/bim/module/model/workspace.py | 13 +-- 6 files changed, 44 insertions(+), 74 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/covering/workspace.py b/src/bonsai/bonsai/bim/module/covering/workspace.py index 383a2322d9..c24f478408 100644 --- a/src/bonsai/bonsai/bim/module/covering/workspace.py +++ b/src/bonsai/bonsai/bim/module/covering/workspace.py @@ -136,8 +136,8 @@ class CoveringToolUI: if AuthoringData.data["ifc_classes"]: if cls.props.ifc_class: box = cls.layout.box() - if AuthoringData.data["type_thumbnail"]: - box.template_icon(icon_value=AuthoringData.data["type_thumbnail"], scale=5) + if thumbnail := AuthoringData.data["relating_type_data"].get("thumbnail"): + box.template_icon(icon_value=thumbnail, scale=5) else: op = box.operator("bim.load_type_thumbnails", text="Load Thumbnails", icon="FILE_REFRESH") op.ifc_class = cls.props.ifc_class @@ -175,14 +175,14 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): element = tool.Ifc.get_entity(active_obj) container = tool.Root.get_default_container() - if AuthoringData.data["predefined_type"] == "FLOORING": + if AuthoringData.data["relating_type_data"].get("predefined_type") == "FLOORING": if element and bpy.context.selected_objects and element.is_a("IfcWall"): bpy.ops.bim.add_instance_flooring_coverings_from_walls() elif container: bpy.ops.bim.add_instance_flooring_covering_from_cursor() else: bpy.ops.bim.add_constr_type_instance() - elif AuthoringData.data["predefined_type"] == "CEILING": + elif AuthoringData.data["relating_type_data"].get("predefined_type") == "CEILING": if element and bpy.context.selected_objects and element.is_a("IfcWall"): bpy.ops.bim.add_instance_ceiling_coverings_from_walls() elif container: diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py index 5cc0f64699..937c586343 100644 --- a/src/bonsai/bonsai/bim/module/model/data.py +++ b/src/bonsai/bonsai/bim/module/model/data.py @@ -26,7 +26,7 @@ from ifcopenshell.util.doc import get_entity_doc, get_predefined_type_doc import bonsai.tool as tool from math import degrees from natsort import natsorted -from typing import Union, Optional +from typing import Union, Optional, Any def refresh(): @@ -43,7 +43,7 @@ def refresh(): class AuthoringData: data = {} - type_thumbnails = {} + type_thumbnails: dict[int, int] = {} types_per_page = 9 is_loaded = False @@ -58,13 +58,10 @@ class AuthoringData: # Make sure .ifc_classes() was run before next lines cls.data["type_elements"] = cls.type_elements() cls.data["type_elements_filtered"] = cls.type_elements_filtered() + # After .type_elements(). cls.data["relating_type_id"] = cls.relating_type_id() # Make sure .relating_type_id() was run before next lines - cls.data["relating_type_id_current"] = cls.relating_type_id_current() - cls.data["relating_type_name"] = cls.relating_type_name() - cls.data["relating_type_description"] = cls.relating_type_description() - cls.data["relating_type_material_usage"] = cls.relating_type_material_usage() - cls.data["predefined_type"] = cls.predefined_type() + cls.data["relating_type_data"] = cls.relating_type_data() # Make sure .type_elements_filtered() was run before next lines cls.data["total_types"] = cls.total_types() cls.data["total_pages"] = cls.total_pages() # Only after .total_types() @@ -72,7 +69,6 @@ class AuthoringData: cls.data["prev_page"] = cls.prev_page() cls.data["paginated_relating_types"] = cls.paginated_relating_types() - cls.data["type_thumbnail"] = cls.type_thumbnail() # Only after .relating_type_id_current() cls.data["materials"] = cls.materials() cls.data["is_voidable_element"] = cls.is_voidable_element() cls.data["has_visible_openings"] = cls.has_visible_openings() @@ -109,10 +105,6 @@ class AuthoringData: version = tool.Ifc.get_schema() return [(c, c, get_entity_doc(version, c).get("description", "")) for c in sorted(names)] - @classmethod - def type_thumbnail(cls): - return cls.type_thumbnails.get(int(cls.data["relating_type_id_current"] or 0), 0) - @classmethod def materials(cls): results = [("0", "None", "No material")] @@ -150,7 +142,7 @@ class AuthoringData: def type_elements_filtered(cls): search_query = cls.props.search_name.lower() - def filter_element(element): + def filter_element(element: ifcopenshell.entity_instance) -> bool: if search_query in (element.Name or "Unnamed").lower(): return True if search_query in (element.Description or "").lower(): @@ -170,21 +162,24 @@ class AuthoringData: elements = cls.data["type_elements_filtered"] elements = elements[(cls.props.type_page - 1) * cls.types_per_page : cls.props.type_page * cls.types_per_page] for element in elements: - predefined_type = ifcopenshell.util.element.get_predefined_type(element) - if predefined_type == "NOTDEFINED": - predefined_type = None - results.append( - { - "id": element.id(), - "ifc_class": element.is_a(), - "name": element.Name or "Unnamed", - "description": element.Description or "No Description", - "predefined_type": predefined_type, - "icon_id": cls.type_thumbnails.get(element.id(), None) or 0, - } - ) + results.append(cls.get_type_data(element)) return results + @classmethod + def get_type_data(cls, element: ifcopenshell.entity_instance) -> dict[str, Any]: + predefined_type = ifcopenshell.util.element.get_predefined_type(element) + if predefined_type == "NOTDEFINED": + predefined_type = None + data = { + "id": element.id(), + "ifc_class": element.is_a(), + "name": element.Name or "Unnamed", + "description": element.Description or "No Description", + "predefined_type": predefined_type, + "icon_id": cls.type_thumbnails.get(element.id(), 0), + } + return data + @classmethod def is_voidable_element(cls): if active_object := tool.Blender.get_active_object(): @@ -313,38 +308,16 @@ class AuthoringData: return [(str(e.id()), e.Name or "Unnamed", e.Description or "") for e in elements] @classmethod - def relating_type_id_current(cls): + def relating_type_data(cls) -> dict[str, Any]: relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id") relating_type_id_data = cls.data["relating_type_id"] - if not relating_type_id and relating_type_id_data: - relating_type_id = relating_type_id_data[0][0] - return relating_type_id - - @classmethod - def relating_type_name(cls): - if relating_type_id := cls.data["relating_type_id_current"]: - return tool.Ifc.get().by_id(int(relating_type_id)).Name or "Unnamed" - - @classmethod - def relating_type_description(cls): - if relating_type_id := cls.data["relating_type_id_current"]: - return tool.Ifc.get().by_id(int(relating_type_id)).Description or "No description" - - @classmethod - def relating_type_material_usage(cls): - if relating_type_id := cls.data["relating_type_id_current"]: - return tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) - - @classmethod - def predefined_type(cls): - relating_type_id = tool.Blender.get_enum_safe(cls.props, "relating_type_id") if relating_type_id is None: - return - relating_type = tool.Ifc.get().by_id(int(relating_type_id)) - if not hasattr(relating_type, "PredefinedType"): - return - predefined_type = relating_type.PredefinedType - return predefined_type + if not relating_type_id_data: + return {} + relating_type_id = relating_type_id_data[0][0] + ifc_file = tool.Ifc.get() + relating_type = ifc_file.by_id(int(relating_type_id)) + return cls.get_type_data(relating_type) @classmethod def selected_material_usages(cls): diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 4e4ebd731d..c8b9e2548f 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -646,8 +646,8 @@ class LoadTypeThumbnails(bpy.types.Operator): queue = queue[offset : offset + 9] # The active type may be in another page than the active one : - if relating_type_id_current := AuthoringData.data["relating_type_id_current"]: - active_element = tool.Ifc.get_entity_by_id(int(relating_type_id_current)) + if relating_type_id_current := AuthoringData.data["relating_type_data"].get("id"): + active_element = tool.Ifc.get_entity_by_id(relating_type_id_current) if active_element and active_element not in queue: queue.append(active_element) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index c8eca8963f..4a93620693 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -60,7 +60,7 @@ def update_ifc_class(self, context): AuthoringData.data["ifc_class_current"] = self.ifc_class AuthoringData.data["type_elements"] = AuthoringData.type_elements() AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id() - AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail() + AuthoringData.data["relating_type_data"] = AuthoringData.relating_type_data() if tool.Blender.get_enum_safe(self, "relating_type_id") is None: self["relating_type_id"] = 0 @@ -73,9 +73,7 @@ def update_ifc_class(self, context): def update_relating_type_id(self, context): AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id() - AuthoringData.data["relating_type_name"] = AuthoringData.relating_type_name() - AuthoringData.data["type_thumbnail"] = AuthoringData.type_thumbnail() - AuthoringData.data["predefined_type"] = AuthoringData.predefined_type() + AuthoringData.data["relating_type_data"] = AuthoringData.relating_type_data() self.type_page = [e[0] for e in AuthoringData.data["relating_type_id"]].index(self.relating_type_id) // 9 + 1 diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 297ce5a2d7..3374b0695d 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -166,9 +166,7 @@ class LaunchTypeManager(bpy.types.Operator): row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] - is_current_relating_type = str(relating_type["id"]) == str( - AuthoringData.data["relating_type_id_current"] - ) + is_current_relating_type = relating_type["id"] == AuthoringData.data["relating_type_data"].get("id") if is_current_relating_type: active_row = row2.row() active_row.alignment = "CENTER" diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 014ebd013b..b35af2b5c9 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -674,12 +674,13 @@ class CreateObjectUI: if not (ifc_class := AuthoringData.data["ifc_class_current"]): return + relating_type_data = AuthoringData.data["relating_type_data"] box = cls.layout.box() row = box.row(align=True) - thumbnail: int = AuthoringData.data["type_thumbnail"] + thumbnail: int = relating_type_data["icon_id"] row.template_icon(icon_value=thumbnail) - row.operator("bim.launch_type_manager", text=AuthoringData.data["relating_type_name"], emboss=False) + row.operator("bim.launch_type_manager", text=relating_type_data["name"], emboss=False) row.operator( "bim.launch_type_manager", icon=tool.Blender.TYPE_MANAGER_ICON, @@ -693,7 +694,7 @@ class CreateObjectUI: row.alignment = "CENTER" row.operator( "bim.launch_type_manager", - text=AuthoringData.data["relating_type_description"], + text=relating_type_data["description"], emboss=False, ) @@ -718,7 +719,7 @@ class CreateObjectUI: row.alignment = "CENTER" row.operator( "bim.launch_type_manager", - text=AuthoringData.data["predefined_type"], + text=AuthoringData.data["relating_type_data"].get("predefined_type"), emboss=False, ) @@ -870,7 +871,7 @@ class EditObjectUI: ) row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator(row, "Rotate 90", "S_R", "Rotate the selected Element by 90 degrees", ui_context) - if AuthoringData.data["relating_type_material_usage"] == "LAYER3": + if AuthoringData.data["relating_type_data"].get("usage") == "LAYER3": row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator( row, @@ -884,7 +885,7 @@ class EditObjectUI: if "LAYER2" in AuthoringData.data["selected_material_usages"]: row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator(cls.layout, "Extend Wall To Slab", "S_E", "", ui_context) - if AuthoringData.data["relating_type_material_usage"] == "LAYER2": + if AuthoringData.data["relating_type_data"].get("usage") == "LAYER2": row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator( row, From 5f4098e5d4a05a60a5002fdc48f349b0ab25b37e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 25 Feb 2025 12:38:52 +0500 Subject: [PATCH 141/476] Update type_elements_filtered on changing ifc class Needed after 1d815cdb67 --- src/bonsai/bonsai/bim/module/model/prop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 4a93620693..512d88aec5 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -59,6 +59,7 @@ def update_ifc_class(self, context): bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class) AuthoringData.data["ifc_class_current"] = self.ifc_class AuthoringData.data["type_elements"] = AuthoringData.type_elements() + AuthoringData.data["type_elements_filtered"] = AuthoringData.type_elements_filtered() AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id() AuthoringData.data["relating_type_data"] = AuthoringData.relating_type_data() if tool.Blender.get_enum_safe(self, "relating_type_id") is None: From 2f6ae1745fffb3745542cfd135b79a27ab49aef7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 25 Feb 2025 11:43:29 +0500 Subject: [PATCH 142/476] typing --- src/bonsai/bonsai/bim/export_ifc.py | 2 +- src/bonsai/bonsai/bim/handler.py | 13 +- src/bonsai/bonsai/bim/ifc.py | 6 +- src/bonsai/bonsai/bim/import_ifc.py | 6 +- src/bonsai/bonsai/bim/module/aggregate/ui.py | 12 +- .../bonsai/bim/module/boundary/operator.py | 3 +- src/bonsai/bonsai/bim/module/boundary/ui.py | 14 +- src/bonsai/bonsai/bim/module/cad/prop.py | 9 + src/bonsai/bonsai/bim/module/cad/workspace.py | 18 +- .../bonsai/bim/module/clash/operator.py | 5 +- .../bonsai/bim/module/constraint/operator.py | 16 +- src/bonsai/bonsai/bim/module/constraint/ui.py | 10 +- src/bonsai/bonsai/bim/module/cost/operator.py | 7 +- src/bonsai/bonsai/bim/module/covering/prop.py | 4 + .../bonsai/bim/module/covering/workspace.py | 5 +- .../bonsai/bim/module/covetool/operator.py | 6 +- .../bonsai/bim/module/debug/operator.py | 2 +- src/bonsai/bonsai/bim/module/diff/operator.py | 8 +- src/bonsai/bonsai/bim/module/document/ui.py | 10 +- .../bonsai/bim/module/drawing/annotation.py | 4 +- .../bonsai/bim/module/drawing/helper.py | 2 +- .../bonsai/bim/module/drawing/operator.py | 6 +- src/bonsai/bonsai/bim/module/drawing/prop.py | 2 +- .../bonsai/bim/module/geometry/__init__.py | 2 +- .../bonsai/bim/module/geometry/operator.py | 7 +- src/bonsai/bonsai/bim/module/geometry/ui.py | 17 +- src/bonsai/bonsai/bim/module/group/ui.py | 5 +- .../bonsai/bim/module/ifcgit/operator.py | 9 +- src/bonsai/bonsai/bim/module/material/ui.py | 16 +- src/bonsai/bonsai/bim/module/misc/operator.py | 2 +- src/bonsai/bonsai/bim/module/model/array.py | 20 +- src/bonsai/bonsai/bim/module/model/data.py | 24 +- src/bonsai/bonsai/bim/module/model/product.py | 5 +- src/bonsai/bonsai/bim/module/model/prop.py | 207 ++++++++++++------ src/bonsai/bonsai/bim/module/model/railing.py | 46 ++-- src/bonsai/bonsai/bim/module/model/roof.py | 30 ++- src/bonsai/bonsai/bim/module/model/stair.py | 26 ++- src/bonsai/bonsai/bim/module/model/ui.py | 16 +- src/bonsai/bonsai/bim/module/model/window.py | 29 ++- .../bonsai/bim/module/model/workspace.py | 9 +- src/bonsai/bonsai/bim/module/nest/prop.py | 2 +- src/bonsai/bonsai/bim/module/nest/ui.py | 12 +- .../bonsai/bim/module/project/operator.py | 9 +- src/bonsai/bonsai/bim/module/pset/operator.py | 19 +- src/bonsai/bonsai/bim/module/pset/ui.py | 16 +- .../bonsai/bim/module/qto/calculator.py | 9 +- src/bonsai/bonsai/bim/module/qto/helper.py | 3 +- src/bonsai/bonsai/bim/module/root/operator.py | 13 +- src/bonsai/bonsai/bim/module/root/ui.py | 4 +- .../bonsai/bim/module/sequence/operator.py | 10 +- src/bonsai/bonsai/bim/module/sequence/prop.py | 8 +- .../bonsai/bim/module/structural/operator.py | 31 +-- src/bonsai/bonsai/bim/module/structural/ui.py | 41 ++-- src/bonsai/bonsai/bim/module/system/ui.py | 20 +- .../bonsai/bim/module/tester/operator.py | 6 +- src/bonsai/bonsai/bim/module/type/operator.py | 2 +- src/bonsai/bonsai/bim/module/type/ui.py | 10 +- src/bonsai/bonsai/bim/module/void/operator.py | 4 +- src/bonsai/bonsai/bim/module/void/ui.py | 2 - src/bonsai/bonsai/bim/operator.py | 4 +- src/bonsai/bonsai/tool/blender.py | 36 ++- src/bonsai/bonsai/tool/cad.py | 10 +- src/bonsai/bonsai/tool/collector.py | 26 ++- src/bonsai/bonsai/tool/covering.py | 11 +- src/bonsai/bonsai/tool/drawing.py | 15 +- src/bonsai/bonsai/tool/geometry.py | 22 +- src/bonsai/bonsai/tool/ifc.py | 9 +- src/bonsai/bonsai/tool/ifcgit.py | 4 +- src/bonsai/bonsai/tool/loader.py | 11 +- src/bonsai/bonsai/tool/model.py | 34 ++- src/bonsai/bonsai/tool/root.py | 3 +- src/bonsai/bonsai/tool/sequence.py | 11 +- src/bonsai/bonsai/tool/spatial.py | 4 +- src/bonsai/bonsai/tool/structural.py | 3 +- src/bonsai/bonsai/tool/surveyor.py | 2 +- src/bonsai/bonsai/tool/system.py | 2 +- src/bonsai/test/bim/bootstrap.py | 26 +-- src/bonsai/test/bim/test_feature.py | 37 ++-- src/bonsai/test/tool/test_collector.py | 4 +- src/bonsai/test/tool/test_drawing.py | 3 +- src/bonsai/test/tool/test_geometry.py | 18 +- src/bonsai/test/tool/test_ifc.py | 6 +- src/bonsai/test/tool/test_model.py | 15 +- src/bonsai/test/tool/test_surveyor.py | 3 +- src/bonsai/test/tool/test_unit.py | 2 +- .../api/geometry/add_window_representation.py | 2 +- src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py | 3 +- 87 files changed, 739 insertions(+), 448 deletions(-) diff --git a/src/bonsai/bonsai/bim/export_ifc.py b/src/bonsai/bonsai/bim/export_ifc.py index 84173a9ec5..8b010fd141 100644 --- a/src/bonsai/bonsai/bim/export_ifc.py +++ b/src/bonsai/bonsai/bim/export_ifc.py @@ -137,7 +137,7 @@ class IfcExporter: return checksum != tool.Geometry.get_material_checksum(obj) def sync_object_placement(self, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]: - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = self.file.by_id(tool.Blender.get_object_bim_props(obj).ifc_definition_id) if tool.Geometry.is_scaled(obj): bpy.ops.bim.update_representation(obj=obj.name) # update_representation might not apply scale if the object has openings diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index 4ed12d74f0..e958098c16 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -64,14 +64,15 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) - refresh_ui_data() return - if not obj.BIMObjectProperties.ifc_definition_id: + props = tool.Blender.get_object_bim_props(obj) + if not props.ifc_definition_id: return - if obj.BIMObjectProperties.is_renaming: - obj.BIMObjectProperties.is_renaming = False + if props.is_renaming: + props.is_renaming = False return - element = tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id) + element = tool.Ifc.get().by_id(props.ifc_definition_id) if "/" in obj.name: object_name = obj.name element_name = obj.name.split("/", 1)[1] @@ -87,8 +88,8 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) - if not element.is_a("IfcRoot"): return element.Name = element_name - if obj.BIMObjectProperties.collection: - obj.BIMObjectProperties.collection.name = object_name + if props.collection: + props.collection.name = object_name refresh_ui_data() diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 35b40b7b3c..ce84c66f02 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -267,7 +267,8 @@ class IfcStore: if element.is_a("IfcSurfaceStyle"): obj.BIMStyleProperties.ifc_definition_id = element.id() else: - obj.BIMObjectProperties.ifc_definition_id = element.id() + props = tool.Blender.get_object_bim_props(obj) + props.ifc_definition_id = element.id() tool.Ifc.setup_listeners(obj) @@ -407,7 +408,8 @@ class IfcStore: if isinstance(obj, bpy.types.Material): obj.BIMStyleProperties.ifc_definition_id = 0 else: # bpy.types.Object - obj.BIMObjectProperties.ifc_definition_id = 0 + props = tool.Blender.get_object_bim_props(obj) + props.ifc_definition_id = 0 @staticmethod def execute_ifc_operator( diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 2a3a5f97a2..e777545780 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -933,7 +933,8 @@ class IfcImporter: self.project = {"ifc": project} obj = tool.Ifc.get_object(project) if obj: - self.project["blender"] = obj.BIMObjectProperties.collection + props = tool.Blender.get_object_bim_props(obj) + self.project["blender"] = props.collection self.has_existing_project = True return self.project["blender"] = bpy.data.collections.new( @@ -943,7 +944,8 @@ class IfcImporter: obj.hide_select = True self.project["blender"].objects.link(obj) self.project["blender"].BIMCollectionProperties.obj = obj - obj.BIMObjectProperties.collection = self.collections[project.GlobalId] = self.project["blender"] + props = tool.Blender.get_object_bim_props(obj) + props.collection = self.collections[project.GlobalId] = self.project["blender"] def create_styles(self) -> None: for style in self.file.by_type("IfcSurfaceStyle"): diff --git a/src/bonsai/bonsai/bim/module/aggregate/ui.py b/src/bonsai/bonsai/bim/module/aggregate/ui.py index 74389c723d..0755d3a54f 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/ui.py +++ b/src/bonsai/bonsai/bim/module/aggregate/ui.py @@ -34,9 +34,9 @@ class BIM_PT_aggregate(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) if not props.ifc_definition_id: return False if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): @@ -66,9 +66,9 @@ class BIM_PT_aggregate(Panel): col.enabled = False op = col.operator("bim.aggregate_assign_object", icon="CHECKMARK") if props.relating_object: - op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id + op.relating_object = tool.Blender.get_object_bim_props(props.relating_object).ifc_definition_id elif props.related_object: - op.related_object = props.related_object.BIMObjectProperties.ifc_definition_id + op.related_object = tool.Blender.get_object_bim_props(props.related_object).ifc_definition_id row.operator("bim.disable_editing_aggregate", icon="CANCEL", text="") return else: @@ -115,9 +115,9 @@ class BIM_PT_linked_aggregate(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) if not props.ifc_definition_id: return False if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py index 75cf32cb66..ac81aa2a65 100644 --- a/src/bonsai/bonsai/bim/module/boundary/operator.py +++ b/src/bonsai/bonsai/bim/module/boundary/operator.py @@ -316,7 +316,8 @@ class ColourByRelatedBuildingElement(bpy.types.Operator): def _execute(self, context): for obj in context.visible_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + props = tool.Blender.get_object_bim_props(obj) + if not props.ifc_definition_id: continue element = tool.Ifc.get_entity(obj) if not element.is_a("IfcRelSpaceBoundary"): diff --git a/src/bonsai/bonsai/bim/module/boundary/ui.py b/src/bonsai/bonsai/bim/module/boundary/ui.py index e7332d50c6..ba7592b34c 100644 --- a/src/bonsai/bonsai/bim/module/boundary/ui.py +++ b/src/bonsai/bonsai/bim/module/boundary/ui.py @@ -52,9 +52,9 @@ class BIM_PT_Boundary(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) if not props.ifc_definition_id: return False if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): @@ -65,7 +65,7 @@ class BIM_PT_Boundary(Panel): def draw(self, context): obj = context.active_object assert obj - props = obj.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) ifc_file = tool.Ifc.get() boundary = ifc_file.by_id(props.ifc_definition_id) self.bprops = tool.Boundary.get_object_boundary_props(obj) @@ -128,9 +128,9 @@ class BIM_PT_SpaceBoundaries(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) if not props.ifc_definition_id: return False if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): @@ -145,7 +145,9 @@ class BIM_PT_SpaceBoundaries(Panel): if not SpaceBoundariesData.is_loaded: SpaceBoundariesData.load() - self.props = context.active_object.BIMObjectProperties + obj = context.active_object + assert obj + self.props = tool.Blender.get_object_bim_props(obj) self.ifc_file = tool.Ifc.get() row = self.layout.row() row.operator("bim.load_space_boundaries") diff --git a/src/bonsai/bonsai/bim/module/cad/prop.py b/src/bonsai/bonsai/bim/module/cad/prop.py index 9fd2dd4d3b..7247408f00 100644 --- a/src/bonsai/bonsai/bim/module/cad/prop.py +++ b/src/bonsai/bonsai/bim/module/cad/prop.py @@ -20,6 +20,7 @@ import bpy from bonsai.bim.module.model.data import AuthoringData from bpy.types import PropertyGroup from math import pi +from typing import TYPE_CHECKING class BIMCadProperties(PropertyGroup): @@ -31,3 +32,11 @@ class BIMCadProperties(PropertyGroup): gable_roof_edge_angle: bpy.props.FloatProperty( name="Gable Roof Edge Angle", default=pi / 2, soft_min=0, soft_max=pi / 2, subtype="ANGLE" ) + + if TYPE_CHECKING: + resolution: int + radius: float + distance: float + x: float + y: float + gable_roof_edge_angle: float diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 4b70d04df1..459fb7ef50 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -146,7 +146,7 @@ class CadTool(WorkSpaceTool): if ( (RailingData.is_loaded or not RailingData.load()) and RailingData.data["pset_data"] - and obj.BIMRailingProperties.is_editing_path + and tool.Model.get_railing_props(obj).is_editing_path ): add_header_apply_button( layout, @@ -159,7 +159,7 @@ class CadTool(WorkSpaceTool): elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] - and obj.BIMRoofProperties.is_editing_path + and tool.Model.get_roof_props(obj).is_editing_path ): add_header_apply_button( layout, "Edit Roof Path", "bim.finish_editing_roof_path", "bim.cancel_editing_roof_path", ui_context @@ -195,12 +195,14 @@ class CadHotkey(bpy.types.Operator): return operator.description or "" def execute(self, context): - self.props = context.scene.BIMCadProperties + self.props = tool.Cad.get_cad_props() getattr(self, f"hotkey_{self.hotkey}")() return {"FINISHED"} def draw(self, context): - props = context.scene.BIMCadProperties + props = tool.Cad.get_cad_props() + obj = context.active_object + if self.hotkey == "S_C": if tool.Geometry.is_profile_object_active(): row = self.layout.row() @@ -226,7 +228,7 @@ class CadHotkey(bpy.types.Operator): elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] - and bpy.context.active_object.BIMRoofProperties.is_editing_path + and tool.Model.get_roof_props(obj).is_editing_path ): self.layout.row().prop(props, "gable_roof_edge_angle") @@ -281,13 +283,17 @@ class CadHotkey(bpy.types.Operator): bpy.ops.bim.edit_extrusion_axis() def hotkey_S_R(self): + obj = bpy.context.active_object + if not obj: + return + if tool.Geometry.is_profile_object_active(): si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion) elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] - and bpy.context.active_object.BIMRoofProperties.is_editing_path + and tool.Model.get_roof_props(obj).is_editing_path ): bpy.ops.bim.set_gable_roof_edge_angle(angle=self.props.gable_roof_edge_angle) diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index c3d87e4759..3fd4e3b4de 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -316,7 +316,8 @@ class SelectIfcClashResults(bpy.types.Operator): global_ids.extend([clash["a_global_id"], clash["b_global_id"]]) for obj in context.visible_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + props = tool.Blender.get_object_bim_props(obj) + if not props.ifc_definition_id: continue ifc_file = "" @@ -335,7 +336,7 @@ class SelectIfcClashResults(bpy.types.Operator): element_file = self.file try: - element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = element_file.by_id(props.ifc_definition_id) except: continue diff --git a/src/bonsai/bonsai/bim/module/constraint/operator.py b/src/bonsai/bonsai/bim/module/constraint/operator.py index 3a66b074c7..4ae0dde395 100644 --- a/src/bonsai/bonsai/bim/module/constraint/operator.py +++ b/src/bonsai/bonsai/bim/module/constraint/operator.py @@ -161,8 +161,12 @@ class AssignConstraint(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.file = tool.Ifc.get() - objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects - products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)] + objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects + products = [ + self.file.by_id(obj_id) + for obj in objs + if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id) + ] if products: ifcopenshell.api.run( "constraint.assign_constraint", @@ -184,8 +188,12 @@ class UnassignConstraint(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.file = tool.Ifc.get() - objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects - products = [self.file.by_id(obj_id) for obj in objs if (obj_id := obj.BIMObjectProperties.ifc_definition_id)] + objs = [bpy.data.objects[self.obj]] if self.obj else context.selected_objects + products = [ + self.file.by_id(obj_id) + for obj in objs + if (obj_id := tool.Blender.get_object_bim_props(obj).ifc_definition_id) + ] if products: ifcopenshell.api.run( "constraint.unassign_constraint", diff --git a/src/bonsai/bonsai/bim/module/constraint/ui.py b/src/bonsai/bonsai/bim/module/constraint/ui.py index 76f4c4a1bb..3693e9565a 100644 --- a/src/bonsai/bonsai/bim/module/constraint/ui.py +++ b/src/bonsai/bonsai/bim/module/constraint/ui.py @@ -79,18 +79,18 @@ class BIM_PT_object_constraints(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): - return False - return bool(context.active_object.BIMObjectProperties.ifc_definition_id) + props = tool.Blender.get_object_bim_props(obj) + return bool(tool.Ifc.get_object_by_identifier(props.ifc_definition_id)) def draw(self, context): if not ObjectConstraintsData.is_loaded: ObjectConstraintsData.load() obj = context.active_object - self.oprops = obj.BIMObjectProperties + assert obj + self.oprops = tool.Blender.get_object_bim_props(obj) self.sprops = context.scene.BIMConstraintProperties self.props = obj.BIMObjectConstraintProperties self.file = tool.Ifc.get() diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index 00d5f5fec0..ead79ba57a 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -740,15 +740,14 @@ class LoadProductCostItems(bpy.types.Operator): @classmethod def poll(cls, context): - if not tool.Ifc.get() or not (obj := context.active_object) or not (obj.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get() or not (obj := context.active_object) or not (tool.Blender.get_ifc_definition_id(obj)): cls.poll_message_set("No IFC object is active.") return False return True def execute(self, context): - core.load_product_cost_items( - tool.Cost, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id) - ) + obj = context.active_object + core.load_product_cost_items(tool.Cost, product=tool.Ifc.get_entity(obj)) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/covering/prop.py b/src/bonsai/bonsai/bim/module/covering/prop.py index 2a26d88e35..3dc65903aa 100644 --- a/src/bonsai/bonsai/bim/module/covering/prop.py +++ b/src/bonsai/bonsai/bim/module/covering/prop.py @@ -18,9 +18,13 @@ import bpy from bpy.types import PropertyGroup +from typing import TYPE_CHECKING class BIMCoveringProperties(PropertyGroup): ceiling_height: bpy.props.FloatProperty( name="ceiling_height", default=2.7, subtype="DISTANCE", description="Ceiling height" ) + + if TYPE_CHECKING: + ceiling_height: float diff --git a/src/bonsai/bonsai/bim/module/covering/workspace.py b/src/bonsai/bonsai/bim/module/covering/workspace.py index c24f478408..904bd98a71 100644 --- a/src/bonsai/bonsai/bim/module/covering/workspace.py +++ b/src/bonsai/bonsai/bim/module/covering/workspace.py @@ -55,7 +55,7 @@ class CoveringToolUI: def draw(cls, context, layout, ifc_element_type=None): cls.layout = layout cls.props = tool.Model.get_model_props() - cls.covering_props = context.scene.BIMCoveringProperties + cls.covering_props = tool.Covering.get_covering_props() row = cls.layout.row(align=True) if not tool.Ifc.get(): @@ -159,12 +159,11 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return operator.description or "" def _execute(self, context): - # self.props = context.scene.BIMCoveringProperties + self.props = tool.Covering.get_covering_props() getattr(self, f"hotkey_{self.hotkey}")() def invoke(self, context, event): # https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey - # self.props = context.scene.BIMSpatialProperties return self.execute(context) def draw(self, context): diff --git a/src/bonsai/bonsai/bim/module/covetool/operator.py b/src/bonsai/bonsai/bim/module/covetool/operator.py index 1d64dd958e..a11e836750 100644 --- a/src/bonsai/bonsai/bim/module/covetool/operator.py +++ b/src/bonsai/bonsai/bim/module/covetool/operator.py @@ -181,12 +181,12 @@ class RunAnalysis(bpy.types.Operator): if modifier.type == "TRIANGULATE": return True - def get_covetool_category(self, obj): + def get_covetool_category(self, obj: bpy.types.Object): if not hasattr(obj, "data") or not isinstance(obj.data, bpy.types.Mesh): return - if not obj.BIMObjectProperties.ifc_definition_id: + if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): return - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = self.file.by_id(ifc_id) ifc_class = element.is_a() if "IfcSlab" in ifc_class: return "floors" diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 4b16c3fb12..67ba6e5199 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -382,7 +382,7 @@ class InspectFromObject(bpy.types.Operator): def get_active_object_ifc_definition(cls, context: bpy.types.Context) -> Union[int, None]: obj = context.active_object assert obj - if ifc_id := obj.BIMObjectProperties.ifc_definition_id: + if ifc_id := tool.Blender.get_ifc_definition_id(obj): return ifc_id if ( (data := obj.data) diff --git a/src/bonsai/bonsai/bim/module/diff/operator.py b/src/bonsai/bonsai/bim/module/diff/operator.py index dc9e981b19..4c243bc28b 100644 --- a/src/bonsai/bonsai/bim/module/diff/operator.py +++ b/src/bonsai/bonsai/bim/module/diff/operator.py @@ -65,7 +65,7 @@ class VisualiseDiff(bpy.types.Operator): obj.color = (0.0, 0.0, 0.7, 1.0) continue - if not obj.BIMObjectProperties.ifc_definition_id: + if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): continue ifc_file = "" @@ -84,7 +84,7 @@ class VisualiseDiff(bpy.types.Operator): element_file = ifc_file try: - element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = element_file.by_id(ifc_id) except: continue global_id = getattr(element, "GlobalId", None) @@ -253,7 +253,7 @@ class SelectDiffObjects(bpy.types.Operator): obj.select_set(True) continue - if not obj.BIMObjectProperties.ifc_definition_id: + if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): continue ifc_file = "" @@ -272,7 +272,7 @@ class SelectDiffObjects(bpy.types.Operator): element_file = ifc_file try: - element = element_file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = element_file.by_id(ifc_id) except: continue global_id = getattr(element, "GlobalId", None) diff --git a/src/bonsai/bonsai/bim/module/document/ui.py b/src/bonsai/bonsai/bim/module/document/ui.py index 4bd935c8f6..fefb87fe6c 100644 --- a/src/bonsai/bonsai/bim/module/document/ui.py +++ b/src/bonsai/bonsai/bim/module/document/ui.py @@ -90,18 +90,20 @@ class BIM_PT_object_documents(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): + if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): return False - return bool(context.active_object.BIMObjectProperties.ifc_definition_id) + if not tool.Ifc.get_object_by_identifier(ifc_id): + return False + return True def draw(self, context): if not ObjectDocumentData.is_loaded: ObjectDocumentData.load() obj = context.active_object - self.oprops = obj.BIMObjectProperties + self.oprops = tool.Blender.get_object_bim_props(obj) self.props = tool.Document.get_document_props() self.file = tool.Ifc.get() diff --git a/src/bonsai/bonsai/bim/module/drawing/annotation.py b/src/bonsai/bonsai/bim/module/drawing/annotation.py index 9f02989384..d312298f1b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/annotation.py +++ b/src/bonsai/bonsai/bim/module/drawing/annotation.py @@ -51,7 +51,7 @@ class Annotator: curve.font = font props = tool.Drawing.get_text_props(obj) props.font_size = "2.5" - collection = bpy.context.scene.camera.BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(bpy.context.scene.camera).collection collection.objects.link(obj) Annotator.resize_text(obj) return obj @@ -133,7 +133,7 @@ class Annotator: co1, _, _, _ = Annotator.get_placeholder_coords(camera) matrix_world = tool.Drawing.get_camera_matrix(camera) matrix_world.translation = co1 - collection = camera.BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(camera).collection if object_type == "TEXT": obj = bpy.data.objects.new(object_type, None) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index c63fd3d910..5a46b8e27b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -343,7 +343,7 @@ def get_active_drawing( props = tool.Drawing.get_document_props() try: camera = tool.Ifc.get_object(tool.Ifc.get().by_id(props.active_drawing_id)) - return camera.BIMObjectProperties.collection, camera + return tool.Blender.get_object_bim_props(camera).collection, camera except: return None, None diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 3528e4dc36..2b75406d5c 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -246,7 +246,7 @@ class CreateDrawing(bpy.types.Operator): def execute(self, context): self.props = tool.Drawing.get_document_props() - active_drawing_id = context.scene.camera.BIMObjectProperties.ifc_definition_id + active_drawing_id = tool.Blender.get_ifc_definition_id(context.scene.camera) if self.print_all: original_drawing_id = active_drawing_id drawings_to_print = [d.ifc_definition_id for d in self.props.drawings if d.is_selected and d.is_drawing] @@ -391,7 +391,7 @@ class CreateDrawing(bpy.types.Operator): bpy.ops.render.render(write_still=True) else: previous_visibility = {} - for obj in self.camera.BIMObjectProperties.collection.objects: + for obj in tool.Blender.get_object_bim_props(self.camera).collection.objects: if bpy.context.view_layer.objects.get(obj.name): previous_visibility[obj.name] = obj.hide_get() obj.hide_set(True) @@ -2095,7 +2095,7 @@ class ResizeText(bpy.types.Operator): # TODO: check undo redo def execute(self, context): - for obj in context.scene.camera.BIMObjectProperties.collection.objects: + for obj in tool.Blender.get_object_bim_props(context.scene.camera).collection.objects: if isinstance(obj.data, bpy.types.TextCurve): annotation.Annotator.resize_text(obj) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 6c875d1cfe..8963fa5b8f 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -266,7 +266,7 @@ def update_titleblocks(self, context): def update_should_draw_decorations(self, context: bpy.types.Context) -> None: if self.should_draw_decorations: # TODO: design a proper text variable templating renderer - collection = context.scene.camera.BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(context.scene.camera).collection for obj in collection.objects: element = tool.Ifc.get_entity(obj) if not element or not tool.Drawing.is_annotation_object_type(element, ["TEXT", "TEXT_LEADER"]): diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index 4e9eb9cf6f..79f2ceeb67 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -102,7 +102,7 @@ def block_scale(scene: bpy.types.Scene) -> None: import bonsai.tool as tool if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active): - if isinstance(obj, bpy.types.Object) and obj.BIMObjectProperties.ifc_definition_id: + if isinstance(obj, bpy.types.Object) and tool.Blender.get_ifc_definition_id(obj): if obj.scale != (1, 1, 1): obj.scale = (1, 1, 1) elif isinstance(obj, bpy.types.Mesh) and tool.Geometry.get_mesh_props(obj).ifc_definition_id: diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 0763bd4963..99612e638f 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -464,7 +464,8 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator): assert tool.Geometry.is_data_supported_for_adding_representation(data) mprops = tool.Geometry.get_mesh_props(data) - product = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + product = tool.Ifc.get_entity(obj) + assert product material = ifcopenshell.util.element.get_material(product, should_skip_usage=True) # NOTE: Currently iterator doesn't detect whether opening is actually affected the representation @@ -903,7 +904,7 @@ class OverrideOutlinerDelete(bpy.types.Operator): if element := tool.Ifc.get_entity(obj): if tool.Geometry.is_locked(element): self.report({"ERROR"}, lock_error_message(obj.name)) - if collection := obj.BIMObjectProperties.collection: + if collection := tool.Blender.get_object_bim_props(obj).collection: collections_to_delete.discard(collection) continue tool.Geometry.delete_ifc_object(obj) @@ -1067,7 +1068,7 @@ class OverrideDuplicateMove(bpy.types.Operator): continue # clear object's collection so it will be able to have it's own - new_obj.BIMObjectProperties.collection = None + tool.Blender.get_object_bim_props(new_obj).collection = None # copy the actual class new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj) diff --git a/src/bonsai/bonsai/bim/module/geometry/ui.py b/src/bonsai/bonsai/bim/module/geometry/ui.py index 8ba3958222..b1fe7bea61 100644 --- a/src/bonsai/bonsai/bim/module/geometry/ui.py +++ b/src/bonsai/bonsai/bim/module/geometry/ui.py @@ -334,9 +334,9 @@ class BIM_PT_connections(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - if not tool.Ifc.get_object_by_identifier(context.active_object.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(tool.Blender.get_ifc_definition_id(obj)): return False return tool.Ifc.get() @@ -345,7 +345,6 @@ class BIM_PT_connections(Panel): ConnectionsData.load() layout = self.layout - props = context.active_object.BIMObjectProperties if not ConnectionsData.data["connections"] and not ConnectionsData.data["is_connection_realization"]: layout.label(text="No connections found") @@ -480,12 +479,16 @@ class BIM_PT_placement(Panel): @classmethod def poll(cls, context): - return (obj := context.active_object) and obj.BIMObjectProperties.ifc_definition_id + return (obj := context.active_object) and tool.Blender.get_ifc_definition_id(obj) def draw(self, context): if not PlacementData.is_loaded: PlacementData.load() + obj = context.active_object + assert obj + props = tool.Blender.get_object_bim_props(obj) + if not PlacementData.data["has_placement"]: row = self.layout.row() row.label(text="No Object Placement Found") @@ -496,12 +499,12 @@ class BIM_PT_placement(Panel): row = self.layout.row() row.prop(context.active_object, "rotation_euler", text="Rotation") - if context.active_object.BIMObjectProperties.blender_offset_type != "NONE": + if props.blender_offset_type != "NONE": row = self.layout.row(align=True) row.label(text="Blender Offset", icon="TRACKING_REFINE_FORWARDS") - row.label(text=context.active_object.BIMObjectProperties.blender_offset_type) + row.label(text=props.blender_offset_type) - if context.active_object.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE": + if props.blender_offset_type != "NOT_APPLICABLE": row = self.layout.row(align=True) row.label(text=PlacementData.data["original_x"], icon="EMPTY_AXIS") row.label(text=PlacementData.data["original_y"]) diff --git a/src/bonsai/bonsai/bim/module/group/ui.py b/src/bonsai/bonsai/bim/module/group/ui.py index 8cd7d1fe2a..309c49d5d0 100644 --- a/src/bonsai/bonsai/bim/module/group/ui.py +++ b/src/bonsai/bonsai/bim/module/group/ui.py @@ -91,9 +91,10 @@ class BIM_PT_object_groups(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - return tool.Ifc.get() and context.active_object.BIMObjectProperties.ifc_definition_id + props = tool.Blender.get_object_bim_props(obj) + return tool.Ifc.get() and props.ifc_definition_id def draw(self, context): if not ObjectGroupsData.is_loaded: diff --git a/src/bonsai/bonsai/bim/module/ifcgit/operator.py b/src/bonsai/bonsai/bim/module/ifcgit/operator.py index bbb7133122..de339c111c 100644 --- a/src/bonsai/bonsai/bim/module/ifcgit/operator.py +++ b/src/bonsai/bonsai/bim/module/ifcgit/operator.py @@ -374,16 +374,17 @@ class ObjectLog(bpy.types.Operator): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): cls.poll_message_set("No Active Object") - elif not context.active_object.BIMObjectProperties.ifc_definition_id: + elif not tool.Blender.get_ifc_definition_id(obj): cls.poll_message_set("Active Object doesn't have an IFC definition") else: return True def execute(self, context): - - step_id = context.active_object.BIMObjectProperties.ifc_definition_id + obj = context.active_object + assert obj + step_id = tool.Blender.get_ifc_definition_id(obj) core.entity_log(tool.IfcGit, tool.Ifc, step_id, self) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index fe0fce2413..eaf4ee06c4 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -137,14 +137,14 @@ class BIM_PT_object_material(Panel): def poll(cls, context): if not tool.Blender.is_tab(context, "GEOMETRY"): return False - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False - if not hasattr(tool.Ifc.get().by_id(props.ifc_definition_id), "HasAssociations"): + if not hasattr(tool.Ifc.get().by_id(ifc_id), "HasAssociations"): return False return True @@ -152,9 +152,11 @@ class BIM_PT_object_material(Panel): if not ObjectMaterialData.is_loaded: ObjectMaterialData.load() + obj = context.active_object + assert obj self.file = tool.Ifc.get() - self.oprops = context.active_object.BIMObjectProperties - self.props = context.active_object.BIMObjectMaterialProperties + self.oprops = tool.Blender.get_object_bim_props(obj) + self.props = obj.BIMObjectMaterialProperties self.mprops = tool.Material.get_material_props() if not ObjectMaterialData.data["materials"]: diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index 20bd454d35..a1ff930408 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -257,7 +257,7 @@ class DrawSystemArrows(bpy.types.Operator, tool.Ifc.Operator): sources = [] for obj in bpy.context.selected_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + if not tool.Blender.get_ifc_definition_id(obj): continue element = tool.Ifc.get_entity(obj) diff --git a/src/bonsai/bonsai/bim/module/model/array.py b/src/bonsai/bonsai/bim/module/model/array.py index 7e91db2804..0ec49d5561 100644 --- a/src/bonsai/bonsai/bim/module/model/array.py +++ b/src/bonsai/bonsai/bim/module/model/array.py @@ -72,7 +72,9 @@ class DisableEditingArray(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - context.active_object.BIMArrayProperties.is_editing = -1 + obj = context.active_object + assert obj + tool.Model.get_array_props(obj).is_editing = -1 return {"FINISHED"} @@ -84,8 +86,9 @@ class EnableEditingArray(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMArrayProperties + props = tool.Model.get_array_props(obj) relating_obj = props.relating_array_object @@ -119,7 +122,7 @@ class EditArray(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object element = tool.Ifc.get_entity(obj) - props = obj.BIMArrayProperties + props = tool.Model.get_array_props(obj) si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") @@ -182,7 +185,7 @@ class RemoveArray(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object element = tool.Ifc.get_entity(obj) - props = obj.BIMArrayProperties + props = tool.Model.get_array_props(obj) pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array") data = json.loads(pset["Data"]) @@ -302,7 +305,8 @@ class Input3DCursorXArray(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMArrayProperties + assert obj + props = tool.Model.get_array_props(obj) cursor = context.scene.cursor if props.use_local_space: props.x = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.translation).x @@ -318,7 +322,8 @@ class Input3DCursorYArray(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMArrayProperties + assert obj + props = tool.Model.get_array_props(obj) cursor = context.scene.cursor if props.use_local_space: props.y = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.translation).y @@ -334,7 +339,8 @@ class Input3DCursorZArray(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMArrayProperties + assert obj + props = tool.Model.get_array_props(obj) cursor = context.scene.cursor if props.use_local_space: props.z = (Matrix.inverted(obj.matrix_world) @ cursor.matrix.translation).z diff --git a/src/bonsai/bonsai/bim/module/model/data.py b/src/bonsai/bonsai/bim/module/model/data.py index 937c586343..3af67e4990 100644 --- a/src/bonsai/bonsai/bim/module/model/data.py +++ b/src/bonsai/bonsai/bim/module/model/data.py @@ -385,7 +385,9 @@ class StairData: @classmethod def general_params(cls): - props = bpy.context.active_object.BIMStairProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_stair_props(obj) data = cls.data["pset_data"]["data_dict"] general_params = {} general_props = props.get_props_kwargs(stair_type=data["stair_type"]) @@ -451,7 +453,9 @@ class WindowData: @classmethod def general_params(cls): - props = bpy.context.active_object.BIMWindowProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_window_props(obj) data = cls.data["pset_data"]["data_dict"] general_params = {} general_props = props.get_general_kwargs() @@ -462,7 +466,9 @@ class WindowData: @classmethod def lining_params(cls): - props = bpy.context.active_object.BIMWindowProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_window_props(obj) data = cls.data["pset_data"]["data_dict"] lining_data = data["lining_properties"] lining_params = {} @@ -474,7 +480,9 @@ class WindowData: @classmethod def panel_params(cls): - props = bpy.context.active_object.BIMWindowProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_window_props(obj) panel_data = cls.data["pset_data"]["data_dict"]["panel_properties"] panel_params = {} panel_props = props.get_panel_kwargs() @@ -565,7 +573,9 @@ class RailingData: @classmethod def general_params(cls): - props = bpy.context.active_object.BIMRailingProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_railing_props(obj) data = cls.data["pset_data"]["data_dict"] general_params = {} general_props = props.get_general_kwargs(railing_type=data["railing_type"]) @@ -599,7 +609,9 @@ class RoofData: @classmethod def general_params(cls): - props = bpy.context.active_object.BIMRoofProperties + obj = bpy.context.active_object + assert obj + props = tool.Model.get_roof_props(obj) data = cls.data["pset_data"]["data_dict"] general_params = {} general_props = props.get_general_kwargs(generation_method=data["generation_method"]) diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index c8b9e2548f..04446deaeb 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -718,14 +718,15 @@ class MirrorElements(bpy.types.Operator, tool.Ifc.Operator): obj.matrix_world = newmat -def generate_box(usecase_path, ifc_file, settings): +def generate_box(usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None: box_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Box", "MODEL_VIEW") if not box_context: return obj = settings["blender_object"] if 0 in list(obj.dimensions): return - product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id) + product = tool.Ifc.get_entity(obj) + assert product old_box = ifcopenshell.util.representation.get_representation(product, "Model", "Box", "MODEL_VIEW") if settings["context"].ContextType == "Model" and getattr(settings["context"], "ContextIdentifier") == "Body": if old_box: diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 512d88aec5..a8e486c2a6 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -28,34 +28,36 @@ from math import pi, radians from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator from bonsai.bim.module.model.door import update_door_modifier_bmesh from bonsai.bim.module.model.window import update_window_modifier_bmesh -from typing import TYPE_CHECKING, Literal, get_args +from typing import TYPE_CHECKING, Literal, get_args, Union, get_args -def get_ifc_class(self, context): +def get_ifc_class(self: "BIMModelProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not AuthoringData.is_loaded: AuthoringData.load() return AuthoringData.data["ifc_classes"] -def get_boundary_class(self, context): +def get_boundary_class(self: "BIMModelProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not AuthoringData.is_loaded: AuthoringData.load() return AuthoringData.data["boundary_class"] -def get_relating_type_id(self, context): +def get_relating_type_id(self: "BIMModelProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not AuthoringData.is_loaded: AuthoringData.load() return AuthoringData.data["relating_type_id"] -def get_materials(self, context): +def get_materials( + self: Union["BIMWindowProperties", "BIMDoorProperties"], context: bpy.types.Context +) -> list[tuple[str, str, str]]: if not AuthoringData.is_loaded: AuthoringData.load() return AuthoringData.data["materials"] -def update_ifc_class(self, context): +def update_ifc_class(self: "BIMModelProperties", context: bpy.types.Context) -> None: bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class) AuthoringData.data["ifc_class_current"] = self.ifc_class AuthoringData.data["type_elements"] = AuthoringData.type_elements() @@ -72,46 +74,46 @@ def update_ifc_class(self, context): AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types() -def update_relating_type_id(self, context): +def update_relating_type_id(self: "BIMModelProperties", context: bpy.types.Context) -> None: AuthoringData.data["relating_type_id"] = AuthoringData.relating_type_id() AuthoringData.data["relating_type_data"] = AuthoringData.relating_type_data() self.type_page = [e[0] for e in AuthoringData.data["relating_type_id"]].index(self.relating_type_id) // 9 + 1 -def update_type_page(self, context): +def update_type_page(self: "BIMModelProperties", context: bpy.types.Context) -> None: AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types() bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class, offset=9 * (self.type_page - 1), limit=9) self["type_page"] = min(self["type_page"], AuthoringData.data["total_pages"]) self["type_page"] = max(self["type_page"], 1) -def update_relating_array_from_object(self, context): +def update_relating_array_from_object(self: "BIMArrayProperties", context: bpy.types.Context) -> None: bpy.ops.bim.enable_editing_array(item=self.is_editing) return -def is_object_array_applicable(self, obj): +def is_object_array_applicable(self: "BIMArrayProperties", obj: bpy.types.Object) -> bool: element = tool.Ifc.get_entity(obj) if not element: return False return ifcopenshell.util.element.get_pset(element, "BBIM_Array") -def update_wall_axis_decorator(self, context): +def update_wall_axis_decorator(self: "BIMModelProperties", context: bpy.types.Context) -> None: if self.show_wall_axis: WallAxisDecorator.install(bpy.context) else: WallAxisDecorator.uninstall() -def update_slab_direction_decorator(self, context): +def update_slab_direction_decorator(self: "BIMModelProperties", context: bpy.types.Context) -> None: if self.show_slab_direction: SlabDirectionDecorator.install(bpy.context) else: SlabDirectionDecorator.uninstall() -def update_search_name(self, context): +def update_search_name(self: "BIMModelProperties", context: bpy.types.Context) -> None: AuthoringData.load() # Total number of pages may decrease when using the search bar : if self.type_page > AuthoringData.data["total_pages"]: @@ -119,17 +121,17 @@ def update_search_name(self, context): bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class) -def update_x_angle(self, context): +def update_x_angle(self: "BIMModelProperties", context: bpy.types.Context) -> None: angle_deg = math.degrees(self.x_angle) if tool.Cad.is_x(angle_deg, -90, 0.5) or tool.Cad.is_x(angle_deg, 90, 0.5): self.x_angle = 0 -def update_door(self, context): +def update_door(self: "BIMDoorProperties", context: bpy.types.Context) -> None: update_door_modifier_bmesh(context) -def update_window(self, context): +def update_window(self: "BIMWindowProperties", context: bpy.types.Context) -> None: update_window_modifier_bmesh(context) @@ -303,36 +305,45 @@ class BIMArrayProperties(PropertyGroup): poll=is_object_array_applicable, ) + if TYPE_CHECKING: + is_editing: int + count: int + x: float + y: float + z: float + use_local_space: bool + method: Literal["OFFSET", "DISTRIBUTE"] + sync_children: bool + relating_array_object: Union[bpy.types.Object, None] -def update_total_length_target(self, context): + +def update_total_length_target(self: "BIMStairProperties", context: bpy.types.Context) -> None: self["tread_run"] = self.total_length_target / (self.number_of_treads + 1) -def update_tread_run(self, context): +def update_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None: if self.total_length_lock: self["number_of_treads"] = int((self.total_length_target / self.tread_run) - 1) else: self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run -def update_number_of_treads(self, context): +def update_number_of_treads(self: "BIMStairProperties", context: bpy.types.Context) -> None: if self.total_length_lock: self["tread_run"] = self.total_length_target / (self.number_of_treads + 1) else: self["total_length_target"] = (self.number_of_treads + 1) * self.tread_run +StairType = Literal["CONCRETE", "WOOD/STEEL", "GENERIC"] + + class BIMStairProperties(PropertyGroup): - def validate_nosing_value(self, context): + def validate_nosing_value(self, context: bpy.types.Context) -> None: if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0: self["nosing_length"] = 0 non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type") - stair_types = ( - ("CONCRETE", "Concrete", ""), - ("WOOD/STEEL", "Wood / Steel", ""), - ("GENERIC", "Generic", ""), - ) is_editing: bpy.props.BoolProperty(default=False) width: bpy.props.FloatProperty(name="Width", default=1.2, soft_min=0.01, subtype="DISTANCE") @@ -361,7 +372,10 @@ class BIMStairProperties(PropertyGroup): top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, soft_min=0, subtype="DISTANCE") has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True) stair_type: bpy.props.EnumProperty( - name="Stair Type", items=stair_types, default="CONCRETE", update=validate_nosing_value + name="Stair Type", + items=[(i, i.replace("/", " / ").title(), "") for i in get_args(StairType)], + default="CONCRETE", + update=validate_nosing_value, ) custom_first_last_tread_run: bpy.props.FloatVectorProperty( name="Custom First / Last Treads Widths", @@ -385,6 +399,23 @@ class BIMStairProperties(PropertyGroup): name="Nosing Depth", description="Depth of the tread's nosing", min=0, default=0, unit="LENGTH" ) + if TYPE_CHECKING: + is_editing: bool + width: float + height: float + number_of_treads: int + total_length_target: float + total_length_lock: bool + tread_depth: float + tread_run: float + base_slab_depth: float + top_slab_depth: float + has_top_nib: bool + stair_type: str + custom_first_last_tread_run: tuple[float, float] + nosing_length: float + nosing_depth: float + def get_props_kwargs(self, convert_to_project_units=False, stair_type=None): if not stair_type: stair_type = self.stair_type @@ -445,20 +476,22 @@ def window_type_prop_update(self, context): update_window(self, context) +WindowType = Literal[ + "SINGLE_PANEL", + "DOUBLE_PANEL_HORIZONTAL", + "DOUBLE_PANEL_VERTICAL", + "TRIPLE_PANEL_BOTTOM", + "TRIPLE_PANEL_TOP", + "TRIPLE_PANEL_LEFT", + "TRIPLE_PANEL_RIGHT", + "TRIPLE_PANEL_HORIZONTAL", + "TRIPLE_PANEL_VERTICAL", +] + + # default prop values are in mm and converted later class BIMWindowProperties(PropertyGroup): non_si_units_props = ("is_editing", "window_type") - window_types = ( - ("SINGLE_PANEL", "SINGLE_PANEL", ""), - ("DOUBLE_PANEL_HORIZONTAL", "DOUBLE_PANEL_HORIZONTAL", ""), - ("DOUBLE_PANEL_VERTICAL", "DOUBLE_PANEL_VERTICAL", ""), - ("TRIPLE_PANEL_BOTTOM", "TRIPLE_PANEL_BOTTOM", ""), - ("TRIPLE_PANEL_TOP", "TRIPLE_PANEL_TOP", ""), - ("TRIPLE_PANEL_LEFT", "TRIPLE_PANEL_LEFT", ""), - ("TRIPLE_PANEL_RIGHT", "TRIPLE_PANEL_RIGHT", ""), - ("TRIPLE_PANEL_HORIZONTAL", "TRIPLE_PANEL_HORIZONTAL", ""), - ("TRIPLE_PANEL_VERTICAL", "TRIPLE_PANEL_VERTICAL", ""), - ) # number of panels and default mullion/transom values # fmt: off @@ -477,7 +510,10 @@ class BIMWindowProperties(PropertyGroup): is_editing: bpy.props.BoolProperty(default=False) window_type: bpy.props.EnumProperty( - name="Window Type", items=window_types, default="SINGLE_PANEL", update=window_type_prop_update + name="Window Type", + items=[(i, i, "") for i in get_args(WindowType)], + default="SINGLE_PANEL", + update=window_type_prop_update, ) overall_height: bpy.props.FloatProperty( name="Overall Height", default=0.9, subtype="DISTANCE", update=update_window @@ -546,6 +582,31 @@ class BIMWindowProperties(PropertyGroup): framing_material: bpy.props.EnumProperty(name="Framing Material", items=get_materials, options=set()) glazing_material: bpy.props.EnumProperty(name="Glazing Material", items=get_materials, options=set()) + if TYPE_CHECKING: + is_editing: bool + window_type: WindowType + overall_height: float + overall_width: float + lining_depth: float + lining_thickness: float + lining_offset: float + lining_to_panel_offset_x: float + lining_to_panel_offset_y: float + mullion_thickness: float + first_mullion_offset: float + second_mullion_offset: float + first_transom_offset: float + second_transom_offset: float + + # Panel properties. + frame_depth: tuple[float, float, float] + frame_thickness: tuple[float, float, float] + + # Material properties. + lining_material: str + framing_material: str + glazing_material: str + def get_general_kwargs(self, convert_to_project_units=False): kwargs = { "window_type": self.window_type, @@ -824,6 +885,10 @@ class BIMDoorProperties(PropertyGroup): setattr(self, prop_name, kwargs[prop_name]) +RailingType = Literal["FRAMELESS_PANEL", "WALL_MOUNTED_HANDRAIL"] +CapType = Literal["TO_END_POST_AND_FLOOR", "TO_END_POST", "TO_FLOOR", "TO_WALL", "180", "NONE"] + + class BIMRailingProperties(PropertyGroup): non_si_units_props = ( "is_editing", @@ -833,23 +898,12 @@ class BIMRailingProperties(PropertyGroup): "path_data", ) - railing_types = ( - ("FRAMELESS_PANEL", "FRAMELESS_PANEL", ""), - ("WALL_MOUNTED_HANDRAIL", "WALL_MOUNTED_HANDRAIL", ""), - ) - cap_types = ( - ("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", ""), - ("NONE", "NONE", ""), - ) - is_editing: bpy.props.BoolProperty(default=False) is_editing_path: bpy.props.BoolProperty(default=False) - railing_type: bpy.props.EnumProperty(name="Railing Type", items=railing_types, default="FRAMELESS_PANEL") + railing_type: bpy.props.EnumProperty( + name="Railing Type", items=[(i, i, "") for i in get_args(RailingType)], default="FRAMELESS_PANEL" + ) height: bpy.props.FloatProperty(name="Height", default=1.0, subtype="DISTANCE") thickness: bpy.props.FloatProperty(name="Thickness", default=0.050, subtype="DISTANCE") spacing: bpy.props.FloatProperty(name="Spacing", default=0.050, subtype="DISTANCE") @@ -875,7 +929,24 @@ class BIMRailingProperties(PropertyGroup): description="Clear width between the railing and the wall", subtype="DISTANCE", ) - terminal_type: bpy.props.EnumProperty(name="Terminal Type", items=cap_types, default="180") + terminal_type: bpy.props.EnumProperty( + name="Terminal Type", items=[(i, i, "") for i in get_args(CapType)], default="180" + ) + + if TYPE_CHECKING: + is_editing: bool + is_editing_path: bool + + railing_type: RailingType + height: float + thickness: float + spacing: float + + use_manual_supports: bool + support_spacing: float + railing_diameter: float + clear_width: float + terminal_type: CapType def get_general_kwargs(self, railing_type=None, convert_to_project_units=False): if railing_type is None: @@ -921,11 +992,15 @@ def to_percentage(angle: float) -> float: return math.tan(angle) * 100 +RoofType = Literal["HIP/GABLE ROOF"] +RoofGenerationMethod = Literal["HEIGHT", "ANGLE"] + + class BIMRoofProperties(PropertyGroup): - def update_angle(self, context) -> None: + def update_angle(self, context: bpy.types.Context) -> None: self["angle"] = to_angle(self.percentage) - def update_percentage(self, context) -> None: + def update_percentage(self, context: bpy.types.Context) -> None: self["percentage"] = to_percentage(self.angle) non_si_units_props = ( @@ -937,18 +1012,15 @@ class BIMRoofProperties(PropertyGroup): "percentage", "rafter_edge_angle", ) - roof_types = (("HIP/GABLE ROOF", "HIP/GABLE ROOF", ""),) - roof_generation_methods = ( - ("HEIGHT", "HEIGHT", ""), - ("ANGLE", "ANGLE", ""), - ) is_editing: bpy.props.BoolProperty(default=False) is_editing_path: bpy.props.BoolProperty(default=False) - roof_type: bpy.props.EnumProperty(name="Roof Type", items=roof_types, default="HIP/GABLE ROOF") + roof_type: bpy.props.EnumProperty( + name="Roof Type", items=[(i, i, "") for i in get_args(RoofType)], default="HIP/GABLE ROOF" + ) generation_method: bpy.props.EnumProperty( - name="Roof Generation Method", items=roof_generation_methods, default="ANGLE" + name="Roof Generation Method", items=[(i, i, "") for i in get_args(RoofGenerationMethod)], default="ANGLE" ) height: bpy.props.FloatProperty( name="Height", default=1.0, description="Maximum height of the roof to be generated.", subtype="DISTANCE" @@ -978,6 +1050,17 @@ class BIMRoofProperties(PropertyGroup): name="Rafter Edge Angle", min=0, max=pi / 2, default=pi / 2, subtype="ANGLE" ) + if TYPE_CHECKING: + is_editing: bool + is_editing_path: bool + roof_type: Literal["HIP/GABLE ROOF"] + generation_method: Literal["HEIGHT", "ANGLE"] + height: float + angle: float + percentage: float + roof_thickness: float + rafter_edge_angle: float + def get_general_kwargs(self, generation_method=None, convert_to_project_units=False): if generation_method is None: generation_method = self.generation_method diff --git a/src/bonsai/bonsai/bim/module/model/railing.py b/src/bonsai/bonsai/bim/module/model/railing.py index 78c94f4ed3..632e1748be 100644 --- a/src/bonsai/bonsai/bim/module/model/railing.py +++ b/src/bonsai/bonsai/bim/module/model/railing.py @@ -55,8 +55,10 @@ def update_railing_modifier_ifc_data(context: bpy.types.Context) -> None: since it's going to update ifc representation """ obj = context.active_object - props = obj.BIMRailingProperties + assert obj + props = tool.Model.get_railing_props(obj) element = tool.Ifc.get_entity(obj) + assert element ifc_file = tool.Ifc.get() # type attributes @@ -122,7 +124,8 @@ def update_railing_modifier_bmesh(context: bpy.types.Context) -> None: If BBIM Pset just changed should call refresh() before updating bmesh """ obj = context.active_object - props = obj.BIMRailingProperties + assert obj + props = tool.Model.get_railing_props(obj) V_ = tool.Blender.V_ # NOTE: using Data since bmesh update will hapen very often @@ -327,8 +330,10 @@ class AddRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMRailingProperties + assert element + props = tool.Model.get_railing_props(obj) si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) railing_data = props.get_general_kwargs(convert_to_project_units=True) @@ -367,7 +372,8 @@ class CopyRailingParameters(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): source_obj = context.active_object - source_props = source_obj.BIMRailingProperties + assert source_obj + source_props = tool.Model.get_railing_props(source_obj) railing_data = source_props.get_general_kwargs(convert_to_project_units=True) for target_obj in context.selected_objects: @@ -379,7 +385,8 @@ class CopyRailingParameters(bpy.types.Operator, tool.Ifc.Operator): continue railing_data["path_data"] = RailingData.data["path_data"] target_element = tool.Ifc.get_entity(target_obj) - target_props = target_obj.BIMRailingProperties + assert target_element + target_props = tool.Model.get_railing_props(target_obj) target_props.set_props_kwargs_from_ifc_data(railing_data) update_bbim_railing_pset(target_element, railing_data) @@ -398,7 +405,8 @@ class EnableEditingRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMRailingProperties + assert obj + props = tool.Model.get_railing_props(obj) data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"] data["path_data"] = json.dumps(data["path_data"]) @@ -416,8 +424,9 @@ class CancelEditingRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"] - props = obj.BIMRailingProperties + props = tool.Model.get_railing_props(obj) # restore previous settings since editing was canceled props.set_props_kwargs_from_ifc_data(data) @@ -434,8 +443,10 @@ class FinishEditingRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMRailingProperties + assert element + props = tool.Model.get_railing_props(obj) pset_data = tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing") path_data = pset_data["data_dict"]["path_data"] @@ -457,8 +468,10 @@ class FlipRailingPathOrder(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMRailingProperties + assert element + props = tool.Model.get_railing_props(obj) pset_data = tool.Model.get_modeling_bbim_pset_data(bpy.context.active_object, "BBIM_Railing") path_data = pset_data["data_dict"]["path_data"] @@ -488,7 +501,8 @@ class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object [o.select_set(False) for o in context.selected_objects if o != obj] - props = obj.BIMRailingProperties + assert obj + props = tool.Model.get_railing_props(obj) data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Railing")["data_dict"] # required since we could load pset from .ifc and BIMRoofProperties won't be set props.set_props_kwargs_from_ifc_data(data) @@ -505,7 +519,8 @@ class EnableEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): def cancel_editing_railing_path(context: bpy.types.Context) -> set[str]: obj = context.active_object - props = obj.BIMRailingProperties + assert obj + props = tool.Model.get_railing_props(obj) ProfileDecorator.uninstall() props.is_editing_path = False @@ -517,6 +532,7 @@ def cancel_editing_railing_path(context: bpy.types.Context) -> set[str]: update_railing_modifier_bmesh(context) else: element = tool.Ifc.get_entity(obj) + assert element body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") bonsai.core.geometry.switch_representation( tool.Ifc, @@ -547,8 +563,9 @@ class FinishEditingRailingPath(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMRailingProperties + props = tool.Model.get_railing_props(obj) railing_data = props.get_general_kwargs(convert_to_project_units=True) path_data = get_path_data(obj) @@ -574,8 +591,11 @@ class RemoveRailing(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - obj.BIMRailingProperties.is_editing = False + assert element + props = tool.Model.get_railing_props(obj) + props.is_editing = False pset = tool.Pset.get_element_pset(element, "BBIM_Railing") ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset) diff --git a/src/bonsai/bonsai/bim/module/model/roof.py b/src/bonsai/bonsai/bim/module/model/roof.py index 8afd41bbee..77058bca39 100644 --- a/src/bonsai/bonsai/bim/module/model/roof.py +++ b/src/bonsai/bonsai/bim/module/model/roof.py @@ -409,7 +409,8 @@ def update_roof_modifier_ifc_data(context: bpy.types.Context) -> None: since it's going to update ifc representation """ obj = context.active_object - props = obj.BIMRoofProperties + assert obj + props = tool.Model.get_roof_props(obj) element = tool.Ifc.get_entity(obj) def roof_is_gabled() -> bool: @@ -442,7 +443,7 @@ def update_roof_modifier_bmesh(obj: bpy.types.Object) -> None: """before using should make sure that Data contains up-to-date information. If BBIM Pset just changed should call refresh() before updating bmesh """ - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) assert isinstance(obj.data, bpy.types.Mesh) # NOTE: using Data since bmesh update will hapen very often @@ -559,7 +560,7 @@ class AddRoof(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) # rejecting original roof shape to be safe @@ -607,7 +608,8 @@ class EnableEditingRoof(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMRoofProperties + assert obj + props = tool.Model.get_roof_props(obj) data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")["data_dict"] # required since we could load pset from .ifc and BIMRoofProperties won't be set props.set_props_kwargs_from_ifc_data(data) @@ -624,7 +626,7 @@ class CancelEditingRoof(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object assert obj data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")["data_dict"] - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) # restore previous settings since editing was canceled props.set_props_kwargs_from_ifc_data(data) @@ -642,7 +644,7 @@ class FinishEditingRoof(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object element = tool.Ifc.get_entity(obj) - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) pset_data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof") path_data = pset_data["data_dict"]["path_data"] @@ -666,7 +668,7 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object assert obj [o.select_set(False) for o in context.selected_objects if o != obj] - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) data = tool.Model.get_modeling_bbim_pset_data(obj, "BBIM_Roof")["data_dict"] # required since we could load pset from .ifc and BIMRoofProperties won't be set props.set_props_kwargs_from_ifc_data(data) @@ -720,7 +722,7 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): def cancel_editing_roof_path(context: bpy.types.Context) -> set[str]: obj = context.active_object assert obj - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) ProfileDecorator.uninstall() props.is_editing_path = False @@ -751,7 +753,8 @@ class CopyRoofParameters(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): source_obj = context.active_object - source_props = source_obj.BIMRoofProperties + assert source_obj + source_props = tool.Model.get_roof_props(source_obj) data = source_props.get_general_kwargs(convert_to_project_units=True) for target_obj in context.selected_objects: @@ -763,7 +766,7 @@ class CopyRoofParameters(bpy.types.Operator, tool.Ifc.Operator): continue data["path_data"] = RoofData.data["path_data"] target_element = tool.Ifc.get_entity(target_obj) - target_props = target_obj.BIMRoofProperties + target_props = tool.Model.get_roof_props(target_obj) target_props.set_props_kwargs_from_ifc_data(data) update_bbim_roof_pset(target_element, data) @@ -783,7 +786,7 @@ class FinishEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object element = tool.Ifc.get_entity(obj) - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) bm = tool.Blender.get_bmesh_for_mesh(obj.data) op_status, error_message = is_valid_roof_footprint(bm) @@ -815,9 +818,12 @@ class RemoveRoof(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - obj.BIMRoofProperties.is_editing = False + props = tool.Model.get_roof_props(obj) + props.is_editing = False + assert element pset = tool.Pset.get_element_pset(element, "BBIM_Roof") ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/model/stair.py b/src/bonsai/bonsai/bim/module/model/stair.py index 8abd07b838..e325497525 100644 --- a/src/bonsai/bonsai/bim/module/model/stair.py +++ b/src/bonsai/bonsai/bim/module/model/stair.py @@ -34,7 +34,8 @@ from bpy_extras.object_utils import AddObjectHelper, object_data_add def regenerate_stair_mesh(obj: bpy.types.Object) -> None: - props_kwargs = obj.BIMStairProperties.get_props_kwargs() + props = tool.Model.get_stair_props(obj) + props_kwargs = props.get_props_kwargs() vertices, edges, faces = tool.Model.generate_stair_2d_profile(**props_kwargs) bm = bmesh.new() @@ -69,7 +70,8 @@ def update_ifc_stair_props(obj: bpy.types.Object) -> None: since it's going to update ifc representation """ element = tool.Ifc.get_entity(obj) - props = obj.BIMStairProperties + assert element + props = tool.Model.get_stair_props(obj) ifc_file = tool.Ifc.get() if tool.Ifc.get_schema() != "IFC2X3" and element.is_a("IfcStairFlight"): @@ -183,7 +185,8 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMStairProperties + assert element + props = tool.Model.get_stair_props(obj) ifc_file = tool.Ifc.get() stair_data = props.get_props_kwargs(convert_to_project_units=True) @@ -214,9 +217,11 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) + assert element data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Stair", "Data")) - props = obj.BIMStairProperties + props = tool.Model.get_stair_props(obj) # restore previous settings since editing was canceled props.set_props_kwargs_from_ifc_data(data) regenerate_stair_mesh(obj) @@ -233,8 +238,10 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMStairProperties + assert element + props = tool.Model.get_stair_props(obj) data = props.get_props_kwargs(convert_to_project_units=True) props.is_editing = False @@ -257,7 +264,8 @@ class EnableEditingStair(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMStairProperties + assert obj + props = tool.Model.get_stair_props(obj) element = tool.Ifc.get_entity(obj) data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Stair", "Data")) # required since we could load pset from .ifc and BIMStairProperties won't be set @@ -273,9 +281,11 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMStairProperties + assert obj + props = tool.Model.get_stair_props(obj) element = tool.Ifc.get_entity(obj) - obj.BIMStairProperties.is_editing = False + assert element + props.is_editing = False pset = tool.Pset.get_element_pset(element, "BBIM_Stair") ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 3374b0695d..688e7abf42 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -217,7 +217,9 @@ class BIM_PT_array(bpy.types.Panel): if not ArrayData.is_loaded: ArrayData.load() - props = context.active_object.BIMArrayProperties + obj = context.active_object + assert obj + props = tool.Model.get_array_props(obj) if ArrayData.data["parameters"]: row = self.layout.row(align=True) @@ -291,7 +293,7 @@ class BIM_PT_stair(bpy.types.Panel): obj = context.active_object assert obj - props = obj.BIMStairProperties + props = tool.Model.get_stair_props(obj) if StairData.data["pset_data"]: row = self.layout.row(align=True) @@ -404,7 +406,9 @@ class BIM_PT_window(bpy.types.Panel): if not WindowData.is_loaded: WindowData.load() - props = context.active_object.BIMWindowProperties + obj = context.active_object + assert obj + props = tool.Model.get_window_props(obj) if WindowData.data["pset_data"]: row = self.layout.row(align=True) @@ -594,7 +598,9 @@ class BIM_PT_railing(bpy.types.Panel): if not RailingData.is_loaded: RailingData.load() - props = context.active_object.BIMRailingProperties + obj = context.active_object + assert obj + props = tool.Model.get_railing_props(obj) if RailingData.data["pset_data"]: row = self.layout.row(align=True) @@ -659,7 +665,7 @@ class BIM_PT_roof(bpy.types.Panel): obj = context.active_object assert obj - props = obj.BIMRoofProperties + props = tool.Model.get_roof_props(obj) if RoofData.data["pset_data"]: row = self.layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/model/window.py b/src/bonsai/bonsai/bim/module/model/window.py index e940ec194d..e005a1b3ac 100644 --- a/src/bonsai/bonsai/bim/module/model/window.py +++ b/src/bonsai/bonsai/bim/module/model/window.py @@ -41,8 +41,10 @@ V_ = tool.Blender.V_ def update_window_modifier_representation(context: bpy.types.Context) -> None: obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMWindowProperties + assert element + props = tool.Model.get_window_props(obj) ifc_file = tool.Ifc.get() si_conversion = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) @@ -258,7 +260,8 @@ def create_bm_window( def update_window_modifier_bmesh(context: bpy.types.Context) -> None: obj = context.active_object - props = obj.BIMWindowProperties + assert obj + props = tool.Model.get_window_props(obj) panel_schema = DEFAULT_PANEL_SCHEMAS[props.window_type] accumulated_height = [0] * len(panel_schema[0]) built_panels = [] @@ -447,8 +450,10 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMWindowProperties + assert element + props = tool.Model.get_window_props(obj) window_data = props.get_general_kwargs(convert_to_project_units=True) lining_props = props.get_lining_kwargs(convert_to_project_units=True) @@ -477,11 +482,13 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) + assert element data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Window", "Data")) data.update(data.pop("lining_properties")) data.update(data.pop("panel_properties")) - props = obj.BIMWindowProperties + props = tool.Model.get_window_props(obj) props.set_props_kwargs_from_ifc_data(data) body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") @@ -506,8 +513,10 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object + assert obj element = tool.Ifc.get_entity(obj) - props = obj.BIMWindowProperties + assert element + props = tool.Model.get_window_props(obj) window_data = props.get_general_kwargs(convert_to_project_units=True) lining_props = props.get_lining_kwargs(convert_to_project_units=True) @@ -533,8 +542,10 @@ class EnableEditingWindow(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMWindowProperties + assert obj + props = tool.Model.get_window_props(obj) element = tool.Ifc.get_entity(obj) + assert element data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Window", "Data")) data.update(data.pop("lining_properties")) data.update(data.pop("panel_properties")) @@ -553,9 +564,11 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMWindowProperties + assert obj element = tool.Ifc.get_entity(obj) - obj.BIMWindowProperties.is_editing = False + assert element + props = tool.Model.get_window_props(obj) + props.is_editing = False pset = tool.Pset.get_element_pset(element, "BBIM_Window") ifcopenshell.api.run("pset.remove_pset", tool.Ifc.get(), product=element, pset=pset) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index b35af2b5c9..d1ea3ec9ea 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1046,6 +1046,7 @@ class EditObjectUI: @classmethod def draw_modes(cls, context: bpy.types.Context) -> None: + obj = context.active_object ui_context = str(context.region.type) row = cls.layout.row(align=True) row.separator() @@ -1055,9 +1056,7 @@ class EditObjectUI: if len(context.selected_objects) == 1 and AuthoringData.data["has_extrusion"]: row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator(row, "Edit Profile", "S_E", "", ui_context) - elif ( - tool.Model.is_parametric_railing_active() and not context.active_object.BIMRailingProperties.is_editing_path - ): + elif tool.Model.is_parametric_railing_active() and not tool.Model.get_railing_props(obj).is_editing_path: row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row row.operator( "bim.enable_editing_railing_path", @@ -1208,12 +1207,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): # and it might conflict with one of the conditions below if ( tool.Model.is_parametric_railing_active() - and not bpy.context.active_object.BIMRailingProperties.is_editing_path + and not tool.Model.get_railing_props(active_object).is_editing_path ): bpy.ops.bim.enable_editing_railing_path() return - elif tool.Model.is_parametric_roof_active() and not bpy.context.active_object.BIMRoofProperties.is_editing_path: + elif tool.Model.is_parametric_roof_active() and not tool.Model.get_roof_props(active_object).is_editing_path: # undo the unselection done above because roof has no usage type bpy.ops.bim.enable_editing_roof_path() return diff --git a/src/bonsai/bonsai/bim/module/nest/prop.py b/src/bonsai/bonsai/bim/module/nest/prop.py index 67fed8fe2d..d5e48b2479 100644 --- a/src/bonsai/bonsai/bim/module/nest/prop.py +++ b/src/bonsai/bonsai/bim/module/nest/prop.py @@ -40,7 +40,7 @@ def update_relating_object(self, context): if self.relating_object is None: return - if not self.relating_object.BIMObjectProperties.ifc_definition_id: + if not tool.Blender.get_ifc_definition_id(self.relating_object): context.window_manager.popup_menu(message, title="Invalid Element Selected", icon="INFO") self.relating_object = None diff --git a/src/bonsai/bonsai/bim/module/nest/ui.py b/src/bonsai/bonsai/bim/module/nest/ui.py index 1fd8f5ca4a..601fedf098 100644 --- a/src/bonsai/bonsai/bim/module/nest/ui.py +++ b/src/bonsai/bonsai/bim/module/nest/ui.py @@ -32,14 +32,14 @@ class BIM_PT_nest(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False - if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcObjectDefinition"): + if not tool.Ifc.get().by_id(ifc_id).is_a("IfcObjectDefinition"): return False return True @@ -55,7 +55,7 @@ class BIM_PT_nest(Panel): row.prop(props, "relating_object", text="") if props.relating_object: op = row.operator("bim.nest_assign_object", icon="CHECKMARK", text="") - op.relating_object = props.relating_object.BIMObjectProperties.ifc_definition_id + op.relating_object = tool.Blender.get_ifc_definition_id(props.relating_object) row.operator("bim.disable_editing_nest", icon="CANCEL", text="") else: row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index b91e0d8158..acc363bf16 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2327,11 +2327,12 @@ class RefreshClippingPlanes(bpy.types.Operator): else: break - def is_moved(self, obj): - if not obj.BIMObjectProperties.location_checksum: + def is_moved(self, obj: bpy.types.Object) -> bool: + props = tool.Blender.get_object_bim_props(obj) + if not props.location_checksum: return True # Let's be conservative - loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum)) - rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum)) + loc_check = np.frombuffer(eval(props.location_checksum)) + rot_check = np.frombuffer(eval(props.rotation_checksum)) loc_real = np.array(obj.matrix_world.translation).flatten() rot_real = np.array(obj.matrix_world.to_3x3()).flatten() if np.allclose(loc_check, loc_real, atol=1e-4) and np.allclose(rot_check, rot_real, atol=1e-2): diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py index 691d2e6930..e7a1301c6f 100644 --- a/src/bonsai/bonsai/bim/module/pset/operator.py +++ b/src/bonsai/bonsai/bim/module/pset/operator.py @@ -351,10 +351,9 @@ class BIM_OT_add_edit_custom_property(bpy.types.Operator, tool.Ifc.Operator): props = context.scene.AddEditProperties for obj in tool.Blender.get_selected_objects(): - ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id - if not ifc_definition_id: + ifc_element = tool.Ifc.get_entity(obj) + if not ifc_element: continue - ifc_element = tool.Ifc.get().by_id(ifc_definition_id) for prop in props: value = getattr(prop, prop.get_value_name()) @@ -406,23 +405,19 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator): props = context.scene.DeletePsets for obj in tool.Blender.get_selected_objects(): - ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id - if not ifc_definition_id: + ifc_element = tool.Ifc.get_entity(obj) + if not ifc_element: continue - ifc_element = tool.Ifc.get().by_id(ifc_definition_id) psets = ifcopenshell.util.element.get_psets(ifc_element) for prop in props: pset = prop.pset_name if pset in psets: try: - ifcopenshell.api.run( - "pset.remove_pset", + ifcopenshell.api.pset.remove_pset( self.file, - **{ - "product": self.file.by_id(ifc_definition_id), - "pset": self.file.by_id(psets[pset]["id"]), - }, + product=ifc_element, + pset=self.file.by_id(psets[pset]["id"]), ) except KeyError: pass # Sometimes the pset id is not found, I'm not sure why this happens though. - vulevukusej diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index 4adda725dc..c155937da9 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -241,12 +241,12 @@ class BIM_PT_object_psets(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False return True @@ -319,12 +319,12 @@ class BIM_PT_object_qtos(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False return True diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index 871b128db6..24db92e11f 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -648,7 +648,8 @@ def get_opening_area( """ total_opening_area = 0 ifc = tool.Ifc.get() - ifc_element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id) + ifc_element = tool.Ifc.get_entity(obj) + assert ifc_element if len(openings := ifc_element.HasOpenings) != 0: for opening in openings: opening_id = opening.RelatedOpeningElement.GlobalId @@ -887,7 +888,7 @@ def get_OBB_object(obj: bpy.types.Object) -> bpy.types.Object: :param blender-object obj: Blender Object :return blender-object: OBB of the Object """ - ifc_id = obj.BIMObjectProperties.ifc_definition_id + ifc_id = tool.Blender.get_ifc_definition_id(obj) bbox = obj.bound_box # matrix transformation to go from obj coordinates to world coordinates: obb = [Vector(v) for v in bbox] @@ -929,7 +930,7 @@ def get_AABB_object(obj: bpy.types.Object) -> bpy.types.Object: :param blender-object obj: Blender Object :return blender-object: AABB of the Object """ - ifc_id = obj.BIMObjectProperties.ifc_definition_id + ifc_id = tool.Blender.get_ifc_definition_id(obj) aabb_mesh = bpy.data.meshes.new(f"OBB_{ifc_id}") x = [v.co.x for v in obj.data.vertices] @@ -994,7 +995,7 @@ def get_bisected_obj( :param tuple(x,y,z) plane_no_neg: Tuple describing the normal vector of the lower bisection plane. Example: (0,0,-1) :return _type_: _description_ """ - ifc_id = obj.BIMObjectProperties.ifc_definition_id + ifc_id = tool.Blender.get_ifc_definition_id(obj) bis_obj = obj.copy() bis_obj.data = obj.data.copy() diff --git a/src/bonsai/bonsai/bim/module/qto/helper.py b/src/bonsai/bonsai/bim/module/qto/helper.py index 7265aa4400..47fd42642e 100644 --- a/src/bonsai/bonsai/bim/module/qto/helper.py +++ b/src/bonsai/bonsai/bim/module/qto/helper.py @@ -18,6 +18,7 @@ import bpy import bmesh +import bonsai.tool as tool from typing import Callable @@ -86,7 +87,7 @@ def calculate_formwork_area(objs: list[bpy.types.Object], context: bpy.types.Con bpy.ops.object.modifier_apply(modifier="Boolean") copied_obj.name = "Formwork" - copied_obj.BIMObjectProperties.ifc_definition_id = 0 + tool.Blender.get_object_bim_props(copied_obj).ifc_definition_id = 0 modifier = copied_obj.modifiers.new("Formwork", "REMESH") assert isinstance(modifier, bpy.types.RemeshModifier) modifier.mode = "SHARP" diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index a699287365..0e583e76cd 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -48,7 +48,8 @@ class EnableReassignClass(bpy.types.Operator): element = tool.Ifc.get_entity(obj) assert element ifc_class = element.is_a() - context.active_object.BIMObjectProperties.is_reassigning_class = True + props = tool.Blender.get_object_bim_props(obj) + props.is_reassigning_class = True ifc_products = tool.Root.get_ifc_products() schema = tool.Ifc.schema() declaration = schema.declaration_by_name(ifc_class) @@ -58,10 +59,10 @@ class EnableReassignClass(bpy.types.Operator): break else: self.report({"ERROR"}, f"Couldn't find matching IFC product for the selected object: '{element}'.") - obj.BIMObjectProperties.is_reassigning_class = False + props.is_reassigning_class = False return {"CANCELLED"} - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = self.file.by_id(tool.Blender.get_ifc_definition_id(obj)) rprops.ifc_class = element.is_a() rprops.relating_class_object = None if hasattr(element, "PredefinedType"): @@ -78,7 +79,8 @@ class DisableReassignClass(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.active_object.BIMObjectProperties.is_reassigning_class = False + props = tool.Blender.get_object_bim_props(context.active_object) + props.is_reassigning_class = False return {"FINISHED"} @@ -127,7 +129,8 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator): ) return {"CANCELLED"} - obj.BIMObjectProperties.is_reassigning_class = False + props = tool.Blender.get_object_bim_props(obj) + props.is_reassigning_class = False if element.is_a("IfcTypeObject"): elements_to_reassign[element] = ifc_class elements_to_update.update(ifcopenshell.util.element.get_types(element)) diff --git a/src/bonsai/bonsai/bim/module/root/ui.py b/src/bonsai/bonsai/bim/module/root/ui.py index 6985bf832b..5e9fa73eb0 100644 --- a/src/bonsai/bonsai/bim/module/root/ui.py +++ b/src/bonsai/bonsai/bim/module/root/ui.py @@ -43,7 +43,9 @@ class BIM_PT_class(Panel): def draw(self, context): if not IfcClassData.is_loaded: IfcClassData.load() - props = context.active_object.BIMObjectProperties + obj = context.active_object + assert obj + props = tool.Blender.get_object_bim_props(obj) rprops = tool.Root.get_root_props() if props.ifc_definition_id: if not IfcClassData.data["has_entity"]: diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 5f5854edd2..8e75097993 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -1449,15 +1449,17 @@ class LoadProductTasks(bpy.types.Operator): @classmethod def poll(cls, context): - if not tool.Ifc.get() or not (obj := context.active_object) or not (obj.BIMObjectProperties.ifc_definition_id): + if not tool.Ifc.get() or not (obj := context.active_object) or not (tool.Blender.get_ifc_definition_id(obj)): cls.poll_message_set("No IFC object is active.") return False return True def execute(self, context): - result = core.load_product_related_tasks( - tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id) - ) + obj = context.active_object + assert obj + product = tool.Ifc.get_entity(obj) + assert product + result = core.load_product_related_tasks(tool.Sequence, product=product) if isinstance(result, str): self.report({"INFO"}, result) else: diff --git a/src/bonsai/bonsai/bim/module/sequence/prop.py b/src/bonsai/bonsai/bim/module/sequence/prop.py index ce0e307076..a63a4928c0 100644 --- a/src/bonsai/bonsai/bim/module/sequence/prop.py +++ b/src/bonsai/bonsai/bim/module/sequence/prop.py @@ -271,10 +271,10 @@ def update_sort_reversed(self, context): def update_filter_by_active_schedule(self, context): - if context.active_object: - core.load_product_related_tasks( - tool.Sequence, product=tool.Ifc.get().by_id(context.active_object.BIMObjectProperties.ifc_definition_id) - ) + if obj := context.active_object: + product = tool.Ifc.get_entity(obj) + assert product + core.load_product_related_tasks(tool.Sequence, product=product) def switch_options(self, context): diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index ba08227179..c483e9d236 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -75,11 +75,13 @@ class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties + assert obj + oprops = tool.Blender.get_object_bim_props(obj) props = obj.BIMStructuralProperties file = tool.Ifc.get() related_structural_connection = file.by_id(oprops.ifc_definition_id) - relating_structural_member = file.by_id(props.relating_structural_member.BIMObjectProperties.ifc_definition_id) + relating_structural_member = tool.Ifc.get_entity(props.relating_structural_member) + assert relating_structural_member if not relating_structural_member.is_a("IfcStructuralMember"): return {"FINISHED"} ifcopenshell.api.structural.add_structural_member_connection( @@ -99,7 +101,6 @@ class EnableEditingStructuralConnectionCondition(bpy.types.Operator): def execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties props = obj.BIMStructuralProperties props.active_connects_structural_member = self.connects_structural_member return {"FINISHED"} @@ -350,7 +351,8 @@ class EnableEditingStructuralItemAxis(bpy.types.Operator): def execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties + assert obj + oprops = tool.Blender.get_object_bim_props(obj) props = obj.BIMStructuralProperties self.file = tool.Ifc.get() @@ -403,7 +405,8 @@ class EditStructuralItemAxis(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties + assert obj + oprops = tool.Blender.get_object_bim_props(obj) props = obj.BIMStructuralProperties relative_matrix = props.axis_empty.matrix_world @ obj.matrix_world.inverted() z_axis = relative_matrix.col[2][0:3] @@ -425,11 +428,12 @@ class EnableEditingStructuralConnectionCS(bpy.types.Operator): def execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties + assert obj props = obj.BIMStructuralProperties self.file = tool.Ifc.get() - item = self.file.by_id(oprops.ifc_definition_id) + item = tool.Ifc.get_entity(obj) + assert item location = obj.data.vertices[0].co empty = bpy.data.objects.new("Item Connection CS", None) @@ -491,16 +495,17 @@ class EditStructuralConnectionCS(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - oprops = obj.BIMObjectProperties + assert obj + item = tool.Ifc.get_entity(obj) + assert item props = obj.BIMStructuralProperties relative_matrix = props.ccs_empty.matrix_world @ obj.matrix_world.inverted() x_axis = relative_matrix.col[0][0:3] z_axis = relative_matrix.col[2][0:3] self.file = tool.Ifc.get() - ifcopenshell.api.run( - "structural.edit_structural_connection_cs", + ifcopenshell.api.structural.edit_structural_connection_cs( self.file, - structural_item=self.file.by_id(oprops.ifc_definition_id), + structural_item=item, axis=z_axis, ref_direction=x_axis, ) @@ -695,9 +700,9 @@ class AddStructuralActivity(bpy.types.Operator, tool.Ifc.Operator): self.props = context.scene.BIMStructuralProperties self.file = tool.Ifc.get() for obj in context.selected_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + element = tool.Ifc.get_entity(obj) + if not element: continue - element = self.file.by_id(obj.BIMObjectProperties.ifc_definition_id) applied_load_class = self.props.applicable_structural_load_types allowed_load_classes = { diff --git a/src/bonsai/bonsai/bim/module/structural/ui.py b/src/bonsai/bonsai/bim/module/structural/ui.py index d8f69e232e..1e4ceee997 100644 --- a/src/bonsai/bonsai/bim/module/structural/ui.py +++ b/src/bonsai/bonsai/bim/module/structural/ui.py @@ -85,14 +85,14 @@ class BIM_PT_structural_boundary_conditions(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not (tool.Ifc.get_object_by_identifier(ifc_id)): return False - if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(ifc_id).is_a("IfcStructuralConnection"): return False return True @@ -120,14 +120,14 @@ class BIM_PT_connected_structural_members(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not (tool.Ifc.get_object_by_identifier(ifc_id)): return False - if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(ifc_id).is_a("IfcStructuralConnection"): return False return True @@ -173,14 +173,14 @@ class BIM_PT_structural_member(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False - if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralMember"): + if not tool.Ifc.get().by_id(ifc_id).is_a("IfcStructuralMember"): return False return True @@ -216,14 +216,14 @@ class BIM_PT_structural_connection(Panel): @classmethod def poll(cls, context): - if not context.active_object: + if not (obj := context.active_object): return False - props = context.active_object.BIMObjectProperties - if not props.ifc_definition_id: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id: return False - if not tool.Ifc.get_object_by_identifier(props.ifc_definition_id): + if not tool.Ifc.get_object_by_identifier(ifc_id): return False - if not tool.Ifc.get().by_id(props.ifc_definition_id).is_a("IfcStructuralConnection"): + if not tool.Ifc.get().by_id(ifc_id).is_a("IfcStructuralConnection"): return False return True @@ -315,7 +315,6 @@ class BIM_UL_structural_analysis_models(UIList): row.label(text=item.name) if context.active_object: - oprops = context.active_object.BIMObjectProperties if item.ifc_definition_id in StructuralAnalysisModelsData.data["active_model_ids"]: op = row.operator( "bim.unassign_structural_analysis_model", text="", icon="KEYFRAME_HLT", emboss=False diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index b09181c327..0d500caa15 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -187,9 +187,8 @@ class BIM_PT_ports(Panel): if connected_obj_name: connected_obj = bpy.data.objects[connected_obj_name] cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port.id() - cols[4].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( - connected_obj.BIMObjectProperties.ifc_definition_id - ) + ifc_id = tool.Blender.get_ifc_definition_id(connected_obj) + cols[4].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id cols[5].label(text=connected_obj_name) else: cols[3].label(text="", icon="UNLINKED") @@ -244,9 +243,8 @@ class BIM_PT_port(Panel): relating_object = bpy.data.objects[relating_object_name] row.label(text="Port located on:") row.label(text=relating_object_name) - row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( - relating_object.BIMObjectProperties.ifc_definition_id - ) + ifc_id = tool.Blender.get_ifc_definition_id(relating_object) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id # object connected to the port row = layout.row(align=True) @@ -255,9 +253,8 @@ class BIM_PT_port(Panel): connected_object = bpy.data.objects[connected_object_name] row.label(text="Port connected to:") row.label(text=connected_object_name) - row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( - connected_object.BIMObjectProperties.ifc_definition_id - ) + ifc_id = tool.Blender.get_ifc_definition_id(connected_object) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id else: row.label(text="Port is not connected to any element") @@ -304,9 +301,8 @@ class BIM_PT_flow_controls(Panel): op.flow_control = control_id op.flow_element = flow_element_id op.assign = False - row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ( - displayed_object.BIMObjectProperties.ifc_definition_id - ) + ifc_id = tool.Blender.get_ifc_definition_id(displayed_object) + row.operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id row.label(text=f"{displayed_object_name}") element = tool.Ifc.get_entity(context.active_object) diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index b16abd3fcd..a438bdcfb5 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -150,7 +150,8 @@ class SelectRequirement(bpy.types.Operator): area.spaces[0].shading.show_xray = True failed_ids = [e["id"] for e in failed_entities] for obj in context.scene.objects: - if obj.BIMObjectProperties.ifc_definition_id in failed_ids: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if ifc_id in failed_ids: obj.color = (1, 0, 0, 1) else: obj.color = (1, 1, 1, 1) @@ -175,7 +176,8 @@ class SelectFailedEntities(bpy.types.Operator): failed_ids = [e["id"] for e in failed_entities] for obj in context.scene.objects: - if obj.BIMObjectProperties.ifc_definition_id in failed_ids: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if ifc_id in failed_ids: obj.select_set(True) else: obj.select_set(False) diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index ee370e5ae5..89ac192a65 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -337,5 +337,5 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): # Set duplicated type as active in current tool. if ifc_class in (i[0] for i in (bonsai.bim.helper.get_enum_items(props, "ifc_class", context) or ()) if i): props.ifc_class = new.is_a() - props.relating_type_id = str(new_obj.BIMObjectProperties.ifc_definition_id) + props.relating_type_id = str(tool.Blender.get_ifc_definition_id(new_obj)) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/type/ui.py b/src/bonsai/bonsai/bim/module/type/ui.py index 405507f168..340662cb3a 100644 --- a/src/bonsai/bonsai/bim/module/type/ui.py +++ b/src/bonsai/bonsai/bim/module/type/ui.py @@ -46,7 +46,9 @@ class BIM_PT_type(Panel): if not TypeData.is_loaded: TypeData.load() - oprops = context.active_object.BIMObjectProperties + obj = context.active_object + assert obj + oprops = tool.Blender.get_object_bim_props(obj) if TypeData.data["is_product"]: self.draw_product_ui(context) @@ -54,21 +56,19 @@ class BIM_PT_type(Panel): self.draw_type_ui(context) def draw_type_ui(self, context): - props = context.active_object.BIMTypeProperties - oprops = context.active_object.BIMObjectProperties + oprops = tool.Blender.get_object_bim_props(context.active_object) row = self.layout.row(align=True) row.label(text=f"{TypeData.data['total_instances']} Typed Objects") select_type_objects_row = row.row(align=True) select_type_objects_row.operator("bim.select_type_objects", icon="RESTRICT_SELECT_OFF", text="") select_type_objects_row.enabled = int(TypeData.data["total_instances"]) > 0 op = row.operator("bim.duplicate_type", icon="DUPLICATE", text="") - op.element = context.active_object.BIMObjectProperties.ifc_definition_id + op.element = oprops.ifc_definition_id row.operator("bim.auto_rename_occurrences", icon="ITALIC", text="") def draw_product_ui(self, context): layout = self.layout props = context.active_object.BIMTypeProperties - oprops = context.active_object.BIMObjectProperties if props.is_editing_type: row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/void/operator.py b/src/bonsai/bonsai/bim/module/void/operator.py index 91537ed528..3274b974d7 100644 --- a/src/bonsai/bonsai/bim/module/void/operator.py +++ b/src/bonsai/bonsai/bim/module/void/operator.py @@ -215,8 +215,8 @@ class AddFilling(bpy.types.Operator, tool.Ifc.Operator): if opening is None: return {"FINISHED"} self.file = tool.Ifc.get() - element_id = obj.BIMObjectProperties.ifc_definition_id - opening_id = opening.BIMObjectProperties.ifc_definition_id + element_id = tool.Blender.get_object_bim_props(obj).ifc_definition_id + opening_id = tool.Blender.get_object_bim_props(opening).ifc_definition_id if not element_id or not opening_id or element_id == opening_id: return {"FINISHED"} ifcopenshell.api.run( diff --git a/src/bonsai/bonsai/bim/module/void/ui.py b/src/bonsai/bonsai/bim/module/void/ui.py index 69b34a40e1..66701510e3 100644 --- a/src/bonsai/bonsai/bim/module/void/ui.py +++ b/src/bonsai/bonsai/bim/module/void/ui.py @@ -52,8 +52,6 @@ class BIM_PT_voids(Panel): if not VoidsData.is_loaded: VoidsData.load() - props = context.active_object.BIMObjectProperties - if len(context.selected_objects) >= 2: row = self.layout.row(align=True) op = row.operator("bim.add_opening", icon="ADD", text="Add Opening") diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index f32cd2ef65..9b0d38c5ab 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -857,7 +857,9 @@ class FetchObjectPassport(bpy.types.Operator): def execute(self, context): # TODO: this is dead code, awaiting reimplementation. See #1222. - for reference in context.active_object.BIMObjectProperties.document_references: + obj = context.active_object + props = tool.Blender.get_object_bim_props(obj) + for reference in props.document_references: bim_props = tool.Blender.get_bim_props() reference = bim_props.document_references[reference.name] if reference.location[-6:] == ".blend": diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index a4a2ed4558..7a91cce678 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -41,7 +41,7 @@ from typing import Any, Optional, Union, Literal, Iterable, Callable, TypeVar, G from typing_extensions import assert_never if TYPE_CHECKING: - from bonsai.bim.prop import BIMProperties + from bonsai.bim.prop import BIMProperties, BIMObjectProperties VIEWPORT_ATTRIBUTES = [ @@ -192,7 +192,8 @@ class Blender(bonsai.core.tool.Blender): if context is None: context = bpy.context if obj_type == "Object": - return bpy.data.objects.get(obj).BIMObjectProperties.ifc_definition_id + props = tool.Blender.get_object_bim_props(bpy.data.objects[obj]) + return props.ifc_definition_id elif obj_type == "Material": props = tool.Material.get_material_props() return props.materials[props.active_material_index].ifc_definition_id @@ -221,7 +222,8 @@ class Blender(bonsai.core.tool.Blender): @classmethod def is_ifc_object(cls, obj: bpy.types.Object) -> bool: - return bool(obj.BIMObjectProperties.ifc_definition_id) + props = tool.Blender.get_object_bim_props(obj) + return bool(props.ifc_definition_id) @classmethod def is_ifc_class_active(cls, ifc_class: str) -> bool: @@ -880,7 +882,7 @@ class Blender(bonsai.core.tool.Blender): @classmethod def get_layer_collection(cls, collection: bpy.types.Collection) -> Union[bpy.types.LayerCollection, None]: project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - project_collection = project.BIMObjectProperties.collection + project_collection = tool.Blender.get_object_bim_props(project).collection for layer_collection in bpy.context.view_layer.layer_collection.children: if layer_collection.collection == project_collection: for layer_collection2 in layer_collection.children: @@ -1019,23 +1021,28 @@ class Blender(bonsai.core.tool.Blender): @classmethod def is_editing_railing_path(cls, obj: bpy.types.Object): - return obj.BIMRailingProperties.is_editing_path + props = tool.Model.get_railing_props(obj) + return props.is_editing_path @classmethod def is_editing_roof_path(cls, obj: bpy.types.Object) -> bool: - return obj.BIMRoofProperties.is_editing_path + props = tool.Model.get_roof_props(obj) + return props.is_editing_path @classmethod def is_editing_railing_parameters(cls, obj: bpy.types.Object) -> bool: - return obj.BIMRailingProperties.is_editing + props = tool.Model.get_railing_props(obj) + return props.is_editing @classmethod def is_editing_roof_parameters(cls, obj: bpy.types.Object) -> bool: - return obj.BIMRoofProperties.is_editing + props = tool.Model.get_roof_props(obj) + return props.is_editing @classmethod def is_editing_window_parameters(cls, obj: bpy.types.Object) -> bool: - return obj.BIMWindowProperties.is_editing + props = tool.Model.get_window_props(obj) + return props.is_editing @classmethod def is_editing_door_parameters(cls, obj: bpy.types.Object) -> bool: @@ -1044,7 +1051,8 @@ class Blender(bonsai.core.tool.Blender): @classmethod def is_editing_stair_parameters(cls, obj: bpy.types.Object) -> bool: - return obj.BIMStairProperties.is_editing + props = tool.Model.get_stair_props(obj) + return props.is_editing @classmethod def is_modifier_with_non_editable_path(cls, element: entity_instance) -> bool: @@ -1545,3 +1553,11 @@ class Blender(bonsai.core.tool.Blender): if scene is None: scene = bpy.context.scene return scene.BIMProperties + + @classmethod + def get_object_bim_props(cls, obj: bpy.types.Object) -> BIMObjectProperties: + return obj.BIMObjectProperties + + @classmethod + def get_ifc_definition_id(cls, obj: bpy.types.Object) -> int: + return tool.Blender.get_object_bim_props(obj).ifc_definition_id diff --git a/src/bonsai/bonsai/tool/cad.py b/src/bonsai/bonsai/tool/cad.py index 9812497967..8b2b34dae2 100644 --- a/src/bonsai/bonsai/tool/cad.py +++ b/src/bonsai/bonsai/tool/cad.py @@ -28,7 +28,7 @@ # - An arc is reconstructed from 3 points instead of a full circle # - You can now derive the center from an arc without generating geometry - +from __future__ import annotations import sys import bpy import math @@ -36,12 +36,20 @@ import bmesh import mathutils.geometry from mathutils import Vector, Matrix, geometry import itertools +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.cad.prop import BIMCadProperties VTX_PRECISION = 1.0e-5 class Cad: + @classmethod + def get_cad_props(cls) -> BIMCadProperties: + return bpy.context.scene.BIMCadProperties + @classmethod def is_point_on_edge(cls, p, edge): """ diff --git a/src/bonsai/bonsai/tool/collector.py b/src/bonsai/bonsai/tool/collector.py index 2f669fe32e..a93b5e405f 100644 --- a/src/bonsai/bonsai/tool/collector.py +++ b/src/bonsai/bonsai/tool/collector.py @@ -29,7 +29,7 @@ class Collector(bonsai.core.tool.Collector): """Links an object to an appropriate Blender collection.""" if should_clean_users_collection: for users_collection in obj.users_collection: - if obj.BIMObjectProperties.collection == users_collection: + if tool.Blender.get_object_bim_props(obj).collection == users_collection: continue # Users are free to use extra collections for their own # purposes except for the reserved keyword "Ifc" and @@ -87,7 +87,7 @@ class Collector(bonsai.core.tool.Collector): if collection := cls._create_own_collection(obj): cls.link_collection_object_safe(collection, obj) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - cls.link_collection_child_safe(project_obj.BIMObjectProperties.collection, collection) + cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection) elif ( tool.Ifc.get_schema() != "IFC2X3" and element.is_a("IfcSpatialElement") @@ -98,21 +98,21 @@ class Collector(bonsai.core.tool.Collector): if collection := cls._create_own_collection(obj): cls.link_collection_object_safe(collection, obj) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - cls.link_collection_child_safe(project_obj.BIMObjectProperties.collection, collection) + cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection) elif element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING": if collection := cls._create_own_collection(obj): cls.link_collection_object_safe(collection, obj) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - cls.link_collection_child_safe(project_obj.BIMObjectProperties.collection, collection) + cls.link_collection_child_safe(tool.Blender.get_object_bim_props(project_obj).collection, collection) elif element.is_a("IfcAnnotation") and (drawing_obj := cls.get_annotation_drawing_obj(element)): - cls.link_collection_object_safe(drawing_obj.BIMObjectProperties.collection, obj) + cls.link_collection_object_safe(tool.Blender.get_object_bim_props(drawing_obj).collection, obj) elif container := ifcopenshell.util.element.get_container(element): while container.is_a("IfcSpace"): container = ifcopenshell.util.element.get_aggregate(container) container_obj = tool.Ifc.get_object(container) - if not (collection := container_obj.BIMObjectProperties.collection): + if not (collection := tool.Blender.get_object_bim_props(container_obj).collection): cls.assign(container_obj) - collection = container_obj.BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(container_obj).collection cls.link_collection_object_safe(collection, obj) else: collection = cls._create_project_child_collection("Unsorted") @@ -128,7 +128,7 @@ class Collector(bonsai.core.tool.Collector): return collection collection = bpy.data.collections.new(name) project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - project_obj.BIMObjectProperties.collection.children.link(collection) + tool.Blender.get_object_bim_props(project_obj).collection.children.link(collection) if layer_collection := tool.Blender.get_layer_collection(collection): cls.set_layer_collection_visibility(layer_collection) return collection @@ -136,11 +136,12 @@ class Collector(bonsai.core.tool.Collector): @classmethod def _create_own_collection(cls, obj: bpy.types.Object) -> bpy.types.Collection: """get or create own collection for the element""" - if obj.BIMObjectProperties.collection: - obj.BIMObjectProperties.collection.name = obj.name + props = tool.Blender.get_object_bim_props(obj) + if props.collection: + props.collection.name = obj.name return collection = bpy.data.collections.new(obj.name) - obj.BIMObjectProperties.collection = collection + props.collection = collection collection.BIMCollectionProperties.obj = obj return collection @@ -184,7 +185,8 @@ class Collector(bonsai.core.tool.Collector): @classmethod def reset_default_visibility(cls) -> None: project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - project_collection = project.BIMObjectProperties.collection + assert project + project_collection = tool.Blender.get_object_bim_props(project).collection for layer_collection in bpy.context.view_layer.layer_collection.children: if layer_collection.collection == project_collection: for layer_collection2 in layer_collection.children: diff --git a/src/bonsai/bonsai/tool/covering.py b/src/bonsai/bonsai/tool/covering.py index 5141cbffbd..c83a6f3e09 100644 --- a/src/bonsai/bonsai/tool/covering.py +++ b/src/bonsai/bonsai/tool/covering.py @@ -16,17 +16,26 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import ifcopenshell import ifcopenshell.util.element import bonsai.core.tool import bonsai.tool as tool +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.covering.prop import BIMCoveringProperties class Covering(bonsai.core.tool.Covering): + @classmethod + def get_covering_props(cls) -> BIMCoveringProperties: + return bpy.context.scene.BIMCoveringProperties + @classmethod def get_z_from_ceiling_height(cls) -> float: - props = bpy.context.scene.BIMCoveringProperties + props = cls.get_covering_props() return props.ceiling_height # def toggle_spaces_visibility_wired_and_textured(cls, spaces): diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 9f72fd51ee..c223b3ee58 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -524,7 +524,7 @@ class Drawing(bonsai.core.tool.Drawing): def get_drawing_collection(cls, drawing: ifcopenshell.entity_instance) -> Union[bpy.types.Collection, None]: obj = tool.Ifc.get_object(drawing) if obj: - return obj.BIMObjectProperties.collection + return tool.Blender.get_object_bim_props(obj).collection @classmethod def get_drawing_group(cls, drawing: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: @@ -1516,8 +1516,8 @@ class Drawing(bonsai.core.tool.Drawing): dst = src.copy() dst.data = dst.data.copy() dst.name = dst.name.replace("IfcGridAxis/", "") - dst.BIMObjectProperties.ifc_definition_id = 0 - tool.Geometry.get_geometry_props(dst).ifc_definition_id = 0 + tool.Blender.get_object_bim_props(dst).ifc_definition_id = 0 + tool.Geometry.get_geometry_props(dst.data).ifc_definition_id = 0 return dst def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]: @@ -1888,7 +1888,7 @@ class Drawing(bonsai.core.tool.Drawing): return bool( camera is not None and camera.type == "CAMERA" - and camera.BIMObjectProperties.ifc_definition_id + and tool.Blender.get_ifc_definition_id(camera) and area is not None ) @@ -1910,11 +1910,12 @@ class Drawing(bonsai.core.tool.Drawing): def isolate_camera_collection(cls, camera: bpy.types.Object) -> None: drawings = [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"] drawing_collections = [] - camera_collection = camera.BIMObjectProperties.collection + camera_collection = tool.Blender.get_object_bim_props(camera).collection for drawing in drawings: if not (drawing_obj := tool.Ifc.get_object(drawing)): continue - if not (drawing_collection := drawing_obj.BIMObjectProperties.collection): + oprops = tool.Blender.get_object_bim_props(drawing_obj) + if not (drawing_collection := oprops.collection): continue if drawing_obj == camera: drawing_collection.hide_render = False @@ -1922,7 +1923,7 @@ class Drawing(bonsai.core.tool.Drawing): drawing_collection.hide_render = True project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - project_collection = project.BIMObjectProperties.collection + project_collection = tool.Blender.get_object_bim_props(project).collection for layer_collection in bpy.context.view_layer.layer_collection.children: if layer_collection.collection == project_collection: for layer_collection2 in layer_collection.children: diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 4947666bd6..56c8e275c6 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -235,7 +235,7 @@ class Geometry(bonsai.core.tool.Geometry): bpy.data.objects.remove(axis_obj) ifcopenshell.api.grid.remove_grid_axis(tool.Ifc.get(), axis=axis) - collection = obj.BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(obj).collection if collection: parent = ifcopenshell.util.element.get_aggregate(element) if not parent: @@ -243,7 +243,7 @@ class Geometry(bonsai.core.tool.Geometry): if parent: parent_obj = tool.Ifc.get_object(parent) if parent_obj: - parent_collection = parent_obj.BIMObjectProperties.collection + parent_collection = tool.Blender.get_object_bim_props(parent_obj).collection for child in collection.children: parent_collection.children.link(child) for child_object in collection.objects: @@ -557,11 +557,9 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def get_cartesian_point_offset(cls, obj: bpy.types.Object) -> npt.NDArray[np.float64] | None: - if ( - obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT" - and obj.BIMObjectProperties.cartesian_point_offset - ): - return np.array(tuple(map(float, obj.BIMObjectProperties.cartesian_point_offset.split(",")))) + props = tool.Blender.get_object_bim_props(obj) + if props.blender_offset_type == "CARTESIAN_POINT" and props.cartesian_point_offset: + return np.array(tuple(map(float, props.cartesian_point_offset.split(",")))) @classmethod def get_element_type(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: @@ -1084,8 +1082,9 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def record_object_position(cls, obj: bpy.types.Object) -> None: # These are recorded separately because they have different numerical tolerances - obj.BIMObjectProperties.location_checksum = repr(np.array(obj.matrix_world.translation).tobytes()) - obj.BIMObjectProperties.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes()) + props = tool.Blender.get_object_bim_props(obj) + props.location_checksum = repr(np.array(obj.matrix_world.translation).tobytes()) + props.rotation_checksum = repr(np.array(obj.matrix_world.to_3x3()).tobytes()) @classmethod def remove_connection(cls, connection: ifcopenshell.entity_instance) -> None: @@ -1570,8 +1569,9 @@ class Geometry(bonsai.core.tool.Geometry): def get_blender_offset_type(cls, obj: bpy.types.Object) -> Optional[str]: props = tool.Georeference.get_georeference_props() if props.has_blender_offset: - if (result := obj.BIMObjectProperties.blender_offset_type) == "NONE": - result = obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" + props = tool.Blender.get_object_bim_props(obj) + if (result := props.blender_offset_type) == "NONE": + result = props.blender_offset_type = "OBJECT_PLACEMENT" return result @classmethod diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index ce9f856f80..5f68fc4b0f 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -77,13 +77,14 @@ class Ifc(bonsai.core.tool.Ifc): return False if element and (element.is_a("IfcTypeProduct") or element.is_a("IfcProject")): return False - if not obj.BIMObjectProperties.location_checksum: + oprops = tool.Blender.get_object_bim_props(obj) + if not oprops.location_checksum: return True # Let's be conservative - loc_check = np.frombuffer(eval(obj.BIMObjectProperties.location_checksum)) + loc_check = np.frombuffer(eval(oprops.location_checksum)) loc_real = np.array(obj.matrix_world.translation).flatten() if not np.allclose(loc_check, loc_real, atol=1e-4): # 0.1 mm return True - rot_check = np.frombuffer(eval(obj.BIMObjectProperties.rotation_checksum)).reshape(3, 3) + rot_check = np.frombuffer(eval(oprops.rotation_checksum)).reshape(3, 3) rot_real = np.array(obj.matrix_world.to_3x3()) rot_dot = np.dot(rot_check, rot_real.T) angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1)) @@ -107,7 +108,7 @@ class Ifc(bonsai.core.tool.Ifc): props = None if isinstance(obj, bpy.types.Object): - props = obj.BIMObjectProperties + props = tool.Blender.get_object_bim_props(obj) elif isinstance(obj, bpy.types.Material): props = obj.BIMStyleProperties else: diff --git a/src/bonsai/bonsai/tool/ifcgit.py b/src/bonsai/bonsai/tool/ifcgit.py index 93475c090c..e306271ebd 100644 --- a/src/bonsai/bonsai/tool/ifcgit.py +++ b/src/bonsai/bonsai/tool/ifcgit.py @@ -397,9 +397,9 @@ class IfcGit: bpy.ops.object.select_all(action="DESELECT") for obj in bpy.context.visible_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + props = tool.Blender.get_object_bim_props(obj) + if not (step_id := props.ifc_definition_id): continue - step_id = obj.BIMObjectProperties.ifc_definition_id if step_id in step_ids["modified"]: obj.color = (0.3, 0.3, 1.0, 1) obj.select_set(True) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 7fb5fc4788..7a5576a1e2 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -929,6 +929,7 @@ class Loader(bonsai.core.tool.Loader): @classmethod def apply_blender_offset_to_matrix_world(cls, obj: bpy.types.Object, matrix: np.ndarray) -> Matrix: + oprops = tool.Blender.get_object_bim_props(obj) if ( not obj.data and tool.Cad.is_x(matrix[0][3], 0) @@ -939,13 +940,13 @@ class Loader(bonsai.core.tool.Loader): # positionally significant and is left alone. This handles # scenarios where often spatial elements are left at 0,0,0 and # everything else is at map coordinates. - obj.BIMObjectProperties.blender_offset_type = "NOT_APPLICABLE" + oprops.blender_offset_type = "NOT_APPLICABLE" return Matrix(matrix.tolist()) if obj.data and obj.data.get("has_cartesian_point_offset", None): - obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" + oprops.blender_offset_type = "CARTESIAN_POINT" if cartesian_point_offset := obj.data.get("cartesian_point_offset", None): - obj.BIMObjectProperties.cartesian_point_offset = cartesian_point_offset + oprops.cartesian_point_offset = cartesian_point_offset offset_xyz = list(map(float, cartesian_point_offset.split(","))) + [1.0] offset_xyz = matrix @ offset_xyz matrix[0][3] = offset_xyz[0] @@ -954,8 +955,8 @@ class Loader(bonsai.core.tool.Loader): props = tool.Georeference.get_georeference_props() if props.has_blender_offset: - if obj.BIMObjectProperties.blender_offset_type == "NONE": - obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" + if oprops.blender_offset_type == "NONE": + oprops.blender_offset_type = "OBJECT_PLACEMENT" matrix = ifcopenshell.util.geolocation.global2local( matrix, float(props.blender_offset_x) * cls.unit_scale, diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 0596971e53..27cb0180bf 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -55,7 +55,15 @@ T = TypeVar("T") V_ = tool.Blender.V_ if TYPE_CHECKING: - from bonsai.bim.module.model.prop import BIMModelProperties, BIMDoorProperties + from bonsai.bim.module.model.prop import ( + BIMModelProperties, + BIMDoorProperties, + BIMArrayProperties, + BIMRoofProperties, + BIMWindowProperties, + BIMStairProperties, + BIMRailingProperties, + ) class Model(bonsai.core.tool.Model): @@ -67,6 +75,26 @@ class Model(bonsai.core.tool.Model): def get_door_props(cls, obj: bpy.types.Object) -> BIMDoorProperties: return obj.BIMDoorProperties + @classmethod + def get_window_props(cls, obj: bpy.types.Object) -> BIMWindowProperties: + return obj.BIMWindowProperties + + @classmethod + def get_stair_props(cls, obj: bpy.types.Object) -> BIMStairProperties: + return obj.BIMStairProperties + + @classmethod + def get_roof_props(cls, obj: bpy.types.Object) -> BIMRoofProperties: + return obj.BIMRoofProperties + + @classmethod + def get_railing_props(cls, obj: bpy.types.Object) -> BIMRailingProperties: + return obj.BIMRailingProperties + + @classmethod + def get_array_props(cls, obj: bpy.types.Object) -> BIMArrayProperties: + return obj.BIMArrayProperties + @classmethod def convert_si_to_unit(cls, value: T) -> T: if isinstance(value, (tuple, list)): @@ -1989,9 +2017,9 @@ class Model(bonsai.core.tool.Model): if z < 0 and y < 0: y = abs(y) z = abs(z) - if z < 0 and y >=0: + if z < 0 and y >= 0: vector = Vector((0, -1)) - + x_angle = vector.angle_signed(Vector((y, z))) return x_angle diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index 6e92d8cf5a..4c824f8b62 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -409,7 +409,8 @@ class Root(bonsai.core.tool.Root): def set_object_name(cls, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None: name = tool.Loader.get_name(element) if obj.name != name: - obj.BIMObjectProperties.is_renaming = True + props = tool.Blender.get_object_bim_props(obj) + props.is_renaming = True obj.name = name # The handler will trigger, and reset is_renaming to False @classmethod diff --git a/src/bonsai/bonsai/tool/sequence.py b/src/bonsai/bonsai/tool/sequence.py index 4dd4cfbb99..046906387e 100644 --- a/src/bonsai/bonsai/tool/sequence.py +++ b/src/bonsai/bonsai/tool/sequence.py @@ -1158,7 +1158,7 @@ class Sequence(bonsai.core.tool.Sequence): bpy.context.scene.frame_start = 1 bpy.context.scene.frame_end = 2 for obj in bpy.data.objects: - if not obj.BIMObjectProperties.ifc_definition_id: + if not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): continue obj.color = (1.0, 1.0, 1.0, 1) obj.hide_viewport = False @@ -1317,7 +1317,7 @@ class Sequence(bonsai.core.tool.Sequence): @classmethod def clear_objects_animation(cls, include_blender_objects=True): for obj in bpy.data.objects: - if not include_blender_objects and not obj.BIMObjectProperties.ifc_definition_id: + if not include_blender_objects and not (ifc_id := tool.Blender.get_ifc_definition_id(obj)): continue cls.clear_object_animation(obj) cls.clear_object_color(obj) @@ -1326,13 +1326,14 @@ class Sequence(bonsai.core.tool.Sequence): @classmethod def animate_objects(cls, settings, frames, animation_type=""): for obj in bpy.data.objects: - if not obj.BIMObjectProperties.ifc_definition_id: + element = tool.Ifc.get_entity(obj) + if not element: continue - if tool.Ifc.get().by_id(obj.BIMObjectProperties.ifc_definition_id).is_a("IfcSpace"): + if element.is_a("IfcSpace"): cls.hide_object(obj) continue cls.earliest_frame = None - product_frames = frames.get(obj.BIMObjectProperties.ifc_definition_id, []) + product_frames = frames.get(element.id(), []) for product_frame in product_frames: if product_frame["relationship"] == "input": cls.animate_input(obj, settings["start_frame"], product_frame, animation_type) diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 2d400229c5..4e6f8dc457 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -1180,9 +1180,9 @@ class Spatial(bonsai.core.tool.Spatial): SpatialDecompositionData.data["default_container"] = SpatialDecompositionData.default_container() project = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0]) - project_collection = project.BIMObjectProperties.collection + project_collection = tool.Blender.get_object_bim_props(project).collection obj = tool.Ifc.get_object(container) - if obj and (collection := obj.BIMObjectProperties.collection): + if obj and (collection := tool.Blender.get_object_bim_props(obj).collection): for layer_collection in bpy.context.view_layer.layer_collection.children: if layer_collection.collection == project_collection: for layer_collection2 in layer_collection.children: diff --git a/src/bonsai/bonsai/tool/structural.py b/src/bonsai/bonsai/tool/structural.py index 30cbb00191..5796bf6f44 100644 --- a/src/bonsai/bonsai/tool/structural.py +++ b/src/bonsai/bonsai/tool/structural.py @@ -110,7 +110,8 @@ class Structural(bonsai.core.tool.Structural): def get_product_or_active_object(cls, product: str) -> Union[bpy.types.Object, None]: product = bpy.data.objects.get(product) if product else bpy.context.active_object try: - if product.BIMObjectProperties.ifc_definition_id: + props = tool.Blender.get_object_bim_props(product) + if props.ifc_definition_id: return product else: return None diff --git a/src/bonsai/bonsai/tool/surveyor.py b/src/bonsai/bonsai/tool/surveyor.py index 28856a19f8..7c203586f2 100644 --- a/src/bonsai/bonsai/tool/surveyor.py +++ b/src/bonsai/bonsai/tool/surveyor.py @@ -33,7 +33,7 @@ class Surveyor(bonsai.core.tool.Surveyor): M_TRANSLATION = (slice(0, 3), 3) matrix = np.array(obj.matrix_world) props = tool.Georeference.get_georeference_props() - if props.has_blender_offset and obj.BIMObjectProperties.blender_offset_type != "NOT_APPLICABLE": + if props.has_blender_offset and tool.Blender.get_object_bim_props(obj).blender_offset_type != "NOT_APPLICABLE": unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) coordinate_offset = tool.Geometry.get_cartesian_point_offset(obj) if coordinate_offset is not None: diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 51334be3a2..b7eae11989 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -189,7 +189,7 @@ class System(bonsai.core.tool.System): container = ifcopenshell.util.element.get_container(element) if container: - collection = tool.Ifc.get_object(container).BIMObjectProperties.collection + collection = tool.Blender.get_object_bim_props(tool.Ifc.get_object(container)).collection ifc_importer.collections[container.GlobalId] = collection ifc_importer.place_objects_in_collections() diff --git a/src/bonsai/test/bim/bootstrap.py b/src/bonsai/test/bim/bootstrap.py index 308d140298..6d997bdd7f 100644 --- a/src/bonsai/test/bim/bootstrap.py +++ b/src/bonsai/test/bim/bootstrap.py @@ -192,12 +192,12 @@ def the_object_name_does_not_exist(name): def the_object_name_is_an_ifc_class(name, ifc_class): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert element.is_a(ifc_class), f'Object "{name}" is an {element.is_a()}' def the_object_name_is_not_an_ifc_element(name): - id = the_object_name_exists(name).BIMObjectProperties.ifc_definition_id + id = tool.Blender.get_ifc_definition_id(the_object_name_exists(name)) assert id == 0, f"The ID is {id}" @@ -229,7 +229,7 @@ def the_object_name_is_placed_in_the_collection_collection(name, collection): def the_object_name_has_a_type_representation_of_context(name, type, context): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) context, subcontext, target_view = context.split("/") assert ifcopenshell.util.representation.get_representation( element, context, subcontext or None, target_view or None @@ -238,7 +238,7 @@ def the_object_name_has_a_type_representation_of_context(name, type, context): def the_object_name_is_contained_in_container_name(name, container_name): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) container = ifcopenshell.util.element.get_container(element) if not container: assert False, f'Object "{name}" is not in any container' @@ -257,8 +257,8 @@ def i_delete_the_selected_objects(): def the_object_name1_and_name2_are_different_elements(name1, name2): ifc = an_ifc_file_exists() - element1 = ifc.by_id(the_object_name_exists(name1).BIMObjectProperties.ifc_definition_id) - element2 = ifc.by_id(the_object_name_exists(name2).BIMObjectProperties.ifc_definition_id) + element1 = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name1))) + element2 = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name2))) assert element1 != element2, f"Objects {name1} and {name2} have same elements {element1} and {element2}" @@ -284,7 +284,7 @@ def the_object_name1_has_no_boolean_difference_by_name2(name1, name2): def the_object_name_is_voided_by_void(name, void): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) for rel in element.HasOpenings: if rel.RelatedOpeningElement.Name == void: return True @@ -293,7 +293,7 @@ def the_object_name_is_voided_by_void(name, void): def the_object_name_is_not_voided_by_void(name, void): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) for rel in element.HasOpenings: if rel.RelatedOpeningElement.Name == void: assert False, "A void was found" @@ -301,21 +301,21 @@ def the_object_name_is_not_voided_by_void(name, void): def the_object_name_is_not_voided(name): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) if any(element.HasOpenings): assert False, "An opening was found" def the_object_name_is_not_a_void(name): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) if any(element.VoidsElements): assert False, "A void was found" def the_void_name_is_filled_by_filling(name, filling): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings): return True assert False, "No filling found" @@ -323,14 +323,14 @@ def the_void_name_is_filled_by_filling(name, filling): def the_void_name_is_not_filled_by_filling(name, filling): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) if any(rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings): assert False, "A filling was found" def the_object_name_is_not_a_filling(name): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) if any(element.FillsVoids): assert False, "A filling was found" diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 1332c63e8e..2163489685 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -718,8 +718,8 @@ def the_collection_exclude_status_is(name: str, exclude: str) -> None: @then(parsers.parse('the object "{name1}" and "{name2}" are different elements')) def the_object_name1_and_name2_are_different_elements(name1, name2): ifc = an_ifc_file_exists() - element1 = ifc.by_id(the_object_name_exists(name1).BIMObjectProperties.ifc_definition_id) - element2 = ifc.by_id(the_object_name_exists(name2).BIMObjectProperties.ifc_definition_id) + element1 = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name1))) + element2 = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name2))) assert element1 != element2, f"Objects {name1} and {name2} have same elements {element1} and {element2}" @@ -732,7 +732,7 @@ def the_object_name_has_a_body_of_value(name, value): @then(parsers.parse('the object "{name}" has a "{type}" representation of "{context}"')) def the_object_name_has_a_representation_type_of_context(name, type, context): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) context, subcontext, target_view = context.split("/") rep = ifcopenshell.util.representation.get_representation(element, context, subcontext or None, target_view or None) assert rep @@ -808,7 +808,7 @@ def the_object_name_should_display_as_mode(name, mode): @then(parsers.parse('the object "{name}" is voided by "{void}"')) def the_object_name_is_voided_by_void(name, void): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert any((rel for rel in element.HasOpenings if rel.RelatedOpeningElement.Name == void)), "No void found" @@ -824,7 +824,7 @@ def the_object_name_is_not_voided_by_void(name, void): @then(parsers.parse('the object "{name}" is not voided')) def the_object_name_is_not_voided(name): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert not element.HasOpenings, "A void was found" @@ -832,7 +832,7 @@ def the_object_name_is_not_voided(name): def the_object_name_is_a_void(name): ifc = tool.Ifc.get() obj = the_object_name_exists(name) - element = ifc.by_id(obj.BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(obj)) assert any((element.VoidsElements)), "No void was found" @@ -872,14 +872,14 @@ def the_object_name_is_not_visible(name): @then(parsers.parse('the object "{name}" is an "{ifc_class}"')) def the_object_name_is_an_ifc_class(name, ifc_class): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert element.is_a(ifc_class), f'Object "{name}" is an {element.is_a()}' @then(parsers.parse('the object "{name}" is not an IFC element')) def the_object_name_is_not_an_ifc_element(name): obj = the_object_name_exists(name) - ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id + ifc_definition_id = tool.Blender.get_ifc_definition_id(obj) assert ifc_definition_id == 0, f"The object {obj} has an ID of {ifc_definition_id}" @@ -897,14 +897,14 @@ def the_object_name_has_ifc_representation_data(name): @then(parsers.parse('the material "{name}" is an IFC material')) def the_material_name_is_an_ifc_material(name): obj = the_material_name_exists(name) - ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id + ifc_definition_id = tool.Blender.get_ifc_definition_id(obj) assert ifc_definition_id != 0, f"The material {obj} has no ID: {ifc_definition_id}" @then(parsers.parse('the material "{name}" is not an IFC material')) def the_material_name_is_not_an_ifc_material(name): obj = the_material_name_exists(name) - ifc_definition_id = obj.BIMObjectProperties.ifc_definition_id + ifc_definition_id = tool.Blender.get_ifc_definition_id(obj) assert ifc_definition_id == 0, f"The material {obj} has an ID of {ifc_definition_id}" @@ -937,7 +937,7 @@ def the_object_name_has_number_vertices(name, number): @then(parsers.parse('the void "{name}" is filled by "{filling}"')) def the_void_name_is_filled_by_filling(name, filling): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert any((rel.RelatedBuildingElement.Name == filling for rel in element.HasFillings)), "No filling found" @@ -954,7 +954,7 @@ def the_void_name_is_not_filled_by_filling(name, filling): @then(parsers.parse('the object "{name}" is not a filling')) def the_object_name_is_not_a_filling(name): ifc = tool.Ifc.get() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) assert not any(element.FillsVoids), "A filling was found" @@ -1010,8 +1010,9 @@ def prop_is_roughly_value(prop, value): def the_object_name_has_a_cartesian_point_offset_of_offset(name: str, offset: str) -> None: offset = replace_variables(offset) obj = the_object_name_exists(name) - assert obj.BIMObjectProperties.blender_offset_type == "CARTESIAN_POINT" - obj_offset = np.array(tuple(map(float, obj.BIMObjectProperties.cartesian_point_offset.split(",")))) + props = tool.Blender.get_object_props(obj) + assert props.blender_offset_type == "CARTESIAN_POINT" + obj_offset = np.array(tuple(map(float, props.cartesian_point_offset.split(",")))) offset = np.array(tuple(map(float, offset.split(",")))) assert np.allclose(obj_offset, offset) @@ -1169,7 +1170,7 @@ def the_object_name_bottom_left_corner_is_at_location(name, location): @then(parsers.parse('the object "{name}" is contained in "{container_name}"')) def the_object_name_is_contained_in_container_name(name, container_name): ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) container = ifcopenshell.util.element.get_container(element) if not container: assert False, f'Object "{name}" is not in any container' @@ -1179,7 +1180,7 @@ def the_object_name_is_contained_in_container_name(name, container_name): @then(parsers.parse('the object "{name}" is contained in object "{container_name}"')) def the_object_name_is_contained_in_object_container_name(name: str, container_name: str) -> None: ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) container = ifcopenshell.util.element.get_container(element) if not container: assert False, f'Object "{name}" is not in any container' @@ -1190,7 +1191,7 @@ def the_object_name_is_contained_in_object_container_name(name: str, container_n @then(parsers.parse('the object "{name}" is aggregated by object "{aggregate_name}"')) def the_object_name_is_aggregated_by_object_aggregate_name(name: str, aggregate_name: str) -> None: ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) aggregate = ifcopenshell.util.element.get_aggregate(element) if not aggregate: assert False, f'Object "{name}" is not aggregated by any element' @@ -1201,7 +1202,7 @@ def the_object_name_is_aggregated_by_object_aggregate_name(name: str, aggregate_ @then(parsers.parse('the object "{name}" has no aggregate')) def the_object_name_has_no_aggregate(name: str) -> None: ifc = an_ifc_file_exists() - element = ifc.by_id(the_object_name_exists(name).BIMObjectProperties.ifc_definition_id) + element = ifc.by_id(tool.Blender.get_ifc_definition_id(the_object_name_exists(name))) aggregate = ifcopenshell.util.element.get_aggregate(element) if aggregate: assert False, f'Object "{name}" is aggregated by element "{aggregate}"' diff --git a/src/bonsai/test/tool/test_collector.py b/src/bonsai/test/tool/test_collector.py index aff221632f..8c0cd557c6 100644 --- a/src/bonsai/test/tool/test_collector.py +++ b/src/bonsai/test/tool/test_collector.py @@ -19,6 +19,7 @@ import bpy import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.aggregate import ifcopenshell.util.element import bonsai.core.tool import bonsai.tool as tool @@ -167,7 +168,8 @@ class TestAssign(NewIfc): tool.Ifc.link(building_element, building_obj) building_collection = bpy.data.collections.new("Foobar") bpy.context.scene.collection.children.link(building_collection) - building_obj.BIMObjectProperties.collection = building_collection + props = tool.Blender.get_object_bim_props(building_obj) + props.collection = building_collection building_collection.objects.link(building_obj) ifcopenshell.api.aggregate.assign_object( tool.Ifc.get(), diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index b004a92efb..2a2cb6b718 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -319,7 +319,8 @@ class TestGetDrawingCollection(NewFile): collection = bpy.data.collections.new("Collection") bpy.context.scene.collection.children.link(collection) collection.objects.link(obj) - obj.BIMObjectProperties.collection = collection + props = tool.Blender.get_object_bim_props(obj) + props.collection = collection collection.BIMCollectionProperties.obj = obj element = ifc.createIfcAnnotation() diff --git a/src/bonsai/test/tool/test_geometry.py b/src/bonsai/test/tool/test_geometry.py index 63721bcdb7..c0d1819d9a 100644 --- a/src/bonsai/test/tool/test_geometry.py +++ b/src/bonsai/test/tool/test_geometry.py @@ -170,22 +170,25 @@ class TestGetTextLiteral(NewFile): class TestGetCartesianPointCoordinateOffset(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) - obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" + oprops = tool.Blender.get_object_bim_props(obj) + oprops.blender_offset_type = "CARTESIAN_POINT" props = tool.Georeference.get_georeference_props() props.has_blender_offset = True - obj.BIMObjectProperties.cartesian_point_offset = "1,2,3" + oprops.cartesian_point_offset = "1,2,3" assert np.allclose(subject.get_cartesian_point_offset(obj), np.array((1.0, 2.0, 3.0))) def test_get_null_if_not_a_cartesian_point_offset_type(self): obj = bpy.data.objects.new("Object", None) props = tool.Georeference.get_georeference_props() props.has_blender_offset = True - obj.BIMObjectProperties.cartesian_point_offset = "1,2,3" + oprops = tool.Blender.get_object_bim_props(obj) + oprops.cartesian_point_offset = "1,2,3" assert subject.get_cartesian_point_offset(obj) is None def test_get_null_if_no_blender_offset(self): obj = bpy.data.objects.new("Object", None) - obj.BIMObjectProperties.blender_offset_type = "CARTESIAN_POINT" + oprops = tool.Blender.get_object_bim_props(obj) + oprops.blender_offset_type = "CARTESIAN_POINT" props = tool.Georeference.get_georeference_props() props.has_blender_offset = False assert subject.get_cartesian_point_offset(obj) is None @@ -307,15 +310,16 @@ class TestRecordObjectMaterials(NewFile): material.BIMStyleProperties.ifc_definition_id = style.id() obj.data.materials.append(material) subject.record_object_materials(obj) - assert tool.Geometry.get_mesh_props(obj).material_checksum == str([style.id()]) + assert tool.Geometry.get_mesh_props(obj.data).material_checksum == str([style.id()]) class TestRecordObjectPosition(NewFile): def test_run(self): obj = bpy.data.objects.new("Object", None) + props = tool.Blender.get_object_bim_props(obj) subject.record_object_position(obj) - assert obj.BIMObjectProperties.location_checksum == repr(np.array(obj.matrix_world.translation).tobytes()) - assert obj.BIMObjectProperties.rotation_checksum == repr(np.array(obj.matrix_world.to_3x3()).tobytes()) + assert props.location_checksum == repr(np.array(obj.matrix_world.translation).tobytes()) + assert props.rotation_checksum == repr(np.array(obj.matrix_world.to_3x3()).tobytes()) class TestRemoveConnection(NewFile): diff --git a/src/bonsai/test/tool/test_ifc.py b/src/bonsai/test/tool/test_ifc.py index aedcea3465..c12af2a23f 100644 --- a/src/bonsai/test/tool/test_ifc.py +++ b/src/bonsai/test/tool/test_ifc.py @@ -129,14 +129,16 @@ class TestGetEntity(test.bim.bootstrap.NewFile): def test_attempting_without_a_file(self): obj = bpy.data.objects.new("Object", None) - obj.BIMObjectProperties.ifc_definition_id = 1 + props = tool.Blender.get_object_bim_props(obj) + props.ifc_definition_id = 1 assert subject.get_entity(obj) is None def test_attempting_to_get_an_invalidly_linked_object(self): ifc = ifcopenshell.file() subject.set(ifc) obj = bpy.data.objects.new("Object", None) - obj.BIMObjectProperties.ifc_definition_id = 1 + props = tool.Blender.get_object_bim_props(obj) + props.ifc_definition_id = 1 assert subject.get_entity(obj) is None diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index f01399e88c..c93532ab25 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -383,23 +383,26 @@ class TestUsingArrays(NewFile): bpy.ops.mesh.primitive_cube_add() obj = bpy.context.active_object + assert obj rprops = tool.Root.get_root_props() rprops.ifc_product = "IfcElement" bpy.ops.bim.assign_class(ifc_class="IfcActuator", predefined_type="ELECTRICACTUATOR", userdefined_type="") bpy.ops.bim.add_array() bpy.ops.bim.enable_editing_array(item=0) - obj.BIMArrayProperties.count = 4 - obj.BIMArrayProperties.x = 4 - obj.BIMArrayProperties.sync_children = sync_children + props = tool.Model.get_array_props(obj) + props.count = 4 + props.x = 4 + props.sync_children = sync_children bpy.ops.bim.edit_array(item=0) if add_second_layer: bpy.ops.bim.add_array() bpy.ops.bim.enable_editing_array(item=1) - obj.BIMArrayProperties.count = 3 - obj.BIMArrayProperties.y = 4 - obj.BIMArrayProperties.sync_children = sync_children + props = tool.Model.get_array_props(obj) + props.count = 3 + props.y = 4 + props.sync_children = sync_children bpy.ops.bim.edit_array(item=1) def test_remove_array_last_to_first(self): diff --git a/src/bonsai/test/tool/test_surveyor.py b/src/bonsai/test/tool/test_surveyor.py index bfba0cb480..5d3684fdcf 100644 --- a/src/bonsai/test/tool/test_surveyor.py +++ b/src/bonsai/test/tool/test_surveyor.py @@ -54,6 +54,7 @@ class TestGetGlobalMatrix(test.bim.bootstrap.NewFile): props.blender_x_axis_abscissa = "0" props.blender_x_axis_ordinate = "1" obj = bpy.data.objects.new("Object", None) - obj.BIMObjectProperties.blender_offset_type = "OBJECT_PLACEMENT" + props = tool.Blender.get_object_bim_props(obj) + props.blender_offset_type = "OBJECT_PLACEMENT" matrix = ifcopenshell.util.geolocation.local2global(np.array(obj.matrix_world), 1.0, 2.0, 3.0, 0.0, 1.0) assert (subject.get_absolute_matrix(obj) == matrix).all() diff --git a/src/bonsai/test/tool/test_unit.py b/src/bonsai/test/tool/test_unit.py index a3bad12ab3..09db8b72bd 100644 --- a/src/bonsai/test/tool/test_unit.py +++ b/src/bonsai/test/tool/test_unit.py @@ -240,7 +240,7 @@ class TestImportUnitAttributes(NewFile): assert props.unit_attributes["UnitType"].enum_value == "ABSORBEDDOSEUNIT" assert props.unit_attributes["Prefix"].enum_value == "EXA" assert props.unit_attributes["Name"].enum_value == "AMPERE" - assert props.unit_attributes["Dimensions"] is None + assert "Dimensions" not in props.unit_attributes class TestImportUnits(NewFile): diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py index fdc457367b..9de9dc5d55 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py @@ -442,7 +442,7 @@ class Usecase: panel_schema: list[list[int]] = self.settings["panel_schema"] panels: list[dict[str, Any]] = self.settings["panel_properties"] - accumulated_height = [0] * len(panel_schema[0]) + accumulated_height: list[float] = [0] * len(panel_schema[0]) built_panels: list[int] = [] window_items: list[ifcopenshell.entity_instance] = [] lining_items: list[ifcopenshell.entity_instance] = [] diff --git a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py index 6a52abd742..a673bf377c 100644 --- a/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py +++ b/src/ifcpatch/ifcpatch/recipes/FixRevitTINs.py @@ -96,7 +96,8 @@ class Patcher: angle_threshold = 0.3 for obj in bpy.data.objects: - if not obj.BIMObjectProperties.ifc_definition_id or not obj.data: + ifc_id = tool.Blender.get_ifc_definition_id(obj) + if not ifc_id or not obj.data: continue if not obj.data.polygons: continue From f63d6b80c53cda21a1a31b05adbb083769a0e8cd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 25 Feb 2025 16:39:35 +0500 Subject: [PATCH 143/476] quantitifaction - not to add empty qto if nothing was quantified It already works that way for ifcopenshell quantitification, making it work the same for Blender quantifications. Noticed in #6220 --- src/ifc5d/ifc5d/qto.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index e23ae2f442..2863634f07 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -380,24 +380,27 @@ class Blender: import bonsai.bim.module.qto.calculator as calculator unit_converter = SI2ProjectUnitConverter(ifc_file) - formula_functions = {} + formula_functions: dict[str, types.FunctionType] = {} for element in elements: obj = tool.Ifc.get_object(element) if not obj or obj.type != "MESH": continue - results.setdefault(element, {}) + element_results = {} for name, quantities in qtos.items(): - results[element].setdefault(name, {}) + qto_results = {} for quantity, formula in quantities.items(): if not formula: continue if not (formula_function := formula_functions.get(formula)): formula_function = formula_functions[formula] = getattr(calculator, formula) if (value := formula_function(obj)) is not None: - results[element][name][quantity] = unit_converter.convert( - value, Blender.functions[formula].measure - ) + qto_results[quantity] = unit_converter.convert(value, Blender.functions[formula].measure) + if qto_results: + element_results[name] = qto_results + # Avoid adding empty qsets if nothing was calculated. + if element_results: + results[element] = element_results calculators = {"Blender": Blender, "IfcOpenShell": IfcOpenShell} From aaf008f0076722564f9cad2faa58524f0e225a04 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 25 Feb 2025 18:13:02 +0500 Subject: [PATCH 144/476] Fix adding transom to door #6230 (d23cfcb) --- .../ifcopenshell/api/geometry/add_door_representation.py | 5 ++++- .../ifcopenshell/api/geometry/add_window_representation.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py index 6b4fd7e510..fc60a25bbc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_door_representation.py @@ -622,7 +622,7 @@ class Usecase: window_lining_size = V(overall_width, lining_depth, window_lining_height) window_position = V(0, 0, overall_height - window_lining_height) frame_size = V(door_opening_width, frame_depth, frame_height) - window_lining_items, frame_items, glass_items = create_ifc_window( + current_window_items = create_ifc_window( builder, window_lining_size, window_lining_thickness, @@ -633,6 +633,9 @@ class Usecase: glass_thickness, window_position, ) + window_lining_items = current_window_items["Lining"] + frame_items = current_window_items["Framing"] + glass_items = current_window_items["Glazing"] lining_offset_items = lining_items + door_items + window_lining_items + frame_items + glass_items builder.translate(lining_offset_items, (0, lining_offset, 0)) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py index 9de9dc5d55..4477694ca5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_window_representation.py @@ -181,7 +181,7 @@ def create_ifc_window( glass_thickness: float, position: np.ndarray, x_offsets: Optional[list[float]] = None, -) -> tuple[list[ifcopenshell.entity_instance], list[ifcopenshell.entity_instance], list[ifcopenshell.entity_instance]]: +) -> dict[str, list[ifcopenshell.entity_instance]]: """`lining_thickness` and `x_offsets` are expected to be defined as a list, similarly to `create_ifc_window_frame_simple` `thickness` argument""" lining_items: list[ifcopenshell.entity_instance] = [] From f8ec8402fed463a2d8213761baba529e41e078ed Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 25 Feb 2025 18:27:08 +0500 Subject: [PATCH 145/476] Remove test for deprecator operator (c85eb79) --- src/bonsai/test/bim/feature/root.feature | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/bonsai/test/bim/feature/root.feature b/src/bonsai/test/bim/feature/root.feature index d9606b62a0..f8cd1c14bf 100644 --- a/src/bonsai/test/bim/feature/root.feature +++ b/src/bonsai/test/bim/feature/root.feature @@ -35,16 +35,6 @@ Scenario: Unlink object And the material "Style" is an IFC style And the material "Style.001" is not an IFC style -Scenario: Copy class - Given an empty IFC project - And I add a cube - And the object "Cube" is selected - And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" - And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" - And I press "bim.assign_class" - When I press "bim.copy_class(obj='IfcWall/Cube')" - Then the object "IfcWall/Cube" is an "IfcWall" - Scenario: Assign a class to a cube Given an empty IFC project And I add a cube From a20a8a68a6b6ccfba87de63281f4877b726e0e52 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 25 Feb 2025 18:31:39 +0500 Subject: [PATCH 146/476] Smal UI fix - display "Types" instead of "Typess" --- src/bonsai/bonsai/bim/module/model/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 688e7abf42..855c1e4434 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -104,7 +104,7 @@ class LaunchTypeManager(bpy.types.Operator): props = tool.Model.get_model_props() row = self.layout.row(align=True) text = f"{AuthoringData.data['total_types']} {AuthoringData.data['ifc_element_type'] or 'Types'}" - if AuthoringData.data["total_types"] > 1: + if AuthoringData.data["ifc_element_type"] and AuthoringData.data["total_types"] > 1: text += "s" row.label(text=text, icon="FILE_VOLUME") row.menu("BIM_MT_type_manager_menu", text="", icon="PREFERENCES") From b0efb648f2d6ce38585cd185228a09a1beda6481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 25 Feb 2025 17:12:06 -0300 Subject: [PATCH 147/476] Make snapping system zoom-dependent. See #6162 Refactor the snapping system to better organize the weighting and ordering of snap distances. This change enhances the "stickiness" of snapping points, allowing for prioritized control over different types of snapping points. --- src/bonsai/bonsai/tool/raycast.py | 24 ++++++++++------- src/bonsai/bonsai/tool/snap.py | 43 +++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index f519462361..8e1997abf2 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -140,10 +140,12 @@ class Raycast(bonsai.core.tool.Raycast): ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event) points = [] - # Makes the snapping point more or less sticky then others + # Makes the snapping point more or less sticky than others # It changes the distance and affects how the snapping point are sorted - reference = 0.2 - stick_factor = 0.02 + # We multiply by the increment snap which is based on the viewport zoom + snap_threshold = 10 * tool.Snap.get_increment_snap_value(bpy.context) + if face: + snap_threshold = tool.Snap.get_increment_snap_value(bpy.context) try: loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction) @@ -167,12 +169,12 @@ class Raycast(bonsai.core.tool.Raycast): v = obj.matrix_world.copy() @ v intersection = tool.Cad.point_on_edge(v, (ray_target, loc)) distance = (v - intersection).length - if distance < 0.2: + if distance < snap_threshold: snap_point = { "object": obj, "type": "Vertex", "point": v.copy(), - "distance": distance - stick_factor, + "distance": distance, } points.append(snap_point) @@ -186,7 +188,7 @@ class Raycast(bonsai.core.tool.Raycast): intersection = tool.Cad.point_on_edge(division_point, (ray_target, loc)) distance = (division_point - intersection).length - if distance < 0.2: + if distance < snap_threshold: snap_point = { "object": obj, "type": "Edge Center", @@ -199,13 +201,13 @@ class Raycast(bonsai.core.tool.Raycast): if intersection[0]: if tool.Cad.is_point_on_edge(intersection[1], (v1, v2)): distance = (intersection[1] - intersection[0]).length - if distance < 0.2: + if distance < snap_threshold: snap_point = { "object": obj, "type": "Edge", "point": intersection[1].copy(), "edge_verts": (v1, v2), - "distance": distance + 2 * stick_factor, + "distance": distance, } points.append(snap_point) bm.free() @@ -218,6 +220,7 @@ class Raycast(bonsai.core.tool.Raycast): rv3d = context.region_data mouse_pos = event.mouse_region_x, event.mouse_region_y ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event) + snap_threshold = tool.Snap.get_increment_snap_value(bpy.context) try: loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction) @@ -235,7 +238,7 @@ class Raycast(bonsai.core.tool.Raycast): intersection, _ = mathutils.geometry.intersect_point_line(vertex, ray_target, loc) distance = (vertex - intersection).length - if distance < 0.2: + if distance < snap_threshold: snap_point = { "type": "Vertex", "point": vertex, @@ -289,6 +292,7 @@ class Raycast(bonsai.core.tool.Raycast): rv3d = context.region_data mouse_pos = event.mouse_region_x, event.mouse_region_y ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event) + snap_threshold = tool.Snap.get_increment_snap_value(bpy.context) try: loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction) @@ -303,7 +307,7 @@ class Raycast(bonsai.core.tool.Raycast): edge_intersection[1], ray_target, loc ) distance = (edge_intersection[1] - mouse_intersection).length - if distance < 0.2: + if distance < snap_threshold: snap_point = { "object": None, "type": "Edge Intersection", diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 8f89088e63..d2cf4c397c 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -155,7 +155,8 @@ class Snap(bonsai.core.tool.Snap): # Makes the snapping point more or less sticky than others # It changes the distance and affects how the snapping point is sorted - stick_factor = 0.15 + # We multiply by the increment snap which is based on the viewport zoom + snap_threshold = 1 * cls.get_increment_snap_value(bpy.context) default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline @@ -195,7 +196,7 @@ class Snap(bonsai.core.tool.Snap): if tool_state.plane_method == "XZ": proximity = rot_intersection.x - is_on_rot_axis = abs(proximity) <= stick_factor + is_on_rot_axis = abs(proximity) <= snap_threshold if is_on_rot_axis: elegible_axis.append((abs(proximity), axis)) @@ -543,6 +544,19 @@ class Snap(bonsai.core.tool.Snap): filtered_groups = [group for group in detected_snaps if group["group"] in options] return filtered_groups + def sort_points_by_weighted_distance(snapping_points): + for snap in snapping_points: + weight_factor = 100 * cls.get_increment_snap_value(context) + if snap["type"] == "Vertex": + snap["distance"] *= weight_factor / 12 + if snap["type"] == "Edge": + snap["distance"] *= weight_factor + if snap["type"] == "Edge Center": + snap["distance"] *= weight_factor / 5 + if snap["type"] == "Edge Intersection": + snap["distance"] *= weight_factor / 10 + return sorted(snapping_points, key=lambda x: x["distance"]) + snaps_by_group = filter_snapping_points_by_group(detected_snaps) edges = [] # Get edges to create edge-intersection snap for snapping_point in snaps_by_group: @@ -560,30 +574,33 @@ class Snap(bonsai.core.tool.Snap): snaps_by_group.insert(0, snap_point) snaps_by_type = filter_snapping_points_by_type(snaps_by_group) - snaps_by_type = sorted(snaps_by_type, key=lambda x: x["distance"]) + ordered_snaps = sort_points_by_weighted_distance(snaps_by_type) + + for snap in ordered_snaps: + print("\n", snap["type"], snap["distance"]) # Make Axis first priority if tool_state.lock_axis or tool_state.axis_method in {"X", "Y", "Z"}: - cls.update_snapping_ref(snaps_by_type[0]["point"], snaps_by_type[0]["type"]) - for point in snaps_by_type: + cls.update_snapping_ref(ordered_snaps[0]["point"], ordered_snaps[0]["type"]) + for point in ordered_snaps: if point["type"] == "Axis": - if snaps_by_type[0]["type"] not in {"Axis", "Plane"}: - obj = snaps_by_type[0]["object"] - mixed_snap = cls.mix_snap_and_axis(snaps_by_type[0]["point"], axis_start, axis_end) + if ordered_snaps[0]["type"] not in {"Axis", "Plane"}: + obj = ordered_snaps[0]["object"] + mixed_snap = cls.mix_snap_and_axis(ordered_snaps[0]["point"], axis_start, axis_end) for mixed_point in mixed_snap: snap_point = { "point": mixed_point, "type": "Mix", "object": obj, } - snaps_by_type.insert(0, snap_point) + ordered_snaps.insert(0, snap_point) cls.update_snapping_point(snap_point["point"], snap_point["type"]) - return snaps_by_type + return ordered_snaps cls.update_snapping_point(point["point"], point["type"]) - return snaps_by_type + return ordered_snaps - cls.update_snapping_point(snaps_by_type[0]["point"], snaps_by_type[0]["type"], snaps_by_type[0]["object"]) - return snaps_by_type + cls.update_snapping_point(ordered_snaps[0]["point"], ordered_snaps[0]["type"], ordered_snaps[0]["object"]) + return ordered_snaps @classmethod def modify_snapping_point_selection(cls, snapping_points, lock_axis=False): From 10776b7de8c833c4fc546d24697f580acc562ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 25 Feb 2025 17:23:38 -0300 Subject: [PATCH 148/476] Remove debug print statement --- src/bonsai/bonsai/tool/snap.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index d2cf4c397c..01ce5ecaaf 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -575,9 +575,6 @@ class Snap(bonsai.core.tool.Snap): snaps_by_type = filter_snapping_points_by_type(snaps_by_group) ordered_snaps = sort_points_by_weighted_distance(snaps_by_type) - - for snap in ordered_snaps: - print("\n", snap["type"], snap["distance"]) # Make Axis first priority if tool_state.lock_axis or tool_state.axis_method in {"X", "Y", "Z"}: From 81f4b08b6d5290d43904c5e41113868bd7508dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 25 Feb 2025 18:00:59 -0300 Subject: [PATCH 149/476] Fix issue where long lines where not being detected by the snapping system. --- src/bonsai/bonsai/tool/raycast.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 8e1997abf2..919efd94b9 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -58,6 +58,8 @@ class Raycast(bonsai.core.tool.Raycast): for i, axis in enumerate(zip(*transposed_bbox)): min_point = min(axis) max_point = max(axis) + if min_point == max_point: + min_point = 0 if min_point < borders[i] and max_point > 0: bbox_2d.extend([min_point, max_point]) else: From 0104a2cc9f6393a88c0701f4f12d7cfaf202bab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 25 Feb 2025 18:05:12 -0300 Subject: [PATCH 150/476] Remove polyline wall size limit restriction. See #6162 --- src/bonsai/bonsai/bim/module/model/wall.py | 2 -- src/bonsai/bonsai/tool/polyline.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index daf9d9a43b..5b60551143 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -712,8 +712,6 @@ class DumbWallGenerator: def create_wall_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]: direction = coords[1] - coords[0] length = direction.length - if round(length, 4) < 0.1: - return data = {"coords": coords} self.length = length diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index b5c9ad4232..ff9a4ca441 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -534,8 +534,6 @@ class Polyline(bonsai.core.tool.Polyline): length = ( Vector((x, y, z)) - Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z)) ).length - if round(length, 4) < 0.1: - return "Cannot create a segment smaller then 10cm" polyline_point = polyline_points.add() polyline_point.x = x From 0f3de599a6f2a7e67a65e5167f372e908e4f71e8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 26 Feb 2025 13:13:22 +1100 Subject: [PATCH 151/476] Don't break if no area unit defined --- src/bonsai/bonsai/bim/module/drawing/helper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index 5a46b8e27b..a9333fb6a4 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -137,9 +137,10 @@ def format_distance( scaleFactor = bpy.context.scene.unit_settings.scale_length unit_system = bpy.context.scene.unit_settings.system unit_length = bpy.context.scene.unit_settings.length_unit - area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol( - ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT") - ) + if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"): + area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit) + else: + area_unit_symbol = "" value *= scaleFactor From 34acd6de3515632c612782439e9a85fde69dd4f8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 26 Feb 2025 17:56:29 +1100 Subject: [PATCH 152/476] See #1227. Implement ATPATH wall connections (going both ways). --- src/bonsai/bonsai/tool/loader.py | 2 +- src/bonsai/scripts/waldo.py | 327 +++++++++++++++++++++++++------ 2 files changed, 270 insertions(+), 59 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 7a5576a1e2..cd7e0471ec 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1053,7 +1053,7 @@ class Loader(bonsai.core.tool.Loader): styles[style] = i for layer in layer_set.MaterialLayers[:-1]: prev_co = co.copy() - co.y = layer.LayerThickness * cls.unit_scale * sense_factor + co.y += layer.LayerThickness * cls.unit_scale * sense_factor bisect_geom = bmesh.ops.bisect_plane( bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no ) diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index 18c6267d39..ad382b101a 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -12,11 +12,12 @@ import ifcopenshell.util.shape_builder import ifcopenshell.util.element # from ifcopenshell.util.shape_builder import VectorType, SequenceOfVectors +from itertools import cycle from collections import namedtuple f = ifcopenshell.api.project.create_file() -ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject") +project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject") meters = ifcopenshell.api.unit.add_si_unit(f) ifcopenshell.api.unit.assign_unit(f, units=[meters]) @@ -31,6 +32,7 @@ body = ifcopenshell.api.context.add_context( material1 = ifcopenshell.api.material.add_material(f, name="material1", category="material1") material2 = ifcopenshell.api.material.add_material(f, name="material2", category="material2") site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite") +ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project) builder = ifcopenshell.util.shape_builder.ShapeBuilder(f) style = ifcopenshell.api.style.add_style(f) @@ -68,23 +70,32 @@ def test_wall(offset, p1, p2, p3, p4): ifcopenshell.api.material.assign_material(f, products=[wall_type_a], material=set_a) ifcopenshell.api.material.assign_material(f, products=[wall_type_b], material=set_b) - for i, rotation in enumerate((-90, -60, -120, 90, 60, 120)): + for i, rotation in enumerate((-90, -75, -105, 90, 75, 105)): for i2, connection in enumerate(("ATEND", "ATSTART", "MIX")): + # if rotation != -90: + # continue + # if connection != "ATEND": + # continue wall_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"A{p1}{p2}") wall_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"B{p3}{p4}") + wall_c = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"C{p3}{p4}") - ifcopenshell.api.spatial.assign_container(f, products=[wall_a, wall_b], relating_structure=site) + ifcopenshell.api.spatial.assign_container(f, products=[wall_a, wall_b, wall_c], relating_structure=site) ifcopenshell.api.type.assign_type(f, related_objects=[wall_a], relating_type=wall_type_a) ifcopenshell.api.type.assign_type(f, related_objects=[wall_b], relating_type=wall_type_b) + ifcopenshell.api.type.assign_type(f, related_objects=[wall_c], relating_type=wall_type_b) axis_a = builder.polyline(((0.0, 0.0), (1.0, 0.0))) axis_b = builder.polyline(((0.0, 0.0), (1.0, 0.0))) + axis_c = builder.polyline(((0.0, 0.0), (1.0, 0.0))) rep_a = builder.get_representation(axis, [axis_a]) rep_b = builder.get_representation(axis, [axis_b]) + rep_c = builder.get_representation(axis, [axis_c]) ifcopenshell.api.geometry.assign_representation(f, product=wall_a, representation=rep_a) ifcopenshell.api.geometry.assign_representation(f, product=wall_b, representation=rep_b) + ifcopenshell.api.geometry.assign_representation(f, product=wall_c, representation=rep_c) x_offset = i * 2 x_offset += i2 * (2 * 6) @@ -95,8 +106,12 @@ def test_wall(offset, p1, p2, p3, p4): matrix_b = np.eye(4) matrix_b = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_b matrix_b[:, 3][0:3] = (1 + x_offset, 1 + offset - sign_offset, 0) + matrix_c = np.eye(4) + matrix_c = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_c + matrix_c[:, 3][0:3] = (0.5 + x_offset, 0.5 + offset, 0) ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) ifcopenshell.api.geometry.edit_object_placement(f, product=wall_b, matrix=matrix_b) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_c, matrix=matrix_c) ifcopenshell.api.geometry.connect_path( f, @@ -105,6 +120,13 @@ def test_wall(offset, p1, p2, p3, p4): relating_connection="ATEND", related_connection="ATEND", ) + ifcopenshell.api.geometry.connect_path( + f, + relating_element=wall_c, + related_element=wall_a, + relating_connection="ATEND", + related_connection="ATPATH", + ) elif connection == "ATSTART": sign_offset = 0 if rotation < 0 else 1 matrix_a = np.eye(4) @@ -142,6 +164,69 @@ def test_wall(offset, p1, p2, p3, p4): Foo(f, body, axis).regenerate(wall_a) Foo(f, body, axis).regenerate(wall_b) + Foo(f, body, axis).regenerate(wall_c) + + +def create_type(name, layers): + wall_type = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name=name) + layer_set = ifcopenshell.api.material.add_material_set(f, set_type="IfcMaterialLayerSet") + materials = cycle((material1, material2)) + for layer in layers: + material = next(materials) + item = ifcopenshell.api.material.add_layer(f, layer_set=layer_set, material=material, name="structure") + item.Priority = layer[0] + item.LayerThickness = layer[1] + ifcopenshell.api.material.assign_material(f, products=[wall_type], material=layer_set) + return wall_type + + +def test_atpath(offset): + offset *= 1.5 + wall_type_a = create_type("A", [(1, 0.05), (2, 0.1), (3, 0.05)]) + wall_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="A123") + ifcopenshell.api.spatial.assign_container(f, products=[wall_a], relating_structure=site) + ifcopenshell.api.type.assign_type(f, related_objects=[wall_a], relating_type=wall_type_a) + axis_a = builder.polyline(((0.0, 0.0), (30.0, 0.0))) + rep_a = builder.get_representation(axis, [axis_a]) + ifcopenshell.api.geometry.assign_representation(f, product=wall_a, representation=rep_a) + matrix_a = np.eye(4) + matrix_a[:, 3][0:3] = (0, 0 + offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall_a, matrix=matrix_a) + + def create_branch(name, p1, p2, p3, x, y, rotation): + wall_type = create_type(name, [(p1, 0.05), (p2, 0.1), (p3, 0.05)]) + wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name=f"{name}{p1}{p2}{p3}") + ifcopenshell.api.spatial.assign_container(f, products=[wall], relating_structure=site) + ifcopenshell.api.type.assign_type(f, related_objects=[wall], relating_type=wall_type) + axis_a = builder.polyline(((0.0, 0.0), (1.0, 0.0))) + rep_a = builder.get_representation(axis, [axis_a]) + ifcopenshell.api.geometry.assign_representation(f, product=wall, representation=rep_a) + matrix_a = np.eye(4) + matrix_a = ifcopenshell.util.placement.rotation(rotation, "Z") @ matrix_a + matrix_a[:, 3][0:3] = (x, y + offset, 0) + ifcopenshell.api.geometry.edit_object_placement(f, product=wall, matrix=matrix_a) + ifcopenshell.api.geometry.connect_path( + f, + relating_element=wall, + related_element=wall_a, + relating_connection="ATEND", + related_connection="ATPATH", + ) + Foo(f, body, axis).regenerate(wall) + + create_branch("B", 1, 1, 1, 1, 1, -75) + create_branch("C", 1, 2, 3, 2, 1, -75) + create_branch("D", 1, 4, 2, 3, 1, -75) + create_branch("E", 4, 4, 4, 4, 1, -75) + create_branch("F", 4, 2, 4, 5, 1, -75) + + create_branch("B", 1, 1, 1, 0.5, -1, 75) + create_branch("C", 1, 2, 3, 1.5, -1, 75) + create_branch("D", 1, 4, 2, 2.5, -1, 75) + create_branch("E", 4, 4, 4, 3.5, -1, 75) + create_branch("F", 4, 2, 4, 4.5, -1, 75) + + Foo(f, body, axis).regenerate(wall_a) PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness") @@ -162,8 +247,13 @@ class Foo: reference = self.get_reference_line(wall) self.reference_p1, self.reference_p2 = reference axes = self.get_axes(wall, reference, layers) + self.miny = axes[0][0][1] + self.maxy = axes[-1][0][1] self.end_point = None self.start_points = [] + self.split_points = [] + self.maxpath_points = [] + self.minpath_points = [] self.end_points = [] for rel in wall.ConnectedTo: if rel.is_a("IfcRelConnectsPathElements"): @@ -183,9 +273,6 @@ class Foo: continue self.join(wall, wall2, layers1, layers2, rel.RelatedConnectionType, rel.RelatingConnectionType) - # for rel in wall.ConnectedFrom: - # if rel.is_a("IfcRelConnectsPathElements"): - # connection = rel.RelatedConnectionType if not self.start_points: minx = axes[0][0][0] self.start_points = [ @@ -202,18 +289,60 @@ class Foo: print(self.start_points) print(self.end_points) - points = [] - if self.start_points[0][1] < self.start_points[-1][1]: - points.extend((self.start_points)) - else: - points.extend(reversed(self.start_points)) - if self.end_points[0][1] > self.end_points[-1][1]: - points.extend((self.end_points)) - else: - points.extend(reversed(self.end_points)) + if self.start_points[0][1] > self.start_points[-1][1]: # Canonicalise to the +Y direction + self.start_points.reverse() + if self.end_points[0][1] > self.end_points[-1][1]: # Canonicalise to the +Y direction + self.end_points.reverse() builder = ifcopenshell.util.shape_builder.ShapeBuilder(wall.file) - item = builder.extrude(builder.polyline(points, closed=True), magnitude=1.0) + # A wall footprint may be multiple profiles if the wall is split into two due to an ATPATH connection + profiles = [] + split_points = sorted(self.split_points, key=lambda x: x[0][0]) # Sort islands in the +X direction + split_points.insert(0, self.start_points) + split_points.append(self.end_points) + split_points = iter(split_points) + + while True: + # Draw each profile as clockwise starting from (minx, miny) + start_split = next(split_points, None) + if not start_split: + break + end_split = next(split_points, None) + if not end_split: + break + maxy_minx = start_split[-1][0] + maxy_maxx = end_split[-1][0] + miny_minx = start_split[0][0] + miny_maxx = end_split[0][0] + # Do more defensive checks here + points = start_split + remaining_path_points = [] + for maxpath_points in self.maxpath_points: + if maxpath_points[0][0] > maxy_minx and maxpath_points[-1][0] < maxy_maxx: + points.extend(maxpath_points) + else: + remaining_path_points.append(maxpath_points) + self.maxpath_points = remaining_path_points + points.extend(end_split[::-1]) + remaining_path_points = [] + for minpath_points in self.minpath_points: + if minpath_points[0][0] < miny_maxx and minpath_points[-1][0] > miny_minx: + points.extend(minpath_points) + else: + remaining_path_points.append(minpath_points) + self.minpath_points = remaining_path_points + + profiles.append(builder.profile(builder.polyline(points, closed=True))) + + for points in self.maxpath_points + self.minpath_points: + profiles.append(builder.profile(builder.polyline(points, closed=True))) + + if len(profiles) > 1: + profile = wall.file.createIfcCompositeProfileDef("AREA", Profiles=profiles) + else: + profile = profiles[0] + + item = builder.extrude(profile, magnitude=1.0) rep = builder.get_representation(self.body, items=[item]) if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body): ifcopenshell.util.element.replace_element(old_rep, rep) @@ -230,6 +359,8 @@ class Foo: def join(self, wall1, wall2, layers1, layers2, connection1, connection2): if connection1 == "NOTDEFINED" or connection2 == "NOTDEFINED": return + if connection1 == "ATPATH" and connection2 == "ATPATH": + return print("joining", wall1, layers1, connection1) print("to", wall2, layers2, connection2) @@ -277,51 +408,130 @@ class Foo: print(axes1) print(axes2) # Checked + if connection1 == "ATPATH": + first_axis2 = axes2[0] + last_axis2 = axes2[-1] + first_y = axes1[0][0][1] + last_y = axes1[-1][0][1] + p0 = np.array((self.intersect_axis(*first_axis2, y=first_y), first_y)) + pN = np.array((self.intersect_axis(*last_axis2, y=first_y), first_y)) - last_y = axes1[-1][0][1] - ys = iter([a[0][1] for a in axes1]) - print("ys are", [a[0][1] for a in axes1]) - - last_axis2 = axes2[-1] - axes2 = iter(axes2) - axis2 = next(axes2) - y = next(ys) - x = self.intersect_axis(*axis2, y=y) - points = [np.array((x, y))] - print("first point", points) - - layers1 = iter(layers1) - layers2 = iter(layers2) - layer1 = next(layers1, None) - layer2 = next(layers2, None) - - while layer1 and layer2: - print("considering", layer1, layer2) - if layer1.priority > layer2.priority: + # Generate CurveOnRelating/RelatedElement + points = [p0] + axes2 = iter(axes2) + axis2 = next(axes2) + for layer2 in layers2: + ys = iter([a[0][1] for a in axes1]) + y = next(ys) + for layer1 in layers1: + if layer2.priority <= layer1.priority: + break + y = next(ys) + p1 = np.array((self.intersect_axis(*axis2, y=y), y)) axis2 = next(axes2) - x = self.intersect_axis(*axis2, y=y) - layer2 = next(layers2, None) - elif layer2.priority > layer1.priority: - y = next(ys) - x = self.intersect_axis(*axis2, y=y) - layer1 = next(layers1, None) - else: - y = next(ys) - x = self.intersect_axis(*next(axes2), y=y) - layer1 = next(layers1, None) - layer2 = next(layers2, None) - points.append(np.array((x, y))) + p2 = np.array((self.intersect_axis(*axis2, y=y), y)) + if points and np.allclose(points[-1], p1): + points[-1] = p2 # Just slide along previous point + else: + points.extend((p1, p2)) - print("points", points) - if points[-1][1] != last_y: - points.append(np.array((self.intersect_axis(*last_axis2, y=last_y), last_y))) - print("fpoints", points) - if connection1 == "ATSTART": - self.start_points = points - self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) - elif connection1 == "ATEND": - self.end_points = points - self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) + # The curve must end at pN + if not np.allclose(points[-1], pN): + points.append(pN) + + # Categorise our points into a segment that either splits or cuts the wall + split_ys = {first_y, last_y} + segment = [] + for point in points: + segment.append(point) + if len(segment) == 1: # Not enough points to categorise the segment + continue + elif {segment[0][1], segment[-1][1]} == split_ys: # This segment splits the wall + if segment[0][1] > segment[-1][1]: # Go in the +Y direction + segment.reverse() + self.split_points.append(segment) + segment = [] + elif segment[0][1] == segment[-1][1]: # This segment cuts some of the wall + if segment[0][1] == self.maxy: # Go in the +X direction + if segment[0][0] > segment[-1][0]: + segment.reverse() + self.maxpath_points.append(segment) + elif segment[0][1] == self.miny: # Go in the -X direction + if segment[-1][0] > segment[0][0]: + segment.reverse() + self.minpath_points.append(segment) + segment = [] + elif connection2 == "ATPATH": + points = [] + ys = iter([a[0][1] for a in axes1]) + y = next(ys) + for layer1 in layers1: + axes2_iter = iter(axes2) + axis2 = next(axes2_iter) + for layer2 in layers2: + if layer1.priority <= layer2.priority: + break + axis2 = next(axes2_iter) + x = self.intersect_axis(*axis2, y=y) + p1 = np.array((x, y)) + y = next(ys) + x = self.intersect_axis(*axis2, y=y) + p2 = np.array((x, y)) + if points and np.allclose(points[-1], p1): + points.append(p2) + else: + points.extend((p1, p2)) + + if connection1 == "ATSTART": + self.start_points = points + self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) + elif connection1 == "ATEND": + self.end_points = points + self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) + else: + last_y = axes1[-1][0][1] + ys = iter([a[0][1] for a in axes1]) + + last_axis2 = axes2[-1] + axes2 = iter(axes2) + axis2 = next(axes2) + y = next(ys) + x = self.intersect_axis(*axis2, y=y) + points = [np.array((x, y))] + + layers1 = iter(layers1) + layers2 = iter(layers2) + layer1 = next(layers1, None) + layer2 = next(layers2, None) + + # This creates "mitering" behaviour which is an ambiguity by bSI. + while layer1 and layer2: + print("considering", layer1, layer2) + if layer1.priority > layer2.priority: + axis2 = next(axes2) + x = self.intersect_axis(*axis2, y=y) + layer2 = next(layers2, None) + elif layer2.priority > layer1.priority: + y = next(ys) + x = self.intersect_axis(*axis2, y=y) + layer1 = next(layers1, None) + else: + y = next(ys) + x = self.intersect_axis(*next(axes2), y=y) + layer1 = next(layers1, None) + layer2 = next(layers2, None) + points.append(np.array((x, y))) + + print("points", points) + if points[-1][1] != last_y: + points.append(np.array((self.intersect_axis(*last_axis2, y=last_y), last_y))) + + if connection1 == "ATSTART": + self.start_points = points + self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) + elif connection1 == "ATEND": + self.end_points = points + self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) def get_layers(self, wall) -> list: material = ifcopenshell.util.element.get_material(wall, should_skip_usage=True) @@ -387,6 +597,7 @@ test_wall(2, 2, 1, 1, 1) test_wall(3, 1, 2, 1, 1) test_wall(4, 1, 2, 1, 2) test_wall(5, 3, 1, 2, 4) +test_atpath(7) f.write("/home/dion/wall.ifc") From cfb7d026d1f4818de7cd5a4c30da8278b77aa751 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 26 Feb 2025 18:13:20 +1100 Subject: [PATCH 153/476] IfcPatch recipe for Revit 2025 TINs import workaround To understand what's going on, please read the docstring. Then please take as much alcohol required to erase that memory. --- .../ifcpatch/recipes/FixRevit2025TINs.py | 540 ++++++++++++++++++ 1 file changed, 540 insertions(+) create mode 100644 src/ifcpatch/ifcpatch/recipes/FixRevit2025TINs.py diff --git a/src/ifcpatch/ifcpatch/recipes/FixRevit2025TINs.py b/src/ifcpatch/ifcpatch/recipes/FixRevit2025TINs.py new file mode 100644 index 0000000000..f6f375ea23 --- /dev/null +++ b/src/ifcpatch/ifcpatch/recipes/FixRevit2025TINs.py @@ -0,0 +1,540 @@ +# IfcPatch - IFC patching utiliy +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcPatch. +# +# IfcPatch 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. +# +# IfcPatch 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 IfcPatch. If not, see . + + +import ifcopenshell +import logging +from typing import Optional + + +class Patcher: + def __init__( + self, file: None, logger: logging.Logger, filepath: str, is_solid: bool = True, should_create_edges: bool = True + ): + """Fix missing or spot-coordinate bugged TINs loading in Revit + + TINs exported from 12D or Civil 3D may contain dense or highly obtuse + triangles. Although these will load in Revit, you will not be able to + use Revit's Spot Coordinate or Spot Elevation tool. + + See bug: https://github.com/Autodesk/revit-ifc/issues/511 + + If `is_solid` is enabled, we assume the surface is represented as a + solid (e.g. I have come across surfaces which are extruded by 1mm from + Civil 3D). The solution will delete any faces with a Z normal less + than 0.5. In case the mesh has any side faces or thickness, this should + leave only the top surface which is relevant for spot coordinates and + elevations. + + Vertices closer than 10mm will also be merged to prevent dense + portions of the TIN at a minor sacrifice of surveying accuracy. It will + also triangulate all meshes to prevent non-coplanar surfaces, and + delete any obtuse triangles where one of their XY angles is less than + 0.3 degrees. Therefore the result will contain some minor "holes" in + the TIN, but these holes will only be in dense triangles that Revit + can't handle anyway and won't affect most coordination tasks. + + After that, it will: + + 1. Reassign everything to an IfcGeographicElement + 2. Detect boundary edges and create an edge-only IfcVirtualElement. + Good for clean viz in Revit. + 3. Create a copy of the object which has no sharp faces. This will + allow Revit's spot coordinate tool to work on any arbitrary face + surface. Note that Revit cannot snap to edges or vertices on this + object. + 4. Create a copy of the obejct which has one artifically injected sharp + face. This trick allows Revit's spot coordinate tool to snap to + edges and points. However, Revit cannot sample an arbitrary + surface. By combining this object with the previous object, you get + the best of both worlds. + + If you're thinking that this overlapping, Z-fighting, duplication of + objects with arbitrary almost-degenerate triangles being added is a + horrific abomination in the world of software workarounds, you are + absolutely correct. + + This is a variation of FixRevitTINs which has been tested on Revit <= + 2023. I've tested this one on Revit 2025 (the behaviour has changed). + + Note that you may may want to run other tools like + OffsetObjectPlacements or ResetAbsoluteCoordinates to fix large + coordinates as these can also cause issues in Revit (such as inaccuracy + or inability to use the Spot Coordinate / Elevation tool). + + This patch is designed to work on any TIN-like export, typically coming + from civil software. It also requires you to run it using Blender, as + the geometric modification uses the Blender geometry engine. + + `filepath` argument is required for this recipe, `file` argument is + ignored. + + :param filepath: The filepath of the IFC model. This is required to + load into Bonsai. + :param is_solid: If true, assume a thickness and delete anything that + isn't the top face. + :param should_create_edges: If true, a new IfcVirtualElement is created + representing the perimeter of the objects. This allows you to to + hide regular surface edges in Revit and only use the perimeter edge + for visualisation. + :filter_glob filepath: *.ifc;*.ifczip;*.ifcxml + + Example: + + .. code:: python + + ifcpatch.execute({"input": "input.ifc", "recipe": "FixRevit2025TINs", "arguments": []}) + """ + self.file = file + self.filepath = filepath + self.logger = logger + self.is_solid = is_solid + self.should_create_edges = should_create_edges + + def patch(self) -> None: + import bpy + import bmesh + import bonsai.tool as tool + import ifcopenshell.util.shape_builder + + bpy.context.scene.BIMProjectProperties.should_use_native_meshes = True + bpy.ops.bim.load_project(filepath=self.filepath) + + old_history_size = tool.Ifc.get().history_size + old_undo_steps = bpy.context.preferences.edit.undo_steps + tool.Ifc.get().history_size = 0 + bpy.context.preferences.edit.undo_steps = 0 + + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + for obj in bpy.data.objects: + if not obj.BIMObjectProperties.ifc_definition_id or not obj.data: + continue + if not obj.data.polygons: + continue + element = tool.Ifc.get_entity(obj) + element.PredefinedType = "USERDEFINED" + element.ObjectType = "TIN" + element = ifcopenshell.util.schema.reassign_class(tool.Ifc.get(), element, "IfcGeographicElement") + + bm = bmesh.new() + bm.from_mesh(obj.data) + faces_to_delete = [] + if self.is_solid: + for face in bm.faces: + global_normal = obj.matrix_world.to_3x3() @ face.normal + if global_normal.z < 0.5: + faces_to_delete.append(face) + bmesh.ops.delete(bm, geom=faces_to_delete, context="FACES_ONLY") + bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method="BEAUTY", ngon_method="BEAUTY") + bm.to_mesh(obj.data) + bm.free() + obj.data.update() + + if self.should_create_edges: + self.create_edges(obj) + + self.create_face_sampleable_object(obj) + self.create_edge_sampleable_object(obj) + + tool.Ifc.get().history_size = old_history_size + bpy.context.preferences.edit.undo_steps = old_undo_steps + + self.file = tool.Ifc.get() + + def create_edges(self, obj): + import bpy + import bmesh + import bonsai.tool as tool + import ifcopenshell.util.element + import ifcopenshell.util.representation + import ifcopenshell.api.root + import ifcopenshell.api.type + import ifcopenshell.api.spatial + import ifcopenshell.api.geometry + + element = tool.Ifc.get_entity(obj) + data = obj.data + bm = bmesh.new() + bm.from_mesh(data) + + bm.faces.ensure_lookup_table() + + if self.is_solid: + faces_to_delete = [] + for face in bm.faces: + if face.normal.z < 0.5: + faces_to_delete.append(face) + bmesh.ops.delete(bm, geom=faces_to_delete, context="FACES_ONLY") + + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.01) + + edges_to_delete = [] + bm.faces.ensure_lookup_table() + for edge in bm.edges: + if len(edge.link_faces) != 1: + edges_to_delete.append(edge) + + bmesh.ops.delete(bm, geom=edges_to_delete, context="EDGES_FACES") + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) + + bm.verts.ensure_lookup_table() + for vert in bm.verts: + vert.co.z += 0.003 + + mesh = bpy.data.meshes.new("Mesh") + bm.to_mesh(mesh) + + obj = bpy.data.objects.new("Perimeter", mesh) + bpy.context.scene.collection.objects.link(obj) + with bpy.context.temp_override(**tool.Blender.get_viewport_context()): + tool.Blender.select_and_activate_single_object(bpy.context, obj) + bpy.ops.object.convert(target="CURVE") + + context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") + + if self.file.schema == "IFC2X3": + curves = self.create_curves_from_curve_ifc2x3(is_2d=False, curve_object_data=obj.data) + else: + curves = self.create_curves_from_curve(is_2d=False, curve_object_data=obj.data) + + representation = tool.Ifc.get().createIfcShapeRepresentation( + context, context.ContextIdentifier, "Curve3D", curves + ) + + element2 = ifcopenshell.api.root.copy_class(tool.Ifc.get(), product=element) + element2.Name += "-boundary" + element2.ObjectType = "TINBOUNDARY" + ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element2, representation) + ifcopenshell.util.schema.reassign_class(tool.Ifc.get(), element2, "IfcVirtualElement") + bm.free() + + def create_face_sampleable_object(self, obj): + # No sharp faces + import bpy + import bmesh + import bonsai.tool as tool + import ifcopenshell.util.element + import ifcopenshell.util.representation + import ifcopenshell.api.root + import ifcopenshell.api.type + import ifcopenshell.api.spatial + import ifcopenshell.api.geometry + from math import degrees + + print("working on ", obj.name) + element = tool.Ifc.get_entity(obj) + data = obj.data + bm = bmesh.new() + bm.from_mesh(data) + + bm.faces.ensure_lookup_table() + + angle_threshold = 0.3 + for polygon in bm.faces: + try: + v1, v2, v3 = [v.co for v in polygon.verts] + d1 = degrees((v2 - v1).angle(v3 - v1)) + d2 = degrees((v3 - v2).angle(v1 - v2)) + d3 = degrees((v1 - v3).angle(v2 - v3)) + if d1 < angle_threshold or d2 < angle_threshold or d3 < angle_threshold: + print("removing", d1, d2, d3, polygon) + bm.faces.remove(polygon) + except: + print("removing", polygon) + bm.faces.remove(polygon) + + context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") + + builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) + verts = [v.co / self.unit_scale for v in bm.verts] + faces = [[v.index for v in p.verts] for p in bm.faces] + item = builder.mesh(verts, faces) + representation = builder.get_representation(context, [item]) + element2 = ifcopenshell.api.root.copy_class(tool.Ifc.get(), product=element) + element2.Name += "-face-sample" + print("new", element2, element) + ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element2, representation) + + bm.free() + + def create_edge_sampleable_object(self, obj): + # This is crazy but we need a sharp face per island + import bpy + import bmesh + import bonsai.tool as tool + import ifcopenshell.util.element + import ifcopenshell.util.representation + import ifcopenshell.api.root + import ifcopenshell.api.type + import ifcopenshell.api.spatial + import ifcopenshell.api.geometry + from math import degrees, radians, sin + from mathutils import Matrix + + # Get the active object (assumed to have a mesh) + mesh = obj.data + + # Create a BMesh representation + bm = bmesh.new() + bm.from_mesh(mesh) + + # First purge existing sharp edges (don't ask me why, really don't) + bm.faces.ensure_lookup_table() + angle_threshold = 0.3 + for polygon in bm.faces: + try: + # v1, v2, v3 = [v.co.to_2d() for v in polygon.verts] + v1, v2, v3 = [v.co for v in polygon.verts] + d1 = degrees((v2 - v1).angle(v3 - v1)) + d2 = degrees((v3 - v2).angle(v1 - v2)) + d3 = degrees((v1 - v3).angle(v2 - v3)) + if d1 < angle_threshold or d2 < angle_threshold or d3 < angle_threshold: + print("removing", d1, d2, d3, polygon) + bm.faces.remove(polygon) + except: + print("removing", polygon) + bm.faces.remove(polygon) + bm.faces.ensure_lookup_table() + + # Now we add our own. + + # A set to mark faces that have been visited + visited_faces = set() + + def get_island(start_face): + """Return the connected set of faces (a 'mesh island') starting from start_face.""" + island = set() + stack = [start_face] + while stack: + f = stack.pop() + if f in island: + continue + island.add(f) + for edge in f.edges: + # For every face sharing this edge, add to the stack + for f2 in edge.link_faces: + if f2 not in island: + stack.append(f2) + return island + + # Loop over all faces and process each island once + islands_count = 0 + for face in bm.faces: + if face in visited_faces: + continue + + # Get the connected component (island) containing this face + island = get_island(face) + visited_faces |= island # mark all island faces as visited + islands_count += 1 + + boundary_edge = None + for face in island: + for edge in face.edges: + # Count how many faces in the island use this edge. + count = sum(1 for f in edge.link_faces if f in island) + if count == 1: + boundary_edge = edge + break + if boundary_edge: + break + + # If no boundary edge is found, skip this island. + if boundary_edge is None: + continue + + # Use the two vertices of the boundary edge as A and B. + A, B = boundary_edge.verts[0], boundary_edge.verts[1] + + # THIRD ATTEMPT + + # Use the normal from the boundary face (i.e. the single linked face of the boundary edge) + base_face = boundary_edge.link_faces[0] + plane_normal = ( + base_face.normal.copy() + ) # This normal defines the plane in which we'll construct the triangle + + # Compute the edge AB vector and its length. + AB_vec = B.co - A.co + d = AB_vec.length + if d == 0: + continue # degenerate edge + + # --- Desired angles for the new triangle (in degrees) --- + # This is the crazy degenerate triangle + angle_A_deg = 0.004 # angle at vertex A + angle_B_deg = 1.146 # angle at vertex B + angle_C_deg = 178.85 # angle at vertex C (note: 180 - (0.004 + 1.146) = 178.85) + + # Convert angles to radians. + angle_A = radians(angle_A_deg) + angle_B = radians(angle_B_deg) + angle_C = radians(angle_C_deg) + + # --- Use the law of sines to compute the new triangle's side lengths --- + # In triangle ABC, with AB opposite angle C: + # AB / sin(angle_C) = AC / sin(angle_B) = BC / sin(angle_A) + # We'll compute AC (from A) as it is needed to place the new vertex. + AC_length = d * sin(angle_B) / sin(angle_C) + + # --- Determine the direction for AC in the triangle's plane --- + # Starting at A, the direction of AB is our baseline. + u = AB_vec.normalized() + + # To get the direction for AC, rotate u by angle_A about the plane_normal. + rot_mat = Matrix.Rotation(angle_A, 3, plane_normal) + dA = u.copy() + dA.rotate(rot_mat) + + # Compute the position for the new vertex C. + C_co = A.co + AC_length * dA + + # --- Create the new vertex and triangle face in the BMesh --- + new_vert = bm.verts.new(C_co) + bm.verts.index_update() # update indices if needed + + # Create the new triangle face from vertices A, B, and new_vert. + # (The order of vertices may be adjusted if you need a specific winding.) + try: + new_face = bm.faces.new((A, B, new_vert)) + visited_faces.add(new_face) + print("added new face") + except ValueError: + # Face already exists or some error occurred + print("Could not create face on island", islands_count) + + context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") + builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) + verts = [v.co / self.unit_scale for v in bm.verts] + faces = [[v.index for v in p.verts] for p in bm.faces] + item = builder.mesh(verts, faces) + representation = builder.get_representation(context, [item]) + element = tool.Ifc.get_entity(obj) + element2 = ifcopenshell.api.root.copy_class(tool.Ifc.get(), product=element) + element2.Name += "-edge-sample" + print("new", element2, element) + ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element2, representation) + + bm.free() + + print("Added a triangle to", islands_count, "mesh island(s).") + return + + def create_curves_from_curve_ifc2x3( + self, is_2d: bool = False, curve_object_data=None + ) -> list[ifcopenshell.entity_instance]: + import bonsai.tool as tool + + dim = (lambda v: v.xy) if is_2d else (lambda v: v.xyz) + results = [] + for spline in curve_object_data.splines: + points = spline.bezier_points[:] + spline.points[:] + if spline.use_cyclic_u: + points.append(points[0]) + ifc_points = [self.create_cartesian_point(*dim(point.co)) for point in points] + results.append(tool.Ifc.get().createIfcPolyline(ifc_points)) + return results + + def create_curves_from_curve( + self, is_2d: bool = False, curve_object_data=None + ) -> list[ifcopenshell.entity_instance]: + import bonsai.tool as tool + import numpy as np + + dim = (lambda v: v.xy) if is_2d else (lambda v: v.xyz) + to_units = lambda v: np.array([self.convert_si_to_unit(i) for i in v]) + builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) + results = [] + + for spline in curve_object_data.splines: + points = spline.bezier_points[:] + spline.points[:] + + points = [to_units(dim(p.co)) for p in points] + closed_polyline = spline.use_cyclic_u and len(points) > 1 + results.append(builder.polyline(points, closed=closed_polyline)) + + return results + + def create_cartesian_point( + self, x: float, y: float, z: Optional[float] = None, is_model_coords: bool = True + ) -> ifcopenshell.entity_instance: + """Create IfcCartesianPoint. + + x, y, z coords are provided in SI units. + """ + x = self.convert_si_to_unit(x) + y = self.convert_si_to_unit(y) + z = self.convert_si_to_unit(z) + return self.file.createIfcCartesianPoint((x, y, z)) + + def create_curves_from_mesh(self, geom_data) -> list[ifcopenshell.entity_instance]: + curves = [] + points = self.create_cartesian_point_list_from_vertices(geom_data.vertices) + edge_loops = [] + previous_edge = None + edge_loop = [] + for i, edge in enumerate(geom_data.edges): + if previous_edge is None: + edge_loop = [self.file.createIfcLineIndex((edge.vertices[0] + 1, edge.vertices[1] + 1))] + elif edge.vertices[0] == previous_edge.vertices[1]: + edge_loop.append(self.file.createIfcLineIndex((edge.vertices[0] + 1, edge.vertices[1] + 1))) + else: + edge_loops.append(edge_loop) + edge_loop = [self.file.createIfcLineIndex((edge.vertices[0] + 1, edge.vertices[1] + 1))] + previous_edge = edge + edge_loops.append(edge_loop) + for edge_loop in edge_loops: + curves.append(self.file.createIfcIndexedPolyCurve(points, edge_loop)) + return curves + + def create_curves_from_mesh_ifc2x3(self, geom_data) -> list[ifcopenshell.entity_instance]: + curves = [] + points = [self.create_cartesian_point(v.co.x, v.co.y, v.co.z) for v in geom_data.vertices] + edge_loops = [] + previous_edge = None + edge_loop = [] + for i, edge in enumerate(geom_data.edges): + if previous_edge is None: + edge_loop = [edge.vertices] + elif edge.vertices[0] == previous_edge.vertices[1]: + edge_loop.append(edge.vertices) + else: + edge_loops.append(edge_loop) + edge_loop = [edge.vertices] + previous_edge = edge + edge_loops.append(edge_loop) + for edge_loop in edge_loops: + loop_points = [points[p[0]] for p in edge_loop] + loop_points.append(points[edge_loop[-1][1]]) + curves.append(self.file.createIfcPolyline(loop_points)) + return curves + + def create_cartesian_point_list_from_vertices(self, vertices) -> ifcopenshell.entity_instance: + import numpy as np + from ifcopenshell.util.shape_builder import ifc_safe_vector_type + + # Catch values as floats to benefit from fast buffer copy. + coords = np.empty(len(vertices) * 3, dtype="f") + vertices.foreach_get("co", coords) + coords = coords.reshape(-1, 3) + coords_class = "IfcCartesianPointList3D" + return self.file.create_entity(coords_class, ifc_safe_vector_type(self.convert_si_to_unit(coords))) + + def convert_si_to_unit(self, co): + return co / self.unit_scale From b4f2039367ac39076c3e86d3bc5d509406d945b8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 26 Feb 2025 15:55:23 +0500 Subject: [PATCH 154/476] Fix bim.purge_unused_openings poll It was returning true regardless whether tool.Geometry.has_openings returned True or not. --- src/bonsai/bonsai/bim/module/model/opening.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index 319ffdd345..5cceda3bce 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -779,11 +779,15 @@ class PurgeUnusedOpenings(Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - return any( - [tool.Geometry.has_openings(element)] + poll = any( + tool.Geometry.has_openings(element) for element in [tool.Ifc.get_entity(obj) for obj in context.selected_objects] if element ) + if not poll: + cls.poll_message_set("No objects with openings selected.") + return False + return True def _execute(self, context): bpy.ops.bim.show_openings() From 257f0b11f5b3c46aaa78972764e805d0ce214da4 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 26 Feb 2025 15:59:11 +0500 Subject: [PATCH 155/476] Fix typo --- src/bonsai/bonsai/bim/module/debug/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 67ba6e5199..3561a005a1 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -527,7 +527,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator): bl_label = "Purge Unused Elements By Class" bl_description = ( "Will find all elements of class that have no inverse references and will remove them, use very carefully.\n" - "If IFC class is provided in neighbour field, will purge only elemnts of the provided class. Otherwise will purge all white-listed elements.\n" + "If IFC class is provided in neighbour field, will purge only elements of the provided class. Otherwise will purge all white-listed elements.\n" "ALT+CLICK to provide a path where to save the IFC file with the removed elements (note changes will be applied to the current IFC too)" ) bl_options = {"REGISTER", "UNDO"} From 24df52e2f7f7af248300744de2e233372b8b3dd7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 26 Feb 2025 16:36:46 +0500 Subject: [PATCH 156/476] Do not purge orphaned IfcDocumentReferences #6117 --- .../bonsai/bim/module/debug/operator.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 3561a005a1..8fa39b1470 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -513,6 +513,7 @@ class PrintUnusedElementStats(bpy.types.Operator): if self.ignore_styled_items: ignore_classes += ["IfcStyledItem"] ignore_classes += [ + "IfcDocumentReference", # Document references for sheet elements (drawings, schedules, etc). "IfcIndexedColourMap", # Only referenced by inverse attributes. "IfcIndexedTextureMap", # Only referenced by inverse attributes. ] @@ -613,18 +614,30 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator): "IfcUnitAssignment", "IfcVirtualGridIntersection", ] + whitelist_exceptions = { + # Document references for sheet elements (drawings, schedules, etc). + "IfcExternalReference": ("IfcDocumentReference",), + } total_purged = 0 + schema = tool.Ifc.schema() while True: total_batches = 0 print("*" * 100) total_batch_purged = 0 for ifc_class in whitelisted_classes: total_class_purged = 0 + + # Ensure class is present in the schema. try: - elements = tool.Ifc.get().by_type(ifc_class) - except: - continue # Probably not in this schema? + schema.declaration_by_name(ifc_class) + except RuntimeError: + continue + + elements = tool.Ifc.get().by_type(ifc_class) + if ifc_class in whitelist_exceptions: + elements = [e for e in elements if not any(e.is_a(c) for c in whitelist_exceptions[ifc_class])] + to_purge = set() for element in elements: try: From 746a61a199ea4cf18c40da9925693443cb0719c3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 26 Feb 2025 16:49:33 +0500 Subject: [PATCH 157/476] Add tool.Blender.KEY_MODIFIERS instead of storing it at workspaces --- src/bonsai/bonsai/bim/module/cad/workspace.py | 6 +----- src/bonsai/bonsai/bim/module/model/workspace.py | 9 +-------- src/bonsai/bonsai/tool/blender.py | 15 ++++++++++----- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 459fb7ef50..573fef6d4f 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -337,7 +337,7 @@ def add_layout_hotkey_operator( modifier, key = parts op_text = "" if ui_context == "TOOL_HEADER" else text custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id - modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", "")) + modifier_icon, modifier_str = tool.Blender.KEY_MODIFIERS.get(modifier, ("NONE", "")) row = layout if ui_context == "TOOL_HEADER" else layout.row(align=True) op = row.operator("bim.cad_hotkey", text=op_text, icon_value=custom_icon) @@ -357,7 +357,3 @@ def add_layout_hotkey_operator( custom_icon_previews = None display_mode = None - -MODIFIERS = { - "S": ("EVENT_SHIFT", "⇧"), -} diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index d1ea3ec9ea..da2f64faac 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -383,7 +383,7 @@ def add_layout_hotkey_operator( op_text = "" if ui_context == "TOOL_HEADER" else text custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id - modifier_icon, modifier_str = MODIFIERS.get(modifier, ("NONE", "")) + modifier_icon, modifier_str = tool.Blender.KEY_MODIFIERS.get(modifier, ("NONE", "")) row = layout.row(align=True) op = row.operator(operator, text=op_text, icon_value=custom_icon) @@ -1441,10 +1441,3 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): custom_icon_previews = None display_mode = None - -MODIFIERS = { - "A": ("EVENT_ALT", "OPTION" if sys.platform == "Darwin" else "ALT"), - "C": ("EVENT_CTRL", "CTRL"), - "S": ("EVENT_SHIFT", "⇧"), - "E": ("EVENT_PADENTER", "ENTER" if sys.platform == "Darwin" else "RETURN"), -} diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 7a91cce678..da4db4f5d6 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . from __future__ import annotations +import sys import bpy import bmesh import json @@ -490,18 +491,22 @@ class Blender(bonsai.core.tool.Blender): ) return keymap + KEY_MODIFIERS = { + "A": ("EVENT_ALT", "OPTION" if sys.platform == "Darwin" else "ALT"), + "C": ("EVENT_CTRL", "CTRL"), + "S": ("EVENT_SHIFT", "⇧"), + "E": ("EVENT_PADENTER", "ENTER" if sys.platform == "Darwin" else "RETURN"), + } + @classmethod def add_layout_hotkey_operator( cls, tool_name: str, layout: bpy.types.UILayout, text: str, hotkey: str, description: str ) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]: - modifiers = { - "A": "EVENT_ALT", - "S": "EVENT_SHIFT", - } modifier, key = hotkey.split("_") row = layout.row(align=True) - row.label(text="", icon=modifiers[modifier]) + modifier_icon, modifier_str = cls.KEY_MODIFIERS.get(modifier, ("NONE", "")) + row.label(text="", icon=modifier_icon) row.label(text="", icon=f"EVENT_{key}") op = row.operator(f"bim.{tool_name}_hotkey", text=text) From 2d0bd9782a4795a04434c442e9d7b6712d0fbe9a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 26 Feb 2025 17:40:06 +0500 Subject: [PATCH 158/476] Reuse tool.Blender.add_layout_hotkey_operator Now all tools are using a general method and it should be easier to maintain the consistency between the tools. Fixes #6238. ping @trhyder just in case --- src/bonsai/bonsai/bim/module/cad/workspace.py | 26 +-------- .../bonsai/bim/module/covering/workspace.py | 5 +- .../bonsai/bim/module/drawing/workspace.py | 22 ++------ .../bonsai/bim/module/model/workspace.py | 39 +------------- .../bonsai/bim/module/spatial/workspace.py | 5 +- .../bonsai/bim/module/structural/workspace.py | 5 +- src/bonsai/bonsai/tool/blender.py | 53 ++++++++++++++++--- 7 files changed, 59 insertions(+), 96 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 573fef6d4f..0571d0c776 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -23,7 +23,7 @@ import bonsai.bim.module.type.prop as type_prop import ifcopenshell.util.unit from bpy.types import WorkSpaceTool from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData -from typing import Union +from functools import partial def load_custom_icons(): @@ -330,29 +330,7 @@ def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_co row.label(text="Tools") -def add_layout_hotkey_operator( - layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None], ui_context: str = "" -) -> bpy.types.OperatorProperties: - parts = hotkey.split("_") - modifier, key = parts - op_text = "" if ui_context == "TOOL_HEADER" else text - custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id - modifier_icon, modifier_str = tool.Blender.KEY_MODIFIERS.get(modifier, ("NONE", "")) - - row = layout if ui_context == "TOOL_HEADER" else layout.row(align=True) - op = row.operator("bim.cad_hotkey", text=op_text, icon_value=custom_icon) - - if ui_context != "TOOL_HEADER" and len(parts) == 2: - layout = layout.row(align=True) # Create a new line for hotkey display - layout.label(text="", icon=modifier_icon) - layout.label(text="", icon=f"EVENT_{key}") - - hotkey_description = f"Hotkey: {modifier_str} {key}" - description = "\n\n".join(filter(None, [hotkey_description if description else ""])) - op.hotkey = hotkey - op.description = description or hotkey_description - - return op +add_layout_hotkey_operator = partial(tool.Blender.add_layout_hotkey_operator, tool_name="cad", module_name=__name__) custom_icon_previews = None diff --git a/src/bonsai/bonsai/bim/module/covering/workspace.py b/src/bonsai/bonsai/bim/module/covering/workspace.py index 904bd98a71..b0f41254b6 100644 --- a/src/bonsai/bonsai/bim/module/covering/workspace.py +++ b/src/bonsai/bonsai/bim/module/covering/workspace.py @@ -24,6 +24,7 @@ import bonsai.tool as tool from bonsai.bim.helper import prop_with_search from bonsai.bim.module.model.data import AuthoringData from bpy.types import WorkSpaceTool +from functools import partial class CoveringTool(WorkSpaceTool): @@ -45,9 +46,7 @@ class CoveringTool(WorkSpaceTool): CoveringToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) -def add_layout_hotkey(layout: bpy.types.UILayout, text: str, hotkey: str, description: str) -> None: - args = ("covering", layout, text, hotkey, description) - tool.Blender.add_layout_hotkey_operator(*args) +add_layout_hotkey = partial(tool.Blender.add_layout_hotkey_operator, tool_name="covering", module_name=__name__) class CoveringToolUI: diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py index 16d26ea7e0..1296e859d5 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -27,7 +27,7 @@ import ifcopenshell.util.representation from bonsai.bim.module.drawing.data import DecoratorData, AnnotationData from bonsai.bim.helper import prop_with_search from bpy.types import WorkSpaceTool -from typing import Union +from functools import partial class LaunchAnnotationTypeManager(bpy.types.Operator): @@ -135,23 +135,9 @@ class AnnotationTool(WorkSpaceTool): AnnotationToolUI.draw(context, layout) -def add_layout_hotkey_operator( - layout: bpy.types.UILayout, text: str, hotkey: str, description: Union[str, None] -) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]: - modifiers = { - "A": "EVENT_ALT", - "S": "EVENT_SHIFT", - } - modifier, key = hotkey.split("_") - - row = layout.row(align=True) - row.label(text="", icon=modifiers[modifier]) - row.label(text="", icon=f"EVENT_{key}") - - op = row.operator("bim.annotation_hotkey", text=text) - op.hotkey = hotkey - op.description = description - return op, row +add_layout_hotkey_operator = partial( + tool.Blender.add_layout_hotkey_operator, tool_name="annotation", module_name=__name__ +) # TODO: move to operator diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index da2f64faac..d0da684ffa 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -30,6 +30,7 @@ from bonsai.bim.module.model.data import AuthoringData, ItemData from bonsai.bim.module.system.data import PortData from bonsai.bim.module.model.prop import get_ifc_class from typing import Optional, Union +from functools import partial def load_custom_icons(): @@ -365,43 +366,7 @@ class CableTool(BimTool): ifc_element_type = "IfcCableSegmentType" -def add_layout_hotkey_operator( - layout: bpy.types.UILayout, - text: str, - hotkey: str, - description: Union[str, None], - ui_context: str = "", - *, - operator: str = "bim.hotkey", -) -> bpy.types.OperatorProperties: - """ - :param operator: Operator to display in UI. Displaying the specific operator in UI can be useful - to provide poll error messages. - """ - parts = hotkey.split("_") if hotkey else [] - modifier, key = (parts + ["", ""])[:2] - - op_text = "" if ui_context == "TOOL_HEADER" else text - custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id - modifier_icon, modifier_str = tool.Blender.KEY_MODIFIERS.get(modifier, ("NONE", "")) - - row = layout.row(align=True) - op = row.operator(operator, text=op_text, icon_value=custom_icon) - - if ui_context != "TOOL_HEADER": - row.label(text="", icon=modifier_icon) - row.label(text="", icon=f"EVENT_{key}" if key else "BLANK1") - - hotkey_description = f"Hotkey: {modifier_str} {key}".strip() - description = "\n\n".join(filter(None, [description, hotkey_description])) - - if operator == "bim.hotkey": - op.hotkey = hotkey - if ui_context == "TOOL_HEADER": - op.description = text + "\n" + description - else: - op.description = description - return op +add_layout_hotkey_operator = partial(tool.Blender.add_layout_hotkey_operator, tool_name="bim", module_name=__name__) def format_ifc_camel_case(string): diff --git a/src/bonsai/bonsai/bim/module/spatial/workspace.py b/src/bonsai/bonsai/bim/module/spatial/workspace.py index 7853f3a4af..26c91d8037 100644 --- a/src/bonsai/bonsai/bim/module/spatial/workspace.py +++ b/src/bonsai/bonsai/bim/module/spatial/workspace.py @@ -23,6 +23,7 @@ import bonsai.tool as tool from bonsai.bim.module.model.data import AuthoringData from bpy.types import WorkSpaceTool import bonsai.core.spatial +from functools import partial class SpatialTool(WorkSpaceTool): @@ -49,9 +50,7 @@ class SpatialTool(WorkSpaceTool): SpatialToolUI.draw(context, layout) -def add_layout_hotkey(layout: bpy.types.UILayout, text: str, hotkey: str, description: str) -> None: - args = ("spatial", layout, text, hotkey, description) - tool.Blender.add_layout_hotkey_operator(*args) +add_layout_hotkey = partial(tool.Blender.add_layout_hotkey_operator, tool_name="spatial", module_name=__name__) class SpatialToolUI: diff --git a/src/bonsai/bonsai/bim/module/structural/workspace.py b/src/bonsai/bonsai/bim/module/structural/workspace.py index cec8115a2d..4991150133 100644 --- a/src/bonsai/bonsai/bim/module/structural/workspace.py +++ b/src/bonsai/bonsai/bim/module/structural/workspace.py @@ -21,6 +21,7 @@ import os import bpy import bonsai.tool as tool from bpy.types import WorkSpaceTool +from functools import partial class StructuralTool(WorkSpaceTool): @@ -41,9 +42,7 @@ class StructuralTool(WorkSpaceTool): StructuralToolUI.draw(context, layout) -def add_layout_hotkey(layout: bpy.types.UILayout, text: str, hotkey: str, description: str) -> None: - args = ("structural", layout, text, hotkey, description) - tool.Blender.add_layout_hotkey_operator(*args) +add_layout_hotkey = partial(tool.Blender.add_layout_hotkey_operator, tool_name="structural", module_name=__name__) # NOTES before adding new operators: diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index da4db4f5d6..6ea77101a1 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -500,18 +500,55 @@ class Blender(bonsai.core.tool.Blender): @classmethod def add_layout_hotkey_operator( - cls, tool_name: str, layout: bpy.types.UILayout, text: str, hotkey: str, description: str + cls, + layout: bpy.types.UILayout, + text: str, + hotkey: str, + description: str, + ui_context: str = "", + *, + tool_name: str, + module_name: str, + operator: Optional[str] = None, ) -> tuple[bpy.types.OperatorProperties, bpy.types.UILayout]: + """ + :param module_name: Provide `__name__` of the current module, + so method could pick up icon previews based on the module's `custom_icon_previews` attribute. + :param operator: Operator to display in UI. Displaying the specific operator in UI can be useful + to provide poll error messages. + """ + if tool_name == "bim": + hotkey_operator = "bim.hotkey" + else: + hotkey_operator = f"bim.{tool_name}_hotkey" + operator_to_use = operator or hotkey_operator + modifier, key = hotkey.split("_") - - row = layout.row(align=True) + op_text = "" if ui_context == "TOOL_HEADER" else text modifier_icon, modifier_str = cls.KEY_MODIFIERS.get(modifier, ("NONE", "")) - row.label(text="", icon=modifier_icon) - row.label(text="", icon=f"EVENT_{key}") - op = row.operator(f"bim.{tool_name}_hotkey", text=text) - op.hotkey = hotkey - op.description = description + row = layout if ui_context == "TOOL_HEADER" else layout.row(align=True) + module = sys.modules[module_name] + icon_previews: Union[bpy.utils.previews.ImagePreviewCollection, None] + icon_previews = getattr(module, "custom_icon_previews", None) + if icon_previews: + custom_icon = icon_previews.get(text.upper().replace(" ", "_"), icon_previews["IFC"]).icon_id + op = row.operator(operator_to_use, text=op_text, icon_value=custom_icon) + else: + op = row.operator(operator_to_use, text=op_text) + if ui_context != "TOOL_HEADER": + row.label(text="", icon=modifier_icon) + row.label(text="", icon=f"EVENT_{key}") + + if operator_to_use == hotkey_operator: + hotkey_description = f"Hotkey: {modifier_str} {key}".strip() + description = "\n\n".join(filter(None, [description, hotkey_description])) + + op.hotkey = hotkey + if ui_context == "TOOL_HEADER": + op.description = text + "\n" + description + else: + op.description = description return op, row @classmethod From 26b2a7d5843deb2cd787175890f22207cb0dcf76 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 27 Feb 2025 18:14:01 +0500 Subject: [PATCH 159/476] typing --- .../bonsai/bim/module/patch/operator.py | 24 ++++++++++++------- src/bonsai/bonsai/bim/module/patch/prop.py | 12 ++++++++-- src/bonsai/bonsai/bim/module/patch/ui.py | 3 +-- src/bonsai/bonsai/bim/operator.py | 8 ++++++- src/bonsai/bonsai/tool/patch.py | 10 +++++++- 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/patch/operator.py b/src/bonsai/bonsai/bim/module/patch/operator.py index 6438b57246..280724cb00 100644 --- a/src/bonsai/bonsai/bim/module/patch/operator.py +++ b/src/bonsai/bonsai/bim/module/patch/operator.py @@ -25,7 +25,10 @@ import bonsai.tool as tool import bonsai.core.patch as core import bonsai.bim.handler from pathlib import Path -from typing import cast +from typing import cast, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.prop import AttributeDataType class SelectIfcPatchInput(bpy.types.Operator): @@ -36,7 +39,8 @@ class SelectIfcPatchInput(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - context.scene.BIMPatchProperties.ifc_patch_input = self.filepath + props = tool.Patch.get_patch_props() + props.ifc_patch_input = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -52,7 +56,8 @@ class SelectIfcPatchOutput(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - context.scene.BIMPatchProperties.ifc_patch_output = self.filepath + props = tool.Patch.get_patch_props() + props.ifc_patch_output = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -67,7 +72,7 @@ class ExecuteIfcPatch(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.BIMPatchProperties + props = tool.Patch.get_patch_props() if props.ifc_patch_recipes == "-": cls.poll_message_set("No recipe selected.") return False @@ -77,7 +82,7 @@ class ExecuteIfcPatch(bpy.types.Operator): return True def execute(self, context): - props = context.scene.BIMPatchProperties + props = tool.Patch.get_patch_props() recipe_name = props.ifc_patch_recipes arguments = [] @@ -121,7 +126,7 @@ class UpdateIfcPatchArguments(bpy.types.Operator): if self.recipe == "-": print("No Recipe Selected. Impossible to load arguments") return {"FINISHED"} - patch_args = context.scene.BIMPatchProperties.ifc_patch_args_attr + patch_args = tool.Patch.get_patch_props().ifc_patch_args_attr patch_args.clear() docs = ifcpatch.extract_docs(self.recipe, "Patcher", "__init__", ("src", "file", "logger", "args")) if docs and "inputs" in docs: @@ -141,14 +146,15 @@ class UpdateIfcPatchArguments(bpy.types.Operator): data_type = [dt for dt in data_type if dt != "NoneType"][0] - new_attr.data_type = { + data_types: dict[str, AttributeDataType] = { "Literal": "enum", "file": "file", "str": "string", "float": "float", "int": "integer", "bool": "boolean", - }[data_type] + } + new_attr.data_type = data_types[data_type] new_attr.name = self.pretty_arg_name(arg_name) if new_attr.data_type == "enum": new_attr.enum_items = json.dumps(arg_info.get("enum_items", [])) @@ -211,7 +217,7 @@ class ExtractSelectedElements(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BIMPatchProperties + props = tool.Patch.get_patch_props() recipe_name = props.ifc_patch_recipes if recipe_name != "ExtractElements": diff --git a/src/bonsai/bonsai/bim/module/patch/prop.py b/src/bonsai/bonsai/bim/module/patch/prop.py index f491cb7ed4..e12da9e722 100644 --- a/src/bonsai/bonsai/bim/module/patch/prop.py +++ b/src/bonsai/bonsai/bim/module/patch/prop.py @@ -33,6 +33,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Literal, Union ifcpatchrecipes_enum = [] @@ -43,7 +44,7 @@ def purge(): ifcpatchrecipes_enum = [] -def get_ifcpatch_recipes(self, context): +def get_ifcpatch_recipes(self: "BIMPatchProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: global ifcpatchrecipes_enum if len(ifcpatchrecipes_enum) < 1: # Have to add a blank entry because otherwise default recipe might be not loaded @@ -61,7 +62,7 @@ def get_ifcpatch_recipes(self, context): return ifcpatchrecipes_enum -def update_ifc_patch_recipe(self, context): +def update_ifc_patch_recipe(self: "BIMPatchProperties", context: bpy.types.Context) -> None: bpy.ops.bim.update_ifc_patch_arguments(recipe=self.ifc_patch_recipes) @@ -75,3 +76,10 @@ class BIMPatchProperties(PropertyGroup): name="Load from Memory", description="Use IFC file currently loaded in Bonsai", ) + + if TYPE_CHECKING: + ifc_patch_recipes_enum: Union[Literal["-"], str] + ifc_patch_input: str + ifc_patch_output: str + ifc_patch_args_attr: bpy.types.bpy_prop_collection_idprop[Attribute] + should_load_from_memory: bool diff --git a/src/bonsai/bonsai/bim/module/patch/ui.py b/src/bonsai/bonsai/bim/module/patch/ui.py index 539544055d..03822642a9 100644 --- a/src/bonsai/bonsai/bim/module/patch/ui.py +++ b/src/bonsai/bonsai/bim/module/patch/ui.py @@ -36,8 +36,7 @@ class BIM_PT_patch(bpy.types.Panel): layout.use_property_split = True layout.use_property_decorate = False - scene = context.scene - props = scene.BIMPatchProperties + props = tool.Patch.get_patch_props() row = layout.row() prop_with_search(row, props, "ifc_patch_recipes") diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 9b0d38c5ab..1690b9497f 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -38,7 +38,10 @@ from mathutils import Vector, Euler from math import radians from pathlib import Path from collections import namedtuple -from typing import List, Iterable, Union +from typing import List, Iterable, Union, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.prop import MultipleFileSelect class SetTab(bpy.types.Operator): @@ -186,6 +189,9 @@ class BIM_OT_multiple_file_selector(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") + if TYPE_CHECKING: + file_props: MultipleFileSelect + @classmethod def poll(cls, context): return getattr(context, "file_props", None) is not None diff --git a/src/bonsai/bonsai/tool/patch.py b/src/bonsai/bonsai/tool/patch.py index 356a5001de..dcfea3bf64 100644 --- a/src/bonsai/bonsai/tool/patch.py +++ b/src/bonsai/bonsai/tool/patch.py @@ -16,14 +16,22 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import ifcopenshell import ifcpatch import bonsai.core.tool -from typing import Any +from typing import Any, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.patch.prop import BIMPatchProperties class Patch(bonsai.core.tool.Patch): + @classmethod + def get_patch_props(cls) -> BIMPatchProperties: + return bpy.context.scene.BIMPatchProperties + @classmethod def run_migrate_patch(cls, infile: str, outfile: str, schema: str) -> None: output = ifcpatch.execute( From 497542d7a0d1a66688b00e6658fccf381d944270 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 27 Feb 2025 15:40:24 +0500 Subject: [PATCH 160/476] ifc2sql - small optimizations --- src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py | 32 +++++++++++++----------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index 04d3cfa6ce..274b9fcb49 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -231,20 +231,22 @@ class Patcher: checkpoint = time.time() shape = iterator.get() if shape: - if shape.geometry.id not in self.geometry_rows: - v = np.array(shape.geometry.verts).tobytes() - e = np.array(shape.geometry.edges).tobytes() - f = np.array(shape.geometry.faces).tobytes() - mids = np.array(shape.geometry.material_ids).tobytes() + shape_id = shape.id + geometry_id = shape.geometry.id + if geometry_id not in self.geometry_rows: + geometry: ShapeType + geometry = shape.geometry + v = geometry.verts_buffer + e = geometry.edges_buffer + f = geometry.faces_buffer + mids = geometry.material_ids_buffer m = json.dumps([m.instance_id() for m in shape.geometry.materials]) - self.geometry_rows[shape.geometry.id] = [shape.geometry.id, v, e, f, mids, m] + self.geometry_rows[geometry_id] = [geometry_id, v, e, f, mids, m] # Copy required since otherwise it is read-only m = ifcopenshell.util.shape.get_shape_matrix(shape).copy() - m[0][3] /= self.unit_scale - m[1][3] /= self.unit_scale - m[2][3] /= self.unit_scale - x, y, z = m[:, 3][0:3] - self.shape_rows[shape.id] = [shape.id, float(x), float(y), float(z), m.tobytes(), shape.geometry.id] + m[:3, 3] /= self.unit_scale + x, y, z = m[:, 3][0:3].tolist() + self.shape_rows[shape_id] = [shape_id, x, y, z, m.tobytes(), geometry_id] if not iterator.next(): break @@ -469,10 +471,10 @@ class Patcher: pset_rows.append([element.id(), pset_name, prop_name, value]) if self.should_get_geometry: - if element.id() not in self.shape_rows and getattr(element, "ObjectPlacement", None): - m = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) - x, y, z = m[:, 3][0:3] - self.shape_rows[element.id()] = [element.id(), float(x), float(y), float(z), m.tobytes(), None] + if element.id() not in self.shape_rows and (placement := getattr(element, "ObjectPlacement", None)): + m = ifcopenshell.util.placement.get_local_placement(placement) + x, y, z = m[:, 3][0:3].tolist() + self.shape_rows[element.id()] = [element.id(), x, y, z, m.tobytes(), None] if self.sql_type == "sqlite": if rows: From 0bd608b6cbd3c64d99acd3ad3a98671cba52b438 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 27 Feb 2025 17:37:32 +0500 Subject: [PATCH 161/476] Bonsai - ifcpatch description to include arguments descriptions Also fix a bug with broken patch descriptions if some argument had more than one line for the description. Example - https://i.imgur.com/1syL4gJ.png Notcied working on #6227 --- src/bonsai/bonsai/bim/module/patch/prop.py | 15 +- src/ifcpatch/ifcpatch/__init__.py | 161 +++++++++++++++++---- 2 files changed, 145 insertions(+), 31 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/patch/prop.py b/src/bonsai/bonsai/bim/module/patch/prop.py index e12da9e722..3d372e5036 100644 --- a/src/bonsai/bonsai/bim/module/patch/prop.py +++ b/src/bonsai/bonsai/bim/module/patch/prop.py @@ -36,7 +36,7 @@ from bpy.props import ( from typing import TYPE_CHECKING, Literal, Union -ifcpatchrecipes_enum = [] +ifcpatchrecipes_enum: list[tuple[str, str, str]] = [] def purge(): @@ -57,7 +57,18 @@ def get_ifcpatch_recipes(self: "BIMPatchProperties", context: bpy.types.Context) if f == "__init__": continue docs = ifcpatch.extract_docs(f, "Patcher", "__init__", ("src", "file", "logger", "args")) - ifcpatchrecipes_enum.append((f, f, docs.get("description", "") if docs else "")) + if docs is None: + description = "" + else: + description = docs["description"] + inputs = docs["inputs"] + if inputs: + if description: + description += "\n\n" + description += "Parameters:" + for param, input_data in inputs.items(): + description += f"\n\n- {param}: {input_data['description']}" + ifcpatchrecipes_enum.append((f, f, description)) ifcpatchrecipes_enum.sort(key=lambda x: x[0]) return ifcpatchrecipes_enum diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py index e949379a03..8303e90a3a 100644 --- a/src/ifcpatch/ifcpatch/__init__.py +++ b/src/ifcpatch/ifcpatch/__init__.py @@ -28,6 +28,7 @@ import inspect import collections import importlib import importlib.util +import re from typing import Union, Iterable, Optional, Any, TypedDict, Literal, Sequence from typing_extensions import NotRequired @@ -133,7 +134,7 @@ def write(output: Union[ifcopenshell.file, str], filepath: str) -> None: def extract_docs( submodule_name: str, cls_name: str, method_name: str = "__init__", boilerplate_args: Optional[Iterable[str]] = None -) -> Union[dict[str, Any], None]: +) -> Union["PatcherDoc", None]: """Extract class docstrings and method arguments :param submodule_name: Submodule from which to extract the class @@ -155,10 +156,26 @@ def extract_docs( print(f"Error : IFCPatch {str(submodule)} could not load because : {str(e)}") -def _extract_docs(cls: type, method_name: str, boilerplate_args: Union[Iterable[str], None]) -> dict[str, Any]: - inputs = collections.OrderedDict() +class PatcherDoc(TypedDict): + class_: type + description: str + output: Union[str, None] + inputs: dict[str, "InputDoc"] + + +class InputDoc(TypedDict): + name: str + description: str + type: Union[str, list[str]] + default: NotRequired[Any] + generic_type: NotRequired[str] + enum_items: NotRequired[list[str]] + filter_glob: NotRequired[str] + + +def _extract_docs(cls: type, method_name: str, boilerplate_args: Union[Iterable[str], None]) -> PatcherDoc: + inputs: dict[str, InputDoc] = {} method = getattr(cls, method_name) - docs: dict[str, Any] = {"class": cls} if boilerplate_args is None: boilerplate_args = [] @@ -166,9 +183,10 @@ def _extract_docs(cls: type, method_name: str, boilerplate_args: Union[Iterable[ for name, parameter in signature.parameters.items(): if name == "self" or name in boilerplate_args: continue - inputs[name] = {"name": name} + input_doc: InputDoc = {"name": name} + inputs[name] = input_doc if isinstance(parameter.default, (str, float, int, bool)): - inputs[name]["default"] = parameter.default + input_doc["default"] = parameter.default # Parse data from type hints. type_hints = typing.get_type_hints(method) @@ -193,28 +211,113 @@ def _extract_docs(cls: type, method_name: str, boilerplate_args: Union[Iterable[ # Parse the docstring. description = "" - doc = method.__doc__ - if doc is not None: - for i, line in enumerate(doc.split("\n")): - line = line.strip() - if i == 0: - docs["name"] = line - elif line.startswith(":return:"): - docs["output"] = {"name": line.split(":")[2].strip(), "description": line.split(":")[3].strip()} - elif line.startswith(":param"): - param_name = line.split(":")[1].strip().replace("param ", "") - if param_name in inputs: - inputs[param_name]["description"] = line.split(":")[2].strip() - # :filter_glob is our special doc-tag. - elif line.startswith(":filter_glob"): - param_name = line.split(":")[1].strip().replace("filter_glob ", "") - if param_name in inputs: - inputs[param_name]["filter_glob"] = line.split(":")[2].strip() - elif i == 2: - description += line - elif i > 2: - description += "\n" + line + # `getdoc` instead of `__doc__` for sane indentation. + doc = inspect.getdoc(method) - docs["description"] = description.strip() - docs["inputs"] = inputs + def is_valid_param_name(param_name: str) -> bool: + if param_name not in inputs: + print( + f"WARNING. Unexpected param name '{param_name}' in {cls.__name__} docstring (missing from signature)." + ) + return False + return True + + if doc is None: + doc_description = "" + doc_output = None + else: + docstring_data = parse_docstring(doc) + doc_description = docstring_data["description"] + doc_output = docstring_data["output"] + + for param_name in docstring_data["param"]: + if not is_valid_param_name(param_name): + continue + inputs[param_name]["description"] = docstring_data["param"][param_name] + + for param_name in docstring_data["filter_glob"]: + if not is_valid_param_name(param_name): + continue + inputs[param_name]["filter_glob"] = docstring_data["filter_glob"][param_name] + + for param_name in inputs: + if "description" not in inputs[param_name]: + inputs[param_name]["description"] = "Undocumented" + + docs = PatcherDoc( + class_=cls, + description=doc_description, + output=doc_output, + inputs=inputs, + ) return docs + + +class DocstringData(TypedDict): + name: str + description: str + param: dict[str, str] + filter_glob: dict[str, str] + output: Union[str, None] + + +def parse_docstring(docstring: str) -> DocstringData: + # Keep left indentation to recognize the sections. + lines = docstring.split("\n") + result = DocstringData( + name=lines[0].strip(), + description="", + param={}, + filter_glob={}, + output=None, + ) + + current_section = None + last_param = None + + PREFIXES = ("param", "filter_glob") + + for line in lines[1:]: + if line.startswith(":"): + line = line[1:] + if line.startswith(PREFIXES): + prefix = line.split(" ")[0] + current_section = prefix + match_ = re.match(rf"{prefix}\s+(\w+):\s+(.*)", line) + assert match_, f"Invalid line: '{line}'." + param_name, param_desc = match_.groups() + result[prefix][param_name] = param_desc + last_param = param_name + continue + elif line.startswith("return:"): + current_section = "output" + match_ = re.match(r"return:\s+(.*)", line) + assert match_ + result["output"] = match_.groups()[0] + continue + elif line.startswith("type"): + # Ignore types in favor of signature annotations. + continue + elif line.startswith("Example:"): + # Ignore code example at the end of the docstring. + break + + # Multiline sections start with indentation. + if line.startswith(" ") and current_section: + line = line.lstrip() + if current_section == "output": + assert result["output"] + result["output"] += f"\n{line}" + elif current_section in PREFIXES: + assert last_param + result[current_section][last_param] += f"\n{line}" + continue + + line = line.lstrip() + result["description"] += f"\n{line}" + current_section = None + last_param = None + + result["description"] = result["description"].strip() + + return result From 9055f14bb9fab6554602d8d7a7980c9acdcdec0b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 27 Feb 2025 18:16:23 +0500 Subject: [PATCH 162/476] Ifc2Sql - hide IFC output argument as there's already database argument #6227 Also handle directory paths more gracefully. --- src/bonsai/bonsai/tool/patch.py | 5 +- src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py | 92 ++++++++++++++---------- 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/src/bonsai/bonsai/tool/patch.py b/src/bonsai/bonsai/tool/patch.py index dcfea3bf64..54e94fceae 100644 --- a/src/bonsai/bonsai/tool/patch.py +++ b/src/bonsai/bonsai/tool/patch.py @@ -46,7 +46,10 @@ class Patch(bonsai.core.tool.Patch): @classmethod def does_patch_has_output(cls, recipe: str) -> bool: - return recipe != "SplitByBuildingStorey" + return recipe not in ( + "Ifc2Sql", + "SplitByBuildingStorey", + ) @classmethod def post_process_patch_arguments(cls, recipe: str, args: list[Any]) -> list[Any]: diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index 274b9fcb49..3bbc156c92 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -34,21 +34,29 @@ import ifcopenshell.util.placement import ifcopenshell.util.schema import ifcopenshell.util.shape import ifcopenshell.util.unit -from typing import Any +from pathlib import Path +from ifcopenshell.geom import ShapeType +from typing import Any, TYPE_CHECKING, Literal SQLTypes = typing.Literal["SQLite", "MySQL"] -try: +if TYPE_CHECKING: import sqlite3 -except: - print("No SQLite support") - SQLTypes = typing.Literal["MySQL"] - -try: import mysql.connector -except: - print("No MySQL support") - SQLTypes = typing.Literal["SQLite"] +else: + try: + import sqlite3 + except: + print("No SQLite support") + SQLTypes = typing.Literal["MySQL"] + + try: + import mysql.connector + except: + print("No MySQL support") + SQLTypes = typing.Literal["SQLite"] + +DEFAULT_DATABASE_NAME = "database" class Patcher: @@ -60,7 +68,7 @@ class Patcher: host: str = "localhost", username: str = "root", password: str = "pass", - database: str = "test", + database: str = f"{DEFAULT_DATABASE_NAME}.db", full_schema: bool = True, is_strict: bool = False, should_expand: bool = False, @@ -71,29 +79,28 @@ class Patcher: ): """Convert an IFC-SPF model to SQLite or MySQL. - There are certain controls which are hardcoded in this recipe that you - may modify, including: - - - full_schema: if True, will create tables for all IFC classes, - regardless if they are used or not in the dataset. If False, will - only create tables for classes in the dataset. - - is_strict: whether or not to enforce null or not null. If your - dataset might contain invalid data, set this to False. - - should_expand: if True, entities with attributes containing lists of - entities will be separated into multiple rows. This means the ifc_id - is no longer a unique primary key. If False, lists will be stored as - JSON. - - should_get_psets: if True, a separate psets table will be created to - make it easy to query properties. This is in addition to regular IFC - tables like IfcPropertySet. - - should_get_geometry: Whether or not to process and store explicit - geometry data as a blob in a separate geometry and shape table. - - should_skip_geometry_data: Whether or not to also create tables for - IfcRepresentation and IfcRepresentationItem classes. These tables are - unnecessary if you are not interested in geometry. - :param sql_type: Choose between "SQLite" or "MySQL" - :type sql_type: typing.Literal["SQLite", "MySQL"] + :param database: Database path to save the SQL database to (already existing or not). + Could also be a directory, then the database will be stored + using default filename (e.g. 'database.db'). + :param full_schema: if True, will create tables for all IFC classes, + regardless if they are used or not in the dataset. If False, will + only create tables for classes in the dataset. + :param is_strict: whether or not to enforce null or not null. If your + dataset might contain invalid data, set this to False. + :param should_expand: if True, entities with attributes containing lists of + entities will be separated into multiple rows. This means the ifc_id + is no longer a unique primary key. If False, lists will be stored as + JSON. + :param should_get_psets: if True, a separate psets table will be created to + make it easy to query properties. This is in addition to regular IFC + tables like IfcPropertySet. + :param should_get_geometry: Whether or not to process and store explicit + geometry data as a blob in a separate geometry and shape table. + :param should_skip_geometry_data: Whether or not to also create tables for + IfcRepresentation and IfcRepresentationItem classes. These tables are + unnecessary if you are not interested in geometry. + Example: @@ -106,7 +113,7 @@ class Patcher: """ self.file = file self.logger = logger - self.sql_type = sql_type.lower() + self.sql_type: Literal["sqlite", "mysql"] = sql_type.lower() self.host = host self.username = username self.password = password @@ -122,19 +129,26 @@ class Patcher: self.should_skip_geometry_data = should_skip_geometry_data def patch(self) -> None: + suffix = ".db" if self.sql_type == "SQLite" else ".sqlite" + database = Path(self.database) + if database.is_dir(): + database = (database / DEFAULT_DATABASE_NAME).with_suffix(suffix) + elif not database.parent.exists(): + database.parent.mkdir(parents=True, exist_ok=True) + else: + # Assume it's a filepath - existing or not. + pass + self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(self.file.schema) if self.sql_type == "sqlite": - db_file = self.database # Use the given datapath - self.db = sqlite3.connect(db_file) + self.db = sqlite3.connect(database) self.c = self.db.cursor() - self.file_patched = db_file elif self.sql_type == "mysql": self.db = mysql.connector.connect( - host=self.host, user=self.username, password=self.password, database=self.database + host=self.host, user=self.username, password=self.password, database=str(database) ) self.c = self.db.cursor() - self.file_patched = None self.create_id_map() self.create_metadata() From 62d53ba62d3a99a5d7f55fe462b38c700b7e30af Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 27 Feb 2025 18:23:33 +0500 Subject: [PATCH 163/476] Bonsai - recognize ifc2sql `database` argument as filepath #6227 --- src/bonsai/bonsai/bim/module/patch/operator.py | 5 +++-- src/bonsai/bonsai/tool/patch.py | 4 ++-- src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/patch/operator.py b/src/bonsai/bonsai/bim/module/patch/operator.py index 280724cb00..66c77eae35 100644 --- a/src/bonsai/bonsai/bim/module/patch/operator.py +++ b/src/bonsai/bonsai/bim/module/patch/operator.py @@ -136,12 +136,13 @@ class UpdateIfcPatchArguments(bpy.types.Operator): new_attr = patch_args.add() data_type = arg_info.get("type", "str") - if tool.Patch.is_filepath_argument(self.recipe, arg_name): + is_filepath_argument = tool.Patch.is_filepath_argument(arg_info) + if is_filepath_argument: data_type = "file" new_attr.metadata = "single_file" if isinstance(data_type, list): - if "file" in data_type or tool.Patch.is_filepath_argument(self.recipe, arg_name): + if "file" in data_type or is_filepath_argument: data_type = ["file"] data_type = [dt for dt in data_type if dt != "NoneType"][0] diff --git a/src/bonsai/bonsai/tool/patch.py b/src/bonsai/bonsai/tool/patch.py index 54e94fceae..ad12d65e41 100644 --- a/src/bonsai/bonsai/tool/patch.py +++ b/src/bonsai/bonsai/tool/patch.py @@ -40,9 +40,9 @@ class Patch(bonsai.core.tool.Patch): ifcpatch.write(output, outfile) @classmethod - def is_filepath_argument(cls, recipe: str, arg_name: str) -> bool: + def is_filepath_argument(cls, arg_info: ifcpatch.InputDoc) -> bool: # There is probably a more explicit way to do this - return "filepath" in arg_name + return "filepath" in arg_info["name"] or "filter_glob" in arg_info @classmethod def does_patch_has_output(cls, recipe: str) -> bool: diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index 3bbc156c92..3eeeeb1f47 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -83,6 +83,7 @@ class Patcher: :param database: Database path to save the SQL database to (already existing or not). Could also be a directory, then the database will be stored using default filename (e.g. 'database.db'). + :filter_glob database: *.db,*.sqlite :param full_schema: if True, will create tables for all IFC classes, regardless if they are used or not in the dataset. If False, will only create tables for classes in the dataset. From 9187f2d2b50c4aa5a890fddc5527d46b750306c4 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 10:59:59 +0500 Subject: [PATCH 164/476] Bonsai patcher - small fix for cases when Blender restarts --- src/bonsai/bonsai/bim/module/patch/operator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/patch/operator.py b/src/bonsai/bonsai/bim/module/patch/operator.py index 66c77eae35..0c2c868f7a 100644 --- a/src/bonsai/bonsai/bim/module/patch/operator.py +++ b/src/bonsai/bonsai/bim/module/patch/operator.py @@ -96,7 +96,7 @@ class ExecuteIfcPatch(bpy.types.Operator): arguments = tool.Patch.post_process_patch_arguments(recipe_name, arguments) args = ifcpatch.ArgumentsDict( - recipe=props.ifc_patch_recipes, + recipe=recipe_name, arguments=arguments, log=tool.Blender.get_data_dir_path("process.log").__str__(), ) @@ -111,9 +111,9 @@ class ExecuteIfcPatch(bpy.types.Operator): ifc_patch_output = props.ifc_patch_output or props.ifc_patch_input output = ifcpatch.execute(args) - if tool.Patch.does_patch_has_output(props.ifc_patch_recipes): + if tool.Patch.does_patch_has_output(recipe_name): ifcpatch.write(output, ifc_patch_output) - self.report({"INFO"}, f"{props.ifc_patch_recipes} patch executed successfully") + self.report({"INFO"}, f"{recipe_name} patch executed successfully") return {"FINISHED"} From 277aaca6c5ed0d5a7d3e0e05943e7d47f1c6b68c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 11:08:42 +0500 Subject: [PATCH 165/476] typing --- src/bonsai/bonsai/bim/handler.py | 2 +- src/bonsai/bonsai/bim/module/cad/workspace.py | 4 +- .../bonsai/bim/module/debug/operator.py | 8 +- .../bonsai/bim/module/material/operator.py | 18 ++--- src/bonsai/bonsai/bim/module/model/prop.py | 12 +-- src/bonsai/bonsai/bim/module/model/window.py | 1 + .../bonsai/bim/module/project/operator.py | 7 +- .../bonsai/bim/module/style/operator.py | 37 +++++---- src/bonsai/bonsai/bim/module/style/prop.py | 76 +++++++++++++++---- src/bonsai/bonsai/bim/module/style/ui.py | 6 +- src/bonsai/bonsai/tool/model.py | 6 +- src/bonsai/bonsai/tool/style.py | 62 +++++++++------ src/bonsai/test/tool/test_model.py | 7 +- src/bonsai/test/tool/test_style.py | 36 +++++---- src/ifcpatch/ifcpatch/__init__.py | 4 +- 15 files changed, 184 insertions(+), 102 deletions(-) diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index e958098c16..9cea2cca2e 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -289,7 +289,7 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None def viewport_shading_changed_callback(area: bpy.types.Area) -> None: shading = area.spaces.active.shading.type if shading == "RENDERED": - bpy.context.scene.BIMStylesProperties.active_style_type = "External" + tool.Style.get_style_props().active_style_type = "Internal" def subscribe_to_viewport_shading_changes(): diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 0571d0c776..07d6a597c5 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -311,7 +311,9 @@ class CadHotkey(bpy.types.Operator): bpy.ops.bim.reset_vertex() -def add_header_apply_button(layout, text, apply_operator, cancel_operator, ui_context=""): +def add_header_apply_button( + layout: bpy.types.UILayout, text: str, apply_operator: str, cancel_operator: str, ui_context: str = "" +) -> None: custom_icon = custom_icon_previews.get(text.upper().replace(" ", "_"), custom_icon_previews["IFC"]).icon_id row = layout.row(align=True) row.label(text=f"{text} Mode", icon_value=custom_icon) diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index 8fa39b1470..c68bedbeb0 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -704,13 +704,13 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator): if purged == 0: return - scene = context.scene if object_type == "PROFILE": props = tool.Profile.get_profile_props() if props.is_editing: bpy.ops.bim.load_profiles() elif object_type == "STYLE": - if scene.BIMStylesProperties.is_editing: + props = tool.Style.get_style_props() + if props.is_editing: bpy.ops.bim.load_styles() elif object_type == "MATERIAL": props = tool.Material.get_material_props() @@ -754,13 +754,13 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator): if merged == 0: return - scene = context.scene if object_type == "PROFILE": props = tool.Profile.get_profile_props() if props.is_editing: bpy.ops.bim.load_profiles() elif object_type == "STYLE": - if scene.BIMStylesProperties.is_editing: + props = tool.Style.get_style_props() + if props.is_editing: bpy.ops.bim.load_styles() elif object_type == "MATERIAL": props = tool.Material.get_material_props() diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 3e9ff20924..80f6aafffc 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -576,30 +576,30 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator): material_set = self.file.by_id(self.material_set) attributes = bonsai.bim.helper.export_attributes(props.material_set_attributes) - ifcopenshell.api.run( - "material.edit_assigned_material", + ifcopenshell.api.material.edit_assigned_material( self.file, - **{"element": material_set, "attributes": attributes}, + element=material_set, + attributes=attributes, ) if self.material_set_usage: material_set_usage = self.file.by_id(self.material_set_usage) attributes = bonsai.bim.helper.export_attributes(props.material_set_usage_attributes) if material_set_usage.is_a("IfcMaterialLayerSetUsage"): - ifcopenshell.api.run( - "material.edit_layer_usage", + ifcopenshell.api.material.edit_layer_usage( self.file, - **{"usage": material_set_usage, "attributes": attributes}, + usage=material_set_usage, + attributes=attributes, ) slab.DumbSlabPlaner().regenerate_from_layer_set(material_set_usage.ForLayerSet) wall.DumbWallPlaner().regenerate_from_layer_set(material_set_usage.ForLayerSet) elif material_set_usage.is_a("IfcMaterialProfileSetUsage"): if attributes.get("CardinalPoint", None): attributes["CardinalPoint"] = int(attributes["CardinalPoint"]) - ifcopenshell.api.run( - "material.edit_profile_usage", + ifcopenshell.api.material.edit_profile_usage( self.file, - **{"usage": material_set_usage, "attributes": attributes}, + usage=material_set_usage, + attributes=attributes, ) bpy.ops.bim.disable_editing_assigned_material(obj=active_obj.name) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index a8e486c2a6..d02adfcf8d 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -28,7 +28,7 @@ from math import pi, radians from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator from bonsai.bim.module.model.door import update_door_modifier_bmesh from bonsai.bim.module.model.window import update_window_modifier_bmesh -from typing import TYPE_CHECKING, Literal, get_args, Union, get_args +from typing import TYPE_CHECKING, Literal, get_args, Union, get_args, Any, Optional def get_ifc_class(self: "BIMModelProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: @@ -607,7 +607,7 @@ class BIMWindowProperties(PropertyGroup): framing_material: str glazing_material: str - def get_general_kwargs(self, convert_to_project_units=False): + def get_general_kwargs(self, convert_to_project_units: bool = False) -> dict[str, Any]: kwargs = { "window_type": self.window_type, "overall_height": self.overall_height, @@ -617,7 +617,9 @@ class BIMWindowProperties(PropertyGroup): return kwargs return tool.Model.convert_data_to_project_units(kwargs, ["window_type"]) - def get_lining_kwargs(self, window_type=None, convert_to_project_units=False): + def get_lining_kwargs( + self, window_type: Optional[WindowType] = None, convert_to_project_units: bool = False + ) -> dict[str, Any]: if not window_type: window_type = self.window_type kwargs = { @@ -660,7 +662,7 @@ class BIMWindowProperties(PropertyGroup): return kwargs return tool.Model.convert_data_to_project_units(kwargs) - def get_panel_kwargs(self, convert_to_project_units=False): + def get_panel_kwargs(self, convert_to_project_units: bool = False) -> dict[str, Any]: kwargs = { "frame_depth": self.frame_depth, "frame_thickness": self.frame_thickness, @@ -669,7 +671,7 @@ class BIMWindowProperties(PropertyGroup): return kwargs return tool.Model.convert_data_to_project_units(kwargs) - def set_props_kwargs_from_ifc_data(self, kwargs): + def set_props_kwargs_from_ifc_data(self, kwargs: dict[str, Any]): kwargs = tool.Model.convert_data_to_si_units(kwargs, self.non_si_units_props) for prop_name in kwargs: setattr(self, prop_name, kwargs[prop_name]) diff --git a/src/bonsai/bonsai/bim/module/model/window.py b/src/bonsai/bonsai/bim/module/model/window.py index e005a1b3ac..41b767d111 100644 --- a/src/bonsai/bonsai/bim/module/model/window.py +++ b/src/bonsai/bonsai/bim/module/model/window.py @@ -28,6 +28,7 @@ import bonsai.core.root import bonsai.core.geometry from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS import ifcopenshell.api +import ifcopenshell.api.material import ifcopenshell.util.element import ifcopenshell.util.representation import ifcopenshell.util.shape_builder diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index acc363bf16..eb49a01535 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -65,6 +65,9 @@ from bonsai.bim.module.model.decorator import PolylineDecorator from bonsai.bim.module.model.polyline import PolylineOperator from typing import Union, TYPE_CHECKING, Literal, get_args +if TYPE_CHECKING: + from bonsai.bim.module.project.prop import Link + class NewProject(bpy.types.Operator): bl_idname = "bim.new_project" @@ -1484,7 +1487,7 @@ class ToggleLinkVisibility(bpy.types.Operator): self.toggle_visibility(link) return {"FINISHED"} - def toggle_wireframe(self, link): + def toggle_wireframe(self, link: "Link") -> None: for collection in self.get_linked_collections(): objs = filter(lambda obj: "IfcOpeningElement" not in obj.name, collection.all_objects) for i, obj in enumerate(objs): @@ -1496,7 +1499,7 @@ class ToggleLinkVisibility(bpy.types.Operator): obj.display_type = display_type link.is_wireframe = display_type == "WIRE" - def toggle_visibility(self, link): + def toggle_visibility(self, link: "Link") -> None: linked_collections = self.get_linked_collections() link.is_hidden = (is_hidden := not link.is_hidden) diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index 5f2d6d2a52..cf5e0a9b66 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -224,7 +224,8 @@ class UpdateCurrentStyle(bpy.types.Operator): current_style_type = material.BIMStyleProperties.active_style_type if self.update_all: - context.scene.BIMStylesProperties.active_style_type = current_style_type + sprops = tool.Style.get_style_props() + sprops.active_style_type = current_style_type return {"FINISHED"} updated_materials = set() @@ -258,8 +259,9 @@ class SetAssetMaterialToExternalStyle(bpy.types.Operator): # the temp override to copy material node tree `right now` name = context.asset.name filepath = context.asset.full_library_path + props = tool.Style.get_style_props() bpy.app.timers.register( - lambda: self.execute_delayed(name, filepath, context.scene.BIMStylesProperties), + lambda: self.execute_delayed(name, filepath, props), first_interval=0.001, ) return {"FINISHED"} @@ -388,7 +390,8 @@ class BrowseExternalStyle(bpy.types.Operator): bpy.data.materials.remove(db["data_block"]) filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) - attributes = context.scene.BIMStylesProperties.external_style_attributes + props = tool.Style.get_style_props() + attributes = props.external_style_attributes attributes["Location"].string_value = filepath attributes["Identification"].string_value = f"{self.data_block_type}/{self.data_block}" attributes["Name"].string_value = self.data_block @@ -416,7 +419,7 @@ class ActivateExternalStyle(bpy.types.Operator): self.report({"INFO"}, "Material '{self.material_name}' is not an IFC style.") return {"CANCELLED"} - props = context.scene.BIMStylesProperties + props = tool.Style.get_style_props() if props.is_editing_style == style.id() and props.is_editing_class == "IfcExternallyDefinedSurfaceStyle": location = props.external_style_attributes["Location"].string_value identification = props.external_style_attributes["Identification"].string_value @@ -509,7 +512,8 @@ class LoadStyles(bpy.types.Operator): style_type: bpy.props.StringProperty() def execute(self, context): - style_type = self.style_type if self.style_type else context.scene.BIMStylesProperties.style_type + props = tool.Style.get_style_props() + style_type = self.style_type if self.style_type else props.style_type core.load_styles(tool.Style, style_type=style_type) bonsai.bim.handler.refresh_ui_data() return {"FINISHED"} @@ -561,7 +565,8 @@ class ChooseTextureMapPath(bpy.types.Operator): return {"CANCELLED"} filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) - texture = context.scene.BIMStylesProperties.textures[self.texture_map_index] + props = tool.Style.get_style_props() + texture = props.textures[self.texture_map_index] texture.path = filepath return {"FINISHED"} @@ -577,7 +582,7 @@ class RemoveTextureMap(bpy.types.Operator): self.report({"ERROR"}, "Provide a texture map index") return {"CANCELLED"} - props = context.scene.BIMStylesProperties + props = tool.Style.get_style_props() props.textures.remove(self.texture_map_index) # just to trigger shader graph update props.surface_colour = props.surface_colour @@ -612,7 +617,8 @@ class DuplicateStyle(bpy.types.Operator, tool.Ifc.Operator): style: bpy.props.IntProperty(name="Style ID") def _execute(self, context): - style_type = context.scene.BIMStylesProperties.style_type + props = tool.Style.get_style_props() + style_type = props.style_type ifc_file = tool.Ifc.get() style = ifc_file.by_id(self.style) tool.Style.duplicate_style(style) @@ -636,7 +642,7 @@ class AddPresentationStyle(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() if props.style_type == "IfcSurfaceStyle": style = ifcopenshell.api.run("style.add_style", tool.Ifc.get(), name=props.style_name) @@ -692,7 +698,7 @@ class EnableEditingSurfaceStyle(bpy.types.Operator): ifc_class: bpy.props.StringProperty(default="") def execute(self, context): - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() style = tool.Ifc.get().by_id(self.style) style_elements = tool.Style.get_style_elements(style) @@ -753,7 +759,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): surface_style: Union[ifcopenshell.entity_instance, None] def _execute(self, context): - self.props = bpy.context.scene.BIMStylesProperties + self.props = tool.Style.get_style_props() self.style = tool.Ifc.get().by_id(self.props.is_editing_style) style_elements = tool.Style.get_style_elements(self.style) @@ -950,13 +956,14 @@ class AddSurfaceTexture(bpy.types.Operator): @classmethod def poll(cls, context): - if len(context.scene.BIMStylesProperties.textures) >= 8: + props = tool.Style.get_style_props() + if len(props.textures) >= 8: cls.poll_message_set("Only 8 texture maps available") return False return True def execute(self, context): - props = context.scene.BIMStylesProperties + props = tool.Style.get_style_props() props.textures.add() return {"FINISHED"} @@ -1183,7 +1190,7 @@ class SelectStyleInStylesUI(bpy.types.Operator): style_id: bpy.props.IntProperty() def execute(self, context): - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() ifc_file = tool.Ifc.get() style = ifc_file.by_id(self.style_id) core.load_styles(tool.Style, style.is_a()) @@ -1204,7 +1211,7 @@ class RemoveSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): ifc_file = tool.Ifc.get() - props = context.scene.BIMStylesProperties + props = tool.Style.get_style_props() style = ifc_file.by_id(props.is_editing_style) surface_style = tool.Style.get_style_elements(style)[props.is_editing_class] ifcopenshell.api.style.remove_surface_style(ifc_file, surface_style) diff --git a/src/bonsai/bonsai/bim/module/style/prop.py b/src/bonsai/bonsai/bim/module/style/prop.py index 9111514d46..040db82f60 100644 --- a/src/bonsai/bonsai/bim/module/style/prop.py +++ b/src/bonsai/bonsai/bim/module/style/prop.py @@ -33,7 +33,7 @@ from bpy.props import ( ) import gettext -from typing import Literal, Union +from typing import Literal, Union, TYPE_CHECKING, get_args _ = gettext.gettext @@ -154,6 +154,17 @@ class ColourRgb(PropertyGroup): return "color_value" +SurfaceStyleClass = Literal[ + "IfcSurfaceStyleShading", + "IfcSurfaceStyleRendering", + "IfcSurfaceStyleWithTextures", + "IfcSurfaceStyleLighting", + "IfcSurfaceStyleRefraction", + "IfcExternallyDefinedSurfaceStyle", +] +ColourClass = Literal["IfcColourRgb", "IfcNormalisedRatioMeasure"] + + class BIMStylesProperties(PropertyGroup): is_adding: BoolProperty(name="Is Adding", description="Is adding new IfcPresentationStyle") is_editing: BoolProperty(name="Is Editing", description="Is editing IfcPresentationStyle") @@ -170,17 +181,7 @@ class BIMStylesProperties(PropertyGroup): style_type: EnumProperty(items=get_style_types, default=2, name="Style Type") style_name: StringProperty(name="Style Name") surface_style_class: EnumProperty( - items=[ - (x, x, "") - for x in ( - "IfcSurfaceStyleShading", - "IfcSurfaceStyleRendering", - "IfcSurfaceStyleWithTextures", - "IfcSurfaceStyleLighting", - "IfcSurfaceStyleRefraction", - "IfcExternallyDefinedSurfaceStyle", - ) - ], + items=[(x, x, "") for x in get_args(SurfaceStyleClass)], name="Surface Style Class", default="IfcSurfaceStyleShading", ) @@ -200,7 +201,7 @@ class BIMStylesProperties(PropertyGroup): # TODO: do something on null? is_diffuse_colour_null: BoolProperty(name="Is Null") diffuse_colour_class: EnumProperty( - items=[(x, x, "") for x in ("IfcColourRgb", "IfcNormalisedRatioMeasure")], + items=[(x, x, "") for x in get_args(ColourClass)], name="Diffuse Colour Class", update=update_shader_graph, ) @@ -212,7 +213,7 @@ class BIMStylesProperties(PropertyGroup): ) is_specular_colour_null: BoolProperty(name="Is Null") specular_colour_class: EnumProperty( - items=[(x, x, "") for x in ("IfcColourRgb", "IfcNormalisedRatioMeasure")], + items=[(x, x, "") for x in get_args(ColourClass)], name="Specular Colour Class", update=update_shader_graph, default="IfcNormalisedRatioMeasure", @@ -269,11 +270,49 @@ class BIMStylesProperties(PropertyGroup): active_style_type: EnumProperty( name="Active Style Type", description="Update current blender material to match style type for all objects in the scene", - items=STYLE_TYPES, + items=[(i, i, "") for i in get_args(tool.Style.StyleType)], default="Shading", update=update_shading_styles, ) + if TYPE_CHECKING: + is_adding: bool + is_editing: bool + is_editing_style: int + is_editing_class: str + is_editing_existing_style: bool + attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + external_style_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + refraction_style_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + lighting_style_colours: bpy.types.bpy_prop_collection_idprop[ColourRgb] + style_type: str + style_name: str + surface_style_class: SurfaceStyleClass + update_graph: bool + + # Shading props. + surface_colour: tuple[float, float, float] + transparency: float + is_diffuse_colour_null: bool + diffuse_colour_class: ColourClass + diffuse_colour: tuple[float, float, float] + diffuse_colour_ratio: float + is_specular_colour_null: bool + specular_colour_class: ColourClass + specular_colour: tuple[float, float, float] + specular_colour_ratio: float + is_specular_highlight_null: bool + specular_highlight: float + reflectance_method: str + + # Texture props. + textures: bpy.types.bpy_prop_collection_idprop[Texture] + uv_mode: Literal["UV", "Generated", "Camera"] + + styles: bpy.types.bpy_prop_collection_idprop[Style] + active_style_index: int + active_style_type: tool.Style.StyleType + def update_shading_style(self: "BIMStyleProperties", context: bpy.types.Context) -> None: blender_material = self.id_data @@ -290,8 +329,13 @@ class BIMStyleProperties(PropertyGroup): active_style_type: EnumProperty( name="Active Style Type", description="Update current blender material to match style type", - items=STYLE_TYPES, + items=[(i, i, "") for i in get_args(tool.Style.StyleType)], default="Shading", update=update_shading_style, ) is_renaming: BoolProperty(description="Used to prevent triggering handler callback.", default=False) + + if TYPE_CHECKING: + ifc_definition_id: int + active_style_type: tool.Style.StyleType + is_renaming: bool diff --git a/src/bonsai/bonsai/bim/module/style/ui.py b/src/bonsai/bonsai/bim/module/style/ui.py index 2b7eaad760..4cdf8aa88d 100644 --- a/src/bonsai/bonsai/bim/module/style/ui.py +++ b/src/bonsai/bonsai/bim/module/style/ui.py @@ -40,7 +40,7 @@ class BIM_PT_styles(Panel): if not StylesData.is_loaded: StylesData.load() - self.props = context.scene.BIMStylesProperties + self.props = tool.Style.get_style_props() if not self.props.is_editing: row = self.layout.row(align=True) @@ -258,7 +258,7 @@ class BIM_UL_styles(UIList): def draw_item(self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_property): if item: row = layout.row(align=True) - props = context.scene.BIMStylesProperties + props = tool.Style.get_style_props() if item.ifc_definition_id == props.is_editing_style: row.label(text="", icon="GREASEPENCIL") row.prop(item, "name", text="", emboss=False) @@ -330,6 +330,6 @@ def draw_asset_browser_context_menu_append(self, context): asset = context.asset if not asset or not asset.id_type == "MATERIAL": return - if not context.scene.BIMStylesProperties.is_editing: + if not tool.Style.get_style_props().is_editing: return self.layout.operator("bim.set_asset_material_to_external_style") diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 27cb0180bf..dedcbe8b82 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -49,7 +49,7 @@ from bonsai.bim.module.geometry.helper import Helper from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData from bonsai.bim.module.model.opening import FilledOpeningGenerator from ifcopenshell.util.shape_builder import ShapeBuilder -from typing import Optional, Union, TypeVar, Any, Iterable, Literal, TYPE_CHECKING +from typing import Optional, Union, TypeVar, Any, Iterable, Literal, TYPE_CHECKING, Sequence T = TypeVar("T") V_ = tool.Blender.V_ @@ -108,7 +108,7 @@ class Model(bonsai.core.tool.Model): return value * cls.unit_scale @classmethod - def convert_data_to_project_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]: + def convert_data_to_project_units(cls, data: dict[str, Any], non_si_props: Sequence[str] = ()) -> dict[str, Any]: si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) for prop_name in data: if prop_name in non_si_props: @@ -121,7 +121,7 @@ class Model(bonsai.core.tool.Model): return data @classmethod - def convert_data_to_si_units(cls, data: dict[str, Any], non_si_props: list[str] = []) -> dict[str, Any]: + def convert_data_to_si_units(cls, data: dict[str, Any], non_si_props: Sequence[str] = ()) -> dict[str, Any]: si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) for prop_name in data: if prop_name in non_si_props: diff --git a/src/bonsai/bonsai/tool/style.py b/src/bonsai/bonsai/tool/style.py index c41669fa81..b7a7cb7241 100644 --- a/src/bonsai/bonsai/tool/style.py +++ b/src/bonsai/bonsai/tool/style.py @@ -16,9 +16,11 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import numpy as np import ifcopenshell +import ifcopenshell.api.style import ifcopenshell.util.element import ifcopenshell.util.representation import bonsai.core.style @@ -26,7 +28,10 @@ import bonsai.core.tool import bonsai.tool as tool import bonsai.bim.helper from mathutils import Color -from typing import Union, Any, Optional, Literal +from typing import Union, Any, Optional, Literal, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.style.prop import BIMStylesProperties, BIMStyleProperties # fmt: off TEXTURE_MAPS_BY_METHODS = { @@ -44,10 +49,18 @@ STYLE_PROPS_MAP = { "specular_colour": "SpecularColour", } -STYLE_TYPES = Literal["Shading", "External"] - class Style(bonsai.core.tool.Style): + StyleType = Literal["Shading", "External"] + + @classmethod + def get_style_props(cls) -> BIMStylesProperties: + return bpy.context.scene.BIMStylesProperties + + @classmethod + def get_material_style_props(cls, material: bpy.types.Material) -> BIMStyleProperties: + return material.BIMStyleProperties + @classmethod def can_support_rendering_style(cls, obj: bpy.types.Material) -> bool: return obj.use_nodes and hasattr(obj.node_tree, "nodes") @@ -58,19 +71,19 @@ class Style(bonsai.core.tool.Style): @classmethod def enable_adding_presentation_style(cls) -> None: - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() props.is_adding = True props.update_graph = False @classmethod def disable_adding_presentation_style(cls) -> None: - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() props.is_adding = False props.update_graph = True @classmethod def disable_editing(cls) -> None: - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() props.is_editing_style = 0 props.is_editing_class = "" props.attributes.clear() @@ -81,7 +94,7 @@ class Style(bonsai.core.tool.Style): @classmethod def disable_editing_styles(cls) -> None: - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() props.is_editing = False props.styles.clear() @@ -100,27 +113,29 @@ class Style(bonsai.core.tool.Style): @classmethod def enable_editing(cls, style: ifcopenshell.entity_instance) -> None: - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() props.is_editing_style = style.id() props.is_editing_class = "IfcSurfaceStyle" @classmethod def enable_editing_styles(cls) -> None: - bpy.context.scene.BIMStylesProperties.is_editing = True + props = cls.get_style_props() + props.is_editing = True @classmethod def export_surface_attributes(cls) -> dict[str, Any]: - props = bpy.context.scene.BIMStylesProperties + props = cls.get_style_props() return bonsai.bim.helper.export_attributes(props.attributes) @classmethod def get_active_style_in_ui(cls) -> Union[bpy.types.PropertyGroup, None]: - props = bpy.context.scene.BIMStylesProperties + props = cls.get_style_props() return props.active_style @classmethod def get_active_style_type(cls) -> str: - return bpy.context.scene.BIMStylesProperties.style_type + props = cls.get_style_props() + return props.style_type @classmethod def get_context(cls) -> Union[ifcopenshell.entity_instance, None]: @@ -136,7 +151,7 @@ class Style(bonsai.core.tool.Style): @classmethod def get_currently_edited_material(cls) -> bpy.types.Material: - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() style = tool.Ifc.get().by_id(props.is_editing_style) obj = tool.Ifc.get_object(style) assert isinstance(obj, bpy.types.Material) @@ -162,7 +177,7 @@ class Style(bonsai.core.tool.Style): """returns style data from blender props in similar way to `Loader.surface_style_to_dict` to be compatible with `Loader.create_surface_style_rendering`""" surface_style_data = dict() - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() available_props = props.bl_rna.properties.keys() for prop_blender, prop_ifc in STYLE_PROPS_MAP.items(): @@ -189,7 +204,7 @@ class Style(bonsai.core.tool.Style): def get_texture_style_data_from_props(cls) -> list[dict[str, Any]]: """returns style data from blender props in similar way to `Loader.surface_texture_to_dict` to be compatible with `Loader.create_surface_style_with_textures`""" - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() textures = [] for texture in props.textures: @@ -210,7 +225,7 @@ class Style(bonsai.core.tool.Style): """set blender style props based on currently edited IfcSurfaceStyle, reset unrelated props to default values""" - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() style = tool.Ifc.get().by_id(props.is_editing_style) # make sure won't be updating while we changing it prev_update_graph_value = props.update_graph @@ -503,7 +518,7 @@ class Style(bonsai.core.tool.Style): @classmethod def get_style_ui_props_attributes(cls, style_type: str) -> Union[bpy.types.PropertyGroup, None]: - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() if style_type == "IfcExternallyDefinedSurfaceStyle": return props.external_style_attributes elif style_type == "IfcSurfaceStyleRefraction": @@ -514,7 +529,7 @@ class Style(bonsai.core.tool.Style): @classmethod def import_presentation_styles(cls, style_type: str) -> None: color_to_tuple = lambda x: (x.Red, x.Green, x.Blue) - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() props.styles.clear() styles = sorted(tool.Ifc.get().by_type(style_type), key=lambda x: x.Name or "Unnamed") for style in styles: @@ -537,7 +552,8 @@ class Style(bonsai.core.tool.Style): @classmethod def import_surface_attributes(cls, style: ifcopenshell.entity_instance) -> None: - attributes = bpy.context.scene.BIMStylesProperties.attributes + props = cls.get_style_props() + attributes = props.attributes attributes.clear() bonsai.bim.helper.import_attributes2(style, attributes) @@ -548,11 +564,13 @@ class Style(bonsai.core.tool.Style): @classmethod def is_editing_styles(cls) -> bool: - return bpy.context.scene.BIMStylesProperties.is_editing + props = cls.get_style_props() + return props.is_editing @classmethod def is_editing_style(cls) -> bool: - return bpy.context.scene.BIMStylesProperties.is_editing_style + props = cls.get_style_props() + return bool(props.is_editing_style) @classmethod def select_elements(cls, elements: list[ifcopenshell.entity_instance]) -> None: @@ -617,7 +635,7 @@ class Style(bonsai.core.tool.Style): blender_material.BIMStyleProperties.active_style_type = blender_material.BIMStyleProperties.active_style_type @classmethod - def switch_shading(cls, blender_material: bpy.types.Material, style_type: STYLE_TYPES) -> None: + def switch_shading(cls, blender_material: bpy.types.Material, style_type: StyleType) -> None: if style_type == "External": try: bpy.ops.bim.activate_external_style(material_name=blender_material.name) diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index c93532ab25..afcf0e6b5a 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -484,20 +484,21 @@ class TestApplyIfcMaterialChanges(NewFile): red_material = ifcopenshell.api.material.add_material(ifc_file, "Red Material") bpy.ops.bim.load_styles(style_type="IfcSurfaceStyle") bpy.ops.bim.enable_adding_presentation_style() - bpy.data.scenes["Scene"].BIMStylesProperties.style_name = "Red" + sprops = tool.Style.get_style_props() + sprops.style_name = "Red" bpy.ops.bim.add_presentation_style() red_style = next((i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Red")) ifcopenshell.api.style.assign_material_style(ifc_file, red_material, red_style, context) blue_material = ifcopenshell.api.material.add_material(ifc_file, "Blue Material") bpy.ops.bim.enable_adding_presentation_style() - bpy.data.scenes["Scene"].BIMStylesProperties.style_name = "Blue" + sprops.style_name = "Blue" bpy.ops.bim.add_presentation_style() blue_style = next((i for i in ifc_file.by_type("IfcSurfaceStyle") if i.Name == "Blue")) ifcopenshell.api.style.assign_material_style(ifc_file, blue_material, blue_style, context) bpy.ops.bim.enable_adding_presentation_style() - bpy.data.scenes["Scene"].BIMStylesProperties.style_name = "Green" + sprops.style_name = "Green" bpy.ops.bim.add_presentation_style() if and_elements: diff --git a/src/bonsai/test/tool/test_style.py b/src/bonsai/test/tool/test_style.py index cf46d8fab7..6af83895a3 100644 --- a/src/bonsai/test/tool/test_style.py +++ b/src/bonsai/test/tool/test_style.py @@ -47,7 +47,7 @@ class TestCanSupportRenderingStyle(NewFile): class TestDisableEditing(NewFile): def test_run(self): - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() props.is_editing_style = 1 subject.disable_editing() assert props.is_editing_style == 0 @@ -55,14 +55,15 @@ class TestDisableEditing(NewFile): class TestDisableEditingStyles(NewFile): def test_run(self): - bpy.context.scene.BIMStylesProperties.is_editing = True + props = tool.Style.get_style_props() + props.is_editing = True subject.disable_editing_styles() - assert bpy.context.scene.BIMStylesProperties.is_editing is False + assert props.is_editing is False class TestEnableEditing(NewFile): def test_run(self): - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() style = ifcopenshell.file().create_entity("IfcSurfaceStyle") subject.enable_editing(style) assert props.is_editing_style is style.id() @@ -70,9 +71,10 @@ class TestEnableEditing(NewFile): class TestEnableEditingStyles(NewFile): def test_run(self): - bpy.context.scene.BIMStylesProperties.is_editing = False + props = props = tool.Style.get_style_props() + props.is_editing = False subject.enable_editing_styles() - assert bpy.context.scene.BIMStylesProperties.is_editing is True + assert props.is_editing is True class TestExportSurfaceAttributes(NewFile): @@ -85,9 +87,10 @@ class TestGetActiveStyleType(NewFile): def test_run(self): ifc = ifcopenshell.file() tool.Ifc.set(ifc) - bpy.context.scene.BIMStylesProperties.style_type = "IfcSurfaceStyle" + props = tool.Style.get_style_props() + props.style_type = "IfcSurfaceStyle" assert subject.get_active_style_type() == "IfcSurfaceStyle" - bpy.context.scene.BIMStylesProperties.style_type = "IfcCurveStyle" + props.style_type = "IfcCurveStyle" assert subject.get_active_style_type() == "IfcCurveStyle" @@ -378,7 +381,7 @@ class TestGetUVMaps(NewFile): class TestImportSurfaceAttributes(NewFile): def test_run(self): tool.Ifc.set(ifc := ifcopenshell.file()) - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() style = ifc.create_entity("IfcSurfaceStyle", "Name", "BOTH") subject.import_surface_attributes(style) assert props.attributes.get("Name").string_value == "Name" @@ -387,7 +390,7 @@ class TestImportSurfaceAttributes(NewFile): def test_importing_surface_attributes_twice(self): tool.Ifc.set(ifc := ifcopenshell.file()) style = ifc.create_entity("IfcSurfaceStyle", "Name", "BOTH") - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() subject.import_surface_attributes(style) assert len(props.attributes) == 2 assert props.attributes.get("Name").string_value == "Name" @@ -404,7 +407,7 @@ class TestImportPresentationStyles(NewFile): tool.Ifc.set(ifc) style = ifc.createIfcCurveStyle(Name="Name") subject.import_presentation_styles("IfcCurveStyle") - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() assert props.styles[0].ifc_definition_id == style.id() assert props.styles[0].name == "Name" assert props.styles[0].total_elements == 0 @@ -414,7 +417,7 @@ class TestImportPresentationStyles(NewFile): tool.Ifc.set(ifc) style = ifc.createIfcFillAreaStyle(Name="Name") subject.import_presentation_styles("IfcFillAreaStyle") - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() assert props.styles[0].ifc_definition_id == style.id() assert props.styles[0].name == "Name" assert props.styles[0].total_elements == 0 @@ -424,7 +427,7 @@ class TestImportPresentationStyles(NewFile): tool.Ifc.set(ifc) style = ifc.createIfcSurfaceStyle(Name="Name") subject.import_presentation_styles("IfcSurfaceStyle") - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() assert props.styles[0].ifc_definition_id == style.id() assert props.styles[0].name == "Name" assert props.styles[0].total_elements == 0 @@ -434,7 +437,7 @@ class TestImportPresentationStyles(NewFile): tool.Ifc.set(ifc) style = ifc.createIfcTextStyle(Name="Name") subject.import_presentation_styles("IfcTextStyle") - props = bpy.context.scene.BIMStylesProperties + props = tool.Style.get_style_props() assert props.styles[0].ifc_definition_id == style.id() assert props.styles[0].name == "Name" assert props.styles[0].total_elements == 0 @@ -442,9 +445,10 @@ class TestImportPresentationStyles(NewFile): class TestIsEditingStyles(NewFile): def test_run(self): - bpy.context.scene.BIMStylesProperties.is_editing = False + props = tool.Style.get_style_props() + props.is_editing = False assert subject.is_editing_styles() is False - bpy.context.scene.BIMStylesProperties.is_editing = True + props.is_editing = True assert subject.is_editing_styles() is True diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py index 8303e90a3a..51f37eacb8 100644 --- a/src/ifcpatch/ifcpatch/__init__.py +++ b/src/ifcpatch/ifcpatch/__init__.py @@ -133,7 +133,7 @@ def write(output: Union[ifcopenshell.file, str], filepath: str) -> None: def extract_docs( - submodule_name: str, cls_name: str, method_name: str = "__init__", boilerplate_args: Optional[Iterable[str]] = None + submodule_name: str, cls_name: str, method_name: str = "__init__", boilerplate_args: Optional[Sequence[str]] = None ) -> Union["PatcherDoc", None]: """Extract class docstrings and method arguments @@ -173,7 +173,7 @@ class InputDoc(TypedDict): filter_glob: NotRequired[str] -def _extract_docs(cls: type, method_name: str, boilerplate_args: Union[Iterable[str], None]) -> PatcherDoc: +def _extract_docs(cls: type, method_name: str, boilerplate_args: Union[Sequence[str], None]) -> PatcherDoc: inputs: dict[str, InputDoc] = {} method = getattr(cls, method_name) if boilerplate_args is None: From 5a1f7000ec558f35631225ed2569ed698a9a29ed Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 11:09:16 +0500 Subject: [PATCH 166/476] ifcpatch - add simple test for docs generator --- src/ifcpatch/test/test_ifcpatch.py | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/ifcpatch/test/test_ifcpatch.py diff --git a/src/ifcpatch/test/test_ifcpatch.py b/src/ifcpatch/test/test_ifcpatch.py new file mode 100644 index 0000000000..53013cffe5 --- /dev/null +++ b/src/ifcpatch/test/test_ifcpatch.py @@ -0,0 +1,34 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# 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 . + +import ifcpatch +from pathlib import Path + + +class Test: + def test_parsing_docs(self): + recipes = Path(ifcpatch.__file__).parent / "recipes" + + for f in recipes.glob("*.py"): + if f.stem in "__init__": + continue + docs = ifcpatch.extract_docs(f.stem, "Patcher", "__init__", ("src", "file", "logger", "args")) + assert docs is not None + expected_keys = ("class_", "description", "output", "inputs") + for key in expected_keys: + assert key in docs From 15a0d4318070ef9bd11620ac88a12150148e928b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 11:50:23 +0500 Subject: [PATCH 167/476] fix for glob pattern in 62d53ba62d --- src/ifcpatch/ifcpatch/__init__.py | 19 ++++++++++++++----- src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/ifcpatch/ifcpatch/__init__.py b/src/ifcpatch/ifcpatch/__init__.py index 51f37eacb8..3fb6573b2b 100644 --- a/src/ifcpatch/ifcpatch/__init__.py +++ b/src/ifcpatch/ifcpatch/__init__.py @@ -215,11 +215,18 @@ def _extract_docs(cls: type, method_name: str, boilerplate_args: Union[Sequence[ doc = inspect.getdoc(method) def is_valid_param_name(param_name: str) -> bool: - if param_name not in inputs: - print( - f"WARNING. Unexpected param name '{param_name}' in {cls.__name__} docstring (missing from signature)." - ) + assert ( + param_name in inputs + ), f"Unexpected param name '{param_name}' in {cls.__name__} docstring (missing from signature)." + return True + + def is_valid_filter_glob(filter_glob: str) -> bool: + # e.g. '*.ifc;*.ifczip;*.ifcxml' + if len(filter_glob) < 3: return False + for pattern in filter_glob.split(";"): + if not re.fullmatch(r"\*\.\w+", pattern): + return False return True if doc is None: @@ -238,7 +245,9 @@ def _extract_docs(cls: type, method_name: str, boilerplate_args: Union[Sequence[ for param_name in docstring_data["filter_glob"]: if not is_valid_param_name(param_name): continue - inputs[param_name]["filter_glob"] = docstring_data["filter_glob"][param_name] + filter_glob = docstring_data["filter_glob"][param_name] + assert is_valid_filter_glob(filter_glob), f"Invalid filter_glob pattern: '{filter_glob}'." + inputs[param_name]["filter_glob"] = filter_glob for param_name in inputs: if "description" not in inputs[param_name]: diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index 3eeeeb1f47..4df753cc5d 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -83,7 +83,7 @@ class Patcher: :param database: Database path to save the SQL database to (already existing or not). Could also be a directory, then the database will be stored using default filename (e.g. 'database.db'). - :filter_glob database: *.db,*.sqlite + :filter_glob database: *.db;*.sqlite :param full_schema: if True, will create tables for all IFC classes, regardless if they are used or not in the dataset. If False, will only create tables for classes in the dataset. From 4dd10b16e629fbb3c356ade357b2a83c2fc72d81 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 11:50:54 +0500 Subject: [PATCH 168/476] SplitByBuildingStorey - Bonsai to recognize filepath argument after cac3d7f --- src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py b/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py index a2cee58a08..b75e3c72c1 100644 --- a/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py +++ b/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py @@ -32,6 +32,7 @@ class Patcher: 0 and {name} is the name of the storey. :param output_dir: Specifies an output directory where the new IFC models will be saved. + :filter_glob output_dir: *.ifc;*.ifczip;*.ifcxml Example: From 323967b9fd9eb86f5caccd7f9fd469377afb3caf Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 11:59:03 +0500 Subject: [PATCH 169/476] Fix ifc2sql test after 9055f14bb9 --- src/ifcpatch/test/test_Ifc2Sql.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ifcpatch/test/test_Ifc2Sql.py b/src/ifcpatch/test/test_Ifc2Sql.py index 3d20c8ba97..e5c40fe245 100644 --- a/src/ifcpatch/test/test_Ifc2Sql.py +++ b/src/ifcpatch/test/test_Ifc2Sql.py @@ -34,16 +34,15 @@ from pathlib import Path class TestIfc2Sql: def test_run(self): TEST_FILE = Path(__file__).parent / "files" / "basic.ifc" - tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".ifcsqlite") - sqlite_path = ifcpatch.execute( + temp = tempfile.NamedTemporaryFile(delete=False, suffix=".ifcsqlite") + sqlite_path = Path(temp.name) + ifcpatch.execute( { "file": ifcopenshell.open(TEST_FILE), "recipe": "Ifc2Sql", - "arguments": ["sqlite", None, None, None, tmp.name], + "arguments": ["sqlite", None, None, None, sqlite_path], } ) - assert isinstance(sqlite_path, str) - assert sqlite_path.endswith(".ifcsqlite") # Ensure file is valid. ifc_sqlite = ifcopenshell.open(sqlite_path) From b89ffcb6d0a18e9eed02ec20538ae26754b46b00 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 12:04:22 +0500 Subject: [PATCH 170/476] SplitByBuildingStorey - ensure user won't pass a file by accident --- src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py b/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py index b75e3c72c1..1d0b4c02f0 100644 --- a/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py +++ b/src/ifcpatch/ifcpatch/recipes/SplitByBuildingStorey.py @@ -50,9 +50,11 @@ class Patcher: from shutil import copyfile if self.output_dir is None: - output_dir = None + output_dir = Path() else: output_dir = Path(self.output_dir) + if output_dir.is_file(): + raise ValueError(f"Provided path is a file, not a directory: {output_dir}") output_dir.mkdir(parents=True, exist_ok=True) temp_file = tempfile.NamedTemporaryFile(suffix=".ifc", delete=False) From bb0d156bbfa4eec26376eaaeaf852aa6cccf6632 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 12:32:30 +0500 Subject: [PATCH 171/476] cad operators - use Blender units for props --- src/bonsai/bonsai/bim/module/cad/operator.py | 93 +++++++++---------- src/bonsai/bonsai/bim/module/cad/workspace.py | 18 ++-- 2 files changed, 53 insertions(+), 58 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/cad/operator.py b/src/bonsai/bonsai/bim/module/cad/operator.py index 710c38d5db..c44f5ea644 100644 --- a/src/bonsai/bonsai/bim/module/cad/operator.py +++ b/src/bonsai/bonsai/bim/module/cad/operator.py @@ -130,7 +130,7 @@ class CadFillet(bpy.types.Operator): bl_label = "CAD Fillet" bl_options = {"REGISTER", "UNDO"} resolution: bpy.props.IntProperty(name="Arc Resolution", min=0, default=1) - radius: bpy.props.FloatProperty(name="Radius", default=0.1) + radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE") @classmethod def poll(cls, context): @@ -143,18 +143,15 @@ class CadFillet(bpy.types.Operator): layout.prop(self, prop) def execute(self, context): - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - self.radius = self.radius * si_conversion - bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="EDIT") obj = bpy.context.active_object - cursor = obj.matrix_world.inverted() @ bpy.context.scene.cursor.location - mesh = obj.data + assert obj and isinstance((mesh := obj.data), bpy.types.Mesh) bm = bmesh.from_edit_mesh(mesh) selected_edges = [e for e in bm.edges if e.select] if len(selected_edges) != 2: + self.report({"ERROR"}, "Exactly 2 edges should be selected.") return {"CANCELLED"} # Assume the user has selected two edges sharing a vert, but merge verts @@ -235,6 +232,7 @@ class CadArcFrom2Points(bpy.types.Operator): if bpy.context.mode != "EDIT_MESH": return {"CANCELLED"} obj = bpy.context.active_object + assert obj region = bpy.context.region region_3d = bpy.context.area.spaces.active.region_3d cursor = bpy.context.scene.cursor.location @@ -242,10 +240,12 @@ class CadArcFrom2Points(bpy.types.Operator): if not center: return {"CANCELLED"} mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) mw = obj.matrix_world bm = bmesh.from_edit_mesh(mesh) selected_verts = [v for v in bm.verts if v.select] if len(selected_verts) != 2: + self.report({"ERROR"}, "Exactly 2 vertices should be selected.") return {"CANCELLED"} v1 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[0].co) v2 = bpy_extras.view3d_utils.location_3d_to_region_2d(region, region_3d, mw @ selected_verts[1].co) @@ -343,10 +343,10 @@ class CadOffset(bpy.types.Operator): bl_idname = "bim.cad_offset" bl_label = "CAD Offset" bl_options = {"REGISTER", "UNDO"} - distance: bpy.props.FloatProperty(name="Distance", default=0.1) + distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE") @classmethod - def poll(self, context): + def poll(cls, context): return context.mode == "EDIT_MESH" def draw(self, context): @@ -369,9 +369,6 @@ class CadOffset(bpy.types.Operator): # effect of converting an unclosed loop into a closed loop which is also # not what users expect. - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - self.distance = self.distance * si_conversion - bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="EDIT") @@ -543,7 +540,7 @@ class CadOffset(bpy.types.Operator): class AddIfcCircle(bpy.types.Operator): bl_idname = "bim.add_ifccircle" bl_label = "Add IfcCircle" - radius: bpy.props.FloatProperty(name="Radius", default=0.5) + radius: bpy.props.FloatProperty(name="Radius", default=0.5, subtype="DISTANCE") @classmethod def poll(cls, context): @@ -551,18 +548,19 @@ class AddIfcCircle(bpy.types.Operator): return bool(obj) and obj.type == "MESH" def execute(self, context): - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - self.radius = self.radius * si_conversion - + obj = context.active_object + assert obj and isinstance((mesh := obj.data), bpy.types.Mesh) + self.obj = obj + self.mesh = mesh if self.has_selected_existing_circle(context): self.change_radius(context) else: self.create_circle(context) return {"FINISHED"} - def has_selected_existing_circle(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + def has_selected_existing_circle(self, context: bpy.types.Context) -> bool: + obj = self.obj + bm = bmesh.from_edit_mesh(self, mesh) verts = [v for v in bm.verts if v.select and not v.hide] if len(verts) != 2: return False @@ -575,32 +573,32 @@ class AddIfcCircle(bpy.types.Operator): for group in groups: if group in verts[0][deform_layer] and group in verts[1][deform_layer]: return True + return False - def change_radius(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + def change_radius(self, context: bpy.types.Object) -> None: + bm = bmesh.from_edit_mesh(self.mesh) verts = [v for v in bm.verts if v.select and not v.hide] center = verts[0].co.lerp(verts[1].co, 0.5) verts[0].co = center + ((verts[0].co - center).normalized() * self.radius) verts[1].co = center + ((verts[1].co - center).normalized() * self.radius) bm.verts.index_update() bm.edges.index_update() - bmesh.update_edit_mesh(obj.data) + bmesh.update_edit_mesh(self.mesh) - def create_circle(self, context): - obj = context.active_object + def create_circle(self, context: bpy.types.Context) -> None: + obj = self.obj bpy.ops.object.mode_set(mode="OBJECT") # The last group may be the result of a prior run of this operator. # I tried looping through all groups but Blender group indices seem to behave unpredictably. if len(obj.vertex_groups): last_group = obj.vertex_groups[-1] - verts_in_group = [v for v in obj.data.vertices if last_group.index in [vg.group for vg in v.groups]] + verts_in_group = [v for v in self.mesh.vertices if last_group.index in [vg.group for vg in v.groups]] if "IFCCIRCLE" in last_group.name and len(verts_in_group) != 2: obj.vertex_groups.remove(last_group) group = obj.vertex_groups.new(name="IFCCIRCLE") bpy.ops.object.mode_set(mode="EDIT") - bm = bmesh.from_edit_mesh(obj.data) + bm = bmesh.from_edit_mesh(self.mesh) bm.verts.layers.deform.verify() deform_layer = bm.verts.layers.deform.active @@ -617,7 +615,7 @@ class AddIfcCircle(bpy.types.Operator): bm.verts.index_update() bm.edges.index_update() - bmesh.update_edit_mesh(obj.data) + bmesh.update_edit_mesh(self.mesh) class AddIfcArcIndexFillet(bpy.types.Operator): @@ -638,18 +636,19 @@ class AddIfcArcIndexFillet(bpy.types.Operator): layout.prop(self, prop) def execute(self, context): - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - self.radius = self.radius * si_conversion - + obj = context.active_object + assert obj and isinstance((mesh := obj.data), bpy.types.Mesh) + self.obj = obj + self.mesh = mesh if self.has_selected_existing_arc(context): self.change_radius(context) else: - self.create_arc(context) + return self.create_arc(context) return {"FINISHED"} def has_selected_existing_arc(self, context: bpy.types.Context) -> bool: - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + obj = self.obj + bm = bmesh.from_edit_mesh(self.mesh) verts = [v for v in bm.verts if v.select and not v.hide] if len(verts) != 3: return False @@ -668,13 +667,14 @@ class AddIfcArcIndexFillet(bpy.types.Operator): return False def change_radius(self, context: bpy.types.Context) -> None: - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + obj = self.obj + bm = bmesh.from_edit_mesh(self.mesh) edges = [e for e in bm.edges if e.select and not e.hide] mid = list(set(edges[0].verts) & set(edges[1].verts))[0] v1 = edges[0].other_vert(mid) v2 = edges[1].other_vert(mid) + assert v1 and v2 center = tool.Cad.get_center_of_arc([v1.co, mid.co, v2.co]) if len(v1.link_edges) != 2 or len(v2.link_edges) != 2: @@ -682,6 +682,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator): v3 = v1.link_edges[1].other_vert(v1) if mid in v1.link_edges[0].verts else v1.link_edges[0].other_vert(v1) v4 = v2.link_edges[1].other_vert(v2) if mid in v2.link_edges[0].verts else v2.link_edges[0].other_vert(v2) + assert v3 and v4 dir1 = (v3.co - v1.co).normalized() dir2 = (v4.co - v2.co).normalized() @@ -702,11 +703,10 @@ class AddIfcArcIndexFillet(bpy.types.Operator): bm.verts.index_update() bm.edges.index_update() - bmesh.update_edit_mesh(obj.data) - - def create_arc(self, context): - obj = bpy.context.active_object + bmesh.update_edit_mesh(self.mesh) + def create_arc(self, context: bpy.types.Context) -> set[str]: + obj = self.obj bpy.ops.object.mode_set(mode="OBJECT") # The last group may be the result of a prior run of this operator. # I tried looping through all groups but Blender group indices seem to behave unpredictably. @@ -718,7 +718,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator): group = obj.vertex_groups.new(name="IFCARCINDEX") bpy.ops.object.mode_set(mode="EDIT") - bm = bmesh.from_edit_mesh(obj.data) + bm = bmesh.from_edit_mesh(self.mesh) bm.verts.layers.deform.verify() deform_layer = bm.verts.layers.deform.active @@ -737,6 +737,7 @@ class AddIfcArcIndexFillet(bpy.types.Operator): shared_vert = list(set(selected_edges[0].verts) & set(selected_edges[1].verts))[0] v1 = selected_edges[0].other_vert(shared_vert) v2 = selected_edges[1].other_vert(shared_vert) + assert v1 and v2 dir1 = (v1.co - shared_vert.co).normalized() dir2 = (v2.co - shared_vert.co).normalized() edge_angle = dir1.angle(dir2) @@ -770,7 +771,8 @@ class AddIfcArcIndexFillet(bpy.types.Operator): bm.verts.index_update() bm.edges.index_update() - bmesh.update_edit_mesh(obj.data) + bmesh.update_edit_mesh(self.mesh) + return {"FINISHED"} class AlignViewToProfile(bpy.types.Operator): @@ -806,8 +808,8 @@ class AlignViewToProfile(bpy.types.Operator): class AddRectangle(bpy.types.Operator): bl_idname = "bim.add_rectangle" bl_label = "Add Rectangle" - x: bpy.props.FloatProperty(name="X", default=1) - y: bpy.props.FloatProperty(name="Y", default=1) + x: bpy.props.FloatProperty(name="X", default=1, subtype="DISTANCE") + y: bpy.props.FloatProperty(name="Y", default=1, subtype="DISTANCE") @classmethod def poll(cls, context): @@ -815,11 +817,8 @@ class AddRectangle(bpy.types.Operator): return bool(obj) and obj.type == "MESH" def execute(self, context): - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - self.x = self.x * si_conversion - self.y = self.y * si_conversion - obj = context.active_object + assert obj and isinstance(obj.data, bpy.types.Mesh) bm = bmesh.from_edit_mesh(obj.data) cursor = obj.matrix_world.inverted() @ context.scene.cursor.location new_verts = [ diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 07d6a597c5..f90c56c35d 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -116,9 +116,9 @@ class CadTool(WorkSpaceTool): row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Rectangle", "S_R", "Rectangle", ui_context) + add_layout_hotkey_operator(row, "Rectangle", "S_R", "", ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Circle", "S_C", "Circle", ui_context) + add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.set_arc_index.__doc__, ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) @@ -238,9 +238,8 @@ class CadHotkey(bpy.types.Operator): row.prop(props, "resolution") def hotkey_S_C(self): - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if tool.Geometry.is_profile_object_active(): - bpy.ops.bim.add_ifccircle(radius=self.props.radius / si_conversion) + bpy.ops.bim.add_ifccircle(radius=self.props.radius) else: bpy.ops.bim.cad_arc_from_2_points() @@ -248,15 +247,13 @@ class CadHotkey(bpy.types.Operator): bpy.ops.bim.cad_trim_extend() def hotkey_S_F(self): - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if tool.Geometry.is_profile_object_active(): - bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius / si_conversion) + bpy.ops.bim.add_ifcarcindex_fillet(radius=self.props.radius) else: - bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius / si_conversion) + bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius) def hotkey_S_O(self): - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - bpy.ops.bim.cad_offset(distance=self.props.distance / si_conversion) + bpy.ops.bim.cad_offset(distance=self.props.distance) def hotkey_S_Q(self): obj = bpy.context.active_object @@ -288,8 +285,7 @@ class CadHotkey(bpy.types.Operator): return if tool.Geometry.is_profile_object_active(): - si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - bpy.ops.bim.add_rectangle(x=self.props.x / si_conversion, y=self.props.y / si_conversion) + bpy.ops.bim.add_rectangle(x=self.props.x, y=self.props.y) elif ( (RoofData.is_loaded or not RoofData.load()) and RoofData.data["pset_data"] From d37bda6871cbb93954d9b8cc21f951a9a1549745 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 12:37:10 +0500 Subject: [PATCH 172/476] cad operators - add descriptions #6238 --- src/bonsai/bonsai/bim/module/cad/operator.py | 5 +++++ src/bonsai/bonsai/bim/module/cad/workspace.py | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/cad/operator.py b/src/bonsai/bonsai/bim/module/cad/operator.py index c44f5ea644..78a0b887c8 100644 --- a/src/bonsai/bonsai/bim/module/cad/operator.py +++ b/src/bonsai/bonsai/bim/module/cad/operator.py @@ -128,6 +128,7 @@ class CadMitre(bpy.types.Operator): class CadFillet(bpy.types.Operator): bl_idname = "bim.cad_fillet" bl_label = "CAD Fillet" + bl_description = "Add fillet to the 2 selected edges." bl_options = {"REGISTER", "UNDO"} resolution: bpy.props.IntProperty(name="Arc Resolution", min=0, default=1) radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE") @@ -216,6 +217,7 @@ class CadFillet(bpy.types.Operator): class CadArcFrom2Points(bpy.types.Operator): bl_idname = "bim.cad_arc_from_2_points" bl_label = "CAD Arc from 2 Points" + bl_description = "Add an arc to the active mesh based on 2 selected vertices." bl_options = {"REGISTER", "UNDO"} resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1) should_flip: bpy.props.BoolProperty(name="Flip", description="Flip arc", default=False) @@ -342,6 +344,7 @@ class CadArcFrom3Points(bpy.types.Operator): class CadOffset(bpy.types.Operator): bl_idname = "bim.cad_offset" bl_label = "CAD Offset" + bl_description = "Copy selected mesh geometry at provided offset. Mesh copied based on the current viewport angle." bl_options = {"REGISTER", "UNDO"} distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE") @@ -540,6 +543,7 @@ class CadOffset(bpy.types.Operator): class AddIfcCircle(bpy.types.Operator): bl_idname = "bim.add_ifccircle" bl_label = "Add IfcCircle" + bl_description = "Add IfcCircle to the currently active mesh." radius: bpy.props.FloatProperty(name="Radius", default=0.5, subtype="DISTANCE") @classmethod @@ -808,6 +812,7 @@ class AlignViewToProfile(bpy.types.Operator): class AddRectangle(bpy.types.Operator): bl_idname = "bim.add_rectangle" bl_label = "Add Rectangle" + bl_description = "Add rectangle shape to the active mesh." x: bpy.props.FloatProperty(name="X", default=1, subtype="DISTANCE") y: bpy.props.FloatProperty(name="Y", default=1, subtype="DISTANCE") diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index f90c56c35d..cc0c683b39 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -114,9 +114,9 @@ class CadTool(WorkSpaceTool): row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context) + add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Rectangle", "S_R", "", ui_context) + add_layout_hotkey_operator(row, "Rectangle", "S_R", bpy.ops.bim.add_rectangle.__doc__, ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator(row, "Circle", "S_C", bpy.ops.bim.add_ifccircle.__doc__, ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) @@ -138,9 +138,9 @@ class CadTool(WorkSpaceTool): row, "Join", "S_T", "Joins two non-parallel paths at their intersection", ui_context ) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Fillet", "S_F", "Fillet", ui_context) + add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.cad_fillet.__doc__, ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context) + add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context) else: if ( @@ -176,9 +176,9 @@ class CadTool(WorkSpaceTool): row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator(row, "Fillet", "S_F", bpy.ops.bim.add_ifcarcindex_fillet.__doc__, ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "Offset", "S_O", "Offset", ui_context) + add_layout_hotkey_operator(row, "Offset", "S_O", bpy.ops.bim.cad_offset.__doc__, ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) - add_layout_hotkey_operator(row, "2-Point Arc", "S_C", "2-Point Arc", ui_context) + add_layout_hotkey_operator(row, "2-Point Arc", "S_C", bpy.ops.bim.cad_arc_from_2_points.__doc__, ui_context) row = row if ui_context == "TOOL_HEADER" else layout.row(align=True) add_layout_hotkey_operator(row, "3-Point Arc", "S_V", bpy.ops.bim.cad_arc_from_3_points.__doc__, ui_context) From b1c87b1eb4b39140b8a13be07670870321b7a563 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 14:37:01 +0500 Subject: [PATCH 173/476] Set tools hotkeys name to empty string to hide it from tooltip #6238 --- src/bonsai/bonsai/bim/module/cad/workspace.py | 4 ++-- src/bonsai/bonsai/bim/module/covering/workspace.py | 4 ++-- src/bonsai/bonsai/bim/module/drawing/workspace.py | 4 ++-- src/bonsai/bonsai/bim/module/model/workspace.py | 4 ++-- src/bonsai/bonsai/bim/module/project/workspace.py | 4 ++-- src/bonsai/bonsai/bim/module/spatial/workspace.py | 4 ++-- src/bonsai/bonsai/bim/module/structural/workspace.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index cc0c683b39..017f493682 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -185,8 +185,8 @@ class CadTool(WorkSpaceTool): class CadHotkey(bpy.types.Operator): bl_idname = "bim.cad_hotkey" - bl_label = "CAD Hotkey" - bl_options = {"REGISTER", "UNDO"} + bl_label = "" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} hotkey: bpy.props.StringProperty() description: bpy.props.StringProperty() diff --git a/src/bonsai/bonsai/bim/module/covering/workspace.py b/src/bonsai/bonsai/bim/module/covering/workspace.py index b0f41254b6..302f6d3c34 100644 --- a/src/bonsai/bonsai/bim/module/covering/workspace.py +++ b/src/bonsai/bonsai/bim/module/covering/workspace.py @@ -144,8 +144,8 @@ class CoveringToolUI: class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.covering_hotkey" - bl_label = "Hotkey" - bl_options = {"REGISTER", "UNDO"} + bl_label = "" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} hotkey: bpy.props.StringProperty() description: bpy.props.StringProperty() diff --git a/src/bonsai/bonsai/bim/module/drawing/workspace.py b/src/bonsai/bonsai/bim/module/drawing/workspace.py index 1296e859d5..cbe9de1dc3 100644 --- a/src/bonsai/bonsai/bim/module/drawing/workspace.py +++ b/src/bonsai/bonsai/bim/module/drawing/workspace.py @@ -251,8 +251,8 @@ class AnnotationToolUI: class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.annotation_hotkey" - bl_label = "Hotkey" - bl_options = {"REGISTER", "UNDO"} + bl_label = "" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} hotkey: bpy.props.StringProperty() description: bpy.props.StringProperty() diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index d0da684ffa..2556e1cd59 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1045,8 +1045,8 @@ class EditObjectUI: class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.hotkey" - bl_label = "BIM Hotkey" - bl_options = {"REGISTER", "UNDO"} + bl_label = "" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} hotkey: bpy.props.StringProperty() description: bpy.props.StringProperty() x: bpy.props.FloatProperty(name="X", default=0.5) diff --git a/src/bonsai/bonsai/bim/module/project/workspace.py b/src/bonsai/bonsai/bim/module/project/workspace.py index 39187a3fae..bf3abe07c9 100644 --- a/src/bonsai/bonsai/bim/module/project/workspace.py +++ b/src/bonsai/bonsai/bim/module/project/workspace.py @@ -74,8 +74,8 @@ class ExploreTool(bpy.types.WorkSpaceTool): class ExploreHotkey(bpy.types.Operator): bl_idname = "bim.explore_hotkey" - bl_label = "Explore Hotkey" - bl_options = {"REGISTER", "UNDO"} + bl_label = "" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} hotkey: bpy.props.StringProperty() description: bpy.props.StringProperty() diff --git a/src/bonsai/bonsai/bim/module/spatial/workspace.py b/src/bonsai/bonsai/bim/module/spatial/workspace.py index 26c91d8037..aa57b03631 100644 --- a/src/bonsai/bonsai/bim/module/spatial/workspace.py +++ b/src/bonsai/bonsai/bim/module/spatial/workspace.py @@ -124,8 +124,8 @@ class SpatialToolUI: class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.spatial_hotkey" - bl_label = "Hotkey" - bl_options = {"REGISTER", "UNDO"} + bl_label = "" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} hotkey: bpy.props.StringProperty() description: bpy.props.StringProperty() diff --git a/src/bonsai/bonsai/bim/module/structural/workspace.py b/src/bonsai/bonsai/bim/module/structural/workspace.py index 4991150133..bb02867af1 100644 --- a/src/bonsai/bonsai/bim/module/structural/workspace.py +++ b/src/bonsai/bonsai/bim/module/structural/workspace.py @@ -85,8 +85,8 @@ class StructuralToolUI: class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.structural_hotkey" - bl_label = "Hotkey" - bl_options = {"REGISTER", "UNDO"} + bl_label = "" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} hotkey: bpy.props.StringProperty() description: bpy.props.StringProperty() From edfcc3f74b8b50b57cd3545f58320706eaa26a28 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 14:39:38 +0500 Subject: [PATCH 174/476] ifc2sql - change default database name to match default sql_type Co-Authored-By: bsmithuk <68438529+bsmithuk@users.noreply.github.com> --- src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index 4df753cc5d..90ac909e3e 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -68,7 +68,7 @@ class Patcher: host: str = "localhost", username: str = "root", password: str = "pass", - database: str = f"{DEFAULT_DATABASE_NAME}.db", + database: str = f"{DEFAULT_DATABASE_NAME}.sqlite", full_schema: bool = True, is_strict: bool = False, should_expand: bool = False, @@ -82,7 +82,7 @@ class Patcher: :param sql_type: Choose between "SQLite" or "MySQL" :param database: Database path to save the SQL database to (already existing or not). Could also be a directory, then the database will be stored - using default filename (e.g. 'database.db'). + using default filename (e.g. 'database.sqlite'). :filter_glob database: *.db;*.sqlite :param full_schema: if True, will create tables for all IFC classes, regardless if they are used or not in the dataset. If False, will From cc7abe6a8d4434822ef2b5b6c4c3025b8d314a41 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 15:45:42 +0500 Subject: [PATCH 175/476] Fix error applying blender offset to read-only matrix (e.g. during linking) Example: ``` File "\bonsai\bim\module\project\operator.py", line 1815, in execute self.process_occurrence(shape) File "\bonsai\bim\module\project\operator.py", line 1952, in process_occurrence obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, mat) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "\bonsai\tool\loader.py", line 952, in apply_blender_offset_to_matrix_world matrix[0][3] = offset_xyz[0] ~~~~~~~~~^^^ ValueError: assignment destination is read-only ``` Noticed investigating #6242 --- src/bonsai/bonsai/bim/import_ifc.py | 9 ++++++--- src/bonsai/bonsai/tool/loader.py | 18 +++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index e777545780..0ed6f3d9e7 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -38,6 +38,7 @@ import bonsai.tool as tool from bonsai.bim.ifc import IfcStore, IFC_CONNECTED_TYPE from bonsai.tool.loader import OBJECT_DATA_TYPE from typing import Dict, Union, Optional, Any, Literal +from ifcopenshell.util.shape import MatrixType class MaterialCreator: @@ -479,14 +480,16 @@ class IfcImporter: if grid.WAxes: self.create_grid_axes(grid.WAxes, grid_obj, grid_placement) - def create_grid_axes(self, axes, grid_obj, grid_placement): + def create_grid_axes( + self, axes: list[ifcopenshell.entity_instance], grid_obj: bpy.types.Object, grid_placement: MatrixType + ) -> None: for axis in axes: shape = tool.Loader.create_generic_shape(axis.AxisCurve) mesh = self.create_mesh(axis, shape) obj = bpy.data.objects.new(tool.Loader.get_name(axis), mesh) obj.show_in_front = True self.link_element(axis, obj) - self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement.copy())) + self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, grid_placement)) def create_element_types(self): for element_type in self.element_types: @@ -809,7 +812,7 @@ class IfcImporter: if shape: # We use numpy here because Blender mathutils.Matrix is not accurate enough - mat = np.array(shape.transformation.matrix).reshape((4, 4), order="F") + mat = ifcopenshell.util.shape.get_shape_matrix(shape) self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, mat)) assert mesh # Type checker. if not materials_updated: diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index cd7e0471ec..c40adddf00 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -929,13 +929,14 @@ class Loader(bonsai.core.tool.Loader): @classmethod def apply_blender_offset_to_matrix_world(cls, obj: bpy.types.Object, matrix: np.ndarray) -> Matrix: + """ + :param matrix: 4x4 numpy matrix. + """ + # Shouldn't mutate original matrix as we return a different object anyway. + M_TRANSLATION = (slice(0, 3), 3) oprops = tool.Blender.get_object_bim_props(obj) - if ( - not obj.data - and tool.Cad.is_x(matrix[0][3], 0) - and tool.Cad.is_x(matrix[1][3], 0) - and tool.Cad.is_x(matrix[2][3], 0) - ): + translation = matrix[M_TRANSLATION] + if not obj.data and np.allclose(translation, 0.0, atol=1e-5): # We assume any non-geometric matrix at 0,0,0 is not # positionally significant and is left alone. This handles # scenarios where often spatial elements are left at 0,0,0 and @@ -947,11 +948,10 @@ class Loader(bonsai.core.tool.Loader): oprops.blender_offset_type = "CARTESIAN_POINT" if cartesian_point_offset := obj.data.get("cartesian_point_offset", None): oprops.cartesian_point_offset = cartesian_point_offset + matrix = matrix.copy() offset_xyz = list(map(float, cartesian_point_offset.split(","))) + [1.0] offset_xyz = matrix @ offset_xyz - matrix[0][3] = offset_xyz[0] - matrix[1][3] = offset_xyz[1] - matrix[2][3] = offset_xyz[2] + matrix[M_TRANSLATION] = offset_xyz[:3] props = tool.Georeference.get_georeference_props() if props.has_blender_offset: From 8d1255f0bc81cf6ba9bf776d98736d6807c20e6c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 16:03:13 +0500 Subject: [PATCH 176/476] Fix toggling wireframe for odd case when there are no objects linked Noticed investigating #6242 --- src/bonsai/bonsai/bim/module/project/operator.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index eb49a01535..ee06a59b11 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1488,16 +1488,12 @@ class ToggleLinkVisibility(bpy.types.Operator): return {"FINISHED"} def toggle_wireframe(self, link: "Link") -> None: + link.is_wireframe = not link.is_wireframe + display_type = "WIRE" if link.is_wireframe else "TEXTURED" for collection in self.get_linked_collections(): objs = filter(lambda obj: "IfcOpeningElement" not in obj.name, collection.all_objects) - for i, obj in enumerate(objs): - if i == 0: - if obj.display_type == "WIRE": - display_type = "TEXTURED" - else: - display_type = "WIRE" + for obj in objs: obj.display_type = display_type - link.is_wireframe = display_type == "WIRE" def toggle_visibility(self, link: "Link") -> None: linked_collections = self.get_linked_collections() From bd4dc501a27f903563a8bac99a5a27bb2660cdbd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 16:35:36 +0500 Subject: [PATCH 177/476] Pamateric doors/windows - add material select shortcut to UI Example - https://i.imgur.com/UfJTjGz.png --- src/bonsai/bonsai/bim/helper.py | 6 +++++- src/bonsai/bonsai/bim/module/model/ui.py | 19 +++++++++++++------ src/bonsai/bonsai/tool/model.py | 8 ++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/bim/helper.py b/src/bonsai/bonsai/bim/helper.py index 3ac994739a..147eb424b2 100644 --- a/src/bonsai/bonsai/bim/helper.py +++ b/src/bonsai/bonsai/bim/helper.py @@ -293,7 +293,10 @@ def prop_with_search( should_click_ok: bool = False, original_operator_path: Optional[str] = None, **kwargs: Any, -): +) -> bpy.types.UILayout: + """ + :return: Added row. + """ # kwargs are layout.prop arguments (text, icon, etc.) row = layout.row(align=True) row.prop(data, prop_name, **kwargs) @@ -307,6 +310,7 @@ def prop_with_search( op.original_operator_path = original_operator_path or "" except TypeError: # Prop is not iterable pass + return row def get_enum_items( diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 855c1e4434..bdaba83524 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -451,9 +451,12 @@ class BIM_PT_window(bpy.types.Panel): self.layout.use_property_split = True self.layout.label(text="Material Properties") - prop_with_search(self.layout, props, "lining_material") - prop_with_search(self.layout, props, "framing_material", text="Panel Material") - prop_with_search(self.layout, props, "glazing_material") + row = prop_with_search(self.layout, props, "lining_material") + tool.Model.draw_material_ui_select(row, props.lining_material) + row = prop_with_search(self.layout, props, "framing_material", text="Panel Material") + tool.Model.draw_material_ui_select(row, props.framing_material) + row = prop_with_search(self.layout, props, "glazing_material") + tool.Model.draw_material_ui_select(row, props.glazing_material) else: row.operator("bim.enable_editing_window", icon="GREASEPENCIL", text="") row.operator("bim.remove_window", icon="X", text="") @@ -548,10 +551,14 @@ class BIM_PT_door(bpy.types.Panel): self.layout.use_property_split = True self.layout.label(text="Material Properties") - prop_with_search(self.layout, props, "lining_material") - prop_with_search(self.layout, props, "framing_material", text="Panel Material") + + row = prop_with_search(self.layout, props, "lining_material") + tool.Model.draw_material_ui_select(row, props.lining_material) + row = prop_with_search(self.layout, props, "framing_material", text="Panel Material") + tool.Model.draw_material_ui_select(row, props.framing_material) if props.transom_thickness: - prop_with_search(self.layout, props, "glazing_material") + row = prop_with_search(self.layout, props, "glazing_material") + tool.Model.draw_material_ui_select(row, props.framing_material) else: row.operator("bim.enable_editing_door", icon="GREASEPENCIL", text="") row.operator("bim.remove_door", icon="X", text="") diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index dedcbe8b82..e7fa36358b 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -2031,3 +2031,11 @@ class Model(bonsai.core.tool.Model): ifcopenshell.api.grid.create_axis_curve( tool.Ifc.get(), p1=points[0], p2=points[1], is_si=True, grid_axis=grid_axis ) + + @classmethod + def draw_material_ui_select(cls, layout: bpy.types.UILayout, material_id: str) -> None: + material_id_int = int(material_id) + if not material_id_int: + return + op = layout.operator("bim.material_ui_select", icon="ZOOM_SELECTED", text="") + op.material_id = material_id_int From b9f4503206cda9443dfea67c47c0ef275c1c704f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 17:02:13 +0500 Subject: [PATCH 178/476] Paramteric doors/windows - detect previously assigned materials when editing is enabled Example - https://imgur.com/a/FEbSBaX --- src/bonsai/bonsai/bim/module/model/door.py | 1 + src/bonsai/bonsai/bim/module/model/prop.py | 17 +++++++++++++++-- src/bonsai/bonsai/bim/module/model/window.py | 1 + src/bonsai/bonsai/tool/model.py | 13 +++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/door.py b/src/bonsai/bonsai/bim/module/model/door.py index 18e8b90f84..1d53a2c279 100644 --- a/src/bonsai/bonsai/bim/module/model/door.py +++ b/src/bonsai/bonsai/bim/module/model/door.py @@ -640,6 +640,7 @@ class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator): data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Door", "Data")) data.update(data.pop("lining_properties")) data.update(data.pop("panel_properties")) + data.update(tool.Model.get_constituents_props_data(element)) # required since we could load pset from .ifc and BIMDoorProperties won't be set props.set_props_kwargs_from_ifc_data(data) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index d02adfcf8d..8c438a384f 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -491,7 +491,13 @@ WindowType = Literal[ # default prop values are in mm and converted later class BIMWindowProperties(PropertyGroup): - non_si_units_props = ("is_editing", "window_type") + non_si_units_props = ( + "is_editing", + "window_type", + "lining_material", + "framing_material", + "glazing_material", + ) # number of panels and default mullion/transom values # fmt: off @@ -690,7 +696,14 @@ DoorType = Literal[ class BIMDoorProperties(PropertyGroup): - non_si_units_props = ("is_editing", "door_type", "panel_width_ratio") + non_si_units_props = ( + "is_editing", + "door_type", + "panel_width_ratio", + "lining_material", + "framing_material", + "glazing_material", + ) is_editing: bpy.props.BoolProperty(default=False) door_type: bpy.props.EnumProperty( name="Door Operation Type", diff --git a/src/bonsai/bonsai/bim/module/model/window.py b/src/bonsai/bonsai/bim/module/model/window.py index 41b767d111..a2ef7507f3 100644 --- a/src/bonsai/bonsai/bim/module/model/window.py +++ b/src/bonsai/bonsai/bim/module/model/window.py @@ -550,6 +550,7 @@ class EnableEditingWindow(bpy.types.Operator, tool.Ifc.Operator): data = json.loads(ifcopenshell.util.element.get_pset(element, "BBIM_Window", "Data")) data.update(data.pop("lining_properties")) data.update(data.pop("panel_properties")) + data.update(tool.Model.get_constituents_props_data(element)) # required since we could load pset from .ifc and BIMWindowProperties won't be set props.set_props_kwargs_from_ifc_data(data) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index e7fa36358b..d8f986f48c 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -133,6 +133,19 @@ class Model(bonsai.core.tool.Model): data[prop_name] = prop_value * si_conversion return data + @classmethod + def get_constituents_props_data(cls, element: ifcopenshell.entity_instance) -> dict[str, str]: + constituents = ("lining", "framing", "glazing") + props: dict[str, str] = {f"{constituent}_material": "0" for constituent in constituents} + material = ifcopenshell.util.element.get_material(element) + if not material or not material.is_a("IfcMaterialConstituentSet"): + return props + for constituent in material.MaterialConstituents: + name = (constituent.Name or "").lower() + if name in constituents: + props[f"{name}_material"] = str(constituent.Material.id()) + return props + @classmethod def convert_mesh_to_curve( cls, position: Matrix, edge_indices: list[tuple[int, int]] From c81fe97cc22e344aed424d30d3294a8d56dbbeb6 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 17:11:19 +0500 Subject: [PATCH 179/476] Fix bim.change_cardinal_point breaking if one of objects had no material --- src/bonsai/bonsai/bim/module/model/profile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index c094da200c..22f7fa5071 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -911,7 +911,7 @@ class ChangeProfileDepth(bpy.types.Operator, tool.Ifc.Operator): class ChangeCardinalPoint(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.change_cardinal_point" bl_label = "Update" - bl_description = "Update Cardinal Point" + bl_description = "Update Cardinal Point for all selected objects." bl_options = {"REGISTER", "UNDO"} cardinal_point: bpy.props.IntProperty() @@ -926,6 +926,8 @@ class ChangeCardinalPoint(bpy.types.Operator, tool.Ifc.Operator): if not element: continue material = ifcopenshell.util.element.get_material(element, should_skip_usage=False) + if not material: + continue if material.is_a("IfcMaterialProfileSetUsage"): material.CardinalPoint = self.cardinal_point objs.append(obj) From db52e701218b2c3df49d38953b829b58ef9787ea Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 18:06:33 +0500 Subject: [PATCH 180/476] Fix possibility of adding new invalid surface styles 1) Add default values for new IfcSurfaceStyleLighting so they won't appear invalid. 2) Temporarily disable starting surface style with a texture style since it requires additional texture UI to be exposed or some default texture to be assigned to keep it valid. --- .../bonsai/bim/module/style/operator.py | 44 +++++++++++++------ .../api/style/add_surface_style.py | 7 +-- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index cf5e0a9b66..6ec0ccdc3c 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -643,32 +643,48 @@ class AddPresentationStyle(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = tool.Style.get_style_props() + ifc_file = tool.Ifc.get() if props.style_type == "IfcSurfaceStyle": - style = ifcopenshell.api.run("style.add_style", tool.Ifc.get(), name=props.style_name) + + def get_colour_dict(name: Union[str, None], r: float, g: float, b: float) -> dict[str, Any]: + return { + "Name": name, + "Red": r, + "Green": g, + "Blue": b, + } # setup surface style element surface_style = None if props.surface_style_class in ("IfcSurfaceStyleShading", "IfcSurfaceStyleRendering"): attributes = { - "SurfaceColour": { - "Name": None, - "Red": props.surface_colour[0], - "Green": props.surface_colour[1], - "Blue": props.surface_colour[2], - } + "SurfaceColour": get_colour_dict(None, *props.surface_colour), } if props.surface_style_class == "IfcSurfaceStyleRendering": attributes["ReflectanceMethod"] = "NOTDEFINED" + elif props.surface_style_class == "IfcSurfaceStyleLighting": + # Requires all those colors to be valid style. + attributes = { + "DiffuseTransmissionColour": get_colour_dict(None, 0.0, 0.0, 0.0), + "DiffuseReflectionColour": get_colour_dict(None, 0.0, 0.0, 0.0), + "TransmissionColour": get_colour_dict(None, 0.0, 0.0, 0.0), + "ReflectanceColour": get_colour_dict(None, 0.0, 0.0, 0.0), + } + elif props.surface_style_class == "IfcSurfaceStyleWithTextures": + # TODO: Requires textures to be valid. + self.report( + {"ERROR"}, + "Adding IfcSurfaceStyleWithTextures directly is not supported yet." + "You can create Rendering style and then add Texture style to it.", + ) + return {"CANCELLED"} else: - # NOTE: for all other styles we produce just empty styles. - # In the future we might need to expose to adding presentation style UI - # LightingStyle colors and TextureStyle textures UI - # as they are required for those surface styles to keep IFC valid + # The rest of styles are valid even without any attributes assigned. attributes = {} - surface_style = ifcopenshell.api.run( - "style.add_surface_style", - tool.Ifc.get(), + style = ifcopenshell.api.style.add_style(ifc_file, name=props.style_name) + surface_style = ifcopenshell.api.style.add_surface_style( + ifc_file, style=style, ifc_class=props.surface_style_class, attributes=attributes, diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py index 944240e618..55af1b35b2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/add_surface_style.py @@ -22,11 +22,12 @@ from typing import Any, Optional, Literal SURFACE_STYLE_TYPES = Literal[ - "IfcExternallyDefinedSurfaceStyle", + "IfcSurfaceStyleShading", + "IfcSurfaceStyleRendering", + "IfcSurfaceStyleWithTextures", "IfcSurfaceStyleLighting", "IfcSurfaceStyleRefraction", - "IfcSurfaceStyleShading", - "IfcSurfaceStyleWithTextures", + "IfcExternallyDefinedSurfaceStyle", ] From 06fa2b05d68b83dcff3f105d9e86eea4255b31a3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 18:12:55 +0500 Subject: [PATCH 181/476] Fix #6245 after 17642ca --- .../ifcopenshell/api/material/edit_profile_usage.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index 8a583d64b4..e330cd9ffb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -98,6 +98,7 @@ class Usecase: file: ifcopenshell.file def execute(self, usage: ifcopenshell.entity_instance, attributes: dict[str, Any]) -> None: + self.usage = usage self.attributes = attributes self.cardinal_point = attributes.get("CardinalPoint") if self.cardinal_point and self.cardinal_point != usage.CardinalPoint: @@ -107,7 +108,7 @@ class Usecase: setattr(usage, name, value) def update_cardinal_point(self): - material_set = self.attributes["usage"].ForProfileSet + material_set = self.usage.ForProfileSet self.profile = material_set.CompositeProfile if not self.profile and material_set.MaterialProfiles: self.profile = material_set.MaterialProfiles[0].Profile @@ -117,13 +118,13 @@ class Usecase: self.position = self.calculate_position() if self.file.schema == "IFC2X3": - for rel in self.file.get_inverse(self.attributes["usage"]): + for rel in self.file.get_inverse(self.usage): if not rel.is_a("IfcRelAssociatesMaterial"): continue for element in rel.RelatedObjects: self.update_representation(element) else: - for rel in self.attributes["usage"].AssociatedTo: + for rel in self.usage.AssociatedTo: for element in rel.RelatedObjects: self.update_representation(element) From e68c47a897d42b667687b6dc0fd1bc6cf1f49f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 26 Feb 2025 09:09:09 -0300 Subject: [PATCH 182/476] Remove code related to 0104a2cc9f6393a88c0701f4f12d7cfaf202bab1 --- src/bonsai/bonsai/tool/polyline.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index ff9a4ca441..471826b22e 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -529,11 +529,6 @@ class Polyline(bonsai.core.tool.Polyline): angle = tool.Cad.angle_3_vectors(v1, v2, v3, new_angle=None, degrees=True) if tool.Cad.is_x(angle, 0): return - # TODO move this limitation to be Wall tool specific. Right now it also affects Measure tool - # Avoids creating segments smaller then 0.1. This is a limitation from create_wall_from_2_points - length = ( - Vector((x, y, z)) - Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z)) - ).length polyline_point = polyline_points.add() polyline_point.x = x From 2609a8293c216763c8a553d15494e846a0607fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 26 Feb 2025 13:00:11 -0300 Subject: [PATCH 183/476] Fix issue where walls are created with obtuse x_angle. See #5938 --- src/bonsai/bonsai/bim/module/model/polyline.py | 1 + .../ifcopenshell/api/geometry/add_wall_representation.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 5788bb148f..2af857f663 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -85,6 +85,7 @@ def get_wall_preview_data(context, relating_type): if x_angle > radians(90) or x_angle < radians(-90): height *= -1 angle_distance = height * tan(x_angle) + thickness *= 1 / cos(x_angle) data = {} data["verts"] = [] diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py index 36691a044b..28e0112be9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_wall_representation.py @@ -18,7 +18,7 @@ import ifcopenshell.util.element import ifcopenshell.util.unit -from math import sin, cos +from math import sin, cos, pi from typing import Optional, Union, Any from ifcopenshell.util.data import Clipping @@ -112,7 +112,7 @@ class Usecase: self.file.createIfcDirection((1.0, 0.0, 0.0)), ), extrusion_direction, - self.convert_si_to_unit(self.settings["height"]) * (1 / cos(self.settings["x_angle"])), + self.convert_si_to_unit(self.settings["height"]) * abs((1 / cos(self.settings["x_angle"]))), ) if self.settings["booleans"]: extrusion = self.apply_booleans(extrusion) From 7a69d6f20e238d230a1e5fb7a51dcddffceff059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 26 Feb 2025 13:12:35 -0300 Subject: [PATCH 184/476] Fix issue when updating wall x_angle with obtuse angle. See #5938 --- src/bonsai/bonsai/bim/module/model/wall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 5b60551143..abe2f5607f 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -1480,7 +1480,7 @@ class DumbWallJoiner: results["direction"] = Vector(item.ExtrudedDirection.DirectionRatios) results["x_angle"] = Vector((0, 1)).angle_signed(Vector((y, z))) results["is_sloped"] = True - results["height"] = (item.Depth * self.unit_scale) / (1 / cos(results["x_angle"])) + results["height"] = (item.Depth * self.unit_scale) / abs(1 / cos(results["x_angle"])) break elif item.is_a("IfcBooleanClippingResult"): # should be before IfcBooleanResult check item = item.FirstOperand From 1855ac421c502cd3b33d41e246f6b8a31760fd42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 27 Feb 2025 20:36:56 -0300 Subject: [PATCH 185/476] Refactor slab addition to support obtuse x_angle. See #5938 To support obtuse x_angle a refactoring had to be made, which helped improve the general code for slab addition. This is challenging because there are a few features that interact with each other to create slabs, like `depth`, `direction_sense`, `offset` and `x_angle`. In addition, these interactions can happen in different parts of the code. This refactor improves the coherence between those different parts. Files changed: - `api/geometry/add_slab_representation.py` - `model/slab.py` - inside the function `change_thickness()` File to be changed in a following commit: - `model/wall.py` - inside the operator `ChangeExtrusionXAngle` - To-do --- src/bonsai/bonsai/bim/module/model/slab.py | 52 ++++++++++++------- src/bonsai/bonsai/tool/model.py | 19 ++----- .../api/geometry/add_slab_representation.py | 36 +++++++------ 3 files changed, 57 insertions(+), 50 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 1010217cae..a5ac6ae121 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -31,7 +31,7 @@ import bonsai.core.geometry import bonsai.core.root import bonsai.tool as tool from bonsai.bim.ifc import IfcStore -from math import cos +from math import cos, pi from mathutils import Vector, Matrix from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator from bonsai.bim.module.model.polyline import PolylineOperator @@ -135,7 +135,7 @@ class DumbSlabGenerator: matrix_world.translation.z = self.container_obj.location.z else: matrix_world.translation.z - obj.matrix_world = matrix_world @ Matrix.Rotation(self.x_angle, 4, "X") + obj.matrix_world = matrix_world bpy.context.view_layer.update() element = bonsai.core.root.assign_class( @@ -170,6 +170,7 @@ class DumbSlabGenerator: is_global=True, should_sync_changes_first=False, ) + obj.matrix_world = obj.matrix_world @ Matrix.Rotation(self.x_angle, 4, "X") if self.footprint_context: extrusion = tool.Model.get_extrusion(representation) @@ -294,28 +295,39 @@ class DumbSlabPlaner: if representation: extrusion = tool.Model.get_extrusion(representation) if extrusion: - x, y, z = extrusion.ExtrudedDirection.DirectionRatios existing_x_angle = tool.Model.get_existing_x_angle(extrusion) - perpendicular_depth = thickness * (1 / cos(existing_x_angle)) - perpendicular_offset = layer_params["offset"] * (1 / cos(existing_x_angle)) - offset_vector = Vector((0.0, 0.0, perpendicular_offset / self.unit_scale)) - if layer_params["direction_sense"] == "POSITIVE": - y = abs(y) if existing_x_angle > 0 else -abs(y) - z = abs(z) - elif layer_params["direction_sense"] == "NEGATIVE": - y = -abs(y) if existing_x_angle > 0 else abs(y) - z = -abs(z) - extrusion.ExtrudedDirection.DirectionRatios = (x, y, z) + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle + direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios) + offset_direction = direction_ratios.copy() + perpendicular_depth = thickness * abs(1 / cos(existing_x_angle)) + perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle)) / self.unit_scale + + # Check angle and z direction to determine whether the extrusion direction is positive or negative + if (existing_x_angle < (pi / 2) and direction_ratios.z > 0) or ( + existing_x_angle > (pi / 2) and direction_ratios.z < 0 + ): + # The extrusion direction is positive. If the layer_parameter is set to negative, + # then the we change the extrusion direction. + # The offset direction must always be positive, so we keep it. + if layer_params["direction_sense"] == "NEGATIVE": + direction_ratios *= -1 + elif (existing_x_angle > (pi / 2) and direction_ratios.z > 0) or ( + existing_x_angle < (pi / 2) and direction_ratios.z < 0 + ): + # The extrusion direction is negative. If the layer_parameter is set to positive, + # then the we change the extrusion direction. + # The offset direction must always be positive, so we change it too. + if layer_params["direction_sense"] == "POSITIVE": + direction_ratios *= -1 + offset_direction *= -1 + + extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios) extrusion.Depth = perpendicular_depth if perpendicular_offset != 0.0 and not extrusion.Position: - tool.Model.add_extrusion_position(extrusion, perpendicular_offset) - - # Update the extrusion's location based on its current rotation angle and offset - if extrusion.Position: - rot_matrix = Matrix.Rotation(existing_x_angle, 4, "X") - rot_offset = offset_vector @ rot_matrix - extrusion.Position.Location.Coordinates = tuple(rot_offset) + position = offset_direction * perpendicular_offset + tool.Model.add_extrusion_position(extrusion, position) else: props = tool.Model.get_model_props() diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index d8f986f48c..2af631b671 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -2008,33 +2008,22 @@ class Model(bonsai.core.tool.Model): FilledOpeningGenerator().generate(filling_obj, voided_obj) @classmethod - def add_extrusion_position(cls, extrusion: ifcopenshell.entity_instance, offset: float) -> None: + def add_extrusion_position(cls, extrusion: ifcopenshell.entity_instance, position: tuple) -> None: ifc_file = tool.Ifc.get() - position = ifc_file.createIfcAxis2Placement3D( - ifc_file.createIfcCartesianPoint((0.0, 0.0, offset)), + new_position = ifc_file.createIfcAxis2Placement3D( + ifc_file.createIfcCartesianPoint(position), ifc_file.createIfcDirection((0.0, 0.0, 1.0)), ifc_file.createIfcDirection((1.0, 0.0, 0.0)), ) - extrusion.Position = position + extrusion.Position = new_position @classmethod def get_existing_x_angle(cls, extrusion: ifcopenshell.entity_instance) -> float: x, y, z = extrusion.ExtrudedDirection.DirectionRatios vector = Vector((0, 1)) x_angle = vector.angle_signed(Vector((y, z))) - - # The extrusion direction is changed by the layer direction change - # So we have to adapt the values of y, z and vector accordingly - if z < 0 and y < 0: - y = abs(y) - z = abs(z) - if z < 0 and y >= 0: - vector = Vector((0, -1)) - - x_angle = vector.angle_signed(Vector((y, z))) - return x_angle @classmethod diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py index 05ed08edf4..6443e3a857 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py @@ -85,31 +85,37 @@ class Usecase: points = ((0.0, 0.0), (size, 0.0), (size, size), (0.0, size), (0.0, 0.0)) if self.settings["polyline"]: points = [ - (self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * (1 / cos(self.settings["x_angle"])))) + (self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.settings["x_angle"])))) for p in self.settings["polyline"] ] if self.file.schema == "IFC2X3": curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points]) else: curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList2D(points)) - if self.settings["x_angle"]: - extrusion_direction = self.file.createIfcDirection( - (0.0, sin(self.settings["x_angle"]), cos(self.settings["x_angle"])) - ) - if self.settings["direction_sense"] == "NEGATIVE": - extrusion_direction = self.file.createIfcDirection( - (0.0, -sin(self.settings["x_angle"]), -cos(self.settings["x_angle"])) - ) - else: - extrusion_direction = self.file.createIfcDirection((0.0, 0.0, 1.0)) - if self.settings["direction_sense"] == "NEGATIVE": - extrusion_direction = self.file.createIfcDirection((0.0, 0.0, -1.0)) + if self.settings["x_angle"]: + direction_ratios = (0.0, sin(self.settings["x_angle"]), cos(self.settings["x_angle"])) + else: + direction_ratios = (0.0, 0.0, 1.0) + + offset_direction = direction_ratios # offset direction doesn't change if direction_sense is negative + extrusion_direction = self.file.createIfcDirection(direction_ratios) + if self.settings["direction_sense"] == "NEGATIVE": + direction_ratios = tuple((-n for n in direction_ratios)) + extrusion_direction = self.file.createIfcDirection(direction_ratios) + + perpendicular_offset = self.convert_si_to_unit(self.settings["offset"]) * abs(1 / cos(self.settings["x_angle"])) + perpendicular_depth = self.convert_si_to_unit(self.settings["depth"]) * abs(1 / cos(self.settings["x_angle"])) position = None # default position for IFC2X3 where .Position is not optional if self.file.schema == "IFC2X3" or self.settings["offset"] != 0: + position_vector = ( + offset_direction[0] * perpendicular_offset, + offset_direction[1] * perpendicular_offset, + offset_direction[2] * perpendicular_offset, + ) position = self.file.createIfcAxis2Placement3D( - self.file.createIfcCartesianPoint((0.0, 0.0, self.convert_si_to_unit(self.settings["offset"]))), + self.file.createIfcCartesianPoint(position_vector), self.file.createIfcDirection((0.0, 0.0, 1.0)), self.file.createIfcDirection((1.0, 0.0, 0.0)), ) @@ -118,7 +124,7 @@ class Usecase: self.file.createIfcArbitraryClosedProfileDef("AREA", None, curve), position, extrusion_direction, - self.convert_si_to_unit(self.settings["depth"]) * 1 / cos(self.settings["x_angle"]), + perpendicular_depth, ) if self.settings["clippings"]: return self.apply_clippings(extrusion) From 0eb32ed7b76a8ae4c19a4d0331a3303eb1b821d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 28 Feb 2025 11:46:02 -0300 Subject: [PATCH 186/476] Refactor `ChangeExtrusionXAngle` to support obtuse x_angle for slabs. To support obtuse x_angle a refactoring had to be made, which helped improve the general code for slab addition. This is challenging because there are a few features that interact with each other to create slabs, like `depth`, `direction_sense`, `offset` and `x_angle`. In addition, these interactions can happen in different parts of the code. This commit addresses `ChangeExtrusionXAngle` in `model/wall.py`. See e8e88c25fdcef55826443fe6b000aec1baa6f25a for more information. --- src/bonsai/bonsai/bim/module/model/slab.py | 9 +-- src/bonsai/bonsai/bim/module/model/wall.py | 77 +++++++++++++--------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index a5ac6ae121..ab9c6a8f8b 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -304,16 +304,17 @@ class DumbSlabPlaner: perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle)) / self.unit_scale # Check angle and z direction to determine whether the extrusion direction is positive or negative - if (existing_x_angle < (pi / 2) and direction_ratios.z > 0) or ( - existing_x_angle > (pi / 2) and direction_ratios.z < 0 + if (abs(existing_x_angle) < (pi / 2) and direction_ratios.z > 0) or ( + abs(existing_x_angle) > (pi / 2) and direction_ratios.z < 0 ): # The extrusion direction is positive. If the layer_parameter is set to negative, # then the we change the extrusion direction. # The offset direction must always be positive, so we keep it. if layer_params["direction_sense"] == "NEGATIVE": direction_ratios *= -1 - elif (existing_x_angle > (pi / 2) and direction_ratios.z > 0) or ( - existing_x_angle < (pi / 2) and direction_ratios.z < 0 + # offset_direction *= -1 + elif (abs(existing_x_angle )> (pi / 2) and direction_ratios.z > 0) or ( + abs(existing_x_angle )< (pi / 2) and direction_ratios.z < 0 ): # The extrusion direction is negative. If the layer_parameter is set to positive, # then the we change the extrusion direction. diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index abe2f5607f..440f022db3 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -232,50 +232,66 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): extrusion = tool.Model.get_extrusion(representation) if not extrusion: return - x, y, z = extrusion.ExtrudedDirection.DirectionRatios - existing_x_angle = tool.Model.get_existing_x_angle(extrusion) - perpendicular_depth = extrusion.Depth / (1 / cos(existing_x_angle)) - if tool.Model.get_usage_type(element) == "LAYER2": - extrusion.Depth = abs(perpendicular_depth * (1 / cos(x_angle))) - if tool.Model.get_usage_type(element) == "LAYER3": - # TODO support angles between 91 and 179 - if x_angle > radians(90) or x_angle < -radians(90): - return - extrusion.Depth = perpendicular_depth * (1 / cos(x_angle)) - extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle)) if tool.Model.get_usage_type(element) == "LAYER2": + x, y, z = extrusion.ExtrudedDirection.DirectionRatios + existing_x_angle = tool.Model.get_existing_x_angle(extrusion) + perpendicular_depth = extrusion.Depth / (1 / cos(existing_x_angle)) + if tool.Model.get_usage_type(element) == "LAYER2": + extrusion.Depth = abs(perpendicular_depth * (1 / cos(x_angle))) + extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle)) layer2_objs.append(obj) else: if tool.Model.get_usage_type(element) == "LAYER3": + existing_x_angle = tool.Model.get_existing_x_angle(extrusion) + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle + x_angle = 0 if tool.Cad.is_x(x_angle, 0, tolerance=0.001) else x_angle + x_angle = 0 if tool.Cad.is_x(x_angle, pi, tolerance=0.001) else x_angle + # Reset the transformation and returns to the original points with 0 degrees extrusion.SweptArea.OuterCurve.Points.CoordList = [ - (p[0], p[1] * (cos(existing_x_angle))) for p in extrusion.SweptArea.OuterCurve.Points.CoordList + (p[0], p[1] * abs(cos(existing_x_angle))) for p in extrusion.SweptArea.OuterCurve.Points.CoordList ] # Apply the transformation for the new x_angle extrusion.SweptArea.OuterCurve.Points.CoordList = [ - (p[0], p[1] * (1 / cos(x_angle))) for p in extrusion.SweptArea.OuterCurve.Points.CoordList + (p[0], p[1] * abs(1 / cos(x_angle))) for p in extrusion.SweptArea.OuterCurve.Points.CoordList ] # The extrusion direction calculated previously default to the positive direction # Here we set the extrusion direction to negative if that's the case - x, y, z = extrusion.ExtrudedDirection.DirectionRatios + direction_ratios = Vector((0.0, sin(x_angle), cos(x_angle))) + # direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios) layer_params = tool.Model.get_material_layer_parameters(element) - perpendicular_offset = layer_params["offset"] * (1 / cos(x_angle)) - offset_vector = Vector((0.0, 0.0, perpendicular_offset / unit_scale)) - if layer_params["direction_sense"] == "NEGATIVE": - y = -abs(y) if x_angle > 0 else abs(y) - z = -abs(z) - extrusion.ExtrudedDirection.DirectionRatios = (x, y, z) + perpendicular_depth= layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale + perpendicular_offset = layer_params["offset"] * abs(1 / cos(x_angle)) / unit_scale + offset_direction = direction_ratios.copy() - if perpendicular_offset != 0.0 and not extrusion.Position: - tool.Model.add_extrusion_position(extrusion, perpendicular_offset) + # Check angle and z direction to determine whether the extrusion direction is positive or negative + if (abs(x_angle) < (pi / 2) and direction_ratios.z > 0) or ( + abs(x_angle) > (pi / 2) and direction_ratios.z < 0 + ): + # The extrusion direction is positive. If the layer_parameter is set to negative, + # then the we change the extrusion direction. + # The offset direction must always be positive, so we keep it. + if layer_params["direction_sense"] == "NEGATIVE": + direction_ratios *= -1 + elif ((x_angle) > (pi / 2) and direction_ratios.z > 0) or ( + (x_angle) < (pi / 2) and direction_ratios.z < 0 + ): + # The extrusion direction is negative. If the layer_parameter is set to positive, + # then the we change the extrusion direction. + # The offset direction must always be positive, so we change it too. + if layer_params["direction_sense"] == "POSITIVE": + direction_ratios *= -1 + offset_direction *= -1 - # Update the extrusion's location based on its current rotation angle - if extrusion.Position: - rot_matrix = Matrix.Rotation(x_angle, 4, "X") - rot_offset = offset_vector @ rot_matrix - extrusion.Position.Location.Coordinates = tuple(rot_offset) + extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios) + extrusion.Depth = perpendicular_depth + + if extrusion.Position or perpendicular_offset != 0: + position = offset_direction * perpendicular_offset + tool.Model.add_extrusion_position(extrusion, position) bonsai.core.geometry.switch_representation( tool.Ifc, @@ -288,11 +304,8 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): ) # Object rotation - local_rot_mat = obj.rotation_euler.to_matrix() - rot_mat = mathutils.Matrix.Rotation(x_angle - existing_x_angle, 4, "X") - new_rot_mat = local_rot_mat.to_4x4() @ rot_mat - new_rot_euler = new_rot_mat.to_euler() - obj.rotation_euler = new_rot_euler + rot_mat = mathutils.Matrix.Rotation(x_angle, 4, "X") + obj.rotation_euler = rot_mat.to_euler() if layer2_objs: DumbWallRecalculator().recalculate(layer2_objs) From 134a480b035802baebd4a3dddae033efc02a3b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 28 Feb 2025 13:01:33 -0300 Subject: [PATCH 187/476] Fix slab preview decorator when using obtuse x_angle. --- src/bonsai/bonsai/bim/module/model/polyline.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 2af857f663..3ff5b349a5 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -201,12 +201,12 @@ def get_slab_preview_data(context, relating_type): layers = tool.Model.get_material_layer_parameters(relating_type) if not layers["thickness"]: return - thickness = layers["thickness"] + thickness = layers["thickness"] * abs(1 / cos(x_angle)) thickness *= direction offset_type = model_props.offset_type_horizontal unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - offset = model_props.offset * unit_scale + offset = model_props.offset * abs(1 / cos(x_angle)) * unit_scale data = {} data["verts"] = [] @@ -238,7 +238,10 @@ def get_slab_preview_data(context, relating_type): bm = create_bmesh_from_vertices(polyline_vertices, is_closed) bm.verts.ensure_lookup_table() if x_angle: - bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=Matrix.Rotation(x_angle, 3, "X")) + rot_mat = Matrix.Rotation(x_angle, 3, "X") + if abs(x_angle) > (pi/2): + rot_mat = rot_mat @ Matrix.Scale(-1, 3, (0, 1, 0)) + bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=rot_mat) new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges) new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:]) new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)] From 43931e951e054e23d1769f1d9bdea9275cfb89d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 28 Feb 2025 13:19:51 -0300 Subject: [PATCH 188/476] Fix another issue (follows 2609a8293c216763c8a553d15494e846a0607fc8) with walls created with obtuse x_angle. --- src/bonsai/bonsai/bim/module/model/wall.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 440f022db3..2f0f41b24a 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -220,7 +220,8 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): layer2_objs = [] other_objs = [] - x_angle = self.x_angle + x_angle = 0 if tool.Cad.is_x(self.x_angle, 0, tolerance=0.001) else self.x_angle + x_angle = 0 if tool.Cad.is_x(self.x_angle, pi, tolerance=0.001) else self.x_angle unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) for obj in context.selected_objects: element = tool.Ifc.get_entity(obj) @@ -232,22 +233,19 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): extrusion = tool.Model.get_extrusion(representation) if not extrusion: return + existing_x_angle = tool.Model.get_existing_x_angle(extrusion) + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle if tool.Model.get_usage_type(element) == "LAYER2": x, y, z = extrusion.ExtrudedDirection.DirectionRatios - existing_x_angle = tool.Model.get_existing_x_angle(extrusion) - perpendicular_depth = extrusion.Depth / (1 / cos(existing_x_angle)) - if tool.Model.get_usage_type(element) == "LAYER2": - extrusion.Depth = abs(perpendicular_depth * (1 / cos(x_angle))) + depth = extrusion.Depth / abs(1 / cos(existing_x_angle)) + perpendicular_depth = depth * abs(1 / cos(x_angle)) + print(extrusion.Depth, perpendicular_depth) extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle)) layer2_objs.append(obj) + extrusion.Depth = perpendicular_depth else: if tool.Model.get_usage_type(element) == "LAYER3": - existing_x_angle = tool.Model.get_existing_x_angle(extrusion) - existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle - existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle - x_angle = 0 if tool.Cad.is_x(x_angle, 0, tolerance=0.001) else x_angle - x_angle = 0 if tool.Cad.is_x(x_angle, pi, tolerance=0.001) else x_angle - # Reset the transformation and returns to the original points with 0 degrees extrusion.SweptArea.OuterCurve.Points.CoordList = [ (p[0], p[1] * abs(cos(existing_x_angle))) for p in extrusion.SweptArea.OuterCurve.Points.CoordList From efc6842a9d4081884df026e78450b90b96a23b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 28 Feb 2025 13:44:00 -0300 Subject: [PATCH 189/476] Improve snapping for empty objects. --- src/bonsai/bonsai/tool/raycast.py | 18 ++++++++++++++++++ src/bonsai/bonsai/tool/snap.py | 23 +++++++---------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 919efd94b9..1dcefd86d2 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -154,6 +154,24 @@ class Raycast(bonsai.core.tool.Raycast): except: loc = Vector((0, 0, 0)) + # For empty object we just get the object location and return + if obj.type == "EMPTY": + v = obj.location + intersection = tool.Cad.point_on_edge(v, (ray_target, loc)) + intersection = tool.Cad.point_on_edge(v, (ray_target, loc)) + distance = (v - intersection).length + if distance < snap_threshold: + snap_point = { + "object": obj, + "type": "Vertex", + "point": v.copy(), + "distance": distance, + } + points.append(snap_point) + print("empty", snap_point) + return points + + if not custom_bmesh: bm = bmesh.new() if face is None: # Object without faces diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 01ce5ecaaf..0392acfad6 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -403,13 +403,13 @@ class Snap(bonsai.core.tool.Snap): # Edge-Vertex for obj in objs_to_raycast: - if obj.type == "MESH": - if len(obj.data.polygons) == 0: - snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj) - if snap_points: - for point in snap_points: - point["group"] = "Edge-Vertex" - detected_snaps.append(point) + if obj.type in {"MESH", "EMPTY"}: + # if len(obj.data.polygons) == 0: + snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj) + if snap_points: + for point in snap_points: + point["group"] = "Edge-Vertex" + detected_snaps.append(point) if obj.type == "CURVE": new_object = bpy.data.objects.new("new_object", obj.to_mesh().copy()) snap_points = tool.Raycast.ray_cast_by_proximity(context, event, new_object) @@ -417,15 +417,6 @@ class Snap(bonsai.core.tool.Snap): for point in snap_points: point["group"] = "Edge-Vertex" detected_snaps.append(point) - if obj.type == "EMPTY": - snap_point = { - "type": "Vertex", - "point": obj.location, - "distance": 10, # High value so it has low priority - "object": obj, - "group": "Edge-Vertex", - } - detected_snaps.append(snap_point) # Obj if (space.shading.type == "SOLID" and space.shading.show_xray) or ( From f4b0fe6560b9d7da170c1276008061ea14014600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 28 Feb 2025 13:52:20 -0300 Subject: [PATCH 190/476] Polyline tool - Fix issue where Y and Z axis lock were inverted when in YZ plane. --- src/bonsai/bonsai/tool/snap.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 0392acfad6..2d2b2ae08b 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -477,11 +477,11 @@ class Snap(bonsai.core.tool.Snap): if tool_state.plane_method: if tool_state.plane_method in {"XY", "XZ"} and tool_state.axis_method == "X": tool_state.snap_angle = 180 - if tool_state.plane_method in {"XY", "YZ"} and tool_state.axis_method == "Y": + if tool_state.plane_method in {"XY"} and tool_state.axis_method == "Y": tool_state.snap_angle = 90 - if tool_state.plane_method in {"YZ"} and tool_state.axis_method == "Z": + if tool_state.plane_method in {"YZ"} and tool_state.axis_method == "Y": tool_state.snap_angle = 180 - if tool_state.plane_method in {"XZ"} and tool_state.axis_method == "Z": + if tool_state.plane_method in {"XZ", "YZ"} and tool_state.axis_method == "Z": tool_state.snap_angle = 90 if tool_state.lock_axis or tool_state.axis_method: # Doesn't update snap_angle so that it keeps in the same axis From 945099b817c6dd646626dd678844ede80bfb69f4 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 1 Mar 2025 18:04:41 -0600 Subject: [PATCH 191/476] Set IfcMaterialLayerSetUsage's DirectionSense/OffsetFromReferenceLine/ReferenceExtent on all selected objects --- .../bonsai/bim/module/material/operator.py | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 80f6aafffc..79168e7d1f 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -582,26 +582,36 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator): attributes=attributes, ) + if self.material_set_usage: material_set_usage = self.file.by_id(self.material_set_usage) attributes = bonsai.bim.helper.export_attributes(props.material_set_usage_attributes) + if material_set_usage.is_a("IfcMaterialLayerSetUsage"): ifcopenshell.api.material.edit_layer_usage( self.file, usage=material_set_usage, attributes=attributes, ) - slab.DumbSlabPlaner().regenerate_from_layer_set(material_set_usage.ForLayerSet) - wall.DumbWallPlaner().regenerate_from_layer_set(material_set_usage.ForLayerSet) - elif material_set_usage.is_a("IfcMaterialProfileSetUsage"): - if attributes.get("CardinalPoint", None): - attributes["CardinalPoint"] = int(attributes["CardinalPoint"]) - ifcopenshell.api.material.edit_profile_usage( - self.file, - usage=material_set_usage, - attributes=attributes, - ) + layer_sets_to_regenerate = set() + + for obj in objects: + obj_element = tool.Ifc.get_entity(obj) + obj_material_usage = ifcopenshell.util.element.get_material(obj_element) + + if obj_material_usage and obj_material_usage.is_a("IfcMaterialLayerSetUsage"): + obj_material_usage.OffsetFromReferenceLine = material.OffsetFromReferenceLine + obj_material_usage.DirectionSense = material.DirectionSense + obj_material_usage.ReferenceExtent = material.ReferenceExtent + + layer_sets_to_regenerate.add(obj_material_usage.ForLayerSet) + + + for layer_set in layer_sets_to_regenerate: + wall.DumbWallPlaner().regenerate_from_layer_set(layer_set) + slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set) + bpy.ops.bim.disable_editing_assigned_material(obj=active_obj.name) From e240ae2669f6e87e2df1c4d2a51cb44472427664 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sun, 2 Mar 2025 11:24:07 +0000 Subject: [PATCH 192/476] Fix bpy.ops.bim.select_by_material() for layersets This operator assumes all material definitions have a Name attribute, mostly they do except that Material Layer Sets have a LayerSetName attribute for reasons. --- src/bonsai/bonsai/bim/module/material/operator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 79168e7d1f..ec415478d6 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -67,7 +67,10 @@ class SelectByMaterial(bpy.types.Operator): core.select_by_material(tool.Material, tool.Spatial, material=material) # copy selection query to clipboard - material_name = material.Name + if material.is_a("IfcMaterialLayerSet"): + material_name = material.LayerSetName + else: + material_name = material.Name result = f'material="{material_name}"' bpy.context.window_manager.clipboard = result self.report({"INFO"}, f"({result}) was copied to the clipboard.") From c1efdbb8ea82ed1b905e462704480431e20f3431 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sun, 2 Mar 2025 11:37:52 +0000 Subject: [PATCH 193/476] black . --- src/bonsai/bonsai/bim/module/material/operator.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index ec415478d6..9d768351d1 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -585,7 +585,6 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator): attributes=attributes, ) - if self.material_set_usage: material_set_usage = self.file.by_id(self.material_set_usage) attributes = bonsai.bim.helper.export_attributes(props.material_set_usage_attributes) @@ -598,23 +597,22 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator): ) layer_sets_to_regenerate = set() - + for obj in objects: obj_element = tool.Ifc.get_entity(obj) obj_material_usage = ifcopenshell.util.element.get_material(obj_element) if obj_material_usage and obj_material_usage.is_a("IfcMaterialLayerSetUsage"): obj_material_usage.OffsetFromReferenceLine = material.OffsetFromReferenceLine - obj_material_usage.DirectionSense = material.DirectionSense + obj_material_usage.DirectionSense = material.DirectionSense obj_material_usage.ReferenceExtent = material.ReferenceExtent layer_sets_to_regenerate.add(obj_material_usage.ForLayerSet) - for layer_set in layer_sets_to_regenerate: wall.DumbWallPlaner().regenerate_from_layer_set(layer_set) slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set) - + bpy.ops.bim.disable_editing_assigned_material(obj=active_obj.name) From 99c49736fa51d23e2a6161a09c98041e20d6291c Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sun, 2 Mar 2025 10:31:46 -0600 Subject: [PATCH 194/476] small tweak --- src/bonsai/bonsai/bim/module/type/prop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/type/prop.py b/src/bonsai/bonsai/bim/module/type/prop.py index aedb5c1993..974e4b5d53 100644 --- a/src/bonsai/bonsai/bim/module/type/prop.py +++ b/src/bonsai/bonsai/bim/module/type/prop.py @@ -85,7 +85,7 @@ class BIMTypeProperties(PropertyGroup): relating_type: EnumProperty(items=get_relating_type, name="Relating Type") relating_type_object: PointerProperty( type=bpy.types.Object, - name="Copy Class", + name="Copy Type", update=update_relating_type_from_object, poll=is_object_class_applicable, ) From a01278aa8ded972a102654071b5d2f1c5bc9be19 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 3 Mar 2025 13:36:12 +0500 Subject: [PATCH 195/476] ifcopenshell.util.shape to return Python floats if return is just a scalar --- .../ifcopenshell/util/shape.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py index 77c28ee1ab..c4a3cfe406 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py @@ -38,6 +38,12 @@ MatrixType = npt.NDArray[np.float64] # NOTE: See IfcGeomRepresentation.h for ShapeType buffer types. +# NOTE: For functions that return a single scalar ensure to use .item() to +# return the Python float instead of numpy float +# as it's less intrusive (doesn't promote numpy arrays on interactions), +# doesn't fail saving to IFC +# and precise enough anyway (internally Python floats are doubles). + def is_x(value: float, x: float, tolerance: Optional[float] = None) -> bool: """Checks whether a value is equivalent to X given a tolerance @@ -89,7 +95,7 @@ def get_x(geometry: ShapeType) -> float: :return: The X dimension """ verts_flat = get_vertices(geometry).ravel() - return np.max(verts_flat[0::3]) - np.min(verts_flat[0::3]) + return (np.max(verts_flat[0::3]) - np.min(verts_flat[0::3])).item() def get_y(geometry: ShapeType) -> float: @@ -99,7 +105,7 @@ def get_y(geometry: ShapeType) -> float: :return: The Y dimension """ verts_flat = get_vertices(geometry).ravel() - return np.max(verts_flat[1::3]) - np.min(verts_flat[1::3]) + return (np.max(verts_flat[1::3]) - np.min(verts_flat[1::3])).item() def get_z(geometry: ShapeType) -> float: @@ -109,7 +115,7 @@ def get_z(geometry: ShapeType) -> float: :return: The Z dimension """ verts_flat = get_vertices(geometry).ravel() - return np.max(verts_flat[2::3]) - np.min(verts_flat[2::3]) + return (np.max(verts_flat[2::3]) - np.min(verts_flat[2::3])).item() def get_max_xy(geometry: ShapeType) -> float: @@ -351,8 +357,8 @@ def get_bottom_elevation(geometry: ShapeType) -> float: :param geometry: Geometry output calculated by IfcOpenShell :return: The Z value """ - z_values = [geometry.verts[i + 2] for i in range(0, len(geometry.verts), 3)] - return min(z_values) + verts_flat = get_vertices(geometry).ravel() + return np.min(verts_flat[2::3]).item() def get_top_elevation(geometry: ShapeType) -> float: @@ -362,7 +368,7 @@ def get_top_elevation(geometry: ShapeType) -> float: :return: The Z value """ verts_flat = get_vertices(geometry).ravel() - return np.max(verts_flat[2::3]) + return np.max(verts_flat[2::3]).item() def get_shape_bottom_elevation(shape: ShapeType, geometry: ShapeType) -> float: @@ -447,7 +453,7 @@ def get_area_vf(vertices: npt.NDArray[np.float64], faces: npt.NDArray[np.int32]) # Sum up the areas to get the total area of the mesh mesh_area = np.sum(triangle_areas) - return mesh_area + return mesh_area.item() def get_area(geometry: ShapeType) -> float: @@ -674,7 +680,7 @@ def get_footprint_perimeter(geometry: ShapeType) -> float: else: all_edges.add(edge) - return sum([np.linalg.norm(vertices[e[0]] - vertices[e[1]]) for e in (all_edges - shared_edges)]) + return np.sum([np.linalg.norm(vertices[e[0]] - vertices[e[1]]) for e in (all_edges - shared_edges)]).item() def get_profiles(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: @@ -723,4 +729,4 @@ def get_total_edge_length(geometry: ShapeType) -> float: """ vertices = get_vertices(geometry) vertices = vertices[get_edges(geometry)] - return np.linalg.norm(vertices[:, 1] - vertices[:, 0], axis=1).sum() + return np.linalg.norm(vertices[:, 1] - vertices[:, 0], axis=1).sum().item() From 7b74b6c68f28a48bbfa659c04a66908e292656e2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 28 Feb 2025 18:23:24 +0500 Subject: [PATCH 196/476] Simplify edit_profile_usage with util.shape --- .../api/material/edit_profile_usage.py | 45 ++++++------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index e330cd9ffb..d02ad167fb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.geom import ifcopenshell.util.representation +import ifcopenshell.util.shape from ifcopenshell.geom import ShapeType from typing import Any @@ -168,62 +169,42 @@ class Usecase: return self.get_top_right(shape) def get_bottom_left(self, shape: ShapeType) -> ifcopenshell.entity_instance: - v = shape.verts - x = [v[i] for i in range(0, len(v), 3)] - y = [v[i + 1] for i in range(0, len(v), 3)] - width = max(x) - min(x) - height = max(y) - min(y) + width = ifcopenshell.util.shape.get_x(shape) + height = ifcopenshell.util.shape.get_y(shape) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, height / 2, 0.0))) def get_bottom_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance: - v = shape.verts - y = [v[i + 1] for i in range(0, len(v), 3)] - height = max(y) - min(y) + height = ifcopenshell.util.shape.get_y(shape) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, height / 2, 0.0))) def get_bottom_right(self, shape: ShapeType) -> ifcopenshell.entity_instance: - v = shape.verts - x = [v[i] for i in range(0, len(v), 3)] - y = [v[i + 1] for i in range(0, len(v), 3)] - width = max(x) - min(x) - height = max(y) - min(y) + width = ifcopenshell.util.shape.get_x(shape) + height = ifcopenshell.util.shape.get_y(shape) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, height / 2, 0.0))) def get_mid_depth_left(self, shape: ShapeType) -> ifcopenshell.entity_instance: - v = shape.verts - x = [v[i] for i in range(0, len(v), 3)] - width = max(x) - min(x) + width = ifcopenshell.util.shape.get_x(shape) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, 0.0, 0.0))) def get_mid_depth_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance: return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, 0.0, 0.0))) def get_mid_depth_right(self, shape: ShapeType) -> ifcopenshell.entity_instance: - v = shape.verts - x = [v[i] for i in range(0, len(v), 3)] - width = max(x) - min(x) + width = ifcopenshell.util.shape.get_x(shape) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, 0.0, 0.0))) def get_top_left(self, shape: ShapeType) -> ifcopenshell.entity_instance: - v = shape.verts - x = [v[i] for i in range(0, len(v), 3)] - y = [v[i + 1] for i in range(0, len(v), 3)] - width = max(x) - min(x) - height = max(y) - min(y) + width = ifcopenshell.util.shape.get_x(shape) + height = ifcopenshell.util.shape.get_y(shape) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((-width / 2, -height / 2, 0.0))) def get_top_centre(self, shape: ShapeType) -> ifcopenshell.entity_instance: - v = shape.verts - y = [v[i + 1] for i in range(0, len(v), 3)] - height = max(y) - min(y) + height = ifcopenshell.util.shape.get_y(shape) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((0.0, -height / 2, 0.0))) def get_top_right(self, shape: ShapeType) -> ifcopenshell.entity_instance: - v = shape.verts - x = [v[i] for i in range(0, len(v), 3)] - y = [v[i + 1] for i in range(0, len(v), 3)] - width = max(x) - min(x) - height = max(y) - min(y) + width = ifcopenshell.util.shape.get_x(shape) + height = ifcopenshell.util.shape.get_y(shape) return self.file.createIfcAxis2Placement3D(self.file.createIfcCartesianPoint((width / 2, -height / 2, 0.0))) def update_representation(self, element: ifcopenshell.entity_instance) -> None: From 855062e36b9d58cb31f13a909d9a955ddfbed686 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 3 Mar 2025 11:41:16 +0500 Subject: [PATCH 197/476] Quick to select profile from BIM Tool in Profile UI Example - https://imgur.com/a/tJdQUGq --- src/bonsai/bonsai/bim/module/model/workspace.py | 3 +++ src/bonsai/bonsai/bim/module/profile/operator.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 2556e1cd59..87584acb04 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -406,6 +406,9 @@ class EditItemUI: if mesh_props.item_profile == "-": op = row.operator("bim.name_profile", text="", icon="TAG") op.extrusion_item_obj = obj.name + else: + op = row.operator("bim.profiles_ui_select", icon="ZOOM_SELECTED", text="") + op.profile_id = int(mesh_props.item_profile) for item_attribute in mesh_props.item_attributes: row = cls.layout.row() diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index eab0db8ef2..06c99711d4 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -342,7 +342,7 @@ class SelectProfileInProfilesUI(bpy.types.Operator): props.active_profile_index = profile_index self.report( {"INFO"}, - f"Profile '{profile.Name or 'Unnamed'}' is selected in Profiles UI.", + f"Profile '{profile.ProfileName or 'Unnamed'}' is selected in Profiles UI.", ) return {"FINISHED"} From fed063a0c6ab8d14fd99c69e738b3910dc287e62 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 3 Mar 2025 13:19:40 +0500 Subject: [PATCH 198/476] edit_profile_usage - add test --- .../api/material/test_edit_profile_usage.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/ifcopenshell-python/test/api/material/test_edit_profile_usage.py diff --git a/src/ifcopenshell-python/test/api/material/test_edit_profile_usage.py b/src/ifcopenshell-python/test/api/material/test_edit_profile_usage.py new file mode 100644 index 0000000000..3a029482c1 --- /dev/null +++ b/src/ifcopenshell-python/test/api/material/test_edit_profile_usage.py @@ -0,0 +1,67 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# 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 . + +import test.bootstrap +import ifcopenshell.api.context +import ifcopenshell.api.geometry +import ifcopenshell.api.material +import ifcopenshell.api.root +import ifcopenshell.util.placement +from ifcopenshell.util.shape_builder import ShapeBuilder + + +# IfcMaterialProfileSetUsage added in IFC4. +class TestEditProfileUsageIFC4(test.bootstrap.IFC4): + def test_update_cardinal_point(self): + model = self.file + builder = ShapeBuilder(model) + + ifcopenshell.api.root.create_entity(model, ifc_class="IfcProject") + model_context = ifcopenshell.api.context.add_context(model, context_type="Model") + body = ifcopenshell.api.context.add_context( + model, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model_context + ) + + material_set = ifcopenshell.api.material.add_material_set(model, name="B1", set_type="IfcMaterialProfileSet") + steel = ifcopenshell.api.material.add_material(model, name="ST01", category="steel") + rectangle = builder.rectangle((100, 100)) + profile = builder.profile(rectangle) + ifcopenshell.api.material.add_profile(model, profile_set=material_set, material=steel, profile=profile) + beam = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBeam", name="B1.01") + rel = ifcopenshell.api.material.assign_material( + model, material=material_set, products=[beam], type="IfcMaterialProfileSetUsage" + ) + assert isinstance(rel, ifcopenshell.entity_instance) + usage = rel.RelatingMaterial + assert usage.CardinalPoint is None + + representation = ifcopenshell.api.geometry.add_profile_representation( + model, + context=body, + profile=profile, + depth=1000, + cardinal_point=5, + ) + assert representation.Items[0].Position.Location.Coordinates == (0.0, 0.0, 0.0) + ifcopenshell.api.geometry.assign_representation(model, product=beam, representation=representation) + ifcopenshell.api.material.edit_profile_usage(model, usage=rel.RelatingMaterial, attributes={"CardinalPoint": 1}) + assert representation.Items[0].Position.Location.Coordinates == (-50.0, 50.0, 0.0) + + +class TestEditProfileUsageIFC4X3(test.bootstrap.IFC4X3, TestEditProfileUsageIFC4): + pass From 7f27785927538fa228fd11d37b6a36f00f3527ad Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 3 Mar 2025 13:23:49 +0500 Subject: [PATCH 199/476] docs fixes 1) material.add_profile_set doesn't exist 2) usecase.file is not defined, use `model` instead` 3) Specify material for material.assign_material --- .../ifcopenshell/api/material/add_profile.py | 2 +- .../ifcopenshell/api/material/assign_profile.py | 6 +++--- .../ifcopenshell/api/material/edit_profile.py | 2 +- .../ifcopenshell/api/material/edit_profile_usage.py | 6 +++--- .../ifcopenshell/api/material/remove_profile.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_profile.py index b62d5e2ef3..160f79e453 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_profile.py @@ -67,7 +67,7 @@ def add_profile( # First, let's create a material set. This will later be assigned # to our beam type element. - material_set = ifcopenshell.api.material.add_profile_set(model, + material_set = ifcopenshell.api.material.add_material_set(model, name="B1", set_type="IfcMaterialProfileSet") # Create a steel material. diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py index 1e7c209e6c..bca30a36c2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/assign_profile.py @@ -48,7 +48,7 @@ def assign_profile( # First, let's create a material set. This will later be assigned # to our beam type element. - material_set = ifcopenshell.api.material.add_profile_set(model, + material_set = ifcopenshell.api.material.add_material_set(model, name="B1", set_type="IfcMaterialProfileSet") # Create a steel material. @@ -56,7 +56,7 @@ def assign_profile( # Create an I-beam profile curve. Notice how we name our profiles # based on standardised steel profile names. - hea100 = usecase.file.create_entity( + hea100 = model.create_entity( "IfcIShapeProfileDef", ProfileName="HEA100", ProfileType="AREA", OverallWidth=100, OverallDepth=96, WebThickness=5, FlangeThickness=8, FilletRadius=12, ) @@ -84,7 +84,7 @@ def assign_profile( # Now let's change the profile to a HEA200 standard profile instead. # This will automatically change the body representation that we # just added as well to a HEA200 profile. - hea200 = usecase.file.create_entity( + hea200 = model.create_entity( "IfcIShapeProfileDef", ProfileName="HEA200", ProfileType="AREA", OverallWidth=200, OverallDepth=190, WebThickness=6.5, FlangeThickness=10, FilletRadius=18, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py index c35755a960..3bc7479388 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile.py @@ -46,7 +46,7 @@ def edit_profile( .. code:: python # Let's create a material set to store our profiles. - material_set = ifcopenshell.api.material.add_profile_set(model, + material_set = ifcopenshell.api.material.add_material_set(model, name="B1", set_type="IfcMaterialProfileSet") # Create a couple steel materials. diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index d02ad167fb..0fc5cccdf9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -50,7 +50,7 @@ def edit_profile_usage( # First, let's create a material set. This will later be assigned # to our beam type element. - material_set = ifcopenshell.api.material.add_profile_set(model, + material_set = ifcopenshell.api.material.add_material_set(model, name="B1", set_type="IfcMaterialProfileSet") # Create a steel material. @@ -58,7 +58,7 @@ def edit_profile_usage( # Create an I-beam profile curve. Notice how we name our profiles # based on standardised steel profile names. - hea100 = usecase.file.create_entity( + hea100 = model.create_entity( "IfcIShapeProfileDef", ProfileName="HEA100", ProfileType="AREA", OverallWidth=100, OverallDepth=96, WebThickness=5, FlangeThickness=8, FilletRadius=12, ) @@ -74,7 +74,7 @@ def edit_profile_usage( # Let's create an occurrence of this beam. beam = ifcopenshell.api.root.create_entity(model, ifc_class="IfcBeam", name="B1.01") - rel = ifcopenshell.api.material.assign_material(model, + rel = ifcopenshell.api.material.assign_material(model, material=material_set, products=[beam], type="IfcMaterialProfileSetUsage") # Let's give a 1000mm long beam body representation. diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_profile.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_profile.py index 2883f21e41..1314bf209e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_profile.py @@ -40,7 +40,7 @@ def remove_profile( .. code:: python # First, let's create a material set. - material_set = ifcopenshell.api.material.add_profile_set(model, + material_set = ifcopenshell.api.material.add_material_set(model, name="B1", set_type="IfcMaterialProfileSet") # Create a steel material. From 10d5378ee322be617247d6b71b65561dea4a8b16 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 3 Mar 2025 13:38:43 +0500 Subject: [PATCH 200/476] TessellateElements - small optimization tolist() supports conversion of multidimensional arrays too --- src/ifcpatch/ifcpatch/recipes/TessellateElements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcpatch/ifcpatch/recipes/TessellateElements.py b/src/ifcpatch/ifcpatch/recipes/TessellateElements.py index 5becec4264..8a264967d7 100644 --- a/src/ifcpatch/ifcpatch/recipes/TessellateElements.py +++ b/src/ifcpatch/ifcpatch/recipes/TessellateElements.py @@ -89,7 +89,7 @@ class Patcher: shape: Union[ifcopenshell.geom.ShapeType, ifcopenshell.geom.ShapeElementType], ) -> None: geometry = getattr(shape, "geometry", shape) - v = [[x.tolist() for x in ifcopenshell.util.shape.get_vertices(geometry)]] + v = [ifcopenshell.util.shape.get_vertices(geometry).tolist()] f = [ifcopenshell.util.shape.get_faces(geometry).tolist()] replacements[element] = (v, f) From 5f86c6d1c2e4fbcd50939ca17062414d172e3578 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 3 Mar 2025 13:45:48 +0500 Subject: [PATCH 201/476] shape_builder.create_axis2_placement_3d_from_matrix - small optimization create_axis2_placement_3d now is fine with numpy arrays --- src/ifcopenshell-python/ifcopenshell/util/shape_builder.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index fb33cdbede..952ebae5ab 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -743,9 +743,7 @@ class ShapeBuilder: """ if matrix is None: matrix = np.eye(4, dtype=float) - return self.create_axis2_placement_3d( - position=matrix[:, 3][:3].tolist(), z_axis=matrix[:, 2][:3].tolist(), x_axis=matrix[:, 0][:3].tolist() - ) + return self.create_axis2_placement_3d(position=matrix[:3, 3], z_axis=matrix[:3, 2], x_axis=matrix[:3, 0]) def create_axis2_placement_2d( self, position: VectorType = (0.0, 0.0), x_direction: Optional[VectorType] = None From 33d3984990b661ae2e2359b6f45355c93836a94d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 4 Mar 2025 11:34:26 +0500 Subject: [PATCH 202/476] add_profile_representation - reuse util.shape methods --- .../api/geometry/add_profile_representation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py index 77bb515996..ddaab44e20 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py @@ -17,6 +17,8 @@ # along with IfcOpenShell. If not, see . import ifcopenshell.geom +import ifcopenshell.util.element +import ifcopenshell.util.shape import ifcopenshell.util.unit from ifcopenshell.util.data import Clipping from typing import Any, Union, Optional, Literal @@ -145,8 +147,7 @@ class Usecase: settings = ifcopenshell.geom.settings() settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) shape = ifcopenshell.geom.create_shape(settings, self.settings["profile"]) - x = [shape.verts[i] for i in range(0, len(shape.verts), 3)] - return self.convert_si_to_unit(max(x) - min(x)) + return self.convert_si_to_unit(ifcopenshell.util.shape.get_x(shape)) return 0.0 def get_y(self): @@ -174,6 +175,5 @@ class Usecase: settings = ifcopenshell.geom.settings() settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) shape = ifcopenshell.geom.create_shape(settings, self.settings["profile"]) - y = [shape.verts[i + 1] for i in range(0, len(shape.verts), 3)] - return self.convert_si_to_unit(max(y) - min(y)) + return self.convert_si_to_unit(ifcopenshell.util.shape.get_y(shape)) return 0.0 From 53a3afaded8217e7360775b089c5f460a827ed6b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 4 Mar 2025 11:58:59 +0500 Subject: [PATCH 203/476] typing --- src/bonsai/bonsai/bim/handler.py | 4 +- src/bonsai/bonsai/bim/ifc.py | 6 +- src/bonsai/bonsai/bim/import_ifc.py | 7 +- .../bonsai/bim/module/drawing/operator.py | 3 +- src/bonsai/bonsai/bim/module/model/profile.py | 2 +- src/bonsai/bonsai/bim/module/style/data.py | 5 +- .../bonsai/bim/module/style/operator.py | 34 ++- src/bonsai/bonsai/bim/module/style/prop.py | 28 ++- src/bonsai/bonsai/bim/module/style/ui.py | 29 ++- src/bonsai/bonsai/tool/blender.py | 6 +- src/bonsai/bonsai/tool/geometry.py | 2 +- src/bonsai/bonsai/tool/ifc.py | 2 +- src/bonsai/bonsai/tool/model.py | 29 ++- src/bonsai/bonsai/tool/root.py | 4 +- src/bonsai/bonsai/tool/style.py | 22 +- src/bonsai/test/bim/test_feature.py | 4 +- src/bonsai/test/tool/test_drawing.py | 7 +- src/bonsai/test/tool/test_geometry.py | 5 +- src/bonsai/test/tool/test_model.py | 2 +- src/bonsai/test/tool/test_polyline.py | 3 + src/bonsai/test/tool/test_style.py | 9 +- .../geometry/add_profile_representation.py | 228 +++++++++++------- .../api/material/edit_profile_usage.py | 2 + .../ifcopenshell/util/data.py | 5 +- .../api/geometry/test_add_shape_aspect.py | 1 + 25 files changed, 282 insertions(+), 167 deletions(-) diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index 9cea2cca2e..e0679a18c2 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -55,10 +55,10 @@ def name_callback(obj: Union[bpy.types.Object, bpy.types.Material], data: str) - return if isinstance(obj, bpy.types.Material): - props = obj.BIMStyleProperties + props = tool.Style.get_material_style_props(obj) if ifc_definition_id := props.ifc_definition_id: if props.is_renaming: - props.is_renmaing = False + props.is_renaming = False return tool.Ifc.get().by_id(ifc_definition_id).Name = obj.name refresh_ui_data() diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index ce84c66f02..281d39df8e 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -265,7 +265,8 @@ class IfcStore: IfcStore.guid_map[global_id] = obj if element.is_a("IfcSurfaceStyle"): - obj.BIMStyleProperties.ifc_definition_id = element.id() + props = tool.Style.get_material_style_props(obj) + props.ifc_definition_id = element.id() else: props = tool.Blender.get_object_bim_props(obj) props.ifc_definition_id = element.id() @@ -406,7 +407,8 @@ class IfcStore: @staticmethod def purge_blender_ifc_data(obj: IFC_CONNECTED_TYPE) -> None: if isinstance(obj, bpy.types.Material): - obj.BIMStyleProperties.ifc_definition_id = 0 + props = tool.Style.get_material_style_props(obj) + props.ifc_definition_id = 0 else: # bpy.types.Object props = tool.Blender.get_object_bim_props(obj) props.ifc_definition_id = 0 diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 0ed6f3d9e7..ade2a99143 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -86,7 +86,7 @@ class MaterialCreator: def load_existing_materials(self) -> None: for material in bpy.data.materials: - if ifc_definition_id := material.BIMStyleProperties.ifc_definition_id: + if ifc_definition_id := tool.Blender.get_ifc_definition_id(material): self.styles[ifc_definition_id] = material def parse_element_type_material_styles(self, element: ifcopenshell.entity_instance) -> None: @@ -964,10 +964,11 @@ class IfcImporter: self.material_creator.styles[style.id()] = blender_material style_elements = tool.Style.get_style_elements(blender_material) + props = tool.Style.get_material_style_props(blender_material) if tool.Style.has_blender_external_style(style_elements): - blender_material.BIMStyleProperties.active_style_type = "External" + props.active_style_type = "External" else: - blender_material.BIMStyleProperties.active_style_type = "Shading" + props.active_style_type = "Shading" def place_objects_in_collections(self) -> None: for ifc_definition_id, obj in self.added_data.items(): diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 2b75406d5c..42e17cf063 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3353,7 +3353,8 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator): obj.material_slots[0].material = material bpy.ops.bim.add_style() - style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id) + style = tool.Ifc.get_entity(material) + assert style tool.Style.assign_style_to_object(style, obj) # TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 22f7fa5071..9b12dbad55 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -1019,7 +1019,7 @@ class EnableEditingExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator): position = Matrix() direction = Vector(extrusion.ExtrudedDirection.DirectionRatios).normalized() - tool.Model.import_axis([Vector((0, 0, 0)), direction * extrusion.Depth], obj=obj, position=position) + tool.Model.import_axis((Vector((0, 0, 0)), direction * extrusion.Depth), obj=obj, position=position) bpy.ops.object.mode_set(mode="EDIT") ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_extrusion_axis(context)) diff --git a/src/bonsai/bonsai/bim/module/style/data.py b/src/bonsai/bonsai/bim/module/style/data.py index afcc7eba8d..09cb5447b1 100644 --- a/src/bonsai/bonsai/bim/module/style/data.py +++ b/src/bonsai/bonsai/bim/module/style/data.py @@ -97,9 +97,8 @@ class BlenderMaterialStyleData: material = obj.active_material if not material: return False - props = material.BIMStyleProperties - style_id = props.ifc_definition_id - style = tool.Ifc.get_entity_by_id(style_id) + + style = tool.Ifc.get_entity(material) if not style: return False diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index 6ec0ccdc3c..0ead9b6751 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -118,8 +118,7 @@ class UnlinkStyle(bpy.types.Operator, tool.Ifc.Operator): # Don't check blender_material and style_id as this operator is only called from UI. assert isinstance(self.blender_material, str) # Type checker. material = bpy.data.materials[self.blender_material] - style_id = material.BIMStyleProperties.ifc_definition_id - style = tool.Ifc.get_entity_by_id(style_id) + style = tool.Ifc.get_entity(material) # Material is linked to a style from a different project. if not style or tool.Ifc.get_object(style) != material: @@ -221,18 +220,25 @@ class UpdateCurrentStyle(bpy.types.Operator): def execute(self, context): style = tool.Ifc.get().by_id(self.style_id) material = tool.Ifc.get_object(style) - current_style_type = material.BIMStyleProperties.active_style_type + msprops = tool.Style.get_material_style_props(material) + current_style_type = msprops.active_style_type if self.update_all: sprops = tool.Style.get_style_props() sprops.active_style_type = current_style_type return {"FINISHED"} - updated_materials = set() + updated_materials: set[bpy.types.Material] = set() for obj in context.selected_objects: + if not isinstance(obj.data, (bpy.types.Mesh, bpy.types.Curve)): + continue for mat in obj.data.materials: - if mat and mat not in updated_materials and mat.BIMStyleProperties.ifc_definition_id != 0: - mat.BIMStyleProperties.active_style_type = current_style_type + if ( + mat + and mat not in updated_materials + and (msprops_ := tool.Style.get_material_style_props(mat)).ifc_definition_id != 0 + ): + msprops_.active_style_type = current_style_type updated_materials.add(mat) return {"FINISHED"} @@ -755,7 +761,8 @@ class EnableEditingSurfaceStyle(bpy.types.Operator): bonsai.bim.helper.import_attributes2(surface_style or self.ifc_class, attributes, callback) material = tool.Ifc.get_object(style) - active_style_type = material.BIMStyleProperties.active_style_type + msprops = tool.Style.get_material_style_props(material) + active_style_type = msprops.active_style_type if self.ifc_class == "IfcExternallyDefinedSurfaceStyle" and active_style_type != "External": if tool.Style.has_blender_external_style(style_elements): tool.Style.switch_shading(material, "External") @@ -803,9 +810,10 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): # restore selected style type material = tool.Ifc.get_object(self.style) - material.BIMStyleProperties.active_style_type = material.BIMStyleProperties.active_style_type + msprops = tool.Style.get_material_style_props(material) + msprops.active_style_type = msprops.active_style_type - def edit_existing_style(self): + def edit_existing_style(self) -> None: ifc_file = tool.Ifc.get() material = tool.Ifc.get_object(self.style) assert self.surface_style @@ -867,7 +875,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): attributes=bonsai.bim.helper.export_attributes(attributes), ) - def add_new_style(self): + def add_new_style(self) -> None: material = tool.Ifc.get_object(self.style) if self.props.is_editing_class == "IfcSurfaceStyleShading": surface_style = ifcopenshell.api.run( @@ -911,13 +919,13 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): attributes=bonsai.bim.helper.export_attributes(attributes), ) - def get_shading_attributes(self): + def get_shading_attributes(self) -> dict[str, Any]: return { "SurfaceColour": self.color_to_dict(self.props.surface_colour), "Transparency": self.props.transparency or None, } - def get_rendering_attributes(self): + def get_rendering_attributes(self) -> dict[str, Any]: if self.props.is_diffuse_colour_null: diffuse_colour = None elif self.props.diffuse_colour_class == "IfcColourRgb": @@ -961,7 +969,7 @@ class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator): textures.append(texture_data) return textures - def color_to_dict(self, x): + def color_to_dict(self, x: tuple[float, float, float]) -> dict[str, Any]: return {"Red": x[0], "Green": x[1], "Blue": x[2]} diff --git a/src/bonsai/bonsai/bim/module/style/prop.py b/src/bonsai/bonsai/bim/module/style/prop.py index 040db82f60..d533473ba2 100644 --- a/src/bonsai/bonsai/bim/module/style/prop.py +++ b/src/bonsai/bonsai/bim/module/style/prop.py @@ -39,13 +39,13 @@ from typing import Literal, Union, TYPE_CHECKING, get_args _ = gettext.gettext -def get_style_types(self, context): +def get_style_types(self: "BIMStylesProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not StylesData.is_loaded: StylesData.load() return StylesData.data["style_types"] -def get_reflectance_methods(self, context): +def get_reflectance_methods(self: "BIMStylesProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not StylesData.is_loaded: StylesData.load() return StylesData.data["reflectance_methods"] @@ -77,6 +77,16 @@ class Style(PropertyGroup): type=bpy.types.Material, ) + if TYPE_CHECKING: + ifc_definition_id: int + total_elements: int + style_classes: bpy.types.bpy_prop_collection_idprop[StrProperty] + has_surface_colour: bool + surface_colour: tuple[float, float, float] + has_diffuse_colour: bool + diffuse_colour: tuple[float, float, float] + blender_material: Union[bpy.types.Material, None] + STYLE_TYPES = [ ("Shading", "Shading", ""), @@ -86,7 +96,7 @@ STYLE_TYPES = [ def update_shading_styles(self: "BIMStylesProperties", context: bpy.types.Context) -> None: for mat in bpy.data.materials: - if mat.BIMStyleProperties.ifc_definition_id == 0: + if tool.Blender.get_ifc_definition_id(mat) == 0: continue tool.Style.change_current_style_type(mat, self.active_style_type) @@ -111,7 +121,9 @@ UV_MODES = [ ("Camera", "Camera", _("UV from position coordinate in camera space")), ] - +TextureMapMode = Literal[ + "DIFFUSE", "NORMAL", "METALLICROUGHNESS", "SPECULAR", "SHININESS", "EMISSIVE", "OCCLUSION", "AMBIENT" +] TEXTURE_MAPS_MODS = ( ("DIFFUSE", "DIFFUSE", ""), ("NORMAL", "NORMAL", ""), @@ -129,6 +141,10 @@ class Texture(PropertyGroup): # NOTE: subtype `FILE_PATH` is not used to avoid .blend relative paths path: StringProperty(name="Texture Path", update=update_shader_graph) + if TYPE_CHECKING: + mode: TextureMapMode + path: str + class ColourRgb(PropertyGroup): name: StringProperty() @@ -136,6 +152,10 @@ class ColourRgb(PropertyGroup): # not exposed in the UI, here just to preserve the data color_name: StringProperty(name="Color Name") + if TYPE_CHECKING: + color_value: tuple[float, float, float] + color_name: str + # to fit blender.bim.helper.export_attributes def get_value(self): return { diff --git a/src/bonsai/bonsai/bim/module/style/ui.py b/src/bonsai/bonsai/bim/module/style/ui.py index 4cdf8aa88d..a5042fa800 100644 --- a/src/bonsai/bonsai/bim/module/style/ui.py +++ b/src/bonsai/bonsai/bim/module/style/ui.py @@ -16,11 +16,16 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import bonsai.bim.helper import bonsai.tool as tool from bpy.types import Panel, UIList from bonsai.bim.module.style.data import StylesData, BlenderMaterialStyleData +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.style.prop import BIMStylesProperties, Style class BIM_PT_styles(Panel): @@ -94,7 +99,8 @@ class BIM_PT_styles(Panel): if active_style: row = self.layout.row(align=True) if material := style.blender_material: - row.prop(material.BIMStyleProperties, "active_style_type", icon="SHADING_RENDERED", text="") + msprops = tool.Style.get_material_style_props(material) + row.prop(msprops, "active_style_type", icon="SHADING_RENDERED", text="") op = row.operator("bim.update_current_style", icon="FILE_REFRESH", text="") op.style_id = style.ifc_definition_id @@ -255,11 +261,19 @@ class BIM_PT_styles(Panel): class BIM_UL_styles(UIList): - def draw_item(self, context, layout: bpy.types.UILayout, data, item, icon, active_data, active_property): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMStylesProperties, + item: Style, + icon, + active_data, + active_property, + ): if item: row = layout.row(align=True) - props = tool.Style.get_style_props() - if item.ifc_definition_id == props.is_editing_style: + if item.ifc_definition_id == data.is_editing_style: row.label(text="", icon="GREASEPENCIL") row.prop(item, "name", text="", emboss=False) if item.has_surface_colour: @@ -295,7 +309,7 @@ class BIM_PT_style(Panel): @classmethod def poll(cls, context): - return bool(tool.Ifc.get() and (material := context.material) and material.BIMStyleProperties.ifc_definition_id) + return bool(tool.Ifc.get() and (material := context.material) and tool.Blender.get_ifc_definition_id(material)) def draw(self, context): # NOTE: this UI is needed only to indicate whether blender material is linked to IFC @@ -305,10 +319,11 @@ class BIM_PT_style(Panel): BlenderMaterialStyleData.load() material = context.material - style_id = material.BIMStyleProperties.ifc_definition_id + assert material + style_id = tool.Blender.get_ifc_definition_id(material) row = self.layout.row(align=True) - if style_id and not BlenderMaterialStyleData.data["is_linked_to_style"]: + if not BlenderMaterialStyleData.data["is_linked_to_style"]: row.label(text="Material has linked IFC from a different project.") op = row.operator("bim.unlink_style", icon="UNLINKED", text="") op.blender_material = material.name diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 6ea77101a1..c8846ace19 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1601,5 +1601,7 @@ class Blender(bonsai.core.tool.Blender): return obj.BIMObjectProperties @classmethod - def get_ifc_definition_id(cls, obj: bpy.types.Object) -> int: - return tool.Blender.get_object_bim_props(obj).ifc_definition_id + def get_ifc_definition_id(cls, obj: IFC_CONNECTED_TYPE) -> int: + if isinstance(obj, bpy.types.Object): + return tool.Blender.get_object_bim_props(obj).ifc_definition_id + return tool.Style.get_material_style_props(obj).ifc_definition_id diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 56c8e275c6..9524e91ea3 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -639,7 +639,7 @@ class Geometry(bonsai.core.tool.Geometry): @classmethod def get_object_materials_without_styles(cls, obj: bpy.types.Object) -> list[bpy.types.Material]: return [ - s.material for s in obj.material_slots if s.material and not s.material.BIMStyleProperties.ifc_definition_id + s.material for s in obj.material_slots if s.material and not tool.Blender.get_ifc_definition_id(s.material) ] @classmethod diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index 5f68fc4b0f..83c05101ef 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -110,7 +110,7 @@ class Ifc(bonsai.core.tool.Ifc): if isinstance(obj, bpy.types.Object): props = tool.Blender.get_object_bim_props(obj) elif isinstance(obj, bpy.types.Material): - props = obj.BIMStyleProperties + props = tool.Style.get_material_style_props(obj) else: props = tool.Geometry.get_mesh_props(obj) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 2af631b671..0704fe7154 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -293,8 +293,19 @@ class Model(bonsai.core.tool.Model): else: break + unit_scale: float + vertices: list[Vector] + edges: list[Sequence[int]] + arcs: list[Sequence[int]] + circles: list[Sequence[int]] + @classmethod - def import_axis(cls, axis, obj=None, position=None): + def import_axis( + cls, + axis: Union[ifcopenshell.entity_instance, tuple[Vector, Vector]], + obj=None, + position: Optional[Matrix] = None, + ) -> bpy.types.Object: cls.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) if position is None: @@ -305,7 +316,7 @@ class Model(bonsai.core.tool.Model): cls.arcs = [] cls.circles = [] - if isinstance(axis, list): + if isinstance(axis, tuple): cls.vertices.extend( [ position @ Vector(cls.convert_unit_to_si(axis[0])).to_3d(), @@ -478,7 +489,7 @@ class Model(bonsai.core.tool.Model): @classmethod def convert_curve_to_mesh( cls, - obj: bpy.types.Object, + obj: Union[bpy.types.Object, None], # Unused argument. position: Matrix, curve: ifcopenshell.entity_instance, x_angle: Optional[float] = None, @@ -1624,7 +1635,7 @@ class Model(bonsai.core.tool.Model): loop_edges = list(bm.edges) # Create loops from edges - loops = [] + loops: list[list[bmesh.types.BMEdge]] = [] while loop_edges: edge = loop_edges.pop() loop = [edge] @@ -1645,19 +1656,19 @@ class Model(bonsai.core.tool.Model): tmp = ifcopenshell.file(schema=tool.Ifc.get().schema) - def is_in_group(v, group_name): + def is_in_group(v: bmesh.types.BMVert, group_name: str) -> bool: for group_index in groups[group_name]: if group_index in v[deform_layer]: return True return False - def get_group_index(v, group_name): + def get_group_index(v: bmesh.types.BMVert, group_name: str) -> Union[int, None]: for group_index in groups[group_name]: if group_index in v[deform_layer]: return group_index # Convert all loops into IFC curves - curves = [] + curves: list[ifcopenshell.entity_instance] = [] for loop in loops: if len(loop) == 1 and all([is_in_group(v, "IFCCIRCLE") for v in loop[0].verts]): @@ -1670,7 +1681,7 @@ class Model(bonsai.core.tool.Model): tmp.createIfcCircle(tmp.createIfcAxis2Placement2D(tmp.createIfcCartesianPoint(list(mid))), radius) ) else: - loop_verts = [] + loop_verts: list[bmesh.types.BMVert] = [] for i, edge in enumerate(loop): if i == 0 and len(loop) == 1: loop_verts.append(edge.verts[0]) @@ -1746,7 +1757,7 @@ class Model(bonsai.core.tool.Model): curves.append(tmp.createIfcIndexedPolyCurve(points)) # Sort IFC curves into either closed, or closed with void profile defs - profile_defs = [] + profile_defs: list[ifcopenshell.entity_instance] = [] settings = ifcopenshell.geom.settings() settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py index 4c824f8b62..b8269f4134 100644 --- a/src/bonsai/bonsai/tool/root.py +++ b/src/bonsai/bonsai/tool/root.py @@ -418,8 +418,8 @@ class Root(bonsai.core.tool.Root): """Rename material without triggerring name callback and unnecessary writing to IFC.""" if material.name == name: return - props = material.BIMStyleProperties - props.is_renaming = True + msprops = tool.Style.get_material_style_props(material) + msprops.is_renaming = True material.name = name # The handler will trigger, and reset is_renaming to False. @classmethod diff --git a/src/bonsai/bonsai/tool/style.py b/src/bonsai/bonsai/tool/style.py index b7a7cb7241..42a3dc207a 100644 --- a/src/bonsai/bonsai/tool/style.py +++ b/src/bonsai/bonsai/tool/style.py @@ -162,14 +162,14 @@ class Style(bonsai.core.tool.Style): cls, blender_material_or_style: Union[bpy.types.Material, ifcopenshell.entity_instance] ) -> dict[str, ifcopenshell.entity_instance]: if isinstance(blender_material_or_style, bpy.types.Material): - if not (ifc_definition_id := blender_material_or_style.BIMStyleProperties.ifc_definition_id): + style = tool.Ifc.get_entity(blender_material_or_style) + if not style: return {} - style = tool.Ifc.get().by_id(ifc_definition_id) else: style = blender_material_or_style style_elements = {} - for style in style.Styles: - style_elements[style.is_a()] = style + for style_ in style.Styles: + style_elements[style_.is_a()] = style_ return style_elements @classmethod @@ -305,7 +305,7 @@ class Style(bonsai.core.tool.Style): return next((l.from_node for l in input_pin.links if l.from_node.type == of_type), None) return next((l.from_node for l in input_pin.links), None) - props = obj.BIMStyleProperties + props = tool.Style.get_material_style_props(obj) transparency = 1 - obj.diffuse_color[3] diffuse_color = obj.diffuse_color viewport_color = color_to_ifc_format(obj.diffuse_color) @@ -487,16 +487,14 @@ class Style(bonsai.core.tool.Style): @classmethod def get_surface_shading_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]: - if ifc_definition_id := obj.BIMStyleProperties.ifc_definition_id: - style = tool.Ifc.get().by_id(ifc_definition_id) + if style := tool.Ifc.get_entity(obj): items = [s for s in style.Styles if s.is_a() == "IfcSurfaceStyleShading"] if items: return items[0] @classmethod def get_surface_texture_style(cls, obj: bpy.types.Material) -> Union[ifcopenshell.entity_instance, None]: - if ifc_definition_id := obj.BIMStyleProperties.ifc_definition_id: - style = tool.Ifc.get().by_id(ifc_definition_id) + if style := tool.Ifc.get_entity(obj): items = [s for s in style.Styles if s.is_a("IfcSurfaceStyleWithTextures")] if items: return items[0] @@ -581,7 +579,8 @@ class Style(bonsai.core.tool.Style): @classmethod def change_current_style_type(cls, blender_material: bpy.types.Material, style_type: str) -> None: - blender_material.BIMStyleProperties.active_style_type = style_type + props = cls.get_material_style_props(blender_material) + props.active_style_type = style_type @classmethod def get_styled_items(cls, style: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: @@ -632,7 +631,8 @@ class Style(bonsai.core.tool.Style): @classmethod def reload_material_from_ifc(cls, blender_material: bpy.types.Material) -> None: - blender_material.BIMStyleProperties.active_style_type = blender_material.BIMStyleProperties.active_style_type + props = cls.get_material_style_props(blender_material) + props.active_style_type = props.active_style_type @classmethod def switch_shading(cls, blender_material: bpy.types.Material, style_type: StyleType) -> None: diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 2163489685..7385543ff9 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -911,14 +911,14 @@ def the_material_name_is_not_an_ifc_material(name): @then(parsers.parse('the material "{name}" is an IFC style')) def the_material_name_is_an_ifc_style(name): obj = the_material_name_exists(name) - ifc_definition_id = obj.BIMStyleProperties.ifc_definition_id + ifc_definition_id = tool.Blender.get_ifc_definition_id(obj) assert ifc_definition_id != 0, f"The material {obj} has a style ID of {ifc_definition_id}" @then(parsers.parse('the material "{name}" is not an IFC style')) def the_material_name_is_not_an_ifc_style(name): obj = the_material_name_exists(name) - ifc_definition_id = obj.BIMStyleProperties.ifc_definition_id + ifc_definition_id = tool.Blender.get_ifc_definition_id(obj) assert ifc_definition_id == 0, f"The material {obj} has a style ID of {ifc_definition_id}" diff --git a/src/bonsai/test/tool/test_drawing.py b/src/bonsai/test/tool/test_drawing.py index 2a2cb6b718..6c4c338b3d 100644 --- a/src/bonsai/test/tool/test_drawing.py +++ b/src/bonsai/test/tool/test_drawing.py @@ -891,11 +891,12 @@ class TestAddReferenceImage(NewFile): assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((3.53982, 2.0, 0.0))) material = obj.active_material + assert material assert material.name == "image" - assert material.BIMStyleProperties.ifc_definition_id != 0 + assert tool.Blender.get_ifc_definition_id(material) != 0 - ifc_file = tool.Ifc.get() - style = ifc_file.by_id(material.BIMStyleProperties.ifc_definition_id) + style = tool.Ifc.get_entity(material) + assert style styled_items = set(tool.Style.get_styled_items(style)) representation_items = set(tool.Geometry.get_active_representation(obj).Items) assert styled_items == representation_items diff --git a/src/bonsai/test/tool/test_geometry.py b/src/bonsai/test/tool/test_geometry.py index c0d1819d9a..85a0a4c79d 100644 --- a/src/bonsai/test/tool/test_geometry.py +++ b/src/bonsai/test/tool/test_geometry.py @@ -112,7 +112,8 @@ class TestGetObjectMaterialsWithoutStyles(NewFile): material1 = bpy.data.materials.new("Material") material2 = bpy.data.materials.new("Material") material3 = bpy.data.materials.new("Material") - material3.BIMStyleProperties.ifc_definition_id = 1 + props = tool.Style.get_material_style_props(material3) + props.ifc_definition_id = 1 obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh")) obj.data.materials.append(material1) obj.data.materials.append(material2) @@ -307,7 +308,7 @@ class TestRecordObjectMaterials(NewFile): tool.Ifc.set(ifc) style = ifc.createIfcSurfaceStyle() material = bpy.data.materials.new("Material") - material.BIMStyleProperties.ifc_definition_id = style.id() + tool.Ifc.link(style, material) obj.data.materials.append(material) subject.record_object_materials(obj) assert tool.Geometry.get_mesh_props(obj.data).material_checksum == str([style.id()]) diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index afcf0e6b5a..8056db1349 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -462,7 +462,7 @@ class TestApplyIfcMaterialChanges(NewFile): def get_used_styles(self, obj: bpy.types.Object) -> set[ifcopenshell.entity_instance]: ifc_file = tool.Ifc.get() return { - ifc_file.by_id(s.material.BIMStyleProperties.ifc_definition_id) for s in obj.material_slots if s.material + ifc_file.by_id(tool.Blender.get_ifc_definition_id(s.material)) for s in obj.material_slots if s.material } def get_mesh(self, obj: bpy.types.Object) -> bpy.types.Mesh: diff --git a/src/bonsai/test/tool/test_polyline.py b/src/bonsai/test/tool/test_polyline.py index b7d4303b0d..9dd1e8f41c 100644 --- a/src/bonsai/test/tool/test_polyline.py +++ b/src/bonsai/test/tool/test_polyline.py @@ -18,6 +18,9 @@ import bpy import ifcopenshell +import ifcopenshell.api.project +import ifcopenshell.api.root +import ifcopenshell.api.unit import bonsai.core.tool import bonsai.tool as tool from test.bim.bootstrap import NewFile diff --git a/src/bonsai/test/tool/test_style.py b/src/bonsai/test/tool/test_style.py index 6af83895a3..5f37d397b5 100644 --- a/src/bonsai/test/tool/test_style.py +++ b/src/bonsai/test/tool/test_style.py @@ -308,7 +308,8 @@ class TestGetSurfaceRenderingStyle(NewFile): style_item = tool.Ifc.get().createIfcSurfaceStyleRendering() style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item]) obj = bpy.data.materials.new("Material") - obj.BIMStyleProperties.ifc_definition_id = style.id() + props = tool.Style.get_material_style_props(obj) + props.ifc_definition_id = style.id() assert subject.get_surface_rendering_style(obj) == style_item @@ -347,7 +348,7 @@ class TestGetSurfaceShadingStyle(NewFile): style_item = tool.Ifc.get().createIfcSurfaceStyleShading() style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item]) obj = bpy.data.materials.new("Material") - obj.BIMStyleProperties.ifc_definition_id = style.id() + tool.Ifc.link(style, obj) assert subject.get_surface_shading_style(obj) == style_item def test_do_not_get_rendering_styles(self): @@ -355,7 +356,7 @@ class TestGetSurfaceShadingStyle(NewFile): style_item = tool.Ifc.get().createIfcSurfaceStyleRendering() style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item]) obj = bpy.data.materials.new("Material") - obj.BIMStyleProperties.ifc_definition_id = style.id() + tool.Ifc.link(style, obj) assert subject.get_surface_shading_style(obj) is None @@ -365,7 +366,7 @@ class TestGetSurfaceTextureStyle(NewFile): style_item = tool.Ifc.get().createIfcSurfaceStyleWithTextures() style = tool.Ifc.get().createIfcSurfaceStyle(Styles=[style_item]) obj = bpy.data.materials.new("Material") - obj.BIMStyleProperties.ifc_definition_id = style.id() + tool.Ifc.link(style, obj) assert subject.get_surface_texture_style(obj) == style_item diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py index ddaab44e20..9458131046 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_profile_representation.py @@ -21,159 +21,203 @@ import ifcopenshell.util.element import ifcopenshell.util.shape import ifcopenshell.util.unit from ifcopenshell.util.data import Clipping -from typing import Any, Union, Optional, Literal +from typing import Any, Union, Optional, Literal, get_args VECTOR_3D = tuple[float, float, float] +CardinalPointNumeric = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] +CardinalPointString = Literal[ + "bottom left", + "bottom centre", + "bottom right", + "mid-depth left", + "mid-depth centre", + "mid-depth right", + "top left", + "top centre", + "top right", + "geometric centroid", + "bottom in line with the geometric centroid", + "left in line with the geometric centroid", + "right in line with the geometric centroid", + "top in line with the geometric centroid", + "shear centre", + "bottom in line with the shear centre", + "left in line with the shear centre", + "right in line with the shear centre", + "top in line with the shear centre", +] +CARDINAL_POINT_VALUES: tuple[CardinalPointString, ...] = get_args(CardinalPointString) +CardinalPoint = Union[CardinalPointNumeric, CardinalPointString] def add_profile_representation( file: ifcopenshell.file, - # IfcGeometricRepresentationContext context: ifcopenshell.entity_instance, profile: ifcopenshell.entity_instance, - # in meters depth: float = 1.0, - cardinal_point: Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] = 5, - # A list of planes that define clipping half space solids - # Planes are defined either by Clipping objects - # or by dictionaries of arguments for `Clipping.parse` + # TODO: None makes more sense as default value? + cardinal_point: Union[CardinalPoint, None] = 5, clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None, placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]] = (None, None), ) -> ifcopenshell.entity_instance: + """Add profile representation. + + :param context: The IfcGeometricRepresentationContext for the representation, + only Model/Body/MODEL_VIEW type of representations are currently supported. + :param profile: The IfcProfileDef to extrude. + :param depth: The depth of the extrusion in meters. + :param cardinal_point: The cardinal point of the profile. + :param clippings: A list of planes that define clipping half space solids. + Planes are defined either by Clipping objects + or by dictionaries of arguments for `Clipping.parse`. + :param placement_zx_axes: A tuple of two vectors that define the placement of the profile. + The first vector is the Z axis, the second vector is the X axis. + :return: IfcShapeRepresentation. + """ usecase = Usecase() usecase.file = file - usecase.settings = { - "context": context, - "profile": profile, - "depth": depth, - "cardinal_point": cardinal_point, - "clippings": clippings if clippings is not None else [], - "placement_zx_axes": placement_zx_axes, - } - return usecase.execute() + clippings = clippings if clippings is not None else [] + return usecase.execute(context, profile, depth, cardinal_point, clippings, placement_zx_axes) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] + clippings: list[Clipping] - def execute(self): - self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - self.settings["clippings"] = [Clipping.parse(c) for c in self.settings["clippings"]] - return self.file.createIfcShapeRepresentation( - self.settings["context"], - self.settings["context"].ContextIdentifier, - "Clipping" if self.settings["clippings"] else "SweptSolid", + def execute( + self, + context: ifcopenshell.entity_instance, + profile: ifcopenshell.entity_instance, + depth: float, + cardinal_point: Union[CardinalPoint, None], + clippings: list[Union[Clipping, dict[str, Any]]], + placement_zx_axes: tuple[Union[VECTOR_3D, None], Union[VECTOR_3D, None]], + ) -> ifcopenshell.entity_instance: + if isinstance(cardinal_point, int): + cardinal_point = CARDINAL_POINT_VALUES[cardinal_point - 1] + + self.cardinal_point = cardinal_point + self.profile = profile + self.clippings = [Clipping.parse(c) for c in clippings] + self.depth = depth + self.placement_zx_axes = placement_zx_axes + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) + return self.file.create_entity( + "IfcShapeRepresentation", + context, + context.ContextIdentifier, + "Clipping" if self.clippings else "SweptSolid", [self.create_item()], ) - def create_item(self): + def create_item(self) -> ifcopenshell.entity_instance: point = self.get_point() placement = self.file.createIfcAxis2Placement3D( point, - self.file.createIfcDirection(self.settings["placement_zx_axes"][0] or (0.0, 0.0, 1.0)), - self.file.createIfcDirection(self.settings["placement_zx_axes"][1] or (1.0, 0.0, 0.0)), + self.file.create_entity("IfcDirection", self.placement_zx_axes[0] or (0.0, 0.0, 1.0)), + self.file.create_entity("IfcDirection", self.placement_zx_axes[1] or (1.0, 0.0, 0.0)), ) - extrusion = self.file.createIfcExtrudedAreaSolid( - self.settings["profile"], + extrusion = self.file.create_entity( + "IfcExtrudedAreaSolid", + self.profile, placement, self.file.createIfcDirection((0.0, 0.0, 1.0)), - self.convert_si_to_unit(self.settings["depth"]), + self.convert_si_to_unit(self.depth), ) - if self.settings["clippings"]: + if self.clippings: return self.apply_clippings(extrusion) return extrusion - def apply_clippings(self, first_operand): - while self.settings["clippings"]: - clipping = self.settings["clippings"].pop() + def apply_clippings(self, first_operand: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + while self.clippings: + clipping = self.clippings.pop() if isinstance(clipping, ifcopenshell.entity_instance): new = ifcopenshell.util.element.copy(self.file, clipping) new.FirstOperand = first_operand first_operand = new else: # Clipping - first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"]) + first_operand = clipping.apply(self.file, first_operand, self.unit_scale) return first_operand - def convert_si_to_unit(self, co): - return co / self.settings["unit_scale"] + def convert_si_to_unit(self, co: float) -> float: + return co / self.unit_scale - def get_point(self): - if not self.settings["cardinal_point"]: + def get_point(self) -> ifcopenshell.entity_instance: + if not self.cardinal_point: return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)) - elif self.settings["cardinal_point"] == 1: + elif self.cardinal_point == "bottom left": return self.file.createIfcCartesianPoint((-self.get_x() / 2, self.get_y() / 2, 0.0)) - elif self.settings["cardinal_point"] == 2: + elif self.cardinal_point == "bottom centre": return self.file.createIfcCartesianPoint((0.0, self.get_y() / 2, 0.0)) - elif self.settings["cardinal_point"] == 3: + elif self.cardinal_point == "bottom right": return self.file.createIfcCartesianPoint((self.get_x() / 2, self.get_y() / 2, 0.0)) - elif self.settings["cardinal_point"] == 4: + elif self.cardinal_point == "mid-depth left": return self.file.createIfcCartesianPoint((-self.get_x() / 2, 0.0, 0.0)) - elif self.settings["cardinal_point"] == 5: + elif self.cardinal_point == "mid-depth centre": return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)) - elif self.settings["cardinal_point"] == 6: + elif self.cardinal_point == "mid-depth right": return self.file.createIfcCartesianPoint((self.get_x() / 2, 0.0, 0.0)) - elif self.settings["cardinal_point"] == 7: + elif self.cardinal_point == "top left": return self.file.createIfcCartesianPoint((-self.get_x() / 2, -self.get_y() / 2, 0.0)) - elif self.settings["cardinal_point"] == 8: + elif self.cardinal_point == "top centre": return self.file.createIfcCartesianPoint((0.0, -self.get_y() / 2, 0.0)) - elif self.settings["cardinal_point"] == 9: + elif self.cardinal_point == "top right": return self.file.createIfcCartesianPoint((self.get_x() / 2, -self.get_y() / 2, 0.0)) # TODO other cardinal points return self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)) - def get_x(self): - if self.settings["profile"].is_a("IfcAsymmetricIShapeProfileDef"): - return self.settings["profile"].OverallWidth - elif self.settings["profile"].is_a("IfcCShapeProfileDef"): - return self.settings["profile"].Width - elif self.settings["profile"].is_a("IfcCircleProfileDef"): - return self.settings["profile"].Radius * 2 - elif self.settings["profile"].is_a("IfcEllipseProfileDef"): - return self.settings["profile"].SemiAxis1 * 2 - elif self.settings["profile"].is_a("IfcIShapeProfileDef"): - return self.settings["profile"].OverallWidth - elif self.settings["profile"].is_a("IfcLShapeProfileDef"): - return self.settings["profile"].Width - elif self.settings["profile"].is_a("IfcRectangleProfileDef"): - return self.settings["profile"].XDim - elif self.settings["profile"].is_a("IfcTShapeProfileDef"): - return self.settings["profile"].FlangeWidth - elif self.settings["profile"].is_a("IfcUShapeProfileDef"): - return self.settings["profile"].FlangeWidth - elif self.settings["profile"].is_a("IfcZShapeProfileDef"): - return (self.settings["profile"].FlangeWidth * 2) - self.settings["profile"].WebThickness + def get_x(self) -> float: + if self.profile.is_a("IfcAsymmetricIShapeProfileDef"): + return self.profile.OverallWidth + elif self.profile.is_a("IfcCShapeProfileDef"): + return self.profile.Width + elif self.profile.is_a("IfcCircleProfileDef"): + return self.profile.Radius * 2 + elif self.profile.is_a("IfcEllipseProfileDef"): + return self.profile.SemiAxis1 * 2 + elif self.profile.is_a("IfcIShapeProfileDef"): + return self.profile.OverallWidth + elif self.profile.is_a("IfcLShapeProfileDef"): + return self.profile.Width + elif self.profile.is_a("IfcRectangleProfileDef"): + return self.profile.XDim + elif self.profile.is_a("IfcTShapeProfileDef"): + return self.profile.FlangeWidth + elif self.profile.is_a("IfcUShapeProfileDef"): + return self.profile.FlangeWidth + elif self.profile.is_a("IfcZShapeProfileDef"): + return (self.profile.FlangeWidth * 2) - self.profile.WebThickness else: settings = ifcopenshell.geom.settings() settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) - shape = ifcopenshell.geom.create_shape(settings, self.settings["profile"]) + shape = ifcopenshell.geom.create_shape(settings, self.profile) return self.convert_si_to_unit(ifcopenshell.util.shape.get_x(shape)) return 0.0 - def get_y(self): - if self.settings["profile"].is_a("IfcAsymmetricIShapeProfileDef"): - return self.settings["profile"].OverallDepth - elif self.settings["profile"].is_a("IfcCShapeProfileDef"): - return self.settings["profile"].Depth - elif self.settings["profile"].is_a("IfcCircleProfileDef"): - return self.settings["profile"].Radius * 2 - elif self.settings["profile"].is_a("IfcEllipseProfileDef"): - return self.settings["profile"].SemiAxis2 * 2 - elif self.settings["profile"].is_a("IfcIShapeProfileDef"): - return self.settings["profile"].OverallDepth - elif self.settings["profile"].is_a("IfcLShapeProfileDef"): - return self.settings["profile"].Depth - elif self.settings["profile"].is_a("IfcRectangleProfileDef"): - return self.settings["profile"].YDim - elif self.settings["profile"].is_a("IfcTShapeProfileDef"): - return self.settings["profile"].Depth - elif self.settings["profile"].is_a("IfcUShapeProfileDef"): - return self.settings["profile"].Depth - elif self.settings["profile"].is_a("IfcZShapeProfileDef"): - return self.settings["profile"].Depth + def get_y(self) -> float: + if self.profile.is_a("IfcAsymmetricIShapeProfileDef"): + return self.profile.OverallDepth + elif self.profile.is_a("IfcCShapeProfileDef"): + return self.profile.Depth + elif self.profile.is_a("IfcCircleProfileDef"): + return self.profile.Radius * 2 + elif self.profile.is_a("IfcEllipseProfileDef"): + return self.profile.SemiAxis2 * 2 + elif self.profile.is_a("IfcIShapeProfileDef"): + return self.profile.OverallDepth + elif self.profile.is_a("IfcLShapeProfileDef"): + return self.profile.Depth + elif self.profile.is_a("IfcRectangleProfileDef"): + return self.profile.YDim + elif self.profile.is_a("IfcTShapeProfileDef"): + return self.profile.Depth + elif self.profile.is_a("IfcUShapeProfileDef"): + return self.profile.Depth + elif self.profile.is_a("IfcZShapeProfileDef"): + return self.profile.Depth else: settings = ifcopenshell.geom.settings() settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) - shape = ifcopenshell.geom.create_shape(settings, self.settings["profile"]) + shape = ifcopenshell.geom.create_shape(settings, self.profile) return self.convert_si_to_unit(ifcopenshell.util.shape.get_y(shape)) return 0.0 diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py index 0fc5cccdf9..4c004ecfdd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/edit_profile_usage.py @@ -149,6 +149,8 @@ class Usecase: self.settings_2d.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) shape = ifcopenshell.geom.create_shape(self.settings_2d, dummy_solid) + # NOTE: points do not need unit conversion + # as dummy file is inherently using project units. if self.cardinal_point == 1: return self.get_bottom_left(shape) elif self.cardinal_point == 2: diff --git a/src/ifcopenshell-python/ifcopenshell/util/data.py b/src/ifcopenshell-python/ifcopenshell/util/data.py index 365f0df2cb..9f02d973a5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/data.py +++ b/src/ifcopenshell-python/ifcopenshell/util/data.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +from __future__ import annotations import numpy as np import ifcopenshell from typing import Any, Union @@ -30,7 +31,9 @@ class Clipping: operand_type: str = "IfcHalfSpaceSolid" @classmethod - def parse(cls, raw_data: Any) -> Union[ifcopenshell.entity_instance, "Clipping", None]: + def parse( + cls, raw_data: Union[ifcopenshell.entity_instance, Clipping, dict[str, Any]] + ) -> Union[ifcopenshell.entity_instance, Clipping]: """Parse various formats into a clipping object `raw_data` can be either: diff --git a/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py b/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py index 1ee1b3ac87..c827f1e33a 100644 --- a/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py +++ b/src/ifcopenshell-python/test/api/geometry/test_add_shape_aspect.py @@ -20,6 +20,7 @@ import test.bootstrap import ifcopenshell.api.root import ifcopenshell.api.context import ifcopenshell.api.geometry +import ifcopenshell.util.shape_builder class TestAddShapeAspect(test.bootstrap.IFC4): From 649a913bbcf48221a782591bb6e2e65897ec1a2a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 4 Mar 2025 12:04:39 +0500 Subject: [PATCH 204/476] add_profile_representation - add test --- .../test_add_profile_representation.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/ifcopenshell-python/test/api/geometry/test_add_profile_representation.py diff --git a/src/ifcopenshell-python/test/api/geometry/test_add_profile_representation.py b/src/ifcopenshell-python/test/api/geometry/test_add_profile_representation.py new file mode 100644 index 0000000000..39ac9aa928 --- /dev/null +++ b/src/ifcopenshell-python/test/api/geometry/test_add_profile_representation.py @@ -0,0 +1,75 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Dion Moult +# +# 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 . + +import test.bootstrap +import ifcopenshell.api.unit +import ifcopenshell.api.root +import ifcopenshell.api.context +import ifcopenshell.api.geometry +from ifcopenshell.util.shape_builder import ShapeBuilder + + +class TestAddProfileRepresentation(test.bootstrap.IFC4): + def setup_profile(self) -> None: + builder = ShapeBuilder(self.file) + + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + # In IFC2X3 the unit is required. + unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix=None) + ifcopenshell.api.unit.assign_unit(self.file, [unit]) + + model_context = ifcopenshell.api.context.add_context(self.file, context_type="Model") + self.body = ifcopenshell.api.context.add_context( + self.file, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model_context + ) + rectangle = builder.rectangle((100, 100)) + self.profile = builder.profile(rectangle) + + def test_run(self): + self.setup_profile() + representation_string_cardinal_point = ifcopenshell.api.geometry.add_profile_representation( + self.file, + context=self.body, + profile=self.profile, + depth=1000, + cardinal_point="bottom left", + ) + representation_numeric_cardinal_point = ifcopenshell.api.geometry.add_profile_representation( + self.file, + context=self.body, + profile=self.profile, + depth=1000, + cardinal_point=1, + ) + representations = [representation_string_cardinal_point, representation_numeric_cardinal_point] + for representation in representations: + assert representation.is_a("IfcShapeRepresentation") + item = representation.Items[0] + assert item.is_a("IfcExtrudedAreaSolid") + assert item.SweptArea == self.profile + assert item.ExtrudedDirection.DirectionRatios == (0.0, 0.0, 1.0) + assert item.Depth == 1000 + assert item.Position.Location.Coordinates == (-50.0, 50.0, 0.0) + + +class TestAddProfileRepresentationIFC2X3(test.bootstrap.IFC2X3, TestAddProfileRepresentation): + pass + + +class TestAddProfileRepresentationIFC4X3(test.bootstrap.IFC4X3, TestAddProfileRepresentation): + pass From 94400961060eab545ed966361a37f7a3185a51f8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 4 Mar 2025 14:56:06 +0500 Subject: [PATCH 205/476] Fix #6260 after 2f6ae17 --- src/bonsai/bonsai/tool/drawing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index c223b3ee58..86fb187db8 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -1517,7 +1517,7 @@ class Drawing(bonsai.core.tool.Drawing): dst.data = dst.data.copy() dst.name = dst.name.replace("IfcGridAxis/", "") tool.Blender.get_object_bim_props(dst).ifc_definition_id = 0 - tool.Geometry.get_geometry_props(dst.data).ifc_definition_id = 0 + tool.Geometry.get_mesh_props(dst.data).ifc_definition_id = 0 return dst def disassemble(obj: bpy.types.Object) -> tuple[bpy.types.Object, bmesh.types.BMesh]: From 86072079de4a7a423fb47ae3190f3d126e039c36 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 4 Mar 2025 16:04:21 +0500 Subject: [PATCH 206/476] draw_image_for_ifc_profile - optimizations using numpy --- src/bonsai/bonsai/tool/profile.py | 35 ++++++++++++++++--------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py index d230835b09..ae8b695b1c 100644 --- a/src/bonsai/bonsai/tool/profile.py +++ b/src/bonsai/bonsai/tool/profile.py @@ -25,6 +25,8 @@ import ifcopenshell.util.element import ifcopenshell.util.unit import ifcopenshell.util.placement import ifcopenshell.util.representation +import ifcopenshell.util.shape +import numpy as np import bonsai.core.tool import bonsai.tool as tool import PIL.ImageDraw @@ -50,31 +52,30 @@ class Profile(bonsai.core.tool.Profile): settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) shape = ifcopenshell.geom.create_shape(settings, profile) - verts = shape.verts - if not verts: + verts = ifcopenshell.util.shape.get_vertices(shape) + if verts.size == 0: raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.") - edges = shape.edges - - grouped_verts = [[verts[i], verts[i + 1]] for i in range(0, len(verts), 3)] - grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)] - - max_x = max([v[0] for v in grouped_verts]) - min_x = min([v[0] for v in grouped_verts]) - max_y = max([v[1] for v in grouped_verts]) - min_y = min([v[1] for v in grouped_verts]) + edges = ifcopenshell.util.shape.get_edges(shape) + verts_flat = verts.ravel() + max_x = np.max(verts_flat[0::3]).item() + min_x = np.min(verts_flat[0::3]).item() + max_y = np.max(verts_flat[1::3]).item() + min_y = np.min(verts_flat[1::3]).item() dim_x = max_x - min_x dim_y = max_y - min_y max_dim = max([dim_x, dim_y]) scale = 100 / max_dim + dim = np.array([dim_x, dim_y]) - for vert in grouped_verts: - vert[0] = round(scale * (vert[0] - min_x)) + ((size / 2) - scale * (dim_x / 2)) - vert[1] = round(scale * (vert[1] - min_y)) + ((size / 2) - scale * (dim_y / 2)) - - for e in grouped_edges: - draw.line((tuple(grouped_verts[e[0]]), tuple(grouped_verts[e[1]])), fill="white", width=2) + verts = verts[:, :2] + verts = np.round(scale * (verts - [min_x, min_y]) + (size / 2) - scale * dim / 2) + for verts_ in verts[edges]: + # draw.line seem to support only tuple of tuples. + verts_tuple: tuple[tuple[float, ...], ...] + verts_tuple = tuple(tuple(i) for i in verts_) + draw.line(verts_tuple, fill="white", width=2) @classmethod def is_editing_profile(cls) -> bool: From d0cb5456b3177dcd67d745fd8767357eb9fdea78 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 4 Mar 2025 17:34:24 +0500 Subject: [PATCH 207/476] Fix importing IfcPolylines always assuming they're closed Resolved issue during import in #6259 but doesn't completely resolves the issue. --- src/bonsai/bonsai/tool/model.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 0704fe7154..bccb9d4ec3 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -497,19 +497,24 @@ class Model(bonsai.core.tool.Model): offset = len(cls.vertices) if curve.is_a("IfcPolyline"): - total_points = len(curve.Points) - last_index = len(curve.Points) - 1 - for i, point in enumerate(curve.Points): - if i == last_index: - continue + curve_points: tuple[ifcopenshell.entity_instance, ...] = curve.Points + # Polyline must have 2 points to be valid. + is_closed = np.allclose(curve_points[0].Coordinates, curve_points[-1].Coordinates) + + points_to_add = curve_points[:-1] if is_closed else curve_points + for point in points_to_add: global_point = position @ Vector(cls.convert_unit_to_si(point.Coordinates)).to_3d() 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 + + cls.edges.extend([(i, i + 1) for i in range(offset, len(cls.vertices) - 1)]) + if is_closed: + 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.convert_curve_to_mesh(obj, position, segment.ParentCurve) + elif curve.is_a("IfcIndexedPolyCurve"): for local_point in curve.Points.CoordList: global_point = position @ Vector(cls.convert_unit_to_si(local_point)).to_3d() From 3fc312752bb641b12c668a11d10bcb8865ee2bc8 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 4 Mar 2025 17:00:31 +0500 Subject: [PATCH 208/476] Show an error for unsupport curve types #6259 --- src/bonsai/bonsai/bim/module/profile/operator.py | 10 ++++++++-- src/bonsai/bonsai/tool/model.py | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index 06c99711d4..f94f711f7d 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -234,9 +234,15 @@ class EnableEditingArbitraryProfile(bpy.types.Operator): props = tool.Profile.get_profile_props() active_profile = props.profiles[props.active_profile_index] profile_id = active_profile.ifc_definition_id - props.active_arbitrary_profile_id = profile_id profile = tool.Ifc.get().by_id(profile_id) - obj = tool.Model.import_profile(profile) + + try: + obj = tool.Model.import_profile(profile) + except tool.Model.UnsupportedCurveForConversion as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + + props.active_arbitrary_profile_id = profile_id tool.Ifc.link(profile, obj) bpy.context.scene.collection.objects.link(obj) tool.Blender.select_and_activate_single_object(context, obj) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index bccb9d4ec3..2a191bde76 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -486,6 +486,9 @@ class Model(bonsai.core.tool.Model): return obj + class UnsupportedCurveForConversion(Exception): + pass + @classmethod def convert_curve_to_mesh( cls, @@ -550,6 +553,8 @@ class Model(bonsai.core.tool.Model): ) cls.circles.append([offset, offset + 1]) cls.edges.append((offset, offset + 1)) + else: + raise cls.UnsupportedCurveForConversion(f"Profile has unsupported curve type: {curve}.") @classmethod def import_rectangle(cls, obj: bpy.types.Object, position: Matrix, profile: ifcopenshell.entity_instance) -> None: From 7c6e124fe90c89dc6cb5636c3bcc4c8320d493ec Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 5 Mar 2025 22:10:33 +1100 Subject: [PATCH 209/476] See #1227. Basic implementation of sloped walls (joins not yet considered) --- src/bonsai/bonsai/tool/loader.py | 24 +++++++++++++++----- src/bonsai/scripts/waldo.py | 38 ++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index c40adddf00..9bdb09dd30 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1043,7 +1043,8 @@ class Loader(bonsai.core.tool.Loader): bm.from_mesh(mesh) prev_co = None co = Vector((0.0, offset, 0.0)) - no = Vector((0.0, 1.0, 0.0)) + # no = Vector((0.0, 1.0, 0.0)) + no = (cls.get_extrusion_vector(element).cross(Vector([1., 0., 0.]))).normalized() # Cache this body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") styles = {} @@ -1053,7 +1054,8 @@ class Loader(bonsai.core.tool.Loader): styles[style] = i for layer in layer_set.MaterialLayers[:-1]: prev_co = co.copy() - co.y += layer.LayerThickness * cls.unit_scale * sense_factor + co += no * layer.LayerThickness * cls.unit_scale * sense_factor + # co.y += layer.LayerThickness * cls.unit_scale * sense_factor bisect_geom = bmesh.ops.bisect_plane( bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no ) @@ -1064,8 +1066,9 @@ class Loader(bonsai.core.tool.Loader): mesh.materials.append(tool.Ifc.get_object(style)) for face in bisect_geom["geom"]: if isinstance(face, bmesh.types.BMFace): - center = face.calc_center_bounds() * sense_factor - if center.y < co.y and center.y > prev_co.y: + center = face.calc_center_median() + # if center.y < co.y and center.y > prev_co.y: + if (center - co).dot(no) < 0 and (center - prev_co).dot(no) >= 0: face.material_index = material_index has_layer_styles = True @@ -1077,8 +1080,9 @@ class Loader(bonsai.core.tool.Loader): mesh.materials.append(tool.Ifc.get_object(style)) for face in bisect_geom["geom"]: if isinstance(face, bmesh.types.BMFace): - center = face.calc_center_bounds() * sense_factor - if center.y > co.y: + center = face.calc_center_median() * sense_factor + # if center.y > co.y: + if (center - co).dot(no) >= 0: face.material_index = material_index has_layer_styles = True @@ -1087,6 +1091,14 @@ class Loader(bonsai.core.tool.Loader): mesh["has_layer_styles"] = has_layer_styles return mesh + @classmethod + def get_extrusion_vector(cls, wall): + if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): + for item in ifcopenshell.util.representation.resolve_representation(body).Items: + if item.is_a("IfcExtrudedAreaSolid"): + return Vector(item.ExtrudedDirection.DirectionRatios) + return Vector([0., 0., 1.]) + @classmethod def create_mesh_from_shape( cls, diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index ad382b101a..0714053d52 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -14,6 +14,7 @@ import ifcopenshell.util.element # from ifcopenshell.util.shape_builder import VectorType, SequenceOfVectors from itertools import cycle from collections import namedtuple +from math import sin, cos, radians f = ifcopenshell.api.project.create_file() @@ -46,7 +47,7 @@ ifcopenshell.api.style.add_surface_style(f, style=style, ifc_class="IfcSurfaceSt ifcopenshell.api.style.assign_material_style(f, material=material2, style=style, context=body) -def test_wall(offset, p1, p2, p3, p4): +def test_wall(offset, p1, p2, p3, p4, a1=None, a2=None): offset *= 1.5 wall_type_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="A") wall_type_b = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWallType", name="B") @@ -162,7 +163,7 @@ def test_wall(offset, p1, p2, p3, p4): related_connection="ATSTART", ) - Foo(f, body, axis).regenerate(wall_a) + Foo(f, body, axis).regenerate(wall_a, angle=a1) Foo(f, body, axis).regenerate(wall_b) Foo(f, body, axis).regenerate(wall_c) @@ -238,7 +239,7 @@ class Foo: self.body = body self.axis = axis - def regenerate(self, wall): + def regenerate(self, wall, angle=None): print("-" * 100) print(wall) layers = self.get_layers(wall) @@ -246,7 +247,8 @@ class Foo: return reference = self.get_reference_line(wall) self.reference_p1, self.reference_p2 = reference - axes = self.get_axes(wall, reference, layers) + self.angle = angle or self.get_angle(wall) + axes = self.get_axes(wall, reference, layers, self.angle) self.miny = axes[0][0][1] self.maxy = axes[-1][0][1] self.end_point = None @@ -342,7 +344,9 @@ class Foo: else: profile = profiles[0] - item = builder.extrude(profile, magnitude=1.0) + item = builder.extrude( + profile, magnitude=1.0, extrusion_vector=np.array([0.0, sin(self.angle), cos(self.angle)]) + ) rep = builder.get_representation(self.body, items=[item]) if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body): ifcopenshell.util.element.replace_element(old_rep, rep) @@ -367,8 +371,8 @@ class Foo: # axes = self.get_axes(wall2, layers2) reference1 = self.get_reference_line(wall1) reference2 = self.get_reference_line(wall2) - axes1 = self.get_axes(wall1, reference1, layers1) - axes2 = self.get_axes(wall2, reference2, layers2) + axes1 = self.get_axes(wall1, reference1, layers1, self.angle) + axes2 = self.get_axes(wall2, reference2, layers2, self.get_angle(wall2)) matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)) matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) print(axes1) @@ -444,14 +448,14 @@ class Foo: segment = [] for point in points: segment.append(point) - if len(segment) == 1: # Not enough points to categorise the segment + if len(segment) == 1: # Not enough points to categorise the segment continue - elif {segment[0][1], segment[-1][1]} == split_ys: # This segment splits the wall + elif {segment[0][1], segment[-1][1]} == split_ys: # This segment splits the wall if segment[0][1] > segment[-1][1]: # Go in the +Y direction segment.reverse() self.split_points.append(segment) segment = [] - elif segment[0][1] == segment[-1][1]: # This segment cuts some of the wall + elif segment[0][1] == segment[-1][1]: # This segment cuts some of the wall if segment[0][1] == self.maxy: # Go in the +X direction if segment[0][0] > segment[-1][0]: segment.reverse() @@ -577,7 +581,16 @@ class Foo: return [np.array(points[1]), np.array(points[0])] return [np.array((0.0, 0.0)), np.array((1.0, 0.0))] - def get_axes(self, wall, reference, layers: list[PrioritisedLayer]): + def get_angle(self, wall): + if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): + for item in ifcopenshell.util.representation.resolve_representation(body).Items: + if item.is_a("IfcExtrudedAreaSolid"): + return ifcopenshell.util.shape_builder.np_angle_signed( + np.array((0.0, 1.0)), np.array(item.ExtrudedDirection.DirectionRatios[1:]) + ) + return 0.0 + + def get_axes(self, wall, reference, layers: list[PrioritisedLayer], angle: float): axes = [[p.copy() for p in reference]] # Apply usage to convert the Reference line into MlsBase sense_factor = 1 @@ -587,7 +600,8 @@ class Foo: sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 for layer in layers: - axes.append([p.copy() + np.array((0.0, layer.thickness * sense_factor)) for p in axes[-1]]) + y_offset = (layer.thickness * sense_factor) / cos(angle) + axes.append([p.copy() + np.array((0.0, y_offset)) for p in axes[-1]]) return axes From 4c97cc8a413d26bbf67319b7f620f6c03488c768 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 5 Mar 2025 14:52:19 +0500 Subject: [PATCH 210/476] import_ifc - not to break completely on facing breaking mesh #6270 create_mesh returns None only in case if it meets some exception and it prints logs in that case, but atleast some breaking mesh won't be in the way of users trying to open some model --- src/bonsai/bonsai/bim/import_ifc.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index ade2a99143..6fb0dd6fd0 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -514,8 +514,9 @@ class IfcImporter: pass elif shape: mesh = self.create_mesh(element, shape) - tool.Loader.link_mesh(shape, mesh) - self.meshes[mesh_name] = mesh + if mesh is not None: + tool.Loader.link_mesh(shape, mesh) + self.meshes[mesh_name] = mesh else: self.ifc_import_settings.logger.error("Failed to generate shape for %s", element) break @@ -798,8 +799,9 @@ class IfcImporter: materials_updated = bool(mesh) if mesh is None: mesh = self.create_mesh(element, shape) - tool.Loader.link_mesh(shape, mesh) - self.meshes[mesh_name] = mesh + if mesh is not None: + tool.Loader.link_mesh(shape, mesh) + self.meshes[mesh_name] = mesh else: mesh = None @@ -810,11 +812,10 @@ class IfcImporter: if element.is_a(ifcclass): obj.display_type = "WIRE" - if shape: + if shape and mesh: # We use numpy here because Blender mathutils.Matrix is not accurate enough mat = ifcopenshell.util.shape.get_shape_matrix(shape) self.set_matrix_world(obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, mat)) - assert mesh # Type checker. if not materials_updated: self.material_creator.create(element, obj, mesh, tool.Geometry.does_shape_has_openings(shape)) elif mesh: # When does this occur? @@ -1033,7 +1034,7 @@ class IfcImporter: element: ifcopenshell.entity_instance, shape: Union[ifcopenshell.geom.ShapeElementType, ifcopenshell.geom.ShapeType], cartesian_point_offset: Union[npt.NDArray[np.float64], Literal[False]] = None, - ) -> bpy.types.Mesh: + ) -> Union[bpy.types.Mesh, None]: try: if hasattr(shape, "geometry"): # shape is ShapeElementType From e1e51cbf2648a7430184764c9de66849b021c169 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 5 Mar 2025 18:53:50 +0500 Subject: [PATCH 211/476] black . --- src/bonsai/bonsai/bim/module/model/polyline.py | 2 +- src/bonsai/bonsai/bim/module/model/slab.py | 4 ++-- src/bonsai/bonsai/bim/module/model/wall.py | 5 +++-- src/bonsai/bonsai/tool/loader.py | 4 ++-- src/bonsai/bonsai/tool/raycast.py | 1 - .../ifcopenshell/api/geometry/add_slab_representation.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 3ff5b349a5..703c25e9e4 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -239,7 +239,7 @@ def get_slab_preview_data(context, relating_type): bm.verts.ensure_lookup_table() if x_angle: rot_mat = Matrix.Rotation(x_angle, 3, "X") - if abs(x_angle) > (pi/2): + if abs(x_angle) > (pi / 2): rot_mat = rot_mat @ Matrix.Scale(-1, 3, (0, 1, 0)) bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=rot_mat) new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index ab9c6a8f8b..996536d8f6 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -313,8 +313,8 @@ class DumbSlabPlaner: if layer_params["direction_sense"] == "NEGATIVE": direction_ratios *= -1 # offset_direction *= -1 - elif (abs(existing_x_angle )> (pi / 2) and direction_ratios.z > 0) or ( - abs(existing_x_angle )< (pi / 2) and direction_ratios.z < 0 + elif (abs(existing_x_angle) > (pi / 2) and direction_ratios.z > 0) or ( + abs(existing_x_angle) < (pi / 2) and direction_ratios.z < 0 ): # The extrusion direction is negative. If the layer_parameter is set to positive, # then the we change the extrusion direction. diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 2f0f41b24a..adf8ee9725 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -248,7 +248,8 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): if tool.Model.get_usage_type(element) == "LAYER3": # Reset the transformation and returns to the original points with 0 degrees extrusion.SweptArea.OuterCurve.Points.CoordList = [ - (p[0], p[1] * abs(cos(existing_x_angle))) for p in extrusion.SweptArea.OuterCurve.Points.CoordList + (p[0], p[1] * abs(cos(existing_x_angle))) + for p in extrusion.SweptArea.OuterCurve.Points.CoordList ] # Apply the transformation for the new x_angle @@ -261,7 +262,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): direction_ratios = Vector((0.0, sin(x_angle), cos(x_angle))) # direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios) layer_params = tool.Model.get_material_layer_parameters(element) - perpendicular_depth= layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale + perpendicular_depth = layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale perpendicular_offset = layer_params["offset"] * abs(1 / cos(x_angle)) / unit_scale offset_direction = direction_ratios.copy() diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 9bdb09dd30..45c1b8fd94 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1044,7 +1044,7 @@ class Loader(bonsai.core.tool.Loader): prev_co = None co = Vector((0.0, offset, 0.0)) # no = Vector((0.0, 1.0, 0.0)) - no = (cls.get_extrusion_vector(element).cross(Vector([1., 0., 0.]))).normalized() + no = (cls.get_extrusion_vector(element).cross(Vector([1.0, 0.0, 0.0]))).normalized() # Cache this body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") styles = {} @@ -1097,7 +1097,7 @@ class Loader(bonsai.core.tool.Loader): for item in ifcopenshell.util.representation.resolve_representation(body).Items: if item.is_a("IfcExtrudedAreaSolid"): return Vector(item.ExtrudedDirection.DirectionRatios) - return Vector([0., 0., 1.]) + return Vector([0.0, 0.0, 1.0]) @classmethod def create_mesh_from_shape( diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 1dcefd86d2..b0c4abd4ec 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -171,7 +171,6 @@ class Raycast(bonsai.core.tool.Raycast): print("empty", snap_point) return points - if not custom_bmesh: bm = bmesh.new() if face is None: # Object without faces diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py index 6443e3a857..3ceca7094c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py @@ -98,7 +98,7 @@ class Usecase: else: direction_ratios = (0.0, 0.0, 1.0) - offset_direction = direction_ratios # offset direction doesn't change if direction_sense is negative + offset_direction = direction_ratios # offset direction doesn't change if direction_sense is negative extrusion_direction = self.file.createIfcDirection(direction_ratios) if self.settings["direction_sense"] == "NEGATIVE": direction_ratios = tuple((-n for n in direction_ratios)) From 1c8ef176b1df1b197aaa06b4964fe7befd83d444 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 5 Mar 2025 14:56:29 +0500 Subject: [PATCH 212/476] typing --- src/bonsai/bonsai/bim/module/boundary/operator.py | 9 +++++---- src/bonsai/bonsai/bim/module/model/door.py | 1 + src/bonsai/bonsai/tool/model.py | 10 +++++++--- .../ifcopenshell/entity_instance.py | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/boundary/operator.py b/src/bonsai/bonsai/bim/module/boundary/operator.py index ac81aa2a65..ece7277275 100644 --- a/src/bonsai/bonsai/bim/module/boundary/operator.py +++ b/src/bonsai/bonsai/bim/module/boundary/operator.py @@ -41,6 +41,7 @@ from bonsai.bim.module.model.decorator import ProfileDecorator from bonsai.bim.module.boundary.decorator import BoundaryDecorator import bonsai.core import bonsai.core.geometry +from typing import Union, Optional def disable_editing_boundary_geometry(context): @@ -58,7 +59,7 @@ def disable_editing_boundary_geometry(context): class Loader: - def __init__(self, operator=None): + def __init__(self, operator: Optional[bpy.types.Operator] = None): self.operator = operator self.ifc_file = None self.logger = None @@ -67,7 +68,7 @@ class Loader: self.fallback_settings = self.load_fallback_settings() self.load_importer() - def create_mesh(self, boundary): + def create_mesh(self, boundary: ifcopenshell.entity_instance) -> Union[bpy.types.Mesh, None]: # ConnectionGeometry is optional in IFC schema for some reasons. if not boundary.ConnectionGeometry: return None @@ -131,7 +132,7 @@ class Loader: settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) return settings - def load_importer(self): + def load_importer(self) -> None: self.ifc_file = tool.Ifc.get() self.logger = logging.getLogger("ImportIFC") ifc_import_settings = import_ifc.IfcImportSettings.factory(bpy.context, IfcStore.path, self.logger) @@ -280,7 +281,7 @@ class SelectProjectBoundaries(bpy.types.Operator): return {"FINISHED"} -def get_colour(ifc_boundary): +def get_colour(ifc_boundary: ifcopenshell.entity_instance) -> tuple[float, float, float, float]: """Return a color depending on IfcClass given""" product_colors = { "IfcWall": (0.7, 0.3, 0, 1), diff --git a/src/bonsai/bonsai/bim/module/model/door.py b/src/bonsai/bonsai/bim/module/model/door.py index 1d53a2c279..3021d47f92 100644 --- a/src/bonsai/bonsai/bim/module/model/door.py +++ b/src/bonsai/bonsai/bim/module/model/door.py @@ -21,6 +21,7 @@ import bpy import bmesh import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.material import ifcopenshell.util.element import ifcopenshell.util.representation import ifcopenshell.util.schema diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 2a191bde76..31069b1d19 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -26,6 +26,7 @@ import collections.abc import numpy as np import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.geometry import ifcopenshell.api.grid import ifcopenshell.api.pset import ifcopenshell.geom @@ -1555,12 +1556,14 @@ class Model(bonsai.core.tool.Model): def add_body_representation(cls, obj: bpy.types.Object) -> None: ifc_file = tool.Ifc.get() body = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW") - representation = ifcopenshell.api.run( - "geometry.add_representation", + assert body + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) + representation = ifcopenshell.api.geometry.add_representation( ifc_file, context=body, blender_object=obj, - geometry=obj.data, + geometry=mesh, coordinate_offset=tool.Geometry.get_cartesian_point_offset(obj), total_items=tool.Geometry.get_total_representation_items(obj), should_force_faceted_brep=tool.Geometry.should_force_faceted_brep(), @@ -1569,6 +1572,7 @@ class Model(bonsai.core.tool.Model): ifc_representation_class=None, profile_set_usage=None, ) + assert representation tool.Model.replace_object_ifc_representation(body, obj, representation) @classmethod diff --git a/src/ifcopenshell-python/ifcopenshell/entity_instance.py b/src/ifcopenshell-python/ifcopenshell/entity_instance.py index 15e5980d41..d01c618d52 100644 --- a/src/ifcopenshell-python/ifcopenshell/entity_instance.py +++ b/src/ifcopenshell-python/ifcopenshell/entity_instance.py @@ -613,7 +613,7 @@ class entity_instance: include_identifier: bool = True, recursive: bool = False, return_type: type[dict] = dict, - ignore: Iterable[str] = (), + ignore: Sequence[str] = (), ) -> dict[str, Any]: """More perfomant version of `.get_info()` but with limited arguments values.\n Method has exactly the same signature as `.get_info()` but it doesn't support getting information non-recursively. From 1257eb087252f6093cdacb541e507d7e7207d292 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 5 Mar 2025 15:46:37 +0500 Subject: [PATCH 213/476] append_asset - allow using same name for different ifcpresentationstyle types Since they are not used interchangeably typically. --- .../ifcopenshell/api/project/append_asset.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 2109bca618..279bdc8fd6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -229,7 +229,9 @@ class Usecase: return next((e for e in self.file.by_type("IfcProfileDef") if e.ProfileName == profile_name), None) elif element.is_a("IfcPresentationStyle"): name = element.Name - return next((e for e in self.file.by_type("IfcPresentationStyle") if e.Name == name), None) + if name is None: + return None + return next((e for e in self.file.by_type(element.is_a()) if e.Name == name), None) else: return None From 08b50bb9ea7e3587962022e767b67f7d390006ac Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 5 Mar 2025 15:54:05 +0500 Subject: [PATCH 214/476] Library UI - recognize already appended styles Example - https://i.imgur.com/XCXHY4C.png Noticed investigating #6269 --- src/bonsai/bonsai/bim/module/project/operator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index ee06a59b11..2b41699261 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -378,6 +378,8 @@ class ChangeLibraryElement(bpy.types.Operator): next(e for e in self.file.by_type("IfcMaterial") if e.Name == name) elif element.is_a("IfcProfileDef"): next(e for e in self.file.by_type("IfcProfileDef") if e.ProfileName == name) + elif element.is_a("IfcPresentationStyle"): + next(e for e in self.file.by_type(element.is_a()) if e.Name == name) else: self.file.by_guid(element.GlobalId) new.is_appended = True From 2987d38a312a6233840f2f11d2262dd50a3de10a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 5 Mar 2025 16:16:30 +0500 Subject: [PATCH 215/476] append_asset - check unique styles based on their name #6269 --- .../ifcopenshell/api/project/append_asset.py | 10 ++++++++- .../test/api/project/test_append_asset.py | 21 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py index 279bdc8fd6..ff9097c2cc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py @@ -525,6 +525,7 @@ class Usecase: if added_element := reuse_identities.get(element_identity): return added_element + ifc_class = element.is_a() attributes_ = None def get_attributes() -> tuple[W.attribute, ...]: @@ -550,6 +551,13 @@ class Usecase: if existing_material is not None: reuse_identities[element_identity] = existing_material return existing_material + elif element.is_a("IfcPresentationStyle"): + style_name = element.Name + if style_name is not None: + existing_style = next((e for e in ifc_file.by_type(ifc_class) if e.Name == style_name), None) + if existing_style is not None: + reuse_identities[element_identity] = existing_style + return existing_style attrs = {} @@ -598,7 +606,7 @@ class Usecase: attrs[attr_index] = attr_value # Adding entity at the end just to keep it consistent with `file.add`. - new = ifc_file.create_entity(element.is_a()) + new = ifc_file.create_entity(ifc_class) reuse_identities[element_identity] = new for attr_index, attr_value in attrs.items(): new[attr_index] = attr_value diff --git a/src/ifcopenshell-python/test/api/project/test_append_asset.py b/src/ifcopenshell-python/test/api/project/test_append_asset.py index c2718f5612..9251f10592 100644 --- a/src/ifcopenshell-python/test/api/project/test_append_asset.py +++ b/src/ifcopenshell-python/test/api/project/test_append_asset.py @@ -611,18 +611,27 @@ class TestAppendAssetIFC4(test.bootstrap.IFC4, TestAppendAssetIFC2X3): assert appended_item.is_a("IfcCostItem") assert appended_item.IsNestedBy[0].RelatedObjects[0].is_a("IfcCostItem") - def test_not_duplicate_profiles_and_materials_based_on_name(self): + def test_not_duplicate_profiles_materials_styles_based_on_name(self): + # Setup library. library = ifcopenshell.api.project.create_file(version=self.file.schema) + ifcopenshell.api.root.create_entity(library, ifc_class="IfcProject") + model = ifcopenshell.api.context.add_context(library, context_type="Model") + body = ifcopenshell.api.context.add_context( + library, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model + ) column_type = ifcopenshell.api.root.create_entity(library, "IfcColumnType") library_profile = ifcopenshell.api.profile.add_parameterized_profile(library, "IfcCircleProfileDef") library_profile.ProfileName = "TestProfile" material_set = ifcopenshell.api.material.add_material_set(library, set_type="IfcMaterialProfileSet") material = ifcopenshell.api.material.add_material(library, "TestMaterial") + style = ifcopenshell.api.style.add_style(library, "TestStyle", ifc_class="IfcSurfaceStyle") + ifcopenshell.api.style.assign_material_style(library, material, style, body) ifcopenshell.api.material.add_profile(library, material_set, material, library_profile) ifcopenshell.api.material.assign_material( library, [column_type], material=material_set, type="IfcMaterialProfileSet" ) + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") # Test adding a profile with existing name. profile = ifcopenshell.api.profile.add_parameterized_profile(self.file, "IfcCircleProfileDef") profile.ProfileName = "TestProfile" @@ -636,10 +645,18 @@ class TestAppendAssetIFC4(test.bootstrap.IFC4, TestAppendAssetIFC2X3): materials = self.file.by_type("IfcMaterial") assert len(materials) == 1 and materials[0].Name == "TestMaterial" - # Test implicitly adding profile+material with existing names. + # Test adding a style with existing name. + style = ifcopenshell.api.style.add_style(self.file, "TestStyle", ifc_class="IfcSurfaceStyle") + ifcopenshell.api.project.append_asset(self.file, library, style) + styles = self.file.by_type("IfcSurfaceStyle") + assert len(styles) == 1 and styles[0].Name == "TestStyle" + + # Test implicitly adding profile+material+style with existing names. ifcopenshell.api.project.append_asset(self.file, library, column_type) assert len(self.file.by_type("IfcColumnType")) == 1 profiles = self.file.by_type("IfcProfileDef") assert len(profiles) == 1 and profiles[0].ProfileName == "TestProfile" materials = self.file.by_type("IfcMaterial") assert len(materials) == 1 and materials[0].Name == "TestMaterial" + styles = self.file.by_type("IfcSurfaceStyle") + assert len(styles) == 1 and styles[0].Name == "TestStyle" From 74900193eef6c1b5c491596af12dc87f12eb0c1c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 5 Mar 2025 16:43:05 +0500 Subject: [PATCH 216/476] merge_identical_objects to use quicker version of get_info --- src/bonsai/bonsai/tool/debug.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 7752423ce4..b04280e1c8 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -120,8 +120,7 @@ class Debug(bonsai.core.tool.Debug): """ def get_hash(element: ifcopenshell.entity_instance) -> int: - # TODO: replace with get_info_2 after bonsai build update. - return hash(json.dumps(element.get_info(include_identifier=False, recursive=True), sort_keys=True)) + return hash(json.dumps(element.get_info_2(include_identifier=False, recursive=True), sort_keys=True)) ifc_file = tool.Ifc.get() merged_element_types: dict[str, list[str]] = {} From c4f2d556f1575e4e1349503dd16aab361e94e2e5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 5 Mar 2025 17:11:22 +0500 Subject: [PATCH 217/476] Uncomment use of get_edges_representation_item_ids as build was updated awhile ago --- src/bonsai/bonsai/tool/loader.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 45c1b8fd94..5d1192f420 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1006,11 +1006,7 @@ class Loader(bonsai.core.tool.Loader): else: edges = ifcopenshell.util.shape.get_edges(geometry) mesh.from_pydata(verts.tolist(), edges.tolist(), []) - # TODO: remove error handling after we update build in Bonsai. - try: - edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist() - except AttributeError: - edges_item_ids = [] + edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist() mesh["ios_edges_item_ids"] = edges_item_ids tool.Blender.Attribute.fill_attribute(mesh, "ios_edges_item_ids", "EDGE", "INT", edges_item_ids) tool.Blender.Attribute.fill_attribute(mesh, "ios_material_ids", "EDGE", "INT", geometry.material_ids) From 2c16839de7005ec15c94320a717752a17be1481d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 5 Mar 2025 17:28:55 +0500 Subject: [PATCH 218/476] Fix parametric stairs/railings/roofs not updating ifc link after representation update #6264 Related to 3107fd9 --- src/bonsai/bonsai/tool/model.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 31069b1d19..62cabec1b5 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -1002,8 +1002,11 @@ class Model(bonsai.core.tool.Model): obj: bpy.types.Object, new_representation: ifcopenshell.entity_instance, ) -> None: + mesh = obj.data + assert isinstance(mesh, bpy.types.Mesh) ifc_file = tool.Ifc.get() ifc_element = tool.Ifc.get_entity(obj) + assert ifc_element old_representation = ifcopenshell.util.representation.get_representation( ifc_element, ifc_context.ContextType, ifc_context.ContextIdentifier, ifc_context.TargetView ) @@ -1017,6 +1020,15 @@ class Model(bonsai.core.tool.Model): ifcopenshell.api.run( "geometry.assign_representation", ifc_file, product=ifc_element, representation=new_representation ) + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=new_representation, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) @classmethod def update_thumbnail_for_element(cls, element: ifcopenshell.entity_instance, refresh: bool = False) -> None: From 53dea65d8b20103fea5142d133cc14e603e9ba67 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 6 Mar 2025 19:38:34 +1100 Subject: [PATCH 219/476] See #1227. Implement joins for sloped walls. --- src/bonsai/bonsai/tool/loader.py | 2 + src/bonsai/scripts/waldo.py | 254 +++++++++++++++++++++++-------- 2 files changed, 191 insertions(+), 65 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 5d1192f420..c98f915134 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1091,6 +1091,8 @@ class Loader(bonsai.core.tool.Loader): def get_extrusion_vector(cls, wall): if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): for item in ifcopenshell.util.representation.resolve_representation(body).Items: + while item.is_a("IfcBooleanResult"): + item = item.FirstOperand if item.is_a("IfcExtrudedAreaSolid"): return Vector(item.ExtrudedDirection.DirectionRatios) return Vector([0.0, 0.0, 1.0]) diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index 0714053d52..b53ae6b21f 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -11,6 +11,9 @@ import ifcopenshell.api.geometry import ifcopenshell.util.shape_builder import ifcopenshell.util.element +# https://stackoverflow.com/a/9184560/9627415 +# Possible optimisation to linalg.norm? + # from ifcopenshell.util.shape_builder import VectorType, SequenceOfVectors from itertools import cycle from collections import namedtuple @@ -164,8 +167,8 @@ def test_wall(offset, p1, p2, p3, p4, a1=None, a2=None): ) Foo(f, body, axis).regenerate(wall_a, angle=a1) - Foo(f, body, axis).regenerate(wall_b) - Foo(f, body, axis).regenerate(wall_c) + Foo(f, body, axis).regenerate(wall_b, angle=a1) + Foo(f, body, axis).regenerate(wall_c, angle=a1) def create_type(name, layers): @@ -181,7 +184,7 @@ def create_type(name, layers): return wall_type -def test_atpath(offset): +def test_atpath(offset, angle=None): offset *= 1.5 wall_type_a = create_type("A", [(1, 0.05), (2, 0.1), (3, 0.05)]) wall_a = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="A123") @@ -213,7 +216,7 @@ def test_atpath(offset): relating_connection="ATEND", related_connection="ATPATH", ) - Foo(f, body, axis).regenerate(wall) + Foo(f, body, axis).regenerate(wall, angle=angle) create_branch("B", 1, 1, 1, 1, 1, -75) create_branch("C", 1, 2, 3, 2, 1, -75) @@ -227,7 +230,7 @@ def test_atpath(offset): create_branch("E", 4, 4, 4, 3.5, -1, 75) create_branch("F", 4, 2, 4, 4.5, -1, 75) - Foo(f, body, axis).regenerate(wall_a) + Foo(f, body, axis).regenerate(wall_a, angle=angle) PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness") @@ -238,25 +241,32 @@ class Foo: self.file = file self.body = body self.axis = axis + self.is_angled = False def regenerate(self, wall, angle=None): print("-" * 100) print(wall) + self.fallback_angle = angle layers = self.get_layers(wall) if not layers: return reference = self.get_reference_line(wall) self.reference_p1, self.reference_p2 = reference - self.angle = angle or self.get_angle(wall) - axes = self.get_axes(wall, reference, layers, self.angle) + self.wall_vectors = self.get_wall_vectors(wall) + axes = self.get_axes(wall, reference, layers, self.wall_vectors["a"]) self.miny = axes[0][0][1] self.maxy = axes[-1][0][1] self.end_point = None self.start_points = [] + self.start_vector = np.array((0.0, 0.0, 1.0)) + self.start_offset = 0.0 + self.atpath_points = [] self.split_points = [] self.maxpath_points = [] self.minpath_points = [] self.end_points = [] + self.end_vector = np.array((0.0, 0.0, 1.0)) + self.end_offset = 0.0 for rel in wall.ConnectedTo: if rel.is_a("IfcRelConnectsPathElements"): wall2 = rel.RelatedElement @@ -297,56 +307,133 @@ class Foo: self.end_points.reverse() builder = ifcopenshell.util.shape_builder.ShapeBuilder(wall.file) - # A wall footprint may be multiple profiles if the wall is split into two due to an ATPATH connection - profiles = [] - split_points = sorted(self.split_points, key=lambda x: x[0][0]) # Sort islands in the +X direction - split_points.insert(0, self.start_points) - split_points.append(self.end_points) - split_points = iter(split_points) - while True: - # Draw each profile as clockwise starting from (minx, miny) - start_split = next(split_points, None) - if not start_split: - break - end_split = next(split_points, None) - if not end_split: - break - maxy_minx = start_split[-1][0] - maxy_maxx = end_split[-1][0] - miny_minx = start_split[0][0] - miny_maxx = end_split[0][0] - # Do more defensive checks here - points = start_split - remaining_path_points = [] - for maxpath_points in self.maxpath_points: - if maxpath_points[0][0] > maxy_minx and maxpath_points[-1][0] < maxy_maxx: - points.extend(maxpath_points) - else: - remaining_path_points.append(maxpath_points) - self.maxpath_points = remaining_path_points - points.extend(end_split[::-1]) - remaining_path_points = [] - for minpath_points in self.minpath_points: - if minpath_points[0][0] < miny_maxx and minpath_points[-1][0] > miny_minx: - points.extend(minpath_points) - else: - remaining_path_points.append(minpath_points) - self.minpath_points = remaining_path_points + if self.is_angled: + start_points = [p.copy() for p in self.start_points] + end_points = [p.copy() for p in self.end_points] + if self.end_offset > 0: + for point in end_points: + point[0] += self.end_offset + if self.start_offset < 0: + for point in start_points: + point[0] += self.start_offset + points = [] + points.extend(start_points) + end_points.reverse() + points.extend(end_points) + item = builder.extrude( + builder.polyline(points, closed=True), + magnitude=self.wall_vectors["d"], + extrusion_vector=self.wall_vectors["z"], + ) - profiles.append(builder.profile(builder.polyline(points, closed=True))) + operands = [] + if not np.allclose(self.start_vector, np.array((0.0, 0.0, 1.0))): + points = self.start_points.copy() + while ifcopenshell.util.shape_builder.is_x(points[0][1], points[1][1]): + points.pop(0) + while ifcopenshell.util.shape_builder.is_x(points[-1][1], points[-2][1]): + points.pop() + newx = min([p[0] for p in points]) - abs(self.start_offset) + p1 = points[-1].copy() + p1[0] = newx + p2 = p1.copy() + p2[1] = points[0][1] + points.extend((p1, p2)) + magnitude = np.linalg.norm(self.start_vector * (self.wall_vectors["h"] / self.start_vector[2])) + operands.append( + builder.extrude( + builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=self.start_vector + ) + ) - for points in self.maxpath_points + self.minpath_points: - profiles.append(builder.profile(builder.polyline(points, closed=True))) + if not np.allclose(self.end_vector, np.array((0.0, 0.0, 1.0))): + points = self.end_points.copy() + while ifcopenshell.util.shape_builder.is_x(points[0][1], points[1][1]): + points.pop(0) + while ifcopenshell.util.shape_builder.is_x(points[-1][1], points[-2][1]): + points.pop() - if len(profiles) > 1: - profile = wall.file.createIfcCompositeProfileDef("AREA", Profiles=profiles) + newx = max([p[0] for p in points]) + abs(self.end_offset) + p1 = points[-1].copy() + p1[0] = newx + p2 = p1.copy() + p2[1] = points[0][1] + points.extend((p1, p2)) + magnitude = np.linalg.norm(self.end_vector * (self.wall_vectors["h"] / self.end_vector[2])) + operands.append( + builder.extrude( + builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=self.end_vector + ) + ) + + for atpath_vector, points in self.atpath_points: + if len(points) <= 2: + continue + magnitude = np.linalg.norm(atpath_vector * (self.wall_vectors["h"] / atpath_vector[2])) + operands.append( + builder.extrude( + builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=atpath_vector + ) + ) + + if operands: + item = ifcopenshell.api.geometry.add_boolean(wall.file, first_item=item, second_items=operands)[-1] else: - profile = profiles[0] + # A wall footprint may be multiple profiles if the wall is split into two due to an ATPATH connection + profiles = [] + split_points = sorted(self.split_points, key=lambda x: x[0][0]) # Sort islands in the +X direction + start_points = [p.copy() for p in self.start_points] + end_points = [p.copy() for p in self.end_points] + split_points.insert(0, start_points) + split_points.append(end_points) + split_points = iter(split_points) - item = builder.extrude( - profile, magnitude=1.0, extrusion_vector=np.array([0.0, sin(self.angle), cos(self.angle)]) - ) + while True: + # Draw each profile as clockwise starting from (minx, miny) + start_split = next(split_points, None) + if not start_split: + break + end_split = next(split_points, None) + if not end_split: + break + maxy_minx = start_split[-1][0] + maxy_maxx = end_split[-1][0] + miny_minx = start_split[0][0] + miny_maxx = end_split[0][0] + # Do more defensive checks here + points = start_split + + remaining_path_points = [] + for maxpath_points in self.maxpath_points: + if maxpath_points[0][0] > maxy_minx and maxpath_points[-1][0] < maxy_maxx: + print("adding maxpath points", maxpath_points) + points.extend(maxpath_points) + else: + remaining_path_points.append(maxpath_points) + self.maxpath_points = remaining_path_points + + points.extend(end_split[::-1]) + + remaining_path_points = [] + for minpath_points in self.minpath_points: + if minpath_points[0][0] < miny_maxx and minpath_points[-1][0] > miny_minx: + points.extend(minpath_points) + else: + remaining_path_points.append(minpath_points) + self.minpath_points = remaining_path_points + + profiles.append(builder.profile(builder.polyline(points, closed=True))) + + for points in self.maxpath_points + self.minpath_points: + profiles.append(builder.profile(builder.polyline(points, closed=True))) + + if len(profiles) > 1: + profile = wall.file.createIfcCompositeProfileDef("AREA", Profiles=profiles) + else: + profile = profiles[0] + + item = builder.extrude(profile, magnitude=self.wall_vectors["d"], extrusion_vector=self.wall_vectors["z"]) rep = builder.get_representation(self.body, items=[item]) if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body): ifcopenshell.util.element.replace_element(old_rep, rep) @@ -371,8 +458,9 @@ class Foo: # axes = self.get_axes(wall2, layers2) reference1 = self.get_reference_line(wall1) reference2 = self.get_reference_line(wall2) - axes1 = self.get_axes(wall1, reference1, layers1, self.angle) - axes2 = self.get_axes(wall2, reference2, layers2, self.get_angle(wall2)) + wall_vectors2 = self.get_wall_vectors(wall2) + axes1 = self.get_axes(wall1, reference1, layers1, self.wall_vectors["a"]) + axes2 = self.get_axes(wall2, reference2, layers2, wall_vectors2["a"]) matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)) matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) print(axes1) @@ -384,6 +472,8 @@ class Foo: axis[1] = (matrix1i @ matrix2 @ np.concatenate((axis[1], (0, 1))))[:2] reference2[0] = (matrix1i @ matrix2 @ np.concatenate((reference2[0], (0, 1))))[:2] reference2[1] = (matrix1i @ matrix2 @ np.concatenate((reference2[1], (0, 1))))[:2] + wall_vectors2["z"] = (matrix1i @ matrix2 @ np.append(wall_vectors2["z"], 0.0))[:3] + wall_vectors2["y"] = (matrix1i @ matrix2 @ np.append(wall_vectors2["y"], 0.0))[:3] # Sort axes from interior to exterior if connection1 == "ATEND": @@ -446,6 +536,8 @@ class Foo: # Categorise our points into a segment that either splits or cuts the wall split_ys = {first_y, last_y} segment = [] + atpath_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.atpath_points.append((atpath_vector, points)) for point in points: segment.append(point) if len(segment) == 1: # Not enough points to categorise the segment @@ -488,9 +580,13 @@ class Foo: if connection1 == "ATSTART": self.start_points = points + self.start_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.start_offset = (self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))[0] self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) elif connection1 == "ATEND": self.end_points = points + self.end_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.end_offset = (self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))[0] self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) else: last_y = axes1[-1][0][1] @@ -532,9 +628,13 @@ class Foo: if connection1 == "ATSTART": self.start_points = points + self.start_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.start_offset = (self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))[0] self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) elif connection1 == "ATEND": self.end_points = points + self.end_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.end_offset = (self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))[0] self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) def get_layers(self, wall) -> list: @@ -581,14 +681,38 @@ class Foo: return [np.array(points[1]), np.array(points[0])] return [np.array((0.0, 0.0)), np.array((1.0, 0.0))] - def get_angle(self, wall): + def get_wall_vectors(self, wall): if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): for item in ifcopenshell.util.representation.resolve_representation(body).Items: + while item.is_a("IfcBooleanResult"): + item = item.FirstOperand if item.is_a("IfcExtrudedAreaSolid"): - return ifcopenshell.util.shape_builder.np_angle_signed( - np.array((0.0, 1.0)), np.array(item.ExtrudedDirection.DirectionRatios[1:]) - ) - return 0.0 + z = np.array(item.ExtrudedDirection.DirectionRatios) + z /= np.linalg.norm(z) + y = np.cross(z, np.array((1.0, 0.0, 0.0))) + d = item.Depth + h = (z * d)[2] + a = ifcopenshell.util.shape_builder.np_angle_signed(np.array((0.0, 1.0)), z[1:]) + if not ifcopenshell.util.shape_builder.is_x(a, 0): + self.is_angled = True + return {"z": z, "y": y, "a": a, "d": d, "h": h} + elif self.fallback_angle: + a = self.fallback_angle + z = np.array([0.0, sin(a), cos(a)]) + y = np.cross(z, np.array((1.0, 0.0, 0.0))) + h = 1.0 # unit scale + d = np.linalg.norm(z * (h / z[2])) + if not ifcopenshell.util.shape_builder.is_x(a, 0): + self.is_angled = True + return {"z": z, "y": y, "a": a, "d": d, "h": h} + # unit scale + return {"z": np.array((0.0, 0.0, 1.0)), "y": np.array((0.0, 1.0, 0.0)), "a": 0.0, "d": 1.0, "h": 1.0} + + def get_join_vector(self, y1, y2): + result = np.cross(y1, y2) + if result[2] < 0: + return result * -1 + return result def get_axes(self, wall, reference, layers: list[PrioritisedLayer], angle: float): axes = [[p.copy() for p in reference]] @@ -605,13 +729,13 @@ class Foo: return axes -test_wall(0, 1, 1, 1, 1) -test_wall(1, 2, 1, 1, 2) -test_wall(2, 2, 1, 1, 1) -test_wall(3, 1, 2, 1, 1) -test_wall(4, 1, 2, 1, 2) -test_wall(5, 3, 1, 2, 4) -test_atpath(7) +test_wall(0, 1, 1, 1, 1, radians(10)) +test_wall(1, 2, 1, 1, 2, radians(10)) +test_wall(2, 2, 1, 1, 1, radians(10)) +test_wall(3, 1, 2, 1, 1, radians(10)) +test_wall(4, 1, 2, 1, 2, radians(10)) +test_wall(5, 3, 1, 2, 4, radians(10)) +test_atpath(7, radians(10)) f.write("/home/dion/wall.ifc") From dfb00f27bbcf03fddaeadfa59caf8c38345adf48 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 14:22:34 +0500 Subject: [PATCH 220/476] Fix georeferencing docs url --- .../ifcopenshell/api/georeference/edit_georeferencing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py index a733c639cf..f896898e05 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py @@ -33,7 +33,7 @@ def edit_georeferencing( surveyor, and a third-party digital engineer with expertise in IFC to moderate. For more information, read the Bonsai documentation for Georeferencing: - https://docs.bonsaibim.org/guides/advanced/georeferencing.html + https://docs.bonsaibim.org/guides/authoring/georeferencing.html For more information about the attributes and data types of an IfcCoordinateOperation, consult the IFC documentation. From 37e131193bc7d5ab2d2098211864b110d3a04984 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 14:34:53 +0500 Subject: [PATCH 221/476] edit_georeferencing - add note on MapUnit data type --- .../ifcopenshell/api/georeference/edit_georeferencing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py index f896898e05..af967e3a79 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py @@ -46,6 +46,8 @@ def edit_georeferencing( :param coordinate_operation: The dictionary of attribute names and values you want to edit. + 'MapUnit' attribute in IFC2X3 should be presented as a full unit name (string), + in other IFC versions it's presented an IfcNamedUnit. :param projected_crs: The IfcProjectedCRS dictionary of attribute names and values you want to edit. From e7866878821e7f56911888179abd33229cae8cd0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 14:38:47 +0500 Subject: [PATCH 222/476] ConvertFileLengthUnits - unify tests, add ifc4x3 tests --- .../test/util/test_unit.py | 93 ++++++++++--------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index 9e554d175b..a8c1c9cced 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -229,7 +229,7 @@ class TestIsAttrType(test.bootstrap.IFC4): assert not subject.is_attr_type(nominal_value, "IfcLengthMeasure", include_select_types=False) -class TestConvertFileLengthUnits(test.bootstrap.IFC4): +class TestConvertFileLengthUnits(test.bootstrap.IFC2X3): def test_run(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") @@ -246,7 +246,12 @@ class TestConvertFileLengthUnits(test.bootstrap.IFC4): ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) output = subject.convert_file_length_units(self.file, target_units="METER") assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" - assert output.by_type("IfcMapConversion")[0].Eastings == 10 + if self.file.schema == "IFC2X3": + parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output) + assert parameters + assert parameters.e == 10 + else: + assert output.by_type("IfcMapConversion")[0].Eastings == 10 def test_preserving_enh_if_there_is_a_map_unit(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") @@ -254,67 +259,67 @@ class TestConvertFileLengthUnits(test.bootstrap.IFC4): meter = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") ifcopenshell.api.context.add_context(self.file, "Model") ifcopenshell.api.georeference.add_georeferencing(self.file) + map_unit = subject.get_full_unit_name(meter) if self.file.schema == "IFC2X3" else meter ifcopenshell.api.georeference.edit_georeferencing( - self.file, projected_crs={"MapUnit": meter}, coordinate_operation={"Eastings": 10, "Scale": 0.001} + self.file, + projected_crs={"MapUnit": map_unit}, + coordinate_operation={"Eastings": 10, "Scale": 0.001}, ) ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) output = subject.convert_file_length_units(self.file, target_units="METER") assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" - assert output.by_type("IfcMapConversion")[0].Eastings == 10 - assert output.by_type("IfcMapConversion")[0].Northings == 0 - assert output.by_type("IfcMapConversion")[0].Scale == 1 - assert subject.get_full_unit_name(output.by_type("IfcProjectedCRS")[0].MapUnit) == "METRE" + if self.file.schema == "IFC2X3": + parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output) + assert parameters + assert parameters.e == 10 + assert parameters.n == 0 + assert parameters.scale == 1 + crs = ifcopenshell.util.element.get_pset(output.by_type("IfcProject")[0], name="ePSet_ProjectedCRS") + assert crs["MapUnit"] == "METRE" + else: + map_conversion = output.by_type("IfcMapConversion")[0] + assert map_conversion.Eastings == 10 + assert map_conversion.Northings == 0 + assert map_conversion.Scale == 1 + assert subject.get_full_unit_name(output.by_type("IfcProjectedCRS")[0].MapUnit) == "METRE" def test_preserving_enh_if_there_is_a_map_unit_which_is_also_the_project_default(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") meter = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") ifcopenshell.api.context.add_context(self.file, "Model") ifcopenshell.api.georeference.add_georeferencing(self.file) + map_unit = subject.get_full_unit_name(meter) if self.file.schema == "IFC2X3" else meter ifcopenshell.api.georeference.edit_georeferencing( - self.file, projected_crs={"MapUnit": meter}, coordinate_operation={"Eastings": 10, "Scale": 1} + self.file, + projected_crs={"MapUnit": map_unit}, + coordinate_operation={"Eastings": 10, "Scale": 1}, ) ifcopenshell.api.unit.assign_unit(self.file, units=[meter]) output = subject.convert_file_length_units(self.file, target_units="MILLIMETER") assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "MILLIMETRE" - assert output.by_type("IfcMapConversion")[0].Eastings == 10 - assert output.by_type("IfcMapConversion")[0].Northings == 0 - assert output.by_type("IfcMapConversion")[0].Scale == 0.001 - assert subject.get_full_unit_name(output.by_type("IfcProjectedCRS")[0].MapUnit) == "METRE" + if self.file.schema == "IFC2X3": + parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output) + assert parameters + assert parameters.e == 10 + assert parameters.n == 0 + assert parameters.scale == 1 + crs = ifcopenshell.util.element.get_pset(output.by_type("IfcProject")[0], name="ePSet_ProjectedCRS") + assert crs["MapUnit"] == "METRE" + else: + map_conversion = output.by_type("IfcMapConversion")[0] + assert map_conversion.Eastings == 10 + assert map_conversion.Northings == 0 + assert map_conversion.Scale == 0.001 + assert subject.get_full_unit_name(output.by_type("IfcProjectedCRS")[0].MapUnit) == "METRE" unit_assignment = subject.get_unit_assignment(output) + assert unit_assignment assert len(unit_assignment.Units) == 1 -class TestConvertFileLengthUnitsIFC2X3(test.bootstrap.IFC2X3): - def test_converting_map_conversion_if_there_is_no_map_unit(self): - ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") - unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") - ifcopenshell.api.context.add_context(self.file, "Model") - ifcopenshell.api.georeference.add_georeferencing(self.file) - ifcopenshell.api.georeference.edit_georeferencing(self.file, coordinate_operation={"Eastings": 10000}) - ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) - output = subject.convert_file_length_units(self.file, target_units="METER") - assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" - parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output) - assert parameters.e == 10 +class TestConvertFileLengthUnitsIFC4(test.bootstrap.IFC4, TestConvertFileLengthUnits): + pass - def test_preserving_enh_if_there_is_a_map_unit(self): - ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") - unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") - meter = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT") - ifcopenshell.api.context.add_context(self.file, "Model") - ifcopenshell.api.georeference.add_georeferencing(self.file) - ifcopenshell.api.georeference.edit_georeferencing( - self.file, - projected_crs={"MapUnit": subject.get_full_unit_name(meter)}, - coordinate_operation={"Eastings": 10, "Scale": 0.001}, - ) - ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) - output = subject.convert_file_length_units(self.file, target_units="METER") - assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" - parameters = ifcopenshell.util.geolocation.get_helmert_transformation_parameters(output) - assert parameters.e == 10 - assert parameters.n == 0 - assert parameters.scale == 1 - crs = ifcopenshell.util.element.get_pset(output.by_type("IfcProject")[0], name="ePSet_ProjectedCRS") - assert crs["MapUnit"] == "METRE" + +class TestConvertFileLengthUnitsIFC4X3(test.bootstrap.IFC4X3, TestConvertFileLengthUnits): + pass From 14eb543662691e827258e22dbd3c7c532937943f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 15:31:50 +0500 Subject: [PATCH 223/476] TestConvertFileLengthUnits - add attributes conversion tests --- .../test/util/test_unit.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index a8c1c9cced..5732c373a2 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -17,11 +17,13 @@ # along with IfcOpenShell. If not, see . import test.bootstrap +import numpy as np import ifcopenshell.api.unit import ifcopenshell.api.root import ifcopenshell.api.georeference import ifcopenshell.util.geolocation import ifcopenshell.util.unit as subject +from ifcopenshell.util.shape_builder import ShapeBuilder from math import pi @@ -237,6 +239,34 @@ class TestConvertFileLengthUnits(test.bootstrap.IFC2X3): output = subject.convert_file_length_units(self.file, target_units="METER") assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" + def test_attribute_conversion(self): + ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") + unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") + + builder = ShapeBuilder(self.file) + rectangle = builder.rectangle((100, 100)) + extrusion = builder.extrude(rectangle, 1000) + + ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) + output = subject.convert_file_length_units(self.file, target_units="METER") + assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" + extrusion = output.by_type("IfcExtrudedAreaSolid")[0] + + # Simple float attribute. + assert extrusion.Depth == 1 + + # List of floats in IFC2X3 and list of lists of floats in IFC4+. + rectangle = extrusion.SweptArea.OuterCurve + expected_points = [(0.0, 0.0), (0.1, 0.0), (0.1, 0.1), (0.0, 0.1)] + if self.file.schema == "IFC2X3": + # IfcPolyline. + points = [p.Coordinates for p in rectangle.Points] + expected_points += expected_points[:1] + else: + # IfcIndexedPolyCurve. + points = rectangle.Points.CoordList + assert np.allclose(points, expected_points) + def test_converting_map_conversion_if_there_is_no_map_unit(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") From 7458040949219f22bf3919f8946f3d161ae58cf9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 15:34:23 +0500 Subject: [PATCH 224/476] Fix ifcpatch unit conversion for lists of ifc entities #6280 --- .../ifcopenshell/util/unit.py | 32 ++++++++++++++++--- .../test/util/test_unit.py | 32 +++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index 47adbc284e..c28595ae5b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -24,6 +24,7 @@ from typing import Iterable from typing import Literal from typing import Optional from typing import Union +from typing import Generator import ifcopenshell import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper @@ -803,9 +804,18 @@ def is_attr_type( return None -def iter_element_and_attributes_per_type( - ifc_file: ifcopenshell.file, attr_type_name: str -) -> Iterable[tuple[ifcopenshell.entity_instance, ifcopenshell_wrapper.attribute, Any]]: +FloatOrSequenceOfFloats = Union[float, tuple["FloatOrSequenceOfFloats", ...]] + + +def iter_element_and_attributes_per_type(ifc_file: ifcopenshell.file, attr_type_name: str) -> Generator[ + tuple[ + ifcopenshell.entity_instance, + ifcopenshell_wrapper.attribute, + Union[FloatOrSequenceOfFloats, ifcopenshell.entity_instance], + ], + None, + None, +]: schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(ifc_file.schema_identifier) for element in ifc_file: @@ -826,6 +836,17 @@ def iter_element_and_attributes_per_type( if isinstance(val, ifcopenshell.entity_instance) and not val.is_a(attr_type_name): continue + elif isinstance(val, tuple): + if not val: + continue + val_ = val[0] + # If it's a tuple of entities, just yield the entities we need to edit. + if isinstance(val_, ifcopenshell.entity_instance): + for val_ in val: + if not val_.is_a(attr_type_name): + continue + yield element, attr, val_ + continue yield element, attr, val @@ -863,9 +884,10 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = " # Traverse all elements and their nested attributes in the file and convert them for element, attr, val in iter_element_and_attributes_per_type(file_patched, "IfcLengthMeasure"): + # NOTE: There is no risk of editing same entities twice as they're all recreated + # after file is reloaded as `file_patched`. if isinstance(val, ifcopenshell.entity_instance): - new_value = convert_value(val.wrappedValue) - getattr(element, attr.name()).wrappedValue = new_value + val.wrappedValue = convert_value(val.wrappedValue) else: new_value = convert_value(val) setattr(element, attr.name(), new_value) diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index 5732c373a2..98c33397a9 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -247,6 +247,32 @@ class TestConvertFileLengthUnits(test.bootstrap.IFC2X3): rectangle = builder.rectangle((100, 100)) extrusion = builder.extrude(rectangle, 1000) + # IfcLengthMeasure entities. + product = ifcopenshell.api.root.create_entity(self.file, "IfcWall") + pset = ifcopenshell.api.pset.add_pset(self.file, product, "TestPset") + # Consider weird case when same entity is reused. + length_measure = self.file.create_entity("IfcLengthMeasure", 50.0) + enum_property = self.file.create_entity( + "IfcPropertyEnumeratedValue", + Name="Enum", + # Not entirely sure if there are real life cases when mixed typed entities used in the list + # but just to be safe. + EnumerationValues=[ + length_measure, + self.file.create_entity("IfcLabel", "TEXT"), + self.file.create_entity("IfcLengthMeasure", 250.0), + length_measure, + ], + ) + ifcopenshell.api.pset.edit_pset( + self.file, + pset, + properties={ + "Length": self.file.create_entity("IfcLengthMeasure", 25.0), + "Enum": enum_property, + }, + ) + ifcopenshell.api.unit.assign_unit(self.file, units=[unit]) output = subject.convert_file_length_units(self.file, target_units="METER") assert subject.get_full_unit_name(subject.get_project_unit(output, "LENGTHUNIT")) == "METRE" @@ -267,6 +293,12 @@ class TestConvertFileLengthUnits(test.bootstrap.IFC2X3): points = rectangle.Points.CoordList assert np.allclose(points, expected_points) + # IfcLengthMeasure entities. + product = output.by_type("IfcWall")[0] + pset_data = ifcopenshell.util.element.get_pset(product, "TestPset") + assert pset_data["Length"] == 0.025 + assert pset_data["Enum"] == [0.05, "TEXT", 0.25, 0.05] + def test_converting_map_conversion_if_there_is_no_map_unit(self): ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcProject") unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT", prefix="MILLI") From 79940641b2c2cf99ec89dd1e72166ac734206fcc Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 16:11:58 +0500 Subject: [PATCH 225/476] typing --- .../bonsai/bim/module/material/operator.py | 3 +++ src/bonsai/bonsai/tool/model.py | 26 ++++++++++++------- .../api/georeference/edit_georeferencing.py | 1 + .../ifcopenshell/api/root/copy_class.py | 24 ++++++++--------- .../ifcopenshell/util/unit.py | 6 ++--- .../test/util/test_unit.py | 3 +++ 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index 9d768351d1..bd515252ee 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -561,9 +561,12 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.file = tool.Ifc.get() active_obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object + assert active_obj props = active_obj.BIMObjectMaterialProperties element = tool.Ifc.get_entity(active_obj) + assert element material = ifcopenshell.util.element.get_material(element) + assert material objects = tool.Blender.get_selected_objects() diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 62cabec1b5..c63a9ba466 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -50,7 +50,7 @@ from bonsai.bim.module.geometry.helper import Helper from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData from bonsai.bim.module.model.opening import FilledOpeningGenerator from ifcopenshell.util.shape_builder import ShapeBuilder -from typing import Optional, Union, TypeVar, Any, Iterable, Literal, TYPE_CHECKING, Sequence +from typing import Optional, Union, TypeVar, Any, Iterable, Literal, TYPE_CHECKING, Sequence, TypedDict T = TypeVar("T") V_ = tool.Blender.V_ @@ -603,8 +603,16 @@ class Model(bonsai.core.tool.Model): if not openings[i].obj: openings.remove(i) + class MaterialLayerParameters(TypedDict): + """Float values are in project units.""" + + layer_set_direction: Literal["AXIS1", "AXIS2", "AXIS3"] + thickness: float + offset: float + direction_sense: Literal["NEGATIVE", "POSITIVE"] + @classmethod - def get_material_layer_parameters(cls, element: ifcopenshell.entity_instance) -> dict[str, Any]: + def get_material_layer_parameters(cls, element: ifcopenshell.entity_instance) -> MaterialLayerParameters: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) layer_set_direction = "AXIS2" offset = 0.0 @@ -619,12 +627,12 @@ class Model(bonsai.core.tool.Model): material = material.ForLayerSet if material.is_a("IfcMaterialLayerSet"): thickness = sum([l.LayerThickness for l in material.MaterialLayers]) * unit_scale - return { - "layer_set_direction": layer_set_direction, - "thickness": thickness, - "offset": offset, - "direction_sense": direction_sense, - } + return cls.MaterialLayerParameters( + layer_set_direction=layer_set_direction, + thickness=thickness, + offset=offset, + direction_sense=direction_sense, + ) @classmethod def get_booleans( @@ -2045,7 +2053,7 @@ class Model(bonsai.core.tool.Model): FilledOpeningGenerator().generate(filling_obj, voided_obj) @classmethod - def add_extrusion_position(cls, extrusion: ifcopenshell.entity_instance, position: tuple) -> None: + def add_extrusion_position(cls, extrusion: ifcopenshell.entity_instance, position: Vector) -> None: ifc_file = tool.Ifc.get() new_position = ifc_file.createIfcAxis2Placement3D( diff --git a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py index af967e3a79..0d4da93bcb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py +++ b/src/ifcopenshell-python/ifcopenshell/api/georeference/edit_georeferencing.py @@ -18,6 +18,7 @@ import ifcopenshell import ifcopenshell.api.pset +import ifcopenshell.util.element from typing import Optional, Any diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py index 6f02a4531d..56100a403c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py @@ -54,9 +54,7 @@ def copy_class(file: ifcopenshell.file, product: ifcopenshell.entity_instance) - connections are still valid. :param product: The IfcProduct to copy. - :type param: ifcopenshell.entity_instance :return: The copied product - :rtype: ifcopenshell.entity_instance Example: @@ -70,26 +68,26 @@ def copy_class(file: ifcopenshell.file, product: ifcopenshell.entity_instance) - """ usecase = Usecase() usecase.file = file - usecase.settings = {"product": product} - return usecase.execute() + return usecase.execute(product) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] - def execute(self): - result = ifcopenshell.util.element.copy(self.file, self.settings["product"]) + def execute(self, product: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + result = ifcopenshell.util.element.copy(self.file, product) self.copy_direct_attributes(result) - self.copy_indirect_attributes(self.settings["product"], result) + self.copy_indirect_attributes(product, result) return result - def copy_direct_attributes(self, to_element): + def copy_direct_attributes(self, to_element: ifcopenshell.entity_instance) -> None: self.remove_representations(to_element) self.copy_object_placements(to_element) self.copy_psets(to_element) - def copy_indirect_attributes(self, from_element, to_element): + def copy_indirect_attributes( + self, from_element: ifcopenshell.entity_instance, to_element: ifcopenshell.entity_instance + ) -> None: for inverse in self.file.get_inverse(from_element): if inverse.is_a("IfcRelDefinesByProperties"): # Properties must not be shared between objects for convenience of authoring @@ -175,13 +173,13 @@ class Usecase: new_value.append(to_element) inverse[i] = new_value - def remove_representations(self, element): + def remove_representations(self, element: ifcopenshell.entity_instance) -> None: if element.is_a("IfcProduct"): element.Representation = None elif element.is_a("IfcTypeProduct"): element.RepresentationMaps = None - def copy_object_placements(self, element): + def copy_object_placements(self, element: ifcopenshell.entity_instance) -> None: if not element.is_a("IfcProduct") or not element.ObjectPlacement: return element.ObjectPlacement = ifcopenshell.util.element.copy(self.file, element.ObjectPlacement) @@ -189,7 +187,7 @@ class Usecase: self.file, element.ObjectPlacement.RelativePlacement ) - def copy_psets(self, element): + def copy_psets(self, element: ifcopenshell.entity_instance) -> None: if not element.is_a("IfcTypeObject") or not element.HasPropertySets: return element.HasPropertySets = [ diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index c28595ae5b..b1862d3882 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -20,7 +20,6 @@ from fractions import Fraction from math import pi from typing import Any from typing import Dict -from typing import Iterable from typing import Literal from typing import Optional from typing import Union @@ -821,7 +820,8 @@ def iter_element_and_attributes_per_type(ifc_file: ifcopenshell.file, attr_type_ for element in ifc_file: entity = schema.declaration_by_name(element.is_a()) attrs = entity.all_attributes() - for attr, val, is_derived in zip(attrs, list(element), entity.derived()): + attrs_derived: tuple[bool, ...] = entity.derived() + for attr, val, is_derived in zip(attrs, list(element), attrs_derived): if is_derived: continue @@ -877,7 +877,7 @@ def convert_file_length_units(ifc_file: ifcopenshell.file, target_units: str = " new_length = ifcopenshell.api.unit.add_conversion_based_unit(file_patched, name=target_units) # support tuple of tuples, as in IfcCartesianPointList3D.CoordList - def convert_value(value): + def convert_value(value: FloatOrSequenceOfFloats) -> FloatOrSequenceOfFloats: if not isinstance(value, tuple): return convert_unit(value, old_length, new_length) return tuple(convert_value(v) for v in value) diff --git a/src/ifcopenshell-python/test/util/test_unit.py b/src/ifcopenshell-python/test/util/test_unit.py index 98c33397a9..b5f1fc537e 100644 --- a/src/ifcopenshell-python/test/util/test_unit.py +++ b/src/ifcopenshell-python/test/util/test_unit.py @@ -18,9 +18,12 @@ import test.bootstrap import numpy as np +import ifcopenshell.api.context import ifcopenshell.api.unit import ifcopenshell.api.root import ifcopenshell.api.georeference +import ifcopenshell.api.pset +import ifcopenshell.util.element import ifcopenshell.util.geolocation import ifcopenshell.util.unit as subject from ifcopenshell.util.shape_builder import ShapeBuilder From b5260d3005cfc64987ef8349e6ebe881a54b0f02 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 16:54:31 +0500 Subject: [PATCH 226/476] copy_class to prevent duplicating profiles #6261 --- src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py index 56100a403c..167680bd78 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/copy_class.py @@ -17,6 +17,7 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.api.material import ifcopenshell.api.root import ifcopenshell.api.system import ifcopenshell.api.geometry @@ -159,9 +160,7 @@ class Usecase: inverse.RelatedObjects = [to_element] elif inverse.is_a("IfcRelAssociatesMaterial") and "Set" in inverse.RelatingMaterial.is_a(): inverse = ifcopenshell.util.element.copy(self.file, inverse) - inverse.RelatingMaterial = ifcopenshell.util.element.copy_deep( - self.file, inverse.RelatingMaterial, exclude=["IfcMaterial"] - ) + inverse.RelatingMaterial = ifcopenshell.api.material.copy_material(self.file, inverse.RelatingMaterial) inverse.RelatedObjects = [to_element] else: for i, value in enumerate(inverse): From 188ca3e40263addbeb7c7f96ff7b7f8a933a05ed Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 17:03:48 +0500 Subject: [PATCH 227/476] Fix bim.profiles_ui_select button in profiles set ui It was using IfcMaterialProfile id instead of IfcProfileDef id, so profile was never selected Example - https://imgur.com/a/Sf1ghPO --- src/bonsai/bonsai/bim/module/material/data.py | 2 ++ src/bonsai/bonsai/bim/module/material/ui.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index a332a89804..b82aeeb5dc 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -274,6 +274,8 @@ class ObjectMaterialData: "icon": icon, "material_id": material_id, } + if item.is_a("IfcMaterialProfile"): + data["profile_id"] = item.Profile.id() if item.is_a("IfcMaterialProfile") and not item.Name: if item.Profile: data["name"] = item.Profile.ProfileName or "Unnamed" diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index eaf4ee06c4..30a7688db5 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -318,7 +318,7 @@ class BIM_PT_object_material(Panel): ): if "Profile" in ObjectMaterialData.data["material_class"]: op = row.operator("bim.profiles_ui_select", icon="ZOOM_SELECTED", text="") - op.profile_id = set_item["id"] + op.profile_id = set_item["profile_id"] op = row.operator("bim.enable_editing_material_set_item_profile", icon="ITALIC", text="") op.material_set_item = set_item["id"] op = row.operator("bim.enable_editing_material_set_item", icon="GREASEPENCIL", text="") From a358cff1b76dfa306dea4594767739566850328a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 18:34:52 +0500 Subject: [PATCH 228/476] Fix extrusion position update on OffsetFromReferenceLine changed #6255 Also fixed an issue when previously there was an offset and now it was set to 0 but slab did not reset it's position. --- src/bonsai/bonsai/bim/module/model/slab.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 996536d8f6..cb6ba498a2 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -283,6 +283,7 @@ class DumbSlabPlaner: if tool.Model.get_usage_type(element) != "LAYER3": return layer_params = tool.Model.get_material_layer_parameters(element) + ifc_file = tool.Ifc.get() body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") obj = tool.Ifc.get_object(element) if not obj: @@ -326,9 +327,18 @@ class DumbSlabPlaner: extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios) extrusion.Depth = perpendicular_depth - if perpendicular_offset != 0.0 and not extrusion.Position: + ifc_position = extrusion.Position + if perpendicular_offset == 0.0: + # Clean up possible previous offset. + if ifc_position: + extrusion.Position = None + ifcopenshell.util.element.remove_deep2(ifc_file, ifc_position) + else: position = offset_direction * perpendicular_offset - tool.Model.add_extrusion_position(extrusion, position) + if ifc_position: + ifc_position.Location.Coordinates = position + else: + tool.Model.add_extrusion_position(extrusion, position) else: props = tool.Model.get_model_props() From a44d0e713abf5dbb4c8fec5b63383751252181a3 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 6 Mar 2025 19:11:43 +0500 Subject: [PATCH 229/476] Bump Blender build and fix dead url --- .github/workflows/ci-bonsai-daily.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-bonsai-daily.yml b/.github/workflows/ci-bonsai-daily.yml index 52f88ea629..d7f59ec18d 100644 --- a/.github/workflows/ci-bonsai-daily.yml +++ b/.github/workflows/ci-bonsai-daily.yml @@ -104,7 +104,7 @@ jobs: # Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo. # Download Blender. - wget -q -O blender.tar.xz https://ftp.nluug.nl/pub/graphics/blender/release/Blender4.3/blender-4.3.0-linux-x64.tar.xz + wget -q -O blender.tar.xz https://download.blender.org/release/Blender4.3/blender-4.3.2-linux-x64.tar.xz tar -xf blender.tar.xz # Setup Blender. From 1271e4b7e33df55321f367563877e3fd367af3c0 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 7 Mar 2025 17:01:14 +1100 Subject: [PATCH 230/476] New IfcPatch recipe to merge identical styles --- src/ifcpatch/ifcpatch/recipes/MergeStyles.py | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/ifcpatch/ifcpatch/recipes/MergeStyles.py diff --git a/src/ifcpatch/ifcpatch/recipes/MergeStyles.py b/src/ifcpatch/ifcpatch/recipes/MergeStyles.py new file mode 100644 index 0000000000..c951ce9db3 --- /dev/null +++ b/src/ifcpatch/ifcpatch/recipes/MergeStyles.py @@ -0,0 +1,52 @@ +# IfcPatch - IFC patching utiliy +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcPatch. +# +# IfcPatch 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. +# +# IfcPatch 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 IfcPatch. If not, see . + +import ifcopenshell +import ifcopenshell.guid + + +class Patcher: + def __init__(self, file, logger): + """Merge identical styles together + + Some software may create an obscene number of styles instead of reusing + them properly. This patch merges all IfcPresentationStyle, + IfcSurfaceStyleShading, and IfcColourRgb if they are identical. + + Example: + + .. code:: python + + ifcpatch.execute({"file": model, "recipe": "MergeStyles", "arguments": []}) + """ + self.file = file + self.logger = logger + + def patch(self): + for ifc_class in ("IfcColourRgb", "IfcSurfaceStyleShading", "IfcPresentationStyle"): + uniques = {} + i = 0 + for element in self.file.by_type(ifc_class): + data = "-".join([str(a) for a in element]) + if (unique := uniques.get(data, None)): + ifcopenshell.util.element.replace_element(element, unique) + self.file.remove(element) + i += 1 + else: + uniques[data] = element + print(f"Replaced {i} {ifc_class}") From a67927ae67dfdb0f33fa81f4e72bae8eb3876c02 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 7 Mar 2025 17:02:05 +1100 Subject: [PATCH 231/476] Automatically merge excessive styles when loading models Came across this problem with some models from 12D --- src/bonsai/bonsai/bim/import_ifc.py | 10 +++++++++- src/bonsai/bonsai/bim/module/project/prop.py | 6 ++++++ src/bonsai/bonsai/bim/module/project/ui.py | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 6fb0dd6fd0..14fa8a43ff 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -20,6 +20,7 @@ from __future__ import annotations import bpy import time import json +import ifcpatch import logging import traceback import mathutils @@ -952,7 +953,12 @@ class IfcImporter: props.collection = self.collections[project.GlobalId] = self.project["blender"] def create_styles(self) -> None: - for style in self.file.by_type("IfcSurfaceStyle"): + styles = self.file.by_type("IfcSurfaceStyle") + if len(styles) > self.ifc_import_settings.style_limit: # Probably something strange happening + print("Warning! Excessive styles were found and merged where applicable.") + ifcpatch.execute({"file": self.file, "recipe": "MergeStyles", "arguments": []}) + styles = self.file.by_type("IfcSurfaceStyle") + for style in styles: self.create_style(style) def create_style(self, style: ifcopenshell.entity_instance) -> None: @@ -1132,6 +1138,7 @@ class IfcImportSettings: self.deflection_tolerance = 0.001 self.angular_tolerance = 0.5 self.void_limit = 30 + self.style_limit = 300 # Locations greater than 1km are not considered "small sites" according to the georeferencing guide # Users can configure this if they have to handle larger sites but beware of surveying precision self.distance_limit = 1000 @@ -1169,6 +1176,7 @@ class IfcImportSettings: settings.deflection_tolerance = props.deflection_tolerance settings.angular_tolerance = props.angular_tolerance settings.void_limit = props.void_limit + settings.style_limit = props.style_limit settings.distance_limit = props.distance_limit settings.false_origin_mode = props.false_origin_mode try: diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index a570724fee..a8ca1f2edd 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -318,6 +318,11 @@ class BIMProjectProperties(PropertyGroup): default=30, description="Maxium number of openings that object can have. If object has more openings, it will be loaded without openings", ) + style_limit: IntProperty( + name="Style Limit", + default=300, + description="Maxium number of styles before styles are automatically merged", + ) distance_limit: FloatProperty(name="Distance Limit", default=1000, subtype="DISTANCE") false_origin_mode: bpy.props.EnumProperty( items=[ @@ -454,6 +459,7 @@ class BIMProjectProperties(PropertyGroup): deflection_tolerance: float angular_tolerance: float void_limit: int + style_limit: int distance_limit: float false_origin_mode: Literal["AUTOMATIC", "MANUAL", "DISABLED"] false_origin: str diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index c5d390c780..237fe75f52 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -213,6 +213,8 @@ class BIM_PT_project(Panel): row = self.layout.row() row.prop(pprops, "void_limit") row = self.layout.row() + row.prop(pprops, "style_limit") + row = self.layout.row() row.prop(pprops, "distance_limit") row = self.layout.row() row.prop(pprops, "false_origin_mode") From 7dee888ecfe63a75c2ec6a6e3cc8054cce1d4521 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 7 Mar 2025 19:00:18 +1100 Subject: [PATCH 232/476] See #1227. New util function in shape builder to do an X axis intersection. --- .../ifcopenshell/util/shape_builder.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 952ebae5ab..27e3574f89 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -249,6 +249,23 @@ def np_intersect_line_line( return point_on_line1, point_on_line2 +def intersect_x_axis_2d(p1: VectorType, p2: VectorType, y=0) -> Optional[float]: + """Intersect a line defined by 2 points to a horizontal line defined by y + + Useful for axis-aligned intersection checks. + + :param p1: First 2D point of the line, order doesn't matter + :param p2: Second 2D point of the line, order doesn't matter + :param y: Intersect at this y value (i.e. defaults to y=0) + """ + x1, y1 = p1 + x2, y2 = p2 + if is_x(y1, y2): # Parallel + return + t = (y - y1) / (y2 - y1) + return x1 + t * (x2 - x1) + + # Note: using ShapeBuilder try not to reuse IFC elements in the process # otherwise you might run into situation where builder.mirror or other operation # is applied twice during one run to the same element From 2b9e3d2a2e0cd3c4451022e61d6965ebefa8865b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 7 Mar 2025 19:03:45 +1100 Subject: [PATCH 233/476] See #1227. Enable layerset slicing. --- src/bonsai/bonsai/tool/loader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index c98f915134..fb55035454 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1017,8 +1017,6 @@ class Loader(bonsai.core.tool.Loader): @classmethod def slice_layerset_mesh(cls, element: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> bpy.types.Mesh: - if True: # This feature is still experimental - return mesh if not (material := ifcopenshell.util.element.get_material(element)): return mesh elif material.is_a("IfcMaterialLayerSetUsage"): From ed722c6f135ffdb09ae109c1afc499db3a2924e8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 7 Mar 2025 19:04:41 +1100 Subject: [PATCH 234/476] See #1227. Refactor out test waldo script into API function. --- src/bonsai/scripts/waldo.py | 506 +-------------- .../ifcopenshell/api/geometry/__init__.py | 2 + .../regenerate_wall_representation.py | 583 ++++++++++++++++++ 3 files changed, 590 insertions(+), 501 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py diff --git a/src/bonsai/scripts/waldo.py b/src/bonsai/scripts/waldo.py index b53ae6b21f..15d8efcaa5 100644 --- a/src/bonsai/scripts/waldo.py +++ b/src/bonsai/scripts/waldo.py @@ -166,9 +166,9 @@ def test_wall(offset, p1, p2, p3, p4, a1=None, a2=None): related_connection="ATSTART", ) - Foo(f, body, axis).regenerate(wall_a, angle=a1) - Foo(f, body, axis).regenerate(wall_b, angle=a1) - Foo(f, body, axis).regenerate(wall_c, angle=a1) + ifcopenshell.api.geometry.regenerate_wall_representation(f, wall=wall_a, angle=a1) + ifcopenshell.api.geometry.regenerate_wall_representation(f, wall=wall_b, angle=a1) + ifcopenshell.api.geometry.regenerate_wall_representation(f, wall=wall_c, angle=a1) def create_type(name, layers): @@ -216,7 +216,7 @@ def test_atpath(offset, angle=None): relating_connection="ATEND", related_connection="ATPATH", ) - Foo(f, body, axis).regenerate(wall, angle=angle) + ifcopenshell.api.geometry.regenerate_wall_representation(f, wall=wall, angle=angle) create_branch("B", 1, 1, 1, 1, 1, -75) create_branch("C", 1, 2, 3, 2, 1, -75) @@ -230,503 +230,7 @@ def test_atpath(offset, angle=None): create_branch("E", 4, 4, 4, 3.5, -1, 75) create_branch("F", 4, 2, 4, 4.5, -1, 75) - Foo(f, body, axis).regenerate(wall_a, angle=angle) - - -PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness") - - -class Foo: - def __init__(self, file, body, axis): - self.file = file - self.body = body - self.axis = axis - self.is_angled = False - - def regenerate(self, wall, angle=None): - print("-" * 100) - print(wall) - self.fallback_angle = angle - layers = self.get_layers(wall) - if not layers: - return - reference = self.get_reference_line(wall) - self.reference_p1, self.reference_p2 = reference - self.wall_vectors = self.get_wall_vectors(wall) - axes = self.get_axes(wall, reference, layers, self.wall_vectors["a"]) - self.miny = axes[0][0][1] - self.maxy = axes[-1][0][1] - self.end_point = None - self.start_points = [] - self.start_vector = np.array((0.0, 0.0, 1.0)) - self.start_offset = 0.0 - self.atpath_points = [] - self.split_points = [] - self.maxpath_points = [] - self.minpath_points = [] - self.end_points = [] - self.end_vector = np.array((0.0, 0.0, 1.0)) - self.end_offset = 0.0 - for rel in wall.ConnectedTo: - if rel.is_a("IfcRelConnectsPathElements"): - wall2 = rel.RelatedElement - layers1 = self.combine_layers(layers.copy(), rel.RelatingPriorities) - layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatedPriorities) - if not layers1 or not layers2: - continue - self.join(wall, wall2, layers1, layers2, rel.RelatingConnectionType, rel.RelatedConnectionType) - - for rel in wall.ConnectedFrom: - if rel.is_a("IfcRelConnectsPathElements"): - wall2 = rel.RelatingElement - layers1 = self.combine_layers(layers.copy(), rel.RelatedPriorities) - layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatingPriorities) - if not layers1 or not layers2: - continue - self.join(wall, wall2, layers1, layers2, rel.RelatedConnectionType, rel.RelatingConnectionType) - - if not self.start_points: - minx = axes[0][0][0] - self.start_points = [ - np.array((minx, axes[0][0][1])), - np.array((minx, axes[-1][0][1])), - ] - if not self.end_points: - maxx = axes[0][1][0] - self.end_points = [ - np.array((maxx, axes[0][0][1])), - np.array((maxx, axes[-1][0][1])), - ] - print("FINISHED") - print(self.start_points) - print(self.end_points) - - if self.start_points[0][1] > self.start_points[-1][1]: # Canonicalise to the +Y direction - self.start_points.reverse() - if self.end_points[0][1] > self.end_points[-1][1]: # Canonicalise to the +Y direction - self.end_points.reverse() - - builder = ifcopenshell.util.shape_builder.ShapeBuilder(wall.file) - - if self.is_angled: - start_points = [p.copy() for p in self.start_points] - end_points = [p.copy() for p in self.end_points] - if self.end_offset > 0: - for point in end_points: - point[0] += self.end_offset - if self.start_offset < 0: - for point in start_points: - point[0] += self.start_offset - points = [] - points.extend(start_points) - end_points.reverse() - points.extend(end_points) - item = builder.extrude( - builder.polyline(points, closed=True), - magnitude=self.wall_vectors["d"], - extrusion_vector=self.wall_vectors["z"], - ) - - operands = [] - if not np.allclose(self.start_vector, np.array((0.0, 0.0, 1.0))): - points = self.start_points.copy() - while ifcopenshell.util.shape_builder.is_x(points[0][1], points[1][1]): - points.pop(0) - while ifcopenshell.util.shape_builder.is_x(points[-1][1], points[-2][1]): - points.pop() - newx = min([p[0] for p in points]) - abs(self.start_offset) - p1 = points[-1].copy() - p1[0] = newx - p2 = p1.copy() - p2[1] = points[0][1] - points.extend((p1, p2)) - magnitude = np.linalg.norm(self.start_vector * (self.wall_vectors["h"] / self.start_vector[2])) - operands.append( - builder.extrude( - builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=self.start_vector - ) - ) - - if not np.allclose(self.end_vector, np.array((0.0, 0.0, 1.0))): - points = self.end_points.copy() - while ifcopenshell.util.shape_builder.is_x(points[0][1], points[1][1]): - points.pop(0) - while ifcopenshell.util.shape_builder.is_x(points[-1][1], points[-2][1]): - points.pop() - - newx = max([p[0] for p in points]) + abs(self.end_offset) - p1 = points[-1].copy() - p1[0] = newx - p2 = p1.copy() - p2[1] = points[0][1] - points.extend((p1, p2)) - magnitude = np.linalg.norm(self.end_vector * (self.wall_vectors["h"] / self.end_vector[2])) - operands.append( - builder.extrude( - builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=self.end_vector - ) - ) - - for atpath_vector, points in self.atpath_points: - if len(points) <= 2: - continue - magnitude = np.linalg.norm(atpath_vector * (self.wall_vectors["h"] / atpath_vector[2])) - operands.append( - builder.extrude( - builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=atpath_vector - ) - ) - - if operands: - item = ifcopenshell.api.geometry.add_boolean(wall.file, first_item=item, second_items=operands)[-1] - else: - # A wall footprint may be multiple profiles if the wall is split into two due to an ATPATH connection - profiles = [] - split_points = sorted(self.split_points, key=lambda x: x[0][0]) # Sort islands in the +X direction - start_points = [p.copy() for p in self.start_points] - end_points = [p.copy() for p in self.end_points] - split_points.insert(0, start_points) - split_points.append(end_points) - split_points = iter(split_points) - - while True: - # Draw each profile as clockwise starting from (minx, miny) - start_split = next(split_points, None) - if not start_split: - break - end_split = next(split_points, None) - if not end_split: - break - maxy_minx = start_split[-1][0] - maxy_maxx = end_split[-1][0] - miny_minx = start_split[0][0] - miny_maxx = end_split[0][0] - # Do more defensive checks here - points = start_split - - remaining_path_points = [] - for maxpath_points in self.maxpath_points: - if maxpath_points[0][0] > maxy_minx and maxpath_points[-1][0] < maxy_maxx: - print("adding maxpath points", maxpath_points) - points.extend(maxpath_points) - else: - remaining_path_points.append(maxpath_points) - self.maxpath_points = remaining_path_points - - points.extend(end_split[::-1]) - - remaining_path_points = [] - for minpath_points in self.minpath_points: - if minpath_points[0][0] < miny_maxx and minpath_points[-1][0] > miny_minx: - points.extend(minpath_points) - else: - remaining_path_points.append(minpath_points) - self.minpath_points = remaining_path_points - - profiles.append(builder.profile(builder.polyline(points, closed=True))) - - for points in self.maxpath_points + self.minpath_points: - profiles.append(builder.profile(builder.polyline(points, closed=True))) - - if len(profiles) > 1: - profile = wall.file.createIfcCompositeProfileDef("AREA", Profiles=profiles) - else: - profile = profiles[0] - - item = builder.extrude(profile, magnitude=self.wall_vectors["d"], extrusion_vector=self.wall_vectors["z"]) - rep = builder.get_representation(self.body, items=[item]) - if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body): - ifcopenshell.util.element.replace_element(old_rep, rep) - else: - ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) - - item = builder.polyline([self.reference_p1, self.reference_p2]) - rep = builder.get_representation(self.axis, items=[item]) - if old_rep := ifcopenshell.util.representation.get_representation(wall, self.axis): - ifcopenshell.util.element.replace_element(old_rep, rep) - else: - ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) - - def join(self, wall1, wall2, layers1, layers2, connection1, connection2): - if connection1 == "NOTDEFINED" or connection2 == "NOTDEFINED": - return - if connection1 == "ATPATH" and connection2 == "ATPATH": - return - print("joining", wall1, layers1, connection1) - print("to", wall2, layers2, connection2) - - # axes = self.get_axes(wall2, layers2) - reference1 = self.get_reference_line(wall1) - reference2 = self.get_reference_line(wall2) - wall_vectors2 = self.get_wall_vectors(wall2) - axes1 = self.get_axes(wall1, reference1, layers1, self.wall_vectors["a"]) - axes2 = self.get_axes(wall2, reference2, layers2, wall_vectors2["a"]) - matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)) - matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) - print(axes1) - print(axes2) - - # Convert wall2 data to wall1 local coordinates - for axis in axes2: - axis[0] = (matrix1i @ matrix2 @ np.concatenate((axis[0], (0, 1))))[:2] - axis[1] = (matrix1i @ matrix2 @ np.concatenate((axis[1], (0, 1))))[:2] - reference2[0] = (matrix1i @ matrix2 @ np.concatenate((reference2[0], (0, 1))))[:2] - reference2[1] = (matrix1i @ matrix2 @ np.concatenate((reference2[1], (0, 1))))[:2] - wall_vectors2["z"] = (matrix1i @ matrix2 @ np.append(wall_vectors2["z"], 0.0))[:3] - wall_vectors2["y"] = (matrix1i @ matrix2 @ np.append(wall_vectors2["y"], 0.0))[:3] - - # Sort axes from interior to exterior - if connection1 == "ATEND": - if axes2[0][0][0] > axes2[-1][0][0]: # We process layers in a +X direction - axes2 = list(reversed(axes2)) - layers2 = list(reversed(layers2)) - elif connection1 == "ATSTART": - if axes2[-1][0][0] > axes2[0][0][0]: # We process layers in a -X direction - axes2 = list(reversed(axes2)) - layers2 = list(reversed(layers2)) - - # wall2_x = matrix2[:,0][:2] - axis2 = axes2[0] # Take an arbitrary axis - if connection2 == "ATSTART": - axis2 = [axis2[1], axis2[0]] # Flip direction so the axis "points" in the direction of join - if axis2[0][1] < axis2[1][1]: # Pointing +Y - if axes1[-1][0][1] < axes1[0][0][1]: # We process layers1 in a +Y direction - axes1 = list(reversed(axes1)) - layers1 = list(reversed(layers1)) - else: # Pointing -Y - if axes1[0][0][1] < axes1[-1][0][1]: # We process layers1 in a -Y direction - axes1 = list(reversed(axes1)) - layers1 = list(reversed(layers1)) - - print("modified") - print(axes1) - print(axes2) - # Checked - if connection1 == "ATPATH": - first_axis2 = axes2[0] - last_axis2 = axes2[-1] - first_y = axes1[0][0][1] - last_y = axes1[-1][0][1] - p0 = np.array((self.intersect_axis(*first_axis2, y=first_y), first_y)) - pN = np.array((self.intersect_axis(*last_axis2, y=first_y), first_y)) - - # Generate CurveOnRelating/RelatedElement - points = [p0] - axes2 = iter(axes2) - axis2 = next(axes2) - for layer2 in layers2: - ys = iter([a[0][1] for a in axes1]) - y = next(ys) - for layer1 in layers1: - if layer2.priority <= layer1.priority: - break - y = next(ys) - p1 = np.array((self.intersect_axis(*axis2, y=y), y)) - axis2 = next(axes2) - p2 = np.array((self.intersect_axis(*axis2, y=y), y)) - if points and np.allclose(points[-1], p1): - points[-1] = p2 # Just slide along previous point - else: - points.extend((p1, p2)) - - # The curve must end at pN - if not np.allclose(points[-1], pN): - points.append(pN) - - # Categorise our points into a segment that either splits or cuts the wall - split_ys = {first_y, last_y} - segment = [] - atpath_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) - self.atpath_points.append((atpath_vector, points)) - for point in points: - segment.append(point) - if len(segment) == 1: # Not enough points to categorise the segment - continue - elif {segment[0][1], segment[-1][1]} == split_ys: # This segment splits the wall - if segment[0][1] > segment[-1][1]: # Go in the +Y direction - segment.reverse() - self.split_points.append(segment) - segment = [] - elif segment[0][1] == segment[-1][1]: # This segment cuts some of the wall - if segment[0][1] == self.maxy: # Go in the +X direction - if segment[0][0] > segment[-1][0]: - segment.reverse() - self.maxpath_points.append(segment) - elif segment[0][1] == self.miny: # Go in the -X direction - if segment[-1][0] > segment[0][0]: - segment.reverse() - self.minpath_points.append(segment) - segment = [] - elif connection2 == "ATPATH": - points = [] - ys = iter([a[0][1] for a in axes1]) - y = next(ys) - for layer1 in layers1: - axes2_iter = iter(axes2) - axis2 = next(axes2_iter) - for layer2 in layers2: - if layer1.priority <= layer2.priority: - break - axis2 = next(axes2_iter) - x = self.intersect_axis(*axis2, y=y) - p1 = np.array((x, y)) - y = next(ys) - x = self.intersect_axis(*axis2, y=y) - p2 = np.array((x, y)) - if points and np.allclose(points[-1], p1): - points.append(p2) - else: - points.extend((p1, p2)) - - if connection1 == "ATSTART": - self.start_points = points - self.start_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) - self.start_offset = (self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))[0] - self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) - elif connection1 == "ATEND": - self.end_points = points - self.end_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) - self.end_offset = (self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))[0] - self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) - else: - last_y = axes1[-1][0][1] - ys = iter([a[0][1] for a in axes1]) - - last_axis2 = axes2[-1] - axes2 = iter(axes2) - axis2 = next(axes2) - y = next(ys) - x = self.intersect_axis(*axis2, y=y) - points = [np.array((x, y))] - - layers1 = iter(layers1) - layers2 = iter(layers2) - layer1 = next(layers1, None) - layer2 = next(layers2, None) - - # This creates "mitering" behaviour which is an ambiguity by bSI. - while layer1 and layer2: - print("considering", layer1, layer2) - if layer1.priority > layer2.priority: - axis2 = next(axes2) - x = self.intersect_axis(*axis2, y=y) - layer2 = next(layers2, None) - elif layer2.priority > layer1.priority: - y = next(ys) - x = self.intersect_axis(*axis2, y=y) - layer1 = next(layers1, None) - else: - y = next(ys) - x = self.intersect_axis(*next(axes2), y=y) - layer1 = next(layers1, None) - layer2 = next(layers2, None) - points.append(np.array((x, y))) - - print("points", points) - if points[-1][1] != last_y: - points.append(np.array((self.intersect_axis(*last_axis2, y=last_y), last_y))) - - if connection1 == "ATSTART": - self.start_points = points - self.start_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) - self.start_offset = (self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))[0] - self.reference_p1[0] = self.intersect_axis(*reference2, y=reference1[0][1]) - elif connection1 == "ATEND": - self.end_points = points - self.end_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) - self.end_offset = (self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))[0] - self.reference_p2[0] = self.intersect_axis(*reference2, y=reference1[0][1]) - - def get_layers(self, wall) -> list: - material = ifcopenshell.util.element.get_material(wall, should_skip_usage=True) - if not material or not material.is_a("IfcMaterialLayerSet"): - return [] - return [PrioritisedLayer(l.Priority or 0, l.LayerThickness) for l in material.MaterialLayers] - - def combine_layers(self, layers, override_priorities): - results = [] - if override_priorities: - for i, priority in enumerate(override_priorities[: len(layers)]): - layers[i][0] = priority - if not layers: - return [] - results = [layers.pop(0)] - for layer in layers: - if not layer.thickness: - continue - if layer.priority == results[-1].priority: - results[-1] = PrioritisedLayer(layer.priority, results[-1].thickness + layer.thickness) - else: - results.append(layer) - return results - - def intersect_axis(self, p1, p2, y=0): - # Assumes lines are horizontal - x1, y1 = p1 - x2, y2 = p2 - t = (y - y1) / (y2 - y1) - return x1 + t * (x2 - x1) - - def get_reference_line(self, wall): - if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"): - for item in ifcopenshell.util.representation.resolve_representation(axis).Items: - if item.is_a("IfcPolyline"): - points = item.Points - elif item.is_a("IfcIndexedPolyCurve"): - points = item.Points.CoordList - else: - continue - if points[0][0] < points[1][0]: # An axis always goes in the +X direction - return [np.array(points[0]), np.array(points[1])] - return [np.array(points[1]), np.array(points[0])] - return [np.array((0.0, 0.0)), np.array((1.0, 0.0))] - - def get_wall_vectors(self, wall): - if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): - for item in ifcopenshell.util.representation.resolve_representation(body).Items: - while item.is_a("IfcBooleanResult"): - item = item.FirstOperand - if item.is_a("IfcExtrudedAreaSolid"): - z = np.array(item.ExtrudedDirection.DirectionRatios) - z /= np.linalg.norm(z) - y = np.cross(z, np.array((1.0, 0.0, 0.0))) - d = item.Depth - h = (z * d)[2] - a = ifcopenshell.util.shape_builder.np_angle_signed(np.array((0.0, 1.0)), z[1:]) - if not ifcopenshell.util.shape_builder.is_x(a, 0): - self.is_angled = True - return {"z": z, "y": y, "a": a, "d": d, "h": h} - elif self.fallback_angle: - a = self.fallback_angle - z = np.array([0.0, sin(a), cos(a)]) - y = np.cross(z, np.array((1.0, 0.0, 0.0))) - h = 1.0 # unit scale - d = np.linalg.norm(z * (h / z[2])) - if not ifcopenshell.util.shape_builder.is_x(a, 0): - self.is_angled = True - return {"z": z, "y": y, "a": a, "d": d, "h": h} - # unit scale - return {"z": np.array((0.0, 0.0, 1.0)), "y": np.array((0.0, 1.0, 0.0)), "a": 0.0, "d": 1.0, "h": 1.0} - - def get_join_vector(self, y1, y2): - result = np.cross(y1, y2) - if result[2] < 0: - return result * -1 - return result - - def get_axes(self, wall, reference, layers: list[PrioritisedLayer], angle: float): - axes = [[p.copy() for p in reference]] - # Apply usage to convert the Reference line into MlsBase - sense_factor = 1 - if (usage := ifcopenshell.util.element.get_material(wall)) and usage.is_a("IfcMaterialLayerSetUage"): - for point in axes[0]: - point[1] += usage.OffsetFromReferenceLine - sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 - - for layer in layers: - y_offset = (layer.thickness * sense_factor) / cos(angle) - axes.append([p.copy() + np.array((0.0, y_offset)) for p in axes[-1]]) - return axes + ifcopenshell.api.geometry.regenerate_wall_representation(f, wall=wall_a, angle=angle) test_wall(0, 1, 1, 1, 1, radians(10)) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py index 49bfe5f758..3dea99d8d2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py @@ -48,6 +48,7 @@ from .disconnect_element import disconnect_element from .disconnect_path import disconnect_path from .edit_object_placement import edit_object_placement from .map_representation import map_representation +from .regenerate_wall_representation import regenerate_wall_representation from .remove_boolean import remove_boolean from .remove_representation import remove_representation from .unassign_representation import unassign_representation @@ -76,6 +77,7 @@ __all__ = [ "disconnect_path", "edit_object_placement", "map_representation", + "regenerate_wall_representation", "remove_boolean", "remove_representation", "unassign_representation", diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py new file mode 100644 index 0000000000..c5ffbe5998 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py @@ -0,0 +1,583 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Dion Moult +# +# 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 . + +import numpy as np +import ifcopenshell +import ifcopenshell.api.geometry +import ifcopenshell.util.shape_builder +import ifcopenshell.util.element +import ifcopenshell.util.unit +from collections import namedtuple +from math import sin, cos +from typing import Optional + +# https://stackoverflow.com/a/9184560/9627415 +# Possible optimisation to linalg.norm? + +PrioritisedLayer = namedtuple("PrioritisedLayer", "priority thickness") + + +def regenerate_wall_representation( + file: ifcopenshell.file, + wall: ifcopenshell.entity_instance, + length: float = 1.0, + height: float = 1.0, + angle: Optional[float] = None, +) -> ifcopenshell.entity_instance: + """ + Regenerate the body representation of a wall taking into account connections. + + IFC defines how a standard (case) wall should behave that has a material + layer set and connections to other walls using IfcRelConnectsPathElements. + This function will regenerate the body geometry of a wall taking into + account the notches, butts, mitres, etc in the wall due to connections with + other walls. + + A standard wall has a 2D axis line as well as parameters defined in terms + of layer thicknesses and priorities. The body geometry is defined as a 2D + XY profile which is extruded in the +Z direction. For this function to + work, a wall must have these defined and the project must have an axis and + body representation context. + + For non-sloped walls, a 2D profile is generated and extruded in the +Z + direction. The profile may be a composite profile, if the wall is split due + to wall joins along the path of the wall that protrude all the way through + the wall. + + For sloped walls, a basic rectangular 2D profile is extruded, and then + additional extrusions are generated for each connection that boolean + difference the base extrusion. + + :param wall: The IfcWall for the representation, + only Model/Body/MODEL_VIEW type of representations are currently supported. + :param length: If the wall doesn't have an axis length, this is the default + length in SI units. + :param height: If the wall doesn't already have a height, this is the + default height in SI units. + :param angle: If the wall doesn't already have a slope, this is the default + angle in radians. Left as none or 0 defines no slope. + :return: The newly generated body IfcShapeRepresentation + """ + return Regenerator(file).regenerate(wall, length=length, height=height, angle=angle) + + +class Regenerator: + def __init__(self, file): + self.file = file + self.body = ifcopenshell.util.representation.get_context(file, "Model", "Body", "MODEL_VIEW") + self.axis = ifcopenshell.util.representation.get_context(file, "Plan", "Axis", "GRAPH_VIEW") + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) + self.is_angled = False + + def regenerate(self, wall, length=1.0, height=1.0, angle=None): + print("-" * 100) + print(wall) + self.fallback_length = length / self.unit_scale + self.fallback_height = height / self.unit_scale + self.fallback_angle = angle + layers = self.get_layers(wall) + if not layers: + return + reference = self.get_reference_line(wall) + self.reference_p1, self.reference_p2 = reference + self.wall_vectors = self.get_wall_vectors(wall) + axes = self.get_axes(wall, reference, layers, self.wall_vectors["a"]) + self.miny = axes[0][0][1] + self.maxy = axes[-1][0][1] + self.end_point = None + self.start_points = [] + self.start_vector = np.array((0.0, 0.0, 1.0)) + self.start_offset = 0.0 + self.atpath_points = [] + self.split_points = [] + self.maxpath_points = [] + self.minpath_points = [] + self.end_points = [] + self.end_vector = np.array((0.0, 0.0, 1.0)) + self.end_offset = 0.0 + for rel in wall.ConnectedTo: + if rel.is_a("IfcRelConnectsPathElements"): + wall2 = rel.RelatedElement + layers1 = self.combine_layers(layers.copy(), rel.RelatingPriorities) + layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatedPriorities) + if not layers1 or not layers2: + continue + self.join(wall, wall2, layers1, layers2, rel.RelatingConnectionType, rel.RelatedConnectionType) + + for rel in wall.ConnectedFrom: + if rel.is_a("IfcRelConnectsPathElements"): + wall2 = rel.RelatingElement + layers1 = self.combine_layers(layers.copy(), rel.RelatedPriorities) + layers2 = self.combine_layers(self.get_layers(wall2), rel.RelatingPriorities) + if not layers1 or not layers2: + continue + self.join(wall, wall2, layers1, layers2, rel.RelatedConnectionType, rel.RelatingConnectionType) + + if not self.start_points: + minx = axes[0][0][0] + self.start_points = [ + np.array((minx, axes[0][0][1])), + np.array((minx, axes[-1][0][1])), + ] + if not self.end_points: + maxx = axes[0][1][0] + self.end_points = [ + np.array((maxx, axes[0][0][1])), + np.array((maxx, axes[-1][0][1])), + ] + print("FINISHED") + print(self.start_points) + print(self.end_points) + + if self.start_points[0][1] > self.start_points[-1][1]: # Canonicalise to the +Y direction + self.start_points.reverse() + if self.end_points[0][1] > self.end_points[-1][1]: # Canonicalise to the +Y direction + self.end_points.reverse() + + builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file) + + if self.is_angled: + start_points = [p.copy() for p in self.start_points] + end_points = [p.copy() for p in self.end_points] + if self.end_offset > 0: + for point in end_points: + point[0] += self.end_offset + if self.start_offset < 0: + for point in start_points: + point[0] += self.start_offset + points = [] + points.extend(start_points) + end_points.reverse() + points.extend(end_points) + item = builder.extrude( + builder.polyline(points, closed=True), + magnitude=self.wall_vectors["d"], + extrusion_vector=self.wall_vectors["z"], + ) + + operands = [] + if not np.allclose(self.start_vector, np.array((0.0, 0.0, 1.0))): + points = self.start_points.copy() + while ifcopenshell.util.shape_builder.is_x(points[0][1], points[1][1]): + points.pop(0) + while ifcopenshell.util.shape_builder.is_x(points[-1][1], points[-2][1]): + points.pop() + newx = min([p[0] for p in points]) - abs(self.start_offset) + p1 = points[-1].copy() + p1[0] = newx + p2 = p1.copy() + p2[1] = points[0][1] + points.extend((p1, p2)) + magnitude = np.linalg.norm(self.start_vector * (self.wall_vectors["h"] / self.start_vector[2])) + operands.append( + builder.extrude( + builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=self.start_vector + ) + ) + + if not np.allclose(self.end_vector, np.array((0.0, 0.0, 1.0))): + points = self.end_points.copy() + while ifcopenshell.util.shape_builder.is_x(points[0][1], points[1][1]): + points.pop(0) + while ifcopenshell.util.shape_builder.is_x(points[-1][1], points[-2][1]): + points.pop() + + newx = max([p[0] for p in points]) + abs(self.end_offset) + p1 = points[-1].copy() + p1[0] = newx + p2 = p1.copy() + p2[1] = points[0][1] + points.extend((p1, p2)) + magnitude = np.linalg.norm(self.end_vector * (self.wall_vectors["h"] / self.end_vector[2])) + operands.append( + builder.extrude( + builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=self.end_vector + ) + ) + + for atpath_vector, points in self.atpath_points: + if len(points) <= 2: + continue + magnitude = np.linalg.norm(atpath_vector * (self.wall_vectors["h"] / atpath_vector[2])) + operands.append( + builder.extrude( + builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=atpath_vector + ) + ) + + if operands: + item = ifcopenshell.api.geometry.add_boolean(self.file, first_item=item, second_items=operands)[-1] + else: + # A wall footprint may be multiple profiles if the wall is split into two due to an ATPATH connection + profiles = [] + split_points = sorted(self.split_points, key=lambda x: x[0][0]) # Sort islands in the +X direction + start_points = [p.copy() for p in self.start_points] + end_points = [p.copy() for p in self.end_points] + split_points.insert(0, start_points) + split_points.append(end_points) + split_points = iter(split_points) + + while True: + # Draw each profile as clockwise starting from (minx, miny) + start_split = next(split_points, None) + if not start_split: + break + end_split = next(split_points, None) + if not end_split: + break + maxy_minx = start_split[-1][0] + maxy_maxx = end_split[-1][0] + miny_minx = start_split[0][0] + miny_maxx = end_split[0][0] + # Do more defensive checks here + points = start_split + + remaining_path_points = [] + for maxpath_points in self.maxpath_points: + if maxpath_points[0][0] > maxy_minx and maxpath_points[-1][0] < maxy_maxx: + points.extend(maxpath_points) + else: + remaining_path_points.append(maxpath_points) + self.maxpath_points = remaining_path_points + + points.extend(end_split[::-1]) + + remaining_path_points = [] + for minpath_points in self.minpath_points: + if minpath_points[0][0] < miny_maxx and minpath_points[-1][0] > miny_minx: + points.extend(minpath_points) + else: + remaining_path_points.append(minpath_points) + self.minpath_points = remaining_path_points + + profiles.append(builder.profile(builder.polyline(points, closed=True))) + + for points in self.maxpath_points + self.minpath_points: + profiles.append(builder.profile(builder.polyline(points, closed=True))) + + if len(profiles) > 1: + profile = self.file.createIfcCompositeProfileDef("AREA", Profiles=profiles) + else: + profile = profiles[0] + + item = builder.extrude(profile, magnitude=self.wall_vectors["d"], extrusion_vector=self.wall_vectors["z"]) + body_rep = builder.get_representation(self.body, items=[item]) + if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body): + ifcopenshell.util.element.replace_element(old_rep, body_rep) + ifcopenshell.util.element.remove_deep2(self.file, old_rep) + else: + ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=body_rep) + + item = builder.polyline([self.reference_p1, self.reference_p2]) + axis_rep = builder.get_representation(self.axis, items=[item]) + if old_rep := ifcopenshell.util.representation.get_representation(wall, self.axis): + ifcopenshell.util.element.replace_element(old_rep, axis_rep) + ifcopenshell.util.element.remove_deep2(self.file, old_rep) + else: + ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=axis_rep) + return body_rep + + def join(self, wall1, wall2, layers1, layers2, connection1, connection2): + if connection1 == "NOTDEFINED" or connection2 == "NOTDEFINED": + return + if connection1 == "ATPATH" and connection2 == "ATPATH": + return + print("joining", wall1, layers1, connection1) + print("to", wall2, layers2, connection2) + + reference1 = self.get_reference_line(wall1) + reference2 = self.get_reference_line(wall2) + wall_vectors2 = self.get_wall_vectors(wall2) + axes1 = self.get_axes(wall1, reference1, layers1, self.wall_vectors["a"]) + axes2 = self.get_axes(wall2, reference2, layers2, wall_vectors2["a"]) + matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)) + matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) + print(axes1) + print(axes2) + + # Convert wall2 data to wall1 local coordinates + for axis in axes2: + axis[0] = (matrix1i @ matrix2 @ np.concatenate((axis[0], (0, 1))))[:2] + axis[1] = (matrix1i @ matrix2 @ np.concatenate((axis[1], (0, 1))))[:2] + reference2[0] = (matrix1i @ matrix2 @ np.concatenate((reference2[0], (0, 1))))[:2] + reference2[1] = (matrix1i @ matrix2 @ np.concatenate((reference2[1], (0, 1))))[:2] + wall_vectors2["z"] = (matrix1i @ matrix2 @ np.append(wall_vectors2["z"], 0.0))[:3] + wall_vectors2["y"] = (matrix1i @ matrix2 @ np.append(wall_vectors2["y"], 0.0))[:3] + + axis2 = axes2[0] # Take an arbitrary axis of wall2 + if ifcopenshell.util.shape_builder.is_x(axis2[0][1], axis2[1][1]): + return # Parallel + + # Sort axes from interior to exterior + if connection1 == "ATEND": + if axes2[0][0][0] > axes2[-1][0][0]: # We process layers in a +X direction + axes2 = list(reversed(axes2)) + layers2 = list(reversed(layers2)) + elif connection1 == "ATSTART": + if axes2[-1][0][0] > axes2[0][0][0]: # We process layers in a -X direction + axes2 = list(reversed(axes2)) + layers2 = list(reversed(layers2)) + + axis2 = axes2[0] # Take an arbitrary axis of wall2 + if connection2 == "ATSTART": + axis2 = [axis2[1], axis2[0]] # Flip direction so the axis "points" in the direction of join + if axis2[0][1] < axis2[1][1]: # Pointing +Y + if axes1[-1][0][1] < axes1[0][0][1]: # We process layers1 in a +Y direction + axes1 = list(reversed(axes1)) + layers1 = list(reversed(layers1)) + else: # Pointing -Y + if axes1[0][0][1] < axes1[-1][0][1]: # We process layers1 in a -Y direction + axes1 = list(reversed(axes1)) + layers1 = list(reversed(layers1)) + + print("modified") + print(axes1) + print(axes2) + if connection1 == "ATPATH": + first_axis2 = axes2[0] + last_axis2 = axes2[-1] + first_y = axes1[0][0][1] + last_y = axes1[-1][0][1] + p0 = np.array((ifcopenshell.util.shape_builder.intersect_x_axis_2d(*first_axis2, y=first_y), first_y)) + pN = np.array((ifcopenshell.util.shape_builder.intersect_x_axis_2d(*last_axis2, y=first_y), first_y)) + + # Generate CurveOnRelating/RelatedElement + points = [p0] + axes2 = iter(axes2) + axis2 = next(axes2) + for layer2 in layers2: + ys = iter([a[0][1] for a in axes1]) + y = next(ys) + for layer1 in layers1: + if layer2.priority <= layer1.priority: + break + y = next(ys) + p1 = np.array((ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y), y)) + axis2 = next(axes2) + p2 = np.array((ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y), y)) + if points and np.allclose(points[-1], p1): + points[-1] = p2 # Just slide along previous point + else: + points.extend((p1, p2)) + + # The curve must end at pN + if not np.allclose(points[-1], pN): + points.append(pN) + + # Categorise our points into a segment that either splits or cuts the wall + split_ys = {first_y, last_y} + segment = [] + atpath_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.atpath_points.append((atpath_vector, points)) + for point in points: + segment.append(point) + if len(segment) == 1: # Not enough points to categorise the segment + continue + elif {segment[0][1], segment[-1][1]} == split_ys: # This segment splits the wall + if segment[0][1] > segment[-1][1]: # Go in the +Y direction + segment.reverse() + self.split_points.append(segment) + segment = [] + elif segment[0][1] == segment[-1][1]: # This segment cuts some of the wall + if segment[0][1] == self.maxy: # Go in the +X direction + if segment[0][0] > segment[-1][0]: + segment.reverse() + self.maxpath_points.append(segment) + elif segment[0][1] == self.miny: # Go in the -X direction + if segment[-1][0] > segment[0][0]: + segment.reverse() + self.minpath_points.append(segment) + segment = [] + elif connection2 == "ATPATH": + points = [] + ys = iter([a[0][1] for a in axes1]) + y = next(ys) + for layer1 in layers1: + axes2_iter = iter(axes2) + axis2 = next(axes2_iter) + for layer2 in layers2: + if layer1.priority <= layer2.priority: + break + axis2 = next(axes2_iter) + x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y) + p1 = np.array((x, y)) + y = next(ys) + x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y) + p2 = np.array((x, y)) + if points and np.allclose(points[-1], p1): + points.append(p2) + else: + points.extend((p1, p2)) + + if connection1 == "ATSTART": + self.start_points = points + self.start_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.start_offset = (self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))[0] + self.reference_p1[0] = ifcopenshell.util.shape_builder.intersect_x_axis_2d( + *reference2, y=reference1[0][1] + ) + elif connection1 == "ATEND": + self.end_points = points + self.end_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.end_offset = (self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))[0] + self.reference_p2[0] = ifcopenshell.util.shape_builder.intersect_x_axis_2d( + *reference2, y=reference1[0][1] + ) + else: # A connection at either end of both walls + last_y = axes1[-1][0][1] + ys = iter([a[0][1] for a in axes1]) + + last_axis2 = axes2[-1] + axes2 = iter(axes2) + axis2 = next(axes2) + y = next(ys) + x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y) + points = [np.array((x, y))] + + layers1 = iter(layers1) + layers2 = iter(layers2) + layer1 = next(layers1, None) + layer2 = next(layers2, None) + + # This creates "mitering" behaviour which is an ambiguity by bSI. + while layer1 and layer2: + print("considering", layer1, layer2) + if layer1.priority > layer2.priority: + axis2 = next(axes2) + x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y) + layer2 = next(layers2, None) + elif layer2.priority > layer1.priority: + y = next(ys) + x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y) + layer1 = next(layers1, None) + else: + y = next(ys) + x = ifcopenshell.util.shape_builder.intersect_x_axis_2d(*next(axes2), y=y) + layer1 = next(layers1, None) + layer2 = next(layers2, None) + points.append(np.array((x, y))) + + print("points", points) + if points[-1][1] != last_y: + points.append( + np.array((ifcopenshell.util.shape_builder.intersect_x_axis_2d(*last_axis2, y=last_y), last_y)) + ) + + if connection1 == "ATSTART": + self.start_points = points + self.start_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.start_offset = (self.start_vector * (self.wall_vectors["h"] / self.start_vector[2]))[0] + self.reference_p1[0] = ifcopenshell.util.shape_builder.intersect_x_axis_2d( + *reference2, y=reference1[0][1] + ) + elif connection1 == "ATEND": + self.end_points = points + self.end_vector = self.get_join_vector(self.wall_vectors["y"], wall_vectors2["y"]) + self.end_offset = (self.end_vector * (self.wall_vectors["h"] / self.end_vector[2]))[0] + self.reference_p2[0] = ifcopenshell.util.shape_builder.intersect_x_axis_2d( + *reference2, y=reference1[0][1] + ) + + def get_layers(self, wall) -> list: + material = ifcopenshell.util.element.get_material(wall, should_skip_usage=True) + if not material or not material.is_a("IfcMaterialLayerSet"): + return [] + return [PrioritisedLayer(l.Priority or 0, l.LayerThickness) for l in material.MaterialLayers] + + def combine_layers(self, layers, override_priorities): + results = [] + if override_priorities: + for i, priority in enumerate(override_priorities[: len(layers)]): + layers[i][0] = priority + if not layers: + return [] + results = [layers.pop(0)] + for layer in layers: + if not layer.thickness: + continue + if layer.priority == results[-1].priority: + results[-1] = PrioritisedLayer(layer.priority, results[-1].thickness + layer.thickness) + else: + results.append(layer) + return results + + def get_reference_line(self, wall): + if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"): + for item in ifcopenshell.util.representation.resolve_representation(axis).Items: + if item.is_a("IfcPolyline"): + points = item.Points + elif item.is_a("IfcIndexedPolyCurve"): + points = item.Points.CoordList + else: + continue + if points[0][0] < points[1][0]: # An axis always goes in the +X direction + return [np.array(points[0]), np.array(points[1])] + return [np.array(points[1]), np.array(points[0])] + return [np.array((0.0, 0.0)), np.array((self.fallback_length, 0.0))] + + def get_wall_vectors(self, wall): + if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): + for item in ifcopenshell.util.representation.resolve_representation(body).Items: + while item.is_a("IfcBooleanResult"): + item = item.FirstOperand + if item.is_a("IfcExtrudedAreaSolid"): + z = np.array(item.ExtrudedDirection.DirectionRatios) + z /= np.linalg.norm(z) + y = np.cross(z, np.array((1.0, 0.0, 0.0))) + d = item.Depth + h = (z * d)[2] + a = ifcopenshell.util.shape_builder.np_angle_signed(np.array((0.0, 1.0)), z[1:]) + if not ifcopenshell.util.shape_builder.is_x(a, 0): + self.is_angled = True + return {"z": z, "y": y, "a": a, "d": d, "h": h} + elif self.fallback_angle: + a = self.fallback_angle + z = np.array([0.0, sin(a), cos(a)]) + y = np.cross(z, np.array((1.0, 0.0, 0.0))) + h = self.fallback_height + d = np.linalg.norm(z * (h / z[2])) + if not ifcopenshell.util.shape_builder.is_x(a, 0): + self.is_angled = True + return {"z": z, "y": y, "a": a, "d": d, "h": h} + return { + "z": np.array((0.0, 0.0, 1.0)), + "y": np.array((0.0, 1.0, 0.0)), + "a": 0.0, + "d": self.fallback_height, + "h": self.fallback_height, + } + + def get_join_vector(self, y1, y2): + result = np.cross(y1, y2) + if result[2] < 0: + return result * -1 + return result + + def get_axes(self, wall, reference, layers: list[PrioritisedLayer], angle: float): + axes = [[p.copy() for p in reference]] + # Apply usage to convert the Reference line into MlsBase + sense_factor = 1 + if (usage := ifcopenshell.util.element.get_material(wall)) and usage.is_a("IfcMaterialLayerSetUage"): + for point in axes[0]: + point[1] += usage.OffsetFromReferenceLine + sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 + + for layer in layers: + y_offset = (layer.thickness * sense_factor) / cos(angle) + axes.append([p.copy() + np.array((0.0, y_offset)) for p in axes[-1]]) + return axes From 269e137ea2ea1d5553320ecb5e8ee50750cc42da Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 17:12:54 +0500 Subject: [PATCH 235/476] black . --- src/ifcpatch/ifcpatch/recipes/MergeStyles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcpatch/ifcpatch/recipes/MergeStyles.py b/src/ifcpatch/ifcpatch/recipes/MergeStyles.py index c951ce9db3..5af2807c8f 100644 --- a/src/ifcpatch/ifcpatch/recipes/MergeStyles.py +++ b/src/ifcpatch/ifcpatch/recipes/MergeStyles.py @@ -43,7 +43,7 @@ class Patcher: i = 0 for element in self.file.by_type(ifc_class): data = "-".join([str(a) for a in element]) - if (unique := uniques.get(data, None)): + if unique := uniques.get(data, None): ifcopenshell.util.element.replace_element(element, unique) self.file.remove(element) i += 1 From 590288496eec1fdd97c10cfadf73f901647a85c0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 12:17:04 +0500 Subject: [PATCH 236/476] typing --- .../bonsai/bim/module/material/operator.py | 67 ++++++++------ src/bonsai/bonsai/bim/module/material/ui.py | 2 +- src/bonsai/bonsai/bim/module/model/slab.py | 11 ++- src/bonsai/bonsai/bim/module/profile/prop.py | 4 +- src/bonsai/bonsai/bim/module/pset/data.py | 4 +- src/bonsai/bonsai/bim/module/pset/prop.py | 8 +- src/bonsai/bonsai/bim/module/pset/ui.py | 3 +- src/bonsai/bonsai/tool/blender.py | 17 +++- src/bonsai/bonsai/tool/material.py | 13 ++- src/bonsai/bonsai/tool/profile.py | 5 +- .../api/geometry/add_slab_representation.py | 88 +++++++++++-------- .../ifcopenshell/api/group/assign_group.py | 3 - .../ifcopenshell/api/system/assign_system.py | 14 +-- .../recipes/ResetSpatialElementLocations.py | 14 +-- 14 files changed, 147 insertions(+), 106 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index bd515252ee..c6abbde92d 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -262,7 +262,8 @@ class AssignMaterial(bpy.types.Operator, tool.Ifc.Operator): if not (material_type := properties.material_type): if not (obj := context.active_object): return "" - material_type = obj.BIMObjectMaterialProperties.material_type + omprops = tool.Material.get_object_material_props(obj) + material_type = omprops.material_type description = "Assign current IfcMaterial to the selected objects" if material_type != "IfcMaterial": @@ -294,14 +295,12 @@ class AddConstituent(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object + omprops = tool.Material.get_object_material_props(obj) self.file = tool.Ifc.get() - ifcopenshell.api.run( - "material.add_constituent", + ifcopenshell.api.material.add_constituent( self.file, - **{ - "constituent_set": self.file.by_id(self.constituent_set), - "material": self.file.by_id(int(obj.BIMObjectMaterialProperties.material)), - }, + constituent_set=self.file.by_id(self.constituent_set), + material=self.file.by_id(int(omprops.material)), ) @@ -330,13 +329,14 @@ class AddProfile(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object + assert obj self.file = tool.Ifc.get() props = tool.Material.get_material_props() - ifcopenshell.api.run( - "material.add_profile", + omprops = tool.Material.get_object_material_props(obj) + ifcopenshell.api.material.add_profile( self.file, profile_set=self.file.by_id(self.profile_set), - material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material)), + material=self.file.by_id(int(omprops.material)), profile=self.file.by_id(int(props.profiles)), ) @@ -367,11 +367,13 @@ class AddLayer(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object + assert obj + omprops = tool.Material.get_object_material_props(obj) layer_set = tool.Ifc.get().by_id(self.layer_set) ifcopenshell.api.material.add_layer( tool.Ifc.get(), layer_set=layer_set, - material=tool.Ifc.get().by_id(int(obj.BIMObjectMaterialProperties.material)), + material=tool.Ifc.get().by_id(int(omprops.material)), ) slab.DumbSlabPlaner().regenerate_from_layer_set(layer_set) wall.DumbWallPlaner().regenerate_from_layer_set(layer_set) @@ -445,12 +447,13 @@ class AddListItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object + assert obj + omprops = tool.Material.get_object_material_props(obj) self.file = tool.Ifc.get() - ifcopenshell.api.run( - "material.add_list_item", + ifcopenshell.api.material.add_list_item( self.file, material_list=self.file.by_id(self.list_item_set), - material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material)), + material=self.file.by_id(int(omprops.material)), ) @@ -484,10 +487,12 @@ class EnableEditingAssignedMaterial(bpy.types.Operator): def execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - props = obj.BIMObjectMaterialProperties + assert obj + props = tool.Material.get_object_material_props(obj) props.is_editing = True element = tool.Ifc.get_entity(obj) material = ifcopenshell.util.element.get_material(element) + assert material if material.is_a("IfcMaterial"): props.material = str(material.id()) @@ -545,7 +550,8 @@ class DisableEditingAssignedMaterial(bpy.types.Operator): def execute(self, context): bpy.ops.bim.disable_editing_material_set_item() obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - props = obj.BIMObjectMaterialProperties + assert obj + props = tool.Material.get_object_material_props(obj) props.is_editing = False return {"FINISHED"} @@ -562,7 +568,7 @@ class EditAssignedMaterial(bpy.types.Operator, tool.Ifc.Operator): self.file = tool.Ifc.get() active_obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object assert active_obj - props = active_obj.BIMObjectMaterialProperties + props = tool.Material.get_object_material_props(active_obj) element = tool.Ifc.get_entity(active_obj) assert element material = ifcopenshell.util.element.get_material(element) @@ -628,7 +634,8 @@ class EnableEditingMaterialSetItemProfile(bpy.types.Operator): def execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.props = obj.BIMObjectMaterialProperties + assert obj + self.props = tool.Material.get_object_material_props(obj) self.props.active_material_set_item_id = self.material_set_item self.props.material_set_item_profile_attributes.clear() profile = tool.Ifc.get().by_id(self.material_set_item).Profile @@ -644,7 +651,8 @@ class DisableEditingMaterialSetItemProfile(bpy.types.Operator): def execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.props = obj.BIMObjectMaterialProperties + assert obj + self.props = tool.Material.get_object_material_props(obj) self.props.active_material_set_item_id = 0 self.props.material_set_item_profile_attributes.clear() return {"FINISHED"} @@ -659,7 +667,8 @@ class EditMaterialSetItemProfile(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - self.props = obj.BIMObjectMaterialProperties + assert obj + self.props = tool.Material.get_object_material_props(obj) attributes = bonsai.bim.helper.export_attributes(self.props.material_set_item_profile_attributes) profile = tool.Ifc.get().by_id(self.material_set_item).Profile ifcopenshell.api.run("profile.edit_profile", tool.Ifc.get(), profile=profile, attributes=attributes) @@ -678,11 +687,13 @@ class EnableEditingMaterialSetItem(bpy.types.Operator): def execute(self, context): self.file = tool.Ifc.get() obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object + assert obj self.mprops = tool.Material.get_material_props() - self.props = obj.BIMObjectMaterialProperties + self.props = tool.Material.get_object_material_props(obj) self.props.active_material_set_item_id = self.material_set_item element = tool.Ifc.get_entity(obj) + assert element material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) material_set_item = self.file.by_id(self.material_set_item) @@ -706,7 +717,8 @@ class DisableEditingMaterialSetItem(bpy.types.Operator): def execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object - props = obj.BIMObjectMaterialProperties + assert obj + props = tool.Material.get_object_material_props(obj) props.active_material_set_item_id = 0 return {"FINISHED"} @@ -721,10 +733,13 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = tool.Ifc.get() - props = obj.BIMObjectMaterialProperties + assert obj + props = tool.Material.get_object_material_props(obj) mprops = tool.Material.get_material_props() element = tool.Ifc.get_entity(obj) + assert element material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) + assert material attributes = bonsai.bim.helper.export_attributes(props.material_set_item_attributes) @@ -733,7 +748,7 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): self.file, constituent=self.file.by_id(self.material_set_item), attributes=attributes, - material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)), + material=self.file.by_id(int(props.material_set_item_material)), ) elif material.is_a("IfcMaterialLayerSet"): layer = self.file.by_id(self.material_set_item) @@ -741,7 +756,7 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): self.file, layer=layer, attributes=attributes, - material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)), + material=self.file.by_id(int(props.material_set_item_material)), ) slab.DumbSlabPlaner().regenerate_from_layer(layer) wall.DumbWallPlaner().regenerate_from_layer(layer) @@ -755,7 +770,7 @@ class EditMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): profile=self.file.by_id(self.material_set_item), attributes=attributes, profile_def=profile_def, - material=self.file.by_id(int(obj.BIMObjectMaterialProperties.material_set_item_material)), + material=self.file.by_id(int(props.material_set_item_material)), ) else: pass diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index 30a7688db5..b743d37ef1 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -156,7 +156,7 @@ class BIM_PT_object_material(Panel): assert obj self.file = tool.Ifc.get() self.oprops = tool.Blender.get_object_bim_props(obj) - self.props = obj.BIMObjectMaterialProperties + self.props = tool.Material.get_object_material_props(obj) self.mprops = tool.Material.get_material_props() if not ObjectMaterialData.data["materials"]: diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index cb6ba498a2..04b22e8060 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -21,6 +21,7 @@ import json import bmesh import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.geometry import ifcopenshell.util.element import ifcopenshell.util.placement import ifcopenshell.util.representation @@ -343,8 +344,7 @@ class DumbSlabPlaner: else: props = tool.Model.get_model_props() x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle - new_rep = ifcopenshell.api.run( - "geometry.add_slab_representation", + new_rep = ifcopenshell.api.geometry.add_slab_representation( tool.Ifc.get(), context=body_context, depth=thickness * self.unit_scale, @@ -368,15 +368,14 @@ class DumbSlabPlaner: else: props = tool.Model.get_model_props() x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle - representation = ifcopenshell.api.run( - "geometry.add_slab_representation", + representation = ifcopenshell.api.geometry.add_slab_representation( tool.Ifc.get(), context=body_context, depth=thickness * self.unit_scale, x_angle=x_angle, ) - ifcopenshell.api.run( - "geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation + ifcopenshell.api.geometry.assign_representation( + tool.Ifc.get(), product=element, representation=representation ) bonsai.core.geometry.switch_representation( diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py index 3f25c5f4e4..d63c555689 100644 --- a/src/bonsai/bonsai/bim/module/profile/prop.py +++ b/src/bonsai/bonsai/bim/module/profile/prop.py @@ -37,7 +37,7 @@ from bpy.props import ( from typing import TYPE_CHECKING, Union -def get_profile_classes(self, context): +def get_profile_classes(self: "BIMProfileProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not ProfileData.is_loaded: ProfileData.load() return ProfileData.data["profile_classes"] @@ -62,7 +62,7 @@ class Profile(PropertyGroup): ifc_definition_id: int -def update_active_profile_index(self, context): +def update_active_profile_index(self: "BIMProfileProperties", context: bpy.types.Context) -> None: ProfileData.data["active_profile_users"] = ProfileData.active_profile_users() diff --git a/src/bonsai/bonsai/bim/module/pset/data.py b/src/bonsai/bonsai/bim/module/pset/data.py index 88d830d976..0c015f784a 100644 --- a/src/bonsai/bonsai/bim/module/pset/data.py +++ b/src/bonsai/bonsai/bim/module/pset/data.py @@ -212,7 +212,9 @@ class MaterialSetItemPsetsData(Data): @classmethod def load(cls): psets = {} - ifc_definition_id = bpy.context.active_object.BIMObjectMaterialProperties.active_material_set_item_id + obj = bpy.context.active_object + assert obj + ifc_definition_id = tool.Material.get_object_material_props(obj).active_material_set_item_id if ifc_definition_id: psets = cls.psetqtos(tool.Ifc.get().by_id(ifc_definition_id)) cls.data = {"psets": psets} diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py index 6a1b68841f..81768c6f95 100644 --- a/src/bonsai/bonsai/bim/module/pset/prop.py +++ b/src/bonsai/bonsai/bim/module/pset/prop.py @@ -109,10 +109,12 @@ def get_material_set_pset_names(self, context): return psetnames[ifc_class] -def get_material_set_item_pset_names(self, context): +def get_material_set_item_pset_names(self, context) -> list[tuple[str, str, str]]: global psetnames - ifc_definition_id = context.active_object.BIMObjectMaterialProperties.active_material_set_item_id - if not ifc_definition_id: + obj = context.active_object + assert obj + omprops = tool.Material.get_object_material_props(obj) + if not omprops.active_material_set_item_id: return [] ifc_class = tool.Ifc.get().by_id(ifc_definition_id).is_a() if ifc_class not in psetnames: diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index c155937da9..de8cea8a95 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -459,7 +459,8 @@ class BIM_PT_material_set_item_psets(Panel): obj = context.active_object assert obj - if not obj.BIMObjectMaterialProperties.active_material_set_item_id: + omprops = tool.Material.get_object_material_props(obj) + if not omprops.active_material_set_item_id: self.layout.label(text="No Material Set Item Edited.") return diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index c8846ace19..da85bb734f 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -44,6 +44,7 @@ from typing_extensions import assert_never if TYPE_CHECKING: from bonsai.bim.prop import BIMProperties, BIMObjectProperties + T = TypeVar("T") VIEWPORT_ATTRIBUTES = [ "view_matrix", @@ -199,7 +200,9 @@ class Blender(bonsai.core.tool.Blender): props = tool.Material.get_material_props() return props.materials[props.active_material_index].ifc_definition_id elif obj_type == "MaterialSetItem": - return bpy.data.objects.get(obj).BIMObjectMaterialProperties.active_material_set_item_id + obj_ = bpy.data.objects[obj] + omprops = tool.Material.get_object_material_props(obj_) + return omprops.active_material_set_item_id elif obj_type == "Task": tprops = tool.Sequence.get_task_tree_props() return tprops.tasks[context.scene.BIMWorkScheduleProperties.active_task_index].ifc_definition_id @@ -1466,9 +1469,9 @@ class Blender(bonsai.core.tool.Blender): def set_prop_from_path(cls, bpy_object: bpy.types.bpy_struct, prop_path: str, value: Any) -> None: """Set `data_block` property value using path from `path_from_id`.""" - T = TypeVar("T", bound=bpy.types.bpy_struct) + T_ = TypeVar("T_", bound=bpy.types.bpy_struct) - def path_resolve(obj: T, prop_path: str) -> tuple[T, str]: + def path_resolve(obj: T_, prop_path: str) -> tuple[T_, str]: if "." in prop_path: extra_path, prop_path = prop_path.rsplit(".", 1) obj = obj.path_resolve(extra_path) @@ -1605,3 +1608,11 @@ class Blender(bonsai.core.tool.Blender): if isinstance(obj, bpy.types.Object): return tool.Blender.get_object_bim_props(obj).ifc_definition_id return tool.Style.get_material_style_props(obj).ifc_definition_id + + @classmethod + def get_active_uilist_element( + cls, collection: bpy.types.bpy_prop_collection_idprop[T], index: int + ) -> Union[T, None]: + if 0 <= index < len(collection): + return collection[index] + return None diff --git a/src/bonsai/bonsai/tool/material.py b/src/bonsai/bonsai/tool/material.py index 0d32bb1ebd..0139a1b783 100644 --- a/src/bonsai/bonsai/tool/material.py +++ b/src/bonsai/bonsai/tool/material.py @@ -33,7 +33,7 @@ from typing_extensions import assert_never if TYPE_CHECKING: # Avoid circular imports. from bonsai.bim.module.material.prop import Material as MaterialItem - from bonsai.bim.module.material.prop import BIMMaterialProperties + from bonsai.bim.module.material.prop import BIMMaterialProperties, BIMObjectMaterialProperties class Material(bonsai.core.tool.Material): @@ -41,6 +41,10 @@ class Material(bonsai.core.tool.Material): def get_material_props(cls) -> BIMMaterialProperties: return bpy.context.scene.BIMMaterialProperties + @classmethod + def get_object_material_props(cls, obj: bpy.types.Object) -> BIMObjectMaterialProperties: + return obj.BIMObjectMaterialProperties + @classmethod def disable_editing_materials(cls) -> None: props = tool.Material.get_material_props() @@ -203,11 +207,14 @@ class Material(bonsai.core.tool.Material): @classmethod def get_object_ui_material_type(cls) -> str: active_obj = bpy.context.active_object - return active_obj.BIMObjectMaterialProperties.material_type + assert active_obj + return tool.Material.get_object_material_props(active_obj).material_type @classmethod def get_object_ui_active_material(cls) -> ifcopenshell.entity_instance: - return tool.Ifc.get().by_id(int(bpy.context.active_object.BIMObjectMaterialProperties.material)) + obj = bpy.context.active_object + assert obj + return tool.Ifc.get().by_id(int(tool.Material.get_object_material_props(obj).material)) @classmethod def get_material( diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py index ae8b695b1c..5c14828892 100644 --- a/src/bonsai/bonsai/tool/profile.py +++ b/src/bonsai/bonsai/tool/profile.py @@ -123,16 +123,13 @@ class Profile(bonsai.core.tool.Profile): @classmethod def get_active_profile_ui(cls) -> Union[bonsai.bim.module.profile.prop.Profile, None]: props = cls.get_profile_props() - index = props.active_profile_index - if len(props.profiles) > index >= 0: - return props.profiles[index] + return tool.Blender.get_active_uilist_element(props.profiles, props.active_profile_index) # Lengths are in meters. DEFAULT_PROFILE_ATTRS = { "IfcCircleProfileDef": { "Radius": 0.05, }, - # TODO: test after debug "IfcAsymmetricIShapeProfileDef": { "BottomFlangeWidth": 0.1, "BottomFlangeThickness": 0.01, diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py index 3ceca7094c..c0d78872f4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_slab_representation.py @@ -27,10 +27,11 @@ def add_slab_representation( file: ifcopenshell.file, context: ifcopenshell.entity_instance, depth: float = 0.2, + # TODO: document remaining args. direction_sense: str = "POSITIVE", offset: float = 0.0, x_angle: float = 0.0, - clippings: Optional[list[Union[Clipping, dict[str, Any]]]] = None, + clippings: Optional[list[Union[Clipping, ifcopenshell.entity_instance]]] = None, polyline: Optional[list[tuple[float, float]]] = None, ) -> ifcopenshell.entity_instance: """ @@ -55,60 +56,74 @@ def add_slab_representation( """ usecase = Usecase() usecase.file = file - usecase.settings = { - "context": context, - "depth": depth, - "direction_sense": direction_sense, - "offset": offset, - "x_angle": x_angle, - "clippings": clippings if clippings is not None else [], - "polyline": polyline, - } - return usecase.execute() + return usecase.execute( + context, + depth, + direction_sense, + offset, + x_angle, + clippings if clippings is not None else [], + polyline, + ) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] - def execute(self): - self.settings["unit_scale"] = ifcopenshell.util.unit.calculate_unit_scale(self.file) - return self.file.createIfcShapeRepresentation( - self.settings["context"], - self.settings["context"].ContextIdentifier, - "Clipping" if self.settings["clippings"] else "SweptSolid", + def execute( + self, + context: ifcopenshell.entity_instance, + depth: float, + direction_sense: str, + offset: float, + x_angle: float, + clippings: list[Union[Clipping, ifcopenshell.entity_instance]], + polyline: Optional[list[tuple[float, float]]], + ) -> ifcopenshell.entity_instance: + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file) + self.clippings = clippings + self.depth = depth + self.direction_sense = direction_sense + self.offset = offset + self.x_angle = x_angle + self.polyline = polyline + return self.file.create_entity( + "IfcShapeRepresentation", + context, + context.ContextIdentifier, + "Clipping" if self.clippings else "SweptSolid", [self.create_item()], ) - def create_item(self): + def create_item(self) -> ifcopenshell.entity_instance: size = self.convert_si_to_unit(1) points = ((0.0, 0.0), (size, 0.0), (size, size), (0.0, size), (0.0, 0.0)) - if self.settings["polyline"]: + if self.polyline: points = [ - (self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.settings["x_angle"])))) - for p in self.settings["polyline"] + (self.convert_si_to_unit(p[0]), self.convert_si_to_unit(p[1] * abs(1 / cos(self.x_angle)))) + for p in self.polyline ] if self.file.schema == "IFC2X3": curve = self.file.createIfcPolyline([self.file.createIfcCartesianPoint(p) for p in points]) else: curve = self.file.createIfcIndexedPolyCurve(self.file.createIfcCartesianPointList2D(points)) - if self.settings["x_angle"]: - direction_ratios = (0.0, sin(self.settings["x_angle"]), cos(self.settings["x_angle"])) + if self.x_angle: + direction_ratios = (0.0, sin(self.x_angle), cos(self.x_angle)) else: direction_ratios = (0.0, 0.0, 1.0) offset_direction = direction_ratios # offset direction doesn't change if direction_sense is negative extrusion_direction = self.file.createIfcDirection(direction_ratios) - if self.settings["direction_sense"] == "NEGATIVE": + if self.direction_sense == "NEGATIVE": direction_ratios = tuple((-n for n in direction_ratios)) extrusion_direction = self.file.createIfcDirection(direction_ratios) - perpendicular_offset = self.convert_si_to_unit(self.settings["offset"]) * abs(1 / cos(self.settings["x_angle"])) - perpendicular_depth = self.convert_si_to_unit(self.settings["depth"]) * abs(1 / cos(self.settings["x_angle"])) + perpendicular_offset = self.convert_si_to_unit(self.offset) * abs(1 / cos(self.x_angle)) + perpendicular_depth = self.convert_si_to_unit(self.depth) * abs(1 / cos(self.x_angle)) position = None # default position for IFC2X3 where .Position is not optional - if self.file.schema == "IFC2X3" or self.settings["offset"] != 0: + if self.file.schema == "IFC2X3" or self.offset != 0: position_vector = ( offset_direction[0] * perpendicular_offset, offset_direction[1] * perpendicular_offset, @@ -120,26 +135,27 @@ class Usecase: self.file.createIfcDirection((1.0, 0.0, 0.0)), ) - extrusion = self.file.createIfcExtrudedAreaSolid( + extrusion = self.file.create_entity( + "IfcExtrudedAreaSolid", self.file.createIfcArbitraryClosedProfileDef("AREA", None, curve), position, extrusion_direction, perpendicular_depth, ) - if self.settings["clippings"]: + if self.clippings: return self.apply_clippings(extrusion) return extrusion - def apply_clippings(self, first_operand): - while self.settings["clippings"]: - clipping = self.settings["clippings"].pop() + def apply_clippings(self, first_operand: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + while self.clippings: + clipping = self.clippings.pop() if isinstance(clipping, ifcopenshell.entity_instance): new = ifcopenshell.util.element.copy(self.file, clipping) new.FirstOperand = first_operand first_operand = new else: # Clipping - first_operand = clipping.apply(self.file, first_operand, self.settings["unit_scale"]) + first_operand = clipping.apply(self.file, first_operand, self.unit_scale) return first_operand - def convert_si_to_unit(self, co): - return co / self.settings["unit_scale"] + def convert_si_to_unit(self, co: float) -> float: + return co / self.unit_scale diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py index 4972058fb4..4a57f148eb 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/assign_group.py @@ -31,12 +31,9 @@ def assign_group( twice. :param products: A list of IfcProduct elements to assign to the group - :type products: list[ifcopenshell.entity_instance] :param group: The IfcGroup to assign the products to - :type group: ifcopenshell.entity_instance :return: The IfcRelAssignsToGroup relationship or `None` if `products` was empty list. - :rtype: Union[ifcopenshell.entity_instance, None] Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py index 4e6f5b89ee..8677f748b4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/assign_system.py @@ -19,24 +19,22 @@ import ifcopenshell import ifcopenshell.api.group import ifcopenshell.util.system +from typing import Union def assign_system( file: ifcopenshell.file, products: list[ifcopenshell.entity_instance], system: ifcopenshell.entity_instance, -) -> None: +) -> Union[ifcopenshell.entity_instance, None]: """Assigns distribution elements to a system Note that it is not necessary to assign distribution ports to a system. :param products: The list of IfcDistributionElements to assign to the system. - :type products: list[ifcopenshell.entity_instance] :param system: The IfcSystem you want to assign the element to. - :type system: ifcopenshell.entity_instance :return: The IfcRelAssignsToGroup relationship or `None` if `products` was empty list. - :rtype: [ifcopenshell.entity_instance, None] Example: @@ -52,14 +50,6 @@ def assign_system( # This duct is part of the system ifcopenshell.api.system.assign_system(model, products=[duct], system=system) """ - settings = { - "products": products, - "system": system, - } - - system = settings["system"] - products = settings["products"] - if not all(ifcopenshell.util.system.is_assignable(failed_product := product, system) for product in products): raise TypeError(f"You cannot assign an {failed_product.is_a()} to an {system.is_a()}") diff --git a/src/ifcpatch/ifcpatch/recipes/ResetSpatialElementLocations.py b/src/ifcpatch/ifcpatch/recipes/ResetSpatialElementLocations.py index aa335320ba..a02ba69285 100644 --- a/src/ifcpatch/ifcpatch/recipes/ResetSpatialElementLocations.py +++ b/src/ifcpatch/ifcpatch/recipes/ResetSpatialElementLocations.py @@ -16,9 +16,12 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcPatch. If not, see . +import ifcopenshell +import logging + class Patcher: - def __init__(self, file, logger, ifc_class="IfcSite"): + def __init__(self, file: ifcopenshell.file, logger: logging.Logger, ifc_class: str = "IfcSite"): """Resets the location of a spatial element to 0,0,0 Another more specialised patch to fix incorrect coordinate usage is to @@ -26,7 +29,6 @@ class Patcher: to 0,0,0. :param ifc_class: The class of spatial element to reset coordinates for. - :type ifc_class: str Example: @@ -39,13 +41,15 @@ class Patcher: self.logger = logger self.ifc_class = ifc_class - def patch(self): + def patch(self) -> None: project = self.file.by_type("IfcProject")[0] spatial_elements = self.find_decomposed_ifc_class(project, self.ifc_class) for spatial_element in spatial_elements: self.patch_placement_to_origin(spatial_element) - def find_decomposed_ifc_class(self, element, ifc_class): + def find_decomposed_ifc_class( + self, element: ifcopenshell.entity_instance, ifc_class: str + ) -> list[ifcopenshell.entity_instance]: results = [] rel_aggregates = element.IsDecomposedBy if not rel_aggregates: @@ -57,7 +61,7 @@ class Patcher: results.extend(self.find_decomposed_ifc_class(part, ifc_class)) return results - def patch_placement_to_origin(self, element): + def patch_placement_to_origin(self, element: ifcopenshell.entity_instance) -> None: element.ObjectPlacement.RelativePlacement.Location.Coordinates = (0.0, 0.0, 0.0) if element.ObjectPlacement.RelativePlacement.Axis: element.ObjectPlacement.RelativePlacement.Axis.DirectionRatios = (0.0, 0.0, 1.0) From eb0d3c38b2c532ccac32e4560c16a844f604e11a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 12:07:14 +0500 Subject: [PATCH 237/476] ifcopenshell.util - make ifc_file argument optional if it can be deduced from ifc element --- .../ifcopenshell/util/data.py | 5 ++- .../ifcopenshell/util/element.py | 38 ++++++++++++++----- .../ifcopenshell/util/schema.py | 5 ++- .../ifcopenshell/util/unit.py | 8 +++- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/data.py b/src/ifcopenshell-python/ifcopenshell/util/data.py index 9f02d973a5..75fae18ed5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/data.py +++ b/src/ifcopenshell-python/ifcopenshell/util/data.py @@ -66,7 +66,7 @@ class Clipping: raise Exception(f"Unexpected clipping type provided: {raw_data}") def apply( - self, ifc_file: ifcopenshell.file, first_operand: ifcopenshell.entity_instance, unit_scale: float + self, ifc_file: Union[ifcopenshell.file, None], first_operand: ifcopenshell.entity_instance, unit_scale: float ) -> ifcopenshell.entity_instance: """Applies the clipping data as an IfcBooleanClippingResult to an operand @@ -76,6 +76,9 @@ class Clipping: :return: An IfcBooleanClippingResult which uses an IfcHalfSpaceSolid to clip the first operand """ + if not ifc_file: + ifc_file = first_operand.file + location = ifc_file.createIfcCartesianPoint([i / unit_scale for i in self.location]) direction = ifc_file.createIfcDirection(self.normal) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 9034e382be..e4b6ee951d 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -689,7 +689,7 @@ def get_styles(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entit # TODO: ifc_file argument is unnecessary for some methods now # since we have entity_instance.file, so we can deprecate it. def get_elements_by_material( - ifc_file: ifcopenshell.file, material: ifcopenshell.entity_instance + ifc_file: Union[ifcopenshell.file, None], material: ifcopenshell.entity_instance ) -> set[ifcopenshell.entity_instance]: """Retrieves the elements related to a material. @@ -707,6 +707,8 @@ def get_elements_by_material( material = file.by_type("IfcMaterial")[0] elements = ifcopenshell.util.element.get_elements_by_material(file, material) """ + if not ifc_file: + ifc_file = material.file results = set() for inverse in ifc_file.get_inverse(material): if inverse.is_a("IfcRelAssociatesMaterial"): @@ -730,7 +732,7 @@ def get_elements_by_material( def get_elements_by_style( - ifc_file: ifcopenshell.file, style: ifcopenshell.entity_instance + ifc_file: Union[ifcopenshell.file, None], style: ifcopenshell.entity_instance ) -> set[ifcopenshell.entity_instance]: """Retrieves the elements whose geometric representation uses a style @@ -745,6 +747,8 @@ def get_elements_by_style( style = file.by_type("IfcSurfaceStyle")[0] elements = ifcopenshell.util.element.get_elements_by_style(file, style) """ + if not ifc_file: + ifc_file = style.file results = set() inverses = list(ifc_file.get_inverse(style)) while inverses: @@ -778,7 +782,7 @@ def get_elements_by_style( def get_elements_by_representation( - ifc_file: ifcopenshell.file, representation: ifcopenshell.entity_instance + ifc_file: Union[ifcopenshell.file, None], representation: ifcopenshell.entity_instance ) -> set[ifcopenshell.entity_instance]: """Gets all elements using a geometric representation @@ -793,6 +797,8 @@ def get_elements_by_representation( representation = file.by_type("IfcShapeRepresentation")[0] elements = ifcopenshell.util.element.get_elements_by_representation(file, representation) """ + if not ifc_file: + ifc_file = representation.file results = set() [results.update(pr.ShapeOfProduct) for pr in representation.OfProductRepresentation] for rep_map in representation.RepresentationMap: @@ -838,7 +844,7 @@ def get_elements_by_profile(profile: ifcopenshell.entity_instance) -> set[ifcope def get_elements_by_layer( - ifc_file: ifcopenshell.file, layer: ifcopenshell.entity_instance + ifc_file: Union[ifcopenshell.file, None], layer: ifcopenshell.entity_instance ) -> set[ifcopenshell.entity_instance]: """Get all the elements that are used by a presentation layer @@ -846,6 +852,8 @@ def get_elements_by_layer( :param layer: The IfcPresentationLayerAssignment layer :return: The elements using the geometric representation """ + if not ifc_file: + ifc_file = layer.file results = set() for item in layer.AssignedItems or []: if item.is_a("IfcShapeRepresentation"): @@ -858,7 +866,7 @@ def get_elements_by_layer( def get_layers( - ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance + ifc_file: Union[ifcopenshell.file, None], element: ifcopenshell.entity_instance ) -> list[ifcopenshell.entity_instance]: """Get the CAD layers that an element is part of @@ -876,6 +884,8 @@ def get_layers( element = ifcopenshell.by_type("IfcWall")[0] layers = ifcopenshell.util.element.get_layers(element) """ + if not ifc_file: + ifc_file = element.file layers = [] representations = [] if representation := getattr(element, "Representation", None): @@ -1352,12 +1362,14 @@ def has_element_reference(value: Any, element: ifcopenshell.entity_instance) -> return value == element -def remove_deep(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> None: +def remove_deep(ifc_file: Union[ifcopenshell.file, None], element: ifcopenshell.entity_instance) -> None: """Recursively purges a subgraph safely. Do not use, use remove_deep2() instead. """ # @todo maybe some sort of try-finally mechanism. + if not ifc_file: + ifc_file = element.file ifc_file.batch() subgraph = list(ifc_file.traverse(element, breadth_first=True)) subgraph_set = set(subgraph) @@ -1431,7 +1443,7 @@ def unbatch_remove_deep2(ifc_file: ifcopenshell.file) -> ifcopenshell.file: def remove_deep2( - ifc_file: ifcopenshell.file, + ifc_file: Union[ifcopenshell.file, None], element: ifcopenshell.entity_instance, also_consider: list[ifcopenshell.entity_instance] = [], do_not_delete: set[ifcopenshell.entity_instance] = set(), @@ -1468,6 +1480,8 @@ def remove_deep2( :param element: The starting element that defines the subgraph """ # ifc_file.batch() + if not ifc_file: + ifc_file = element.file total_inverses = ifc_file.get_total_inverses(element) if total_inverses > 0: @@ -1527,7 +1541,9 @@ def remove_deep2( # ifc_file.unbatch() -def copy(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: +def copy( + ifc_file: Union[ifcopenshell.file, None], element: ifcopenshell.entity_instance +) -> ifcopenshell.entity_instance: """ Copy a single element. Any referenced elements are not copied. @@ -1537,6 +1553,8 @@ def copy(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> :param element: The IFC element to copy :return: The newly copied element """ + if not ifc_file: + ifc_file = element.file new = ifc_file.create_entity(element.is_a()) for i, attribute in enumerate(element): if attribute is None: @@ -1549,7 +1567,7 @@ def copy(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> def copy_deep( - ifc_file: ifcopenshell.file, + ifc_file: Union[ifcopenshell.file, None], element: ifcopenshell.entity_instance, exclude: Optional[Sequence[str]] = None, exclude_callback: Optional[Callable[[ifcopenshell.entity_instance], bool]] = None, @@ -1572,6 +1590,8 @@ def copy_deep( be left as None. :return: The newly copied element """ + if not ifc_file: + ifc_file = element.file if copied_entities is None: copied_entities = {} else: diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema.py b/src/ifcopenshell-python/ifcopenshell/util/schema.py index 70eff36eb9..79c8a9c499 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/util/schema.py @@ -140,7 +140,7 @@ def get_subtypes( def reassign_class( - ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance, new_class: str + ifc_file: Union[ifcopenshell.file, None], element: ifcopenshell.entity_instance, new_class: str ) -> ifcopenshell.entity_instance: """ Attempts to change the class (entity name) of `element` to `new_class` by @@ -156,6 +156,9 @@ def reassign_class( It's unlikely that this affects real-world usage of this function. """ + if not ifc_file: + ifc_file = element.file + schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(ifc_file.schema) try: declaration = schema.declaration_by_name(new_class) diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index b1862d3882..78a0117fa5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -456,7 +456,7 @@ def get_project_unit( def get_property_unit( - prop: ifcopenshell.entity_instance, ifc_file: ifcopenshell.file, use_cache: bool = False + prop: ifcopenshell.entity_instance, ifc_file: Union[ifcopenshell.file, None], use_cache: bool = False ) -> Union[ifcopenshell.entity_instance, None]: """Gets the unit definition of a property or quantity @@ -499,11 +499,13 @@ def get_property_unit( measure_class = value.is_a() if measure_class and (unit_type := get_measure_unit_type(measure_class)): + if not ifc_file: + ifc_file = prop.file return get_project_unit(ifc_file, unit_type, use_cache=use_cache) def get_property_table_unit( - prop: ifcopenshell.entity_instance, ifc_file: ifcopenshell.file, use_cache: bool = False + prop: ifcopenshell.entity_instance, ifc_file: Union[ifcopenshell.file, None], use_cache: bool = False ) -> Dict[str, Union[ifcopenshell.entity_instance, None]]: """ Gets the unit definition of a property table @@ -523,6 +525,8 @@ def get_property_table_unit( If a unit-entity is missing, the value associated to the key is `null`. """ + if not ifc_file: + ifc_file = prop.file defining_unit = None if unit := prop.DefiningUnit: defining_unit = unit From 9076deddbcb24bdf0a8ecece9f9f49f8c3d599f7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 12:13:36 +0500 Subject: [PATCH 238/476] Consider ifc2x3 in a358cff1b7 #6255 In ifc2x3 position is not optional, so we should reset it's coordinates instead. --- src/bonsai/bonsai/bim/module/model/slab.py | 4 +--- src/bonsai/bonsai/tool/model.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 04b22e8060..2c4357e308 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -331,9 +331,7 @@ class DumbSlabPlaner: ifc_position = extrusion.Position if perpendicular_offset == 0.0: # Clean up possible previous offset. - if ifc_position: - extrusion.Position = None - ifcopenshell.util.element.remove_deep2(ifc_file, ifc_position) + tool.Model.reset_extrusion_position(extrusion) else: position = offset_direction * perpendicular_offset if ifc_position: diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index c63a9ba466..d304645d4c 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -2064,6 +2064,21 @@ class Model(bonsai.core.tool.Model): extrusion.Position = new_position + @classmethod + def reset_extrusion_position(cls, extrusion: ifcopenshell.entity_instance) -> None: + ifc_file = extrusion.file + + if ifc_file.schema == "IFC2X3": + # Position is not optional. + extrusion.Position.Location.Coordinates = (0.0, 0.0, 0.0) + return + + position = extrusion.Position + if position is None: + return + extrusion.Position = None + ifcopenshell.util.element.remove_deep2(ifc_file, position) + @classmethod def get_existing_x_angle(cls, extrusion: ifcopenshell.entity_instance) -> float: x, y, z = extrusion.ExtrudedDirection.DirectionRatios From 85fc97eb843c48d55df5ee2747ebb518c992624a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 13:41:45 +0500 Subject: [PATCH 239/476] Profiles UI - add safety check for missing profiles #6284 Example - https://imgur.com/a/bVdV5oB --- src/bonsai/bonsai/bim/module/profile/data.py | 32 ++++++++++++++++---- src/bonsai/bonsai/bim/module/profile/prop.py | 12 +++++--- src/bonsai/bonsai/bim/module/profile/ui.py | 13 ++++++-- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/profile/data.py b/src/bonsai/bonsai/bim/module/profile/data.py index 38a32d34f7..cc28d8b008 100644 --- a/src/bonsai/bonsai/bim/module/profile/data.py +++ b/src/bonsai/bonsai/bim/module/profile/data.py @@ -21,6 +21,7 @@ import bpy.utils import bpy.utils.previews import ifcopenshell.util.doc import bonsai.tool as tool +from typing import Any def refresh(): @@ -28,7 +29,7 @@ def refresh(): class ProfileData: - data = {} + data: dict[str, Any] = {} failed_previews: set[int] = set() preview_collection = bpy.utils.previews.new() is_loaded = False @@ -37,6 +38,7 @@ class ProfileData: def load(cls): cls.data = { "total_profiles": cls.total_profiles(), + "does_active_profile_exist": cls.does_active_profile_exist(), "active_profile_users": cls.active_profile_users(), "profile_classes": cls.profile_classes(), "is_arbitrary_profile": cls.is_arbitrary_profile(), @@ -50,12 +52,30 @@ class ProfileData: return len([p for p in tool.Ifc.get().by_type("IfcProfileDef") if p.ProfileName]) @classmethod - def active_profile_users(cls): - profiles_props = tool.Profile.get_profile_props() - if profiles_props.active_profile_index >= len(profiles_props.profiles): + def update_active_profile_data(cls) -> None: + cls.data["does_active_profile_exist"] = cls.does_active_profile_exist() + cls.data["active_profile_users"] = cls.active_profile_users() + + @classmethod + def does_active_profile_exist(cls) -> bool: + """ + Currently not sure if our UI is always preserving existing named profiles, + so this check is added to avoid breaking UI in case of a missing profile. + """ + active_profile = tool.Profile.get_active_profile_ui() + if active_profile is None: + return False + profile_ifc = tool.Ifc.get_entity_by_id(active_profile.ifc_definition_id) + return profile_ifc is not None + + @classmethod + def active_profile_users(cls) -> int: + active_profile = tool.Profile.get_active_profile_ui() + if active_profile is None: + return 0 + profile_ifc = tool.Ifc.get_entity_by_id(active_profile.ifc_definition_id) + if profile_ifc is None: return 0 - profile_prop = profiles_props.profiles[profiles_props.active_profile_index] - profile_ifc = tool.Ifc.get().by_id(profile_prop.ifc_definition_id) return tool.Ifc.get().get_total_inverses(profile_ifc) @classmethod diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py index d63c555689..e2da08091f 100644 --- a/src/bonsai/bonsai/bim/module/profile/prop.py +++ b/src/bonsai/bonsai/bim/module/profile/prop.py @@ -46,7 +46,9 @@ def get_profile_classes(self: "BIMProfileProperties", context: bpy.types.Context def update_profile_name(self: "Profile", context: bpy.types.Context) -> None: from bonsai.bim.handler import refresh_ui_data - profile = tool.Ifc.get().by_id(self.ifc_definition_id) + profile = tool.Ifc.get_entity_by_id(self.ifc_definition_id) + if not profile: + return profile.ProfileName = self.name refresh_ui_data() @@ -63,15 +65,17 @@ class Profile(PropertyGroup): def update_active_profile_index(self: "BIMProfileProperties", context: bpy.types.Context) -> None: - ProfileData.data["active_profile_users"] = ProfileData.active_profile_users() + ProfileData.update_active_profile_data() class BIMProfileProperties(PropertyGroup): is_editing: BoolProperty(name="Is Editing") profiles: CollectionProperty(name="Profiles", type=Profile) active_profile_index: IntProperty(name="Active Profile Index", update=update_active_profile_index) - active_profile_id: IntProperty(name="Active Profile Id") - active_arbitrary_profile_id: IntProperty(name="Active Arbitrary Profile Id") + active_profile_id: IntProperty(name="Active Profile Id", description="Currently edited profile ID (attributes).") + active_arbitrary_profile_id: IntProperty( + name="Active Arbitrary Profile Id", description="Currently edited arbitrary profile ID." + ) profile_attributes: CollectionProperty(name="Profile Attributes", type=Attribute) profile_classes: EnumProperty(items=get_profile_classes, name="Profile Classes") is_filtering_material_profiles: bpy.props.BoolProperty( diff --git a/src/bonsai/bonsai/bim/module/profile/ui.py b/src/bonsai/bonsai/bim/module/profile/ui.py index ee2a93f107..1de4a077c6 100644 --- a/src/bonsai/bonsai/bim/module/profile/ui.py +++ b/src/bonsai/bonsai/bim/module/profile/ui.py @@ -75,6 +75,8 @@ class BIM_PT_profiles(Panel): if not self.props.is_editing: return + does_active_profile_exist: bool = ProfileData.data["does_active_profile_exist"] + row = self.layout.row(align=True) if self.props.profile_classes == "IfcArbitraryClosedProfileDef": split = row.split(factor=0.5, align=True) @@ -86,7 +88,14 @@ class BIM_PT_profiles(Panel): row.prop(self.props, "profile_classes", text="") row.operator("bim.add_profile_def", text="", icon="ADD") - if active_profile: + if active_profile and not does_active_profile_exist: + box = self.layout.box() + box.label(icon="ERROR", text=f"Active profile is missing from IFC project.") + row = box.row(align=True) + row.label(text="Reload Profiles UI.") + row.operator("bim.load_profiles", text="", icon="FILE_REFRESH") + + elif active_profile and does_active_profile_exist: row = self.layout.row(align=True) row.alignment = "RIGHT" @@ -122,7 +131,7 @@ class BIM_PT_profiles(Panel): row = self.layout.row() row.prop(self.props, "is_filtering_material_profiles", text="Filter Material Profiles") - if active_profile: + if active_profile and does_active_profile_exist: users_of_profile = ProfileData.data["active_profile_users"] self.layout.label(icon="INFO", text=f"Profile has {users_of_profile} inverse relationship(s) in project") From fc7dd3b234515f805136d87094fc7201d7775ca4 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 13:55:48 +0500 Subject: [PATCH 240/476] Preserve ProfileName editing arbitrary profile #6284 --- src/bonsai/bonsai/bim/module/geometry/operator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 99612e638f..e0ec7a8a81 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -2257,6 +2257,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): return old_profile = item.SweptArea + profile.ProfileName = old_profile.ProfileName for inverse in tool.Ifc.get().get_inverse(old_profile): ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile) From d68068a84b8efe3c8cc8d77b83957f321389bf6e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 14:06:31 +0500 Subject: [PATCH 241/476] bim.load_profiles - try not to check all inverses for optimization --- src/bonsai/bonsai/bim/module/profile/operator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py index f94f711f7d..3a2b026778 100644 --- a/src/bonsai/bonsai/bim/module/profile/operator.py +++ b/src/bonsai/bonsai/bim/module/profile/operator.py @@ -44,7 +44,9 @@ class LoadProfiles(bpy.types.Operator): for profile in tool.Ifc.get().by_type("IfcProfileDef"): if filter_material_profiles: inverse_references = tool.Ifc.get().get_inverse(profile) - related_material_profiles = [ref for ref in inverse_references if ref.is_a("IfcMaterialProfile")] + related_material_profiles = next( + (ref for ref in inverse_references if ref.is_a("IfcMaterialProfile")), None + ) if not related_material_profiles: continue if not profile.ProfileName: From 46104f5bf54874a33ef607f7b4196d6268b9a026 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 14:12:22 +0500 Subject: [PATCH 242/476] Update Profiles UI profile editing arbitrary profile #6284 --- src/bonsai/bonsai/bim/module/geometry/operator.py | 1 + src/bonsai/bonsai/tool/profile.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index e0ec7a8a81..18372e40fd 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -2260,6 +2260,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): profile.ProfileName = old_profile.ProfileName for inverse in tool.Ifc.get().get_inverse(old_profile): ifcopenshell.util.element.replace_attribute(inverse, old_profile, profile) + tool.Profile.replace_profile_in_profiles_ui(old_profile.id(), profile.id()) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_profile) tool.Geometry.reload_representation(props.representation_obj) diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py index 5c14828892..0838ef9a6e 100644 --- a/src/bonsai/bonsai/tool/profile.py +++ b/src/bonsai/bonsai/tool/profile.py @@ -125,6 +125,14 @@ class Profile(bonsai.core.tool.Profile): props = cls.get_profile_props() return tool.Blender.get_active_uilist_element(props.profiles, props.active_profile_index) + @classmethod + def replace_profile_in_profiles_ui(cls, old_profile_id: int, new_profile_id: int) -> None: + props = cls.get_profile_props() + for profile in props.profiles: + if profile.ifc_definition_id == old_profile_id: + profile.ifc_definition_id = new_profile_id + return + # Lengths are in meters. DEFAULT_PROFILE_ATTRS = { "IfcCircleProfileDef": { From b86e7355308e599a91e2339cd3f4399cc17734c7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 16:57:57 +0500 Subject: [PATCH 243/476] Fix breaking Systems UI with IfcElectricalCircuits --- src/bonsai/bonsai/bim/module/system/ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 0d500caa15..8c3d00dceb 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -39,6 +39,7 @@ SYSTEM_ICONS = { "IfcBuiltSystem": "MOD_BUILD", "IfcZone": "CUBE", } +SYSTEM_ICONS["IfcElectricalCircuit"] = SYSTEM_ICONS["IfcDistributionCircuit"] class BIM_PT_systems(Panel): From 71a7d0c0867f2ef95640e2456e5f2014a1c4c80a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 17:04:42 +0500 Subject: [PATCH 244/476] Fix missing IfcDistributionCircuit and IfcElectricalCircuit #6202 When checking if element is assignable to the system. --- .../ifcopenshell/util/system.py | 6 ++++++ src/ifcopenshell-python/test/util/test_system.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/system.py b/src/ifcopenshell-python/ifcopenshell/util/system.py index 3a5be9693f..ac19ab3c35 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/system.py +++ b/src/ifcopenshell-python/ifcopenshell/util/system.py @@ -39,6 +39,12 @@ group_types: dict[str, tuple[str, ...]] = { "IfcSystem": ("IfcProduct",), "IfcGroup": ("IfcObjectDefinition",), } +# Subclasses. +group_types["IfcDistributionCircuit"] = group_types["IfcDistributionSystem"] +# Replaced by IfcDistributionCircuit in IFC4, though it wasn't limited to IfcDistributionElements: +# "Usage of IfcElectricalCircuit is as for the supertype IfcSystem". +group_types["IfcElectricalCircuit"] = group_types["IfcSystem"] + FLOW_DIRECTION = Literal["SINK", "SOURCE", "SOURCEANDSINK", "NOTEDEFINED"] diff --git a/src/ifcopenshell-python/test/util/test_system.py b/src/ifcopenshell-python/test/util/test_system.py index 2796c3c3d2..892d9e2fdf 100644 --- a/src/ifcopenshell-python/test/util/test_system.py +++ b/src/ifcopenshell-python/test/util/test_system.py @@ -21,7 +21,23 @@ import test.bootstrap import ifcopenshell.api import ifcopenshell.api.root import ifcopenshell.api.system +import ifcopenshell.util.schema import ifcopenshell.util.system as subject +from typing import get_args + + +class TestValidateGroupTypes: + def test_run(self): + ifcsystem_classes = set() + for schema_name in get_args(ifcopenshell.util.schema.IFC_SCHEMA): + schema = ifcopenshell.schema_by_name(schema_name) + declaration = schema.declaration_by_name("IfcSystem") + declarations = ifcopenshell.util.schema.get_subtypes(declaration) + ifcsystem_classes.update(d.name() for d in declarations) + + used_classes = set(subject.group_types) + unused_classes = ifcsystem_classes - used_classes + assert not unused_classes class TestIsAssignable(test.bootstrap.IFC4): From 7b370e61425e673b3820982d2a68027c480da403 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 17:08:20 +0500 Subject: [PATCH 245/476] Fix operators missing execute after a663216 --- src/bonsai/bonsai/bim/module/system/operator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 5c9750dc23..25f3c9d00a 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -90,7 +90,7 @@ class DisableEditingSystem(bpy.types.Operator): bl_label = "Disable Editing System" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): core.disable_editing_system(tool.System) return {"FINISHED"} @@ -334,7 +334,7 @@ class LoadZones(bpy.types.Operator): bl_label = "Load Zones" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): props = tool.System.get_zone_props() props.zones.clear() for zone in tool.Ifc.get().by_type("IfcZone"): @@ -351,7 +351,7 @@ class UnloadZones(bpy.types.Operator): bl_label = "Unload Zones" bl_options = {"REGISTER", "UNDO"} - def _execute(self, context): + def execute(self, context): props = tool.System.get_zone_props() props.is_loaded = False return {"FINISHED"} From 197281eb5a326b3b0ee7ca097648dd5add91dd16 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 20:21:44 +0500 Subject: [PATCH 246/476] bonsai_translations - use utf-8 for opening files Critical on Windows as it's not using utf-8 by default --- src/bonsai/scripts/bonsai_translations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/scripts/bonsai_translations.py b/src/bonsai/scripts/bonsai_translations.py index 36292eff15..8378b2bcb0 100644 --- a/src/bonsai/scripts/bonsai_translations.py +++ b/src/bonsai/scripts/bonsai_translations.py @@ -184,7 +184,7 @@ def update_translations_from_po(po_directory: Path, translations_module: Path): for po_file_path in po_directory.glob("**/*.po"): lang = po_file_path.stem langs.add(lang) - with open(po_file_path, "r") as po_file: + with open(po_file_path, "r", encoding="utf-8") as po_file: current_chunk = [] for line in po_file: current_chunk.append(line) @@ -210,7 +210,7 @@ def update_translations_from_po(po_directory: Path, translations_module: Path): ret.append("}") - with open(translations_module / "translations.py", "w") as fo: + with open(translations_module / "translations.py", "w", encoding="utf-8") as fo: fo.write("\n".join(ret)) From f0e5430daabda21c0d2a57ac9167a00e2bb160bc Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 20:48:36 +0500 Subject: [PATCH 247/476] bonsai_translations - allow .po files going out of sync We had Chinese translation for Bonsai and some stub file for German and translation.py generation was failing with the error below. Chinese translation is more recent so it has some keys that German file hadn't and some keys were gone in the more recent version. In theory we should make all .po file in sync but let's skip it for now to keep things going. ``` Traceback (most recent call last): File "/home/runner/work/IfcOpenShell/IfcOpenShell/src/bonsai/scripts/bonsai_translations.py", line 379, in update_translations_from_po(po_directory=Path(args.input), translations_module=Path(args.output)) File "/home/runner/work/IfcOpenShell/IfcOpenShell/src/bonsai/scripts/bonsai_translations.py", line 203, in update_translations_from_po if (msgstr := msg.translations[lang]) in (None, ""): ~~~~~~~~~~~~~~~~^^^^^^ KeyError: 'de_DE' ``` --- src/bonsai/scripts/bonsai_translations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bonsai/scripts/bonsai_translations.py b/src/bonsai/scripts/bonsai_translations.py index 8378b2bcb0..a3053115fd 100644 --- a/src/bonsai/scripts/bonsai_translations.py +++ b/src/bonsai/scripts/bonsai_translations.py @@ -200,7 +200,8 @@ def update_translations_from_po(po_directory: Path, translations_module: Path): for lang in langs: ret.append(f'{tab}"{lang}": {{') for msgid, msg in translation_data.items(): - if (msgstr := msg.translations[lang]) in (None, ""): + # World isn't perfect and .po files can get out of sync, so let's make it permissive. + if (msgstr := msg.translations.get(lang)) in (None, ""): continue msgctxt = msg.msgctxt if not msgctxt: From 36744d391e0981a89f55b272cd061f3cd401ba04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 7 Mar 2025 14:14:28 -0300 Subject: [PATCH 248/476] Fix #6236. Product preview properties were not being cleared when data is empty. --- src/bonsai/bonsai/bim/module/model/polyline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 703c25e9e4..502a467a88 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -913,12 +913,12 @@ class PolylineOperator: data = get_generic_product_preview_data(context, relating_type) # Update properties so it can be used by the decorator - if not data: - return props = context.scene.BIMProductPreviewProperties props.verts.clear() props.edges.clear() props.tris.clear() + if not data: + return for vert in data["verts"]: v = props.verts.add() From 01b63d87d0d9219afd4b8f63b1c3ca730b3d41eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 7 Mar 2025 15:42:09 -0300 Subject: [PATCH 249/476] Fix issue with updating slab offset with negative direction sense. --- src/bonsai/bonsai/bim/module/model/slab.py | 7 ++----- src/bonsai/bonsai/bim/module/model/wall.py | 5 +---- src/bonsai/bonsai/tool/model.py | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 2c4357e308..54f8f57fa8 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -300,8 +300,9 @@ class DumbSlabPlaner: existing_x_angle = tool.Model.get_existing_x_angle(extrusion) existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 2*pi, tolerance=0.001) else existing_x_angle direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios) - offset_direction = direction_ratios.copy() + offset_direction = Vector((abs(direction_ratios.x), abs(direction_ratios.y), abs(direction_ratios.z))) # The offset direction doesn't change with direction sense perpendicular_depth = thickness * abs(1 / cos(existing_x_angle)) perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle)) / self.unit_scale @@ -311,19 +312,15 @@ class DumbSlabPlaner: ): # The extrusion direction is positive. If the layer_parameter is set to negative, # then the we change the extrusion direction. - # The offset direction must always be positive, so we keep it. if layer_params["direction_sense"] == "NEGATIVE": direction_ratios *= -1 - # offset_direction *= -1 elif (abs(existing_x_angle) > (pi / 2) and direction_ratios.z > 0) or ( abs(existing_x_angle) < (pi / 2) and direction_ratios.z < 0 ): # The extrusion direction is negative. If the layer_parameter is set to positive, # then the we change the extrusion direction. - # The offset direction must always be positive, so we change it too. if layer_params["direction_sense"] == "POSITIVE": direction_ratios *= -1 - offset_direction *= -1 extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios) extrusion.Depth = perpendicular_depth diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index adf8ee9725..484937b136 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -264,7 +264,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): layer_params = tool.Model.get_material_layer_parameters(element) perpendicular_depth = layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale perpendicular_offset = layer_params["offset"] * abs(1 / cos(x_angle)) / unit_scale - offset_direction = direction_ratios.copy() + offset_direction = Vector((abs(direction_ratios.x), abs(direction_ratios.y), abs(direction_ratios.z))) # The offset direction doesn't change with direction sense # Check angle and z direction to determine whether the extrusion direction is positive or negative if (abs(x_angle) < (pi / 2) and direction_ratios.z > 0) or ( @@ -272,7 +272,6 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): ): # The extrusion direction is positive. If the layer_parameter is set to negative, # then the we change the extrusion direction. - # The offset direction must always be positive, so we keep it. if layer_params["direction_sense"] == "NEGATIVE": direction_ratios *= -1 elif ((x_angle) > (pi / 2) and direction_ratios.z > 0) or ( @@ -280,10 +279,8 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): ): # The extrusion direction is negative. If the layer_parameter is set to positive, # then the we change the extrusion direction. - # The offset direction must always be positive, so we change it too. if layer_params["direction_sense"] == "POSITIVE": direction_ratios *= -1 - offset_direction *= -1 extrusion.ExtrudedDirection.DirectionRatios = tuple(direction_ratios) extrusion.Depth = perpendicular_depth diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index d304645d4c..ecccf21fe2 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -39,7 +39,7 @@ import bonsai.core.geometry import bonsai.core.tool import bonsai.tool as tool import bonsai.core.geometry as geometry -from math import atan, cos, degrees, radians +from math import atan, cos, degrees, radians, pi from mathutils import Matrix, Vector from copy import deepcopy from functools import partial @@ -2084,7 +2084,7 @@ class Model(bonsai.core.tool.Model): x, y, z = extrusion.ExtrudedDirection.DirectionRatios vector = Vector((0, 1)) x_angle = vector.angle_signed(Vector((y, z))) - return x_angle + return x_angle if z > 0 else (x_angle + pi) @classmethod def create_axis_curve(cls, obj: bpy.types.Object, grid_axis: ifcopenshell.entity_instance) -> None: From 48acfad8bb3168031c0c064c687466960d6e8a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 7 Mar 2025 15:49:32 -0300 Subject: [PATCH 250/476] Fix #6213. Adjust wall clipping from slabs to account for slab offset and direction sense. --- src/bonsai/bonsai/bim/module/model/wall.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 484937b136..13137c48a8 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -1735,7 +1735,18 @@ class DumbWallJoiner: extrusion = self.get_extrusion_data(representation) wall_dir = wall1.matrix_world.to_quaternion() @ extrusion["direction"] - slab_pt = slab2.matrix_world @ Vector((0, 0, 0)) + slab_element = tool.Ifc.get_entity(slab2) + slab_params = tool.Model.get_material_layer_parameters(slab_element) + slab_representation = ifcopenshell.util.representation.get_representation(slab_element, "Model", "Body", "MODEL_VIEW") + assert slab_representation + slab_extrusion = tool.Model.get_extrusion(slab_representation) + existing_x_angle = tool.Model.get_existing_x_angle(slab_extrusion) + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle + offset = slab_params["offset"] + if slab_params["direction_sense"] == "NEGATIVE": + offset -= slab_params["thickness"] + slab_pt = slab2.matrix_world @ Vector((0, 0, 0)) + Vector((0, 0, offset * abs(1 / cos(existing_x_angle)))) slab_dir = slab2.matrix_world.to_quaternion() @ Vector((0, 0, -1)) tops = [mathutils.geometry.intersect_line_plane(b, b + wall_dir, slab_pt, slab_dir) for b in bases] From 8c9a657a8c722d78c14e7f0aa4df8ae54673ac94 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 8 Mar 2025 11:13:26 +1100 Subject: [PATCH 251/476] See #1227. Check layer set direction when slicing. --- src/bonsai/bonsai/tool/loader.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index fb55035454..cd75f1bd3d 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1036,9 +1036,13 @@ class Loader(bonsai.core.tool.Loader): bm = bmesh.new() bm.from_mesh(mesh) prev_co = None - co = Vector((0.0, offset, 0.0)) # no = Vector((0.0, 1.0, 0.0)) - no = (cls.get_extrusion_vector(element).cross(Vector([1.0, 0.0, 0.0]))).normalized() + no = cls.get_extrusion_vector(element).normalized() + if usage and usage.LayerSetDirection == "AXIS2": + co = Vector((0.0, offset, 0.0)) + no = no.cross(Vector([1.0, 0.0, 0.0])) + else: + co = Vector((0.0, 0.0, offset)) # Cache this body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") styles = {} From 01ec7508146e3d251aab825af79f3447639315fe Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 8 Mar 2025 11:25:07 +1100 Subject: [PATCH 252/476] See #1227. New connect wall API function since this can be done generically. This will supersede the butt/mitre join functions we used to have. --- .../ifcopenshell/api/geometry/__init__.py | 2 + .../ifcopenshell/api/geometry/connect_wall.py | 63 +++++++++++++++++++ .../regenerate_wall_representation.py | 22 ++----- .../ifcopenshell/util/representation.py | 19 ++++++ 4 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/geometry/connect_wall.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py index 3dea99d8d2..0f17323634 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/__init__.py @@ -43,6 +43,7 @@ from .add_window_representation import add_window_representation from .assign_representation import assign_representation from .connect_element import connect_element from .connect_path import connect_path +from .connect_wall import connect_wall from .create_2pt_wall import create_2pt_wall from .disconnect_element import disconnect_element from .disconnect_path import disconnect_path @@ -72,6 +73,7 @@ __all__ = [ "assign_representation", "connect_element", "connect_path", + "connect_wall", "create_2pt_wall", "disconnect_element", "disconnect_path", diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_wall.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_wall.py new file mode 100644 index 0000000000..a84d70fff8 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_wall.py @@ -0,0 +1,63 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Dion Moult +# +# 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 . + +import numpy as np +import ifcopenshell +import ifcopenshell.api.owner +import ifcopenshell.guid +import ifcopenshell.util.element +import ifcopenshell.util.placement +import ifcopenshell.util.representation +from typing import Optional + + +def connect_wall( + file: ifcopenshell.file, + wall1: ifcopenshell.entity_instance, + wall2: ifcopenshell.entity_instance, + is_atpath: bool = False, +) -> Optional[ifcopenshell.entity_instance]: + matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall1.ObjectPlacement)) + matrix2 = ifcopenshell.util.placement.get_local_placement(wall2.ObjectPlacement) + axis1 = ifcopenshell.util.representation.get_reference_line(wall1) + axis2 = ifcopenshell.util.representation.get_reference_line(wall2) + axis2[0] = (matrix1i @ matrix2 @ np.concatenate((axis2[0], (0, 1))))[:2] + axis2[1] = (matrix1i @ matrix2 @ np.concatenate((axis2[1], (0, 1))))[:2] + midx = (axis1[0][0] + axis1[1][0]) / 2 + starty = axis2[0][1] + endy = axis2[1][1] + y = axis1[0][1] + + if (x := ifcopenshell.util.shape_builder.intersect_x_axis_2d(*axis2, y=y)) is None: + return + + wall1_end = "ATEND" if x > midx else "ATSTART" + if is_atpath: + wall2_end = "ATPATH" + elif abs(y - starty) < abs(y - endy): + wall2_end = "ATSTART" + else: + wall2_end = "ATEND" + + return ifcopenshell.api.geometry.connect_path( + file, + relating_element=wall1, + related_element=wall2, + relating_connection=wall1_end, + related_connection=wall2_end, + ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py index c5ffbe5998..c3f826c219 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py @@ -93,7 +93,7 @@ class Regenerator: layers = self.get_layers(wall) if not layers: return - reference = self.get_reference_line(wall) + reference = ifcopenshell.util.representation.get_reference_line(wall, self.fallback_length) self.reference_p1, self.reference_p2 = reference self.wall_vectors = self.get_wall_vectors(wall) axes = self.get_axes(wall, reference, layers, self.wall_vectors["a"]) @@ -300,8 +300,8 @@ class Regenerator: print("joining", wall1, layers1, connection1) print("to", wall2, layers2, connection2) - reference1 = self.get_reference_line(wall1) - reference2 = self.get_reference_line(wall2) + reference1 = ifcopenshell.util.representation.get_reference_line(wall1, self.fallback_length) + reference2 = ifcopenshell.util.representation.get_reference_line(wall2, self.fallback_length) wall_vectors2 = self.get_wall_vectors(wall2) axes1 = self.get_axes(wall1, reference1, layers1, self.wall_vectors["a"]) axes2 = self.get_axes(wall2, reference2, layers2, wall_vectors2["a"]) @@ -516,20 +516,6 @@ class Regenerator: results.append(layer) return results - def get_reference_line(self, wall): - if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"): - for item in ifcopenshell.util.representation.resolve_representation(axis).Items: - if item.is_a("IfcPolyline"): - points = item.Points - elif item.is_a("IfcIndexedPolyCurve"): - points = item.Points.CoordList - else: - continue - if points[0][0] < points[1][0]: # An axis always goes in the +X direction - return [np.array(points[0]), np.array(points[1])] - return [np.array(points[1]), np.array(points[0])] - return [np.array((0.0, 0.0)), np.array((self.fallback_length, 0.0))] - def get_wall_vectors(self, wall): if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): for item in ifcopenshell.util.representation.resolve_representation(body).Items: @@ -572,7 +558,7 @@ class Regenerator: axes = [[p.copy() for p in reference]] # Apply usage to convert the Reference line into MlsBase sense_factor = 1 - if (usage := ifcopenshell.util.element.get_material(wall)) and usage.is_a("IfcMaterialLayerSetUage"): + if (usage := ifcopenshell.util.element.get_material(wall)) and usage.is_a("IfcMaterialLayerSetUsage"): for point in axes[0]: point[1] += usage.OffsetFromReferenceLine sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 diff --git a/src/ifcopenshell-python/ifcopenshell/util/representation.py b/src/ifcopenshell-python/ifcopenshell/util/representation.py index b032501a20..1b1423cf1f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/representation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/representation.py @@ -461,3 +461,22 @@ def get_material_style( for style in item.Styles: if style.is_a(ifc_class): return style + + +def get_reference_line(wall: ifcopenshell.entity_instance, fallback_length: float = 1.0): + """Fetch the reference axis that goes in the +X direction + + :param wall: ifcopenshell.entity_instance + """ + if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"): + for item in ifcopenshell.util.representation.resolve_representation(axis).Items: + if item.is_a("IfcPolyline"): + points = item.Points + elif item.is_a("IfcIndexedPolyCurve"): + points = item.Points.CoordList + else: + continue + if points[0][0] < points[1][0]: # An axis always goes in the +X direction + return [np.array(points[0]), np.array(points[1])] + return [np.array(points[1]), np.array(points[0])] + return [np.array((0.0, 0.0)), np.array((fallback_length, 0.0))] From 47d11daa154ec8a61a467c4e57bb25929a4c0822 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 8 Mar 2025 11:26:27 +1100 Subject: [PATCH 253/476] See #1227. Small clean up of old sketch-wall based code which we no longer support. --- src/bonsai/bonsai/bim/module/model/wall.py | 61 ---------------------- 1 file changed, 61 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 13137c48a8..13b12ec8fc 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -637,14 +637,6 @@ class DumbWallGenerator: elif insertion_type == "CURSOR": return self.derive_from_cursor() - def has_sketch(self): - return ( - bpy.context.scene.grease_pencil - and len(bpy.context.scene.grease_pencil.layers) == 1 - and bpy.context.scene.grease_pencil.layers[0].info == "Note" - and bpy.context.scene.grease_pencil.layers[0].active_frame.strokes - ) - def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]: polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline polyline_points = polyline_data[0].polyline_points if polyline_data else [] @@ -676,7 +668,6 @@ class DumbWallGenerator: polyline_points = [slab_obj.matrix_world @ Vector((p[0], p[1], elevation)) for p in polyline_points] if not tool.Cad.is_counter_clockwise_order(polyline_points[0], polyline_points[1], polyline_points[2]): polyline_points = polyline_points[::-1] - is_polyline_closed = True walls = [] for i in range(len(polyline_points) - 1): vec1 = polyline_points[i] @@ -685,39 +676,6 @@ class DumbWallGenerator: walls.append(self.create_wall_from_2_points(coords)) return walls - def derive_from_sketch(self): - objs = [] - strokes = [] - layer = bpy.context.scene.grease_pencil.layers[0] - - for stroke in layer.active_frame.strokes: - if len(stroke.points) == 1: - continue - data = self.create_wall_from_2_points((stroke.points[0].co, stroke.points[-1].co), round=True) - if data: - strokes.append(data) - objs.append(data["obj"]) - - if len(objs) < 2: - return objs - - l_joins = set() - for stroke in strokes: - if not stroke["obj"]: - continue - for stroke2 in strokes: - if stroke2 == stroke or not stroke2["obj"]: - continue - if self.has_nearby_ends(stroke, stroke2): - wall_join = "-JOIN-".join(sorted([stroke["obj"].name, stroke2["obj"].name])) - if wall_join not in l_joins: - l_joins.add(wall_join) - DumbWallJoiner(stroke["obj"], stroke2["obj"]).join_L() - elif self.has_end_near_stroke(stroke, stroke2): - DumbWallJoiner(stroke["obj"], stroke2["obj"]).join_T() - bpy.context.scene.grease_pencil.layers.remove(layer) - return objs - def create_wall_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]: direction = coords[1] - coords[0] length = direction.length @@ -735,25 +693,6 @@ class DumbWallGenerator: data["obj"] = self.create_wall() return data - def has_end_near_stroke(self, stroke, stroke2): - point, distance = mathutils.geometry.intersect_point_line(stroke["coords"][0], *stroke2["coords"]) - if distance > 0 and distance < 1 and self.is_near(point, stroke["coords"][0]): - return True - point, distance = mathutils.geometry.intersect_point_line(stroke["coords"][1], *stroke2["coords"]) - if distance > 0 and distance < 1 and self.is_near(point, stroke["coords"][1]): - return True - - def has_nearby_ends(self, stroke, stroke2): - return ( - self.is_near(stroke["coords"][0], stroke2["coords"][0]) - or self.is_near(stroke["coords"][0], stroke2["coords"][1]) - or self.is_near(stroke["coords"][1], stroke2["coords"][0]) - or self.is_near(stroke["coords"][1], stroke2["coords"][1]) - ) - - def is_near(self, point1, point2): - return (point1 - point2).length < 0.1 - def derive_from_cursor(self) -> bpy.types.Object: RAYCAST_PRECISION = 0.01 self.location = bpy.context.scene.cursor.location From 5d9528d791defb1fb31243c3d730e432d7d3c865 Mon Sep 17 00:00:00 2001 From: Massimo Fabbro Date: Fri, 28 Feb 2025 16:19:10 +0100 Subject: [PATCH 254/476] Fix #6168. Now reload link button works It is fixed also an inconsistency problem when use relative path is True --- src/bonsai/bonsai/bim/module/project/operator.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 2b41699261..57262a7da5 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1281,6 +1281,13 @@ class UnloadLink(bpy.types.Operator): # Let's assume that user might delete it. if empty_handle := link.empty_handle: bpy.data.objects.remove(empty_handle) + + #following lines removes the library also when use_relative_path=True, otherwise it doesn't + libraries = bpy.data.libraries + for library in libraries: + if library.name == self.filepath + ".cache.blend": + bpy.data.libraries.remove(library) + link.is_loaded = False if not any([l.is_loaded for l in links]): @@ -1442,6 +1449,12 @@ class ReloadLink(bpy.types.Operator): for library in get_linked_ifcs(): library.reload() + + is_abs = os.path.isabs(Path(self.filepath)) + use_relative_path = not is_abs + bpy.ops.bim.unlink_ifc(filepath = self.filepath) + status = bpy.ops.bim.link_ifc(filepath=self.filepath, use_cache=False, use_relative_path = use_relative_path) + return {"FINISHED"} From af2dd1d7c6dc2d4970754ff766a7788fe8576bd8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 9 Mar 2025 08:25:32 +1100 Subject: [PATCH 255/476] See #1227. Start to integrate into Bonsai. Basic testing of join/unjoin/extend/split/flip done. --- src/bonsai/bonsai/bim/module/model/wall.py | 588 ++++-------------- .../bonsai/bim/module/model/workspace.py | 15 +- src/bonsai/bonsai/core/model.py | 5 +- 3 files changed, 108 insertions(+), 500 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 13b12ec8fc..136d09bfd8 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -21,6 +21,7 @@ import bpy import copy import math +import numpy as np import ifcopenshell import ifcopenshell.api import ifcopenshell.util.unit @@ -264,7 +265,9 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): layer_params = tool.Model.get_material_layer_parameters(element) perpendicular_depth = layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale perpendicular_offset = layer_params["offset"] * abs(1 / cos(x_angle)) / unit_scale - offset_direction = Vector((abs(direction_ratios.x), abs(direction_ratios.y), abs(direction_ratios.z))) # The offset direction doesn't change with direction sense + offset_direction = Vector( + (abs(direction_ratios.x), abs(direction_ratios.y), abs(direction_ratios.z)) + ) # The offset direction doesn't change with direction sense # Check angle and z direction to determine whether the extrusion direction is positive or negative if (abs(x_angle) < (pi / 2) and direction_ratios.z > 0) or ( @@ -356,7 +359,7 @@ class AddWallsFromSlab(bpy.types.Operator, tool.Ifc.Operator): if walls: for wall1, wall2 in zip(walls, walls[1:] + [walls[0]]): - DumbWallJoiner().join_V(wall2["obj"], wall1["obj"]) + DumbWallJoiner().connect(wall2["obj"], wall1["obj"]) class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): @@ -402,10 +405,10 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): if walls: if is_polyline_closed: for wall1, wall2 in zip(walls, walls[1:] + [walls[0]]): - DumbWallJoiner().join_V(wall2["obj"], wall1["obj"]) + DumbWallJoiner().connect(wall2["obj"], wall1["obj"]) else: for wall1, wall2 in zip(walls[:-1], walls[1:]): - DumbWallJoiner().join_V(wall2["obj"], wall1["obj"]) + DumbWallJoiner().connect(wall2["obj"], wall1["obj"]) def modal(self, context, event): return IfcStore.execute_ifc_operator(self, context, event, method="MODAL") @@ -590,11 +593,19 @@ class DumbWallRecalculator: queue: set[tuple[ifcopenshell.entity_instance, bpy.types.Object]] = set() for wall in walls: element = tool.Ifc.get_entity(wall) + if tool.Ifc.is_moved(wall): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall) queue.add((element, wall)) for rel in getattr(element, "ConnectedTo", []): - queue.add((rel.RelatedElement, tool.Ifc.get_object(rel.RelatedElement))) + obj = tool.Ifc.get_object(rel.RelatedElement) + if tool.Ifc.is_moved(obj): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + queue.add((rel.RelatedElement, obj)) for rel in getattr(element, "ConnectedFrom", []): - queue.add((rel.RelatingElement, tool.Ifc.get_object(rel.RelatingElement))) + obj = tool.Ifc.get_object(rel.RelatingElement) + if tool.Ifc.is_moved(obj): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + queue.add((rel.RelatingElement, obj)) joiner = DumbWallJoiner() for element, wall in queue: if tool.Model.get_usage_type(element) == "LAYER2" and wall: @@ -891,12 +902,15 @@ class DumbWallJoiner: element1 = tool.Ifc.get_entity(wall1) if not element1: return + + if tool.Ifc.is_moved(wall1): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1) + axis1 = tool.Model.get_wall_axis(wall1) axis2 = copy.deepcopy(axis1) intersect, cut_percentage = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"]) if cut_percentage < 0 or cut_percentage > 1 or tool.Cad.is_x(cut_percentage, (0, 1)): return - connection = "ATEND" if cut_percentage > 0.5 else "ATSTART" wall2 = self.duplicate_wall(wall1) element2 = tool.Ifc.get_entity(wall2) @@ -961,114 +975,41 @@ class DumbWallJoiner: # The filling should be moved from element1 to element2. FilledOpeningGenerator().generate(filling_obj, wall2, target=filling_obj.matrix_world.translation) - axis1["reference"][1] = intersect - axis2["reference"][0] = intersect + p1, p2 = ifcopenshell.util.representation.get_reference_line(element1) + p3 = (wall1.matrix_world.inverted() @ intersect.to_3d()).to_2d() / unit_scale + self.set_axis(element1, p1, p3) + self.set_axis(element2, p3, p2) - # Create a connection between the walls - wall1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART" - wall2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2["reference"]) > 0.5 else "ATSTART" - - ifcopenshell.api.run( - "geometry.connect_path", - tool.Ifc.get(), - relating_element=element1, - related_element=element2, - relating_connection=wall1_end, - related_connection=wall2_end, - description="MITRE", - ) - - self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"]) - self.recreate_wall(element2, wall2, axis2["reference"], axis2["reference"]) + self.recreate_wall(element1, wall1) + self.recreate_wall(element2, wall2) def flip(self, wall1: bpy.types.Object) -> None: - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - if tool.Ifc.is_moved(wall1): bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1) - element1 = tool.Ifc.get_entity(wall1) - if not element1 or tool.Model.get_usage_type(element1) != "LAYER2": + if ( + not (element1 := tool.Ifc.get_entity(wall1)) + or not (usage := ifcopenshell.util.element.get_material(element1)) + or not usage.is_a("IfcMaterialLayerSetUsage") + or usage.LayerSetDirection != "AXIS2" + ): return - for rel in element1.ConnectedTo: - if rel.is_a("IfcRelConnectsPathElements") and rel.RelatingConnectionType in ["ATSTART", "ATEND"]: - rel.RelatingConnectionType = "ATSTART" if rel.RelatingConnectionType == "ATEND" else "ATEND" - for rel in element1.ConnectedFrom: - if rel.is_a("IfcRelConnectsPathElements") and rel.RelatedConnectionType in ["ATSTART", "ATEND"]: - rel.RelatedConnectionType = "ATSTART" if rel.RelatedConnectionType == "ATEND" else "ATEND" + thickness = sum([l.LayerThickness for l in usage.ForLayerSet.MaterialLayers]) + if usage.DirectionSense == "POSITIVE": + usage.DirectionSense = "NEGATIVE" + else: + thickness *= -1 + usage.DirectionSense = "POSITIVE" - layers1 = tool.Model.get_material_layer_parameters(element1) - axis1 = tool.Model.get_wall_axis(wall1, layers1) - axis1["reference"][0], axis1["reference"][1] = axis1["reference"][1], axis1["reference"][0] - - flip_matrix = Matrix.Rotation(pi, 4, "Z") - wall1.matrix_world = wall1.matrix_world @ flip_matrix - wall1.matrix_world[0][3], wall1.matrix_world[1][3] = axis1["reference"][0] - bpy.context.view_layer.update() - - # The wall should flip, but all openings and fills should stay and shift to the opposite axis - opening_matrixes = {} - filling_matrixes = {} - for opening in [r.RelatedOpeningElement for r in element1.HasOpenings]: - opening_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement).tolist()) - opening_matrix.translation *= unit_scale - location = opening_matrix.translation - location_on_base = tool.Cad.point_on_edge(location, axis1["base"]) - location_on_side = tool.Cad.point_on_edge(location, axis1["side"]) - if (location_on_base - location).length < (location_on_side - location).length: - axis_offset = location_on_side - location_on_base - offset_from_axis = location_on_base - location - opening_matrix.translation = location_on_base - axis_offset - offset_from_axis - else: - axis_offset = location_on_side - location_on_base - offset_from_axis = location_on_side - location - opening_matrix.translation = location_on_side - axis_offset - offset_from_axis - opening_matrixes[opening] = opening_matrix - - for filling in [r.RelatedBuildingElement for r in opening.HasFillings]: - filling_obj = tool.Ifc.get_object(filling) - filling_matrix = filling_obj.matrix_world.copy() - - location = filling_matrix.translation - location_on_base = tool.Cad.point_on_edge(location, axis1["base"]) - location_on_side = tool.Cad.point_on_edge(location, axis1["side"]) - if (location_on_base - location).length < (location_on_side - location).length: - axis_offset = location_on_side - location_on_base - offset_from_axis = location_on_base - location - filling_matrix.translation = location_on_base - axis_offset - offset_from_axis - else: - axis_offset = location_on_side - location_on_base - offset_from_axis = location_on_side - location - filling_matrix.translation = location_on_side - axis_offset - offset_from_axis - filling_matrixes[filling] = filling_matrix - - self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"]) - DumbWallRecalculator().recalculate([wall1]) - - for opening in [r.RelatedOpeningElement for r in element1.HasOpenings]: - opening_matrix = opening_matrixes[opening] - ifcopenshell.api.run( - "geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=opening_matrix - ) - for filling in [r.RelatedBuildingElement for r in opening.HasFillings]: - filling_matrix = filling_matrixes[filling] - filling_obj = tool.Ifc.get_object(filling) - filling_obj.matrix_world = filling_matrix - - if filling_matrixes: - bpy.context.view_layer.update() - - body = ifcopenshell.util.representation.get_representation(element1, "Model", "Body", "MODEL_VIEW") - bonsai.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=wall1, - representation=body, - should_reload=True, - is_global=True, - should_sync_changes_first=False, + matrix = ifcopenshell.util.placement.get_local_placement(element1.ObjectPlacement) + offset = matrix[:, 1] * thickness + matrix[:, 3] += offset + ifcopenshell.api.geometry.edit_object_placement( + tool.Ifc.get(), product=element1, matrix=matrix, is_si=False, should_transform_children=False ) + self.import_position(element1, wall1) + self.recreate_wall(element1, wall1) def merge(self, wall1, wall2): element1 = tool.Ifc.get_entity(wall1) @@ -1159,62 +1100,56 @@ class DumbWallJoiner: self.recreate_wall(element1, wall1) - def join_L(self, wall1, wall2): - element1 = tool.Ifc.get_entity(wall1) - element2 = tool.Ifc.get_entity(wall2) - axis1 = tool.Model.get_wall_axis(wall1) - axis2 = tool.Model.get_wall_axis(wall2) - intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"]) - if intersect: - intersect, _ = intersect + def set_axis(self, wall, p1, p2): + axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW") + builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) + item = builder.polyline([p1, p2]) + rep = builder.get_representation(axis, items=[item]) + if old_rep := ifcopenshell.util.representation.get_representation(wall, axis): + ifcopenshell.util.element.replace_element(old_rep, rep) + ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_rep) else: - return - wall1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART" - wall2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2["reference"]) > 0.5 else "ATSTART" + ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) - ifcopenshell.api.run( - "geometry.connect_path", - tool.Ifc.get(), - relating_element=element1, - related_element=element2, - relating_connection=wall1_end, - related_connection=wall2_end, - description="BUTT", - ) - - self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"]) - self.recreate_wall(element2, wall2, axis2["reference"], axis2["reference"]) + def import_position(self, element, obj): + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) + matrix[:, 3] *= unit_scale + obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix) + tool.Geometry.record_object_position(obj) def join_E(self, wall1, target): + if tool.Ifc.is_moved(wall1): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1) element1 = tool.Ifc.get_entity(wall1) - - axis1 = tool.Model.get_wall_axis(wall1) - intersect, connection = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"]) + p1, p2 = ifcopenshell.util.representation.get_reference_line(element1) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + target = (wall1.matrix_world.inverted() @ target).to_2d() / unit_scale + intersect, connection = mathutils.geometry.intersect_point_line(target, p1, p2) connection = "ATEND" if connection > 0.5 else "ATSTART" ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type=connection) - axis = copy.deepcopy(axis1["reference"]) - body = copy.deepcopy(axis1["reference"]) - axis[1 if connection == "ATEND" else 0] = intersect - body[1 if connection == "ATEND" else 0] = intersect - - self.recreate_wall(element1, wall1, axis, body) + if connection == "ATEND": + self.set_axis(element1, p1, intersect) + else: + self.set_axis(element1, intersect, p2) + self.recreate_wall(element1, wall1) def set_length(self, wall1, si_length): element1 = tool.Ifc.get_entity(wall1) if not element1: return + if tool.Ifc.is_moved(wall1): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1) ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND") - axis1 = tool.Model.get_wall_axis(wall1) - axis = copy.deepcopy(axis1["reference"]) - body = copy.deepcopy(axis1["reference"]) - end = (wall1.matrix_world @ Vector((si_length, 0, 0))).to_2d() - axis[1] = end - body[1] = end - self.recreate_wall(element1, wall1, axis, body) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + p1, p2 = ifcopenshell.util.representation.get_reference_line(element1) + p2[0] = p1[0] + si_length / unit_scale + self.set_axis(element1, p1, p2) + self.recreate_wall(element1, wall1) def join_T(self, wall1, wall2): element1 = tool.Ifc.get_entity(wall1) @@ -1240,132 +1175,30 @@ class DumbWallJoiner: self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"]) - def join_V(self, wall1, wall2): - element1 = tool.Ifc.get_entity(wall1) - element2 = tool.Ifc.get_entity(wall2) - axis1 = tool.Model.get_wall_axis(wall1) - axis2 = tool.Model.get_wall_axis(wall2) - intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"]) - # Allow connecting contiguous walls - if not intersect: - for v1 in axis1["reference"]: - for v2 in axis2["reference"]: - if tool.Cad.are_vectors_equal(v1, v2, 1e-5): - intersect = (v1, v2) - if intersect: - intersect, _ = intersect - else: - return - wall1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART" - wall2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2["reference"]) > 0.5 else "ATSTART" - - ifcopenshell.api.run( - "geometry.connect_path", - tool.Ifc.get(), - relating_element=element1, - related_element=element2, - relating_connection=wall1_end, - related_connection=wall2_end, - description="MITRE", - ) - - self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"]) - self.recreate_wall(element2, wall2, axis2["reference"], axis2["reference"]) + def connect(self, obj1, obj2): + wall1 = tool.Ifc.get_entity(obj1) + wall2 = tool.Ifc.get_entity(obj2) + if tool.Ifc.is_moved(obj1): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj1) + if tool.Ifc.is_moved(obj2): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj2) + ifcopenshell.api.geometry.connect_wall(tool.Ifc.get(), wall1=wall1, wall2=wall2) + self.recreate_wall(wall1, obj1) + self.recreate_wall(wall2, obj2) def recreate_wall(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, axis=None, body=None) -> None: - if axis is None or body is None: - axis = body = tool.Model.get_wall_axis(obj)["reference"] - self.axis = copy.deepcopy(axis) - self.body = copy.deepcopy(body) - representation = tool.Geometry.get_active_representation(obj) - assert representation - extrusion_data = self.get_extrusion_data(representation) - height = extrusion_data["height"] - x_angle = extrusion_data["x_angle"] - self.clippings = [] - layers = tool.Model.get_material_layer_parameters(element) - - for rel in element.ConnectedTo: - if rel.is_a("IfcRelConnectsPathElements"): - connection = rel.RelatingConnectionType - other = tool.Ifc.get_object(rel.RelatedElement) - if connection not in ["ATPATH", "NOTDEFINED"]: - self.join( - obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description - ) - for rel in element.ConnectedFrom: - if rel.is_a("IfcRelConnectsPathElements"): - connection = rel.RelatedConnectionType - other = tool.Ifc.get_object(rel.RelatingElement) - if connection not in ["ATPATH", "NOTDEFINED"]: - self.join( - obj, - other, - connection, - rel.RelatingConnectionType, - is_relating=False, - description=rel.Description, - ) - - previous_matrix = obj.matrix_world.copy() - previous_origin = previous_matrix.translation.xy - obj.matrix_world.translation.xy = self.body[0] - bpy.context.view_layer.update() - - for rel in element.ConnectedFrom: - if rel.is_a() == "IfcRelConnectsElements": - height = self.clip(obj, tool.Ifc.get_object(rel.RelatingElement)) - - new_matrix = copy.deepcopy(obj.matrix_world) - new_matrix.invert() - - for clipping in self.clippings: - if clipping["operand_type"] == "IfcHalfSpaceSolid": - clipping["matrix"] = new_matrix @ clipping["matrix"] - - length = (self.body[1] - self.body[0]).length - - if self.axis_context: - axis = [(new_matrix @ a.to_3d()).to_2d() for a in self.axis] - new_axis = ifcopenshell.api.run( - "geometry.add_axis_representation", tool.Ifc.get(), context=self.axis_context, axis=axis - ) - old_axis = ifcopenshell.util.representation.get_representation(element, "Plan", "Axis", "GRAPH_VIEW") - if old_axis: - for inverse in tool.Ifc.get().get_inverse(old_axis): - ifcopenshell.util.element.replace_attribute(inverse, old_axis, new_axis) - bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_axis) - else: - ifcopenshell.api.run( - "geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_axis - ) - - new_body = ifcopenshell.api.run( - "geometry.add_wall_representation", - tool.Ifc.get(), - context=self.body_context, - length=length, - height=height, - x_angle=x_angle, - direction_sense=layers["direction_sense"], - offset=layers["offset"], - thickness=layers["thickness"], - clippings=self.clippings, - booleans=tool.Model.get_manual_booleans(element), + rep = ifcopenshell.api.geometry.regenerate_wall_representation(tool.Ifc.get(), element) + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=rep, + should_reload=True, + is_global=True, + should_sync_changes_first=False, ) - - old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") - if old_body: - for inverse in tool.Ifc.get().get_inverse(old_body): - ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body) - assert isinstance(mesh := obj.data, bpy.types.Mesh) - tool.Ifc.link(new_body, mesh) - mesh.name = tool.Loader.get_mesh_name(new_body) - bonsai.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body) - else: - ifcopenshell.api.run( - "geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_body - ) + tool.Geometry.record_object_materials(obj) + return wall_moved = tool.Ifc.is_moved(obj) if wall_moved: @@ -1441,222 +1274,7 @@ class DumbWallJoiner: break return results - def join(self, wall1, wall2, connection1, connection2, is_relating=True, description="BUTT"): - element1 = tool.Ifc.get_entity(wall1) - element2 = tool.Ifc.get_entity(wall2) - layers1 = tool.Model.get_material_layer_parameters(element1) - layers2 = tool.Model.get_material_layer_parameters(element2) - axis1 = tool.Model.get_wall_axis(wall1, layers1) - axis2 = tool.Model.get_wall_axis(wall2, layers2) - body1 = ifcopenshell.util.representation.get_representation(element1, "Model", "Body", "MODEL_VIEW") - body2 = ifcopenshell.util.representation.get_representation(element2, "Model", "Body", "MODEL_VIEW") - extrusion1 = self.get_extrusion_data(body1) - extrusion2 = self.get_extrusion_data(body2) - direction1 = (wall1.matrix_world.to_quaternion() @ extrusion1["direction"]).normalized() - direction2 = (wall2.matrix_world.to_quaternion() @ extrusion2["direction"]).normalized() - height1 = extrusion1["height"] * self.unit_scale - height2 = extrusion2["height"] * self.unit_scale - depth1 = direction1 * height1 - depth2 = direction2 * height2 - normal1 = (axis1["base"][1] - axis1["base"][0]).to_3d().normalized().cross(direction1) - normal2 = (axis2["base"][1] - axis2["base"][0]).to_3d().normalized().cross(direction2) - - angle = tool.Cad.angle_edges(axis1["reference"], axis2["reference"], signed=True, degrees=True) - if tool.Cad.is_x(abs(angle), (0, 180), tolerance=0.001): - return False - - # Work out axis line - intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"]) - if intersect: - intersect, _ = intersect - else: - return False - - proposed_axis = [self.axis[0], intersect] if connection1 == "ATEND" else [intersect, self.axis[1]] - - if tool.Cad.is_x(tool.Cad.angle_edges(self.axis, proposed_axis, degrees=True), 180, tolerance=0.001): - # The user has moved the wall into an invalid position that cannot connect at the desired end - return False - - self.axis = proposed_axis - - # Work out body - - # Bottom and top plane point - bp1 = wall1.matrix_world @ Vector(wall1.bound_box[0]) - bp2 = wall2.matrix_world @ Vector(wall2.bound_box[0]) - tp1 = wall1.matrix_world @ Vector(wall1.bound_box[1]) - - # Axis lines on bottom, for reference, base, and side axes - def to_3d_axis(axis, z): - return (Vector((*axis[0], z)), Vector((*axis[1], z))) - - bra1 = to_3d_axis(axis1["reference"], bp1.z) - bba1 = to_3d_axis(axis1["base"], bp1.z) - tba1 = to_3d_axis(axis1["base"], tp1.z) - bsa1 = to_3d_axis(axis1["side"], bp1.z) - bba2 = to_3d_axis(axis2["base"], bp2.z) - bsa2 = to_3d_axis(axis2["side"], bp2.z) - - # Intersecting the walls sides defined by planes gives 4 lines of intersection - # Line point, and line direction - lpb1, ldb1 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bba2[0], normal2) - lpb2, ldb2 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bsa2[0], normal2) - lps1, lds1 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bba2[0], normal2) - lps2, lds2 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bsa2[0], normal2) - - # Intersecting the 4 lines gives the 8 possible verts of intersection - # 4 on bottom, and 4 on top. 4 on our base line, 4 on our side line. - # Diagram: https://i.imgur.com/jwWx2Ox.png - # NOTE: bb/bs always equal lpb/lps? - bb1 = mathutils.geometry.intersect_line_plane(lpb1, lpb1 + ldb1, bp1, Vector((0, 0, 1))) - bb2 = mathutils.geometry.intersect_line_plane(lpb2, lpb2 + ldb2, bp1, Vector((0, 0, 1))) - bs1 = mathutils.geometry.intersect_line_plane(lps1, lps1 + lds1, bp1, Vector((0, 0, 1))) - bs2 = mathutils.geometry.intersect_line_plane(lps2, lps2 + lds2, bp1, Vector((0, 0, 1))) - - # similar to bb/bs but also have local z offset - tb1 = mathutils.geometry.intersect_line_plane(lpb1, lpb1 + ldb1, tp1, Vector((0, 0, 1))) - tb2 = mathutils.geometry.intersect_line_plane(lpb2, lpb2 + ldb2, tp1, Vector((0, 0, 1))) - ts1 = mathutils.geometry.intersect_line_plane(lps1, lps1 + lds1, tp1, Vector((0, 0, 1))) - ts2 = mathutils.geometry.intersect_line_plane(lps2, lps2 + lds2, tp1, Vector((0, 0, 1))) - - # Let's distinguish the 8 points by whether they are nearer or further away from the other end - # These 8 points will be used to find the final body position and clippings. - connected_at_end = connection1 == "ATEND" - i = 0 if connected_at_end else 1 - - def get_closest_and_furthest_vectors(ref_point_2d, vectors, clamp_axis=None): - def clamp_point_by_direction(point, edge): - percent = tool.Cad.edge_percent(point, edge) - if percent < 0: - return edge[0] - return point - - # When there is a small angle between walls, intersection points can occur outside the wall's axis. - # Which can lead to inaccuracies - therefore we bottom clamp them to stay within the axis - if clamp_axis: - # if wall connected at the start then reference point will be at the end - # therefore we reverse the axis - if not connected_at_end: - clamp_axis = clamp_axis[::-1] - vectors = tuple([clamp_point_by_direction(v, clamp_axis) for v in vectors]) - - return tool.Cad.closest_and_furthest_vectors(ref_point_2d.to_3d(), vectors) - - bbn, bbf = get_closest_and_furthest_vectors(axis1["base"][i], (bb1, bb2), bba1) - bsn, bsf = get_closest_and_furthest_vectors(axis1["side"][i], (bs1, bs2)) - tbn, tbf = get_closest_and_furthest_vectors(axis1["base"][i], (tb1, tb2), tba1) - tsn, tsf = get_closest_and_furthest_vectors(axis1["side"][i], (ts1, ts2)) - - j = 1 if connected_at_end else 0 - if description == "MITRE": - # Mitre joints are an unofficial convention - bsf_ = tool.Cad.point_on_edge(bsf, bba1) - tbf_ = tool.Cad.point_on_edge(tbf, bba1) - tsf_ = tool.Cad.point_on_edge(tsf, bba1) - new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_)) - new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_)) - new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tsf_)).copy() - self.body[j] = tool.Cad.point_on_edge(new_body, bra1).to_2d() - - if connection1 == connection2: - if (connected_at_end and angle > 0) or (not connected_at_end and angle < 0): - if layers1["direction_sense"] == "POSITIVE": - pt = bbf.to_2d().to_3d() - x_axis = bsn - bbf - y_axis = tbf - bbf - else: - pt = bsf.to_2d().to_3d() - x_axis = bbn - bsf - y_axis = tsf - bsf - else: - if layers1["direction_sense"] == "POSITIVE": - pt = bbn.to_2d().to_3d() - x_axis = bsf - bbn - y_axis = tbn - bbn - else: - pt = bsn.to_2d().to_3d() - x_axis = bbf - bsn - y_axis = tsn - bsn - else: - if (connected_at_end and angle < 0) or (not connected_at_end and angle > 0): - if layers1["direction_sense"] == "POSITIVE": - pt = bbf.to_2d().to_3d() - x_axis = bsn - bbf - y_axis = tbf - bbf - else: - pt = bsf.to_2d().to_3d() - x_axis = bbn - bsf - y_axis = tsf - bsf - else: - if layers1["direction_sense"] == "POSITIVE": - pt = bbn.to_2d().to_3d() - x_axis = bsf - bbn - y_axis = tbn - bbn - else: - pt = bsn.to_2d().to_3d() - x_axis = bbf - bsn - y_axis = tsn - bsn - - if connection1 != "ATEND": - y_axis *= -1 - - x_axis.normalize() - y_axis.normalize() - z_axis = x_axis.cross(y_axis) - y_axis = z_axis.cross(x_axis) - - self.clippings.append( - { - "type": "IfcBooleanClippingResult", - "operand_type": "IfcHalfSpaceSolid", - "matrix": self.create_matrix(pt, x_axis, y_axis, z_axis), - } - ) - else: - # This is the standard L and T joints described by IFC - if ( - tool.Cad.is_x(abs(angle), (90, 270), tolerance=0.001) - and not extrusion1["is_sloped"] - and not extrusion2["is_sloped"] - ): - if is_relating: - self.body[j] = tool.Cad.point_on_edge(bbf, bra1).to_2d() - else: - self.body[j] = tool.Cad.point_on_edge(bbn, bra1).to_2d() - return True - - bsf_ = tool.Cad.point_on_edge(bsf, bba1) - tbf_ = tool.Cad.point_on_edge(tbf, bba1) - tsf_ = tool.Cad.point_on_edge(tsf, bba1) - new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_)).copy() - new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_)).copy() - new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tsf_)).copy() - self.body[j] = tool.Cad.point_on_edge(new_body, bra1).to_2d() - - if is_relating: - pt = bbf.to_2d().to_3d() - x_axis = bsf - bbf - y_axis = tbf - bbf - else: - pt = bbn.to_2d().to_3d() - x_axis = bsn - bbn - y_axis = tbn - bbn - if connection1 != "ATEND": - y_axis *= -1 - z_axis = x_axis.cross(y_axis) - y_axis = z_axis.cross(x_axis) - - self.clippings.append( - { - "type": "IfcBooleanClippingResult", - "operand_type": "IfcHalfSpaceSolid", - "matrix": self.create_matrix(pt, x_axis, y_axis, z_axis), - } - ) - - return True - + # TODO reimplement in new version and deprecate def clip(self, wall1: bpy.types.Object, slab2: bpy.types.Object) -> float: """returns height of the clipped wall, adds clipping plane to `clippings`""" element1 = tool.Ifc.get_entity(wall1) @@ -1676,7 +1294,9 @@ class DumbWallJoiner: slab_element = tool.Ifc.get_entity(slab2) slab_params = tool.Model.get_material_layer_parameters(slab_element) - slab_representation = ifcopenshell.util.representation.get_representation(slab_element, "Model", "Body", "MODEL_VIEW") + slab_representation = ifcopenshell.util.representation.get_representation( + slab_element, "Model", "Body", "MODEL_VIEW" + ) assert slab_representation slab_extrusion = tool.Model.get_extrusion(slab_representation) existing_x_angle = tool.Model.get_existing_x_angle(slab_extrusion) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 87584acb04..bf56626160 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -823,11 +823,7 @@ class EditObjectUI: add_layout_hotkey_operator(row, "Extend", "S_E", "Extends/reduces element to 3D cursor", ui_context) row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator( - row, "Butt", "S_T", "Intersects two non-parallel elements to a butt corner junction", ui_context - ) - row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row - add_layout_hotkey_operator( - row, "Mitre", "S_Y", "Intersects two non-parallel elements to a mitred corner junction", ui_context + row, "Trim", "S_T", "Connects and trims two non-parallel elements into a joint", ui_context ) row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator(row, "Unjoin Walls", "S_U", "", ui_context) @@ -1336,7 +1332,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return if self.active_material_usage == "LAYER2": try: - core.join_walls_LV(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model, join_type="L") + core.join_walls_LV(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model) except core.RequireTwoWallsError as e: self.report({"ERROR"}, str(e)) elif self.active_material_usage == "PROFILE": @@ -1362,12 +1358,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): def hotkey_S_Y(self): if not bpy.context.selected_objects: return - if self.active_material_usage == "LAYER2": - try: - core.join_walls_LV(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model, join_type="V") - except core.RequireTwoWallsError as e: - self.report({"ERROR"}, str(e)) - elif self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"): + if self.active_class in ("IfcDuctSegment", "IfcPipeSegment", "IfcCableCarrierSegment", "IfcCableSegment"): bpy.ops.bim.fit_flow_segments() elif self.active_material_usage == "PROFILE": bpy.ops.bim.extend_profile(join_type="V") diff --git a/src/bonsai/bonsai/core/model.py b/src/bonsai/bonsai/core/model.py index f3f2d6932a..fe9851e1f9 100644 --- a/src/bonsai/bonsai/core/model.py +++ b/src/bonsai/bonsai/core/model.py @@ -60,10 +60,7 @@ def join_walls_LV( for obj in selected_objs: geometry.clear_scale(obj) - if join_type == "L": - joiner.join_L(another_selected_object, active_obj) - elif join_type == "V": - joiner.join_V(another_selected_object, active_obj) + joiner.connect(another_selected_object, active_obj) def join_walls_TZ(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None: From e5027e0be8f9501fcf99d5479742b0baf283938f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 9 Mar 2025 08:52:05 +1100 Subject: [PATCH 256/476] See #1227. Implement updating of object position. --- src/bonsai/bonsai/bim/module/model/wall.py | 14 +++--- .../api/geometry/edit_object_placement.py | 21 +++++++++ .../regenerate_wall_representation.py | 45 +++++++++++++++---- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 136d09bfd8..ac44904e82 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -1008,7 +1008,6 @@ class DumbWallJoiner: ifcopenshell.api.geometry.edit_object_placement( tool.Ifc.get(), product=element1, matrix=matrix, is_si=False, should_transform_children=False ) - self.import_position(element1, wall1) self.recreate_wall(element1, wall1) def merge(self, wall1, wall2): @@ -1111,13 +1110,6 @@ class DumbWallJoiner: else: ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) - def import_position(self, element, obj): - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) - matrix[:, 3] *= unit_scale - obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix) - tool.Geometry.record_object_position(obj) - def join_E(self, wall1, target): if tool.Ifc.is_moved(wall1): bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1) @@ -1198,6 +1190,12 @@ class DumbWallJoiner: should_sync_changes_first=False, ) tool.Geometry.record_object_materials(obj) + + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) + matrix[:, 3] *= unit_scale + obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix) + tool.Geometry.record_object_position(obj) return wall_moved = tool.Ifc.is_moved(obj) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py index f5a52f13a4..11d53364b3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py @@ -34,6 +34,27 @@ def edit_object_placement( is_si: bool = True, should_transform_children: bool = False, ) -> ifcopenshell.entity_instance: + """Changes the object placement matrix of an element + + The placement matrix is a 4x4 matrix describing the location and + orientation of an element in 3D. See + https://docs.ifcopenshell.org/ifcopenshell-python/geometry_creation.html#object-placements + for more details. + + This only supports local placements. Grid and linear placements are not + supported. + + :param matrix: A 4x4 matrix in numpy. If left blank, it is the identity + matrix (equivalent to ``np.eye(4)``). + :param is_si: If True, the matrix is given in SI units. If false, in + project units. + :param should_transform_children: A child element is a nested element, + opening, filling, etc. If true, child elements will move along with the + parent. If false, child elements will stay where they are. Because most + placements in IFC are relative, this means that if a child moves, we + actually don't change their placement. + :return: The new or updated IfcLocalPlacement entity + """ usecase = Usecase() usecase.file = file usecase.settings = { diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py index c3f826c219..4702912b0e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py @@ -19,9 +19,10 @@ import numpy as np import ifcopenshell import ifcopenshell.api.geometry -import ifcopenshell.util.shape_builder -import ifcopenshell.util.element import ifcopenshell.util.unit +import ifcopenshell.util.element +import ifcopenshell.util.placement +import ifcopenshell.util.shape_builder from collections import namedtuple from math import sin, cos from typing import Optional @@ -63,6 +64,14 @@ def regenerate_wall_representation( additional extrusions are generated for each connection that boolean difference the base extrusion. + This will also update the axis line representation (e.g. trim the axis line + to any connections). + + The wall's object placement will also be updated such that the placement is + equivalent to the axis line's start point (which therefore becomes (0.0, + 0.0)). This is a logical, consistent, and useful placement coordinate + (especially for apps that can pivot using this point). + :param wall: The IfcWall for the representation, only Model/Body/MODEL_VIEW type of representations are currently supported. :param length: If the wall doesn't have an axis length, this is the default @@ -165,7 +174,7 @@ class Regenerator: end_points.reverse() points.extend(end_points) item = builder.extrude( - builder.polyline(points, closed=True), + builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1), magnitude=self.wall_vectors["d"], extrusion_vector=self.wall_vectors["z"], ) @@ -186,7 +195,9 @@ class Regenerator: magnitude = np.linalg.norm(self.start_vector * (self.wall_vectors["h"] / self.start_vector[2])) operands.append( builder.extrude( - builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=self.start_vector + builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1), + magnitude=magnitude, + extrusion_vector=self.start_vector, ) ) @@ -206,7 +217,9 @@ class Regenerator: magnitude = np.linalg.norm(self.end_vector * (self.wall_vectors["h"] / self.end_vector[2])) operands.append( builder.extrude( - builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=self.end_vector + builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1), + magnitude=magnitude, + extrusion_vector=self.end_vector, ) ) @@ -216,7 +229,9 @@ class Regenerator: magnitude = np.linalg.norm(atpath_vector * (self.wall_vectors["h"] / atpath_vector[2])) operands.append( builder.extrude( - builder.polyline(points, closed=True), magnitude=magnitude, extrusion_vector=atpath_vector + builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1), + magnitude=magnitude, + extrusion_vector=atpath_vector, ) ) @@ -265,10 +280,14 @@ class Regenerator: remaining_path_points.append(minpath_points) self.minpath_points = remaining_path_points - profiles.append(builder.profile(builder.polyline(points, closed=True))) + profiles.append( + builder.profile(builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1)) + ) for points in self.maxpath_points + self.minpath_points: - profiles.append(builder.profile(builder.polyline(points, closed=True))) + profiles.append( + builder.profile(builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1)) + ) if len(profiles) > 1: profile = self.file.createIfcCompositeProfileDef("AREA", Profiles=profiles) @@ -283,13 +302,21 @@ class Regenerator: else: ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=body_rep) - item = builder.polyline([self.reference_p1, self.reference_p2]) + item = builder.polyline([self.reference_p1, self.reference_p2], position_offset=self.reference_p1 * -1) axis_rep = builder.get_representation(self.axis, items=[item]) if old_rep := ifcopenshell.util.representation.get_representation(wall, self.axis): ifcopenshell.util.element.replace_element(old_rep, axis_rep) ifcopenshell.util.element.remove_deep2(self.file, old_rep) else: ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=axis_rep) + + if not np.allclose(self.reference_p1, np.array((0.0, 0.0))): + matrix = ifcopenshell.util.placement.get_local_placement(wall.ObjectPlacement) + matrix[:, 3] = matrix @ np.concatenate((self.reference_p1, (0, 1))) + ifcopenshell.api.geometry.edit_object_placement( + self.file, product=wall, matrix=matrix, is_si=False, should_transform_children=False + ) + return body_rep def join(self, wall1, wall2, layers1, layers2, connection1, connection2): From 5fa4f2e79b940c4c47d935c719088e1eafc3d918 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 9 Mar 2025 17:32:19 +1100 Subject: [PATCH 257/476] See #1227. Reimplement merge walls with new wall body generation code. --- src/bonsai/bonsai/bim/module/model/wall.py | 88 +++++++++++----------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index ac44904e82..c712d6f9ad 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -1011,62 +1011,58 @@ class DumbWallJoiner: self.recreate_wall(element1, wall1) def merge(self, wall1, wall2): + if tool.Ifc.is_moved(wall1): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1) + if tool.Ifc.is_moved(wall2): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall2) + element1 = tool.Ifc.get_entity(wall1) element2 = tool.Ifc.get_entity(wall2) - axis1 = tool.Model.get_wall_axis(wall1) - axis2 = tool.Model.get_wall_axis(wall2) - angle = tool.Cad.angle_edges(axis1["reference"], axis2["reference"], signed=False, degrees=True) - if not tool.Cad.is_x(angle, 0, tolerance=0.001): + p1, p2 = ifcopenshell.util.representation.get_reference_line(element1) + p3, p4 = ifcopenshell.util.representation.get_reference_line(element2) + + matrix1i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(element1.ObjectPlacement)) + matrix2 = ifcopenshell.util.placement.get_local_placement(element2.ObjectPlacement) + + p3 = (matrix1i @ matrix2 @ np.concatenate((p3, (0, 1))))[:2] + p4 = (matrix1i @ matrix2 @ np.concatenate((p4, (0, 1))))[:2] + + if not np.isclose(p1[1], p4[1]) or not np.isclose(p3[1], p4[1]): return - intersect1, connection1 = mathutils.geometry.intersect_point_line(axis2["reference"][0], *axis1["reference"]) - if not tool.Cad.is_x((intersect1 - axis2["reference"][0]).length, 0): - return - - intersect2, connection2 = mathutils.geometry.intersect_point_line(axis2["reference"][1], *axis1["reference"]) - if not tool.Cad.is_x((intersect2 - axis2["reference"][1]).length, 0): - return - - changed_connections = set() - - if connection1 < 0: - changed_connections.add("ATSTART") - axis1["reference"][0] = intersect2 if connection2 < connection1 else intersect1 - elif connection1 > 1: - changed_connections.add("ATEND") - axis1["reference"][1] = intersect2 if connection2 > connection1 else intersect1 - - for connection in changed_connections: - ifcopenshell.api.run( - "geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type=connection - ) + x_ordinates = tuple(co[0] for co in (p1, p2, p3, p4)) + p1[0] = min(x_ordinates) + p2[0] = max(x_ordinates) + self.set_axis(element1, p1, p2) for rel in element2.ConnectedTo: - if rel.RelatingConnectionType in changed_connections: - other = tool.Ifc.get_object(rel.RelatedElement) - ifcopenshell.api.run( - "geometry.connect_path", - tool.Ifc.get(), - relating_element=element1, - related_element=rel.RelatedElement, - relating_connection=rel.RelatingConnectionType, - related_connection=rel.RelatedConnectionType, - ) + ifcopenshell.api.geometry.disconnect_path( + tool.Ifc.get(), element=element1, connection_type=rel.RelatingConnectionType + ) + ifcopenshell.api.geometry.connect_path( + tool.Ifc.get(), + relating_element=element1, + related_element=rel.RelatedElement, + relating_connection=rel.RelatingConnectionType, + related_connection=rel.RelatedConnectionType, + ) for rel in element2.ConnectedFrom: - if rel.RelatedConnectionType in changed_connections: - ifcopenshell.api.run( - "geometry.connect_path", - tool.Ifc.get(), - relating_element=rel.RelatingElement, - related_element=element1, - relating_connection=rel.RelatingConnectionType, - related_connection=rel.RelatedConnectionType, - ) + ifcopenshell.api.geometry.disconnect_path( + tool.Ifc.get(), element=element1, connection_type=rel.RelatedConnectionType + ) + ifcopenshell.api.geometry.connect_path( + tool.Ifc.get(), + relating_element=rel.RelatingElement, + related_element=element1, + relating_connection=rel.RelatingConnectionType, + related_connection=rel.RelatedConnectionType, + ) - self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"]) - bpy.data.objects.remove(wall2) + self.recreate_wall(element1, wall1) + + tool.Geometry.delete_ifc_object(wall2) def duplicate_wall(self, wall1): wall2 = wall1.copy() From e78127d54f1a340aa939897256684989bed7db1e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 9 Mar 2025 18:00:13 +1100 Subject: [PATCH 258/476] See #1227. Don't move any child at all when regenerating wall body. --- src/bonsai/bonsai/bim/module/model/wall.py | 50 +------------------ src/bonsai/bonsai/core/model.py | 2 +- .../api/geometry/edit_object_placement.py | 2 +- .../regenerate_wall_representation.py | 14 +++++- 4 files changed, 16 insertions(+), 52 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index c712d6f9ad..f5d69c0415 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -1106,7 +1106,7 @@ class DumbWallJoiner: else: ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) - def join_E(self, wall1, target): + def extend(self, wall1, target): if tool.Ifc.is_moved(wall1): bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1) element1 = tool.Ifc.get_entity(wall1) @@ -1192,54 +1192,6 @@ class DumbWallJoiner: matrix[:, 3] *= unit_scale obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix) tool.Geometry.record_object_position(obj) - return - - wall_moved = tool.Ifc.is_moved(obj) - if wall_moved: - # Openings should move with the host overall ... - # ... except their position should stay the same along the local X axis of the wall - for opening in [ - r.RelatedOpeningElement for r in element.HasOpenings if not r.RelatedOpeningElement.HasFillings - ]: - percent = tool.Cad.edge_percent( - self.body[0], (previous_origin, (previous_matrix @ Vector((1, 0, 0))).to_2d()) - ) - is_x_offset_increased = True if percent < 0 else False - - change_in_x = (self.body[0] - previous_origin).length / self.unit_scale - coordinates = list(opening.ObjectPlacement.RelativePlacement.Location.Coordinates) - if is_x_offset_increased: - coordinates[0] += change_in_x - else: - coordinates[0] -= change_in_x - opening.ObjectPlacement.RelativePlacement.Location.Coordinates = coordinates - - bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) - - # If opening has filling then stick to the filling's position - # We're applying new openings position only after wall position is applied - for opening in [r.RelatedOpeningElement for r in element.HasOpenings if r.RelatedOpeningElement.HasFillings]: - similar_openings = bonsai.core.geometry.get_similar_openings(tool.Ifc, opening) - filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement) - filling_moved = tool.Ifc.is_moved(filling_obj) - if filling_moved: - bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=filling_obj) - if filling_moved or wall_moved: - ifcopenshell.api.run( - "geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=filling_obj.matrix_world - ) - bonsai.core.geometry.edit_similar_opening_placement(tool.Geometry, opening, similar_openings) - - bonsai.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=new_body, - should_reload=True, - is_global=True, - should_sync_changes_first=False, - ) - tool.Geometry.record_object_materials(obj) def create_matrix(self, p, x, y, z): return Matrix([x, y, z, p]).to_4x4().transposed() diff --git a/src/bonsai/bonsai/core/model.py b/src/bonsai/bonsai/core/model.py index fe9851e1f9..d04486ed1c 100644 --- a/src/bonsai/bonsai/core/model.py +++ b/src/bonsai/bonsai/core/model.py @@ -35,7 +35,7 @@ def extend_walls( if not (element := ifc.get_entity(obj)) or model.get_usage_type(element) != "LAYER2": continue geometry.clear_scale(obj) - joiner.join_E(obj, target) + joiner.extend(obj, target) def join_walls_LV( diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py index 11d53364b3..78e95c32c8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/edit_object_placement.py @@ -155,7 +155,7 @@ class Usecase: elif obj.is_a("IfcFeatureElement"): # Feature elements affect the geometry of their parent, and # so logically should always move with the parent. However, - # subchildren shouldn't move. + # subchildren (fillings) shouldn't move. placement2 = obj.ObjectPlacement for referenced_placement2 in placement2.ReferencedByPlacements: matrix2 = ifcopenshell.util.placement.get_local_placement(referenced_placement2) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py index 4702912b0e..5f72756c56 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py @@ -311,12 +311,24 @@ class Regenerator: ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=axis_rep) if not np.allclose(self.reference_p1, np.array((0.0, 0.0))): + children = [] + for referenced_placement in wall.ObjectPlacement.ReferencedByPlacements: + matrix = ifcopenshell.util.placement.get_local_placement(referenced_placement) + children.append((matrix, referenced_placement.PlacesObject)) + matrix = ifcopenshell.util.placement.get_local_placement(wall.ObjectPlacement) matrix[:, 3] = matrix @ np.concatenate((self.reference_p1, (0, 1))) ifcopenshell.api.geometry.edit_object_placement( - self.file, product=wall, matrix=matrix, is_si=False, should_transform_children=False + self.file, product=wall, matrix=matrix, is_si=False, should_transform_children=True ) + # Restore children to their previous location + for matrix, elements in children: + for element in elements: + ifcopenshell.api.geometry.edit_object_placement( + self.file, product=element, matrix=matrix, is_si=False, should_transform_children=True + ) + return body_rep def join(self, wall1, wall2, layers1, layers2, connection1, connection2): From 750353074128c54dd65264577cfb55cfc979d80e Mon Sep 17 00:00:00 2001 From: theseyan Date: Sun, 9 Mar 2025 11:55:53 +0530 Subject: [PATCH 259/476] Fix #4807 --- src/ifcopenshell-python/docs/_static/custom.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ifcopenshell-python/docs/_static/custom.css b/src/ifcopenshell-python/docs/_static/custom.css index 1c1de1de08..2912d9390d 100644 --- a/src/ifcopenshell-python/docs/_static/custom.css +++ b/src/ifcopenshell-python/docs/_static/custom.css @@ -82,3 +82,8 @@ dl.py.property, dl.py.attribute, dl.py.method, dl.py.function { color: var(--color-brand-content); font-style: italic; } + +/* Workaround for highlighting issue in Furo */ +[role=main] .highlighted { + -webkit-text-fill-color: initial !important; +} \ No newline at end of file From 4109b19c3a5ac43d070b692ede7f47b729df37cc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 9 Mar 2025 18:31:43 +1100 Subject: [PATCH 260/476] See #1227. Slightly more defensive to make sure ATPATH is between START and END --- .../api/geometry/regenerate_wall_representation.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py index 5f72756c56..06d2d2720b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py @@ -240,7 +240,13 @@ class Regenerator: else: # A wall footprint may be multiple profiles if the wall is split into two due to an ATPATH connection profiles = [] - split_points = sorted(self.split_points, key=lambda x: x[0][0]) # Sort islands in the +X direction + minx = max([p[0] for p in self.start_points]) + maxx = min([p[0] for p in self.end_points]) + split_points = [] + for points in sorted(self.split_points, key=lambda x: x[0][0]): # Sort islands in the +X direction + if any([p[0] > maxx or p[0] < minx for p in points]): # Can't have anything outside our start/end + continue + split_points.append(points) start_points = [p.copy() for p in self.start_points] end_points = [p.copy() for p in self.end_points] split_points.insert(0, start_points) @@ -259,7 +265,7 @@ class Regenerator: maxy_maxx = end_split[-1][0] miny_minx = start_split[0][0] miny_maxx = end_split[0][0] - # Do more defensive checks here + # Do more defensive checks here? points = start_split remaining_path_points = [] From 8ffb5c56e477d7d7f9cd83424b7482a0072bd82a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 9 Mar 2025 19:28:47 +1100 Subject: [PATCH 261/476] See #1227. Fix inconsistent layer UI based on direction sense and minor UI cleanup. The UI here looks suspicious in that logic is occuring in the draw call instead of the cached data object. --- src/bonsai/bonsai/bim/module/material/data.py | 10 +++- src/bonsai/bonsai/bim/module/material/ui.py | 54 ++++++++----------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index b82aeeb5dc..6c426dd652 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -244,6 +244,8 @@ class ObjectMaterialData: items = [] if cls.material.is_a("IfcMaterialLayerSetUsage"): items = cls.material.ForLayerSet.MaterialLayers + if cls.material.DirectionSense == "POSITIVE": + items = reversed(items) elif cls.material.is_a("IfcMaterialProfileSetUsage"): items = cls.material.ForProfileSet.MaterialProfiles elif cls.material.is_a("IfcMaterialLayerSet"): @@ -308,7 +310,13 @@ class ObjectMaterialData: layers = cls.material.ForLayerSet.MaterialLayers elif cls.material.is_a("IfcMaterialLayerSet"): layers = cls.material.MaterialLayers - return sum([l.LayerThickness for l in layers or []]) + thickness = sum([l.LayerThickness for l in layers or []]) + props = tool.Drawing.get_document_props() + unit_system = bpy.context.scene.unit_settings.system + precision = None + if unit_system == "IMPERIAL": + precision = props.imperial_precision + return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True) @classmethod def set_item_name(cls) -> Union[str, None]: diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index b743d37ef1..f2eb2f155c 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -24,7 +24,6 @@ from bpy.types import Panel, UIList from bonsai.bim.helper import draw_attributes from bonsai.bim.helper import prop_with_search from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData -from bonsai.bim.module.drawing.helper import format_distance from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -336,38 +335,28 @@ class BIM_PT_object_material(Panel): if ObjectMaterialData.data["material_class"] != "IfcMaterialList": row = self.layout.row(align=True) set_name = ObjectMaterialData.data["set"]["name"] - row.label(text=f" Name: {set_name}") + row.label(text="Name") + row.label(text=set_name) - if ObjectMaterialData.data["set"]["description"]: - set_description = ObjectMaterialData.data["set"]["description"] + if value := ObjectMaterialData.data["set"]["description"]: row = self.layout.row(align=True) - row.label(text=f" Description: {set_description}") + row.label(text="Description") + row.label(text=value) if ObjectMaterialData.data["material_class"] == "IfcMaterialProfileSetUsage": - if ObjectMaterialData.data["set_usage"].get("cardinal_point"): - cardinal_point = ObjectMaterialData.data["set_usage"]["cardinal_point"] + if value := ObjectMaterialData.data["set_usage"].get("cardinal_point"): row = self.layout.row(align=True) - row.label(text=f" Cardinal Point: {cardinal_point}") + row.label(text="Cardinal Point") + row.label(text=value) if ObjectMaterialData.data["total_thickness"]: - total_thickness = ObjectMaterialData.data["total_thickness"] - unit_system = bpy.context.scene.unit_settings.system - props = tool.Drawing.get_document_props() - - if unit_system == "IMPERIAL": - precision = props.imperial_precision - else: - precision = None - formatted_thickness = format_distance( - total_thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True - ) row = self.layout.row(align=True) - row.label(text=f" Total Thickness: {formatted_thickness}") + row.label(text="Total Thickness*") + row.label(text=ObjectMaterialData.data["total_thickness"]) - layout = self.layout - box = layout.box() + box = self.layout.box() active_object = bpy.context.active_object - self.layerset_bounds(box, active_object, location="Top_Exterior") + self.layerset_bounds(box, active_object, location="Top_Interior") for set_item in ObjectMaterialData.data["set_items"]: material_name = set_item["material"] @@ -383,22 +372,25 @@ class BIM_PT_object_material(Panel): op = row.operator("bim.select_by_material", text=material_name, emboss=False) op.material = material_id - self.layerset_bounds(box, active_object, location="Bottom_Interior") + self.layerset_bounds(box, active_object, location="Bottom_Exterior") - def layerset_bounds(self, box, obj, location="Top_Exterior"): + def layerset_bounds(self, layout, obj, location="Top_Interior"): set_usage = ObjectMaterialData.data.get("set_usage", {}) layer_set_direction = set_usage.get("layer_set_direction") if layer_set_direction: - if location == "Top_Exterior": + row = layout.row() + row.alignment = "CENTER" + row.enabled = False + if location == "Top_Interior": if layer_set_direction == "AXIS3": - box.label(text="----- Top -----") + row.label(text="Top") else: - box.label(text="----- Exterior -----") - if location == "Bottom_Interior": + row.label(text="Interior") + elif location == "Bottom_Exterior": if layer_set_direction == "AXIS3": - box.label(text="----- Bottom -----") + row.label(text="Bottom") else: - box.label(text="----- Interior -----") + row.label(text="Exterior") class BIM_UL_materials(UIList): From 9d7fe179cf231cefee79ebebabbee67362018e5e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 9 Mar 2025 19:29:35 +1100 Subject: [PATCH 262/476] See #1227. Minor fix of incorrect layerset slicing of negative sense slabs. --- src/bonsai/bonsai/tool/loader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index cd75f1bd3d..17fbdb82d6 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1042,6 +1042,7 @@ class Loader(bonsai.core.tool.Loader): co = Vector((0.0, offset, 0.0)) no = no.cross(Vector([1.0, 0.0, 0.0])) else: + sense_factor = 1 # If it isn't AXIS2, then the normal points in the direction sense co = Vector((0.0, 0.0, offset)) # Cache this body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") From abfe8ec3154ea0708ab1e5b5676c60de9eb8d5f3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 9 Mar 2025 20:47:02 +1100 Subject: [PATCH 263/476] Fix failing IOS tests --- .../test/api/georeference/test_edit_true_north.py | 2 +- src/ifcopenshell-python/test/test_sql.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/test/api/georeference/test_edit_true_north.py b/src/ifcopenshell-python/test/api/georeference/test_edit_true_north.py index 5076b46d67..9f8f2dd07a 100644 --- a/src/ifcopenshell-python/test/api/georeference/test_edit_true_north.py +++ b/src/ifcopenshell-python/test/api/georeference/test_edit_true_north.py @@ -30,7 +30,7 @@ class TestEditTrueNorth(test.bootstrap.IFC4): model = ifcopenshell.api.context.add_context(self.file, "Model") plan = ifcopenshell.api.context.add_context(self.file, "Plan") ifcopenshell.api.georeference.edit_true_north(self.file, true_north=[0.0, 1.0]) - assert model.TrueNorth[0] == (0.0, 1.0, 0.0) + assert model.TrueNorth[0] == (0.0, 1.0) assert plan.TrueNorth[0] == (0.0, 1.0) ifcopenshell.api.georeference.edit_true_north(self.file, true_north=[-0.5, 0.8660254]) assert np.isclose(ifcopenshell.util.geolocation.get_true_north(self.file), 30) diff --git a/src/ifcopenshell-python/test/test_sql.py b/src/ifcopenshell-python/test/test_sql.py index 8b4323a3c8..22fba621e1 100644 --- a/src/ifcopenshell-python/test/test_sql.py +++ b/src/ifcopenshell-python/test/test_sql.py @@ -29,14 +29,14 @@ def get_ifc_sqlite() -> ifcopenshell.sqlite: global SQLITE_PATH if SQLITE_PATH is None: tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".ifcsqlite") - SQLITE_PATH = ifcpatch.execute( + ifcpatch.execute( { "file": ifcopenshell.open(TEST_FILE), "recipe": "Ifc2Sql", "arguments": ["sqlite", None, None, None, tmp.name], } ) - assert isinstance(SQLITE_PATH, str) + SQLITE_PATH = tmp.name ifc_sqlite = ifcopenshell.open(SQLITE_PATH) assert isinstance(ifc_sqlite, ifcopenshell.sqlite) return ifc_sqlite From 076f39e2d69676b8bcf653702a59a8a4eaef639d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 9 Mar 2025 21:35:20 +1100 Subject: [PATCH 264/476] I think bSDD should use the Name for the property name. See 115a48f It seems correct according to https://github.com/buildingSMART/bSDD/blob/master/Documentation/bSDD%20JSON%20import%20model.md --- .../bim/module/classification/operator.py | 1 + src/bonsai/bonsai/tool/bsdd.py | 5 +--- src/bonsai/test/tool/test_classification.py | 27 ++++++++++--------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index fa5e914880..ab3fc08658 100644 --- a/src/bonsai/bonsai/bim/module/classification/operator.py +++ b/src/bonsai/bonsai/bim/module/classification/operator.py @@ -460,6 +460,7 @@ class AddClassificationReferenceFromBSDD(bpy.types.Operator, tool.Ifc.Operator): for prop in blender_properties: properties[prop.name] = prop.get_value() + # TODO: is this still the correct approach? if classification_pset.name == "undefined_set": if "ObjectType" in properties: if hasattr(element, "ObjectType"): diff --git a/src/bonsai/bonsai/tool/bsdd.py b/src/bonsai/bonsai/tool/bsdd.py index b82ed6c47f..8f1d1a1fd7 100644 --- a/src/bonsai/bonsai/tool/bsdd.py +++ b/src/bonsai/bonsai/tool/bsdd.py @@ -111,10 +111,7 @@ class Bsdd(bonsai.core.tool.Bsdd): possible_values = [v["value"] for v in possible_values] description = prop.get("description", "") - # Prefer propertyCode as it's later will be used to set properties, - # so name should be exact. Fallback to name as propertyCode is nullable. - prop_name = prop.get("propertyCode") or prop["name"] - psets[pset][prop_name] = { + psets[pset][prop["name"]] = { "data_type": prop.get("dataType"), "possible_values": possible_values, "description": description, diff --git a/src/bonsai/test/tool/test_classification.py b/src/bonsai/test/tool/test_classification.py index 5d094bf0bc..5a90457cc7 100644 --- a/src/bonsai/test/tool/test_classification.py +++ b/src/bonsai/test/tool/test_classification.py @@ -84,19 +84,18 @@ class TestAddClassificationReferenceFromBSDD(NewFile): assert pset.name == "Pset_SpaceCommon" assert len(pset.properties) == 1 pset_prop = pset.properties[0] - assert pset_prop.name == "HandicapAccessible" + assert pset_prop.name == "Handicap Accessible" pset_prop.bool_value = True bpy.ops.bim.add_classification_reference_from_bsdd(obj="IfcSpace/Cube", obj_type="Object") pset = ifcopenshell.util.element.get_pset(element, "Pset_SpaceCommon") - assert pset and pset["HandicapAccessible"] == True + assert pset and pset["Handicap Accessible"] == True refs = ifcopenshell.util.classification.get_references(element) assert len(refs) == 1 assert list(refs)[0].Location.startswith(uri) def test_add_clasification_reference_with_object_type(self): bpy.ops.bim.create_project() - ifc_file = tool.Ifc.get() context = bpy.context bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4)) obj = bpy.data.objects["Cube"] @@ -104,27 +103,29 @@ class TestAddClassificationReferenceFromBSDD(NewFile): element = tool.Ifc.get_entity(obj) assert element - bpy.ops.bim.load_bsdd_domains() - uri = "https://identifier.buildingsmart.org/uri/ifcairport/ifcairport/1.0" - bpy.ops.bim.set_active_bsdd_domain(name="IFC Airport", uri=uri) props = context.scene.BIMBSDDProperties + props.load_preview_domains = True + bpy.ops.bim.load_bsdd_domains() + uri = "https://identifier.buildingsmart.org/uri/ifcairport/ifcairport/0.9" + bpy.ops.bim.set_active_bsdd_domain(name="IFC Airport", uri=uri) bpy.context.scene.BIMClassificationProperties.classification_source = "BSDD" props.should_filter_ifc_class = False # Important due to class mismatch. - props.keyword = "Check-In Conveyor" + props.keyword = "check-in conveyor" bpy.ops.bim.search_bsdd_classifications() - # https://identifier.buildingsmart.org/uri/ifcairport/ifcairport/1.0/class/ifcairport0000000004 + # https://identifier.buildingsmart.org/uri/bs-airport/airport/0.9/class/AD-BHS-006 props.active_classification_index = next( - i for i, c in enumerate(props.classifications) if c.name == "Check-In Conveyor" + i for i, c in enumerate(props.classifications) if c.name == "Check-in conveyor" ) bpy.ops.bim.get_bsdd_classification_properties() psets = props.classification_psets assert len(psets) == 1 pset = psets[0] - assert pset.name == "undefined_set" - assert len(pset.properties) == 1 + assert pset.name == "ISet_AirportDomain" + assert len(pset.properties) == 20 pset_prop = pset.properties[0] - assert pset_prop.name == "ObjectType" + assert pset_prop.name == "Conveying speed" bpy.ops.bim.add_classification_reference_from_bsdd(obj="IfcSpace/Cube", obj_type="Object") - assert element.ObjectType == "CHECKINCONVEYOR" + # Check this? + # assert element.ObjectType == "CHECKINCONVEYOR" assert not ifcopenshell.util.element.get_psets(element) From b6b553e7264c54b38597d82fae4906950fd2e641 Mon Sep 17 00:00:00 2001 From: Massimo Fabbro Date: Sun, 9 Mar 2025 11:40:43 +0100 Subject: [PATCH 265/476] Fix #6271 Fix a bug about wrong net values qto calculation with IfcOpenshell --- src/ifc5d/ifc5d/qto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 2863634f07..417a2efee5 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -255,7 +255,7 @@ class IfcOpenShell: tasks.append((iterator, gross_qtos)) if net_qtos: - for iterator in IfcOpenShell.create_iterators(ifc_file, cls.gross_settings, list(elements)): + for iterator in IfcOpenShell.create_iterators(ifc_file, cls.net_settings, list(elements)): tasks.append((iterator, net_qtos)) cls.unit_converter = SI2ProjectUnitConverter(ifc_file) From 2318132ea2356e48f2452df108feab80e561a519 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 Mar 2025 08:23:50 +1100 Subject: [PATCH 266/476] Minor refactor add_constr_type_instance is now add_occurrence --- .../bonsai/bim/module/covering/workspace.py | 8 +++---- .../bonsai/bim/module/model/__init__.py | 2 +- src/bonsai/bonsai/bim/module/model/mep.py | 6 ++--- src/bonsai/bonsai/bim/module/model/product.py | 12 +++++----- .../bonsai/bim/module/model/workspace.py | 2 +- src/bonsai/bonsai/bim/module/type/ui.py | 2 +- src/bonsai/test/bim/feature/covering.feature | 15 +++++++------ src/bonsai/test/bim/feature/geometry.feature | 18 +++++++-------- src/bonsai/test/bim/feature/model.feature | 22 +++++++++---------- src/bonsai/test/bim/feature/spatial.feature | 2 +- src/bonsai/test/bim/feature/system.feature | 10 ++++----- src/bonsai/test/bim/test_feature.py | 2 +- src/bonsai/test/tool/test_model.py | 6 ++--- src/bonsai/test/tool/test_root.py | 6 ++--- 14 files changed, 57 insertions(+), 56 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/covering/workspace.py b/src/bonsai/bonsai/bim/module/covering/workspace.py index 302f6d3c34..e0b0195b83 100644 --- a/src/bonsai/bonsai/bim/module/covering/workspace.py +++ b/src/bonsai/bonsai/bim/module/covering/workspace.py @@ -87,7 +87,7 @@ class CoveringToolUI: row = cls.layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_A") - row.operator("bim.add_constr_type_instance", text="Add") + row.operator("bim.add_occurrence", text="Add") row = cls.layout.row(align=True) row.label(text="", icon="EVENT_SHIFT") @@ -179,16 +179,16 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): elif container: bpy.ops.bim.add_instance_flooring_covering_from_cursor() else: - bpy.ops.bim.add_constr_type_instance() + bpy.ops.bim.add_occurrence() elif AuthoringData.data["relating_type_data"].get("predefined_type") == "CEILING": if element and bpy.context.selected_objects and element.is_a("IfcWall"): bpy.ops.bim.add_instance_ceiling_coverings_from_walls() elif container: bpy.ops.bim.add_instance_ceiling_covering_from_cursor() else: - bpy.ops.bim.add_constr_type_instance() + bpy.ops.bim.add_occurrence() else: - bpy.ops.bim.add_constr_type_instance() + bpy.ops.bim.add_occurrence() def hotkey_S_G(self): active_obj = bpy.context.active_object diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index afd360d937..32573c6856 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -55,12 +55,12 @@ classes = ( array.Input3DCursorXArray, array.Input3DCursorYArray, array.Input3DCursorZArray, - product.AddConstrTypeInstance, product.AddDefaultType, product.AddEmptyType, product.AddOccurrence, product.AlignProduct, product.ChangeTypePage, + product.DrawOccurrence, product.LoadTypeThumbnails, product.MirrorElements, product.SetActiveType, diff --git a/src/bonsai/bonsai/bim/module/model/mep.py b/src/bonsai/bonsai/bim/module/model/mep.py index f4c00fb53b..72d7a8e2f7 100644 --- a/src/bonsai/bonsai/bim/module/model/mep.py +++ b/src/bonsai/bonsai/bim/module/model/mep.py @@ -580,7 +580,7 @@ class MEPGenerator: profile_joiner = DumbProfileJoiner() # create obstruction occurrence and setup it's length and port # NOTE: at this point we loose current blender objects selection - bpy.ops.bim.add_constr_type_instance(relating_type_id=obstruction_type.id()) + bpy.ops.bim.add_occurrence(relating_type_id=obstruction_type.id()) obstruction_obj = bpy.context.active_object obstruction_obj.matrix_world = segment_matrix @@ -859,7 +859,7 @@ class MEPAddTransition(bpy.types.Operator, tool.Ifc.Operator): # NOTE: at this point we loose current blender objects selection # create transition element - bpy.ops.bim.add_constr_type_instance(relating_type_id=transition_type.id()) + bpy.ops.bim.add_occurrence(relating_type_id=transition_type.id()) transition_obj = bpy.context.active_object # adjust transition segment rotation and location @@ -1210,7 +1210,7 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): # NOTE: at this point we loose current blender objects selection # create transition element - bpy.ops.bim.add_constr_type_instance(relating_type_id=bend_type.id()) + bpy.ops.bim.add_occurrence(relating_type_id=bend_type.id()) fitting_obj = bpy.context.active_object # adjust fitting object rotation and location diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 04446deaeb..f0e82a7272 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -155,9 +155,9 @@ class AddDefaultType(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.add_element() -class AddOccurrence(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): - bl_idname = "bim.add_occurrence" - bl_label = "Add Occurrence" +class DrawOccurrence(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): + bl_idname = "bim.draw_occurrence" + bl_label = "Draw Occurrence" bl_options = {"REGISTER", "UNDO"} @classmethod @@ -194,7 +194,7 @@ class AddOccurrence(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): context.scene.cursor.location = Vector((point.x, point.y, point.z)) tool.Polyline.clear_polyline() - bpy.ops.bim.add_constr_type_instance("INVOKE_DEFAULT") + bpy.ops.bim.add_occurrence("INVOKE_DEFAULT") if snap_obj: snap_obj.select_set(False) @@ -265,8 +265,8 @@ class AddOccurrence(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): return {"RUNNING_MODAL"} -class AddConstrTypeInstance(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.add_constr_type_instance" +class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.add_occurrence" bl_label = "Add Type Occurrence" bl_options = {"REGISTER", "UNDO"} bl_description = "Add Type Instance" diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index bf56626160..b2f0220582 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1136,7 +1136,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): "IfcPileType", ): return bpy.ops.bim.draw_polyline_profile("INVOKE_DEFAULT") - return bpy.ops.bim.add_occurrence("INVOKE_DEFAULT") + return bpy.ops.bim.draw_occurrence("INVOKE_DEFAULT") def hotkey_S_Q(self): if not bpy.context.selected_objects: diff --git a/src/bonsai/bonsai/bim/module/type/ui.py b/src/bonsai/bonsai/bim/module/type/ui.py index 340662cb3a..291c37f1ba 100644 --- a/src/bonsai/bonsai/bim/module/type/ui.py +++ b/src/bonsai/bonsai/bim/module/type/ui.py @@ -97,4 +97,4 @@ class BIM_PT_type(Panel): def add_object_button(self, context): - self.layout.operator("bim.add_constr_type_instance", icon="PLUGIN") + self.layout.operator("bim.add_occurrence", icon="PLUGIN") diff --git a/src/bonsai/test/bim/feature/covering.feature b/src/bonsai/test/bim/feature/covering.feature index 0800e0edb5..4eb97649da 100644 --- a/src/bonsai/test/bim/feature/covering.feature +++ b/src/bonsai/test/bim/feature/covering.feature @@ -9,26 +9,27 @@ Scenario: Execute generate flooring coverings from walls And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" # 1st wall - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected And I press "bim.change_layer_length(length=3.6)" # 2nd wall And the cursor is at "3.6,0.1,3" And I set "scene.BIMModelProperties.length" to "2.0" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" # 3rd wall And the cursor is at "3.5,2.1,3" And I set "scene.BIMModelProperties.length" to "3.5" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" # 4th wall And the cursor is at "0,2.0,0" And I set "scene.BIMModelProperties.length" to "1.9" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" # add_instance_flooring_coverings_from_walls is expecting FLOORING predefined type. And the object "IfcCoveringType/COV30" is selected - And I press "bim.enable_editing_attributes(obj='IfcCoveringType/COV30')" - And I set "active_object.BIMAttributeProperties.attributes[6].enum_value" to "FLOORING" - And I press "bim.edit_attributes(obj='IfcCoveringType/COV30')" + And I look at the "Attributes" panel + And I click "Edit" + And I set the "PredefinedType" property to "FLOORING" + And I click "Save Attributes" # Run the operator. When the object "IfcWall/Wall" is selected And additionally the object "IfcWall/Wall.001" is selected diff --git a/src/bonsai/test/bim/feature/geometry.feature b/src/bonsai/test/bim/feature/geometry.feature index 50e6e5fdbb..eeda85681f 100644 --- a/src/bonsai/test/bim/feature/geometry.feature +++ b/src/bonsai/test/bim/feature/geometry.feature @@ -33,8 +33,8 @@ Scenario: Add representation - add a new representation to a typed instance And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" And I press "bim.assign_class" - And I press "bim.add_constr_type_instance" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" + And I press "bim.add_occurrence" Then the object "IfcWall/Wall" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" And the object "IfcWall/Wall.001" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" When the object "IfcWall/Wall" is selected @@ -127,8 +127,8 @@ Scenario: Remove representation - remove an instanced representation from an act And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{cube}" - And I press "bim.add_constr_type_instance" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" + And I press "bim.add_occurrence" And the object "IfcWallType/Cube" is selected When the variable "representation_body" is "{ifc}.by_type('IfcWallType')[0].RepresentationMaps[0].MappedRepresentation.id()" And I press "bim.remove_representation(representation_id={representation_body})" @@ -146,8 +146,8 @@ Scenario: Remove representation - remove an instanced representation from an act And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{cube}" - And I press "bim.add_constr_type_instance" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected When the variable "representation_body" is "{ifc}.by_type('IfcWall')[0].Representation.Representations[0].id()" And I press "bim.remove_representation(representation_id={representation_body})" @@ -339,7 +339,7 @@ Scenario: Override duplicate move - copying a type instance with a representatio And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{cube}" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected When I duplicate the selected objects Then the object "IfcWall/Wall.001" exists @@ -425,14 +425,14 @@ Scenario: Override duplicate move - copying objects with connection And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - When I press "bim.add_constr_type_instance" + When I press "bim.add_occurrence" Then the object "IfcWall/Wall" is an "IfcWall" And the object "IfcWall/Wall" dimensions are "1,0.1,3" And the object "IfcWall/Wall" bottom left corner is at "0,0,0" When I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - When I press "bim.add_constr_type_instance" + When I press "bim.add_occurrence" Then the object "IfcSlab/Slab" is an "IfcSlab" When the object "IfcSlab/Slab" is selected And the object "IfcSlab/Slab" is moved to "0,0,4" diff --git a/src/bonsai/test/bim/feature/model.feature b/src/bonsai/test/bim/feature/model.feature index 7467d6af86..04a08c1445 100644 --- a/src/bonsai/test/bim/feature/model.feature +++ b/src/bonsai/test/bim/feature/model.feature @@ -11,7 +11,7 @@ Scenario: Add type instance - add from a mesh And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{cube}" - When I press "bim.add_constr_type_instance" + When I press "bim.add_occurrence" Then the object "IfcWall/Wall" exists Scenario: Add type instance - add from an empty @@ -24,7 +24,7 @@ Scenario: Add type instance - add from an empty And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "empty" is "{ifc}.by_type('IfcWallType')[0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{empty}" - When I press "bim.add_constr_type_instance" + When I press "bim.add_occurrence" Then the object "IfcWall/Wall" exists Scenario: Add type instance - add a mesh where existing instances have changed context @@ -37,14 +37,14 @@ Scenario: Add type instance - add a mesh where existing instances have changed c And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "cube" is "{ifc}.by_type('IfcWallType')[0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{cube}" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" data is a "Tessellation" representation of "Model/Body/MODEL_VIEW" And the object "IfcWall/Wall" is selected And the variable "context" is "[c for c in {ifc}.by_type('IfcGeometricRepresentationSubContext') if c.ContextType == 'Plan' and c.ContextIdentifier == 'Body' and c.TargetView == 'PLAN_VIEW'][0].id()" And I set "active_object.BIMGeometryProperties.contexts" to "{context}" And I press "bim.add_representation" And the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW" - When I press "bim.add_constr_type_instance" + When I press "bim.add_occurrence" Then the object "IfcWall/Wall" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW" And the object "IfcWall/Wall.001" data is a "Annotation2D" representation of "Plan/Body/PLAN_VIEW" @@ -72,7 +72,7 @@ Scenario: Add a wall And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - When I press "bim.add_constr_type_instance" + When I press "bim.add_occurrence" Then the object "IfcWall/Wall" is an "IfcWall" And the object "IfcWall/Wall" dimensions are "1,0.1,3" And the object "IfcWall/Wall" bottom left corner is at "0,0,0" @@ -525,11 +525,11 @@ Scenario: Create a MEP transition And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/RectSegment" And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[1]" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/CircleSegment" And the object "IfcDuctSegment/RectSegment" is moved to "0,0,0" @@ -555,11 +555,11 @@ Scenario: Create a MEP bend between intersecting with different locations And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" And I set "scene.BIMModelProperties.extrusion_depth" to "5.0" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg1" And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg2" And the object "IfcDuctSegment/Seg2" is rotated by "0,0,90" deg @@ -585,11 +585,11 @@ Scenario: Create a MEP bend between intersecting segments at the same location And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" And I set "scene.BIMModelProperties.extrusion_depth" to "5.0" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg1" And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg2" And the object "IfcDuctSegment/Seg2" is rotated by "0,0,90" deg diff --git a/src/bonsai/test/bim/feature/spatial.feature b/src/bonsai/test/bim/feature/spatial.feature index c7b932c308..604159f002 100644 --- a/src/bonsai/test/bim/feature/spatial.feature +++ b/src/bonsai/test/bim/feature/spatial.feature @@ -112,7 +112,7 @@ Scenario: Execute generate spaces from walls And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected When I press "bim.generate_spaces_from_walls" Then nothing happens diff --git a/src/bonsai/test/bim/feature/system.feature b/src/bonsai/test/bim/feature/system.feature index f671b1009b..f9ebbcf915 100644 --- a/src/bonsai/test/bim/feature/system.feature +++ b/src/bonsai/test/bim/feature/system.feature @@ -139,13 +139,13 @@ Scenario: Connect MEP elements And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" And I set "scene.BIMModelProperties.extrusion_depth" to "5.0" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg1" # actuator And I set "scene.BIMModelProperties.ifc_class" to "IfcActuatorType" And I set "scene.BIMModelProperties.relating_type_id" to "{actuator_type_id}" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And the object "IfcActuator/Actuator" is moved to "10,0,0" # connect actuator @@ -169,12 +169,12 @@ Scenario: Connect MEP elements and regenerate And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" And I set "scene.BIMModelProperties.extrusion_depth" to "5.0" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg1" # segment2 And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg2" And the object "IfcDuctSegment/Seg2" is rotated by "0,0,90" deg @@ -186,7 +186,7 @@ Scenario: Connect MEP elements and regenerate # actuator And I set "scene.BIMModelProperties.ifc_class" to "IfcActuatorType" And I set "scene.BIMModelProperties.relating_type_id" to "{actuator_type_id}" - And I press "bim.add_constr_type_instance" + And I press "bim.add_occurrence" And the object "IfcActuator/Actuator" is moved to "10,0,0" # connect actuator diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 7385543ff9..c5e084e031 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -1248,7 +1248,7 @@ def i_display_the_construction_type_browser(): @when("I add the construction type") def i_add_the_active_construction_type(): props = tool.Model.get_model_props() - bpy.ops.bim.add_constr_type_instance(relating_type_id=int(props.relating_type_id)) + bpy.ops.bim.add_occurrence(relating_type_id=int(props.relating_type_id)) @then(parsers.parse("construction type is {relating_type_name}")) diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index 8056db1349..f41a4d5988 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -517,12 +517,12 @@ class TestApplyIfcMaterialChanges(NewFile): # Setup occurrences. relating_type_id = element_type.id() - bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) + bpy.ops.bim.add_occurrence(relating_type_id=relating_type_id) simple = bpy.context.active_object simple.name = "Simple" # Occurrence with an opening. - bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) + bpy.ops.bim.add_occurrence(relating_type_id=relating_type_id) with_opening = bpy.context.active_object with_opening.name = "With Opening" props = tool.Root.get_root_props() @@ -530,7 +530,7 @@ class TestApplyIfcMaterialChanges(NewFile): bpy.ops.bim.add_element(ifc_product="IfcFeatureElement", ifc_class="IfcOpeningElement") # Occurrence with a material override. - bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) + bpy.ops.bim.add_occurrence(relating_type_id=relating_type_id) with_material = bpy.context.active_object with_material.name = "With Material" tool.Blender.set_objects_selection(bpy.context, active_object=with_material, selected_objects=[with_material]) diff --git a/src/bonsai/test/tool/test_root.py b/src/bonsai/test/tool/test_root.py index 2c89a4e5fa..b807c1ddb8 100644 --- a/src/bonsai/test/tool/test_root.py +++ b/src/bonsai/test/tool/test_root.py @@ -184,9 +184,9 @@ class TestReassignClass(NewFile): n_slab_types = len(ifc_file.by_type("IfcSlabType")) # create 3 slabs - bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) - bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) - bpy.ops.bim.add_constr_type_instance(relating_type_id=relating_type_id) + bpy.ops.bim.add_occurrence(relating_type_id=relating_type_id) + bpy.ops.bim.add_occurrence(relating_type_id=relating_type_id) + bpy.ops.bim.add_occurrence(relating_type_id=relating_type_id) slabs = [tool.Ifc.get_object(e) for e in ifc_file.by_type("IfcSlab")] assert len(slabs) == 3 From ba1b6b5b10492af1036cff7374ccdeb766ebf008 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 Mar 2025 18:08:30 +1100 Subject: [PATCH 267/476] Fix regression where type thumbnails don't refresh properly from ae76d3253 The fix in ae76d3253 removed the refresh_ui_data call, so we need to handle it a bit better. I also heavily simplified the thumbnail loading. --- src/bonsai/bonsai/bim/module/model/product.py | 17 +++-------------- src/bonsai/bonsai/bim/module/model/prop.py | 6 +++--- src/bonsai/bonsai/bim/module/model/ui.py | 17 ++++------------- src/bonsai/bonsai/bim/module/model/workspace.py | 2 +- src/bonsai/bonsai/bim/module/type/operator.py | 2 +- src/bonsai/bonsai/tool/project.py | 5 ++--- 6 files changed, 14 insertions(+), 35 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index f0e82a7272..9a6c6a2a97 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -544,8 +544,8 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): props = tool.Model.get_model_props() - bpy.ops.bim.load_type_thumbnails(ifc_class=props.ifc_class, offset=9 * (self.page - 1), limit=9) props.type_page = self.page + bpy.ops.bim.load_type_thumbnails() return {"FINISHED"} @@ -623,29 +623,18 @@ class LoadTypeThumbnails(bpy.types.Operator): bl_idname = "bim.load_type_thumbnails" bl_label = "Load Type Thumbnails" bl_options = {"REGISTER", "UNDO"} - ifc_class: bpy.props.StringProperty() - limit: bpy.props.IntProperty() - offset: bpy.props.IntProperty() def execute(self, context): if bpy.app.background: return {"FINISHED"} - props = tool.Model.get_model_props() # Only process at most one paginated class at a time. # Large projects have hundreds of types which can lead to unnecessary lag. if not AuthoringData.is_loaded: AuthoringData.load() - queue = AuthoringData.data["type_elements_filtered"] - if self.limit: - queue = queue[self.offset : self.offset + self.limit] - else: - offset = 9 * (props.type_page - 1) - if offset < 0: - offset = 0 - queue = queue[offset : offset + 9] + queue = [tool.Ifc.get().by_id(t["id"]) for t in AuthoringData.data["paginated_relating_types"]] - # The active type may be in another page than the active one : + # The active type may be in another page than the active one: if relating_type_id_current := AuthoringData.data["relating_type_data"].get("id"): active_element = tool.Ifc.get_entity_by_id(relating_type_id_current) if active_element and active_element not in queue: diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 8c438a384f..3f7dc9475f 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -58,7 +58,7 @@ def get_materials( def update_ifc_class(self: "BIMModelProperties", context: bpy.types.Context) -> None: - bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class) + bpy.ops.bim.load_type_thumbnails() AuthoringData.data["ifc_class_current"] = self.ifc_class AuthoringData.data["type_elements"] = AuthoringData.type_elements() AuthoringData.data["type_elements_filtered"] = AuthoringData.type_elements_filtered() @@ -82,7 +82,7 @@ def update_relating_type_id(self: "BIMModelProperties", context: bpy.types.Conte def update_type_page(self: "BIMModelProperties", context: bpy.types.Context) -> None: AuthoringData.data["paginated_relating_types"] = AuthoringData.paginated_relating_types() - bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class, offset=9 * (self.type_page - 1), limit=9) + bpy.ops.bim.load_type_thumbnails() self["type_page"] = min(self["type_page"], AuthoringData.data["total_pages"]) self["type_page"] = max(self["type_page"], 1) @@ -118,7 +118,7 @@ def update_search_name(self: "BIMModelProperties", context: bpy.types.Context) - # Total number of pages may decrease when using the search bar : if self.type_page > AuthoringData.data["total_pages"]: self.type_page = max(1, AuthoringData.data["total_pages"]) - bpy.ops.bim.load_type_thumbnails(ifc_class=self.ifc_class) + bpy.ops.bim.load_type_thumbnails() def update_x_angle(self: "BIMModelProperties", context: bpy.types.Context) -> None: diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index bdaba83524..988506f74f 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -31,7 +31,6 @@ from bonsai.bim.module.model.data import ( RailingData, RoofData, ) -from bonsai.bim.module.model.prop import get_ifc_class from bonsai.bim.module.model.stair import regenerate_stair_mesh from bonsai.bim.module.model.railing import update_railing_modifier_bmesh from bonsai.bim.module.model.roof import update_roof_modifier_bmesh @@ -90,14 +89,7 @@ class LaunchTypeManager(bpy.types.Operator): def invoke(self, context, event): props = tool.Model.get_model_props() props.type_page = 1 - if get_ifc_class(None, context): - ifc_class = AuthoringData.data["ifc_class_current"] or AuthoringData.data["ifc_element_type"] - else: - ifc_class = AuthoringData.data["ifc_element_type"] - - # will be None if project has no types - if ifc_class is not None: - bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9) + bpy.ops.bim.load_type_thumbnails() return context.window_manager.invoke_props_dialog(self, width=550, title="Type Manager", confirm_text="Close") def draw(self, context): @@ -157,11 +149,11 @@ class LaunchTypeManager(bpy.types.Operator): op = row.operator("bim.set_active_type", text=relating_type["description"], emboss=False) op.relating_type = relating_type["id"] - if relating_type["icon_id"]: + if icon_id := AuthoringData.type_thumbnails.get(relating_type["id"], 0): # Yep, that's EXACTLY how it's done. And I'm proud of it. row1 = box.row() row1.ui_units_y = 0.01 - row1.template_icon(icon_value=relating_type["icon_id"], scale=4) + row1.template_icon(icon_value=icon_id, scale=4) row2 = box.column(align=True) row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] @@ -175,8 +167,7 @@ class LaunchTypeManager(bpy.types.Operator): row2.operator("bim.set_active_type", text="", emboss=False).relating_type = relating_type["id"] else: row = box.row() - op = box.operator("bim.load_type_thumbnails", text="", icon="FILE_REFRESH", emboss=False) - op.ifc_class = AuthoringData.data["ifc_class_current"] + box.operator("bim.load_type_thumbnails", text="", icon="FILE_REFRESH") row = box.row() row.alignment = "CENTER" diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index b2f0220582..14beafaee7 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -646,7 +646,7 @@ class CreateObjectUI: box = cls.layout.box() row = box.row(align=True) - thumbnail: int = relating_type_data["icon_id"] + thumbnail: int = AuthoringData.type_thumbnails.get(relating_type_data["id"], 0) row.template_icon(icon_value=thumbnail) row.operator("bim.launch_type_manager", text=relating_type_data["name"], emboss=False) row.operator( diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index 89ac192a65..14c1df884b 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -325,7 +325,7 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): new_obj.data = obj.data.copy() new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj) new.Name += " Copy" - bpy.ops.bim.load_type_thumbnails(ifc_class=new.is_a()) + bpy.ops.bim.load_type_thumbnails() if obj in context.selectable_objects: tool.Blender.select_and_activate_single_object(context, new_obj) else: diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 4741e16a59..beab7a4edf 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -35,7 +35,7 @@ from collections import defaultdict from bonsai.bim.ifc import IfcStore from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES from pathlib import Path -from typing import Optional, Union, TYPE_CHECKING, Generator, Callable +from typing import Optional, Union, TYPE_CHECKING, Generator if TYPE_CHECKING: from bonsai.bim.module.project.prop import BIMProjectProperties @@ -65,8 +65,7 @@ class Project(bonsai.core.tool.Project): @classmethod def load_default_thumbnails(cls): if tool.Ifc.get().by_type("IfcElementType"): - ifc_class = sorted(tool.Ifc.get().by_type("IfcElementType"), key=lambda e: e.is_a())[0].is_a() - bpy.ops.bim.load_type_thumbnails(ifc_class=ifc_class, offset=0, limit=9) + bpy.ops.bim.load_type_thumbnails() @classmethod def load_pset_templates(cls): From 9f1ffe63811a2ca85bcf2b2fe8791893789f1145 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 Mar 2025 18:08:43 +1100 Subject: [PATCH 268/476] Minor fix where you shouldn't be able to add an element without an IFC project --- src/bonsai/bonsai/bim/module/root/operator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 0e583e76cd..d141e7075e 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -626,10 +626,14 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): class LaunchAddElement(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.launch_add_element" - bl_label = "Add Element" + bl_label = "Launch Add Element" bl_options = {"REGISTER", "UNDO"} bl_description = "Add an IFC physical product, construction type, and more" + @classmethod + def poll(cls, context): + return tool.Ifc.get() + def execute(self, context): # This stub operator is needed because operators from menu skip the invoke call bpy.ops.bim.add_element("INVOKE_DEFAULT") From 1259607a9f3d2e4425790fe0ca28bbef2c869ee7 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 Mar 2025 18:10:05 +1100 Subject: [PATCH 269/476] See #1227. Fix slicing normal calculation for AXIS3 elements. --- src/bonsai/bonsai/tool/loader.py | 12 ++++++++---- .../ifcopenshell/util/representation.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 17fbdb82d6..e7fd1185a5 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1037,13 +1037,17 @@ class Loader(bonsai.core.tool.Loader): bm.from_mesh(mesh) prev_co = None # no = Vector((0.0, 1.0, 0.0)) - no = cls.get_extrusion_vector(element).normalized() - if usage and usage.LayerSetDirection == "AXIS2": + if not usage: + sense_factor = 1 # Assume the extrusion vector points in the direction sense + no = cls.get_extrusion_vector(element).normalized() + co = Vector((0.0, 0.0, offset)) + elif usage.LayerSetDirection == "AXIS2": co = Vector((0.0, offset, 0.0)) no = no.cross(Vector([1.0, 0.0, 0.0])) - else: - sense_factor = 1 # If it isn't AXIS2, then the normal points in the direction sense + elif usage.LayerSetDirection == "AXIS3": co = Vector((0.0, 0.0, offset)) + no = cls.get_extrusion_vector(element).normalized() + no = Vector([0.0, 0.0, 1.0 if no.z > 0 else -1.0]) # Cache this body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") styles = {} diff --git a/src/ifcopenshell-python/ifcopenshell/util/representation.py b/src/ifcopenshell-python/ifcopenshell/util/representation.py index 1b1423cf1f..31ba5a7bee 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/representation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/representation.py @@ -463,10 +463,19 @@ def get_material_style( return style -def get_reference_line(wall: ifcopenshell.entity_instance, fallback_length: float = 1.0): +def get_reference_line(wall: ifcopenshell.entity_instance, fallback_length: float = 1.0) -> list[npt.NDArray]: """Fetch the reference axis that goes in the +X direction + A base line will then be offset from this reference line based on the + material usage. From that base line, the layer thicknesses will offset + again, and be extruded to form the body representation. + :param wall: ifcopenshell.entity_instance + :param fallback_length: If there is no reference axis, assume it starts at + the object placement (i.e. 0.0, 0.0) and extends for this fallback + length along the +X axis. + :return: A list of two 2D coordinates representing the start and end of the + axis. The axis always goes in the +X direction. """ if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"): for item in ifcopenshell.util.representation.resolve_representation(axis).Items: From 4260313a990f3053c14c431a1d28e74b31ab35a8 Mon Sep 17 00:00:00 2001 From: Massimo Fabbro Date: Mon, 10 Mar 2025 10:04:58 +0100 Subject: [PATCH 270/476] Fix #6271 Fix the wrong length calculation for beams and columns --- src/bonsai/bonsai/bim/module/qto/calculator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index 24db92e11f..38bc8ca423 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -63,12 +63,12 @@ def get_linear_length(o: bpy.types.Object) -> float: return max(x, y, z) -def get_length(o: bpy.types.Object, vg_index: Optional[int] = None, main_axis: str = "x") -> float: +def get_length(o: bpy.types.Object, vg_index: Optional[int] = None) -> float: if vg_index is None: x = get_x(o) y = get_y(o) z = get_z(o) - if get_object_main_axis(o) == "x" or main_axis == "x": + if get_object_main_axis(o) == "x": return max(x, y) if get_object_main_axis(o) == "z": return max(z, x) From 823f27cd6a859470377221d3e36a0a00b1070ee9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 Mar 2025 22:53:55 +1100 Subject: [PATCH 271/476] Add tests for new element add feature --- src/bonsai/test/bim/feature/geometry.feature | 3 +- src/bonsai/test/bim/feature/root.feature | 68 ++++++- src/bonsai/test/bim/test_feature.py | 168 +++++++++++++----- .../ifcopenshell/util/representation.py | 4 +- 4 files changed, 195 insertions(+), 48 deletions(-) diff --git a/src/bonsai/test/bim/feature/geometry.feature b/src/bonsai/test/bim/feature/geometry.feature index eeda85681f..16dddb4a39 100644 --- a/src/bonsai/test/bim/feature/geometry.feature +++ b/src/bonsai/test/bim/feature/geometry.feature @@ -189,8 +189,7 @@ Scenario: Update representation - updating a layered extrusion And I press "bim.edit_material_set_item(material_set_item={layer})" And I press "bim.edit_assigned_material(material_set={layer_set})" And the variable "type" is "{ifc}.by_type('IfcWallType')[0].id()" - And I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')" - When I press "bim.update_representation(obj='IfcWall/Cube')" + When I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')" Then the object "IfcWall/Cube" has a "SweptSolid" representation of "Model/Body/MODEL_VIEW" Scenario: Update representation - updating a profiled extrusion diff --git a/src/bonsai/test/bim/feature/root.feature b/src/bonsai/test/bim/feature/root.feature index f8cd1c14bf..9a5ad5d698 100644 --- a/src/bonsai/test/bim/feature/root.feature +++ b/src/bonsai/test/bim/feature/root.feature @@ -1,6 +1,70 @@ @root Feature: Root +Scenario: Add element - a type with no geometry + Given an empty IFC project + And I trigger "Add Element" + And I set the "Name" property to "Foo" + And I set the "Description" property to "Bar" + And I set the "Definition" property to "IfcElementType" + And I set the "Class" property to "IfcFurnitureType" + And I set the "Predefined Type" property to "SOFA" + And I set the "Representation" property to "No Geometry" + When I click "OK" + Then the object "IfcFurnitureType/Foo" exists + And the object "IfcFurnitureType/Foo" has no data + +Scenario: Add element - an element with no geometry + Given an empty IFC project + And I trigger "Add Element" + And I set the "Definition" property to "IfcElement" + And I set the "Class" property to "IfcFurniture" + And I set the "Predefined Type" property to "SOFA" + And I set the "Representation" property to "Custom Extruded Solid" + When I click "OK" + And I select the object "IfcFurniture/Unnamed" + And I toggle edit mode + Then the object "Item/IfcExtrudedAreaSolid/77" exists + +Scenario: Add element - an element with extrusion geometry + Given an empty IFC project + And I trigger "Add Element" + And I set the "Definition" property to "IfcElement" + And I set the "Class" property to "IfcFurniture" + And I set the "Predefined Type" property to "SOFA" + And I set the "Representation" property to "Custom Extruded Solid" + When I click "OK" + And I select the object "IfcFurniture/Unnamed" + And I toggle edit mode + Then the object "Item/IfcExtrudedAreaSolid/77" exists + +Scenario: Add element - an element with custom tessellation geometry + Given an empty IFC project + And I trigger "Add Element" + And I set the "Definition" property to "IfcElement" + And I set the "Class" property to "IfcFurniture" + And I set the "Predefined Type" property to "SOFA" + And I set the "Representation" property to "Custom Tessellation" + When I click "OK" + And I select the object "IfcFurniture/Unnamed" + And I toggle edit mode + Then the object "Item/IfcPolygonalFaceSet/76" exists + +Scenario: Add element - an element with tessellation geometry from an object + Given an empty IFC project + And I add a cube + And I trigger "Add Element" + And I set the "Definition" property to "IfcElement" + And I set the "Class" property to "IfcFurniture" + And I set the "Predefined Type" property to "SOFA" + And I set the "Representation" property to "Tessellation From Object" + And I set the "Object" property to "Cube" + When I click "OK" + And I select the object "IfcFurniture/Unnamed" + And I toggle edit mode + Then the object "Item/IfcPolygonalFaceSet/76" exists + And the object "Item/IfcPolygonalFaceSet/76" dimensions are "2,2,2" + Scenario: Reassign class Given an empty IFC project And I add a cube @@ -77,7 +141,7 @@ Scenario: Assign a spatial class to a cube already in a collection And I set "scene.BIMRootProperties.ifc_class" to "IfcSpace" And I press "bim.assign_class" Then the object "IfcSpace/Cube" is an "IfcSpace" - And the object "IfcSpace/Cube" is in the collection "IfcSpace/Cube" + And the object "IfcSpace/Cube" is in the collection "IfcSpace" And the object "IfcSpace/Cube" has a "Tessellation" representation of "Model/Body/MODEL_VIEW" Scenario: Assign a class to a cube in a collection @@ -103,7 +167,7 @@ Scenario: Copy a wall Scenario: Copy a storey - when locked Given an empty IFC project And the object "IfcBuildingStorey/My Storey" is selected - When I duplicate the selected objects + Then I expect an error "Error: 'IfcBuildingStorey/My Storey' is locked. Unlock it via the Spatial panel in the Project Overview tab." when "i_duplicate_the_selected_objects()" Then the object "IfcBuildingStorey/My Storey.001" does not exist Scenario: Copy a storey - when unlocked diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index c5e084e031..94ab5db19f 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -65,12 +65,14 @@ class PanelSpy: def __getattr__(self, attr): self.spied_attr = attr + if annotation := self.panel.__annotations__.get(attr, None): + return annotation.keywords.get("default", None) # An operator property if attr == "layout": return self return self def __call__(self, *args, **kwargs): - if self.spied_attr in ("row", "column", "box", "separator"): + if self.spied_attr in ("row", "column", "box", "separator", "menu", "operator_menu_enum"): return self elif self.spied_attr == "template_list": listtype_name, list_id, dataptr, propname, active_dataptr, active_propname = args @@ -123,7 +125,10 @@ class PanelSpy: prefix, op_name = operator.split(".") operator = getattr(getattr(bpy.ops, prefix), op_name) bl_idname = operator.idname() - bl_label = getattr(bpy.types, bl_idname).bl_label + try: + bl_label = getattr(bpy.types, bl_idname).bl_label + except: # Doesn't work on built-ins, I don't know what to do + bl_label = bl_idname text = kwargs.get("text", bl_label) icon = kwargs.get("icon", None) if text: @@ -152,10 +157,29 @@ class TemplateListSpy: self.spied_data = spied_data -panel_name_cache = {} +ui_name_cache = {} panel_spy: PanelSpy = None +def create_ui_name_cache(): + global ui_name_cache + if ui_name_cache: + return + for bl_idname in dir(bpy.types): + try: + panel_type = getattr(bpy.types, bl_idname) + if panel_type.bl_rna.base.name == "Panel": + ui_name_cache[panel_type.bl_label] = panel_type.bl_idname + elif panel_type.bl_rna.base.name == "Operator": + ui_name_cache[panel_type.bl_label] = bl_idname + elif panel_type.bl_rna.base.name == "Menu": + if panel_type.bl_label == "Add" and bl_idname != "VIEW3D_MT_add": + continue # Non-unique, but "VIEW3D_MT_add" is the one we care about + ui_name_cache[panel_type.bl_label] = bl_idname + except: + pass + + def replace_variables(value): for key, new_value in variables.items(): value = value.replace("{" + key + "}", str(new_value)) @@ -218,22 +242,40 @@ def the_brickschema_is_stubbed(): @given(parsers.parse('I look at the "{panel}" panel')) @when(parsers.parse('I look at the "{panel}" panel')) -@when(parsers.parse('I look at the "{panel}" panel')) +@then(parsers.parse('I look at the "{panel}" panel')) def i_look_at_the_panel_panel(panel): - global panel_name_cache + global ui_name_cache global panel_spy - if not panel_name_cache: - for bl_idname in dir(bpy.types): - try: - panel_type = getattr(bpy.types, bl_idname) - if panel_type.bl_rna.base.name != "Panel": - continue - panel_name_cache[panel_type.bl_label] = panel_type.bl_idname - except: - pass - if panel not in panel_name_cache: - assert False, f"Panel {panel} not found in {panel_name_cache}" - panel_spy = PanelSpy(getattr(bpy.types, panel_name_cache[panel])) + create_ui_name_cache() + if panel not in ui_name_cache: + assert False, f"Panel {panel} not found in {ui_name_cache}" + panel_spy = PanelSpy(getattr(bpy.types, ui_name_cache[panel])) + panel_spy.refresh_spy() + + +@given(parsers.parse('I open the "{name}" menu')) +@when(parsers.parse('I open the "{name}" menu')) +@then(parsers.parse('I open the "{name}" menu')) +def i_open_the_name_menu(name): + global ui_name_cache + global panel_spy + create_ui_name_cache() + if name not in ui_name_cache: + assert False, f"Menu {name} not found in {ui_name_cache}" + panel_spy = PanelSpy(getattr(bpy.types, ui_name_cache[name])) + panel_spy.refresh_spy() + + +@given(parsers.parse('I trigger "{operator}"')) +@when(parsers.parse('I trigger "{operator}"')) +@then(parsers.parse('I trigger "{operator}"')) +def i_trigger_operator(operator): + global ui_name_cache + global panel_spy + create_ui_name_cache() + if operator not in ui_name_cache: + assert False, f"Operator {operator} not found in {ui_name_cache}" + panel_spy = PanelSpy(getattr(bpy.types, ui_name_cache[operator])) panel_spy.refresh_spy() @@ -301,27 +343,36 @@ def i_see_the_prop_property_is_value(prop, value): def i_set_the_prop_property_to_value(prop, value): value = value.strip() panel_spy.refresh_spy() - for spied_prop in panel_spy.spied_props: - if prop in (spied_prop["name"], spied_prop["text"], spied_prop["icon"]): - if spied_prop["prop_type"] == "BOOLEAN": - if value == "TRUE": - setattr(spied_prop["props"], spied_prop["name"], True) - elif value == "FALSE": - setattr(spied_prop["props"], spied_prop["name"], False) - elif spied_prop["prop_type"] == "FLOAT": - setattr(spied_prop["props"], spied_prop["name"], float(value)) - elif spied_prop["prop_type"] == "INT": - setattr(spied_prop["props"], spied_prop["name"], int(value)) - elif spied_prop["prop_type"] == "ENUM": - enum_identifier = [i for i in spied_prop["enum_items"] if i is not None and i[1] == value] - if not enum_identifier: - assert False, f"Could not find value {value} in enum {spied_prop['enum_items']}" - setattr(spied_prop["props"], spied_prop["name"], enum_identifier[0][0]) - else: - setattr(spied_prop["props"], spied_prop["name"], value) - panel_spy.is_spy_dirty = True - return - assert False, f"Property {prop} not found in {panel_spy.spied_props}" + is_nth = False + if prop[0].isnumeric() and (prop.endswith("st") or prop.endswith("nd") or prop.endswith("th")): + is_nth = True + for nth, spied_prop in enumerate(panel_spy.spied_props): + if is_nth and nth != int(prop[:-2]) - 1: + continue + if not is_nth and prop not in (spied_prop["name"], spied_prop["text"], spied_prop["icon"]): + continue + if spied_prop["prop_type"] == "BOOLEAN": + if value == "TRUE": + setattr(spied_prop["props"], spied_prop["name"], True) + elif value == "FALSE": + setattr(spied_prop["props"], spied_prop["name"], False) + elif spied_prop["prop_type"] == "FLOAT": + setattr(spied_prop["props"], spied_prop["name"], float(value)) + elif spied_prop["prop_type"] == "INT": + setattr(spied_prop["props"], spied_prop["name"], int(value)) + elif spied_prop["prop_type"] == "ENUM": + enum_identifier = [i for i in spied_prop["enum_items"] if i is not None and i[1] == value] + if not enum_identifier: + assert False, f"Could not find value {value} in enum {spied_prop['enum_items']}" + setattr(spied_prop["props"], spied_prop["name"], enum_identifier[0][0]) + elif spied_prop["prop_type"] == "POINTER": + setattr(spied_prop["props"], spied_prop["name"], bpy.data.objects.get(value)) + else: + setattr(spied_prop["props"], spied_prop["name"], value) + panel_spy.is_spy_dirty = True + return + debug = "\n".join([f"{i} {v}" for i, v in enumerate(panel_spy.spied_props)]) + assert False, f"Property {prop} not found in:\n{debug}" @then(parsers.parse('The "{name}" list has {total} items')) @@ -443,6 +494,20 @@ def i_add_a_plane_of_size_size_at_location(size, location): bpy.ops.mesh.primitive_plane_add(size=float(size), location=[float(co) for co in location.split(",")]) +@then(parsers.parse('I expect an error "{error_msg}" when "{function}"')) +def i_expect_an_error_msg_when_function(error_msg, function): + try: + exec(function) + except Exception as e: + actual_error_msg = str(e).strip() + if str(e).strip() != error_msg: + traceback.print_exc() + msg = f"Got different exception running {function} - '{actual_error_msg}' instead of '{error_msg}'" + assert False, msg + return + assert False, f"Function {function} ran without exception '{error_msg}'" + + @then(parsers.parse('I press "{operator}" and expect error "{error_msg}"')) def i_press_operator_and_expect_error(operator, error_msg): operator = replace_variables(operator) @@ -451,13 +516,14 @@ def i_press_operator_and_expect_error(operator, error_msg): exec(f"bpy.ops.{operator}") else: exec(f"bpy.ops.{operator}()") - assert False, f"Operator bpy.ops.{operator} ran without exception '{error_msg}'" except Exception as e: actual_error_msg = str(e).strip() if str(e).strip() != error_msg: traceback.print_exc() msg = f"Got different exception running bpy.ops.{operator} - '{actual_error_msg}' instead of '{error_msg}'" assert False, msg + return + assert False, f"Operator bpy.ops.{operator} ran without exception '{error_msg}'" @given(parsers.parse('I press "{operator}"')) @@ -489,7 +555,11 @@ def i_click_button(button): val = getattr(spied_prop["props"], spied_prop["name"]) setattr(spied_prop["props"], spied_prop["name"], not bool(val)) return - assert False, f"Could not find {button} in {panel_spy.spied_operators}" + if button == "OK" and panel_spy.panel.bl_rna.base.name == "Operator": + # Clicked confirm on an operator's draw dialog + return i_press_operator(panel_spy.panel.bl_idname) + debug = "\n".join([f"{i} {v}" for i, v in enumerate(panel_spy.spied_operators)]) + assert False, f"Could not find {button}:\n{debug}" @given(parsers.parse('I click "{button}" and expect error "{error_msg}"')) @@ -541,6 +611,8 @@ def i_deselect_all_objects(): @given(parsers.parse('the object "{name}" is selected')) @when(parsers.parse('the object "{name}" is selected')) +@given(parsers.parse('I select the object "{name}"')) +@when(parsers.parse('I select the object "{name}"')) def the_object_name_is_selected(name): i_deselect_all_objects() additionally_the_object_name_is_selected(name) @@ -666,7 +738,8 @@ def the_object_name_exists(name: str) -> bpy.types.Object: else: obj = bpy.data.objects.get(name) if not obj: - assert False, f'The object "{name}" does not exist' + debug = "\n".join([o.name for o in bpy.data.objects]) + assert False, f'The object "{name}" does not exist:\n{debug}' return obj @@ -1258,6 +1331,16 @@ def construction_type(relating_type_name): assert relating_type == relating_type_name, f"Construction Type is a {relating_type}, not a {relating_type_name}" +@given("I toggle edit mode") +@when("I toggle edit mode") +@then("I toggle edit mode") +def i_toggle_edit_mode(): + if bpy.context.mode == "OBJECT": + bpy.ops.bim.override_mode_set_edit() + else: + bpy.ops.bim.override_mode_set_object() + + @when("I move the cursor to the bottom left corner") def move_cursor_bottom_left(): bpy.context.window.cursor_warp(10, 10) @@ -1330,7 +1413,7 @@ def the_obj1_and_obj2_belong_the_same_linked_aggregate_group(obj_name1, obj_name def the_obj_layer_lenght_is_set_to(value): value = float(value) try: - eval(f"bpy.context.scene.BIMModelProperties.length") + eval("bpy.context.scene.BIMModelProperties.length") except: assert False, f"Property BIMModelProperties.length does not exist when trying to set to value {value}" @@ -1356,6 +1439,7 @@ def run_test_code(): @then(parsers.parse("I save sample test files")) def saving_sample_test_files(and_open_in_blender=None): filepath = f"{variables['cwd']}/test/files/temp/sample_test_file" + print(f"Saved to {filepath}") bpy.ops.bim.save_project(filepath=f"{filepath}.ifc", should_save_as=True) bpy.ops.wm.save_as_mainfile(filepath=f"{filepath}.blend") diff --git a/src/ifcopenshell-python/ifcopenshell/util/representation.py b/src/ifcopenshell-python/ifcopenshell/util/representation.py index 31ba5a7bee..f5233005af 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/representation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/representation.py @@ -235,12 +235,12 @@ def guess_type(items: Sequence[ifcopenshell.entity_instance]) -> Union[str, None ] ): return "SurfaceModel" - elif all([True if i.is_a("IfcSolidModel") else False for i in items]): - return "SolidModel" elif all( [True if i.is_a() == "IfcExtrudedAreaSolid" or i.is_a() == "IfcRevolvedAreaSolid" else False for i in items] ): return "SweptSolid" + elif all([True if i.is_a("IfcSolidModel") else False for i in items]): + return "SolidModel" elif all( [ ( From 41188e26e1fba582e63b4acac22b5ed625db0fa8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 Mar 2025 22:54:42 +1100 Subject: [PATCH 272/476] See #1227. Don't touch manual booleans when regenerating wall body. --- .../regenerate_wall_representation.py | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py index 06d2d2720b..0b748d7e0c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import json import numpy as np import ifcopenshell import ifcopenshell.api.geometry @@ -119,6 +120,8 @@ class Regenerator: self.end_points = [] self.end_vector = np.array((0.0, 0.0, 1.0)) self.end_offset = 0.0 + manual_booleans = self.get_manual_booleans(wall) + for rel in wall.ConnectedTo: if rel.is_a("IfcRelConnectsPathElements"): wall2 = rel.RelatedElement @@ -160,6 +163,9 @@ class Regenerator: builder = ifcopenshell.util.shape_builder.ShapeBuilder(self.file) + # Don't offset wall if there are manual booleans, because that'll also shift operands + offset = None if manual_booleans else self.reference_p1 * -1 + if self.is_angled: start_points = [p.copy() for p in self.start_points] end_points = [p.copy() for p in self.end_points] @@ -174,7 +180,7 @@ class Regenerator: end_points.reverse() points.extend(end_points) item = builder.extrude( - builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1), + builder.polyline(points, closed=True, position_offset=offset), magnitude=self.wall_vectors["d"], extrusion_vector=self.wall_vectors["z"], ) @@ -195,7 +201,7 @@ class Regenerator: magnitude = np.linalg.norm(self.start_vector * (self.wall_vectors["h"] / self.start_vector[2])) operands.append( builder.extrude( - builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1), + builder.polyline(points, closed=True, position_offset=offset), magnitude=magnitude, extrusion_vector=self.start_vector, ) @@ -217,7 +223,7 @@ class Regenerator: magnitude = np.linalg.norm(self.end_vector * (self.wall_vectors["h"] / self.end_vector[2])) operands.append( builder.extrude( - builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1), + builder.polyline(points, closed=True, position_offset=offset), magnitude=magnitude, extrusion_vector=self.end_vector, ) @@ -229,7 +235,7 @@ class Regenerator: magnitude = np.linalg.norm(atpath_vector * (self.wall_vectors["h"] / atpath_vector[2])) operands.append( builder.extrude( - builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1), + builder.polyline(points, closed=True, position_offset=offset), magnitude=magnitude, extrusion_vector=atpath_vector, ) @@ -286,14 +292,10 @@ class Regenerator: remaining_path_points.append(minpath_points) self.minpath_points = remaining_path_points - profiles.append( - builder.profile(builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1)) - ) + profiles.append(builder.profile(builder.polyline(points, closed=True, position_offset=offset))) for points in self.maxpath_points + self.minpath_points: - profiles.append( - builder.profile(builder.polyline(points, closed=True, position_offset=self.reference_p1 * -1)) - ) + profiles.append(builder.profile(builder.polyline(points, closed=True, position_offset=offset))) if len(profiles) > 1: profile = self.file.createIfcCompositeProfileDef("AREA", Profiles=profiles) @@ -301,6 +303,10 @@ class Regenerator: profile = profiles[0] item = builder.extrude(profile, magnitude=self.wall_vectors["d"], extrusion_vector=self.wall_vectors["z"]) + for boolean in self.get_manual_booleans(wall): + boolean.FirstOperand = item + item = boolean + body_rep = builder.get_representation(self.body, items=[item]) if old_rep := ifcopenshell.util.representation.get_representation(wall, self.body): ifcopenshell.util.element.replace_element(old_rep, body_rep) @@ -308,7 +314,7 @@ class Regenerator: else: ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=body_rep) - item = builder.polyline([self.reference_p1, self.reference_p2], position_offset=self.reference_p1 * -1) + item = builder.polyline([self.reference_p1, self.reference_p2], position_offset=offset) axis_rep = builder.get_representation(self.axis, items=[item]) if old_rep := ifcopenshell.util.representation.get_representation(wall, self.axis): ifcopenshell.util.element.replace_element(old_rep, axis_rep) @@ -316,7 +322,7 @@ class Regenerator: else: ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=axis_rep) - if not np.allclose(self.reference_p1, np.array((0.0, 0.0))): + if not np.allclose(self.reference_p1, np.array((0.0, 0.0))) and not manual_booleans: children = [] for referenced_placement in wall.ObjectPlacement.ReferencedByPlacements: matrix = ifcopenshell.util.placement.get_local_placement(referenced_placement) @@ -599,7 +605,7 @@ class Regenerator: return result * -1 return result - def get_axes(self, wall, reference, layers: list[PrioritisedLayer], angle: float): + def get_axes(self, wall: ifcopenshell.entity_instance, reference, layers: list[PrioritisedLayer], angle: float): axes = [[p.copy() for p in reference]] # Apply usage to convert the Reference line into MlsBase sense_factor = 1 @@ -612,3 +618,11 @@ class Regenerator: y_offset = (layer.thickness * sense_factor) / cos(angle) axes.append([p.copy() + np.array((0.0, y_offset)) for p in axes[-1]]) return axes + + def get_manual_booleans(self, element: ifcopenshell.entity_instance): + if pset := ifcopenshell.util.element.get_pset(element, "BBIM_Boolean"): + try: + return [self.file.by_id(boolean_id) for boolean_id in json.loads(pset["Data"])] + except: + return [] + return [] From c03cf4ce58f9f633e0786dc691a5b94292b82c24 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 Mar 2025 23:06:16 +1100 Subject: [PATCH 273/476] Fix #6303. I messed up the commit in 1259607a --- src/bonsai/bonsai/tool/loader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index e7fd1185a5..3ca43735c8 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1036,18 +1036,22 @@ class Loader(bonsai.core.tool.Loader): bm = bmesh.new() bm.from_mesh(mesh) prev_co = None - # no = Vector((0.0, 1.0, 0.0)) if not usage: sense_factor = 1 # Assume the extrusion vector points in the direction sense no = cls.get_extrusion_vector(element).normalized() co = Vector((0.0, 0.0, offset)) elif usage.LayerSetDirection == "AXIS2": co = Vector((0.0, offset, 0.0)) + no = cls.get_extrusion_vector(element).normalized() no = no.cross(Vector([1.0, 0.0, 0.0])) elif usage.LayerSetDirection == "AXIS3": co = Vector((0.0, 0.0, offset)) no = cls.get_extrusion_vector(element).normalized() no = Vector([0.0, 0.0, 1.0 if no.z > 0 else -1.0]) + elif usage.LayerSetDirection == "AXIS3": + co = Vector((0.0, 0.0, offset)) + no = cls.get_extrusion_vector(element).normalized() + no = Vector([1.0 if no.x > 0 else -1.0, 0.0, 0.0]) # Cache this body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") styles = {} From 7fb6adc025c43364054230f6b78603957e4a8098 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 10 Mar 2025 23:15:24 +1100 Subject: [PATCH 274/476] Fix #6296. See #1227. More normal fixes in layer set slicing. --- src/bonsai/bonsai/tool/loader.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 3ca43735c8..b9df539566 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1047,11 +1047,12 @@ class Loader(bonsai.core.tool.Loader): elif usage.LayerSetDirection == "AXIS3": co = Vector((0.0, 0.0, offset)) no = cls.get_extrusion_vector(element).normalized() - no = Vector([0.0, 0.0, 1.0 if no.z > 0 else -1.0]) - elif usage.LayerSetDirection == "AXIS3": + no = Vector([0.0, 0.0, 1.0]) + elif usage.LayerSetDirection == "AXIS1": co = Vector((0.0, 0.0, offset)) no = cls.get_extrusion_vector(element).normalized() - no = Vector([1.0 if no.x > 0 else -1.0, 0.0, 0.0]) + no = Vector([1.0, 0.0, 0.0]) + no *= sense_factor # Cache this body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") styles = {} @@ -1061,8 +1062,7 @@ class Loader(bonsai.core.tool.Loader): styles[style] = i for layer in layer_set.MaterialLayers[:-1]: prev_co = co.copy() - co += no * layer.LayerThickness * cls.unit_scale * sense_factor - # co.y += layer.LayerThickness * cls.unit_scale * sense_factor + co += no * layer.LayerThickness * cls.unit_scale bisect_geom = bmesh.ops.bisect_plane( bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no ) @@ -1074,7 +1074,6 @@ class Loader(bonsai.core.tool.Loader): for face in bisect_geom["geom"]: if isinstance(face, bmesh.types.BMFace): center = face.calc_center_median() - # if center.y < co.y and center.y > prev_co.y: if (center - co).dot(no) < 0 and (center - prev_co).dot(no) >= 0: face.material_index = material_index has_layer_styles = True @@ -1087,7 +1086,7 @@ class Loader(bonsai.core.tool.Loader): mesh.materials.append(tool.Ifc.get_object(style)) for face in bisect_geom["geom"]: if isinstance(face, bmesh.types.BMFace): - center = face.calc_center_median() * sense_factor + center = face.calc_center_median() # if center.y > co.y: if (center - co).dot(no) >= 0: face.material_index = material_index From 566fe32ab2e5194dbf52b7cea11b5fbb131166c6 Mon Sep 17 00:00:00 2001 From: Massimo Fabbro Date: Mon, 10 Mar 2025 13:11:36 +0100 Subject: [PATCH 275/476] Fix wrong wall legth qto calculation --- src/ifc5d/ifc5d/IFC4QtoBaseQuantitiesBlender.json | 2 +- src/ifc5d/ifc5d/qto.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ifc5d/ifc5d/IFC4QtoBaseQuantitiesBlender.json b/src/ifc5d/ifc5d/IFC4QtoBaseQuantitiesBlender.json index 3dcfd882ac..ac0acbe67e 100644 --- a/src/ifc5d/ifc5d/IFC4QtoBaseQuantitiesBlender.json +++ b/src/ifc5d/ifc5d/IFC4QtoBaseQuantitiesBlender.json @@ -611,7 +611,7 @@ "GrossVolume": "get_gross_volume", "GrossWeight": "get_gross_weight", "Height": "get_height", - "Length": "get_length", + "Length": "get_x", "NetFootprintArea": "get_net_footprint_area", "NetSideArea": "get_net_side_area", "NetVolume": "get_net_volume", diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 417a2efee5..1c99cccc74 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -336,6 +336,7 @@ class Blender: "get_gross_perimeter": Function("IfcLengthMeasure", "Gross Perimeter", ""), "get_height": Function("IfcLengthMeasure", "Height", ""), "get_length": Function("IfcLengthMeasure", "Length", ""), + "get_x": Function("IfcLengthMeasure", "Length", ""), "get_opening_depth": Function("IfcLengthMeasure", "Opening Depth", ""), "get_opening_height": Function("IfcLengthMeasure", "Opening Height", ""), "get_rectangular_perimeter": Function("IfcLengthMeasure", "Rectangular Perimeter", ""), From 3886c79a2c35695aa252e891347fb21c4af570d1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 10 Mar 2025 18:52:15 +0500 Subject: [PATCH 276/476] black . --- src/bonsai/bonsai/bim/module/model/slab.py | 6 ++++-- src/bonsai/bonsai/bim/module/project/operator.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 54f8f57fa8..8d12a15049 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -300,9 +300,11 @@ class DumbSlabPlaner: existing_x_angle = tool.Model.get_existing_x_angle(extrusion) existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle - existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 2*pi, tolerance=0.001) else existing_x_angle + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 2 * pi, tolerance=0.001) else existing_x_angle direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios) - offset_direction = Vector((abs(direction_ratios.x), abs(direction_ratios.y), abs(direction_ratios.z))) # The offset direction doesn't change with direction sense + offset_direction = Vector( + (abs(direction_ratios.x), abs(direction_ratios.y), abs(direction_ratios.z)) + ) # The offset direction doesn't change with direction sense perpendicular_depth = thickness * abs(1 / cos(existing_x_angle)) perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle)) / self.unit_scale diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 57262a7da5..36abbe8ffd 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1282,7 +1282,7 @@ class UnloadLink(bpy.types.Operator): if empty_handle := link.empty_handle: bpy.data.objects.remove(empty_handle) - #following lines removes the library also when use_relative_path=True, otherwise it doesn't + # following lines removes the library also when use_relative_path=True, otherwise it doesn't libraries = bpy.data.libraries for library in libraries: if library.name == self.filepath + ".cache.blend": @@ -1452,8 +1452,8 @@ class ReloadLink(bpy.types.Operator): is_abs = os.path.isabs(Path(self.filepath)) use_relative_path = not is_abs - bpy.ops.bim.unlink_ifc(filepath = self.filepath) - status = bpy.ops.bim.link_ifc(filepath=self.filepath, use_cache=False, use_relative_path = use_relative_path) + bpy.ops.bim.unlink_ifc(filepath=self.filepath) + status = bpy.ops.bim.link_ifc(filepath=self.filepath, use_cache=False, use_relative_path=use_relative_path) return {"FINISHED"} From c4b57eb9037811a3e96a6af97211aad740a2672e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 7 Mar 2025 20:38:01 +0500 Subject: [PATCH 277/476] typing --- src/bonsai/bonsai/bim/module/style/prop.py | 4 +- .../bonsai/bim/module/system/decorator.py | 3 +- .../bonsai/bim/module/system/operator.py | 8 ++-- src/bonsai/bonsai/bim/module/system/prop.py | 11 ++++- src/bonsai/bonsai/bim/module/system/ui.py | 47 ++++++++++++++----- src/bonsai/bonsai/tool/system.py | 3 +- src/bonsai/scripts/bonsai_translations.py | 9 ++-- src/bonsai/test/tool/test_system.py | 22 +++++---- .../api/system/unassign_system.py | 10 +--- 9 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/style/prop.py b/src/bonsai/bonsai/bim/module/style/prop.py index d533473ba2..7daf36f0c4 100644 --- a/src/bonsai/bonsai/bim/module/style/prop.py +++ b/src/bonsai/bonsai/bim/module/style/prop.py @@ -284,8 +284,8 @@ class BIMStylesProperties(PropertyGroup): active_style_index: IntProperty(name="Active Style Index") @property - def active_style(self): - return self.styles[self.active_style_index] if 0 <= self.active_style_index < len(self.styles) else None + def active_style(self) -> Union[Style, None]: + return tool.Blender.get_active_uilist_element(self.styles, self.active_style_index) active_style_type: EnumProperty( name="Active Style Type", diff --git a/src/bonsai/bonsai/bim/module/system/decorator.py b/src/bonsai/bonsai/bim/module/system/decorator.py index 3f902d0407..a463aae2e9 100644 --- a/src/bonsai/bonsai/bim/module/system/decorator.py +++ b/src/bonsai/bonsai/bim/module/system/decorator.py @@ -41,7 +41,8 @@ def transparent_color(color, alpha=0.1): @persistent def toggle_decorations_on_load(*args): - if bpy.context.scene.BIMSystemProperties.should_draw_decorations: + props = tool.System.get_system_props() + if props.should_draw_decorations: SystemDecorator.install(bpy.context) else: SystemDecorator.uninstall() diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 25f3c9d00a..597c4153c3 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -50,7 +50,8 @@ class AddSystem(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - core.add_system(tool.Ifc, tool.System, ifc_class=context.scene.BIMSystemProperties.system_class) + props = tool.System.get_system_props() + core.add_system(tool.Ifc, tool.System, ifc_class=props.system_class) class EditSystem(bpy.types.Operator, tool.Ifc.Operator): @@ -59,9 +60,8 @@ class EditSystem(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - core.edit_system( - tool.Ifc, tool.System, system=tool.Ifc.get().by_id(context.scene.BIMSystemProperties.edited_system_id) - ) + props = tool.System.get_system_props() + core.edit_system(tool.Ifc, tool.System, system=tool.Ifc.get().by_id(props.edited_system_id)) class RemoveSystem(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index 3e14bf74e5..bd4ccc238a 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -34,7 +34,7 @@ from bpy.props import ( from typing import TYPE_CHECKING -def get_system_class(self, context): +def get_system_class(self: "BIMSystemProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not SystemData.is_loaded: SystemData.load() return SystemData.data["system_class"] @@ -45,13 +45,20 @@ class System(PropertyGroup): ifc_class: StringProperty(name="IFC Class") ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + ifc_class: str + ifc_definition_id: int + class Zone(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + ifc_definition_id: int -def toggle_decorations(self, context): + +def toggle_decorations(self: "BIMSystemProperties", context: bpy.types.Context) -> None: toggle = self.should_draw_decorations if toggle: decorator.SystemDecorator.install(context) diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 8c3d00dceb..432cf1b76a 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -16,12 +16,17 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import bonsai.bim.helper import bonsai.tool as tool from bonsai.bim.helper import prop_with_search, draw_attributes from bpy.types import Panel, UIList from bonsai.bim.module.system.data import SystemData, ZonesData, ActiveObjectZonesData, ObjectSystemData, PortData +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.system.prop import BIMSystemProperties, System, BIMZoneProperties, Zone FLOW_DIRECTION_TO_ICON = { @@ -69,7 +74,7 @@ class BIM_PT_systems(Panel): op = row.operator("bim.unassign_system", text="", icon="X") op.system = system_id - self.props = context.scene.BIMSystemProperties + self.props = tool.System.get_system_props() row = self.layout.row(align=True) row.prop(self.props, "should_draw_decorations") @@ -142,7 +147,8 @@ class BIM_PT_ports(Panel): def draw(self, context): if not PortData.is_loaded: PortData.load() - self.props = context.scene.BIMSystemProperties + + self.props = tool.System.get_system_props() row = self.layout.row(align=True) total_ports = PortData.data["total_ports"] @@ -217,7 +223,7 @@ class BIM_PT_port(Panel): return True def draw(self, context): - self.props = context.scene.BIMSystemProperties + self.props = tool.System.get_system_props() layout = self.layout row = layout.row(align=True) @@ -295,7 +301,7 @@ class BIM_PT_flow_controls(Panel): if not ObjectSystemData.is_loaded: ObjectSystemData.load() - def display_element(control_id, flow_element_id, displayed_object_name): + def display_element(control_id: int, flow_element_id: int, displayed_object_name: str) -> None: displayed_object = bpy.data.objects[displayed_object_name] row = self.layout.row(align=True) op = row.operator("bim.assign_unassign_flow_control", text="", icon="X") @@ -355,13 +361,14 @@ class BIM_PT_zones(Panel): row.operator("bim.load_zones", text="", icon="IMPORT") return - row.operator("bim.add_zone", text="", icon="ADD") row.operator("bim.unload_zones", text="", icon="CANCEL") + row = self.layout.row(align=True) + row.alignment = "RIGHT" + row.operator("bim.add_zone", text="", icon="ADD") if self.props.zones and self.props.active_zone_index < len(self.props.zones): - row = self.layout.row(align=True) ifc_definition_id = self.props.zones[self.props.active_zone_index].ifc_definition_id - row.operator("bim.enable_editing_zone", text="Edit Zone", icon="GREASEPENCIL").zone = ifc_definition_id + row.operator("bim.enable_editing_zone", text="", icon="GREASEPENCIL").zone = ifc_definition_id row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF").system = ifc_definition_id row.operator("bim.assign_system", text="", icon="KEYFRAME_HLT").system = ifc_definition_id row.operator("bim.unassign_system", text="", icon="KEYFRAME").system = ifc_definition_id @@ -403,18 +410,27 @@ class BIM_PT_active_object_zones(Panel): class BIM_UL_systems(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMSystemProperties, + item: System, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) row.label(text=item.name, icon=SYSTEM_ICONS[item.ifc_class]) system_id = item.ifc_definition_id row.operator("bim.assign_system", text="", icon="ADD").system = item.ifc_definition_id - if context.scene.BIMSystemProperties.edited_system_id == system_id: + if data.edited_system_id == system_id: op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") op.system = system_id row.operator("bim.edit_system", text="", icon="CHECKMARK") row.operator("bim.disable_editing_system", text="", icon="CANCEL") - elif context.scene.BIMSystemProperties.edited_system_id: + elif data.edited_system_id: op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") op.system = system_id op = row.operator("bim.remove_system", text="", icon="X") @@ -429,7 +445,16 @@ class BIM_UL_systems(UIList): class BIM_UL_zones(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMZoneProperties, + item: Zone, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) row.label(text=item.name) diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index b7eae11989..e167ffca3a 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -30,12 +30,11 @@ import re from math import pi, cos, sin from mathutils import Matrix, Vector from bonsai.bim.module.system.data import ObjectSystemData, SystemDecorationData -from bonsai.bim.module.drawing.decoration import profile_consequential from enum import Enum from typing import TYPE_CHECKING, Optional, Any, Union if TYPE_CHECKING: - from bonsai.bim.module.system.prop import BIMSystemProperties, BIMZoneProperties + from bonsai.bim.module.system.prop import BIMSystemProperties, BIMZoneProperties, BIMZoneProperties class System(bonsai.core.tool.System): diff --git a/src/bonsai/scripts/bonsai_translations.py b/src/bonsai/scripts/bonsai_translations.py index a3053115fd..7e3e746934 100644 --- a/src/bonsai/scripts/bonsai_translations.py +++ b/src/bonsai/scripts/bonsai_translations.py @@ -75,7 +75,7 @@ class Message: translations: Optional[Dict[str, str]] = field(default_factory=dict) -def bonsai_strings_parse(addon_directory=None, po_directory=None): +def bonsai_strings_parse(addon_directory: Optional[Path] = None, po_directory: Optional[Path] = None): # NOTE: we decided to use our own parser due bug in Blender parser # as it tends to pick up strings from other addons and other Blender parts # and this bug probably would be to low of a priority for Blender to fix @@ -153,7 +153,7 @@ def bonsai_strings_parse(addon_directory=None, po_directory=None): def update_translations_from_po(po_directory: Path, translations_module: Path): translation_data: Dict[str, Message] = dict() - def process_po_entry(language, current_chunk: list[str]): + def process_po_entry(language: str, current_chunk: list[str]) -> None: sources = [] msgid = None msgstr = None @@ -171,6 +171,7 @@ def update_translations_from_po(po_directory: Path, translations_module: Path): elif line.startswith("#:"): sources.append(line.removeprefix("# ").strip()) + assert msgid is not None and msgstr is not None msg = translation_data.get(msgid) if msg is None: msg = Message(msgid, msgctxt, sources, {language: msgstr}) @@ -179,8 +180,8 @@ def update_translations_from_po(po_directory: Path, translations_module: Path): msg.sources.extend(sources) msg.translations[language] = msgstr - # load data from .po files - langs = set() + # load data from .po files to translation_data. + langs: set[str] = set() for po_file_path in po_directory.glob("**/*.po"): lang = po_file_path.stem langs.add(lang) diff --git a/src/bonsai/test/tool/test_system.py b/src/bonsai/test/tool/test_system.py index 7b0dc9e0d1..5a6ce1949f 100644 --- a/src/bonsai/test/tool/test_system.py +++ b/src/bonsai/test/tool/test_system.py @@ -112,22 +112,25 @@ class TestDeleteElementObjects(NewFile): class TestDisableEditingSystem(NewFile): def test_run(self): - bpy.context.scene.BIMSystemProperties.edited_system_id = 10 + props = tool.System.get_system_props() + props.edited_system_id = 10 subject.disable_editing_system() - assert bpy.context.scene.BIMSystemProperties.edited_system_id == 0 + assert props.edited_system_id == 0 class TestDisableSystemEditingUI(NewFile): def test_run(self): subject.enable_system_editing_ui() subject.disable_system_editing_ui() - assert bpy.context.scene.BIMSystemProperties.is_editing is False + props = tool.System.get_system_props() + assert props.is_editing is False class TestEnableSystemEditingUI(NewFile): def test_run(self): subject.enable_system_editing_ui() - assert bpy.context.scene.BIMSystemProperties.is_editing is True + props = tool.System.get_system_props() + assert props.is_editing is True class TestExportSystemAttributes(NewFile): @@ -171,7 +174,7 @@ class TestImportSystemAttributes(NewFile): system.Description = "Description" system.ObjectType = "ObjectType" subject().import_system_attributes(system) - props = bpy.context.scene.BIMSystemProperties + props = tool.System.get_system_props() assert props.system_attributes.get("GlobalId").string_value == "GlobalId" assert props.system_attributes.get("Name").string_value == "Name" assert props.system_attributes.get("Description").string_value == "Description" @@ -188,7 +191,7 @@ class TestImportSystemAttributes(NewFile): system.PredefinedType = "SHADING" system.LongName = "LongName" subject().import_system_attributes(system) - props = bpy.context.scene.BIMSystemProperties + props = tool.System.get_system_props() assert props.system_attributes.get("GlobalId").string_value == "GlobalId" assert props.system_attributes.get("Name").string_value == "Name" assert props.system_attributes.get("Description").string_value == "Description" @@ -207,7 +210,7 @@ class TestImportSystemAttributes(NewFile): system.PredefinedType = "ELECTRICAL" system.LongName = "LongName" subject().import_system_attributes(system) - props = bpy.context.scene.BIMSystemProperties + props = tool.System.get_system_props() assert props.system_attributes.get("GlobalId").string_value == "GlobalId" assert props.system_attributes.get("Name").string_value == "Name" assert props.system_attributes.get("Description").string_value == "Description" @@ -223,7 +226,7 @@ class TestImportSystems(NewFile): system = ifc.createIfcDistributionSystem() zone = ifc.createIfcZone() subject.import_systems() - props = bpy.context.scene.BIMSystemProperties + props = tool.System.get_system_props() assert len(props.systems) == 2 assert props.systems[0].ifc_definition_id == system.id() assert props.systems[0].name == "Unnamed" @@ -277,7 +280,8 @@ class TestSetActiveSystem(NewFile): tool.Ifc().set(ifc) system = ifcopenshell.api.run("system.add_system", ifc, ifc_class="IfcSystem") subject.set_active_edited_system(system) - assert bpy.context.scene.BIMSystemProperties.edited_system_id == system.id() + props = tool.System.get_system_props() + assert props.edited_system_id == system.id() class TestFlowElementAndControls(NewFile): diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py index 4956b33b56..2921524094 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_system.py @@ -29,11 +29,8 @@ def unassign_system( """Unassigns list of products from a system :param products: The list of IfcDistributionElements to unassign from the system. - :type products: list[ifcopenshell.entity_instance] :param system: The IfcSystem you want to unassign the element from. - :type system: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -52,9 +49,4 @@ def unassign_system( # Not anymore! ifcopenshell.api.system.unassign_system(model, products=[duct], system=system) """ - settings = { - "products": products, - "system": system, - } - - ifcopenshell.api.group.unassign_group(file, products=settings["products"], group=settings["system"]) + ifcopenshell.api.group.unassign_group(file, products=products, group=system) From 469b4f3502f87a5cd0ae07a1bf9a1e102fc0d290 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 10 Mar 2025 14:21:14 +0500 Subject: [PATCH 278/476] Systems UI - move buttons to the header to unclutter UI --- src/bonsai/bonsai/bim/module/system/prop.py | 7 ++++- src/bonsai/bonsai/bim/module/system/ui.py | 35 ++++++++++----------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index bd4ccc238a..0325a74997 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bonsai.bim.module.system.data import SystemData import bonsai.bim.module.system.decorator as decorator from bonsai.bim.prop import StrProperty, Attribute @@ -31,7 +32,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union def get_system_class(self: "BIMSystemProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: @@ -90,6 +91,10 @@ class BIMSystemProperties(PropertyGroup): system_class: str should_draw_decorations: bool + @property + def active_system_ui_item(self) -> Union[System, None]: + return tool.Blender.get_active_uilist_element(self.systems, self.active_system_index) + class BIMZoneProperties(PropertyGroup): attributes: CollectionProperty(name="Attributes", type=Attribute) diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 432cf1b76a..ff894c97e7 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -75,6 +75,7 @@ class BIM_PT_systems(Panel): op.system = system_id self.props = tool.System.get_system_props() + active_system_item = self.props.active_system_ui_item row = self.layout.row(align=True) row.prop(self.props, "should_draw_decorations") @@ -103,6 +104,20 @@ class BIM_PT_systems(Panel): row = self.layout.row(align=True) prop_with_search(row, self.props, "system_class", text="") row.operator("bim.add_system", text="", icon="ADD") + if active_system_item: + system_id = active_system_item.ifc_definition_id + op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") + op.system = system_id + row.operator("bim.assign_system", text="", icon="KEYFRAME_HLT").system = system_id + row.operator("bim.unassign_system", text="", icon="KEYFRAME").system = system_id + if self.props.edited_system_id == system_id: + row.operator("bim.edit_system", text="", icon="CHECKMARK") + row.operator("bim.disable_editing_system", text="", icon="CANCEL") + else: + op = row.operator("bim.enable_editing_system", text="", icon="GREASEPENCIL") + op.system = system_id + op = row.operator("bim.remove_system", text="", icon="X") + op.system = system_id else: row.operator("bim.load_systems", text="", icon="IMPORT") @@ -422,26 +437,10 @@ class BIM_UL_systems(UIList): ): if item: row = layout.row(align=True) - row.label(text=item.name, icon=SYSTEM_ICONS[item.ifc_class]) system_id = item.ifc_definition_id - row.operator("bim.assign_system", text="", icon="ADD").system = item.ifc_definition_id if data.edited_system_id == system_id: - op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") - op.system = system_id - row.operator("bim.edit_system", text="", icon="CHECKMARK") - row.operator("bim.disable_editing_system", text="", icon="CANCEL") - elif data.edited_system_id: - op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") - op.system = system_id - op = row.operator("bim.remove_system", text="", icon="X") - op.system = system_id - else: - op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") - op.system = system_id - op = row.operator("bim.enable_editing_system", text="", icon="GREASEPENCIL") - op.system = system_id - op = row.operator("bim.remove_system", text="", icon="X") - op.system = system_id + row.label(text="", icon="GREASEPENCIL") + row.label(text=item.name, icon=SYSTEM_ICONS[item.ifc_class]) class BIM_UL_zones(UIList): From f3f7939c8b9e93216ae4d8331aba40099a33decb Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 10 Mar 2025 14:34:38 +0500 Subject: [PATCH 279/476] util.get_element_systems to consider all kinds of ifcsystems --- src/ifcopenshell-python/ifcopenshell/util/system.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/system.py b/src/ifcopenshell-python/ifcopenshell/util/system.py index ac19ab3c35..3cfc842ebb 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/system.py +++ b/src/ifcopenshell-python/ifcopenshell/util/system.py @@ -66,13 +66,8 @@ def get_system_elements(system: ifcopenshell.entity_instance) -> list[ifcopenshe def get_element_systems(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: results = [] for rel in element.HasAssignments: - if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a() in ( - "IfcSystem", - "IfcDistributionSystem", - "IfcBuildingSystem", - "IfcZone", - ): - results.append(rel.RelatingGroup) + if rel.is_a("IfcRelAssignsToGroup") and (group := rel.RelatingGroup).is_a("IfcSystem"): + results.append(group) return results From 5b9ef23bdd33eecbd332d9e543386b74f4be5a5a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 10 Mar 2025 14:55:18 +0500 Subject: [PATCH 280/476] bim.assign_system/unassign_system - add info messages Also check whether element is assignable prior to running system.assign_system to avoid scary exceptions. --- .../bonsai/bim/module/system/operator.py | 50 ++++++++++++++++--- src/bonsai/bonsai/core/system.py | 12 +++-- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 597c4153c3..8a1535193f 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -18,6 +18,7 @@ import bpy import ifcopenshell.api +import ifcopenshell.util.system import bonsai.tool as tool import bonsai.core.system as core import bonsai.bim.helper @@ -98,27 +99,60 @@ class DisableEditingSystem(bpy.types.Operator): class AssignSystem(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.assign_system" bl_label = "Assign System" + bl_description = "Assign system to the selected objects.\n\nIf object is not assignable to this type of system, it will be skiped." bl_options = {"REGISTER", "UNDO"} system: bpy.props.IntProperty() + @classmethod + def poll(cls, context): + if not context.selected_objects: + cls.poll_message_set("No objects selected.") + return False + return True + def _execute(self, context): - for obj in context.selected_objects: - element = tool.Ifc.get_entity(obj) - if element: - core.assign_system(tool.Ifc, system=tool.Ifc.get().by_id(self.system), product=element) + elements = [e for o in context.selected_objects if (e := tool.Ifc.get_entity(o))] + if not elements: + self.report({"ERROR"}, "No IFC elements selected.") + return {"CANCELLED"} + system = tool.Ifc.get().by_id(self.system) + assignable_elements = [e for e in elements if ifcopenshell.util.system.is_assignable(e, system)] + if not assignable_elements: + supported_elements_str = ", ".join(ifcopenshell.util.system.group_types[system.is_a()]) + self.report( + {"ERROR"}, + f"No elements assignable to {system.is_a()} is selected.\n" + f"Assignable elements types are: {supported_elements_str}.", + ) + return {"CANCELLED"} + core.assign_system(tool.Ifc, system=system, products=assignable_elements) + self.report({"INFO"}, f"System assigned to {len(assignable_elements)} elements.") + return {"FINISHED"} class UnassignSystem(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.unassign_system" bl_label = "Unassign System" + bl_description = "Unassign system from the selected objects." bl_options = {"REGISTER", "UNDO"} system: bpy.props.IntProperty() + @classmethod + def poll(cls, context): + if not context.selected_objects: + cls.poll_message_set("No objects selected.") + return False + return True + def _execute(self, context): - for obj in context.selected_objects: - element = tool.Ifc.get_entity(obj) - if element: - core.unassign_system(tool.Ifc, system=tool.Ifc.get().by_id(self.system), product=element) + elements = [e for o in context.selected_objects if (e := tool.Ifc.get_entity(o))] + if not elements: + self.report({"ERROR"}, "No IFC elements selected.") + return {"CANCELLED"} + system = tool.Ifc.get().by_id(self.system) + core.unassign_system(tool.Ifc, system=system, products=elements) + self.report({"INFO"}, f"System unassigned from {len(elements)} elements.") + return {"FINISHED"} class SelectSystemProducts(bpy.types.Operator): diff --git a/src/bonsai/bonsai/core/system.py b/src/bonsai/bonsai/core/system.py index 2acd1d21d4..8b2edb2e41 100644 --- a/src/bonsai/bonsai/core/system.py +++ b/src/bonsai/bonsai/core/system.py @@ -62,12 +62,16 @@ def disable_editing_system(system: tool.System) -> None: system.disable_editing_system() -def assign_system(ifc: tool.Ifc, system: ifcopenshell.entity_instance, product: ifcopenshell.entity_instance) -> None: - ifc.run("system.assign_system", products=[product], system=system) +def assign_system( + ifc: tool.Ifc, system: ifcopenshell.entity_instance, products: list[ifcopenshell.entity_instance] +) -> None: + ifc.run("system.assign_system", products=products, system=system) -def unassign_system(ifc: tool.Ifc, system: ifcopenshell.entity_instance, product: ifcopenshell.entity_instance) -> None: - ifc.run("system.unassign_system", products=[product], system=system) +def unassign_system( + ifc: tool.Ifc, system: ifcopenshell.entity_instance, products: list[ifcopenshell.entity_instance] +) -> None: + ifc.run("system.unassign_system", products=products, system=system) def select_system_products(system_tool: tool.System, system: ifcopenshell.entity_instance) -> None: From 8ed9c3240469ba1db965f3ecc06051038e5c54d5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 10 Mar 2025 17:10:58 +0500 Subject: [PATCH 281/476] Rename systems and zones from UIList example - https://imgur.com/a/8yTzmy3 --- .../bonsai/bim/module/system/operator.py | 2 +- src/bonsai/bonsai/bim/module/system/prop.py | 19 ++++++++++++++++++- src/bonsai/bonsai/bim/module/system/ui.py | 4 ++-- src/bonsai/bonsai/tool/system.py | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index 8a1535193f..df098fa04b 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -374,7 +374,7 @@ class LoadZones(bpy.types.Operator): for zone in tool.Ifc.get().by_type("IfcZone"): new = props.zones.add() new.ifc_definition_id = zone.id() - new.name = zone.Name or "Unnamed" + new["name"] = zone.Name or "Unnamed" props.is_loaded = True props.is_editing = 0 return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/system/prop.py b/src/bonsai/bonsai/bim/module/system/prop.py index 0325a74997..a984b62bb2 100644 --- a/src/bonsai/bonsai/bim/module/system/prop.py +++ b/src/bonsai/bonsai/bim/module/system/prop.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.bim.handler import bonsai.tool as tool from bonsai.bim.module.system.data import SystemData import bonsai.bim.module.system.decorator as decorator @@ -41,8 +42,16 @@ def get_system_class(self: "BIMSystemProperties", context: bpy.types.Context) -> return SystemData.data["system_class"] +def update_system_name(self: "System", context: bpy.types.Context) -> None: + system = tool.Ifc.get().by_id(self.ifc_definition_id) + if system.Name == self.name: + return + system.Name = self.name + bonsai.bim.handler.refresh_ui_data() + + class System(PropertyGroup): - name: StringProperty(name="Name") + name: StringProperty(name="Name", update=update_system_name) ifc_class: StringProperty(name="IFC Class") ifc_definition_id: IntProperty(name="IFC Definition ID") @@ -51,6 +60,14 @@ class System(PropertyGroup): ifc_definition_id: int +def update_zone_name(self: "Zone", context: bpy.types.Context) -> None: + zone = tool.Ifc.get().by_id(self.ifc_definition_id) + if zone.Name == self.name: + return + zone.Name = self.name + bonsai.bim.handler.refresh_ui_data() + + class Zone(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index ff894c97e7..0cae9e9a54 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -440,7 +440,7 @@ class BIM_UL_systems(UIList): system_id = item.ifc_definition_id if data.edited_system_id == system_id: row.label(text="", icon="GREASEPENCIL") - row.label(text=item.name, icon=SYSTEM_ICONS[item.ifc_class]) + row.prop(item, "name", text="", icon=SYSTEM_ICONS[item.ifc_class], emboss=False) class BIM_UL_zones(UIList): @@ -456,4 +456,4 @@ class BIM_UL_zones(UIList): ): if item: row = layout.row(align=True) - row.label(text=item.name) + row.prop(item, "name", text="", emboss=False) diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index e167ffca3a..2a891b8136 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -164,7 +164,7 @@ class System(bonsai.core.tool.System): continue new = props.systems.add() new.ifc_definition_id = system.id() - new.name = system.Name or "Unnamed" + new["name"] = system.Name or "Unnamed" new.ifc_class = system.is_a() @classmethod From e1ba98c5f8684d0cdc0111cb7b0d9c73c3ec593c Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Mon, 10 Mar 2025 21:07:18 +0000 Subject: [PATCH 282/476] fix loading ifc2x3 project libraries --- src/bonsai/bonsai/tool/project.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index beab7a4edf..9e5e646da7 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -308,6 +308,8 @@ class Project(bonsai.core.tool.Project): @classmethod def get_project_library_rels(cls, ifc_file: ifcopenshell.file) -> set[ifcopenshell.entity_instance]: + if tool.Ifc.get_schema() == "IFC2X3": + return set() return set(rel for lib in ifc_file.by_type("IfcProjectLibrary") for rel in lib.Declares) @classmethod @@ -361,6 +363,8 @@ class Project(bonsai.core.tool.Project): """ hierarchy: HiearchyDict = defaultdict(dict) + if tool.Ifc.get_schema() == "IFC2X3": + return hierarchy for project_library in ifc_file.by_type("IfcProjectLibrary"): parent_library = cls.get_parent_library(project_library) hierarchy[parent_library][project_library] = hierarchy[project_library] From 570513b60be61d8070a188718b9215d9b4276ec1 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 11 Mar 2025 09:13:48 +1100 Subject: [PATCH 283/476] Revert "util.get_element_systems to consider all kinds of ifcsystems" This reverts commit f3f7939c8b9e93216ae4d8331aba40099a33decb. --- src/ifcopenshell-python/ifcopenshell/util/system.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/system.py b/src/ifcopenshell-python/ifcopenshell/util/system.py index 3cfc842ebb..ac19ab3c35 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/system.py +++ b/src/ifcopenshell-python/ifcopenshell/util/system.py @@ -66,8 +66,13 @@ def get_system_elements(system: ifcopenshell.entity_instance) -> list[ifcopenshe def get_element_systems(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: results = [] for rel in element.HasAssignments: - if rel.is_a("IfcRelAssignsToGroup") and (group := rel.RelatingGroup).is_a("IfcSystem"): - results.append(group) + if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a() in ( + "IfcSystem", + "IfcDistributionSystem", + "IfcBuildingSystem", + "IfcZone", + ): + results.append(rel.RelatingGroup) return results From bf541328051d5135dd37a559d3a0b9851c426efc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 11 Mar 2025 18:37:53 +1100 Subject: [PATCH 284/476] Fix #6307. Warning: critical bug where saving a file broke undo state. Previously, changing props.ifc_file had an update hook to reload information about the IFC model. But this isn't always correct because there are two situations: 1. The ifc_file path changed because you saved the file for the first time or saved as, and this is merely recording a new saved path of the existing file object. 2. The ifc_file path changed because the user manually changed it or selected a new file. This could reference an entirely new file object. This is dangerous because we can't trust anything anymore, including our undo history. So the new default situation is that there is no magic hook. If you change props.ifc_file, that's all it changes ... just a path stored in Blender with not much significance. If the user runs select_ifc_file to explicitly relink the file, it now explicitly purges in that situation and clears the undo history. Basically now behaviour is explicit, not using magic hooks. --- src/bonsai/bonsai/bim/handler.py | 4 +--- src/bonsai/bonsai/bim/ifc.py | 14 +++++++++----- src/bonsai/bonsai/bim/module/project/operator.py | 5 ++--- src/bonsai/bonsai/bim/operator.py | 3 +++ src/bonsai/bonsai/bim/prop.py | 8 +------- src/bonsai/bonsai/tool/blender.py | 13 +++++++++++++ src/bonsai/bonsai/tool/ifc.py | 12 ++++++++++++ 7 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index e0679a18c2..2f46d51e66 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -24,17 +24,15 @@ import ifcopenshell.util.unit import ifcopenshell.api.owner.settings import bonsai.bim import bonsai.tool as tool -import bonsai.core.owner as core_owner from bpy.app.handlers import persistent from bonsai.bim.ifc import IfcStore -from bonsai.bim.module.owner.prop import get_user_person, get_user_organisation from bonsai.bim.module.model.data import AuthoringData from bonsai.bim.module.aggregate.decorator import AggregateDecorator from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDecorator from bonsai.bim.module.nest.decorator import NestDecorator from mathutils import Vector -from math import cos, degrees +from math import cos from typing import Union, Callable diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index 281d39df8e..1cac1d5cb1 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -33,7 +33,7 @@ import bonsai.bim.handler import bonsai.tool as tool from pathlib import Path from bonsai.tool.brick import BrickStore -from typing import Set, Union, Optional, TypedDict, Callable, NotRequired, cast +from typing import Set, Union, Optional, TypedDict, Callable, NotRequired IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object] @@ -106,10 +106,7 @@ class IfcStore: def get_file(): if IfcStore.file is None: props = tool.Blender.get_bim_props() - IfcStore.path = props.ifc_file - # Interpret relative paths as relative to .blend file. - if IfcStore.path and not os.path.isabs(IfcStore.path): - IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path)) + IfcStore.set_path(props.ifc_file) if IfcStore.path: try: IfcStore.load_file(IfcStore.path) @@ -117,6 +114,13 @@ class IfcStore: print(f"Failed to load file {IfcStore.path}. Error details: {e}") return IfcStore.file + @staticmethod + def set_path(value): + IfcStore.path = value + # Interpret relative paths as relative to .blend file. + if IfcStore.path and not os.path.isabs(IfcStore.path): + IfcStore.path = os.path.abspath(os.path.join(bpy.path.abspath("//"), IfcStore.path)) + @staticmethod def get_cache(): if IfcStore.cache is None and IfcStore.path: diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 36abbe8ffd..8a26654cbe 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -985,8 +985,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): if not self.is_advanced and not self.should_start_fresh_session: bpy.ops.bim.convert_to_blender() - bim_props = tool.Blender.get_bim_props() - bim_props.ifc_file = filepath + tool.Ifc.set_path(filepath) if not tool.Ifc.get(): self.report( {"ERROR"}, @@ -1650,7 +1649,7 @@ class ExportIFC(bpy.types.Operator): output_file = os.path.relpath(output_file, bpy.path.abspath("//")) bim_props = tool.Blender.get_bim_props() if bim_props.ifc_file != output_file and extension not in ("ifczip", "ifcjson"): - bim_props.ifc_file = output_file + tool.Ifc.set_path(output_file) save_blend_file = bool(bpy.data.is_saved and bpy.data.is_dirty and bpy.data.filepath) if save_blend_file: bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath) diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 1690b9497f..3e4f651daa 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -30,6 +30,7 @@ import webbrowser import ifcopenshell import bonsai.bim import bonsai.tool as tool +import bonsai.bim.handler from bonsai.bim import import_ifc from bonsai.bim.prop import StrProperty from bonsai.bim.ui import IFCFileSelector @@ -222,6 +223,8 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector): if self.is_existing_ifc_file(): props = tool.Blender.get_bim_props() props.ifc_file = self.get_filepath() + bonsai.bim.handler.loadIfcStore(bpy.context.scene) + tool.Blender.clear_undo_history() return {"FINISHED"} def invoke(self, context, event): diff --git a/src/bonsai/bonsai/bim/prop.py b/src/bonsai/bonsai/bim/prop.py index 9a69807c23..af378bb8f4 100644 --- a/src/bonsai/bonsai/bim/prop.py +++ b/src/bonsai/bonsai/bim/prop.py @@ -137,12 +137,6 @@ def update_cache_dir(self: "BIMProperties", context: bpy.types.Context) -> None: bonsai.bim.schema.ifc.cache_dir = bim_props.cache_dir -def update_ifc_file(self: "BIMProperties", context: bpy.types.Context) -> None: - bim_props = tool.Blender.get_bim_props() - if bim_props.ifc_file: - bonsai.bim.handler.loadIfcStore(context.scene) - - def update_section_color(self: "BIMProperties", context: bpy.types.Context) -> None: section_node_group = bpy.data.node_groups.get("Section Override") if section_node_group is None: @@ -506,7 +500,7 @@ class BIMProperties(PropertyGroup): ) has_blend_warning: BoolProperty(name="Has Blend Warning", default=False) pset_dir: StringProperty(default=os.path.join("psets") + os.path.sep, name="Default Psets Directory") - ifc_file: StringProperty(name="IFC File", update=update_ifc_file) + ifc_file: StringProperty(name="IFC File") last_transaction: StringProperty(name="Last Transaction") should_section_selected_objects: BoolProperty(name="Section Selected Objects", default=False) section_plane_colour: FloatVectorProperty( diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index da85bb734f..8ac081eb93 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1616,3 +1616,16 @@ class Blender(bonsai.core.tool.Blender): if 0 <= index < len(collection): return collection[index] return None + + @classmethod + def clear_undo_history(cls) -> None: + """Clears the Blender history, Bonsai history, and IfcOpenShell history""" + old_undo_steps = bpy.context.preferences.edit.undo_steps + bpy.context.preferences.edit.undo_steps = 2 + for i in range(3): + bpy.ops.ed.undo_push(message="Undo history cleared") + bpy.context.preferences.edit.undo_steps = old_undo_steps + tool.Ifc.clear_history() + old_history_size = tool.Ifc.get().history_size + tool.Ifc.get().set_history_size(0) + tool.Ifc.get().set_history_size(old_history_size) diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index 83c05101ef..0474fa52f5 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -56,6 +56,12 @@ class Ifc(bonsai.core.tool.Ifc): def get(cls) -> ifcopenshell.file: return IfcStore.get_file() + @classmethod + def set_path(cls, value: str) -> None: + bim_props = tool.Blender.get_bim_props() + bim_props.ifc_file = value + IfcStore.set_path(value) + @classmethod def get_path(cls) -> str: """Get absolute filepath to the IFC file, return empty string if file is not saved.""" @@ -66,6 +72,12 @@ class Ifc(bonsai.core.tool.Ifc): if IfcStore.get_file(): return IfcStore.get_file().schema + @classmethod + def clear_history(cls) -> None: + IfcStore.last_transaction = "" + IfcStore.history = [] + IfcStore.future = [] + @classmethod def is_edited(cls, obj: bpy.types.Object, *, ignore_scale: bool = False) -> bool: return (not ignore_scale and tool.Geometry.is_scaled(obj)) or obj in IfcStore.edited_objs From 1e97d2c5fac101edff912ab04e374f9f9f08c06c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 11 Mar 2025 18:38:45 +1100 Subject: [PATCH 285/476] See #6307. Prevent user from manually changing the ifc file path. You can still hover over and copy paste if you want. --- src/bonsai/bonsai/bim/module/project/ui.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 237fe75f52..f066b0c51d 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -324,7 +324,9 @@ class BIM_PT_project(Panel): row.label(text=ProjectData.data["last_saved"]) row = self.layout.row(align=True) - row.prop(props, "ifc_file", text="") + col = row.column() + col.enabled = False + col.prop(props, "ifc_file", text="") row.operator("bim.select_ifc_file", icon="FILE_FOLDER", text="") From f08dba648317870b4e11fb18a14c473e3574aa7e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 11 Mar 2025 18:40:00 +1100 Subject: [PATCH 286/476] Improve debug logs when running BDD tests I've been spoilt by tools like Behat and so this make it much nicer because it tells you exactly the feature/scenario/step where it failed. --- src/bonsai/test/bim/conftest.py | 19 +++++++++++++++++++ src/bonsai/test/bim/test_feature.py | 21 ++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 src/bonsai/test/bim/conftest.py diff --git a/src/bonsai/test/bim/conftest.py b/src/bonsai/test/bim/conftest.py new file mode 100644 index 0000000000..2d69fe415a --- /dev/null +++ b/src/bonsai/test/bim/conftest.py @@ -0,0 +1,19 @@ +import pytest + +# pytest by default doesn't print steps and where it failed. Let's fix that. + + +@pytest.hookimpl +def pytest_bdd_before_scenario(request, feature, scenario): + print(f"\033[94m# {feature.name}\033[0m") + print(f"\033[94m## {scenario.name}\033[0m") + + +@pytest.hookimpl(tryfirst=True) +def pytest_bdd_after_step(request, feature, scenario, step, step_func, step_func_args): + print(f"\033[92m>>> {step.name}\033[0m") + + +@pytest.hookimpl(tryfirst=True) +def pytest_bdd_step_error(request, feature, scenario, step, step_func, step_func_args): + print(f"\033[1;91m>>> {step.name} <-- FAILED\033[0m") diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 94ab5db19f..e6932cb1b5 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -18,6 +18,7 @@ import os import bpy +import pytest import traceback import webbrowser import numpy as np @@ -542,6 +543,7 @@ def i_press_operator(operator): @given(parsers.parse('I click "{button}"')) @when(parsers.parse('I click "{button}"')) +@then(parsers.parse('I click "{button}"')) def i_click_button(button): panel_spy.refresh_spy() for spied_operator in panel_spy.spied_operators: @@ -559,6 +561,8 @@ def i_click_button(button): # Clicked confirm on an operator's draw dialog return i_press_operator(panel_spy.panel.bl_idname) debug = "\n".join([f"{i} {v}" for i, v in enumerate(panel_spy.spied_operators)]) + if not debug: + debug = f"No buttons were found, here is the text we see: {panel_spy.spied_labels}" assert False, f"Could not find {button}:\n{debug}" @@ -603,6 +607,7 @@ def i_refresh_the_selected_objects(): bonsai.bim.handler.active_object_callback() +@given("I deselect all objects") @when("I deselect all objects") def i_deselect_all_objects(): bpy.context.view_layer.objects.active = None @@ -668,7 +673,9 @@ def then_the_object_name_is_placed_in_the_collection_collection(name: str, colle def additionally_the_object_name_is_selected(name): obj = bpy.context.scene.objects.get(name) if not obj: - assert False, f'The object "{name}" could not be selected' + total = len(bpy.context.scene.objects) + debug = "\n".join([o.name for o in bpy.context.scene.objects]) + assert False, f'The object "{name}" could not be selected. Available objects ({total} total):\n{debug}' bpy.context.view_layer.objects.active = obj obj.select_set(True) @@ -729,6 +736,8 @@ def nothing_happens(): pass +@given(parsers.parse('the object "{name}" exists')) +@when(parsers.parse('the object "{name}" exists')) @then(parsers.parse('the object "{name}" exists')) def the_object_name_exists(name: str) -> bpy.types.Object: # Some objects from linked collections may share the same name. This disambiguates them. @@ -1335,10 +1344,14 @@ def construction_type(relating_type_name): @when("I toggle edit mode") @then("I toggle edit mode") def i_toggle_edit_mode(): + props = tool.Geometry.get_geometry_props() + print(f"Toggling from {bpy.context.mode} / {props.mode} ...") + print("Selected items:", bpy.context.active_object, bpy.context.selected_objects) if bpy.context.mode == "OBJECT": bpy.ops.bim.override_mode_set_edit() else: bpy.ops.bim.override_mode_set_object() + print(f"... mode is now {bpy.context.mode} / {props.mode}") @when("I move the cursor to the bottom left corner") @@ -1357,8 +1370,10 @@ def prepare_undo(): @when(parsers.parse("I undo")) @then(parsers.parse("I undo")) def hit_undo(): - bpy.ops.ed.undo_push(message="UNDO STEP") - bpy.ops.ed.undo() + # bpy.ops.ed.undo_push(message="UNDO STEP") + override = tool.Blender.get_viewport_context() + with bpy.context.temp_override(**override): + bpy.ops.ed.undo() @then(parsers.parse('the object "{obj_name1}" has a connection with "{obj_name2}"')) From d9e48467cdc20db450744a7c382fd889218cb634 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 11 Mar 2025 21:13:04 +1100 Subject: [PATCH 287/476] Fix bug where removing a boolean didn't unmark it as a manual boolean. --- src/bonsai/bonsai/tool/geometry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 9524e91ea3..840466f62d 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1394,6 +1394,9 @@ class Geometry(bonsai.core.tool.Geometry): also_consider = list(consider_inverses) ifcopenshell.util.element.remove_deep2(ifc_file, representation_item, also_consider=also_consider) + props = tool.Geometry.get_geometry_props() + rep_element = tool.Ifc.get_entity(props.representation_obj) + tool.Model.unmark_manual_booleans(rep_element, [b.id() for b in boolean_results_to_remove]) for boolean_result in boolean_results_to_remove: cls.remove_representation_item(boolean_result) From 9cfcaeb5875912fe910ffd30b42e6205b67bcfe3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 11 Mar 2025 21:14:11 +1100 Subject: [PATCH 288/476] Fix bug and add boolean tests where if you added a boolean that fully clipped something you couldn't edit it due to lack of item ids. --- .../bonsai/bim/module/geometry/operator.py | 9 ++++ src/bonsai/pytest.ini | 1 + src/bonsai/test/bim/feature/boolean.feature | 49 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 src/bonsai/test/bim/feature/boolean.feature diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index 18372e40fd..f1c6d8630f 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -2888,6 +2888,15 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): else: assert False, "Unexpected mesh type." + if not item_ids: + # It is possible that the user has created a shape that + # IfcOpenShell cannot render (i.e. boolean clipped everything), but + # we still want to edit items. I'm not sure the best way to handle + # this, but for now perhaps we can detect when there are no + # item_ids at all. + representation = tool.Ifc.get_entity(data) + item_ids = [i["item"].id() for i in ifcopenshell.util.representation.resolve_items(representation)] + queue = list(set(item_ids)) processed_ids = set() boolean_ids = set() diff --git a/src/bonsai/pytest.ini b/src/bonsai/pytest.ini index eee9c46ded..f3a641a5c4 100644 --- a/src/bonsai/pytest.ini +++ b/src/bonsai/pytest.ini @@ -2,6 +2,7 @@ markers = aggregate attribute + boolean brick classification context diff --git a/src/bonsai/test/bim/feature/boolean.feature b/src/bonsai/test/bim/feature/boolean.feature new file mode 100644 index 0000000000..9b06580172 --- /dev/null +++ b/src/bonsai/test/bim/feature/boolean.feature @@ -0,0 +1,49 @@ +@boolean +Feature: Boolean + Manage boolean hierarchies and boolean results + +Scenario: Ensure added booleans are marked as manual + Given an empty IFC project + And I open the "Add" menu + And I trigger "Add Element" + And I set the "Definition" property to "IfcElement" + And I set the "Class" property to "IfcFurniture" + And I set the "Representation" property to "Custom Extruded Solid" + And I click "OK" + And the object "IfcFurniture/Unnamed" exists + And I toggle edit mode + And the object "Item/IfcExtrudedAreaSolid/77" exists + And I open the "Add Item" menu + When I click "Half Space Solid" + And the object "Item/IfcHalfSpaceSolid/90" exists + And I deselect all objects + And I toggle edit mode + And I select the object "IfcFurniture/Unnamed" + And I look at the "Property Sets" panel + Then I see "BBIM_Boolean" + And I see "[91]" + +Scenario: Ensure removed booleans are unmarked as manual + Given an empty IFC project + And I open the "Add" menu + And I trigger "Add Element" + And I set the "Definition" property to "IfcElement" + And I set the "Class" property to "IfcFurniture" + And I set the "Representation" property to "Custom Extruded Solid" + And I click "OK" + And the object "IfcFurniture/Unnamed" exists + And I toggle edit mode + And the object "Item/IfcExtrudedAreaSolid/77" exists + And I open the "Add Item" menu + And I click "Half Space Solid" + And I deselect all objects + And I toggle edit mode + And I select the object "IfcFurniture/Unnamed" + And I toggle edit mode + And I select the object "Item/IfcHalfSpaceSolid/90" + When I delete the selected objects + And I toggle edit mode + And I select the object "IfcFurniture/Unnamed" + And I look at the "Property Sets" panel + Then I don't see "BBIM_Boolean" + And I don't see "[91]" From c1bc36ab869e6208485bdb2fdc176d9ba297114d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 11 Mar 2025 21:59:53 +1100 Subject: [PATCH 289/476] Bump IOS --- src/bonsai/Makefile | 2 +- src/ifcopenshell-python/Makefile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile index 90867ff0ec..a5000795c2 100644 --- a/src/bonsai/Makefile +++ b/src/bonsai/Makefile @@ -85,7 +85,7 @@ BLENDER_PLATFORM:=windows-x64 endif # Current build commit hash. -OLD:=c49ca69 +OLD:=cfb7d02 .PHONY: bump bump: ifndef NEW diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index 6e99b5d6d9..a6084ea62f 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -54,8 +54,8 @@ ifeq ($(PLATFORM), win64) PLATFORMTAG:=win_amd64 endif -IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.8.1-c49ca69-$(PLATFORM).zip -IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.8.1-c49ca69-$(PLATFORM).zip +IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.8.1-cfb7d02-$(PLATFORM).zip +IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.8.1-cfb7d02-$(PLATFORM).zip .PHONY: test test: From ed0b1c5009a1acca3be7de40262e7c31490531d7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 11 Mar 2025 13:07:54 +0500 Subject: [PATCH 290/476] typing --- src/bonsai/bonsai/bim/module/brick/data.py | 2 +- .../bonsai/bim/module/brick/operator.py | 23 +-- src/bonsai/bonsai/bim/module/brick/prop.py | 39 ++++- src/bonsai/bonsai/bim/module/brick/ui.py | 10 +- src/bonsai/bonsai/bim/module/root/operator.py | 6 +- src/bonsai/bonsai/core/brick.py | 56 ++++-- src/bonsai/bonsai/tool/brick.py | 163 ++++++++++-------- src/bonsai/test/tool/test_brick.py | 84 +++++---- .../ifcopenshell/api/root/reassign_class.py | 20 +-- .../ifcopenshell/api/type/unassign_type.py | 16 +- .../ifcopenshell/util/brick.py | 7 +- 11 files changed, 253 insertions(+), 173 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/brick/data.py b/src/bonsai/bonsai/bim/module/brick/data.py index 737896ad3c..b7ac6f61e1 100644 --- a/src/bonsai/bonsai/bim/module/brick/data.py +++ b/src/bonsai/bonsai/bim/module/brick/data.py @@ -52,7 +52,7 @@ class BrickschemaData: def active_relations(cls): if BrickStore.graph is None: return [] - props = bpy.context.scene.BIMBrickProperties + props = tool.Brick.get_brick_props() try: brick = props.bricks[props.active_brick_index] except: diff --git a/src/bonsai/bonsai/bim/module/brick/operator.py b/src/bonsai/bonsai/bim/module/brick/operator.py index e76da467fa..522241a88e 100644 --- a/src/bonsai/bonsai/bim/module/brick/operator.py +++ b/src/bonsai/bonsai/bim/module/brick/operator.py @@ -36,7 +36,7 @@ class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower(): - root = context.scene.BIMBrickProperties.brick_list_root + root = tool.Brick.get_brick_props().brick_list_root core.load_brick_project(tool.Brick, filepath=self.filepath, brick_root=root) else: self.report({"ERROR"}, f"Failed to load {self.filepath}") @@ -111,10 +111,10 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Assign the selected Ifc entity to the selected Brick entity" def _execute(self, context): - if not context.active_object: + if not (obj := context.active_object) or not (element := tool.Ifc.get_entity(obj)): self.report({"ERROR"}, f"No Ifc selected") return - props = context.scene.BIMBrickProperties + props = tool.Brick.get_brick_props() try: props.bricks[props.active_brick_index] except: @@ -123,7 +123,7 @@ class AssignBrickReference(bpy.types.Operator, tool.Ifc.Operator): core.assign_brick_reference( tool.Ifc, tool.Brick, - element=tool.Ifc.get_entity(context.active_object), + element=element, library=tool.Ifc.get().by_id(int(props.libraries)), brick_uri=props.bricks[props.active_brick_index].uri, ) @@ -136,7 +136,7 @@ class AddBrick(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Create the Brick entity" def _execute(self, context): - props = context.scene.BIMBrickProperties + props = tool.Brick.get_brick_props() core.add_brick( tool.Ifc, tool.Brick, @@ -155,7 +155,7 @@ class AddBrickRelation(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Create the Brick relationship" def _execute(self, context): - props = context.scene.BIMBrickProperties + props = tool.Brick.get_brick_props() brick = props.bricks[props.active_brick_index] if props.new_brick_relation_type == "http://www.w3.org/2000/01/rdf-schema#label": object = props.new_brick_relation_object @@ -173,7 +173,7 @@ class ConvertIfcToBrick(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Convert Ifc entities and relations to Brick entities and relations" def _execute(self, context): - props = context.scene.BIMBrickProperties + props = tool.Brick.get_brick_props() library = None if props.libraries: library = tool.Ifc.get().by_id(int(props.libraries)) @@ -201,7 +201,8 @@ class NewBrickFile(bpy.types.Operator): return {"FINISHED"} def _execute(self, context): - root = context.scene.BIMBrickProperties.brick_list_root + props = tool.Brick.get_brick_props() + root = props.brick_list_root core.new_brick_file(tool.Brick, brick_root=root) def rollback(self, data): @@ -230,7 +231,7 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Delete this entity" def _execute(self, context): - props = context.scene.BIMBrickProperties + props = tool.Brick.get_brick_props() core.remove_brick( tool.Ifc, tool.Brick, @@ -273,7 +274,7 @@ class AddBrickNamespace(bpy.types.Operator, tool.Ifc.Operator): bl_description = "Bind a new namespace to the Brick project" def _execute(self, context): - props = context.scene.BIMBrickProperties + props = tool.Brick.get_brick_props() alias = props.new_brick_namespace_alias uri = props.new_brick_namespace_uri core.add_brick_namespace(tool.Brick, alias=alias, uri=uri) @@ -288,6 +289,6 @@ class RemoveBrickRelation(bpy.types.Operator, tool.Ifc.Operator): object: bpy.props.StringProperty(name="Object") def _execute(self, context): - props = context.scene.BIMBrickProperties + props = tool.Brick.get_brick_props() brick = props.bricks[props.active_brick_index] core.remove_brick_relation(tool.Brick, brick_uri=brick.uri, predicate=self.predicate, object=self.object) diff --git a/src/bonsai/bonsai/bim/module/brick/prop.py b/src/bonsai/bonsai/bim/module/brick/prop.py index 1a226fd1a0..4b738a0c77 100644 --- a/src/bonsai/bonsai/bim/module/brick/prop.py +++ b/src/bonsai/bonsai/bim/module/brick/prop.py @@ -33,6 +33,7 @@ from bpy.props import ( import bonsai.core.brick as core import bonsai.tool.brick as tool from bonsai.tool.brick import BrickStore +from typing import TYPE_CHECKING def update_active_brick_index(self, context): @@ -74,13 +75,13 @@ def get_brick_relations(self, context): return BRICK_RELATIONS_ENUM_ITEMS -def update_view(self, context): - root = context.scene.BIMBrickProperties.brick_list_root +def update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None: + root = self.brick_list_root core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=False) -def split_screen_update_view(self, context): - root = context.scene.BIMBrickProperties.split_screen_brick_list_root +def split_screen_update_view(self: "BIMBrickProperties", context: bpy.types.Context) -> None: + root = self.split_screen_brick_list_root core.set_brick_list_root(tool.Brick, brick_root=root, split_screen=True) @@ -90,6 +91,11 @@ class Brick(PropertyGroup): uri: StringProperty(name="URI") total_items: IntProperty(name="Total Items") + if TYPE_CHECKING: + label: str + uri: str + total_items: int + class BIMBrickProperties(PropertyGroup): active_brick_class: StringProperty(name="Active Brick Class") @@ -124,3 +130,28 @@ class BIMBrickProperties(PropertyGroup): split_screen_brick_list_root: EnumProperty( name="Split Screen Brick List Root", items=get_brick_roots, update=split_screen_update_view ) + + if TYPE_CHECKING: + active_brick_class: str + brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty] + bricks: bpy.types.bpy_prop_collection_idprop[Brick] + active_brick_index: int + libraries: str + set_list_root_toggled: bool + brick_list_root: str + namespace: str + new_brick_namespace_alias: str + new_brick_namespace_uri: str + new_brick_label: str + brick_entity_create_type: str + brick_entity_class: str + brick_create_relations_toggled: bool + brick_edit_relations_toggled: bool + new_brick_relation_type: str + new_brick_relation_object: str + split_screen_toggled: bool + split_screen_bricks: bpy.types.bpy_prop_collection_idprop[Brick] + split_screen_active_brick_index: int + split_screen_active_brick_class: str + split_screen_brick_breadcrumbs: bpy.types.bpy_prop_collection_idprop[StrProperty] + split_screen_brick_list_root: str diff --git a/src/bonsai/bonsai/bim/module/brick/ui.py b/src/bonsai/bonsai/bim/module/brick/ui.py index 141dba5559..e7d0a1be69 100644 --- a/src/bonsai/bonsai/bim/module/brick/ui.py +++ b/src/bonsai/bonsai/bim/module/brick/ui.py @@ -45,7 +45,7 @@ class BIM_PT_brickschema_project_info(Panel): bl_parent_id = "BIM_PT_brickschema" def draw(self, context): - self.props = context.scene.BIMBrickProperties + self.props = tool.Brick.get_brick_props() if not BrickschemaData.data["is_loaded"]: row = self.layout.row(align=True) @@ -87,7 +87,7 @@ class BIM_PT_brickschema_namespaces(Panel): return BrickStore.graph != None def draw(self, context): - self.props = context.scene.BIMBrickProperties + self.props = tool.Brick.get_brick_props() row = self.layout.row(align=True) row.label(text="Active Namespace:") @@ -122,7 +122,7 @@ class BIM_PT_brickschema_create_entity(Panel): return BrickStore.graph != None def draw(self, context): - self.props = context.scene.BIMBrickProperties + self.props = tool.Brick.get_brick_props() # TO DO: hide this if selected entity already has a reference, or something similar row = self.layout.row(align=True) @@ -156,7 +156,7 @@ class BIM_PT_brickschema_viewport(Panel): return BrickStore.graph != None def draw(self, context): - self.props = context.scene.BIMBrickProperties + self.props = tool.Brick.get_brick_props() row = self.layout.row(align=True) row.column().alignment = "RIGHT" @@ -292,7 +292,7 @@ class BIM_PT_ifc_brickschema_references(Panel): def draw(self, context): if not BrickschemaReferencesData.is_loaded: BrickschemaReferencesData.load() - self.props = context.scene.BIMBrickProperties + self.props = tool.Brick.get_brick_props() if not BrickschemaReferencesData.data["is_loaded"]: row = self.layout.row() diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index d141e7075e..4accda58d9 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -21,6 +21,7 @@ import bmesh import ifcopenshell import ifcopenshell.api import ifcopenshell.api.geometry +import ifcopenshell.api.root import ifcopenshell.util.schema import ifcopenshell.util.element import ifcopenshell.util.shape_builder @@ -149,10 +150,9 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator): elements_to_update = elements_to_update | set(elements_to_reassign) objects_to_update = set(o for e in elements_to_update if (o := tool.Ifc.get_object(e))) - reassigned_elements = set() + reassigned_elements: set[ifcopenshell.entity_instance] = set() for element, ifc_class_ in elements_to_reassign.items(): - element = ifcopenshell.api.run( - "root.reassign_class", + element = ifcopenshell.api.root.reassign_class( self.file, product=element, ifc_class=ifc_class_, diff --git a/src/bonsai/bonsai/core/brick.py b/src/bonsai/bonsai/core/brick.py index 4d537a6751..27b82d43f8 100644 --- a/src/bonsai/bonsai/core/brick.py +++ b/src/bonsai/bonsai/core/brick.py @@ -16,8 +16,16 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations +from typing import TYPE_CHECKING, Optional, Union -def load_brick_project(brick, filepath=None, brick_root=None): +if TYPE_CHECKING: + import bpy + import ifcopenshell + import bonsai.tool as tool + + +def load_brick_project(brick: tool.Brick, filepath: str, brick_root: str) -> None: brick.load_brick_file(filepath) brick.import_brick_classes(brick_root) brick.import_brick_classes(brick_root, split_screen=True) @@ -25,7 +33,7 @@ def load_brick_project(brick, filepath=None, brick_root=None): brick.set_active_brick_class(brick_root, split_screen=True) -def new_brick_file(brick, brick_root=None): +def new_brick_file(brick: tool.Brick, brick_root: str) -> None: brick.new_brick_file() brick.import_brick_classes(brick_root) brick.import_brick_classes(brick_root, split_screen=True) @@ -33,7 +41,7 @@ def new_brick_file(brick, brick_root=None): brick.set_active_brick_class(brick_root, split_screen=True) -def view_brick_class(brick, brick_class=None, split_screen=False): +def view_brick_class(brick: tool.Brick, brick_class: str, split_screen: bool = False) -> None: brick.add_brick_breadcrumb(split_screen=split_screen) brick.clear_brick_browser(split_screen=split_screen) brick.import_brick_classes(brick_class, split_screen=split_screen) @@ -41,13 +49,13 @@ def view_brick_class(brick, brick_class=None, split_screen=False): brick.set_active_brick_class(brick_class, split_screen=split_screen) -def view_brick_item(brick, item=None, split_screen=False): +def view_brick_item(brick: tool.Brick, item: str, split_screen: bool = False) -> None: brick_class = brick.get_item_class(item) brick.run_view_brick_class(brick_class=brick_class, split_screen=split_screen) brick.select_browser_item(item, split_screen=split_screen) -def rewind_brick_class(brick, split_screen=False): +def rewind_brick_class(brick: tool.Brick, split_screen: bool = False) -> None: previous_class = brick.pop_brick_breadcrumb(split_screen=split_screen) brick.clear_brick_browser(split_screen=split_screen) brick.import_brick_classes(previous_class, split_screen=split_screen) @@ -55,7 +63,7 @@ def rewind_brick_class(brick, split_screen=False): brick.set_active_brick_class(previous_class, split_screen=split_screen) -def close_brick_project(brick): +def close_brick_project(brick: tool.Brick) -> None: brick.clear_project() brick.clear_brick_browser() brick.clear_brick_browser(split_screen=True) @@ -63,13 +71,19 @@ def close_brick_project(brick): brick.clear_breadcrumbs(split_screen=True) -def convert_brick_project(ifc, brick): +def convert_brick_project(ifc: tool.Ifc, brick: tool.Brick) -> None: library = ifc.run("library.add_library", name=brick.get_brick_path_name()) if ifc.get_schema() != "IFC2X3": ifc.run("library.edit_library", library=library, attributes={"Location": brick.get_brick_path()}) -def assign_brick_reference(ifc, brick, element=None, library=None, brick_uri=None): +def assign_brick_reference( + ifc: tool.Ifc, + brick: tool.Brick, + element: ifcopenshell.entity_instance, + library: ifcopenshell.entity_instance, + brick_uri: str, +) -> None: reference = brick.get_library_brick_reference(library, brick_uri) if not reference: reference = ifc.run("library.add_reference", library=library) @@ -81,7 +95,15 @@ def assign_brick_reference(ifc, brick, element=None, library=None, brick_uri=Non brick.add_brickifc_reference(brick_uri, element, project) -def add_brick(ifc, brick, element=None, namespace=None, brick_class=None, library=None, label="Unnamed"): +def add_brick( + ifc: tool.Ifc, + brick: tool.Brick, + element: Union[ifcopenshell.entity_instance, None], + namespace: str, + brick_class: str, + library: Union[str, None], + label: str = "Unnamed", +) -> None: if element: brick_uri = brick.add_brick_from_element(element, namespace, brick_class) if library: @@ -91,12 +113,12 @@ def add_brick(ifc, brick, element=None, namespace=None, brick_class=None, librar brick.run_refresh_brick_viewer() -def add_brick_relation(brick, brick_uri=None, predicate=None, object=None): +def add_brick_relation(brick: tool.Brick, brick_uri: str, predicate: str, object: str) -> None: brick.add_relation(brick_uri, predicate, object) brick.run_refresh_brick_viewer() -def convert_ifc_to_brick(brick, namespace=None, library=None): +def convert_ifc_to_brick(brick: tool.Brick, namespace: str, library: Union[ifcopenshell.entity_instance, None]) -> None: # convert spaces to brick spaces = brick.get_convertable_brick_spaces() space_uris = {} @@ -141,14 +163,14 @@ def convert_ifc_to_brick(brick, namespace=None, library=None): brick.run_refresh_brick_viewer() -def refresh_brick_viewer(brick): +def refresh_brick_viewer(brick: tool.Brick) -> None: brick.run_view_brick_class(brick_class=brick.get_active_brick_class()) brick.pop_brick_breadcrumb() brick.run_view_brick_class(brick_class=brick.get_active_brick_class(split_screen=True), split_screen=True) brick.pop_brick_breadcrumb(split_screen=True) -def remove_brick(ifc, brick, library=None, brick_uri=None): +def remove_brick(ifc: tool.Ifc, brick: tool.Brick, library: ifcopenshell.entity_instance, brick_uri: str) -> None: if library: reference = brick.get_library_brick_reference(library, brick_uri) if reference: @@ -157,18 +179,18 @@ def remove_brick(ifc, brick, library=None, brick_uri=None): brick.run_refresh_brick_viewer() -def serialize_brick(brick): +def serialize_brick(brick: tool.Brick) -> None: brick.serialize_brick() -def add_brick_namespace(brick, alias=None, uri=None): +def add_brick_namespace(brick: tool.Brick, alias: str, uri: str) -> None: brick.add_namespace(alias, uri) -def set_brick_list_root(brick, brick_root=None, split_screen=False): +def set_brick_list_root(brick: tool.Brick, brick_root: str, split_screen: bool = False) -> None: brick.run_view_brick_class(brick_class=brick_root, split_screen=split_screen) brick.clear_breadcrumbs(split_screen=split_screen) -def remove_brick_relation(brick, brick_uri=None, predicate=None, object=None): +def remove_brick_relation(brick: tool.Brick, brick_uri: str, predicate: str, object: str) -> None: brick.remove_relation(brick_uri, predicate, object) diff --git a/src/bonsai/bonsai/tool/brick.py b/src/bonsai/bonsai/tool/brick.py index acd8d50b01..ce34cb65fb 100644 --- a/src/bonsai/bonsai/tool/brick.py +++ b/src/bonsai/bonsai/tool/brick.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import os import bpy import datetime @@ -29,6 +30,7 @@ import bonsai.core.tool import bonsai.tool as tool from pathlib import Path from contextlib import contextmanager +from typing import Generator, Any, Union, TYPE_CHECKING try: import brickschema @@ -41,6 +43,11 @@ except: # See #1860 print("Warning: brickschema not available.") +if TYPE_CHECKING: + import brickschema + from rdflib import Literal, URIRef, Namespace, BNode + from bonsai.bim.module.brick.prop import BIMBrickProperties + # silence known rdflib_sqlalchemy TypeError warning # see https://github.com/BrickSchema/Brick/issues/513#issuecomment-1558493675 import logging @@ -51,7 +58,11 @@ logger.setLevel(logging.ERROR) class Brick(bonsai.core.tool.Brick): @classmethod - def add_brick(cls, namespace, brick_class, label): + def get_brick_props(cls) -> BIMBrickProperties: + return bpy.context.scene.BIMBrickProperties + + @classmethod + def add_brick(cls, namespace: str, brick_class: str, label: str) -> str: ns = Namespace(namespace) brick = ns[ifcopenshell.guid.expand(ifcopenshell.guid.new())] with BrickStore.new_changeset() as cs: @@ -60,8 +71,8 @@ class Brick(bonsai.core.tool.Brick): return str(brick) @classmethod - def add_brick_breadcrumb(cls, split_screen=False): - props = bpy.context.scene.BIMBrickProperties + def add_brick_breadcrumb(cls, split_screen: bool = False) -> None: + props = tool.Brick.get_brick_props() if split_screen: new = props.split_screen_brick_breadcrumbs.add() new.name = props.split_screen_active_brick_class @@ -70,7 +81,7 @@ class Brick(bonsai.core.tool.Brick): new.name = props.active_brick_class @classmethod - def add_brick_from_element(cls, element, namespace, brick_class): + def add_brick_from_element(cls, element: ifcopenshell.entity_instance, namespace: str, brick_class: str) -> str: ns = Namespace(namespace) brick = ns[element.GlobalId] with BrickStore.new_changeset() as cs: @@ -82,7 +93,7 @@ class Brick(bonsai.core.tool.Brick): return str(brick) @classmethod - def add_brickifc_project(cls, namespace): + def add_brickifc_project(cls, namespace: str) -> str: project = tool.Ifc.get().by_type("IfcProject")[0] ns = Namespace(namespace) brick_project = ns[project.GlobalId] @@ -96,7 +107,7 @@ class Brick(bonsai.core.tool.Brick): return str(brick_project) @classmethod - def add_brickifc_reference(cls, brick, element, project): + def add_brickifc_reference(cls, brick: str, element: ifcopenshell.entity_instance, project: str) -> None: with BrickStore.new_changeset() as cs: bnode = BNode() cs.add((URIRef(brick), REF.hasExternalReference, bnode)) @@ -107,23 +118,24 @@ class Brick(bonsai.core.tool.Brick): cs.add((bnode, REF.ifcName, Literal(element.Name))) @classmethod - def add_relation(cls, brick_uri, predicate, object): + def add_relation(cls, brick_uri: str, predicate: str, object: str) -> None: + props = tool.Brick.get_brick_props() if predicate == "http://www.w3.org/2000/01/rdf-schema#label": with BrickStore.new_changeset() as cs: cs.add((URIRef(brick_uri), URIRef(predicate), Literal(object))) - bpy.context.scene.BIMBrickProperties.new_brick_relation_type = BrickStore.relationships[0] - bpy.context.scene.BIMBrickProperties.add_brick_relation_failed = False + props.new_brick_relation_type = BrickStore.relationships[0] + props.add_brick_relation_failed = False return query = BrickStore.graph.query("ASK { <{object_uri}> a ?o . }".replace("{object_uri}", object)) if query: with BrickStore.new_changeset() as cs: cs.add((URIRef(brick_uri), URIRef(predicate), URIRef(object))) - bpy.context.scene.BIMBrickProperties.add_brick_relation_failed = False + props.add_brick_relation_failed = False else: - bpy.context.scene.BIMBrickProperties.add_brick_relation_failed = True + props.add_brick_relation_failed = True @classmethod - def remove_relation(cls, brick_uri, predicate, object): + def remove_relation(cls, brick_uri: str, predicate: str, object: str) -> None: with BrickStore.new_changeset() as cs: for s, p, o in BrickStore.graph.triples((brick_uri, predicate, object)): cs.remove((s, p, o)) @@ -132,21 +144,22 @@ class Brick(bonsai.core.tool.Brick): cs.remove(triple) @classmethod - def clear_brick_browser(cls, split_screen=False): - props = bpy.context.scene.BIMBrickProperties + def clear_brick_browser(cls, split_screen: bool = False) -> None: + props = cls.get_brick_props() if split_screen: props.split_screen_bricks.clear() else: props.bricks.clear() @classmethod - def clear_project(cls): + def clear_project(cls) -> None: BrickStore.purge() - bpy.context.scene.BIMBrickProperties.active_brick_class == "" - bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == "" + props = cls.get_brick_props() + props.active_brick_class = "" + props.split_screen_active_brick_class = "" @classmethod - def export_brick_attributes(cls, brick_uri): + def export_brick_attributes(cls, brick_uri: str) -> dict[str, Any]: query = BrickStore.graph.query( """ PREFIX rdfs: @@ -169,13 +182,14 @@ class Brick(bonsai.core.tool.Brick): return {"Identification": brick_uri, "Name": name} @classmethod - def get_active_brick_class(cls, split_screen=False): + def get_active_brick_class(cls, split_screen: bool = False) -> str: + props = cls.get_brick_props() if split_screen: - return bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class - return bpy.context.scene.BIMBrickProperties.active_brick_class + return props.split_screen_active_brick_class + return props.active_brick_class @classmethod - def get_brick(cls, element): + def get_brick(cls, element: ifcopenshell.entity_instance) -> Union[str, None]: for rel in element.HasAssociations: if rel.is_a("IfcRelAssociatesLibrary"): if tool.Ifc.get_schema() == "IFC2X3" and "#" in rel.RelatingLibrary.ItemReference: @@ -184,21 +198,21 @@ class Brick(bonsai.core.tool.Brick): return rel.RelatingLibrary.Identification @classmethod - def get_brick_class(cls, element): + def get_brick_class(cls, element: ifcopenshell.entity_instance) -> Union[str, None]: return ifcopenshell.util.brick.get_brick_type(element) @classmethod - def get_brick_path(cls): + def get_brick_path(cls) -> Union[str, None]: return BrickStore.path @classmethod - def get_brick_path_name(cls): + def get_brick_path_name(cls) -> str: if BrickStore.path: return os.path.basename(BrickStore.path) return "Unnamed" @classmethod - def get_brickifc_project(cls): + def get_brickifc_project(cls) -> Union[str, None]: project = tool.Ifc.get().by_type("IfcProject")[0] query = BrickStore.graph.query( """ @@ -217,45 +231,46 @@ class Brick(bonsai.core.tool.Brick): return results[0][0].toPython() @classmethod - def get_convertable_brick_elements(cls): + def get_convertable_brick_elements(cls) -> set[ifcopenshell.entity_instance]: equipment = set(tool.Ifc.get().by_type("IfcDistributionElement")) equipment -= set(tool.Ifc.get().by_type("IfcFlowSegment")) equipment -= set(tool.Ifc.get().by_type("IfcFlowFitting")) return equipment @classmethod - def get_convertable_brick_spaces(cls): + def get_convertable_brick_spaces(cls) -> set[ifcopenshell.entity_instance]: if tool.Ifc.get_schema() == "IFC2X3": return set(tool.Ifc.get().by_type("IfcSpatialStructureElement")) return set(tool.Ifc.get().by_type("IfcSpatialElement")) @classmethod - def get_convertable_brick_systems(cls): + def get_convertable_brick_systems(cls) -> set[ifcopenshell.entity_instance]: systems = set(tool.Ifc.get().by_type("IfcSystem")) systems -= set(tool.Ifc.get().by_type("IfcStructuralAnalysisModel")) systems -= set(tool.Ifc.get().by_type("IfcZone")) return systems @classmethod - def get_parent_space(cls, space): + def get_parent_space(cls, space: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: element = ifcopenshell.util.element.get_aggregate(space) + assert element if not element.is_a("IfcProject"): return element @classmethod - def get_element_container(cls, element): + def get_element_container(cls, element: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: return ifcopenshell.util.element.get_container(element) @classmethod - def get_element_systems(cls, element): + def get_element_systems(cls, element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: return ifcopenshell.util.system.get_element_systems(element) @classmethod - def get_element_feeds(cls, element): + def get_element_feeds(cls, element: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: return ifcopenshell.util.brick.get_element_feeds(element) @classmethod - def get_item_class(cls, item): + def get_item_class(cls, item: str) -> Union[str, None]: query = BrickStore.graph.query( """ PREFIX brick: @@ -271,7 +286,9 @@ class Brick(bonsai.core.tool.Brick): return row.get("class").toPython().split("#")[-1] @classmethod - def get_library_brick_reference(cls, library, brick_uri): + def get_library_brick_reference( + cls, library: ifcopenshell.entity_instance, brick_uri: str + ) -> Union[ifcopenshell.entity_instance, None]: if tool.Ifc.get_schema() == "IFC2X3": for reference in library.LibraryReference or []: if reference.ItemReference == brick_uri: @@ -282,11 +299,11 @@ class Brick(bonsai.core.tool.Brick): return reference @classmethod - def get_namespace(cls, uri): + def get_namespace(cls, uri: str) -> str: return uri.split("#")[0] + "#" @classmethod - def import_brick_classes(cls, brick_class, split_screen=False): + def import_brick_classes(cls, brick_class: str, split_screen: bool = False) -> None: query = BrickStore.graph.query( """ PREFIX brick: @@ -305,10 +322,11 @@ class Brick(bonsai.core.tool.Brick): "{brick_class}", brick_class ) ) + props = tool.Brick.get_brick_props() if split_screen: - bricks = bpy.context.scene.BIMBrickProperties.split_screen_bricks + bricks = props.split_screen_bricks else: - bricks = bpy.context.scene.BIMBrickProperties.bricks + bricks = props.bricks for row in query: new = bricks.add() label = row.get("label") @@ -319,7 +337,7 @@ class Brick(bonsai.core.tool.Brick): new.total_items = row.get("total_items").toPython() @classmethod - def import_brick_items(cls, brick_class, split_screen=False): + def import_brick_items(cls, brick_class: str, split_screen: bool = False) -> None: query = BrickStore.graph.query( """ PREFIX brick: @@ -336,10 +354,11 @@ class Brick(bonsai.core.tool.Brick): "{brick_class}", brick_class ) ) + props = tool.Brick.get_brick_props() if split_screen: - bricks = bpy.context.scene.BIMBrickProperties.split_screen_bricks + bricks = props.split_screen_bricks else: - bricks = bpy.context.scene.BIMBrickProperties.bricks + bricks = props.bricks for row in query: new = bricks.add() label = row.get("label") @@ -349,7 +368,7 @@ class Brick(bonsai.core.tool.Brick): new.uri = row.get("item").toPython() @classmethod - def load_brick_file(cls, filepath): + def load_brick_file(cls, filepath: str) -> None: if not BrickStore.schema: # important check for running under test cases BrickStore.schema = tool.Blender.get_data_dir_path(Path("brick", "Brick.ttl")) BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") @@ -365,7 +384,7 @@ class Brick(bonsai.core.tool.Brick): BrickStore.load_relationships() @classmethod - def new_brick_file(cls): + def new_brick_file(cls) -> None: if not BrickStore.schema: # important check for running under test cases BrickStore.schema = tool.Blender.get_data_dir_path(Path("brick", "Brick.ttl")) BrickStore.graph = brickschema.persistent.VersionedGraphCollection("sqlite://") @@ -378,8 +397,8 @@ class Brick(bonsai.core.tool.Brick): BrickStore.load_relationships() @classmethod - def pop_brick_breadcrumb(cls, split_screen=False): - props = bpy.context.scene.BIMBrickProperties + def pop_brick_breadcrumb(cls, split_screen: bool = False) -> str: + props = cls.get_brick_props() if split_screen: breadcrumbs = props.split_screen_brick_breadcrumbs else: @@ -391,7 +410,7 @@ class Brick(bonsai.core.tool.Brick): return name @classmethod - def remove_brick(cls, brick_uri): + def remove_brick(cls, brick_uri: str) -> None: with BrickStore.new_changeset() as cs: for s, p, o in BrickStore.graph.triples((URIRef(brick_uri), None, None)): cs.remove((s, p, o)) @@ -406,51 +425,55 @@ class Brick(bonsai.core.tool.Brick): ) @classmethod - def run_refresh_brick_viewer(cls): + def run_refresh_brick_viewer(cls) -> None: return bonsai.core.brick.refresh_brick_viewer(tool.Brick) @classmethod - def run_view_brick_class(cls, brick_class=None, split_screen=False): + def run_view_brick_class(cls, brick_class: Union[str, None] = None, split_screen: bool = False) -> None: return bonsai.core.brick.view_brick_class(tool.Brick, brick_class=brick_class, split_screen=split_screen) @classmethod - def select_browser_item(cls, item, split_screen=False): + def select_browser_item(cls, item: str, split_screen: bool = False) -> None: name = item.split("#")[-1] - props = bpy.context.scene.BIMBrickProperties + props = cls.get_brick_props() if split_screen: props.split_screen_active_brick_index = props.split_screen_bricks.find(name) else: props.active_brick_index = props.bricks.find(name) @classmethod - def set_active_brick_class(cls, brick_class, split_screen=False): - props = bpy.context.scene.BIMBrickProperties + def set_active_brick_class(cls, brick_class: str, split_screen: bool = False) -> None: + props = cls.get_brick_props() if split_screen: props.split_screen_active_brick_class = brick_class else: props.active_brick_class = brick_class @classmethod - def serialize_brick(cls): + def serialize_brick(cls) -> None: BrickStore.get_project().serialize(destination=BrickStore.path, format="turtle") BrickStore.set_last_saved() @classmethod - def add_namespace(cls, alias, uri): + def add_namespace(cls, alias: str, uri: str) -> None: + assert BrickStore.graph BrickStore.graph.bind(alias, Namespace(uri)) BrickStore.load_namespaces() @classmethod - def clear_breadcrumbs(cls, split_screen=False): + def clear_breadcrumbs(cls, split_screen: bool = False) -> None: + props = cls.get_brick_props() if split_screen: - bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs.clear() + props.split_screen_brick_breadcrumbs.clear() else: - bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.clear() + props.brick_breadcrumbs.clear() class BrickStore: schema = None # this is now a os path + path: Union[str, None] path = None # file path if the project was loaded in + graph: Union[brickschema.persistent.VersionedGraphCollection, None] graph = None # this is the VersionedGraphCollection with 2 arbitrarily named graphs: "schema" and "project" # "SCHEMA" holds the Brick.ttl metadata; "PROJECT" holds all the authored entities last_saved = None @@ -475,11 +498,11 @@ class BrickStore: BrickStore.relationships = [] @classmethod - def get_project(cls): + def get_project(cls) -> brickschema.graph.Graph: return BrickStore.graph.graph_at(graph="PROJECT") @classmethod - def load_sub_roots(cls): + def load_sub_roots(cls) -> None: query = BrickStore.graph.query( """ PREFIX brick: @@ -505,7 +528,7 @@ class BrickStore: BrickStore.root_classes.append(sub_root) @classmethod - def load_namespaces(cls): + def load_namespaces(cls) -> None: BrickStore.namespaces = [] keyword_filter = [ "brickschema.org", @@ -529,7 +552,7 @@ class BrickStore: BrickStore.namespaces.append((alias, str(uri))) @classmethod - def load_entity_classes(cls): + def load_entity_classes(cls) -> None: for root_class in BrickStore.root_classes: query = BrickStore.graph.query( """ @@ -551,7 +574,7 @@ class BrickStore: BrickStore.entity_classes[root_class].append(uri) @classmethod - def load_relationships(cls): + def load_relationships(cls) -> None: query = BrickStore.graph.query( """ PREFIX brick: @@ -565,30 +588,30 @@ class BrickStore: BrickStore.relationships.append(uri) @classmethod - def set_history_size(cls, size): + def set_history_size(cls, size: int) -> None: cls.history_size = size while len(cls.history) > cls.history_size: cls.history.pop(0) @classmethod - def begin_transaction(cls): + def begin_transaction(cls) -> None: cls.current_changesets = 0 @classmethod - def end_transaction(cls): + def end_transaction(cls) -> None: cls.history.append(cls.current_changesets) if len(cls.history) > cls.history_size: cls.history.pop(0) @classmethod @contextmanager - def new_changeset(cls): + def new_changeset(cls) -> Generator[Any, None, None]: cls.current_changesets += 1 with BrickStore.graph.new_changeset("PROJECT") as cs: yield cs @classmethod - def undo(cls): + def undo(cls) -> None: if not BrickStore.graph or not BrickStore.history: return total_changesets = BrickStore.history.pop() @@ -597,7 +620,7 @@ class BrickStore: BrickStore.future.append(total_changesets) @classmethod - def redo(cls): + def redo(cls) -> None: if not BrickStore.graph or not BrickStore.future: return total_changesets = BrickStore.future.pop() @@ -606,7 +629,7 @@ class BrickStore: BrickStore.history.append(total_changesets) @classmethod - def set_last_saved(cls): + def set_last_saved(cls) -> None: save = os.path.getmtime(BrickStore.path) save = datetime.datetime.fromtimestamp(save) BrickStore.last_saved = f"{save.year}-{save.month}-{save.day} {save.hour}:{save.minute}" diff --git a/src/bonsai/test/tool/test_brick.py b/src/bonsai/test/tool/test_brick.py index b26a2acd43..0220cb2962 100644 --- a/src/bonsai/test/tool/test_brick.py +++ b/src/bonsai/test/tool/test_brick.py @@ -59,16 +59,18 @@ class TestAddBrickBreadcrumb(NewFile): def test_run(self): subject.set_active_brick_class("brick_class") subject.add_brick_breadcrumb() - assert bpy.context.scene.BIMBrickProperties.brick_breadcrumbs[0].name == "brick_class" + props = tool.Brick.get_brick_props() + assert props.brick_breadcrumbs[0].name == "brick_class" subject.add_brick_breadcrumb() - assert bpy.context.scene.BIMBrickProperties.brick_breadcrumbs[1].name == "brick_class" + assert props.brick_breadcrumbs[1].name == "brick_class" def test_run_split_screen(self): subject.set_active_brick_class("brick_class", split_screen=True) subject.add_brick_breadcrumb(split_screen=True) - assert bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs[0].name == "brick_class" + props = tool.Brick.get_brick_props() + assert props.split_screen_brick_breadcrumbs[0].name == "brick_class" subject.add_brick_breadcrumb(split_screen=True) - assert bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs[1].name == "brick_class" + assert props.split_screen_brick_breadcrumbs[1].name == "brick_class" class TestAddBrickFromElement(NewFile): @@ -188,25 +190,28 @@ class TestRemoveRelation(NewFile): class TestClearBrickBrowser(NewFile): def test_run(self): - bpy.context.scene.BIMBrickProperties.bricks.add() + props = tool.Brick.get_brick_props() + props.bricks.add() subject.clear_brick_browser() - assert len(bpy.context.scene.BIMBrickProperties.bricks) == 0 + assert len(props.bricks) == 0 def test_run_split_screen(self): - bpy.context.scene.BIMBrickProperties.split_screen_bricks.add() + props = tool.Brick.get_brick_props() + props.split_screen_bricks.add() subject.clear_brick_browser(split_screen=True) - assert len(bpy.context.scene.BIMBrickProperties.split_screen_bricks) == 0 + assert len(props.split_screen_bricks) == 0 class TestClearProject(NewFile): def test_run(self): BrickStore.graph = "graph" - bpy.context.scene.BIMBrickProperties.active_brick_class == "brick_class" - bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == "brick_class2" + props = tool.Brick.get_brick_props() + props.active_brick_class = "brick_class" + props.split_screen_active_brick_class = "brick_class2" subject.clear_project() assert BrickStore.graph is None - assert bpy.context.scene.BIMBrickProperties.active_brick_class == "" - assert bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == "" + assert props.active_brick_class == "" + assert props.split_screen_active_brick_class == "" class TestExportBrickAttributes(NewFile): @@ -374,13 +379,14 @@ class TestImportBrickClasses(NewFile): def test_run(self): TestLoadBrickFile().test_run() subject.import_brick_classes("Class") - assert len(bpy.context.scene.BIMBrickProperties.bricks) == 2 - brick = bpy.context.scene.BIMBrickProperties.bricks[0] + props = tool.Brick.get_brick_props() + assert len(props.bricks) == 2 + brick = props.bricks[0] assert brick.name == "Building" assert brick.uri == "https://brickschema.org/schema/Brick#Building" assert brick.total_items == 1 assert not brick.label - brick = bpy.context.scene.BIMBrickProperties.bricks[1] + brick = props.bricks[1] assert brick.name == "Location" assert brick.uri == "https://brickschema.org/schema/Brick#Location" assert brick.total_items == 1 @@ -389,13 +395,14 @@ class TestImportBrickClasses(NewFile): def test_run_split_sccreen(self): TestLoadBrickFile().test_run() subject.import_brick_classes("Class", split_screen=True) - assert len(bpy.context.scene.BIMBrickProperties.split_screen_bricks) == 2 - brick = bpy.context.scene.BIMBrickProperties.split_screen_bricks[0] + props = tool.Brick.get_brick_props() + assert len(props.split_screen_bricks) == 2 + brick = props.split_screen_bricks[0] assert brick.name == "Building" assert brick.uri == "https://brickschema.org/schema/Brick#Building" assert brick.total_items == 1 assert not brick.label - brick = bpy.context.scene.BIMBrickProperties.split_screen_bricks[1] + brick = props.split_screen_bricks[1] assert brick.name == "Location" assert brick.uri == "https://brickschema.org/schema/Brick#Location" assert brick.total_items == 1 @@ -406,8 +413,9 @@ class TestImportBrickItems(NewFile): def test_run(self): TestLoadBrickFile().test_run() subject.import_brick_items("Building") - assert len(bpy.context.scene.BIMBrickProperties.bricks) == 1 - brick = bpy.context.scene.BIMBrickProperties.bricks[0] + props = tool.Brick.get_brick_props() + assert len(props.bricks) == 1 + brick = props.bricks[0] assert brick.name == "bldg" assert brick.label == "My Building" assert brick.uri == "https://example.org/digitaltwin#bldg" @@ -416,8 +424,9 @@ class TestImportBrickItems(NewFile): def test_run_split_screen(self): TestLoadBrickFile().test_run() subject.import_brick_items("Building", split_screen=True) - assert len(bpy.context.scene.BIMBrickProperties.split_screen_bricks) == 1 - brick = bpy.context.scene.BIMBrickProperties.split_screen_bricks[0] + props = tool.Brick.get_brick_props() + assert len(props.split_screen_bricks) == 1 + brick = props.split_screen_bricks[0] assert brick.name == "bldg" assert brick.label == "My Building" assert brick.uri == "https://example.org/digitaltwin#bldg" @@ -456,18 +465,20 @@ class TestNewBrickFile(NewFile): class TestPopBrickBreadcrumb(NewFile): def test_run(self): - bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.add().name = "foo" - bpy.context.scene.BIMBrickProperties.brick_breadcrumbs.add().name = "bar" + props = tool.Brick.get_brick_props() + props.brick_breadcrumbs.add().name = "foo" + props.brick_breadcrumbs.add().name = "bar" assert subject.pop_brick_breadcrumb() == "bar" - assert len(bpy.context.scene.BIMBrickProperties.brick_breadcrumbs) == 1 - assert bpy.context.scene.BIMBrickProperties.brick_breadcrumbs[0].name == "foo" + assert len(props.brick_breadcrumbs) == 1 + assert props.brick_breadcrumbs[0].name == "foo" def test_run_split_screen(self): - bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs.add().name = "foo" - bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs.add().name = "bar" + props = tool.Brick.get_brick_props() + props.split_screen_brick_breadcrumbs.add().name = "foo" + props.split_screen_brick_breadcrumbs.add().name = "bar" assert subject.pop_brick_breadcrumb(split_screen=True) == "bar" - assert len(bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs) == 1 - assert bpy.context.scene.BIMBrickProperties.split_screen_brick_breadcrumbs[0].name == "foo" + assert len(props.split_screen_brick_breadcrumbs) == 1 + assert props.split_screen_brick_breadcrumbs[0].name == "foo" class TestRemoveBrick(NewFile): @@ -512,19 +523,22 @@ class TestRunViewBrickClass(NewFile): class TestSelectBrowserItem(NewFile): def test_run(self): subject.set_active_brick_class("brick_class") - assert bpy.context.scene.BIMBrickProperties.active_brick_class == "brick_class" + props = tool.Brick.get_brick_props() + assert props.active_brick_class == "brick_class" def test_run(self): subject.set_active_brick_class("brick_class", split_screen=True) - assert bpy.context.scene.BIMBrickProperties.split_screen_active_brick_class == "brick_class" + props = tool.Brick.get_brick_props() + assert props.split_screen_active_brick_class == "brick_class" class TestSetActiveBrickClass(NewFile): def test_run(self): - bpy.context.scene.BIMBrickProperties.bricks.add().name = "foo" - bpy.context.scene.BIMBrickProperties.bricks.add().name = "bar" + props = tool.Brick.get_brick_props() + props.bricks.add().name = "foo" + props.bricks.add().name = "bar" subject.select_browser_item("namespace#bar") - assert bpy.context.scene.BIMBrickProperties.active_brick_index == 1 + assert props.active_brick_index == 1 class TestSerializeBrick(NewFile): diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py index 7682856864..a7588962dd 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py @@ -27,7 +27,7 @@ import ifcopenshell.util.representation import ifcopenshell.util.type import ifcopenshell.util.schema import ifcopenshell.util.element -from typing import Optional, Union, Literal, Any +from typing import Optional, Union, Literal def reassign_class( @@ -56,7 +56,6 @@ def reassign_class( Reassigning type class to occurrence (and vice versa) is supported. :param product: The IfcProduct that you want to change the class of. - :type product: ifcopenshell.entity_instance :param ifc_class: The new IFC class you want to change it to. :param predefined_type: In case you want to change the predefined type too. User defined types are also allowed, just type what you want. @@ -77,25 +76,18 @@ def reassign_class( """ usecase = Usecase() usecase.file = file - usecase.settings = { - "product": product, - "ifc_class": ifc_class, - "predefined_type": predefined_type, - } - return usecase.execute() + return usecase.execute(product, ifc_class, predefined_type) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] - - def execute(self): - ifc_class: str = self.settings["ifc_class"] - product: ifcopenshell.entity_instance = self.settings["product"] - predefined_type: Union[str, None] = self.settings["predefined_type"] + def execute( + self, product: ifcopenshell.entity_instance, ifc_class: str, predefined_type: Union[str, None] + ) -> ifcopenshell.entity_instance: was_type_product_before = product.is_a("IfcTypeProduct") schema = ifcopenshell.schema_by_name(self.file.schema) + is_type_product_after: bool is_type_product_after = schema.declaration_by_name(ifc_class)._is("IfcTypeProduct") if was_type_product_before == is_type_product_after: diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/unassign_type.py b/src/ifcopenshell-python/ifcopenshell/api/type/unassign_type.py index 2723dd760f..572327afb7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/unassign_type.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/unassign_type.py @@ -28,9 +28,7 @@ def unassign_type(file: ifcopenshell.file, related_objects: list[ifcopenshell.en and material usages associated with the previously assigned type. :param related_objects: List of IfcElement occurrences. - :type related_objects: list[ifcopenshell.entity_instance] :return: None - :rtype: None Example: @@ -50,23 +48,21 @@ def unassign_type(file: ifcopenshell.file, related_objects: list[ifcopenshell.en # Change our mind. Maybe it's a different type? ifcopenshell.api.type.unassign_type(model, related_objects=[furniture]) """ - settings = {"related_objects": related_objects} - - related_objects = set(settings["related_objects"]) + related_objects_set = set(related_objects) if file.schema == "IFC2X3": rels = set( rel - for object in related_objects + for object in related_objects_set if (rel := next((rel for rel in object.IsDefinedBy if rel.is_a("IfcRelDefinesByType")), None)) ) else: - rels = set(rel for object in related_objects if (rel := next((rel for rel in object.IsTypedBy), None))) + rels = set(rel for object in related_objects_set if (rel := next((rel for rel in object.IsTypedBy), None))) for rel in rels: - related_objects = set(rel.RelatedObjects) - related_objects - if related_objects: - rel.RelatedObjects = list(related_objects) + related_objects_set = set(rel.RelatedObjects) - related_objects_set + if related_objects_set: + rel.RelatedObjects = list(related_objects_set) ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) else: history = rel.OwnerHistory diff --git a/src/ifcopenshell-python/ifcopenshell/util/brick.py b/src/ifcopenshell-python/ifcopenshell/util/brick.py index 9070a0054b..a50caeaa05 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/brick.py +++ b/src/ifcopenshell-python/ifcopenshell/util/brick.py @@ -21,6 +21,7 @@ import json import ifcopenshell import ifcopenshell.util.element import ifcopenshell.util.classification +from typing import Union cwd = os.path.dirname(os.path.realpath(__file__)) @@ -31,7 +32,7 @@ with open(os.path.join(cwd, "ifc4_to_brick.json")) as f: ifc4_to_brick_map = json.load(f) -def get_brick_type(element): +def get_brick_type(element: ifcopenshell.entity_instance) -> Union[str, None]: references = ifcopenshell.util.classification.get_references(element) for reference in references: system = ifcopenshell.util.classification.get_classification(reference) @@ -61,10 +62,10 @@ def get_brick_type(element): return f"https://brickschema.org/schema/Brick#System" -def get_element_feeds(element): +def get_element_feeds(element: ifcopenshell.entity_instance) -> set[ifcopenshell.entity_instance]: current_element = element processed_elements = set() - downstream_equipment = set() + downstream_equipment: set[ifcopenshell.entity_instance] = set() # A queue is a list of branches. A branch is a list of elements in # sequence, each one connecting to another element. An element in a From 64f51d733e9b1a633df0100fbf1e4f8ea1d715a1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 11 Mar 2025 12:06:25 +0500 Subject: [PATCH 291/476] Fixed version of f3f7939c8b + get_element_zones --- src/bonsai/bonsai/bim/module/system/data.py | 7 ++++-- .../ifcopenshell-python/selector_syntax.rst | 1 + .../ifcopenshell/util/selector.py | 2 ++ .../ifcopenshell/util/system.py | 25 +++++++++++++------ 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/data.py b/src/bonsai/bonsai/bim/module/system/data.py index 836bf17079..3dd6856e72 100644 --- a/src/bonsai/bonsai/bim/module/system/data.py +++ b/src/bonsai/bonsai/bim/module/system/data.py @@ -267,5 +267,8 @@ class ActiveObjectZonesData: @classmethod def zones(cls): - systems = ifcopenshell.util.system.get_element_systems(tool.Ifc.get_entity(bpy.context.active_object)) - return [s.Name or "Unnamed" for s in systems if s.is_a("IfcZone")] + obj = bpy.context.active_object + assert obj + element = tool.Ifc.get_entity(obj) + assert element + return [z.Name or "Unnamed" for z in ifcopenshell.util.system.get_element_zones(element)] diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst index a5eeb47c1b..cfb49034fb 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst @@ -194,6 +194,7 @@ Valid keys are: "``classification``", "Gets the element's classification reference(s)" "``group``", "Gets the element's group(s)" "``system``", "Gets the element's system(s). This is a subset of group(s)." + "``zone``", "Gets the element's zone(s). This is a subset of group(s)." "``material`` or ``mat``", "Gets the assigned material, which may be a material set." "``item`` or ``i``", "If the previous key returns a material set, gets the relevant material set items" "``materials`` or ``mats``", "Gets a list of IfcMaterials assigned directly or indirectly (such as via a material set) to the element" diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index f0a1025583..22608af9cc 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -347,6 +347,8 @@ def _get_element_value(element: ifcopenshell.entity_instance, keys: list[str]) - value = ifcopenshell.util.element.get_groups(value) elif key == "system": value = ifcopenshell.util.system.get_element_systems(value) + elif key == "zone": + value = ifcopenshell.util.system.get_element_zones(value) elif key in ("x", "y", "z", "easting", "northing", "elevation") and hasattr(value, "ObjectPlacement"): if getattr(value, "ObjectPlacement", None): matrix = ifcopenshell.util.placement.get_local_placement(value.ObjectPlacement) diff --git a/src/ifcopenshell-python/ifcopenshell/util/system.py b/src/ifcopenshell-python/ifcopenshell/util/system.py index ac19ab3c35..ef84be03c5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/system.py +++ b/src/ifcopenshell-python/ifcopenshell/util/system.py @@ -66,13 +66,24 @@ def get_system_elements(system: ifcopenshell.entity_instance) -> list[ifcopenshe def get_element_systems(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: results = [] for rel in element.HasAssignments: - if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup.is_a() in ( - "IfcSystem", - "IfcDistributionSystem", - "IfcBuildingSystem", - "IfcZone", - ): - results.append(rel.RelatingGroup) + if rel.is_a("IfcRelAssignsToGroup"): + continue + group = rel.RelatingGroup + if not group.is_a("IfcSystem") or group.is_a() in ("IfcStructuralAnalysisModel", "IfcZone"): + continue + results.append(group) + return results + + +def get_element_zones(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: + results = [] + for rel in element.HasAssignments: + if rel.is_a("IfcRelAssignsToGroup"): + continue + group = rel.RelatingGroup + if not group.is_a("IfcZone"): + continue + results.append(group) return results From 85b9906e20ce6d2541fa11f730c2b932e5b67a47 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 11 Mar 2025 12:14:15 +0500 Subject: [PATCH 292/476] Zones UI - add buttons for unassigning zone from the active objects --- src/bonsai/bonsai/bim/module/system/data.py | 11 +++++++++-- src/bonsai/bonsai/bim/module/system/ui.py | 17 +++-------------- src/bonsai/bonsai/tool/system.py | 11 +++++++++++ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/data.py b/src/bonsai/bonsai/bim/module/system/data.py index 3dd6856e72..2a1cf5d3d2 100644 --- a/src/bonsai/bonsai/bim/module/system/data.py +++ b/src/bonsai/bonsai/bim/module/system/data.py @@ -23,6 +23,7 @@ import ifcopenshell.util.system import ifcopenshell.util.unit from ifcopenshell.util.doc import get_entity_doc import bonsai.tool as tool +from typing import Any def refresh(): @@ -266,9 +267,15 @@ class ActiveObjectZonesData: cls.is_loaded = True @classmethod - def zones(cls): + def zones(cls) -> list[dict[str, Any]]: obj = bpy.context.active_object assert obj element = tool.Ifc.get_entity(obj) assert element - return [z.Name or "Unnamed" for z in ifcopenshell.util.system.get_element_zones(element)] + return [ + { + "id": z.id(), + "Name": (z.Name or "Unnamed"), + } + for z in ifcopenshell.util.system.get_element_zones(element) + ] diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 0cae9e9a54..23d73ebb0a 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -66,14 +66,6 @@ class BIM_PT_systems(Panel): if not ObjectSystemData.is_loaded: ObjectSystemData.load() - def draw_system_ui(row, system_id, system_name, system_class): - row = self.layout.row(align=True) - row.label(text=system_name, icon=SYSTEM_ICONS[system_class]) - op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") - op.system = system_id - op = row.operator("bim.unassign_system", text="", icon="X") - op.system = system_id - self.props = tool.System.get_system_props() active_system_item = self.props.active_system_ui_item row = self.layout.row(align=True) @@ -82,8 +74,7 @@ class BIM_PT_systems(Panel): row = self.layout.row() if active_system := tool.System.get_active_system(): row.label(text=f"Active system:") - row = self.layout.row(align=True) - draw_system_ui(row, active_system.id(), active_system.Name, active_system.is_a()) + tool.System.draw_system_ui(self.layout, active_system.id(), active_system.Name, active_system.is_a()) else: row.label(text="No active system is selected") @@ -91,8 +82,7 @@ class BIM_PT_systems(Panel): row = self.layout.row() row.label(text="Active object systems:") for system in ObjectSystemData.data["systems"]: - row = self.layout.row(align=True) - draw_system_ui(row, system["id"], system["name"], system["ifc_class"]) + tool.System.draw_system_ui(self.layout, system["id"], system["name"], system["ifc_class"]) else: self.layout.label(text="No System associated with active object") @@ -416,8 +406,7 @@ class BIM_PT_active_object_zones(Panel): self.props = tool.System.get_zone_props() for zone in ActiveObjectZonesData.data["zones"]: - row = self.layout.row() - row.label(text=zone, icon="SEQ_STRIP_META") + tool.System.draw_system_ui(self.layout, zone["id"], zone["Name"], "IfcZone") if not ActiveObjectZonesData.data["zones"]: row = self.layout.row() diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 2a891b8136..91c0a2a9ba 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -426,3 +426,14 @@ class System(bonsai.core.tool.System): if not element.AssignedToFlowElement: return return element.AssignedToFlowElement[0].RelatingFlowElement + + @classmethod + def draw_system_ui(cls, layout: bpy.types.UILayout, system_id: int, system_name: str, system_class: str) -> None: + from bonsai.bim.module.system.ui import SYSTEM_ICONS + + row = layout.row(align=True) + row.label(text=system_name, icon=SYSTEM_ICONS[system_class]) + op = row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF") + op.system = system_id + op = row.operator("bim.unassign_system", text="", icon="X") + op.system = system_id From 5b95bb37e6882ac8d5c9113aa43d2b7049ee3223 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 11 Mar 2025 12:21:03 +0500 Subject: [PATCH 293/476] Systems UI - cache active system data --- src/bonsai/bonsai/bim/module/system/data.py | 10 +++++++++- src/bonsai/bonsai/bim/module/system/operator.py | 5 +++-- src/bonsai/bonsai/bim/module/system/ui.py | 6 ++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/data.py b/src/bonsai/bonsai/bim/module/system/data.py index 2a1cf5d3d2..aa36696965 100644 --- a/src/bonsai/bonsai/bim/module/system/data.py +++ b/src/bonsai/bonsai/bim/module/system/data.py @@ -23,7 +23,7 @@ import ifcopenshell.util.system import ifcopenshell.util.unit from ifcopenshell.util.doc import get_entity_doc import bonsai.tool as tool -from typing import Any +from typing import Any, Union def refresh(): @@ -44,6 +44,7 @@ class SystemData: cls.data = { "system_class": cls.system_class(), "total_systems": cls.total_systems(), + "active_system": cls.active_system(), } cls.is_loaded = True @@ -64,6 +65,13 @@ class SystemData: def total_systems(cls): return len(tool.System.get_systems()) + @classmethod + def active_system(cls) -> Union[dict[str, Any], None]: + active_system = tool.System.get_active_system() + if not active_system: + return None + return {"id": active_system.id(), "Name": active_system.Name, "ifc_class": active_system.is_a()} + class ObjectSystemData: data = {} diff --git a/src/bonsai/bonsai/bim/module/system/operator.py b/src/bonsai/bonsai/bim/module/system/operator.py index df098fa04b..afd45ec735 100644 --- a/src/bonsai/bonsai/bim/module/system/operator.py +++ b/src/bonsai/bonsai/bim/module/system/operator.py @@ -22,7 +22,7 @@ import ifcopenshell.util.system import bonsai.tool as tool import bonsai.core.system as core import bonsai.bim.helper -from bonsai.bim.module.system.data import PortData +from bonsai.bim.module.system.data import PortData, SystemData class LoadSystems(bpy.types.Operator): @@ -157,12 +157,13 @@ class UnassignSystem(bpy.types.Operator, tool.Ifc.Operator): class SelectSystemProducts(bpy.types.Operator): bl_idname = "bim.select_system_products" - bl_label = "Select System Products" + bl_label = "Select System Products And Set Active System" bl_options = {"REGISTER", "UNDO"} system: bpy.props.IntProperty() def execute(self, context): core.select_system_products(tool.System, system=tool.Ifc.get().by_id(self.system)) + SystemData.data["active_system"] = SystemData.active_system() return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 23d73ebb0a..e16bf85971 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -72,9 +72,11 @@ class BIM_PT_systems(Panel): row.prop(self.props, "should_draw_decorations") row = self.layout.row() - if active_system := tool.System.get_active_system(): + if active_system := SystemData.data["active_system"]: row.label(text=f"Active system:") - tool.System.draw_system_ui(self.layout, active_system.id(), active_system.Name, active_system.is_a()) + tool.System.draw_system_ui( + self.layout, active_system["id"], active_system["Name"], active_system["ifc_class"] + ) else: row.label(text="No active system is selected") From 977d814d39a0b09b85c57d940f65bb8543722683 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 11 Mar 2025 18:35:34 +0500 Subject: [PATCH 294/476] bim.reassign_class - fix issue in ifc2x3 when selected product class would be ignored E.g. you select IfcSlab and change it to IfcRoof - since there is not IfcRoofType in IFC2X3 it would figure the matching product type is IfcBeamType (which is confusing too but that's another subject) and change this slab's type object to IfcBeamType which consequently change IfcSlab to IfcBeam instead of IfcRoof that was selected originally. Noticed investigating #5918 --- src/bonsai/bonsai/bim/module/root/operator.py | 3 +++ .../ifcopenshell/api/root/reassign_class.py | 22 ++++++++++++++---- .../test/api/root/test_reassign_class.py | 23 ++++++++++++++++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 4accda58d9..624eefc11d 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -157,6 +157,9 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator): product=element, ifc_class=ifc_class_, predefined_type=predefined_type, + # Provide occurrence class in all cases as it won't really matter + # for non-IfcTypeProducts. + occurrence_class=ifc_class, ) reassigned_elements.add(element) diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py index a7588962dd..a4f4f09b93 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py @@ -35,6 +35,7 @@ def reassign_class( product: ifcopenshell.entity_instance, ifc_class: str = "IfcBuildingElementProxy", predefined_type: Optional[str] = None, + occurrence_class: Optional[str] = None, ) -> ifcopenshell.entity_instance: """Changes the class of a product @@ -59,6 +60,11 @@ def reassign_class( :param ifc_class: The new IFC class you want to change it to. :param predefined_type: In case you want to change the predefined type too. User defined types are also allowed, just type what you want. + :param occurrence_class: IFC class to assign to occurrences in case + if provided ``ifc_class`` is IfcTypeProduct. + If omitted, class will be deduced automatically from the type. + Only really needed in IFC2X3, since in IFC4+ there is no ambiguity on + what class to assign to occurrences. :return: The newly modified product. Example: @@ -76,15 +82,20 @@ def reassign_class( """ usecase = Usecase() usecase.file = file - return usecase.execute(product, ifc_class, predefined_type) + return usecase.execute(product, ifc_class, predefined_type, occurrence_class) class Usecase: file: ifcopenshell.file def execute( - self, product: ifcopenshell.entity_instance, ifc_class: str, predefined_type: Union[str, None] + self, + product: ifcopenshell.entity_instance, + ifc_class: str, + predefined_type: Union[str, None], + occurrence_class: Union[str, None], ) -> ifcopenshell.entity_instance: + self.occurrence_class = occurrence_class was_type_product_before = product.is_a("IfcTypeProduct") schema = ifcopenshell.schema_by_name(self.file.schema) is_type_product_after: bool @@ -154,7 +165,7 @@ class Usecase: for rep in representations: ifcopenshell.api.geometry.assign_representation(self.file, product=element, representation=rep) - # Keep IFC valid. + # Keep IFC valid (PlacementForShapeRepresentation). if switch_type == "type_to_occurrence" and representations: ifcopenshell.api.geometry.edit_object_placement(self.file, product=element) @@ -169,7 +180,10 @@ class Usecase: element = self.reassign_class(element, ifc_class, predefined_type) if element.is_a("IfcTypeProduct"): for occurrence in ifcopenshell.util.element.get_types(element): - ifc_class_ = ifcopenshell.util.type.get_applicable_entities(ifc_class, self.file.schema)[0] + if self.occurrence_class: + ifc_class_ = self.occurrence_class + else: + ifc_class_ = next(iter(ifcopenshell.util.type.get_applicable_entities(ifc_class, self.file.schema))) self.reassign_class(occurrence, ifc_class_, predefined_type) else: element_type = ifcopenshell.util.element.get_type(element) diff --git a/src/ifcopenshell-python/test/api/root/test_reassign_class.py b/src/ifcopenshell-python/test/api/root/test_reassign_class.py index f7b1024b6b..f8053e13b5 100644 --- a/src/ifcopenshell-python/test/api/root/test_reassign_class.py +++ b/src/ifcopenshell-python/test/api/root/test_reassign_class.py @@ -198,4 +198,25 @@ class TestReassignClass(test.bootstrap.IFC4): class TestReassignClassIFC2X3(test.bootstrap.IFC2X3, TestReassignClass): - pass + def test_providing_occurrence_class(self): + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + element1 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element1], relating_type=element_type) + element2 = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element2], relating_type=element_type) + + new_element_type = ifcopenshell.api.root.reassign_class( + self.file, + product=element_type, + ifc_class="IfcBuildingElementProxyType", + occurrence_class="IfcRoof", + ) + assert new_element_type.is_a("IfcBuildingElementProxyType") + occurrences = ifcopenshell.util.element.get_types(new_element_type) + assert len(occurrences) == 2 + # Assign IfcRoof instead of IfcBuildingElementProxy. + assert all(o.is_a("IfcRoof") for o in occurrences) + + # original clases are gone + assert len(self.file.by_type("IfcWall")) == 0 + assert len(self.file.by_type("IfcWallType")) == 0 From a9f7d4ab3b5732217e934b0ad4b266750772cb59 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 11 Mar 2025 18:20:12 +0500 Subject: [PATCH 295/476] get_applicable_types in ifc2x3 to prioritize IfcBuildingElementProxyType for occurrence classes without special type. E.g. previously: you select IfcSlab and change it to IfcRoof - since there is not IfcRoofType in IFC2X3 it would figure the matching product type is IfcBeamType and change this slab's type element to IfcBeamType. IfcBuildingElementProxyType seems more generic and fitting. --- .../ifcopenshell/api/root/reassign_class.py | 4 ++++ src/ifcopenshell-python/ifcopenshell/util/type.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py index a4f4f09b93..0f36891a28 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/reassign_class.py @@ -183,6 +183,10 @@ class Usecase: if self.occurrence_class: ifc_class_ = self.occurrence_class else: + # NOTE: in theory we can skip reassignment in IFC2X3 in some cases + # e.g. if occurrence is IfcRoof and we're reassigning to IfcBuildingElementProxyType + # but currently type_to_entity_map doesn't completely match entity_to_type_map, + # see type.py for more details. ifc_class_ = next(iter(ifcopenshell.util.type.get_applicable_entities(ifc_class, self.file.schema))) self.reassign_class(occurrence, ifc_class_, predefined_type) else: diff --git a/src/ifcopenshell-python/ifcopenshell/util/type.py b/src/ifcopenshell-python/ifcopenshell/util/type.py index 8ff0fe1a3c..ab58814267 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/type.py +++ b/src/ifcopenshell-python/ifcopenshell/util/type.py @@ -43,7 +43,18 @@ for schema in mapped_schemas: type_to_entity_map[schema].setdefault(element_type, []).append(element) if schema == "IFC2X3": + # Prioritize IfcBuildingElementProxyType if it's available as it seems to be the most generic type. + # Otherwise classes that don't have a special type in IFC2X3 (e.g. IfcBuildingElementPart, IfcRoof) + # have IfcBeamType as their first matching type, which can be confusing. + for occurrence_type, element_types in entity_to_type_map[schema].items(): + if "IfcBuildingElementProxyType" in element_types: + element_types.sort(key=lambda x: x == "IfcBuildingElementProxyType", reverse=True) + # There is no official mapping for IFC2X3 but this method gets us something that looks correct + # + # NOTE: currently `type_to_entity_map` in IFC2X3 doesn't completely match `entity_to_type_map`, + # e.g. `get_applicabl_types(IfcRoof)` returns `[IfcBuildingElementProxyType, IfcBeamType, ...]` + # but `get_applicable_entities(IfcBuildingElementProxyType)` returns `[IfcBuildingElementProxy`]. for element_type, elements in type_to_entity_map[schema].items(): # need to take both Type (4 symbols) and Style (5 symbols) into account guessed_element = element_type[:-5] if element_type.endswith("Style") else element_type[:-4] From 6bd8636c92a18c2ded152a415a4319e7a8e38125 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 11 Mar 2025 18:46:35 +0500 Subject: [PATCH 296/476] Revert "Bump IOS" This reverts commit c1bc36ab869e6208485bdb2fdc176d9ba297114d. --- src/bonsai/Makefile | 2 +- src/ifcopenshell-python/Makefile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile index a5000795c2..90867ff0ec 100644 --- a/src/bonsai/Makefile +++ b/src/bonsai/Makefile @@ -85,7 +85,7 @@ BLENDER_PLATFORM:=windows-x64 endif # Current build commit hash. -OLD:=cfb7d02 +OLD:=c49ca69 .PHONY: bump bump: ifndef NEW diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile index a6084ea62f..6e99b5d6d9 100644 --- a/src/ifcopenshell-python/Makefile +++ b/src/ifcopenshell-python/Makefile @@ -54,8 +54,8 @@ ifeq ($(PLATFORM), win64) PLATFORMTAG:=win_amd64 endif -IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.8.1-cfb7d02-$(PLATFORM).zip -IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.8.1-cfb7d02-$(PLATFORM).zip +IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.8.1-c49ca69-$(PLATFORM).zip +IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.8.1-c49ca69-$(PLATFORM).zip .PHONY: test test: From 7af672712f5413ee66ed39bc2fac4aa0f0c3d4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 11 Mar 2025 11:56:08 -0300 Subject: [PATCH 297/476] See #6164. Allow measure tool to work without IFC Project. --- .../bonsai/bim/module/drawing/helper.py | 30 +++++++---- .../bonsai/bim/module/model/decorator.py | 5 +- .../bonsai/bim/module/model/polyline.py | 2 +- src/bonsai/bonsai/tool/blender.py | 13 +++++ src/bonsai/bonsai/tool/polyline.py | 53 +++++++++++++------ src/bonsai/bonsai/tool/raycast.py | 9 ++-- src/bonsai/bonsai/tool/snap.py | 10 +++- 7 files changed, 89 insertions(+), 33 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index a9333fb6a4..f782cdbb7a 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -131,18 +131,30 @@ def format_distance( suppress_zero_inches=False, in_unit_length=False, ): - s_code = "\u00b2" # Superscript two THIS IS LEGACY (but being kept for when Area Measurements are re-implimented) - - # Get Scene Unit Settings - scaleFactor = bpy.context.scene.unit_settings.scale_length + # Get Blender Scene Unit Settings + unit_scale = bpy.context.scene.unit_settings.scale_length unit_system = bpy.context.scene.unit_settings.system unit_length = bpy.context.scene.unit_settings.length_unit - if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"): - area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit) - else: - area_unit_symbol = "" + area_unit_symbol = " m2" if unit_system == "METRIC" else " ft2" + # Get IFC Unit Settings + if tool.Ifc.get(): + if length_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT"): + unit_system = "METRIC" if length_unit.Name == "METRE" else "IMPERIAL" + unit_length = length_unit.Name + if hasattr(length_unit, "Prefix") and length_unit.Prefix: + unit_length = length_unit.Prefix + length_unit.Name + string_conversion = { + "foot": "FEET", + "inch": "INCHES", + "METRE": "METERS", + "CENTIMETRE": "CENTIMETERS", + "MILLIMETRE": "MILLIMETERS", + } + unit_length = string_conversion[unit_length] + if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"): + area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit) - value *= scaleFactor + value *= unit_scale # Imperial Formatting if unit_system == "IMPERIAL": diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 264865cd3a..ee6df0f75e 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -637,7 +637,10 @@ class PolylineDecorator: mouse_point = [Vector((snap_prop.x, snap_prop.y, snap_prop.z))] # Plane Method or Default Container - default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + if tool.Ifc.get(): + default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + else: + default_container_elevation = 0.0 projection_point = [] if not self.tool_state: pass diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 502a467a88..a202faccce 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -558,7 +558,6 @@ class PolylineOperator: return context.space_data.type == "VIEW_3D" def __init__(self): - self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) self.mousemove_count = 0 self.action_count = 0 self.visible_objs = [] @@ -950,6 +949,7 @@ class PolylineOperator: elif getattr(props, offset_type) in {"INTERIOR", "TOP"}: self.offset = -thickness * direction + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) props.offset = self.offset / self.unit_scale tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 8ac081eb93..235b1a23d0 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -1629,3 +1629,16 @@ class Blender(bonsai.core.tool.Blender): old_history_size = tool.Ifc.get().history_size tool.Ifc.get().set_history_size(0) tool.Ifc.get().set_history_size(old_history_size) + + @classmethod + def get_unit_scale(cls): + unit_length = bpy.context.scene.unit_settings.length_unit + unit_scale = 1.0 + if unit_length == "CENTIMETERS": + unit_scale = 0.01 + if unit_length == "MILLIMETERS": + unit_scale = 0.001 + if unit_length == "FEET": + unit_scale = 0.3048 + + return unit_scale diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 471826b22e..86193ad29e 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -113,16 +113,24 @@ class Polyline(bonsai.core.tool.Polyline): cls, context: bpy.types.Context, input_ui: PolylineUI, tool_state: ToolState, should_round: bool = False ) -> None: - try: - polyline_data = context.scene.BIMPolylineProperties.insertion_polyline[0] + polyline_data = context.scene.BIMPolylineProperties.insertion_polyline + if len(polyline_data) > 0: + polyline_data = polyline_data[0] polyline_points = polyline_data.polyline_points - default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z - last_point_data = polyline_points[len(polyline_points) - 1] - except: + if len(polyline_points) > 0: + last_point_data = polyline_points[len(polyline_points) - 1] + else: + last_point_data = None + else: polyline_points = [] - default_container_elevation = 0 last_point_data = None + if tool.Ifc.get(): + default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + else: + default_container_elevation = 0 + + mouse_point = context.scene.BIMPolylineProperties.snap_mouse_point[0] if last_point_data: @@ -253,16 +261,23 @@ class Polyline(bonsai.core.tool.Polyline): @classmethod def calculate_x_y_and_z(cls, context: bpy.types.Context, input_ui: PolylineUI, tool_state: ToolState) -> None: - try: + polyline_data = context.scene.BIMPolylineProperties.insertion_polyline + if len(polyline_data) > 0: polyline_data = context.scene.BIMPolylineProperties.insertion_polyline[0] polyline_points = polyline_data.polyline_points - default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z - last_point_data = polyline_points[len(polyline_points) - 1] - last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) - except: + if len(polyline_points) > 0: + last_point_data = polyline_points[len(polyline_points) - 1] + last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) + else: + last_point = Vector((0, 0, 0)) + else: polyline_points = [] - default_container_elevation = 0 last_point = Vector((0, 0, 0)) + + if tool.Ifc.get(): + default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + else: + default_container_elevation = 0 snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0] snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) @@ -440,7 +455,10 @@ class Polyline(bonsai.core.tool.Polyline): return dimension * unit_scale try: - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + if tool.Ifc.get(): + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + else: + unit_scale = tool.Blender.get_unit_scale() if bpy.context.scene.unit_settings.system == "IMPERIAL": parser = Lark(grammar_imperial) else: @@ -461,14 +479,15 @@ class Polyline(bonsai.core.tool.Polyline): @classmethod def format_input_ui_units(cls, value: float, is_area: bool = False) -> str: - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + if tool.Ifc.get(): + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + else: + unit_scale = tool.Blender.get_unit_scale() if bpy.context.scene.unit_settings.system == "IMPERIAL": dprops = tool.Drawing.get_document_props() precision = dprops.imperial_precision if is_area: - props = tool.Blender.get_bim_props() - area_unit = props.area_unit - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), unit_type=area_unit) + unit_scale = 1 else: precision = None diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index b0c4abd4ec..9dbcc57ad9 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -155,7 +155,8 @@ class Raycast(bonsai.core.tool.Raycast): loc = Vector((0, 0, 0)) # For empty object we just get the object location and return - if obj.type == "EMPTY": + + if obj and obj.type == "EMPTY": v = obj.location intersection = tool.Cad.point_on_edge(v, (ray_target, loc)) intersection = tool.Cad.point_on_edge(v, (ray_target, loc)) @@ -168,7 +169,6 @@ class Raycast(bonsai.core.tool.Raycast): "distance": distance, } points.append(snap_point) - print("empty", snap_point) return points if not custom_bmesh: @@ -292,7 +292,10 @@ class Raycast(bonsai.core.tool.Raycast): mouse_pos = event.mouse_region_x, event.mouse_region_y ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event) - default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + if tool.Ifc.get(): + default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + else: + default_container_elevation = 0.0 intersection = Vector((0, 0, default_container_elevation)) try: loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 2d2b2ae08b..c20ff83b3a 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -53,7 +53,10 @@ class Snap(bonsai.core.tool.Snap): distances = [3, 5, 15, 30] unit_system = tool.Drawing.get_unit_system() - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + if tool.Ifc.get(): + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + else: + unit_scale = tool.Blender.get_unit_scale() if unit_system == "IMPERIAL": factor = unit_scale fractions = [24, 12, 6, 2] @@ -452,7 +455,10 @@ class Snap(bonsai.core.tool.Snap): detected_snaps.append(snap_point) # Axis and Plane - elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + if tool.Ifc.get(): + elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + else: + elevation = 0.0 plane_origin, plane_normal = select_plane_method() tool_state.plane_origin = plane_origin # This will be used along with plane method From 19c103c627ccd868f01cbb7c20f765ba4fec71a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 10 Mar 2025 14:04:57 -0300 Subject: [PATCH 298/476] Fix issue where "C" to close polyline where not updating measurement dimension. --- src/bonsai/bonsai/bim/module/model/polyline.py | 18 +++++++++++++++++- src/bonsai/bonsai/tool/polyline.py | 16 ---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index a202faccce..1ede1890d3 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -803,7 +803,23 @@ class PolylineOperator: tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "C": - tool.Polyline.close_polyline() + # Get the first point coordinates to close the polyline + polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline + polyline_points = polyline_data[0].polyline_points if polyline_data else [] + if len(polyline_points) > 2: + first_point = polyline_points[0] + last_point = polyline_points[-1] + if not (first_point.x == last_point.x and first_point.y == last_point.y and first_point.z == last_point.z): + self.input_ui.set_value("X", first_point.x) + self.input_ui.set_value("Y", first_point.y) + if self.input_ui.get_number_value("Z") is not None: + self.input_ui.set_value("Z", first_point.z) + else: + self.input_ui.set_value("Z", 0) + result = tool.Polyline.insert_polyline_point(self.input_ui, self.tool_state) + if result: + self.report({"WARNING"}, result) + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 86193ad29e..61766f4a64 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -568,22 +568,6 @@ class Polyline(bonsai.core.tool.Polyline): total_length = tool.Polyline.format_input_ui_units(total_length) polyline_data.total_length = total_length - @classmethod - def close_polyline(cls) -> None: - polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline - polyline_points = polyline_data[0].polyline_points if polyline_data else [] - if len(polyline_points) > 2: - first_point = polyline_points[0] - last_point = polyline_points[-1] - if not (first_point.x == last_point.x and first_point.y == last_point.y and first_point.z == last_point.z): - polyline_point = polyline_points.add() - polyline_point.x = first_point.x - polyline_point.y = first_point.y - polyline_point.z = first_point.z - polyline_point.dim = first_point.dim - polyline_point.angle = first_point.angle - polyline_point.position = first_point.position - @classmethod def clear_polyline(cls) -> None: bpy.context.scene.BIMPolylineProperties.insertion_polyline.clear() From aa478ab359517e8a8db8d86f806568b2fd13931c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 10 Mar 2025 14:20:39 -0300 Subject: [PATCH 299/476] Fix area measure tool to display first dimension correctly. --- src/bonsai/bonsai/bim/module/project/decorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py index 1f738dbc90..0adb1a4c4e 100644 --- a/src/bonsai/bonsai/bim/module/project/decorator.py +++ b/src/bonsai/bonsai/bim/module/project/decorator.py @@ -323,7 +323,7 @@ class MeasureDecorator: all_positions = [] for i in range(len(polyline_points)): - if i < 2 and measure_type == "AREA": + if i < 1 and measure_type == "AREA": continue if i == 0: continue From db1d8d938f3d15a6d94e71ee386bf6199294e5fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 11 Mar 2025 11:49:11 -0300 Subject: [PATCH 300/476] Allow dimensions to use custom units from `BBIM_Dimension` Pset. - Implementation of @theoryshaw PR #5922. - Note: `IfcPropertyEnumeration` allows multiple choices, but this is not applicable for this use case. Users must manually deselect other options; otherwise, it defaults to the first selected option. --- .../bim/data/pset/Psets_BBIM_Annotation.ifc | 4 +- src/bonsai/bonsai/bim/module/drawing/data.py | 3 ++ .../bonsai/bim/module/drawing/decoration.py | 8 ++-- .../bonsai/bim/module/drawing/helper.py | 47 +++++++++++++++++-- .../bonsai/bim/module/drawing/svgwriter.py | 6 ++- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc b/src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc index 37278587a9..dd4851f9c5 100644 --- a/src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc +++ b/src/bonsai/bonsai/bim/data/pset/Psets_BBIM_Annotation.ifc @@ -28,11 +28,13 @@ DATA; #21=IFCSIMPLEPROPERTYTEMPLATE('1UDakJ5_f7kBhggNSW4$h5',$,'SymbolsPath','Default symbols SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #22=IFCSIMPLEPROPERTYTEMPLATE('0d53LEtgLDQxnv__NfgH7i',$,'PatternsPath','Default patterns SVG',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #23=IFCSIMPLEPROPERTYTEMPLATE('26qFNMv7nCHgU6Jd7Anga5',$,'ShadingStylesPath','Default shading styles',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); -#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcTypeProduct',(#25,#26,#27,#28)); +#24=IFCPROPERTYSETTEMPLATE('0I9merLinF5Ap$aZwaclgm',$,'BBIM_Dimension','',.PSET_TYPEDRIVENOVERRIDE.,'IfcAnnotation/DIMENSION,IfcAnnotation/RADIUS,IfcAnnotation/DIAMETER,IfcTypeProduct',(#25,#26,#27,#28,#30)); #25=IFCSIMPLEPROPERTYTEMPLATE('1rL2AbQsXD8RbpoWH5pYOV',$,'ShowDescriptionOnly','Hide the measurement values and show only annotation description',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); #26=IFCSIMPLEPROPERTYTEMPLATE('0SVyOfB0rC2xNfdRYf3XvY',$,'SuppressZeroInches','Suppress 0 inch values in dimension annotation text (for example: 12'' - 0" -> 12'')',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.); #27=IFCSIMPLEPROPERTYTEMPLATE('2bUmj458PBqPAtUoI3MXsb',$,'TextPrefix','Text to add before annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #28=IFCSIMPLEPROPERTYTEMPLATE('0bnzttUb9BPuN597uNTXOE',$,'TextSuffix','Text to add after annotation measurement value',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); #29=IFCSIMPLEPROPERTYTEMPLATE('2pJmUDpB50VBdCOib1zcJJ',$,'Newline_At','',.P_SINGLEVALUE.,'IfcInteger',$,$,$,$,$,.READWRITE.); +#30=IFCSIMPLEPROPERTYTEMPLATE('2TJn72t_v2cvBUG916Dpev',$,'CustomUnit','Dimension''s custom unit',.P_ENUMERATEDVALUE.,'IfcText',$,#31,$,$,$,.READWRITE.); +#31=IFCPROPERTYENUMERATION('CustomUnit',(IFCTEXT('Feet and Inches - Fractional'),IFCTEXT('Feet - Decimal'),IFCTEXT('Inches - Fractional'),IFCTEXT('Inches - Decimal'),IFCTEXT('Meters'),IFCTEXT('Decimeters'),IFCTEXT('Centimeters'),IFCTEXT('Millimeters')),$); ENDSEC; END-ISO-10303-21; diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index 30fda54828..579677cb98 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -386,6 +386,8 @@ class DecoratorData: suppress_zero_inches = pset_data.get("SuppressZeroInches", False) text_prefix = pset_data.get("TextPrefix", None) or "" text_suffix = pset_data.get("TextSuffix", None) or "" + custom_unit_list = pset_data.get("CustomUnit", None) or "" + custom_unit = custom_unit_list[0] if custom_unit_list else "" dimension_data = { "dimension_style": dimension_style, @@ -394,6 +396,7 @@ class DecoratorData: "text_prefix": text_prefix, "text_suffix": text_suffix, "fill_bg": fill_bg, + "custom_unit": custom_unit, } cls.data[obj.name] = dimension_data return dimension_data diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 9c26c2b1c5..df68e7e8cd 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -495,14 +495,14 @@ class BaseDecorator: self.draw_label(context, text=text, line_no=line_number_start, multiline=True, **draw_label_kwargs) @lru_cache(maxsize=None) - def format_value(self, context, value): + def format_value(self, context, value, custom_unit=None): drawing_pset_data = DrawingsData.data["active_drawing_pset_data"] precision = drawing_pset_data.get("MetricPrecision", None) if not precision: precision = drawing_pset_data.get("ImperialPrecision", None) decimal_places = drawing_pset_data.get("DecimalPlaces", None) - return format_distance(value, precision=precision, decimal_places=decimal_places) + return format_distance(value, precision=precision, decimal_places=decimal_places, suppress_zero_inches=True, custom_unit=custom_unit) def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None: """`pos` is a world space position\n @@ -737,7 +737,7 @@ class DimensionDecorator(BaseDecorator): if not show_description_only: length = (v1 - v0).length - text = self.format_value(context, length) + text = self.format_value(context, length, custom_unit=dimension_data["custom_unit"]) if isinstance(self, DiameterDecorator): text = "D" + text text = text_prefix + text + text_suffix @@ -946,7 +946,7 @@ class RadiusDecorator(BaseDecorator): def get_text(): length = (spline_points[-1] - spline_points[-2]).length - return "R" + self.format_value(context, length) + return "R" + self.format_value(context, length, custom_unit=dimension_data["custom_unit"]) self.draw_dimension_text( context, get_text, description, dimension_data, pos=pos, text_dir=Vector((1, 0)), box_alignment="center" diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index f782cdbb7a..6a53de6c9f 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -130,30 +130,52 @@ def format_distance( decimal_places=None, suppress_zero_inches=False, in_unit_length=False, + custom_unit=None, ): # Get Blender Scene Unit Settings unit_scale = bpy.context.scene.unit_settings.scale_length unit_system = bpy.context.scene.unit_settings.system unit_length = bpy.context.scene.unit_settings.length_unit area_unit_symbol = " m2" if unit_system == "METRIC" else " ft2" + # Get IFC Unit Settings if tool.Ifc.get(): + unit_scale = 1 if length_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT"): unit_system = "METRIC" if length_unit.Name == "METRE" else "IMPERIAL" unit_length = length_unit.Name if hasattr(length_unit, "Prefix") and length_unit.Prefix: unit_length = length_unit.Prefix + length_unit.Name - string_conversion = { + unit_length_mapping = { "foot": "FEET", "inch": "INCHES", "METRE": "METERS", + "DECIMETRE": "DECIMETERS", "CENTIMETRE": "CENTIMETERS", "MILLIMETRE": "MILLIMETERS", } - unit_length = string_conversion[unit_length] + unit_length = unit_length_mapping[unit_length] + # For now we only format area in IFC Units if area_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "AREAUNIT"): area_unit_symbol = " " + ifcopenshell.util.unit.get_unit_symbol(area_unit) + unit_fraction = True if unit_system == "IMPERIAL" else False + + # Custom Unit Settings + if custom_unit: + unit_mapping = { + "Feet and Inches - Fractional": ("IMPERIAL", "FEET", True), + "Feet - Decimal": ("IMPERIAL", "FEET", False), + "Inches - Fractional": ("IMPERIAL", "INCHES", True), + "Inches - Decimal": ("IMPERIAL", "INCHES", False), + "Meters": ("METRIC", "METERS", False), + "Decimeters": ("METRIC", "DECIMETERS", False), + "Centimeters": ("METRIC", "CENTIMETERS", False), + "Millimeters": ("METRIC", "MILLIMETERS", False), + } + if custom_unit in unit_mapping: + unit_system, unit_length, unit_fraction = unit_mapping[custom_unit] + value *= unit_scale # Imperial Formatting @@ -181,8 +203,11 @@ def format_distance( decInches = value * toInches # Separate ft and inches - # Unless Inches are the specified Length Unit - if unit_length != "INCHES": + # Unless Inches are the specified Length Unit or unit_fraction is False + if (unit_length == "FEET" and not unit_fraction): + feet = round(decInches / inPerFoot, 3) # keep decimal + decInches = 0 + elif unit_length != "INCHES": feet = int(decInches / inPerFoot) # remove decimal decInches -= feet * inPerFoot else: @@ -217,6 +242,10 @@ def format_distance( feet += 1 inches = 0 + # Check whether decimal or fractional + if not unit_fraction: + inches = round(decInches, 3) + frac = None if not isArea: add_inches = bool(inches) or not suppress_zero_inches or (inches == 0 and frac) tx_dist = "" @@ -265,6 +294,8 @@ def format_distance( # METRIC FORMATTING elif unit_system == "METRIC": if in_unit_length: + if unit_length == "DECIMETERS": + value = value / 10 if unit_length == "CENTIMETERS": value = value / 100 if unit_length == "MILLIMETERS": @@ -283,6 +314,14 @@ def format_distance( if hide_units is False: fmt += " m" tx_dist = fmt % value + # Decimeters + elif unit_length == "DECIMETERS": + if decimal_places is None: + fmt = "%1.1f" + if hide_units is False: + fmt += " dm" + d_dm = value * (10) + tx_dist = fmt % d_dm # Centimeters elif unit_length == "CENTIMETERS": if decimal_places is None: diff --git a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py index 9a249450f7..ba9f69664e 100644 --- a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py +++ b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py @@ -1128,7 +1128,7 @@ class SvgWriter: def get_text(): radius = (points[-1].co - points[-2].co).length - radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places) + radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places, custom_unit=dimension_data["custom_unit"]) text = f"R{radius}" return text @@ -1256,6 +1256,7 @@ class SvgWriter: text_prefix=dimension_data["text_prefix"], text_suffix=dimension_data["text_suffix"], fill_bg=dimension_data["fill_bg"], + custom_unit=dimension_data["custom_unit"] ) def draw_dimension_annotations(self, obj): @@ -1280,6 +1281,7 @@ class SvgWriter: text_prefix=dimension_data["text_prefix"], text_suffix=dimension_data["text_suffix"], fill_bg=dimension_data["fill_bg"], + custom_unit=dimension_data["custom_unit"], ) def draw_measureit_arch_dimension_annotations(self): @@ -1306,6 +1308,7 @@ class SvgWriter: text_prefix="", text_suffix="", fill_bg=False, + custom_unit=None, ): offset = Vector([self.raw_width, self.raw_height]) / 2 v0 = self.project_point_onto_camera(v0_global) @@ -1339,6 +1342,7 @@ class SvgWriter: precision=self.precision, decimal_places=self.decimal_places, suppress_zero_inches=suppress_zero_inches, + custom_unit=custom_unit, ) text = text_prefix + str(dimension) + text_suffix else: From 031540e982337b82eab5d8d5f50402cb78796feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 11 Mar 2025 12:37:47 -0300 Subject: [PATCH 301/476] Add #5496. Objects from linked ifc files can now be snapped. --- src/bonsai/bonsai/bim/module/model/polyline.py | 3 +++ src/bonsai/bonsai/tool/snap.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 1ede1890d3..a7d968c682 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -984,9 +984,12 @@ class PolylineOperator: self.tool_state.plane_method = None self.tool_state.mode = "Mouse" self.visible_objs = tool.Raycast.get_visible_objects(context) + # print(self.visible_objs) for obj in self.visible_objs: self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) + print(self.objs_2d_bbox) detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) + print("detected_snaps", detected_snaps) self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index c20ff83b3a..e99f99b929 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -376,7 +376,7 @@ class Snap(bonsai.core.tool.Snap): if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d, offset): if obj.visible_in_viewport_get( context.space_data - ): # Check for local view and local collections for this viewport and object + ) or obj.library: # Check for local view and local collections for this viewport and object objs_to_raycast.append(obj) # Polyline From a32735ac6a52a0d3503e254598e1907af3291dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 11 Mar 2025 14:04:34 -0300 Subject: [PATCH 302/476] Small fix after 7af672712f5413ee66ed39bc2fac4aa0f0c3d4a0 --- src/bonsai/bonsai/tool/snap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index e99f99b929..f7f49d2307 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -161,7 +161,10 @@ class Snap(bonsai.core.tool.Snap): # We multiply by the increment snap which is based on the viewport zoom snap_threshold = 1 * cls.get_increment_snap_value(bpy.context) - default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + if tool.Ifc.get(): + default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + else: + default_container_elevation = 0.0 polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline polyline_points = polyline_data[0].polyline_points if polyline_data else [] if polyline_points: From 41c1c6e8fa985ee2d970e05a21086be0b89eaf4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 11 Mar 2025 16:11:47 -0300 Subject: [PATCH 303/476] Fix #6291. When changing slab `x_angle` the rotation matrix now takes into account object's world matrix. --- src/bonsai/bonsai/bim/module/model/wall.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index f5d69c0415..927aa571ec 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -304,6 +304,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): # Object rotation rot_mat = mathutils.Matrix.Rotation(x_angle, 4, "X") + rot_mat = obj.matrix_world @ rot_mat obj.rotation_euler = rot_mat.to_euler() if layer2_objs: From 72bbf244f2f1ca5a56ae285a345097ad02b3eada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 11 Mar 2025 16:33:40 -0300 Subject: [PATCH 304/476] Fix issue #6221 where `crtl+shift+D` were not moving the whole copied aggregate. --- src/bonsai/bonsai/bim/module/geometry/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index 79f2ceeb67..14f201e2f1 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -118,6 +118,7 @@ def register(): operator.OverrideDuplicateMoveLinkedMacro.define("BIM_OT_override_object_duplicate_move_linked") operator.OverrideDuplicateMoveLinkedMacro.define("TRANSFORM_OT_translate") operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_object_duplicate_move_linked_aggregate") + operator.DuplicateMoveLinkedAggregateMacro.define("BIM_OT_override_move") operator.DuplicateMoveLinkedAggregateMacro.define("TRANSFORM_OT_translate") operator.OverrideMoveMacro.define("BIM_OT_override_move") operator.OverrideMoveMacro.define("TRANSFORM_OT_translate") From 6e0e19d69003d85bb5b45969306891ca5db688d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 11 Mar 2025 16:58:08 -0300 Subject: [PATCH 305/476] See #6130. Fix issue where aggregate mode where not being disabled. The issue was caused by deleting an object outside the aggregate while in aggregate mode. The selection for these objects are now disabled to prevent this issue --- src/bonsai/bonsai/bim/module/aggregate/prop.py | 1 + src/bonsai/bonsai/tool/aggregate.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/aggregate/prop.py b/src/bonsai/bonsai/bim/module/aggregate/prop.py index f49c0d3dc5..1d524b3645 100644 --- a/src/bonsai/bonsai/bim/module/aggregate/prop.py +++ b/src/bonsai/bonsai/bim/module/aggregate/prop.py @@ -102,6 +102,7 @@ class BIMObjectAggregateProperties(PropertyGroup): class Objects(bpy.types.PropertyGroup): obj: PointerProperty(type=bpy.types.Object) previous_display_type: bpy.props.StringProperty(default="TEXTURED") + previous_hide_select: bpy.props.BoolProperty(default=False) class BIMAggregateProperties(PropertyGroup): diff --git a/src/bonsai/bonsai/tool/aggregate.py b/src/bonsai/bonsai/tool/aggregate.py index 4ad7a95425..d3c0589cea 100644 --- a/src/bonsai/bonsai/tool/aggregate.py +++ b/src/bonsai/bonsai/tool/aggregate.py @@ -118,7 +118,9 @@ class Aggregate(bonsai.core.tool.Aggregate): not_editing_obj = props.not_editing_objects.add() not_editing_obj.obj = obj.original not_editing_obj.previous_display_type = obj.original.display_type + not_editing_obj.previous_hide_select = obj.original.hide_select obj.original.display_type = "WIRE" + obj.hide_select = True else: editing_obj = props.editing_objects.add() editing_obj.obj = obj.original @@ -132,13 +134,15 @@ class Aggregate(bonsai.core.tool.Aggregate): props = context.scene.BIMAggregateProperties for obj_prop in props.not_editing_objects: obj = obj_prop.obj + if not obj: + continue obj.original.display_type = obj_prop.previous_display_type + obj.hide_select = obj_prop.previous_hide_select element = tool.Ifc.get_entity(obj) if not element: continue parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(props.editing_aggregate)) - objs = [tool.Ifc.get_object(part) for part in parts] if context.space_data.local_view: bpy.ops.view3d.localview() From a7a4198fc10a31377e27544470e1c34b8381e5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 11 Mar 2025 17:10:52 -0300 Subject: [PATCH 306/476] Polyline tool - add "dm" and "cm" to the input validation. --- src/bonsai/bonsai/tool/polyline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 61766f4a64..724a8c8b64 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -379,7 +379,7 @@ class Polyline(bonsai.core.tool.Polyline): FORMULA: "=" - metric: NUMBER "mm"? "m"? "°"? + metric: NUMBER "mm"? "cm"? "dm"? "m"? "°"? expr: (ADD | SUB | MUL | DIV) dim From f5a1c4aae315406e5511fab3ace0125c554cbd8d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 12 Mar 2025 18:05:51 +1100 Subject: [PATCH 307/476] See #1227. Reimplement extend walls to slab (or anything with negative Z faces) There are a few differences to the previous implementation: 1. It is no longer recalculated on wall regeneration. The new wall regeneration is strict to the spec on the rules of rel connects path, and so this being a "userdefined" connection we only calculate it explicitly when the user invokes the operator. 2. It uses meshes instead of clipping planes, so you can clip to strange shapes or gable roofs or whatever. Nice. --- .../bonsai/bim/module/model/workspace.py | 15 +++- src/bonsai/bonsai/core/model.py | 17 +++- src/bonsai/bonsai/core/tool.py | 15 ++-- src/bonsai/bonsai/tool/blender.py | 19 +++-- src/bonsai/bonsai/tool/debug.py | 10 +++ src/bonsai/bonsai/tool/geometry.py | 4 + src/bonsai/bonsai/tool/model.py | 84 ++++++++++++++++++- .../ifcopenshell/util/shape.py | 20 +++++ 8 files changed, 166 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 14beafaee7..fc35945aea 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1241,10 +1241,17 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): # Extend LAYER2s to LAYER3 [o.select_set(False) for o in selected_usages.get("PROFILE", [])] [o.select_set(False) for o in selected_usages.get("LAYER3", []) if o != bpy.context.active_object] - try: - core.join_walls_TZ(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model) - except core.RequireAtLeastTwoLayeredElements as e: - self.report({"ERROR"}, str(e)) + slab = None + walls = [] + if (obj := tool.Blender.get_active_object(is_selected=True)) and (element := tool.Ifc.get_entity(obj)) and tool.Model.get_usage_type(element) == "LAYER3": + slab = obj + for obj in tool.Blender.get_selected_objects(include_active=False): + if (element := tool.Ifc.get_entity(obj)) and tool.Model.get_usage_type(element) == "LAYER2": + walls.append(obj) + if slab and walls: + core.extend_wall_to_slab(tool.Ifc, tool.Geometry, tool.Model, slab, walls) + else: + self.report({"ERROR"}, "Please select at least one LAYER2 element and an active LAYER3 element") elif self.active_material_usage == "LAYER2": # Extend LAYER2s to LAYER2 diff --git a/src/bonsai/bonsai/core/model.py b/src/bonsai/bonsai/core/model.py index d04486ed1c..897b1af632 100644 --- a/src/bonsai/bonsai/core/model.py +++ b/src/bonsai/bonsai/core/model.py @@ -17,7 +17,7 @@ # along with Bonsai. If not, see . import bonsai.core.tool as tool -from typing import Literal +from typing import Literal, Iterable def unjoin_walls(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None: @@ -63,6 +63,21 @@ def join_walls_LV( joiner.connect(another_selected_object, active_obj) +def extend_wall_to_slab( + ifc: tool.Ifc, geometry: tool.Geometry, model: tool.Model, slab_obj, wall_objs: Iterable +) -> None: + if not (clip := model.get_slab_clipping_bmesh(slab_obj)): + return # Nothing to clip? + slab = ifc.get_entity(slab_obj) + for obj in wall_objs: + if ifc.is_moved(obj): + geometry.run_edit_object_placement(obj=obj) + wall = ifc.get_entity(obj) + model.clip_wall_to_slab(wall, clip) + model.connect_wall_to_slab(wall, slab) + model.reload_body_representation(wall_objs) + + def join_walls_TZ(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner, model: tool.Model) -> None: selected_objs = [ o diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index d96b019df2..4763ef7e8f 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -432,6 +432,7 @@ class Geometry: def rename_object(cls, obj, name): pass def replace_object_data_globally(cls, old_data, new_data): pass def resolve_mapped_representation(cls, representation): pass + def run_edit_object_placement(cls, obj=None): pass def run_geometry_update_representation(cls, obj=None): pass def run_style_add_style(cls, obj=None): pass def select_connection(cls, connection): pass @@ -562,22 +563,26 @@ class Misc: @interface class Model: + def clip_wall_to_slab(cls, element, bm): pass + def connect_wall_to_slab(cls, wall, slab): pass def convert_si_to_unit(cls, value): pass def convert_unit_to_si(cls, value): pass def export_points(cls, position, indices): pass def export_profile(cls, obj, position=None): pass def generate_occurrence_name(cls, element_type, ifc_class): pass def get_extrusion(cls, representation): pass - def import_profile(cls, profile, obj=None, position=None): pass + def get_manual_booleans(cls, element): pass + def get_material_layer_parameters(cls, element): pass + def get_slab_clipping_bmesh(cls, obj): pass + def get_usage_type(cls, element): pass + def get_wall_axis(cls, obj, layers=None): pass def import_curve(cls, curve, obj=None, position=None): pass + def import_profile(cls, profile, obj=None, position=None): pass def import_rectangle(cls, obj, position, profile): pass def load_openings(cls, openings): pass def purge_scene_openings(cls): pass - def get_usage_type(cls, element): pass - def get_material_layer_parameters(cls, element): pass - def get_manual_booleans(cls, element): pass - def get_wall_axis(cls, obj, layers=None): pass def regenerate_array(cls, parent, data): pass + def reload_body_representation(cls, obj_or_objects): pass def replace_object_ifc_representation(cls, ifc_file, ifc_context, obj, new_representation): pass diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 235b1a23d0..297b3eb4df 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -154,15 +154,22 @@ class Blender(bonsai.core.tool.Blender): @classmethod def get_active_object(cls, is_selected: bool = False) -> Union[bpy.types.Object, None]: - obj = getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active - if not is_selected: - return obj - if obj in cls.get_selected_objects(include_active=False): - return obj + """Gets the active object + + :param is_selected: If true, the active object also needs to be selected. + """ + if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active): + if not is_selected: + return obj + if obj.select_get(): + return obj @classmethod def get_selected_objects(cls, include_active: bool = True) -> set[bpy.types.Object]: - """Get selected objects including active object.""" + """Get selected objects + + :param include_active: If true, the active object is included regardless if it is also selected. + """ if selected_objects := getattr(bpy.context, "selected_objects", None): if include_active and (active_obj := cls.get_active_object()): return set(selected_objects + [active_obj]) diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index b04280e1c8..bda7f3c682 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -63,6 +63,16 @@ class Debug(bonsai.core.tool.Debug): except PermissionError: pass + @classmethod + def debug_bmesh( + cls, bm: bpy.types.BMesh, name: str = "Debug" + ) -> bpy.types.Object: + mesh = bpy.data.meshes.new("Debug") + bm.to_mesh(mesh) + obj = bpy.data.objects.new(name, mesh) + bpy.context.scene.collection.objects.link(obj) + return obj + @classmethod def debug_geometry( cls, verts: list[Vector] = [], edges: list[tuple[int, int]] = [], name: str = "Debug" diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 840466f62d..81cffcece3 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1986,3 +1986,7 @@ class Geometry(bonsai.core.tool.Geometry): bm = tool.Blender.get_bmesh_for_mesh(obj.data) bm.transform(obj.matrix_world) return BVHTree.FromBMesh(bm) + + @classmethod + def run_edit_object_placement(cls, obj: bpy.types.Object) -> None: + return bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index ecccf21fe2..358456b05a 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -39,7 +39,7 @@ import bonsai.core.geometry import bonsai.core.tool import bonsai.tool as tool import bonsai.core.geometry as geometry -from math import atan, cos, degrees, radians, pi +from math import atan, cos, degrees, pi, inf from mathutils import Matrix, Vector from copy import deepcopy from functools import partial @@ -284,7 +284,7 @@ class Model(bonsai.core.tool.Model): @classmethod def get_extrusion(cls, representation: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]: - """return first found IfcExtrudedAreaSolid""" + """Return first found IfcExtrudedAreaSolid""" item = representation.Items[0] while True: if item.is_a("IfcExtrudedAreaSolid"): @@ -2101,3 +2101,83 @@ class Model(bonsai.core.tool.Model): return op = layout.operator("bim.material_ui_select", icon="ZOOM_SELECTED", text="") op.material_id = material_id_int + + @classmethod + def get_slab_clipping_bmesh(cls, obj: bpy.types.Object) -> bpy.types.BMesh | None: + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + bm = bmesh.new() + bm.from_mesh(obj.data) + bm.faces.ensure_lookup_table() + + clipping_bm = bmesh.new() + vertex_map = {} + + for face in bm.faces: + face.normal_update() + normal = face.normal.to_4d() + normal.w = 0 + if (obj.matrix_world @ normal).z >= 0: + continue + new_verts = [] + for vert in face.verts: + if not (new_vert := vertex_map.get(vert.index, None)): + new_vert = clipping_bm.verts.new(obj.matrix_world @ vert.co / unit_scale) + vertex_map[vert.index] = new_vert + new_verts.append(new_vert) + clipping_bm.faces.new(new_verts) + + if not len(clipping_bm.faces): + return + + return clipping_bm # clipping_bm is in project units + + @classmethod + def clip_wall_to_slab(cls, wall: ifcopenshell.entity_instance, clipping_bm: bpy.types.BMesh) -> None: + matrix_i = np.linalg.inv(ifcopenshell.util.placement.get_local_placement(wall.ObjectPlacement)) + bm = clipping_bm.copy() + bmesh.ops.transform(bm, matrix=Matrix(matrix_i.tolist()), verts=bm.verts) + + bm.verts.ensure_lookup_table() + zs = [v.co.z for v in bm.verts] + min_z = min(zs) + max_z = max(zs) + + operand = None + if (z := max_z - min_z) and not np.isclose(z, 0.0): + builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get()) + + result = bmesh.ops.extrude_face_region(bm, geom=bm.faces) + extruded_verts = [elem for elem in result["geom"] if isinstance(elem, bmesh.types.BMVert)] + bmesh.ops.translate(bm, verts=extruded_verts, vec=(0, 0, z)) + + verts = [v.co for v in bm.verts] + faces = [[v.index for v in p.verts] for p in bm.faces] + operand = builder.mesh(verts, faces) + + for extrusion in ifcopenshell.util.shape.get_base_extrusions(wall) or []: + if extrusion.Position: + position = ifcopenshell.util.placement.get_axis2placement(extrusion.Position) + else: + position = np.eye(4) + + direction = np.array(extrusion.ExtrudedDirection[0]) + direction /= np.linalg.norm(direction) + direction = position @ np.append(direction, 0.0) + + if direction[2] <= 0 or position[2][3] > max_z: + continue + + extrusion.Depth = max_z / direction[2] + + if operand: + booleans = ifcopenshell.api.geometry.add_boolean( + tool.Ifc.get(), first_item=extrusion, second_items=[operand] + ) + tool.Model.mark_manual_booleans(wall, booleans) + + @classmethod + def connect_wall_to_slab(cls, wall: ifcopenshell.entity_instance, slab: ifcopenshell.entity_instance) -> None: + ifcopenshell.api.geometry.connect_element( + tool.Ifc.get(), relating_element=slab, related_element=wall, description="TOP" + ) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py index c4a3cfe406..0948f9e1f6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py @@ -721,6 +721,26 @@ def get_extrusions(element: ifcopenshell.entity_instance) -> Union[list[ifcopens return extrusions +def get_base_extrusions(element: ifcopenshell.entity_instance) -> Union[list[ifcopenshell.entity_instance], None]: + """Gets all base extrusions used to define an element's model body geometry + + A base extrusion is assumed to be an extrusion prior to all boolean + results. + + :param element: The element occurrence + :return: A list of extrusion representation items or `None` if element has no representation. + """ + if not (rep := ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")): + return + extrusions = [] + for item in ifcopenshell.util.representation.resolve_representation(rep).Items: + while item.is_a("IfcBooleanResult"): + item = item.FirstOperand + if item.is_a("IfcExtrudedAreaSolid"): + extrusions.append(item) + return extrusions + + def get_total_edge_length(geometry: ShapeType) -> float: """Calculates the total length of edges in a given geometry. From 226736eb4113ddf82348e67c4413d30696620874 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 12 Mar 2025 18:41:18 +1100 Subject: [PATCH 308/476] Fix #3357. Remove Shift-E for parametric objects (superseded by tab) and you can now extend to anything not just slabs. This Shift-E still needs more polish as we figure out the best hotkeys and what's most natural to users when they select different combinations of objects, so expect the conditionals that govern when Shift-E does things to still change in the future. --- .../bonsai/bim/module/model/__init__.py | 3 +- src/bonsai/bonsai/bim/module/model/wall.py | 22 +++++++- .../bonsai/bim/module/model/workspace.py | 54 ++++--------------- 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 32573c6856..7eed142ee2 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -66,12 +66,13 @@ classes = ( product.SetActiveType, workspace.Hotkey, workspace.BIM_MT_add_representation_item, + wall.AddWallsFromSlab, wall.AlignWall, wall.ChangeExtrusionDepth, wall.ChangeExtrusionXAngle, wall.ChangeLayerLength, - wall.AddWallsFromSlab, wall.DrawPolylineWall, + wall.ExtendWallsToUnderside, wall.FlipWall, wall.MergeWall, wall.RecalculateWall, diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 927aa571ec..eef3487217 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -36,7 +36,7 @@ import bonsai.core.geometry import bonsai.core.model as core import bonsai.tool as tool from bonsai.bim.ifc import IfcStore -from math import pi, sin, cos, degrees, radians +from math import pi, sin, cos, degrees from mathutils import Vector, Matrix from bonsai.bim.module.model.opening import FilledOpeningGenerator from bonsai.bim.module.model.decorator import PolylineDecorator, ProductDecorator @@ -58,6 +58,26 @@ class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator): core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model) +class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.extend_walls_to_underside" + bl_label = "Extend Walls To Underside" + bl_description = "Extend and clip selected walls at the bottom faces of an object" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + slab = None + walls = [] + if (obj := tool.Blender.get_active_object(is_selected=True)) and (element := tool.Ifc.get_entity(obj)): + slab = obj + for obj in tool.Blender.get_selected_objects(include_active=False): + if (element := tool.Ifc.get_entity(obj)) and tool.Model.get_usage_type(element) == "LAYER2": + walls.append(obj) + if slab and walls: + core.extend_wall_to_slab(tool.Ifc, tool.Geometry, tool.Model, slab, walls) + else: + self.report({"ERROR"}, "Please select at least one LAYER2 element and an active element") + + class AlignWall(bpy.types.Operator): bl_idname = "bim.align_wall" bl_label = "Align Wall" diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index fc35945aea..21e1dce148 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -675,13 +675,12 @@ class CreateObjectUI: for _ in range(4): row2.operator("bim.launch_type_manager", text="", emboss=False) else: - op = box.operator( + box.operator( "bim.load_type_thumbnails", text="", icon="FILE_REFRESH", emboss=False, ) - op.ifc_class = ifc_class row = box.row(align=True) row.alignment = "CENTER" @@ -848,7 +847,7 @@ class EditObjectUI: elif AuthoringData.data["active_material_usage"] == "LAYER3": if "LAYER2" in AuthoringData.data["selected_material_usages"]: row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row - add_layout_hotkey_operator(cls.layout, "Extend Wall To Slab", "S_E", "", ui_context) + add_layout_hotkey_operator(cls.layout, "Extend To Underside", "S_E", "", ui_context) if AuthoringData.data["relating_type_data"].get("usage") == "LAYER2": row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator( @@ -907,6 +906,11 @@ class EditObjectUI: row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row add_layout_hotkey_operator(row, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context) + else: + if "LAYER2" in AuthoringData.data["selected_material_usages"]: + row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row + add_layout_hotkey_operator(cls.layout, "Extend To Undersideb", "S_E", "", ui_context) + if AuthoringData.data["is_flippable_element"]: cls.draw_flip(ui_context, row) @@ -1167,23 +1171,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): if not bpy.context.selected_objects or not (active_object := bpy.context.active_object): return - # NOTE: placing it before the other operations because railing can also be SweptSolid - # and it might conflict with one of the conditions below - if ( - tool.Model.is_parametric_railing_active() - and not tool.Model.get_railing_props(active_object).is_editing_path - ): - bpy.ops.bim.enable_editing_railing_path() - return - - elif tool.Model.is_parametric_roof_active() and not tool.Model.get_roof_props(active_object).is_editing_path: - # undo the unselection done above because roof has no usage type - bpy.ops.bim.enable_editing_roof_path() - return - - elif tool.Model.is_parametric_window_active() or tool.Model.is_parametric_door_active(): - return - selected_usages: dict[str, list[bpy.types.Object]] = {} for obj in bpy.context.selected_objects: element = tool.Ifc.get_entity(obj) @@ -1194,11 +1181,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): if not usage: representation = tool.Geometry.get_active_representation(obj) representation = tool.Geometry.resolve_mapped_representation(representation) - if representation and representation.RepresentationType == "SweptSolid": - usage = "SWEPTSOLID" - else: - obj.select_set(False) - continue selected_usages.setdefault(usage, []).append(obj) if len(bpy.context.selected_objects) == 1: @@ -1227,9 +1209,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): elif self.active_material_usage == "PROFILE": # Extend PROFILE to cursor bpy.ops.bim.extend_profile(join_type="T") - else: - # Edit SWEPTSOLID profile (assuming single profile for now) - bpy.ops.bim.enable_editing_extrusion_profile() elif self.active_material_usage == "LAYER2" and selected_usages.get("PROFILE", []): # Extend PROFILEs to LAYER2 @@ -1237,22 +1216,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): [o.select_set(False) for o in selected_usages.get("LAYER2", []) if o != bpy.context.active_object] bpy.ops.bim.extend_profile(join_type="T") - elif self.active_material_usage == "LAYER3" and selected_usages.get("LAYER2", []): - # Extend LAYER2s to LAYER3 - [o.select_set(False) for o in selected_usages.get("PROFILE", [])] - [o.select_set(False) for o in selected_usages.get("LAYER3", []) if o != bpy.context.active_object] - slab = None - walls = [] - if (obj := tool.Blender.get_active_object(is_selected=True)) and (element := tool.Ifc.get_entity(obj)) and tool.Model.get_usage_type(element) == "LAYER3": - slab = obj - for obj in tool.Blender.get_selected_objects(include_active=False): - if (element := tool.Ifc.get_entity(obj)) and tool.Model.get_usage_type(element) == "LAYER2": - walls.append(obj) - if slab and walls: - core.extend_wall_to_slab(tool.Ifc, tool.Geometry, tool.Model, slab, walls) - else: - self.report({"ERROR"}, "Please select at least one LAYER2 element and an active LAYER3 element") - elif self.active_material_usage == "LAYER2": # Extend LAYER2s to LAYER2 [o.select_set(False) for o in selected_usages.get("LAYER3", [])] @@ -1268,6 +1231,9 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): [o.select_set(False) for o in selected_usages.get("LAYER2", [])] bpy.ops.bim.extend_profile(join_type="T") + else: + bpy.ops.bim.extend_walls_to_underside() + def hotkey_S_F(self): if not bpy.context.selected_objects: return From a941eb3f903a5db3c733ac968dd429b8854c7b84 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 12 Mar 2025 18:47:50 +1100 Subject: [PATCH 309/476] Parametric roof geometry is now whitelisted for IfcSlab.ROOF and IfcCovering.ROOFING too. --- src/bonsai/bonsai/bim/module/model/roof.py | 10 ++++++++-- src/bonsai/bonsai/bim/module/root/data.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/roof.py b/src/bonsai/bonsai/bim/module/model/roof.py index 77058bca39..5f19800362 100644 --- a/src/bonsai/bonsai/bim/module/model/roof.py +++ b/src/bonsai/bonsai/bim/module/model/roof.py @@ -425,8 +425,14 @@ def update_roof_modifier_ifc_data(context: bpy.types.Context) -> None: return False # type attributes - if props.roof_type == "HIP/GABLE ROOF": - element.PredefinedType = "GABLE_ROOF" if roof_is_gabled() else "HIP_ROOF" + ifc_class = element.is_a() + if ifc_class in ("IfcRoof", "IfcRoofType"): + if props.roof_type == "HIP/GABLE ROOF": + element.PredefinedType = "GABLE_ROOF" if roof_is_gabled() else "HIP_ROOF" + elif ifc_class in ("IfcSlab", "IfcSlabType"): + element.PredefinedType = "ROOF" + elif ifc_class in ("IfcCoveringType", "IfcCoveringType"): + element.PredefinedType = "ROOFING" tool.Model.add_body_representation(obj) diff --git a/src/bonsai/bonsai/bim/module/root/data.py b/src/bonsai/bonsai/bim/module/root/data.py index 99b17220b5..4f6b234a19 100644 --- a/src/bonsai/bonsai/bim/module/root/data.py +++ b/src/bonsai/bonsai/bim/module/root/data.py @@ -170,7 +170,7 @@ class IfcClassData: templates.extend([None, ("STAIR", "Stair", "Parametric stair")]) elif ifc_class in ("IfcRailingType", "IfcRailing"): templates.extend([None, ("RAILING", "Railing", "Parametric railing")]) - elif ifc_class in ("IfcRoofType", "IfcRoof"): + elif ifc_class in ("IfcRoofType", "IfcRoof", "IfcSlabType", "IfcSlab", "IfcCovering", "IfcCoveringType"): templates.extend([None, ("ROOF", "Roof", "Parametric roof with a constant pitch")]) elif ifc_class and "Segment" in ifc_class: templates.extend( From dfd30fbcaa166455ec38fb7ad01f3f7050151df7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 12 Mar 2025 16:12:21 +0500 Subject: [PATCH 310/476] black . --- .../bonsai/bim/module/drawing/decoration.py | 8 +++++++- .../bonsai/bim/module/drawing/helper.py | 20 +++++++++---------- .../bonsai/bim/module/drawing/svgwriter.py | 9 +++++++-- .../bonsai/bim/module/model/polyline.py | 4 +++- src/bonsai/bonsai/tool/debug.py | 4 +--- src/bonsai/bonsai/tool/polyline.py | 3 +-- src/bonsai/bonsai/tool/snap.py | 6 +++--- 7 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index df68e7e8cd..962cd169f1 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -502,7 +502,13 @@ class BaseDecorator: precision = drawing_pset_data.get("ImperialPrecision", None) decimal_places = drawing_pset_data.get("DecimalPlaces", None) - return format_distance(value, precision=precision, decimal_places=decimal_places, suppress_zero_inches=True, custom_unit=custom_unit) + return format_distance( + value, + precision=precision, + decimal_places=decimal_places, + suppress_zero_inches=True, + custom_unit=custom_unit, + ) def draw_asterisk(self, context: bpy.types.Context, pos: Vector, rotation: float = 0.0, scale: float = 1.0) -> None: """`pos` is a world space position\n diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index 6a53de6c9f..ac0b4fbcd8 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -164,15 +164,15 @@ def format_distance( # Custom Unit Settings if custom_unit: unit_mapping = { - "Feet and Inches - Fractional": ("IMPERIAL", "FEET", True), - "Feet - Decimal": ("IMPERIAL", "FEET", False), - "Inches - Fractional": ("IMPERIAL", "INCHES", True), - "Inches - Decimal": ("IMPERIAL", "INCHES", False), - "Meters": ("METRIC", "METERS", False), - "Decimeters": ("METRIC", "DECIMETERS", False), - "Centimeters": ("METRIC", "CENTIMETERS", False), - "Millimeters": ("METRIC", "MILLIMETERS", False), - } + "Feet and Inches - Fractional": ("IMPERIAL", "FEET", True), + "Feet - Decimal": ("IMPERIAL", "FEET", False), + "Inches - Fractional": ("IMPERIAL", "INCHES", True), + "Inches - Decimal": ("IMPERIAL", "INCHES", False), + "Meters": ("METRIC", "METERS", False), + "Decimeters": ("METRIC", "DECIMETERS", False), + "Centimeters": ("METRIC", "CENTIMETERS", False), + "Millimeters": ("METRIC", "MILLIMETERS", False), + } if custom_unit in unit_mapping: unit_system, unit_length, unit_fraction = unit_mapping[custom_unit] @@ -204,7 +204,7 @@ def format_distance( # Separate ft and inches # Unless Inches are the specified Length Unit or unit_fraction is False - if (unit_length == "FEET" and not unit_fraction): + if unit_length == "FEET" and not unit_fraction: feet = round(decInches / inPerFoot, 3) # keep decimal decInches = 0 elif unit_length != "INCHES": diff --git a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py index ba9f69664e..0aaa66ef50 100644 --- a/src/bonsai/bonsai/bim/module/drawing/svgwriter.py +++ b/src/bonsai/bonsai/bim/module/drawing/svgwriter.py @@ -1128,7 +1128,12 @@ class SvgWriter: def get_text(): radius = (points[-1].co - points[-2].co).length - radius = helper.format_distance(radius, precision=self.precision, decimal_places=self.decimal_places, custom_unit=dimension_data["custom_unit"]) + radius = helper.format_distance( + radius, + precision=self.precision, + decimal_places=self.decimal_places, + custom_unit=dimension_data["custom_unit"], + ) text = f"R{radius}" return text @@ -1256,7 +1261,7 @@ class SvgWriter: text_prefix=dimension_data["text_prefix"], text_suffix=dimension_data["text_suffix"], fill_bg=dimension_data["fill_bg"], - custom_unit=dimension_data["custom_unit"] + custom_unit=dimension_data["custom_unit"], ) def draw_dimension_annotations(self, obj): diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index a7d968c682..e688c2a7fb 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -809,7 +809,9 @@ class PolylineOperator: if len(polyline_points) > 2: first_point = polyline_points[0] last_point = polyline_points[-1] - if not (first_point.x == last_point.x and first_point.y == last_point.y and first_point.z == last_point.z): + if not ( + first_point.x == last_point.x and first_point.y == last_point.y and first_point.z == last_point.z + ): self.input_ui.set_value("X", first_point.x) self.input_ui.set_value("Y", first_point.y) if self.input_ui.get_number_value("Z") is not None: diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index bda7f3c682..51008e04a4 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -64,9 +64,7 @@ class Debug(bonsai.core.tool.Debug): pass @classmethod - def debug_bmesh( - cls, bm: bpy.types.BMesh, name: str = "Debug" - ) -> bpy.types.Object: + def debug_bmesh(cls, bm: bpy.types.BMesh, name: str = "Debug") -> bpy.types.Object: mesh = bpy.data.meshes.new("Debug") bm.to_mesh(mesh) obj = bpy.data.objects.new(name, mesh) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 724a8c8b64..473ef10cd5 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -130,7 +130,6 @@ class Polyline(bonsai.core.tool.Polyline): else: default_container_elevation = 0 - mouse_point = context.scene.BIMPolylineProperties.snap_mouse_point[0] if last_point_data: @@ -273,7 +272,7 @@ class Polyline(bonsai.core.tool.Polyline): else: polyline_points = [] last_point = Vector((0, 0, 0)) - + if tool.Ifc.get(): default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z else: diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index f7f49d2307..32906f7f90 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -377,9 +377,9 @@ class Snap(bonsai.core.tool.Snap): for obj, bbox_2d in objs_2d_bbox: if obj.type in {"MESH", "EMPTY", "CURVE"} and bbox_2d: if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d, offset): - if obj.visible_in_viewport_get( - context.space_data - ) or obj.library: # Check for local view and local collections for this viewport and object + if ( + obj.visible_in_viewport_get(context.space_data) or obj.library + ): # Check for local view and local collections for this viewport and object objs_to_raycast.append(obj) # Polyline From e9a3d06352536753cfe6fb704660ecf012227588 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 12 Mar 2025 16:05:12 +0500 Subject: [PATCH 311/476] Document copy and paste workarounds in docs #6327 --- src/bonsai/docs/guides/troubleshooting.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/bonsai/docs/guides/troubleshooting.rst b/src/bonsai/docs/guides/troubleshooting.rst index 72b33691fd..15b52634db 100644 --- a/src/bonsai/docs/guides/troubleshooting.rst +++ b/src/bonsai/docs/guides/troubleshooting.rst @@ -131,6 +131,25 @@ incompatible features may result in data loss. objects that have been scaled in object mode will have their scale reset to 1, and scaling an object may result in unpredictable operations. Instead, scale objects within edit mode. +3. **Copy and Paste for IFC objects**. Copying and pasting objects preserving their IFC data + is not currently supported as pasting object may be unsafe: + object may come from a different Blender session or from current session but it's earlier state. + + If you copy and paste IFC object, you will find that pasted IFC object is unlinked to any IFC data + to keep it safe. + + Current workarounds: + + - copying IFC objects in current session instead use "IFC Duplicate Object" operator (:kbd:`Ctrl+D`). + + - copying IFC occurrences objects from other projects - link IFC project, + query element using Explore Tool and then append it from Links UI. + - copying any IFC objects to a new separate project - use IfcPatch with ExtractElements recipe. + + - copying IFC types from a different project use Project Library UI. + + WARNING. Manually using Blender's "Paste Objects" operator instead of "IFC Paste BIM Objects" to paste IFC objects + will have unpredictable results and will lead to data corruption. Where is the add-on installed? ------------------------------ From 9be0a60c0abb9b822c702f5575013356ea69a2ea Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 12 Mar 2025 16:29:10 +0500 Subject: [PATCH 312/476] github issue templates - use comment blocks for instructions As users sometimes tend not to remove the intsruction messages... --- .github/ISSUE_TEMPLATE/bug_report.md | 12 +++++++++++- .github/ISSUE_TEMPLATE/feature_request.md | 7 ++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 714c304f5a..8ba9fe7703 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,14 +7,24 @@ assignees: '' --- -Replace this text describing what problem occurred and what you expected to happen instead. +**Error Description and Steps to Reproduce** + + **Attachments** + + **Debug information** + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 42394b3486..18044de82c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,4 +7,9 @@ assignees: '' --- -Replace this text and describe a feature you'd like us to add. If it's not obvious, explain why this feature is awesome. Note that feature requests must be specific and measurable. + +**Feature Description** + + From acdc40fb4107581aff9a0c5badc834701214d0d3 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 12 Mar 2025 23:04:41 +1100 Subject: [PATCH 313/476] See #1227. Reimplement extending walls to another wall --- .../bonsai/bim/module/model/__init__.py | 1 + src/bonsai/bonsai/bim/module/model/wall.py | 40 +++++ .../bonsai/bim/module/model/workspace.py | 8 +- src/bonsai/test/bim/feature/geometry.feature | 15 +- src/bonsai/test/bim/feature/model.feature | 160 +++++++----------- src/bonsai/test/bim/test_feature.py | 20 +-- 6 files changed, 122 insertions(+), 122 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 7eed142ee2..e3299578e2 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -73,6 +73,7 @@ classes = ( wall.ChangeLayerLength, wall.DrawPolylineWall, wall.ExtendWallsToUnderside, + wall.ExtendWallsToWall, wall.FlipWall, wall.MergeWall, wall.RecalculateWall, diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index eef3487217..6c74184083 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -78,6 +78,46 @@ class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator): self.report({"ERROR"}, "Please select at least one LAYER2 element and an active element") +class ExtendWallsToWall(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.extend_walls_to_wall" + bl_label = "Extend Walls To Wall" + bl_description = "Extend and trim selected walls to another wall" + bl_options = {"REGISTER", "UNDO"} + + def _execute(self, context): + target_obj = None + objs = [] + if ( + (obj := tool.Blender.get_active_object(is_selected=True)) + and (element := tool.Ifc.get_entity(obj)) + and tool.Model.get_usage_type(element) == "LAYER2" + ): + target_obj = obj + for obj in tool.Blender.get_selected_objects(include_active=False): + if ( + obj != target_obj + and (element := tool.Ifc.get_entity(obj)) + and tool.Model.get_usage_type(element) == "LAYER2" + ): + objs.append(obj) + if target_obj and objs: + if tool.Ifc.is_moved(target_obj): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=target_obj) + joiner = DumbWallJoiner() + target_element = tool.Ifc.get_entity(target_obj) + for obj in objs: + if tool.Ifc.is_moved(obj): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + element = tool.Ifc.get_entity(obj) + ifcopenshell.api.geometry.connect_wall( + tool.Ifc.get(), wall1=element, wall2=target_element, is_atpath=True + ) + joiner.recreate_wall(element, obj) + joiner.recreate_wall(target_element, target_obj) + else: + self.report({"ERROR"}, "Please select at least one LAYER2 element and one active LAYER2 element") + + class AlignWall(bpy.types.Operator): bl_idname = "bim.align_wall" bl_label = "Align Wall" diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 21e1dce148..cb1cdde985 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1217,13 +1217,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.extend_profile(join_type="T") elif self.active_material_usage == "LAYER2": - # Extend LAYER2s to LAYER2 - [o.select_set(False) for o in selected_usages.get("LAYER3", [])] - [o.select_set(False) for o in selected_usages.get("PROFILE", [])] - try: - core.join_walls_TZ(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model) - except core.RequireAtLeastTwoLayeredElements as e: - self.report({"ERROR"}, str(e)) + bpy.ops.bim.extend_walls_to_wall() elif self.active_material_usage == "PROFILE": # Extend PROFILEs to PROFILE diff --git a/src/bonsai/test/bim/feature/geometry.feature b/src/bonsai/test/bim/feature/geometry.feature index 16dddb4a39..7d3459fdc7 100644 --- a/src/bonsai/test/bim/feature/geometry.feature +++ b/src/bonsai/test/bim/feature/geometry.feature @@ -453,14 +453,14 @@ Scenario: Override duplicate move - copying walls with mitre joint And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the cursor is at "0.5,0,0" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcWall/Wall.001" is selected And additionally the object "IfcWall/Wall" is selected - When I press "bim.hotkey(hotkey='S_Y')" - Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0" + When I press "bim.hotkey(hotkey='S_T')" + Then the object "IfcWall/Wall" dimensions are "0.6,0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0,0,0" And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3" And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0" And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3" @@ -549,10 +549,11 @@ Scenario: Refresh linked aggregate When I deselect all objects And the object "IfcWall/Wall_01.001" is selected When the object layer length is set to "3" - Then the object "IfcWall/Wall_01.001" dimensions are "3,0.1,3" + # Extra 0.1 due to mitre + Then the object "IfcWall/Wall_01.001" dimensions are "3.1,0.1,3" When I refresh linked aggregate the selected object Then the object "IfcWall/Wall_01" exists - And the object "IfcWall/Wall_01" dimensions are "3,0.1,3" + And the object "IfcWall/Wall_01" dimensions are "3.1,0.1,3" Scenario: Refresh linked aggregate - after deleting an object Given I load the IFC test file "/test/files/linked-aggregates.ifc" diff --git a/src/bonsai/test/bim/feature/model.feature b/src/bonsai/test/bim/feature/model.feature index 04a08c1445..729bd52bd5 100644 --- a/src/bonsai/test/bim/feature/model.feature +++ b/src/bonsai/test/bim/feature/model.feature @@ -83,7 +83,7 @@ Scenario: Extend a wall to the cursor And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected And the cursor is at "2,0,0" When I press "bim.hotkey(hotkey='S_E')" @@ -96,9 +96,9 @@ Scenario: Add a wall perpendicular to an existing wall And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the cursor is at "0.5,0,0" - When I press "bim.hotkey(hotkey='S_A')" + When I press "bim.add_occurrence" Then the object "IfcWall/Wall" dimensions are "1,0.1,3" And the object "IfcWall/Wall" bottom left corner is at "0,0,0" And the object "IfcWall/Wall.001" dimensions are "1,0.1,3" @@ -111,9 +111,9 @@ Scenario: Extend one wall to another And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the cursor is at "0.5,0,0" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcWall/Wall.001" is moved to "0.5,-1,0" And the object "IfcWall/Wall.001" is selected And additionally the object "IfcWall/Wall" is selected @@ -124,59 +124,23 @@ Scenario: Extend one wall to another And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0" And the object "IfcWall/Wall.001" top right corner is at "0.6,-2,3" -Scenario: Join two walls with a butt joint - first wall has priority - Given an empty IFC project - And I load the demo construction library - And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" - And the cursor is at "0.5,0,0" - And I press "bim.hotkey(hotkey='S_A')" - And the object "IfcWall/Wall.001" is selected - And additionally the object "IfcWall/Wall" is selected - When I press "bim.hotkey(hotkey='S_T')" - Then the object "IfcWall/Wall" dimensions are "0.4,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "0.6,0,0" - And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3" - And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0" - And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3" - -Scenario: Join two walls with a butt joint - second wall has priority - Given an empty IFC project - And I load the demo construction library - And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" - And the cursor is at "0.5,0,0" - And I press "bim.hotkey(hotkey='S_A')" - And the object "IfcWall/Wall" is selected - And additionally the object "IfcWall/Wall.001" is selected - When I press "bim.hotkey(hotkey='S_T')" - Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0" - And the object "IfcWall/Wall.001" dimensions are "1,0.1,3" - And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0,0" - And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3" - Scenario: Join two walls with a mitre joint Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" - And the cursor is at "0.5,0,0" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" + And the cursor is at "0.7,0,0" + And I press "bim.add_occurrence" And the object "IfcWall/Wall.001" is selected And additionally the object "IfcWall/Wall" is selected - When I press "bim.hotkey(hotkey='S_Y')" - Then the object "IfcWall/Wall" dimensions are "0.5,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "0.5,0,0" + When I press "bim.hotkey(hotkey='S_T')" + Then the object "IfcWall/Wall" dimensions are "0.8,0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0.0,0,0" And the object "IfcWall/Wall.001" dimensions are "1.1,0.1,3" - And the object "IfcWall/Wall.001" bottom left corner is at "0.5,0.1,0" - And the object "IfcWall/Wall.001" top right corner is at "0.6,-1,3" + And the object "IfcWall/Wall.001" bottom left corner is at "0.7,0.1,0" + And the object "IfcWall/Wall.001" top right corner is at "0.8,-1,3" Scenario: Change the height of a wall Given an empty IFC project @@ -184,7 +148,7 @@ Scenario: Change the height of a wall And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected And I set "scene.BIMModelProperties.extrusion_depth" to "2.0" When I press "bim.change_extrusion_depth(depth=2.0)" @@ -196,7 +160,7 @@ Scenario: Change the length of a wall And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected And I set "scene.BIMModelProperties.length" to "2.0" When I press "bim.change_layer_length(length=2.0)" @@ -208,12 +172,12 @@ Scenario: Flip a wall And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected When I press "bim.hotkey(hotkey='S_F')" Then the object "IfcWall/Wall" dimensions are "1,0.1,3" - And the object "IfcWall/Wall" bottom left corner is at "1,0,0" - And the object "IfcWall/Wall" top right corner is at "0,-0.1,3" + And the object "IfcWall/Wall" bottom left corner is at "0,0,0" + And the object "IfcWall/Wall" top right corner is at "1,0.1,3" Scenario: Split a wall Given an empty IFC project @@ -221,7 +185,7 @@ Scenario: Split a wall And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected And the cursor is at "0.5,0,0" When I press "bim.hotkey(hotkey='S_K')" @@ -240,7 +204,7 @@ Scenario: Rotate a wall by 90 degrees And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected When I press "bim.hotkey(hotkey='S_R')" Then the object "IfcWall/Wall" dimensions are "1,0.1,3" @@ -253,7 +217,7 @@ Scenario: Regenerate a wall - after doing nothing interesting And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected When I press "bim.hotkey(hotkey='S_G')" Then the object "IfcWall/Wall" is an "IfcWall" @@ -266,11 +230,11 @@ Scenario: Add a slab And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - When I press "bim.hotkey(hotkey='S_A')" + When I press "bim.add_occurrence" Then the object "IfcSlab/Slab" is an "IfcSlab" And the object "IfcSlab/Slab" dimensions are "1,1,0.2" - And the object "IfcSlab/Slab" bottom left corner is at "0,0,-0.2" - And the object "IfcSlab/Slab" top right corner is at "1,1,0" + And the object "IfcSlab/Slab" bottom left corner is at "0,0,0" + And the object "IfcSlab/Slab" top right corner is at "1,1,0.2" Scenario: Enable editing a slab profile Given an empty IFC project @@ -278,12 +242,12 @@ Scenario: Enable editing a slab profile And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcSlab/Slab" is selected When I press "bim.hotkey(hotkey='S_E')" Then the object "IfcSlab/Slab" dimensions are "1,1,0" - And the object "IfcSlab/Slab" bottom left corner is at "0,0,-0.2" - And the object "IfcSlab/Slab" top right corner is at "1,1,-0.2" + And the object "IfcSlab/Slab" bottom left corner is at "0,0,0" + And the object "IfcSlab/Slab" top right corner is at "1,1,0" Scenario: Disable editing a slab profile Given an empty IFC project @@ -291,13 +255,13 @@ Scenario: Disable editing a slab profile And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcSlab/Slab" is selected And I press "bim.hotkey(hotkey='S_E')" When I press "bim.disable_editing_extrusion_profile" Then the object "IfcSlab/Slab" dimensions are "1,1,0.2" - And the object "IfcSlab/Slab" bottom left corner is at "0,0,-0.2" - And the object "IfcSlab/Slab" top right corner is at "1,1,0" + And the object "IfcSlab/Slab" bottom left corner is at "0,0,0" + And the object "IfcSlab/Slab" top right corner is at "1,1,0.2" Scenario: Edit a slab profile Given an empty IFC project @@ -305,13 +269,13 @@ Scenario: Edit a slab profile And I set "scene.BIMModelProperties.ifc_class" to "IfcSlabType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcSlabType') if e.Name == 'FLR200'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcSlab/Slab" is selected And I press "bim.hotkey(hotkey='S_E')" When I press "bim.edit_extrusion_profile" Then the object "IfcSlab/Slab" dimensions are "1,1,0.2" - And the object "IfcSlab/Slab" bottom left corner is at "0,0,-0.2" - And the object "IfcSlab/Slab" top right corner is at "1,1,0" + And the object "IfcSlab/Slab" bottom left corner is at "0,0,0" + And the object "IfcSlab/Slab" top right corner is at "1,1,0.2" Scenario: Add a beam Given an empty IFC project @@ -319,7 +283,7 @@ Scenario: Add a beam And I set "scene.BIMModelProperties.ifc_class" to "IfcBeamType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcBeamType') if e.Name == 'B1'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - When I press "bim.hotkey(hotkey='S_A')" + When I press "bim.add_occurrence" Then the object "IfcBeam/Beam" is an "IfcBeam" And the object "IfcBeam/Beam" dimensions are "0.1,0.2,3" And the object "IfcBeam/Beam" bottom left corner is at "0,-0.05,-0.1" @@ -331,7 +295,7 @@ Scenario: Extend a beam to the cursor And I set "scene.BIMModelProperties.ifc_class" to "IfcBeamType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcBeamType') if e.Name == 'B1'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcBeam/Beam" is selected And the cursor is at "2,0,0" When I press "bim.hotkey(hotkey='S_E')" @@ -344,9 +308,9 @@ Scenario: Extend one beam to another And I set "scene.BIMModelProperties.ifc_class" to "IfcBeamType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcBeamType') if e.Name == 'B1'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the cursor is at "1,1,0" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcBeam/Beam.001" is selected And I press "bim.hotkey(hotkey='S_R')" And the object "IfcBeam/Beam.001" is selected @@ -365,9 +329,9 @@ Scenario: Join two beams with a butt joint - first beam has priority And I set "scene.BIMModelProperties.ifc_class" to "IfcBeamType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcBeamType') if e.Name == 'B1'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the cursor is at "1,1,0" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcBeam/Beam.001" is selected And I press "bim.hotkey(hotkey='S_R')" And the object "IfcBeam/Beam.001" is selected @@ -386,9 +350,9 @@ Scenario: Join two beams with a butt joint - second beam has priority And I set "scene.BIMModelProperties.ifc_class" to "IfcBeamType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcBeamType') if e.Name == 'B1'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the cursor is at "1,1,0" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcBeam/Beam.001" is selected And I press "bim.hotkey(hotkey='S_R')" And the object "IfcBeam/Beam" is selected @@ -407,9 +371,9 @@ Scenario: Join two beams with a mitre joint And I set "scene.BIMModelProperties.ifc_class" to "IfcBeamType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcBeamType') if e.Name == 'B1'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the cursor is at "1,1,0" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcBeam/Beam.001" is selected And I press "bim.hotkey(hotkey='S_R')" And the object "IfcBeam/Beam" is selected @@ -428,7 +392,7 @@ Scenario: Change the length of a beam And I set "scene.BIMModelProperties.ifc_class" to "IfcBeamType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcBeamType') if e.Name == 'B1'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcBeam/Beam" is selected And I set "scene.BIMModelProperties.extrusion_depth" to "2.0" When I press "bim.change_profile_depth(depth=2.0)" @@ -440,7 +404,7 @@ Scenario: Rotate a beam by 90 degrees And I set "scene.BIMModelProperties.ifc_class" to "IfcBeamType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcBeamType') if e.Name == 'B1'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcBeam/Beam" is selected When I press "bim.hotkey(hotkey='S_R')" Then the object "IfcBeam/Beam" dimensions are "0.1,0.2,3" @@ -453,7 +417,7 @@ Scenario: Regenerate a beam - after doing nothing interesting And I set "scene.BIMModelProperties.ifc_class" to "IfcBeamType" And the variable "element_type" is "[e for e in {ifc}.by_type('IfcBeamType') if e.Name == 'B1'][0].id()" And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcBeam/Beam" is selected When I press "bim.hotkey(hotkey='S_G')" Then the object "IfcBeam/Beam" dimensions are "0.1,0.2,3" @@ -464,13 +428,13 @@ Scenario: Undo test - create a wall and couple windows and undo the last window Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And I set "scene.BIMModelProperties.ifc_class" to "IfcWindowType" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And I prepare to undo And the object "IfcWall/Wall" is selected And I set "scene.BIMModelProperties.ifc_class" to "IfcWindowType" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And I undo Then nothing happens @@ -478,9 +442,9 @@ Scenario: Undo test - create a wall with window opening, flip it and undo Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And I set "scene.BIMModelProperties.ifc_class" to "IfcWindowType" - And I press "bim.hotkey(hotkey='S_A')" + And I press "bim.add_occurrence" And the object "IfcWall/Wall" is selected And I prepare to undo And I press "bim.hotkey(hotkey='S_F')" @@ -489,22 +453,24 @@ Scenario: Undo test - create a wall with window opening, flip it and undo Scenario: Create window type based on window modifier, add an occurrence of it and edit it Given an empty IFC project - And I set "scene.BIMModelProperties.type_class" to "IfcWindowType" - And I set "scene.BIMModelProperties.type_predefined_type" to "WINDOW" - And I set "scene.BIMModelProperties.type_template" to "WINDOW" - And I press "bim.add_type()" - And I press "bim.hotkey(hotkey='S_A')" + And I trigger "Add Element" + And I set the "Class" property to "IfcWindowType" + And I set the "Predefined Type" property to "WINDOW" + And I set the "Representation" property to "Window" + When I click "OK" + And I press "bim.add_occurrence" And I press "bim.enable_editing_window()" And I press "bim.finish_editing_window()" Then nothing happens Scenario: Create door type based on door modifier, add an occurrence of it and edit it Given an empty IFC project - And I set "scene.BIMModelProperties.type_class" to "IfcDoorType" - And I set "scene.BIMModelProperties.type_predefined_type" to "DOOR" - And I set "scene.BIMModelProperties.type_template" to "DOOR" - And I press "bim.add_type()" - And I press "bim.hotkey(hotkey='S_A')" + And I trigger "Add Element" + And I set the "Class" property to "IfcDoorType" + And I set the "Predefined Type" property to "DOOR" + And I set the "Representation" property to "Door" + When I click "OK" + And I press "bim.add_occurrence" And I press "bim.enable_editing_door()" And I press "bim.finish_editing_door()" Then nothing happens diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index e6932cb1b5..cdb080e65c 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -1228,7 +1228,7 @@ def the_object_name_dimensions_are_dimensions(name, dimensions): actual_dimensions = list(the_object_name_exists(name).dimensions) expected_dimensions = [float(co) for co in dimensions.split(",")] for i, number in enumerate(actual_dimensions): - assert is_x(number, expected_dimensions[i]), f"Expected {expected_dimensions[i]} but got {number}" + assert is_x(number, expected_dimensions[i]), f"Expected {expected_dimensions} but got {actual_dimensions}" @then(parsers.parse('the object "{name}" top right corner is at "{location}"')) @@ -1237,7 +1237,7 @@ def the_object_name_top_right_corner_is_at_location(name, location): obj_corner = obj.matrix_world @ Vector(obj.bound_box[6]) assert ( obj_corner - Vector([float(co) for co in location.split(",")]) - ).length < 0.1, f"Object has top right corner {obj_corner}" + ).length < 0.1, f"Object has top right corner {obj_corner} instead of {location}" @then(parsers.parse('the object "{name}" bottom left corner is at "{location}"')) @@ -1246,7 +1246,7 @@ def the_object_name_bottom_left_corner_is_at_location(name, location): obj_corner = obj.matrix_world @ Vector(obj.bound_box[0]) assert ( obj_corner - Vector([float(co) for co in location.split(",")]) - ).length < 0.1, f"Object has bottom left corner {obj_corner}" + ).length < 0.1, f"Object has bottom left corner {obj_corner} instead of {location}" @then(parsers.parse('the object "{name}" is contained in "{container_name}"')) @@ -1305,7 +1305,7 @@ def the_object_name_has_no_modifiers(name): @given(parsers.parse('I load the IFC test file "{filepath}"')) def i_load_the_ifc_test_file(filepath): filepath = f"{variables['cwd']}{filepath}" - bpy.ops.bim.load_project(filepath=filepath, use_relative_path=True) + bpy.ops.bim.load_project(filepath=filepath) @given("I load the demo construction library") @@ -1370,10 +1370,11 @@ def prepare_undo(): @when(parsers.parse("I undo")) @then(parsers.parse("I undo")) def hit_undo(): - # bpy.ops.ed.undo_push(message="UNDO STEP") - override = tool.Blender.get_viewport_context() - with bpy.context.temp_override(**override): - bpy.ops.ed.undo() + bpy.ops.ed.undo_push(message="UNDO STEP") + bpy.ops.ed.undo() + # override = tool.Blender.get_viewport_context() + # with bpy.context.temp_override(**override): + # bpy.ops.ed.undo() @then(parsers.parse('the object "{obj_name1}" has a connection with "{obj_name2}"')) @@ -1431,9 +1432,6 @@ def the_obj_layer_lenght_is_set_to(value): eval("bpy.context.scene.BIMModelProperties.length") except: assert False, f"Property BIMModelProperties.length does not exist when trying to set to value {value}" - - print(50 * "@", bpy.context.selected_objects) - props = tool.Model.get_model_props() props.length = value bpy.ops.bim.change_layer_length(length=value) From 6596f5bc4bae0acfcf36eae67e6afe117ed97037 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Wed, 12 Mar 2025 23:05:15 +1100 Subject: [PATCH 314/476] Fix regressions in MEP joins after switching over to numpy from mathutils --- src/bonsai/test/bim/test_feature.py | 27 +++++++++++-------- .../ifcopenshell/util/shape_builder.py | 4 +-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index cdb080e65c..34484fa018 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -417,20 +417,25 @@ def i_create_default_mep_types(): model_props = tool.Model.get_model_props() # add couple segments types - model_props.type_class = "IfcDuctSegmentType" - model_props.type_name = "RECT1" - model_props.type_template = "FLOW_SEGMENT_RECTANGULAR" - bpy.ops.bim.add_type() + i_trigger_operator("Add Element") + i_set_the_prop_property_to_value("Name", "RECT1") + i_set_the_prop_property_to_value("Class", "IfcDuctSegmentType") + i_set_the_prop_property_to_value("Representation", "Rectangular Distribution Segment") + i_click_button("OK") - model_props.type_template = "FLOW_SEGMENT_CIRCULAR" - model_props.type_name = "CIRCLE1" - bpy.ops.bim.add_type() + i_trigger_operator("Add Element") + i_set_the_prop_property_to_value("Name", "CIRCLE1") + i_set_the_prop_property_to_value("Class", "IfcDuctSegmentType") + i_set_the_prop_property_to_value("Representation", "Circular Distribution Segment") + i_click_button("OK") # add an actuator type - model_props.type_class = "IfcActuatorType" - model_props.type_template = "MESH" # cube representation - model_props.type_name = "ACTUATOR" - bpy.ops.bim.add_type() + i_trigger_operator("Add Element") + i_set_the_prop_property_to_value("Name", "ACTUATOR") + i_set_the_prop_property_to_value("Class", "IfcActuatorType") + i_set_the_prop_property_to_value("Representation", "Custom Tessellation") + i_click_button("OK") + with bpy.context.temp_override(active_object=bpy.data.objects["IfcActuatorType/ACTUATOR"]): bpy.ops.bim.add_port() # port at cube's left side diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py index 27e3574f89..47926c7e0a 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py @@ -1378,7 +1378,7 @@ class ShapeBuilder: # prevent mutating arguments, deepcopy doesn't work start_points = np.array(points) - if offset: + if offset is not None and offset.any(): start_points += offset extrusion_offset = np.multiply(extrusion_vector, magnitude) end_points = start_points + extrusion_offset @@ -1573,7 +1573,7 @@ class ShapeBuilder: circle_points += end_extrusion_offset # circle verts are 0-15, rect verts are 16-19 - points = circle_points + rect_points + points = np.concatenate((circle_points, rect_points)) transition_faces = [ (0, 19, 16), # base (0, 16, 1), From 63aada25dd6afc2072f8648f021580062262e92e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 12 Mar 2025 17:05:16 +0500 Subject: [PATCH 315/476] Deprecate LaunchAddElement, LaunchRenameType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TIL that it's possible to set invoke context from draw by changing operator_context property in UILayout 😅 --- src/bonsai/bonsai/bim/module/model/ui.py | 8 +++++--- src/bonsai/bonsai/bim/module/root/__init__.py | 1 - src/bonsai/bonsai/bim/module/root/operator.py | 16 ---------------- src/bonsai/bonsai/bim/module/type/__init__.py | 1 - src/bonsai/bonsai/bim/module/type/operator.py | 13 ------------- 5 files changed, 5 insertions(+), 34 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 988506f74f..57950f7459 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -54,7 +54,8 @@ class BIM_MT_type_menu(bpy.types.Menu): def draw(self, context): props = tool.Model.get_model_props() layout = self.layout - op = layout.operator("bim.launch_rename_type", icon="GREASEPENCIL", text="Rename Type") + layout.operator_context = "INVOKE_REGION_WIN" + op = layout.operator("bim.rename_type", icon="GREASEPENCIL", text="Rename Type") op.element = props.menu_relating_type_id op = layout.operator("bim.select_type", icon="OBJECT_DATA") op.relating_type = props.menu_relating_type_id @@ -699,6 +700,7 @@ class BIM_PT_roof(bpy.types.Panel): row.operator("bim.add_roof", icon="ADD", text="") -def add_menu(self, context): - self.layout.operator("bim.launch_add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element") +def add_menu(self: bpy.types.Menu, context: bpy.types.Context) -> None: + self.layout.operator_context = "INVOKE_REGION_WIN" + self.layout.operator("bim.add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element") self.layout.separator() diff --git a/src/bonsai/bonsai/bim/module/root/__init__.py b/src/bonsai/bonsai/bim/module/root/__init__.py index daba174815..3e6f280eb2 100644 --- a/src/bonsai/bonsai/bim/module/root/__init__.py +++ b/src/bonsai/bonsai/bim/module/root/__init__.py @@ -24,7 +24,6 @@ classes = ( operator.AssignClass, operator.DisableReassignClass, operator.EnableReassignClass, - operator.LaunchAddElement, operator.ReassignClass, operator.UnlinkObject, prop.BIMRootProperties, diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 624eefc11d..f4ac9dafc2 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -625,19 +625,3 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): row.prop(props, "profile", text="Profile") if props.representation_template != "EMPTY": prop_with_search(self.layout, props, "contexts", should_click_ok=True) - - -class LaunchAddElement(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.launch_add_element" - bl_label = "Launch Add Element" - bl_options = {"REGISTER", "UNDO"} - bl_description = "Add an IFC physical product, construction type, and more" - - @classmethod - def poll(cls, context): - return tool.Ifc.get() - - def execute(self, context): - # This stub operator is needed because operators from menu skip the invoke call - bpy.ops.bim.add_element("INVOKE_DEFAULT") - return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/type/__init__.py b/src/bonsai/bonsai/bim/module/type/__init__.py index a083333456..646ab48fcf 100644 --- a/src/bonsai/bonsai/bim/module/type/__init__.py +++ b/src/bonsai/bonsai/bim/module/type/__init__.py @@ -25,7 +25,6 @@ classes = ( operator.DisableEditingType, operator.DuplicateType, operator.EnableEditingType, - operator.LaunchRenameType, operator.RemoveType, operator.RenameType, operator.SelectSimilarType, diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index 14c1df884b..a43e0b72af 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -279,19 +279,6 @@ class RenameType(bpy.types.Operator, tool.Ifc.Operator): self.layout.prop(self, "name") -class LaunchRenameType(bpy.types.Operator, tool.Ifc.Operator): - bl_idname = "bim.launch_rename_type" - bl_label = "Launch Rename Type" - bl_options = {"REGISTER", "UNDO"} - element: bpy.props.IntProperty() - name: bpy.props.StringProperty(name="Name") - - def execute(self, context): - # This stub operator is needed because operators from menu skip the invoke call - bpy.ops.bim.rename_type("INVOKE_DEFAULT", element=self.element, name=self.name) - return {"FINISHED"} - - class AutoRenameOccurrences(bpy.types.Operator): bl_idname = "bim.auto_rename_occurrences" bl_label = "Auto Rename Occurrences" From f6640d3904df276959706981f6c4b02dc4e6804e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 12 Mar 2025 16:48:06 +0500 Subject: [PATCH 316/476] typing --- src/bonsai/bonsai/bim/module/system/data.py | 15 +++++++++------ .../ifcopenshell/util/schema.py | 14 +++++++++++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/data.py b/src/bonsai/bonsai/bim/module/system/data.py index aa36696965..908248203b 100644 --- a/src/bonsai/bonsai/bim/module/system/data.py +++ b/src/bonsai/bonsai/bim/module/system/data.py @@ -130,7 +130,10 @@ class PortData: @classmethod def load(cls): - element = tool.Ifc.get_entity(bpy.context.active_object) + obj = bpy.context.active_object + assert obj + element = tool.Ifc.get_entity(obj) + assert element cls.element = element is_port = cls.is_port() cls.data = { @@ -145,19 +148,19 @@ class PortData: cls.is_loaded = True @classmethod - def total_ports(cls): + def total_ports(cls) -> int: return len(ifcopenshell.util.system.get_ports(cls.element)) @classmethod - def is_port(cls): - return cls.element and cls.element.is_a("IfcDistributionPort") + def is_port(cls) -> bool: + return bool(cls.element and cls.element.is_a("IfcDistributionPort")) @classmethod - def port_relating_object_name(cls): + def port_relating_object_name(cls) -> str: return tool.Ifc.get_object(tool.System.get_port_relating_element(cls.element)).name @classmethod - def port_connected_object_name(cls): + def port_connected_object_name(cls) -> Union[str, None]: connected_port = tool.System.get_connected_port(cls.element) if not connected_port: return diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema.py b/src/ifcopenshell-python/ifcopenshell/util/schema.py index 79c8a9c499..edffea2b2f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/util/schema.py @@ -30,13 +30,21 @@ cwd = os.path.dirname(os.path.realpath(__file__)) IFC_SCHEMA = Literal["IFC2X3", "IFC4", "IFC4X3"] -def get_fallback_schema(version: str) -> str: - """fallback to the schema version we do have docs and mapping for, - needed to support IFC versions like 4X3_RC1, 4X1 etc""" +def get_fallback_schema(version: str) -> IFC_SCHEMA: + """Fallback to the schema version we do have docs and mapping for. + + Needed to support IFC versions like 4X3_RC1, 4X1 etc. + + :param version: Typically a string from ``ifcopenshell.file.schema_identifier``, e.g. IFC4X3_ADD2 + """ if version.startswith("IFC4X3"): version = "IFC4X3" elif version.startswith("IFC4"): version = "IFC4" + elif version.startswith("IFC2X3"): + version = "IFC2X3" + else: + assert False, f"Unexpected schema version: {version}." return version From dbf4a2a5ed1f7db7fd3de5186b3601ef54cc0662 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 12 Mar 2025 16:48:51 +0500 Subject: [PATCH 317/476] Fix reassign_class on IFC4X3, use ValueError Use ValueError to make it more specific --- src/ifcopenshell-python/ifcopenshell/util/schema.py | 10 +++++++--- .../test/api/root/test_reassign_class.py | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema.py b/src/ifcopenshell-python/ifcopenshell/util/schema.py index edffea2b2f..65a12b230e 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/util/schema.py @@ -162,16 +162,20 @@ def reassign_class( (such as IfcRelNests) It's unlikely that this affects real-world usage of this function. + + :raises ValueError: If ``new_class`` does not exist in the provided file schema. """ if not ifc_file: ifc_file = element.file - schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(ifc_file.schema) + schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(ifc_file.schema_identifier) try: declaration = schema.declaration_by_name(new_class) - except: - raise Exception(f"Class of {element} could not be changed to {new_class} as the class does not exist") + except RuntimeError: + raise ValueError( + f"Class of {element} could not be changed to {new_class} as the class does not exist in schema {ifc_file.schema_identifier}." + ) info = element.get_info() diff --git a/src/ifcopenshell-python/test/api/root/test_reassign_class.py b/src/ifcopenshell-python/test/api/root/test_reassign_class.py index f8053e13b5..f8be13225e 100644 --- a/src/ifcopenshell-python/test/api/root/test_reassign_class.py +++ b/src/ifcopenshell-python/test/api/root/test_reassign_class.py @@ -197,6 +197,10 @@ class TestReassignClass(test.bootstrap.IFC4): assert len(self.file.by_type("IfcSlab")) == 1 +class TestReassignClassIFC4X3(test.bootstrap.IFC4X3, TestReassignClass): + pass + + class TestReassignClassIFC2X3(test.bootstrap.IFC2X3, TestReassignClass): def test_providing_occurrence_class(self): element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") From ffc6c6a7ad9f422f819ae3ab1daeba205bee97ec Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 12 Mar 2025 12:04:29 +0500 Subject: [PATCH 318/476] Zones UI - rearrange button to match the Systems UI layout --- src/bonsai/bonsai/bim/module/system/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index e16bf85971..4d3e887337 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -375,10 +375,10 @@ class BIM_PT_zones(Panel): row.operator("bim.add_zone", text="", icon="ADD") if self.props.zones and self.props.active_zone_index < len(self.props.zones): ifc_definition_id = self.props.zones[self.props.active_zone_index].ifc_definition_id - row.operator("bim.enable_editing_zone", text="", icon="GREASEPENCIL").zone = ifc_definition_id row.operator("bim.select_system_products", text="", icon="RESTRICT_SELECT_OFF").system = ifc_definition_id row.operator("bim.assign_system", text="", icon="KEYFRAME_HLT").system = ifc_definition_id row.operator("bim.unassign_system", text="", icon="KEYFRAME").system = ifc_definition_id + row.operator("bim.enable_editing_zone", text="", icon="GREASEPENCIL").zone = ifc_definition_id row.operator("bim.remove_zone", text="", icon="X").zone = ifc_definition_id self.layout.template_list("BIM_UL_zones", "", self.props, "zones", self.props, "active_zone_index") From dccfb634d9310b06ea2b88a5c7100b9a27394bb7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 12 Mar 2025 12:15:13 +0500 Subject: [PATCH 319/476] Ports UI - cache instead of accesing IFC on draw calls --- src/bonsai/bonsai/bim/module/system/data.py | 21 ++++++++++++++------- src/bonsai/bonsai/bim/module/system/ui.py | 19 +++++++++---------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/system/data.py b/src/bonsai/bonsai/bim/module/system/data.py index 908248203b..ac328014e0 100644 --- a/src/bonsai/bonsai/bim/module/system/data.py +++ b/src/bonsai/bonsai/bim/module/system/data.py @@ -168,7 +168,7 @@ class PortData: return tool.Ifc.get_object(connected_element).name @classmethod - def located_ports_data(cls): + def located_ports_data(cls) -> list[dict[str, Any]]: ports = ifcopenshell.util.system.get_ports(cls.element) data = [] @@ -181,17 +181,24 @@ class PortData: else: connected_obj_name = None - data.append((port, port_obj_name, connected_obj_name)) + data.append( + { + "id": port.id(), + "FlowDirection": port.FlowDirection, + "port_obj_name": port_obj_name, + "connected_obj_name": connected_obj_name, + } + ) return data @classmethod - def selected_objects_flow_direction(cls): - for port, _, connected_obj_name in cls.data["located_ports_data"]: - if connected_obj_name is None: + def selected_objects_flow_direction(cls) -> Union[str, None]: + for port_data in cls.data["located_ports_data"]: + if port_data["connected_obj_name"] is None: continue - connected_obj = bpy.data.objects[connected_obj_name] + connected_obj = bpy.data.objects[port_data["connected_obj_name"]] if connected_obj in bpy.context.selected_objects: - return port.FlowDirection + return port_data["FlowDirection"] class SystemDecorationData: diff --git a/src/bonsai/bonsai/bim/module/system/ui.py b/src/bonsai/bonsai/bim/module/system/ui.py index 4d3e887337..23c3d9d371 100644 --- a/src/bonsai/bonsai/bim/module/system/ui.py +++ b/src/bonsai/bonsai/bim/module/system/ui.py @@ -186,24 +186,23 @@ class BIM_PT_ports(Panel): row = self.layout.row(align=True) cols = [row.column(align=True) for i in range(6)] - for i, port_data in enumerate(PortData.data["located_ports_data"]): - port, port_obj_name, connected_obj_name = port_data - flow_direction_icon = FLOW_DIRECTION_TO_ICON[port.FlowDirection or "NOTDEFINED"] - if port_obj_name: + for port_data in PortData.data["located_ports_data"]: + flow_direction_icon = FLOW_DIRECTION_TO_ICON[port_data["FlowDirection"] or "NOTDEFINED"] + if port_data["port_obj_name"]: cols[0].label(text="", icon=flow_direction_icon) - cols[1].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port.id() - cols[2].label(text=port_obj_name) + cols[1].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = port_data["id"] + cols[2].label(text=port_data["port_obj_name"]) else: cols[0].label(text="", icon=flow_direction_icon) cols[1].label(text="", icon="HIDE_ON") cols[2].label(text="Port is hidden") - if connected_obj_name: - connected_obj = bpy.data.objects[connected_obj_name] - cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port.id() + if port_data["connected_obj_name"]: + connected_obj = bpy.data.objects[port_data["connected_obj_name"]] + cols[3].operator("bim.disconnect_port", text="", icon="UNLINKED").element_id = port_data["id"] ifc_id = tool.Blender.get_ifc_definition_id(connected_obj) cols[4].operator("bim.select_entity", text="", icon="RESTRICT_SELECT_OFF").ifc_id = ifc_id - cols[5].label(text=connected_obj_name) + cols[5].label(text=port_data["connected_obj_name"]) else: cols[3].label(text="", icon="UNLINKED") cols[4].label(text="", icon="BLANK1") From ece91a94db704c53f616d132584690769ca8e695 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 12 Mar 2025 15:29:43 +0500 Subject: [PATCH 320/476] util.schema.reassign_class to return early if class already assigned --- src/ifcopenshell-python/ifcopenshell/util/schema.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/util/schema.py b/src/ifcopenshell-python/ifcopenshell/util/schema.py index 65a12b230e..059312f89d 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/schema.py +++ b/src/ifcopenshell-python/ifcopenshell/util/schema.py @@ -166,6 +166,9 @@ def reassign_class( :raises ValueError: If ``new_class`` does not exist in the provided file schema. """ + if element.is_a() == new_class: + return element + if not ifc_file: ifc_file = element.file From 7e9985758fc88293afcc6d5d23b1ecc06ce7a6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 12 Mar 2025 11:09:34 -0300 Subject: [PATCH 321/476] Remove debug print statements. --- src/bonsai/bonsai/bim/module/model/polyline.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index e688c2a7fb..94f17c0ec2 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -986,12 +986,9 @@ class PolylineOperator: self.tool_state.plane_method = None self.tool_state.mode = "Mouse" self.visible_objs = tool.Raycast.get_visible_objects(context) - # print(self.visible_objs) for obj in self.visible_objs: self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) - print(self.objs_2d_bbox) detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state) - print("detected_snaps", detected_snaps) self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps) tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state) From 18431dec310b3b4086d8fd05ce89e8add442974b Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Wed, 12 Mar 2025 14:23:43 -0500 Subject: [PATCH 322/476] added `material.item.Material.Name.0` to selector syntax --- .../docs/ifcopenshell-python/selector_syntax.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst index cfb49034fb..61b9e08841 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst @@ -163,6 +163,7 @@ without needing to write complex code for it. "``materials.count``", "Count the number of materials assigned to an element." "``material.Name``", "Get the name of the assigned material." "``material.item.0.Name``", "Get the name of the first item in a material set (e.g. the first material layer)" + "``material.item.Material.Name.0``", "Get the name of the material in the first item in a material set" The element value syntax works by specifying one or more query keys separated by a ``.`` character. Each query key returns data based of the results of the From b06ddbcad4fc744ba9134e77186e123b4df3ff7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 12 Mar 2025 13:57:49 -0300 Subject: [PATCH 323/476] Remove duplicate line --- src/bonsai/bonsai/tool/raycast.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 9dbcc57ad9..ca531b7be1 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -159,7 +159,6 @@ class Raycast(bonsai.core.tool.Raycast): if obj and obj.type == "EMPTY": v = obj.location intersection = tool.Cad.point_on_edge(v, (ray_target, loc)) - intersection = tool.Cad.point_on_edge(v, (ray_target, loc)) distance = (v - intersection).length if distance < snap_threshold: snap_point = { From 5cc0aa71c71bece4754efe9b9fe82671a4081b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 12 Mar 2025 13:58:04 -0300 Subject: [PATCH 324/476] Fix issues with snapping empties and faceless objects in x-ray mode. --- src/bonsai/bonsai/tool/snap.py | 100 +++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 32906f7f90..ee952f53ac 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -328,23 +328,34 @@ class Snap(bonsai.core.tool.Snap): def cast_rays_to_single_object( obj: bpy.types.Object, mouse_pos: tuple[int, int] ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: - if obj.type != "MESH": - return None, None, None - hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj) - if hit is None: - # Tried original mouse position. Now it will try the offsets. - original_mouse_pos = mouse_pos - for value in mouse_offset: - mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value)) - hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, mouse_pos) + hit = None + face_index = None + # Wireframes + if obj.type in {"EMPTY", "CURVE"} or (hasattr(obj.data, "polygons") and len(obj.data.polygons) == 0) : + snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj) + if snap_points: + hit = sorted(snap_points, key=lambda x: x["distance"])[0]["point"] if hit: - break - mouse_pos = original_mouse_pos - if hit: - hit_world = obj.original.matrix_world @ hit - return obj, hit_world, face_index - else: + hit_world = obj.original.matrix_world @ hit + return obj, hit_world, face_index return None, None, None + # Meshes + else: + hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj) + if hit is None: + # Tried original mouse position. Now it will try the offsets. + original_mouse_pos = mouse_pos + for value in mouse_offset: + mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value)) + hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, mouse_pos) + if hit: + break + mouse_pos = original_mouse_pos + if hit: + hit_world = obj.original.matrix_world @ hit + return obj, hit_world, face_index + else: + return None, None, None def cast_rays_and_get_best_object( objs_to_raycast: list[bpy.types.Object], mouse_pos: tuple[int, int] @@ -407,24 +418,7 @@ class Snap(bonsai.core.tool.Snap): point["group"] = "Measure" detected_snaps.append(point) - # Edge-Vertex - for obj in objs_to_raycast: - if obj.type in {"MESH", "EMPTY"}: - # if len(obj.data.polygons) == 0: - snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj) - if snap_points: - for point in snap_points: - point["group"] = "Edge-Vertex" - detected_snaps.append(point) - if obj.type == "CURVE": - new_object = bpy.data.objects.new("new_object", obj.to_mesh().copy()) - snap_points = tool.Raycast.ray_cast_by_proximity(context, event, new_object) - if snap_points: - for point in snap_points: - point["group"] = "Edge-Vertex" - detected_snaps.append(point) - - # Obj + # Objects if (space.shading.type == "SOLID" and space.shading.show_xray) or ( space.shading.type == "WIREFRAME" and space.shading.show_xray_wireframe ): @@ -434,29 +428,49 @@ class Snap(bonsai.core.tool.Snap): else: results = [] results.append(cast_rays_and_get_best_object(objs_to_raycast, mouse_pos)) + for result in results: snap_obj = result[0] hit = result[1] face_index = result[2] if hit is not None: - snap_points = tool.Raycast.ray_cast_by_proximity( - context, event, snap_obj, snap_obj.data.polygons[face_index] - ) - if snap_points: - for point in snap_points: - point["group"] = "Object" - detected_snaps.append(point) + # Wireframes + if snap_obj.type == "EMPTY" or (snap_obj.type == "MESH" and len(snap_obj.data.polygons) == 0): + snap_points = tool.Raycast.ray_cast_by_proximity(context, event, snap_obj) + if snap_points: + for point in snap_points: + point["group"] = "Wireframe" + detected_snaps.append(point) + + elif snap_obj.type == "CURVE": + new_object = bpy.data.objects.new("new_object", obj.to_mesh().copy()) + snap_points = tool.Raycast.ray_cast_by_proximity(context, event, new_object) + if snap_points: + for point in snap_points: + point["group"] = "Wireframe" + detected_snaps.append(point) + # Meshes else: + # Add face snap snap_point = { "point": hit, "type": "Face", "group": "Object", "object": snap_obj, "face_index": face_index, - "distance": 10, # High value so it has low priority + "distance": 9, # High value so it has low priority } detected_snaps.append(snap_point) + # Add vertex and edge snap + snap_points = tool.Raycast.ray_cast_by_proximity( + context, event, snap_obj, snap_obj.data.polygons[face_index] + ) + if snap_points: + for point in snap_points: + point["group"] = "Object" + detected_snaps.append(point) + # Axis and Plane if tool.Ifc.get(): elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z @@ -536,7 +550,7 @@ class Snap(bonsai.core.tool.Snap): return filtered_points def filter_snapping_points_by_group(detected_snaps): - options = ["Edge-Vertex", "Axis", "Plane"] + options = ["Wireframe", "Axis", "Plane"] props = context.scene.BIMSnapGroups for prop in props.__annotations__.keys(): if getattr(props, prop): @@ -560,7 +574,7 @@ class Snap(bonsai.core.tool.Snap): snaps_by_group = filter_snapping_points_by_group(detected_snaps) edges = [] # Get edges to create edge-intersection snap for snapping_point in snaps_by_group: - if snapping_point["group"] in {"Polyline", "Measure", "Edge-Vertex", "Object"}: + if snapping_point["group"] in {"Polyline", "Measure", "Wireframe", "Object"}: if snapping_point["type"] == "Edge": edges.append(snapping_point) if snapping_point["group"] == "Axis": From 5df9c45795a32b3ca6c7f211dd952526f864b3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Wed, 12 Mar 2025 14:04:08 -0300 Subject: [PATCH 325/476] Improve weight in snapping for "Plane", "Axis" and "Face" type. --- src/bonsai/bonsai/tool/snap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index ee952f53ac..3e2e68db97 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -569,6 +569,8 @@ class Snap(bonsai.core.tool.Snap): snap["distance"] *= weight_factor / 5 if snap["type"] == "Edge Intersection": snap["distance"] *= weight_factor / 10 + if snap["type"] in ["Plane", "Axis", "Face"]: + snap["distance"] = weight_factor / 50 return sorted(snapping_points, key=lambda x: x["distance"]) snaps_by_group = filter_snapping_points_by_group(detected_snaps) From ba8753d55edad4339c286f2129fee0acdab86ee8 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Wed, 12 Mar 2025 16:27:48 -0500 Subject: [PATCH 326/476] Updated docs regarding material selector syntax. --- .../docs/ifcopenshell-python/selector_syntax.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst index 61b9e08841..d2857c7810 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst @@ -161,9 +161,9 @@ without needing to write complex code for it. "``types.count``", "Count the number of occurrences of a type." "``storey.Name``", "Get the ``Name`` attribute of the storey that the element is contained in." "``materials.count``", "Count the number of materials assigned to an element." - "``material.Name``", "Get the name of the assigned material." - "``material.item.0.Name``", "Get the name of the first item in a material set (e.g. the first material layer)" - "``material.item.Material.Name.0``", "Get the name of the material in the first item in a material set" + "``material.Name``", "**IfcMaterial**: The name of the assigned material. **IfcMaterialLayerSet**: name of the LayerSetName. **IfcMaterialProfileSet**: The name of the overall material profile set. **IfcMaterialConstituent**: The name of the overall material constituent set." + "``material.item.0.Name``", "**IfcMaterial**: N/A. **IfcMaterialLayerSet**: The name of the 1st material layer. **IfcMaterialProfileSet**: The name of the 1st material profile. **IfcMaterialConstituent**: The name of the 1st material constituent." + "``material.item.Material.Name.0``", "**IfcMaterial**: The assigned material name. **IfcMaterialLayerSet**: The material name of the 1st material layer. **IfcMaterialProfileSet**: The material name of the 1st material profile. **IfcMaterialConstituent**: The material name of the 1st material constituent." The element value syntax works by specifying one or more query keys separated by a ``.`` character. Each query key returns data based of the results of the From d89957faab24754f642117569d5627c71f04ee35 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 Mar 2025 09:18:01 +1100 Subject: [PATCH 327/476] Fix regression in 227f31574 which prevented quantifications The key in the dictionary is not a class name, it's a query string to allow for complex quantification rules. --- src/bonsai/bonsai/bim/module/qto/operator.py | 2 +- src/bonsai/test/bim/test_feature.py | 2 +- src/ifc5d/ifc5d/qto.py | 15 ++++++--------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/qto/operator.py b/src/bonsai/bonsai/bim/module/qto/operator.py index 63d16335a1..8a996fc383 100644 --- a/src/bonsai/bonsai/bim/module/qto/operator.py +++ b/src/bonsai/bonsai/bim/module/qto/operator.py @@ -128,7 +128,7 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator): props = context.scene.BIMQtoProperties elements = set() - for obj in context.selected_objects: + for obj in tool.Blender.get_selected_objects(include_active=False): element = tool.Ifc.get_entity(obj) if element: elements.add(element) diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 34484fa018..4f263f93c9 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -1097,7 +1097,7 @@ def prop_is_roughly_value(prop, value): def the_object_name_has_a_cartesian_point_offset_of_offset(name: str, offset: str) -> None: offset = replace_variables(offset) obj = the_object_name_exists(name) - props = tool.Blender.get_object_props(obj) + props = tool.Blender.get_object_bim_props(obj) assert props.blender_offset_type == "CARTESIAN_POINT" obj_offset = np.array(tuple(map(float, props.cartesian_point_offset.split(",")))) offset = np.array(tuple(map(float, offset.split(",")))) diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 1c99cccc74..2206fe6a1a 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -47,10 +47,12 @@ for name in get_args(RULE_SET): def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict) -> ResultsDict: - """ + """Quantify elements from a rules using preset quantification rules + + Rules placed as a JSON configuration file in the ``ifc5d`` folder will be + autodetected and loaded with the module for convenience. :param rules: Set of rules from `ifc5d.qto.rules`. - """ results: ResultsDict = {} elements_by_classes: defaultdict[str, set[ifcopenshell.entity_instance]] = defaultdict(set) @@ -59,13 +61,8 @@ def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_inst for calculator, queries in rules["calculators"].items(): calculator = calculators[calculator] - for ifc_class, qtos in queries.items(): - filtered_elements = set() - ifc_classes = [ifc_class] + ifcopenshell.util.type.get_applicable_types(ifc_class, ifc_file.schema) - for ifc_class in ifc_classes: - if ifc_class not in elements_by_classes: - continue - filtered_elements.update(elements_by_classes[ifc_class]) + for query, qtos in queries.items(): + filtered_elements = ifcopenshell.util.selector.filter_elements(ifc_file, query, elements) if filtered_elements: calculator.calculate(ifc_file, filtered_elements, qtos, results) return results From 6b85e9ec851054867b35b96fbbd3b607a926e8f3 Mon Sep 17 00:00:00 2001 From: falken10 <33285113+falken10@users.noreply.github.com> Date: Mon, 10 Mar 2025 19:18:46 +0100 Subject: [PATCH 328/476] Update operator.py This will sort the groups by name so it is easy to find them. --- .../bonsai/bim/module/group/operator.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index 9a9124ebfa..113b520e7f 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -35,9 +35,12 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator): self.expanded_groups = json.loads(context.scene.ExpandedGroups.json_string) self.props.groups.clear() - for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False): - if not group.HasAssignments: - self.load_group(group) + groups = [group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.HasAssignments] + sorted_groups = sorted(groups, key=lambda group: group.Name or "Unnamed") + + for group in sorted_groups: + self.load_group(group) + self.props.is_editing = True bpy.ops.bim.disable_editing_group() @@ -51,14 +54,14 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator): new.has_children = False new.is_expanded = group.id() in self.expanded_groups - for rel in group.IsGroupedBy or []: - for related_object in rel.RelatedObjects: - if not related_object.is_a("IfcGroup"): - continue - new.has_children = True - if not new.is_expanded: - return - self.load_group(related_object, tree_depth=tree_depth + 1) + related_groups = [related_object for rel in group.IsGroupedBy or [] for related_object in rel.RelatedObjects if related_object.is_a("IfcGroup")] + sorted_related_groups = sorted(related_groups, key=lambda group: group.Name or "Unnamed") + + for related_group in sorted_related_groups: + new.has_children = True + if not new.is_expanded: + return + self.load_group(related_group, tree_depth=tree_depth + 1) class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator): From 9bafe07c36be497eb94719fc876d84319dd8f160 Mon Sep 17 00:00:00 2001 From: falken10 <33285113+falken10@users.noreply.github.com> Date: Tue, 11 Mar 2025 18:06:11 +0100 Subject: [PATCH 329/476] Update system.py so two ortogonal arrows (in x and y local) are used for MEP systems decorations Currently the flow direction decorations run in the local Y. This means that if one is creating ducts/pipes in the xy plane, the decorations are drawn in planes perpendicular to that one and than means that the arrows are not visible from a top/bottom view. By adding another set of arrows perpendicular the arrows are always visible no matter what projection is taken --- src/bonsai/bonsai/tool/system.py | 53 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 91c0a2a9ba..fe4013195f 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -340,32 +340,37 @@ class System(bonsai.core.tool.System): # for now it's hardcoded to local Y axis to avoid using viewport data # for performance reasons - edge_ortho = obj.matrix_world.col[1].to_3d().normalized() - second_ortho = edge_dir.cross(edge_ortho) - edge_ortho = second_ortho.cross(edge_dir) - - # direction lines should be around the edge center - n_direction_lines, start_offset = divmod(edge_length, direction_lines_offset) - n_direction_lines = int(n_direction_lines) + 1 - start_offset /= 2 - start_offset = edge_dir * start_offset + base_vert - cur_vert_index = start_vert_i + len(port_data) - - for i in range(n_direction_lines): - cur_offset = start_offset + edge_dir * i * direction_lines_offset + for j in range(2): + edge_ortho = obj.matrix_world.col[j].to_3d().normalized() + second_ortho = edge_dir.cross(edge_ortho) + edge_ortho = second_ortho.cross(edge_dir) + + # direction lines should be around the edge center + n_direction_lines, start_offset = divmod(edge_length, direction_lines_offset) + n_direction_lines = int(n_direction_lines) + 1 + start_offset /= 2 + start_offset = edge_dir * start_offset + base_vert + if both_directions: - verts_pos.append(cur_offset + edge_ortho * direction_lines_width) - verts_pos.append(cur_offset - edge_ortho * direction_lines_width) - edges.append((cur_vert_index, cur_vert_index + 1)) - cur_vert_index += 2 + cur_vert_index = start_vert_i + len(port_data) + j * 2 * n_direction_lines else: - arrow_base = cur_offset - edge_dir * direction_lines_width - verts_pos.append(arrow_base + edge_ortho * direction_lines_width) - verts_pos.append(cur_offset) - verts_pos.append(arrow_base - edge_ortho * direction_lines_width) - edges.append((cur_vert_index, cur_vert_index + 1)) - edges.append((cur_vert_index + 1, cur_vert_index + 2)) - cur_vert_index += 3 + cur_vert_index = start_vert_i + len(port_data) + j * 3 * n_direction_lines + + for i in range(n_direction_lines): + cur_offset = start_offset + edge_dir * i * direction_lines_offset + if both_directions: + verts_pos.append(cur_offset + edge_ortho * direction_lines_width) + verts_pos.append(cur_offset - edge_ortho * direction_lines_width) + edges.append((cur_vert_index, cur_vert_index + 1)) + cur_vert_index += 2 + else: + arrow_base = cur_offset - edge_dir * direction_lines_width + verts_pos.append(arrow_base + edge_ortho * direction_lines_width) + verts_pos.append(cur_offset) + verts_pos.append(arrow_base - edge_ortho * direction_lines_width) + edges.append((cur_vert_index, cur_vert_index + 1)) + edges.append((cur_vert_index + 1, cur_vert_index + 2)) + cur_vert_index += 3 all_vertices.extend(verts_pos) From 5114576457b2063a7f40d6371928fee6a45978eb Mon Sep 17 00:00:00 2001 From: falken10 <33285113+falken10@users.noreply.github.com> Date: Tue, 11 Mar 2025 15:51:23 +0100 Subject: [PATCH 330/476] Update profile.py to add MEPGenerator().setup_ports when the profile has only one segment (no DumbProfileJoiner called) When adding a single duct in MEP no ports where attached. The function create_profiles_from_polyline does not call DumbProfileJoiner (which eventually calls MEPGenerator().setup_ports). I have added a check so if there is only one segment, the MEPGenerator().setup_ports is called explictily --- src/bonsai/bonsai/bim/module/model/profile.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 9b12dbad55..bafbc2ae72 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -1148,6 +1148,14 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato for profile1, profile2 in zip(profiles, profiles[1:] + [profiles[0]]): DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"]) else: + if len(profiles) == 1: + profile1 = profiles[0] + element1 = tool.Ifc.get_entity(profile1["obj"]) + if element1.is_a("IfcFlowSegment") or element1.is_a("IfcFlowFitting"): + # lazy import to avoid circular import errors + from bonsai.bim.module.model.mep import MEPGenerator + MEPGenerator().setup_ports(profile1["obj"]) + else: for profile1, profile2 in zip(profiles[:-1], profiles[1:]): DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"]) From 6517cafd987b6e5b50d6d568c7598c803ade28f4 Mon Sep 17 00:00:00 2001 From: falken10 <33285113+falken10@users.noreply.github.com> Date: Tue, 11 Mar 2025 23:32:05 +0100 Subject: [PATCH 331/476] Update add_prop_template.py so the properties are sorted This allows to keep the pset properties sorted for easy access --- .../ifcopenshell/api/pset_template/add_prop_template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_prop_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_prop_template.py index 4a621dd3d1..39d1987fa4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_prop_template.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_prop_template.py @@ -110,5 +110,6 @@ def add_prop_template( ) has_property_templates = list(pset_template.HasPropertyTemplates or []) has_property_templates.append(prop_template) + has_property_templates.sort(key=lambda pt: pt.Name) pset_template.HasPropertyTemplates = has_property_templates return prop_template From 4cff2f1fc195ec291692329f7686c5df830646ba Mon Sep 17 00:00:00 2001 From: falken10 Date: Mon, 10 Mar 2025 14:44:11 +0100 Subject: [PATCH 332/476] Add traverse_transparent property to control sun light traversal through transparent objects --- src/bonsai/bonsai/bim/module/light/prop.py | 31 ++++++++++++++++++++++ src/bonsai/bonsai/bim/module/light/ui.py | 6 +++++ 2 files changed, 37 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/light/prop.py b/src/bonsai/bonsai/bim/module/light/prop.py index c8f249cefd..1f2c17b898 100644 --- a/src/bonsai/bonsai/bim/module/light/prop.py +++ b/src/bonsai/bonsai/bim/module/light/prop.py @@ -73,6 +73,8 @@ def update_sun_path_size(self, context): def update_display_shadows(self, context): if self.display_shadows: + if context.scene.BIMSolarProperties.traverse_transparent: + context.scene.BIMSolarProperties.traverse_transparent = False update_sun_path(self) context.scene.render.engine = "BLENDER_WORKBENCH" context.scene.display.shading.light = "FLAT" @@ -86,6 +88,27 @@ def update_display_shadows(self, context): space = tool.Blender.get_view3d_space() space.shading.type = "SOLID" +def update_traverse_transparent(self, context): + if self.traverse_transparent: + if context.scene.BIMSolarProperties.display_shadows: + context.scene.BIMSolarProperties.display_shadows = False + if context.scene.sun_pos_properties.sun_object is None: + bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1)) + bpy.ops.object.move_to_collection(collection_index=0) + context.scene.sun_pos_properties.sun_object = bpy.context.active_object + update_sun_path(self) + context.scene.render.engine = "BLENDER_EEVEE_NEXT" + context.scene.display.shading.light = "FLAT" + context.scene.display.shading.show_shadows = True + context.scene.display.shading.show_object_outline = True + context.scene.display.shadow_focus = 1.0 + context.scene.view_settings.view_transform = "Standard" # Preserve shading colours + space = tool.Blender.get_view3d_space() + space.shading.type = "RENDERED" + else: + context.scene.render.engine = "BLENDER_WORKBENCH" + space = tool.Blender.get_view3d_space() + space.shading.type = "SOLID" def update_display_sun_path(self, context): if self.display_sun_path: @@ -400,6 +423,14 @@ class BIMSolarProperties(PropertyGroup): description="Enables a visual style to display shadows easily", update=update_display_shadows, ) + + traverse_transparent: BoolProperty( + name="Enable Sun: Shadows and Light traversal of transparent objects", + default=False, + description="Enables a visual style so Sun light can traverse transparent objects and cast shadows (it toggles the render engine to EEVEE)", + update=update_traverse_transparent, + ) + display_sun_path: BoolProperty( name="Display Sun Path", default=False, diff --git a/src/bonsai/bonsai/bim/module/light/ui.py b/src/bonsai/bonsai/bim/module/light/ui.py index 37aa632372..1ecdd009c4 100644 --- a/src/bonsai/bonsai/bim/module/light/ui.py +++ b/src/bonsai/bonsai/bim/module/light/ui.py @@ -239,5 +239,11 @@ class BIM_PT_solar(bpy.types.Panel): row.prop(props, "display_shadows", icon="SHADING_RENDERED") row.prop(context.scene.display.shading, "shadow_intensity", text="Shadow Intensity") + row = self.layout.row(align=True) + row.prop(props, "traverse_transparent", icon="SHADING_RENDERED") + if props.traverse_transparent: + row = self.layout.row(align=True) + row.prop(context.scene.sun_pos_properties.sun_object.data , "energy", text="Sun Intensity") + row = self.layout.row(align=True) row.operator("bim.view_from_sun", icon="LIGHT_HEMI") From 52fbe09dd42a97654bf9566362886bc173a049e1 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 Mar 2025 11:06:56 +1100 Subject: [PATCH 333/476] Fix syntax error otherwise nothing loads Also PortData is loaded in the authoring tool and there might not be any active object (e.g. fresh session). --- src/bonsai/bonsai/bim/module/model/profile.py | 7 ++++--- src/bonsai/bonsai/bim/module/system/data.py | 8 +++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index bafbc2ae72..413546110d 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -1154,10 +1154,11 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato if element1.is_a("IfcFlowSegment") or element1.is_a("IfcFlowFitting"): # lazy import to avoid circular import errors from bonsai.bim.module.model.mep import MEPGenerator + MEPGenerator().setup_ports(profile1["obj"]) - else: - for profile1, profile2 in zip(profiles[:-1], profiles[1:]): - DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"]) + else: + for profile1, profile2 in zip(profiles[:-1], profiles[1:]): + DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"]) def modal(self, context, event): return IfcStore.execute_ifc_operator(self, context, event, method="MODAL") diff --git a/src/bonsai/bonsai/bim/module/system/data.py b/src/bonsai/bonsai/bim/module/system/data.py index ac328014e0..d630f14af1 100644 --- a/src/bonsai/bonsai/bim/module/system/data.py +++ b/src/bonsai/bonsai/bim/module/system/data.py @@ -130,11 +130,9 @@ class PortData: @classmethod def load(cls): - obj = bpy.context.active_object - assert obj - element = tool.Ifc.get_entity(obj) - assert element - cls.element = element + cls.element = None + if obj := bpy.context.active_object: + cls.element = tool.Ifc.get_entity(obj) is_port = cls.is_port() cls.data = { "total_ports": cls.total_ports(), From 5cb9e1c68dc660a067692e3d297170eb91724410 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 Mar 2025 11:09:42 +1100 Subject: [PATCH 334/476] Minor tweak to UI to make the shadow UI more elegant and reflect IFC style types --- src/bonsai/bonsai/bim/module/light/prop.py | 51 +++++++++++----------- src/bonsai/bonsai/bim/module/light/ui.py | 15 ++++--- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/light/prop.py b/src/bonsai/bonsai/bim/module/light/prop.py index 1f2c17b898..c7b243e367 100644 --- a/src/bonsai/bonsai/bim/module/light/prop.py +++ b/src/bonsai/bonsai/bim/module/light/prop.py @@ -71,10 +71,8 @@ def update_sun_path_size(self, context): update_sun_path(self) -def update_display_shadows(self, context): - if self.display_shadows: - if context.scene.BIMSolarProperties.traverse_transparent: - context.scene.BIMSolarProperties.traverse_transparent = False +def update_shadow_mode(self, context): + if self.shadow_mode == "SHADING": update_sun_path(self) context.scene.render.engine = "BLENDER_WORKBENCH" context.scene.display.shading.light = "FLAT" @@ -84,14 +82,7 @@ def update_display_shadows(self, context): context.scene.view_settings.view_transform = "Standard" # Preserve shading colours space = tool.Blender.get_view3d_space() space.shading.type = "RENDERED" - else: - space = tool.Blender.get_view3d_space() - space.shading.type = "SOLID" - -def update_traverse_transparent(self, context): - if self.traverse_transparent: - if context.scene.BIMSolarProperties.display_shadows: - context.scene.BIMSolarProperties.display_shadows = False + elif self.shadow_mode == "RENDERING": if context.scene.sun_pos_properties.sun_object is None: bpy.ops.object.light_add(type="SUN", radius=1, align="WORLD", location=(0, 0, 0), scale=(1, 1, 1)) bpy.ops.object.move_to_collection(collection_index=0) @@ -106,10 +97,10 @@ def update_traverse_transparent(self, context): space = tool.Blender.get_view3d_space() space.shading.type = "RENDERED" else: - context.scene.render.engine = "BLENDER_WORKBENCH" space = tool.Blender.get_view3d_space() space.shading.type = "SOLID" + def update_display_sun_path(self, context): if self.display_sun_path: update_sun_path(self) @@ -417,20 +408,28 @@ class BIMSolarProperties(PropertyGroup): azimuth: FloatProperty(name="Azimuth") elevation: FloatProperty(name="Elevation") UTC_zone: FloatProperty(name="UTC Zone") - display_shadows: BoolProperty( - name="Display Shadows", - default=False, - description="Enables a visual style to display shadows easily", - update=update_display_shadows, + shadow_mode: bpy.props.EnumProperty( + items=( + ("NONE", "No Shadows", "No shadows"), + ( + "SHADING", + "Shaded", + "Fast shadows sufficient for external shadow analysis based on shading styles", + "SHADING_SOLID", + 1, + ), + ( + "RENDERING", + "Rendered", + "Raycast (Eevee) shadows considering transparency based on rendering styles", + "SHADING_RENDERED", + 2, + ), + ), + name="Shadow Mode", + description="How to display shadows in the scene", + update=update_shadow_mode, ) - - traverse_transparent: BoolProperty( - name="Enable Sun: Shadows and Light traversal of transparent objects", - default=False, - description="Enables a visual style so Sun light can traverse transparent objects and cast shadows (it toggles the render engine to EEVEE)", - update=update_traverse_transparent, - ) - display_sun_path: BoolProperty( name="Display Sun Path", default=False, diff --git a/src/bonsai/bonsai/bim/module/light/ui.py b/src/bonsai/bonsai/bim/module/light/ui.py index 1ecdd009c4..3d5a1350cf 100644 --- a/src/bonsai/bonsai/bim/module/light/ui.py +++ b/src/bonsai/bonsai/bim/module/light/ui.py @@ -236,14 +236,15 @@ class BIM_PT_solar(bpy.types.Panel): row.operator("bim.move_sun_path_to_3d_cursor") row = self.layout.row(align=True) - row.prop(props, "display_shadows", icon="SHADING_RENDERED") - row.prop(context.scene.display.shading, "shadow_intensity", text="Shadow Intensity") + # row.prop(props, "display_shadows", icon="SHADING_RENDERED") + row.prop(props, "shadow_mode", icon="SHADING_RENDERED", expand=True) - row = self.layout.row(align=True) - row.prop(props, "traverse_transparent", icon="SHADING_RENDERED") - if props.traverse_transparent: - row = self.layout.row(align=True) - row.prop(context.scene.sun_pos_properties.sun_object.data , "energy", text="Sun Intensity") + if props.shadow_mode == "SHADING": + row = self.layout.row() + row.prop(context.scene.display.shading, "shadow_intensity", text="Shadow Intensity") + elif props.shadow_mode == "RENDERING": + row = self.layout.row() + row.prop(context.scene.sun_pos_properties.sun_object.data, "energy", text="Sun Intensity") row = self.layout.row(align=True) row.operator("bim.view_from_sun", icon="LIGHT_HEMI") From 3852e3c63f6c609b84d32a9c0fb48848347a9615 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 Mar 2025 22:06:21 +1100 Subject: [PATCH 335/476] Add tests for new light rendered shadows feature --- src/bonsai/bonsai/bim/module/light/ui.py | 1 - src/bonsai/bonsai/bim/module/style/ui.py | 4 ++-- src/bonsai/test/bim/feature/light.feature | 19 +++++++++++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/light/ui.py b/src/bonsai/bonsai/bim/module/light/ui.py index 3d5a1350cf..8cbb637e06 100644 --- a/src/bonsai/bonsai/bim/module/light/ui.py +++ b/src/bonsai/bonsai/bim/module/light/ui.py @@ -236,7 +236,6 @@ class BIM_PT_solar(bpy.types.Panel): row.operator("bim.move_sun_path_to_3d_cursor") row = self.layout.row(align=True) - # row.prop(props, "display_shadows", icon="SHADING_RENDERED") row.prop(props, "shadow_mode", icon="SHADING_RENDERED", expand=True) if props.shadow_mode == "SHADING": diff --git a/src/bonsai/bonsai/bim/module/style/ui.py b/src/bonsai/bonsai/bim/module/style/ui.py index a5042fa800..e7fc9676b0 100644 --- a/src/bonsai/bonsai/bim/module/style/ui.py +++ b/src/bonsai/bonsai/bim/module/style/ui.py @@ -170,8 +170,8 @@ class BIM_PT_styles(Panel): row.prop(self.props, "reflectance_method") if self.props.reflectance_method not in ("PHYSICAL", "NOTDEFINED", "FLAT"): - self.layout.label(text=f"Supported reflectance methods are:") - self.layout.label(text=f"PHYSICAL / NOTDEFINED / FLAT") + self.layout.label(text="Supported reflectance methods are:") + self.layout.label(text="PHYSICAL / NOTDEFINED / FLAT") row = self.layout.row(align=True) row.label(text="Emissive" if self.props.reflectance_method == "FLAT" else "Diffuse") diff --git a/src/bonsai/test/bim/feature/light.feature b/src/bonsai/test/bim/feature/light.feature index d98a230b59..04559e4729 100644 --- a/src/bonsai/test/bim/feature/light.feature +++ b/src/bonsai/test/bim/feature/light.feature @@ -44,11 +44,26 @@ Scenario: Display the sun path And I set the "Sun Path Size" property to "100.0" Then nothing happens -Scenario: Display shadows +Scenario: See no shadows + Given an empty IFC project + When I look at the "Solar Access / Shadow" panel + Then I don't see "Sun Intensity" + And I don't see "Shadow Intensity" + +Scenario: Display shaded shadows Given an empty IFC project And I look at the "Solar Access / Shadow" panel - When I click "Display Shadows" + When I set the "Shadow Mode" property to "Shaded" And I set the "Shadow Intensity" property to "1.0" + And I don't see "Sun Intensity" + Then nothing happens + +Scenario: Display rendered shadows + Given an empty IFC project + And I look at the "Solar Access / Shadow" panel + When I set the "Shadow Mode" property to "Rendered" + And I set the "Sun Intensity" property to "1.0" + And I don't see "Shadow Intensity" Then nothing happens Scenario: View from sun From 609f468ce1802091c5fb29e89b38b9f6e4b07b30 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 13 Mar 2025 22:07:00 +1100 Subject: [PATCH 336/476] Update failing tests --- src/bonsai/test/bim/feature/spatial.feature | 18 ++++++++++++++++-- src/bonsai/test/bim/feature/style.feature | 15 ++++++++------- src/bonsai/test/bim/test_feature.py | 3 +-- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/bonsai/test/bim/feature/spatial.feature b/src/bonsai/test/bim/feature/spatial.feature index 604159f002..64dbbd2f12 100644 --- a/src/bonsai/test/bim/feature/spatial.feature +++ b/src/bonsai/test/bim/feature/spatial.feature @@ -103,8 +103,7 @@ Scenario: Select similar container Scenario: Execute generate space from cursor position Given an empty IFC project - When I press "bim.generate_space" - Then nothing happens + Then I press "bim.generate_space" and expect error "Error: Couldn't find any polygons to form the space shape. Perhaps, RL value need to be adjusted." Scenario: Execute generate spaces from walls Given an empty IFC project @@ -133,6 +132,11 @@ Scenario: Spatial decomposition - see panel Scenario: Isolate spatial container Given an empty IFC project + And I trigger "Add Element" + And I set the "Definition" property to "IfcElement" + And I set the "Class" property to "IfcFurniture" + And I set the "Representation" property to "No Geometry" + And I click "OK" And I look at the "Spatial Decomposition" panel When I select the "My Site" item in the "BIM_UL_containers_manager" list And I click "Isolate" @@ -140,6 +144,11 @@ Scenario: Isolate spatial container Scenario: Show spatial container Given an empty IFC project + And I trigger "Add Element" + And I set the "Definition" property to "IfcElement" + And I set the "Class" property to "IfcFurniture" + And I set the "Representation" property to "No Geometry" + And I click "OK" And I look at the "Spatial Decomposition" panel When I select the "My Site" item in the "BIM_UL_containers_manager" list And I click "HIDE_OFF" @@ -147,6 +156,11 @@ Scenario: Show spatial container Scenario: Hide spatial container Given an empty IFC project + And I trigger "Add Element" + And I set the "Definition" property to "IfcElement" + And I set the "Class" property to "IfcFurniture" + And I set the "Representation" property to "No Geometry" + And I click "OK" And I look at the "Spatial Decomposition" panel When I select the "My Site" item in the "BIM_UL_containers_manager" list And I click "HIDE_ON" diff --git a/src/bonsai/test/bim/feature/style.feature b/src/bonsai/test/bim/feature/style.feature index 7e4c70b35f..68912ace0a 100644 --- a/src/bonsai/test/bim/feature/style.feature +++ b/src/bonsai/test/bim/feature/style.feature @@ -49,14 +49,15 @@ Scenario: Remove style Scenario: Edit style Given an empty IFC project - And I press "bim.load_styles(style_type='IfcSurfaceStyle')" - And I press "bim.enable_adding_presentation_style" - And I set "scene.BIMStylesProperties.style_name" to "Style" - And I press "bim.add_presentation_style" + And I look at the "Styles" panel + And I click "IMPORT" + And I click "ADD" + And I set the "Name" property to "Style" + And I click "Save New Style" And the variable "style" is "{ifc}.by_type('IfcSurfaceStyle')[0].id()" - And I press "bim.enable_editing_style(style={style})" - And I set "scene.BIMStylesProperties.attributes[0].string_value" to "NewStyle" - When I press "bim.edit_style" + And I click "GREASEPENCIL" + And I set the "Name" property to "NewStyle" + When I click "Save Attributes" Then the material "Style" does not exist And the material "NewStyle" exists diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 4f263f93c9..9cc71bd4f5 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -566,8 +566,7 @@ def i_click_button(button): # Clicked confirm on an operator's draw dialog return i_press_operator(panel_spy.panel.bl_idname) debug = "\n".join([f"{i} {v}" for i, v in enumerate(panel_spy.spied_operators)]) - if not debug: - debug = f"No buttons were found, here is the text we see: {panel_spy.spied_labels}" + debug += f"\nHere is the text we see: {panel_spy.spied_labels}" assert False, f"Could not find {button}:\n{debug}" From ef67d866b637a88f746123dc052fc33469493846 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 13 Mar 2025 18:43:14 +0500 Subject: [PATCH 337/476] black . --- src/bonsai/bonsai/bim/module/group/operator.py | 12 +++++++++--- src/bonsai/bonsai/tool/snap.py | 4 ++-- src/bonsai/bonsai/tool/system.py | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/group/operator.py b/src/bonsai/bonsai/bim/module/group/operator.py index 113b520e7f..6932ec6f78 100644 --- a/src/bonsai/bonsai/bim/module/group/operator.py +++ b/src/bonsai/bonsai/bim/module/group/operator.py @@ -35,13 +35,14 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator): self.expanded_groups = json.loads(context.scene.ExpandedGroups.json_string) self.props.groups.clear() - groups = [group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.HasAssignments] + groups = [ + group for group in tool.Ifc.get().by_type("IfcGroup", include_subtypes=False) if not group.HasAssignments + ] sorted_groups = sorted(groups, key=lambda group: group.Name or "Unnamed") for group in sorted_groups: self.load_group(group) - self.props.is_editing = True bpy.ops.bim.disable_editing_group() return {"FINISHED"} @@ -54,7 +55,12 @@ class LoadGroups(bpy.types.Operator, tool.Ifc.Operator): new.has_children = False new.is_expanded = group.id() in self.expanded_groups - related_groups = [related_object for rel in group.IsGroupedBy or [] for related_object in rel.RelatedObjects if related_object.is_a("IfcGroup")] + related_groups = [ + related_object + for rel in group.IsGroupedBy or [] + for related_object in rel.RelatedObjects + if related_object.is_a("IfcGroup") + ] sorted_related_groups = sorted(related_groups, key=lambda group: group.Name or "Unnamed") for related_group in sorted_related_groups: diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 3e2e68db97..680766b197 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -331,7 +331,7 @@ class Snap(bonsai.core.tool.Snap): hit = None face_index = None # Wireframes - if obj.type in {"EMPTY", "CURVE"} or (hasattr(obj.data, "polygons") and len(obj.data.polygons) == 0) : + if obj.type in {"EMPTY", "CURVE"} or (hasattr(obj.data, "polygons") and len(obj.data.polygons) == 0): snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj) if snap_points: hit = sorted(snap_points, key=lambda x: x["distance"])[0]["point"] @@ -569,7 +569,7 @@ class Snap(bonsai.core.tool.Snap): snap["distance"] *= weight_factor / 5 if snap["type"] == "Edge Intersection": snap["distance"] *= weight_factor / 10 - if snap["type"] in ["Plane", "Axis", "Face"]: + if snap["type"] in ["Plane", "Axis", "Face"]: snap["distance"] = weight_factor / 50 return sorted(snapping_points, key=lambda x: x["distance"]) diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index fe4013195f..649cd22a2b 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -344,13 +344,13 @@ class System(bonsai.core.tool.System): edge_ortho = obj.matrix_world.col[j].to_3d().normalized() second_ortho = edge_dir.cross(edge_ortho) edge_ortho = second_ortho.cross(edge_dir) - + # direction lines should be around the edge center n_direction_lines, start_offset = divmod(edge_length, direction_lines_offset) n_direction_lines = int(n_direction_lines) + 1 start_offset /= 2 start_offset = edge_dir * start_offset + base_vert - + if both_directions: cur_vert_index = start_vert_i + len(port_data) + j * 2 * n_direction_lines else: From 5be076cb961224f08c361444df3d0a38a2053218 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 13 Mar 2025 11:22:00 +0500 Subject: [PATCH 338/476] Reset operator context in 63aada25dd for clarity --- src/bonsai/bonsai/bim/module/model/ui.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 57950f7459..9154e0dff6 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bl_ui_utils import bonsai.bim import bonsai.tool as tool from bpy.types import Panel, Menu @@ -54,9 +55,9 @@ class BIM_MT_type_menu(bpy.types.Menu): def draw(self, context): props = tool.Model.get_model_props() layout = self.layout - layout.operator_context = "INVOKE_REGION_WIN" - op = layout.operator("bim.rename_type", icon="GREASEPENCIL", text="Rename Type") - op.element = props.menu_relating_type_id + with bl_ui_utils.layout.operator_context(layout, "INVOKE_REGION_WIN"): + op = layout.operator("bim.rename_type", icon="GREASEPENCIL", text="Rename Type") + op.element = props.menu_relating_type_id op = layout.operator("bim.select_type", icon="OBJECT_DATA") op.relating_type = props.menu_relating_type_id op = layout.operator("bim.duplicate_type", icon="DUPLICATE") From 4eae11208749314d5762b85e95b152c91dd46ae4 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 13 Mar 2025 16:56:11 +0500 Subject: [PATCH 339/476] not real ifc4x3 quantities, just adding ifc4 quantities for diff --- src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json | 637 ++++++++++++++++++ .../ifc5d/IFC4X3QtoBaseQuantitiesBlender.json | 637 ++++++++++++++++++ 2 files changed, 1274 insertions(+) create mode 100644 src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json create mode 100644 src/ifc5d/ifc5d/IFC4X3QtoBaseQuantitiesBlender.json diff --git a/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json b/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json new file mode 100644 index 0000000000..2d7e54faab --- /dev/null +++ b/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json @@ -0,0 +1,637 @@ +{ + "name": "IFC4X3 Base Quantities - IfcOpenShell", + "description": "This ruleset quantifies every single possible standardised base quantity in IFC4X3 using only IfcOpenShell as a geometry processor.", + "calculators": { + "IfcOpenShell": { + "IfcActuator": { + "Qto_ActuatorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcAirTerminal": { + "Qto_AirTerminalBaseQuantities": { + "GrossWeight": null, + "Perimeter": null, + "TotalSurfaceArea": null + } + }, + "IfcAirTerminalBox": { + "Qto_AirTerminalBoxTypeBaseQuantities": { + "GrossWeight": null + } + }, + "IfcAirToAirHeatRecovery": { + "Qto_AirToAirHeatRecoveryBaseQuantities": { + "GrossWeight": null + } + }, + "IfcAlarm": { + "Qto_AlarmBaseQuantities": { + "GrossWeight": null + } + }, + "IfcAudioVisualAppliance": { + "Qto_AudioVisualApplianceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcBeam": { + "Qto_BeamBaseQuantities": { + "CrossSectionArea": null, + "GrossSurfaceArea": "gross_get_area", + "GrossVolume": "gross_get_volume", + "GrossWeight": null, + "Length": "net_get_max_xyz", + "NetSurfaceArea": "net_get_area", + "NetVolume": "net_get_volume", + "NetWeight": null, + "OuterSurfaceArea": "net_get_outer_surface_area" + } + }, + "IfcBoiler": { + "Qto_BoilerBaseQuantities": { + "GrossWeight": null, + "NetWeight": null, + "TotalSurfaceArea": null + } + }, + "IfcBuilding": { + "Qto_BuildingBaseQuantities": { + "EavesHeight": null, + "FootprintArea": null, + "GrossFloorArea": null, + "GrossVolume": null, + "Height": null, + "NetFloorArea": null, + "NetVolume": null + } + }, + "IfcBuildingElementProxy": { + "Qto_BuildingElementProxyQuantities": { + "NetSurfaceArea": null, + "NetVolume": null + } + }, + "IfcBuildingStorey": { + "Qto_BuildingStoreyBaseQuantities": { + "GrossFloorArea": null, + "GrossHeight": null, + "GrossPerimeter": null, + "GrossVolume": null, + "NetFloorArea": null, + "NetHeigtht": null, + "NetVolume": null + } + }, + "IfcBurner": { + "Qto_BurnerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCableCarrierFitting": { + "Qto_CableCarrierFittingBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCableCarrierSegment": { + "Qto_CableCarrierSegmentBaseQuantities": { + "CrossSectionArea": null, + "GrossWeight": null, + "Length": "net_get_segment_length", + "OuterSurfaceArea": null + } + }, + "IfcCableFitting": { + "Qto_CableFittingBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCableSegment": { + "Qto_CableSegmentBaseQuantities": { + "CrossSectionArea": null, + "GrossWeight": null, + "Length": "net_get_segment_length", + "OuterSurfaceArea": null + } + }, + "IfcChiller": { + "Qto_ChillerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcChimney": { + "Qto_ChimneyBaseQuantities": { + "Length": "net_get_max_xyz" + } + }, + "IfcCoil": { + "Qto_CoilBaseQuantities": { + "GrossWeight": null + } + }, + "IfcColumn": { + "Qto_ColumnBaseQuantities": { + "CrossSectionArea": null, + "GrossSurfaceArea": "gross_get_area", + "GrossVolume": "gross_get_volume", + "GrossWeight": null, + "Length": "net_get_max_xyz", + "NetSurfaceArea": "net_get_area", + "NetVolume": "net_get_volume", + "NetWeight": null, + "OuterSurfaceArea": "net_get_outer_surface_area" + } + }, + "IfcCommunicationsAppliance": { + "Qto_CommunicationsApplianceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCompressor": { + "Qto_CompressorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCondenser": { + "Qto_CondenserBaseQuantities": { + "GrossWeight": null + } + }, + "IfcConstructionEquipmentResource": { + "Qto_ConstructionEquipmentResourceBaseQuantities": { + "OperatingTime": null, + "UsageTime": null + } + }, + "IfcConstructionMaterialResource": { + "Qto_ConstructionMaterialResourceBaseQuantities": { + "GrossVolume": null, + "GrossWeight": null, + "NetVolume": null, + "NetWeight": null + } + }, + "IfcController": { + "Qto_ControllerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCooledBeam": { + "Qto_CooledBeamBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCoolingTower": { + "Qto_CoolingTowerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCovering": { + "Qto_CoveringBaseQuantities": { + "GrossArea": "gross_get_max_side_area", + "NetArea": "net_get_max_side_area", + "Width": "gross_get_min_xyz" + } + }, + "IfcCurtainWall": { + "Qto_CurtainWallQuantities": { + "GrossSideArea": null, + "Height": null, + "Length": null, + "NetSideArea": null, + "Width": null + } + }, + "IfcDamper": { + "Qto_DamperBaseQuantities": { + "GrossWeight": null + } + }, + "IfcDistributionChamberElement": { + "Qto_DistributionChamberElementBaseQuantities": { + "GrossSurfaceArea": null, + "GrossVolume": null, + "NetSurfaceArea": null, + "NetVolume": null + } + }, + "IfcDoor": { + "Qto_DoorBaseQuantities": { + "Area": null, + "Height": null, + "Perimeter": null, + "Width": null + } + }, + "IfcDuctFitting": { + "Qto_DuctFittingBaseQuantities": { + "GrossCrossSectionArea": null, + "GrossWeight": null, + "Length": null, + "NetCrossSectionArea": null, + "OuterSurfaceArea": null + } + }, + "IfcDuctSegment": { + "Qto_DuctSegmentBaseQuantities": { + "GrossCrossSectionArea": null, + "GrossWeight": null, + "Length": "net_get_segment_length", + "NetCrossSectionArea": null, + "OuterSurfaceArea": null + } + }, + "IfcDuctSilencer": { + "Qto_DuctSilencerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcElectricAppliance": { + "Qto_ElectricApplianceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcElectricDistributionBoard": { + "Qto_ElectricDistributionBoardBaseQuantities": { + "GrossWeight": null, + "NumberOfCircuits": null + } + }, + "IfcElectricFlowStorageDevice": { + "Qto_ElectricFlowStorageDeviceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcElectricGenerator": { + "Qto_ElectricGeneratorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcElectricMotor": { + "Qto_ElectricMotorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcElectricTimeControl": { + "Qto_ElectricTimeControlBaseQuantities": { + "GrossWeight": null + } + }, + "IfcEvaporativeCooler": { + "Qto_EvaporativeCoolerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcEvaporator": { + "Qto_EvaporatorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFan": { + "Qto_FanBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFilter": { + "Qto_FilterBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFireSuppressionTerminal": { + "Qto_FireSuppressionTerminalBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFlowInstrument": { + "Qto_FlowInstrumentBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFlowMeter": { + "Qto_FlowMeterBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFooting": { + "Qto_FootingBaseQuantities": { + "CrossSectionArea": null, + "GrossSurfaceArea": null, + "GrossVolume": null, + "GrossWeight": null, + "Height": "net_get_z", + "Length": "net_get_max_xy", + "NetVolume": "net_get_volume", + "NetWeight": null, + "OuterSurfaceArea": null, + "Width": null + } + }, + "IfcHeatExchanger": { + "Qto_HeatExchangerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcHumidifier": { + "Qto_HumidifierBaseQuantities": { + "GrossWeight": null + } + }, + "IfcInterceptor": { + "Qto_InterceptorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcJunctionBox": { + "Qto_JunctionBoxBaseQuantities": { + "GrossWeight": null, + "NumberOfGangs": null + } + }, + "IfcLaborResource": { + "Qto_LaborResourceBaseQuantities": { + "OvertimeWork": null, + "StandardWork": null + } + }, + "IfcLamp": { + "Qto_LampBaseQuantities": { + "GrossWeight": null + } + }, + "IfcLightFixture": { + "Qto_LightFixtureBaseQuantities": { + "GrossWeight": null + } + }, + "IfcMember": { + "Qto_MemberBaseQuantities": { + "CrossSectionArea": null, + "GrossSurfaceArea": "gross_get_area", + "GrossVolume": "gross_get_volume", + "GrossWeight": null, + "Length": "net_get_max_xyz", + "NetSurfaceArea": "net_get_area", + "NetVolume": "net_get_volume", + "NetWeight": null, + "OuterSurfaceArea": "net_get_outer_surface_area" + } + }, + "IfcMotorConnection": { + "Qto_MotorConnectionBaseQuantities": { + "GrossWeight": null + } + }, + "IfcOpeningElement": { + "Qto_OpeningElementBaseQuantities": { + "Area": "gross_get_max_side_area", + "Depth": "gross_get_z", + "Height": "gross_get_y", + "Volume": "gross_get_volume", + "Width": "gross_get_x" + } + }, + "IfcOutlet": { + "Qto_OutletBaseQuantities": { + "GrossWeight": null + } + }, + "IfcPile": { + "Qto_PileBaseQuantities": { + "CrossSectionArea": null, + "GrossSurfaceArea": null, + "GrossVolume": null, + "GrossWeight": null, + "Length": "net_get_z", + "NetVolume": null, + "NetWeight": null, + "OuterSurfaceArea": null + } + }, + "IfcPipeFitting": { + "Qto_PipeFittingBaseQuantities": { + "GrossCrossSectionArea": null, + "GrossWeight": null, + "Length": null, + "NetCrossSectionArea": null, + "NetWeight": null, + "OuterSurfaceArea": null + } + }, + "IfcPipeSegment": { + "Qto_PipeSegmentBaseQuantities": { + "GrossCrossSectionArea": null, + "GrossWeight": null, + "Length": "net_get_segment_length", + "NetCrossSectionArea": null, + "NetWeight": null, + "OuterSurfaceArea": null + } + }, + "IfcPlate": { + "Qto_PlateBaseQuantities": { + "GrossArea": "gross_get_max_side_area", + "GrossVolume": "gross_get_volume", + "GrossWeight": null, + "NetArea": "net_get_max_side_area", + "NetVolume": "net_get_volume", + "NetWeight": null, + "Perimeter": null, + "Width": "net_get_min_xyz" + } + }, + "IfcProjectionElement": { + "Qto_ProjectionElementBaseQuantities": { + "Area": null, + "Volume": null + } + }, + "IfcProtectiveDevice": { + "Qto_ProtectiveDeviceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcProtectiveDeviceTrippingUnit": { + "Qto_ProtectiveDeviceTrippingUnitBaseQuantities": { + "GrossWeight": null + } + }, + "IfcPump": { + "Qto_PumpBaseQuantities": { + "GrossWeight": null + } + }, + "IfcRailing": { + "Qto_RailingBaseQuantities": { + "Length": null + } + }, + "IfcRampFlight": { + "Qto_RampFlightBaseQuantities": { + "GrossArea": null, + "GrossVolume": null, + "Length": null, + "NetArea": null, + "NetVolume": null, + "Width": null + } + }, + "IfcReinforcingElement": { + "Qto_ReinforcingElementBaseQuantities": { + "Count": null, + "Length": null, + "Weight": null + } + }, + "IfcRoof": { + "Qto_RoofBaseQuantities": { + "GrossArea": "gross_get_top_area", + "NetArea": "net_get_top_area", + "ProjectedArea": null + } + }, + "IfcSanitaryTerminal": { + "Qto_SanitaryTerminalBaseQuantities": { + "GrossWeight": null + } + }, + "IfcSensor": { + "Qto_SensorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcSite": { + "Qto_SiteBaseQuantities": { + "GrossArea": null, + "GrossPerimeter": null + } + }, + "IfcSlab": { + "Qto_SlabBaseQuantities": { + "Depth": "net_get_z", + "GrossArea": "gross_get_footprint_area", + "GrossVolume": "gross_get_volume", + "GrossWeight": null, + "Length": "net_get_x", + "NetArea": "net_get_footprint_area", + "NetVolume": "net_get_volume", + "NetWeight": null, + "Perimeter": "net_get_footprint_perimeter", + "Width": "net_get_y" + } + }, + "IfcSolarDevice": { + "Qto_SolarDeviceBaseQuantities": { + "GrossArea": null, + "GrossWeight": null + } + }, + "IfcSpace": { + "Qto_SpaceBaseQuantities": { + "FinishCeilingHeight": null, + "FinishFloorHeight": null, + "GrossCeilingArea": null, + "GrossFloorArea": null, + "GrossPerimeter": null, + "GrossVolume": null, + "GrossWallArea": null, + "Height": null, + "NetCeilingArea": null, + "NetFloorArea": null, + "NetPerimeter": null, + "NetVolume": null, + "NetWallArea": null + } + }, + "IfcSpaceHeater": { + "Qto_SpaceHeaterBaseQuantities": { + "GrossWeight": null, + "Length": null, + "NetWeight": null + } + }, + "IfcStackTerminal": { + "Qto_StackTerminalBaseQuantities": { + "GrossWeight": null + } + }, + "IfcStairFlight": { + "Qto_StairFlightBaseQuantities": { + "GrossVolume": null, + "Length": "net_get_max_xy", + "NetVolume": "net_get_volume" + } + }, + "IfcSwitchingDevice": { + "Qto_SwitchingDeviceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcTank": { + "Qto_TankBaseQuantities": { + "GrossWeight": null, + "NetWeight": null, + "TotalSurfaceArea": null + } + }, + "IfcTransformer": { + "Qto_TransformerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcTubeBundle": { + "Qto_TubeBundleBaseQuantities": { + "GrossWeight": null, + "NetWeight": null + } + }, + "IfcUnitaryControlElement": { + "Qto_UnitaryControlElementBaseQuantities": { + "GrossWeight": null + } + }, + "IfcUnitaryEquipment": { + "Qto_UnitaryEquipmentBaseQuantities": { + "GrossWeight": null + } + }, + "IfcValve": { + "Qto_ValveBaseQuantities": { + "GrossWeight": null + } + }, + "IfcVibrationIsolator": { + "Qto_VibrationIsolatorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcWall": { + "Qto_WallBaseQuantities": { + "GrossFootprintArea": null, + "GrossSideArea": "gross_get_side_area", + "GrossVolume": "gross_get_volume", + "GrossWeight": null, + "Height": "net_get_z", + "Length": "net_get_x", + "NetFootprintArea": null, + "NetSideArea": "net_get_side_area", + "NetVolume": "net_get_volume", + "NetWeight": null, + "Width": "net_get_y" + } + }, + "IfcWasteTerminal": { + "Qto_WasteTerminalBaseQuantities": { + "GrossWeight": null + } + }, + "IfcWindow": { + "Qto_WindowBaseQuantities": { + "Area": "net_get_max_side_area", + "Height": "net_get_z", + "Perimeter": null, + "Width": "net_get_x" + } + } + } + } +} diff --git a/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantitiesBlender.json b/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantitiesBlender.json new file mode 100644 index 0000000000..f5e7c628de --- /dev/null +++ b/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantitiesBlender.json @@ -0,0 +1,637 @@ +{ + "name": "IFC4X3 Base Quantities - Blender", + "description": "This ruleset quantifies every single possible standardised base quantity in IFC4X3 using Blender.", + "calculators": { + "Blender": { + "IfcActuator": { + "Qto_ActuatorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcAirTerminal": { + "Qto_AirTerminalBaseQuantities": { + "GrossWeight": null, + "Perimeter": null, + "TotalSurfaceArea": null + } + }, + "IfcAirTerminalBox": { + "Qto_AirTerminalBoxTypeBaseQuantities": { + "GrossWeight": null + } + }, + "IfcAirToAirHeatRecovery": { + "Qto_AirToAirHeatRecoveryBaseQuantities": { + "GrossWeight": null + } + }, + "IfcAlarm": { + "Qto_AlarmBaseQuantities": { + "GrossWeight": null + } + }, + "IfcAudioVisualAppliance": { + "Qto_AudioVisualApplianceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcBeam": { + "Qto_BeamBaseQuantities": { + "CrossSectionArea": "get_cross_section_area", + "GrossSurfaceArea": "get_gross_surface_area", + "GrossVolume": "get_gross_volume", + "GrossWeight": "get_gross_weight", + "Length": "get_length", + "NetSurfaceArea": "get_net_surface_area", + "NetVolume": "get_net_volume", + "NetWeight": "get_net_weight", + "OuterSurfaceArea": "get_outer_surface_area" + } + }, + "IfcBoiler": { + "Qto_BoilerBaseQuantities": { + "GrossWeight": null, + "NetWeight": null, + "TotalSurfaceArea": null + } + }, + "IfcBuilding": { + "Qto_BuildingBaseQuantities": { + "EavesHeight": null, + "FootprintArea": null, + "GrossFloorArea": null, + "GrossVolume": null, + "Height": null, + "NetFloorArea": null, + "NetVolume": null + } + }, + "IfcBuildingElementProxy": { + "Qto_BuildingElementProxyQuantities": { + "NetSurfaceArea": "get_net_surface_area", + "NetVolume": "get_net_volume" + } + }, + "IfcBuildingStorey": { + "Qto_BuildingStoreyBaseQuantities": { + "GrossFloorArea": null, + "GrossHeight": null, + "GrossPerimeter": null, + "GrossVolume": null, + "NetFloorArea": null, + "NetHeigtht": null, + "NetVolume": null + } + }, + "IfcBurner": { + "Qto_BurnerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCableCarrierFitting": { + "Qto_CableCarrierFittingBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCableCarrierSegment": { + "Qto_CableCarrierSegmentBaseQuantities": { + "CrossSectionArea": null, + "GrossWeight": null, + "Length": null, + "OuterSurfaceArea": null + } + }, + "IfcCableFitting": { + "Qto_CableFittingBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCableSegment": { + "Qto_CableSegmentBaseQuantities": { + "CrossSectionArea": null, + "GrossWeight": null, + "Length": "get_length", + "OuterSurfaceArea": "get_outer_surface_area" + } + }, + "IfcChiller": { + "Qto_ChillerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcChimney": { + "Qto_ChimneyBaseQuantities": { + "Length": "get_height" + } + }, + "IfcCoil": { + "Qto_CoilBaseQuantities": { + "GrossWeight": null + } + }, + "IfcColumn": { + "Qto_ColumnBaseQuantities": { + "CrossSectionArea": "get_cross_section_area", + "GrossSurfaceArea": "get_gross_surface_area", + "GrossVolume": "get_gross_volume", + "GrossWeight": "get_gross_weight", + "Length": "get_length", + "NetSurfaceArea": "get_net_surface_area", + "NetVolume": "get_net_volume", + "NetWeight": "get_net_weight", + "OuterSurfaceArea": "get_outer_surface_area" + } + }, + "IfcCommunicationsAppliance": { + "Qto_CommunicationsApplianceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCompressor": { + "Qto_CompressorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCondenser": { + "Qto_CondenserBaseQuantities": { + "GrossWeight": null + } + }, + "IfcConstructionEquipmentResource": { + "Qto_ConstructionEquipmentResourceBaseQuantities": { + "OperatingTime": null, + "UsageTime": null + } + }, + "IfcConstructionMaterialResource": { + "Qto_ConstructionMaterialResourceBaseQuantities": { + "GrossVolume": "get_gross_volume", + "GrossWeight": null, + "NetVolume": "get_net_volume", + "NetWeight": null + } + }, + "IfcController": { + "Qto_ControllerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCooledBeam": { + "Qto_CooledBeamBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCoolingTower": { + "Qto_CoolingTowerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcCovering": { + "Qto_CoveringBaseQuantities": { + "GrossArea": "get_covering_gross_area", + "NetArea": "get_covering_net_area", + "Width": "get_covering_width" + } + }, + "IfcCurtainWall": { + "Qto_CurtainWallQuantities": { + "GrossSideArea": null, + "Height": null, + "Length": null, + "NetSideArea": null, + "Width": null + } + }, + "IfcDamper": { + "Qto_DamperBaseQuantities": { + "GrossWeight": null + } + }, + "IfcDistributionChamberElement": { + "Qto_DistributionChamberElementBaseQuantities": { + "GrossSurfaceArea": "get_gross_surface_area", + "GrossVolume": "get_gross_volume", + "NetSurfaceArea": "get_net_surface_area", + "NetVolume": "get_net_volume" + } + }, + "IfcDoor": { + "Qto_DoorBaseQuantities": { + "Area": "get_net_side_area", + "Height": "get_height", + "Perimeter": "get_rectangular_perimeter", + "Width": "get_length" + } + }, + "IfcDuctFitting": { + "Qto_DuctFittingBaseQuantities": { + "GrossCrossSectionArea": null, + "GrossWeight": null, + "Length": "get_length", + "NetCrossSectionArea": null, + "OuterSurfaceArea": "get_outer_surface_area" + } + }, + "IfcDuctSegment": { + "Qto_DuctSegmentBaseQuantities": { + "GrossCrossSectionArea": null, + "GrossWeight": null, + "Length": "get_length", + "NetCrossSectionArea": null, + "OuterSurfaceArea": "get_outer_surface_area" + } + }, + "IfcDuctSilencer": { + "Qto_DuctSilencerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcElectricAppliance": { + "Qto_ElectricApplianceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcElectricDistributionBoard": { + "Qto_ElectricDistributionBoardBaseQuantities": { + "GrossWeight": null, + "NumberOfCircuits": null + } + }, + "IfcElectricFlowStorageDevice": { + "Qto_ElectricFlowStorageDeviceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcElectricGenerator": { + "Qto_ElectricGeneratorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcElectricMotor": { + "Qto_ElectricMotorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcElectricTimeControl": { + "Qto_ElectricTimeControlBaseQuantities": { + "GrossWeight": null + } + }, + "IfcEvaporativeCooler": { + "Qto_EvaporativeCoolerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcEvaporator": { + "Qto_EvaporatorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFan": { + "Qto_FanBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFilter": { + "Qto_FilterBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFireSuppressionTerminal": { + "Qto_FireSuppressionTerminalBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFlowInstrument": { + "Qto_FlowInstrumentBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFlowMeter": { + "Qto_FlowMeterBaseQuantities": { + "GrossWeight": null + } + }, + "IfcFooting": { + "Qto_FootingBaseQuantities": { + "CrossSectionArea": "get_cross_section_area", + "GrossSurfaceArea": "get_gross_surface_area", + "GrossVolume": "get_gross_volume", + "GrossWeight": "get_gross_weight", + "Height": "get_height", + "Length": "get_length", + "NetVolume": "get_net_volume", + "NetWeight": "get_net_weight", + "OuterSurfaceArea": "get_outer_surface_area", + "Width": "get_width" + } + }, + "IfcHeatExchanger": { + "Qto_HeatExchangerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcHumidifier": { + "Qto_HumidifierBaseQuantities": { + "GrossWeight": null + } + }, + "IfcInterceptor": { + "Qto_InterceptorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcJunctionBox": { + "Qto_JunctionBoxBaseQuantities": { + "GrossWeight": null, + "NumberOfGangs": null + } + }, + "IfcLaborResource": { + "Qto_LaborResourceBaseQuantities": { + "OvertimeWork": null, + "StandardWork": null + } + }, + "IfcLamp": { + "Qto_LampBaseQuantities": { + "GrossWeight": null + } + }, + "IfcLightFixture": { + "Qto_LightFixtureBaseQuantities": { + "GrossWeight": null + } + }, + "IfcMember": { + "Qto_MemberBaseQuantities": { + "CrossSectionArea": "get_cross_section_area", + "GrossSurfaceArea": "get_gross_surface_area", + "GrossVolume": "get_gross_volume", + "GrossWeight": "get_gross_weight", + "Length": "get_length", + "NetSurfaceArea": "get_net_surface_area", + "NetVolume": "get_net_volume", + "NetWeight": "get_net_weight", + "OuterSurfaceArea": "get_outer_surface_area" + } + }, + "IfcMotorConnection": { + "Qto_MotorConnectionBaseQuantities": { + "GrossWeight": null + } + }, + "IfcOpeningElement": { + "Qto_OpeningElementBaseQuantities": { + "Area": "get_opening_mapping_area", + "Depth": "get_opening_depth", + "Height": "get_opening_height", + "Volume": "get_net_volume", + "Width": "get_length" + } + }, + "IfcOutlet": { + "Qto_OutletBaseQuantities": { + "GrossWeight": null + } + }, + "IfcPile": { + "Qto_PileBaseQuantities": { + "CrossSectionArea": "get_cross_section_area", + "GrossSurfaceArea": "get_gross_surface_area", + "GrossVolume": "get_gross_volume", + "GrossWeight": "get_gross_weight", + "Length": "get_length", + "NetVolume": "get_net_volume", + "NetWeight": "get_net_weight", + "OuterSurfaceArea": "get_outer_surface_area" + } + }, + "IfcPipeFitting": { + "Qto_PipeFittingBaseQuantities": { + "GrossCrossSectionArea": null, + "GrossWeight": null, + "Length": null, + "NetCrossSectionArea": null, + "NetWeight": null, + "OuterSurfaceArea": null + } + }, + "IfcPipeSegment": { + "Qto_PipeSegmentBaseQuantities": { + "GrossCrossSectionArea": null, + "GrossWeight": "get_gross_weight", + "Length": "get_length", + "NetCrossSectionArea": "get_cross_section_area", + "NetWeight": "get_net_weight", + "OuterSurfaceArea": "get_outer_surface_area" + } + }, + "IfcPlate": { + "Qto_PlateBaseQuantities": { + "GrossArea": "get_gross_footprint_area", + "GrossVolume": "get_gross_volume", + "GrossWeight": "get_gross_weight", + "NetArea": "get_net_footprint_area", + "NetVolume": "get_net_volume", + "NetWeight": "get_net_weight", + "Perimeter": "get_gross_perimeter", + "Width": "get_height" + } + }, + "IfcProjectionElement": { + "Qto_ProjectionElementBaseQuantities": { + "Area": "get_net_side_area", + "Volume": "get_net_volume" + } + }, + "IfcProtectiveDevice": { + "Qto_ProtectiveDeviceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcProtectiveDeviceTrippingUnit": { + "Qto_ProtectiveDeviceTrippingUnitBaseQuantities": { + "GrossWeight": null + } + }, + "IfcPump": { + "Qto_PumpBaseQuantities": { + "GrossWeight": null + } + }, + "IfcRailing": { + "Qto_RailingBaseQuantities": { + "Length": "get_length" + } + }, + "IfcRampFlight": { + "Qto_RampFlightBaseQuantities": { + "GrossArea": "get_gross_stair_area", + "GrossVolume": "get_gross_volume", + "Length": "get_stair_length", + "NetArea": "get_net_stair_area", + "NetVolume": "get_net_volume", + "Width": "get_width" + } + }, + "IfcReinforcingElement": { + "Qto_ReinforcingElementBaseQuantities": { + "Count": null, + "Length": "get_length", + "Weight": null + } + }, + "IfcRoof": { + "Qto_RoofBaseQuantities": { + "GrossArea": "get_gross_top_area", + "NetArea": "get_net_top_area", + "ProjectedArea": null + } + }, + "IfcSanitaryTerminal": { + "Qto_SanitaryTerminalBaseQuantities": { + "GrossWeight": null + } + }, + "IfcSensor": { + "Qto_SensorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcSite": { + "Qto_SiteBaseQuantities": { + "GrossArea": "get_gross_footprint_area", + "GrossPerimeter": "get_gross_perimeter" + } + }, + "IfcSlab": { + "Qto_SlabBaseQuantities": { + "Depth": "get_height", + "GrossArea": "get_gross_footprint_area", + "GrossVolume": "get_gross_volume", + "GrossWeight": "get_gross_weight", + "Length": "get_length", + "NetArea": "get_net_footprint_area", + "NetVolume": "get_net_volume", + "NetWeight": "get_net_weight", + "Perimeter": "get_gross_perimeter", + "Width": "get_width" + } + }, + "IfcSolarDevice": { + "Qto_SolarDeviceBaseQuantities": { + "GrossArea": null, + "GrossWeight": null + } + }, + "IfcSpace": { + "Qto_SpaceBaseQuantities": { + "FinishCeilingHeight": "get_finish_ceiling_height", + "FinishFloorHeight": "get_finish_floor_height", + "GrossCeilingArea": "get_gross_ceiling_area", + "GrossFloorArea": "get_gross_footprint_area", + "GrossPerimeter": "get_gross_perimeter", + "GrossVolume": "get_gross_volume", + "GrossWallArea": null, + "Height": "get_height", + "NetCeilingArea": "get_net_ceiling_area", + "NetFloorArea": "get_net_floor_area", + "NetPerimeter": null, + "NetVolume": "get_space_net_volume", + "NetWallArea": null + } + }, + "IfcSpaceHeater": { + "Qto_SpaceHeaterBaseQuantities": { + "GrossWeight": null, + "Length": "get_length", + "NetWeight": null + } + }, + "IfcStackTerminal": { + "Qto_StackTerminalBaseQuantities": { + "GrossWeight": null + } + }, + "IfcStairFlight": { + "Qto_StairFlightBaseQuantities": { + "GrossVolume": "get_gross_volume", + "Length": "get_stair_length", + "NetVolume": "get_net_volume" + } + }, + "IfcSwitchingDevice": { + "Qto_SwitchingDeviceBaseQuantities": { + "GrossWeight": null + } + }, + "IfcTank": { + "Qto_TankBaseQuantities": { + "GrossWeight": null, + "NetWeight": null, + "TotalSurfaceArea": "get_outer_surface_area" + } + }, + "IfcTransformer": { + "Qto_TransformerBaseQuantities": { + "GrossWeight": null + } + }, + "IfcTubeBundle": { + "Qto_TubeBundleBaseQuantities": { + "GrossWeight": null, + "NetWeight": null + } + }, + "IfcUnitaryControlElement": { + "Qto_UnitaryControlElementBaseQuantities": { + "GrossWeight": null + } + }, + "IfcUnitaryEquipment": { + "Qto_UnitaryEquipmentBaseQuantities": { + "GrossWeight": null + } + }, + "IfcValve": { + "Qto_ValveBaseQuantities": { + "GrossWeight": null + } + }, + "IfcVibrationIsolator": { + "Qto_VibrationIsolatorBaseQuantities": { + "GrossWeight": null + } + }, + "IfcWall": { + "Qto_WallBaseQuantities": { + "GrossFootprintArea": "get_gross_footprint_area", + "GrossSideArea": "get_gross_side_area", + "GrossVolume": "get_gross_volume", + "GrossWeight": "get_gross_weight", + "Height": "get_height", + "Length": "get_x", + "NetFootprintArea": "get_net_footprint_area", + "NetSideArea": "get_net_side_area", + "NetVolume": "get_net_volume", + "NetWeight": "get_net_weight", + "Width": "get_width" + } + }, + "IfcWasteTerminal": { + "Qto_WasteTerminalBaseQuantities": { + "GrossWeight": null + } + }, + "IfcWindow": { + "Qto_WindowBaseQuantities": { + "Area": "get_net_side_area", + "Height": "get_height", + "Perimeter": "get_rectangular_perimeter", + "Width": "get_length" + } + } + } + } +} From 6c0466954302624f71a5d0c720c88c15140fd9f9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 13 Mar 2025 17:17:37 +0500 Subject: [PATCH 340/476] ifc5d - add ifc4x3 base quantities #6325 --- src/bonsai/bonsai/bim/module/qto/prop.py | 5 + src/bonsai/scripts/get_all_qtos.py | 124 +++++- src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json | 361 +++++++++++++----- .../ifc5d/IFC4X3QtoBaseQuantitiesBlender.json | 361 +++++++++++++----- src/ifc5d/ifc5d/qto.py | 7 +- .../ifcopenshell/util/pset.py | 52 ++- .../test/util/test_pset.py | 70 ++++ 7 files changed, 771 insertions(+), 209 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/qto/prop.py b/src/bonsai/bonsai/bim/module/qto/prop.py index 3d9664ec73..0a73240e64 100644 --- a/src/bonsai/bonsai/bim/module/qto/prop.py +++ b/src/bonsai/bonsai/bim/module/qto/prop.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import ifc5d.qto from bonsai.bim.prop import StrProperty, Attribute from bpy.types import PropertyGroup @@ -33,8 +34,12 @@ from bpy.props import ( def get_qto_rule(self, context): + ifc_file = tool.Ifc.get() + is_ifc4x3 = ifc_file.schema == "IFC4X3" results = [] for rule_id, rule in ifc5d.qto.rules.items(): + if rule_id.startswith("IFC4X3") != is_ifc4x3: + continue results.append((rule_id, rule["name"], rule["description"])) return results diff --git a/src/bonsai/scripts/get_all_qtos.py b/src/bonsai/scripts/get_all_qtos.py index ae0787335d..7f23dca55a 100644 --- a/src/bonsai/scripts/get_all_qtos.py +++ b/src/bonsai/scripts/get_all_qtos.py @@ -1,6 +1,12 @@ +"""Update ifc5d json files with qtos from the provided pset templates path.""" + import json import ifcopenshell.util.pset import ifcopenshell.util.type +import ifc5d +from collections import defaultdict +from pathlib import Path +from typing import Union def order_dict(dictionary): @@ -8,20 +14,110 @@ def order_dict(dictionary): return {k: order_dict(v) if isinstance(v, dict) else v for k, v in sorted(dictionary.items())} -results = {} +PSET_TEMPLATES_FOLDER = Path(ifcopenshell.util.__file__).parent / "schema" +JSON_FOLDER = Path(ifc5d.__file__).parent +QueriesData = dict[str, dict[str, dict[str, Union[str, None]]]] -psetqto = ifcopenshell.util.pset.get_template("IFC4") -for template in psetqto.templates: - for pset_template in template.by_type("IfcPropertySetTemplate"): - if not pset_template.Name.startswith("Qto_"): + +def main() -> None: + # Update IFC4X3 json files from pset templates. + update_json_with_qtos_from_template_file( + JSON_FOLDER / "IFC4X3QtoBaseQuantities.json", PSET_TEMPLATES_FOLDER / "Pset_IFC4X3.ifc" + ) + update_json_with_qtos_from_template_file( + JSON_FOLDER / "IFC4X3QtoBaseQuantitiesBlender.json", PSET_TEMPLATES_FOLDER / "Pset_IFC4X3.ifc" + ) + # Reuse methods defined in IFC4 in IFC4X3 calculators. + reuse_methods_from_other_json_file( + JSON_FOLDER / "IFC4QtoBaseQuantities.json", JSON_FOLDER / "IFC4X3QtoBaseQuantities.json" + ) + reuse_methods_from_other_json_file( + JSON_FOLDER / "IFC4QtoBaseQuantitiesBlender.json", JSON_FOLDER / "IFC4X3QtoBaseQuantitiesBlender.json" + ) + + +def update_json_with_qtos_from_template_file(json_filepath: Path, ifc_filepath: Path) -> None: + """Add missing qtos and properties to the json file from the provided pset template file.""" + qto_templates: dict[str, list[ifcopenshell.entity_instance]] = defaultdict(list) + qto_props: dict[str, set[str]] = defaultdict(set) + ifc_file: ifcopenshell.file + ifc_file = ifcopenshell.open(ifc_filepath) + for template in ifc_file.by_type("IfcPropertySetTemplate"): + template_name = template.Name + if not template_name.startswith("Qto_"): continue - query = pset_template.ApplicableEntity - results.setdefault(query, {}) - results[query].setdefault(pset_template.Name, {}) - for quantity in pset_template.HasPropertyTemplates: - results[query][pset_template.Name][quantity.Name] = None + qto_templates[template_name].append(template) + for prop in template.HasPropertyTemplates: + qto_props[template_name].add(prop.Name) -results = order_dict(results) -print(results) -with open("results.json", "w") as f: - json.dump(results, f, indent=4) + qto_base_quantities_data = json.loads(json_filepath.read_text()) + + calculator_queries_data: QueriesData + added_qtos: set[str] = set() + + for calculator, calculator_queries_data in qto_base_quantities_data["calculators"].items(): + # Gather all supported QTOs. + calculator_supported_qtos: set[str] = set() + for selector_query, query_qtos_data in calculator_queries_data.items(): + for qto_name in query_qtos_data: + calculator_supported_qtos.add(qto_name) + + # Check for missing QTOs. + for template_name in qto_templates: + template = qto_templates[template_name][0] + + applicable_entity_value: str + applicable_entity_value = template.ApplicableEntity + applicable_entities = ifcopenshell.util.pset.parse_applicable_entity(applicable_entity_value) + + selector_query = ifcopenshell.util.pset.convert_applicable_entities_to_query(applicable_entities) + # Add missing selector queries and qtos. + query_qtos_data = calculator_queries_data.setdefault(selector_query, {}) + qto_props_data = query_qtos_data.setdefault(template_name, {}) + + # Add missing properties. + for prop_name in qto_props[template_name]: + if prop_name in qto_props_data: + continue + qto_props_data[prop_name] = None + added_qtos.add(template_name) + + if not added_qtos: + print(f"No Qtos updates for calculator '{calculator}'.") + continue + + # Sort dict alphabetically to keep it looking nice. + # Don't sort the entire json to keep the header structure. + qto_base_quantities_data["calculators"][calculator] = order_dict(calculator_queries_data) + print(f"Added Qtos for {calculator}: {added_qtos}") + json_filepath.write_text(json.dumps(qto_base_quantities_data, indent=4) + "\n") + + +def reuse_methods_from_other_json_file(json_source_filepath: Path, json_target_filepath: Path) -> None: + json_source_data = json.loads(json_source_filepath.read_text()) + json_target_data = json.loads(json_target_filepath.read_text()) + queries_data: QueriesData + queries_data_target: QueriesData + for calculator, queries_data in json_source_data["calculators"].items(): + for selector_query, query_qtos_data in queries_data.items(): + queries_data_target = json_target_data["calculators"][calculator] + # Don't match exactly since queries between IFC4 and IFC4X3 are not in sync currently. + target_query = next((q for q in queries_data_target if q.startswith(selector_query)), None) + if target_query is None: + continue + query_qtos_data_target = queries_data_target[target_query] + for qto_name in query_qtos_data: + if qto_name not in query_qtos_data_target: + continue + props_data = query_qtos_data[qto_name] + props_data_target = query_qtos_data_target[qto_name] + for prop_name in props_data: + if prop_name not in props_data_target: + continue + props_data_target[prop_name] = props_data[prop_name] + + json_target_filepath.write_text(json.dumps(json_target_data, indent=4) + "\n") + + +if __name__ == "__main__": + main() diff --git a/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json b/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json index 2d7e54faab..90e791bce4 100644 --- a/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json +++ b/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json @@ -3,39 +3,39 @@ "description": "This ruleset quantifies every single possible standardised base quantity in IFC4X3 using only IfcOpenShell as a geometry processor.", "calculators": { "IfcOpenShell": { - "IfcActuator": { + "IfcActuator + IfcActuatorType": { "Qto_ActuatorBaseQuantities": { "GrossWeight": null } }, - "IfcAirTerminal": { + "IfcAirTerminal + IfcAirTerminalType": { "Qto_AirTerminalBaseQuantities": { "GrossWeight": null, "Perimeter": null, "TotalSurfaceArea": null } }, - "IfcAirTerminalBox": { + "IfcAirTerminalBox + IfcAirTerminalBoxType": { "Qto_AirTerminalBoxTypeBaseQuantities": { "GrossWeight": null } }, - "IfcAirToAirHeatRecovery": { + "IfcAirToAirHeatRecovery + IfcAirToAirHeatRecoveryType": { "Qto_AirToAirHeatRecoveryBaseQuantities": { "GrossWeight": null } }, - "IfcAlarm": { + "IfcAlarm + IfcAlarmType": { "Qto_AlarmBaseQuantities": { "GrossWeight": null } }, - "IfcAudioVisualAppliance": { + "IfcAudioVisualAppliance + IfcAudioVisualApplianceType": { "Qto_AudioVisualApplianceBaseQuantities": { "GrossWeight": null } }, - "IfcBeam": { + "IfcBeam + IfcBeamType": { "Qto_BeamBaseQuantities": { "CrossSectionArea": null, "GrossSurfaceArea": "gross_get_area", @@ -48,7 +48,7 @@ "OuterSurfaceArea": "net_get_outer_surface_area" } }, - "IfcBoiler": { + "IfcBoiler + IfcBoilerType": { "Qto_BoilerBaseQuantities": { "GrossWeight": null, "NetWeight": null, @@ -58,7 +58,7 @@ "IfcBuilding": { "Qto_BuildingBaseQuantities": { "EavesHeight": null, - "FootprintArea": null, + "FootPrintArea": null, "GrossFloorArea": null, "GrossVolume": null, "Height": null, @@ -66,7 +66,7 @@ "NetVolume": null } }, - "IfcBuildingElementProxy": { + "IfcBuildingElementProxy + IfcBuildingElementProxyType": { "Qto_BuildingElementProxyQuantities": { "NetSurfaceArea": null, "NetVolume": null @@ -79,21 +79,21 @@ "GrossPerimeter": null, "GrossVolume": null, "NetFloorArea": null, - "NetHeigtht": null, + "NetHeight": null, "NetVolume": null } }, - "IfcBurner": { + "IfcBurner + IfcBurnerType": { "Qto_BurnerBaseQuantities": { "GrossWeight": null } }, - "IfcCableCarrierFitting": { + "IfcCableCarrierFitting + IfcCableCarrierFittingType": { "Qto_CableCarrierFittingBaseQuantities": { "GrossWeight": null } }, - "IfcCableCarrierSegment": { + "IfcCableCarrierSegment + IfcCableCarrierSegmentType": { "Qto_CableCarrierSegmentBaseQuantities": { "CrossSectionArea": null, "GrossWeight": null, @@ -101,12 +101,18 @@ "OuterSurfaceArea": null } }, - "IfcCableFitting": { + "IfcCableCarrierSegment, PredefinedType=\"CONDUITSEGMENT\" + IfcCableCarrierSegmentType, PredefinedType=\"CONDUITSEGMENT\"": { + "Qto_ConduitSegmentBaseQuantities": { + "InnerDiameter": null, + "OuterDiameter": null + } + }, + "IfcCableFitting + IfcCableFittingType": { "Qto_CableFittingBaseQuantities": { "GrossWeight": null } }, - "IfcCableSegment": { + "IfcCableSegment + IfcCableSegmentType": { "Qto_CableSegmentBaseQuantities": { "CrossSectionArea": null, "GrossWeight": null, @@ -114,22 +120,22 @@ "OuterSurfaceArea": null } }, - "IfcChiller": { + "IfcChiller + IfcChillerType": { "Qto_ChillerBaseQuantities": { "GrossWeight": null } }, - "IfcChimney": { + "IfcChimney + IfcChimneyType": { "Qto_ChimneyBaseQuantities": { "Length": "net_get_max_xyz" } }, - "IfcCoil": { + "IfcCoil + IfcCoilType": { "Qto_CoilBaseQuantities": { "GrossWeight": null } }, - "IfcColumn": { + "IfcColumn + IfcColumnType": { "Qto_ColumnBaseQuantities": { "CrossSectionArea": null, "GrossSurfaceArea": "gross_get_area", @@ -142,28 +148,28 @@ "OuterSurfaceArea": "net_get_outer_surface_area" } }, - "IfcCommunicationsAppliance": { + "IfcCommunicationsAppliance + IfcCommunicationsApplianceType": { "Qto_CommunicationsApplianceBaseQuantities": { "GrossWeight": null } }, - "IfcCompressor": { + "IfcCompressor + IfcCompressorType": { "Qto_CompressorBaseQuantities": { "GrossWeight": null } }, - "IfcCondenser": { + "IfcCondenser + IfcCondenserType": { "Qto_CondenserBaseQuantities": { "GrossWeight": null } }, - "IfcConstructionEquipmentResource": { + "IfcConstructionEquipmentResource + IfcConstructionEquipmentResourceType": { "Qto_ConstructionEquipmentResourceBaseQuantities": { "OperatingTime": null, "UsageTime": null } }, - "IfcConstructionMaterialResource": { + "IfcConstructionMaterialResource + IfcConstructionMaterialResourceType": { "Qto_ConstructionMaterialResourceBaseQuantities": { "GrossVolume": null, "GrossWeight": null, @@ -171,29 +177,39 @@ "NetWeight": null } }, - "IfcController": { + "IfcController + IfcControllerType": { "Qto_ControllerBaseQuantities": { "GrossWeight": null } }, - "IfcCooledBeam": { + "IfcCooledBeam + IfcCooledBeamType": { "Qto_CooledBeamBaseQuantities": { "GrossWeight": null } }, - "IfcCoolingTower": { + "IfcCoolingTower + IfcCoolingTowerType": { "Qto_CoolingTowerBaseQuantities": { "GrossWeight": null } }, - "IfcCovering": { + "IfcCourse + IfcCourseType": { + "Qto_CourseBaseQuantities": { + "GrossVolume": null, + "Length": null, + "Thickness": null, + "Volume": null, + "Weight": null, + "Width": null + } + }, + "IfcCovering + IfcCoveringType": { "Qto_CoveringBaseQuantities": { "GrossArea": "gross_get_max_side_area", "NetArea": "net_get_max_side_area", "Width": "gross_get_min_xyz" } }, - "IfcCurtainWall": { + "IfcCurtainWall + IfcCurtainWallType": { "Qto_CurtainWallQuantities": { "GrossSideArea": null, "Height": null, @@ -202,20 +218,21 @@ "Width": null } }, - "IfcDamper": { + "IfcDamper + IfcDamperType": { "Qto_DamperBaseQuantities": { "GrossWeight": null } }, - "IfcDistributionChamberElement": { + "IfcDistributionChamberElement + IfcDistributionChamberElementType": { "Qto_DistributionChamberElementBaseQuantities": { + "Depth": null, "GrossSurfaceArea": null, "GrossVolume": null, "NetSurfaceArea": null, "NetVolume": null } }, - "IfcDoor": { + "IfcDoor + IfcDoorType": { "Qto_DoorBaseQuantities": { "Area": null, "Height": null, @@ -223,7 +240,7 @@ "Width": null } }, - "IfcDuctFitting": { + "IfcDuctFitting + IfcDuctFittingType": { "Qto_DuctFittingBaseQuantities": { "GrossCrossSectionArea": null, "GrossWeight": null, @@ -232,7 +249,7 @@ "OuterSurfaceArea": null } }, - "IfcDuctSegment": { + "IfcDuctSegment + IfcDuctSegmentType": { "Qto_DuctSegmentBaseQuantities": { "GrossCrossSectionArea": null, "GrossWeight": null, @@ -241,78 +258,106 @@ "OuterSurfaceArea": null } }, - "IfcDuctSilencer": { + "IfcDuctSilencer + IfcDuctSilencerType": { "Qto_DuctSilencerBaseQuantities": { "GrossWeight": null } }, - "IfcElectricAppliance": { + "IfcEarthworksCut": { + "Qto_EarthworksCutBaseQuantities": { + "Depth": null, + "Length": null, + "LooseVolume": null, + "UndisturbedVolume": null, + "Weight": null, + "Width": null + } + }, + "IfcEarthworksFill": { + "Qto_EarthworksFillBaseQuantities": { + "CompactedVolume": null, + "Depth": null, + "Length": null, + "LooseVolume": null, + "Width": null + } + }, + "IfcElectricAppliance + IfcElectricApplianceType": { "Qto_ElectricApplianceBaseQuantities": { "GrossWeight": null } }, - "IfcElectricDistributionBoard": { - "Qto_ElectricDistributionBoardBaseQuantities": { + "IfcElectricDistributionBoard + IfcElectricDistributionBoardType": { + "Qto_DistributionBoardBaseQuantities": { "GrossWeight": null, "NumberOfCircuits": null } }, - "IfcElectricFlowStorageDevice": { + "IfcElectricFlowStorageDevice + IfcElectricFlowStorageDeviceType": { "Qto_ElectricFlowStorageDeviceBaseQuantities": { "GrossWeight": null } }, - "IfcElectricGenerator": { + "IfcElectricGenerator + IfcElectricGeneratorType": { "Qto_ElectricGeneratorBaseQuantities": { "GrossWeight": null } }, - "IfcElectricMotor": { + "IfcElectricMotor + IfcElectricMotorType": { "Qto_ElectricMotorBaseQuantities": { "GrossWeight": null } }, - "IfcElectricTimeControl": { + "IfcElectricTimeControl + IfcElectricTimeControlType": { "Qto_ElectricTimeControlBaseQuantities": { "GrossWeight": null } }, - "IfcEvaporativeCooler": { + "IfcEvaporativeCooler + IfcEvaporativeCoolerType": { "Qto_EvaporativeCoolerBaseQuantities": { "GrossWeight": null } }, - "IfcEvaporator": { + "IfcEvaporator + IfcEvaporatorType": { "Qto_EvaporatorBaseQuantities": { "GrossWeight": null } }, - "IfcFan": { + "IfcFacilityPart": { + "Qto_FacilityPartBaseQuantities": { + "Area": null, + "Height": null, + "Length": null, + "Volume": null, + "Width": null + } + }, + "IfcFan + IfcFanType": { "Qto_FanBaseQuantities": { "GrossWeight": null } }, - "IfcFilter": { + "IfcFilter + IfcFilterType": { "Qto_FilterBaseQuantities": { "GrossWeight": null } }, - "IfcFireSuppressionTerminal": { + "IfcFireSuppressionTerminal + IfcFireSuppressionTerminalType": { "Qto_FireSuppressionTerminalBaseQuantities": { "GrossWeight": null } }, - "IfcFlowInstrument": { + "IfcFlowInstrument + IfcFlowInstrumentType": { "Qto_FlowInstrumentBaseQuantities": { "GrossWeight": null } }, - "IfcFlowMeter": { + "IfcFlowMeter + IfcFlowMeterType": { "Qto_FlowMeterBaseQuantities": { "GrossWeight": null } }, - "IfcFooting": { + "IfcFooting + IfcFootingType": { "Qto_FootingBaseQuantities": { "CrossSectionArea": null, "GrossSurfaceArea": null, @@ -326,44 +371,88 @@ "Width": null } }, - "IfcHeatExchanger": { + "IfcGeotechnicalStratum": { + "Qto_ArealStratumBaseQuantities": { + "Area": null, + "Length": null, + "PlanLength": null + }, + "Qto_LinearStratumBaseQuantities": { + "Diameter": null, + "Length": null + }, + "Qto_VolumetricStratumBaseQuantities": { + "Area": null, + "Mass": null, + "PlanArea": null, + "Volume": null + } + }, + "IfcHeatExchanger + IfcHeatExchangerType": { "Qto_HeatExchangerBaseQuantities": { "GrossWeight": null } }, - "IfcHumidifier": { + "IfcHumidifier + IfcHumidifierType": { "Qto_HumidifierBaseQuantities": { "GrossWeight": null } }, - "IfcInterceptor": { + "IfcImpactProtectionDevice + IfcImpactProtectionDeviceType": { + "Qto_ImpactProtectionDeviceBaseQuantities": { + "Weight": null + } + }, + "IfcInterceptor + IfcInterceptorType": { "Qto_InterceptorBaseQuantities": { "GrossWeight": null } }, - "IfcJunctionBox": { + "IfcJunctionBox + IfcJunctionBoxType": { "Qto_JunctionBoxBaseQuantities": { "GrossWeight": null, - "NumberOfGangs": null + "Height": null, + "Length": null, + "NumberOfGangs": null, + "Width": null } }, - "IfcLaborResource": { + "IfcKerb + IfcKerbType": { + "Qto_KerbBaseQuantities": { + "Depth": null, + "Height": null, + "Length": null, + "Volume": null, + "Weight": null, + "Width": null + } + }, + "IfcLaborResource + IfcLaborResourceType": { "Qto_LaborResourceBaseQuantities": { "OvertimeWork": null, "StandardWork": null } }, - "IfcLamp": { + "IfcLamp + IfcLampType": { "Qto_LampBaseQuantities": { "GrossWeight": null } }, - "IfcLightFixture": { + "IfcLightFixture + IfcLightFixtureType": { "Qto_LightFixtureBaseQuantities": { "GrossWeight": null } }, - "IfcMember": { + "IfcMarineFacility": { + "Qto_MarineFacilityBaseQuantities": { + "Area": null, + "Height": null, + "Length": null, + "Volume": null, + "Width": null + } + }, + "IfcMember + IfcMemberType": { "Qto_MemberBaseQuantities": { "CrossSectionArea": null, "GrossSurfaceArea": "gross_get_area", @@ -376,7 +465,7 @@ "OuterSurfaceArea": "net_get_outer_surface_area" } }, - "IfcMotorConnection": { + "IfcMotorConnection + IfcMotorConnectionType": { "Qto_MotorConnectionBaseQuantities": { "GrossWeight": null } @@ -390,12 +479,23 @@ "Width": "gross_get_x" } }, - "IfcOutlet": { + "IfcOutlet + IfcOutletType": { "Qto_OutletBaseQuantities": { "GrossWeight": null } }, - "IfcPile": { + "IfcPavement + IfcPavementType": { + "Qto_PavementBaseQuantities": { + "Depth": null, + "GrossArea": null, + "GrossVolume": null, + "Length": null, + "NetArea": null, + "NetVolume": null, + "Width": null + } + }, + "IfcPile + IfcPileType": { "Qto_PileBaseQuantities": { "CrossSectionArea": null, "GrossSurfaceArea": null, @@ -407,7 +507,7 @@ "OuterSurfaceArea": null } }, - "IfcPipeFitting": { + "IfcPipeFitting + IfcPipeFittingType": { "Qto_PipeFittingBaseQuantities": { "GrossCrossSectionArea": null, "GrossWeight": null, @@ -417,8 +517,9 @@ "OuterSurfaceArea": null } }, - "IfcPipeSegment": { + "IfcPipeSegment + IfcPipeSegmentType": { "Qto_PipeSegmentBaseQuantities": { + "FootPrintArea": null, "GrossCrossSectionArea": null, "GrossWeight": null, "Length": "net_get_segment_length", @@ -427,7 +528,7 @@ "OuterSurfaceArea": null } }, - "IfcPlate": { + "IfcPlate + IfcPlateType": { "Qto_PlateBaseQuantities": { "GrossArea": "gross_get_max_side_area", "GrossVolume": "gross_get_volume", @@ -439,33 +540,50 @@ "Width": "net_get_min_xyz" } }, + "IfcProduct": { + "Qto_BodyGeometryValidation": { + "GrossSurfaceArea": null, + "GrossVolume": null, + "NetSurfaceArea": null, + "NetVolume": null, + "SurfaceGenusAfterFeatures": null, + "SurfaceGenusBeforeFeatures": null + } + }, "IfcProjectionElement": { "Qto_ProjectionElementBaseQuantities": { "Area": null, "Volume": null } }, - "IfcProtectiveDevice": { + "IfcProtectiveDevice + IfcProtectiveDeviceType": { "Qto_ProtectiveDeviceBaseQuantities": { "GrossWeight": null } }, - "IfcProtectiveDeviceTrippingUnit": { + "IfcProtectiveDeviceTrippingUnit + IfcProtectiveDeviceTrippingUnitType": { "Qto_ProtectiveDeviceTrippingUnitBaseQuantities": { "GrossWeight": null } }, - "IfcPump": { + "IfcPump + IfcPumpType": { "Qto_PumpBaseQuantities": { "GrossWeight": null } }, - "IfcRailing": { + "IfcRail + IfcRailType": { + "Qto_RailBaseQuantities": { + "Length": null, + "Volume": null, + "Weight": null + } + }, + "IfcRailing + IfcRailingType": { "Qto_RailingBaseQuantities": { "Length": null } }, - "IfcRampFlight": { + "IfcRampFlight + IfcRampFlightType": { "Qto_RampFlightBaseQuantities": { "GrossArea": null, "GrossVolume": null, @@ -475,37 +593,65 @@ "Width": null } }, - "IfcReinforcingElement": { + "IfcReinforcedSoil": { + "Qto_ReinforcedSoilBaseQuantities": { + "Area": null, + "Depth": null, + "Length": null, + "Volume": null, + "Width": null + } + }, + "IfcReinforcingElement + IfcReinforcingElementType": { "Qto_ReinforcingElementBaseQuantities": { "Count": null, "Length": null, "Weight": null } }, - "IfcRoof": { + "IfcRoof + IfcRoofType": { "Qto_RoofBaseQuantities": { "GrossArea": "gross_get_top_area", "NetArea": "net_get_top_area", "ProjectedArea": null } }, - "IfcSanitaryTerminal": { + "IfcSanitaryTerminal + IfcSanitaryTerminalType": { "Qto_SanitaryTerminalBaseQuantities": { "GrossWeight": null } }, - "IfcSensor": { + "IfcSensor + IfcSensorType": { "Qto_SensorBaseQuantities": { "GrossWeight": null } }, + "IfcSign + IfcSignType": { + "Qto_SignBaseQuantities": { + "Height": null, + "Thickness": null, + "Weight": null, + "Width": null + } + }, + "IfcSign, PredefinedType=\"PICTORAL\" + IfcSignType, PredefinedType=\"PICTORAL\"": { + "Qto_PictorialSignQuantities": { + "Area": null, + "SignArea": null + } + }, + "IfcSignal + IfcSignalType": { + "Qto_SignalBaseQuantities": { + "Weight": null + } + }, "IfcSite": { "Qto_SiteBaseQuantities": { "GrossArea": null, "GrossPerimeter": null } }, - "IfcSlab": { + "IfcSlab + IfcSlabType": { "Qto_SlabBaseQuantities": { "Depth": "net_get_z", "GrossArea": "gross_get_footprint_area", @@ -519,13 +665,13 @@ "Width": "net_get_y" } }, - "IfcSolarDevice": { + "IfcSolarDevice + IfcSolarDeviceType": { "Qto_SolarDeviceBaseQuantities": { "GrossArea": null, "GrossWeight": null } }, - "IfcSpace": { + "IfcSpace + IfcSpaceType": { "Qto_SpaceBaseQuantities": { "FinishCeilingHeight": null, "FinishFloorHeight": null, @@ -542,89 +688,116 @@ "NetWallArea": null } }, - "IfcSpaceHeater": { + "IfcSpaceHeater + IfcSpaceHeaterType": { "Qto_SpaceHeaterBaseQuantities": { "GrossWeight": null, "Length": null, "NetWeight": null } }, - "IfcStackTerminal": { + "IfcSpatialZone + IfcSpatialZoneType": { + "Qto_SpatialZoneBaseQuantities": { + "Height": null, + "Length": null, + "Width": null + } + }, + "IfcStackTerminal + IfcStackTerminalType": { "Qto_StackTerminalBaseQuantities": { "GrossWeight": null } }, - "IfcStairFlight": { + "IfcStairFlight + IfcStairFlightType": { "Qto_StairFlightBaseQuantities": { "GrossVolume": null, "Length": "net_get_max_xy", "NetVolume": "net_get_volume" } }, - "IfcSwitchingDevice": { + "IfcSurfaceFeature": { + "Qto_SurfaceFeatureBaseQuantities": { + "Area": null, + "Length": null + } + }, + "IfcSwitchingDevice + IfcSwitchingDeviceType": { "Qto_SwitchingDeviceBaseQuantities": { "GrossWeight": null } }, - "IfcTank": { + "IfcTank + IfcTankType": { "Qto_TankBaseQuantities": { "GrossWeight": null, "NetWeight": null, "TotalSurfaceArea": null } }, - "IfcTransformer": { + "IfcTrackElement, PredefinedType=\"SLEEPER\" + IfcTrackElementType, PredefinedType=\"SLEEPER\"": { + "Qto_SleeperBaseQuantities": { + "Height": null, + "Length": null, + "Width": null + } + }, + "IfcTransformer + IfcTransformerType": { "Qto_TransformerBaseQuantities": { "GrossWeight": null } }, - "IfcTubeBundle": { + "IfcTubeBundle + IfcTubeBundleType": { "Qto_TubeBundleBaseQuantities": { "GrossWeight": null, "NetWeight": null } }, - "IfcUnitaryControlElement": { + "IfcUnitaryControlElement + IfcUnitaryControlElementType": { "Qto_UnitaryControlElementBaseQuantities": { "GrossWeight": null } }, - "IfcUnitaryEquipment": { + "IfcUnitaryEquipment + IfcUnitaryEquipmentType": { "Qto_UnitaryEquipmentBaseQuantities": { "GrossWeight": null } }, - "IfcValve": { + "IfcValve + IfcValveType": { "Qto_ValveBaseQuantities": { "GrossWeight": null } }, - "IfcVibrationIsolator": { + "IfcVehicle, PredefinedType=\"ROLLINGSTOCK\" + IfcVehicle, PredefinedType=\"VEHICLEAIR\" + IfcVehicle, PredefinedType=\"VEHICLEMARINE\" + IfcVehicle, PredefinedType=\"VEHICLE\" + IfcVehicle, PredefinedType=\"VEHICLETRACKED\" + IfcVehicleType, PredefinedType=\"ROLLINGSTOCK\" + IfcVehicleType, PredefinedType=\"VEHICLEAIR\" + IfcVehicleType, PredefinedType=\"VEHICLEMARINE\" + IfcVehicleType, PredefinedType=\"VEHICLE\" + IfcVehicleType, PredefinedType=\"VEHICLETRACKED\"": { + "Qto_VehicleBaseQuantities": { + "Height": null, + "Length": null, + "Width": null + } + }, + "IfcVibrationIsolator + IfcVibrationIsolatorType": { "Qto_VibrationIsolatorBaseQuantities": { "GrossWeight": null } }, - "IfcWall": { + "IfcWall + IfcWallType": { "Qto_WallBaseQuantities": { - "GrossFootprintArea": null, + "GrossFootPrintArea": null, "GrossSideArea": "gross_get_side_area", "GrossVolume": "gross_get_volume", "GrossWeight": null, "Height": "net_get_z", "Length": "net_get_x", - "NetFootprintArea": null, + "NetFootPrintArea": null, "NetSideArea": "net_get_side_area", "NetVolume": "net_get_volume", "NetWeight": null, "Width": "net_get_y" } }, - "IfcWasteTerminal": { + "IfcWasteTerminal + IfcWasteTerminalType": { "Qto_WasteTerminalBaseQuantities": { "GrossWeight": null } }, - "IfcWindow": { + "IfcWindow + IfcWindowType": { "Qto_WindowBaseQuantities": { "Area": "net_get_max_side_area", "Height": "net_get_z", diff --git a/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantitiesBlender.json b/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantitiesBlender.json index f5e7c628de..a1351c07ff 100644 --- a/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantitiesBlender.json +++ b/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantitiesBlender.json @@ -3,39 +3,39 @@ "description": "This ruleset quantifies every single possible standardised base quantity in IFC4X3 using Blender.", "calculators": { "Blender": { - "IfcActuator": { + "IfcActuator + IfcActuatorType": { "Qto_ActuatorBaseQuantities": { "GrossWeight": null } }, - "IfcAirTerminal": { + "IfcAirTerminal + IfcAirTerminalType": { "Qto_AirTerminalBaseQuantities": { "GrossWeight": null, "Perimeter": null, "TotalSurfaceArea": null } }, - "IfcAirTerminalBox": { + "IfcAirTerminalBox + IfcAirTerminalBoxType": { "Qto_AirTerminalBoxTypeBaseQuantities": { "GrossWeight": null } }, - "IfcAirToAirHeatRecovery": { + "IfcAirToAirHeatRecovery + IfcAirToAirHeatRecoveryType": { "Qto_AirToAirHeatRecoveryBaseQuantities": { "GrossWeight": null } }, - "IfcAlarm": { + "IfcAlarm + IfcAlarmType": { "Qto_AlarmBaseQuantities": { "GrossWeight": null } }, - "IfcAudioVisualAppliance": { + "IfcAudioVisualAppliance + IfcAudioVisualApplianceType": { "Qto_AudioVisualApplianceBaseQuantities": { "GrossWeight": null } }, - "IfcBeam": { + "IfcBeam + IfcBeamType": { "Qto_BeamBaseQuantities": { "CrossSectionArea": "get_cross_section_area", "GrossSurfaceArea": "get_gross_surface_area", @@ -48,7 +48,7 @@ "OuterSurfaceArea": "get_outer_surface_area" } }, - "IfcBoiler": { + "IfcBoiler + IfcBoilerType": { "Qto_BoilerBaseQuantities": { "GrossWeight": null, "NetWeight": null, @@ -58,7 +58,7 @@ "IfcBuilding": { "Qto_BuildingBaseQuantities": { "EavesHeight": null, - "FootprintArea": null, + "FootPrintArea": null, "GrossFloorArea": null, "GrossVolume": null, "Height": null, @@ -66,7 +66,7 @@ "NetVolume": null } }, - "IfcBuildingElementProxy": { + "IfcBuildingElementProxy + IfcBuildingElementProxyType": { "Qto_BuildingElementProxyQuantities": { "NetSurfaceArea": "get_net_surface_area", "NetVolume": "get_net_volume" @@ -79,21 +79,21 @@ "GrossPerimeter": null, "GrossVolume": null, "NetFloorArea": null, - "NetHeigtht": null, + "NetHeight": null, "NetVolume": null } }, - "IfcBurner": { + "IfcBurner + IfcBurnerType": { "Qto_BurnerBaseQuantities": { "GrossWeight": null } }, - "IfcCableCarrierFitting": { + "IfcCableCarrierFitting + IfcCableCarrierFittingType": { "Qto_CableCarrierFittingBaseQuantities": { "GrossWeight": null } }, - "IfcCableCarrierSegment": { + "IfcCableCarrierSegment + IfcCableCarrierSegmentType": { "Qto_CableCarrierSegmentBaseQuantities": { "CrossSectionArea": null, "GrossWeight": null, @@ -101,12 +101,18 @@ "OuterSurfaceArea": null } }, - "IfcCableFitting": { + "IfcCableCarrierSegment, PredefinedType=\"CONDUITSEGMENT\" + IfcCableCarrierSegmentType, PredefinedType=\"CONDUITSEGMENT\"": { + "Qto_ConduitSegmentBaseQuantities": { + "InnerDiameter": null, + "OuterDiameter": null + } + }, + "IfcCableFitting + IfcCableFittingType": { "Qto_CableFittingBaseQuantities": { "GrossWeight": null } }, - "IfcCableSegment": { + "IfcCableSegment + IfcCableSegmentType": { "Qto_CableSegmentBaseQuantities": { "CrossSectionArea": null, "GrossWeight": null, @@ -114,22 +120,22 @@ "OuterSurfaceArea": "get_outer_surface_area" } }, - "IfcChiller": { + "IfcChiller + IfcChillerType": { "Qto_ChillerBaseQuantities": { "GrossWeight": null } }, - "IfcChimney": { + "IfcChimney + IfcChimneyType": { "Qto_ChimneyBaseQuantities": { "Length": "get_height" } }, - "IfcCoil": { + "IfcCoil + IfcCoilType": { "Qto_CoilBaseQuantities": { "GrossWeight": null } }, - "IfcColumn": { + "IfcColumn + IfcColumnType": { "Qto_ColumnBaseQuantities": { "CrossSectionArea": "get_cross_section_area", "GrossSurfaceArea": "get_gross_surface_area", @@ -142,28 +148,28 @@ "OuterSurfaceArea": "get_outer_surface_area" } }, - "IfcCommunicationsAppliance": { + "IfcCommunicationsAppliance + IfcCommunicationsApplianceType": { "Qto_CommunicationsApplianceBaseQuantities": { "GrossWeight": null } }, - "IfcCompressor": { + "IfcCompressor + IfcCompressorType": { "Qto_CompressorBaseQuantities": { "GrossWeight": null } }, - "IfcCondenser": { + "IfcCondenser + IfcCondenserType": { "Qto_CondenserBaseQuantities": { "GrossWeight": null } }, - "IfcConstructionEquipmentResource": { + "IfcConstructionEquipmentResource + IfcConstructionEquipmentResourceType": { "Qto_ConstructionEquipmentResourceBaseQuantities": { "OperatingTime": null, "UsageTime": null } }, - "IfcConstructionMaterialResource": { + "IfcConstructionMaterialResource + IfcConstructionMaterialResourceType": { "Qto_ConstructionMaterialResourceBaseQuantities": { "GrossVolume": "get_gross_volume", "GrossWeight": null, @@ -171,29 +177,39 @@ "NetWeight": null } }, - "IfcController": { + "IfcController + IfcControllerType": { "Qto_ControllerBaseQuantities": { "GrossWeight": null } }, - "IfcCooledBeam": { + "IfcCooledBeam + IfcCooledBeamType": { "Qto_CooledBeamBaseQuantities": { "GrossWeight": null } }, - "IfcCoolingTower": { + "IfcCoolingTower + IfcCoolingTowerType": { "Qto_CoolingTowerBaseQuantities": { "GrossWeight": null } }, - "IfcCovering": { + "IfcCourse + IfcCourseType": { + "Qto_CourseBaseQuantities": { + "GrossVolume": null, + "Length": null, + "Thickness": null, + "Volume": null, + "Weight": null, + "Width": null + } + }, + "IfcCovering + IfcCoveringType": { "Qto_CoveringBaseQuantities": { "GrossArea": "get_covering_gross_area", "NetArea": "get_covering_net_area", "Width": "get_covering_width" } }, - "IfcCurtainWall": { + "IfcCurtainWall + IfcCurtainWallType": { "Qto_CurtainWallQuantities": { "GrossSideArea": null, "Height": null, @@ -202,20 +218,21 @@ "Width": null } }, - "IfcDamper": { + "IfcDamper + IfcDamperType": { "Qto_DamperBaseQuantities": { "GrossWeight": null } }, - "IfcDistributionChamberElement": { + "IfcDistributionChamberElement + IfcDistributionChamberElementType": { "Qto_DistributionChamberElementBaseQuantities": { + "Depth": null, "GrossSurfaceArea": "get_gross_surface_area", "GrossVolume": "get_gross_volume", "NetSurfaceArea": "get_net_surface_area", "NetVolume": "get_net_volume" } }, - "IfcDoor": { + "IfcDoor + IfcDoorType": { "Qto_DoorBaseQuantities": { "Area": "get_net_side_area", "Height": "get_height", @@ -223,7 +240,7 @@ "Width": "get_length" } }, - "IfcDuctFitting": { + "IfcDuctFitting + IfcDuctFittingType": { "Qto_DuctFittingBaseQuantities": { "GrossCrossSectionArea": null, "GrossWeight": null, @@ -232,7 +249,7 @@ "OuterSurfaceArea": "get_outer_surface_area" } }, - "IfcDuctSegment": { + "IfcDuctSegment + IfcDuctSegmentType": { "Qto_DuctSegmentBaseQuantities": { "GrossCrossSectionArea": null, "GrossWeight": null, @@ -241,78 +258,106 @@ "OuterSurfaceArea": "get_outer_surface_area" } }, - "IfcDuctSilencer": { + "IfcDuctSilencer + IfcDuctSilencerType": { "Qto_DuctSilencerBaseQuantities": { "GrossWeight": null } }, - "IfcElectricAppliance": { + "IfcEarthworksCut": { + "Qto_EarthworksCutBaseQuantities": { + "Depth": null, + "Length": null, + "LooseVolume": null, + "UndisturbedVolume": null, + "Weight": null, + "Width": null + } + }, + "IfcEarthworksFill": { + "Qto_EarthworksFillBaseQuantities": { + "CompactedVolume": null, + "Depth": null, + "Length": null, + "LooseVolume": null, + "Width": null + } + }, + "IfcElectricAppliance + IfcElectricApplianceType": { "Qto_ElectricApplianceBaseQuantities": { "GrossWeight": null } }, - "IfcElectricDistributionBoard": { - "Qto_ElectricDistributionBoardBaseQuantities": { + "IfcElectricDistributionBoard + IfcElectricDistributionBoardType": { + "Qto_DistributionBoardBaseQuantities": { "GrossWeight": null, "NumberOfCircuits": null } }, - "IfcElectricFlowStorageDevice": { + "IfcElectricFlowStorageDevice + IfcElectricFlowStorageDeviceType": { "Qto_ElectricFlowStorageDeviceBaseQuantities": { "GrossWeight": null } }, - "IfcElectricGenerator": { + "IfcElectricGenerator + IfcElectricGeneratorType": { "Qto_ElectricGeneratorBaseQuantities": { "GrossWeight": null } }, - "IfcElectricMotor": { + "IfcElectricMotor + IfcElectricMotorType": { "Qto_ElectricMotorBaseQuantities": { "GrossWeight": null } }, - "IfcElectricTimeControl": { + "IfcElectricTimeControl + IfcElectricTimeControlType": { "Qto_ElectricTimeControlBaseQuantities": { "GrossWeight": null } }, - "IfcEvaporativeCooler": { + "IfcEvaporativeCooler + IfcEvaporativeCoolerType": { "Qto_EvaporativeCoolerBaseQuantities": { "GrossWeight": null } }, - "IfcEvaporator": { + "IfcEvaporator + IfcEvaporatorType": { "Qto_EvaporatorBaseQuantities": { "GrossWeight": null } }, - "IfcFan": { + "IfcFacilityPart": { + "Qto_FacilityPartBaseQuantities": { + "Area": null, + "Height": null, + "Length": null, + "Volume": null, + "Width": null + } + }, + "IfcFan + IfcFanType": { "Qto_FanBaseQuantities": { "GrossWeight": null } }, - "IfcFilter": { + "IfcFilter + IfcFilterType": { "Qto_FilterBaseQuantities": { "GrossWeight": null } }, - "IfcFireSuppressionTerminal": { + "IfcFireSuppressionTerminal + IfcFireSuppressionTerminalType": { "Qto_FireSuppressionTerminalBaseQuantities": { "GrossWeight": null } }, - "IfcFlowInstrument": { + "IfcFlowInstrument + IfcFlowInstrumentType": { "Qto_FlowInstrumentBaseQuantities": { "GrossWeight": null } }, - "IfcFlowMeter": { + "IfcFlowMeter + IfcFlowMeterType": { "Qto_FlowMeterBaseQuantities": { "GrossWeight": null } }, - "IfcFooting": { + "IfcFooting + IfcFootingType": { "Qto_FootingBaseQuantities": { "CrossSectionArea": "get_cross_section_area", "GrossSurfaceArea": "get_gross_surface_area", @@ -326,44 +371,88 @@ "Width": "get_width" } }, - "IfcHeatExchanger": { + "IfcGeotechnicalStratum": { + "Qto_ArealStratumBaseQuantities": { + "Area": null, + "Length": null, + "PlanLength": null + }, + "Qto_LinearStratumBaseQuantities": { + "Diameter": null, + "Length": null + }, + "Qto_VolumetricStratumBaseQuantities": { + "Area": null, + "Mass": null, + "PlanArea": null, + "Volume": null + } + }, + "IfcHeatExchanger + IfcHeatExchangerType": { "Qto_HeatExchangerBaseQuantities": { "GrossWeight": null } }, - "IfcHumidifier": { + "IfcHumidifier + IfcHumidifierType": { "Qto_HumidifierBaseQuantities": { "GrossWeight": null } }, - "IfcInterceptor": { + "IfcImpactProtectionDevice + IfcImpactProtectionDeviceType": { + "Qto_ImpactProtectionDeviceBaseQuantities": { + "Weight": null + } + }, + "IfcInterceptor + IfcInterceptorType": { "Qto_InterceptorBaseQuantities": { "GrossWeight": null } }, - "IfcJunctionBox": { + "IfcJunctionBox + IfcJunctionBoxType": { "Qto_JunctionBoxBaseQuantities": { "GrossWeight": null, - "NumberOfGangs": null + "Height": null, + "Length": null, + "NumberOfGangs": null, + "Width": null } }, - "IfcLaborResource": { + "IfcKerb + IfcKerbType": { + "Qto_KerbBaseQuantities": { + "Depth": null, + "Height": null, + "Length": null, + "Volume": null, + "Weight": null, + "Width": null + } + }, + "IfcLaborResource + IfcLaborResourceType": { "Qto_LaborResourceBaseQuantities": { "OvertimeWork": null, "StandardWork": null } }, - "IfcLamp": { + "IfcLamp + IfcLampType": { "Qto_LampBaseQuantities": { "GrossWeight": null } }, - "IfcLightFixture": { + "IfcLightFixture + IfcLightFixtureType": { "Qto_LightFixtureBaseQuantities": { "GrossWeight": null } }, - "IfcMember": { + "IfcMarineFacility": { + "Qto_MarineFacilityBaseQuantities": { + "Area": null, + "Height": null, + "Length": null, + "Volume": null, + "Width": null + } + }, + "IfcMember + IfcMemberType": { "Qto_MemberBaseQuantities": { "CrossSectionArea": "get_cross_section_area", "GrossSurfaceArea": "get_gross_surface_area", @@ -376,7 +465,7 @@ "OuterSurfaceArea": "get_outer_surface_area" } }, - "IfcMotorConnection": { + "IfcMotorConnection + IfcMotorConnectionType": { "Qto_MotorConnectionBaseQuantities": { "GrossWeight": null } @@ -390,12 +479,23 @@ "Width": "get_length" } }, - "IfcOutlet": { + "IfcOutlet + IfcOutletType": { "Qto_OutletBaseQuantities": { "GrossWeight": null } }, - "IfcPile": { + "IfcPavement + IfcPavementType": { + "Qto_PavementBaseQuantities": { + "Depth": null, + "GrossArea": null, + "GrossVolume": null, + "Length": null, + "NetArea": null, + "NetVolume": null, + "Width": null + } + }, + "IfcPile + IfcPileType": { "Qto_PileBaseQuantities": { "CrossSectionArea": "get_cross_section_area", "GrossSurfaceArea": "get_gross_surface_area", @@ -407,7 +507,7 @@ "OuterSurfaceArea": "get_outer_surface_area" } }, - "IfcPipeFitting": { + "IfcPipeFitting + IfcPipeFittingType": { "Qto_PipeFittingBaseQuantities": { "GrossCrossSectionArea": null, "GrossWeight": null, @@ -417,8 +517,9 @@ "OuterSurfaceArea": null } }, - "IfcPipeSegment": { + "IfcPipeSegment + IfcPipeSegmentType": { "Qto_PipeSegmentBaseQuantities": { + "FootPrintArea": null, "GrossCrossSectionArea": null, "GrossWeight": "get_gross_weight", "Length": "get_length", @@ -427,7 +528,7 @@ "OuterSurfaceArea": "get_outer_surface_area" } }, - "IfcPlate": { + "IfcPlate + IfcPlateType": { "Qto_PlateBaseQuantities": { "GrossArea": "get_gross_footprint_area", "GrossVolume": "get_gross_volume", @@ -439,33 +540,50 @@ "Width": "get_height" } }, + "IfcProduct": { + "Qto_BodyGeometryValidation": { + "GrossSurfaceArea": null, + "GrossVolume": null, + "NetSurfaceArea": null, + "NetVolume": null, + "SurfaceGenusAfterFeatures": null, + "SurfaceGenusBeforeFeatures": null + } + }, "IfcProjectionElement": { "Qto_ProjectionElementBaseQuantities": { "Area": "get_net_side_area", "Volume": "get_net_volume" } }, - "IfcProtectiveDevice": { + "IfcProtectiveDevice + IfcProtectiveDeviceType": { "Qto_ProtectiveDeviceBaseQuantities": { "GrossWeight": null } }, - "IfcProtectiveDeviceTrippingUnit": { + "IfcProtectiveDeviceTrippingUnit + IfcProtectiveDeviceTrippingUnitType": { "Qto_ProtectiveDeviceTrippingUnitBaseQuantities": { "GrossWeight": null } }, - "IfcPump": { + "IfcPump + IfcPumpType": { "Qto_PumpBaseQuantities": { "GrossWeight": null } }, - "IfcRailing": { + "IfcRail + IfcRailType": { + "Qto_RailBaseQuantities": { + "Length": null, + "Volume": null, + "Weight": null + } + }, + "IfcRailing + IfcRailingType": { "Qto_RailingBaseQuantities": { "Length": "get_length" } }, - "IfcRampFlight": { + "IfcRampFlight + IfcRampFlightType": { "Qto_RampFlightBaseQuantities": { "GrossArea": "get_gross_stair_area", "GrossVolume": "get_gross_volume", @@ -475,37 +593,65 @@ "Width": "get_width" } }, - "IfcReinforcingElement": { + "IfcReinforcedSoil": { + "Qto_ReinforcedSoilBaseQuantities": { + "Area": null, + "Depth": null, + "Length": null, + "Volume": null, + "Width": null + } + }, + "IfcReinforcingElement + IfcReinforcingElementType": { "Qto_ReinforcingElementBaseQuantities": { "Count": null, "Length": "get_length", "Weight": null } }, - "IfcRoof": { + "IfcRoof + IfcRoofType": { "Qto_RoofBaseQuantities": { "GrossArea": "get_gross_top_area", "NetArea": "get_net_top_area", "ProjectedArea": null } }, - "IfcSanitaryTerminal": { + "IfcSanitaryTerminal + IfcSanitaryTerminalType": { "Qto_SanitaryTerminalBaseQuantities": { "GrossWeight": null } }, - "IfcSensor": { + "IfcSensor + IfcSensorType": { "Qto_SensorBaseQuantities": { "GrossWeight": null } }, + "IfcSign + IfcSignType": { + "Qto_SignBaseQuantities": { + "Height": null, + "Thickness": null, + "Weight": null, + "Width": null + } + }, + "IfcSign, PredefinedType=\"PICTORAL\" + IfcSignType, PredefinedType=\"PICTORAL\"": { + "Qto_PictorialSignQuantities": { + "Area": null, + "SignArea": null + } + }, + "IfcSignal + IfcSignalType": { + "Qto_SignalBaseQuantities": { + "Weight": null + } + }, "IfcSite": { "Qto_SiteBaseQuantities": { "GrossArea": "get_gross_footprint_area", "GrossPerimeter": "get_gross_perimeter" } }, - "IfcSlab": { + "IfcSlab + IfcSlabType": { "Qto_SlabBaseQuantities": { "Depth": "get_height", "GrossArea": "get_gross_footprint_area", @@ -519,13 +665,13 @@ "Width": "get_width" } }, - "IfcSolarDevice": { + "IfcSolarDevice + IfcSolarDeviceType": { "Qto_SolarDeviceBaseQuantities": { "GrossArea": null, "GrossWeight": null } }, - "IfcSpace": { + "IfcSpace + IfcSpaceType": { "Qto_SpaceBaseQuantities": { "FinishCeilingHeight": "get_finish_ceiling_height", "FinishFloorHeight": "get_finish_floor_height", @@ -542,89 +688,116 @@ "NetWallArea": null } }, - "IfcSpaceHeater": { + "IfcSpaceHeater + IfcSpaceHeaterType": { "Qto_SpaceHeaterBaseQuantities": { "GrossWeight": null, "Length": "get_length", "NetWeight": null } }, - "IfcStackTerminal": { + "IfcSpatialZone + IfcSpatialZoneType": { + "Qto_SpatialZoneBaseQuantities": { + "Height": null, + "Length": null, + "Width": null + } + }, + "IfcStackTerminal + IfcStackTerminalType": { "Qto_StackTerminalBaseQuantities": { "GrossWeight": null } }, - "IfcStairFlight": { + "IfcStairFlight + IfcStairFlightType": { "Qto_StairFlightBaseQuantities": { "GrossVolume": "get_gross_volume", "Length": "get_stair_length", "NetVolume": "get_net_volume" } }, - "IfcSwitchingDevice": { + "IfcSurfaceFeature": { + "Qto_SurfaceFeatureBaseQuantities": { + "Area": null, + "Length": null + } + }, + "IfcSwitchingDevice + IfcSwitchingDeviceType": { "Qto_SwitchingDeviceBaseQuantities": { "GrossWeight": null } }, - "IfcTank": { + "IfcTank + IfcTankType": { "Qto_TankBaseQuantities": { "GrossWeight": null, "NetWeight": null, "TotalSurfaceArea": "get_outer_surface_area" } }, - "IfcTransformer": { + "IfcTrackElement, PredefinedType=\"SLEEPER\" + IfcTrackElementType, PredefinedType=\"SLEEPER\"": { + "Qto_SleeperBaseQuantities": { + "Height": null, + "Length": null, + "Width": null + } + }, + "IfcTransformer + IfcTransformerType": { "Qto_TransformerBaseQuantities": { "GrossWeight": null } }, - "IfcTubeBundle": { + "IfcTubeBundle + IfcTubeBundleType": { "Qto_TubeBundleBaseQuantities": { "GrossWeight": null, "NetWeight": null } }, - "IfcUnitaryControlElement": { + "IfcUnitaryControlElement + IfcUnitaryControlElementType": { "Qto_UnitaryControlElementBaseQuantities": { "GrossWeight": null } }, - "IfcUnitaryEquipment": { + "IfcUnitaryEquipment + IfcUnitaryEquipmentType": { "Qto_UnitaryEquipmentBaseQuantities": { "GrossWeight": null } }, - "IfcValve": { + "IfcValve + IfcValveType": { "Qto_ValveBaseQuantities": { "GrossWeight": null } }, - "IfcVibrationIsolator": { + "IfcVehicle, PredefinedType=\"ROLLINGSTOCK\" + IfcVehicle, PredefinedType=\"VEHICLEAIR\" + IfcVehicle, PredefinedType=\"VEHICLEMARINE\" + IfcVehicle, PredefinedType=\"VEHICLE\" + IfcVehicle, PredefinedType=\"VEHICLETRACKED\" + IfcVehicleType, PredefinedType=\"ROLLINGSTOCK\" + IfcVehicleType, PredefinedType=\"VEHICLEAIR\" + IfcVehicleType, PredefinedType=\"VEHICLEMARINE\" + IfcVehicleType, PredefinedType=\"VEHICLE\" + IfcVehicleType, PredefinedType=\"VEHICLETRACKED\"": { + "Qto_VehicleBaseQuantities": { + "Height": null, + "Length": null, + "Width": null + } + }, + "IfcVibrationIsolator + IfcVibrationIsolatorType": { "Qto_VibrationIsolatorBaseQuantities": { "GrossWeight": null } }, - "IfcWall": { + "IfcWall + IfcWallType": { "Qto_WallBaseQuantities": { - "GrossFootprintArea": "get_gross_footprint_area", + "GrossFootPrintArea": null, "GrossSideArea": "get_gross_side_area", "GrossVolume": "get_gross_volume", "GrossWeight": "get_gross_weight", "Height": "get_height", "Length": "get_x", - "NetFootprintArea": "get_net_footprint_area", + "NetFootPrintArea": null, "NetSideArea": "get_net_side_area", "NetVolume": "get_net_volume", "NetWeight": "get_net_weight", "Width": "get_width" } }, - "IfcWasteTerminal": { + "IfcWasteTerminal + IfcWasteTerminalType": { "Qto_WasteTerminalBaseQuantities": { "GrossWeight": null } }, - "IfcWindow": { + "IfcWindow + IfcWindowType": { "Qto_WindowBaseQuantities": { "Area": "get_net_side_area", "Height": "get_height", diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 2206fe6a1a..3766be8d0d 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -35,7 +35,12 @@ from typing import Any, Literal, get_args, Union, Iterable Function = namedtuple("Function", ["measure", "name", "description"]) -RULE_SET = Literal["IFC4QtoBaseQuantities", "IFC4QtoBaseQuantitiesBlender"] +RULE_SET = Literal[ + "IFC4QtoBaseQuantities", + "IFC4QtoBaseQuantitiesBlender", + "IFC4X3QtoBaseQuantities", + "IFC4X3QtoBaseQuantitiesBlender", +] rules: dict[RULE_SET, dict[str, Any]] = {} ResultsDict = dict[ifcopenshell.entity_instance, dict[str, dict[str, float]]] QtosFormulas = dict[str, dict[str, str]] diff --git a/src/ifcopenshell-python/ifcopenshell/util/pset.py b/src/ifcopenshell-python/ifcopenshell/util/pset.py index 08b10996c0..bf90570251 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/pset.py +++ b/src/ifcopenshell-python/ifcopenshell/util/pset.py @@ -24,7 +24,7 @@ import ifcopenshell.util.schema import ifcopenshell.util.type from ifcopenshell.entity_instance import entity_instance from functools import lru_cache -from typing import Optional, Literal +from typing import Optional, Literal, NamedTuple, Union templates: dict[str, "PsetQto"] = {} @@ -111,11 +111,17 @@ class PsetQto: template_type: str = "NOTDEFINED", schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4", ) -> bool: - """applicables can have multiple possible patterns : - IfcBoilerType (IfcClass) - IfcBoilerType/STEAM (IfcClass/PREDEFINEDTYPE) - IfcBoilerType[PerformanceHistory] (IfcClass[PerformanceHistory]) - IfcBoilerType/STEAM[PerformanceHistory] (IfcClass/PREDEFINEDTYPE[PerformanceHistory]) + """ + + applicables can have multiple possible patterns : + + .. code-block:: text + + IfcBoilerType (IfcClass) + IfcBoilerType/STEAM (IfcClass/PREDEFINEDTYPE) + IfcBoilerType[PerformanceHistory] (IfcClass[PerformanceHistory]) + IfcBoilerType/STEAM[PerformanceHistory] (IfcClass/PREDEFINEDTYPE[PerformanceHistory]) + """ for applicable in applicables.split(","): match = re.match(r"(\w+)(\[\w+\])*/*(\w+)*(\[\w+\])*", applicable) @@ -194,3 +200,37 @@ def get_pset_template_type(pset_template: entity_instance) -> Literal["PSET", "Q pset_type = next(iter(pset_types)) if len(pset_types) == 1 else None return pset_type + + +class ApplicableEntity(NamedTuple): + value: str + ifc_class: str + predefined_type: Union[str, None] + performance_history: bool + + +def parse_applicable_entity(applicable_entity: str) -> list[ApplicableEntity]: + """Parse ApplicableEntity string query to tuples. + + :param applicable_entity: IfcPropertySetTemplate.ApplicableEntity query. + :return: List of ApplicableEntity tuples. + """ + items: list[ApplicableEntity] = [] + for item in applicable_entity.split(","): + value = item + item, predefined_type = parts if len(parts := item.split("/")) > 1 else (item, None) + ifc_class, performance_history = (parts[0], True) if len(parts := item.split("[")) > 1 else (item, False) + items.append(ApplicableEntity(value, ifc_class, predefined_type, performance_history)) + return items + + +def convert_applicable_entities_to_query(applicable_entities: list[ApplicableEntity]) -> str: + """Get query supported by :func:`ifcopenshell.util.selector.filter_elements`.""" + parts: list[str] = [] + for entity in applicable_entities: + # NOTE: selector currently doesn't support checking if element has performance history. + part = entity.ifc_class + if entity.predefined_type: + part += f', PredefinedType="{entity.predefined_type}"' + parts.append(part) + return " + ".join(parts) diff --git a/src/ifcopenshell-python/test/util/test_pset.py b/src/ifcopenshell-python/test/util/test_pset.py index 90b90f6e35..c99a0b86a6 100644 --- a/src/ifcopenshell-python/test/util/test_pset.py +++ b/src/ifcopenshell-python/test/util/test_pset.py @@ -19,6 +19,7 @@ """Run this test from src/ifcopenshell-python folder: pytest --durations=0 ifcopenshell/util/test_pset.py""" from ifcopenshell.util import pset from ifcopenshell import util +from ifcopenshell.util.pset import ApplicableEntity class TestPsetQto: @@ -66,3 +67,72 @@ class TestPsetQto: assert "Pset_MaterialConcrete" not in names names = self.pset_qto.get_applicable_names("IfcMaterial", "concrete") assert "Pset_MaterialConcrete" in names + + +class TestParseApplicableEntity: + def test_run(self): + assert pset.parse_applicable_entity("IfcBoilerType") == [ + ApplicableEntity("IfcBoilerType", "IfcBoilerType", None, False) + ] + + def test_two_entities(self): + assert pset.parse_applicable_entity("IfcBoilerType,IfcWallType") == [ + ApplicableEntity("IfcBoilerType", "IfcBoilerType", None, False), + ApplicableEntity("IfcWallType", "IfcWallType", None, False), + ] + + def test_two_entities_with_performance_history(self): + assert pset.parse_applicable_entity("IfcBoilerType[PerformanceHistory],IfcWallType") == [ + ApplicableEntity("IfcBoilerType[PerformanceHistory]", "IfcBoilerType", None, True), + ApplicableEntity("IfcWallType", "IfcWallType", None, False), + ] + + def test_two_entities_with_predefined_type(self): + assert pset.parse_applicable_entity("IfcBoilerType/STEAM,IfcWallType") == [ + ApplicableEntity("IfcBoilerType/STEAM", "IfcBoilerType", "STEAM", False), + ApplicableEntity("IfcWallType", "IfcWallType", None, False), + ] + + def test_two_entities_with_predefined_type_and_performance_history(self): + assert pset.parse_applicable_entity("IfcBoilerType[PerformanceHistory]/STEAM,IfcWallType") == [ + ApplicableEntity("IfcBoilerType[PerformanceHistory]/STEAM", "IfcBoilerType", "STEAM", True), + ApplicableEntity("IfcWallType", "IfcWallType", None, False), + ] + + +class TestConvertApplicableEntitiesToQuery: + def test_run(self): + entities = [ApplicableEntity("IfcBoilerType", "IfcBoilerType", None, False)] + assert pset.convert_applicable_entities_to_query(entities) == "IfcBoilerType" + + def test_two_entities(self): + entities = [ + ApplicableEntity("IfcBoilerType", "IfcBoilerType", None, False), + ApplicableEntity("IfcWallType", "IfcWallType", None, False), + ] + assert pset.convert_applicable_entities_to_query(entities) == "IfcBoilerType + IfcWallType" + + def test_two_entities_with_performance_history(self): + entities = [ + ApplicableEntity("IfcBoilerType[PerformanceHistory]", "IfcBoilerType", None, True), + ApplicableEntity("IfcWallType", "IfcWallType", None, False), + ] + assert pset.convert_applicable_entities_to_query(entities) == "IfcBoilerType + IfcWallType" + + def test_two_entities_with_predefined_type(self): + entities = [ + ApplicableEntity("IfcBoilerType/STEAM", "IfcBoilerType", "STEAM", False), + ApplicableEntity("IfcWallType", "IfcWallType", None, False), + ] + assert ( + pset.convert_applicable_entities_to_query(entities) == 'IfcBoilerType, PredefinedType="STEAM" + IfcWallType' + ) + + def test_two_entities_with_predefined_type_and_performance_history(self): + entities = [ + ApplicableEntity("IfcBoilerType[PerformanceHistory]/STEAM", "IfcBoilerType", "STEAM", True), + ApplicableEntity("IfcWallType", "IfcWallType", None, False), + ] + assert ( + pset.convert_applicable_entities_to_query(entities) == 'IfcBoilerType, PredefinedType="STEAM" + IfcWallType' + ) From edd60a73cad16a931eba2bbfc559179df2d91cc4 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 13 Mar 2025 18:04:33 +0500 Subject: [PATCH 341/476] generate_pset_templates - fix running after ifc4x3 release The error was: ``` Traceback (most recent call last): File "\IfcOpenShell\src\ifcopenshell-python\ifcopenshell\util\generate_pset_templates.py", line 416, in templates_generator.parse_ifc4x3_data() File "\IfcOpenShell\src\ifcopenshell-python\ifcopenshell\util\generate_pset_templates.py", line 94, in parse_ifc4x3_data self.parse_psets_data("IFC4X3", pset_data_glob, "IFC4X3 Property Set Templates", str(IFC4x3_OUTPUT_PATH)) File "\IfcOpenShell\src\ifcopenshell-python\ifcopenshell\util\generate_pset_templates.py", line 129, in parse_psets_data schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "\ifcopenshell\ifcopenshell_wrapper.py", line 9037, in schema_by_name return _ifcopenshell_wrapper.schema_by_name(arg1) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RuntimeError: No schema named IFC4X3 ``` --- .../ifcopenshell/util/generate_pset_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py b/src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py index 1de000fa4e..b0bf0cdda5 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py +++ b/src/ifcopenshell-python/ifcopenshell/util/generate_pset_templates.py @@ -126,7 +126,7 @@ class PsetTemplatesGenerator: schema_name = schema_name.upper() self.ifc_file = ifcopenshell.api.project.create_file(version=schema_name) self.units = dict() - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name) + schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(self.ifc_file.schema_identifier) self.ifc_derived_unit_enum = ( schema.declaration_by_name("IfcDerivedUnitEnum").as_enumeration_type().enumeration_items() From 4daf4693f392732084dfd812d329722b194edf52 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 13 Mar 2025 18:34:47 +0500 Subject: [PATCH 342/476] selector syntax - note on PredefinedType --- .../docs/ifcopenshell-python/selector_syntax.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst index d2857c7810..3dd4456fdc 100644 --- a/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst +++ b/src/ifcopenshell-python/docs/ifcopenshell-python/selector_syntax.rst @@ -102,7 +102,7 @@ will search through all IfcTypeProducts and IfcProducts in the IFC project. "Class", "Add", "``[!] {{ifc_class_name}}``", "``IfcWall`` adds all IfcWall elements and their subclasses. ``! IfcWall`` subtracts all non-IfcWall elements from the filter group." "GlobalId", "Add", "``[!] {{global_id}}``", "``325Q7Fhnf67OZC$$r43uzK`` adds the single element with that GlobalId attribute. ``! 325Q7Fhnf67OZC$$r43uzK`` subtracts that single element." - "Attribute", "Filter", "``{{name}}{{=}}{{value}}``", "``Name=Foo`` specifies the criteria that elements must have a ``Name`` attribute with a value of ``Foo``. Attribute names must be spelled exactly the same as in IFC, which means that they must start with an uppercase character." + "Attribute", "Filter", "``{{name}}{{=}}{{value}}``", "``Name=Foo`` specifies the criteria that elements must have a ``Name`` attribute with a value of ``Foo``. Attribute names must be spelled exactly the same as in IFC, which means that they must start with an uppercase character. For convenience, ``PredefinedType`` will be get using :func:`ifcopenshell.util.element.get_predefined_type` instead of getting the attribute directly." "Property", "Filter", "``{{pset}}.{{prop}}{{=}}{{value}}``", "``Pset_WallCommon.FireRating=2HR`` specifies the criteria that elements must have a ``Pset_WallCommon`` property set, with a ``FireRating`` property within it with a value of ``2HR``. The property set name and the property name are separated by a ``.``." "Type", "Filter", "``type{{=}}{{value}}``", "``type=Foo`` specifies the criteria that elements must have a type which has a ``Name`` attribute with a value of ``Foo``." "Material", "Filter", "``material{{=}}{{value}}``", "``material=Foo`` specifies the criteria that elements must have a IfcMaterial assigned directly or indirectly (such as within a layer set). That IfcMaterial must have either a ``Name`` or ``Category`` attribute with a value of ``Foo``." From 6949f9ceaed0cb83db0e5cccfd6ff7039519ea02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 09:31:11 -0300 Subject: [PATCH 343/476] Refactor - Move filtering logic for snap objects into a separate function --- src/bonsai/bonsai/tool/snap.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 680766b197..bb815b1ce3 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -269,6 +269,24 @@ class Snap(bonsai.core.tool.Snap): sorted_intersections = sorted(valid_intersections, key=lambda x: (x - last_point).length, reverse=True) return sorted_intersections + @classmethod + def filter_objects_to_raycast( + cls, + context: bpy.types.Context, + objs_2d_bbox: Union[tuple[bpy.types.Object, list[float]]], + mouse_pos: tuple[int, int], + offset: int = None, + ) -> list[bpy.types.Object]: + objs_to_raycast = [] + for obj, bbox_2d in objs_2d_bbox: + if obj.type in {"MESH", "EMPTY", "CURVE"} and bbox_2d: + if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d, offset): + if ( + obj.visible_in_viewport_get(bpy.context.space_data) or obj.library + ): # Check for local view and local collections for this viewport and object + objs_to_raycast.append(obj) + return objs_to_raycast + @classmethod def detect_snapping_points(cls, context: bpy.types.Context, event: bpy.types.Event, objs_2d_bbox, tool_state): rv3d = context.region_data @@ -384,14 +402,7 @@ class Snap(bonsai.core.tool.Snap): ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event) - objs_to_raycast = [] - for obj, bbox_2d in objs_2d_bbox: - if obj.type in {"MESH", "EMPTY", "CURVE"} and bbox_2d: - if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d, offset): - if ( - obj.visible_in_viewport_get(context.space_data) or obj.library - ): # Check for local view and local collections for this viewport and object - objs_to_raycast.append(obj) + objs_to_raycast = cls.filter_objects_to_raycast(context, objs_2d_bbox, mouse_pos, offset) # Polyline try: From be76d4cb440fdea0211f221bd95ecff195cf09c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 09:49:46 -0300 Subject: [PATCH 344/476] Refactor - Update input options display in Polyline tool UI --- .../bonsai/bim/module/model/decorator.py | 10 +++++--- .../bonsai/bim/module/model/polyline.py | 5 +--- src/bonsai/bonsai/bim/module/model/profile.py | 2 +- .../bonsai/bim/module/project/operator.py | 5 +--- src/bonsai/bonsai/tool/polyline.py | 23 ++++++------------- 5 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index ee6df0f75e..0a8c80b658 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -410,6 +410,7 @@ class PolylineDecorator: "X": "X coord: ", "Y": "Y coord: ", "Z": "Z coord:", + "AREA": "Area:", } try: mouse_pos = self.event.mouse_region_x, self.event.mouse_region_y @@ -425,11 +426,14 @@ class PolylineDecorator: color = self.addon_prefs.decorations_colour color_highlight = self.addon_prefs.decorator_color_special offset = 20 - new_line = 20 + new_line = 0 for i, (key, field_name) in enumerate(texts.items()): - formatted_value = None if self.input_ui: + # Controls which options are displayed in the UI + if key not in self.input_ui.input_options: + continue + new_line += 20 if self.tool_state and key != self.tool_state.input_type: formatted_value = self.input_ui.get_formatted_value(key) else: @@ -441,7 +445,7 @@ class PolylineDecorator: blf.color(self.font_id, *color_highlight) else: blf.color(self.font_id, *color) - blf.position(self.font_id, mouse_pos[0] + offset, mouse_pos[1] - (new_line * i), 0) + blf.position(self.font_id, mouse_pos[0] + offset, mouse_pos[1] - (new_line), 0) blf.draw(self.font_id, field_name + formatted_value) blf.disable(self.font_id, blf.SHADOW) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 94f17c0ec2..5bc1613785 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -550,9 +550,6 @@ class PolylineOperator: # TODO Fill doc strings """ """ - number_input: list[str] - input_type: tool.Polyline.InputType - @classmethod def poll(cls, context: bpy.types.Context) -> bool: return context.space_data.type == "VIEW_3D" @@ -589,7 +586,7 @@ class PolylineOperator: self.input_options = ["D", "A", "X", "Y"] self.input_type = None self.input_value_xy = [None, None] - self.input_ui = tool.Polyline.create_input_ui() + self.input_ui = tool.Polyline.create_input_ui(input_options=self.input_options) self.is_typing = False self.snap_angle = None self.snapping_points = [] diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 413546110d..424c56f5e9 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -1128,8 +1128,8 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato def __init__(self): super().__init__() - self.input_ui = tool.Polyline.create_input_ui(init_z=True) self.input_options = ["D", "A", "X", "Y", "Z"] + self.input_ui = tool.Polyline.create_input_ui(input_options=self.input_options) self.relating_type = None props = tool.Model.get_model_props() relating_type_id = props.relating_type_id diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 8a26654cbe..077ada3b6c 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2600,11 +2600,8 @@ class MeasureTool(bpy.types.Operator, PolylineOperator): def __init__(self): super().__init__() - if self.measure_type == "AREA": - self.input_ui = tool.Polyline.create_input_ui(init_z=True, init_area=True) - else: - self.input_ui = tool.Polyline.create_input_ui(init_z=True) self.input_options = ["D", "A", "X", "Y", "Z"] + self.input_ui = tool.Polyline.create_input_ui(input_options=self.input_options) def modal(self, context, event): PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index 473ef10cd5..ac9f741d3b 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -24,7 +24,7 @@ import ifcopenshell.util.unit import bonsai.core.tool import bonsai.tool as tool from bonsai.bim.module.drawing.helper import format_distance -from dataclasses import dataclass +from dataclasses import dataclass, field from lark import Lark, Transformer from math import degrees, radians, sin, cos, tan from mathutils import Vector, Matrix @@ -39,16 +39,9 @@ class Polyline(bonsai.core.tool.Polyline): _WORLD_ANGLE: str = "" # Relative to World Origin. Only used for specific operation. Not used on the UI. _X: str = "" _Y: str = "" - _Z: Optional[str] = None - _AREA: Optional[str] = None - init_z: bool = False - init_area: bool = False - - def __post_init__(self): - if self.init_z: - self._Z = "" - if self.init_area: - self._AREA = "0" + _Z: str = "" + _AREA: str = "0" + input_options: List[str] = field(default_factory=list) def set_value(self, attribute_name, value): value = str(value) @@ -76,8 +69,6 @@ class Polyline(bonsai.core.tool.Polyline): else: return Polyline.format_input_ui_units(value) - InputType = Literal["D", "A", "X", "Y", None] - @dataclass class ToolState: use_default_container: bool = None @@ -101,8 +92,8 @@ class Polyline(bonsai.core.tool.Polyline): input_type: "Polyline.InputType" = None @classmethod - def create_input_ui(cls, init_z: bool = False, init_area: bool = False) -> PolylineUI: - return cls.PolylineUI(init_z=init_z, init_area=init_area) + def create_input_ui(cls, input_options: List[str] = []) -> PolylineUI: + return cls.PolylineUI(input_options=input_options) @classmethod def create_tool_state(cls) -> ToolState: @@ -343,7 +334,7 @@ class Polyline(bonsai.core.tool.Polyline): return @classmethod - def validate_input(cls, input_number: str, input_type: InputType) -> tuple[bool, str]: + def validate_input(cls, input_number: str, input_type: str) -> tuple[bool, str]: """ :return: Tuple with a boolean indicating if the input is valid and the final string output. From f99015e12334d1c130ca7aef535c53f32e0e460e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 13:57:39 -0300 Subject: [PATCH 345/476] Polyline tool - move "remove_last_polyline_point" to "handle_inserting_polyline". --- src/bonsai/bonsai/bim/module/model/polyline.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 5bc1613785..ed887440d7 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -842,6 +842,12 @@ class PolylineOperator: PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() + if not self.tool_state.is_input_on: + if event.value == "RELEASE" and event.type == "BACK_SPACE": + tool.Polyline.remove_last_polyline_point() + tool.Blender.update_viewport() + + def handle_snap_selection(self, context: bpy.types.Context, event: bpy.types.Event) -> None: if not self.tool_state.is_input_on and event.value == "PRESS" and event.type == "M": self.snapping_points = tool.Snap.modify_snapping_point_selection( @@ -909,10 +915,6 @@ class PolylineOperator: tool.Blender.update_viewport() return {"RUNNING_MODAL"} - if event.value == "RELEASE" and event.type == "BACK_SPACE": - tool.Polyline.remove_last_polyline_point() - tool.Blender.update_viewport() - def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_isntance): if tool.Model.get_usage_type(relating_type) == "PROFILE": if relating_type.is_a() in {"IfcColumnType", "IfcPileType"}: From b40f6307838248899d687f5592d7ccf0c7d709ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 16:55:37 -0300 Subject: [PATCH 346/476] Refactor - move raycast functions from snap.py to raycast.py --- src/bonsai/bonsai/tool/raycast.py | 91 +++++++++++++++++++++++++++++++ src/bonsai/bonsai/tool/snap.py | 25 ++------- 2 files changed, 95 insertions(+), 21 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index ca531b7be1..7ab648bc5b 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -24,6 +24,7 @@ import bonsai.core.tool import bonsai.tool as tool import mathutils from mathutils import Vector +from typing import Union class Raycast(bonsai.core.tool.Raycast): @@ -336,3 +337,93 @@ class Raycast(bonsai.core.tool.Raycast): "distance": distance, } return snap_point + + @classmethod + def filter_objects_to_raycast( + cls, + context: bpy.types.Context, + event: bpy.types.Event, + objs_2d_bbox: Union[tuple[bpy.types.Object, list[float]]], + offset: int = None, + ) -> list[bpy.types.Object]: + mouse_pos = event.mouse_region_x, event.mouse_region_y + objs_to_raycast = [] + for obj, bbox_2d in objs_2d_bbox: + if obj.type in {"MESH", "EMPTY", "CURVE"} and bbox_2d: + if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d, offset): + if ( + obj.visible_in_viewport_get(bpy.context.space_data) or obj.library + ): # Check for local view and local collections for this viewport and object + objs_to_raycast.append(obj) + return objs_to_raycast + + @classmethod + def cast_rays_to_single_object( + cls, + context: bpy.types.Context, + event: bpy.types.Event, + obj: bpy.types.Object, + mouse_offset: tuple[tuple[int, int]] = None, + ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: + + mouse_pos = event.mouse_region_x, event.mouse_region_y + hit = None + face_index = None + # Wireframes + if obj.type in {"EMPTY", "CURVE"} or (hasattr(obj.data, "polygons") and len(obj.data.polygons) == 0): + snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj) + if snap_points: + hit = sorted(snap_points, key=lambda x: x["distance"])[0]["point"] + if hit: + hit_world = obj.original.matrix_world @ hit + return obj, hit_world, face_index + return None, None, None + # Meshes + else: + hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj) + if hit is None: + # Tried original mouse position. Now it will try the offsets. + original_mouse_pos = mouse_pos + for value in mouse_offset: + mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value)) + hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, mouse_pos) + if hit: + break + mouse_pos = original_mouse_pos + if hit: + hit_world = obj.original.matrix_world @ hit + return obj, hit_world, face_index + else: + return None, None, None + + @classmethod + def cast_rays_and_get_best_object( + cls, + context: bpy.types.Context, + event: bpy.types.Event, + objs_to_raycast: list[bpy.types.Object], + mouse_offset: tuple[tuple[int, int]] = None, + ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: + best_length_squared = 1.0 + best_obj = None + best_hit = None + best_face_index = None + + ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event) + + for obj in objs_to_raycast: + snap_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, obj, mouse_offset) + + if hit is not None: + length_squared = (hit - ray_origin).length_squared + if best_obj is None or length_squared < best_length_squared: + best_length_squared = length_squared + best_obj = snap_obj + best_hit = hit + best_face_index = face_index + + if best_obj is not None: + return best_obj, best_hit, best_face_index + + else: + return None, None, None diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index bb815b1ce3..4adf4f1fd3 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -269,24 +269,6 @@ class Snap(bonsai.core.tool.Snap): sorted_intersections = sorted(valid_intersections, key=lambda x: (x - last_point).length, reverse=True) return sorted_intersections - @classmethod - def filter_objects_to_raycast( - cls, - context: bpy.types.Context, - objs_2d_bbox: Union[tuple[bpy.types.Object, list[float]]], - mouse_pos: tuple[int, int], - offset: int = None, - ) -> list[bpy.types.Object]: - objs_to_raycast = [] - for obj, bbox_2d in objs_2d_bbox: - if obj.type in {"MESH", "EMPTY", "CURVE"} and bbox_2d: - if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d, offset): - if ( - obj.visible_in_viewport_get(bpy.context.space_data) or obj.library - ): # Check for local view and local collections for this viewport and object - objs_to_raycast.append(obj) - return objs_to_raycast - @classmethod def detect_snapping_points(cls, context: bpy.types.Context, event: bpy.types.Event, objs_2d_bbox, tool_state): rv3d = context.region_data @@ -349,7 +331,7 @@ class Snap(bonsai.core.tool.Snap): hit = None face_index = None # Wireframes - if obj.type in {"EMPTY", "CURVE"} or (hasattr(obj.data, "polygons") and len(obj.data.polygons) == 0): + if obj.type in {"EMPTY", "CURVE"} or (hasattr(obj.data, "polygons") and len(obj.data.polygons) == 0) : snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj) if snap_points: hit = sorted(snap_points, key=lambda x: x["distance"])[0]["point"] @@ -430,15 +412,16 @@ class Snap(bonsai.core.tool.Snap): detected_snaps.append(point) # Objects + objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, objs_2d_bbox, offset) if (space.shading.type == "SOLID" and space.shading.show_xray) or ( space.shading.type == "WIREFRAME" and space.shading.show_xray_wireframe ): results = [] for obj in objs_to_raycast: - results.append(cast_rays_to_single_object(obj, mouse_pos)) + results.append(tool.Raycast.cast_rays_to_single_object(context, event, obj, mouse_offset)) else: results = [] - results.append(cast_rays_and_get_best_object(objs_to_raycast, mouse_pos)) + results.append(tool.Raycast.cast_rays_and_get_best_object(context, event, objs_to_raycast, mouse_offset)) for result in results: snap_obj = result[0] From ed7cf248ec211c76c88839c98991a85ebe37f2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 14:51:20 -0300 Subject: [PATCH 347/476] typing --- src/bonsai/bonsai/tool/raycast.py | 49 ++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 7ab648bc5b..99cae37461 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -29,7 +29,7 @@ from typing import Union class Raycast(bonsai.core.tool.Raycast): @classmethod - def get_visible_objects(cls, context): + def get_visible_objects(cls, context: bpy.types.Context): depsgraph = context.evaluated_depsgraph_get() all_objs = [] for dup in depsgraph.object_instances: @@ -42,7 +42,7 @@ class Raycast(bonsai.core.tool.Raycast): return all_objs @classmethod - def get_on_screen_2d_bounding_boxes(cls, context, obj): + def get_on_screen_2d_bounding_boxes(cls, context: bpy.types.Context, obj: bpy.types.Object): obj_matrix = obj.matrix_world.copy() bbox = [obj_matrix @ Vector(v) for v in obj.bound_box] @@ -69,7 +69,11 @@ class Raycast(bonsai.core.tool.Raycast): return (obj, bbox_2d) @classmethod - def intersect_mouse_2d_bounding_box(cls, mouse_pos, bbox, offset=None): + def intersect_mouse_2d_bounding_box( + cls, mouse_pos: tuple[int, int], bbox: list[float, float, float, float], offset: int = None + ): + print("bbox", type(bbox)) + print(bbox) x, y = mouse_pos xmin, xmax, ymin, ymax = bbox @@ -86,7 +90,9 @@ class Raycast(bonsai.core.tool.Raycast): return False @classmethod - def get_viewport_ray_data(cls, context, event, mouse_pos=None): + def get_viewport_ray_data( + cls, context: bpy.types.Context, event: bpy.types.Event, mouse_pos: tuple[int, int] = None + ): region = context.region rv3d = context.region_data original_perspective = rv3d.view_perspective @@ -109,7 +115,13 @@ class Raycast(bonsai.core.tool.Raycast): return ray_origin, ray_target, ray_direction @classmethod - def get_object_ray_data(cls, context, event, obj_matrix, mouse_pos=None): + def get_object_ray_data( + cls, + context: bpy.types.Context, + event: bpy.types.Event, + obj_matrix: mathutils.Matrix, + mouse_pos: tuple[int, int] = None, + ): if mouse_pos: ray_origin, ray_target, _ = cls.get_viewport_ray_data(context, event, mouse_pos) else: @@ -122,7 +134,13 @@ class Raycast(bonsai.core.tool.Raycast): return ray_origin_obj, ray_target_obj, ray_direction_obj @classmethod - def obj_ray_cast(cls, context, event, obj, mouse_pos=None): + def obj_ray_cast( + cls, + context: bpy.types.Context, + event: bpy.types.Event, + obj: bpy.types.Object, + mouse_pos: tuple[int, int] = None, + ): if mouse_pos: ray_origin_obj, _, ray_direction_obj = cls.get_object_ray_data( context, event, obj.matrix_world.copy(), mouse_pos @@ -136,7 +154,14 @@ class Raycast(bonsai.core.tool.Raycast): return None, None, None @classmethod - def ray_cast_by_proximity(cls, context, event, obj, face=None, custom_bmesh=None): + def ray_cast_by_proximity( + cls, + context: bpy.types.Context, + event: bpy.types.Event, + obj: bpy.types.Object, + face: bpy.types.MeshPolygon = None, + custom_bmesh: bmesh.types.BMesh = None, + ): region = context.region rv3d = context.region_data mouse_pos = event.mouse_region_x, event.mouse_region_y @@ -234,7 +259,7 @@ class Raycast(bonsai.core.tool.Raycast): return points @classmethod - def ray_cast_to_polyline(cls, context, event): + def ray_cast_to_polyline(cls, context: bpy.types.Context, event: bpy.types.Event): region = context.region rv3d = context.region_data mouse_pos = event.mouse_region_x, event.mouse_region_y @@ -269,7 +294,7 @@ class Raycast(bonsai.core.tool.Raycast): return polyline_verts @classmethod - def ray_cast_to_measure(cls, context, event, points): + def ray_cast_to_measure(cls, context: bpy.types.Context, event: bpy.types.Event, points: bpy.types.Collection): bm = bmesh.new() bm.verts.index_update() bm.edges.index_update() @@ -286,7 +311,9 @@ class Raycast(bonsai.core.tool.Raycast): return snapping_points @classmethod - def ray_cast_to_plane(cls, context, event, plane_origin, plane_normal): + def ray_cast_to_plane( + cls, context: bpy.types.Context, event: bpy.types.Event, plane_origin: Vector, plane_normal: Vector + ): region = context.region rv3d = context.region_data mouse_pos = event.mouse_region_x, event.mouse_region_y @@ -309,7 +336,7 @@ class Raycast(bonsai.core.tool.Raycast): return intersection @classmethod - def ray_cast_to_edge_intersection(cls, context, event, edges): + def ray_cast_to_edge_intersection(cls, context: bpy.types.Context, event: bpy.types.Event, edges: list[dict]): region = context.region rv3d = context.region_data mouse_pos = event.mouse_region_x, event.mouse_region_y From c80c7c6d36995f780e8686701c008c8705a5ff3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 16:46:09 -0300 Subject: [PATCH 348/476] Refactor - move `mouse_offset` from snap.py to raycast.py --- src/bonsai/bonsai/tool/raycast.py | 42 ++++++++++++++++++++----------- src/bonsai/bonsai/tool/snap.py | 21 +++------------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 99cae37461..4618096a47 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -28,6 +28,19 @@ from typing import Union class Raycast(bonsai.core.tool.Raycast): + offset = 10 + mouse_offset = ( + (-offset, offset), + (0, offset), + (offset, offset), + (-offset, 0), + (0, 0), + (offset, 0), + (-offset, -offset), + (0, -offset), + (offset, -offset), + ) + @classmethod def get_visible_objects(cls, context: bpy.types.Context): depsgraph = context.evaluated_depsgraph_get() @@ -70,19 +83,17 @@ class Raycast(bonsai.core.tool.Raycast): @classmethod def intersect_mouse_2d_bounding_box( - cls, mouse_pos: tuple[int, int], bbox: list[float, float, float, float], offset: int = None + cls, mouse_pos: tuple[int, int], bbox: list[float, float, float, float] ): - print("bbox", type(bbox)) - print(bbox) x, y = mouse_pos xmin, xmax, ymin, ymax = bbox # extends bbox boundaries to improve snap - if offset: - xmin -= offset - xmax += offset - ymin -= offset - ymax += offset + if cls.offset: + xmin -= cls.offset + xmax += cls.offset + ymin -= cls.offset + ymax += cls.offset if xmin < x < xmax and ymin < y < ymax: return True @@ -371,13 +382,12 @@ class Raycast(bonsai.core.tool.Raycast): context: bpy.types.Context, event: bpy.types.Event, objs_2d_bbox: Union[tuple[bpy.types.Object, list[float]]], - offset: int = None, ) -> list[bpy.types.Object]: mouse_pos = event.mouse_region_x, event.mouse_region_y objs_to_raycast = [] for obj, bbox_2d in objs_2d_bbox: if obj.type in {"MESH", "EMPTY", "CURVE"} and bbox_2d: - if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d, offset): + if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d): if ( obj.visible_in_viewport_get(bpy.context.space_data) or obj.library ): # Check for local view and local collections for this viewport and object @@ -390,7 +400,6 @@ class Raycast(bonsai.core.tool.Raycast): context: bpy.types.Context, event: bpy.types.Event, obj: bpy.types.Object, - mouse_offset: tuple[tuple[int, int]] = None, ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: mouse_pos = event.mouse_region_x, event.mouse_region_y @@ -411,7 +420,7 @@ class Raycast(bonsai.core.tool.Raycast): if hit is None: # Tried original mouse position. Now it will try the offsets. original_mouse_pos = mouse_pos - for value in mouse_offset: + for value in cls.mouse_offset: mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value)) hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, mouse_pos) if hit: @@ -429,7 +438,7 @@ class Raycast(bonsai.core.tool.Raycast): context: bpy.types.Context, event: bpy.types.Event, objs_to_raycast: list[bpy.types.Object], - mouse_offset: tuple[tuple[int, int]] = None, + include_wireframes: bool=True, ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: best_length_squared = 1.0 best_obj = None @@ -439,7 +448,12 @@ class Raycast(bonsai.core.tool.Raycast): ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event) for obj in objs_to_raycast: - snap_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, obj, mouse_offset) + if not include_wireframes and ( + obj.type in {"EMPTY", "CURVE"} or (hasattr(obj.data, "polygons") and len(obj.data.polygons) == 0) + ): + continue + + snap_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, obj) if hit is not None: length_squared = (hit - ray_origin).length_squared diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 4adf4f1fd3..20d551c121 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -273,23 +273,8 @@ class Snap(bonsai.core.tool.Snap): def detect_snapping_points(cls, context: bpy.types.Context, event: bpy.types.Event, objs_2d_bbox, tool_state): rv3d = context.region_data space = context.space_data - mouse_pos = event.mouse_region_x, event.mouse_region_y detected_snaps = [] - snap_threshold = 0.3 - offset = 10 - mouse_offset = ( - (-offset, offset), - (0, offset), - (offset, offset), - (-offset, 0), - (0, 0), - (offset, 0), - (-offset, -offset), - (0, -offset), - (offset, -offset), - ) - def select_plane_method(): if not last_polyline_point: plane_origin = Vector((0, 0, 0)) @@ -412,16 +397,16 @@ class Snap(bonsai.core.tool.Snap): detected_snaps.append(point) # Objects - objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, objs_2d_bbox, offset) + objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, objs_2d_bbox) if (space.shading.type == "SOLID" and space.shading.show_xray) or ( space.shading.type == "WIREFRAME" and space.shading.show_xray_wireframe ): results = [] for obj in objs_to_raycast: - results.append(tool.Raycast.cast_rays_to_single_object(context, event, obj, mouse_offset)) + results.append(tool.Raycast.cast_rays_to_single_object(context, event, obj)) else: results = [] - results.append(tool.Raycast.cast_rays_and_get_best_object(context, event, objs_to_raycast, mouse_offset)) + results.append(tool.Raycast.cast_rays_and_get_best_object(context, event, objs_to_raycast)) for result in results: snap_obj = result[0] From 46a31386da6dbd6611544430347a68fd4a57e87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 16:47:34 -0300 Subject: [PATCH 349/476] Add "Face Area" option to measure tool. See #6163. --- .../bonsai/bim/module/model/decorator.py | 70 ++++++++++++-- src/bonsai/bonsai/bim/module/model/prop.py | 1 + .../bonsai/bim/module/project/__init__.py | 1 + .../bonsai/bim/module/project/operator.py | 93 ++++++++++++++++++- src/bonsai/bonsai/bim/module/project/prop.py | 3 +- .../bonsai/bim/module/project/workspace.py | 5 +- src/bonsai/bonsai/tool/polyline.py | 2 + 7 files changed, 166 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 0a8c80b658..1919ffa97d 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -33,6 +33,7 @@ from gpu_extras.batch import batch_for_shader from gpu_extras.presets import draw_circle_2d from typing import Union from bonsai.bim.module.drawing.helper import format_distance +from itertools import chain def transparent_color(color, alpha=0.1): @@ -320,14 +321,15 @@ class PolylineDecorator: relating_type = None @classmethod - def install(cls, context): + def install(cls, context, ui_only=False): if cls.is_installed: cls.uninstall() handler = cls() - cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_snap_point, (context,), "WINDOW", "POST_PIXEL")) - cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL")) cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_ui, (context,), "WINDOW", "POST_PIXEL")) - cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")) + if not ui_only: + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_snap_point, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")) cls.is_installed = True @classmethod @@ -376,6 +378,7 @@ class PolylineDecorator: return (x_axis, y_axis, z_axis), (x_middle, y_middle, z_middle) + @classmethod def calculate_polygon(self, points): bm = bmesh.new() @@ -389,8 +392,8 @@ class PolylineDecorator: bm.verts.index_update() bm.edges.index_update() - verts = bm.verts - edges = bm.edges + verts = [v.co for v in bm.verts] + edges = [[v.index for v in e.verts] for e in bm.edges] tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()] bm.free() @@ -970,3 +973,58 @@ class SlabDirectionDecorator: base = [obj.matrix_world @ Vector(d) for d in base] self.draw_batch("LINES", dir, selected_elements_color, [(0, 1), (1, 2), (1, 3)]) self.draw_batch("LINES", base, selected_elements_color, [(0, 1)]) + + +class FaceAreaDecorator: + is_installed = False + handlers = [] + + @classmethod + def install(cls, context): + if cls.is_installed: + cls.uninstall() + handler = cls() + cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_face_area, (context,), "WINDOW", "POST_VIEW")) + cls.is_installed = True + + @classmethod + def uninstall(cls): + for handler in cls.handlers: + try: + SpaceView3D.draw_handler_remove(handler, "WINDOW") + except ValueError: + pass + cls.is_installed = False + + 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 draw_face_area(self, context): + def transparent_color(color, alpha=0.1): + color = [i for i in color] + color[3] = alpha + return color + + self.addon_prefs = tool.Blender.get_addon_preferences() + self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") + self.line_shader.bind() # required to be able to change uniforms of the shader + self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height)) + self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") + self.line_shader.uniform_float("lineWidth", 2.0) + gpu.state.point_size_set(6) + gpu.state.blend_set("ALPHA") + decorator_color = self.addon_prefs.decorator_color_special + + polyline_data = context.scene.BIMPolylineProperties.insertion_polyline + for i, polyline in enumerate(polyline_data): + vertices = [] + for point in polyline.polyline_points: + vertices.append((point.x, point.y, point.z)) + data = PolylineDecorator.calculate_polygon(vertices) + if data: + self.draw_batch("POINTS", data["verts"], decorator_color) + self.draw_batch("LINES", data["verts"], decorator_color, data["edges"]) + self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color, alpha=0.5), data["tris"]) diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 3f7dc9475f..e5e56115fd 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -1119,6 +1119,7 @@ class PolylinePoint(PropertyGroup): class Polyline(PropertyGroup): + id: bpy.props.StringProperty(name="Id") polyline_points: bpy.props.CollectionProperty(type=PolylinePoint) measurement_type: bpy.props.StringProperty(name="Measurement Type") area: bpy.props.StringProperty(name="Measured Area") diff --git a/src/bonsai/bonsai/bim/module/project/__init__.py b/src/bonsai/bonsai/bim/module/project/__init__.py index 6cc33bb7b9..81bf9e9ec7 100644 --- a/src/bonsai/bonsai/bim/module/project/__init__.py +++ b/src/bonsai/bonsai/bim/module/project/__init__.py @@ -48,6 +48,7 @@ classes = ( operator.LoadProject, operator.LoadProjectElements, operator.MeasureTool, + operator.MeasureFaceAreaTool, operator.ClearMeasurement, operator.NewProject, operator.QueryLinkedElement, diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 077ada3b6c..e84dabda74 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -61,7 +61,7 @@ from ifcopenshell.geom import ShapeElementType from bonsai.bim.module.project.data import LinksData, ProjectLibraryData from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator, MeasureDecorator from bonsai.bim.module.project.prop import BreadcrumbType -from bonsai.bim.module.model.decorator import PolylineDecorator +from bonsai.bim.module.model.decorator import PolylineDecorator, FaceAreaDecorator from bonsai.bim.module.model.polyline import PolylineOperator from typing import Union, TYPE_CHECKING, Literal, get_args @@ -2681,6 +2681,97 @@ class MeasureTool(bpy.types.Operator, PolylineOperator): return {"RUNNING_MODAL"} +class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator): + bl_idname = "bim.measure_face_area_tool" + bl_label = "Measure Face Area Tool" + bl_options = {"REGISTER", "UNDO"} + + measure_type: bpy.props.StringProperty() + + @classmethod + def poll(cls, context): + return context.space_data.type == "VIEW_3D" + + def __init__(self): + super().__init__() + self.input_options = ["AREA"] + self.input_ui = tool.Polyline.create_input_ui(input_options=self.input_options) + self.clicked_faces = [] + self.total_area = 0 + if tool.Ifc.get(): + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + else: + self.unit_scale = tool.Blender.get_unit_scale() + + def modal(self, context, event): + def select_face(mouse_pos): + objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, self.objs_2d_bbox) + obj, _, face_index = tool.Raycast.cast_rays_and_get_best_object( + context, event, objs_to_raycast, include_wireframes=False + ) + if face_index: + return obj, face_index + return None, None + + PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) + tool.Blender.update_viewport() + + if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: + self.handle_mouse_move(context, event) + return {"PASS_THROUGH"} + + self.handle_mouse_move(context, event) + + if event.value == "PRESS" and event.type == "LEFTMOUSE": + tool.Blender.update_viewport() + mouse_pos = event.mouse_region_x, event.mouse_region_y + obj, face_index = select_face(mouse_pos) + if face_index: + if obj.data.polygons[face_index] not in self.clicked_faces: + self.clicked_faces.append(obj.data.polygons[face_index]) + self.total_area += obj.data.polygons[face_index].area + self.input_ui.set_value("AREA", self.total_area) + polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline.add() + polyline_data.id = obj.name + str(face_index) + for v_id in obj.data.polygons[face_index].vertices: + vertex = obj.matrix_world @ obj.data.vertices[v_id].co + polyline_point = polyline_data.polyline_points.add() + polyline_point.x = vertex.x + polyline_point.y = vertex.y + polyline_point.z = vertex.z + tool.Blender.update_viewport() + + if event.shift and (event.value == "PRESS" and event.type == "LEFTMOUSE"): + mouse_pos = event.mouse_region_x, event.mouse_region_y + obj, face_index = select_face(mouse_pos) + if face_index: + if obj.data.polygons[face_index] in self.clicked_faces: + self.clicked_faces.remove(obj.data.polygons[face_index]) + self.total_area -= obj.data.polygons[face_index].area + self.input_ui.set_value("AREA", self.total_area) + polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline + for i, polyline in enumerate(polyline_data): + if polyline.id == obj.name + str(face_index): + polyline_data.remove(i) + tool.Blender.update_viewport() + + if event.value == "RELEASE" and event.type in {"ESC", "RIGHTMOUSE"}: + bpy.context.scene.BIMPolylineProperties.insertion_polyline.clear() + PolylineDecorator.uninstall() + FaceAreaDecorator.uninstall() + tool.Blender.update_viewport() + return {"CANCELLED"} + + return {"RUNNING_MODAL"} + + def invoke(self, context, event): + super().invoke(context, event) + PolylineDecorator.uninstall() + PolylineDecorator.install(context, ui_only=True) + FaceAreaDecorator.install(context) + return {"RUNNING_MODAL"} + + class ClearMeasurement(bpy.types.Operator): bl_idname = "bim.clear_measurement" bl_label = "Clear measurement from the screen" diff --git a/src/bonsai/bonsai/bim/module/project/prop.py b/src/bonsai/bonsai/bim/module/project/prop.py index a8ca1f2edd..3d57a7b926 100644 --- a/src/bonsai/bonsai/bim/module/project/prop.py +++ b/src/bonsai/bonsai/bim/module/project/prop.py @@ -499,7 +499,8 @@ class MeasureToolSettings(PropertyGroup): measurement_type_items = [ ("SINGLE", "SINGLE", "Single", "FIXED_SIZE", 1), ("POLYLINE", "POLYLINE", "Polyline", "DRIVER_ROTATIONAL_DIFFERENCE", 2), - ("AREA", "AREA", "Area", "OUTLINER_DATA_LIGHTPROBE", 3), + ("POLY_AREA", "POLY_AREA", "Poyline Area", "OUTLINER_DATA_LIGHTPROBE", 3), + ("FACE_AREA", "FACE_AREA", "Face Area", "FACESEL", 4), ] measurement_type: bpy.props.EnumProperty(items=measurement_type_items, default="POLYLINE") diff --git a/src/bonsai/bonsai/bim/module/project/workspace.py b/src/bonsai/bonsai/bim/module/project/workspace.py index bf3abe07c9..d58ad55adb 100644 --- a/src/bonsai/bonsai/bim/module/project/workspace.py +++ b/src/bonsai/bonsai/bim/module/project/workspace.py @@ -106,4 +106,7 @@ class ExploreHotkey(bpy.types.Operator): for obj in tool.Blender.get_selected_objects(): obj.select_set(False) measure_type = bpy.context.scene.MeasureToolSettings.measurement_type - bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type=measure_type) + if measure_type == "FACE_AREA": + bpy.ops.bim.measure_face_area_tool("INVOKE_DEFAULT") + else: + bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type=measure_type) diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py index ac9f741d3b..870cd1b278 100644 --- a/src/bonsai/bonsai/tool/polyline.py +++ b/src/bonsai/bonsai/tool/polyline.py @@ -66,6 +66,8 @@ class Polyline(bonsai.core.tool.Polyline): if attribute_name == "A": value = float(self.get_text_value(attribute_name)) return f"{value:.2f}°" + if attribute_name == "AREA": + return Polyline.format_input_ui_units(value, True) else: return Polyline.format_input_ui_units(value) From ebd719783c5572aea2b99e4d3d9f1635c3bcd348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 16:56:22 -0300 Subject: [PATCH 350/476] black . --- .../bonsai/bim/module/model/decorator.py | 8 ++- src/bonsai/bonsai/tool/raycast.py | 6 +- src/bonsai/bonsai/tool/snap.py | 61 ------------------- 3 files changed, 8 insertions(+), 67 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 1919ffa97d..020b6e11ad 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -327,8 +327,12 @@ class PolylineDecorator: handler = cls() cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_ui, (context,), "WINDOW", "POST_PIXEL")) if not ui_only: - cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_snap_point, (context,), "WINDOW", "POST_PIXEL")) - cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append( + SpaceView3D.draw_handler_add(handler.draw_snap_point, (context,), "WINDOW", "POST_PIXEL") + ) + cls.handlers.append( + SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL") + ) cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW")) cls.is_installed = True diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 4618096a47..dae97fcf95 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -82,9 +82,7 @@ class Raycast(bonsai.core.tool.Raycast): return (obj, bbox_2d) @classmethod - def intersect_mouse_2d_bounding_box( - cls, mouse_pos: tuple[int, int], bbox: list[float, float, float, float] - ): + def intersect_mouse_2d_bounding_box(cls, mouse_pos: tuple[int, int], bbox: list[float, float, float, float]): x, y = mouse_pos xmin, xmax, ymin, ymax = bbox @@ -438,7 +436,7 @@ class Raycast(bonsai.core.tool.Raycast): context: bpy.types.Context, event: bpy.types.Event, objs_to_raycast: list[bpy.types.Object], - include_wireframes: bool=True, + include_wireframes: bool = True, ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: best_length_squared = 1.0 best_obj = None diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 20d551c121..e49d97ad5c 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -310,67 +310,6 @@ class Snap(bonsai.core.tool.Snap): plane_normal = tool.Polyline.use_transform_orientations(plane_normal) return plane_origin, plane_normal - def cast_rays_to_single_object( - obj: bpy.types.Object, mouse_pos: tuple[int, int] - ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: - hit = None - face_index = None - # Wireframes - if obj.type in {"EMPTY", "CURVE"} or (hasattr(obj.data, "polygons") and len(obj.data.polygons) == 0) : - snap_points = tool.Raycast.ray_cast_by_proximity(context, event, obj) - if snap_points: - hit = sorted(snap_points, key=lambda x: x["distance"])[0]["point"] - if hit: - hit_world = obj.original.matrix_world @ hit - return obj, hit_world, face_index - return None, None, None - # Meshes - else: - hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj) - if hit is None: - # Tried original mouse position. Now it will try the offsets. - original_mouse_pos = mouse_pos - for value in mouse_offset: - mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value)) - hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, mouse_pos) - if hit: - break - mouse_pos = original_mouse_pos - if hit: - hit_world = obj.original.matrix_world @ hit - return obj, hit_world, face_index - else: - return None, None, None - - def cast_rays_and_get_best_object( - objs_to_raycast: list[bpy.types.Object], mouse_pos: tuple[int, int] - ) -> Union[tuple[bpy.types.Object, Vector, int], tuple[None, None, None]]: - best_length_squared = 1.0 - best_obj = None - best_hit = None - best_face_index = None - - for obj in objs_to_raycast: - snap_obj, hit, face_index = cast_rays_to_single_object(obj, mouse_pos) - - if hit is not None: - length_squared = (hit - ray_origin).length_squared - if best_obj is None or length_squared < best_length_squared: - best_length_squared = length_squared - best_obj = snap_obj - best_hit = hit - best_face_index = face_index - - if best_obj is not None: - return best_obj, best_hit, best_face_index - - else: - return None, None, None - - ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event) - - objs_to_raycast = cls.filter_objects_to_raycast(context, objs_2d_bbox, mouse_pos, offset) - # Polyline try: polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline[0] From 7b7108b9389104077feb225c6cbfc554c8338c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 17:02:40 -0300 Subject: [PATCH 351/476] `MeasureFaceAreaTool` - Fix issue where face with index zero where not being selected --- src/bonsai/bonsai/bim/module/project/operator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index e84dabda74..5322a69fe5 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2709,7 +2709,7 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator): obj, _, face_index = tool.Raycast.cast_rays_and_get_best_object( context, event, objs_to_raycast, include_wireframes=False ) - if face_index: + if face_index is not None: return obj, face_index return None, None @@ -2726,7 +2726,7 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator): tool.Blender.update_viewport() mouse_pos = event.mouse_region_x, event.mouse_region_y obj, face_index = select_face(mouse_pos) - if face_index: + if face_index is not None: if obj.data.polygons[face_index] not in self.clicked_faces: self.clicked_faces.append(obj.data.polygons[face_index]) self.total_area += obj.data.polygons[face_index].area From 8f6cd1828820645485c853c9051a520ac529ac39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Thu, 13 Mar 2025 21:32:13 -0300 Subject: [PATCH 352/476] Add instruction to `MeasureFaceArea` tool --- src/bonsai/bonsai/bim/module/model/polyline.py | 15 +++++++++------ src/bonsai/bonsai/bim/module/project/operator.py | 8 ++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index ed887440d7..9c2aa1468a 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -662,7 +662,7 @@ class PolylineOperator: tool.Blender.update_viewport() def handle_instructions( - self, context: bpy.types.Context, custom_instructions: dict = {}, custom_info: str = "" + self, context: bpy.types.Context, custom_instructions: dict = {}, custom_info: str = "", overwrite: bool = False ) -> None: self.info = [ f"Axis: {self.tool_state.axis_method}", @@ -670,9 +670,12 @@ class PolylineOperator: f"Snap: {self.snapping_points[0]['type']}", ] instructions = self.instructions | custom_instructions if custom_instructions else self.instructions - infos = self.info + custom_info if custom_info else self.info + if overwrite: + instructions = custom_instructions + infos = custom_info + def draw_instructions(self: bpy.types.Header, context: bpy.types.Context) -> None: for action, settings in instructions.items(): if settings["icons"]: @@ -683,10 +686,10 @@ class PolylineOperator: key = settings["keys"][0] self.layout.label(text=key + action) - self.layout.label(text="|") - - for info in infos: - self.layout.label(text=info) + if infos: + self.layout.label(text="|") + for info in infos: + self.layout.label(text=info) context.workspace.status_text_set(draw_instructions) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 5322a69fe5..1364be707e 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2716,6 +2716,13 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator): PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0]) tool.Blender.update_viewport() + custom_instructions = { + "Select Face": {"icons": True, "keys": ["MOUSE_LMB"]}, + "Deselect Face": {"icons": True, "keys": ["EVENT_SHIFT", "MOUSE_LMB"]} + } + custom_info = [] + self.handle_instructions(context, custom_instructions, custom_info, overwrite=True) + if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: self.handle_mouse_move(context, event) return {"PASS_THROUGH"} @@ -2757,6 +2764,7 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator): if event.value == "RELEASE" and event.type in {"ESC", "RIGHTMOUSE"}: bpy.context.scene.BIMPolylineProperties.insertion_polyline.clear() + context.workspace.status_text_set(text=None) PolylineDecorator.uninstall() FaceAreaDecorator.uninstall() tool.Blender.update_viewport() From 069b630b160c71758e97ec8298dd2e342ccdc868 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 14 Mar 2025 14:41:14 +1100 Subject: [PATCH 353/476] Minor fix where you'd randomly get incorrect ports connected based on the first birdsong of spring --- src/bonsai/bonsai/bim/module/model/mep.py | 10 +++-- src/bonsai/bonsai/bim/module/model/product.py | 5 ++- src/bonsai/test/bim/feature/system.feature | 39 +++++++++++++++++++ src/bonsai/test/bim/test_feature.py | 9 ++++- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/mep.py b/src/bonsai/bonsai/bim/module/model/mep.py index 72d7a8e2f7..6744448f08 100644 --- a/src/bonsai/bonsai/bim/module/model/mep.py +++ b/src/bonsai/bonsai/bim/module/model/mep.py @@ -921,11 +921,11 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): start_element = tool.Ifc.get_entity(start_object) end_element = tool.Ifc.get_entity(end_object) if not start_element or not end_element: - self.report({"ERROR"}, f"Two IFC elements should be selected for the bend.") + self.report({"ERROR"}, "Two IFC elements should be selected for the bend.") return {"CANCELLED"} else: - self.report({"ERROR"}, f"Two IFC elements should be provided for the bend.") + self.report({"ERROR"}, "Two IFC elements should be provided for the bend.") return {"CANCELLED"} # check rotation difference @@ -1248,10 +1248,14 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): return matrix fitting_obj.matrix_world = get_fitting_matrix() + tool.Model.sync_object_ifc_position(fitting_obj) # add ports and connect them ports = tool.System.get_ports(tool.Ifc.get_entity(fitting_obj)) - if not start_port_match: + start_co = ifcopenshell.util.placement.get_local_placement(start_port.ObjectPlacement)[:,3] + port0_co = ifcopenshell.util.placement.get_local_placement(ports[0].ObjectPlacement)[:,3] + # We cannot use start_port_match because tool.System.get_ports is unordered + if not np.allclose(start_co, port0_co): start_port, end_port = end_port, start_port tool.Ifc.run("system.connect_port", port1=ports[0], port2=start_port, direction="NOTDEFINED") tool.Ifc.run("system.connect_port", port1=ports[1], port2=end_port, direction="NOTDEFINED") diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 9a6c6a2a97..12bae7ae18 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -495,15 +495,16 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator): elif props.rl_mode == "CURSOR": pass + tool.Model.sync_object_ifc_position(obj) + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) for port in ifcopenshell.util.system.get_ports(relating_type): mat = Matrix(ifcopenshell.util.placement.get_local_placement(port.ObjectPlacement)) mat.translation *= unit_scale mat = obj.matrix_world @ mat - new_port = tool.Ifc.run("root.create_entity", ifc_class="IfcDistributionPort") + new_port = tool.Ifc.run("system.add_port", element=element) new_port.PredefinedType = port.PredefinedType new_port.SystemType = port.SystemType - tool.Ifc.run("system.assign_port", element=element, port=new_port) tool.Ifc.run("geometry.edit_object_placement", product=new_port, matrix=mat, is_si=True) if ifc_class == "IfcDoorType" and len(context.selected_objects) >= 1: diff --git a/src/bonsai/test/bim/feature/system.feature b/src/bonsai/test/bim/feature/system.feature index f9ebbcf915..6b900a0c04 100644 --- a/src/bonsai/test/bim/feature/system.feature +++ b/src/bonsai/test/bim/feature/system.feature @@ -159,6 +159,45 @@ Scenario: Connect MEP elements And the object "IfcActuator/Actuator" is at "5.5,0.0,1.0" And the variable "connected_elements" is "set(tool.System.get_connected_elements({ifc}.by_type('IfcActuator')[0]))" +Scenario: Add bend - and regenerate with no changes + Given an empty IFC project + And I create default MEP types + And the variable "segment_types" is "[str(e.id()) for e in {ifc}.by_type('IfcDuctSegmentType')]" + And the variable "actuator_type_id" is "{ifc}.by_type('IfcActuatorType')[0].id()" + + # segment1 + And I set "scene.BIMModelProperties.ifc_class" to "IfcDuctSegmentType" + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" + And I set "scene.BIMModelProperties.extrusion_depth" to "5.0" + And I press "bim.add_occurrence" + And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg1" + + # segment2 + And I set "scene.BIMModelProperties.relating_type_id" to "{segment_types}[0]" + And I press "bim.add_occurrence" + And I rename the object "IfcDuctSegment/DuctSegment" to "IfcDuctSegment/Seg2" + And the object "IfcDuctSegment/Seg2" is rotated by "0,0,90" deg + + # bend between segments 1 and 2 + When the object "IfcDuctSegment/Seg1" is selected + And additionally the object "IfcDuctSegment/Seg2" is selected + And I press "bim.mep_add_bend" + + Then the object "IfcDuctSegment/Seg1" is at "0.5,0,1" + And the object "IfcDuctSegment/Seg2" is at "0,0.5,1" + And the object "IfcDuctFitting/DuctFitting" is at "0,0.5,1" + And the object "IfcDuctFitting/DuctFitting" dimensions are "0.7,0.2,0.7" + And the object "IfcDuctSegment/Seg1" dimensions are "0.4,0.2,4.5" + And the object "IfcDuctSegment/Seg2" dimensions are "0.4,0.2,4.5" + + When I press "bim.regenerate_distribution_element" + Then the object "IfcDuctSegment/Seg1" is at "0.5,0,1" + And the object "IfcDuctSegment/Seg2" is at "0,0.5,1" + And the object "IfcDuctFitting/DuctFitting" is at "0,0.5,1" + And the object "IfcDuctFitting/DuctFitting" dimensions are "0.7,0.2,0.7" + And the object "IfcDuctSegment/Seg1" dimensions are "0.4,0.2,4.5" + And the object "IfcDuctSegment/Seg2" dimensions are "0.4,0.2,4.5" + Scenario: Connect MEP elements and regenerate Given an empty IFC project And I create default MEP types diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 9cc71bd4f5..4a37caa2e1 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -1200,7 +1200,7 @@ def the_object_name_is_at_location(name, location): obj_location = the_object_name_exists(name).location assert ( obj_location - Vector([float(co) for co in location.split(",")]) - ).length < 0.1, f"Object is at {obj_location}" + ).length < 0.1, f"Object is at {obj_location} instead of {location}" @then(parsers.parse('the object "{name}" has a vertex at "{location}"')) @@ -1451,6 +1451,13 @@ def run_test_code(): pass +@given(parsers.parse("I fail")) +@when(parsers.parse("I fail")) +@then(parsers.parse("I fail")) +def i_fail(): + assert False + + @given(parsers.parse("I save sample test files")) @when(parsers.parse("I save sample test files")) @then(parsers.parse("I save sample test files")) From 10ecac33b81684e3958edd311fd15706cca670da Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 16:33:28 +0500 Subject: [PATCH 354/476] black . --- src/bonsai/bonsai/bim/module/model/mep.py | 4 ++-- src/bonsai/bonsai/bim/module/model/polyline.py | 1 - src/bonsai/bonsai/bim/module/project/operator.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/mep.py b/src/bonsai/bonsai/bim/module/model/mep.py index 6744448f08..bbbf11a021 100644 --- a/src/bonsai/bonsai/bim/module/model/mep.py +++ b/src/bonsai/bonsai/bim/module/model/mep.py @@ -1252,8 +1252,8 @@ class MEPAddBend(bpy.types.Operator, tool.Ifc.Operator): # add ports and connect them ports = tool.System.get_ports(tool.Ifc.get_entity(fitting_obj)) - start_co = ifcopenshell.util.placement.get_local_placement(start_port.ObjectPlacement)[:,3] - port0_co = ifcopenshell.util.placement.get_local_placement(ports[0].ObjectPlacement)[:,3] + start_co = ifcopenshell.util.placement.get_local_placement(start_port.ObjectPlacement)[:, 3] + port0_co = ifcopenshell.util.placement.get_local_placement(ports[0].ObjectPlacement)[:, 3] # We cannot use start_port_match because tool.System.get_ports is unordered if not np.allclose(start_co, port0_co): start_port, end_port = end_port, start_port diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 9c2aa1468a..a4cd9cc22f 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -850,7 +850,6 @@ class PolylineOperator: tool.Polyline.remove_last_polyline_point() tool.Blender.update_viewport() - def handle_snap_selection(self, context: bpy.types.Context, event: bpy.types.Event) -> None: if not self.tool_state.is_input_on and event.value == "PRESS" and event.type == "M": self.snapping_points = tool.Snap.modify_snapping_point_selection( diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 1364be707e..49b58cd184 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2718,7 +2718,7 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator): custom_instructions = { "Select Face": {"icons": True, "keys": ["MOUSE_LMB"]}, - "Deselect Face": {"icons": True, "keys": ["EVENT_SHIFT", "MOUSE_LMB"]} + "Deselect Face": {"icons": True, "keys": ["EVENT_SHIFT", "MOUSE_LMB"]}, } custom_info = [] self.handle_instructions(context, custom_instructions, custom_info, overwrite=True) From 0e8810c1f5bc24be4bf41fb2a2aedbf5c1daad71 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 10:43:47 +0500 Subject: [PATCH 355/476] typing --- src/bonsai/bonsai/bim/module/pset/ui.py | 2 +- .../bonsai/bim/module/qto/calculator.py | 8 + src/bonsai/bonsai/bim/module/qto/helper.py | 1 + src/bonsai/bonsai/bim/module/qto/operator.py | 14 +- src/bonsai/bonsai/bim/module/qto/prop.py | 29 +++- src/bonsai/bonsai/bim/module/qto/ui.py | 7 +- .../bonsai/bim/module/structural/data.py | 7 +- .../module/structural/load_decoration_data.py | 11 +- .../bonsai/bim/module/structural/operator.py | 140 +++++++++--------- .../bonsai/bim/module/structural/prop.py | 99 +++++++++++-- src/bonsai/bonsai/bim/module/structural/ui.py | 110 +++++++++++--- .../bonsai/bim/module/structural/workspace.py | 6 +- src/bonsai/bonsai/tool/qto.py | 13 +- src/bonsai/bonsai/tool/structural.py | 37 +++-- src/bonsai/test/tool/test_qto.py | 3 +- src/ifc5d/ifc5d/qto.py | 54 ++++--- .../api/aggregate/assign_object.py | 21 +-- .../api/boundary/copy_boundary.py | 6 +- .../api/boundary/remove_boundary.py | 12 +- .../ifcopenshell/api/constraint/add_metric.py | 13 +- .../api/constraint/add_objective.py | 3 - .../api/constraint/remove_constraint.py | 4 +- .../api/context/remove_context.py | 20 +-- .../api/control/assign_control.py | 26 ++-- .../api/control/unassign_control.py | 15 +- .../api/cost/add_cost_item_quantity.py | 15 +- .../api/cost/add_cost_schedule.py | 9 +- .../ifcopenshell/api/cost/add_cost_value.py | 22 ++- .../api/cost/assign_cost_value.py | 13 +- .../calculate_cost_item_resource_value.py | 10 +- .../ifcopenshell/api/cost/remove_cost_item.py | 14 +- .../api/cost/remove_cost_schedule.py | 8 +- .../api/document/add_reference.py | 12 +- .../api/document/unassign_document.py | 17 +-- .../api/drawing/assign_product.py | 35 ++--- .../api/drawing/unassign_product.py | 14 +- .../ifcopenshell/api/feature/add_filling.py | 13 +- .../api/feature/remove_filling.py | 6 +- .../api/geometry/add_axis_representation.py | 3 - .../api/geometry/connect_element.py | 26 ++-- .../api/geometry/map_representation.py | 16 +- .../ifcopenshell/api/group/add_group.py | 19 +-- .../ifcopenshell/api/group/remove_group.py | 12 +- .../ifcopenshell/api/library/add_reference.py | 10 +- .../api/library/assign_reference.py | 26 ++-- .../api/library/unassign_reference.py | 16 +- .../api/material/add_list_item.py | 11 +- .../ifcopenshell/api/material/add_material.py | 16 +- .../api/material/add_material_set.py | 13 +- .../api/material/remove_material.py | 8 +- .../api/material/remove_material_set.py | 22 ++- .../ifcopenshell/api/owner/add_actor.py | 12 +- .../ifcopenshell/api/owner/add_address.py | 11 +- .../api/owner/add_organisation.py | 11 +- .../ifcopenshell/api/owner/add_person.py | 16 +- .../api/owner/add_person_and_organisation.py | 7 +- .../ifcopenshell/api/owner/assign_actor.py | 24 +-- .../ifcopenshell/api/owner/remove_actor.py | 8 +- .../ifcopenshell/api/owner/remove_address.py | 10 +- .../api/owner/remove_application.py | 6 +- .../owner/remove_person_and_organisation.py | 12 +- .../ifcopenshell/api/owner/remove_role.py | 12 +- .../ifcopenshell/api/owner/unassign_actor.py | 14 +- .../api/owner/update_owner_history.py | 3 - .../api/profile/add_parameterized_profile.py | 6 +- .../api/profile/remove_profile.py | 5 +- .../ifcopenshell/api/project/create_file.py | 4 +- .../ifcopenshell/api/pset/remove_pset.py | 25 ++-- .../api/pset_template/add_pset_template.py | 12 +- .../api/pset_template/remove_prop_template.py | 10 +- .../api/pset_template/remove_pset_template.py | 6 +- .../ifcopenshell/api/resource/add_resource.py | 23 +-- .../api/resource/assign_resource.py | 27 +--- .../api/resource/calculate_resource_usage.py | 17 ++- .../api/resource/remove_resource_quantity.py | 9 +- .../api/resource/unassign_resource.py | 14 +- .../ifcopenshell/api/root/create_entity.py | 32 ++-- .../ifcopenshell/api/sequence/add_task.py | 40 ++--- .../api/sequence/add_time_period.py | 18 +-- .../api/sequence/add_work_calendar.py | 8 +- .../api/sequence/add_work_plan.py | 17 +-- .../api/sequence/add_work_schedule.py | 25 ++-- .../api/sequence/add_work_time.py | 17 +-- .../api/sequence/assign_process.py | 24 +-- .../api/sequence/assign_product.py | 24 +-- .../api/sequence/assign_work_plan.py | 16 +- .../api/sequence/duplicate_task.py | 5 +- .../ifcopenshell/api/sequence/remove_task.py | 36 ++--- .../api/sequence/remove_time_period.py | 6 +- .../api/sequence/remove_work_calendar.py | 20 +-- .../api/sequence/remove_work_plan.py | 10 +- .../api/sequence/remove_work_schedule.py | 26 ++-- .../api/sequence/unassign_process.py | 14 +- .../api/sequence/unassign_product.py | 14 +- .../sequence/unassign_recurrence_pattern.py | 8 +- .../api/sequence/unassign_sequence.py | 14 +- .../api/spatial/assign_container.py | 22 +-- .../api/spatial/unassign_container.py | 12 +- .../api/structural/add_structural_activity.py | 25 +--- .../add_structural_boundary_condition.py | 20 +-- .../add_structural_member_connection.py | 15 +- .../remove_structural_connection_condition.py | 12 +- .../api/style/remove_styled_representation.py | 10 +- .../ifcopenshell/api/system/add_port.py | 14 +- .../ifcopenshell/api/system/add_system.py | 6 +- .../api/system/assign_flow_control.py | 26 ++-- .../ifcopenshell/api/system/assign_port.py | 44 +++--- .../api/system/disconnect_port.py | 8 +- .../ifcopenshell/api/system/remove_system.py | 14 +- .../api/system/unassign_flow_control.py | 16 +- .../ifcopenshell/api/system/unassign_port.py | 27 ++-- .../api/type/map_type_representations.py | 22 +-- .../api/unit/add_context_dependent_unit.py | 12 +- .../api/unit/add_conversion_based_unit.py | 15 +- .../api/unit/add_monetary_unit.py | 6 +- .../ifcopenshell/api/unit/add_si_unit.py | 9 +- .../ifcopenshell/api/unit/remove_unit.py | 10 +- .../ifcopenshell/util/cost.py | 2 +- 118 files changed, 869 insertions(+), 1203 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index de8cea8a95..5bc6de9b6f 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -380,7 +380,7 @@ class BIM_PT_object_qtos(Panel): filter_keyword=context.scene.GlobalPsetProperties.qto_filter, ) layout = self.layout - qtoprops = context.scene.BIMQtoProperties + qtoprops = tool.Qto.get_qto_props() row = layout.row(align=True) row.prop(qtoprops, "qto_rule", text="") # A bit confusing as we typically use this icon for is_null. diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index 38bc8ca423..1cfd731bb3 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -76,6 +76,7 @@ def get_length(o: bpy.types.Object, vg_index: Optional[int] = None) -> float: return max(y, z) length = 0 + assert isinstance(o.data, bpy.types.Mesh) edges = [ e for e in o.data.edges @@ -269,6 +270,7 @@ def get_net_perimeter(o: bpy.types.Object) -> float: def get_gross_perimeter(o: bpy.types.Object) -> float: element = tool.Ifc.get_entity(o) + assert element mesh = get_gross_element_mesh(element) gross_obj = bpy.data.objects.new("GrossObj", mesh) gross_perimeter = get_net_perimeter(gross_obj) @@ -420,6 +422,7 @@ def get_gross_footprint_area(o: bpy.types.Object) -> float: return get_net_footprint_area(o) element = tool.Ifc.get_entity(o) + assert element mesh = get_gross_element_mesh(element) gross_obj = bpy.data.objects.new("GrossObj", mesh) gross_footprint_area = get_net_footprint_area(gross_obj) @@ -474,6 +477,7 @@ def get_gross_surface_area(o: bpy.types.Object, vg_index: Optional[int] = None) return get_net_surface_area(o) element = tool.Ifc.get_entity(o) + assert element mesh = get_gross_element_mesh(element) area = get_mesh_area(mesh) bpy.data.meshes.remove(mesh) @@ -506,6 +510,7 @@ def is_polygon_in_vg(polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.ty def get_net_volume(o: bpy.types.Object) -> float: + assert isinstance(o.data, bpy.types.Mesh) o_mesh = bmesh.new() o_mesh.from_mesh(o.data) volume = o_mesh.calc_volume() @@ -518,6 +523,7 @@ def get_gross_volume(o: bpy.types.Object) -> float: return get_net_volume(o) element = tool.Ifc.get_entity(o) + assert element mesh = get_gross_element_mesh(element) bm = get_bmesh_from_mesh(mesh) @@ -562,6 +568,7 @@ def get_net_weight(obj: bpy.types.Object) -> Union[float, None]: def get_obj_mass_density(obj: bpy.types.Object) -> Union[float, None]: entity = tool.Ifc.get_entity(obj) + assert entity material = ifcopenshell.util.element.get_material(entity) if material is None: return @@ -764,6 +771,7 @@ def get_outer_surface_area(obj: bpy.types.Object) -> float: def get_end_area(obj: bpy.types.Object) -> float: element = tool.Ifc.get_entity(obj) + assert element gross_mesh = get_gross_element_mesh(element) gross_obj = bpy.data.objects.new("MyObject", gross_mesh) diff --git a/src/bonsai/bonsai/bim/module/qto/helper.py b/src/bonsai/bonsai/bim/module/qto/helper.py index 47fd42642e..a796d0a164 100644 --- a/src/bonsai/bonsai/bim/module/qto/helper.py +++ b/src/bonsai/bonsai/bim/module/qto/helper.py @@ -51,6 +51,7 @@ def calculate_mesh_quantity( result = 0 edit_mode = context.active_object.mode == "EDIT" for obj in objs: + assert isinstance(obj.data, bpy.types.Mesh) if edit_mode: bm = bmesh.from_edit_mesh(obj.data) result += operation(bm) diff --git a/src/bonsai/bonsai/bim/module/qto/operator.py b/src/bonsai/bonsai/bim/module/qto/operator.py index 8a996fc383..0f4c706dbd 100644 --- a/src/bonsai/bonsai/bim/module/qto/operator.py +++ b/src/bonsai/bonsai/bim/module/qto/operator.py @@ -49,7 +49,7 @@ class CalculateEdgeLengths(bpy.types.Operator): def execute(self, context): result = helper.calculate_edges_lengths([o for o in context.selected_objects if o.type == "MESH"], context) - context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) + tool.Qto.set_qto_result(result) return {"FINISHED"} @@ -64,7 +64,7 @@ class CalculateFaceAreas(bpy.types.Operator): def execute(self, context): result = helper.calculate_faces_areas([o for o in context.selected_objects if o.type == "MESH"], context) - context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) + tool.Qto.set_qto_result(result) return {"FINISHED"} @@ -79,7 +79,7 @@ class CalculateObjectVolumes(bpy.types.Operator): def execute(self, context): result = helper.calculate_volumes([o for o in context.selected_objects if o.type == "MESH"], context) - context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) + tool.Qto.set_qto_result(result) return {"FINISHED"} @@ -94,7 +94,7 @@ class CalculateFormworkArea(bpy.types.Operator): def execute(self, context): result = helper.calculate_formwork_area([o for o in context.selected_objects if o.type == "MESH"], context) - context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) + tool.Qto.set_qto_result(result) return {"FINISHED"} @@ -109,7 +109,7 @@ class CalculateSideFormworkArea(bpy.types.Operator): def execute(self, context): result = helper.calculate_side_formwork_area([o for o in context.selected_objects if o.type == "MESH"], context) - context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) + tool.Qto.set_qto_result(result) return {"FINISHED"} @@ -126,7 +126,7 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): import ifc5d.qto - props = context.scene.BIMQtoProperties + props = tool.Qto.get_qto_props() elements = set() for obj in tool.Blender.get_selected_objects(include_active=False): element = tool.Ifc.get_entity(obj) @@ -163,7 +163,7 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): import ifc5d.qto - props = context.scene.BIMQtoProperties + props = tool.Qto.get_qto_props() elements: set[ifcopenshell.entity_instance] if context.selected_objects: diff --git a/src/bonsai/bonsai/bim/module/qto/prop.py b/src/bonsai/bonsai/bim/module/qto/prop.py index 0a73240e64..fa90ba6f27 100644 --- a/src/bonsai/bonsai/bim/module/qto/prop.py +++ b/src/bonsai/bonsai/bim/module/qto/prop.py @@ -31,12 +31,16 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Union -def get_qto_rule(self, context): +CALCULATOR_FUNCTION_ENUM_ITEMS: list[Union[tuple[str, str, str], None]] = [] + + +def get_qto_rule(self: "BIMQtoProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: ifc_file = tool.Ifc.get() is_ifc4x3 = ifc_file.schema == "IFC4X3" - results = [] + results: list[tuple[str, str, str]] = [] for rule_id, rule in ifc5d.qto.rules.items(): if rule_id.startswith("IFC4X3") != is_ifc4x3: continue @@ -44,14 +48,16 @@ def get_qto_rule(self, context): return results -def get_calculator(self, context): - results = [] +def get_calculator(self: "BIMQtoProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: + results: list[tuple[str, str, str]] = [] for name, calculator in ifc5d.qto.calculators.items(): - results.append((name, name, calculator.__doc__)) + results.append((name, name, calculator.__doc__ or "")) return results -def get_calculator_function(self, context): +def get_calculator_function( + self: "BIMQtoProperties", context: bpy.types.Context +) -> list[Union[tuple[str, str, str], None]]: global CALCULATOR_FUNCTION_ENUM_ITEMS calculator = ifc5d.qto.calculators[self.calculator] CALCULATOR_FUNCTION_ENUM_ITEMS = [] @@ -60,6 +66,8 @@ def get_calculator_function(self, context): measure = function.measure.split("Measure")[0][3:] if previous_measure is not None and measure != previous_measure: CALCULATOR_FUNCTION_ENUM_ITEMS.append(None) + description = function.description + description += f"\n\nInternal function id: '{function_id}'." CALCULATOR_FUNCTION_ENUM_ITEMS.append((function_id, f"{measure}: {function.name}", function.description)) previous_measure = measure return CALCULATOR_FUNCTION_ENUM_ITEMS @@ -80,3 +88,12 @@ class BIMQtoProperties(PropertyGroup): ), default=False, ) + + if TYPE_CHECKING: + qto_rule: str + calculator: str + calculator_function: str + qto_result: str + qto_name: str + prop_name: str + fallback: bool diff --git a/src/bonsai/bonsai/bim/module/qto/ui.py b/src/bonsai/bonsai/bim/module/qto/ui.py index 49840ba4be..4762062799 100644 --- a/src/bonsai/bonsai/bim/module/qto/ui.py +++ b/src/bonsai/bonsai/bim/module/qto/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bonsai.bim.module.qto.data import QtoData @@ -32,7 +33,7 @@ class BIM_PT_qto(bpy.types.Panel): def draw(self, context): layout = self.layout - props = context.scene.BIMQtoProperties + props = tool.Qto.get_qto_props() row = layout.row() if context.selected_objects: @@ -57,7 +58,7 @@ class BIM_PT_qto_manual(bpy.types.Panel): def draw(self, context): layout = self.layout - props = context.scene.BIMQtoProperties + props = tool.Qto.get_qto_props() row = layout.row() row.prop(props, "calculator") @@ -83,7 +84,7 @@ class BIM_PT_qto_simple(bpy.types.Panel): def draw(self, context): layout = self.layout - props = context.scene.BIMQtoProperties + props = tool.Qto.get_qto_props() row = layout.row() row.prop(props, "qto_result", text="Results") diff --git a/src/bonsai/bonsai/bim/module/structural/data.py b/src/bonsai/bonsai/bim/module/structural/data.py index 5c98eadbe8..d2cf1f3fbd 100644 --- a/src/bonsai/bonsai/bim/module/structural/data.py +++ b/src/bonsai/bonsai/bim/module/structural/data.py @@ -59,7 +59,7 @@ class LoadGroupDecorationData: return ret m = models[0] - props = bpy.context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() if props.activity_type == "Action": groups = m.LoadedBy or [] for g in groups: @@ -128,7 +128,8 @@ class ConnectedStructuralMembersData: if not element: return [] results = [] - props = bpy.context.active_object.BIMStructuralProperties + assert obj + props = tool.Structural.get_object_structural_props(obj) for rel in element.ConnectsStructuralMembers or []: condition = rel.AppliedCondition if condition: @@ -257,7 +258,7 @@ class StructuralLoadCasesData: @classmethod def applicable_structural_loads(cls): - props = bpy.context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() results = [] for load in tool.Ifc.get().by_type("IfcStructuralLoad"): if not load.Name or not load.is_a(props.applicable_structural_load_types): diff --git a/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py b/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py index a91a7947c5..f3e7f267cb 100644 --- a/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py +++ b/src/bonsai/bonsai/bim/module/structural/load_decoration_data.py @@ -24,6 +24,7 @@ from mathutils import Vector import ifcopenshell import ifcopenshell.api import ifcopenshell.util.attribute +import ifcopenshell.util.placement import ifcopenshell.util.unit as ifcunit import bonsai.tool as tool from bonsai.bim.module.structural.shader import DecorationShader @@ -304,7 +305,7 @@ class ShaderInfo: populate_members_dict("surface_members", element, activity, factor) recursive_subgroups(subgorups, rec_limit - 1, activity_type, factor=factor) - props = bpy.context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() group_definition_id = int(props.load_group_to_show) file = tool.Ifc.get() groups = [file.by_id(group_definition_id)] @@ -327,7 +328,7 @@ class ShaderInfo: maximum = max([abs(float(i)) for i in values]) if maximum == 0: continue - props = bpy.context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() reference_frame = props.reference_frame orientation = np.eye(3) if reference_frame == "LOCAL_COORDS": @@ -435,7 +436,7 @@ class ShaderInfo: ) -> np.ndarray: "provides the transformation matrix to convert between reference frames" global_or_local = activity.GlobalOrLocal - props = bpy.context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() reference_frame = props.reference_frame transform_matrix = np.eye(3) if reference_frame == "LOCAL_COORDS" and global_or_local != reference_frame: @@ -473,7 +474,7 @@ class ShaderInfo: "mz": (np.array((1, 0, 0)), np.array((0, 1, 0))), } keys = ["fx", "fy", "fz", "mx", "my", "mz"] - props = bpy.context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() reference_frame = props.reference_frame if reference_frame == "LOCAL_COORDS": for key in keys: @@ -591,7 +592,7 @@ class ShaderInfo: z_axis = x_axis.cross(y_axis).normalized() rotation = self.get_curve_member_rotation(member) - props = bpy.context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() reference_frame = props.reference_frame is_local = reference_frame == "LOCAL_COORDS" x_match = abs(Vector((1, 0, 0)).dot(x_axis)) > 0.99 diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index c483e9d236..92b88a78ec 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -20,6 +20,7 @@ import bpy import json import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.group import ifcopenshell.api.structural import ifcopenshell.util.attribute import bonsai.bim.helper @@ -77,7 +78,7 @@ class AddStructuralMemberConnection(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object assert obj oprops = tool.Blender.get_object_bim_props(obj) - props = obj.BIMStructuralProperties + props = tool.Structural.get_object_structural_props(obj) file = tool.Ifc.get() related_structural_connection = file.by_id(oprops.ifc_definition_id) relating_structural_member = tool.Ifc.get_entity(props.relating_structural_member) @@ -101,7 +102,8 @@ class EnableEditingStructuralConnectionCondition(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMStructuralProperties + assert obj + props = tool.Structural.get_object_structural_props(obj) props.active_connects_structural_member = self.connects_structural_member return {"FINISHED"} @@ -113,7 +115,8 @@ class DisableEditingStructuralConnectionCondition(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMStructuralProperties + assert obj + props = tool.Structural.get_object_structural_props(obj) props.active_connects_structural_member = 0 return {"FINISHED"} @@ -166,7 +169,8 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMStructuralProperties + assert obj + props = tool.Structural.get_object_structural_props(obj) props.boundary_condition_attributes.clear() condition = tool.Ifc.get().by_id(self.boundary_condition) @@ -206,7 +210,8 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): obj = context.active_object - props = obj.BIMStructuralProperties + assert obj + props = tool.Structural.get_object_structural_props(obj) file = tool.Ifc.get() connection = file.by_id(self.connection) @@ -236,7 +241,10 @@ class DisableEditingStructuralBoundaryCondition(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.active_object.BIMStructuralProperties.active_boundary_condition = 0 + obj = context.active_object + assert obj + props = tool.Structural.get_object_structural_props(obj) + props.active_boundary_condition = 0 return {"FINISHED"} @@ -353,7 +361,7 @@ class EnableEditingStructuralItemAxis(bpy.types.Operator): obj = context.active_object assert obj oprops = tool.Blender.get_object_bim_props(obj) - props = obj.BIMStructuralProperties + props = tool.Structural.get_object_structural_props(obj) self.file = tool.Ifc.get() item = self.file.by_id(oprops.ifc_definition_id) @@ -392,7 +400,8 @@ class DisableEditingStructuralItemAxis(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMStructuralProperties + assert obj + props = tool.Structural.get_object_structural_props(obj) props.is_editing_axis = False if props.axis_empty: bpy.data.objects.remove(props.axis_empty) @@ -407,7 +416,7 @@ class EditStructuralItemAxis(bpy.types.Operator, tool.Ifc.Operator): obj = context.active_object assert obj oprops = tool.Blender.get_object_bim_props(obj) - props = obj.BIMStructuralProperties + props = tool.Structural.get_object_structural_props(obj) relative_matrix = props.axis_empty.matrix_world @ obj.matrix_world.inverted() z_axis = relative_matrix.col[2][0:3] self.file = tool.Ifc.get() @@ -429,7 +438,7 @@ class EnableEditingStructuralConnectionCS(bpy.types.Operator): def execute(self, context): obj = context.active_object assert obj - props = obj.BIMStructuralProperties + props = tool.Structural.get_object_structural_props(obj) self.file = tool.Ifc.get() item = tool.Ifc.get_entity(obj) @@ -481,7 +490,8 @@ class DisableEditingStructuralConnectionCS(bpy.types.Operator): def execute(self, context): obj = context.active_object - props = obj.BIMStructuralProperties + assert obj + props = tool.Structural.get_object_structural_props(obj) props.is_editing_connection_cs = False if props.ccs_empty: bpy.data.objects.remove(props.ccs_empty) @@ -498,7 +508,7 @@ class EditStructuralConnectionCS(bpy.types.Operator, tool.Ifc.Operator): assert obj item = tool.Ifc.get_entity(obj) assert item - props = obj.BIMStructuralProperties + props = tool.Structural.get_object_structural_props(obj) relative_matrix = props.ccs_empty.matrix_world @ obj.matrix_world.inverted() x_axis = relative_matrix.col[0][0:3] z_axis = relative_matrix.col[2][0:3] @@ -567,13 +577,13 @@ class EditStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() attributes = bonsai.bim.helper.export_attributes(props.load_case_attributes) self.file = tool.Ifc.get() - ifcopenshell.api.run( - "structural.edit_structural_load_case", + ifcopenshell.api.structural.edit_structural_load_case( self.file, - **{"load_case": self.file.by_id(props.active_load_case_id), "attributes": attributes}, + load_case=self.file.by_id(props.active_load_case_id), + attributes=attributes, ) bpy.ops.bim.disable_editing_structural_load_case() return {"FINISHED"} @@ -587,9 +597,7 @@ class RemoveStructuralLoadCase(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.file = tool.Ifc.get() - ifcopenshell.api.run( - "structural.remove_structural_load_case", self.file, load_case=self.file.by_id(self.load_case) - ) + ifcopenshell.api.structural.remove_structural_load_case(self.file, load_case=self.file.by_id(self.load_case)) return {"FINISHED"} @@ -600,7 +608,7 @@ class EnableEditingStructuralLoadCase(bpy.types.Operator): load_case: bpy.props.IntProperty() def execute(self, context): - self.props = context.scene.BIMStructuralProperties + self.props = tool.Structural.get_structural_props() self.props.active_load_case_id = self.load_case self.props.load_case_editing_type = "ATTRIBUTES" self.props.load_case_attributes.clear() @@ -620,7 +628,8 @@ class DisableEditingStructuralLoadCase(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.BIMStructuralProperties.active_load_case_id = 0 + props = tool.Structural.get_structural_props() + props.active_load_case_id = 0 return {"FINISHED"} @@ -631,9 +640,9 @@ class EnableEditingStructuralLoadCaseGroups(bpy.types.Operator): load_case: bpy.props.IntProperty() def execute(self, context): - self.props = context.scene.BIMStructuralProperties - self.props.active_load_case_id = self.load_case - self.props.load_case_editing_type = "GROUPS" + props = tool.Structural.get_structural_props() + props.active_load_case_id = self.load_case + props.load_case_editing_type = "GROUPS" return {"FINISHED"} @@ -645,10 +654,8 @@ class AddStructuralLoadGroup(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.file = tool.Ifc.get() - load_group = ifcopenshell.api.run("structural.add_structural_load_group", self.file) - ifcopenshell.api.run( - "group.assign_group", self.file, products=[load_group], group=self.file.by_id(self.load_case) - ) + load_group = ifcopenshell.api.structural.add_structural_load_group(self.file) + ifcopenshell.api.group.assign_group(self.file, products=[load_group], group=self.file.by_id(self.load_case)) return {"FINISHED"} @@ -660,9 +667,7 @@ class RemoveStructuralLoadGroup(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): self.file = tool.Ifc.get() - ifcopenshell.api.run( - "structural.remove_structural_load_group", self.file, load_group=self.file.by_id(self.load_group) - ) + ifcopenshell.api.structural.remove_structural_load_group(self.file, load_group=self.file.by_id(self.load_group)) return {"FINISHED"} @@ -674,15 +679,15 @@ class EnableEditingStructuralLoadGroupActivities(bpy.types.Operator): def execute(self, context): self.file = tool.Ifc.get() - self.props = context.scene.BIMStructuralProperties + self.props = tool.Structural.get_structural_props() self.props.active_load_group_id = self.load_group self.props.load_group_editing_type = "ACTIVITY" self.load_structural_activities() return {"FINISHED"} - def load_structural_activities(self): + def load_structural_activities(self) -> None: self.props.load_group_activities.clear() - for rel in tool.Ifc.get().by_id(self.load_group).IsGroupedBy: + for rel in self.file.by_id(self.load_group).IsGroupedBy: for activity in rel.RelatedObjects: new = self.props.load_group_activities.add() new.ifc_definition_id = activity.id() @@ -697,7 +702,7 @@ class AddStructuralActivity(bpy.types.Operator, tool.Ifc.Operator): load_group: bpy.props.IntProperty() def _execute(self, context): - self.props = context.scene.BIMStructuralProperties + self.props = tool.Structural.get_structural_props() self.file = tool.Ifc.get() for obj in context.selected_objects: element = tool.Ifc.get_entity(obj) @@ -726,16 +731,13 @@ class AddStructuralActivity(bpy.types.Operator, tool.Ifc.Operator): ifc_class = applicable_activity_class[element.is_a()] - activity = ifcopenshell.api.run( - "structural.add_structural_activity", + activity = ifcopenshell.api.structural.add_structural_activity( self.file, ifc_class=ifc_class, applied_load=self.file.by_id(int(self.props.applicable_structural_loads)), structural_member=element, ) - ifcopenshell.api.run( - "group.assign_group", self.file, products=[activity], group=self.file.by_id(self.load_group) - ) + ifcopenshell.api.group.assign_group(self.file, products=[activity], group=self.file.by_id(self.load_group)) bpy.ops.bim.enable_editing_structural_load_group_activities(load_group=self.load_group) return {"FINISHED"} @@ -747,7 +749,7 @@ class LoadStructuralLoads(bpy.types.Operator): def execute(self, context): self.file = tool.Ifc.get() - props = context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() props.structural_loads.clear() loads = tool.Ifc.get().by_type("IfcStructuralLoad") if props.filtered_structural_loads: @@ -780,7 +782,8 @@ class DisableStructuralLoadEditingUI(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.BIMStructuralProperties.is_editing_loads = False + props = tool.Structural.get_structural_props() + props.is_editing_loads = False return {"FINISHED"} @@ -806,7 +809,7 @@ class EnableEditingStructuralLoad(bpy.types.Operator): structural_load: bpy.props.IntProperty() def execute(self, context): - props = context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() props.structural_load_attributes.clear() bonsai.bim.helper.import_attributes2( tool.Ifc.get().by_id(self.structural_load), props.structural_load_attributes @@ -821,7 +824,8 @@ class DisableEditingStructuralLoad(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.BIMStructuralProperties.active_structural_load_id = 0 + props = tool.Structural.get_structural_props() + props.active_structural_load_id = 0 return {"FINISHED"} @@ -832,12 +836,10 @@ class RemoveStructuralLoad(bpy.types.Operator, tool.Ifc.Operator): structural_load: bpy.props.IntProperty() def _execute(self, context): - props = context.scene.BIMStructuralProperties self.file = tool.Ifc.get() - ifcopenshell.api.run( - "structural.remove_structural_load", + ifcopenshell.api.structural.remove_structural_load( self.file, - **{"structural_load": self.file.by_id(self.structural_load)}, + structural_load=self.file.by_id(self.structural_load), ) bpy.ops.bim.load_structural_loads() return {"FINISHED"} @@ -849,16 +851,13 @@ class EditStructuralLoad(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() attributes = bonsai.bim.helper.export_attributes(props.structural_load_attributes) self.file = tool.Ifc.get() - ifcopenshell.api.run( - "structural.edit_structural_load", + ifcopenshell.api.structural.edit_structural_load( self.file, - **{ - "structural_load": self.file.by_id(props.active_structural_load_id), - "attributes": attributes, - }, + structural_load=self.file.by_id(props.active_structural_load_id), + attributes=attributes, ) bpy.ops.bim.load_structural_loads() return {"FINISHED"} @@ -870,7 +869,7 @@ class ToggleFilterStructuralLoads(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() props.filtered_structural_loads = not props.filtered_structural_loads bpy.ops.bim.load_structural_loads() return {"FINISHED"} @@ -883,7 +882,7 @@ class LoadBoundaryConditions(bpy.types.Operator): def execute(self, context): self.file = tool.Ifc.get() - props = context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() props.boundary_conditions.clear() conditions = tool.Ifc.get().by_type("IfcBoundaryCondition") if props.filtered_boundary_conditions: @@ -916,7 +915,7 @@ class ToggleFilterBoundaryConditions(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - props = context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() props.filtered_boundary_conditions = not props.filtered_boundary_conditions bpy.ops.bim.load_boundary_conditions() return {"FINISHED"} @@ -928,7 +927,8 @@ class DisableBoundaryConditionEditingUI(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.BIMStructuralProperties.is_editing_boundary_conditions = False + props = tool.Structural.get_structural_props() + props.is_editing_boundary_conditions = False return {"FINISHED"} @@ -939,8 +939,7 @@ class AddBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): ifc_class: bpy.props.StringProperty() def _execute(self, context): - result = ifcopenshell.api.run( - "structural.add_structural_boundary_condition", + result = ifcopenshell.api.structural.add_structural_boundary_condition( tool.Ifc.get(), name="New Load", ifc_class=self.ifc_class, @@ -957,7 +956,7 @@ class EnableEditingBoundaryCondition(bpy.types.Operator): boundary_condition: bpy.props.IntProperty() def execute(self, context): - props = context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() props.boundary_condition_attributes.clear() boundary_condition = tool.Ifc.get().by_id(self.boundary_condition) @@ -994,7 +993,8 @@ class DisableEditingBoundaryCondition(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.BIMStructuralProperties.active_boundary_condition_id = 0 + props = tool.Structural.get_structural_props() + props.active_boundary_condition_id = 0 return {"FINISHED"} @@ -1005,12 +1005,10 @@ class RemoveBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): boundary_condition: bpy.props.IntProperty() def _execute(self, context): - props = context.scene.BIMStructuralProperties self.file = tool.Ifc.get() - ifcopenshell.api.run( - "structural.remove_structural_boundary_condition", + ifcopenshell.api.structural.remove_structural_boundary_condition( self.file, - **{"boundary_condition": self.file.by_id(self.boundary_condition)}, + boundary_condition=self.file.by_id(self.boundary_condition), ) bpy.ops.bim.load_boundary_conditions() return {"FINISHED"} @@ -1022,7 +1020,7 @@ class EditBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} def _execute(self, context): - props = context.scene.BIMStructuralProperties + props = tool.Structural.get_structural_props() self.file = tool.Ifc.get() # attributes = bonsai.bim.helper.export_attributes(props.boundary_condition_attributes) attributes = {} @@ -1035,10 +1033,10 @@ class EditBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator): attributes[attribute.name] = {"value": attribute.bool_value, "type": attribute.enum_value} else: attributes[attribute.name] = {"value": attribute.float_value, "type": attribute.enum_value} - ifcopenshell.api.run( - "structural.edit_structural_boundary_condition", + ifcopenshell.api.structural.edit_structural_boundary_condition( self.file, - **{"condition": self.file.by_id(props.active_boundary_condition_id), "attributes": attributes}, + condition=self.file.by_id(props.active_boundary_condition_id), + attributes=attributes, ) bpy.ops.bim.load_boundary_conditions() return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/structural/prop.py b/src/bonsai/bonsai/bim/module/structural/prop.py index 2ecc29834a..c0db44fac6 100644 --- a/src/bonsai/bonsai/bim/module/structural/prop.py +++ b/src/bonsai/bonsai/bim/module/structural/prop.py @@ -37,52 +37,62 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Union -def get_load_groups_to_show(self, context): +def get_load_groups_to_show(self: "BIMStructuralProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: if not LoadGroupDecorationData.is_loaded: LoadGroupDecorationData.load() return LoadGroupDecorationData.data["load_groups_to_show"] -def update_activity_type(self, context): +def update_activity_type(self: "BIMStructuralProperties", context: bpy.types.Context) -> None: LoadGroupDecorationData.is_loaded = False -def get_applicable_structural_load_types(self, context): +def get_applicable_structural_load_types( + self: "BIMStructuralProperties", context: bpy.types.Context +) -> list[tuple[str, str, str]]: if not StructuralLoadCasesData.is_loaded: StructuralLoadCasesData.load() return StructuralLoadCasesData.data["applicable_structural_load_types"] -def updateApplicableStructuralLoadTypes(self, context): +def updateApplicableStructuralLoadTypes(self: "BIMStructuralProperties", context: bpy.types.Context) -> None: StructuralLoadCasesData.data["applicable_structural_load_types"] = ( StructuralLoadCasesData.applicable_structural_load_types() ) -def get_applicable_structural_loads(self, context): +def get_applicable_structural_loads( + self: "BIMStructuralProperties", context: bpy.types.Context +) -> list[tuple[str, str, str]]: if not StructuralLoadCasesData.is_loaded: StructuralLoadCasesData.load() return StructuralLoadCasesData.data["applicable_structural_loads"] -def get_structural_load_types(self, context): +def get_structural_load_types( + self: "BIMStructuralProperties", context: bpy.types.Context +) -> list[tuple[str, str, str]]: if not StructuralLoadsData.is_loaded: StructuralLoadsData.load() return StructuralLoadsData.data["structural_load_types"] -def get_boundary_condition_types(self, context): +def get_boundary_condition_types( + self: "BIMStructuralProperties", context: bpy.types.Context +) -> list[tuple[str, str, str]]: if not BoundaryConditionsData.is_loaded: BoundaryConditionsData.load() return BoundaryConditionsData.data["boundary_condition_types"] -def updateAxisAngle(self, context): +def updateAxisAngle(self: "BIMObjectStructuralProperties", context: bpy.types.Context) -> None: if not self.axis_empty: return obj = context.active_object + assert obj and isinstance(obj.data, bpy.types.Mesh) empty = self.axis_empty x_axis = obj.data.vertices[1].co - obj.data.vertices[0].co empty.location = obj.data.vertices[0].co @@ -92,10 +102,11 @@ def updateAxisAngle(self, context): empty.rotation_euler[0] = radians(self.axis_angle) -def updateConnectionCS(self, context): +def updateConnectionCS(self: "BIMObjectStructuralProperties", context: bpy.types.Context) -> None: if not self.ccs_empty: return obj = context.active_object + assert obj and isinstance(obj.data, bpy.types.Mesh) empty = self.ccs_empty empty.location = obj.data.vertices[0].co empty.rotation_mode = "XYZ" @@ -108,24 +119,39 @@ class StructuralAnalysisModel(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + ifc_definition_id: int + class StructuralActivity(PropertyGroup): name: StringProperty(name="Name") applied_load_class: StringProperty(name="Applied Load Class") ifc_definition_id: IntProperty(name="IFC Definition ID") + if TYPE_CHECKING: + applied_load_class: str + ifc_definition_id: int + class StructuralLoad(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") number_of_inverse_references: IntProperty(name="Number of Inverse References") + if TYPE_CHECKING: + ifc_definition_id: int + number_of_inverse_references: int + class BoundaryCondition(PropertyGroup): name: StringProperty(name="Name") ifc_definition_id: IntProperty(name="IFC Definition ID") number_of_inverse_references: IntProperty(name="Number of Inverse References") + if TYPE_CHECKING: + ifc_definition_id: int + number_of_inverse_references: int + class BIMStructuralProperties(PropertyGroup): structural_analysis_model_attributes: CollectionProperty( @@ -187,6 +213,45 @@ class BIMStructuralProperties(PropertyGroup): ) load_group_to_show: EnumProperty(items=get_load_groups_to_show, name="Load Groups") + if TYPE_CHECKING: + structural_analysis_model_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + is_editing: bool + structural_analysis_models: bpy.types.bpy_prop_collection_idprop[StructuralAnalysisModel] + active_structural_analysis_model_index: int + active_structural_analysis_model_id: int + load_case_editing_type: str + load_case_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + active_load_case_id: int + load_group_editing_type: str + active_load_group_id: int + applicable_structural_load_types: str + applicable_structural_loads: str + load_group_activities: bpy.types.bpy_prop_collection_idprop[StructuralActivity] + active_load_group_activity_index: int + + structural_loads: bpy.types.bpy_prop_collection_idprop[StructuralLoad] + active_structural_load_index: int + active_structural_load_id: int + is_editing_loads: bool + structural_load_types: str + structural_load_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + filtered_structural_loads: bool + + boundary_conditions: bpy.types.bpy_prop_collection_idprop[BoundaryCondition] + active_boundary_condition_index: int + active_boundary_condition_id: int + is_editing_boundary_conditions: bool + boundary_condition_types: str + boundary_condition_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + filtered_boundary_conditions: bool + + show_loads: bool + update_load_repr: bool + enable_repr_auto_update: bool + reference_frame: str + activity_type: str + load_group_to_show: str + class BIMObjectStructuralProperties(PropertyGroup): boundary_condition_attributes: CollectionProperty(name="Boundary Condition Attributes", type=Attribute) @@ -203,3 +268,19 @@ class BIMObjectStructuralProperties(PropertyGroup): ccs_y_angle: FloatProperty(name="Connection CS Y Angle", update=updateConnectionCS) ccs_z_angle: FloatProperty(name="Connection CS Z Angle", update=updateConnectionCS) ccs_empty: PointerProperty(name="CCS Empty", type=bpy.types.Object) + + if TYPE_CHECKING: + boundary_condition_attributes: bpy.types.bpy_prop_collection_idprop[Attribute] + active_boundary_condition: int + active_connects_structural_member: int + relating_structural_member: Union[bpy.types.Object, None] + is_editing_axis: bool + axis_angle: float + axis_empty: Union[bpy.types.Object, None] + + # relating_structural_activity: Union[bpy.types.Object, None] + is_editing_connection_cs: bool + ccs_x_angle: float + ccs_y_angle: float + ccs_z_angle: float + ccs_empty: Union[bpy.types.Object, None] diff --git a/src/bonsai/bonsai/bim/module/structural/ui.py b/src/bonsai/bonsai/bim/module/structural/ui.py index 1e4ceee997..ea359e4ca2 100644 --- a/src/bonsai/bonsai/bim/module/structural/ui.py +++ b/src/bonsai/bonsai/bim/module/structural/ui.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import bonsai.tool as tool import bonsai.bim.helper @@ -31,9 +32,25 @@ from bonsai.bim.module.structural.data import ( StructuralConnectionData, BoundaryConditionsData, ) +from typing import TYPE_CHECKING, Any, Union + +if TYPE_CHECKING: + from bonsai.bim.module.structural.prop import ( + BIMStructuralProperties, + BIMObjectStructuralProperties, + BoundaryCondition, + StructuralLoad, + StructuralActivity, + StructuralAnalysisModel, + ) -def draw_boundary_condition_ui(layout, boundary_condition, connection_id, props): +def draw_boundary_condition_ui( + layout: bpy.types.UILayout, + boundary_condition: dict[str, Any], + connection_id: int, + props: BIMObjectStructuralProperties, +) -> None: row = layout.row(align=True) if not boundary_condition: row.label(text="No Boundary Condition Found", icon="CON_TRACKTO") @@ -59,11 +76,13 @@ def draw_boundary_condition_ui(layout, boundary_condition, connection_id, props) draw_boundary_condition_read_only_ui(layout, boundary_condition) -def draw_boundary_condition_editable_ui(layout: bpy.types.UILayout, props: bpy.types.PropertyGroup) -> None: +def draw_boundary_condition_editable_ui( + layout: bpy.types.UILayout, props: Union[BIMStructuralProperties, BIMObjectStructuralProperties] +) -> None: draw_attributes(props.boundary_condition_attributes, layout) -def draw_boundary_condition_read_only_ui(layout, boundary_condition): +def draw_boundary_condition_read_only_ui(layout: bpy.types.UILayout, boundary_condition: dict[str, Any]) -> None: for attribute in boundary_condition["attributes"]: row = layout.row(align=True) row.label(text=attribute["name"]) @@ -100,11 +119,15 @@ class BIM_PT_structural_boundary_conditions(Panel): if not StructuralBoundaryConditionsData.is_loaded: StructuralBoundaryConditionsData.load() + obj = context.active_object + assert obj + self.props = tool.Structural.get_object_structural_props(obj) + draw_boundary_condition_ui( self.layout, StructuralBoundaryConditionsData.data["boundary_condition"], StructuralBoundaryConditionsData.data["connection_id"], - context.active_object.BIMStructuralProperties, + self.props, ) @@ -135,7 +158,9 @@ class BIM_PT_connected_structural_members(Panel): if not ConnectedStructuralMembersData.is_loaded: ConnectedStructuralMembersData.load() - self.props = context.active_object.BIMStructuralProperties + obj = context.active_object + assert obj + self.props = tool.Structural.get_object_structural_props(obj) row = self.layout.row(align=True) row.prop(self.props, "relating_structural_member", text="", icon="CON_TRACKTO") @@ -188,7 +213,9 @@ class BIM_PT_structural_member(Panel): if not StructuralMemberData.is_loaded: StructuralMemberData.load() - self.props = context.active_object.BIMStructuralProperties + obj = context.active_object + assert obj + self.props = tool.Structural.get_object_structural_props(obj) if StructuralMemberData.data["active_object_class"] == "IfcStructuralCurveMember": if self.props.is_editing_axis: @@ -231,7 +258,9 @@ class BIM_PT_structural_connection(Panel): if not StructuralConnectionData.is_loaded: StructuralConnectionData.load() - self.props = context.active_object.BIMStructuralProperties + obj = context.active_object + assert obj + self.props = tool.Structural.get_object_structural_props(obj) if StructuralConnectionData.data["active_object_class"] == "IfcStructuralCurveConnection": if self.props.is_editing_axis: @@ -281,7 +310,7 @@ class BIM_PT_structural_analysis_models(Panel): if not StructuralAnalysisModelsData.is_loaded: StructuralAnalysisModelsData.load() - self.props = context.scene.BIMStructuralProperties + self.props = tool.Structural.get_structural_props() row = self.layout.row(align=True) row.label( @@ -309,7 +338,16 @@ class BIM_PT_structural_analysis_models(Panel): class BIM_UL_structural_analysis_models(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context: bpy.types.Context, + layout: bpy.types.UILayout, + data: BIMStructuralProperties, + item: StructuralAnalysisModel, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) row.label(text=item.name) @@ -324,10 +362,10 @@ class BIM_UL_structural_analysis_models(UIList): op = row.operator("bim.assign_structural_analysis_model", text="", icon="KEYFRAME", emboss=False) op.structural_analysis_model = item.ifc_definition_id - if context.scene.BIMStructuralProperties.active_structural_analysis_model_id == item.ifc_definition_id: + if data.active_structural_analysis_model_id == item.ifc_definition_id: row.operator("bim.edit_structural_analysis_model", text="", icon="CHECKMARK") row.operator("bim.disable_editing_structural_analysis_model", text="", icon="CANCEL") - elif context.scene.BIMStructuralProperties.active_structural_analysis_model_id: + elif data.active_structural_analysis_model_id: op = row.operator("bim.remove_structural_analysis_model", text="", icon="X") op.structural_analysis_model = item.ifc_definition_id else: @@ -354,7 +392,7 @@ class BIM_PT_structural_load_cases(Panel): if not StructuralLoadCasesData.is_loaded: StructuralLoadCasesData.load() - self.props = context.scene.BIMStructuralProperties + self.props = tool.Structural.get_structural_props() row = self.layout.row() row.operator("bim.add_structural_load_case", icon="ADD") @@ -425,7 +463,16 @@ class BIM_PT_structural_load_cases(Panel): class BIM_UL_structural_activities(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMStructuralProperties, + item: StructuralActivity, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) row.label(text=item.name) @@ -446,8 +493,7 @@ class BIM_PT_show_structural_activities(Panel): return file and hasattr(file, "schema") and file.schema != "IFC2X3" def draw(self, context): - - self.props = context.scene.BIMStructuralProperties + self.props = tool.Structural.get_structural_props() row = self.layout.row(align=True) row.operator( @@ -480,7 +526,7 @@ class BIM_PT_structural_loads(Panel): if not StructuralLoadsData.is_loaded: StructuralLoadsData.load() - self.props = context.scene.BIMStructuralProperties + self.props = tool.Structural.get_structural_props() row = self.layout.row(align=True) row.label(text=f"{StructuralLoadsData.data['total_loads']} Structural Loads Found", icon="ANIM_DATA") @@ -513,16 +559,25 @@ class BIM_PT_structural_loads(Panel): class BIM_UL_structural_loads(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMStructuralProperties, + item: StructuralLoad, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) row.label(text=f"{item.name} ({item.number_of_inverse_references})") row.label(text=StructuralLoadsData.data["load_classes"][item.ifc_definition_id]) - if context.scene.BIMStructuralProperties.active_structural_load_id == item.ifc_definition_id: + if data.active_structural_load_id == item.ifc_definition_id: row.operator("bim.edit_structural_load", text="", icon="CHECKMARK") row.operator("bim.disable_editing_structural_load", text="", icon="CANCEL") - elif context.scene.BIMStructuralProperties.active_structural_load_id: + elif data.active_structural_load_id: op = row.operator("bim.remove_structural_load", text="", icon="X") op.structural_load = item.ifc_definition_id else: @@ -549,7 +604,7 @@ class BIM_PT_boundary_conditions(Panel): if not BoundaryConditionsData.is_loaded: BoundaryConditionsData.load() - self.props = context.scene.BIMStructuralProperties + self.props = tool.Structural.get_structural_props() row = self.layout.row(align=True) row.label( @@ -587,16 +642,25 @@ class BIM_PT_boundary_conditions(Panel): class BIM_UL_boundary_conditions(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_item( + self, + context, + layout: bpy.types.UILayout, + data: BIMStructuralProperties, + item: BoundaryCondition, + icon, + active_data, + active_propname, + ): if item: row = layout.row(align=True) row.label(text=f"{item.name} ({item.number_of_inverse_references})") row.label(text=BoundaryConditionsData.data["condition_classes"][item.ifc_definition_id]) - if context.scene.BIMStructuralProperties.active_boundary_condition_id == item.ifc_definition_id: + if data.active_boundary_condition_id == item.ifc_definition_id: row.operator("bim.edit_boundary_condition", text="", icon="CHECKMARK") row.operator("bim.disable_editing_boundary_condition", text="", icon="CANCEL") - elif context.scene.BIMStructuralProperties.active_boundary_condition_id: + elif data.active_boundary_condition_id: op = row.operator("bim.remove_boundary_condition", text="", icon="X") op.boundary_condition = item.ifc_definition_id else: diff --git a/src/bonsai/bonsai/bim/module/structural/workspace.py b/src/bonsai/bonsai/bim/module/structural/workspace.py index bb02867af1..fbacac3e1a 100644 --- a/src/bonsai/bonsai/bim/module/structural/workspace.py +++ b/src/bonsai/bonsai/bim/module/structural/workspace.py @@ -54,7 +54,7 @@ class StructuralToolUI: @classmethod def draw(cls, context, layout): cls.layout = layout - # cls.props = context.scene.BIMStructuralProperties + cls.props = tool.Structural.get_structural_props() row = cls.layout.row(align=True) if not tool.Ifc.get(): @@ -99,12 +99,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return operator.description or "" def _execute(self, context): - # self.props = context.scene.BIMStructuralProperties + # self.props = tool.Structural.get_structural_props() getattr(self, f"hotkey_{self.hotkey}")() def invoke(self, context, event): # https://blender.stackexchange.com/questions/276035/how-do-i-make-operators-remember-their-property-values-when-called-from-a-hotkey - # self.props = context.scene.BIMStructuralProperties + # self.props = tool.Structural.get_structural_props() return self.execute(context) def draw(self, context): diff --git a/src/bonsai/bonsai/tool/qto.py b/src/bonsai/bonsai/tool/qto.py index 803a00e560..4a17be291b 100644 --- a/src/bonsai/bonsai/tool/qto.py +++ b/src/bonsai/bonsai/tool/qto.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import bonsai.core.tool import bonsai.bim.schema @@ -24,12 +25,19 @@ import ifcopenshell import ifcopenshell.util.unit import ifcopenshell.util.element from mathutils import Vector -from typing import Optional, Union, Literal +from typing import Optional, Union, Literal, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.qto.prop import BIMQtoProperties QuantityTypes = Literal["Q_LENGTH", "Q_AREA", "Q_VOLUME"] class Qto(bonsai.core.tool.Qto): + @classmethod + def get_qto_props(cls) -> BIMQtoProperties: + return bpy.context.scene.BIMQtoProperties + @classmethod def get_radius_of_selected_vertices(cls, obj: bpy.types.Object) -> float: selected_verts = [v.co for v in obj.data.vertices if v.select] @@ -41,7 +49,8 @@ class Qto(bonsai.core.tool.Qto): @classmethod def set_qto_result(cls, result: float) -> None: - bpy.context.scene.BIMQtoProperties.qto_result = str(round(result, 3)) + props = cls.get_qto_props() + props.qto_result = str(round(result, 3)) @classmethod def get_rounded_value(cls, new_quantity: float) -> float: diff --git a/src/bonsai/bonsai/tool/structural.py b/src/bonsai/bonsai/tool/structural.py index 5796bf6f44..54c73ca537 100644 --- a/src/bonsai/bonsai/tool/structural.py +++ b/src/bonsai/bonsai/tool/structural.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import bpy import ifcopenshell import ifcopenshell.util.representation @@ -23,30 +24,46 @@ import json import bonsai.bim.helper import bonsai.core.tool import bonsai.tool as tool -from typing import Union, Any +from typing import Union, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from bonsai.bim.module.structural.prop import BIMStructuralProperties, BIMObjectStructuralProperties class Structural(bonsai.core.tool.Structural): + @classmethod + def get_structural_props(cls) -> BIMStructuralProperties: + return bpy.context.scene.BIMStructuralProperties + + @classmethod + def get_object_structural_props(cls, obj: bpy.types.Object) -> BIMObjectStructuralProperties: + return obj.BIMStructuralProperties + @classmethod def disable_editing_structural_analysis_model(cls) -> None: - bpy.context.scene.BIMStructuralProperties.active_structural_analysis_model_id = 0 + props = cls.get_structural_props() + props.active_structural_analysis_model_id = 0 @classmethod def disable_structural_analysis_model_editing_ui(cls) -> None: - bpy.context.scene.BIMStructuralProperties.is_editing = False + props = cls.get_structural_props() + props.is_editing = False @classmethod def enable_editing_structural_analysis_model(cls, model: Union[int, None]) -> None: if model: - bpy.context.scene.BIMStructuralProperties.active_structural_analysis_model_id = model + props = cls.get_structural_props() + props.active_structural_analysis_model_id = model @classmethod def enable_structural_analysis_model_editing_ui(cls) -> None: - bpy.context.scene.BIMStructuralProperties.is_editing = True + props = cls.get_structural_props() + props.is_editing = True @classmethod def enabled_structural_analysis_model_editing_ui(cls) -> bool: - return bpy.context.scene.BIMStructuralProperties.is_editing + props = cls.get_structural_props() + return props.is_editing @classmethod def ensure_representation_contexts(cls) -> None: @@ -71,7 +88,7 @@ class Structural(bonsai.core.tool.Structural): @classmethod def get_active_structural_analysis_model(cls) -> ifcopenshell.entity_instance: - props = bpy.context.scene.BIMStructuralProperties + props = cls.get_structural_props() model = tool.Ifc.get().by_id(props.active_structural_analysis_model_id) return model @@ -120,13 +137,13 @@ class Structural(bonsai.core.tool.Structural): @classmethod def get_structural_analysis_model_attributes(cls) -> dict[str, Any]: - props = bpy.context.scene.BIMStructuralProperties + props = cls.get_structural_props() attributes = bonsai.bim.helper.export_attributes(props.structural_analysis_model_attributes) return attributes @classmethod def load_structural_analysis_model_attributes(cls, data: dict[str, Any]) -> None: - props = bpy.context.scene.BIMStructuralProperties + props = cls.get_structural_props() props.structural_analysis_model_attributes.clear() schema = tool.Ifc.schema() for attribute in schema.declaration_by_name("IfcStructuralAnalysisModel").all_attributes(): @@ -149,7 +166,7 @@ class Structural(bonsai.core.tool.Structural): @classmethod def load_structural_analysis_models(cls) -> None: models = tool.Structural.get_ifc_structural_analysis_models() - props = bpy.context.scene.BIMStructuralProperties + props = cls.get_structural_props() props.structural_analysis_models.clear() for ifc_definition_id, model in models.items(): new = props.structural_analysis_models.add() diff --git a/src/bonsai/test/tool/test_qto.py b/src/bonsai/test/tool/test_qto.py index b9e3926f65..7ab26cd9d8 100644 --- a/src/bonsai/test/tool/test_qto.py +++ b/src/bonsai/test/tool/test_qto.py @@ -42,7 +42,8 @@ class TestGetRadiusOfSelectedVertices(test.bim.bootstrap.NewFile): class TestSetQtoResult(test.bim.bootstrap.NewFile): def test_run(self): subject.set_qto_result(123.4567) - assert bpy.context.scene.BIMQtoProperties.qto_result == "123.457" + props = tool.Qto.get_qto_props() + assert props.qto_result == "123.457" class TestGetRoundedValue(test.bim.bootstrap.NewFile): diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 3766be8d0d..8827bef51e 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -30,11 +30,16 @@ import ifcopenshell.util.shape import ifcopenshell.util.representation import ifcopenshell.util.type import multiprocessing -from collections import namedtuple, defaultdict -from typing import Any, Literal, get_args, Union, Iterable +from collections import defaultdict +from typing import Any, Literal, get_args, Union, Iterable, NamedTuple + + +class Function(NamedTuple): + measure: str + name: str + description: str -Function = namedtuple("Function", ["measure", "name", "description"]) RULE_SET = Literal[ "IFC4QtoBaseQuantities", "IFC4QtoBaseQuantitiesBlender", @@ -161,7 +166,23 @@ class IteratorForTypes: return True -class IfcOpenShell: +class QtoCalculator: + """Abstract class for Qto calculators.""" + + functions: dict[str, Function] + + @classmethod + def calculate( + cls, + ifc_file: ifcopenshell.file, + elements: set[ifcopenshell.entity_instance], + qtos: dict[str, dict[str, Union[str, None]]], + results: ResultsDict, + ) -> None: + raise NotImplementedError + + +class IfcOpenShell(QtoCalculator): """Calculates Model body context geometry using the default IfcOpenShell iterator on triangulation elements.""" @@ -221,13 +242,7 @@ class IfcOpenShell: functions[f"net_{k}"] = Function(v.measure, f"Net {v.name}", v.description) @classmethod - def calculate( - cls, - ifc_file: ifcopenshell.file, - elements: set[ifcopenshell.entity_instance], - qtos: dict[str, dict[str, Union[str, None]]], - results: ResultsDict, - ) -> None: + def calculate(cls, ifc_file, elements, qtos, results): formula_functions: dict[str, types.FunctionType] = {} cls.gross_settings = ifcopenshell.geom.settings() @@ -327,9 +342,10 @@ class IfcOpenShell: return max([x, y, z]) -class Blender: +class Blender(QtoCalculator): """Calculates geometry based on currently loaded Blender objects.""" + # Implementations are located in bonsai.bim.module.qto.calculator. functions = { # IfcLengthMeasure "get_covering_width": Function("IfcLengthMeasure", "Covering Width", ""), @@ -372,13 +388,8 @@ class Blender: "get_net_weight": Function("IfcMassMeasure", "Net Weight", ""), } - @staticmethod - def calculate( - ifc_file: ifcopenshell.file, - elements: set[ifcopenshell.entity_instance], - qtos: dict[str, dict[str, Union[str, None]]], - results: ResultsDict, - ) -> None: + @classmethod + def calculate(cls, ifc_file, elements, qtos, results): import bonsai.tool as tool import bonsai.bim.module.qto.calculator as calculator @@ -406,4 +417,7 @@ class Blender: results[element] = element_results -calculators = {"Blender": Blender, "IfcOpenShell": IfcOpenShell} +calculators: dict[str, type[QtoCalculator]] = { + "Blender": Blender, + "IfcOpenShell": IfcOpenShell, +} diff --git a/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py b/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py index dac156daea..b03f003594 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py +++ b/src/ifcopenshell-python/ifcopenshell/api/aggregate/assign_object.py @@ -64,13 +64,10 @@ def assign_object( :param products: The list of parts of the aggregate, typically of IfcElement or IfcSpatialStructureElement subclass - :type product: list[ifcopenshell.entity_instance] :param relating_object: The whole of the aggregate, typically an IfcElement or IfcSpatialStructureElement subclass - :type relating_object: ifcopenshell.entity_instance :return: The IfcRelAggregate relationship instance or `None` if `products` was empty list. - :rtype: Union[ifcopenshell.entity_instance, None] Example: @@ -86,16 +83,10 @@ def assign_object( # The site has a building ifcopenshell.api.aggregate.assign_object(model, products=[subelement], relating_object=element) """ - settings = { - "products": products, - "relating_object": relating_object, - } - - if not settings["products"]: + if not products: return - products = set(settings["products"]) - relating_object = settings["relating_object"] + products_set = set(products) is_decomposed_by = next((i for i in relating_object.IsDecomposedBy if i.is_a("IfcRelAggregates")), None) previous_aggregates_rels: set[ifcopenshell.entity_instance] = set() @@ -103,7 +94,7 @@ def assign_object( products_with_aggregates: list[ifcopenshell.entity_instance] = [] # check if there is anything to change - for product in products: + for product in products_set: product_rel = next(iter(product.Decomposes), None) if product_rel is None: @@ -129,7 +120,7 @@ def assign_object( # unassign elements from previous aggregates for decomposes in previous_aggregates_rels: - related_objects = set(decomposes.RelatedObjects) - products + related_objects = set(decomposes.RelatedObjects) - products_set if related_objects: decomposes.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(file, **{"element": decomposes}) @@ -141,7 +132,7 @@ def assign_object( # assign elements to a new aggregate if is_decomposed_by: - is_decomposed_by.RelatedObjects = list(set(is_decomposed_by.RelatedObjects) | products) + is_decomposed_by.RelatedObjects = list(set(is_decomposed_by.RelatedObjects) | products_set) ifcopenshell.api.owner.update_owner_history(file, **{"element": is_decomposed_by}) else: is_decomposed_by = file.create_entity( @@ -149,7 +140,7 @@ def assign_object( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": list(products), + "RelatedObjects": list(products_set), "RelatingObject": relating_object, } ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/boundary/copy_boundary.py b/src/ifcopenshell-python/ifcopenshell/api/boundary/copy_boundary.py index af2bc72cb2..a2e74b5a11 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/boundary/copy_boundary.py +++ b/src/ifcopenshell-python/ifcopenshell/api/boundary/copy_boundary.py @@ -23,9 +23,7 @@ def copy_boundary(file: ifcopenshell.file, boundary: ifcopenshell.entity_instanc """Copies a space boundary :param boundary: The IfcRelSpaceBoundary you want to copy. - :type boundary: ifcopenshell.entity_instance :return: Duplicate of the IfcRelSpaceBoundary - :rtype: ifcopenshell.entity_instance Example: @@ -36,9 +34,7 @@ def copy_boundary(file: ifcopenshell.file, boundary: ifcopenshell.entity_instanc # And now we have two boundary_copy = ifcopenshell.api.boundary.copy_boundary(model, boundary=boundary) """ - settings = {"boundary": boundary} - - result = ifcopenshell.util.element.copy(file, settings["boundary"]) + result = ifcopenshell.util.element.copy(file, boundary) if result.ConnectionGeometry: result.ConnectionGeometry = ifcopenshell.util.element.copy_deep(file, result.ConnectionGeometry) return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/boundary/remove_boundary.py b/src/ifcopenshell-python/ifcopenshell/api/boundary/remove_boundary.py index 13dfb7ab6b..4aadabcbce 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/boundary/remove_boundary.py +++ b/src/ifcopenshell-python/ifcopenshell/api/boundary/remove_boundary.py @@ -27,9 +27,7 @@ def remove_boundary(file: ifcopenshell.file, boundary: ifcopenshell.entity_insta boundary and its connection geometry is removed. :param boundary: The IfcRelSpaceBoundary you want to remove. - :type boundary: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -40,13 +38,11 @@ def remove_boundary(file: ifcopenshell.file, boundary: ifcopenshell.entity_insta # Let's remove it! ifcopenshell.api.boundary.remove_boundary(model, boundary=boundary) """ - settings = {"boundary": boundary} - - geometry = settings["boundary"].ConnectionGeometry + geometry = boundary.ConnectionGeometry if geometry: - settings["boundary"].ConnectionGeometry = None + boundary.ConnectionGeometry = None ifcopenshell.util.element.remove_deep2(file, geometry) - history = settings["boundary"].OwnerHistory - file.remove(settings["boundary"]) + history = boundary.OwnerHistory + file.remove(boundary) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py index f1fcf20a0e..001c86a540 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_metric.py @@ -28,9 +28,7 @@ def add_metric(file: ifcopenshell.file, objective: ifcopenshell.entity_instance) to meet the objective of the constraint. :param objective: The IfcObjective that this metric is a benchmark of. - :type objective: ifcopenshell.entity_instance :return: The newly created IfcMetric entity - :rtype: ifcopenshell.entity_instance Example: @@ -40,10 +38,6 @@ def add_metric(file: ifcopenshell.file, objective: ifcopenshell.entity_instance) metric = ifcopenshell.api.constraint.add_metric(model, objective=objective) """ - settings = { - "objective": objective, - } - metric = file.create_entity( "IfcMetric", **{ @@ -52,8 +46,9 @@ def add_metric(file: ifcopenshell.file, objective: ifcopenshell.entity_instance) "Benchmark": "EQUALTO", }, ) - if settings["objective"]: - benchmark_values = list(settings["objective"].BenchmarkValues or []) + if objective: + benchmark_values: list[ifcopenshell.entity_instance] + benchmark_values = list(objective.BenchmarkValues or []) benchmark_values.append(metric) - settings["objective"].BenchmarkValues = benchmark_values + objective.BenchmarkValues = benchmark_values return metric diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_objective.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_objective.py index 064648f1b7..4c434a9edf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/add_objective.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/add_objective.py @@ -29,7 +29,6 @@ def add_objective(file: ifcopenshell.file) -> ifcopenshell.entity_instance: quantities. See ifcopenshell.api.constraint.add_metric for more information. :return: The newly created IfcObjective entity - :rtype: ifcopenshell.entity_instance Example: @@ -42,8 +41,6 @@ def add_objective(file: ifcopenshell.file) -> ifcopenshell.entity_instance: # Note: the objective right now is purely qualitative and for # information purposes. You may wish to add quantiative metrics. """ - settings = {} - return file.create_entity( "IfcObjective", **{"Name": "Unnamed", "ConstraintGrade": "NOTDEFINED", "ObjectiveQualifier": "NOTDEFINED"} ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_constraint.py b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_constraint.py index 381a9e191e..c98f08ade7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_constraint.py +++ b/src/ifcopenshell-python/ifcopenshell/api/constraint/remove_constraint.py @@ -41,9 +41,7 @@ def remove_constraint(file: ifcopenshell.file, constraint: ifcopenshell.entity_i ifcopenshell.api.constraint.remove_constraint(model, constraint=objective) """ - settings = {"constraint": constraint} - - file.remove(settings["constraint"]) + file.remove(constraint) for rel in file.by_type("IfcRelAssociatesConstraint"): if not rel.RelatingConstraint: history = rel.OwnerHistory diff --git a/src/ifcopenshell-python/ifcopenshell/api/context/remove_context.py b/src/ifcopenshell-python/ifcopenshell/api/context/remove_context.py index 2d6defeaf3..7fee232456 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/context/remove_context.py +++ b/src/ifcopenshell-python/ifcopenshell/api/context/remove_context.py @@ -29,9 +29,7 @@ def remove_context(file: ifcopenshell.file, context: ifcopenshell.entity_instanc removed. If a context is removed, then any subcontexts are also removed. :param context: The IfcGeometricRepresentationContext entity to remove - :type context: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -46,22 +44,20 @@ def remove_context(file: ifcopenshell.file, context: ifcopenshell.entity_instanc # Let's just get rid of it completely ifcopenshell.api.context.remove_context(model, context=body) """ - settings = {"context": context} - - for subcontext in settings["context"].HasSubContexts: + for subcontext in context.HasSubContexts: ifcopenshell.api.context.remove_context(file, context=subcontext) - if getattr(settings["context"], "ParentContext", None): - new = settings["context"].ParentContext - for inverse in file.get_inverse(settings["context"]): + if getattr(context, "ParentContext", None): + new = context.ParentContext + for inverse in file.get_inverse(context): if inverse.is_a("IfcCoordinateOperation"): inverse.SourceCRS = inverse.TargetCRS ifcopenshell.util.element.remove_deep(file, inverse) else: - ifcopenshell.util.element.replace_attribute(inverse, settings["context"], new) - file.remove(settings["context"]) + ifcopenshell.util.element.replace_attribute(inverse, context, new) + file.remove(context) else: - representations_in_context = settings["context"].RepresentationsInContext - file.remove(settings["context"]) + representations_in_context = context.RepresentationsInContext + file.remove(context) for element in representations_in_context: ifcopenshell.api.geometry.remove_representation(file, representation=element) diff --git a/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py b/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py index 303cfa6726..2a3ad405f2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py +++ b/src/ifcopenshell-python/ifcopenshell/api/control/assign_control.py @@ -42,12 +42,9 @@ def assign_control( :param relating_control: The IfcControl entity that is creating the control or constraint - :type relating_control: ifcopenshell.entity_instance :param related_object: The IfcObjectDefinition that is being controlled - :type related_object: ifcopenshell.entity_instance :return: The newly created IfcRelAssignsToControl. If relationship already existed before and wasn't changed then returns None. - :rtype: ifcopenshell.entity_instance, None Example: @@ -72,25 +69,20 @@ def assign_control( ifcopenshell.api.control.assign_control(model, relating_control=cost_item, related_object=wall) """ - settings = { - "relating_control": relating_control, - "related_object": related_object, - } - - if settings["related_object"].HasAssignments: - for assignment in settings["related_object"].HasAssignments: - if assignment.is_a("IfcRelAssignsToControl") and assignment.RelatingControl == settings["relating_control"]: + if related_object.HasAssignments: + for assignment in related_object.HasAssignments: + if assignment.is_a("IfcRelAssignsToControl") and assignment.RelatingControl == relating_control: return controls = None - if settings["relating_control"].Controls: - controls = settings["relating_control"].Controls[0] + if relating_control.Controls: + controls = relating_control.Controls[0] if controls: - if settings["related_object"] in controls.RelatedObjects: + if related_object in controls.RelatedObjects: return related_objects = set(controls.RelatedObjects) - related_objects.add(settings["related_object"]) + related_objects.add(related_object) controls.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(file, **{"element": controls}) else: @@ -99,8 +91,8 @@ def assign_control( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": [settings["related_object"]], - "RelatingControl": settings["relating_control"], + "RelatedObjects": [related_object], + "RelatingControl": relating_control, }, ) return controls diff --git a/src/ifcopenshell-python/ifcopenshell/api/control/unassign_control.py b/src/ifcopenshell-python/ifcopenshell/api/control/unassign_control.py index 77530316d8..44cc4fad6e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/control/unassign_control.py +++ b/src/ifcopenshell-python/ifcopenshell/api/control/unassign_control.py @@ -31,12 +31,9 @@ def unassign_control( :param relating_control: The IfcControl entity that is creating the control or constraint - :type relating_control: ifcopenshell.entity_instance :param related_object: The IfcObjectDefinition that is being controlled - :type related_object: ifcopenshell.entity_instance :return: If the control still is related to other objects, the IfcRelAssignsToControl is returned, otherwise None. - :rtype: ifcopenshell.entity_instance, None Example: @@ -54,14 +51,8 @@ def unassign_control( ifcopenshell.api.control.unassign_control(model, relating_control=cost_item, related_object=wall) """ - - settings = { - "relating_control": relating_control, - "related_object": related_object, - } - - for rel in settings["related_object"].HasAssignments or []: - if not rel.is_a("IfcRelAssignsToControl") or rel.RelatingControl != settings["relating_control"]: + for rel in related_object.HasAssignments or []: + if not rel.is_a("IfcRelAssignsToControl") or rel.RelatingControl != relating_control: continue if len(rel.RelatedObjects) == 1: history = rel.OwnerHistory @@ -70,7 +61,7 @@ def unassign_control( ifcopenshell.util.element.remove_deep2(file, history) return related_objects = list(rel.RelatedObjects) - related_objects.remove(settings["related_object"]) + related_objects.remove(related_object) rel.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_quantity.py index c2a020cac8..58f3ed343a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_item_quantity.py @@ -54,12 +54,9 @@ def add_cost_item_quantity( using another API call. :param cost_item: The IfcCostItem to add the quantity to - :type cost_item: ifcopenshell.entity_instance :param ifc_class: The type of quantity to add - :type ifc_class: str, optional :return: The newly created quantity entity, chosen from the ifc_class parameter - :rtype: ifcopenshell.entity_instance Example: @@ -76,20 +73,18 @@ def add_cost_item_quantity( ifcopenshell.api.cost.add_cost_item_quantity(model, cost_item=item, ifc_class="IfcQuantityCount") """ - settings = {"cost_item": cost_item, "ifc_class": ifc_class} - - quantity = file.create_entity(settings["ifc_class"], Name="Unnamed") + quantity = file.create_entity(ifc_class, Name="Unnamed") # 3 IfcPhysicalSimpleQuantity Value # This is a bold assumption # https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564 - if settings["ifc_class"] == "IfcQuantityCount": + if ifc_class == "IfcQuantityCount": count = 0 - for rel in settings["cost_item"].Controls: + for rel in cost_item.Controls: count += len(rel.RelatedObjects) quantity[3] = count else: quantity[3] = 0.0 - quantities = list(settings["cost_item"].CostQuantities or []) + quantities = list(cost_item.CostQuantities or []) quantities.append(quantity) - settings["cost_item"].CostQuantities = quantities + cost_item.CostQuantities = quantities return quantity diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py index 2231c8c07f..d85534bb83 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_schedule.py @@ -39,13 +39,10 @@ def add_cost_schedule( managing any cost items. :param name: The name of the cost schedule. - :type name: str, optional :param predefined_type: The predefined type of the cost schedule, chosen from a valid type in the IFC documentation for IfcCostScheduleTypeEnum - :type predefined_type: str, optional :return: The newly created IfcCostSchedule entity - :rtype: ifcopenshell.entity_instance Example: @@ -55,13 +52,11 @@ def add_cost_schedule( # Now that we have a cost schedule, we may add cost items to it item = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=schedule) """ - settings = {"name": name, "predefined_type": predefined_type} - cost_schedule = ifcopenshell.api.root.create_entity( file, ifc_class="IfcCostSchedule", - predefined_type=settings["predefined_type"], - name=settings["name"], + predefined_type=predefined_type, + name=name, ) if file.schema == "IFC2X3": cost_schedule.UpdateDate = createIfcDateAndTime(file, datetime.now()) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_value.py index ebee87ee44..972849ca96 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/add_cost_value.py @@ -46,9 +46,7 @@ def add_cost_value(file: ifcopenshell.file, parent: ifcopenshell.entity_instance :param parent: A parent IfcCostItem, if specifying a price directly to a cost item, or a top-level price component. Alternatively, this can be set to a IfcCostValue, if specifying price subcomponents. - :type parent: ifcopenshell.entity_instance :return: The newly created IfcCostValue - :rtype: ifcopenshell.entity_instance Example: @@ -91,19 +89,17 @@ def add_cost_value(file: ifcopenshell.file, parent: ifcopenshell.entity_instance ifcopenshell.api.cost.edit_cost_value(model, cost_value=subvalue2, attributes={"AppliedValue": 3.0}) """ - settings = {"parent": parent} - value = file.create_entity("IfcCostValue") - if settings["parent"].is_a("IfcCostItem"): - values = list(settings["parent"].CostValues or []) + if parent.is_a("IfcCostItem"): + values = list(parent.CostValues or []) values.append(value) - settings["parent"].CostValues = values - elif settings["parent"].is_a("IfcConstructionResource"): - values = list(settings["parent"].BaseCosts or []) + parent.CostValues = values + elif parent.is_a("IfcConstructionResource"): + values = list(parent.BaseCosts or []) values.append(value) - settings["parent"].BaseCosts = values - elif settings["parent"].is_a("IfcCostValue"): - values = list(settings["parent"].Components or []) + parent.BaseCosts = values + elif parent.is_a("IfcCostValue"): + values = list(parent.Components or []) values.append(value) - settings["parent"].Components = values + parent.Components = values return value diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_value.py index 2eeb02f89d..d012c02c5b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_value.py @@ -36,11 +36,8 @@ def assign_cost_value( rates as a "template" to quickly populate your rates from. :param cost_item: The IfcCostItem that you want to copy the values to - :type cost_item: ifcopenshell.entity_instance :param cost_rate: The IfcCostItem that you want to copy the values from - :type cost_rate: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -61,16 +58,14 @@ def assign_cost_value( # Now the cost item has the same rate as the one from the schedule of rate's item ifcopenshell.api.cost.assign_cost_value(model, cost_item=item, cost_rate=rate) """ - settings = {"cost_item": cost_item, "cost_rate": cost_rate} - - if settings["cost_item"].CostValues: + if cost_item.CostValues: [ ifcopenshell.api.cost.remove_cost_value( file, - parent=settings["cost_item"], + parent=cost_item, cost_value=cost_value, ) - for cost_value in settings["cost_item"].CostValues + for cost_value in cost_item.CostValues ] # This is an assumption, and not part of the official IFC documentation - settings["cost_item"].CostValues = settings["cost_rate"].CostValues + cost_item.CostValues = cost_rate.CostValues diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py index e1eb12db85..86e149ebd3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py @@ -83,13 +83,11 @@ def calculate_cost_item_resource_value(file: ifcopenshell.file, cost_item: ifcop # (42 * 200) + 50000 = 58400 is our calculated cost ifcopenshell.api.cost.calculate_cost_item_resource_value(model, cost_item=item) """ - settings = {"cost_item": cost_item} - - for cost_value in settings["cost_item"].CostValues or []: - ifcopenshell.api.cost.remove_cost_value(file, parent=settings["cost_item"], cost_value=cost_value) + for cost_value in cost_item.CostValues or []: + ifcopenshell.api.cost.remove_cost_value(file, parent=cost_item, cost_value=cost_value) resources = [] - for rel in settings["cost_item"].Controls or []: + for rel in cost_item.Controls or []: for related_object in rel.RelatedObjects: if related_object.is_a("IfcConstructionResource"): resources.append(related_object) @@ -112,6 +110,6 @@ def calculate_cost_item_resource_value(file: ifcopenshell.file, cost_item: ifcop if unit and "day" in unit: quantity = quantity / 8 # Assume 8 hour working day - TODO implement resource calendar formula = "{}*{}".format(cost, quantity) - cost_value = ifcopenshell.api.cost.add_cost_value(file, parent=settings["cost_item"]) + cost_value = ifcopenshell.api.cost.add_cost_value(file, parent=cost_item) cost_value.Name = resource.Name ifcopenshell.api.cost.edit_cost_value_formula(file, cost_value=cost_value, formula=formula) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py index bb9b1bd18d..b5acaaf1cc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_item.py @@ -29,9 +29,7 @@ def remove_cost_item(file: ifcopenshell.file, cost_item: ifcopenshell.entity_ins retained. :param cost_item: The IfcCostItem entity you want to remove - :type cost_item: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -41,15 +39,13 @@ def remove_cost_item(file: ifcopenshell.file, cost_item: ifcopenshell.entity_ins item = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=schedule) ifcopenshell.api.cost.remove_cost_item(model, cost_item=item) """ - settings = {"cost_item": cost_item} - # TODO: do a deep purge - for inverse in file.get_inverse(settings["cost_item"]): + for inverse in file.get_inverse(cost_item): if inverse.is_a("IfcRelNests"): - if inverse.RelatingObject == settings["cost_item"]: + if inverse.RelatingObject == cost_item: for related_object in inverse.RelatedObjects: ifcopenshell.api.cost.remove_cost_item(file, cost_item=related_object) - elif inverse.RelatedObjects == (settings["cost_item"],): + elif inverse.RelatedObjects == (cost_item,): history = inverse.OwnerHistory file.remove(inverse) if history: @@ -59,7 +55,7 @@ def remove_cost_item(file: ifcopenshell.file, cost_item: ifcopenshell.entity_ins file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["cost_item"].OwnerHistory - file.remove(settings["cost_item"]) + history = cost_item.OwnerHistory + file.remove(cost_item) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py index f5591ee9b1..0e05708f7f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/cost/remove_cost_schedule.py @@ -40,17 +40,15 @@ def remove_cost_schedule(file: ifcopenshell.file, cost_schedule: ifcopenshell.en item = ifcopenshell.api.cost.add_cost_item(model, cost_schedule=schedule) ifcopenshell.api.cost.remove_cost_schedule(model, cost_schedule=schedule) """ - settings = {"cost_schedule": cost_schedule} - # TODO: do a deep purge - for inverse in file.get_inverse(settings["cost_schedule"]): + for inverse in file.get_inverse(cost_schedule): if inverse.is_a("IfcRelAssignsToControl"): [ ifcopenshell.api.cost.remove_cost_item(file, cost_item=related_object) for related_object in inverse.RelatedObjects if related_object.is_a("IfcCostItem") ] - history = settings["cost_schedule"].OwnerHistory - file.remove(settings["cost_schedule"]) + history = cost_schedule.OwnerHistory + file.remove(cost_schedule) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py index 0fd2361c8d..ae56b0b935 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/add_reference.py @@ -38,9 +38,7 @@ def add_reference(file: ifcopenshell.file, information: ifcopenshell.entity_inst :param information: The IfcDocumentInformation that the reference will be created for - :type information: ifcopenshell.entity_instance :return: The newly created IfcDocumentReference entity - :rtype: ifcopenshell.entity_instance Example: @@ -63,13 +61,11 @@ def add_reference(file: ifcopenshell.file, information: ifcopenshell.entity_inst ifcopenshell.api.document.edit_reference(model, reference=reference2, attributes={"Identification": "2.1.15"}) """ - settings = {"information": information} - if file.schema == "IFC2X3": reference = file.create_entity("IfcDocumentReference", ItemReference="X") - if settings["information"]: - references = list(settings["information"].DocumentReferences or []) + if information: + references = list(information.DocumentReferences or []) references.append(reference) - settings["information"].DocumentReferences = references + information.DocumentReferences = references return reference - return file.create_entity("IfcDocumentReference", ReferencedDocument=settings["information"], Identification="X") + return file.create_entity("IfcDocumentReference", ReferencedDocument=information, Identification="X") diff --git a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py index 6de39062f9..a81ae71973 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py +++ b/src/ifcopenshell-python/ifcopenshell/api/document/unassign_document.py @@ -30,12 +30,9 @@ def unassign_document( :param product: The list of objects that the document reference or information is related to. - :type product: list[ifcopenshell.entity_instance] :param document: The IfcDocumentReference (typically) or in rare cases the IfcDocumentInformation that is associated with the product - :type document: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -54,27 +51,21 @@ def unassign_document( # Now let's change our mind and remove the association ifcopenshell.api.document.unassign_document(model, products=[storey], document=reference) """ - settings = { - "products": products, - "document": document, - } # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? # NOTE: reuses code from `library.un assign_reference` reference_rels: set[ifcopenshell.entity_instance] = set() - products = set(settings["products"]) - for product in products: + products_set = set(products) + for product in products_set: reference_rels.update(product.HasAssociations) reference_rels = { - rel - for rel in reference_rels - if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument == settings["document"] + rel for rel in reference_rels if rel.is_a("IfcRelAssociatesDocument") and rel.RelatingDocument == document } for rel in reference_rels: - related_objects = set(rel.RelatedObjects) - products + related_objects = set(rel.RelatedObjects) - products_set if related_objects: rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/drawing/assign_product.py b/src/ifcopenshell-python/ifcopenshell/api/drawing/assign_product.py index fcd739ffad..87f786d807 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/drawing/assign_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/drawing/assign_product.py @@ -46,12 +46,9 @@ def assign_product( in 3D. :param relating_product: The IfcProduct the object is related to - :type relating_product: ifcopenshell.entity_instance :param related_object: The object (typically IfcAnnotation) that the product is related to - :type related_object: ifcopenshell.entity_instance :return: The created IfcRelAssignsToProduct relationship - :rtype: ifcopenshell.entity_instance Example: @@ -62,42 +59,36 @@ def assign_product( ifcopenshell.api.drawing.assign_product(model, relating_product=furniture, related_object=annotation) """ - settings = { - "relating_product": relating_product, - "related_object": related_object, - } - - is_grid_axis = settings["relating_product"].is_a("IfcGridAxis") + is_grid_axis = relating_product.is_a("IfcGridAxis") if is_grid_axis: - if settings["related_object"].HasAssignments: - for rel in settings["related_object"].HasAssignments: - if rel.is_a("IfcRelAssignsToProduct") and rel.Name == settings["relating_product"].AxisTag: + if related_object.HasAssignments: + for rel in related_object.HasAssignments: + if rel.is_a("IfcRelAssignsToProduct") and rel.Name == relating_product.AxisTag: return - elif settings["related_object"].HasAssignments: - for rel in settings["related_object"].HasAssignments: - if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == settings["relating_product"]: + elif related_object.HasAssignments: + for rel in related_object.HasAssignments: + if rel.is_a("IfcRelAssignsToProduct") and rel.RelatingProduct == relating_product: return referenced_by = None if is_grid_axis: - axis = settings["relating_product"] + axis = relating_product grid = None for attribute in ("PartOfW", "PartOfV", "PartOfU"): if getattr(axis, attribute, None): grid = getattr(axis, attribute)[0] - settings["relating_product"] = grid for rel in grid.ReferencedBy: if rel.Name == axis.AxisTag: referenced_by = rel break - elif settings["relating_product"].ReferencedBy: - referenced_by = settings["relating_product"].ReferencedBy[0] + elif relating_product.ReferencedBy: + referenced_by = relating_product.ReferencedBy[0] if referenced_by: related_objects = list(referenced_by.RelatedObjects) - related_objects.append(settings["related_object"]) + related_objects.append(related_object) referenced_by.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, **{"element": referenced_by}) else: @@ -106,8 +97,8 @@ def assign_product( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": [settings["related_object"]], - "RelatingProduct": settings["relating_product"], + "RelatedObjects": [related_object], + "RelatingProduct": relating_product, }, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/drawing/unassign_product.py b/src/ifcopenshell-python/ifcopenshell/api/drawing/unassign_product.py index 03812040cc..f95a98a1fe 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/drawing/unassign_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/drawing/unassign_product.py @@ -34,12 +34,9 @@ def unassign_product( object later or leave the annotation as a "dumb" annotation. :param relating_product: The IfcProduct the object is related to - :type relating_product: ifcopenshell.entity_instance :param related_object: The object (typically IfcAnnotation) that the product is related to - :type related_object: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -54,13 +51,8 @@ def unassign_product( ifcopenshell.api.drawing.unassign_product(model, relating_product=furniture, related_object=annotation) """ - settings = { - "relating_product": relating_product, - "related_object": related_object, - } - - for rel in settings["related_object"].HasAssignments or []: - if not rel.is_a("IfcRelAssignsToProduct") or rel.RelatingProduct != settings["relating_product"]: + for rel in related_object.HasAssignments or []: + if not rel.is_a("IfcRelAssignsToProduct") or rel.RelatingProduct != relating_product: continue if len(rel.RelatedObjects) == 1: history = rel.OwnerHistory @@ -69,6 +61,6 @@ def unassign_product( ifcopenshell.util.element.remove_deep2(file, history) return related_objects = list(rel.RelatedObjects) - related_objects.remove(settings["related_object"]) + related_objects.remove(related_object) rel.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, element=rel) diff --git a/src/ifcopenshell-python/ifcopenshell/api/feature/add_filling.py b/src/ifcopenshell-python/ifcopenshell/api/feature/add_filling.py index 7f59d65ec8..bb6035cd79 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/feature/add_filling.py +++ b/src/ifcopenshell-python/ifcopenshell/api/feature/add_filling.py @@ -33,11 +33,8 @@ def add_filling( filled. :param opening: The IfcOpeningElement to fill with the element. - :type opening: ifcopenshell.entity_instance :param element: The IfcElement to be inserted into the opening. - :type element: ifcopenshell.entity_instance :return: The new IfcRelFillsElement relationship - :rtype: ifcopenshell.entity_instance Example: @@ -102,12 +99,10 @@ def add_filling( # The door will now fill the opening. ifcopenshell.api.feature.add_filling(model, opening=opening, element=door) """ - settings = {"opening": opening, "element": element} - - fills_voids = settings["element"].FillsVoids + fills_voids = element.FillsVoids if fills_voids: - if fills_voids[0].RelatingOpeningElement == settings["opening"]: + if fills_voids[0].RelatingOpeningElement == opening: return fills_voids[0] history = fills_voids[0].OwnerHistory file.remove(fills_voids[0]) @@ -117,6 +112,6 @@ def add_filling( return file.create_entity( "IfcRelFillsElement", GlobalId=ifcopenshell.guid.new(), - RelatingOpeningElement=settings["opening"], - RelatedBuildingElement=settings["element"], + RelatingOpeningElement=opening, + RelatedBuildingElement=element, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/feature/remove_filling.py b/src/ifcopenshell-python/ifcopenshell/api/feature/remove_filling.py index f6931b4f9d..b009b1e0c4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/feature/remove_filling.py +++ b/src/ifcopenshell-python/ifcopenshell/api/feature/remove_filling.py @@ -28,9 +28,7 @@ def remove_filling(file: ifcopenshell.file, element: ifcopenshell.entity_instanc fills the opening. :param element: The element filling an opening. - :type element: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -52,10 +50,8 @@ def remove_filling(file: ifcopenshell.file, element: ifcopenshell.entity_instanc # Not anymore! ifcopenshell.api.feature.remove_filling(model, element=door) """ - settings = {"element": element} - for rel in file.by_type("IfcRelFillsElement"): - if rel.RelatedBuildingElement == settings["element"]: + if rel.RelatedBuildingElement == element: history = rel.OwnerHistory file.remove(rel) if history: diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py index 174c475f92..02bd057cc6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/add_axis_representation.py @@ -56,13 +56,10 @@ def add_axis_representation( :param context: The IfcGeometricRepresentationContext that the representation is part of. This must be either a Model/Axis/GRAPH_VIEW (3D) or Plan/Axis/GRAPH_VIEW (2D). - :type context: ifcopenshell.entity_instance :param axis: The axis, as a list of two coordinates, the coordinates being either a list of 2 or 3 float coordinates depending on whether the axis is 2D or 3D. - :type axis: list[list[float]] :return: The newly created IfcShapeRepresentation entity - :rtype: ifcopenshell.entity_instance Example: diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_element.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_element.py index 89c0c1be19..f737b54c77 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_element.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/connect_element.py @@ -29,20 +29,14 @@ def connect_element( related_element: ifcopenshell.entity_instance, description: Optional[str] = None, ) -> ifcopenshell.entity_instance: - settings = { - "relating_element": relating_element, - "related_element": related_element, - "description": description, - } - incompatible_connections = [] - for rel in settings["relating_element"].ConnectedFrom: - if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == settings["related_element"]: + for rel in relating_element.ConnectedFrom: + if rel.is_a() == "IfcRelConnectsElements" and rel.RelatingElement == related_element: incompatible_connections.append(rel) - for rel in settings["related_element"].ConnectedTo: - if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == settings["relating_element"]: + for rel in related_element.ConnectedTo: + if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == relating_element: incompatible_connections.append(rel) if incompatible_connections: @@ -52,15 +46,15 @@ def connect_element( if history: ifcopenshell.util.element.remove_deep2(file, history) - for rel in settings["relating_element"].ConnectedTo: - if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == settings["related_element"]: - rel.Description = settings["description"] + for rel in relating_element.ConnectedTo: + if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement == related_element: + rel.Description = description return rel return file.createIfcRelConnectsElements( ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.owner.create_owner_history(file), - Description=settings["description"], - RelatingElement=settings["relating_element"], - RelatedElement=settings["related_element"], + Description=description, + RelatingElement=relating_element, + RelatedElement=related_element, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/map_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/map_representation.py index b559a66d5f..e868beea66 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/map_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/map_representation.py @@ -17,7 +17,6 @@ # along with IfcOpenShell. If not, see . import ifcopenshell -from typing import Any def map_representation( @@ -25,15 +24,14 @@ def map_representation( ) -> ifcopenshell.entity_instance: usecase = Usecase() usecase.file = file - usecase.settings = {"representation": representation} - return usecase.execute() + return usecase.execute(representation) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] - def execute(self) -> ifcopenshell.entity_instance: + def execute(self, representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: + self.representation = representation mapping_source = self.get_mapping_source() zero = self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)) @@ -46,15 +44,15 @@ class Usecase: return self.file.create_entity( "IfcShapeRepresentation", **{ - "ContextOfItems": self.settings["representation"].ContextOfItems, - "RepresentationIdentifier": self.settings["representation"].RepresentationIdentifier, + "ContextOfItems": representation.ContextOfItems, + "RepresentationIdentifier": representation.RepresentationIdentifier, "RepresentationType": "MappedRepresentation", "Items": [mapped_item], } ) def get_mapping_source(self) -> ifcopenshell.entity_instance: - for inverse in self.file.get_inverse(self.settings["representation"]): + for inverse in self.file.get_inverse(self.representation): if inverse.is_a("IfcRepresentationMap"): return inverse zero = self.file.createIfcCartesianPoint((0.0, 0.0, 0.0)) @@ -62,5 +60,5 @@ class Usecase: z_axis = self.file.createIfcDirection((0.0, 0.0, 1.0)) mapping_origin = self.file.createIfcAxis2Placement3D(zero, z_axis, x_axis) return self.file.createIfcRepresentationMap( - MappingOrigin=mapping_origin, MappedRepresentation=self.settings["representation"] + MappingOrigin=mapping_origin, MappedRepresentation=self.representation ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py index eb40127209..a2a877ef6e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/add_group.py @@ -35,12 +35,9 @@ def add_group( or structural load groups, which group together loads for structural analysis, or inventories, which are groups of assets. - :param Name: The name of the group. Defaults to "Unnamed" - :type Name: str, optional + :param name: The name of the group. Defaults to "Unnamed" :param description: The description of the purpose of the group. - :type description: str, optional :return: The newly created IfcGroup - :rtype: ifcopenshell.entity_instance Example: @@ -48,17 +45,11 @@ def add_group( ifcopenshell.api.group.add_group(model, name="Unit 1A") """ - settings = { - "name": name or "Unnamed", - "description": description, - } return file.create_entity( "IfcGroup", - **{ - "GlobalId": ifcopenshell.guid.new(), - "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "Name": settings["name"], - "Description": settings["description"], - } + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=ifcopenshell.api.owner.create_owner_history(file), + Name=name, + Description=description, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/group/remove_group.py b/src/ifcopenshell-python/ifcopenshell/api/group/remove_group.py index 3f3987f66a..34011eeca9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/group/remove_group.py +++ b/src/ifcopenshell-python/ifcopenshell/api/group/remove_group.py @@ -39,9 +39,7 @@ def remove_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance) - group = ifcopenshell.api.group.add_group(model, name="Unit 1A") ifcopenshell.api.group.remove_group(model, group=group) """ - settings = {"group": group} - - for inverse_id in [i.id() for i in file.get_inverse(settings["group"])]: + for inverse_id in [i.id() for i in file.get_inverse(group)]: try: inverse = file.by_id(inverse_id) except: @@ -49,11 +47,11 @@ def remove_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance) - if inverse.is_a("IfcRelDefinesByProperties"): ifcopenshell.api.pset.remove_pset( file, - product=settings["group"], + product=group, pset=inverse.RelatingPropertyDefinition, ) elif inverse.is_a("IfcRelAssignsToGroup"): - if inverse.RelatingGroup == settings["group"]: + if inverse.RelatingGroup == group: history = inverse.OwnerHistory file.remove(inverse) if history: @@ -63,7 +61,7 @@ def remove_group(file: ifcopenshell.file, group: ifcopenshell.entity_instance) - file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["group"].OwnerHistory - file.remove(settings["group"]) + history = group.OwnerHistory + file.remove(group) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py index 428e0b01d7..8e138954d5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/add_reference.py @@ -50,14 +50,10 @@ def add_reference(file: ifcopenshell.file, library: ifcopenshell.entity_instance ifcopenshell.api.library.edit_reference(model, reference=reference, attributes={"Identification": "http://example.org/digitaltwin#AHU01"}) """ - settings = { - "library": library, - } - if file.schema == "IFC2X3": reference = file.createIfcLibraryReference() - references = list(settings["library"].LibraryReference or []) + references = list(library.LibraryReference or []) references.append(reference) - settings["library"].LibraryReference = references + library.LibraryReference = references return reference - return file.createIfcLibraryReference(ReferencedLibrary=settings["library"]) + return file.createIfcLibraryReference(ReferencedLibrary=library) diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py index 40b3a92f56..a9b6169304 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/assign_reference.py @@ -33,14 +33,11 @@ def assign_reference( detail about how references work. :param products: The list of IfcProducts you want to associate with the reference - :type products: list[ifcopenshell.entity_instance] :param reference: The IfcLibraryReference you want the product to be associated with. - :type reference: ifcopenshell.entity_instance :return: The IfcRelAssociatesLibrary relationship entity or `None` if `products` was an empty list or all products were already assigned to the `reference`. - :rtype: Union[ifcopenshell.entity_instance, None] Example: @@ -60,38 +57,33 @@ def assign_reference( # And now assign the IFC model's AHU with its Brickschema counterpart ifcopenshell.api.library.assign_reference(model, reference=reference, products=[ahu]) """ - settings = { - "products": products, - "reference": reference, - } - # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? - referenced_elements = ifcopenshell.util.element.get_referenced_elements(settings["reference"]) - products: set[ifcopenshell.entity_instance] = set(settings["products"]) - products = products - referenced_elements + referenced_elements = ifcopenshell.util.element.get_referenced_elements(reference) + products_set: set[ifcopenshell.entity_instance] = set(products) + products_set = products_set - referenced_elements - if not products: + if not products_set: return if file.schema == "IFC2X3": rel = next( - (r for r in file.by_type("IfcRelAssociatesLibrary") if r.RelatingLibrary == settings["reference"]), + (r for r in file.by_type("IfcRelAssociatesLibrary") if r.RelatingLibrary == reference), None, ) else: - rel = next(iter(settings["reference"].LibraryRefForObjects), None) + rel = next(iter(reference.LibraryRefForObjects), None) if not rel: return file.create_entity( "IfcRelAssociatesLibrary", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.owner.create_owner_history(file), - RelatedObjects=list(products), - RelatingLibrary=settings["reference"], + RelatedObjects=list(products_set), + RelatingLibrary=reference, ) - related_objects = set(rel.RelatedObjects) | products + related_objects = set(rel.RelatedObjects) | products_set rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(file, element=rel) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py index 876a9b86e6..d92ed2fc1e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py +++ b/src/ifcopenshell-python/ifcopenshell/api/library/unassign_reference.py @@ -31,11 +31,8 @@ def unassign_reference( If the product isn't assigned to the reference, nothing will happen. :param reference: The IfcLibraryReference to unassign from - :type reference: ifcopenshell.entity_instance :param products: A list of IfcProduct elements to unassign from the reference - :type products: list[ifcopenshell.entity_instance] :return: None - :rtype: None Example: @@ -58,24 +55,19 @@ def unassign_reference( # Let's change our mind and unassign it. ifcopenshell.api.library.unassign_reference(model, reference=reference, products=[ahu]) """ - - settings = {"reference": reference, "products": products} - # TODO: do we need to support non-ifcroot elements like we do in classification.add_reference? reference_rels: set[ifcopenshell.entity_instance] = set() - products = set(settings["products"]) - for product in products: + products_set = set(products) + for product in products_set: reference_rels.update(product.HasAssociations) reference_rels = { - rel - for rel in reference_rels - if rel.is_a("IfcRelAssociatesLibrary") and rel.RelatingLibrary == settings["reference"] + rel for rel in reference_rels if rel.is_a("IfcRelAssociatesLibrary") and rel.RelatingLibrary == reference } for rel in reference_rels: - related_objects = set(rel.RelatedObjects) - products + related_objects = set(rel.RelatedObjects) - products_set if related_objects: rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_list_item.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_list_item.py index 944a835493..994d4665ef 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_list_item.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_list_item.py @@ -40,11 +40,8 @@ def add_list_item( :param material_list: The IfcMaterialList the material should be added to. - :type material_list: ifcopenshell.entity_instance :param material: The IfcMaterial to add to the list - :type material: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -80,8 +77,6 @@ def add_list_item( # aluminium and glass. ifcopenshell.api.material.assign_material(model, products=[window_type], material=material_set) """ - settings = {"material_list": material_list, "material": material} - - materials = list(settings["material_list"].Materials or []) - materials.append(settings["material"]) - settings["material_list"].Materials = materials + materials = list(material_list.Materials or []) + materials.append(material) + material_list.Materials = materials diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py index d938c75380..6b04f78be2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_material.py @@ -58,13 +58,9 @@ def add_material( :param name: The name of the material, typically tagged in a finishes drawing or schedule. - :type name: str, optional :param category: The category of the material. - :type category: str, optional :param description: A description of the material. - :type description: str, optional :return: The newly created IfcMaterial - :rtype: ifcopenshell.entity_instance Example: @@ -81,11 +77,9 @@ def add_material( # "Style" has been specified. ifcopenshell.api.material.assign_material(model, products=[concrete_bench], material=concrete) """ - settings = {"name": name or "Unnamed", "category": category, "description": description} - - material = file.create_entity("IfcMaterial", **{"Name": settings["name"] or "Unnamed"}) - if settings["category"]: - material.Category = settings["category"] - if settings["description"]: - material.Description = settings["description"] + material = file.create_entity("IfcMaterial", **{"Name": name or "Unnamed"}) + if category: + material.Category = category + if description: + material.Description = description return material diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/add_material_set.py b/src/ifcopenshell-python/ifcopenshell/api/material/add_material_set.py index 72c090b1ae..bd23688ac8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/add_material_set.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/add_material_set.py @@ -68,14 +68,11 @@ def add_material_set( :param name: The name of the material set, which may be purely descriptive or annotated in drawings. Defaults to "Unnamed". - :type name: str, optional :param set_type: What type of set you want to create, chosen from IfcMaterialLayerSet, IfcMaterialProfileSet, IfcMaterialConstituentSet, or IfcMaterialList. Defaults to IfcMaterialConstituentSet. - :type set_type: str, optional :return: The newly created material set element - :rtype: ifcopenshell.entity_instance Example: @@ -113,10 +110,8 @@ def add_material_set( # Great! Let's assign our material set to our wall type. ifcopenshell.api.material.assign_material(model, products=[wall_type], material=material_set) """ - settings = {"name": name or "Unnamed", "set_type": set_type} - - if settings["set_type"] == "IfcMaterialLayerSet": - return file.create_entity("IfcMaterialLayerSet", LayerSetName=settings["name"] or "Unnamed") - elif settings["set_type"] == "IfcMaterialList": + if set_type == "IfcMaterialLayerSet": + return file.create_entity("IfcMaterialLayerSet", LayerSetName=name or "Unnamed") + elif set_type == "IfcMaterialList": return file.create_entity("IfcMaterialList") - return file.create_entity(settings["set_type"], Name=settings["name"] or "Unnamed") + return file.create_entity(set_type, Name=name or "Unnamed") diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_material.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_material.py index d9894b0d96..c919f1f629 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_material.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_material.py @@ -29,9 +29,7 @@ def remove_material(file: ifcopenshell.file, material: ifcopenshell.entity_insta take care of this situation themselves. :param material: The IfcMaterial entity you want to remove - :type material: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -43,10 +41,8 @@ def remove_material(file: ifcopenshell.file, material: ifcopenshell.entity_insta # ... and remove it ifcopenshell.api.material.remove_material(model, material=aluminium) """ - settings = {"material": material} - - inverse_elements = file.get_inverse(settings["material"]) - file.remove(settings["material"]) + inverse_elements = file.get_inverse(material) + file.remove(material) # TODO: Right now, we we choose only to delete set items (e.g. a layer) but not the material set # This can lead to invalid material sets, but we assume the user will deal with it for inverse in inverse_elements: diff --git a/src/ifcopenshell-python/ifcopenshell/api/material/remove_material_set.py b/src/ifcopenshell-python/ifcopenshell/api/material/remove_material_set.py index 30edd7ad9e..51decaaa98 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/material/remove_material_set.py +++ b/src/ifcopenshell-python/ifcopenshell/api/material/remove_material_set.py @@ -29,9 +29,7 @@ def remove_material_set(file: ifcopenshell.file, material: ifcopenshell.entity_i :param material: The IfcMaterialLayerSet, IfcMaterialConstituentSet, IfcMaterialProfileSet entity you want to remove. - :type material: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -55,20 +53,18 @@ def remove_material_set(file: ifcopenshell.file, material: ifcopenshell.entity_i ifcopenshell.api.material.remove_material_set(model, material=material_set) """ - settings = {"material": material} - - inverse_elements = file.get_inverse(settings["material"]) - if settings["material"].is_a("IfcMaterialLayerSet"): - set_items = settings["material"].MaterialLayers or [] - elif settings["material"].is_a("IfcMaterialProfileSet"): - set_items = settings["material"].MaterialProfiles or [] - elif settings["material"].is_a("IfcMaterialConstituentSet"): - set_items = settings["material"].MaterialConstituents or [] - elif settings["material"].is_a("IfcMaterialList"): + inverse_elements = file.get_inverse(material) + if material.is_a("IfcMaterialLayerSet"): + set_items = material.MaterialLayers or [] + elif material.is_a("IfcMaterialProfileSet"): + set_items = material.MaterialProfiles or [] + elif material.is_a("IfcMaterialConstituentSet"): + set_items = material.MaterialConstituents or [] + elif material.is_a("IfcMaterialList"): set_items = [] for set_item in set_items: file.remove(set_item) - file.remove(settings["material"]) + file.remove(material) for inverse in inverse_elements: if inverse.is_a("IfcRelAssociatesMaterial"): history = inverse.OwnerHistory diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_actor.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_actor.py index 284020a224..1456a99d5e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_actor.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_actor.py @@ -49,11 +49,8 @@ def add_actor( IfcPerson if it is a sole individual, or an IfcPersonAndOrganization if a specific person is liable within an organisation and must be legally nominated. - :type actor: ifcopenshell.entity_instance :param ifc_class: Either "IfcActor" or "IfcOccupant". - :type ifc_class: str, optional :return: The newly created IfcActor or IfcOccupant - :rtype: ifcopenshell.entity_instance Example: @@ -67,8 +64,7 @@ def add_actor( # Assign that organisation to a newly created actor actor = ifcopenshell.api.owner.add_actor(model, actor=organisation) """ - settings = {"actor": actor, "ifc_class": ifc_class or "IfcActor"} - - actor = ifcopenshell.api.root.create_entity(file, ifc_class=settings["ifc_class"]) - actor.TheActor = settings["actor"] - return actor + ifc_class = ifc_class or "IfcActor" + actor_ = ifcopenshell.api.root.create_entity(file, ifc_class=ifc_class) + actor_.TheActor = actor + return actor_ diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_address.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_address.py index 5b03f9f18a..791f41c6a2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_address.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_address.py @@ -39,12 +39,9 @@ def add_address( :param assigned_object: The IfcOrganization or IfcPerson the contact address belongs to. - :type assigned_object: ifcopenshell.entity_instance :param ifc_class: Either IfcPostalAddress or IfcTelecomAddress. Defaults to IfcPostalAddress. - :type ifc_class: str, optional :return: The new IfcPostalAddress or IfcTelecomAddress - :rtype: ifcopenshell.entity_instance Example: @@ -67,10 +64,8 @@ def add_address( "ElectronicMailAddresses": ["bobthebuilder@example.com"], "WWWHomePageURL": "https://thinkmoult.com"}) """ - settings = {"assigned_object": assigned_object, "ifc_class": ifc_class} - - address = file.create_entity(settings["ifc_class"], "OFFICE") - addresses = list(settings["assigned_object"].Addresses) if settings["assigned_object"].Addresses else [] + address = file.create_entity(ifc_class, "OFFICE") + addresses = list(assigned_object.Addresses) if assigned_object.Addresses else [] addresses.append(address) - settings["assigned_object"].Addresses = addresses + assigned_object.Addresses = addresses return address diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_organisation.py index b5c2fae582..be308536e3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_organisation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_organisation.py @@ -31,11 +31,8 @@ def add_organisation( Sometimes used in drawing naming schemes. Otherise used as a canonicalised way of computers to identify the organisation. Like their stock name. - :type identification: str, optional :param name: The legal name of the organisation - :type name: str, optional :return: The newly created IfcOrganization - :rtype: ifcopenshell.entity_instance Example: @@ -44,11 +41,9 @@ def add_organisation( organisation = ifcopenshell.api.owner.add_organisation(model, identification="AWB", name="Architects Without Ballpens") """ - settings = {"identification": identification, "name": name} - - data = {"Name": settings["name"]} + data = {"Name": name} if file.schema == "IFC2X3": - data["Id"] = settings["identification"] + data["Id"] = identification else: - data["Identification"] = settings["identification"] + data["Identification"] = identification return file.create_entity("IfcOrganization", **data) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_person.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_person.py index 30d81ef117..1953d80028 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_person.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_person.py @@ -31,13 +31,9 @@ def add_person( :param identification: The computer readable unique identification of the person. For example, their username in a CDE or alias. - :type identification: str, optional :param family_name: The family name - :type family_name: str, optional :param given_name: The given name - :type given_name: str, optional :return: The newly created IfcPerson - :rtype: ifcopenshell.entity_instance Example: @@ -46,15 +42,9 @@ def add_person( ifcopenshell.api.owner.add_person(model, identification="bobthebuilder", family_name="Thebuilder", given_name="Bob") """ - settings = { - "identification": identification, - "family_name": family_name, - "given_name": given_name, - } - - data = {"FamilyName": settings["family_name"], "GivenName": settings["given_name"]} + data = {"FamilyName": family_name, "GivenName": given_name} if file.schema == "IFC2X3": - data["Id"] = settings["identification"] + data["Id"] = identification else: - data["Identification"] = settings["identification"] + data["Identification"] = identification return file.create_entity("IfcPerson", **data) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/add_person_and_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/add_person_and_organisation.py index 6d6a9bdfb7..3fbec452c0 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/add_person_and_organisation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/add_person_and_organisation.py @@ -30,11 +30,8 @@ def add_person_and_organisation( :param person: The IfcPerson being the representative of the organisation. - :type person: ifcopenshell.entity_instance :param organisation: The IfcOrganization it - :type organisation: ifcopenshell.entity_instance :return: The newly created IfcPersonAndOrganization - :rtype: ifcopenshell.entity_instance Example: @@ -48,6 +45,4 @@ def add_person_and_organisation( ifcopenshell.api.owner.add_person_and_organisation(model, person=person, organisation=organisation) """ - settings = {"person": person, "organisation": organisation} - - return file.createIfcPersonAndOrganization(settings["person"], settings["organisation"]) + return file.create_entity("IfcPersonAndOrganization", person, organisation) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/assign_actor.py b/src/ifcopenshell-python/ifcopenshell/api/owner/assign_actor.py index dcda100b34..7f2df90511 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/assign_actor.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/assign_actor.py @@ -43,11 +43,8 @@ def assign_actor( ifcopenshell.api.resource.assign_resource. :param relating_actor: The IfcActor who is responsible for the object. - :type relating_actor: ifcopenshell.entity_instance :param related_object: The object the actor is responsible for. - :type related_object: ifcopenshell.entity_instance :return: The newly created IfcRelAssignsToActor relationship. - :rtype: ifcopenshell.entity_instance Example: @@ -74,24 +71,19 @@ def assign_actor( ifcopenshell.api.owner.assign_actor(model, relating_actor=manufacturer, related_object=pump_type) """ - settings = { - "relating_actor": relating_actor, - "related_object": related_object, - } - - if settings["related_object"].HasAssignments: - for rel in settings["related_object"].HasAssignments: - if rel.is_a("IfcRelAssignsToActor") and rel.RelatingActor == settings["relating_actor"]: + if related_object.HasAssignments: + for rel in related_object.HasAssignments: + if rel.is_a("IfcRelAssignsToActor") and rel.RelatingActor == relating_actor: return rel rel = None - if settings["relating_actor"].IsActingUpon: - rel = settings["relating_actor"].IsActingUpon[0] + if relating_actor.IsActingUpon: + rel = relating_actor.IsActingUpon[0] if rel: related_objects = list(rel.RelatedObjects) - related_objects.append(settings["related_object"]) + related_objects.append(related_object) rel.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) else: @@ -100,8 +92,8 @@ def assign_actor( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": [settings["related_object"]], - "RelatingActor": settings["relating_actor"], + "RelatedObjects": [related_object], + "RelatingActor": relating_actor, } ) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_actor.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_actor.py index 026808b7d2..dd86a6f937 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_actor.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_actor.py @@ -24,9 +24,7 @@ def remove_actor(file: ifcopenshell.file, actor: ifcopenshell.entity_instance) - """Removes an actor :param actor: The IfcActor to remove. - :type actor: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -44,9 +42,7 @@ def remove_actor(file: ifcopenshell.file, actor: ifcopenshell.entity_instance) - # Actually we need ballpens on this project ifcopenshell.api.owner.remove_actor(model, actor=actor) """ - settings = {"actor": actor} - - history = settings["actor"].OwnerHistory - file.remove(settings["actor"]) + history = actor.OwnerHistory + file.remove(actor) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_address.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_address.py index 0d8ff96ff9..28942943a4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_address.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_address.py @@ -25,9 +25,7 @@ def remove_address(file: ifcopenshell.file, address: ifcopenshell.entity_instanc relationship removed. :param address: The IfcAddress to remove. - :type address: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -40,10 +38,8 @@ def remove_address(file: ifcopenshell.file, address: ifcopenshell.entity_instanc # Change our mind and delete it ifcopenshell.api.owner.remove_address(model, address=address) """ - settings = {"address": address} - - for inverse in file.get_inverse(settings["address"]): + for inverse in file.get_inverse(address): if inverse.is_a() in ("IfcOrganization", "IfcPerson"): - if inverse.Addresses == (settings["address"],): + if inverse.Addresses == (address,): inverse.Addresses = None - file.remove(settings["address"]) + file.remove(address) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_application.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_application.py index c744d2f97f..1a63bc4949 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_application.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_application.py @@ -25,9 +25,7 @@ def remove_application(file: ifcopenshell.file, application: ifcopenshell.entity Check whether or not the application is used anywhere prior to removal. :param address: The IfcApplication to remove. - :type address: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -36,6 +34,4 @@ def remove_application(file: ifcopenshell.file, application: ifcopenshell.entity application = ifcopenshell.api.owner.add_application(model) ifcopenshell.api.owner.remove_address(model, application=application) """ - settings = {"application": application} - - file.remove(settings["application"]) + file.remove(application) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person_and_organisation.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person_and_organisation.py index 95f137f046..13607b22f3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person_and_organisation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_person_and_organisation.py @@ -28,9 +28,7 @@ def remove_person_and_organisation( the "person and organisation" group. :param person_and_organisation: The IfcPersonAndOrganization to remove. - :type person_and_organisation: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -46,17 +44,15 @@ def remove_person_and_organisation( ifcopenshell.api.owner.remove_person_and_organisation(model, person_and_organisation=user) """ - settings = {"person_and_organisation": person_and_organisation} - - for inverse in file.get_inverse(settings["person_and_organisation"]): + for inverse in file.get_inverse(person_and_organisation): if inverse.is_a("IfcDocumentInformation"): - if inverse.Editors == (settings["person_and_organisation"],): + if inverse.Editors == (person_and_organisation,): inverse.Editors = None elif inverse.is_a("IfcActor"): ifcopenshell.api.root.remove_product(file, product=inverse) elif inverse.is_a("IfcResourceLevelRelationship"): - if inverse.RelatedResourceObjects == (settings["person_and_organisation"],): + if inverse.RelatedResourceObjects == (person_and_organisation,): file.remove(inverse) elif inverse.is_a("IfcOwnerHistory"): file.remove(inverse) - file.remove(settings["person_and_organisation"]) + file.remove(person_and_organisation) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_role.py b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_role.py index 07c2d36a9a..10c648cca5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/remove_role.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/remove_role.py @@ -25,9 +25,7 @@ def remove_role(file: ifcopenshell.file, role: ifcopenshell.entity_instance) -> leave some of them without roles. :param role: The IfcActorRole to remove. - :type role: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -40,13 +38,11 @@ def remove_role(file: ifcopenshell.file, role: ifcopenshell.entity_instance) -> # After running this, the organisation will have no role again ifcopenshell.api.owner.remove_role(model, role=role) """ - settings = {"role": role} - - for inverse in file.get_inverse(settings["role"]): + for inverse in file.get_inverse(role): if inverse.is_a() in ("IfcOrganization", "IfcPerson", "IfcPersonAndOrganization"): - if inverse.Roles == (settings["role"],): + if inverse.Roles == (role,): inverse.Roles = None elif inverse.is_a("IfcResourceLevelRelationship") and not inverse.is_a("IfcOrganizationRelationship"): - if inverse.RelatedResourceObjects == (settings["organisation"],): + if inverse.RelatedResourceObjects == (organisation,): file.remove(inverse) - file.remove(settings["role"]) + file.remove(role) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/unassign_actor.py b/src/ifcopenshell-python/ifcopenshell/api/owner/unassign_actor.py index e161e046e2..98279c128d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/unassign_actor.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/unassign_actor.py @@ -29,11 +29,8 @@ def unassign_actor( This means that the actor is no longer responsible for the object. :param relating_actor: The IfcActor who is responsible for the object. - :type relating_actor: ifcopenshell.entity_instance :param related_object: The object the actor is responsible for. - :type related_object: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -55,13 +52,8 @@ def unassign_actor( ifcopenshell.api.owner.unassign_actor(model, relating_actor=manufacturer, related_object=pump_type) """ - settings = { - "relating_actor": relating_actor, - "related_object": related_object, - } - - for rel in settings["related_object"].HasAssignments or []: - if not rel.is_a("IfcRelAssignsToActor") or rel.RelatingActor != settings["relating_actor"]: + for rel in related_object.HasAssignments or []: + if not rel.is_a("IfcRelAssignsToActor") or rel.RelatingActor != relating_actor: continue if len(rel.RelatedObjects) == 1: history = rel.OwnerHistory @@ -70,6 +62,6 @@ def unassign_actor( ifcopenshell.util.element.remove_deep2(file, history) return related_objects = list(rel.RelatedObjects) - related_objects.remove(settings["related_object"]) + related_objects.remove(related_object) rel.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/owner/update_owner_history.py b/src/ifcopenshell-python/ifcopenshell/api/owner/update_owner_history.py index c7cb3b719d..af40aa3b46 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/owner/update_owner_history.py +++ b/src/ifcopenshell-python/ifcopenshell/api/owner/update_owner_history.py @@ -59,9 +59,6 @@ def update_owner_history( # API calls either. ifcopenshell.api.attribute.edit_attributes(model, product=space, attributes={"Name": "Lobby"}) """ - settings = {"element": element} - - element = settings["element"] if not element.is_a("IfcRoot"): return user = ifcopenshell.api.owner.settings.get_user(file) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py index 3a23507e54..97d8ba3f81 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/add_parameterized_profile.py @@ -30,9 +30,7 @@ def add_parameterized_profile(file: ifcopenshell.file, ifc_class: str) -> ifcope :param ifc_class: The subclass of IfcParameterizedProfileDef that you'd like to create. - :type ifc_class: str :return: The newly created element depending on the specified ifc_class. - :rtype: ifcopenshell.entity_instance Example: @@ -42,6 +40,4 @@ def add_parameterized_profile(file: ifcopenshell.file, ifc_class: str) -> ifcope ifc_class="IfcCircleProfileDef") circle.Radius = 1. """ - settings = {"ifc_class": ifc_class} - - return file.create_entity(settings["ifc_class"]) + return file.create_entity(ifc_class) diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/remove_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/remove_profile.py index 5e3816fb93..be74271736 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/profile/remove_profile.py +++ b/src/ifcopenshell-python/ifcopenshell/api/profile/remove_profile.py @@ -35,11 +35,10 @@ def remove_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instanc circle = 1. ifcopenshell.api.profile.remove_profile(model, profile=circle) """ - settings = {"profile": profile} is_ifc2x3 = file.schema == "IFC2X3" subelements = set() - for attribute in settings["profile"]: + for attribute in profile: if isinstance(attribute, ifcopenshell.entity_instance): subelements.add(attribute) @@ -56,6 +55,6 @@ def remove_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instanc for pset in profile_psets: ifcopenshell.api.pset.remove_pset(file, product=profile, pset=pset) - file.remove(settings["profile"]) + file.remove(profile) for subelement in subelements: ifcopenshell.util.element.remove_deep2(file, subelement) diff --git a/src/ifcopenshell-python/ifcopenshell/api/project/create_file.py b/src/ifcopenshell-python/ifcopenshell/api/project/create_file.py index 38da958ed5..6eaa324b82 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/project/create_file.py +++ b/src/ifcopenshell-python/ifcopenshell/api/project/create_file.py @@ -50,9 +50,7 @@ def create_file(version: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4") -> ifcope # ... and off we go! """ - settings = {"version": version} - - file = ifcopenshell.file(schema=settings["version"]) + file = ifcopenshell.file(schema=version) file.wrapped_data.header.file_name.name = "/dev/null" # Hehehe file.wrapped_data.header.file_name.time_stamp = ( datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone().replace(microsecond=0).isoformat() diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py index d6636e9f09..7c27dbd9bf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset/remove_pset.py @@ -28,11 +28,8 @@ def remove_pset( All properties that are part of this property set are also removed. :param product: The IfcObject to remove the property set from. - :type product: ifcopenshell.entity_instance :param pset: The IfcPropertySet or IfcElementQuantity to remove. - :type pset: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -45,27 +42,25 @@ def remove_pset( # Remove it! ifcopenshell.api.pset.remove_pset(model, product=wall_type, pset=pset) """ - settings = {"product": product, "pset": pset} - to_purge = [] should_remove_pset = True - for inverse in file.get_inverse(settings["pset"]): + for inverse in file.get_inverse(pset): if inverse.is_a("IfcRelDefinesByProperties"): if not inverse.RelatedObjects or len(inverse.RelatedObjects) == 1: to_purge.append(inverse) else: related_objects = list(inverse.RelatedObjects) - related_objects.remove(settings["product"]) + related_objects.remove(product) inverse.RelatedObjects = related_objects should_remove_pset = False if should_remove_pset: properties = [] # Predefined psets have no properties - if settings["pset"].is_a("IfcPropertySet"): - properties = settings["pset"].HasProperties or [] - elif settings["pset"].is_a("IfcQuantitySet"): - properties = settings["pset"].Quantities or [] - elif settings["pset"].is_a() in ("IfcMaterialProperties", "IfcProfileProperties"): - properties = settings["pset"].Properties or [] + if pset.is_a("IfcPropertySet"): + properties = pset.HasProperties or [] + elif pset.is_a("IfcQuantitySet"): + properties = pset.Quantities or [] + elif pset.is_a() in ("IfcMaterialProperties", "IfcProfileProperties"): + properties = pset.Properties or [] for prop in properties: if file.get_total_inverses(prop) != 1: continue @@ -75,8 +70,8 @@ def remove_pset( file.remove(enumeration) file.remove(prop) # IfcMaterialProperties and IfcProfileProperties don't have OwnerHistory - history = getattr(settings["pset"], "OwnerHistory", None) - file.remove(settings["pset"]) + history = getattr(pset, "OwnerHistory", None) + file.remove(pset) if history: ifcopenshell.util.element.remove_deep2(file, history) for element in to_purge: diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_pset_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_pset_template.py index fc1b9a4265..d66e5b2302 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_pset_template.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/add_pset_template.py @@ -71,19 +71,15 @@ def add_pset_template( overridden by occurrences, and is applicable to everything. :param name: The name of the property set - :type name: str,optional :param template_type: Choose from one of PSET_TYPEDRIVENONLY, PSET_TYPEDRIVENOVERRIDE, PSET_OCCURRENCEDRIVEN, PSET_PERFORMANCEDRIVEN, QTO_TYPEDRIVENONLY, QTO_TYPEDRIVENOVERRIDE, QTO_OCCURRENCEDRIVEN, NOTDEFINED - :type template_type: str,optional :param applicable_entity: The entity that this template is allowed to be applied to. For example, IfcWall means that the property set may be assigned to walls only. IfcTypeObject, the default, means that the property set may be assigned to any type. - :type applicable_entity: str,optional :return: The newly created IfcPropertySetTemplate - :rtype: ifcopenshell.entity_instance Example: @@ -99,12 +95,10 @@ def add_pset_template( name="HighVoltage", description="Whether there is a risk of high voltage.", primary_measure_type="IfcBoolean") """ - settings = {"name": name, "template_type": template_type, "applicable_entity": applicable_entity} - return file.create_entity( "IfcPropertySetTemplate", GlobalId=ifcopenshell.guid.new(), - Name=settings["name"], - TemplateType=settings["template_type"], - ApplicableEntity=settings["applicable_entity"], + Name=name, + TemplateType=template_type, + ApplicableEntity=applicable_entity, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/remove_prop_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/remove_prop_template.py index 99558cf6e8..ca8d5ba55e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/remove_prop_template.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/remove_prop_template.py @@ -27,9 +27,7 @@ def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.en templates. :param prop_template: The IfcSimplePropertyTemplate to remove. - :type prop_template: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -44,13 +42,11 @@ def remove_prop_template(file: ifcopenshell.file, prop_template: ifcopenshell.en # Let's remove the second one. ifcopenshell.api.pset_template.remove_prop_template(model, prop_template=prop2) """ - settings = {"prop_template": prop_template} - - for inverse in file.get_inverse(settings["prop_template"]): + for inverse in file.get_inverse(prop_template): if len(inverse.HasPropertyTemplates) == 1: inverse.HasPropertyTemplates = [] else: has_property_templates = list(inverse.HasPropertyTemplates) - has_property_templates.remove(settings["prop_template"]) + has_property_templates.remove(prop_template) inverse.HasPropertyTemplates = has_property_templates - ifcopenshell.util.element.remove_deep(file, settings["prop_template"]) + ifcopenshell.util.element.remove_deep(file, prop_template) diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset_template/remove_pset_template.py b/src/ifcopenshell-python/ifcopenshell/api/pset_template/remove_pset_template.py index de321ea6fd..07d5cf6daf 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/pset_template/remove_pset_template.py +++ b/src/ifcopenshell-python/ifcopenshell/api/pset_template/remove_pset_template.py @@ -26,9 +26,7 @@ def remove_pset_template(file: ifcopenshell.file, pset_template: ifcopenshell.en along with it. :param pset_template: The IfcPropertySetTemplate to remove. - :type pset_template: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -40,6 +38,4 @@ def remove_pset_template(file: ifcopenshell.file, pset_template: ifcopenshell.en # Let's remove the template. ifcopenshell.api.pset_template.remove_pset_template(model, pset_template=template) """ - settings = {"pset_template": pset_template} - - ifcopenshell.util.element.remove_deep(file, settings["pset_template"]) + ifcopenshell.util.element.remove_deep(file, pset_template) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource.py b/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource.py index 41aa8bec8c..691ad2ec6a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/add_resource.py @@ -51,20 +51,15 @@ def add_resource( :param parent_resource: If this is a child resource (typically to a crew resource), then nominate the parent IfcConstructionResource here. - :type parent_resource: ifcopenshell.entity_instance, optional :param ifc_class: The class of resource chosen from IfcConstructionEquipmentResource, IfcConstructionMaterialResource, IfcConstructionProductResource, IfcCrewResource, IfcLaborResource, or IfcSubContractResource. - :type ifc_class: str,optional :param name: The name of the resource - :type name: str,optional :param predefined_type: Consult the IFC documentation for the valid predefined types for each type of resource class. - :type predefined_type: str,optional :return: The newly created resource depending on the nominated IFC class. - :rtype: ifcopenshell.entity_instance Example: @@ -76,25 +71,17 @@ def add_resource( # Add some labour to our crew. ifcopenshell.api.resource.add_resource(model, parent_resource=crew, ifc_class="IfcLaborResource") """ - settings = { - "parent_resource": parent_resource, - "ifc_class": ifc_class, - "name": name, - "predefined_type": predefined_type, - } resource = ifcopenshell.api.root.create_entity( file, - ifc_class=settings["ifc_class"], - predefined_type=settings["predefined_type"], - name=settings["name"] or "Unnamed", + ifc_class=ifc_class, + predefined_type=predefined_type, + name=name or "Unnamed", ) # TODO: this is an ambiguity by buildingSMART: Can we nest an IfcCrewResource under an IfcCrewResource ? # https://forums.buildingsmart.org/t/what-are-allowed-to-be-root-level-construction-resources/3550 - if settings["parent_resource"]: - ifcopenshell.api.nest.assign_object( - file, related_objects=[resource], relating_object=settings["parent_resource"] - ) + if parent_resource: + ifcopenshell.api.nest.assign_object(file, related_objects=[resource], relating_object=parent_resource) elif file.schema != "IFC2X3": context = file.by_type("IfcContext")[0] ifcopenshell.api.project.assign_declaration(file, definitions=[resource], relating_context=context) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/assign_resource.py b/src/ifcopenshell-python/ifcopenshell/api/resource/assign_resource.py index 626082bce2..618d10895a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/assign_resource.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/assign_resource.py @@ -42,12 +42,9 @@ def assign_resource( (e.g. if the resource is a labour resource). :param relating_resource: The IfcResource to assign the object to. - :type relating_resource: ifcopenshell.entity_instance :param related_object: The IfcProduct or IfcActor to assign to the object. - :type related_object: ifcopenshell.entity_instance :return: The newly created IfcRelAssignsToResource - :rtype: ifcopenshell.entity_instance Example: @@ -82,26 +79,18 @@ def assign_resource( # This means that UCO is now our crane operator. ifcopenshell.api.resource.assign_resource(model, relating_resource=crane, related_object=actor) """ - settings = { - "relating_resource": relating_resource, - "related_object": related_object, - } - - if settings["related_object"].HasAssignments: - for assignment in settings["related_object"].HasAssignments: - if ( - assignment.is_a("IfclRelAssignsToResource") - and assignment.RelatingResource == settings["relating_resource"] - ): + if related_object.HasAssignments: + for assignment in related_object.HasAssignments: + if assignment.is_a("IfclRelAssignsToResource") and assignment.RelatingResource == relating_resource: return assignment resource_of = None - if settings["relating_resource"].ResourceOf: - resource_of = settings["relating_resource"].ResourceOf[0] + if relating_resource.ResourceOf: + resource_of = relating_resource.ResourceOf[0] if resource_of: related_objects = list(resource_of.RelatedObjects) - related_objects.append(settings["related_object"]) + related_objects.append(related_object) resource_of.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, **{"element": resource_of}) else: @@ -110,8 +99,8 @@ def assign_resource( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": [settings["related_object"]], - "RelatingResource": settings["relating_resource"], + "RelatedObjects": [related_object], + "RelatingResource": relating_resource, } ) return resource_of diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py index 0a68a83df4..545016bfd2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/calculate_resource_usage.py @@ -25,15 +25,18 @@ import ifcopenshell.util.resource def calculate_resource_usage(file: ifcopenshell.file, resource: ifcopenshell.entity_instance) -> None: - """Calculates the number of resources required to perform scheduled work on a task.""" - settings = {"resource": resource} + """Calculates the number of resources required to perform scheduled work on a task. - if ifcopenshell.util.constraint.is_attribute_locked(settings["resource"], "Usage.ScheduleUsage"): + :param resource: The IfcConstructionResource to calculate the usage for. + :return: None + """ + + if ifcopenshell.util.constraint.is_attribute_locked(resource, "Usage.ScheduleUsage"): return - if not settings["resource"].Usage or not settings["resource"].Usage.ScheduleWork: + if not resource.Usage or not resource.Usage.ScheduleWork: return - task = ifcopenshell.util.resource.get_task_assignments(settings["resource"]) + task = ifcopenshell.util.resource.get_task_assignments(resource) if not task or not task.TaskTime: return @@ -46,7 +49,7 @@ def calculate_resource_usage(file: ifcopenshell.file, resource: ifcopenshell.ent seconds = task_duration.days * hours_per_day * 60 * 60 seconds += task_duration.seconds - person_hours = ifcopenshell.util.date.ifc2datetime(settings["resource"].Usage.ScheduleWork) + person_hours = ifcopenshell.util.date.ifc2datetime(resource.Usage.ScheduleWork) required_resources = person_hours.total_seconds() / seconds - settings["resource"].Usage.ScheduleUsage = float(required_resources) + resource.Usage.ScheduleUsage = float(required_resources) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource_quantity.py index e7c91e6073..a6b014e1db 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource_quantity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/remove_resource_quantity.py @@ -22,6 +22,9 @@ import ifcopenshell.util.element def remove_resource_quantity(file: ifcopenshell.file, resource: ifcopenshell.entity_instance) -> None: """Removes the base quantity of a resource + :param resource: The IfcConstructionResource to remove the quantity from. + :return: None + Example: .. code:: python @@ -41,9 +44,7 @@ def remove_resource_quantity(file: ifcopenshell.file, resource: ifcopenshell.ent # let's clean up our mess and remove the quantity. ifcopenshell.api.resource.remove_resource_quantity(model, resource=labour) """ - settings = {"resource": resource} - - old_quantity = settings["resource"].BaseQuantity - settings["resource"].BaseQuantity = None + old_quantity = resource.BaseQuantity + resource.BaseQuantity = None if old_quantity: ifcopenshell.util.element.remove_deep(file, old_quantity) diff --git a/src/ifcopenshell-python/ifcopenshell/api/resource/unassign_resource.py b/src/ifcopenshell-python/ifcopenshell/api/resource/unassign_resource.py index 76ea9e6d5b..dc0807754d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/resource/unassign_resource.py +++ b/src/ifcopenshell-python/ifcopenshell/api/resource/unassign_resource.py @@ -29,12 +29,9 @@ def unassign_resource( """Removes the relationship between a resource and object :param relating_resource: The IfcResource to assign the object to. - :type relating_resource: ifcopenshell.entity_instance :param related_object: The IfcProduct or IfcActor to assign to the object. - :type related_object: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -60,13 +57,8 @@ def unassign_resource( ifcopenshell.api.resource.unassign_resource(model, relating_resource=crane, related_object=product) """ - settings = { - "relating_resource": relating_resource, - "related_object": related_object, - } - - for rel in settings["related_object"].HasAssignments or []: - if not rel.is_a("IfcRelAssignsToResource") or rel.RelatingResource != settings["relating_resource"]: + for rel in related_object.HasAssignments or []: + if not rel.is_a("IfcRelAssignsToResource") or rel.RelatingResource != relating_resource: continue if len(rel.RelatedObjects) == 1: history = rel.OwnerHistory @@ -75,6 +67,6 @@ def unassign_resource( ifcopenshell.util.element.remove_deep2(file, history) return related_objects = list(rel.RelatedObjects) - related_objects.remove(settings["related_object"]) + related_objects.remove(related_object) rel.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py b/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py index 67d67ed7f2..ad44cede8d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/root/create_entity.py @@ -71,48 +71,44 @@ def create_entity( """ usecase = Usecase() usecase.file = file - usecase.settings = { - "ifc_class": ifc_class, - "predefined_type": predefined_type, - "name": name, - } - return usecase.execute() + return usecase.execute(ifc_class, predefined_type, name) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] - def execute(self): + def execute( + self, ifc_class: str, predefined_type: Optional[str] = None, name: Optional[str] = None + ) -> ifcopenshell.entity_instance: element = self.file.create_entity( - self.settings["ifc_class"], + ifc_class, **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(self.file), } ) - element.Name = self.settings["name"] or None - if self.settings["predefined_type"]: + element.Name = name or None + if predefined_type: if hasattr(element, "PredefinedType"): try: - element.PredefinedType = self.settings["predefined_type"] + element.PredefinedType = predefined_type except: element.PredefinedType = "USERDEFINED" if hasattr(element, "ObjectType"): - element.ObjectType = self.settings["predefined_type"] + element.ObjectType = predefined_type elif hasattr(element, "ElementType"): - element.ElementType = self.settings["predefined_type"] + element.ElementType = predefined_type elif hasattr(element, "ProcessType"): - element.ProcessType = self.settings["predefined_type"] + element.ProcessType = predefined_type elif hasattr(element, "ObjectType"): - element.ObjectType = self.settings["predefined_type"] + element.ObjectType = predefined_type if self.file.schema == "IFC2X3": self.handle_2x3_defaults(element) else: self.handle_4_defaults(element) return element - def handle_2x3_defaults(self, element): + def handle_2x3_defaults(self, element: ifcopenshell.entity_instance) -> None: if element.is_a("IfcElementType"): if hasattr(element, "PredefinedType") and not element.PredefinedType: element.PredefinedType = "NOTDEFINED" @@ -129,7 +125,7 @@ class Usecase: element.ParameterTakesPrecedence = False element.Sizeable = False - def handle_4_defaults(self, element): + def handle_4_defaults(self, element: ifcopenshell.entity_instance) -> None: if element.is_a("IfcElementType"): if hasattr(element, "PredefinedType") and not element.PredefinedType: element.PredefinedType = "NOTDEFINED" diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py index 1e1b4fe2df..f6fa8feb8e 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_task.py @@ -69,23 +69,16 @@ def add_task( :param work_schedule: The work schedule to group the task in, if the task is to be a top-level or root task. This is mutually exclusive with the parent_task parameter. - :type work_schedule: ifcopenshell.entity_instance, optional :param parent_task: The parent task, if the task is to be a subtask or child task. This is mutually exclusive with the work_schedule parameter. - :type parent_task: ifcopenshell.entity_instance, optioanl :param name: The name of the task. - :type name: str,optional :param description: The description of the task. - :type description: str,optional :param identification: The identification code of the task. - :type identification: str,optional :param predefined_type: The predefined type of the task. Common ones include CONSTRUCTION, DEMOLITION, or MAINTENANCE. Consultant the IFC documentation for IfcTaskTypeEnum for more information. - :type predefined_type: str :return: The newly created IfcTask - :rtype: ifcopenshell.entity_instance Example: @@ -139,39 +132,28 @@ def add_task( ifcopenshell.api.sequence.add_task(model, parent_task=cleaning, identification="3", description="Setup the water pressure by tapping to a water supply and connecting to a ...") """ - settings = { - "work_schedule": work_schedule, - "parent_task": parent_task, - "name": name, - "description": description, - "identification": identification, - "predefined_type": predefined_type, - } - - task = ifcopenshell.api.root.create_entity( - file, ifc_class="IfcTask", name=settings["name"], predefined_type=settings["predefined_type"] - ) - if settings["description"]: - task.Description = settings["description"] - if settings["identification"]: - task.Identification = settings["identification"] + task = ifcopenshell.api.root.create_entity(file, ifc_class="IfcTask", name=name, predefined_type=predefined_type) + if description: + task.Description = description + if identification: + task.Identification = identification task.IsMilestone = False - if settings["work_schedule"]: + if work_schedule: file.create_entity( "IfcRelAssignsToControl", **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), "RelatedObjects": [task], - "RelatingControl": settings["work_schedule"], + "RelatingControl": work_schedule, } ) - elif settings["parent_task"]: + elif parent_task: rel = ifcopenshell.api.nest.assign_object( file, related_objects=[task], - relating_object=settings["parent_task"], + relating_object=parent_task, ) - if file.schema != "IFC2X3" and settings["parent_task"].Identification: - task.Identification = settings["parent_task"].Identification + "." + str(len(rel.RelatedObjects)) + if file.schema != "IFC2X3" and parent_task.Identification: + task.Identification = parent_task.Identification + "." + str(len(rel.RelatedObjects)) return task diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_time_period.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_time_period.py index 28814da438..41e0785b1d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_time_period.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_time_period.py @@ -44,17 +44,13 @@ def add_time_period( :param recurrence_pattern: The IfcRecurrencePattern to add the time period to. See ifcopenshell.api.sequence.assign_recurrence_pattern. - :type recurrence_pattern: ifcopenshell.entity_instance :param start_time: The start time of the time period, in a format compatible with IfcTime, such as an ISO format time string or a datetime.time object. - :type start_time: str,datetime.time :param end_time: The end time of the time period, in a format compatible with IfcTime, such as an ISO format time string or a datetime.time object. - :type end_time: str,datetime.time :return: The newly created IfcTimePeriod - :rtype: ifcopenshell.entity_instance Example: @@ -81,18 +77,12 @@ def add_time_period( ifcopenshell.api.sequence.add_time_period(model, recurrence_pattern=pattern, start_time="13:00", end_time="17:00") """ - settings = { - "recurrence_pattern": recurrence_pattern, - "start_time": start_time, - "end_time": end_time, - } - time_period = file.create_entity("IfcTimePeriod") - time_period.StartTime = ifcopenshell.util.date.datetime2ifc(settings["start_time"], "IfcTime") - time_period.EndTime = ifcopenshell.util.date.datetime2ifc(settings["end_time"], "IfcTime") - time_periods = list(settings["recurrence_pattern"].TimePeriods or []) + time_period.StartTime = ifcopenshell.util.date.datetime2ifc(start_time, "IfcTime") + time_period.EndTime = ifcopenshell.util.date.datetime2ifc(end_time, "IfcTime") + time_periods = list(recurrence_pattern.TimePeriods or []) time_periods.append(time_period) - settings["recurrence_pattern"].TimePeriods = time_periods + recurrence_pattern.TimePeriods = time_periods ifcopenshell.util.sequence.is_working_day.cache_clear() ifcopenshell.util.sequence.is_calendar_applicable.cache_clear() diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_calendar.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_calendar.py index e4af1d88b2..254188dcd8 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_calendar.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_calendar.py @@ -38,12 +38,10 @@ def add_work_calendar( :param name: The name of the calendar. Typically something like "5 Day Working Week" or "24/7". - :type name: str, optional :param predefined_type: The type of calendar, typically used to more specifically define shifts, such as FIRSTSHIFT, SECONDSHIFT, or THIRDSHIFT. Leave as NOTDEFINED for basic calendar usage. :return: The newly created IfcWorkCalendar - :rtype: ifcopenshell.entity_instance Example: @@ -79,13 +77,11 @@ def add_work_calendar( # this calendar by default (though you can override them). ifcopenshell.api.control.assign_control(model, relating_control=calendar, related_object=task) """ - settings = {"name": name, "predefined_type": predefined_type} - work_calendar = ifcopenshell.api.root.create_entity( file, ifc_class="IfcWorkCalendar", - predefined_type=settings["predefined_type"], - name=settings["name"], + predefined_type=predefined_type, + name=name, ) context = file.by_type("IfcContext")[0] ifcopenshell.api.project.assign_declaration( diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_plan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_plan.py index 0bd53cb14f..2cb4f05ec6 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_plan.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_plan.py @@ -41,15 +41,11 @@ def add_work_plan( :param name: The name of the work plan. Recommended to be "Maintenance" or "Construction" for the two main purposes. - :type name: str, optional :param predefined_type: The type of work plan, used for baselining. Leave as "NOTDEFINED" if unsure. - :type predefined_type: str :param start_time: The earliest start time when the schedules grouped within the work plan are relevant. - :type start_time: str,datetime.time :return: The newly created IfcWorkPlan - :rtype: ifcopenshell.entity_instance Example: @@ -62,23 +58,18 @@ def add_work_plan( schedule = ifcopenshell.api.sequence.add_work_schedule(model, name="Construction Schedule A", work_plan=work_plan) """ - settings = { - "name": name, - "predefined_type": predefined_type, - "start_time": start_time or datetime.now(), - } - + start_time = start_time or datetime.now() work_plan = ifcopenshell.api.root.create_entity( file, ifc_class="IfcWorkPlan", - predefined_type=settings["predefined_type"], - name=settings["name"], + predefined_type=predefined_type, + name=name, ) work_plan.CreationDate = ifcopenshell.util.date.datetime2ifc(datetime.now(), "IfcDateTime") user = ifcopenshell.api.owner.settings.get_user(file) if user: work_plan.Creators = [user.ThePerson] - work_plan.StartTime = ifcopenshell.util.date.datetime2ifc(settings["start_time"], "IfcDateTime") + work_plan.StartTime = ifcopenshell.util.date.datetime2ifc(start_time, "IfcDateTime") context = file.by_type("IfcContext")[0] ifcopenshell.api.project.assign_declaration( diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py index 4bdc77b955..d81998021b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_schedule.py @@ -77,19 +77,12 @@ def add_work_schedule( construction = ifcopenshell.api.sequence.add_task(model, work_schedule=schedule, name="Construction", identification="C") """ - settings = { - "name": name, - "predefined_type": predefined_type, - "object_type": object_type, - "start_time": start_time or datetime.now(), - "work_plan": work_plan, - } - + start_time = start_time or datetime.now() work_schedule = ifcopenshell.api.root.create_entity( file, ifc_class="IfcWorkSchedule", - predefined_type=settings["predefined_type"], - name=settings["name"], + predefined_type=predefined_type, + name=name, ) if file.schema == "IFC2X3": work_schedule.CreationDate = createIfcDateAndTime(file, datetime.now()) @@ -99,17 +92,17 @@ def add_work_schedule( if user: work_schedule.Creators = [user.ThePerson] if file.schema == "IFC2X3": - work_schedule.StartTime = createIfcDateAndTime(file, settings["start_time"]) + work_schedule.StartTime = createIfcDateAndTime(file, start_time) else: - work_schedule.StartTime = ifcopenshell.util.date.datetime2ifc(settings["start_time"], "IfcDateTime") - if settings["object_type"]: - work_schedule.ObjectType = settings["object_type"] - if settings["work_plan"]: + work_schedule.StartTime = ifcopenshell.util.date.datetime2ifc(start_time, "IfcDateTime") + if object_type: + work_schedule.ObjectType = object_type + if work_plan: ifcopenshell.api.aggregate.assign_object( file, **{ "products": [work_schedule], - "relating_object": settings["work_plan"], + "relating_object": work_plan, } ) elif file.schema != "IFC2X3": diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_time.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_time.py index af55e01a2e..05b4960e09 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_time.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/add_work_time.py @@ -36,12 +36,9 @@ def add_work_time( :param work_calendar: The IfcWorkCalendar to add the work or holiday time definition to. - :type work_calendar: ifcopenshell.entity_instance :param time_type: Either WorkingTimes or ExceptionTimes, depending on what you want to define. - :type time_type: str :return: The newly created IfcWorkTime - :rtype: ifcopenshell.entity_instance Example: @@ -74,15 +71,13 @@ def add_work_time( ifcopenshell.api.sequence.edit_recurrence_pattern(model, recurrence_pattern=pattern, attributes={"DayComponent": [1], "MonthComponent": [1]}) """ - settings = {"work_calendar": work_calendar, "time_type": time_type} - work_time = file.create_entity("IfcWorkTime") - if settings["time_type"] == "WorkingTimes": - working_times = list(settings["work_calendar"].WorkingTimes or []) + if time_type == "WorkingTimes": + working_times = list(work_calendar.WorkingTimes or []) working_times.append(work_time) - settings["work_calendar"].WorkingTimes = working_times - elif settings["time_type"] == "ExceptionTimes": - exception_times = list(settings["work_calendar"].ExceptionTimes or []) + work_calendar.WorkingTimes = working_times + elif time_type == "ExceptionTimes": + exception_times = list(work_calendar.ExceptionTimes or []) exception_times.append(work_time) - settings["work_calendar"].ExceptionTimes = exception_times + work_calendar.ExceptionTimes = exception_times return work_time diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_process.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_process.py index fac605ac40..238e9eedc2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_process.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_process.py @@ -65,12 +65,9 @@ def assign_process( :param relating_process: The IfcProcess (typically IfcTask) that the input, control, or resource is related to. - :type relating_process: ifcopenshell.entity_instance :param related_object: The IfcProduct (for input), IfcCostItem (for control) or IfcConstructionResource (for resource). - :type related_object: ifcopenshell.entity_instance :return: The newly created IfcRelAssignsToProcess relationship - :rtype: ifcopenshell.entity_instance Example: @@ -91,23 +88,18 @@ def assign_process( # Let's demolish that wall! ifcopenshell.api.sequence.assign_process(model, relating_process=task, related_object=wall) """ - settings = { - "relating_process": relating_process, - "related_object": related_object, - } - - if settings["related_object"].HasAssignments: - for assignment in settings["related_object"].HasAssignments: - if assignment.is_a("IfcRelAssignsToProcess") and assignment.RelatingProcess == settings["relating_process"]: + if related_object.HasAssignments: + for assignment in related_object.HasAssignments: + if assignment.is_a("IfcRelAssignsToProcess") and assignment.RelatingProcess == relating_process: return operates_on = None - if settings["relating_process"].OperatesOn: - operates_on = settings["relating_process"].OperatesOn[0] + if relating_process.OperatesOn: + operates_on = relating_process.OperatesOn[0] if operates_on: related_objects = list(operates_on.RelatedObjects) - related_objects.append(settings["related_object"]) + related_objects.append(related_object) operates_on.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, **{"element": operates_on}) else: @@ -116,8 +108,8 @@ def assign_process( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": [settings["related_object"]], - "RelatingProcess": settings["relating_process"], + "RelatedObjects": [related_object], + "RelatingProcess": relating_process, } ) return operates_on diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py index 65971cdae4..8a657302b5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_product.py @@ -41,12 +41,9 @@ def assign_product( :param relating_product: The IfcProduct that was constructed as a result of the task. - :type relating_product: ifcopenshell.entity_instance :param related_object: The IfcProcess (typically IfcTask) of the construction task. - :type related_object: ifcopenshell.entity_instance :return: The newly created IfcRelAssignsToProduct relationship - :rtype: ifcopenshell.entity_instance Example: @@ -67,23 +64,18 @@ def assign_product( # Let's construct that wall! ifcopenshell.api.sequence.assign_product(model, relating_product=wall, related_object=task) """ - settings = { - "relating_product": relating_product, - "related_object": related_object, - } - - if settings["related_object"].HasAssignments: - for assignment in settings["related_object"].HasAssignments: - if assignment.is_a("IfcRelAssignsToProduct") and assignment.RelatingProduct == settings["relating_product"]: + if related_object.HasAssignments: + for assignment in related_object.HasAssignments: + if assignment.is_a("IfcRelAssignsToProduct") and assignment.RelatingProduct == relating_product: return assignment referenced_by = None - if settings["relating_product"].ReferencedBy: - referenced_by = settings["relating_product"].ReferencedBy[0] + if relating_product.ReferencedBy: + referenced_by = relating_product.ReferencedBy[0] if referenced_by: related_objects = list(referenced_by.RelatedObjects) - related_objects.append(settings["related_object"]) + related_objects.append(related_object) referenced_by.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, **{"element": referenced_by}) else: @@ -92,8 +84,8 @@ def assign_product( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedObjects": [settings["related_object"]], - "RelatingProduct": settings["relating_product"], + "RelatedObjects": [related_object], + "RelatingProduct": relating_product, } ) return referenced_by diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_work_plan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_work_plan.py index 0a32b9d580..4b2fd704f4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_work_plan.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/assign_work_plan.py @@ -19,11 +19,12 @@ import ifcopenshell import ifcopenshell.api.project import ifcopenshell.api.aggregate +from typing import Union def assign_work_plan( file: ifcopenshell.file, work_schedule: ifcopenshell.entity_instance, work_plan: ifcopenshell.entity_instance -) -> ifcopenshell.entity_instance: +) -> Union[ifcopenshell.entity_instance, None]: """Assigns a work schedule to a work plan Typically, work schedules would be assigned to a work plan at creation. @@ -31,11 +32,8 @@ def assign_work_plan( :param work_schedule: The IfcWorkSchedule that will be assigned to the work plan. - :type work_schedule: ifcopenshell.entity_instance :param work_plan: The IfcWorkPlan for the schedule to be assigned to. - :type work_plan: ifcopenshell.entity_instance :return: The IfcRelAggregates relationship - :rtype: ifcopenshell.entity_instance Example: @@ -50,20 +48,16 @@ def assign_work_plan( # ... you can assign the work plan afterwards. ifcopenshell.api.sequence.assign_work_plan(work_schedule=schedule, work_plan=work_plan) """ - settings = {"work_schedule": work_schedule, "work_plan": work_plan} - # TODO: this is an ambiguity by buildingSMART # See https://forums.buildingsmart.org/t/is-the-ifcworkschedule-project-declaration-mutually-exclusive-to-aggregation-within-a-relating-ifcworkplan/3510 ifcopenshell.api.project.unassign_declaration( file, - definitions=[settings["work_schedule"]], + definitions=[work_schedule], relating_context=file.by_type("IfcContext")[0], ) rel_aggregates = ifcopenshell.api.aggregate.assign_object( file, - **{ - "products": [settings["work_schedule"]], - "relating_object": settings["work_plan"], - } + products=[work_schedule], + relating_object=work_plan, ) return rel_aggregates diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py index 7f974bb84e..a70868b084 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/duplicate_task.py @@ -21,6 +21,7 @@ import ifcopenshell.guid import ifcopenshell.api.nest import ifcopenshell.api.owner import ifcopenshell.api.sequence +import ifcopenshell.util.date import ifcopenshell.util.element import ifcopenshell.util.sequence from typing import Union, Any @@ -155,7 +156,9 @@ class Usecase: duration_type=inverse.TimeLag.DurationType, ) - def create_object_reference(self, relating_object, related_object): + def create_object_reference( + self, relating_object: ifcopenshell.entity_instance, related_object: ifcopenshell.entity_instance + ) -> ifcopenshell.entity_instance: referenced_by = None if relating_object.Declares: referenced_by = relating_object.Declares[0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_task.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_task.py index 9af73898ea..107dcada2b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_task.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_task.py @@ -31,9 +31,7 @@ def remove_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> sequences or controls are also removed. :param task: The IfcTask to remove. - :type task: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -56,76 +54,74 @@ def remove_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> # just fix it on site. ifcopenshell.api.sequence.remove_task(model, task=design) """ - settings = {"task": task} - # TODO: do a deep purge ifcopenshell.api.project.unassign_declaration( file, - definitions=[settings["task"]], + definitions=[task], relating_context=file.by_type("IfcContext")[0], ) - if task_time := settings["task"].TaskTime: + if task_time := task.TaskTime: if task_time.is_a("IfcTaskTimeRecurring"): ifcopenshell.api.sequence.unassign_recurrence_pattern(file, task_time.Recurrence) file.remove(task_time) # Handle IfcRelNests. - if rels := settings["task"].IsNestedBy: + if rels := task.IsNestedBy: subtasks = rels[0].RelatedObjects # Use batching for optimization. ifcopenshell.api.nest.unassign_object(file, subtasks) for task_ in subtasks: ifcopenshell.api.sequence.remove_task(file, task=task_) - if settings["task"].Nests: - ifcopenshell.api.nest.unassign_object(file, [settings["task"]]) + if task.Nests: + ifcopenshell.api.nest.unassign_object(file, [task]) - for inverse in file.get_inverse(settings["task"]): + for inverse in file.get_inverse(task): if inverse.is_a("IfcRelSequence"): history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelAssignsToControl"): - if inverse.RelatingControl == settings["task"] or len(inverse.RelatedObjects) == 1: + if inverse.RelatingControl == task or len(inverse.RelatedObjects) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) else: related_objects = list(inverse.RelatedObjects) - related_objects.remove(settings["task"]) + related_objects.remove(task) inverse.RelatedObjects = related_objects elif inverse.is_a("IfcRelDefinesByProperties"): ifcopenshell.api.pset.remove_pset( file, - product=settings["task"], + product=task, pset=inverse.RelatingPropertyDefinition, ) elif inverse.is_a("IfcRelAssignsToProcess"): - if inverse.RelatingProcess == settings["task"] or len(inverse.RelatedObjects) == 1: + if inverse.RelatingProcess == task or len(inverse.RelatedObjects) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) elif inverse.is_a("IfcRelAssignsToProduct"): - if inverse.RelatingProduct == settings["task"] or len(inverse.RelatedObjects) == 1: + if inverse.RelatingProduct == task or len(inverse.RelatedObjects) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) else: related_objects = list(inverse.RelatedObjects) - related_objects.remove(settings["task"]) + related_objects.remove(task) inverse.RelatedObjects = related_objects elif inverse.is_a("IfcRelAssignsToObject"): - if inverse.RelatingObject == settings["task"] or len(inverse.RelatedObjects) == 1: + if inverse.RelatingObject == task or len(inverse.RelatedObjects) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) else: related_objects = list(inverse.RelatedObjects) - related_objects.remove(settings["task"]) + related_objects.remove(task) inverse.RelatedObjects = related_objects elif inverse.is_a("IfcRelAssignsToProcess"): history = inverse.OwnerHistory @@ -133,7 +129,7 @@ def remove_task(file: ifcopenshell.file, task: ifcopenshell.entity_instance) -> if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["task"].OwnerHistory - file.remove(settings["task"]) + history = task.OwnerHistory + file.remove(task) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_time_period.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_time_period.py index 247323c04a..50314f8362 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_time_period.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_time_period.py @@ -23,9 +23,7 @@ def remove_time_period(file: ifcopenshell.file, time_period: ifcopenshell.entity """Removes a time period :param time_period: The IfcTimePeriod to remove. - :type time_period: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -55,6 +53,4 @@ def remove_time_period(file: ifcopenshell.file, time_period: ifcopenshell.entity # Let's take the afternoon off! ifcopenshell.api.sequence.remove_time_period(model, time_period=afternoon) """ - settings = {"time_period": time_period} - - file.remove(settings["time_period"]) + file.remove(time_period) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_calendar.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_calendar.py index 27213630e5..0ff9c5109d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_calendar.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_calendar.py @@ -30,9 +30,7 @@ def remove_work_calendar(file: ifcopenshell.file, work_calendar: ifcopenshell.en calendar. :param work_calendar: The IfcWorkCalendar to remove - :type work_calendar: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -44,32 +42,30 @@ def remove_work_calendar(file: ifcopenshell.file, work_calendar: ifcopenshell.en # And remove it immediately ifcopenshell.api.sequence.remove_work_calendar(model, work_calendar=calendar) """ - settings = {"work_calendar": work_calendar} - # TODO: do a deep purge ifcopenshell.api.project.unassign_declaration( file, - definitions=[settings["work_calendar"]], + definitions=[work_calendar], relating_context=file.by_type("IfcContext")[0], ) - if settings["work_calendar"].Controls: - for rel in settings["work_calendar"].Controls: + if work_calendar.Controls: + for rel in work_calendar.Controls: for related_object in rel.RelatedObjects: ifcopenshell.api.control.unassign_control( file, - relating_control=settings["work_calendar"], + relating_control=work_calendar, related_object=related_object, ) # Currently in API work times are created already attached # to the work calendar, so they are never reused. - for working_time in settings["work_calendar"].WorkingTimes or []: + for working_time in work_calendar.WorkingTimes or []: ifcopenshell.api.sequence.remove_work_time(file, work_time=working_time) - for exception_time in settings["work_calendar"].ExceptionTimes or []: + for exception_time in work_calendar.ExceptionTimes or []: ifcopenshell.api.sequence.remove_work_time(file, work_time=exception_time) - history = settings["work_calendar"].OwnerHistory - file.remove(settings["work_calendar"]) + history = work_calendar.OwnerHistory + file.remove(work_calendar) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py index 4f655fbd0c..1d7fd8ce3a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_plan.py @@ -29,9 +29,7 @@ def remove_work_plan(file: ifcopenshell.file, work_plan: ifcopenshell.entity_ins removed. :param work_plan: The IfcWorkPlan to remove. - :type work_plan: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -43,11 +41,9 @@ def remove_work_plan(file: ifcopenshell.file, work_plan: ifcopenshell.entity_ins # And remove it immediately ifcopenshell.api.sequence.remove_work_plan(model, work_plan=work_plan) """ - settings = {"work_plan": work_plan} - ifcopenshell.api.project.unassign_declaration( file, - definitions=[settings["work_plan"]], + definitions=[work_plan], relating_context=file.by_type("IfcContext")[0], ) @@ -55,7 +51,7 @@ def remove_work_plan(file: ifcopenshell.file, work_plan: ifcopenshell.entity_ins if related_objects: ifcopenshell.api.aggregate.unassign_object(file, related_objects) - history = settings["work_plan"].OwnerHistory - file.remove(settings["work_plan"]) + history = work_plan.OwnerHistory + file.remove(work_plan) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_schedule.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_schedule.py index d14258e6ec..5304ac64ad 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_schedule.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/remove_work_schedule.py @@ -47,32 +47,30 @@ def remove_work_schedule(file: ifcopenshell.file, work_schedule: ifcopenshell.en # And remove it immediately ifcopenshell.api.sequence.remove_work_schedule(model, work_schedule=schedule) """ - settings = {"work_schedule": work_schedule} - # TODO: do a deep purge ifcopenshell.api.project.unassign_declaration( - file, definitions=[settings["work_schedule"]], relating_context=file.by_type("IfcContext")[0] + file, definitions=[work_schedule], relating_context=file.by_type("IfcContext")[0] ) - if settings["work_schedule"].Declares: - for rel in settings["work_schedule"].Declares: - for work_schedule in rel.RelatedObjects: - ifcopenshell.api.sequence.remove_work_schedule(file, work_schedule=work_schedule) + if work_schedule.Declares: + for rel in work_schedule.Declares: + for work_schedule_ in rel.RelatedObjects: + ifcopenshell.api.sequence.remove_work_schedule(file, work_schedule=work_schedule_) # Unassign from work plans. - if settings["work_schedule"].Decomposes: - ifcopenshell.api.aggregate.unassign_object(file, [settings["work_schedule"]]) + if work_schedule.Decomposes: + ifcopenshell.api.aggregate.unassign_object(file, [work_schedule]) - for inverse in file.get_inverse(settings["work_schedule"]): + for inverse in file.get_inverse(work_schedule): if inverse.is_a("IfcRelDefinesByObject"): - if inverse.RelatingObject == settings["work_schedule"] or len(inverse.RelatedObjects) == 1: + if inverse.RelatingObject == work_schedule or len(inverse.RelatedObjects) == 1: history = inverse.OwnerHistory file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) else: related_objects = list(inverse.RelatedObjects) - related_objects.remove(settings["work_schedule"]) + related_objects.remove(work_schedule) inverse.RelatedObjects = related_objects elif inverse.is_a("IfcRelAssignsToControl"): [ @@ -81,7 +79,7 @@ def remove_work_schedule(file: ifcopenshell.file, work_schedule: ifcopenshell.en if related_object.is_a("IfcTask") ] - history = settings["work_schedule"].OwnerHistory - file.remove(settings["work_schedule"]) + history = work_schedule.OwnerHistory + file.remove(work_schedule) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_process.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_process.py index 20e801a76c..1b2be09d20 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_process.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_process.py @@ -31,11 +31,8 @@ def unassign_process( See ifcopenshell.api.sequence.assign_process for details. :param relating_process: The IfcTask in the relationship. - :type relating_process: ifcopenshell.entity_instance :param related_object: The related object. - :type related_object: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -59,13 +56,8 @@ def unassign_process( # Change our mind. ifcopenshell.api.sequence.unassign_process(model, relating_process=task, related_object=wall) """ - settings = { - "relating_process": relating_process, - "related_object": related_object, - } - - for rel in settings["related_object"].HasAssignments or []: - if not rel.is_a("IfcRelAssignsToProcess") or rel.RelatingProcess != settings["relating_process"]: + for rel in related_object.HasAssignments or []: + if not rel.is_a("IfcRelAssignsToProcess") or rel.RelatingProcess != relating_process: continue if len(rel.RelatedObjects) == 1: history = rel.OwnerHistory @@ -74,7 +66,7 @@ def unassign_process( ifcopenshell.util.element.remove_deep2(file, history) return related_objects = list(rel.RelatedObjects) - related_objects.remove(settings["related_object"]) + related_objects.remove(related_object) rel.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, element=rel) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_product.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_product.py index 30f2675aeb..ff59b463fa 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_product.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_product.py @@ -31,11 +31,8 @@ def unassign_product( See ifcopenshell.api.sequence.assign_product for details. :param relating_product: The IfcProduct in the relationship. - :type relating_product: ifcopenshell.entity_instance :param related_object: The IfcTask in the relationship. - :type related_object: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -59,13 +56,8 @@ def unassign_product( # Change our mind. ifcopenshell.api.sequence.unassign_product(relating_product=wall, related_object=task) """ - settings = { - "relating_product": relating_product, - "related_object": related_object, - } - - for rel in settings["related_object"].HasAssignments or []: - if not rel.is_a("IfcRelAssignsToProduct") or rel.RelatingProduct != settings["relating_product"]: + for rel in related_object.HasAssignments or []: + if not rel.is_a("IfcRelAssignsToProduct") or rel.RelatingProduct != relating_product: continue if len(rel.RelatedObjects) == 1: history = rel.OwnerHistory @@ -74,7 +66,7 @@ def unassign_product( ifcopenshell.util.element.remove_deep2(file, history) return related_objects = list(rel.RelatedObjects) - related_objects.remove(settings["related_object"]) + related_objects.remove(related_object) rel.RelatedObjects = related_objects ifcopenshell.api.owner.update_owner_history(file, element=rel) return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_recurrence_pattern.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_recurrence_pattern.py index 2ba9cdb229..0c0dac45ee 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_recurrence_pattern.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_recurrence_pattern.py @@ -28,9 +28,7 @@ def unassign_recurrence_pattern(file: ifcopenshell.file, recurrence_pattern: ifc or replace IfcTaskTimeRecurring with IfcTaskTime). :param recurrence_pattern: The IfcRecurrencePattern to remove. - :type recurrence_pattern: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -50,8 +48,6 @@ def unassign_recurrence_pattern(file: ifcopenshell.file, recurrence_pattern: ifc # Change our mind, let's just maintain it whenever we feel like it. ifcopenshell.api.sequence.unassign_recurrence_pattern(recurrence_pattern=pattern) """ - settings = {"recurrence_pattern": recurrence_pattern} - - for time_period in settings["recurrence_pattern"].TimePeriods or []: + for time_period in recurrence_pattern.TimePeriods or []: file.remove(time_period) - file.remove(settings["recurrence_pattern"]) + file.remove(recurrence_pattern) diff --git a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_sequence.py b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_sequence.py index eea4e3eff5..15ed7d3cf1 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_sequence.py +++ b/src/ifcopenshell-python/ifcopenshell/api/sequence/unassign_sequence.py @@ -29,11 +29,8 @@ def unassign_sequence( """Removes a sequence relationship between tasks :param relating_process: The previous / predecessor task. - :type relating_process: ifcopenshell.entity_instance :param related_process: The next / successor task. - :type related_process: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -60,15 +57,10 @@ def unassign_sequence( ifcopenshell.api.sequence.unassign_sequence(model, relating_process=zone1, related_process=zone2) """ - settings = { - "relating_process": relating_process, - "related_process": related_process, - } - - for rel in settings["related_process"].IsSuccessorFrom or []: - if rel.RelatingProcess == settings["relating_process"]: + for rel in related_process.IsSuccessorFrom or []: + if rel.RelatingProcess == relating_process: history = rel.OwnerHistory file.remove(rel) if history: ifcopenshell.util.element.remove_deep2(file, history) - ifcopenshell.api.sequence.cascade_schedule(file, task=settings["related_process"]) + ifcopenshell.api.sequence.cascade_schedule(file, task=related_process) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py index 81dca88437..244b5ae8f7 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/assign_container.py @@ -69,13 +69,11 @@ def assign_container( previous aggregation, containment, or nesting relationships it may have. :param products: A list of physical IfcElements existing in the space. - :type products: list[ifcopenshell.entity_instance] :param relating_structure: The IfcSpatialStructureElement element, such as IfcBuilding, IfcBuildingStorey, or IfcSpace that the element exists in. :return: The IfcRelContainedInSpatialStructure relationship instance or `None` if `products` was empty list. - :rtype: Union[ifcopenshell.entity_instance, None] Example: @@ -103,16 +101,10 @@ def assign_container( ifcopenshell.api.spatial.assign_container(model, products=[wall], relating_structure=storey) ifcopenshell.api.spatial.assign_container(model, products=[furniture], relating_structure=space) """ - settings = { - "products": products, - "relating_structure": relating_structure, - } - - if not settings["products"]: + if not products: return - products = set(settings["products"]) - relating_structure = settings["relating_structure"] + products_set = set(products) structure_rel = next(iter(relating_structure.ContainsElements), None) previous_containers_rels: set[ifcopenshell.entity_instance] = set() @@ -120,7 +112,7 @@ def assign_container( products_with_containers: list[ifcopenshell.entity_instance] = [] # check if there is anything to change - for product in products: + for product in products_set: product_rel = next(iter(product.ContainedInStructure), None) if product_rel is None: @@ -144,7 +136,7 @@ def assign_container( # unassign elements from previous containers for rel in previous_containers_rels: - related_elements = set(rel.RelatedElements) - products + related_elements = set(rel.RelatedElements) - products_set if related_elements: rel.RelatedElements = list(related_elements) ifcopenshell.api.owner.update_owner_history(file, **{"element": rel}) @@ -156,7 +148,7 @@ def assign_container( # assign elements to a new container if structure_rel: - structure_rel.RelatedElements = list(set(structure_rel.RelatedElements) | products) + structure_rel.RelatedElements = list(set(structure_rel.RelatedElements) | products_set) ifcopenshell.api.owner.update_owner_history(file, **{"element": structure_rel}) else: structure_rel = file.create_entity( @@ -164,8 +156,8 @@ def assign_container( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedElements": list(products), - "RelatingStructure": settings["relating_structure"], + "RelatedElements": list(products_set), + "RelatingStructure": relating_structure, } ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/spatial/unassign_container.py b/src/ifcopenshell-python/ifcopenshell/api/spatial/unassign_container.py index 9567fa4841..cd95dae448 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/spatial/unassign_container.py +++ b/src/ifcopenshell-python/ifcopenshell/api/spatial/unassign_container.py @@ -25,9 +25,7 @@ def unassign_container(file: ifcopenshell.file, products: list[ifcopenshell.enti """Unassigns a container from products. :param product: A list of IfcProducts to remove the containment from. - :type product: list[ifcopenshell.entity_instance] :return: None - :rtype: None Example: @@ -54,15 +52,11 @@ def unassign_container(file: ifcopenshell.file, products: list[ifcopenshell.enti # Not anymore! ifcopenshell.api.spatial.unassign_container(model, products=[wall]) """ - settings = { - "products": products, - } - - products = set(settings["products"]) - rels = set(rel for product in products if (rel := next(iter(product.ContainedInStructure), None))) + products_set = set(products) + rels = set(rel for product in products_set if (rel := next(iter(product.ContainedInStructure), None))) for rel in rels: - related_elements = set(rel.RelatedElements) - products + related_elements = set(rel.RelatedElements) - products_set if related_elements: rel.RelatedElements = list(related_elements) ifcopenshell.api.owner.update_owner_history(file, element=rel) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_activity.py b/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_activity.py index 53d07abcc6..466d26526f 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_activity.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_activity.py @@ -27,7 +27,7 @@ def add_structural_activity( ifc_class: str = "IfcStructuralPlanarAction", predefined_type: str = "CONST", global_or_local: Literal["GLOBAL_COORDS", "LOCAL_COORDS"] = "GLOBAL_COORDS", -) -> None: +) -> ifcopenshell.entity_instance: """Adds a new structural activity A structural activity is either a structural action or a reaction. It @@ -38,42 +38,29 @@ def add_structural_activity( a structural member. :param ifc_class: Choose from any subtype of IfcStructuralActivity. - :type ifc_class: str :param predefined_type: View the IFC documentation for what valid predefined types may be chosen. - :type predefined_type: str :param global_or_local: The location coordinates of the load is always defined locally relative to the structural member the activity is assigned to. However, the directions of the applied load may either be specified globally or locally depending on how this argument is set. Choose from GLOBAL_COORDS or LOCAL_COORDS. - :type global_or_local: str :param applied_load: The IfcStructuralLoad that is applied in this activity. - :type applied_load: ifcopenshell.entity_instance :param structural_member: The IfcStructuralMember that the load is applied to. - :type structural_member: ifcopenshell.entity_instance :return: The newly created entity based on the ifc_class - :rtype: ifcopenshell.entity_instance """ - settings = { - "ifc_class": ifc_class, - "predefined_type": predefined_type, - "global_or_local": global_or_local, - "applied_load": applied_load, - "structural_member": structural_member, - } activity = ifcopenshell.api.root.create_entity( file, - ifc_class=settings["ifc_class"], - predefined_type=settings["predefined_type"], + ifc_class=ifc_class, + predefined_type=predefined_type, ) - activity.AppliedLoad = settings["applied_load"] - activity.GlobalOrLocal = settings["global_or_local"] + activity.AppliedLoad = applied_load + activity.GlobalOrLocal = global_or_local rel = ifcopenshell.api.root.create_entity(file, ifc_class="IfcRelConnectsStructuralActivity") - rel.RelatingElement = settings["structural_member"] + rel.RelatingElement = structural_member rel.RelatedStructuralActivity = activity return activity diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_boundary_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_boundary_condition.py index 36c232ecdd..4e3ac0c1b9 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_boundary_condition.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_boundary_condition.py @@ -32,18 +32,14 @@ def add_structural_boundary_condition( edge condition, and surface connections will have a face condition. :param name: The name of the boundary condition. - :type name: str,optional :param connection: The IfcStructuralConnection to apply the boundary condition to. This will determine the type of condition that is created. If no connection is supplied, an orphan boundary condition will be created using the ifc_class that you specify. - :type connection: ifcopenshell.entity_instance,optional :param ifc_class: The class of IfcBoundaryCondition to create, only relevant if you do not specify a connection and want to create an orphaned boundary condition. - :type ifc_class: str,optional :return: The newly created IfcBoundaryCondition - :rtype: ifcopenshell.entity_instance Example: @@ -51,14 +47,12 @@ def add_structural_boundary_condition( ifcopenshell.api.structural.add_structural_boundary_condition(model, connection=connection) """ - settings = {"name": name, "connection": connection, "ifc_class": ifc_class} - - if settings["connection"]: + if connection: # assign boundary condition to a connection - if settings["connection"].is_a("IfcRelConnectsStructuralMember"): - related_connection = settings["connection"].RelatedStructuralConnection + if connection.is_a("IfcRelConnectsStructuralMember"): + related_connection = connection.RelatedStructuralConnection else: - related_connection = settings["connection"] + related_connection = connection if related_connection.is_a("IfcStructuralPointConnection"): boundary_class = "IfcBoundaryNodeCondition" @@ -67,9 +61,9 @@ def add_structural_boundary_condition( elif related_connection.is_a("IfcStructuralSurfaceConnection"): boundary_class = "IfcBoundaryFaceCondition" - condition = file.create_entity(boundary_class, Name=settings["name"]) - settings["connection"].AppliedCondition = condition + condition = file.create_entity(boundary_class, Name=name) + connection.AppliedCondition = condition return condition else: # add an orphan boundary condition - return file.create_entity(settings["ifc_class"], Name=settings["name"]) + return file.create_entity(ifc_class, Name=name) diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_member_connection.py b/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_member_connection.py index 077e0a55fc..56ad73dca3 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_member_connection.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/add_structural_member_connection.py @@ -29,22 +29,15 @@ def add_structural_member_connection( :param relating_structural_member: The IfcStructuralMember to have a connection added to it. - :type relating_structural_member: ifcopenshell.entity_instance :param related_structural_connection: The IfcStructuralConnection to add to the IfcStructuralMember. - :type related_structural_connection: ifcopenshell.entity_instance :return: The IfcRelConnectsStructuralMember relationship - :rtype: ifcopenshell.entity_instance """ - settings = { - "relating_structural_member": relating_structural_member, - "related_structural_connection": related_structural_connection, - } - for connection in settings["related_structural_connection"].ConnectsStructuralMembers or []: - if connection.RelatingStructuralMember == settings["relating_structural_member"]: + for connection in related_structural_connection.ConnectsStructuralMembers or []: + if connection.RelatingStructuralMember == relating_structural_member: return connection rel = ifcopenshell.api.root.create_entity(file, ifc_class="IfcRelConnectsStructuralMember") - rel.RelatingStructuralMember = settings["relating_structural_member"] - rel.RelatedStructuralConnection = settings["related_structural_connection"] + rel.RelatingStructuralMember = relating_structural_member + rel.RelatedStructuralConnection = related_structural_connection return rel diff --git a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_connection_condition.py b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_connection_condition.py index c16a441753..eea472ef73 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_connection_condition.py +++ b/src/ifcopenshell-python/ifcopenshell/api/structural/remove_structural_connection_condition.py @@ -27,18 +27,14 @@ def remove_structural_connection_condition(file: ifcopenshell.file, relation: if The condition and the member itself is preserved. :param relation: The IfcRelConnectsStructuralMember to remove. - :type relation: ifcopenshell.entity_instance :return: None - :rtype: None """ - settings = {"relation": relation} - - if settings["relation"].AppliedCondition: + if relation.AppliedCondition: ifcopenshell.api.structural.remove_structural_boundary_condition( file, - connection=settings["relation"].RelatedStructuralConnection, + connection=relation.RelatedStructuralConnection, ) - history = settings["relation"].OwnerHistory - file.remove(settings["relation"]) + history = relation.OwnerHistory + file.remove(relation) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/style/remove_styled_representation.py b/src/ifcopenshell-python/ifcopenshell/api/style/remove_styled_representation.py index bc6267f0f7..d8441cd43a 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/style/remove_styled_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/style/remove_styled_representation.py @@ -25,9 +25,7 @@ def remove_styled_representation(file: ifcopenshell.file, representation: ifcope removes the representation but not the underlying styles. :param representation: The IfcStyledRepresentation to remove. - :type representation: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -36,17 +34,15 @@ def remove_styled_representation(file: ifcopenshell.file, representation: ifcope # Remove a styled representation ifcopenshell.api.style.remove_styled_representation(model, representation=representation) """ - settings = {"representation": representation} - - for inverse in file.get_inverse(settings["representation"]): + for inverse in file.get_inverse(representation): if inverse.is_a("IfcMaterialDefinitionRepresentation") and len(inverse.Representations) == 1: file.remove(inverse) - for item in settings["representation"].Items: + for item in representation.Items: if item.is_a("IfcStyledItem") and file.get_total_inverses(item) == 1: for style in item.Styles: if style.is_a("IfcPresentationStyleAssignment"): file.remove(style) file.remove(item) - file.remove(settings["representation"]) + file.remove(representation) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/add_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/add_port.py index faa7c7a584..5c1ba67bda 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/add_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/add_port.py @@ -22,7 +22,9 @@ import ifcopenshell.api.system from typing import Optional -def add_port(file: ifcopenshell.file, element: Optional[ifcopenshell.entity_instance] = None) -> None: +def add_port( + file: ifcopenshell.file, element: Optional[ifcopenshell.entity_instance] = None +) -> ifcopenshell.entity_instance: """Adds a new distribution port to an element A distribution port represents a connection point on an element, where @@ -36,9 +38,7 @@ def add_port(file: ifcopenshell.file, element: Optional[ifcopenshell.entity_inst :param element: The IfcDistributionElement you want to add a distribution port to. - :type element: ifcopenshell.entity_instance, optional :return: The newly created IfcDistributionPort - :rtype: ifcopenshell.entity_instance Example: @@ -52,11 +52,7 @@ def add_port(file: ifcopenshell.file, element: Optional[ifcopenshell.entity_inst port1 = ifcopenshell.api.system.add_port(model, element=duct) port2 = ifcopenshell.api.system.add_port(model, element=duct) """ - settings = { - "element": element, - } - port = ifcopenshell.api.root.create_entity(file, ifc_class="IfcDistributionPort") - if settings["element"]: - ifcopenshell.api.system.assign_port(file, element=settings["element"], port=port) + if element: + ifcopenshell.api.system.assign_port(file, element=element, port=port) return port diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py index a9272ee989..dbd1f81e3c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/add_system.py @@ -34,9 +34,7 @@ def add_system(file: ifcopenshell.file, ifc_class: str = "IfcDistributionSystem" security systems. Alternatively you may choose IfcBuildingSystem for specialised building facade systems or similar. For IFC2X3, choose IfcSystem. - :type ifc_class: str :return: The newly created IfcSystem. - :rtype: ifcopenshell.entity_instance Example: @@ -45,9 +43,7 @@ def add_system(file: ifcopenshell.file, ifc_class: str = "IfcDistributionSystem" # A completely empty distribution system system = ifcopenshell.api.system.add_system(model) """ - settings = {"ifc_class": ifc_class} - - ifc_class = settings["ifc_class"] + ifc_class = ifc_class # workaround for failing default argument in ifc2x3 if file.schema == "IFC2X3" and ifc_class == "IfcDistributionSystem": ifc_class = "IfcSystem" diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/assign_flow_control.py b/src/ifcopenshell-python/ifcopenshell/api/system/assign_flow_control.py index b41db08c79..011a510705 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/assign_flow_control.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/assign_flow_control.py @@ -34,12 +34,9 @@ def assign_flow_control( :param related_flow_control: IfcDistributionControlElement which may be used to impart control on the flow element - :type related_flow_control: ifcopenshell.entity_instance :param relating_flow_element: The IfcDistributionFlowElement that is being controlled / sensed - :type relating_flow_element: ifcopenshell.entity_instance :return: Matching or newly created IfcRelFlowControlElements. If control is already assigned to some other element method will return None. - :rtype: ifcopenshell.entity_instance, None Example: @@ -51,26 +48,21 @@ def assign_flow_control( model, related_flow_control=flow_control, relating_flow_element=flow_element ) """ - settings = { - "relating_flow_element": relating_flow_element, - "related_flow_control": related_flow_control, - } - - if settings["related_flow_control"].AssignedToFlowElement: + if related_flow_control.AssignedToFlowElement: # only 1 control per 1 flow element is possible - assignment = settings["related_flow_control"].AssignedToFlowElement[0] - if assignment.RelatingFlowElement == settings["relating_flow_element"]: + assignment = related_flow_control.AssignedToFlowElement[0] + if assignment.RelatingFlowElement == relating_flow_element: return assignment # return None if this control is already assigned to another flow element return - if settings["relating_flow_element"].HasControlElements: - assignment = settings["relating_flow_element"].HasControlElements[0] - if settings["related_flow_control"] in assignment.RelatedControlElements: + if relating_flow_element.HasControlElements: + assignment = relating_flow_element.HasControlElements[0] + if related_flow_control in assignment.RelatedControlElements: return assignment related_flow_controls = set(assignment.RelatedControlElements) - related_flow_controls.add(settings["related_flow_control"]) + related_flow_controls.add(related_flow_control) assignment.RelatedControlElements = list(related_flow_controls) ifcopenshell.api.owner.update_owner_history(file, **{"element": assignment}) return assignment @@ -80,8 +72,8 @@ def assign_flow_control( **{ "GlobalId": ifcopenshell.guid.new(), "OwnerHistory": ifcopenshell.api.owner.create_owner_history(file), - "RelatedControlElements": [settings["related_flow_control"]], - "RelatingFlowElement": settings["relating_flow_element"], + "RelatedControlElements": [related_flow_control], + "RelatingFlowElement": relating_flow_element, }, ) return assignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/assign_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/assign_port.py index 8506e6b53d..75415fd98b 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/assign_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/assign_port.py @@ -34,12 +34,9 @@ def assign_port( it may be useful when patching up models. :param element: The IfcDistributionElement to assign the port to. - :type element: ifcopenshell.entity_instance :param port: The IfcDistributionPort you want to assign. - :type port: ifcopenshell.entity_instance :return: The IfcRelNests relationship, or the IfcRelConnectsPortToElement for IFC2X3. - :rtype: ifcopenshell.entity_instance Example: @@ -61,31 +58,30 @@ def assign_port( """ usecase = Usecase() usecase.file = file - usecase.settings = { - "element": element, - "port": port, - } - return usecase.execute() + return usecase.execute(element, port) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] - def execute(self): + def execute( + self, element: ifcopenshell.entity_instance, port: ifcopenshell.entity_instance + ) -> ifcopenshell.entity_instance: + self.element = element + self.port = port if self.file.schema == "IFC2X3": return self.execute_ifc2x3() - rels = self.settings["element"].IsNestedBy or [] + rels = self.element.IsNestedBy or [] for rel in rels: - if self.settings["port"] in rel.RelatedObjects: + if self.port in rel.RelatedObjects: return rel if rels: rel = rels[0] related_objects = set(rel.RelatedObjects) or set() - related_objects.add(self.settings["port"]) + related_objects.add(self.port) rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(self.file, **{"element": rel}) else: @@ -93,34 +89,34 @@ class Usecase: "IfcRelNests", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.owner.create_owner_history(self.file), - RelatedObjects=[self.settings["port"]], - RelatingObject=self.settings["element"], + RelatedObjects=[self.port], + RelatingObject=self.element, ) self.update_port_placement() return rel - def execute_ifc2x3(self): - for rel in self.settings["element"].HasPorts or []: - if rel.RelatingPort == self.settings["port"]: + def execute_ifc2x3(self) -> ifcopenshell.entity_instance: + for rel in self.element.HasPorts or []: + if rel.RelatingPort == self.port: return rel rel = self.file.create_entity( "IfcRelConnectsPortToElement", GlobalId=ifcopenshell.guid.new(), OwnerHistory=ifcopenshell.api.owner.create_owner_history(self.file), - RelatingPort=self.settings["port"], - RelatedElement=self.settings["element"], + RelatingPort=self.port, + RelatedElement=self.element, ) self.update_port_placement() return rel - def update_port_placement(self): - placement = getattr(self.settings["port"], "ObjectPlacement", None) + def update_port_placement(self) -> None: + placement = getattr(self.port, "ObjectPlacement", None) if placement and placement.is_a("IfcLocalPlacement"): ifcopenshell.api.geometry.edit_object_placement( self.file, - product=self.settings["port"], - matrix=ifcopenshell.util.placement.get_local_placement(self.settings["port"].ObjectPlacement), + product=self.port, + matrix=ifcopenshell.util.placement.get_local_placement(self.port.ObjectPlacement), is_si=False, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py index cd6cd38fb1..3bc1d6d2b5 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/disconnect_port.py @@ -64,12 +64,8 @@ def disconnect_port(file: ifcopenshell.file, port: ifcopenshell.entity_instance) # fitting_port1 instead of duct_port2 ifcopenshell.api.system.disconnect_port(model, port=duct_port2) """ - settings = { - "port": port, - } - - rels = settings["port"].ConnectedTo or () - rels += settings["port"].ConnectedFrom or () + rels = port.ConnectedTo or () + rels += port.ConnectedFrom or () for rel in rels: rel.RelatingPort.FlowDirection = None diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/remove_system.py b/src/ifcopenshell-python/ifcopenshell/api/system/remove_system.py index cef253cded..6cd39cc298 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/remove_system.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/remove_system.py @@ -27,9 +27,7 @@ def remove_system(file: ifcopenshell.file, system: ifcopenshell.entity_instance) All the distribution elements within the system are retained. :param system: The IfcSystem to remove. - :type system: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -41,9 +39,7 @@ def remove_system(file: ifcopenshell.file, system: ifcopenshell.entity_instance) # Delete it. ifcopenshell.api.system.remove_system(model, system=system) """ - settings = {"system": system} - - for inverse_id in [i.id() for i in file.get_inverse(settings["system"])]: + for inverse_id in [i.id() for i in file.get_inverse(system)]: try: inverse = file.by_id(inverse_id) except: @@ -51,11 +47,11 @@ def remove_system(file: ifcopenshell.file, system: ifcopenshell.entity_instance) if inverse.is_a("IfcRelDefinesByProperties"): ifcopenshell.api.pset.remove_pset( file, - product=settings["system"], + product=system, pset=inverse.RelatingPropertyDefinition, ) elif inverse.is_a("IfcRelAssignsToGroup"): - if inverse.RelatingGroup == settings["system"]: + if inverse.RelatingGroup == system: history = inverse.OwnerHistory file.remove(inverse) if history: @@ -65,7 +61,7 @@ def remove_system(file: ifcopenshell.file, system: ifcopenshell.entity_instance) file.remove(inverse) if history: ifcopenshell.util.element.remove_deep2(file, history) - history = settings["system"].OwnerHistory - file.remove(settings["system"]) + history = system.OwnerHistory + file.remove(system) if history: ifcopenshell.util.element.remove_deep2(file, history) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_flow_control.py b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_flow_control.py index 4aeefe5a47..69b9ddfc62 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_flow_control.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_flow_control.py @@ -30,11 +30,8 @@ def unassign_flow_control( :param related_flow_control: IfcDistributionControlElement controling the flow element - :type related_flow_control: ifcopenshell.entity_instance :param relating_flow_element: The IfcDistributionFlowElement that is being controlled - :type relating_flow_element: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -53,15 +50,10 @@ def unassign_flow_control( ) """ - settings = { - "relating_flow_element": relating_flow_element, - "related_flow_control": related_flow_control, - } - - if not settings["related_flow_control"].AssignedToFlowElement: + if not related_flow_control.AssignedToFlowElement: return - assignment = settings["related_flow_control"].AssignedToFlowElement[0] - if assignment.RelatingFlowElement != settings["relating_flow_element"]: + assignment = related_flow_control.AssignedToFlowElement[0] + if assignment.RelatingFlowElement != relating_flow_element: return if len(assignment.RelatedControlElements) == 1: history = assignment.OwnerHistory @@ -70,6 +62,6 @@ def unassign_flow_control( ifcopenshell.util.element.remove_deep2(file, history) return related_flow_controls = list(assignment.RelatedControlElements) - related_flow_controls.remove(settings["related_flow_control"]) + related_flow_controls.remove(related_flow_control) assignment.RelatedControlElements = related_flow_controls ifcopenshell.api.owner.update_owner_history(file, **{"element": assignment}) diff --git a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py index b5b69e276c..87b6c0fc16 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py +++ b/src/ifcopenshell-python/ifcopenshell/api/system/unassign_port.py @@ -19,7 +19,6 @@ import ifcopenshell import ifcopenshell.api.owner import ifcopenshell.util.element -from typing import Any def unassign_port( @@ -32,11 +31,8 @@ def unassign_port( port for cleaning or patchin purposes. :param element: The IfcDistributionElement to unassign the port from. - :type element: ifcopenshell.entity_instance :param port: The IfcDistributionPort you want to unassign. - :type port: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -55,23 +51,20 @@ def unassign_port( """ usecase = Usecase() usecase.file = file - usecase.settings = { - "element": element, - "port": port, - } - return usecase.execute() + return usecase.execute(element, port) class Usecase: file: ifcopenshell.file - settings: dict[str, Any] - def execute(self): + def execute(self, element: ifcopenshell.entity_instance, port: ifcopenshell.entity_instance) -> None: if self.file.schema == "IFC2X3": + self.element = element + self.port = port return self.execute_ifc2x3() - for rel in self.settings["element"].IsNestedBy or []: - if self.settings["port"] in rel.RelatedObjects: + for rel in element.IsNestedBy or []: + if port in rel.RelatedObjects: if len(rel.RelatedObjects) == 1: history = rel.OwnerHistory self.file.remove(rel) @@ -79,13 +72,13 @@ class Usecase: ifcopenshell.util.element.remove_deep2(self.file, history) return related_objects = set(rel.RelatedObjects) or set() - related_objects.remove(self.settings["port"]) + related_objects.remove(port) rel.RelatedObjects = list(related_objects) ifcopenshell.api.owner.update_owner_history(self.file, **{"element": rel}) - def execute_ifc2x3(self): - for rel in self.settings["element"].HasPorts or []: - if rel.RelatingPort == self.settings["port"]: + def execute_ifc2x3(self) -> None: + for rel in self.element.HasPorts or []: + if rel.RelatingPort == self.port: history = rel.OwnerHistory self.file.remove(rel) if history: diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/map_type_representations.py b/src/ifcopenshell-python/ifcopenshell/api/type/map_type_representations.py index 995408b063..0c4c66ae16 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/map_type_representations.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/map_type_representations.py @@ -33,11 +33,8 @@ def map_type_representations( be used to ensure consistency of the occurrence's representations. :param related_object: The IfcElement occurrence. - :type related_object: ifcopenshell.entity_instance :param relating_type: The IfcElementType type. - :type relating_type: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -84,28 +81,23 @@ def map_type_representations( # ifcopenshell.api.type.map_type_representations(model, # related_object=furniture, relating_type=furniture_type) """ - settings = { - "related_object": related_object, - "relating_type": relating_type, - } - - if not settings["relating_type"].RepresentationMaps: + if not relating_type.RepresentationMaps: return representations = [] - if settings["related_object"].Representation: - representations = settings["related_object"].Representation.Representations + if related_object.Representation: + representations = related_object.Representation.Representations for representation in representations: ifcopenshell.api.geometry.unassign_representation( file, - product=settings["related_object"], + product=related_object, representation=representation, ) - ifcopenshell.api.geometry.remove_representation(file, **{"representation": representation}) - for representation_map in settings["relating_type"].RepresentationMaps: + ifcopenshell.api.geometry.remove_representation(file, representation=representation) + for representation_map in relating_type.RepresentationMaps: representation = representation_map.MappedRepresentation mapped_representation = ifcopenshell.api.geometry.map_representation(file, representation=representation) ifcopenshell.api.geometry.assign_representation( file, - product=settings["related_object"], + product=related_object, representation=mapped_representation, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_context_dependent_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_context_dependent_unit.py index 135c805f2f..f9c76f9072 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_context_dependent_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_context_dependent_unit.py @@ -35,9 +35,7 @@ def add_context_dependent_unit( sensible normal unit for. In that case, firstly stop whatever you're doing and have a hard think about your life, and then if life really is going that badly for you, check out the IFC docs for IfcUnitEnum. - :type unit_type: str :param name: Give your unit a name. X what? X bananas? - :type name: str :param dimensions: Units typically measure one of 7 fundamental physical dimensions: length, mass, time, electric current, temperature, substance amount, or luminous intensity. These are represented as a @@ -46,9 +44,7 @@ def add_context_dependent_unit( where as an area unit is (2, 0, 0, 0, 0, 0, 0). A unit of meters per second is (1, 0, -1, 0, 0, 0, 0). For context dependent units, it is recommended to leave this as the default of (0, 0, 0, 0, 0, 0, 0). - :type dimensions: list[int] :return: The new IfcContextDependentUnit - :rtype: ifcopenshell.entity_instance Example: @@ -57,11 +53,9 @@ def add_context_dependent_unit( # Boxes of things ifcopenshell.api.unit.add_context_dependent_unit(model, name="BOXES") """ - settings = {"unit_type": unit_type, "name": name, "dimensions": dimensions} - return file.create_entity( "IfcContextDependentUnit", - Dimensions=file.createIfcDimensionalExponents(*settings["dimensions"]), - UnitType=settings["unit_type"], - Name=settings["name"], + Dimensions=file.createIfcDimensionalExponents(*dimensions), + UnitType=unit_type, + Name=name, ) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py index daa91145ee..bb1e920257 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_conversion_based_unit.py @@ -36,17 +36,14 @@ def add_conversion_based_unit( kip, psi, ksi, minute, hour, day, btu, and fahrenheit. :param name: A converted name chosen from the list above. - :type name: str :param conversion_offset: If you want to offset the conversion further by a set number, you may specify it here. For example, fahrenheit is 1.8 * kelvin - 459.67. The -459.67 is the conversion offset. Note that this is just an example and you don't actually need to specify that for fahrenheit as it's built into this API function. For advanced users only. - :type conversion_offset: float, optional :return: The new IfcConversionBasedUnit or IfcConversionBasedUnitWithOffset - :rtype: ifcopenshell.entity_instance Example: @@ -59,28 +56,26 @@ def add_conversion_based_unit( # Make it our default units, if we are doing an imperial building ifcopenshell.api.unit.assign_unit(model, units=[length, area]) """ - settings = {"name": name, "conversion_offset": conversion_offset} - unit_type = ifcopenshell.util.unit.imperial_types.get(settings["name"], "USERDEFINED") + unit_type = ifcopenshell.util.unit.imperial_types.get(name, "USERDEFINED") dimensions = ifcopenshell.util.unit.named_dimensions[unit_type] exponents = file.createIfcDimensionalExponents(*dimensions) si_name = ifcopenshell.util.unit.si_type_names[unit_type] si_unit = file.createIfcSIUnit(UnitType=unit_type, Name=si_name) - conversion_real = ifcopenshell.util.unit.si_conversions.get(settings["name"], 1) + conversion_real = ifcopenshell.util.unit.si_conversions.get(name, 1) value_component = file.create_entity("IfcReal", **{"wrappedValue": conversion_real}) conversion_factor = file.createIfcMeasureWithUnit(value_component, si_unit) - conversion_offset = settings["conversion_offset"] if not conversion_offset: - conversion_offset = ifcopenshell.util.unit.si_offsets.get(settings["name"], 0) + conversion_offset = ifcopenshell.util.unit.si_offsets.get(name, 0) if conversion_offset: return file.createIfcConversionBasedUnitWithOffset( exponents, unit_type, - settings["name"], + name, conversion_factor, conversion_offset, ) - return file.createIfcConversionBasedUnit(exponents, unit_type, settings["name"], conversion_factor) + return file.createIfcConversionBasedUnit(exponents, unit_type, name, conversion_factor) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_monetary_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_monetary_unit.py index 8d4a3130f0..6c5a90e12d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_monetary_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_monetary_unit.py @@ -26,9 +26,7 @@ def add_monetary_unit(file: ifcopenshell.file, currency: str = "DOLLARYDOO") -> USD, GBP, AUD, MYR, etc. :param currency: The currency code - :type currency: str :return: The newly created IfcMonetaryUnit - :rtype: ifcopenshell.entity_instance Example: @@ -41,6 +39,4 @@ def add_monetary_unit(file: ifcopenshell.file, currency: str = "DOLLARYDOO") -> # Make it our default currency ifcopenshell.api.unit.assign_unit(model, units=[zwl]) """ - settings = {"currency": currency} - - return file.create_entity("IfcMonetaryUnit", settings["currency"]) + return file.create_entity("IfcMonetaryUnit", currency) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/add_si_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/add_si_unit.py index 56cab159ab..50da1c10f2 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/add_si_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/add_si_unit.py @@ -40,12 +40,9 @@ def add_si_unit( :param unit_type: A type of unit chosen from the list above. For example, choosing LENGTHUNIT will give you a metre. - :type unit_type: str :param prefix: A prefix chosen from the list above, or None for no prefix. - :type prefix: str,optional :return: The newly created IfcSIUnit - :rtype: ifcopenshell.entity_instance Example: @@ -58,7 +55,5 @@ def add_si_unit( # Make it our default units, if we are doing a metric building ifcopenshell.api.unit.assign_unit(model, units=[length, area]) """ - settings = {"unit_type": unit_type, "prefix": prefix} - - name = ifcopenshell.util.unit.si_type_names.get(settings["unit_type"], None) - return file.create_entity("IfcSIUnit", UnitType=settings["unit_type"], Name=name, Prefix=settings["prefix"]) + name = ifcopenshell.util.unit.si_type_names.get(unit_type, None) + return file.create_entity("IfcSIUnit", UnitType=unit_type, Name=name, Prefix=prefix) diff --git a/src/ifcopenshell-python/ifcopenshell/api/unit/remove_unit.py b/src/ifcopenshell-python/ifcopenshell/api/unit/remove_unit.py index 88cffbc4a0..a9920e0e5c 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/unit/remove_unit.py +++ b/src/ifcopenshell-python/ifcopenshell/api/unit/remove_unit.py @@ -27,9 +27,7 @@ def remove_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance) -> defined quantities in the model completely lose their meaning. :param unit: The unit element to remove - :type unit: ifcopenshell.entity_instance :return: None - :rtype: None Example: @@ -41,14 +39,12 @@ def remove_unit(file: ifcopenshell.file, unit: ifcopenshell.entity_instance) -> # Yeah maybe not. ifcopenshell.api.unit.remove_unit(model, unit=unit) """ - settings = {"unit": unit} - unit_assignment = ifcopenshell.util.unit.get_unit_assignment(file) - if unit_assignment and settings["unit"] in unit_assignment.Units: + if unit_assignment and unit in unit_assignment.Units: units = list(unit_assignment.Units) - units.remove(settings["unit"]) + units.remove(unit) if units: unit_assignment.Units = units else: file.remove(unit_assignment) - ifcopenshell.util.element.remove_deep(file, settings["unit"]) + ifcopenshell.util.element.remove_deep(file, unit) diff --git a/src/ifcopenshell-python/ifcopenshell/util/cost.py b/src/ifcopenshell-python/ifcopenshell/util/cost.py index b63976a4ed..5d9189b337 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/cost.py +++ b/src/ifcopenshell-python/ifcopenshell/util/cost.py @@ -280,7 +280,7 @@ def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, s return results -def get_cost_schedule_types(file): +def get_cost_schedule_types(file: ifcopenshell.file) -> list[dict[str, str]]: schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(file.schema) results = [] declaration = schema.declaration_by_name("IfcCostSchedule") From e038f968f2f5fbd797465eb92fa70226b1c188c2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 10:50:29 +0500 Subject: [PATCH 356/476] bim.calculate_single_quantity - add info message --- src/bonsai/bonsai/bim/module/qto/operator.py | 16 ++++++---------- src/bonsai/bonsai/tool/qto.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/qto/operator.py b/src/bonsai/bonsai/bim/module/qto/operator.py index 0f4c706dbd..09f29ea1af 100644 --- a/src/bonsai/bonsai/bim/module/qto/operator.py +++ b/src/bonsai/bonsai/bim/module/qto/operator.py @@ -127,7 +127,7 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator): import ifc5d.qto props = tool.Qto.get_qto_props() - elements = set() + elements: set[ifcopenshell.entity_instance] = set() for obj in tool.Blender.get_selected_objects(include_active=False): element = tool.Ifc.get_entity(obj) if element: @@ -144,6 +144,10 @@ class CalculateSingleQuantity(bpy.types.Operator, tool.Ifc.Operator): ifc_file = tool.Ifc.get() results = ifc5d.qto.quantify(ifc_file, elements, rules) ifc5d.qto.edit_qtos(ifc_file, results) + + not_quantified_elements = elements - set(results.keys()) + not_quantified_message = tool.Qto.get_not_quantified_elements_message(not_quantified_elements) + self.report({"INFO"}, f"Quantity was calculated for {len(elements)} elements.{not_quantified_message}") return {"FINISHED"} @@ -190,14 +194,6 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator): alternative_rules = next(rule for rule in ifc5d.qto.rules if rule != props.qto_rule) not_quantified_elements = run_quantification(alternative_rules, not_quantified_elements) - not_quantified_message = "" - if not_quantified_elements: - print("Elements that were not quantified:") - for element in not_quantified_elements: - print(f"- {element}") - not_quantified_message = ( - f" {len(not_quantified_elements)} of them were not quantified, see system console for the details." - ) - + not_quantified_message = tool.Qto.get_not_quantified_elements_message(not_quantified_elements) self.report({"INFO"}, f"Quantities are calculated for {len(elements)} elements.{not_quantified_message}") return {"FINISHED"} diff --git a/src/bonsai/bonsai/tool/qto.py b/src/bonsai/bonsai/tool/qto.py index 4a17be291b..60985b5cb3 100644 --- a/src/bonsai/bonsai/tool/qto.py +++ b/src/bonsai/bonsai/tool/qto.py @@ -167,3 +167,15 @@ class Qto(bonsai.core.tool.Qto): } ) return result + + @classmethod + def get_not_quantified_elements_message(cls, not_quantified_elements: set[ifcopenshell.entity_instance]) -> str: + not_quantified_message = "" + if not_quantified_elements: + print("Elements that were not quantified:") + for element in not_quantified_elements: + print(f"- {element}") + not_quantified_message = ( + f" {len(not_quantified_elements)} of them were not quantified, see system console for the details." + ) + return not_quantified_message From 8ca344259f6956b3f640bbd078a2019f457a5f42 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 10:56:23 +0500 Subject: [PATCH 357/476] bim.perform_quantity_take_off - support fallback calculator in ifc4x3 too --- src/bonsai/bonsai/bim/module/qto/operator.py | 2 +- src/bonsai/bonsai/bim/module/qto/prop.py | 6 +----- src/bonsai/bonsai/tool/qto.py | 10 +++++++++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/qto/operator.py b/src/bonsai/bonsai/bim/module/qto/operator.py index 09f29ea1af..1d57e51227 100644 --- a/src/bonsai/bonsai/bim/module/qto/operator.py +++ b/src/bonsai/bonsai/bim/module/qto/operator.py @@ -191,7 +191,7 @@ class PerformQuantityTakeOff(bpy.types.Operator, tool.Ifc.Operator): not_quantified_elements = run_quantification(props.qto_rule, elements) if props.fallback and not_quantified_elements: - alternative_rules = next(rule for rule in ifc5d.qto.rules if rule != props.qto_rule) + alternative_rules = next(rule for rule in tool.Qto.get_qto_rules() if rule != props.qto_rule) not_quantified_elements = run_quantification(alternative_rules, not_quantified_elements) not_quantified_message = tool.Qto.get_not_quantified_elements_message(not_quantified_elements) diff --git a/src/bonsai/bonsai/bim/module/qto/prop.py b/src/bonsai/bonsai/bim/module/qto/prop.py index fa90ba6f27..23f6f707c5 100644 --- a/src/bonsai/bonsai/bim/module/qto/prop.py +++ b/src/bonsai/bonsai/bim/module/qto/prop.py @@ -38,12 +38,8 @@ CALCULATOR_FUNCTION_ENUM_ITEMS: list[Union[tuple[str, str, str], None]] = [] def get_qto_rule(self: "BIMQtoProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]: - ifc_file = tool.Ifc.get() - is_ifc4x3 = ifc_file.schema == "IFC4X3" results: list[tuple[str, str, str]] = [] - for rule_id, rule in ifc5d.qto.rules.items(): - if rule_id.startswith("IFC4X3") != is_ifc4x3: - continue + for rule_id, rule in tool.Qto.get_qto_rules().items(): results.append((rule_id, rule["name"], rule["description"])) return results diff --git a/src/bonsai/bonsai/tool/qto.py b/src/bonsai/bonsai/tool/qto.py index 60985b5cb3..fdd9c038d7 100644 --- a/src/bonsai/bonsai/tool/qto.py +++ b/src/bonsai/bonsai/tool/qto.py @@ -25,7 +25,7 @@ import ifcopenshell import ifcopenshell.util.unit import ifcopenshell.util.element from mathutils import Vector -from typing import Optional, Union, Literal, TYPE_CHECKING +from typing import Optional, Union, Literal, TYPE_CHECKING, Any if TYPE_CHECKING: from bonsai.bim.module.qto.prop import BIMQtoProperties @@ -168,6 +168,14 @@ class Qto(bonsai.core.tool.Qto): ) return result + @classmethod + def get_qto_rules(cls) -> dict[str, dict[str, Any]]: + import ifc5d.qto + + ifc_file = tool.Ifc.get() + is_ifc4x3 = ifc_file.schema == "IFC4X3" + return {rule_id: rule for rule_id, rule in ifc5d.qto.rules.items() if rule_id.startswith("IFC4X3") == is_ifc4x3} + @classmethod def get_not_quantified_elements_message(cls, not_quantified_elements: set[ifcopenshell.entity_instance]) -> str: not_quantified_message = "" From 4f6dbbe72b25ca06bda99a9f8a251d10c724bdbb Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 11:11:05 +0500 Subject: [PATCH 358/476] bim.calculate_single_quantity - ifcopenshell calculator to appear first --- src/bonsai/bonsai/bim/module/qto/prop.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/qto/prop.py b/src/bonsai/bonsai/bim/module/qto/prop.py index 23f6f707c5..fd5c1d58e8 100644 --- a/src/bonsai/bonsai/bim/module/qto/prop.py +++ b/src/bonsai/bonsai/bim/module/qto/prop.py @@ -48,6 +48,8 @@ def get_calculator(self: "BIMQtoProperties", context: bpy.types.Context) -> list results: list[tuple[str, str, str]] = [] for name, calculator in ifc5d.qto.calculators.items(): results.append((name, name, calculator.__doc__ or "")) + # Make IfcOpenShell appear first. + results.sort(key=lambda x: x[0] == "IfcOpenShell", reverse=True) return results From ec702e2ec192c02807960b219b4b8ab428f64ead Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 11:24:39 +0500 Subject: [PATCH 359/476] Simple quantity calculator - add operators descriptions --- src/bonsai/bonsai/bim/module/qto/operator.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/qto/operator.py b/src/bonsai/bonsai/bim/module/qto/operator.py index 1d57e51227..eb2ccda282 100644 --- a/src/bonsai/bonsai/bim/module/qto/operator.py +++ b/src/bonsai/bonsai/bim/module/qto/operator.py @@ -27,6 +27,7 @@ from bonsai.bim.module.qto import helper class CalculateCircleRadius(bpy.types.Operator): bl_idname = "bim.calculate_circle_radius" bl_label = "Calculate Circle Radius" + bl_description = "Calculate circle radius for the selected object's selected vertices." bl_options = {"REGISTER", "UNDO"} @classmethod @@ -41,6 +42,7 @@ class CalculateCircleRadius(bpy.types.Operator): class CalculateEdgeLengths(bpy.types.Operator): bl_idname = "bim.calculate_edge_lengths" bl_label = "Calculate Edge Lengths" + bl_description = "Calculate edge lengths for the selected mesh objects." bl_options = {"REGISTER", "UNDO"} @classmethod @@ -56,6 +58,7 @@ class CalculateEdgeLengths(bpy.types.Operator): class CalculateFaceAreas(bpy.types.Operator): bl_idname = "bim.calculate_face_areas" bl_label = "Calculate Face Areas" + bl_description = "Calculate face areas for the selected mesh objects." bl_options = {"REGISTER", "UNDO"} @classmethod @@ -71,6 +74,7 @@ class CalculateFaceAreas(bpy.types.Operator): class CalculateObjectVolumes(bpy.types.Operator): bl_idname = "bim.calculate_object_volumes" bl_label = "Calculate Object Volumes" + bl_description = "Calculate volumes for the selected mesh objects." bl_options = {"REGISTER", "UNDO"} @classmethod @@ -86,6 +90,7 @@ class CalculateObjectVolumes(bpy.types.Operator): class CalculateFormworkArea(bpy.types.Operator): bl_idname = "bim.calculate_formwork_area" bl_label = "Calculate Formwork Area" + bl_description = "Calculate formwork area for the selected mesh objects." bl_options = {"REGISTER", "UNDO"} @classmethod @@ -101,6 +106,7 @@ class CalculateFormworkArea(bpy.types.Operator): class CalculateSideFormworkArea(bpy.types.Operator): bl_idname = "bim.calculate_side_formwork_area" bl_label = "Calculate Side Formwork Area" + bl_description = "Calculate side formwork area for the selected mesh objects." bl_options = {"REGISTER", "UNDO"} @classmethod From 3f6a9a55cdfb256d7ad99dfc2b75d77ab3a1b31b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 11:24:51 +0500 Subject: [PATCH 360/476] bim.calculate_side_formwork_area - expose to ui --- src/bonsai/bonsai/bim/module/qto/ui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/qto/ui.py b/src/bonsai/bonsai/bim/module/qto/ui.py index 4762062799..9029a63066 100644 --- a/src/bonsai/bonsai/bim/module/qto/ui.py +++ b/src/bonsai/bonsai/bim/module/qto/ui.py @@ -99,6 +99,8 @@ class BIM_PT_qto_simple(bpy.types.Panel): row.operator("bim.calculate_object_volumes") row = layout.row() row.operator("bim.calculate_formwork_area") + row = layout.row() + row.operator("bim.calculate_side_formwork_area") class BIM_PT_qto_cost(bpy.types.Panel): From 791a2e40f1030dca613f7dca0d0161eb261733fb Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 11:42:27 +0500 Subject: [PATCH 361/476] qto - add note on gross and net functions --- src/bonsai/bonsai/bim/module/qto/prop.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/qto/prop.py b/src/bonsai/bonsai/bim/module/qto/prop.py index fd5c1d58e8..b4d01ff29b 100644 --- a/src/bonsai/bonsai/bim/module/qto/prop.py +++ b/src/bonsai/bonsai/bim/module/qto/prop.py @@ -74,7 +74,14 @@ def get_calculator_function( class BIMQtoProperties(PropertyGroup): qto_rule: EnumProperty(items=get_qto_rule, name="Qto Rule") calculator: EnumProperty(items=get_calculator, name="Calculator") - calculator_function: EnumProperty(items=get_calculator_function, name="Calculator Function") + calculator_function: EnumProperty( + items=get_calculator_function, + name="Calculator Function", + description=( + "Gross functions calculate the measure for the original element's geometry, without openings.\n" + "Net functions include the openings substractions.\n\nCurrently selected function" + ), + ) qto_result: StringProperty(default="", name="Qto Result") qto_name: StringProperty(name="Qto Name", default="My_Qto") prop_name: StringProperty(name="Prop Name", default="MyDimension") From 1408beeac517e06b09f6bf457533b9165f68018d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 11:43:19 +0500 Subject: [PATCH 362/476] ifc5d.qto.Blender - functions descriptions --- .../bonsai/bim/module/qto/calculator.py | 11 +++++++++++ src/bonsai/bonsai/bim/module/qto/prop.py | 4 ++++ src/ifc5d/ifc5d/qto.py | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index 1cfd731bb3..dbb8fe225d 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -36,14 +36,17 @@ VectorTuple = tuple[float, float, float] def get_x(o: bpy.types.Object) -> float: + """Calculate the length along the local X axis.""" return o.bound_box[6][0] - o.bound_box[0][0] def get_y(o: bpy.types.Object) -> float: + """Calculate the length along the local Y axis.""" return o.bound_box[6][1] - o.bound_box[0][1] def get_z(o: bpy.types.Object) -> float: + """Calculate the length along the local Z axis.""" return o.bound_box[6][2] - o.bound_box[0][2] @@ -64,6 +67,7 @@ def get_linear_length(o: bpy.types.Object) -> float: def get_length(o: bpy.types.Object, vg_index: Optional[int] = None) -> float: + """Calculate the object length trying to guess the main axis.""" if vg_index is None: x = get_x(o) y = get_y(o) @@ -158,6 +162,8 @@ def get_covering_width(obj: bpy.types.Object) -> float: def get_width(o: bpy.types.Object) -> float: """_summary_: Returns the width of the object bounding box + Min value between X and Y axes lengths. + :param blender-object o: blender object :return float: width """ @@ -169,6 +175,8 @@ def get_width(o: bpy.types.Object) -> float: def get_height(o: bpy.types.Object) -> float: """_summary_: Returns the height of the object bounding box + Based on the the length along the local Z axis. + :param blender-object o: blender object :return float: height """ @@ -549,6 +557,7 @@ def get_obj_decompositions(obj: bpy.types.Object) -> set[ifcopenshell.entity_ins def get_gross_weight(obj: bpy.types.Object) -> Union[float, None]: + """Get gross weight of the object (based on gross volume and Pset_MaterialCommon.MassDensity)""" obj_mass_density = get_obj_mass_density(obj) if not obj_mass_density: return @@ -558,6 +567,7 @@ def get_gross_weight(obj: bpy.types.Object) -> Union[float, None]: def get_net_weight(obj: bpy.types.Object) -> Union[float, None]: + """Get net weight of the object (based on net volume and Pset_MaterialCommon.MassDensity)""" obj_mass_density = get_obj_mass_density(obj) if not obj_mass_density: return @@ -567,6 +577,7 @@ def get_net_weight(obj: bpy.types.Object) -> Union[float, None]: def get_obj_mass_density(obj: bpy.types.Object) -> Union[float, None]: + """Calculate object mass density based on Pset_MaterialCommon.MassDensity.""" entity = tool.Ifc.get_entity(obj) assert entity material = ifcopenshell.util.element.get_material(entity) diff --git a/src/bonsai/bonsai/bim/module/qto/prop.py b/src/bonsai/bonsai/bim/module/qto/prop.py index b4d01ff29b..4f54fa3893 100644 --- a/src/bonsai/bonsai/bim/module/qto/prop.py +++ b/src/bonsai/bonsai/bim/module/qto/prop.py @@ -58,6 +58,10 @@ def get_calculator_function( ) -> list[Union[tuple[str, str, str], None]]: global CALCULATOR_FUNCTION_ENUM_ITEMS calculator = ifc5d.qto.calculators[self.calculator] + + if calculator is ifc5d.qto.Blender: + calculator.populate_descriptions() + CALCULATOR_FUNCTION_ENUM_ITEMS = [] previous_measure = None for function_id, function in calculator.functions.items(): diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 8827bef51e..1ed520193a 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -388,6 +388,25 @@ class Blender(QtoCalculator): "get_net_weight": Function("IfcMassMeasure", "Net Weight", ""), } + description_populated = False + + @classmethod + def populate_descriptions(cls) -> None: + """Populate the descriptions based on the function docstrings. + + The action is postponed to ensure ifc5d package works without Blender. + """ + if cls.description_populated: + return + + import bonsai.bim.module.qto.calculator as calculator + + for function in cls.functions: + doc = getattr(calculator, function).__doc__ or "" + doc = doc[: doc.find(":param")].strip() + old_function = cls.functions[function] + cls.functions[function] = Function(old_function.measure, old_function.name, doc) + @classmethod def calculate(cls, ifc_file, elements, qtos, results): import bonsai.tool as tool From 1de67ffd9c42aef1e3471d008d36dc380169f121 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 11:48:52 +0500 Subject: [PATCH 363/476] ifc5d.qto - add x,y,z functions just for consistency with ifcopenshell calculator --- src/ifc5d/ifc5d/qto.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 1ed520193a..016fd06da2 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -348,13 +348,15 @@ class Blender(QtoCalculator): # Implementations are located in bonsai.bim.module.qto.calculator. functions = { # IfcLengthMeasure + "get_x": Function("IfcLengthMeasure", "X", ""), + "get_y": Function("IfcLengthMeasure", "Y", ""), + "get_z": Function("IfcLengthMeasure", "Z", ""), "get_covering_width": Function("IfcLengthMeasure", "Covering Width", ""), "get_finish_ceiling_height": Function("IfcLengthMeasure", "Finish Ceiling Height", ""), "get_finish_floor_height": Function("IfcLengthMeasure", "Finish Floor Height", ""), "get_gross_perimeter": Function("IfcLengthMeasure", "Gross Perimeter", ""), "get_height": Function("IfcLengthMeasure", "Height", ""), "get_length": Function("IfcLengthMeasure", "Length", ""), - "get_x": Function("IfcLengthMeasure", "Length", ""), "get_opening_depth": Function("IfcLengthMeasure", "Opening Depth", ""), "get_opening_height": Function("IfcLengthMeasure", "Opening Height", ""), "get_rectangular_perimeter": Function("IfcLengthMeasure", "Rectangular Perimeter", ""), From 86ebc968e53b9b19b1ba7b5a39d2c8ca1c0082e6 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 12:16:03 +0500 Subject: [PATCH 364/476] ifcopenshell.util.element.get_element_mass_density Move it from Blender qto calculator as this method can be generally useful --- .../bonsai/bim/module/qto/calculator.py | 44 +--------------- .../ifcopenshell/util/element.py | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index dbb8fe225d..7501d11821 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -580,49 +580,7 @@ def get_obj_mass_density(obj: bpy.types.Object) -> Union[float, None]: """Calculate object mass density based on Pset_MaterialCommon.MassDensity.""" entity = tool.Ifc.get_entity(obj) assert entity - material = ifcopenshell.util.element.get_material(entity) - if material is None: - return - - if ( - material.is_a("IfcMaterialLayerSet") - or material.is_a("IfcMaterialProfileSet") - or material.is_a("IfcMaterialConstituentSet") - ): - return - - if material.is_a("IfcMaterial"): - material_mass_density = ifcopenshell.util.element.get_pset(material, "Pset_MaterialCommon", "MassDensity") - return material_mass_density - - if material.is_a("IfcMaterialLayerSetUsage"): - material_layers = material.ForLayerSet.MaterialLayers - densities = [] - thicknesses = [] - obj_mass_density = 0 - for material_layer in material_layers: - material_mass_density = ifcopenshell.util.element.get_pset( - material_layer.Material, "Pset_MaterialCommon", "MassDensity" - ) - if material_mass_density is None: - return - densities.append(material_mass_density) - thickness = material_layer.LayerThickness - thicknesses.append(thickness) - obj_mass_density = obj_mass_density + (material_mass_density * thickness) - total_thickness = sum(thicknesses) - obj_mass_density = obj_mass_density / total_thickness - return obj_mass_density - - if material.is_a("IfcMaterialProfileSetUsage"): - material_profiles = material.ForProfileSet.MaterialProfiles - if len(material_profiles) == 1: - material_mass_density = ifcopenshell.util.element.get_pset( - material_profiles[0].Material, "Pset_MaterialCommon", "MassDensity" - ) - return material_mass_density - else: - return + return ifcopenshell.util.element.get_element_mass_density(entity) def get_opening_type(opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]: diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index e4b6ee951d..3f9dc7e40f 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -454,6 +454,58 @@ def get_elements_by_pset(pset: ifcopenshell.entity_instance) -> set[ifcopenshell return elements +def get_element_mass_density(element: ifcopenshell.entity_instance) -> Union[float, None]: + """Calculate object mass density based on material's Pset_MaterialCommon.MassDensity. + + :param element: IFC element entity. + :return: ``float`` mass density in project units if calculation was successful, ``None`` if element either + doesn't have a material or this type of material is not supported. + """ + material = ifcopenshell.util.element.get_material(element) + if material is None: + return + + if ( + material.is_a("IfcMaterialLayerSet") + or material.is_a("IfcMaterialProfileSet") + or material.is_a("IfcMaterialConstituentSet") + ): + return + + if material.is_a("IfcMaterial"): + material_mass_density = ifcopenshell.util.element.get_pset(material, "Pset_MaterialCommon", "MassDensity") + return material_mass_density + + if material.is_a("IfcMaterialLayerSetUsage"): + material_layers = material.ForLayerSet.MaterialLayers + densities = [] + thicknesses = [] + obj_mass_density = 0 + for material_layer in material_layers: + material_mass_density = ifcopenshell.util.element.get_pset( + material_layer.Material, "Pset_MaterialCommon", "MassDensity" + ) + if material_mass_density is None: + return + densities.append(material_mass_density) + thickness = material_layer.LayerThickness + thicknesses.append(thickness) + obj_mass_density = obj_mass_density + (material_mass_density * thickness) + total_thickness = sum(thicknesses) + obj_mass_density = obj_mass_density / total_thickness + return obj_mass_density + + if material.is_a("IfcMaterialProfileSetUsage"): + material_profiles = material.ForProfileSet.MaterialProfiles + if len(material_profiles) == 1: + material_mass_density = ifcopenshell.util.element.get_pset( + material_profiles[0].Material, "Pset_MaterialCommon", "MassDensity" + ) + return material_mass_density + else: + return + + def get_predefined_type(element: ifcopenshell.entity_instance) -> Union[str, None]: """Retrieves the PrefefinedType attribute of an element. From 4c53c2ab1f9baf0fc35804d8ddca31c37a2384b6 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 12:37:56 +0500 Subject: [PATCH 365/476] ifc5d.qto.IfcOpenShell - get_weight function Added support for it in .json for the same elements that support it in IFC4QtoBaseQuantitiesBlender --- src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json | 36 ++++++++-------- src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json | 36 ++++++++-------- src/ifc5d/ifc5d/qto.py | 43 +++++++++++++++++--- 3 files changed, 74 insertions(+), 41 deletions(-) diff --git a/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json b/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json index dfe870bbe2..2c3cdcd0f8 100644 --- a/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json +++ b/src/ifc5d/ifc5d/IFC4QtoBaseQuantities.json @@ -40,11 +40,11 @@ "CrossSectionArea": null, "GrossSurfaceArea": "gross_get_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_max_xyz", "NetSurfaceArea": "net_get_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": "net_get_outer_surface_area" } }, @@ -134,11 +134,11 @@ "CrossSectionArea": null, "GrossSurfaceArea": "gross_get_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_max_xyz", "NetSurfaceArea": "net_get_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": "net_get_outer_surface_area" } }, @@ -317,11 +317,11 @@ "CrossSectionArea": null, "GrossSurfaceArea": null, "GrossVolume": null, - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Height": "net_get_z", "Length": "net_get_max_xy", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": null, "Width": null } @@ -368,11 +368,11 @@ "CrossSectionArea": null, "GrossSurfaceArea": "gross_get_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_max_xyz", "NetSurfaceArea": "net_get_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": "net_get_outer_surface_area" } }, @@ -400,10 +400,10 @@ "CrossSectionArea": null, "GrossSurfaceArea": null, "GrossVolume": null, - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_z", "NetVolume": null, - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": null } }, @@ -420,10 +420,10 @@ "IfcPipeSegment": { "Qto_PipeSegmentBaseQuantities": { "GrossCrossSectionArea": null, - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_segment_length", "NetCrossSectionArea": null, - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": null } }, @@ -431,10 +431,10 @@ "Qto_PlateBaseQuantities": { "GrossArea": "gross_get_max_side_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "NetArea": "net_get_max_side_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "Perimeter": null, "Width": "net_get_min_xyz" } @@ -510,11 +510,11 @@ "Depth": "net_get_z", "GrossArea": "gross_get_footprint_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_x", "NetArea": "net_get_footprint_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "Perimeter": "net_get_footprint_perimeter", "Width": "net_get_y" } @@ -609,13 +609,13 @@ "GrossFootprintArea": null, "GrossSideArea": "gross_get_side_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Height": "net_get_z", "Length": "net_get_x", "NetFootprintArea": null, "NetSideArea": "net_get_side_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "Width": "net_get_y" } }, diff --git a/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json b/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json index 90e791bce4..cf0bf7ebde 100644 --- a/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json +++ b/src/ifc5d/ifc5d/IFC4X3QtoBaseQuantities.json @@ -40,11 +40,11 @@ "CrossSectionArea": null, "GrossSurfaceArea": "gross_get_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_max_xyz", "NetSurfaceArea": "net_get_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": "net_get_outer_surface_area" } }, @@ -140,11 +140,11 @@ "CrossSectionArea": null, "GrossSurfaceArea": "gross_get_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_max_xyz", "NetSurfaceArea": "net_get_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": "net_get_outer_surface_area" } }, @@ -362,11 +362,11 @@ "CrossSectionArea": null, "GrossSurfaceArea": null, "GrossVolume": null, - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Height": "net_get_z", "Length": "net_get_max_xy", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": null, "Width": null } @@ -457,11 +457,11 @@ "CrossSectionArea": null, "GrossSurfaceArea": "gross_get_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_max_xyz", "NetSurfaceArea": "net_get_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": "net_get_outer_surface_area" } }, @@ -500,10 +500,10 @@ "CrossSectionArea": null, "GrossSurfaceArea": null, "GrossVolume": null, - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_z", "NetVolume": null, - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": null } }, @@ -521,10 +521,10 @@ "Qto_PipeSegmentBaseQuantities": { "FootPrintArea": null, "GrossCrossSectionArea": null, - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_segment_length", "NetCrossSectionArea": null, - "NetWeight": null, + "NetWeight": "net_get_weight", "OuterSurfaceArea": null } }, @@ -532,10 +532,10 @@ "Qto_PlateBaseQuantities": { "GrossArea": "gross_get_max_side_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "NetArea": "net_get_max_side_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "Perimeter": null, "Width": "net_get_min_xyz" } @@ -656,11 +656,11 @@ "Depth": "net_get_z", "GrossArea": "gross_get_footprint_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Length": "net_get_x", "NetArea": "net_get_footprint_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "Perimeter": "net_get_footprint_perimeter", "Width": "net_get_y" } @@ -782,13 +782,13 @@ "GrossFootPrintArea": null, "GrossSideArea": "gross_get_side_area", "GrossVolume": "gross_get_volume", - "GrossWeight": null, + "GrossWeight": "gross_get_weight", "Height": "net_get_z", "Length": "net_get_x", "NetFootPrintArea": null, "NetSideArea": "net_get_side_area", "NetVolume": "net_get_volume", - "NetWeight": null, + "NetWeight": "net_get_weight", "Width": "net_get_y" } }, diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 016fd06da2..84fa67879f 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -234,6 +234,12 @@ class IfcOpenShell(QtoCalculator): ), # IfcVolumeMeasure "get_volume": Function("IfcVolumeMeasure", "Volume", "Calculates the volume of a manifold shape"), + # IfcMassMeasure + "get_weight": Function( + "IfcMassMeasure", + "Weight", + "The weight of the object based on it's volume and material density (from Pset_MaterialCommon.MassDensity).", + ), } functions = {} @@ -241,6 +247,11 @@ class IfcOpenShell(QtoCalculator): functions[f"gross_{k}"] = Function(v.measure, f"Gross {v.name}", v.description) functions[f"net_{k}"] = Function(v.measure, f"Net {v.name}", v.description) + internal_functions = ( + "get_segment_length", + "get_weight", + ) + @classmethod def calculate(cls, ifc_file, elements, qtos, results): formula_functions: dict[str, types.FunctionType] = {} @@ -258,7 +269,7 @@ class IfcOpenShell(QtoCalculator): if not formula: continue gross_or_net_qtos = gross_qtos if formula.startswith("gross_") else net_qtos - if formula.endswith("get_segment_length"): + if formula.endswith(cls.internal_functions): gross_or_net_qtos.setdefault(name, {})[quantity] = formula.partition("_")[2] elif formula.startswith(("gross_", "net_")): formula = formula.partition("_")[2] @@ -280,6 +291,7 @@ class IfcOpenShell(QtoCalculator): for iterator, qtos_ in tasks: if iterator.initialize(): while True: + geometry: ifcopenshell.geom.main.ShapeType if isinstance(iterator, ifcopenshell.geom.iterator): shape = iterator.get() geometry = shape.geometry @@ -293,11 +305,15 @@ class IfcOpenShell(QtoCalculator): for quantity, formula in quantities.items(): if formula == "get_segment_length": results[element][name][quantity] = cls.get_segment_length(element) + elif formula == "get_weight": + value = cls.get_weight(element, geometry) + if value is None: + continue else: - results[element][name][quantity] = cls.unit_converter.convert( - formula_functions[formula](geometry), - IfcOpenShell.raw_functions[formula].measure, - ) + value = formula_functions[formula](geometry) + assert isinstance(value, (float, int)) + value = cls.unit_converter.convert(value, IfcOpenShell.raw_functions[formula].measure) + results[element][name][quantity] = value if not iterator.next(): break @@ -341,6 +357,23 @@ class IfcOpenShell(QtoCalculator): z = item.Depth return max([x, y, z]) + @classmethod + def get_weight( + cls, element: ifcopenshell.entity_instance, geometry: ifcopenshell.geom.ShapeType + ) -> Union[float, None]: + """Get element's weight. + + :param element: IFC element entity. + :return: ``float`` weight in project units + or ``None`` if mass density calculation for this element is not supported. + """ + + density = ifcopenshell.util.element.get_element_mass_density(element) + if density is None: + return + volume = ifcopenshell.util.shape.get_volume(geometry) + return volume * density + class Blender(QtoCalculator): """Calculates geometry based on currently loaded Blender objects.""" From 1d447e6e23f417b0789b9eca24028c5d0baad448 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 12:39:59 +0500 Subject: [PATCH 366/476] ifc5d.qto.IfcOpenshell - skip None values Just for consistency, in all other cases besides get_segment_length we were only preserving floats anyway --- src/ifc5d/ifc5d/qto.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index 84fa67879f..a71aa32942 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -304,7 +304,9 @@ class IfcOpenShell(QtoCalculator): results[element].setdefault(name, {}) for quantity, formula in quantities.items(): if formula == "get_segment_length": - results[element][name][quantity] = cls.get_segment_length(element) + value = cls.get_segment_length(element) + if value is None: + continue elif formula == "get_weight": value = cls.get_weight(element, geometry) if value is None: @@ -334,7 +336,13 @@ class IfcOpenShell(QtoCalculator): return iterators @classmethod - def get_segment_length(cls, element: ifcopenshell.entity_instance) -> float: + def get_segment_length(cls, element: ifcopenshell.entity_instance) -> Union[float, None]: + """Get segment length. + + :param element: IFC element entity. + :return: ``float`` segment length in project units + or ``None`` if element doesn't have a representation or it's not supported. + """ rep = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") if rep and len(rep.Items or []) == 1 and rep.Items[0].is_a("IfcExtrudedAreaSolid"): item = rep.Items[0] From 9f78d9881e5bc963beeaac35666c8e9ad926ede1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 14:13:38 +0500 Subject: [PATCH 367/476] ifcopenshell.util.shape - use refs to functions in docs --- .../ifcopenshell/util/shape.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape.py b/src/ifcopenshell-python/ifcopenshell/util/shape.py index 0948f9e1f6..08ce7bf25b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/shape.py +++ b/src/ifcopenshell-python/ifcopenshell/util/shape.py @@ -181,7 +181,7 @@ def get_element_bbox_centroid(element: ifcopenshell.entity_instance, geometry: S """Calculates the element's bounding box centroid The centroid is in global coordinates. Note that if you have the shape, it - is more efficient to use ``get_shape_bbox_centroid``. + is more efficient to use :func:`get_shape_bbox_centroid`. :param element: The element occurrence :param geometry: Geometry output calculated by IfcOpenShell @@ -198,7 +198,7 @@ def get_shape_bbox_centroid(shape: ShapeElementType, geometry: ShapeType) -> npt """Calculates the shape's bounding box centroid The centroid is in global coordinates. Note that if you do not have the - shape, you can use ``get_element_bbox_centroid``. + shape, you can use :func:`get_element_bbox_centroid`. :param shape: Shape output calculated by IfcOpenShell :param geometry: Geometry output calculated by IfcOpenShell @@ -243,7 +243,7 @@ def get_faces(geometry: ShapeType) -> npt.NDArray[np.int32]: """Get all the faces as a numpy array Faces are always triangulated. If the shape is a BRep and you want to get - the original untriangulated output, refer to ``get_edges``. + the original untriangulated output, refer to :func:`get_edges`. Results are a nested numpy array e.g. [[f1v1, f1v2, f1v3], [f2v1, f2v2, f2v3], ...] @@ -318,7 +318,7 @@ def get_shape_vertices(shape: ShapeElementType, geometry: ShapeType) -> npt.NDAr """Get the shape's vertices as a numpy array Vertices are in global coordinates. If you do not have the shape, you can - use ``get_element_vertices``. + use :func:`get_element_vertices`. Results are a nested numpy array e.g. [[v1x, v1y, v1z], [v2x, v2y, v2z], ...] @@ -336,7 +336,7 @@ def get_element_vertices(element: ifcopenshell.entity_instance, geometry: ShapeT """Get the element's vertices as a numpy array Vertices are in global coordinates. Note that if you have the shape, it is - more efficient to use ``get_shape_vertices``. + more efficient to use :func:`get_shape_vertices`. Results are a nested numpy array e.g. [[v1x, v1y, v1z], [v2x, v2y, v2z], ...] @@ -374,7 +374,7 @@ def get_top_elevation(geometry: ShapeType) -> float: def get_shape_bottom_elevation(shape: ShapeType, geometry: ShapeType) -> float: """Gets the lowest global Z ordinate of the shape - If you do not have the shape, you can use ``get_element_bottom_elevation`` + If you do not have the shape, you can use :func:`get_element_bottom_elevation` instead. :param shape: Shape output calculated by IfcOpenShell @@ -387,7 +387,7 @@ def get_shape_bottom_elevation(shape: ShapeType, geometry: ShapeType) -> float: def get_shape_top_elevation(shape: ShapeType, geometry: ShapeType) -> float: """Gets the highest global Z ordinate of the shape - If you do not have the shape, you can use ``get_element_top_elevation`` + If you do not have the shape, you can use :func:`get_element_top_elevation` instead. :param shape: Shape output calculated by IfcOpenShell @@ -401,7 +401,7 @@ def get_element_bottom_elevation(element: ifcopenshell.entity_instance, geometry """Gets the lowest global Z ordinate of the element Note that if you have the shape, it is more efficient to use - ``get_shape_bottom_elevation``. + :func:`get_shape_bottom_elevation`. :param element: The element occurrence :param geometry: Geometry output calculated by IfcOpenShell @@ -414,7 +414,7 @@ def get_element_top_elevation(element: ifcopenshell.entity_instance, geometry: S """Gets the highest global Z ordinate of the element Note that if you have the shape, it is more efficient to use - ``get_shape_top_elevation``. + :func:`get_shape_top_elevation`. :param element: The element occurrence :param geometry: Geometry output calculated by IfcOpenShell @@ -484,7 +484,7 @@ def get_side_area( axis. Note that this calculates the actual area, not the projected 2D area. If - you want the projected area, use ``get_footprint_area``. + you want the projected area, use :func:`get_footprint_area`. :param geometry: Geometry output calculated by IfcOpenShell :param axis: Either X, Y, or Z. Defaults to Y, which is used for standard @@ -549,7 +549,7 @@ def get_footprint_area( axis. Note that this calculates the 2D projected area, not the actual surface - area. If you want the actual area, use ``get_side_area``. + area. If you want the actual area, use :func:`get_side_area`. :param geometry: Geometry output calculated by IfcOpenShell :param axis: Either X, Y, or Z. Defaults to Z. From 2f87029434b11bc5668ea2ccb4aae34e25559194 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 14:49:19 +0500 Subject: [PATCH 368/476] Use schema_identifier to get IFC4X3 schema more precisely --- src/bonsai/bonsai/bim/module/pset_template/data.py | 4 ++-- src/bonsai/scripts/generate_util_type_json.py | 6 +++--- src/ifcopenshell-python/ifcopenshell/__init__.py | 4 ++++ src/ifcopenshell-python/ifcopenshell/util/cost.py | 2 +- src/ifcopenshell-python/ifcopenshell/util/doc.py | 3 +-- src/ifcopenshell-python/ifcopenshell/util/pset.py | 4 +--- src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py | 2 +- src/ifcsverchok/nodes/ifc/by_type.py | 2 +- src/ifcsverchok/nodes/ifc/pick_ifc_class.py | 2 +- src/ifcsverchok/nodes/ifc/read_entity.py | 2 +- src/ifctester/ifctester/facet.py | 2 +- 11 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/pset_template/data.py b/src/bonsai/bonsai/bim/module/pset_template/data.py index ccf589b463..8eae1fdc42 100644 --- a/src/bonsai/bonsai/bim/module/pset_template/data.py +++ b/src/bonsai/bonsai/bim/module/pset_template/data.py @@ -56,7 +56,7 @@ class PsetTemplatesData: ifc_file = IfcStore.pset_template_file if not ifc_file: return [] - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema) + schema = ifcopenshell.schema_by_name(ifc_file.schema) version = ifc_file.schema enum_items = [ (t, t, ifcopenshell.util.doc.get_type_doc(version, t).get("description", "")) @@ -97,7 +97,7 @@ class PsetTemplatesData: # If mixed typed are used (e.g. during transition), assume type is not specified. pset_type = next(iter(pset_types)) if len(pset_types) == 1 else None - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema) + schema = ifcopenshell.schema_by_name(ifc_file.schema) attribute = schema.declaration_by_name("IfcSimplePropertyTemplate").attributes()[0] enum_items = [ a diff --git a/src/bonsai/scripts/generate_util_type_json.py b/src/bonsai/scripts/generate_util_type_json.py index f48b35dcbf..c162800f54 100644 --- a/src/bonsai/scripts/generate_util_type_json.py +++ b/src/bonsai/scripts/generate_util_type_json.py @@ -24,7 +24,7 @@ import ifcopenshell.util.schema def generate_ifc4_entity_map(filepath, schema_name, manual_corrections={}): filepath = filepath - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name) + schema = ifcopenshell.schema_by_name(schema_name) entity_to_type_map = {} @@ -154,7 +154,7 @@ ifc4x3_corrections = { "IfcDistributionElement": ["IfcDistributionElementType"], "IfcBuiltElement": ["IfcBuiltElementType"], } -entity_to_type_map4x3 = generate_ifc4_entity_map("IFC4X3_TC1.exp", "ifc4x3", ifc4x3_corrections) +entity_to_type_map4x3 = generate_ifc4_entity_map("IFC4X3_TC1.exp", "IFC4X3", ifc4x3_corrections) # some manual IFC4 corrections (more details in entity_to_type_map_4.json commits history) ifc4_corrections = { @@ -165,6 +165,6 @@ ifc4_corrections = { "IfcDistributionElement": ["IfcDistributionElementType"], } # -entity_to_type_map4 = generate_ifc4_entity_map("IFC4.exp", "ifc4", ifc4_corrections) +entity_to_type_map4 = generate_ifc4_entity_map("IFC4.exp", "IFC4", ifc4_corrections) entity_to_type_map2x3 = generate_ifc2x3_entity_map() diff --git a/src/ifcopenshell-python/ifcopenshell/__init__.py b/src/ifcopenshell-python/ifcopenshell/__init__.py index ee5ed67f48..260b3e5f41 100644 --- a/src/ifcopenshell-python/ifcopenshell/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/__init__.py @@ -237,6 +237,10 @@ def schema_by_name( :param schema: Which IFC schema to use, chosen from "IFC2X3", "IFC4", or "IFC4X3". These refer to the ISO approved versions of IFC. + E.g. from ``ifcopenshell.file.schema_identifier``. + Passing ``ifcopenshell.file.schema`` also will work but may result + in not precisely matching schema but it's only conern if you're + using not one of the main schemas. :param schema_version: If you want to specify an exact version of IFC that may not be an ISO approved version, use this argument instead of ``schema``. IFC versions on technical.buildingsmart.org are diff --git a/src/ifcopenshell-python/ifcopenshell/util/cost.py b/src/ifcopenshell-python/ifcopenshell/util/cost.py index 5d9189b337..9c434d6ce3 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/cost.py +++ b/src/ifcopenshell-python/ifcopenshell/util/cost.py @@ -281,7 +281,7 @@ def get_cost_values(cost_item: ifcopenshell.entity_instance) -> list[dict[str, s def get_cost_schedule_types(file: ifcopenshell.file) -> list[dict[str, str]]: - schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(file.schema) + schema: ifcopenshell_wrapper.schema_definition = ifcopenshell_wrapper.schema_by_name(file.schema_identifier) results = [] declaration = schema.declaration_by_name("IfcCostSchedule") version = file.schema_identifier diff --git a/src/ifcopenshell-python/ifcopenshell/util/doc.py b/src/ifcopenshell-python/ifcopenshell/util/doc.py index b444966068..0a2dad205b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/doc.py +++ b/src/ifcopenshell-python/ifcopenshell/util/doc.py @@ -113,8 +113,7 @@ def get_schema_by_name(version: str) -> ifcopenshell_wrapper.schema_definition: global schema_by_name version = ifcopenshell.util.schema.get_fallback_schema(version) if not schema_by_name[version]: - schema_name = "IFC4X3_ADD2" if version == "IFC4X3" else version - schema_by_name[version] = ifcopenshell_wrapper.schema_by_name(schema_name) + schema_by_name[version] = ifcopenshell.schema_by_name(version) return schema_by_name[version] diff --git a/src/ifcopenshell-python/ifcopenshell/util/pset.py b/src/ifcopenshell-python/ifcopenshell/util/pset.py index bf90570251..025f9f042c 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/pset.py +++ b/src/ifcopenshell-python/ifcopenshell/util/pset.py @@ -46,9 +46,7 @@ class PsetQto: # fmt: on def __init__(self, schema_identifier: str, templates=None) -> None: - if schema_identifier == "IFC4X3": - schema_identifier = "IFC4X3_ADD2" - self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_identifier) + self.schema = ifcopenshell.schema_by_name(schema_identifier) if not templates: folder_path = pathlib.Path(__file__).parent.absolute() path = str(folder_path.joinpath("schema", self.templates_path[schema_identifier])) diff --git a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py index 90ac909e3e..d4ef3feba9 100644 --- a/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py +++ b/src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py @@ -140,7 +140,7 @@ class Patcher: # Assume it's a filepath - existing or not. pass - self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(self.file.schema) + self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(self.file.schema_identifier) if self.sql_type == "sqlite": self.db = sqlite3.connect(database) diff --git a/src/ifcsverchok/nodes/ifc/by_type.py b/src/ifcsverchok/nodes/ifc/by_type.py index 96c9d84942..c4c6f9bdb7 100644 --- a/src/ifcsverchok/nodes/ifc/by_type.py +++ b/src/ifcsverchok/nodes/ifc/by_type.py @@ -68,7 +68,7 @@ def get_ifc_classes(self, context): file = SvIfcStore.get_file() if not file: return [] - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(file.schema) + schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(file.schema_identifier) declaration = schema.declaration_by_name(self.ifc_product) def get_classes(declaration): diff --git a/src/ifcsverchok/nodes/ifc/pick_ifc_class.py b/src/ifcsverchok/nodes/ifc/pick_ifc_class.py index 5cf24b9602..3cbbf03af0 100644 --- a/src/ifcsverchok/nodes/ifc/pick_ifc_class.py +++ b/src/ifcsverchok/nodes/ifc/pick_ifc_class.py @@ -64,7 +64,7 @@ def get_ifc_classes(self, context): file = SvIfcStore.get_file() if not file: return [] - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(file.schema) + schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(file.schema_identifier) declaration = schema.declaration_by_name(self.ifc_product) def get_classes(declaration): diff --git a/src/ifcsverchok/nodes/ifc/read_entity.py b/src/ifcsverchok/nodes/ifc/read_entity.py index fb581d1b63..9ea2fa3490 100644 --- a/src/ifcsverchok/nodes/ifc/read_entity.py +++ b/src/ifcsverchok/nodes/ifc/read_entity.py @@ -58,7 +58,7 @@ class SvIfcReadEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.S ifc_class = entity.is_a() file = SvIfcStore.get_file() if file: - schema_name = file.wrapped_data.schema + schema_name = file.schema_identifier else: schema_name = "IFC4" self.schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema_name) diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 44b2bedfe9..7ea972087f 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -257,7 +257,7 @@ class Attribute(Facet): return super().filter(ifc_file, elements) results = [] - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema) + schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(ifc_file.schema_identifier) entities = {entity.name(): entity for entity in schema.entities()} def ignore_subtypes(entity): From f36830d6dc58196ebd9b07c767d34f9aebb67c59 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 14:54:43 +0500 Subject: [PATCH 369/476] bim.show_loads - handle absence of structural items --- src/bonsai/bonsai/bim/module/structural/operator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index 92b88a78ec..aec6dc4097 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -52,7 +52,11 @@ class ShowLoads(bpy.types.Operator): return {"PASS_THROUGH"} def invoke(self, context, event): - collection = bpy.data.collections["IfcStructuralItem"] + collection = bpy.data.collections.get("IfcStructuralItem") + if collection is None: + self.report({"ERROR"}, "No IfcStructuralItems found.") + return {"CANCELLED"} + collection.hide_viewport = False context.window.cursor_modal_set("WAIT") try: From e55523ce3e05cfe1066e8c44f2c5c3c6bc744202 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 14:55:40 +0500 Subject: [PATCH 370/476] bim.show_loads - add undo for safety In Blender it's unsafe to leave operators without UNDO options if they edit any ID data-block. --- src/bonsai/bonsai/bim/module/structural/operator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/bim/module/structural/operator.py b/src/bonsai/bonsai/bim/module/structural/operator.py index aec6dc4097..6836633d54 100644 --- a/src/bonsai/bonsai/bim/module/structural/operator.py +++ b/src/bonsai/bonsai/bim/module/structural/operator.py @@ -36,6 +36,7 @@ class ShowLoads(bpy.types.Operator): bl_idname = "bim.show_loads" bl_label = "Show Loads in 3D View" + bl_options = {"REGISTER", "UNDO"} def modal(self, context, event): if event.type == "F5": From 6cfd15e819c50319640947ffbe948cc76076e621 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 16:41:36 +0500 Subject: [PATCH 371/476] Fix mistake in 64f51d7 --- src/ifcopenshell-python/ifcopenshell/util/system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/system.py b/src/ifcopenshell-python/ifcopenshell/util/system.py index ef84be03c5..833d85a941 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/system.py +++ b/src/ifcopenshell-python/ifcopenshell/util/system.py @@ -66,7 +66,7 @@ def get_system_elements(system: ifcopenshell.entity_instance) -> list[ifcopenshe def get_element_systems(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: results = [] for rel in element.HasAssignments: - if rel.is_a("IfcRelAssignsToGroup"): + if not rel.is_a("IfcRelAssignsToGroup"): continue group = rel.RelatingGroup if not group.is_a("IfcSystem") or group.is_a() in ("IfcStructuralAnalysisModel", "IfcZone"): From 9ba3b6a61fdfd88ece8f7fac540605d20bfe259d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 16:43:55 +0500 Subject: [PATCH 372/476] add cgal-original-edges option to settings (5599e52) --- src/ifcopenshell-python/ifcopenshell/geom/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index 1f9215cb5c..d56c9873a8 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -66,6 +66,7 @@ SETTING = Literal[ "apply-default-materials", "boolean-attempt-2d", "building-local-placement", + "cgal-original-edges", "circle-segments", "context-identifiers", "context-ids", From eed47c8c87b6c678025c167c211020a188d79f6a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 14 Mar 2025 18:32:42 +0500 Subject: [PATCH 373/476] Display internal ID in calculator functions description Example - https://i.imgur.com/3C3TBCu.png --- src/bonsai/bonsai/bim/module/qto/prop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/qto/prop.py b/src/bonsai/bonsai/bim/module/qto/prop.py index 4f54fa3893..e632bb2915 100644 --- a/src/bonsai/bonsai/bim/module/qto/prop.py +++ b/src/bonsai/bonsai/bim/module/qto/prop.py @@ -70,7 +70,7 @@ def get_calculator_function( CALCULATOR_FUNCTION_ENUM_ITEMS.append(None) description = function.description description += f"\n\nInternal function id: '{function_id}'." - CALCULATOR_FUNCTION_ENUM_ITEMS.append((function_id, f"{measure}: {function.name}", function.description)) + CALCULATOR_FUNCTION_ENUM_ITEMS.append((function_id, f"{measure}: {function.name}", description)) previous_measure = measure return CALCULATOR_FUNCTION_ENUM_ITEMS From 04e07db0a3a47ae3c9bf76fa147cdbfddca85080 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Fri, 14 Mar 2025 07:31:50 -0700 Subject: [PATCH 374/476] Import alignment from csv (#6234) * Start of the official alignment API * Import alignment into bonsai model using CSV file --- src/bonsai/bonsai/bim/__init__.py | 1 + .../bonsai/bim/module/alignment/__init__.py | 36 + .../bonsai/bim/module/alignment/operator.py | 113 + .../bonsai/bim/module/sequence/operator.py | 2 +- src/bonsai/docs/guides/alignment.rst | 37 + src/bonsai/docs/index.rst | 1 + src/bonsai/docs/reference/topbar.rst | 1 + src/ifcgeom/ConversionSettings.h | 8 +- src/ifcgeom/function_item_evaluator.cpp | 56 +- src/ifcgeom/function_item_evaluator.h | 15 +- src/ifcgeom/mapping/IfcCurveSegment.cpp | 348 ++- .../mapping/IfcOffsetCurveByDistance.cpp | 7 +- .../mapping/IfcSectionedSolidHorizontal.cpp | 2 +- .../ifcopenshell/alignment.py | 1158 --------- .../ifcopenshell/api/alignment/__init__.py | 70 + .../api/alignment/add_segment_to_curve.py | 76 + .../api/alignment/add_segment_to_layout.py | 52 + .../alignment/add_stationing_to_alignment.py | 79 + .../api/alignment/add_vertical_alignment.py | 198 ++ .../add_vertical_alignment_by_pi_method.py | 59 + .../api/alignment/add_zero_length_segment.py | 101 + .../create_alignment_by_pi_method.py | 80 + .../alignment/create_alignment_from_csv.py | 116 + .../create_geometric_representation.py | 172 ++ ...reate_horizontal_alignment_by_pi_method.py | 216 ++ .../create_segment_representations.py | 76 + .../create_vertical_alignment_by_pi_method.py | 194 ++ .../api/alignment/get_alignment_layouts.py | 41 + .../api/alignment/get_axis_subcontext.py | 40 + .../api/alignment/get_basis_curve.py | 51 + .../api/alignment/get_child_alignments.py | 44 + .../ifcopenshell/api/alignment/get_curve.py | 52 + .../api/alignment/get_parent_alignment.py | 45 + .../api/alignment/has_zero_length_segment.py | 61 + .../alignment/map_alignment_cant_segment.py | 84 + .../map_alignment_horizontal_segment.py | 439 ++++ .../api/alignment/map_alignment_segment.py | 47 + .../api/alignment/map_alignment_segments.py | 62 + .../map_alignment_vertical_segment.py | 225 ++ .../api/alignment/name_segments.py | 42 + .../api/alignment/remove_last_segment.py | 59 + .../alignment/remove_zero_length_segment.py | 35 + .../update_curve_segment_transition_code.py | 77 + .../ifcopenshell/api/alignment/util.py | 134 ++ .../ifcopenshell/geom/main.py | 1 + .../alignment/test_add_segment_to_curve.py | 72 + .../alignment/test_add_segment_to_layout.py | 75 + .../test_add_stationing_to_alignment.py | 56 + .../test_add_vertical_by_pi_method.py | 74 + .../api/alignment/test_get_basis_curve.py | 70 + .../test/api/alignment/test_get_curve.py | 70 + .../alignment/test_has_zero_length_segment.py | 123 + .../test_map_alignment_cant_segment.py | 26 + .../test_map_alignment_horizontal_segment.py | 2101 +++++++++++++++++ .../alignment/test_map_alignment_segments.py | 102 + .../test_map_alignment_vertical_segment.py | 795 +++++++ .../test/api/alignment/test_name_segments.py | 53 + ...st_update_curve_segment_transition_code.py | 248 ++ src/ifcparse/IfcAlignmentHelper.cpp | 2 +- 59 files changed, 7369 insertions(+), 1311 deletions(-) create mode 100644 src/bonsai/bonsai/bim/module/alignment/__init__.py create mode 100644 src/bonsai/bonsai/bim/module/alignment/operator.py create mode 100644 src/bonsai/docs/guides/alignment.rst delete mode 100644 src/ifcopenshell-python/ifcopenshell/alignment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_curve.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_layout.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_to_alignment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment_by_pi_method.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_by_pi_method.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_from_csv.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/create_horizontal_alignment_by_pi_method.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/create_segment_representations.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/create_vertical_alignment_by_pi_method.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/get_alignment_layouts.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/get_axis_subcontext.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/get_child_alignments.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/get_parent_alignment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_cant_segment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_horizontal_segment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segments.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_vertical_segment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/name_segments.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/remove_last_segment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/remove_zero_length_segment.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/update_curve_segment_transition_code.py create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/util.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_add_segment_to_curve.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_add_stationing_to_alignment.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_add_vertical_by_pi_method.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_get_basis_curve.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_get_curve.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_has_zero_length_segment.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_map_alignment_cant_segment.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_map_alignment_horizontal_segment.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_map_alignment_segments.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_name_segments.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_update_curve_segment_transition_code.py diff --git a/src/bonsai/bonsai/bim/__init__.py b/src/bonsai/bonsai/bim/__init__.py index 5985459e2a..930e067c24 100644 --- a/src/bonsai/bonsai/bim/__init__.py +++ b/src/bonsai/bonsai/bim/__init__.py @@ -83,6 +83,7 @@ modules = { "covering": None, "web": None, "light": None, + "alignment": None, # Uncomment this line to enable loading of the demo module. Happy hacking! # The name "demo" must correlate to a folder name in `bim/module/`. # "demo": None, diff --git a/src/bonsai/bonsai/bim/module/alignment/__init__.py b/src/bonsai/bonsai/bim/module/alignment/__init__.py new file mode 100644 index 0000000000..3c49e0b4fe --- /dev/null +++ b/src/bonsai/bonsai/bim/module/alignment/__init__.py @@ -0,0 +1,36 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult +# +# This file is part of Bonsai. +# +# Bonsai 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. +# +# Bonsai 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 Bonsai. If not, see . + +import bpy + +# from . import ui, prop, operator +from . import operator + +classes = (operator.ImportAlignmentCSV,) + + +def menu_func_import(self, context): + self.layout.operator(operator.ImportAlignmentCSV.bl_idname, text="Alignment (.csv)") + + +def register(): + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + + +def unregister(): + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) diff --git a/src/bonsai/bonsai/bim/module/alignment/operator.py b/src/bonsai/bonsai/bim/module/alignment/operator.py new file mode 100644 index 0000000000..1fcbdd52c8 --- /dev/null +++ b/src/bonsai/bonsai/bim/module/alignment/operator.py @@ -0,0 +1,113 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2020, 2021 Dion Moult , 2022 Yassine Oualid +# +# This file is part of Bonsai. +# +# Bonsai 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. +# +# Bonsai 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 Bonsai. If not, see . + +# pyright: reportUnnecessaryTypeIgnoreComment=error + +import os + +import ifcopenshell.api.alignment +import ifcopenshell.api.alignment.add_stationing_to_alignment + +import bpy +import json +import time +import calendar +import isodate +import bonsai.core.sequence as core +import bonsai.tool as tool +import bonsai.bim.module.sequence.helper as helper +import ifcopenshell.util.sequence +import ifcopenshell.util.selector +from datetime import datetime +from dateutil import parser, relativedelta +from bpy_extras.io_utils import ImportHelper +from typing import get_args, TYPE_CHECKING +from typing_extensions import assert_never + + +class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): + bl_idname = "bim.import_alignment_csv" + bl_label = "Import Alignment CSV" + bl_options = {"REGISTER", "UNDO"} + filename_ext = ".csv" + filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) + + @classmethod + def poll(cls, context): + ifc_file = tool.Ifc.get() + if ifc_file is None: + cls.poll_message_set("No IFC file is loaded.") + return False + elif ifc_file.schema != "IFC4X3": + cls.poll_message_set("Schema must be IFC4x3.") + return False + return True + + def _execute(self, context): + import ifcopenshell.api.alignment + + self.file = tool.Ifc.get() + start = time.time() + alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath) + ifcopenshell.api.alignment.create_geometric_representation(self.file, alignment) + ifcopenshell.api.alignment.add_stationing_to_alignment(self.file, alignment=alignment, start_station=0.0) + + # IFC 4.1.5.1 alignments cannot be contained in spatial structures, but can be referenced into them + sites = self.file.by_type("IfcSite") + for site in sites: + ifcopenshell.api.spatial.reference_structure(self.file, products=[alignment], relating_structure=site) + + # process the generated IfcReferent for the alignment + for rel in alignment.IsNestedBy: + for referent in rel.RelatedObjects: + if referent.is_a("IfcReferent"): + referent_obj = bpy.data.objects.new(tool.Loader.get_name(referent), None) + tool.Geometry.link(referent, referent_obj) + tool.Collector.assign(referent_obj, should_clean_users_collection=False) + + # an alignment can be an aggregation of multiple child alignments (ie. multiple verticals for a single horizontal) + # get all the alignment curves + curves = [] + for rel in alignment.IsDecomposedBy: + for agg in rel.RelatedObjects: + if agg.is_a("IfcAlignment"): + curves.append(ifcopenshell.api.alignment.get_curve(agg)) # 3D curve + + # if there aren't any curves from aggregation, then there is only a single vertical or no vertical + if len(curves) == 0: + curves.append(ifcopenshell.api.alignment.get_curve(alignment)) + + settings = ifcopenshell.geom.settings() + for curve in curves: + shape = ifcopenshell.geom.create_shape(settings, curve) + + # create a new Blender mesh + mesh_name = tool.Loader.get_mesh_name_from_shape(shape) + mesh = bpy.data.meshes.new(mesh_name) + m = tool.Loader.convert_geometry_to_mesh(shape, mesh) + + # create a new Blender object + alignment_obj = bpy.data.objects.new(tool.Loader.get_name(alignment), m) + + # link the blender object to with the alignment element + tool.Geometry.link(alignment, alignment_obj) + + # assign the object to the blender collections + tool.Collector.assign(alignment_obj, should_clean_users_collection=False) + + self.report({"INFO"}, "Imported in %s seconds" % (time.time() - start)) diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 8e75097993..b7730f2b93 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -19,6 +19,7 @@ # pyright: reportUnnecessaryTypeIgnoreComment=error import os + import bpy import json import time @@ -653,7 +654,6 @@ class DisableEditingWorkCalendar(bpy.types.Operator): core.disable_editing_work_calendar(tool.Sequence) return {"FINISHED"} - class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.import_csv" bl_label = "Import CSV" diff --git a/src/bonsai/docs/guides/alignment.rst b/src/bonsai/docs/guides/alignment.rst new file mode 100644 index 0000000000..279fd11437 --- /dev/null +++ b/src/bonsai/docs/guides/alignment.rst @@ -0,0 +1,37 @@ +Road and Rail Alignments +======================== + +.. Note:: + + Bonsai lacks modeling features for road and rail alignments. This feature is intended to be a stop-gap measure to allow alignments + to be defined and imported into an IFC model. This feature is most likely temporary and will be phased out as robust alignment + modeling capabilities are developed. + +Alignments may be defined by the PI method in a CSV file for import into an IFC4X3 Bonsai project. The format of the CSV file is as follows: + +.. csv-table:: Alignment by PI Method + + "X1","Y1","R1","X2","Y2","R2","...,","Xn-1","Yn-1","Rn-1","Xn","Yn","Rn" + "D1","Z1","L1","D2","Z2","L2","...,","Dn-1","Zn-1","Ln-1","Dn","Zn","Ln" + "D1","Z1","L1","D2","Z2","L2","...,","Dn-1","Zn-1","Ln-1","Dn","Zn","Ln" + + +where: + Xi,Yi are horizontal alignment PI points + Ri are horizontal curve radii. + Di,Zi are vertical alignment PI points as Distance_Along,Elevation + Li are the horizontal length of parabolic vertical transition curves + +R1 and Rn, as well as L1 and Ln, are placeholder values and should be set to 0.0 + +The CSV file must contain exactly one horizontal alignment definition with a minimum of three points. +X1,Y1 is the Point of Beginning (POB). Xn,Yn is the Point of Ending (POE). + +The CSV file may contain zero, one or more vertical alignment definitions. + +Alignments with a single horizontal layout and zero or one vertical layout are modeled per `IFC Concept Template 4.1.4.4.1.1, Alignment Layout - Horizontal, Vertical, and Cant, `_. Alignments with multiple vertical layouts are modeled per `IFC Concept Template 4.1.4.4.1.2, Alignment Layout - Reusing Horizontal Layout, `_. + +Example based on the `FHWA Bridge Geometry Manual `_: + +500,2500,0.0,3340,660,1000,4340,5000,1250,7600,4560,950,8480,2010,0 +0,100,0,2000,135,1600,5000,105,1200,7400,153,2000,9800,105,800,12800,90,0 \ No newline at end of file diff --git a/src/bonsai/docs/index.rst b/src/bonsai/docs/index.rst index f9a8c490ef..664a619e72 100644 --- a/src/bonsai/docs/index.rst +++ b/src/bonsai/docs/index.rst @@ -56,6 +56,7 @@ and data-rich OpenBIM with Blender :) guides/authoring/georeferencing guides/authoring/git_support guides/development/index + guides/alignment guides/authoring/other_addons guides/troubleshooting guides/debugging diff --git a/src/bonsai/docs/reference/topbar.rst b/src/bonsai/docs/reference/topbar.rst index 555c413757..8744e55809 100644 --- a/src/bonsai/docs/reference/topbar.rst +++ b/src/bonsai/docs/reference/topbar.rst @@ -98,3 +98,4 @@ Imports data from external sources into the Blender session or IFC model. - **P6 (.xer)**: Imports a P6 XER file containing a work schedule into the active IFC model. - **Powerproject (.pp)**: Imports a Powerproject file containing a work schedule into the active IFC model. - **Microsoft Project (.xml)**: Imports a Microsoft Project XML file containing a work schedule into the active IFC model. +- **Alignment (.csv)**: Imports a CSV containing horizontal and vertical alignments defined by the PI method into the active IFC model. \ No newline at end of file diff --git a/src/ifcgeom/ConversionSettings.h b/src/ifcgeom/ConversionSettings.h index ed172cad69..8902a8de26 100644 --- a/src/ifcgeom/ConversionSettings.h +++ b/src/ifcgeom/ConversionSettings.h @@ -365,6 +365,12 @@ namespace ifcopenshell { static constexpr bool defaultvalue = false; }; + struct ComputeCurvature : public SettingBase { + static constexpr const char* const name = "compute-curvature"; + static constexpr const char* const description = "Specifies whether function_item_evaluator.evaluate() computes curvature."; + static constexpr bool defaultvalue = false; + }; + enum FunctionStepMethod { MAXSTEPSIZE, MINSTEPS }; @@ -504,7 +510,7 @@ namespace ifcopenshell { }; class IFC_GEOM_API Settings : public SettingsContainer< - std::tuple + std::tuple > {}; } diff --git a/src/ifcgeom/function_item_evaluator.cpp b/src/ifcgeom/function_item_evaluator.cpp index 1d5c0d186a..b4dade6ab8 100644 --- a/src/ifcgeom/function_item_evaluator.cpp +++ b/src/ifcgeom/function_item_evaluator.cpp @@ -5,12 +5,21 @@ using namespace ifcopenshell::geometry; -double ifcopenshell::geometry::polynomial_length(double A, double B, double C, double horizontal_length) { - auto fn = [A, B, C](double x) -> double { return sqrt(pow(B + 2 * C * x, 2.0) + 1.0); }; - auto l = boost::math::quadrature::trapezoidal(fn, 0.0, horizontal_length); - return l; -} +std::vector ifcopenshell::geometry::helmert_curve_point(double A0, double A1, double A2, double s) { + auto theta = [A0, A1, A2](double t) -> double { + auto a0 = A0 ? t / A0 : 0.0; + auto a1 = A1 ? A1 * std::pow(t, 2) / (2 * fabs(std::pow(A1, 3))) : 0.0; + auto a2 = A2 ? std::pow(t, 3) / (3 * std::pow(A2, 3)) : 0.0; + return a0 + a1 + a2; + }; + auto fn_x = [theta](double t) -> double { return cos(theta(t)); }; + auto fn_y = [theta](double t) -> double { return sin(theta(t)); }; + auto x = boost::math::quadrature::trapezoidal(fn_x, 0.0, s); + auto y = boost::math::quadrature::trapezoidal(fn_y, 0.0, s); + auto angle = theta(x); + return {x, y, angle}; +} struct functor_fn_evaluator : public fn_evaluator { functor_fn_evaluator(taxonomy::functor_item::const_ptr fn, const ifcopenshell::geometry::Settings& settings) : fn_evaluator(settings), @@ -97,12 +106,27 @@ struct gradient_fn_evaluator : public fn_evaluator { auto xy = horizontal_evaluator_.evaluate(u + start_); auto uz = vertical_evaluator_.evaluate(u); - uz.col(3)(0) = 0.0; // x is distance along. zero it out so it doesn't add to the x from horizontal + // curvature is stored in row 3 - capture it and remove it from the xy and uz matrices + // so the matrix operations (ie multiplication) works correct.y + auto horizontal_curvature = xy.row(3); + xy.row(3) = Eigen::Vector4d(0, 0, 0, 1); + + auto vertical_curvature = uz.row(3); + uz.row(3) = Eigen::Vector4d(0, 0, 0, 1); + + uz(0, 3) = 0.0; // x is distance along. zero it out so it doesn't add to the x from horizontal uz.col(1).swap(uz.col(2)); // uz is 2D in distance along - y plane, swap y and z so elevations become z uz.row(1).swap(uz.row(2)); Eigen::Matrix4d m; m = xy * uz; // combine horizontal and vertical + + // Put curvature back into the solution matrix + // curvature for vertical is in column 0, need it to be in column 1 + // so it doesn't add to curvature for horizontal + std::swap(vertical_curvature(3, 0), vertical_curvature(3, 1)); + m.row(3) = horizontal_curvature + vertical_curvature; + return m; } @@ -129,6 +153,15 @@ struct cant_fn_evaluator : public fn_evaluator { auto g = gradient_evaluator_.evaluate(u + start_); auto c = cant_evaluator_.evaluate(u); + + // curvature is stored in row 3 - capture it and remove it from the xy and uz matrices + // so the matrix operations (ie multiplication) works correctly + auto gradient_curvature = g.row(3); + g.row(3) = Eigen::Vector4d(0, 0, 0, 1); + + auto cant_curvature = c.row(3); + c.row(3) = Eigen::Vector4d(0, 0, 0, 1); + // Need to multiply g and c so the axis vectors // from cant have the correct rotation applied so // they are relative to the gradient curve coordinate system @@ -155,6 +188,11 @@ struct cant_fn_evaluator : public fn_evaluator { m(1, 3) = y; m(2, 3) = z + s; + // reinstate values for curvature. + // cant_curvature is cant alone. this needs to be combined with gradient in column 3 + gradient_curvature[3] = gradient_curvature[2] + cant_curvature[3]; + m.row(3) = gradient_curvature; + return m; } @@ -274,5 +312,9 @@ taxonomy::item::ptr function_item_evaluator::evaluate(const std::vector& } Eigen::Matrix4d function_item_evaluator::evaluate(double u) const { - return fn_evaluator_->evaluate(u); + Eigen::Matrix4d m = fn_evaluator_->evaluate(u); + if (!fn_evaluator_->settings_.get().get()) { + m.row(3) = Eigen::Vector4d(0, 0, 0, 1); + } + return m; } diff --git a/src/ifcgeom/function_item_evaluator.h b/src/ifcgeom/function_item_evaluator.h index 3107b615dc..a936bfbe89 100644 --- a/src/ifcgeom/function_item_evaluator.h +++ b/src/ifcgeom/function_item_evaluator.h @@ -7,16 +7,9 @@ namespace ifcopenshell { namespace geometry { -/// @brief Computes the curve length of a polynomial of the form y = A + Bx + Cx^2 -/// This function is needed on the python side. To do this computation, a large library like scipy -/// is needed. That is too much overhead. For this reason, a simple function is here on the C++ side -/// that the python side can call -/// @param A constant term -/// @param B linear term -/// @param C quadradic term -/// @param horizontal_length length of the polynomal projected onto the horizontal axis -/// @return curve length -double polynomial_length(double A, double B, double C,double horizontal_length); +/// @brief Computes a point on a helmert curve at s. +/// Returns (x,y,theta) at L/2. The results are in a vector so they can be returned to python +std::vector helmert_curve_point(double A0, double A1, double A2, double s); /// @brief Abstract class for evaluating a function_item. This class is specialized for each of the function_item types. struct fn_evaluator { @@ -66,7 +59,7 @@ class function_item_evaluator { /// @brief evaluates the function at u /// @param u u is constrained to be between start_ and start_+length - /// @return 4x4 placement matrix + /// @return 4x4 placement matrix. Curvature values for horizontal, vertical, and vertical + cant are stored in the last row. Eigen::Matrix4d evaluate(double u) const; private: diff --git a/src/ifcgeom/mapping/IfcCurveSegment.cpp b/src/ifcgeom/mapping/IfcCurveSegment.cpp index c7e4ca1cfa..bfe8a191ca 100644 --- a/src/ifcgeom/mapping/IfcCurveSegment.cpp +++ b/src/ifcgeom/mapping/IfcCurveSegment.cpp @@ -100,18 +100,15 @@ typedef boost::mpl::vector< struct parent_curve_function { parent_curve_function() = default; parent_curve_function(const parent_curve_function&) = default; - parent_curve_function(std::function fn) : fn_(fn) { - } - - parent_curve_function& operator=(std::function fn) { - fn_ = fn; - return *this; + parent_curve_function(std::function fn, std::function cfn) : fn_(fn), cfn_(cfn) { } virtual Eigen::Matrix4d operator()(double u) const { return fn_(u); } + virtual Eigen::Matrix4d curvature(double u) const { return cfn_(u); } private: std::function fn_; + std::function cfn_; }; struct polynomial_parent_curve : public parent_curve_function { @@ -142,7 +139,7 @@ struct curve_segment_function { Eigen::Matrix4d operator()(double u) const { Eigen::Matrix4d parent_curve_point = (*parent_curve_fn_)(u); Eigen::Matrix4d curve_segment_point = curve_segment_placement_ * remove_parent_curve_rotation_ * remove_parent_curve_translation_ * parent_curve_point; - return curve_segment_point; + return curve_segment_point + parent_curve_fn_->curvature(u); } private: @@ -167,7 +164,7 @@ struct cant_curve_segment_function { Eigen::Matrix4d parent_curve_point = (*parent_curve_fn_)(u); Eigen::Matrix4d cant_increment = parent_curve_point - parent_curve_start_point_; Eigen::Matrix4d curve_segment_point = curve_segment_placement_ + cant_increment; - return curve_segment_point; + return curve_segment_point + parent_curve_fn_->curvature(u); } private: @@ -246,7 +243,7 @@ class curve_segment_evaluator { Logger::Error(std::runtime_error("multiple uses of IfcSegmentCurve not supported"), inst_); } - segment_type_ = is_horizontal ? ST_HORIZONTAL : is_vertical ? ST_VERTICAL : ST_CANT; + segment_type_ = is_horizontal ? ST_HORIZONTAL : is_vertical ? ST_VERTICAL : is_cant ? ST_CANT : ST_HORIZONTAL; start_ = translate_if_param_value(inst->ParentCurve(), inst->SegmentStart()) * length_unit; @@ -336,7 +333,7 @@ class curve_segment_evaluator { } } - void set_spiral_function(double s, std::function fnX, std::function fnY) { + void set_spiral_function(double s, std::function fnX, std::function fnY, std::function curvature) { if (segment_type_ == ST_HORIZONTAL || segment_type_ == ST_VERTICAL) { // start of trimmed curve double pcStartX = 0.0, pcStartY = 0.0; @@ -381,24 +378,32 @@ class curve_segment_evaluator { }; } - parent_curve_fn_ = std::make_shared([start=start_, s, convert_u, fnX, fnY](double u) { - u = convert_u(u+start); + parent_curve_fn_ = std::make_shared( + [start=start_, s, convert_u, fnX, fnY](double u)->Eigen::Matrix4d { + u = convert_u(u+start); - // integration limits, integrate from a to b - auto b = s ? u / s : 0.0; + // integration limits, integrate from a to b + auto b = s ? u / s : 0.0; - // point on parent curve - auto x = boost::math::quadrature::trapezoidal(fnX, 0.0, b); - auto y = boost::math::quadrature::trapezoidal(fnY, 0.0, b); - auto dx = s ? fnX(b) / s : 1.0; - auto dy = s ? fnY(b) / s : 0.0; + // point on parent curve + auto x = boost::math::quadrature::trapezoidal(fnX, 0.0, b); + auto y = boost::math::quadrature::trapezoidal(fnY, 0.0, b); + auto dx = s ? fnX(b) / s : 1.0; + auto dy = s ? fnY(b) / s : 0.0; - Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); - m.col(0) = Eigen::Vector4d(dx, dy, 0, 0); - m.col(1) = Eigen::Vector4d(-dy, dx, 0, 0); - m.col(3) = Eigen::Vector4d(x, y, 0, 1); - return m; - }); + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.col(0) = Eigen::Vector4d(dx, dy, 0, 0); + m.col(1) = Eigen::Vector4d(-dy, dx, 0, 0); + m.col(3) = Eigen::Vector4d(x, y, 0, 1); + return m; + }, + [start = start_, convert_u, curvature](double u) -> Eigen::Matrix4d { + u = convert_u(u + start); + Eigen::Matrix4d c = Eigen::Matrix4d::Zero(); + c(3, 0) = curvature(u); + return c; + } + ); if (segment_type_ == ST_VERTICAL) { // for vertical, the input curve length is measured along the spiral. @@ -428,10 +433,16 @@ class curve_segment_evaluator { } } else if (segment_type_ == ST_CANT) { Logger::Error(std::runtime_error("Unexpected segment type encountered - cant is handled in set_cant_spiral_function - should never get here")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); } + ); } else { Logger::Error(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); } + ); } } @@ -450,33 +461,40 @@ class curve_segment_evaluator { auto end_cant = Cant(/* start_ + */ length_); auto delta_cant = end_cant - start_cant; - parent_curve_fn_ = std::make_shared([start_angle,delta_angle,start_cant,delta_cant,Superelevation, SuperelevationSlope, Cant](double u) -> Eigen::Matrix4d { - // departure of the curve segment from the base curve (superelevation) - auto super_elevation = Superelevation(u); - auto slope = SuperelevationSlope(u); + parent_curve_fn_ = std::make_shared( + [start_angle,delta_angle,start_cant,delta_cant,Superelevation, SuperelevationSlope, Cant](double u) -> Eigen::Matrix4d { + // departure of the curve segment from the base curve (superelevation) + auto super_elevation = Superelevation(u); + auto slope = SuperelevationSlope(u); - // direction along curve segment - auto angle = atan(slope); - auto dx = cos(angle); - auto dy = sin(angle); - Eigen::Vector4d ref_dir(dx, dy, 0.0, 0.0); + // direction along curve segment + auto angle = atan(slope); + auto dx = cos(angle); + auto dy = sin(angle); + Eigen::Vector4d ref_dir(dx, dy, 0.0, 0.0); - // tilt angle in the plane of the cross section - auto cant = Cant(u); - auto tilt_angle = start_angle + delta_angle * (cant - start_cant) / delta_cant; - Eigen::Vector4d z(0.0, cos(tilt_angle), sin(tilt_angle), 0.0); + // tilt angle in the plane of the cross section + auto cant = Cant(u); + auto tilt_angle = start_angle + delta_angle * (cant - start_cant) / delta_cant; + Eigen::Vector4d z(0.0, cos(tilt_angle), sin(tilt_angle), 0.0); - // compute axis direction - Eigen::Vector4d y = z.cross3(ref_dir); - Eigen::Vector4d axis = ref_dir.cross3(y); + // compute axis direction + Eigen::Vector4d y = z.cross3(ref_dir); + Eigen::Vector4d axis = ref_dir.cross3(y); - Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); - m.col(0) = ref_dir; - m.col(1) = y; - m.col(2) = axis; - m.col(3) = Eigen::Vector4d(u, super_elevation, 0.0, 1.0); - return m; - }); + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.col(0) = ref_dir; + m.col(1) = y; + m.col(2) = axis; + m.col(3) = Eigen::Vector4d(u, super_elevation, 0.0, 1.0); + return m; + }, + [Cant](double u) -> Eigen::Matrix4d { + Eigen::Matrix4d c = Eigen::Matrix4d::Zero(); + c(3, 0) = Cant(u); + return c; + } + ); parent_curve_start_point_ = (*parent_curve_fn_)(0.0); } @@ -526,7 +544,8 @@ class curve_segment_evaluator { auto s = fabs(A * sqrt(PI)); // curve length when u = 1.0 auto fn_x = [A, s](double t) -> double { return A ? s * cos(PI * A * t * t / (2 * fabs(A))) : 0.0; }; auto fn_y = [A, s](double t) -> double { return A ? s * sin(PI * A * t * t / (2 * fabs(A))) : 0.0; }; - set_spiral_function(s, fn_x, fn_y); + auto curvature = [A](double t) -> double { return A ? A * t / fabs(A * A * A) : 0.0; }; + set_spiral_function(s, fn_x, fn_y, curvature); } } #endif @@ -548,8 +567,13 @@ class curve_segment_evaluator { }; auto fn_x = [theta](double t) -> double { return cos(theta(t)); }; auto fn_y = [theta](double t) -> double { return sin(theta(t)); }; + auto curvature = [constant_term, cosine_term, L](double t) -> double { + auto a0 = constant_term.has_value() ? L / constant_term.value() : 0.0; + auto a1 = (L / cosine_term) * cos((PI / L) * t); + return a0 + a1; + }; double s = 1.0; - set_spiral_function(s, fn_x, fn_y); + set_spiral_function(s, fn_x, fn_y, curvature); } else if (segment_type_ == ST_CANT) { boost::optional> super, slope; std::tie(super, slope) = get_superelevation_functions(); @@ -574,10 +598,16 @@ class curve_segment_evaluator { set_cant_spiral_function(*super, *slope, cant); } else if (segment_type_ == ST_VERTICAL) { Logger::Error(std::runtime_error("IfcCosineSpiral cannot be used for vertical alignment")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); } + ); } else { Logger::Error(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); } + ); } } #endif @@ -603,8 +633,14 @@ class curve_segment_evaluator { }; auto fn_x = [theta](double t) -> double { return cos(theta(t)); }; auto fn_y = [theta](double t) -> double { return sin(theta(t)); }; + auto curvature = [constant_term, linear_term, sine_term, L](double t) -> double { + auto a0 = constant_term.has_value() ? L / constant_term.value() : 0.0; + auto a1 = linear_term.has_value() ? sign(linear_term.value()) * pow(L / linear_term.value(), 2.0)*(t/L) : 0.0; + auto a2 = (L / sine_term) * sin(2 * PI * t / L); + return a0 + a1 + a2; + }; double s = 1.0; - set_spiral_function(s, fn_x, fn_y); + set_spiral_function(s, fn_x, fn_y, curvature); } else if (segment_type_ == ST_CANT) { boost::optional> super, slope; std::tie(super, slope) = get_superelevation_functions(); @@ -631,16 +667,20 @@ class curve_segment_evaluator { set_cant_spiral_function(*super, *slope, cant); } else if (segment_type_ == ST_VERTICAL) { Logger::Error(std::runtime_error("IfcSineSpiral cannot be used for vertical alignment")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } else { Logger::Error(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } } #endif void polynomial_spiral(boost::optional A0, boost::optional A1, boost::optional A2, boost::optional A3, boost::optional A4, boost::optional A5, boost::optional A6, boost::optional A7) { - auto theta = [A0, A1, A2, A3, A4, A5, A6, A7, start = start_ * length_unit_, lu = length_unit_](double t) { + auto theta = [A0, A1, A2, A3, A4, A5, A6, A7, start = start_ * length_unit_, lu = length_unit_](double t) -> double { auto a0 = A0.has_value() ? t / (A0.value() * lu) : 0.0; auto a1 = A1.has_value() ? A1.value() * lu * std::pow(t, 2) / (2 * fabs(std::pow(A1.value() * lu, 3))) : 0.0; auto a2 = A2.has_value() ? std::pow(t, 3) / (3 * std::pow(A2.value() * lu, 3)) : 0.0; @@ -655,15 +695,31 @@ class curve_segment_evaluator { auto fn_x = [theta](double t) -> double { return cos(theta(t)); }; auto fn_y = [theta](double t) -> double { return sin(theta(t)); }; + + // this is same as cant function in polynomial_cant_spiral + auto curvature = [A0, A1, A2, A3, A4, A5, A6, A7, start = start_, L = length_, lu = length_unit_, length = length_](double t) -> double { + t += start; + auto a0 = A0.has_value() ? 1 / (A0.value() * lu) : 0.0; + auto a1 = A1.has_value() ? A1.value() * lu * t / fabs(std::pow(A1.value() * lu, 3)) : 0.0; + auto a2 = A2.has_value() ? std::pow(t, 2) / std::pow(A2.value() * lu, 3) : 0.0; + auto a3 = A3.has_value() ? A3.value() * lu * std::pow(t, 3) / fabs(std::pow(A3.value() * lu, 5)) : 0.0; + auto a4 = A4.has_value() ? std::pow(t, 4) / std::pow(A4.value() * lu, 5) : 0.0; + auto a5 = A5.has_value() ? A5.value() * lu * std::pow(t, 5) / fabs(std::pow(A5.value() * lu, 7)) : 0.0; + auto a6 = A6.has_value() ? std::pow(t, 6) / std::pow(A6.value() * lu, 7) : 0.0; + auto a7 = A7.has_value() ? A7.value() * lu * std::pow(t, 7) / fabs(std::pow(A7.value() * lu, 9)) : 0.0; + return L * (a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7); + }; + + double s = 1.0; - set_spiral_function(s, fn_x, fn_y); + set_spiral_function(s, fn_x, fn_y, curvature); } void polynomial_cant_spiral(boost::optional A0, boost::optional A1, boost::optional A2, boost::optional A3, boost::optional A4, boost::optional A5, boost::optional A6, boost::optional A7) { boost::optional> super, slope; std::tie(super, slope) = get_superelevation_functions(); - auto cant = [A0, A1, A2, A3, A4, A5, A6, A7, start = start_, L = length_, lu = length_unit_, length = length_](double t) { + auto cant = [A0, A1, A2, A3, A4, A5, A6, A7, start = start_, L = length_, lu = length_unit_, length = length_](double t) -> double { t += start; auto a0 = A0.has_value() ? 1 / (A0.value() * lu) : 0.0; auto a1 = A1.has_value() ? A1.value() * lu * t / fabs(std::pow(A1.value() * lu, 3)) : 0.0; @@ -681,7 +737,7 @@ class curve_segment_evaluator { } if (!slope.has_value()) { - slope = [A1, A2, A3, A4, A5, A6, A7, start = start_, L = length_, lu = length_unit_, length = length_](double t) { + slope = [A1, A2, A3, A4, A5, A6, A7, start = start_, L = length_, lu = length_unit_, length = length_](double t) -> double { t += start; auto a1 = A1.has_value() ? A1.value() * lu / fabs(std::pow(A1.value() * lu, 3)) : 0.0; auto a2 = A2.has_value() ? 2 * t / std::pow(A2.value() * lu, 3) : 0.0; @@ -813,29 +869,36 @@ class curve_segment_evaluator { }; } - parent_curve_fn_ = std::make_shared([segment_type = segment_type_, R, pcCenterX, pcCenterY, start_angle, sign_l, convert_u](double u) { - u = convert_u(u); + parent_curve_fn_ = std::make_shared( + [segment_type = segment_type_, R, pcCenterX, pcCenterY, start_angle, sign_l, convert_u](double u)->Eigen::Matrix4d { + u = convert_u(u); - // u is measured along the circle - // angle from the X=0 axis to the current point - auto delta = R ? sign_l * u / R : 0.0; - auto sweep_angle = start_angle + delta; - auto cos_sweep_angle = cos(sweep_angle); - auto sin_sweep_angle = sin(sweep_angle); + // u is measured along the circle + // angle from the X=0 axis to the current point + auto delta = R ? sign_l * u / R : 0.0; + auto sweep_angle = start_angle + delta; + auto cos_sweep_angle = cos(sweep_angle); + auto sin_sweep_angle = sin(sweep_angle); - // point on the parent curve - auto pcX = R * cos_sweep_angle + pcCenterX; - auto pcY = R * sin_sweep_angle + pcCenterY; + // point on the parent curve + auto pcX = R * cos_sweep_angle + pcCenterX; + auto pcY = R * sin_sweep_angle + pcCenterY; - auto pcDx = -sign_l * sin_sweep_angle; - auto pcDy = sign_l * cos_sweep_angle; + auto pcDx = -sign_l * sin_sweep_angle; + auto pcDy = sign_l * cos_sweep_angle; - Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); - m.col(0) = Eigen::Vector4d(pcDx, pcDy, 0, 0); - m.col(1) = Eigen::Vector4d(-pcDy, pcDx, 0, 0); - m.col(3) = Eigen::Vector4d(pcX, pcY, 0.0, 1.0); - return m; - }); + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.col(0) = Eigen::Vector4d(pcDx, pcDy, 0, 0); + m.col(1) = Eigen::Vector4d(-pcDy, pcDx, 0, 0); + m.col(3) = Eigen::Vector4d(pcX, pcY, 0.0, 1.0); + return m; + }, + [R](double) -> Eigen::Matrix4d { + Eigen::Matrix4d c = Eigen::Matrix4d::Zero(); + c(3, 0) = 1 / R; + return c; + } + ); if (segment_type_ == ST_HORIZONTAL) { parent_curve_start_point_ = (*parent_curve_fn_)(start_); @@ -865,10 +928,14 @@ class curve_segment_evaluator { } else if (segment_type_ == ST_CANT) { Logger::Warning(std::runtime_error("Use of IfcCircle for cant is not supported")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } else { Logger::Error(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } } @@ -916,23 +983,32 @@ class curve_segment_evaluator { convert_u = [pcDx](double u) { return u/pcDx; }; } - parent_curve_fn_ = std::make_shared([pcX, pcY, pcDx, pcDy, convert_u](double u) { - u = convert_u(u); + parent_curve_fn_ = std::make_shared( + [pcX, pcY, pcDx, pcDy, convert_u](double u)->Eigen::Matrix4d { + u = convert_u(u); - auto x = pcX + pcDx * u; - auto y = pcY + pcDy * u; + auto x = pcX + pcDx * u; + auto y = pcY + pcDy * u; - Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); - m.col(0) = Eigen::Vector4d(pcDx, pcDy, 0, 0); - m.col(1) = Eigen::Vector4d(-pcDy, pcDx, 0, 0); - m.col(3) = Eigen::Vector4d(x, y, 0.0, 1.0); - return m; - }); + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.col(0) = Eigen::Vector4d(pcDx, pcDy, 0, 0); + m.col(1) = Eigen::Vector4d(-pcDy, pcDx, 0, 0); + m.col(3) = Eigen::Vector4d(x, y, 0.0, 1.0); + return m; + }, + [](double /*u*/) -> Eigen::Matrix4d { + // curvature is zero for a line. identity initializes c(3,0) = 0 + Eigen::Matrix4d c = Eigen::Matrix4d::Zero(); + return c; + } + ); parent_curve_start_point_ = (*parent_curve_fn_)(start_); } else { - Logger::Warning(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + Logger::Warning(std::runtime_error("Unexpected segment type encountered")); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } } @@ -1022,50 +1098,62 @@ class curve_segment_evaluator { } // This functor evaluates the polynomial at a distance u along the curve - parent_curve_fn_ = std::make_shared([start = start_, lu = length_unit_, coeffX, coeffY, convert_u](double u)->Eigen::Matrix4d { - auto x = convert_u(u + start); // find x for u - // evaluate the polynomial at x - std::array*, 2> coefficients{&coeffX, &coeffY}; - std::array position{0.0, 0.0}; // = SUM(coeff*u^pos) - std::array slope{0.0, 0.0}; // slope is derivative of the curve = SUM( coeff*pos*u^(pos-1) ) - for (int i = 0; i < 2; i++) { // loop over X and Y - auto begin = coefficients[i]->cbegin(); - auto end = coefficients[i]->cend(); - for (auto iter = begin; iter != end; iter++) { - auto exp = std::distance(begin, iter); - auto coeff = (*iter); - position[i] += coeff * pow(lu, 1-exp) * pow(x, exp); + parent_curve_fn_ = std::make_shared( + [start = start_, lu = length_unit_, coeffX, coeffY, convert_u](double u)->Eigen::Matrix4d { + auto x = convert_u(u + start); // find x for u + // evaluate the polynomial at x + std::array*, 2> coefficients{&coeffX, &coeffY}; + std::array position{0.0, 0.0}; // = SUM(coeff*u^pos) + std::array slope{0.0, 0.0}; // slope is derivative of the curve = SUM( coeff*pos*u^(pos-1) ) + for (int i = 0; i < 2; i++) { // loop over X and Y + auto begin = coefficients[i]->cbegin(); + auto end = coefficients[i]->cend(); + for (auto iter = begin; iter != end; iter++) { + auto exp = std::distance(begin, iter); + auto coeff = (*iter); + position[i] += coeff * pow(lu, 1-exp) * pow(x, exp); - if (iter != begin) { - slope[i] += exp * coeff * pow(lu, 1-exp) * pow(x, exp - 1); - } - } + if (iter != begin) { + slope[i] += exp * coeff * pow(lu, 1-exp) * pow(x, exp - 1); + } + } + } + + auto X = position[0]; + auto Y = position[1]; + + auto Dx = slope[0]; + auto Dy = slope[1]; + + auto angle = atan2(Dy, Dx); + Dx = cos(angle); + Dy = sin(angle); + + Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); + m.col(0) = Eigen::Vector4d(Dx, Dy, 0, 0); + m.col(1) = Eigen::Vector4d(-Dy, Dx, 0, 0); + m.col(3) = Eigen::Vector4d(X, Y, 0.0, 1.0); + return m; + }, + [start = start_, lu = length_unit_, coeffX, coeffY, convert_u](double u) -> Eigen::Matrix4d { + auto x = convert_u(u + start); // find x for u + Eigen::Matrix4d c = Eigen::Matrix4d::Zero(); + c(3, 0) = coeffY[2]; // this may need a unit conversion (also assume there is only 3 coefficients) + return c; } - - auto X = position[0]; - auto Y = position[1]; - - auto Dx = slope[0]; - auto Dy = slope[1]; - - auto angle = atan2(Dy, Dx); - Dx = cos(angle); - Dy = sin(angle); - - Eigen::Matrix4d m = Eigen::Matrix4d::Identity(); - m.col(0) = Eigen::Vector4d(Dx, Dy, 0, 0); - m.col(1) = Eigen::Vector4d(-Dy, Dx, 0, 0); - m.col(3) = Eigen::Vector4d(X, Y, 0.0, 1.0); - return m; - }); + ); parent_curve_start_point_ = (*parent_curve_fn_)(0.0); // start is added to u in parent_curve_fn_, so use 0.0 here } else if (segment_type_ == ST_CANT) { Logger::Warning(std::runtime_error("Use of IfcPolynomialCurve for cant is not supported")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } else { Logger::Error(std::runtime_error("Unexpected segment type encountered")); - parent_curve_fn_ = std::make_shared([](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); + parent_curve_fn_ = std::make_shared( + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }, + [](double /*u*/) -> Eigen::Matrix4d { return Eigen::Matrix4d::Identity(); }); } } }; diff --git a/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp b/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp index df790d2738..e6d3afdfce 100644 --- a/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp +++ b/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp @@ -39,7 +39,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances* inst auto first_offset_value = *(offset_values->begin()); auto basis_curve = inst->BasisCurve(); - auto curve = taxonomy::dcast(map(basis_curve)); + auto curve = taxonomy::dcast(map(basis_curve)); + if (!curve) { + // Only implement on alignment curves + Logger::Warning("IfcOffsetCurveByDistances is only implemented for BasisCurves curves based on taxonomy::function_item", inst); + return nullptr; + } double start = curve->start(); double basis_curve_length = curve->length(); diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index 52187d7e5e..640b5a24f0 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -43,7 +43,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in auto csps = inst->CrossSectionPositions(); std::vector faces; - // The PointByDistanceExpressesions are factored out into (a) a cartesian offset relative to the + // The PointByDistanceExpressions are factored out into (a) a cartesian offset relative to the // reference frame along a certain curve location (b) the longitude. // The longitudes determine the range of the sweep and the offsets are interpolated in between diff --git a/src/ifcopenshell-python/ifcopenshell/alignment.py b/src/ifcopenshell-python/ifcopenshell/alignment.py deleted file mode 100644 index 45eb8bc054..0000000000 --- a/src/ifcopenshell-python/ifcopenshell/alignment.py +++ /dev/null @@ -1,1158 +0,0 @@ -# IfcOpenShell - IFC toolkit and geometry engine -# Copyright (C) 2021 Thomas Krijnen -# -# 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 . - - -import math -from typing import Sequence - -import numpy as np - -import ifcopenshell -import ifcopenshell.geom -import ifcopenshell.guid -import ifcopenshell.template -from ifcopenshell import entity_instance -from ifcopenshell import ifcopenshell_wrapper -import ifcopenshell.util -import ifcopenshell.util.stationing - - -def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np.ndarray: - """ - Calculate the 4x4 geometric transform at a point on an alignment segment - @param shape_rep: The representation shape (composite curve, gradient curve, or segmented reference curve) to evaluate - @param dist_along: The distance along this representation at the point of interest (point to be calculated) - """ - supported_rep_types = ["IFCCOMPOSITECURVE", "IFCGRADIENTCURVE", "IFCSEGMENTEDREFERENCECURVE"] - shape_rep_type = shape_rep.is_a().upper() - if not shape_rep_type in supported_rep_types: - raise NotImplementedError( - f"Expected entity type to be one of {[_ for _ in supported_rep_types]}, got '{shape_rep_type}" - ) - - # TODO: confirm point is not beyond limits of alignment - - s = ifcopenshell.geom.settings() - function_item = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data) - evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) - - trans_matrix = evaluator.evaluate(dist_along) - - return np.array(trans_matrix, dtype=np.float64).T - - -def evaluate_segment(segment: entity_instance, dist_along: float) -> np.ndarray: - """ - Calculate the 4x4 geometric transform at a point on an alignment segment - @param segment: The segment containing the point that we would like to - @param dist_along: The distance along this segment at the point of interest (point to be calculated) - """ - supported_segment_types = ["IFCCURVESEGMENT"] - segment_type = segment.is_a().upper() - if not segment_type in supported_segment_types: - raise NotImplementedError(f"Expected entity type 'IFCCURVESEGMENT', got '{segment_type}") - if dist_along > segment.SegmentLength: - raise ValueError(f"Provided value {dist_along=} is beyond the end of the segment ({segment.SegmentLength}).") - - s = ifcopenshell.geom.settings() - function_item = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data) - evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) - - trans_matrix = evaluator.evaluate(dist_along) - - return np.array(trans_matrix, dtype=np.float64).T - - -def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0) -> np.ndarray: - """ - Generate vertices along an alignment - - @param rep_curve: The alignment's representation curve to use to generate vertices. - - Note: rep_curve must be IfcCompositeCurve, IfcGradientCurve, or IfcSegmentedReferenceCurve - - @param distance_interval: The distance between points along the alignment at which to generate the points - """ - if rep_curve is None: - raise ValueError("Alignment representation not found.") - - s = ifcopenshell.geom.settings() - s.set("piecewise-step-type", 0) # 0 = step-size is maximum step size, 1 = step-size is mininimum number of steps - s.set("piecewise-step-size", distance_interval) - shape = ifcopenshell.geom.create_shape(s, rep_curve) - vertices = shape.verts - if len(vertices) == 0: - msg = f"[ERROR] No vertices generated by ifcopenshell.geom.create_shape()." - raise ValueError(msg) - return np.array(vertices).reshape((-1, 3)) - - -def print_structure(alignment, indent=0): - """ - Debugging function to print alignment decomposition - """ - print(" " * indent, str(alignment)[0:100]) - for rel in alignment.IsNestedBy: - for child in rel.RelatedObjects: - print_structure(child, indent + 2) - - -def name_segments(prefix: str, segments: Sequence[entity_instance]) -> None: - """ - Sets the segment name like ("H1" for horizontal, "V1" for vertical, "C1" for cant) - """ - for i, segment in enumerate(segments): - segment.Name = f"{prefix}{i + 1}" - - -class IfcAlignmentHelper: - """ - Create a new IfcAlignment including horizontal and vertical alignments by PI points. - - Currently only supports horizontal lines and circular arcs (no spirals or other transitions) - Currently only supports parabolic vertical curves. - Does not yet accommodate cant alignment considerations. - """ - - # TODO: add missing functionality noted in the docstring - - def __init__( - self, - file: ifcopenshell.file = None, - filename: str = None, - creator: str = None, - organization: str = None, - application: str = None, - project_globalid=None, - project_name: str = None, - ): - """ - @param file: An existing model that the alignment will be added to - @param filename: Name for a new model to be created that will contain the alignment - @param creator: Name of the actor creating the file - @param organization: Name of the creator's organization - @param application: Name of the authoring application - @param project_globalid: value for the file's IfcProject.GlobalId attribute - @param project_name: value for the file's IfcProject.Name attribute - """ - if file is None: - self._file = ifcopenshell.template.create( - filename=filename, - creator=creator, - organization=organization, - application=application, - project_globalid=project_globalid, - project_name=project_name, - schema_identifier="IFC4X3_ADD2", - ) - else: - self._file = file - - self._geom_context = self._file.by_type("IfcGeometricRepresentationContext")[0] - self._axis_geom_subcontext = self._file.createIfcGeometricRepresentationSubContext( - ContextIdentifier="Axis", ContextType="Model", ParentContext=self._geom_context, TargetView="GRAPH_VIEW" - ) - - def _create_segment_representations( - self, - global_placement: entity_instance, - curve_segments: Sequence[entity_instance], - segments: Sequence[entity_instance], - ): - for curve_segment, alignment_segment in zip(curve_segments, segments): - axis_representation = self._file.create_entity( - type="IfcShapeRepresentation", - ContextOfItems=self._axis_geom_subcontext, - RepresentationIdentifier="Axis", - RepresentationType="Segment", - Items=(curve_segment,), - ) - product = self._file.create_entity( - type="IfcProductDefinitionShape", Name=None, Description=None, Representations=(axis_representation,) - ) - alignment_segment.ObjectPlacement = global_placement - alignment_segment.Representation = product - - def _map_alignment_vertical_segment(self, segment: entity_instance) -> Sequence[entity_instance]: - segment_type = segment.is_a().upper() - expected_type = "IFCALIGNMENTVERTICALSEGMENT" - if not segment_type == expected_type: - raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment_type}'.") - - start_distance_along = segment.StartDistAlong - horizontal_length = segment.HorizontalLength - start_height = segment.StartHeight - start_gradient = segment.StartGradient - end_gradient = segment.EndGradient - radius_of_curvature = segment.RadiusOfCurvature - - if math.isclose(horizontal_length, 0): - # set transition value based on whether this is the final zero-length segment - transition = "DISCONTINUOUS" - else: - transition = "CONTSAMEGRADIENTSAMECURVATURE" - - _type = segment.PredefinedType - - match _type: - case "CONSTANTGRADIENT": - parent_curve = self._file.create_entity( - type="IfcLine", - Pnt=self._file.create_entity( - type="IfcCartesianPoint", - Coordinates=(0.0, 0.0), - ), - Dir=self._file.create_entity( - type="IfcVector", - Orientation=self._file.create_entity( - type="IfcDirection", - DirectionRatios=(1.0, 0.0), - ), - Magnitude=1.0, - ), - ) - - dx = math.cos(math.atan(start_gradient)) - dy = math.sin(math.atan(start_gradient)) - curve_segment_length = horizontal_length / dx - - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=self._file.create_entity( - type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) - ), - RefDirection=self._file.createIfcDirection((dx, dy)), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - - case "PARABOLICARC": - A = start_height - B = start_gradient - C = (end_gradient - start_gradient) / (2.0 * horizontal_length) - - parent_curve = self._file.create_entity( - type="IfcPolynomialCurve", - Position=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=self._file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)), - RefDirection=self._file.createIfcDirection( - (1.0, 0.0), - ), - ), - CoefficientsX=(0.0, 1.0), - CoefficientsY=(A, B, C), - ) - - dx = math.cos(math.atan(start_gradient)) - dy = math.sin(math.atan(start_gradient)) - curve_segment_length = ifcopenshell_wrapper.polynomial_length(A, B, C, horizontal_length) - - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=self._file.create_entity( - type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) - ), - RefDirection=self._file.createIfcDirection((dx, dy)), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - - case "CIRCULARARC": - start_angle = math.atan(start_gradient) - end_angle = math.atan(end_gradient) - if start_angle < end_angle: - radius = horizontal_length / (math.sin(end_angle) - math.sin(start_angle)) - else: - radius = horizontal_length / (math.sin(start_angle) - math.sin(end_angle)) - - parent_curve = self._file.create_entity( - type="IfcCircle", - Position=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=self._file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)), - RefDirection=self._file.createIfcDirection( - (1.0, 0.0), - ), - ), - Radius=radius, - ) - - segment_curve_length = radius * math.fabs(end_angle - start_angle) - - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=self._file.create_entity( - type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height) - ), - RefDirection=self._file.createIfcDirection( - (1.0, 0.0), - ), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(curve_segment_length), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - - case _: - result = (None, None) - - return result - - def _map_alignment_horizontal_segment(self, segment: entity_instance) -> Sequence[entity_instance]: - segment_type = segment.is_a().upper() - expected_type = "IFCALIGNMENTHORIZONTALSEGMENT" - if not segment_type == expected_type: - raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment_type}'.") - - start_point = segment.StartPoint - start_direction = segment.StartDirection - start_radius = segment.StartRadiusOfCurvature - length = segment.SegmentLength - _type = segment.PredefinedType - - if math.isclose(length, 0): - # set transition value based on whether this is the final zero-length segment - transition = "DISCONTINUOUS" - else: - transition = "CONTSAMEGRADIENTSAMECURVATURE" - - if _type == "LINE": - parent_curve = self._file.create_entity( - type="IfcLine", - Pnt=self._file.create_entity( - type="IfcCartesianPoint", - Coordinates=(0.0, 0.0), - ), - Dir=self._file.create_entity( - type="IfcVector", - Orientation=self._file.create_entity( - type="IfcDirection", - DirectionRatios=(1.0, 0.0), - ), - Magnitude=1.0, - ), - ) - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=start_point, - RefDirection=self._file.createIfcDirection( - (math.cos(start_direction), math.sin(start_direction)), - ), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(length), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - elif _type == "CIRCULARARC": - parent_curve = self._file.createIfcCircle( - Position=self._file.createIfcAxis2Placement2D( - Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)), - RefDirection=self._file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), - ), - Radius=abs(start_radius), - ) - - curve_segment = self._file.create_entity( - type="IfcCurveSegment", - Transition=transition, - Placement=self._file.create_entity( - type="IfcAxis2Placement2D", - Location=start_point, - RefDirection=self._file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), - ), - SegmentStart=self._file.createIfcLengthMeasure(0.0), - SegmentLength=self._file.createIfcLengthMeasure(length * start_radius / abs(start_radius)), - ParentCurve=parent_curve, - ) - result = (curve_segment, None) - - else: - result = (None, None) - - return result - - def _create_horizontal_alignment( - self, - name: str, - description: str, - points: Sequence[Sequence[float]], - radii: Sequence[float], - include_geometry: bool = True, - ): - """ - Create a horizontal alignment using the PI layout method. - - @param name: value for Name attribute - @param description: value for Description attribute - @param points: (X, Y) pairs denoting the location of the horizontal PIs, including start (POB) and end (POE). - @param radii: radii values to use for transition - @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic - """ - horizontal_segments = list() # business logic - horizontal_curve_segments = list() # geometry - - xBT, yBT = points[0] - xPI, yPI = points[1] - - i = 1 - - for radius in radii: - # back tangent - dxBT = xPI - xBT - dyBT = yPI - yBT - angleBT = math.atan2(dyBT, dxBT) - lengthBT = math.sqrt(dxBT * dxBT + dyBT * dyBT) - - # forward tangent - i += 1 - xFT, yFT = points[i] - dxFT = xFT - xPI - dyFT = yFT - yPI - angleFT = math.atan2(dyFT, dxFT) - - delta = angleFT - angleBT - - tangent = abs(radius * math.tan(delta / 2)) - - lc = abs(radius * delta) - - radius *= delta / abs(delta) - - xPC = xPI - tangent * math.cos(angleBT) - yPC = yPI - tangent * math.sin(angleBT) - - xPT = xPI + tangent * math.cos(angleFT) - yPT = yPI + tangent * math.sin(angleFT) - - tangent_run = lengthBT - tangent - - # create back tangent run - pt = self._file.create_entity( - type="IfcCartesianPoint", - Coordinates=(xBT, yBT), - ) - design_parameters = self._file.create_entity( - type="IfcAlignmentHorizontalSegment", - StartTag=None, - EndTag=None, - StartPoint=pt, - StartDirection=angleBT, - StartRadiusOfCurvature=0.0, - EndRadiusOfCurvature=0.0, - SegmentLength=tangent_run, - GravityCenterLineHeight=None, - PredefinedType="LINE", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - horizontal_segments.append(alignment_segment) - - if include_geometry: - horizontal_curve_segments.append(self._map_alignment_horizontal_segment(design_parameters)[0]) - - # create circular curve - pc = self._file.create_entity( - type="IfcCartesianPoint", - Coordinates=(xPC, yPC), - ) - design_parameters = self._file.create_entity( - type="IfcAlignmentHorizontalSegment", - StartTag=None, - EndTag=None, - StartPoint=pc, - StartDirection=angleBT, - StartRadiusOfCurvature=float(radius), - EndRadiusOfCurvature=float(radius), - SegmentLength=lc, - GravityCenterLineHeight=None, - PredefinedType="CIRCULARARC", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - horizontal_segments.append(alignment_segment) - - if include_geometry: - horizontal_curve_segments.append(self._map_alignment_horizontal_segment(design_parameters)[0]) - - xBT = xPT - yBT = yPT - xPI = xFT - yPI = yFT - - # done processing radii - # create last tangent run - dx = xPI - xBT - dy = yPI - yBT - angleBT = math.atan2(dy, dx) - tangent_run = math.sqrt(dx * dx + dy * dy) - pt = self._file.create_entity(type="IfcCartesianPoint", Coordinates=(xBT, yBT)) - - design_parameters = self._file.create_entity( - type="IfcAlignmentHorizontalSegment", - StartTag=None, - EndTag=None, - StartPoint=pt, - StartDirection=angleBT, - StartRadiusOfCurvature=0.0, - EndRadiusOfCurvature=0.0, - SegmentLength=tangent_run, - GravityCenterLineHeight=None, - PredefinedType="LINE", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - horizontal_segments.append(alignment_segment) - if include_geometry: - horizontal_curve_segments.append(self._map_alignment_horizontal_segment(design_parameters)[0]) - - # create zero length terminator segment - poe = self._file.create_entity(type="IfcCartesianPoint", Coordinates=(xPI, yPI)) - - design_parameters = self._file.create_entity( - type="IfcAlignmentHorizontalSegment", - StartTag="POE", - EndTag="POE", - StartPoint=poe, - StartDirection=angleBT, - StartRadiusOfCurvature=0.0, - EndRadiusOfCurvature=0.0, - SegmentLength=0.0, - GravityCenterLineHeight=None, - PredefinedType="LINE", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - horizontal_segments.append(alignment_segment) - if include_geometry: - horizontal_curve_segments.append(self._map_alignment_horizontal_segment(design_parameters)[0]) - - if include_geometry: - composite_curve = self._file.create_entity( - type="IfcCompositeCurve", - Segments=horizontal_curve_segments, - SelfIntersect=False, - ) - else: - composite_curve = None - - return horizontal_segments, horizontal_curve_segments, composite_curve - - def _add_horizontal_alignment( - self, - alignment_name: str, - points: Sequence[Sequence[float]], - radii: Sequence[float], - include_geometry: bool = True, - alignment_description: str = None, - start_station: float = 1000.0, - ): - horizontal_segments, horizontal_curve_segments, composite_curve = self._create_horizontal_alignment( - alignment_name, - alignment_description, - points, - radii, - include_geometry, - ) - - name_segments(prefix="H", segments=horizontal_segments) - - # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments - horizontal_alignment = self._file.create_entity( - type="IfcAlignmentHorizontal", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=f"{alignment_name} - Horizontal", - Description=alignment_description, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - ) - - nests_horizontal_segments = self._file.create_entity( - type="IfcRelNests", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Nests horizontal alignment segments under horizontal alignment", - RelatingObject=horizontal_alignment, - RelatedObjects=horizontal_segments, - ) - - placement = self._file.createIfcLocalPlacement( - PlacementRelTo=None, - RelativePlacement=self._file.createIfcAxis2Placement2D( - Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)) - ), - ) - - # create the alignment - alignment = self._file.create_entity( - type="IfcAlignment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=alignment_name, - Description=alignment_description, - ObjectType=None, - ObjectPlacement=placement, - Representation=None, - PredefinedType=None, - ) - - # create geometric representation - if include_geometry: - # create the footprint representation - footprint_shape_representation = self._file.create_entity( - type="IfcShapeRepresentation", - ContextOfItems=self._axis_geom_subcontext, - RepresentationIdentifier="FootPrint", - RepresentationType="Curve2D", - Items=(composite_curve,), - ) - - # create the alignment product definition - product_definition_shape = self._file.create_entity( - type="IfcProductDefinitionShape", - Name="Alignment Product Definition Shape", - Description=None, - Representations=(footprint_shape_representation,), - ) - - # create representations for each segment - self._create_segment_representations(placement, horizontal_curve_segments, horizontal_segments) - - # add the representation to the alignment - alignment.Representation = product_definition_shape - - # create referent for start station - start_station_name = "Start Station ({})".format( - ifcopenshell.util.stationing.station_as_string(start_station) - ) - start_referent = self._file.createIfcReferent( - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=start_station_name, - Description=None, - ObjectType=None, - ObjectPlacement=self._file.createIfcLinearPlacement( - RelativePlacement=self._file.createIfcAxis2PlacementLinear( - Location=self._file.createIfcPointByDistanceExpression( - DistanceAlong=self._file.createIfcLengthMeasure(0.0), - OffsetLateral=None, - OffsetVertical=None, - OffsetLongitudinal=None, - BasisCurve=composite_curve, - ), - ), - CartesianPosition=None, - ), - Representation=None, - PredefinedType="STATION", - ) - pset_stationing = ifcopenshell.api.pset.add_pset(self._file, product=start_referent, name="Pset_Stationing") - ifcopenshell.api.pset.edit_pset(self._file, pset=pset_stationing, properties={"Station": start_station}) - - # nest the horizontal and the referent under the alignment - nesting_of_alignment = self._file.create_entity( - type="IfcRelNests", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Nests horizontal alignment and referents under overall alignment", - RelatingObject=alignment, - RelatedObjects=(horizontal_alignment, start_referent), - ) - - # aggregate the horizontal under the project - project = self._file.by_type("IfcProject")[0] - alignment_within_project = self._file.createIfcRelAggregates( - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Aggregates alignment under the project", - RelatingObject=project, - RelatedObjects=(alignment,), - ) - - return alignment - - def _create_vertical_alignment( - self, - composite_curve: entity_instance, - vpoints: Sequence[Sequence[float]], - lengths: Sequence[float], - include_geometry: bool = True, - ): - """ - Create a vertical alignment using the PI layout method. - - @param name: value for Name attribute - @param description: value for Description attribute - @param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. - @param vclengths: horizontal length of parabolic vertical curves - @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic - """ - vertical_segments = list() # business logic - vertical_curve_segments = list() # geometry - xPBG, yPBG = vpoints[0] - xPVI, yPVI = vpoints[1] - i = 1 - for length in lengths: - # back gradient - dxBG = xPVI - xPBG - dyBG = yPVI - yPBG - start_slope = math.tan(math.atan2(dyBG, dxBG)) - - # forward gradient - i += 1 - xPFG, yPFG = vpoints[i] - dxFG = xPFG - xPVI - dyFG = yPFG - yPVI - end_slope = math.tan(math.atan2(dyFG, dxFG)) - - xEVC = xPVI + length / 2.0 - yEVC = yPVI + end_slope * length / 2.0 - - # create gradient - gradient_length = dxBG - length / 2.0 - design_parameters = self._file.create_entity( - type="IfcAlignmentVerticalSegment", - StartTag=None, - EndTag=None, - StartDistAlong=xPBG, - HorizontalLength=gradient_length, - StartHeight=yPBG, - StartGradient=start_slope, - EndGradient=start_slope, - RadiusOfCurvature=None, - PredefinedType="CONSTANTGRADIENT", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - vertical_segments.append(alignment_segment) - - if include_geometry: - vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) - - # create vertical curve - k = (end_slope - start_slope) / length - xBVC = xPVI - length / 2.0 - yBVC = yPVI - start_slope * length / 2.0 - - design_parameters = self._file.create_entity( - type="IfcAlignmentVerticalSegment", - StartTag=None, - EndTag=None, - StartDistAlong=xBVC, - HorizontalLength=length, - StartHeight=yBVC, - StartGradient=start_slope, - EndGradient=end_slope, - RadiusOfCurvature=1 / k, - PredefinedType="PARABOLICARC", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - vertical_segments.append(alignment_segment) - - if include_geometry: - vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) - - # start of next curve is end of this curve - xPBG = xEVC - yPBG = yEVC - xPVI = xPFG - yPVI = yPFG - - # create last gradient run - dx = xPVI - xPBG - dy = yPVI - yPBG - slope = math.tan(math.atan2(dy, dx)) - gradient_length = dx - - design_parameters = self._file.create_entity( - type="IfcAlignmentVerticalSegment", - StartTag=None, - EndTag=None, - StartDistAlong=xPBG, - HorizontalLength=gradient_length, - StartHeight=yPBG, - StartGradient=slope, - EndGradient=slope, - RadiusOfCurvature=None, - PredefinedType="CONSTANTGRADIENT", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - vertical_segments.append(alignment_segment) - - if include_geometry: - vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) - - # create zero length terminator segment - design_parameters = self._file.create_entity( - type="IfcAlignmentVerticalSegment", - StartTag="VPOE", - EndTag="VPOE", - StartDistAlong=xPVI, - HorizontalLength=0.0, - StartHeight=yPVI, - StartGradient=slope, - EndGradient=slope, - RadiusOfCurvature=None, - PredefinedType="CONSTANTGRADIENT", - ) - alignment_segment = self._file.create_entity( - type="IfcAlignmentSegment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=None, - Description=None, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - DesignParameters=design_parameters, - ) - vertical_segments.append(alignment_segment) - - if include_geometry: - vertical_curve_segments.append(self._map_alignment_vertical_segment(design_parameters)[0]) - - if include_geometry: - gradient_curve = self._file.create_entity( - type="IfcGradientCurve", - Segments=vertical_curve_segments, - SelfIntersect=False, - BaseCurve=composite_curve, - EndPoint=None, - ) - else: - gradient_curve = None - - return vertical_segments, vertical_curve_segments, gradient_curve - - def create_alignment_by_pi_method( - self, - alignment_name: str, - points: Sequence[Sequence[float]], - radii: Sequence[float], - vpoints: Sequence[Sequence[float]], - lengths: Sequence[float], - alignment_description: str = None, - start_station: float = 1000.0, - include_geometry: bool = True, - ): - """ - Create an alignment using the PI layout method for both horizontal and vertical alignments. - - @param alignment_name: value for Name attribute - @param alignment_description: value for Description attribute - @param points: (X,Y) pairs denoting the location of the horizontal PIs, including start and end - @param radii: radii values to use for transition - @param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. - @param lengths: parabolic vertical curve horizontal length values to use for transition - @param start_station: ??? NOT USED AT THIS TIME ??? - @param include_geometry: optionally create the alignment geometric representation as well as the semantic business logic - """ - - horizontal_segments, horizontal_curve_segments, composite_curve = self._create_horizontal_alignment( - alignment_name, alignment_description, points, radii, include_geometry - ) - vertical_segments, vertical_curve_segments, gradient_curve = self._create_vertical_alignment( - composite_curve, vpoints, lengths - ) - - name_segments(prefix="H", segments=horizontal_segments) - name_segments(prefix="V", segments=vertical_segments) - - # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments - horizontal_alignment = self._file.create_entity( - type="IfcAlignmentHorizontal", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=f"{alignment_name} - Horizontal", - Description=alignment_description, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - ) - - nests_horizontal_segments = self._file.create_entity( - type="IfcRelNests", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Nests horizontal alignment segments under horizontal alignment", - RelatingObject=horizontal_alignment, - RelatedObjects=horizontal_segments, - ) - - # Create the vertical alignment (IfcAlignmentVertical) and nest alignment segments - vertical_alignment = self._file.create_entity( - type="IfcAlignmentVertical", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=f"{alignment_name} - Vertical", - Description=alignment_description, - ObjectType=None, - ObjectPlacement=None, - Representation=None, - ) - - nests_vertical_segments = self._file.create_entity( - type="IfcRelNests", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Nests vertical alignment segments under vertical alignment", - RelatingObject=vertical_alignment, - RelatedObjects=vertical_segments, - ) - - # create the alignment - placement = self._file.createIfcLocalPlacement( - PlacementRelTo=None, - RelativePlacement=self._file.createIfcAxis2Placement2D( - Location=self._file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)) - ), - ) - - alignment = self._file.create_entity( - type="IfcAlignment", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=alignment_name, - Description=alignment_description, - ObjectType=None, - ObjectPlacement=placement, - Representation=None, - PredefinedType=None, - ) - - # create referent for start station - start_station_name = "Start Station ({})".format(ifcopenshell.util.stationing.station_as_string(start_station)) - start_referent = self._file.createIfcReferent( - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name=start_station_name, - Description=None, - ObjectType=None, - ObjectPlacement=self._file.createIfcLinearPlacement( - RelativePlacement=self._file.createIfcAxis2PlacementLinear( - Location=self._file.createIfcPointByDistanceExpression( - DistanceAlong=self._file.createIfcLengthMeasure(0.0), - OffsetLateral=None, - OffsetVertical=None, - OffsetLongitudinal=None, - BasisCurve=composite_curve, - ), - ), - CartesianPosition=None, - ), - Representation=None, - PredefinedType="STATION", - ) - pset_stationing = ifcopenshell.api.pset.add_pset(self._file, product=start_referent, name="Pset_Stationing") - ifcopenshell.api.pset.edit_pset(self._file, pset=pset_stationing, properties={"Station": start_station}) - - # nest the horizontal, vertical and the referent under the alignment - nesting_of_alignment = self._file.create_entity( - type="IfcRelNests", - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Nests horizontal alignment, vertical alginment, and referents under overall alignment", - RelatingObject=alignment, - RelatedObjects=(horizontal_alignment, vertical_alignment, start_referent), - ) - - # aggregate the alignment under the project - project = self._file.by_type("IfcProject")[0] - alignment_within_project = self._file.createIfcRelAggregates( - GlobalId=ifcopenshell.guid.new(), - OwnerHistory=None, - Name="Aggregates alignment under the project", - RelatingObject=project, - RelatedObjects=(alignment,), - ) - - # create geometric representation - if include_geometry: - # create the footprint representation - footprint_shape_representation = self._file.create_entity( - type="IfcShapeRepresentation", - ContextOfItems=self._axis_geom_subcontext, - RepresentationIdentifier="FootPrint", - RepresentationType="Curve2D", - Items=(composite_curve,), - ) - - # create the Curve3D representation - axis3d_shape_representation = self._file.create_entity( - type="IfcShapeRepresentation", - ContextOfItems=self._axis_geom_subcontext, - RepresentationIdentifier="Axis", - RepresentationType="Curve3D", - Items=(gradient_curve,), - ) - - # create the alignment product definition - product_definition_shape = self._file.create_entity( - type="IfcProductDefinitionShape", - Name="Alignment Product Definition Shape", - Description=None, - Representations=( - footprint_shape_representation, - axis3d_shape_representation, - ), - ) - - # create representations for each segment - self._create_segment_representations(placement, horizontal_curve_segments, horizontal_segments) - self._create_segment_representations(placement, vertical_curve_segments, vertical_segments) - - # add the representation to the alignment - alignment.Representation = product_definition_shape - - return alignment - - def create_horizontal_alignment_by_pi_method( - self, - name: str, - hpoints: Sequence[Sequence[float]], - radii: Sequence[float], - include_geometry: bool = True, - description: str = None, - start_station: float = 1000.0, - ): - """ - Create a new alignment with a horizontal alignment using the PI layout method - """ - return self._add_horizontal_alignment( - alignment_name=name, - points=hpoints, - radii=radii, - include_geometry=include_geometry, - alignment_description=description, - start_station=start_station, - ) - - def save_file(self, filename) -> None: - self._file.write(filename) - - -if __name__ == "__main__": - import sys - from matplotlib import pyplot as plt - - f = ifcopenshell.file(schema="IFC4X3_ADD2") - project = f.create_entity(type="IfcProject", GlobalId=ifcopenshell.guid.new()) - context = f.create_entity(type="IfcGeometricRepresentationContext") - - points = [(0.0, 0.0), (100.0, 0.0), (200.0, 150.0)] - radii = [50.0] - - helper = IfcAlignmentHelper(f) - helper.create_horizontal_alignment_by_pi_method(name="MyAlignment", hpoints=points, radii=radii) - - # f = ifcopenshell.open(sys.argv[1]) - print_structure(f.by_type("IfcAlignment")[0]) - - al_hor_rep = f.by_type("IfcCompositeCurve")[0] - - xy = generate_vertices(rep_curve=al_hor_rep, distance_interval=10.0) - - plt.plot(xy[0], xy[1]) - plt.savefig("horizontal_alignment.png") diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py new file mode 100644 index 0000000000..1098c23fe6 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py @@ -0,0 +1,70 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2022 Dion Moult +# +# 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 . + +""" +Manages alignment layout (business logical) and alignment geometry (geometric representations). + +This API does not determine alignment parameters based on rules, such as minimum curve radius as a function of design speed or sight distance. + +This API is under development and subject to code breaking changes in the future. + +Presently, this API supports: + 1. Creating alignments, both horizontal and vertical, using the PI method. Alignment definition can be read from a CSV file. + 2. Adding business logic and geometric segments to the end of an alignment + 3. Adding and removing the zero length segment at the end of alignments + 4. Creating geometric representations from a business logical definition + 5. Mapping individual business logical segments to geometric segments (complete for horizontal, missing clothoid for vertical, not implemented for cant) + 6. Using curve geometry to determine IfcCurveSegment.Transition transition code. + 7. Utility functions for printing business logical and geometric representations, as well as minimumal geometry evaluations + +Future versions of this API will support: + 1. Defining alignments using the PI method, including transition spirals + 2. Updating horizontal curve definitions by revising transition spiral parameters and circular curve radii + 3. Updating vertical curve definitions by revising horizontal length of curves + 4. Removing a segment at any location along a curve + 5. Adding a segment at any location along a curve +""" + +from .add_segment_to_curve import add_segment_to_curve +from .add_segment_to_layout import add_segment_to_layout +from .add_stationing_to_alignment import add_stationing_to_alignment +from .add_vertical_alignment_by_pi_method import add_vertical_alignment_by_pi_method +from .add_vertical_alignment import add_vertical_alignment +from .add_zero_length_segment import add_zero_length_segment +from .create_alignment_by_pi_method import create_alignment_by_pi_method +from .create_alignment_from_csv import create_alignment_from_csv +from .create_horizontal_alignment_by_pi_method import create_horizontal_alignment_by_pi_method +from .create_geometric_representation import create_geometric_representation +from .create_vertical_alignment_by_pi_method import create_vertical_alignment_by_pi_method +from .get_alignment_layouts import get_alignment_layouts +from .get_axis_subcontext import get_axis_subcontext +from .get_basis_curve import get_basis_curve +from .get_child_alignments import get_child_alignments +from .get_curve import get_curve +from .get_parent_alignment import get_parent_alignment +from .has_zero_length_segment import has_zero_length_segment +from .map_alignment_segments import map_alignment_segments +from .map_alignment_segment import map_alignment_segment +from .map_alignment_horizontal_segment import map_alignment_horizontal_segment +from .map_alignment_vertical_segment import map_alignment_vertical_segment +from .map_alignment_cant_segment import map_alignment_cant_segment +from .name_segments import name_segments +from .remove_last_segment import remove_last_segment +from .remove_zero_length_segment import remove_zero_length_segment +from .update_curve_segment_transition_code import update_curve_segment_transition_code +from .util import * diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_curve.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_curve.py new file mode 100644 index 0000000000..7b8370fdcc --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_curve.py @@ -0,0 +1,76 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +import ifcopenshell.geom +from ifcopenshell import entity_instance + + +def add_segment_to_curve(file: ifcopenshell.file, segment: entity_instance, composite_curve: entity_instance) -> None: + """ + Adds a segment to a composite curve. The segment must not belong to another composite curve (len(segment.UsingCurves) == 0). + If the composite curve does not have any segments, the segment is simply appended to the curve. + If the composite curve has segments, the position, ref. direction, and curvature at the end of the last segment is + compared to the position, ref. direction and curvature at the start of the new segment. The IfcCurveSegment.Transition of the last curve segment is updated. + + :param segment: The segment to be added to the curve + :param composite_curve: The curve receiving the segment + :return: None + """ + expected_type = "IfcCurveSegment" + if not segment.is_a(expected_type): + raise TypeError(f"Expected to see '{expected_type}', instead received '{segment.is_a()}'.") + + if 0 < len(segment.UsingCurves): + raise TypeError("IfcCurveSegment cannot belong to other curves") + + expected_type = "IfcCompositeCurve" + if not composite_curve.is_a(expected_type): + raise TypeError(f"Expected to see '{expected_type}', instead received '{composite_curve.is_a()}'.") + + settings = ifcopenshell.geom.settings() + if composite_curve.Segments == None or 0 == len(composite_curve.Segments): + # this is the first segment so just add it + if composite_curve.Segments == None: + composite_curve.Segments = [] + + # the last segment is always discontinuous + segment.Transition = "DISCONTINUOUS" + + composite_curve.Segments += (segment,) + assert len(segment.UsingCurves) == 1 + else: + zero_length_segment = ( + ifcopenshell.api.alignment.remove_zero_length_segment(file, composite_curve) + if ifcopenshell.api.alignment.has_zero_length_segment(composite_curve) + else None + ) + + prev_segment = composite_curve.Segments[-1] + + # the last segment is always discontinuous + segment.Transition = "DISCONTINUOUS" + + # must add the new segment to the curve before updating the transition code + composite_curve.Segments += (segment,) + + ifcopenshell.api.alignment.update_curve_segment_transition_code(prev_segment, segment) + + if zero_length_segment: + ifcopenshell.api.alignment.add_segment_to_curve(zero_length_segment, composite_curve) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_layout.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_layout.py new file mode 100644 index 0000000000..c92e475106 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_segment_to_layout.py @@ -0,0 +1,52 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +import ifcopenshell.api.nest +from ifcopenshell import entity_instance +from typing import Sequence + + +def add_segment_to_layout(file: ifcopenshell.file, alignment: entity_instance, segment: entity_instance) -> None: + """ + Adds a segment to a layout alignment (horizontal, vertical, or cant) + + :param alignment: The alignment + :param segment: The segment to be appended + :return: None + """ + expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"] + if not alignment.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{alignment.is_a()}" + ) + + if not (segment.is_a("IfcAlignmentSegment")): + raise TypeError(f"Expected to see IfcAlignmentSegment, instead received '{segment.is_a()}.") + + zero_length_segment = ( + ifcopenshell.api.alignment.remove_zero_length_segment(file, alignment) + if ifcopenshell.api.alignment.has_zero_length_segment(alignment) + else None + ) + + ifcopenshell.api.nest.assign_object(file, related_objects=[segment], relating_object=alignment) + + if zero_length_segment: + ifcopenshell.api.nest.assign_object(file, related_objects=[zero_length_segment], relating_object=alignment) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_to_alignment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_to_alignment.py new file mode 100644 index 0000000000..ffc42c70d8 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_stationing_to_alignment.py @@ -0,0 +1,79 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +import ifcopenshell.api.nest +import ifcopenshell.guid +from ifcopenshell import entity_instance + + +def add_stationing_to_alignment(file: ifcopenshell.file, alignment: entity_instance, start_station: float) -> None: + """ + Adds stationing to an alignment by creating an IfcReferent with the Pset_Stationing property set to establish the stationing at the start of the alignment. + Note - this function assumes the stationing has not been previously defined + + :param alignment: the alignment to be stationed + :param start_station: station value at the start of the alignment + :return: None + + Example: + + .. code:: python + + alignment = model.by_type("IfcAlignment")[0] + ifcopenshell.api.alignment.add_stationing_to_alignment(model,alignment=alignment,start_station=100.0) + """ + # this commented out code is what you would do to add a geometric representation of the referent + # the example is a circle. a better way would be to pass a representation into the function + object_placement = None + representation = None + # basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + # if basis_curve: + # object_placement = file.createIfcLinearPlacement( + # RelativePlacement=file.createIfcAxis2PlacementLinear( + # Location=file.createIfcPointByDistanceExpression( + # DistanceAlong=file.createIfcLengthMeasure(0.0), + # OffsetLateral=None, + # OffsetVertical=None, + # OffsetLongitudinal=None, + # BasisCurve=basis_curve, + # ) + # ), + # CartesianPosition=None, + # ) + # representation = file.create_entity( + # name="IfcCircle", + # position=file.createIfcAxis2Placement2D(Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)), + # radius=1.0) + # ) + + # create referent for start station + start_referent = file.createIfcReferent( + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=ifcopenshell.util.stationing.station_as_string(start_station), + Description=None, + ObjectType=None, + ObjectPlacement=object_placement, + Representation=representation, + PredefinedType="STATION", + ) + pset_stationing = ifcopenshell.api.pset.add_pset(file, product=start_referent, name="Pset_Stationing") + ifcopenshell.api.pset.edit_pset(file, pset=pset_stationing, properties={"Station": start_station}) + ifcopenshell.api.nest.assign_object(file, related_objects=[start_referent], relating_object=alignment) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment.py new file mode 100644 index 0000000000..d2f7d228a8 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment.py @@ -0,0 +1,198 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.aggregate +import ifcopenshell.api.alignment +import ifcopenshell.api.geometry +import ifcopenshell.api.nest +import ifcopenshell.guid +import ifcopenshell.util.element +import ifcopenshell.util.representation +import ifcopenshell.util.stationing +import ifcopenshell.api +from ifcopenshell import entity_instance + + +def _move_vertical_to_child_alignment( + file: ifcopenshell.file, parent_alignment: entity_instance, vertical_alignment: entity_instance +): + """ + Creates a new child alignment and aggregates it to the parent alignment. Moves the vertical alignment from the parent + alignment to the child alignment. Also moves the "Axis/Curve3D" representation to the child alignment, if present. + This function supports the transition of vertical alignment between CT 4.1.4.4.1.1 and 4.1.4.4.1.2 because a subsequent + vertical alignment is being added and the Alignment Layout - Reusing Horizontal Layout concept applies. + """ + # unhook the vertical alignment from the parent alignment + ifcopenshell.api.nest.unassign_object(file, related_objects=[vertical_alignment]) + + # create the child alignment + child_alignment = ifcopenshell.api.root.create_entity( + file, ifc_class="IfcAlignment", name=f"Child of {parent_alignment.Name}" + ) + + # nest the vertical alignment onto the child alignment + ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_alignment], relating_object=child_alignment) + + # aggreage the child alignment to the parent alignment + ifcopenshell.api.aggregate.assign_object(file, products=[child_alignment], relating_object=parent_alignment) + + # if the parent alignment has a representation, move the Axis/Curve3D represention to the child alignment + base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment) + if base_curve: + representations = ifcopenshell.util.representation.get_representations_iter(parent_alignment) + for representation in representations: + if representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve3D": + ifcopenshell.api.geometry.unassign_representation(file, parent_alignment, representation) + ifcopenshell.api.geometry.assign_representation(file, child_alignment, representation) + break + + +def add_vertical_alignment( + file: ifcopenshell.file, parent_alignment: entity_instance, vertical_alignment: entity_instance +) -> None: + """ + Adds a vertical alignment to a previously created alignment. + + If this is the first vertical alignment assigned to the parent_alignment the IFC CT 4.1.4.4.1.1 Alignment Layout - Horizontal, Vertical and Cant + is followed. If this is the second or subsequent vertical alignment assigned to the parent_alignment the + IFC CT 4.1.4.4.1.2 Alignment Layout - Reusing Horizontal Layout is followed. + + When the second vertical alignment is added, the structure of the IFC model must transition from one concept template to the other. + Specifically, the following occurs: + + 1) The first child IfcAlignment is created and is IfcRelAggregates with the parent alignment. + 2) The first vertical alignment is unassigned from the IfcRelNests of the parent alignment and assigned to the new child alignment IfcRelNests + 3) A second child IfcAlignment is created ant is is IfcRelAggregates with the parent alignment. + 4) The vertical_alignment is assigned to the second child alignment + + For the third and subsequent vertical alignments, a new child alignment is created and aggregated to the parent alignment and an IfcAlignmentVertical is created + from vpoints and lengths and assigned to the new child alignment. + + If the parent_alignment has a geometric representation, a geometric representation will be created for the vertical alignment. + + :param parent_alignment: The parent alignment + :param vertical_alignment: The vertical alignment to be added + :return: None + """ + + # get all the child alignments under alignment + child_alignments = [ + c for c in ifcopenshell.util.element.get_decomposition(parent_alignment) if c.is_a("IfcAlignment") + ] + + # Get all the IfcAlignmentVertical that are nesting alignment (there should be 0 or 1) + # if 0, alignment is just horizontal and we are adding the first vertical so it will nest to the alignment, + # or there are multiple vertical and they nest to the aggregated child alignments + # if 1, there is one vertical alignments. Move it to a child alignment + vertical_alignments_nesting_alignment = [ + c for c in ifcopenshell.util.element.get_components(parent_alignment) if c.is_a("IfcAlignmentVertical") + ] + + # move the vertical alignment to a child alignment because there is going to be more than one vertical + assert len(vertical_alignments_nesting_alignment) == 0 or len(vertical_alignments_nesting_alignment) == 1 + for vertical_alignment_nesting_alignment in vertical_alignments_nesting_alignment: + _move_vertical_to_child_alignment(file, parent_alignment, vertical_alignment_nesting_alignment) + + if len(child_alignments) == 0 and len(vertical_alignments_nesting_alignment) == 0: + # this is the first vertical alignment so nest it into the parent alignment (IFC CT 4.1.4.4.1.1) + ifcopenshell.api.nest.assign_object( + file, related_objects=[vertical_alignment], relating_object=parent_alignment + ) + + base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment) + if base_curve: + # the parent alignment has a Representation so create a representation for the vertical + gradient_curve = file.create_entity( + type="IfcGradientCurve", Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None + ) + + # using the business logic definition of vertical_alignment, create the curve segments and assign to gradient_curve + ifcopenshell.api.alignment.map_alignment_segments(file, vertical_alignment, gradient_curve) + + # Per IFC CT 4.1.7.1.1.1, the shape representation for Horizontal geometry only is + # RepresentationIdentifier="Axis" and RepresentationType="Curve2D". + # However, per IFC CT 4.1.7.1.1.2 and 3 the shape represenation with Horizontal, Vertical and Cant + # is RepresentationIdentifier="FootPrint" and RepresentationType="Curve2D" for the horizontal and + # RepresentationIdentifier="Axis" and RepresentationType="Curve3D" for the 2.5D curve. + # Since the alignment is transitioning from horizontal only to horizontal+vertical, the + # RepresentationIdentifier must change from "Axis" to "FootPrint" + representations = ifcopenshell.util.representation.get_representations_iter(parent_alignment) + for representation in representations: + if representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D": + representation.RepresentationIdentifier = "FootPrint" + break + + # create the Axis,Curve3D representation + axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) + axis3d_shape_representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(gradient_curve,), + ) + + ifcopenshell.api.geometry.assign_representation(file, parent_alignment, axis3d_shape_representation) + else: + # there are multiple vertical reusing the horizontal (IFC CT 4.1.4.4.1.2) + # this is the second or subsequent vertical reusing the horizontal + + # create a new child alignment for the new vertical + child_alignment = file.create_entity( + type="IfcAlignment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=f"Child of {parent_alignment.Name}", + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + PredefinedType=None, + ) + + # Aggregate the child alignment to the parent alignment + ifcopenshell.api.aggregate.assign_object(file, (child_alignment,), parent_alignment) + + # nest the vertical under the child alignment + ifcopenshell.api.nest.assign_object(file, related_objects=[vertical_alignment], relating_object=child_alignment) + + base_curve = ifcopenshell.api.alignment.get_basis_curve(parent_alignment) + if base_curve: + child_alignment.ObjectPlacement = parent_alignment.ObjectPlacement + + # the parent alignment has a Representation so create a representation for the vertical + gradient_curve = file.create_entity( + type="IfcGradientCurve", Segments=[], SelfIntersect=False, BaseCurve=base_curve, EndPoint=None + ) + + ifcopenshell.api.alignment.map_alignment_segments(file, vertical_alignment, gradient_curve) + + axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) + + # create the Curve3D representation + axis3d_shape_representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(gradient_curve,), + ) + + # add the representation to the child alignment + ifcopenshell.api.geometry.assign_representation(file, child_alignment, axis3d_shape_representation) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment_by_pi_method.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment_by_pi_method.py new file mode 100644 index 0000000000..67d55f70dc --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_vertical_alignment_by_pi_method.py @@ -0,0 +1,59 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment.add_vertical_alignment +from ifcopenshell import entity_instance +from typing import Sequence + + +def add_vertical_alignment_by_pi_method( + file: ifcopenshell.file, + parent_alignment: entity_instance, + vpoints: Sequence[Sequence[float]], + lengths: Sequence[float], +) -> None: + """ + Adds a vertical alignment to a previously created alignment using the PI method. + + If this is the first vertical alignment assigned to the parent_alignment the IFC CT 4.1.4.4.1.1 Alignment Layout - Horizontal, Vertical and Cant + is followed. If this is the second or subsequent vertical alignment assigned to the parent_alignment the + IFC CT 4.1.4.4.1.2 Alignment Layout - Reusing Horizontal Layout is followed. + + When the second vertical alignment is added, the structure of the IFC model must transition from one concept template to the other. + Specifically, the following occurs: + + 1) The first child IfcAlignment is created and is IfcRelAggregates with the parent alignment. + 2) The first vertical alignment is unassigned from the IfcRelNests of the parent alignment and assigned to the new child alignment IfcRelNests + 3) A second child IfcAlignment is created and it is IfcRelAggregates with the parent alignment. + 4) An IfcAlignmentVertical is created from vpoints and lengths and it is assigned to the second child alignment + + For the third and subsequent vertical alignments, a new child alignment is created and aggregated to the parent alignment and an IfcAlignmentVertical is created + from vpoints and lengths and assigned to the new child alignment. + + If the parent_alignment has a geometric representation, a geometric representation will be created for the vertical alignment. + + :param parent_alignment: The parent alignment + :param vpoints: A sequence of (D,Z) points where D is distance along horizontal and Z is elevation + :param: lengths: Lengths of parabolic vertical curves occuring at each VPI + :return: None + """ + vertical_alignment = ifcopenshell.api.alignment.create_vertical_alignment_by_pi_method( + file, parent_alignment.Name, vpoints, lengths + ) + ifcopenshell.api.alignment.add_vertical_alignment(file, parent_alignment, vertical_alignment) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py new file mode 100644 index 0000000000..a161757213 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/add_zero_length_segment.py @@ -0,0 +1,101 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +import ifcopenshell.api.nest +import ifcopenshell.geom +import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper +import numpy as np +from ifcopenshell import entity_instance + + +def add_zero_length_segment(file: ifcopenshell.file, entity: entity_instance) -> None: + """ + Adds a zero length segment to the end of entity. + + :param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve (or subtype) + :return: None + """ + expected_types = [ + "IfcAlignmentHorizontal", + "IfcAlignmentVertical", + "IfcAlignmentCant", + "IfcCompositeCurve", + "IfcGradientCurve", + "IfcSegmentedReferenceCurve", + ] + if not entity.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{entity.is_a()}" + ) + + if entity.is_a("IfcCompositeCurve"): + last_segment = entity.Segments[-1] + settings = ifcopenshell.geom.settings() + segment_fn = ifcopenshell_wrapper.map_shape(settings, last_segment.wrapped_data) + segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn) + e = segment_evaluator.evaluate(segment_fn.end()) + end = np.array(e) + x = float(end[0, 3]) + y = float(end[1, 3]) + dx = float(end[0, 0]) + dy = float(end[1, 0]) + + parent_curve = file.createIfcLine( + Pnt=file.createIfcCartesianPoint(Coordinates=((0.0, 0.0))), + Dir=file.createIfcVector( + Orientation=file.createIfcDirection(DirectionRatios=((1.0, 0.0))), + Magnitude=1.0, + ), + ) + curve_segment = file.createIfcCurveSegment( + Transition="DISCONTINUOUS", + Placement=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint(Coordinates=((x, y))), + RefDirection=file.createIfcDirection((dx, dy)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(0.0), + ParentCurve=parent_curve, + ) + ifcopenshell.api.alignment.add_segment_to_curve(file, curve_segment, entity) + else: + for rel in entity.IsNestedBy: + if 0 < len(rel.RelatedObjects): + last_segment = rel.RelatedObjects[-1] + if last_segment.is_a("IfcAlignmentSegment"): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint( + (0.0, 0.0) + ), # this is a little problematic. need to know the end point and tangent + StartDirection=0.0, # of the previous segment, which requires geometry mapping + SegmentLength=0.0, + PredefinedType="LINE", + ) + segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + ifcopenshell.api.nest.assign_object( + file, + related_objects=[ + segment, + ], + relating_object=entity, + ) + break diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_by_pi_method.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_by_pi_method.py new file mode 100644 index 0000000000..47dc64ef60 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_by_pi_method.py @@ -0,0 +1,80 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +from ifcopenshell import entity_instance +from typing import Sequence + + +def create_alignment_by_pi_method( + file: ifcopenshell.file, + alignment_name: str, + hpoints: Sequence[Sequence[float]], + radii: Sequence[float], + vpoints: Sequence[Sequence[float]] = None, + lengths: Sequence[float] = None, + alignment_description: str = None, +) -> entity_instance: + """ + Create an alignment using the PI layout method for both horizontal and vertical alignments. + If vpoints and lengths are omitted, only a horizontal alignment is created. Only the business logic + entities are creaed. Use create_geometric_representation() to create the geometric entities. + + :param alignment_name: value for Name attribute + :param points: (X,Y) pairs denoting the location of the horizontal PIs, including start and end + :param radii: radii values to use for transition + :param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. + :param lengths: parabolic vertical curve horizontal length values to use for transition + :param alignment_description: value for Description attribute + :return: Returns an IfcAlignment + """ + alignments = [] + + horizontal_alignment = ifcopenshell.api.alignment.create_horizontal_alignment_by_pi_method( + file, alignment_name, hpoints, radii + ) + alignments.append(horizontal_alignment) + + if vpoints and lengths: + vertical_alignment = ifcopenshell.api.alignment.create_vertical_alignment_by_pi_method( + file, alignment_name, vpoints, lengths + ) + alignments.append(vertical_alignment) + + # create the alignment + alignment = file.create_entity( + type="IfcAlignment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=alignment_name, + Description=alignment_description, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + PredefinedType=None, + ) + + # nest the horizontal and vertical under the alignment + ifcopenshell.api.nest.assign_object(file, related_objects=alignments, relating_object=alignment) + + # IFC 4.1.4.1.1 Alignment Aggregation To Project + project = file.by_type("IfcProject")[0] + ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project) + + return alignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_from_csv.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_from_csv.py new file mode 100644 index 0000000000..8643ea1405 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_alignment_from_csv.py @@ -0,0 +1,116 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.aggregate +import ifcopenshell.api.alignment +import ifcopenshell.api.geometry +import ifcopenshell.api.nest +import ifcopenshell.guid +import ifcopenshell.util.element +import ifcopenshell.util.representation +import ifcopenshell.util.stationing +import ifcopenshell.api +from ifcopenshell import entity_instance +from ifcopenshell.api.alignment import get_axis_subcontext + +import math +from typing import Sequence + +import csv + + +def create_alignment_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance: + """ + Creates an alignment from PI data stored in a CSV file. Only the business logic + entities are creaed. Use create_geometric_representation() to create the geometric entities. + + The format of the file is: + + X1,Y1,R1,X2,Y2,R2 ... Xn-1,Yn-1,Rn-1,Xn,Yn + + D1,Z1,L1,D2,Z2,L2 ... Dn-1,Zn-1,Ln-1,Dn,Zn + + D1,Z1,L1,D2,Z2,L2 ... Dn-1,Zn-1,Ln-1,Dn,Zn + + ... + + where: + X,Y are PI coordinates + + R is the horizontal circular curve radius + + D,Z are VPI coordinates as "Distance Along","Elevation" + + L is the horizontal length of a parabolic vertical transition curve + + R1 and Rn, as well as L1 and Ln are placeholders and not used. They are recommended to have values of 0.0. + + R2 and Rn-2 are the radii of the first and last horizontal curves. + + L2 and Ln-2 are the length of the first and last vertical curves. + + The CSV file contains one horizontal alignment, zero, one, or more vertical alignments + + :param filepath: path the to CSV file + :return: IfcAlignment + """ + with open(filepath, newline="") as csvfile: + reader = csv.reader(csvfile) + row_count = 0 + for row in reader: + data = list(map(float, row)) # Convert all values to float + coordinates: list[list[float]] = ( + [] + ) # horizontal coordinates for first row, vertical coordinates for subsequent rows + radii: list[float] = [] # horizontal curve radii for first row, vertical curve length for subsequent rows + + row_count += 1 + + i = 0 + while i < len(data): + if i + 1 < len(data): + x, y = float(data[i]), float(data[i + 1]) + coordinates.append((x, y)) # Store (X, Y) pair + i += 2 + if i < len(data) and (i + 1) % 3 == 0: # Every third element after an (X,Y) pair is R + radii.append(data[i]) + i += 1 + + radii = radii[1:-1] # The first radius value is a placeholder, remove it + + if row_count == 1: + # create the alignment + alignment = file.createIfcAlignment(GlobalId=ifcopenshell.guid.new()) + # create the horizontal alignment + horizontal_alignment = ifcopenshell.api.alignment.create_horizontal_alignment_by_pi_method( + file, "Alignment_from_CSV", coordinates, radii + ) + # nest them together + ifcopenshell.api.nest.assign_object( + file, related_objects=(horizontal_alignment,), relating_object=alignment + ) + else: + # add all subsequent vertical alignments + ifcopenshell.api.alignment.add_vertical_alignment_by_pi_method(file, alignment, coordinates, radii) + + # IFC 4.1.4.1.1 Alignment Aggregation To Project + project = file.by_type("IfcProject")[0] + ifcopenshell.api.aggregate.assign_object(file, products=[alignment], relating_object=project) + + return alignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py new file mode 100644 index 0000000000..86353c9e82 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py @@ -0,0 +1,172 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +from ifcopenshell import entity_instance + +import math +from typing import Sequence + + +def create_geometric_representation(file: ifcopenshell.file, alignment: entity_instance) -> None: + """ + Create geometric representation for the alignment. + + There are 5 different cases: + + 1) Horizontal only + 2) Horizontal + Vertical + 3) Horizontal + Vertical + Cant + 4) Vertical only (this occurs when horizontal is reused from a parent alignment) + 5) Vertical + Cant (this occurs when horizontal is reused from a parent alignment) + + :param alignment: The alignment for which the representation is being created + :return: None + """ + + expected_type = "IfcAlignment" + if not alignment.is_a(expected_type): + raise TypeError("Expected '{expected_type}' but got '{alignment.is_a()}'") + + placement = file.createIfcLocalPlacement( + PlacementRelTo=None, + RelativePlacement=file.createIfcAxis2Placement2D(Location=file.createIfcCartesianPoint(Coordinates=(0.0, 0.0))), + ) + + alignment.ObjectPlacement = placement + + axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) + + layouts = ifcopenshell.api.alignment.get_alignment_layouts(alignment) + children = ifcopenshell.api.alignment.get_child_alignments(alignment) + + if len(layouts) == 1 and len(children) == 0: + assert layouts[0].is_a("IfcAlignmentHorizontal") + # Horizontal only - IFC CT 4.1.7.1.1.1 + composite_curve = file.createIfcCompositeCurve() + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve2D", + Items=(composite_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + elif len(layouts) == 2 and len(children) == 0: + # Horizontal and Vertical - IFC CT 4.1.7.1.1.1 + assert layouts[0].is_a("IfcAlignmentHorizontal") + assert layouts[1].is_a("IfcAlignmentVertical") + composite_curve = file.createIfcCompositeCurve() + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="FootPrint", + RepresentationType="Curve2D", + Items=(composite_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + + gradient_curve = file.createIfcGradientCurve(BaseCurve=composite_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[1], gradient_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(gradient_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + elif len(layouts) == 3 and len(children) == 0: + # Horizontal, Vertical, and Cant - IFC CT 4.1.7.1.1.3 + assert layouts[0].is_a("IfcAlignmentHorizontal") + assert layouts[1].is_a("IfcAlignmentVertical") + assert layouts[2].is_a("IfcAlignmentCant") + composite_curve = file.createIfcCompositeCurve() + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="FootPrint", + RepresentationType="Curve2D", + Items=(composite_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + + gradient_curve = file.createIfcGradientCurve(BaseCurve=composite_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[1], gradient_curve) + segmented_reference_curve = file.createIfcSegmentedReferenceCurve(BaseCurve=gradient_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[2], segmented_reference_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(segmented_reference_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + else: + # Reusing Horizontal - CT 4.1.4.4.1.2 + # Create a representation on the parent alignment + composite_curve = file.createIfcCompositeCurve() + ifcopenshell.api.alignment.map_alignment_segments(file, layouts[0], composite_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="FootPrint", + RepresentationType="Curve2D", + Items=(composite_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, alignment, representation) + + for child_alignment in children: + child_alignment.ObjectPlacement = placement + child_layouts = ifcopenshell.api.alignment.get_alignment_layouts(child_alignment) + if len(child_layouts) == 1: + assert child_layouts[0].is_a("IfcAlignmentVertical") + base_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + gradient_curve = file.createIfcGradientCurve(BaseCurve=base_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, child_layouts[0], gradient_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(gradient_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, child_alignment, representation) + elif len(child_layouts) == 2: + assert child_layouts[0].is_a("IfcAlignmentVertical") + assert child_layouts[1].is_a("IfcAlignmentCant") + base_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + gradient_curve = file.createIfcGradientCurve(BaseCurve=base_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, child_layouts[0], gradient_curve) + segmented_reference_curve = file.createIfcSegmentedReferenceCurve(BaseCurve=gradient_curve) + ifcopenshell.api.alignment.map_alignment_segments(file, child_layouts[1], segmented_reference_curve) + representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Curve3D", + Items=(segmented_reference_curve,), + ) + ifcopenshell.api.geometry.assign_representation(file, child_alignment, representation) + else: + assert False # should never get here - can't have more than one vertical and cant in a child alignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_horizontal_alignment_by_pi_method.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_horizontal_alignment_by_pi_method.py new file mode 100644 index 0000000000..e7d15c942f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_horizontal_alignment_by_pi_method.py @@ -0,0 +1,216 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +from ifcopenshell import entity_instance + +import math +from typing import Sequence + + +def create_horizontal_alignment_by_pi_method( + file: ifcopenshell.file, name: str, hpoints: Sequence[Sequence[float]], radii: Sequence[float] +) -> entity_instance: + """ + Create a horizontal alignment using the PI layout method. + + :param name: value for Name attribute + :param hpoints: (X, Y) pairs denoting the location of the horizontal PIs, including start (POB) and end (POE). + :param radii: radius values to use for transition + :return: Returns a IfcAlignmentHorizontal + """ + if not (len(hpoints) - 2 == len(radii)): + raise ValueError("radii should have two fewer elements that hpoints") + + # Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments + horizontal_alignment = file.create_entity( + type="IfcAlignmentHorizontal", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=f"{name} - Horizontal", + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + ) + + xBT, yBT = hpoints[0] + xPI, yPI = hpoints[1] + + i = 1 + + for radius in radii: + # back tangent + dxBT = xPI - xBT + dyBT = yPI - yBT + angleBT = math.atan2(dyBT, dxBT) + lengthBT = math.sqrt(dxBT * dxBT + dyBT * dyBT) + + # forward tangent + i += 1 + xFT, yFT = hpoints[i] + dxFT = xFT - xPI + dyFT = yFT - yPI + angleFT = math.atan2(dyFT, dxFT) + + delta = angleFT - angleBT + + tangent = abs(radius * math.tan(delta / 2)) + + lc = abs(radius * delta) + + radius *= delta / abs(delta) + + xPC = xPI - tangent * math.cos(angleBT) + yPC = yPI - tangent * math.sin(angleBT) + + xPT = xPI + tangent * math.cos(angleFT) + yPT = yPI + tangent * math.sin(angleFT) + + tangent_run = lengthBT - tangent + + # create back tangent run + pt = file.create_entity( + type="IfcCartesianPoint", + Coordinates=(xBT, yBT), + ) + design_parameters = file.create_entity( + type="IfcAlignmentHorizontalSegment", + StartTag=None, + EndTag=None, + StartPoint=pt, + StartDirection=angleBT, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=tangent_run, + GravityCenterLineHeight=None, + PredefinedType="LINE", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment) + + # create circular curve + if radius != 0.0: + pc = file.create_entity( + type="IfcCartesianPoint", + Coordinates=(xPC, yPC), + ) + design_parameters = file.create_entity( + type="IfcAlignmentHorizontalSegment", + StartTag=None, + EndTag=None, + StartPoint=pc, + StartDirection=angleBT, + StartRadiusOfCurvature=float(radius), + EndRadiusOfCurvature=float(radius), + SegmentLength=lc, + GravityCenterLineHeight=None, + PredefinedType="CIRCULARARC", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment) + + xBT = xPT + yBT = yPT + xPI = xFT + yPI = yFT + + # done processing radii + # create last tangent run + dx = xPI - xBT + dy = yPI - yBT + angleBT = math.atan2(dy, dx) + tangent_run = math.sqrt(dx * dx + dy * dy) + pt = file.create_entity(type="IfcCartesianPoint", Coordinates=(xBT, yBT)) + + design_parameters = file.create_entity( + type="IfcAlignmentHorizontalSegment", + StartTag=None, + EndTag=None, + StartPoint=pt, + StartDirection=angleBT, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=tangent_run, + GravityCenterLineHeight=None, + PredefinedType="LINE", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment) + + # create zero length terminator segment + poe = file.create_entity(type="IfcCartesianPoint", Coordinates=(xPI, yPI)) + + design_parameters = file.create_entity( + type="IfcAlignmentHorizontalSegment", + StartTag="POE", + EndTag="POE", + StartPoint=poe, + StartDirection=angleBT, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=0.0, + GravityCenterLineHeight=None, + PredefinedType="LINE", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment) + + return horizontal_alignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_segment_representations.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_segment_representations.py new file mode 100644 index 0000000000..a5dc42cfc3 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_segment_representations.py @@ -0,0 +1,76 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +from ifcopenshell import entity_instance +from ifcopenshell import ifcopenshell_wrapper +import math +from typing import Sequence + + +def create_segment_representations( + file: ifcopenshell.file, + alignment: entity_instance, +) -> None: + """ + Creates curve segment representations for the alignment for IFC CT 4.1.7.1.1.4. The alignment is expected to have representations + for "Axis/Curve2D" (horizontal only) or "FootPrint/Curve2D" and "Axis/Curve3D" (horizontal + vertical/cant). There is the additional + expectation that there is a 1-to-1 relationship between IfcAlignmentSegment and IfcCurveSegment. + That is, no Helmert curves in the alignment which have a 1-to-2 relationship + + :param alignment: The alignment to create segment representations. + """ + expected_type = "IfcAlignment" + if not alignment.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{alignment.is_a()}'.") + + axis_geom_subcontext = ifcopenshell.api.alignment.get_axis_subcontext(file) + representations = ifcopenshell.util.representation.get_representations_iter(alignment) + for representation in representations: + curve = None + nested_alignment = None + if (representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D") or ( + representation.RepresentationIdentifier == "FootPrint" and representation.RepresentationType == "Curve2D" + ): + curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + nested_alignment = [ + c for c in ifcopenshell.util.element.get_components(alignment) if c.is_a("IfcAlignmentHorizontal") + ][0] + elif representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve3D": + curve = ifcopenshell.api.alignment.get_curve(alignment) + nested_alignment = [ + c for c in ifcopenshell.util.element.get_components(alignment) if c.is_a("IfcAlignmentVertical") + ][0] + + curve_segments = curve.Segments + segments = nested_alignment.IsNestedBy[0].RelatingObjects + + for curve_segment, alignment_segment in zip(curve_segments, segments): + axis_representation = file.create_entity( + type="IfcShapeRepresentation", + ContextOfItems=axis_geom_subcontext, + RepresentationIdentifier="Axis", + RepresentationType="Segment", + Items=(curve_segment,), + ) + product = file.create_entity( + type="IfcProductDefinitionShape", Name=None, Description=None, Representations=(axis_representation,) + ) + alignment_segment.ObjectPlacement = alignment.ObjectPlacement + alignment_segment.Representation = product diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_vertical_alignment_by_pi_method.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_vertical_alignment_by_pi_method.py new file mode 100644 index 0000000000..42652fc9b7 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_vertical_alignment_by_pi_method.py @@ -0,0 +1,194 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +from ifcopenshell import entity_instance + +import math +from typing import Sequence + + +def create_vertical_alignment_by_pi_method( + file: ifcopenshell.file, name: str, vpoints: Sequence[Sequence[float]], lengths: Sequence[float] +) -> entity_instance: + """ + Create a vertical alignment using the PI layout method. + + :param name: value for Name attribute + :param base_curve: base curve representing the 2D projection of the gradient curve + :param vpoints: (distance_along, Z_height) pairs denoting the location of the vertical PIs, including start and end. + :param lengths: horizontal length of parabolic vertical curves + :return: IfcAlignmentHorizontal + """ + if not (len(vpoints) - 2 == len(lengths)): + raise ValueError("lengths should have two fewer elements that vpoints") + + # Create the vertical alignment (IfcAlignmentVertical) and nest alignment segments + vertical_alignment = file.create_entity( + type="IfcAlignmentVertical", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=f"{name} - Vertical", + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + ) + + xPBG, yPBG = vpoints[0] + xPVI, yPVI = vpoints[1] + i = 1 + for length in lengths: + # back gradient + dxBG = xPVI - xPBG + dyBG = yPVI - yPBG + start_slope = math.tan(math.atan2(dyBG, dxBG)) + + # forward gradient + i += 1 + xPFG, yPFG = vpoints[i] + dxFG = xPFG - xPVI + dyFG = yPFG - yPVI + end_slope = math.tan(math.atan2(dyFG, dxFG)) + + xEVC = xPVI + length / 2.0 + yEVC = yPVI + end_slope * length / 2.0 + + # create gradient + gradient_length = dxBG - length / 2.0 + design_parameters = file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xPBG, + HorizontalLength=gradient_length, + StartHeight=yPBG, + StartGradient=start_slope, + EndGradient=start_slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment) + + # create vertical curve + if 0.0 < length: + k = (end_slope - start_slope) / length + xBVC = xPVI - length / 2.0 + yBVC = yPVI - start_slope * length / 2.0 + + design_parameters = file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xBVC, + HorizontalLength=length, + StartHeight=yBVC, + StartGradient=start_slope, + EndGradient=end_slope, + RadiusOfCurvature=1 / k, + PredefinedType="PARABOLICARC", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment) + + # start of next curve is end of this curve + xPBG = xEVC + yPBG = yEVC + xPVI = xPFG + yPVI = yPFG + + # create last gradient run + dx = xPVI - xPBG + dy = yPVI - yPBG + slope = math.tan(math.atan2(dy, dx)) + gradient_length = dx + + design_parameters = file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag=None, + EndTag=None, + StartDistAlong=xPBG, + HorizontalLength=gradient_length, + StartHeight=yPBG, + StartGradient=slope, + EndGradient=slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment) + + # create zero length terminator segment + design_parameters = file.create_entity( + type="IfcAlignmentVerticalSegment", + StartTag="VPOE", + EndTag="VPOE", + StartDistAlong=xPVI, + HorizontalLength=0.0, + StartHeight=yPVI, + StartGradient=slope, + EndGradient=slope, + RadiusOfCurvature=None, + PredefinedType="CONSTANTGRADIENT", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + ifcopenshell.api.alignment.add_segment_to_layout(file, vertical_alignment, alignment_segment) + + return vertical_alignment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_alignment_layouts.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_alignment_layouts.py new file mode 100644 index 0000000000..929634f6b9 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_alignment_layouts.py @@ -0,0 +1,41 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.util +from ifcopenshell import entity_instance +from typing import Sequence + +import ifcopenshell.util.representation + + +def get_alignment_layouts(alignment: entity_instance) -> Sequence[entity_instance]: + """ + Returns the layout alignments nested to this alignment + """ + layouts = [] + for rel in alignment.IsNestedBy: + for layout in rel.RelatedObjects: + if ( + layout.is_a("IfcAlignmentHorizontal") + or layout.is_a("IfcAlignmentVertical") + or layout.is_a("IfcAlignmentCant") + ): + layouts.append(layout) + + return layouts diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_axis_subcontext.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_axis_subcontext.py new file mode 100644 index 0000000000..033c94d9bb --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_axis_subcontext.py @@ -0,0 +1,40 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.util.representation +import ifcopenshell.api.context +from ifcopenshell import entity_instance + + +def get_axis_subcontext(file: ifcopenshell.file) -> entity_instance: + """ + Returns the IfcGeometricRepresentationSubContext for Model, Axis, MODEL_VIEW. If one does not exist, it is created. + """ + axis_geom_subcontext = ifcopenshell.util.representation.get_context(file, "Model", "Axis", "MODEL_VIEW") + if axis_geom_subcontext == None: + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_geom_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + return axis_geom_subcontext diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py new file mode 100644 index 0000000000..b59d623f26 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_basis_curve.py @@ -0,0 +1,51 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.util +from ifcopenshell import entity_instance +from typing import Sequence + +import ifcopenshell.util.representation + + +def get_basis_curve(alignment: entity_instance) -> entity_instance: + """ + Returns the basis curve for an alignment. This curve is the geometric representation that is used + as the basis curve for vertical and cant alignments. + + :param alignment: The alignment + :return: The geometric representation that is used as a basis curve, typically an IfcCompositeCurve, or None if the alignment does not have a representation + + Example: + + .. code:: python + alignment = model.by_type("IfcAlignment")[0] + composite_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + """ + axis = None + + representations = ifcopenshell.util.representation.get_representations_iter(alignment) + for representation in representations: + if (representation.RepresentationIdentifier == "Axis" and representation.RepresentationType == "Curve2D") or ( + representation.RepresentationIdentifier == "FootPrint" and representation.RepresentationType == "Curve2D" + ): + axis = representation + break + + return None if axis == None or axis.Items == None or len(axis.Items) == 0 else axis.Items[0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_child_alignments.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_child_alignments.py new file mode 100644 index 0000000000..7e80477a03 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_child_alignments.py @@ -0,0 +1,44 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.util +from ifcopenshell import entity_instance +from typing import Sequence + +import ifcopenshell.util.element + + +def get_child_alignments(alignment: entity_instance) -> Sequence[entity_instance]: + """ + Returns the aggregated child alignments to this alignment + + Example: + + .. code:: python + + alignment = model.by_type("IfcAlignment")[0] + children = ifcopenshell.api.alignment.get_child_alignments(alignment) + """ + children = [] + for rel in alignment.IsDecomposedBy: + for child in rel.RelatedObjects: + if child.is_a("IfcAlignment"): + children.append(child) + + return children diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py new file mode 100644 index 0000000000..c2889bdc3f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_curve.py @@ -0,0 +1,52 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.util +from ifcopenshell import entity_instance +from typing import Sequence + +import ifcopenshell.util.representation + + +def get_curve(alignment: entity_instance) -> entity_instance: + """ + Returns the geometric representation curve for an alignment. + A horizontal only will have a curve of type IfcCompositeCurve + A horizontal+vertical will have a curve of type IfcGradientCurve + A horizontal+vertical+cant will have a curve of tyep IfcSegmentedReferenceCurve + + :param alignment: The alignment + :return: The geometric representation of the alignemnt or None if the alignment does not have a representation + + Example: + + .. code:: python + alignment = model.by_type("IfcAlignment")[0] + gradient_curve = ifcopenshell.api.alignment.get_curve(alignment) + """ + axis = None + representations = ifcopenshell.util.representation.get_representations_iter(alignment) + for representation in representations: + if representation.RepresentationIdentifier == "Axis" and ( + representation.RepresentationType == "Curve2D" or representation.RepresentationType == "Curve3D" + ): + axis = representation + break + + return None if axis == None or len(axis.Items) == 0 else axis.Items[0] diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/get_parent_alignment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_parent_alignment.py new file mode 100644 index 0000000000..dfd6fb8629 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/get_parent_alignment.py @@ -0,0 +1,45 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.util +from ifcopenshell import entity_instance +from typing import Sequence + +import ifcopenshell.util.representation + + +def get_parent_alignment(alignment: entity_instance) -> entity_instance: + """ + Returns the parent alignment. When multiple vertical alignments share a horizontal alignment + the horizontal alignment is nested to the parent alignment, a child alignment is aggregated + to the parent alignment for each vertical alignment, and the vertical alignment is nested with + its child alignment. + + Example: + + .. code:: python + alignment = model.by_type("IfcAlignment")[0] + parent = ifcopenshell.api.alignment.get_parent_alignment(alignment) + """ + + for rel in alignment.Decomposes: + if rel.RelatingObject.is_a("IfcAlignment"): + return rel.RelatingObject + + return None diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py new file mode 100644 index 0000000000..2b26cb6fe7 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/has_zero_length_segment.py @@ -0,0 +1,61 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +import ifcopenshell.util.element +from ifcopenshell import entity_instance + + +def has_zero_length_segment(entity: entity_instance) -> bool: + """ + Returns true if the entity ends with a zero length segment. If the entity is an IfcCompositeCurve the IfcCurveSegment.Transition must be DISCONTINUOUS + + :param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve + :return: True if the zero length segment is present + """ + expected_types = [ + "IfcAlignmentHorizontal", + "IfcAlignmentVertical", + "IfcAlignmentCant", + "IfcCompositeCurve", + "IfcGradientCurve", + "IfcSegmentedReferenceCurve", + ] + if not entity.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{entity.is_a()}" + ) + + if entity.is_a("IfcCompositeCurve"): + last_segment = entity.Segments[-1] + return last_segment.Transition == "DISCONTINUOUS" and last_segment.SegmentLength.wrappedValue == 0.0 + else: + segments = ifcopenshell.util.element.get_components(entity) + for rel in entity.IsNestedBy: + if 0 < len(rel.RelatedObjects): + last_segment = rel.RelatedObjects[-1] + if last_segment.is_a("IfcAlignmentSegment"): + if last_segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"): + return last_segment.DesignParameters.SegmentLength == 0.0 + elif last_segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"): + return last_segment.DesignParameters.HorizontalLength == 0.0 + elif last_segment.DesignParameters.is_a("IfcAlignmentCantSegment"): + return last_segment.DesignParameters.HorizontalLength == 0.0 + + return False diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_cant_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_cant_segment.py new file mode 100644 index 0000000000..b911fc6b45 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_cant_segment.py @@ -0,0 +1,84 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +from ifcopenshell import entity_instance +from ifcopenshell.api.alignment import get_axis_subcontext +from typing import Sequence + + +def _map_constant_cant(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("CONSTANTCANT not implemented") + + +def _map_linear_transition(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("LINEARTRANSTION not implemented") + + +def _map_helmert_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("HELMERTCURVE not implemented") + + +def _map_bloss_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("BLOSSCURVE not implemented") + + +def _map_cosine_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("COSINECURVE not implemented") + + +def _map_sine_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("SINECURVE not implemented") + + +def _map_viennese_bend(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("VIENNESEBEND not implemented") + + +def map_alignment_cant_segment( + file: ifcopenshell.file, design_parameters: entity_instance +) -> Sequence[entity_instance]: + """ + Creates IfcCurveSegment entities for the represention of the supplied IfcAlignmentCantSegment business logic entity instance. + A pair of entities is returned because a single business logic segment of type HELMERTCURVE maps to two representaiton entities. + + The IfcCurveSegment.Transition transition code is set to DISCONTINUOUS. + """ + expected_type = "IfcAlignmentCantSegment" + if not design_parameters.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{design_parameters.is_a()}'.") + + match design_parameters.PredefinedType: + case "CONSTANTCANT": + result = _map_constant_cant(file, design_parameters) + case "LINEARTRANSITION": + result = _map_linear_transition(file, design_parameters) + case "HELMERTCURVE": + result = _map_helmert_curve(file, design_parameters) + case "BLOSSCURVE": + result = _map_bloss_curve(file, design_parameters) + case "COSINECURVE": + result = _map_cosine_curve(file, design_parameters) + case "SINECURVE": + result = _map_sine_curve(file, design_parameters) + case "VIENNESEBEND": + result = _map_viennese_bend(file, design_parameters) + case _: + raise TypeError("Unexpected predefined type") + + return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_horizontal_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_horizontal_segment.py new file mode 100644 index 0000000000..bb86ea6bbb --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_horizontal_segment.py @@ -0,0 +1,439 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +from ifcopenshell import entity_instance +import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper +from typing import Sequence +import math + + +def _get_curve_factor(design_parameters: entity_instance) -> float: + start_radius = design_parameters.StartRadiusOfCurvature + end_radius = design_parameters.EndRadiusOfCurvature + length = design_parameters.SegmentLength + + f = (0.0 if end_radius == 0.0 else length / end_radius) - (0.0 if start_radius == 0.0 else length / start_radius) + return f + + +def _map_line(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + parent_curve = file.create_entity( + type="IfcLine", + Pnt=file.create_entity( + type="IfcCartesianPoint", + Coordinates=(0.0, 0.0), + ), + Dir=file.create_entity( + type="IfcVector", + Orientation=file.create_entity( + type="IfcDirection", + DirectionRatios=(1.0, 0.0), + ), + Magnitude=1.0, + ), + ) + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection( + (math.cos(start_direction), math.sin(start_direction)), + ), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_circular_arc(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + parent_curve = file.createIfcCircle( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + Radius=math.fabs(start_radius), + ) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.createIfcAxis2Placement2D( + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length * (start_radius / math.fabs(start_radius))), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_clothoid(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + end_radius = design_parameters.EndRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + f = _get_curve_factor(design_parameters) + A = (length / math.sqrt(math.fabs(f))) * (f / math.fabs(f)) + parent_curve = file.createIfcClothoid( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + ClothoidConstant=A, + ) + + if (math.fabs(start_radius) < math.fabs(end_radius) and start_radius != 0.0) or end_radius == 0.0: + offset = -length - (length * start_radius / (end_radius - start_radius) if end_radius != 0.0 else 0.0) + else: + offset = length * end_radius / (start_radius - end_radius) if start_radius != 0.0 else 0.0 + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(offset), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_cubic(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + end_radius = design_parameters.EndRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + offset = 0.0 + A0 = 0.0 # constant term + A1 = 0.0 # linear term + A2 = 0.0 # quadratic term + A3 = 0.0 # cubic term + + if end_radius != 0.0 and start_radius != 0.0 and end_radius != start_radius: + f = (start_radius - end_radius) / end_radius # note, this "f" is different that _get_curve_factor computes + A3 = f / (6.0 * start_radius * length) + offset = length / f + elif end_radius != 0.0: + A3 = 1.0 / (6.0 * end_radius * length) + offset = 0.0 + elif start_radius != 0.0: + A3 = -1.0 / (6.0 * start_radius * length) + offset = -length + + parent_curve = file.createIfcPolynomialCurve( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + CoefficientsX=(0.0, 1.0), + CoefficientsY=(A0, A1, A2, A3), + ) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(offset), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_helmert_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + end_radius = design_parameters.EndRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + f = _get_curve_factor(design_parameters) + + a0_1 = 0.0 * f + length / start_radius if start_radius != 0 else 0.0 # constant term, first half + a1_1 = 0.0 * f # linear term, first half + a2_1 = 2.0 * f # quadratic term, first half + + A0_1 = length * math.pow(math.fabs(a0_1), -1.0 / 1.0) * a0_1 / math.fabs(a0_1) if a0_1 != 0.0 else 0.0 + A1_1 = length * math.pow(math.fabs(a1_1), -1.0 / 2.0) * a1_1 / math.fabs(a1_1) if a1_1 != 0.0 else 0.0 + A2_1 = length * math.pow(math.fabs(a2_1), -1.0 / 3.0) * a2_1 / math.fabs(a2_1) if a2_1 != 0.0 else 0.0 + + x1, y1, angle1 = ifcopenshell_wrapper.helmert_curve_point(A0_1, A1_1, A2_1, length / 2) + + parent_curve1 = file.createIfcSecondOrderPolynomialSpiral( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0)) + ), + QuadraticTerm=A2_1, + LinearTerm=A1_1 if A1_1 != 0.0 else None, + ConstantTerm=A0_1 if A0_1 != 0.0 else None, + ) + + curve_segment1 = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length / 2), + ParentCurve=parent_curve1, + ) + + a0_2 = -1.0 * f + (length / start_radius if start_radius != 0.0 else 0.0) # constant term, second half + a1_2 = 4.0 * f # linear term, second half + a2_2 = -2.0 * f # quadratic term, second half + + A0_2 = length * math.pow(math.fabs(a0_2), -1.0 / 1.0) * (a0_2 / math.fabs(a0_2)) if a0_2 != 0.0 else 0.0 + A1_2 = length * math.pow(math.fabs(a1_2), -1.0 / 2.0) * (a1_2 / math.fabs(a1_2)) if a1_2 != 0.0 else 0.0 + A2_2 = length * math.pow(math.fabs(a2_2), -1.0 / 3.0) * (a2_2 / math.fabs(a2_2)) if a2_2 != 0.0 else 0.0 + + x2, y2, angle2 = ifcopenshell_wrapper.helmert_curve_point(A0_2, A1_2, A2_2, length / 2) + anglep = angle1 - angle2 + xp = x1 - x2 * math.cos(anglep) + y2 * math.sin(anglep) + yp = y1 - x2 * math.sin(anglep) - y2 * math.cos(anglep) + + parent_curve2 = file.createIfcSecondOrderPolynomialSpiral( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((xp, yp)), + RefDirection=file.createIfcDirection((math.cos(anglep), math.sin(anglep))), + ), + QuadraticTerm=A2_2, + LinearTerm=A1_2 if A1_2 != 0.0 else None, + ConstantTerm=A0_2 if A0_2 != 0.0 else None, + ) + + curve_segment2 = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=file.createIfcCartesianPoint((x1, y1)), + RefDirection=file.createIfcDirection((math.cos(angle1), math.sin(angle1))), + ), + SegmentStart=file.createIfcLengthMeasure(length / 2), + SegmentLength=file.createIfcLengthMeasure(length / 2), + ParentCurve=parent_curve2, + ) + + return curve_segment1, curve_segment2 + + +def _map_bloss_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + f = _get_curve_factor(design_parameters) + + a0 = length / start_radius if start_radius != 0.0 else 0.0 # constant term + a1 = 0.0 # linear term + a2 = 3.0 * f # quadratic term + a3 = -2.0 * f # cubic term + + A0 = length * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0 + A1 = length * math.pow(math.fabs(a1), -1.0 / 2.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0 + A2 = length * math.pow(math.fabs(a2), -1.0 / 3.0) * (a2 / math.fabs(a2)) if a2 != 0.0 else 0.0 + A3 = length * math.pow(math.fabs(a3), -1.0 / 4.0) * (a3 / math.fabs(a3)) if a3 != 0.0 else 0.0 + + parent_curve = file.createIfcThirdOrderPolynomialSpiral( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0)) + ), + CubicTerm=A3, + QuadraticTerm=A2 if A2 != 0.0 else None, + LinearTerm=A1 if A1 != 0.0 else None, + ConstantTerm=A0 if A0 != 0.0 else None, + ) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_cosine_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + f = _get_curve_factor(design_parameters) + + a0 = 0.5 * f + (length / start_radius if start_radius != 0.0 else 0.0) + a1 = -0.5 * f + + A0 = length * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0 + A1 = length * math.pow(math.fabs(a1), -1.0 / 1.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0 + + parent_curve = file.createIfcCosineSpiral( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + CosineTerm=A1, + ConstantTerm=(A0 if A0 != 0.0 else None), + ) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_sine_curve(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_point = design_parameters.StartPoint + start_direction = design_parameters.StartDirection + start_radius = design_parameters.StartRadiusOfCurvature + length = design_parameters.SegmentLength + + transition = "DISCONTINUOUS" + + f = _get_curve_factor(design_parameters) + a0 = length / start_radius if start_radius != 0.0 else 0.0 + a1 = f + a2 = -f / (2.0 * math.pi) + + A0 = length * math.pow(math.fabs(a0), -1.0 / 1.0) * (a0 / math.fabs(a0)) if a0 != 0.0 else 0.0 + A1 = length * math.pow(math.fabs(a1), -1.0 / 2.0) * (a1 / math.fabs(a1)) if a1 != 0.0 else 0.0 + A2 = length * math.pow(math.fabs(a2), -1.0 / 1.0) * (a2 / math.fabs(a2)) if a2 != 0.0 else 0.0 + + parent_curve = file.createIfcSineSpiral( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((0.0, 0.0)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + SineTerm=A2, + LinearTerm=(A1 if A1 != 0.0 else None), + ConstantTerm=(A0 if A0 != 0.0 else None), + ) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=start_point, + RefDirection=file.createIfcDirection((math.cos(start_direction), math.sin(start_direction))), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_viennese_bend(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("VIENNESEBEND not implemented") + + +def map_alignment_horizontal_segment( + file: ifcopenshell.file, design_parameters: entity_instance +) -> Sequence[entity_instance]: + """ + Creates IfcCurveSegment entities for the represention of the supplied IfcAlignmentHorizontalSegment business logic entity instance. + A pair of entities is returned because a single business logic segment of type HELMERTCURVE maps to two representaiton entities. + + The IfcCurveSegment.Transition transition code is set to DISCONTINUOUS + """ + expected_type = "IfcAlignmentHorizontalSegment" + if not design_parameters.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{design_parameters.is_a()}'.") + + match design_parameters.PredefinedType: + case "LINE": + result = _map_line(file, design_parameters) + case "CIRCULARARC": + result = _map_circular_arc(file, design_parameters) + case "CLOTHOID": + result = _map_clothoid(file, design_parameters) + case "CUBIC": + result = _map_cubic(file, design_parameters) + case "HELMERTCURVE": + result = _map_helmert_curve(file, design_parameters) + case "BLOSSCURVE": + result = _map_bloss_curve(file, design_parameters) + case "COSINECURVE": + result = _map_cosine_curve(file, design_parameters) + case "SINECURVE": + result = _map_sine_curve(file, design_parameters) + case "VIENNESEBEND": + result = _map_viennese_bend(file, design_parameters) + case _: + raise TypeError("Unexpected predefined type") + + return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segment.py new file mode 100644 index 0000000000..d7b05d5544 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segment.py @@ -0,0 +1,47 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api +from ifcopenshell import entity_instance +from typing import Sequence + + +def map_alignment_segment(file: ifcopenshell.file, segment: entity_instance) -> Sequence[entity_instance]: + """ + Creates IfcCurveSegment entities for the represention of the supplied IfcAlignmentSegment business logic entity instance. + A pair of entities is returned because a single business logic segment of type HELMERTCURVE maps to two representaiton entities. + + The IfcCurveSegment.Transition transition code is set to DISCONTINUOUS, except for the transition between helmert curve segments. + + This function will evaluate the IfcAlignmentSegment.DesignParameters type and call the correct lower level mapping function. + """ + expected_type = "IfcAlignmentSegment" + if not segment.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{segment.is_a()}'.") + + if segment.DesignParameters.is_a("IfcAlignmentHorizontalSegment"): + return ifcopenshell.api.alignment.map_alignment_horizontal_segment(file, segment.DesignParameters) + elif segment.DesignParameters.is_a("IfcAlignmentVerticalSegment"): + return ifcopenshell.api.alignment.map_alignment_vertical_segment(file, segment.DesignParameters) + elif segment.DesignParameters.is_a("IfcAlignmentCantSegment"): + return ifcopenshell.api.alignment.map_alignment_cant_segment(file, segment.DesignParameters) + else: + raise TypeError("Unexpected type for segment.DesignParameters") + + return (None, None) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segments.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segments.py new file mode 100644 index 0000000000..d9012d59ef --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_segments.py @@ -0,0 +1,62 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +from ifcopenshell import entity_instance +from typing import Sequence + + +def map_alignment_segments( + file: ifcopenshell.file, alignment: entity_instance, composite_curve: entity_instance +) -> None: + """ + Creates IfcCurveSegment entities for the supplied alignment business logic entity instance and assigns them to the composite curve. + End-Start points of adjacent segments are evaluated and the IfcCurveSegment.Transition is set. + + This function does not create an IfcShapeRepresentation. Use create_geometric_representation to create all the representations + for an alignment. This function only populates the composite curve with IfcCurveSegment entities. + + :param alignment: The business logic alignment, expected to be IfcAlignmentHorizontal, IfcAlignmentVertical, or IfcAlignmentCant + :param composite_curve: The IfcCompositeCurve (or subclass) which will receive the IfcCurveSegment + :return: None + """ + expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"] + if not alignment.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{alignment.is_a()}" + ) + + if alignment.is_a("IfcAlignmentHorizontal") and not composite_curve.is_a("IfcCompositeCurve"): + raise TypeError(f"Expected to see IfcCompositeCurve, instead received '{composite_curve.is_a()}'.") + elif alignment.is_a("IfcAlignmentVertical") and not composite_curve.is_a("IfcGradientCurve"): + raise TypeError(f"Expected to see IfcGradientCurve, instead received '{composite_curve.is_a()}'.") + elif alignment.is_a("IfcAlignmentCant") and not composite_curve.is_a("IfcSegmentedReferenceCurve"): + raise TypeError(f"Expected to see IfcSegmentedReferenceCurve, instead received '{composite_curve.is_a()}'.") + + settings = ifcopenshell.geom.settings() + + composite_curve.SelfIntersect = False + + for rel_nests in alignment.IsNestedBy: + for layout in rel_nests.RelatedObjects: + if layout.is_a("IfcLinearElement"): + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, layout) + for mapped_segment in mapped_segments: + if mapped_segment: + ifcopenshell.api.alignment.add_segment_to_curve(file, mapped_segment, composite_curve) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_vertical_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_vertical_segment.py new file mode 100644 index 0000000000..3e80f3c6f3 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/map_alignment_vertical_segment.py @@ -0,0 +1,225 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +from ifcopenshell import ifcopenshell_wrapper +from ifcopenshell import entity_instance +from typing import Sequence +import math + + +def _polynomial_length(A: float, B: float, C: float, L: float) -> float: + # closed form solultion for length of parabolic curve. + # see https://www.integral-table.com, equation #37 + # Parabolic curve equation: y = A + Bx + Cx^2 + # y' = B + 2Cx + # Length of a curve = Integral[0,L]( (y')^2 + 1) dx) + # y'^2 = 4C^2x^2 + 4BCx + B^2 + # Substituting, Length of a curve = Integral[0,L]( (4C^2)x^2 + (4BC)x + (B^2 + 1)) dx) + # for eq. #37 cited above, a = 4C^2, b = 4BC, c = B^2 + 1 + a = 4.0 * C * C + b = 4.0 * B * C + c = B * B + 1 + + v1 = lambda a, b, c, x: (b + 2.0 * a * x) / (4.0 * a) + v2 = lambda a, b, c, x: math.sqrt(a * x * x + b * x + c) + v3 = lambda a, b, c, x: (4.0 * a * c - b * b) / (8.0 * math.pow(a, 1.5)) + v4 = lambda a, b, c, x: math.log(math.fabs(2.0 * a * x + b + 2.0 * math.sqrt(a * (a * x * x + b * x + c)))) + + fn = lambda a, b, c, x: v1(a, b, c, x) * v2(a, b, c, x) + v3(a, b, c, x) * v4(a, b, c, x) + + curve_length = fn(a, b, c, L) - fn( + a, b, c, 0 + ) # remember when evaluating an integral, it must be evaluated at end points (L and 0) + return curve_length + + +def _map_constant_gradient(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_distance_along = design_parameters.StartDistAlong + horizontal_length = design_parameters.HorizontalLength + start_height = design_parameters.StartHeight + start_gradient = design_parameters.StartGradient + end_gradient = design_parameters.EndGradient + radius_of_curvature = design_parameters.RadiusOfCurvature + transition = "DISCONTINUOUS" + + parent_curve = file.create_entity( + type="IfcLine", + Pnt=file.create_entity( + type="IfcCartesianPoint", + Coordinates=(0.0, 0.0), + ), + Dir=file.create_entity( + type="IfcVector", + Orientation=file.create_entity( + type="IfcDirection", + DirectionRatios=(1.0, 0.0), + ), + Magnitude=1.0, + ), + ) + + dx = math.cos(math.atan(start_gradient)) + dy = math.sin(math.atan(start_gradient)) + curve_segment_length = horizontal_length / dx + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=file.create_entity(type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height)), + RefDirection=file.createIfcDirection((dx, dy)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(curve_segment_length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_parabolic_arc(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_distance_along = design_parameters.StartDistAlong + horizontal_length = design_parameters.HorizontalLength + start_height = design_parameters.StartHeight + start_gradient = design_parameters.StartGradient + end_gradient = design_parameters.EndGradient + radius_of_curvature = design_parameters.RadiusOfCurvature + transition = "DISCONTINUOUS" + + A = start_height + B = start_gradient + C = (end_gradient - start_gradient) / (2.0 * horizontal_length) + + parent_curve = file.create_entity( + type="IfcPolynomialCurve", + Position=file.create_entity( + type="IfcAxis2Placement2D", + Location=file.create_entity(type="IfcCartesianPoint", Coordinates=(0.0, 0.0)), + RefDirection=file.createIfcDirection( + (1.0, 0.0), + ), + ), + CoefficientsX=(0.0, 1.0), + CoefficientsY=(A, B, C), + ) + + dx = math.cos(math.atan(start_gradient)) + dy = math.sin(math.atan(start_gradient)) + curve_segment_length = _polynomial_length(A, B, C, horizontal_length) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.create_entity( + type="IfcAxis2Placement2D", + Location=file.create_entity(type="IfcCartesianPoint", Coordinates=(start_distance_along, start_height)), + RefDirection=file.createIfcDirection((dx, dy)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(curve_segment_length), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_circular_arc(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + start_distance_along = design_parameters.StartDistAlong + horizontal_length = design_parameters.HorizontalLength + start_height = design_parameters.StartHeight + start_gradient = design_parameters.StartGradient + end_gradient = design_parameters.EndGradient + radius_of_curvature = design_parameters.RadiusOfCurvature + transition = "DISCONTINUOUS" + + start_angle = math.atan(start_gradient) + end_angle = math.atan(end_gradient) + dx = math.cos(start_angle) + dy = math.sin(start_angle) + if start_angle < end_angle: + radius = horizontal_length / (math.sin(end_angle) - math.sin(start_angle)) + x = -radius * math.sin(start_angle) + y = radius * math.cos(start_angle) + start_angle += 3.0 * math.pi / 2.0 + end_angle += 3.0 * math.pi / 2.0 + else: + radius = horizontal_length / (math.sin(start_angle) - math.sin(end_angle)) + x = radius * math.sin(start_angle) + y = -radius * math.cos(start_angle) + start_angle += math.pi / 2.0 + end_angle += math.pi / 2.0 + + parent_curve = file.createIfcCircle( + Position=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((x, y)), + RefDirection=file.createIfcDirection((1.0, 0.0)), + ), + Radius=radius, + ) + + segment_curve_length = radius * math.fabs(end_angle - start_angle) + + curve_segment = file.create_entity( + type="IfcCurveSegment", + Transition=transition, + Placement=file.createIfcAxis2Placement2D( + Location=file.createIfcCartesianPoint((start_distance_along, start_height)), + RefDirection=file.createIfcDirection( + (dx, dy), + ), + ), + SegmentStart=file.createIfcLengthMeasure(radius * start_angle), + SegmentLength=file.createIfcLengthMeasure(radius * (end_angle - start_angle)), + ParentCurve=parent_curve, + ) + return (curve_segment, None) + + +def _map_clothoid(file: ifcopenshell.file, design_parameters: entity_instance) -> Sequence[entity_instance]: + raise NotImplementedError("mapping for IfcVerticalSegment.CLOTHOID not implemented") + + +def map_alignment_vertical_segment( + file: ifcopenshell.file, design_parameters: entity_instance +) -> Sequence[entity_instance]: + """ + Creates IfcCurveSegment entities for the represention of the supplied IfcAlignmentVerticalSegment business logic entity instance. + A pair of entities is returned for consistency with map_alignment_horizontal_segment and map_alignment_cant_segment. + + """ + expected_type = "IfcAlignmentVerticalSegment" + if not design_parameters.is_a(expected_type): + raise TypeError(f"Expected to see type '{expected_type}', instead received '{design_parameters.is_a()}'.") + + match design_parameters.PredefinedType: + case "CONSTANTGRADIENT": + result = _map_constant_gradient(file, design_parameters) + + case "PARABOLICARC": + result = _map_parabolic_arc(file, design_parameters) + + case "CIRCULARARC": + result = _map_circular_arc(file, design_parameters) + + case "CLOTHOID": + result = _map_clothoid(file, design_parameters) + + case _: + raise TypeError("Unexpected predefined type") + + return result diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/name_segments.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/name_segments.py new file mode 100644 index 0000000000..41d6ff968f --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/name_segments.py @@ -0,0 +1,42 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +from ifcopenshell import entity_instance +from typing import Sequence + + +def name_segments(prefix: str, alignment: entity_instance) -> None: + """ + Sets the segment name like ("H1" for horizontal, "V1" for vertical, "C1" for cant) + + :param prefix: The naming prefix + :param alignment: The alignment whose segments are to be named. This should be a IfcAlignmentHorizontal, IfcAlignmentVertical or IfcAlignmentCant + """ + expected_types = ["IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"] + if not alignment.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{v.is_a()}" + ) + + i = 1 + for rel in alignment.IsNestedBy: + for segment in rel.RelatedObjects: + if segment.is_a("IfcAlignmentSegment"): + segment.Name = f"{prefix}{i}" + i += 1 diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_last_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_last_segment.py new file mode 100644 index 0000000000..3f6d3d8c87 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_last_segment.py @@ -0,0 +1,59 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +import ifcopenshell.api.nest +import ifcopenshell.geom +import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper +import numpy as np +from ifcopenshell import entity_instance +import ifcopenshell.util +import ifcopenshell.util.element + + +def remove_last_segment(file: ifcopenshell.file, entity: entity_instance) -> entity_instance: + """ + Removes the last segment from the end of entity. + + :param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve + :return: The segment + """ + expected_types = [ + "IfcAlignmentHorizontal", + "IfcAlignmentVertical", + "IfcAlignmentCant", + "IfcCompositeCurve", + "IfcGradientCurve", + "IfcSegmentedReferenceCurve", + ] + if not entity.is_a() in expected_types: + raise TypeError( + f"Expected entity type to be one of {[_ for _ in expected_types]}, instead received '{entity.is_a()}" + ) + + if entity.is_a("IfcCompositeCurve"): + last_segment = entity.Segments[-1] + entity.Segments = tuple(set(entity.Segments) - {last_segment}) + entity.Segments[-1].Transition = "DISCONTINUOUS" + return last_segment + else: + components = ifcopenshell.util.element.get_components(entity) + last_segment = components[-1] + ifcopenshell.api.nest.unassign_object(file, (last_segment,)) + return last_segment diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_zero_length_segment.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_zero_length_segment.py new file mode 100644 index 0000000000..00fa307e49 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/remove_zero_length_segment.py @@ -0,0 +1,35 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +from ifcopenshell import entity_instance +import ifcopenshell.api.alignment.remove_last_segment + + +def remove_zero_length_segment(file: ifcopenshell.file, entity: entity_instance) -> entity_instance: + """ + Removes the zero length segment from the end of entity. + + :param entity: An IfcAlignmentHorizontal, IfcAlignmentVertical, IfcAlignmentCant or IfcCompositeCurve + :return: The zero length segment + """ + if not ifcopenshell.api.alignment.has_zero_length_segment(entity): + return None + + return ifcopenshell.api.alignment.remove_last_segment(file, entity) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/update_curve_segment_transition_code.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/update_curve_segment_transition_code.py new file mode 100644 index 0000000000..2eae483eb2 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/update_curve_segment_transition_code.py @@ -0,0 +1,77 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api +from ifcopenshell import ifcopenshell_wrapper +import ifcopenshell.geom +from ifcopenshell import entity_instance +from typing import Sequence +import numpy as np +import math + + +def update_curve_segment_transition_code(prev_segment: entity_instance, segment: entity_instance) -> None: + """ + Updates IfcCurveSegment.Transition of prev_segment based on a comparison of + the position, ref. direction, and curvature at the end of the prev_segment and the start of segment. + """ + expected_type = "IfcCurveSegment" + if not prev_segment.is_a(expected_type): + raise TypeError(f"Expected to see '{expected_type}', instead received '{prev_segment.is_a()}'.") + + if not segment.is_a(expected_type): + raise TypeError(f"Expected to see '{expected_type}', instead received '{segment.is_a()}'.") + + if len(prev_segment.UsingCurves) != 1: + raise TypeError("prev_segment must belong to exactly one curve") + + if len(segment.UsingCurves) != 1: + raise TypeError("segment must belong to exactly one curve") + + if prev_segment.UsingCurves[0] != segment.UsingCurves[0]: + raise TypeError("Both segments must belong to the same curve") + + settings = ifcopenshell.geom.settings() + settings.set("COMPUTE_CURVATURE", True) + + prev_segment_fn = ifcopenshell_wrapper.map_shape(settings, prev_segment.wrapped_data) + prev_segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, prev_segment_fn) + e = prev_segment_evaluator.evaluate(prev_segment_fn.end()) + end = np.array(e) + + # must add the new segment to the container before mapping it, otherwise the segment doesn't + # have enough context to know if it is for horizontal, vertical, cant + + segment_fn = ifcopenshell_wrapper.map_shape(settings, segment.wrapped_data) + segment_evaluator = ifcopenshell_wrapper.function_item_evaluator(settings, segment_fn) + s = segment_evaluator.evaluate(segment_fn.start()) + start = np.array(s) + + same_position = True if np.allclose(end[:3], start[:3]) else False + same_gradient = True if np.allclose(end[:0], start[:0]) else False + same_curvature = True if np.allclose(end[3:], start[3:]) else False + + if same_position: + prev_segment.Transition = "CONTINUOUS" + if same_gradient: + prev_segment.Transition = "CONTSAMEGRADIENT" + if same_curvature: + prev_segment.Transition = "CONTSAMEGRADIENTSAMECURVATURE" + else: + prev_segment.Transition = "DISCONTINUOUS" diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/util.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/util.py new file mode 100644 index 0000000000..0a07c8a328 --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/util.py @@ -0,0 +1,134 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +import math +from typing import Sequence + +import numpy as np + +import ifcopenshell +import ifcopenshell.geom +import ifcopenshell.guid +import ifcopenshell.template +from ifcopenshell import entity_instance +from ifcopenshell import ifcopenshell_wrapper +import ifcopenshell.util +import ifcopenshell.util.stationing + + +def evaluate_representation(shape_rep: entity_instance, dist_along: float) -> np.ndarray: + """ + Calculate the 4x4 geometric transform at a point on an alignment segment + + :param shape_rep: The representation shape (composite curve, gradient curve, or segmented reference curve) to evaluate + :param dist_along: The distance along this representation at the point of interest (point to be calculated) + """ + supported_rep_types = ["IFCCOMPOSITECURVE", "IFCGRADIENTCURVE", "IFCSEGMENTEDREFERENCECURVE"] + shape_rep_type = shape_rep.is_a().upper() + if not shape_rep_type in supported_rep_types: + raise NotImplementedError( + f"Expected entity type to be one of {[_ for _ in supported_rep_types]}, got '{shape_rep_type}" + ) + + # TODO: confirm point is not beyond limits of alignment + + s = ifcopenshell.geom.settings() + function_item = ifcopenshell_wrapper.map_shape(s, shape_rep.wrapped_data) + evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) + + trans_matrix = evaluator.evaluate(dist_along) + + return np.array(trans_matrix, dtype=np.float64).T + + +def evaluate_segment(segment: entity_instance, dist_along: float) -> np.ndarray: + """ + Calculate the 4x4 geometric transform at a point on an alignment segment + + :param segment: The segment containing the point that we would like to + :param dist_along: The distance along this segment at the point of interest (point to be calculated) + """ + supported_segment_types = ["IFCCURVESEGMENT"] + segment_type = segment.is_a().upper() + if not segment_type in supported_segment_types: + raise NotImplementedError(f"Expected entity type 'IFCCURVESEGMENT', got '{segment_type}") + if dist_along > segment.SegmentLength: + raise ValueError(f"Provided value {dist_along=} is beyond the end of the segment ({segment.SegmentLength}).") + + s = ifcopenshell.geom.settings() + function_item = ifcopenshell_wrapper.map_shape(s, segment.wrapped_data) + evaluator = ifcopenshell_wrapper.function_item_evaluator(s, function_item) + + trans_matrix = evaluator.evaluate(dist_along) + + return np.array(trans_matrix, dtype=np.float64).T + + +def generate_vertices(rep_curve: entity_instance, distance_interval: float = 5.0) -> np.ndarray: + """ + Generate vertices along an alignment + + :param rep_curve: The alignment's representation curve to use to generate vertices. + :param distance_interval: The distance between points along the alignment at which to generate the points + """ + if rep_curve is None: + raise ValueError("Alignment representation not found.") + + supported_rep_types = ["IFCCOMPOSITECURVE", "IFCGRADIENTCURVE", "IFCSEGMENTEDREFERENCECURVE"] + shape_rep_type = rep_curve.is_a().upper() + if not shape_rep_type in supported_rep_types: + raise NotImplementedError( + f"Expected entity type to be one of {[_ for _ in supported_rep_types]}, got '{shape_rep_type}" + ) + + s = ifcopenshell.geom.settings() + s.set("piecewise-step-type", 0) # 0 = step-size is maximum step size, 1 = step-size is mininimum number of steps + s.set("piecewise-step-size", distance_interval) + shape = ifcopenshell.geom.create_shape(s, rep_curve) + vertices = shape.verts + if len(vertices) == 0: + msg = f"[ERROR] No vertices generated by ifcopenshell.geom.create_shape()." + raise ValueError(msg) + return np.array(vertices).reshape((-1, 3)) + + +def print_alignment(alignment, indent=0): + """ + Debugging function to print alignment decomposition + """ + print(" " * indent, alignment) + + for rel in alignment.IsNestedBy: + for child in rel.RelatedObjects: + print_alignment(child, indent + 2) + + for agg in alignment.IsDecomposedBy: + for child in agg.RelatedObjects: + print_alignment(child, indent + 2) + + +def print_composite_curve(curve): + """ + Debugging function to print composite curve segments + """ + print(str(curve)[0:100]) + + for segment in curve.Segments: + print(" " * 2, segment) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index d56c9873a8..508aa449e4 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -68,6 +68,7 @@ SETTING = Literal[ "building-local-placement", "cgal-original-edges", "circle-segments", + "compute-curvature", "context-identifiers", "context-ids", "context-types", diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_curve.py b/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_curve.py new file mode 100644 index 0000000000..8098f7b51b --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_curve.py @@ -0,0 +1,72 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_add_segment_to_curve(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + circular_arc = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((4084.115884, 3889.462938)), + file.createIfcDirection((0.224530986099614, 0.974466949814685)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(-1848.115835), + ParentCurve=file.createIfcCircle( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), file.createIfcDirection((1.0, 0.0)) + ), + Radius=1250.0, + ), + ) + + line = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((5469.395067, 4847.56631)), + file.createIfcDirection((0.991014275066766, -0.133756146078947)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(1564.635765), + ParentCurve=file.createIfcLine( + Pnt=file.createIfcCartesianPoint((0.0, 0.0)), + Dir=file.createIfcVector(Orientation=file.createIfcDirection((1.0, 0.0)), Magnitude=1.0), + ), + ) + + composite_curve = file.createIfcCompositeCurve(SelfIntersect=False) + + ifcopenshell.api.alignment.add_segment_to_curve(file, circular_arc, composite_curve) + assert circular_arc.UsingCurves[0] == composite_curve + assert composite_curve.Segments[-1] == circular_arc + + ifcopenshell.api.alignment.add_segment_to_curve(file, line, composite_curve) + assert line.UsingCurves[0] == composite_curve + assert composite_curve.Segments[-1] == line diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py b/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py new file mode 100644 index 0000000000..96ad157f60 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_add_segment_to_layout.py @@ -0,0 +1,75 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_add_segment_to_layout(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + horizontal_alignment = file.create_entity( + type="IfcAlignmentHorizontal", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + ) + + design_parameters = file.create_entity( + type="IfcAlignmentHorizontalSegment", + StartTag=None, + EndTag=None, + StartPoint=file.createIfcCartesianPoint(Coordinates=((0.0, 0.0))), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + GravityCenterLineHeight=None, + PredefinedType="LINE", + ) + alignment_segment = file.create_entity( + type="IfcAlignmentSegment", + GlobalId=ifcopenshell.guid.new(), + OwnerHistory=None, + Name=None, + Description=None, + ObjectType=None, + ObjectPlacement=None, + Representation=None, + DesignParameters=design_parameters, + ) + + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal_alignment, alignment_segment) + + assert len(horizontal_alignment.IsNestedBy) == 1 + assert len(horizontal_alignment.IsNestedBy[0].RelatedObjects) == 1 + assert horizontal_alignment.IsNestedBy[0].RelatedObjects[0] == alignment_segment diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_stationing_to_alignment.py b/src/ifcopenshell-python/test/api/alignment/test_add_stationing_to_alignment.py new file mode 100644 index 0000000000..e559653618 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_add_stationing_to_alignment.py @@ -0,0 +1,56 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_add_stationing_to_alignment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + + ifcopenshell.api.alignment.add_stationing_to_alignment(file, alignment, 2000.0) + + for rel in alignment.IsNestedBy: + for referent in rel.RelatedObjects: + if referent.is_a("IfcReferent"): + assert referent.PredefinedType == "STATION" + assert referent.Name == "2+000.000" + assert ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing") + assert ( + ifcopenshell.util.element.get_pset(element=referent, name="Pset_Stationing", prop="Station") + == 2000.0 + ) diff --git a/src/ifcopenshell-python/test/api/alignment/test_add_vertical_by_pi_method.py b/src/ifcopenshell-python/test/api/alignment/test_add_vertical_by_pi_method.py new file mode 100644 index 0000000000..53a8f7c0df --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_add_vertical_by_pi_method.py @@ -0,0 +1,74 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_add_vertical_by_pi_method(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + # single horizontal alignment + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method(file, "TestAlignment", coordinates, radii) + + assert len(alignment.IsDecomposedBy) == 0 # no child alignments + assert len(alignment.IsNestedBy) == 1 # nesting IfcAlignemtHorizontal + assert len(alignment.IsNestedBy[0].RelatedObjects) == 1 # nesting one IfcAlignmentHorizontal + assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcAlignmentHorizontal") + assert ( + len(alignment.IsNestedBy[0].RelatedObjects[0].IsNestedBy) == 1 + ) # nesting of segments beneath IfcAlignmentHorizontal + assert len(alignment.IsNestedBy[0].RelatedObjects[0].IsNestedBy[0].RelatedObjects) == 8 # segments + + # add first vertical + ifcopenshell.api.alignment.add_vertical_alignment_by_pi_method(file, alignment, vpoints, lengths) + assert len(alignment.IsDecomposedBy) == 0 # no child alignments + assert len(alignment.IsNestedBy) == 1 # 1 nesting relationsip for the alignments + assert len(alignment.IsNestedBy[0].RelatedObjects) == 2 # nesting IfcAlignmentHorizontal and IfcAlignmentVertical + assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcAlignmentHorizontal") + assert alignment.IsNestedBy[0].RelatedObjects[1].is_a("IfcAlignmentVertical") + + # add second vertical + ifcopenshell.api.alignment.add_vertical_alignment_by_pi_method(file, alignment, vpoints, lengths) + assert len(alignment.IsDecomposedBy) == 1 # 1 IfcRelAggreates relationship for the child algiments + assert ( + len(alignment.IsDecomposedBy[0].RelatedObjects) == 2 + ) # two child alignments, one for the first vertical and one for the vertical just added + for child_alignment in alignment.IsDecomposedBy[0].RelatedObjects: + assert child_alignment.is_a("IfcAlignment") + assert len(child_alignment.IsNestedBy) == 1 # one nesting relationship for the IfcAlignmentVertical + assert len(child_alignment.IsNestedBy[0].RelatedObjects) == 1 # The IfcAlignmentVertical + assert child_alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcAlignmentVertical") + assert len(alignment.IsNestedBy) == 1 # 1 nesting relationsip for the alignments + assert len(alignment.IsNestedBy[0].RelatedObjects) == 1 # nesting one IfcAlignmentHorizontal + assert alignment.IsNestedBy[0].RelatedObjects[0].is_a("IfcAlignmentHorizontal") diff --git a/src/ifcopenshell-python/test/api/alignment/test_get_basis_curve.py b/src/ifcopenshell-python/test/api/alignment/test_get_basis_curve.py new file mode 100644 index 0000000000..ee1a65af19 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_get_basis_curve.py @@ -0,0 +1,70 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest + +# import test.bootstrap +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +# class TestGetBasisCurve(test.bootstrap.IFC4X3): +def test_horizontal(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method(file, "TestAlignment", coordinates, radii) + ifcopenshell.api.alignment.create_geometric_representation(file, alignment) + basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + assert basis_curve.is_a("IfcCompositeCurve") + + +def test_horizontal_and_vertical(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + ifcopenshell.api.alignment.create_geometric_representation(file, alignment) + basis_curve = ifcopenshell.api.alignment.get_basis_curve(alignment) + assert basis_curve.is_a("IfcCompositeCurve") diff --git a/src/ifcopenshell-python/test/api/alignment/test_get_curve.py b/src/ifcopenshell-python/test/api/alignment/test_get_curve.py new file mode 100644 index 0000000000..a00104d1b6 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_get_curve.py @@ -0,0 +1,70 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest + +# import test.bootstrap +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +# class TestGetCurve(test.bootstrap.IFC4X3): +def test_horizontal(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method(file, "TestAlignment", coordinates, radii) + ifcopenshell.api.alignment.create_geometric_representation(file, alignment) + curve = ifcopenshell.api.alignment.get_curve(alignment) + assert curve.is_a("IfcCompositeCurve") + + +def test_horizontal_and_vertical(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + ifcopenshell.api.alignment.create_geometric_representation(file, alignment) + curve = ifcopenshell.api.alignment.get_curve(alignment) + assert curve.is_a("IfcGradientCurve") diff --git a/src/ifcopenshell-python/test/api/alignment/test_has_zero_length_segment.py b/src/ifcopenshell-python/test/api/alignment/test_has_zero_length_segment.py new file mode 100644 index 0000000000..b70002baa5 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_has_zero_length_segment.py @@ -0,0 +1,123 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.alignment.has_zero_length_segment +import ifcopenshell.api.alignment.remove_zero_length_segment +import ifcopenshell.api.context +import ifcopenshell.guid +import ifcopenshell.api.nest + + +def _test_business_definition(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + horizontal = file.createIfcAlignmentHorizontal("Horizontal Alignment") + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + segment = file.createIfcAlignmentSegment(GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters) + ifcopenshell.api.nest.assign_object( + file, + related_objects=[ + segment, + ], + relating_object=horizontal, + ) + + assert False == ifcopenshell.api.alignment.has_zero_length_segment(horizontal) + + ifcopenshell.api.alignment.add_zero_length_segment(file, horizontal) + assert len(horizontal.IsNestedBy[0].RelatedObjects) == 2 + + assert True == ifcopenshell.api.alignment.has_zero_length_segment(horizontal) + + zero_length_segment = ifcopenshell.api.alignment.remove_zero_length_segment(file, horizontal) + assert len(horizontal.IsNestedBy[0].RelatedObjects) == 1 + assert False == ifcopenshell.api.alignment.has_zero_length_segment(horizontal) + + ifcopenshell.api.alignment.add_segment_to_layout(file, horizontal, zero_length_segment) + assert len(horizontal.IsNestedBy[0].RelatedObjects) == 2 + assert True == ifcopenshell.api.alignment.has_zero_length_segment(horizontal) + + +def _test_geometric_definition(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + circular_arc = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((4084.115884, 3889.462938)), + file.createIfcDirection((0.224530986099614, 0.974466949814685)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(-1848.115835), + ParentCurve=file.createIfcCircle( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), file.createIfcDirection((1.0, 0.0)) + ), + Radius=1250.0, + ), + ) + + composite_curve = file.createIfcCompositeCurve(Segments=(circular_arc,), SelfIntersect=False) + + assert False == ifcopenshell.api.alignment.has_zero_length_segment(composite_curve) + + ifcopenshell.api.alignment.add_zero_length_segment(file, composite_curve) + + assert True == ifcopenshell.api.alignment.has_zero_length_segment(composite_curve) + + assert len(composite_curve.Segments) == 2 + + zero_length_segment = ifcopenshell.api.alignment.remove_zero_length_segment(file, composite_curve) + assert len(composite_curve.Segments) == 1 + assert False == ifcopenshell.api.alignment.has_zero_length_segment(composite_curve) + + ifcopenshell.api.alignment.add_segment_to_curve(file, zero_length_segment, composite_curve) + assert len(composite_curve.Segments) == 2 + assert True == ifcopenshell.api.alignment.has_zero_length_segment(composite_curve) + + segment = composite_curve.Segments[-1] + assert segment.Placement.Location.Coordinates == (5469.394535876198, 4847.567078630914) + assert segment.Placement.RefDirection.DirectionRatios == (0.9910142986043448, -0.13375597168627318) + + +def test_has_zero_length_segment(): + _test_business_definition() + _test_geometric_definition() diff --git a/src/ifcopenshell-python/test/api/alignment/test_map_alignment_cant_segment.py b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_cant_segment.py new file mode 100644 index 0000000000..755fe3e13c --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_cant_segment.py @@ -0,0 +1,26 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_map_alignment_cant_segment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + # create tests as the mapping functions are implemented diff --git a/src/ifcopenshell-python/test/api/alignment/test_map_alignment_horizontal_segment.py b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_horizontal_segment.py new file mode 100644 index 0000000000..0d8e55293d --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_horizontal_segment.py @@ -0,0 +1,2101 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +# These are test cases generated from https://github.com/bSI-RailwayRoom/IFC-Rail-Unit-Test-Reference-Code/tree/master/alignment_testset/IFC-WithGeneratedGeometry +# for horizontal alignment. + +import pytest +import ifcopenshell.api.alignment + + +def _BlossCurve_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(120.989673502444) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-112.624788044361) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + + +def _BlossCurve_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(-120.989673502444) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(112.624788044361) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + + +def _BlossCurve_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(110.668191970032) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + + +def _BlossCurve_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(-110.668191970032) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(100.0) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + + +def _BlossCurve_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(-120.989673502444) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(112.624788044361) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(1000.0) + + +def _BlossCurve_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(120.989673502444) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-112.624788044361) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-1000.0) + + +def _BlossCurve_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(-110.668191970032) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(100.0) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + + +def _BlossCurve_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="BLOSSCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcThirdOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CubicTerm == pytest.approx(110.668191970032) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + + +def _CircularArc_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(1000.0) + + +def _CircularArc_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _CircularArc_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(300.0) + + +def _Clothoid_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-142.857142857143) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(-207.019667802706) + + +def _Clothoid_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-142.857142857143) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(207.019667802706) + + +def _Clothoid_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(-173.205080756888) + + +def _Clothoid_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(173.205080756888) + + +def _Clothoid_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(42.8571428571429) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(207.019667802706) + + +def _Clothoid_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(42.8571428571429) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(-207.019667802706) + + +def _Clothoid_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(173.205080756888) + + +def _Clothoid_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CLOTHOID", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcClothoid") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.ClothoidConstant == pytest.approx(-173.205080756888) + + +def _CosineCurve_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(857.142857142857) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(461.538461538462) + + +def _CosineCurve_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(-857.142857142857) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-461.538461538462) + + +def _CosineCurve_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(600.0) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(600.0) + + +def _CosineCurve_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(-600.0) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-600.0) + + +def _CosineCurve_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(-857.142857142857) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(461.538461538462) + + +def _CosineCurve_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(857.142857142857) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-461.538461538462) + + +def _CosineCurve_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(-600.0) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(600.0) + + +def _CosineCurve_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="COSINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcCosineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CosineTerm == pytest.approx(600.0) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-600.0) + + +def _Cubic_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-142.857142857143) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, -3.88888888888889e-06)) + + +def _Cubic_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-142.857142857143) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, 3.88888888888889e-06)) + + +def _Cubic_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, -5.55555555555556e-06)) + + +def _Cubic_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(-100.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, 5.55555555555556e-06)) + + +def _Cubic_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(42.8571428571429) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, 3.88888888888889e-06)) + + +def _Cubic_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(42.8571428571429) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, -3.88888888888889e-06)) + + +def _Cubic_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, 5.55555555555556e-06)) + + +def _Cubic_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="CUBIC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((0.0, 0.0, 0.0, -5.55555555555556e-06)) + + +def _HelmertCurve_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.7998035122387, 3.91603145329256)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.9892460407218963, 0.146260968532457) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-0.009321141429516372, 0.46831933573745577) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9992574637140321, -0.03852948496670688) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-103.509833901353) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(176.470588235294) + + +def _HelmertCurve_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.7998035122387, -3.91603145329256)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.9892460407218963, -0.146260968532457) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-0.009321141429516372, -0.46831933573745577) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9992574637140321, 0.03852948496670688) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(103.509833901353) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-176.470588235294) + + +def _HelmertCurve_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.8122545525202, 3.81263503030693)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.9904138664989948, 0.1381317235341378) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-0.010305467756443198, 0.6738837916692928) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9984794480380026, -0.05512523782919828) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-86.6025403784439) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(150.0) + + +def _HelmertCurve_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.8122545525202, -3.81263503030693)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.9904138664989948, -0.1381317235341378) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-0.010305467756443198, -0.6738837916692928) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9984794480380026, 0.05512523782919828) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(86.6025403784439) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-150.0) + + +def _HelmertCurve_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(1000.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.9681012468824, 1.49252747074135)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.997594495159641, 0.0693197174487962) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (0.010408767953926904, -0.4828832446956578) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9992463304688143, 0.03881714884698913) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(103.509833901353) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-750.0) + + +def _HelmertCurve_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-1000.0) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.9681012468824, -1.49252747074135)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.997594495159641, -0.0693197174487962) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (0.010408767953926904, 0.4828832446956578) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9992463304688143, -0.03881714884698913) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(128.92319893893) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-103.509833901353) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(750.0) + + +def _HelmertCurve_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.9972443634885, 0.347204361427475)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.999614222337484, 0.027769614722351524) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (0.011625841243773832, -0.6968669147609581) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9984543318840984, 0.05557829739996359) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(86.6025403784439) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + + +def _HelmertCurve_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="HELMERTCURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(-114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(None) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + mapped_segment = mapped_segments[1] + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((49.9972443634885, -0.347204361427475)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.999614222337484, -0.027769614722351524) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(50.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(50.0) + assert mapped_segment.ParentCurve.is_a("IfcSecondOrderPolynomialSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (0.011625841243773832, 0.6968669147609581) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx( + (0.9984543318840984, -0.05557829739996359) + ) + assert mapped_segment.ParentCurve.QuadraticTerm == pytest.approx(114.471424255333) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-86.6025403784439) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + + +def _Line_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _Line_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="LINE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _SineCurve_100_0_300_1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=1000.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(2692.79370307697) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-207.019667802706) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + + +def _SineCurve_100_0__300__1000_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=-1000.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(-2692.79370307697) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(207.019667802706) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + + +def _SineCurve_100_0_300_inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(1884.95559215388) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-173.205080756888) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(300.0) + + +def _SineCurve_100_0__300__inf_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-300.0, + EndRadiusOfCurvature=0.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(-1884.95559215388) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(173.205080756888) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-300.0) + + +def _SineCurve_100_0_1000_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=1000.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(-2692.79370307697) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(207.019667802706) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(1000.0) + + +def _SineCurve_100_0__1000__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=-1000.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(2692.79370307697) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-207.019667802706) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(-1000.0) + + +def _SineCurve_100_0_inf_300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=300.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(-1884.95559215388) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(173.205080756888) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + + +def _SineCurve_100_0__inf__300_1_Meter(file): + design_parameters = file.createIfcAlignmentHorizontalSegment( + StartPoint=file.createIfcCartesianPoint((0.0, 0.0)), + StartDirection=0.0, + StartRadiusOfCurvature=0.0, + EndRadiusOfCurvature=-300.0, + SegmentLength=100.0, + PredefinedType="SINECURVE", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcSineSpiral") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.SineTerm == pytest.approx(1884.95559215388) + assert mapped_segment.ParentCurve.LinearTerm == pytest.approx(-173.205080756888) + assert mapped_segment.ParentCurve.ConstantTerm == pytest.approx(None) + + +def test_map_alignment_horizontal_segment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + _BlossCurve_100_0_300_1000_1_Meter(file) + _BlossCurve_100_0__300__1000_1_Meter(file) + _BlossCurve_100_0_300_inf_1_Meter(file) + _BlossCurve_100_0__300__inf_1_Meter(file) + _BlossCurve_100_0_1000_300_1_Meter(file) + _BlossCurve_100_0__1000__300_1_Meter(file) + _BlossCurve_100_0_inf_300_1_Meter(file) + _BlossCurve_100_0__inf__300_1_Meter(file) + _CircularArc_100_0_300_1000_1_Meter(file) + _CircularArc_100_0__300__1000_1_Meter(file) + _CircularArc_100_0_300_inf_1_Meter(file) + _CircularArc_100_0__300__inf_1_Meter(file) + _CircularArc_100_0_1000_300_1_Meter(file) + _CircularArc_100_0__1000__300_1_Meter(file) + _CircularArc_100_0_inf_300_1_Meter(file) + _CircularArc_100_0__inf__300_1_Meter(file) + _Clothoid_100_0_300_1000_1_Meter(file) + _Clothoid_100_0__300__1000_1_Meter(file) + _Clothoid_100_0_300_inf_1_Meter(file) + _Clothoid_100_0__300__inf_1_Meter(file) + _Clothoid_100_0_1000_300_1_Meter(file) + _Clothoid_100_0__1000__300_1_Meter(file) + _Clothoid_100_0_inf_300_1_Meter(file) + _Clothoid_100_0__inf__300_1_Meter(file) + _CosineCurve_100_0_300_1000_1_Meter(file) + _CosineCurve_100_0__300__1000_1_Meter(file) + _CosineCurve_100_0_300_inf_1_Meter(file) + _CosineCurve_100_0__300__inf_1_Meter(file) + _CosineCurve_100_0_1000_300_1_Meter(file) + _CosineCurve_100_0__1000__300_1_Meter(file) + _CosineCurve_100_0_inf_300_1_Meter(file) + _CosineCurve_100_0__inf__300_1_Meter(file) + _Cubic_100_0_300_1000_1_Meter(file) + _Cubic_100_0__300__1000_1_Meter(file) + _Cubic_100_0_300_inf_1_Meter(file) + _Cubic_100_0__300__inf_1_Meter(file) + _Cubic_100_0_1000_300_1_Meter(file) + _Cubic_100_0__1000__300_1_Meter(file) + _Cubic_100_0_inf_300_1_Meter(file) + _Cubic_100_0__inf__300_1_Meter(file) + _HelmertCurve_100_0_300_1000_1_Meter(file) + _HelmertCurve_100_0__300__1000_1_Meter(file) + _HelmertCurve_100_0_300_inf_1_Meter(file) + _HelmertCurve_100_0__300__inf_1_Meter(file) + _HelmertCurve_100_0_1000_300_1_Meter(file) + _HelmertCurve_100_0__1000__300_1_Meter(file) + _HelmertCurve_100_0_inf_300_1_Meter(file) + _HelmertCurve_100_0__inf__300_1_Meter(file) + _Line_100_0_300_1000_1_Meter(file) + _Line_100_0__300__1000_1_Meter(file) + _Line_100_0_300_inf_1_Meter(file) + _Line_100_0__300__inf_1_Meter(file) + _Line_100_0_1000_300_1_Meter(file) + _Line_100_0__1000__300_1_Meter(file) + _Line_100_0_inf_300_1_Meter(file) + _Line_100_0__inf__300_1_Meter(file) + _SineCurve_100_0_300_1000_1_Meter(file) + _SineCurve_100_0__300__1000_1_Meter(file) + _SineCurve_100_0_300_inf_1_Meter(file) + _SineCurve_100_0__300__inf_1_Meter(file) + _SineCurve_100_0_1000_300_1_Meter(file) + _SineCurve_100_0__1000__300_1_Meter(file) + _SineCurve_100_0_inf_300_1_Meter(file) + _SineCurve_100_0__inf__300_1_Meter(file) + + # VIENESSE BEND NOT IMPLEMENTED diff --git a/src/ifcopenshell-python/test/api/alignment/test_map_alignment_segments.py b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_segments.py new file mode 100644 index 0000000000..1c9d04bc24 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_segments.py @@ -0,0 +1,102 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_map_alignment_horizontal_segment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + + horizontal_alignment = alignment.IsNestedBy[0].RelatedObjects[0] + assert horizontal_alignment.is_a("IfcAlignmentHorizontal") + + composite_curve = file.create_entity( + type="IfcCompositeCurve", + Segments=[], + SelfIntersect=False, + ) + + ifcopenshell.api.alignment.map_alignment_segments(file, horizontal_alignment, composite_curve) + assert len(composite_curve.Segments) == 8 + assert composite_curve.Segments[0].ParentCurve.is_a("IfcLine") + assert composite_curve.Segments[0].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[1].ParentCurve.is_a("IfcCircle") + assert composite_curve.Segments[1].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[2].ParentCurve.is_a("IfcLine") + assert composite_curve.Segments[2].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[3].ParentCurve.is_a("IfcCircle") + assert composite_curve.Segments[3].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[4].ParentCurve.is_a("IfcLine") + assert composite_curve.Segments[4].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[5].ParentCurve.is_a("IfcCircle") + assert composite_curve.Segments[5].Transition == "CONTSAMEGRADIENT" + assert composite_curve.Segments[6].ParentCurve.is_a("IfcLine") + assert composite_curve.Segments[6].Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert composite_curve.Segments[7].ParentCurve.is_a("IfcLine") + assert composite_curve.Segments[7].Transition == "DISCONTINUOUS" + + vertical_alignment = alignment.IsNestedBy[0].RelatedObjects[1] + assert vertical_alignment.is_a("IfcAlignmentVertical") + + gradient_curve = file.create_entity( + type="IfcGradientCurve", Segments=[], SelfIntersect=False, BaseCurve=composite_curve, EndPoint=None + ) + + ifcopenshell.api.alignment.map_alignment_segments(file, vertical_alignment, gradient_curve) + assert len(gradient_curve.Segments) == 10 + + assert gradient_curve.Segments[0].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[0].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[1].ParentCurve.is_a("IfcPolynomialCurve") + assert gradient_curve.Segments[1].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[2].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[2].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[3].ParentCurve.is_a("IfcPolynomialCurve") + assert gradient_curve.Segments[3].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[4].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[4].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[5].ParentCurve.is_a("IfcPolynomialCurve") + assert gradient_curve.Segments[5].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[6].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[6].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[7].ParentCurve.is_a("IfcPolynomialCurve") + assert gradient_curve.Segments[7].Transition == "CONTSAMEGRADIENT" + assert gradient_curve.Segments[8].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[8].Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert gradient_curve.Segments[9].ParentCurve.is_a("IfcLine") + assert gradient_curve.Segments[9].Transition == "DISCONTINUOUS" diff --git a/src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py new file mode 100644 index 0000000000..867183d369 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_map_alignment_vertical_segment.py @@ -0,0 +1,795 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +# These are test cases generated from https://github.com/bSI-RailwayRoom/IFC-Rail-Unit-Test-Reference-Code/tree/master/alignment_testset/IFC-WithGeneratedGeometry +# for vertical alignment. + +import pytest +import ifcopenshell.api.alignment + + +def _CircularArc_100_0_10_0_0_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=0.5, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(1053.72220965611) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(103.674757133105) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((-0.0, 223.606797749979)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(223.606797749979) + + +def _CircularArc_100_0_10_0_0_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=-0.5, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(351.240736552036) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-103.674757133105) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-1.36919674566051e-14, -223.606797749979) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(223.606797749979) + + +def _CircularArc_100_0_10_0_0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=0.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(454.915493685141) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-103.674757133105) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((100.0, -200.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(223.606797749979) + + +def _CircularArc_100_0_10_0__0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=0.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(950.047452523004) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(103.674757133105) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((100.0, 200.0)) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(223.606797749979) + + +def _CircularArc_100_0_10_0_0_5_1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=1.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(1991.60150186753) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(123.801073716741) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-172.075922005613, 344.151844011225) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(384.773458895502) + + +def _CircularArc_100_0_10_0__0_5__1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=-1.0, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(426.001441657352) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-123.801073716741) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (-172.075922005613, -344.151844011225) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(384.773458895502) + + +def _CircularArc_100_0_10_0_1_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=1.0, + EndGradient=0.5, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, 0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(906.601103821832) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(-123.801073716741) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (272.075922005613, -272.075922005613) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(384.773458895502) + + +def _CircularArc_100_0_10_0__1_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-1.0, + EndGradient=-0.5, + PredefinedType="CIRCULARARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, -0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(1511.00183970305) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(123.801073716741) + assert mapped_segment.ParentCurve.is_a("IfcCircle") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx( + (272.075922005613, 272.075922005613) + ) + assert mapped_segment.ParentCurve.Position.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Radius == pytest.approx(384.773458895502) + + +def _ConstantGradient_100_0_10_0_0_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=0.5, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0_0_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=-0.5, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(100.0) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0_0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=0.0, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(111.803398874989) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0__0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=0.0, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(111.803398874989) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0_0_5_1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=1.0, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(111.803398874989) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0__0_5__1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=-1.0, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(111.803398874989) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0_1_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=1.0, + EndGradient=0.5, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, 0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(141.42135623731) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ConstantGradient_100_0_10_0__1_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-1.0, + EndGradient=-0.5, + PredefinedType="CONSTANTGRADIENT", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, -0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(141.42135623731) + assert mapped_segment.ParentCurve.is_a("IfcLine") + assert mapped_segment.ParentCurve.Pnt.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Orientation.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.ParentCurve.Dir.Magnitude == pytest.approx(1.0) + + +def _ParabolicArc_100_0_10_0_0_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=0.5, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(104.02288238772185) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, 0.0, 0.0025)) + + +def _ParabolicArc_100_0_10_0_0_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.0, + EndGradient=-0.5, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx((1.0, 0.0)) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(104.02288238772185) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, 0.0, -0.0025)) + + +def _ParabolicArc_100_0_10_0_0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=0.0, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(104.02288238772185) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, 0.5, -0.0025)) + + +def _ParabolicArc_100_0_10_0__0_5_0_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=0.0, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(104.02288238772185) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, -0.5, 0.0025)) + + +def _ParabolicArc_100_0_10_0_0_5_1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=0.5, + EndGradient=1.0, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, 0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(125.53583325398947) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, 0.5, 0.0025)) + + +def _ParabolicArc_100_0_10_0__0_5__1_0_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-0.5, + EndGradient=-1.0, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.894427190999916, -0.447213595499958) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(125.53583325398947) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, -0.5, -0.0025)) + + +def _ParabolicArc_100_0_10_0_1_0_0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=1.0, + EndGradient=0.5, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, 0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(125.53583325398947) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, 1.0, -0.0025)) + + +def _ParabolicArc_100_0_10_0__1_0__0_5_1_Meter(file): + design_parameters = file.createIfcAlignmentVerticalSegment( + StartDistAlong=0.0, + HorizontalLength=100.0, + StartHeight=10.0, + StartGradient=-1.0, + EndGradient=-0.5, + PredefinedType="PARABOLICARC", + ) + + alignment_segment = file.createIfcAlignmentSegment( + GlobalId=ifcopenshell.guid.new(), DesignParameters=design_parameters + ) + + mapped_segments = ifcopenshell.api.alignment.map_alignment_segment(file, alignment_segment) + mapped_segment = mapped_segments[0] + assert len(mapped_segments) == 2 + assert mapped_segments[1] == None + assert "DISCONTINUOUS" == mapped_segment.Transition + assert mapped_segment.Placement.Location.Coordinates == pytest.approx((0.0, 10.0)) + assert mapped_segment.Placement.RefDirection.DirectionRatios == pytest.approx( + (0.707106781186547, -0.707106781186547) + ) + assert mapped_segment.SegmentStart.wrappedValue == pytest.approx(0.0) + assert mapped_segment.SegmentLength.wrappedValue == pytest.approx(125.53583325398947) + assert mapped_segment.ParentCurve.is_a("IfcPolynomialCurve") + assert mapped_segment.ParentCurve.Position.Location.Coordinates == pytest.approx((0.0, 0.0)) + assert mapped_segment.ParentCurve.CoefficientsX == pytest.approx((0.0, 1.0)) + assert mapped_segment.ParentCurve.CoefficientsY == pytest.approx((10.0, -1.0, 0.0025)) + + +def test_map_alignment_vertical_segment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + _CircularArc_100_0_10_0_0_0_0_5_1_Meter(file) + _CircularArc_100_0_10_0_0_0__0_5_1_Meter(file) + _CircularArc_100_0_10_0_0_5_0_0_1_Meter(file) + _CircularArc_100_0_10_0__0_5_0_0_1_Meter(file) + _CircularArc_100_0_10_0_0_5_1_0_1_Meter(file) + _CircularArc_100_0_10_0__0_5__1_0_1_Meter(file) + _CircularArc_100_0_10_0_1_0_0_5_1_Meter(file) + _CircularArc_100_0_10_0__1_0__0_5_1_Meter(file) + _ConstantGradient_100_0_10_0_0_0_0_5_1_Meter(file) + _ConstantGradient_100_0_10_0_0_0__0_5_1_Meter(file) + _ConstantGradient_100_0_10_0_0_5_0_0_1_Meter(file) + _ConstantGradient_100_0_10_0__0_5_0_0_1_Meter(file) + _ConstantGradient_100_0_10_0_0_5_1_0_1_Meter(file) + _ConstantGradient_100_0_10_0__0_5__1_0_1_Meter(file) + _ConstantGradient_100_0_10_0_1_0_0_5_1_Meter(file) + _ConstantGradient_100_0_10_0__1_0__0_5_1_Meter(file) + _ParabolicArc_100_0_10_0_0_0_0_5_1_Meter(file) + _ParabolicArc_100_0_10_0_0_0__0_5_1_Meter(file) + _ParabolicArc_100_0_10_0_0_5_0_0_1_Meter(file) + _ParabolicArc_100_0_10_0__0_5_0_0_1_Meter(file) + _ParabolicArc_100_0_10_0_0_5_1_0_1_Meter(file) + _ParabolicArc_100_0_10_0__0_5__1_0_1_Meter(file) + _ParabolicArc_100_0_10_0_1_0_0_5_1_Meter(file) + _ParabolicArc_100_0_10_0__1_0__0_5_1_Meter(file) + + # VERTICAL CLOTHOID NOT IMPLEMENTED diff --git a/src/ifcopenshell-python/test/api/alignment/test_name_segments.py b/src/ifcopenshell-python/test/api/alignment/test_name_segments.py new file mode 100644 index 0000000000..4ad01063d5 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_name_segments.py @@ -0,0 +1,53 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_name_segments(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + + for rel in alignment.IsNestedBy: + for a in rel.RelatedObjects: + if a.is_a("IfcLinearElement"): + ifcopenshell.api.alignment.name_segments("Q", a) + i = 1 + for sr in a.IsNestedBy: + for s in sr.RelatedObjects: + assert f"Q{i}" == s.Name + i += 1 diff --git a/src/ifcopenshell-python/test/api/alignment/test_update_curve_segment_transition_code.py b/src/ifcopenshell-python/test/api/alignment/test_update_curve_segment_transition_code.py new file mode 100644 index 0000000000..fcfe6cb2af --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_update_curve_segment_transition_code.py @@ -0,0 +1,248 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def _test1(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + # 26=IFCCARTESIANPOINT((4084.115884,3889.462938)); + # 70=IFCDIRECTION((0.224530986099614,0.974466949814685)); + # 71=IFCAXIS2PLACEMENT2D(#26,#70); + # 72=IFCCARTESIANPOINT((0.,0.)); + # 73=IFCDIRECTION((1.,0.)); + # 74=IFCAXIS2PLACEMENT2D(#72,#73); + # 75=IFCCIRCLE(#74,1250.); + # 76=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#71,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(-1848.115835),#75); + circular_arc = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((4084.115884, 3889.462938)), + file.createIfcDirection((0.224530986099614, 0.974466949814685)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(-1848.115835), + ParentCurve=file.createIfcCircle( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), file.createIfcDirection((1.0, 0.0)) + ), + Radius=1250.0, + ), + ) + + # 27=IFCCARTESIANPOINT((5469.395067,4847.56631)); + # 77=IFCDIRECTION((0.991014275066766,-0.133756146078947)); + # 78=IFCAXIS2PLACEMENT2D(#27,#77); + # 79=IFCCARTESIANPOINT((0.,0.)); + # 80=IFCDIRECTION((1.,0.)); + # 81=IFCVECTOR(#80,1.); + # 82=IFCLINE(#79,#81); + # 83=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#78,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(1564.635765),#82); + line = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((5469.395067, 4847.56631)), + file.createIfcDirection((0.991014275066766, -0.133756146078947)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(1564.635765), + ParentCurve=file.createIfcLine( + Pnt=file.createIfcCartesianPoint((0.0, 0.0)), + Dir=file.createIfcVector(Orientation=file.createIfcDirection((1.0, 0.0)), Magnitude=1.0), + ), + ) + + composite_curve = file.createIfcCompositeCurve(Segments=(circular_arc, line), SelfIntersect=False) + + ifcopenshell.api.alignment.update_curve_segment_transition_code(circular_arc, line) + assert circular_arc.Transition == "CONTSAMEGRADIENT" + + +def _test2(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + # 30=IFCCARTESIANPOINT((0.,0.)); + # 31=IFCALIGNMENTHORIZONTALSEGMENT($,$,#30,0.523598775598299,0.,0.,27.8843513637174,$,.LINE.); + # 32=IFCALIGNMENTSEGMENT('3$jiMaOgfAoujgvRyMLw0X',$,'H1',$,$,#111,#113,#31); + # 33=IFCDIRECTION((0.866025403784439,0.5)); + # 34=IFCAXIS2PLACEMENT2D(#30,#33); + # 35=IFCCARTESIANPOINT((0.,0.)); + # 36=IFCDIRECTION((1.,0.)); + # 37=IFCVECTOR(#36,1.); + # 38=IFCLINE(#35,#37); + # 39=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#34,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(27.8843513637174),#38); + + line1 = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), + file.createIfcDirection((0.866025403784439, 0.5)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(27.8843513637174), + ParentCurve=file.createIfcLine( + Pnt=file.createIfcCartesianPoint((0.0, 0.0)), + Dir=file.createIfcVector(Orientation=file.createIfcDirection((1.0, 0.0)), Magnitude=1.0), + ), + ) + + # 40=IFCCARTESIANPOINT((24.1485566490305,13.9421756818587)); + # 41=IFCALIGNMENTHORIZONTALSEGMENT($,$,#40,0.523598775598299,0.,1524.,152.4,$,.CLOTHOID.); + # 42=IFCALIGNMENTSEGMENT('0Rd38fCkHF1Q11ppiqdMP6',$,'H2',$,$,#111,#115,#41); + # 43=IFCDIRECTION((0.866025403784439,0.5)); + # 44=IFCAXIS2PLACEMENT2D(#40,#43); + # 45=IFCCARTESIANPOINT((0.,0.)); + # 46=IFCDIRECTION((1.,0.)); + # 47=IFCAXIS2PLACEMENT2D(#45,#46); + # 48=IFCCLOTHOID(#47,481.931115409661); + # 49=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#44,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(152.4),#48); + + clothoid1 = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((24.1485566490305, 13.9421756818587)), + file.createIfcDirection((0.866025403784439, 0.5)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(152.4), + ParentCurve=file.createIfcClothoid( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0)) + ), + ClothoidConstant=481.931115409661, + ), + ) + + # 50=IFCCARTESIANPOINT((154.828063204281,92.32243963907)); + # 51=IFCALIGNMENTHORIZONTALSEGMENT($,$,#50,0.573598775598299,1524.,1524.,246.582267005904,$,.CIRCULARARC.); + # 52=IFCALIGNMENTSEGMENT('2OGYY2lQjCnRlzxKU9vtdu',$,'H3',$,$,#111,#117,#51); + # 53=IFCDIRECTION((0.839953512903025,0.542658360445933)); + # 54=IFCAXIS2PLACEMENT2D(#50,#53); + # 55=IFCCARTESIANPOINT((0.,0.)); + # 56=IFCDIRECTION((1.,0.)); + # 57=IFCAXIS2PLACEMENT2D(#55,#56); + # 58=IFCCIRCLE(#57,1524.); + # 59=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#54,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(246.582267005904),#58); + + circular_arc = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((154.828063204281, 92.32243963907)), + file.createIfcDirection((0.839953512903025, 0.542658360445933)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(246.582267005904), + ParentCurve=file.createIfcCircle( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), file.createIfcDirection((1.0, 0.0)) + ), + Radius=1524.0, + ), + ) + + # 60=IFCCARTESIANPOINT((350.24160971216,242.268527691248)); + # 61=IFCALIGNMENTHORIZONTALSEGMENT($,$,#60,0.735398163397447,1524.,0.,152.4,$,.CLOTHOID.); + # 62=IFCALIGNMENTSEGMENT('13CGRzUN9CAfNVKnf0Pxza',$,'H4',$,$,#111,#119,#61); + # 63=IFCDIRECTION((0.741563691346478,0.670882472327743)); + # 64=IFCAXIS2PLACEMENT2D(#60,#63); + # 65=IFCCARTESIANPOINT((0.,0.)); + # 66=IFCDIRECTION((1.,0.)); + # 67=IFCAXIS2PLACEMENT2D(#65,#66); + # 68=IFCCLOTHOID(#67,-481.931115409661); + # 69=IFCCURVESEGMENT(.CONTSAMEGRADIENT.,#64,IFCLENGTHMEASURE(-152.4),IFCLENGTHMEASURE(152.4),#68); + + clothoid2 = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((350.24160971216, 242.268527691248)), + file.createIfcDirection((0.741563691346478, 0.670882472327743)), + ), + SegmentStart=file.createIfcLengthMeasure(-152.4), + SegmentLength=file.createIfcLengthMeasure(152.4), + ParentCurve=file.createIfcClothoid( + Position=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((0.0, 0.0)), RefDirection=file.createIfcDirection((1.0, 0.0)) + ), + ClothoidConstant=-481.931115409661, + ), + ) + + # 70=IFCCARTESIANPOINT((459.773476040884,348.208932967387)); + # 71=IFCALIGNMENTHORIZONTALSEGMENT($,$,#70,0.785398163397448,0.,0.,0.,$,.LINE.); + # 72=IFCALIGNMENTSEGMENT('0FlcTrOfT5YBi0fqVhTMyc',$,'H5',$,$,#111,#121,#71); + # 73=IFCDIRECTION((0.707106781186548,0.707106781186548)); + # 74=IFCAXIS2PLACEMENT2D(#70,#73); + # 75=IFCCARTESIANPOINT((0.,0.)); + # 76=IFCDIRECTION((1.,0.)); + # 77=IFCVECTOR(#76,1.); + # 78=IFCLINE(#75,#77); + # 79=IFCCURVESEGMENT(.DISCONTINUOUS.,#74,IFCLENGTHMEASURE(0.),IFCLENGTHMEASURE(0.),#78); + + line2 = file.createIfcCurveSegment( + Placement=file.createIfcAxis2Placement2d( + file.createIfcCartesianPoint((459.773476040884, 348.208932967387)), + file.createIfcDirection((0.707106781186548, 0.707106781186548)), + ), + SegmentStart=file.createIfcLengthMeasure(0.0), + SegmentLength=file.createIfcLengthMeasure(0.0), + ParentCurve=file.createIfcLine( + Pnt=file.createIfcCartesianPoint((0.0, 0.0)), + Dir=file.createIfcVector(Orientation=file.createIfcDirection((1.0, 0.0)), Magnitude=1.0), + ), + ) + + composite_curve = file.createIfcCompositeCurve(Segments=[], SelfIntersect=False) + + # add_segment_to_curve calls update_curve_segment_transition_code + ifcopenshell.api.alignment.add_segment_to_curve(file, line1, composite_curve) + assert line1.Transition == "DISCONTINUOUS" + + ifcopenshell.api.alignment.add_segment_to_curve(file, clothoid1, composite_curve) + assert line1.Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert clothoid1.Transition == "DISCONTINUOUS" + + ifcopenshell.api.alignment.add_segment_to_curve(file, circular_arc, composite_curve) + assert clothoid1.Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert circular_arc.Transition == "DISCONTINUOUS" + + ifcopenshell.api.alignment.add_segment_to_curve(file, clothoid2, composite_curve) + assert circular_arc.Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert clothoid2.Transition == "DISCONTINUOUS" + + ifcopenshell.api.alignment.add_segment_to_curve(file, line2, composite_curve) + assert clothoid2.Transition == "CONTSAMEGRADIENTSAMECURVATURE" + assert line2.Transition == "DISCONTINUOUS" + + +def test_update_curve_segment_transition_code(): + _test1() + _test2() diff --git a/src/ifcparse/IfcAlignmentHelper.cpp b/src/ifcparse/IfcAlignmentHelper.cpp index a2953c16c2..b5936561a5 100644 --- a/src/ifcparse/IfcAlignmentHelper.cpp +++ b/src/ifcparse/IfcAlignmentHelper.cpp @@ -718,7 +718,7 @@ std::pair mapAlign // dy/dx = B + 2Cx auto dx = cos(atan(start_gradient)); auto dy = sin(atan(start_gradient)); - auto curve_length_fn = [B, C](double x) { return sqrt(1 + pow(B + C * x, 2)); }; + auto curve_length_fn = [B, C](double x) { return sqrt(1 + pow(B + 2*C * x, 2)); }; auto segment_curve_length = boost::math::quadrature::trapezoidal(curve_length_fn, 0.0, horizontal_length); auto curve_segment = new Ifc4x3_add2::IfcCurveSegment( From 7e42b10159385d6cda7bf4457bed0621babe8fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 14 Mar 2025 11:29:44 -0300 Subject: [PATCH 375/476] =?UTF-8?q?Snap=20-=20Add=20face=20normal=20inters?= =?UTF-8?q?ection=20to=20=C2=B4mix=5Fsnap=5Fand=5Faxis=C2=B4=20function.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bonsai/bonsai/tool/snap.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index e49d97ad5c..cf238be46b 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -243,13 +243,18 @@ class Snap(bonsai.core.tool.Snap): def mix_snap_and_axis(cls, snap_point, axis_start, axis_end): # Creates a mixed snap point between the locked axis and the object snap # Then it sorts them to get the shortest first - x_axis = tool.Polyline.use_transform_orientations(Vector((1, 0, 0))) - y_axis = tool.Polyline.use_transform_orientations(Vector((0, 1, 0))) - z_axis = tool.Polyline.use_transform_orientations(Vector((0, 0, 1))) intersections = [] - intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, x_axis)) - intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, y_axis)) - intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point, z_axis)) + if snap_point["type"] == "Face": + face_normal = snap_point["object"].rotation_euler.to_matrix() @ snap_point["object"].data.polygons[snap_point["face_index"]].normal + if face_normal.z == 0: + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point["point"], face_normal.normalized())) + if not intersections: + x_axis = tool.Polyline.use_transform_orientations(Vector((1, 0, 0))) + y_axis = tool.Polyline.use_transform_orientations(Vector((0, 1, 0))) + z_axis = tool.Polyline.use_transform_orientations(Vector((0, 0, 1))) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point["point"], x_axis)) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point["point"], y_axis)) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point["point"], z_axis)) polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline polyline_points = polyline_data[0].polyline_points if polyline_data else [] @@ -517,7 +522,7 @@ class Snap(bonsai.core.tool.Snap): if point["type"] == "Axis": if ordered_snaps[0]["type"] not in {"Axis", "Plane"}: obj = ordered_snaps[0]["object"] - mixed_snap = cls.mix_snap_and_axis(ordered_snaps[0]["point"], axis_start, axis_end) + mixed_snap = cls.mix_snap_and_axis(ordered_snaps[0], axis_start, axis_end) for mixed_point in mixed_snap: snap_point = { "point": mixed_point, From 615ca2482527e59f8d944fc2bbef3d93b6c851cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Fri, 14 Mar 2025 21:43:26 -0300 Subject: [PATCH 376/476] Fix #6363. Improve how Curves are handled by the snapping system. --- src/bonsai/bonsai/tool/raycast.py | 2 ++ src/bonsai/bonsai/tool/snap.py | 9 +-------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index dae97fcf95..628660077a 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -204,6 +204,8 @@ class Raycast(bonsai.core.tool.Raycast): } points.append(snap_point) return points + if obj and obj.type == "CURVE": + obj = bpy.data.objects.new("new_object", obj.to_mesh().copy()) if not custom_bmesh: bm = bmesh.new() diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index cf238be46b..4915f93bf3 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -358,20 +358,13 @@ class Snap(bonsai.core.tool.Snap): face_index = result[2] if hit is not None: # Wireframes - if snap_obj.type == "EMPTY" or (snap_obj.type == "MESH" and len(snap_obj.data.polygons) == 0): + if snap_obj.type in {"EMPTY", "CURVE"} or (snap_obj.type == "MESH" and len(snap_obj.data.polygons) == 0): snap_points = tool.Raycast.ray_cast_by_proximity(context, event, snap_obj) if snap_points: for point in snap_points: point["group"] = "Wireframe" detected_snaps.append(point) - elif snap_obj.type == "CURVE": - new_object = bpy.data.objects.new("new_object", obj.to_mesh().copy()) - snap_points = tool.Raycast.ray_cast_by_proximity(context, event, new_object) - if snap_points: - for point in snap_points: - point["group"] = "Wireframe" - detected_snaps.append(point) # Meshes else: # Add face snap From bef7ff33f7deef855d5601e15f378efe8e743791 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 15 Mar 2025 20:54:15 +1100 Subject: [PATCH 377/476] Prevent errors where decorator is still running and there is no camera --- src/bonsai/bonsai/bim/module/drawing/decoration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 962cd169f1..8144fdff09 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -1609,6 +1609,9 @@ class CutDecorator: cls.installed = None def __call__(self, context): + if not context.scene.camera: + return + self.addon_prefs = tool.Blender.get_addon_preferences() selected_elements_color = self.addon_prefs.decorator_color_selected From faef2df2a60b2c01149f41b0870f62fa5fb742ab Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 15 Mar 2025 20:56:56 +1100 Subject: [PATCH 378/476] See #1227. Basic uncached implementation of visualising wall layers in drawings --- .../bonsai/bim/module/drawing/decoration.py | 219 ++++++++++-------- 1 file changed, 126 insertions(+), 93 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 8144fdff09..9701d67270 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -1621,6 +1621,8 @@ class CutDecorator: selected_edges = [] layer_vertices = [] layer_edges = [] + fills = {} + self.layer_fills = {} all_vertex_i_offset = 0 selected_vertex_i_offset = 0 layer_vertex_i_offset = 0 @@ -1665,6 +1667,10 @@ class CutDecorator: self.draw_batch("LINES", layer_vertices, black, layer_edges) self.draw_batch("POINTS", layer_vertices, black) + for colour, fills in self.layer_fills.items(): + for fill in fills: + self.draw_batch("TRIS", fill[0], colour, fill[1]) + gpu.state.point_size_set(2) self.line_shader.uniform_float("lineWidth", 3.0) @@ -1737,104 +1743,102 @@ class CutDecorator: 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"]))] - max_edge = [ - Vector((minx, layers["offset"] + layers["thickness"])), - Vector((maxx, layers["offset"] + layers["thickness"])), - ] - centerline = [ - Vector((minx, layers["offset"] + layers["thickness"] / 2)), - Vector((maxx, layers["offset"] + layers["thickness"] / 2)), - ] - connections = self.get_connections(element, obj, centerline, min_edge, max_edge) + if not (material := ifcopenshell.util.element.get_material(element)): + return None, None + elif material.is_a("IfcMaterialLayerSetUsage"): + usage = material + layer_set = material.ForLayerSet + offset = usage.OffsetFromReferenceLine * self.unit_scale + sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 + elif material.is_a("IfcMaterialLayerSet"): + usage = None + layer_set = material + offset = 0 + sense_factor = 1 + else: + return None, None - start_point = None - end_point = None - if connections["ATSTART"]: - boundary_edge = min_edge if connections["ATSTART"]["angle"] > 0 else max_edge - rel_dir = connections["ATSTART"]["centerline"][1] - connections["ATSTART"]["centerline"][0] - offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[0], centerline[0] + rel_dir]) - offset_x = (offset - centerline[0]).length + abs(offset.x) - intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATSTART"]["centerline"]) - if tool.Cad.is_point_on_edge(intersect, boundary_edge): - centerline[0] = centerline[0] + Vector((offset_x, 0)) - start_point = centerline[0] + rel_dir - if connections["ATEND"]: - boundary_edge = min_edge if connections["ATEND"]["angle"] > 0 else max_edge - rel_dir = connections["ATEND"]["centerline"][1] - connections["ATEND"]["centerline"][0] - offset, _ = tool.Cad.intersect_edges(boundary_edge, [centerline[1], centerline[1] + rel_dir]) + if len(layer_set.MaterialLayers) == 1: + return None, None - offset_x = (offset - centerline[1]).length + abs((maxx - abs(offset.x))) - intersect, _ = tool.Cad.intersect_edges(boundary_edge, connections["ATEND"]["centerline"]) - if tool.Cad.is_point_on_edge(intersect, boundary_edge): - centerline[1] = centerline[1] - Vector((offset_x, 0)) - end_point = centerline[1] + rel_dir + mesh = obj.data + bm_original = bmesh.new() + bm_original.from_mesh(mesh) + bm = bm_original.copy() + prev_co = None + if not usage: + sense_factor = 1 # Assume the extrusion vector points in the direction sense + no = self.get_extrusion_vector(element).normalized() + co = Vector((0.0, 0.0, offset)) + elif usage.LayerSetDirection == "AXIS2": + co = Vector((0.0, offset, 0.0)) + no = self.get_extrusion_vector(element).normalized() + no = no.cross(Vector([1.0, 0.0, 0.0])) + elif usage.LayerSetDirection == "AXIS3": + co = Vector((0.0, 0.0, offset)) + no = self.get_extrusion_vector(element).normalized() + no = Vector([0.0, 0.0, 1.0]) + elif usage.LayerSetDirection == "AXIS1": + co = Vector((0.0, 0.0, offset)) + no = self.get_extrusion_vector(element).normalized() + no = Vector([1.0, 0.0, 0.0]) + no *= sense_factor + # Cache this + body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") + slice_plane_geom = [] + last_i = len(layer_set.MaterialLayers) - 1 + for i, layer in enumerate(layer_set.MaterialLayers): + prev_co = co.copy() + co += no * layer.LayerThickness * self.unit_scale + if i != last_i: + bisect_geom = bmesh.ops.bisect_plane( + bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no + ) + slice_plane_geom.extend(bisect_geom["geom_cut"]) + edges = [g for g in bisect_geom["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + fill = bmesh.ops.edgenet_fill(bm, edges=edges) + slice_plane_geom.extend(fill["faces"]) + if style := ifcopenshell.util.representation.get_material_style(layer.Material, body): + if not (styles := [s for s in style.Styles if s.is_a("IfcSurfaceStyleShading")]): + continue + colour = styles[0].SurfaceColour + colour = (colour.Red, colour.Green, colour.Blue, 1) + bm_fill = bm_original.copy() + if i != last_i: + geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] + bisect = bmesh.ops.bisect_plane( + bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True + ) + edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) + if i != 0: + geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] + bisect = bmesh.ops.bisect_plane( + bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True + ) + edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) - min_segments = self.get_segments(connections["MINPATH"], centerline, start_point, end_point) - max_segments = self.get_segments(connections["MAXPATH"], centerline, start_point, end_point) + geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] + verts, tris = self.bisect_mesh_tris(obj, bm_fill, geom, context.scene.camera) + self.layer_fills.setdefault(colour, []).append((verts, tris)) - final_segments = [] - for segment in min_segments: - segment_line = shapely.LineString([co for co in segment]) - for distance in layers["min_layers"]: - distance *= -1 - layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre) - final_segments.append(list(layer_line.coords)) - for segment in max_segments: - segment_line = shapely.LineString([co for co in segment]) - for distance in layers["max_layers"]: - layer_line = shapely.offset_curve(segment_line, distance, join_style=shapely.BufferJoinStyle.mitre) - final_segments.append(list(layer_line.coords)) - - # Extrude and bisect layers - bm = bmesh.new() - verts = [] - edges = [] - offset = 0 - for segment in final_segments: - if not segment: - # Why does this occur? - continue - if isinstance(segment[0], tuple): - segment = [Vector(co) for co in segment] - verts.extend([bm.verts.new(co.to_3d()) for co in segment]) - [bm.edges.new((verts[i + offset], verts[i + 1 + offset])) for i in range(0, len(segment) - 1)] - offset += len(segment) - - extrusion_dir = (0, 0, 3) - extruded_geom = bmesh.ops.extrude_edge_only(bm, edges=bm.edges[:]) - bmesh.ops.translate( - bm, vec=extrusion_dir, verts=[v for v in extruded_geom["geom"] if isinstance(v, bmesh.types.BMVert)] - ) - - verts, edges = self.bisect_mesh(obj, bm, context.scene.camera) - layer_linestrings = [shapely.LineString((verts[e[0]], verts[e[1]])) for e in edges] - - clipped_linestrings = [] - - polygons = shapely.polygonize([shapely.LineString((cut_verts[e[0]], cut_verts[e[1]])) for e in cut_edges]) - for polygon in polygons.geoms: - clipped_linestrings.extend([polygon.intersection(ls) for ls in layer_linestrings]) - - verts = [] - edges = [] - offset = 0 - for linestring in clipped_linestrings: - try: - linestring = list(linestring.coords) - except: - print("Failed ... ", linestring) - continue - verts.extend(linestring) - edges.extend([(i + offset, i + 1 + offset) for i in range(0, len(linestring) - 1)]) - offset += len(linestring) + verts, edges = self.bisect_mesh(obj, bm, slice_plane_geom, context.scene.camera) + bm_original.free() DecoratorData.layerset_cache[element.id()] = (verts, edges) return verts, edges - def bisect_mesh(self, obj, bm, camera): + def get_extrusion_vector(self, wall): + if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): + for item in ifcopenshell.util.representation.resolve_representation(body).Items: + while item.is_a("IfcBooleanResult"): + item = item.FirstOperand + if item.is_a("IfcExtrudedAreaSolid"): + return Vector(item.ExtrudedDirection.DirectionRatios) + return Vector([0.0, 0.0, 1.0]) + + def bisect_mesh(self, obj, bm, geom, camera): camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world plane_co = camera_matrix.translation plane_no = camera_matrix.col[2].xyz @@ -1842,7 +1846,6 @@ class CutDecorator: global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start # Run the bisect operation - geom = bm.verts[:] + bm.edges[:] + bm.faces[:] results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no) vert_map = {} @@ -1858,10 +1861,40 @@ class CutDecorator: # It seems as though edges always appear after verts edges.append([vert_map[v.index] for v in geom.verts]) - bm.free() - return verts, edges + def bisect_mesh_tris(self, obj, bm, geom, camera): + camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world + plane_co = camera_matrix.translation + plane_no = camera_matrix.col[2].xyz + + global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start + + bmesh.ops.bisect_plane( + bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no, clear_inner=True, clear_outer=True + ) + bmesh.ops.remove_doubles(bm, verts=bm.verts[:]) + fill = bmesh.ops.edgenet_fill(bm, edges=bm.edges[:]) + triangulate = bmesh.ops.triangulate(bm, faces=fill["faces"]) + + vert_map = {} + verts = [] + tris = [] + i = 0 + for face in triangulate["faces"]: + tri = [] + for vert in face.verts: + if index := vert_map.get(vert.index, None): + tri.append(index) + else: + verts.append(tuple((obj.matrix_world @ vert.co) + global_offset)) + vert_map[vert.index] = i + tri.append(i) + i += 1 + tris.append(tri) + + return verts, tris + def get_layer_data(self, element): usage = ifcopenshell.util.element.get_material(element) offset = usage.OffsetFromReferenceLine * self.unit_scale From 2270185f8c35d386b68b66f0f8934e7e2e58f4c2 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sat, 15 Mar 2025 22:56:01 +1100 Subject: [PATCH 379/476] See #1227. Drawing decorator now does material-aware fills for everything, and also basic caching. --- src/bonsai/bonsai/bim/module/drawing/data.py | 3 +- .../bonsai/bim/module/drawing/decoration.py | 331 ++++++------------ src/bonsai/bonsai/tool/loader.py | 2 +- 3 files changed, 107 insertions(+), 229 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/data.py b/src/bonsai/bonsai/bim/module/drawing/data.py index 579677cb98..b5103b7b8c 100644 --- a/src/bonsai/bonsai/bim/module/drawing/data.py +++ b/src/bonsai/bonsai/bim/module/drawing/data.py @@ -236,7 +236,8 @@ class DecoratorData: # stores 1 type of data per object data = {} cut_cache = {} - layerset_cache = {} + slice_cache = {} + fill_cache = {} @classmethod def get_batting_thickness(cls, obj): diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 9701d67270..dd734ed9e3 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -1614,38 +1614,45 @@ class CutDecorator: self.addon_prefs = tool.Blender.get_addon_preferences() selected_elements_color = self.addon_prefs.decorator_color_selected + self.fallback_colour = (0.3, 0.3, 0.3, 1) all_vertices = [] all_edges = [] selected_vertices = [] selected_edges = [] - layer_vertices = [] - layer_edges = [] fills = {} - self.layer_fills = {} all_vertex_i_offset = 0 selected_vertex_i_offset = 0 - layer_vertex_i_offset = 0 for obj in [o for o in bpy.context.visible_objects if o.type == "MESH"]: - verts, edges = self.decorate(context, obj) - if not verts: + if not (element := tool.Ifc.get_entity(obj)): continue + self.decorate(context, obj, element) - obj_layer_verts, obj_layer_edges = self.slice_layersets(context, obj, verts, edges) - if obj_layer_verts: - layer_vertices.extend(obj_layer_verts) - layer_edges.extend([[vi + layer_vertex_i_offset for vi in e] for e in obj_layer_edges]) - layer_vertex_i_offset += len(obj_layer_verts) + verts, edges = DecoratorData.cut_cache[element.id()] + if verts: + if obj.select_get(): + selected_vertices.extend(verts) + selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges]) + selected_vertex_i_offset += len(verts) + else: + all_vertices.extend(verts) + all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges]) + all_vertex_i_offset += len(verts) - if obj.select_get(): - selected_vertices.extend(verts) - selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges]) - selected_vertex_i_offset += len(verts) - else: - all_vertices.extend(verts) - all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges]) - all_vertex_i_offset += len(verts) + verts, edges = DecoratorData.slice_cache.get(element.id(), (None, None)) + if verts: + if obj.select_get(): + selected_vertices.extend(verts) + selected_edges.extend([[vi + selected_vertex_i_offset for vi in e] for e in edges]) + selected_vertex_i_offset += len(verts) + else: + all_vertices.extend(verts) + all_edges.extend([[vi + all_vertex_i_offset for vi in e] for e in edges]) + all_vertex_i_offset += len(verts) + + for colour, element_fills in DecoratorData.fill_cache[element.id()].items(): + fills.setdefault(colour, []).append(element_fills) gpu.state.point_size_set(1) gpu.state.blend_set("ALPHA") @@ -1663,13 +1670,10 @@ class CutDecorator: black = (0, 0, 0, 1) - if layer_vertices: - self.draw_batch("LINES", layer_vertices, black, layer_edges) - self.draw_batch("POINTS", layer_vertices, black) - - for colour, fills in self.layer_fills.items(): - for fill in fills: - self.draw_batch("TRIS", fill[0], colour, fill[1]) + for colour, element_fills in fills.items(): + for verts_tris in element_fills: + for verts, tris in verts_tris: + self.draw_batch("TRIS", verts, colour, tris) gpu.state.point_size_set(2) self.line_shader.uniform_float("lineWidth", 3.0) @@ -1687,65 +1691,56 @@ class CutDecorator: shader.uniform_float("color", color) batch.draw(shader) - def decorate(self, context, obj): - element = tool.Ifc.get_entity(obj) - if not element: - return None, None + def decorate(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None: + has_cut_cache = element.id() in DecoratorData.cut_cache + has_fill_cache = element.id() in DecoratorData.fill_cache - # 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.cut_cache.get(element.id(), (None, None)) + # Currently selected objects must be recalculated as they may be being moved / edited. + # If the camera is selected, we also recalculate as the user may be moving the camera. - if verts is False: - return None, None - elif verts: - return verts, edges + if not has_cut_cache or obj.select_get() or context.scene.camera.select_get(): + self.recalculate_cut(context, obj, element) + if not has_fill_cache or obj.select_get() or context.scene.camera.select_get(): + self.recalculate_fill(context, obj, element) + def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None: if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera): DecoratorData.cut_cache[element.id()] = (False, False) - return None, None - - if verts is None: + else: verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera) DecoratorData.cut_cache[element.id()] = (verts, edges) - return verts, edges - def slice_layersets(self, context, obj, cut_verts, cut_edges): - element = tool.Ifc.get_entity(obj) - - # 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 + def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None: + element_id = element.id() if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera): - DecoratorData.layerset_cache[element.id()] = (False, False) - return None, None + DecoratorData.fill_cache[element_id] = {} + return - if tool.Model.get_usage_type(element) != "LAYER2": - DecoratorData.layerset_cache[element.id()] = (False, False) - return None, None + DecoratorData.fill_cache[element_id] = {} - 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 + mesh = obj.data + bm_original = bmesh.new() + bm_original.from_mesh(mesh) if not (material := ifcopenshell.util.element.get_material(element)): - return None, None - elif material.is_a("IfcMaterialLayerSetUsage"): + geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:] + verts, tris = self.bisect_mesh_tris(obj, bm_original, geom, context.scene.camera) + DecoratorData.fill_cache[element_id].setdefault(self.fallback_colour, []).append((verts, tris)) + return + + if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"): + # Constituents, lists, and item styles not supported yet + material = ifcopenshell.util.element.get_materials(element)[0] + geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:] + verts, tris = self.bisect_mesh_tris(obj, bm_original, geom, context.scene.camera) + colour = self.get_material_colour(material) + DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) + return + + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + if material.is_a("IfcMaterialLayerSetUsage"): usage = material layer_set = material.ForLayerSet offset = usage.OffsetFromReferenceLine * self.unit_scale @@ -1755,15 +1750,15 @@ class CutDecorator: layer_set = material offset = 0 sense_factor = 1 - else: - return None, None if len(layer_set.MaterialLayers) == 1: - return None, None + material = layer_set.MaterialLayers[0].Material + geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:] + verts, tris = self.bisect_mesh_tris(obj, bm_original, geom, context.scene.camera) + colour = self.get_material_colour(material) + DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) + return - mesh = obj.data - bm_original = bmesh.new() - bm_original.from_mesh(mesh) bm = bm_original.copy() prev_co = None if not usage: @@ -1783,8 +1778,6 @@ class CutDecorator: no = self.get_extrusion_vector(element).normalized() no = Vector([1.0, 0.0, 0.0]) no *= sense_factor - # Cache this - body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") slice_plane_geom = [] last_i = len(layer_set.MaterialLayers) - 1 for i, layer in enumerate(layer_set.MaterialLayers): @@ -1798,36 +1791,44 @@ class CutDecorator: edges = [g for g in bisect_geom["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] fill = bmesh.ops.edgenet_fill(bm, edges=edges) slice_plane_geom.extend(fill["faces"]) - if style := ifcopenshell.util.representation.get_material_style(layer.Material, body): - if not (styles := [s for s in style.Styles if s.is_a("IfcSurfaceStyleShading")]): - continue - colour = styles[0].SurfaceColour - colour = (colour.Red, colour.Green, colour.Blue, 1) - bm_fill = bm_original.copy() - if i != last_i: - geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] - bisect = bmesh.ops.bisect_plane( - bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True - ) - edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] - fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) - if i != 0: - geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] - bisect = bmesh.ops.bisect_plane( - bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True - ) - edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] - fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) + colour = self.get_material_colour(layer.Material) + bm_fill = bm_original.copy() + if i != last_i: geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] - verts, tris = self.bisect_mesh_tris(obj, bm_fill, geom, context.scene.camera) - self.layer_fills.setdefault(colour, []).append((verts, tris)) + bisect = bmesh.ops.bisect_plane( + bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True + ) + edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) + print("test FILL1", fill) + if i != 0: + geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] + bisect = bmesh.ops.bisect_plane( + bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True + ) + edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) + print("test FILL2", fill) + + geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] + print("gonna bisect mesh tris", geom) + verts, tris = self.bisect_mesh_tris(obj, bm_fill, geom, context.scene.camera) + DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) verts, edges = self.bisect_mesh(obj, bm, slice_plane_geom, context.scene.camera) + DecoratorData.slice_cache[element.id()] = (verts, edges) bm_original.free() - DecoratorData.layerset_cache[element.id()] = (verts, edges) - return verts, edges + def get_material_colour(self, material: ifcopenshell.entity_instance) -> tuple[float, float, float, float]: + body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") + style = ifcopenshell.util.representation.get_material_style(material, body) + if not style: + return self.fallback_colour + if not (styles := [s for s in style.Styles if s.is_a("IfcSurfaceStyleShading")]): + return self.fallback_colour + colour = styles[0].SurfaceColour + return (colour.Red, colour.Green, colour.Blue, 1) def get_extrusion_vector(self, wall): if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): @@ -1895,130 +1896,6 @@ class CutDecorator: return verts, tris - def get_layer_data(self, element): - 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 = [] - max_layers = [] - current_thickness = 0 - for layer in layer_set.MaterialLayers: - current_thickness += layer.LayerThickness - if current_thickness / total_thickness < 0.5: - min_layers.append((half_thickness - current_thickness) * self.unit_scale) - else: - max_layers.append((current_thickness - half_thickness) * self.unit_scale) - min_layers.reverse() - max_layers.pop() - if usage.DirectionSense == "NEGATIVE": - total_thickness *= -1 - offset *= -1 - min_layers = [l * -1 for l in min_layers] - max_layers = [l * -1 for l in max_layers] - return { - "offset": offset, - "min_layers": min_layers, - "max_layers": max_layers, - "thickness": total_thickness * self.unit_scale, - } - - def get_segments(self, connections, centerline, start_point, end_point): - segments = [] - total_connections = len(connections) - if total_connections: - for i in range(0, total_connections + 1): - if i == 0: - connection = connections[i] - segments.append([centerline[0], connection["intersection"], connection["out_point"]]) - elif i == total_connections: - segments.append([segments[-1][-1], segments[-1][-2], centerline[1]]) - else: - connection = connections[i] - segments.append( - [segments[-1][-1], segments[-1][-2], connection["intersection"], connection["out_point"]] - ) - else: - segments.append([centerline[0], centerline[1]]) - if start_point: - segments[0].insert(0, start_point) - if end_point: - segments[-1].append(end_point) - return segments - - 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": - metadata = self.get_connection_metadata(obj, rel.RelatedElement, centerline, min_edge, max_edge) - if not metadata: - continue - connections["ATPATH"].append(metadata) - else: - 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": - 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: - connections["MINPATH"].append(connection) - else: - connections["MAXPATH"].append(connection) - return connections - - def get_connection_metadata(self, obj, rel_element, centerline, min_edge, max_edge): - 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 = [ - Vector((minx, layers["offset"] + layers["thickness"] / 2)), - Vector((maxx, layers["offset"] + layers["thickness"] / 2)), - ] - 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) - 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]] - angle = tool.Cad.angle_edges(centerline, rel_centerline, signed=True) - # A little extreme, but maybe it's OK? - out_point = tool.Cad.furthest_vector(intersection, tuple(rel_centerline)) - return { - "element": rel_element, - "obj": rel_obj, - "centerline": rel_centerline, - "intersection": intersection, - "angle": angle, - "out_point": out_point, - } - class DecorationsHandler: decorators_classes: list[Type[BaseDecorator]] = [ diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index b9df539566..9a0ad1da21 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1057,7 +1057,7 @@ class Loader(bonsai.core.tool.Loader): body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW") styles = {} has_layer_styles = False - for i, material in mesh.materials: + for i, material in enumerate(mesh.materials): if style := tool.Ifc.get_entity(material): styles[style] = i for layer in layer_set.MaterialLayers[:-1]: From b2e2f765ca461445d7056d2d81c35191d517e223 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Mar 2025 00:23:47 +1100 Subject: [PATCH 380/476] See #1227. Clip based on active camera for legibility of drawings. Needs polish. --- .../bonsai/bim/module/drawing/decoration.py | 3 -- .../bonsai/bim/module/drawing/operator.py | 1 + .../bonsai/bim/module/project/operator.py | 32 +++++++++++-------- src/bonsai/bonsai/tool/ifc.py | 13 ++++---- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index dd734ed9e3..f2346f61f1 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -1801,7 +1801,6 @@ class CutDecorator: ) edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) - print("test FILL1", fill) if i != 0: geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] bisect = bmesh.ops.bisect_plane( @@ -1809,10 +1808,8 @@ class CutDecorator: ) edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) - print("test FILL2", fill) geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] - print("gonna bisect mesh tris", geom) verts, tris = self.bisect_mesh_tris(obj, bm_fill, geom, context.scene.camera) DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 42e17cf063..7295a4b2b9 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -2020,6 +2020,7 @@ class ActivateDrawingBase: camera_props = camera.data.BIMCameraProperties if camera_props.update_representation(camera): bpy.ops.bim.update_representation(obj=camera.name, ifc_representation_class="") + bpy.ops.bim.refresh_clipping_planes("INVOKE_DEFAULT") return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 49b58cd184..956808980e 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2299,6 +2299,7 @@ class RefreshClippingPlanes(bpy.types.Operator): def __init__(self): self.total_planes = 0 + self.camera = None def invoke(self, context, event): context.window_manager.modal_handler_add(self) @@ -2311,16 +2312,22 @@ class RefreshClippingPlanes(bpy.types.Operator): self.clean_deleted_planes(context) for clipping_plane in props.clipping_planes: - if clipping_plane.obj and self.is_moved(clipping_plane.obj): + if clipping_plane.obj and tool.Ifc.is_moved(clipping_plane.obj, ifc_only=False): should_refresh = True break + if self.camera != context.scene.camera: + should_refresh = True + elif self.camera and tool.Ifc.is_moved(self.camera, ifc_only=False): + should_refresh = True + total_planes = len(props.clipping_planes) if should_refresh or total_planes != self.total_planes: self.refresh_clipping_planes(context) for clipping_plane in props.clipping_planes: if clipping_plane.obj: tool.Geometry.record_object_position(clipping_plane.obj) + self.camera = context.scene.camera self.total_planes = total_planes return {"PASS_THROUGH"} @@ -2340,18 +2347,6 @@ class RefreshClippingPlanes(bpy.types.Operator): else: break - def is_moved(self, obj: bpy.types.Object) -> bool: - props = tool.Blender.get_object_bim_props(obj) - if not props.location_checksum: - return True # Let's be conservative - loc_check = np.frombuffer(eval(props.location_checksum)) - rot_check = np.frombuffer(eval(props.rotation_checksum)) - loc_real = np.array(obj.matrix_world.translation).flatten() - rot_real = np.array(obj.matrix_world.to_3x3()).flatten() - if np.allclose(loc_check, loc_real, atol=1e-4) and np.allclose(rot_check, rot_real, atol=1e-2): - return False - return True - def refresh_clipping_planes(self, context): import bmesh from itertools import cycle @@ -2360,8 +2355,9 @@ class RefreshClippingPlanes(bpy.types.Operator): region = next(r for r in area.regions if r.type == "WINDOW") data = region.data + camera = context.scene.camera props = tool.Project.get_project_props() - if not len(props.clipping_planes): + if not len(props.clipping_planes) and not camera: data.use_clip_planes = False else: with bpy.context.temp_override(area=area, region=region): @@ -2390,6 +2386,14 @@ class RefreshClippingPlanes(bpy.types.Operator): clip_planes.append(clip_plane) bm.free() + if camera: + normal = camera.matrix_world.col[2].to_3d() + normal *= -1 + center = camera.matrix_world.translation + distance = -center.dot(normal) + clip_plane = (normal.x, normal.y, normal.z, distance) + clip_planes.append(clip_plane) + clip_planes = cycle(clip_planes) data.clip_planes = [tuple(next(clip_planes)) for i in range(0, 6)] data.update() diff --git a/src/bonsai/bonsai/tool/ifc.py b/src/bonsai/bonsai/tool/ifc.py index 0474fa52f5..8a2de3b742 100644 --- a/src/bonsai/bonsai/tool/ifc.py +++ b/src/bonsai/bonsai/tool/ifc.py @@ -83,12 +83,13 @@ class Ifc(bonsai.core.tool.Ifc): return (not ignore_scale and tool.Geometry.is_scaled(obj)) or obj in IfcStore.edited_objs @classmethod - def is_moved(cls, obj: bpy.types.Object) -> bool: - element = cls.get_entity(obj) - if not element and not tool.Geometry.is_representation_item(obj): - return False - if element and (element.is_a("IfcTypeProduct") or element.is_a("IfcProject")): - return False + def is_moved(cls, obj: bpy.types.Object, ifc_only: bool = True) -> bool: + if ifc_only: + element = cls.get_entity(obj) + if not element and not tool.Geometry.is_representation_item(obj): + return False + if element and (element.is_a("IfcTypeProduct") or element.is_a("IfcProject")): + return False oprops = tool.Blender.get_object_bim_props(obj) if not oprops.location_checksum: return True # Let's be conservative From 2941a4ad1a94cfc614d153ac72808ddb38cefe3f Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Mar 2025 10:56:18 +1100 Subject: [PATCH 381/476] See #1227. Recalculate normals and limited dissolve when layerset slicing and generating underside clipping geom. --- src/bonsai/bonsai/tool/loader.py | 33 ++++++++++++-------------------- src/bonsai/bonsai/tool/model.py | 9 ++++----- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 9a0ad1da21..0b8c4f321e 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -20,7 +20,6 @@ from __future__ import annotations import os import re import bpy -import math import bmesh import logging import ifcopenshell.geom @@ -36,6 +35,7 @@ import bonsai.bim.import_ifc import numpy as np import numpy.typing as npt from ifcopenshell.util.shape_builder import np_to_4d +from math import atan, radians from mathutils import Vector, Matrix from pathlib import Path from typing import Union, Any, Optional @@ -857,9 +857,9 @@ class Loader(bonsai.core.tool.Loader): camera.BIMCameraProperties.height = height if width > height: - fov = 2 * math.atan(width / (2 * abs_min_z)) + fov = 2 * atan(width / (2 * abs_min_z)) else: - fov = 2 * math.atan(height / (2 * abs_min_z)) + fov = 2 * atan(height / (2 * abs_min_z)) camera.angle = fov @@ -1060,13 +1060,15 @@ class Loader(bonsai.core.tool.Loader): for i, material in enumerate(mesh.materials): if style := tool.Ifc.get_entity(material): styles[style] = i - for layer in layer_set.MaterialLayers[:-1]: + last_i = len(layer_set.MaterialLayers) - 1 + for i, layer in enumerate(layer_set.MaterialLayers): prev_co = co.copy() co += no * layer.LayerThickness * cls.unit_scale - bisect_geom = bmesh.ops.bisect_plane( - bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no - ) - bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"]) + if i != last_i: + bisect_geom = bmesh.ops.bisect_plane( + bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no + ) + bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"]) if style := ifcopenshell.util.representation.get_material_style(layer.Material, body): if (material_index := styles.get(style, None)) is None: material_index = len(mesh.materials) @@ -1078,19 +1080,8 @@ class Loader(bonsai.core.tool.Loader): face.material_index = material_index has_layer_styles = True - # Last layer - layer = layer_set.MaterialLayers[-1] - if style := ifcopenshell.util.representation.get_material_style(layer.Material, body): - if (material_index := styles.get(style, None)) is None: - material_index = len(mesh.materials) - mesh.materials.append(tool.Ifc.get_object(style)) - for face in bisect_geom["geom"]: - if isinstance(face, bmesh.types.BMFace): - center = face.calc_center_median() - # if center.y > co.y: - if (center - co).dot(no) >= 0: - face.material_index = material_index - has_layer_styles = True + bmesh.ops.dissolve_limit(bm, angle_limit=radians(1), verts=bm.verts, edges=bm.edges, delimit={"MATERIAL"}) + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) bm.to_mesh(mesh) bm.free() diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 358456b05a..b31dfacfb9 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -38,15 +38,12 @@ import ifcopenshell.util.unit import bonsai.core.geometry import bonsai.core.tool import bonsai.tool as tool -import bonsai.core.geometry as geometry -from math import atan, cos, degrees, pi, inf +from math import atan, cos, degrees, pi, radians from mathutils import Matrix, Vector from copy import deepcopy from functools import partial from bonsai.bim import import_ifc -# TODO: This line is somehow keeping the world from falling apart with a circular import error. -from bonsai.bim.module.geometry.helper import Helper from bonsai.bim.module.model.data import AuthoringData, RailingData, RoofData, WindowData, DoorData from bonsai.bim.module.model.opening import FilledOpeningGenerator from ifcopenshell.util.shape_builder import ShapeBuilder @@ -2108,6 +2105,7 @@ class Model(bonsai.core.tool.Model): bm = bmesh.new() bm.from_mesh(obj.data) + bmesh.ops.dissolve_limit(bm, angle_limit=radians(1), verts=bm.verts, edges=bm.edges) bm.faces.ensure_lookup_table() clipping_bm = bmesh.new() @@ -2117,7 +2115,7 @@ class Model(bonsai.core.tool.Model): face.normal_update() normal = face.normal.to_4d() normal.w = 0 - if (obj.matrix_world @ normal).z >= 0: + if (obj.matrix_world @ normal).z >= -0.5: continue new_verts = [] for vert in face.verts: @@ -2130,6 +2128,7 @@ class Model(bonsai.core.tool.Model): if not len(clipping_bm.faces): return + bmesh.ops.recalc_face_normals(clipping_bm, faces=clipping_bm.faces) return clipping_bm # clipping_bm is in project units @classmethod From 67b0ab6685f771ab9111219f0000fb3e5c5ed2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Sat, 15 Mar 2025 20:52:49 -0300 Subject: [PATCH 382/476] Fix #6366. Improve extrusion x_angle and direction handling. This solution brings new questions to #5938. Currently, we lack a reliable way to calculate the existing x_angle only based solely on the extrusion direction. For example, a 30 degree angled extrusion with positive direction has the same extrusion direction as a -150 degree angled extrusion with negative direction. The difference lies in the object's rotation. This means that things can get messy if the user changes the object x angle somehow. We may need to explore alternative approaches. --- src/bonsai/bonsai/bim/module/model/slab.py | 14 +++++++++----- src/bonsai/bonsai/bim/module/model/wall.py | 13 +++++++++---- src/bonsai/bonsai/tool/collector.py | 3 +++ src/bonsai/bonsai/tool/geometry.py | 4 ++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 8d12a15049..ad553bbfc7 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -297,14 +297,16 @@ class DumbSlabPlaner: if representation: extrusion = tool.Model.get_extrusion(representation) if extrusion: - existing_x_angle = tool.Model.get_existing_x_angle(extrusion) + # TODO Right now we don't have a reliable way to calculate the existing x_angle only based solely on the extrusion direction. + # For instances, a 30 degrees angled extrusion with positive direction has the same extrusion direction as a + # -150 degrees angled extrusion with negative direction. The difference lies in the object's rotation. + # This means that things can get messy if the user changes the object x angle somehow. We have to figure out an alternative approach. + existing_x_angle = obj.rotation_euler.x existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 2 * pi, tolerance=0.001) else existing_x_angle direction_ratios = Vector(extrusion.ExtrudedDirection.DirectionRatios) - offset_direction = Vector( - (abs(direction_ratios.x), abs(direction_ratios.y), abs(direction_ratios.z)) - ) # The offset direction doesn't change with direction sense + offset_direction = direction_ratios.copy() perpendicular_depth = thickness * abs(1 / cos(existing_x_angle)) perpendicular_offset = layer_params["offset"] * abs(1 / cos(existing_x_angle)) / self.unit_scale @@ -320,7 +322,9 @@ class DumbSlabPlaner: abs(existing_x_angle) < (pi / 2) and direction_ratios.z < 0 ): # The extrusion direction is negative. If the layer_parameter is set to positive, - # then the we change the extrusion direction. + # then the we change the extrusion direction. And the offset direction should remain positive + # for either direction sense, so we change it. + offset_direction *= -1 if layer_params["direction_sense"] == "POSITIVE": direction_ratios *= -1 diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 6c74184083..ed3e32702e 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -307,6 +307,9 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): extrusion.Depth = perpendicular_depth else: if tool.Model.get_usage_type(element) == "LAYER3": + existing_x_angle = obj.rotation_euler.x + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, 0, tolerance=0.001) else existing_x_angle + existing_x_angle = 0 if tool.Cad.is_x(existing_x_angle, pi, tolerance=0.001) else existing_x_angle # Reset the transformation and returns to the original points with 0 degrees extrusion.SweptArea.OuterCurve.Points.CoordList = [ (p[0], p[1] * abs(cos(existing_x_angle))) @@ -325,9 +328,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): layer_params = tool.Model.get_material_layer_parameters(element) perpendicular_depth = layer_params["thickness"] * abs(1 / cos(x_angle)) / unit_scale perpendicular_offset = layer_params["offset"] * abs(1 / cos(x_angle)) / unit_scale - offset_direction = Vector( - (abs(direction_ratios.x), abs(direction_ratios.y), abs(direction_ratios.z)) - ) # The offset direction doesn't change with direction sense + offset_direction = direction_ratios.copy() # Check angle and z direction to determine whether the extrusion direction is positive or negative if (abs(x_angle) < (pi / 2) and direction_ratios.z > 0) or ( @@ -342,6 +343,9 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): ): # The extrusion direction is negative. If the layer_parameter is set to positive, # then the we change the extrusion direction. + # then the we change the extrusion direction. And the offset direction should remain positive + # for either direction sense, so we change it. + offset_direction *= -1 if layer_params["direction_sense"] == "POSITIVE": direction_ratios *= -1 @@ -363,9 +367,10 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): ) # Object rotation + current_z_rot = obj.rotation_euler.z rot_mat = mathutils.Matrix.Rotation(x_angle, 4, "X") - rot_mat = obj.matrix_world @ rot_mat obj.rotation_euler = rot_mat.to_euler() + obj.rotation_euler.z = current_z_rot if layer2_objs: DumbWallRecalculator().recalculate(layer2_objs) diff --git a/src/bonsai/bonsai/tool/collector.py b/src/bonsai/bonsai/tool/collector.py index a93b5e405f..55bcd75f05 100644 --- a/src/bonsai/bonsai/tool/collector.py +++ b/src/bonsai/bonsai/tool/collector.py @@ -44,6 +44,9 @@ class Collector(bonsai.core.tool.Collector): # Note that tool.Geometry.is_locked is only checked within the if # statements for efficiency as it is a slow check. tool.Geometry.lock_scale(obj) + if element.is_a("IfcSlab"): + tool.Geometry.lock_rotation(obj, x=True) + if element.is_a("IfcGridAxis"): if tool.Geometry.is_locked(element): diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 81cffcece3..cca73f3fb4 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -177,6 +177,10 @@ class Geometry(bonsai.core.tool.Geometry): def unlock_scale(cls, obj: bpy.types.Object) -> None: obj.lock_scale = (False, False, False) + @classmethod + def lock_rotation(cls, obj: bpy.types.Object, x: bool=False, y: bool=False, z: bool=False,) -> None: + obj.lock_rotation = (x, y, z) + @classmethod def unlock_scale_object_with_openings(cls, obj: bpy.types.Object) -> None: element = tool.Ifc.get_entity(obj) From 7262a501842fc05d6a83a8d19ebe3f5bb30b5678 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Mar 2025 11:40:25 +1100 Subject: [PATCH 383/476] See #1227. Merging doubles gives more reliable results in bisection fills. --- .../bonsai/bim/module/drawing/decoration.py | 19 +++++++---------- src/bonsai/bonsai/tool/loader.py | 21 +++++++++++++------ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index f2346f61f1..cd778099c2 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -1724,16 +1724,14 @@ class CutDecorator: bm_original.from_mesh(mesh) if not (material := ifcopenshell.util.element.get_material(element)): - geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:] - verts, tris = self.bisect_mesh_tris(obj, bm_original, geom, context.scene.camera) + verts, tris = self.bisect_mesh_tris(obj, bm_original, context.scene.camera) DecoratorData.fill_cache[element_id].setdefault(self.fallback_colour, []).append((verts, tris)) return if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"): # Constituents, lists, and item styles not supported yet material = ifcopenshell.util.element.get_materials(element)[0] - geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:] - verts, tris = self.bisect_mesh_tris(obj, bm_original, geom, context.scene.camera) + verts, tris = self.bisect_mesh_tris(obj, bm_original, context.scene.camera) colour = self.get_material_colour(material) DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) return @@ -1753,8 +1751,7 @@ class CutDecorator: if len(layer_set.MaterialLayers) == 1: material = layer_set.MaterialLayers[0].Material - geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:] - verts, tris = self.bisect_mesh_tris(obj, bm_original, geom, context.scene.camera) + verts, tris = self.bisect_mesh_tris(obj, bm_original, context.scene.camera) colour = self.get_material_colour(material) DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) return @@ -1809,8 +1806,7 @@ class CutDecorator: edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) - geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] - verts, tris = self.bisect_mesh_tris(obj, bm_fill, geom, context.scene.camera) + verts, tris = self.bisect_mesh_tris(obj, bm_fill, context.scene.camera) DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) verts, edges = self.bisect_mesh(obj, bm, slice_plane_geom, context.scene.camera) @@ -1861,18 +1857,19 @@ class CutDecorator: return verts, edges - def bisect_mesh_tris(self, obj, bm, geom, camera): + def bisect_mesh_tris(self, obj, bm, camera): camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world plane_co = camera_matrix.translation plane_no = camera_matrix.col[2].xyz global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start + geom = bm.verts[:] + bm.edges[:] + bm.faces[:] bmesh.ops.bisect_plane( bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no, clear_inner=True, clear_outer=True ) - bmesh.ops.remove_doubles(bm, verts=bm.verts[:]) - fill = bmesh.ops.edgenet_fill(bm, edges=bm.edges[:]) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-6) + fill = bmesh.ops.edgenet_fill(bm, edges=bm.edges) triangulate = bmesh.ops.triangulate(bm, faces=fill["faces"]) vert_map = {} diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 0b8c4f321e..91cfbc4629 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -1062,17 +1062,26 @@ class Loader(bonsai.core.tool.Loader): styles[style] = i last_i = len(layer_set.MaterialLayers) - 1 for i, layer in enumerate(layer_set.MaterialLayers): - prev_co = co.copy() - co += no * layer.LayerThickness * cls.unit_scale if i != last_i: + prev_co = co.copy() + co += no * layer.LayerThickness * cls.unit_scale bisect_geom = bmesh.ops.bisect_plane( bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no ) bmesh.ops.duplicate(bm, geom=bisect_geom["geom_cut"]) - if style := ifcopenshell.util.representation.get_material_style(layer.Material, body): - if (material_index := styles.get(style, None)) is None: - material_index = len(mesh.materials) - mesh.materials.append(tool.Ifc.get_object(style)) + if not (style := ifcopenshell.util.representation.get_material_style(layer.Material, body)): + continue + if (material_index := styles.get(style, None)) is None: + material_index = len(mesh.materials) + mesh.materials.append(tool.Ifc.get_object(style)) + if i == last_i: + for face in bisect_geom["geom"]: + if isinstance(face, bmesh.types.BMFace): + center = face.calc_center_median() + if (center - co).dot(no) >= 0: + face.material_index = material_index + has_layer_styles = True + else: for face in bisect_geom["geom"]: if isinstance(face, bmesh.types.BMFace): center = face.calc_center_median() From ed48afc63355d03d14603dc3c9e113d129a9ae7e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Mar 2025 11:53:35 +1100 Subject: [PATCH 384/476] Only clip on visible drawing cameras. --- .../bonsai/bim/module/project/operator.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 956808980e..5e9b392720 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2316,18 +2316,23 @@ class RefreshClippingPlanes(bpy.types.Operator): should_refresh = True break - if self.camera != context.scene.camera: + if context.scene.camera.visible_get() and tool.Ifc.get_entity(context.scene.camera): + camera = context.scene.camera + else: + camera = None + + if self.camera != camera: should_refresh = True elif self.camera and tool.Ifc.is_moved(self.camera, ifc_only=False): should_refresh = True total_planes = len(props.clipping_planes) if should_refresh or total_planes != self.total_planes: + self.camera = camera self.refresh_clipping_planes(context) for clipping_plane in props.clipping_planes: if clipping_plane.obj: tool.Geometry.record_object_position(clipping_plane.obj) - self.camera = context.scene.camera self.total_planes = total_planes return {"PASS_THROUGH"} @@ -2355,9 +2360,8 @@ class RefreshClippingPlanes(bpy.types.Operator): region = next(r for r in area.regions if r.type == "WINDOW") data = region.data - camera = context.scene.camera props = tool.Project.get_project_props() - if not len(props.clipping_planes) and not camera: + if not len(props.clipping_planes) and not self.camera: data.use_clip_planes = False else: with bpy.context.temp_override(area=area, region=region): @@ -2386,10 +2390,10 @@ class RefreshClippingPlanes(bpy.types.Operator): clip_planes.append(clip_plane) bm.free() - if camera: - normal = camera.matrix_world.col[2].to_3d() + if self.camera: + normal = self.camera.matrix_world.col[2].to_3d() normal *= -1 - center = camera.matrix_world.translation + center = self.camera.matrix_world.translation distance = -center.dot(normal) clip_plane = (normal.x, normal.y, normal.z, distance) clip_planes.append(clip_plane) From ba3775406850b28f611950dca5929a5571b428e9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Mar 2025 14:17:37 +1100 Subject: [PATCH 385/476] Fix #6310. See #1227. Fix regression with layer ordering. --- src/bonsai/bonsai/bim/module/material/data.py | 14 ++++++++-- .../bonsai/bim/module/material/operator.py | 15 ++++------- src/bonsai/bonsai/bim/module/material/ui.py | 26 +++++++++---------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index 6c426dd652..daaba6ed07 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -244,8 +244,6 @@ class ObjectMaterialData: items = [] if cls.material.is_a("IfcMaterialLayerSetUsage"): items = cls.material.ForLayerSet.MaterialLayers - if cls.material.DirectionSense == "POSITIVE": - items = reversed(items) elif cls.material.is_a("IfcMaterialProfileSetUsage"): items = cls.material.ForProfileSet.MaterialProfiles elif cls.material.is_a("IfcMaterialLayerSet"): @@ -300,6 +298,18 @@ class ObjectMaterialData: else: data["material"] = item.Material.Name or "Unnamed" results.append(data) + should_reverse = cls.material.DirectionSense == "POSITIVE" + last_i = len(results) - 1 + for i, result in enumerate(results): + result["index"] = i + if should_reverse: + result["index_up"] = i + 1 if i != last_i else None + result["index_down"] = i - 1 if i != 0 else None + else: + result["index_down"] = i + 1 if i != last_i else None + result["index_up"] = i - 1 if i != 0 else None + if should_reverse: + return list(reversed(results)) return results @classmethod diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py index c6abbde92d..5562f4a3e3 100644 --- a/src/bonsai/bonsai/bim/module/material/operator.py +++ b/src/bonsai/bonsai/bim/module/material/operator.py @@ -382,7 +382,7 @@ class AddLayer(bpy.types.Operator, tool.Ifc.Operator): class ReorderMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.reorder_material_set_item" bl_label = "Reorder Material Set Item" - bl_description = "The List is Ordered From Origin Point" + bl_description = "Change the order of materials" bl_options = {"REGISTER", "UNDO"} obj: bpy.props.StringProperty() old_index: bpy.props.IntProperty() @@ -390,17 +390,12 @@ class ReorderMaterialSetItem(bpy.types.Operator, tool.Ifc.Operator): material_set: bpy.props.IntProperty() def _execute(self, context): - obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object self.file = tool.Ifc.get() - material_set = self.file.by_id(self.material_set) - ifcopenshell.api.run( - "material.reorder_set_item", + ifcopenshell.api.material.reorder_set_item( self.file, - **{ - "material_set": material_set, - "old_index": self.old_index, - "new_index": self.new_index, - }, + material_set=self.file.by_id(self.material_set), + old_index=self.old_index, + new_index=self.new_index, ) diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index f2eb2f155c..6ca1d659ed 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -253,7 +253,7 @@ class BIM_PT_object_material(Panel): active_object = bpy.context.active_object self.layerset_bounds(box, active_object, location="Top_Exterior") - for index, set_item in enumerate(ObjectMaterialData.data["set_items"]): + for set_item in ObjectMaterialData.data["set_items"]: if ( len(self.props.material_set_item_profile_attributes) and self.props.active_material_set_item_id == set_item["id"] @@ -262,9 +262,7 @@ class BIM_PT_object_material(Panel): elif self.props.active_material_set_item_id == set_item["id"]: self.draw_editable_set_item_ui(box, set_item) else: - self.draw_read_only_set_item_ui( - box, set_item, index, is_first=index == 0, is_last=index == total_items - 1 - ) + self.draw_read_only_set_item_ui(box, set_item) self.layerset_bounds(box, active_object, location="Bottom_Interior") @@ -291,7 +289,7 @@ class BIM_PT_object_material(Panel): row = box.row() prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="Profile") - def draw_read_only_set_item_ui(self, box, set_item, index, is_first=False, is_last=False): + def draw_read_only_set_item_ui(self, box, set_item): if ObjectMaterialData.data["material_class"] == "IfcMaterialList": row = box.row(align=True) row.label(text="IfcMaterial", icon="LAYER_ACTIVE") @@ -301,15 +299,15 @@ class BIM_PT_object_material(Panel): row.label(text=set_item["name"], icon=set_item["icon"]) row.label(text=set_item["material"], icon="MATERIAL") - if not is_first: - op = row.operator(f"bim.reorder_material_set_item", icon="TRIA_UP", text="") - op.old_index = index - op.new_index = index - 1 + if set_item["index_up"] is not None: + op = row.operator("bim.reorder_material_set_item", icon="TRIA_UP", text="") + op.old_index = set_item["index"] + op.new_index = set_item["index_up"] setattr(op, "material_set", ObjectMaterialData.data["set"]["id"]) - if not is_last: - op = row.operator(f"bim.reorder_material_set_item", icon="TRIA_DOWN", text="") - op.old_index = index - op.new_index = index + 1 + if set_item["index_down"] is not None: + op = row.operator("bim.reorder_material_set_item", icon="TRIA_DOWN", text="") + op.old_index = set_item["index"] + op.new_index = set_item["index_down"] setattr(op, "material_set", ObjectMaterialData.data["set"]["id"]) if ( not self.props.active_material_set_item_id @@ -329,7 +327,7 @@ class BIM_PT_object_material(Panel): setattr(op, "list_item_set", ObjectMaterialData.data["set"]["id"]) setattr(op, ObjectMaterialData.data["set_item_name"], set_item["id"]) if hasattr(op, f"{ObjectMaterialData.data['set_item_name']}_index"): - setattr(op, f"{ObjectMaterialData.data['set_item_name']}_index", index) + setattr(op, f"{ObjectMaterialData.data['set_item_name']}_index", set_item["index"]) def draw_read_only_set_ui(self): if ObjectMaterialData.data["material_class"] != "IfcMaterialList": From 8845440eddc12062aaafa58d2a82c886b68ccbbb Mon Sep 17 00:00:00 2001 From: Sayanjyoti Das Date: Sat, 15 Mar 2025 01:30:40 +0530 Subject: [PATCH 386/476] fix #6360 --- src/bonsai/bonsai/bim/operator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 3e4f651daa..4397e959cd 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -707,13 +707,14 @@ class BIM_OT_remove_section_plane(bpy.types.Operator): ( n for n in section_override.nodes - if isinstance(n, bpy.types.ShaderNodeTexCoord) and n.object.name == name + if isinstance(n, bpy.types.ShaderNodeTexCoord) and n.object and n.object.name == name ), None, ) if tex_coords is not None: section_compare = tex_coords.outputs["Object"].links[0].to_node - if section_compare.inputs[0].links: + + if section_compare.inputs[0].links and section_compare.outputs[0].links: previous_section_compare = section_compare.inputs[0].links[0].from_node next_section_compare = section_compare.outputs[0].links[0].to_node section_override.links.new(previous_section_compare.outputs[0], next_section_compare.inputs[0]) From a21939dc0da7d5a03a30624c14b6c4e3b614b00a Mon Sep 17 00:00:00 2001 From: Sayanjyoti Das Date: Fri, 14 Mar 2025 18:24:00 +0530 Subject: [PATCH 387/476] fix error --- src/bonsai/bonsai/bim/operator.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 4397e959cd..77b92f2ce6 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -676,8 +676,17 @@ class BIM_OT_add_section_plane(bpy.types.Operator): material.use_nodes = True if material.node_tree.nodes.get("Section Override"): continue - material.blend_method = "HASHED" - material.shadow_method = "HASHED" + # In EEVEE rendering engine, `blend_mode` is deprecated and replaced by `surface_render_method` + if (hasattr(material, "surface_render_method")): + material.surface_render_method = "DITHERED" + else: + material.blend_method = "HASHED" + + # https://developer.blender.org/docs/release_notes/4.2/eevee_migration/#materials + # TODO: Find an alternative for EEVEE engine + if (hasattr(material, "shadow_method")): + material.shadow_method = "HASHED" + material_output = tool.Blender.get_material_node(material, "OUTPUT_MATERIAL", {"is_active_output": True}) if not material_output: continue From d50e8060ac70284822fed4229d14a42c78ea99d8 Mon Sep 17 00:00:00 2001 From: Sayanjyoti Das Date: Fri, 14 Mar 2025 18:37:12 +0530 Subject: [PATCH 388/476] better comments --- src/bonsai/bonsai/bim/operator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 77b92f2ce6..bcb31264c5 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -677,13 +677,13 @@ class BIM_OT_add_section_plane(bpy.types.Operator): if material.node_tree.nodes.get("Section Override"): continue # In EEVEE rendering engine, `blend_mode` is deprecated and replaced by `surface_render_method` + # https://developer.blender.org/docs/release_notes/4.2/eevee_migration/#materials if (hasattr(material, "surface_render_method")): material.surface_render_method = "DITHERED" else: material.blend_method = "HASHED" - # https://developer.blender.org/docs/release_notes/4.2/eevee_migration/#materials - # TODO: Find an alternative for EEVEE engine + # TODO: Find an alternative to `shadow_method` for EEVEE engine if (hasattr(material, "shadow_method")): material.shadow_method = "HASHED" From bfc4490c97516643bee51d52ab404f67fadf4953 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 16 Mar 2025 21:43:59 +1100 Subject: [PATCH 389/476] See #1227. Implement wall layers in SVG drawings. --- .../bonsai/bim/module/drawing/decoration.py | 48 +----- .../bonsai/bim/module/drawing/operator.py | 153 +++++++++++++++++- src/bonsai/bonsai/tool/drawing.py | 39 +++++ 3 files changed, 192 insertions(+), 48 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index cd778099c2..1aa6345793 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -1760,19 +1760,19 @@ class CutDecorator: prev_co = None if not usage: sense_factor = 1 # Assume the extrusion vector points in the direction sense - no = self.get_extrusion_vector(element).normalized() + no = tool.Drawing.get_extrusion_vector(element).normalized() co = Vector((0.0, 0.0, offset)) elif usage.LayerSetDirection == "AXIS2": co = Vector((0.0, offset, 0.0)) - no = self.get_extrusion_vector(element).normalized() + no = tool.Drawing.get_extrusion_vector(element).normalized() no = no.cross(Vector([1.0, 0.0, 0.0])) elif usage.LayerSetDirection == "AXIS3": co = Vector((0.0, 0.0, offset)) - no = self.get_extrusion_vector(element).normalized() + no = tool.Drawing.get_extrusion_vector(element).normalized() no = Vector([0.0, 0.0, 1.0]) elif usage.LayerSetDirection == "AXIS1": co = Vector((0.0, 0.0, offset)) - no = self.get_extrusion_vector(element).normalized() + no = tool.Drawing.get_extrusion_vector(element).normalized() no = Vector([1.0, 0.0, 0.0]) no *= sense_factor slice_plane_geom = [] @@ -1797,19 +1797,19 @@ class CutDecorator: bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True ) edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] - fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) + bmesh.ops.edgenet_fill(bm_fill, edges=edges) if i != 0: geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] bisect = bmesh.ops.bisect_plane( bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True ) edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] - fill = bmesh.ops.edgenet_fill(bm_fill, edges=edges) + bmesh.ops.edgenet_fill(bm_fill, edges=edges) verts, tris = self.bisect_mesh_tris(obj, bm_fill, context.scene.camera) DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) - verts, edges = self.bisect_mesh(obj, bm, slice_plane_geom, context.scene.camera) + verts, edges = tool.Drawing.bisect_bmesh(obj, bm, slice_plane_geom, context.scene.camera) DecoratorData.slice_cache[element.id()] = (verts, edges) bm_original.free() @@ -1823,40 +1823,6 @@ class CutDecorator: colour = styles[0].SurfaceColour return (colour.Red, colour.Green, colour.Blue, 1) - def get_extrusion_vector(self, wall): - if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): - for item in ifcopenshell.util.representation.resolve_representation(body).Items: - while item.is_a("IfcBooleanResult"): - item = item.FirstOperand - if item.is_a("IfcExtrudedAreaSolid"): - return Vector(item.ExtrudedDirection.DirectionRatios) - return Vector([0.0, 0.0, 1.0]) - - def bisect_mesh(self, obj, bm, geom, camera): - camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world - plane_co = camera_matrix.translation - plane_no = camera_matrix.col[2].xyz - - global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start - - # Run the bisect operation - results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no) - - vert_map = {} - verts = [] - edges = [] - i = 0 - for geom in results["geom_cut"]: - if isinstance(geom, bmesh.types.BMVert): - verts.append(tuple((obj.matrix_world @ geom.co) + global_offset)) - vert_map[geom.index] = i - i += 1 - else: - # It seems as though edges always appear after verts - edges.append([vert_map[v.index] for v in geom.verts]) - - return verts, edges - def bisect_mesh_tris(self, obj, bm, camera): camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world plane_co = camera_matrix.translation diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 7295a4b2b9..754b262ba8 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -562,6 +562,105 @@ class CreateDrawing(bpy.types.Operator): path.attrib["d"] = d group.append(g) + def generate_wall_layers(self, context: bpy.types.Context, root): + for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"): + if "projection" in el.get("class", "").split(): + continue + element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid")) + if not (obj := tool.Ifc.get_object(element)): + continue + if not (material := ifcopenshell.util.element.get_material(element)): + continue + if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"): + continue + + self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + + if material.is_a("IfcMaterialLayerSetUsage"): + usage = material + layer_set = material.ForLayerSet + offset = usage.OffsetFromReferenceLine * self.unit_scale + sense_factor = 1 if usage.DirectionSense == "POSITIVE" else -1 + elif material.is_a("IfcMaterialLayerSet"): + usage = None + layer_set = material + offset = 0 + sense_factor = 1 + + camera_matrix_i = context.scene.camera.matrix_world.inverted() + + group = root.find("{http://www.w3.org/2000/svg}g") + raw_width, raw_height = self.get_camera_dimensions() + x_offset = raw_width / 2 + y_offset = raw_height / 2 + svg_scale = self.scale * 1000 # IFC is in meters, SVG is in mm + + mesh = obj.data + bm = bmesh.new() + bm.from_mesh(mesh) + + prev_co = None + if not usage: + sense_factor = 1 # Assume the extrusion vector points in the direction sense + no = tool.Drawing.get_extrusion_vector(element).normalized() + co = Vector((0.0, 0.0, offset)) + elif usage.LayerSetDirection == "AXIS2": + co = Vector((0.0, offset, 0.0)) + no = tool.Drawing.get_extrusion_vector(element).normalized() + no = no.cross(Vector([1.0, 0.0, 0.0])) + elif usage.LayerSetDirection == "AXIS3": + co = Vector((0.0, 0.0, offset)) + no = tool.Drawing.get_extrusion_vector(element).normalized() + no = Vector([0.0, 0.0, 1.0]) + elif usage.LayerSetDirection == "AXIS1": + co = Vector((0.0, 0.0, offset)) + no = tool.Drawing.get_extrusion_vector(element).normalized() + no = Vector([1.0, 0.0, 0.0]) + no *= sense_factor + last_i = len(layer_set.MaterialLayers) - 1 + for i, layer in enumerate(layer_set.MaterialLayers): + prev_co = co.copy() + co += no * layer.LayerThickness * self.unit_scale + + bm_fill = bm.copy() + if i != last_i: + geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] + bisect = bmesh.ops.bisect_plane( + bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True + ) + edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + bmesh.ops.edgenet_fill(bm_fill, edges=edges) + if i != 0: + geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] + bisect = bmesh.ops.bisect_plane( + bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True + ) + edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + bmesh.ops.edgenet_fill(bm_fill, edges=edges) + + geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] + verts, edges = tool.Drawing.bisect_bmesh(obj, bm_fill, geom, context.scene.camera) + + g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g") + g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId + g.attrib["{http://www.ifcopenshell.org/ns}name"] = element.Name or "" + g.attrib["{http://www.ifcopenshell.org/ns}layer-id"] = str(layer.id()) + + lines = [] + for edge in edges: + start = [o for o in (camera_matrix_i @ Vector(verts[edge[0]])).xy] + end = [o for o in (camera_matrix_i @ Vector(verts[edge[1]])).xy] + coords = [start, end] + d = " ".join( + ["L{},{}".format((x_offset + p[0]) * svg_scale, (y_offset - p[1]) * svg_scale) for p in coords] + ) + d = "M{}".format(d[1:]) + path = etree.SubElement(g, "{http://www.w3.org/2000/svg}path") + path.attrib["d"] = d + group.append(g) + + bm.free() + def generate_freestyle_linework(self, context: bpy.types.Context) -> str | None: if not ifcopenshell.util.element.get_pset(self.drawing, "EPset_Drawing", "HasLinework"): return @@ -769,10 +868,12 @@ class CreateDrawing(bpy.types.Operator): if self.camera.data.BIMCameraProperties.cut_mode == "BISECT": self.remove_cut_linework(root) self.generate_bisect_linework(context, root) + self.generate_wall_layers(context, root) self.merge_linework_and_add_metadata(root) self.move_elements_to_top(root) elif self.camera.data.BIMCameraProperties.cut_mode == "OPENCASCADE": self.move_projection_to_bottom(root) + self.generate_wall_layers(context, root) self.merge_linework_and_add_metadata(root) self.move_elements_to_top(root) @@ -1101,7 +1202,7 @@ class CreateDrawing(bpy.types.Operator): # the style of the face when running tree.select_ray() # tree.enable_face_styles(True) - def get_svg_classes(self, element): + def get_svg_classes(self, element, layer=None): classes = [element.is_a()] material = ifcopenshell.util.element.get_material(element, should_skip_usage=True) material_name = "" @@ -1113,7 +1214,13 @@ class CreateDrawing(bpy.types.Operator): material_name = tool.Drawing.canonicalise_class_name(material_name) classes.append(f"material-{material_name}") else: - classes.append(f"material-null") + classes.append("material-null") + + if layer: + classes.append(layer.is_a()) + material_name = layer.Material.Name or "null" + material_name = tool.Drawing.canonicalise_class_name(material_name) + classes.append(f"layer-material-{material_name}") for key in self.metadata: value = ifcopenshell.util.selector.get_element_value(element, key) @@ -1152,6 +1259,23 @@ class CreateDrawing(bpy.types.Operator): except: continue + def get_element_by_id(self, step_id): + try: + step_id = int(step_id) + except: + return + try: + return tool.Ifc.get().by_id(step_id) + except: + props = tool.Project.get_project_props() + for link in props.links: + if link.name not in IfcStore.session_files: + IfcStore.session_files[link.name] = ifcopenshell.open(link.name) + try: + return IfcStore.session_files[link.name].by_id(step_id) + except: + continue + def remove_cut_linework(self, root): for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"): if "projection" not in el.get("class", "").split(): @@ -1163,15 +1287,15 @@ class CreateDrawing(bpy.types.Operator): join_criteria = join_criteria.split(",") else: # Drawing convention states that same objects classes with the same material are merged when cut. - join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status"] + join_criteria = ["class", "material.Name", "/Pset_.*Common/.Status", "EPset_Status.Status", "Material.Name"] group = root.find("{http://www.w3.org/2000/svg}g") joined_paths = {} self.is_manifold_cache = {} - ifc = tool.Ifc.get() for el in root.findall(".//{http://www.w3.org/2000/svg}g[@{http://www.ifcopenshell.org/ns}guid]"): element = self.get_element_by_guid(el.get("{http://www.ifcopenshell.org/ns}guid")) + layer = self.get_element_by_id(el.get("{http://www.ifcopenshell.org/ns}layer-id")) if "projection" in el.get("class", "").split(): classes = self.get_svg_classes(element) @@ -1179,7 +1303,7 @@ class CreateDrawing(bpy.types.Operator): el.set("class", " ".join(classes)) continue else: - classes = self.get_svg_classes(element) + classes = self.get_svg_classes(element, layer) classes.append("cut") el.set("class", " ".join(classes)) @@ -1188,7 +1312,12 @@ class CreateDrawing(bpy.types.Operator): if not obj: # This is a linked model object. For now, do nothing. continue - if not self.is_manifold(obj): + if (material := ifcopenshell.util.element.get_material(element)) and material.is_a() in ( + "IfcMaterialLayerSet", + "IfcMaterialLayerSetUsage", + ): + pass # These are always manifold + elif not self.is_manifold(obj): continue # An element group will contain a bunch of paths representing the @@ -1273,6 +1402,15 @@ class CreateDrawing(bpy.types.Operator): else: keys.append(key) + if layer: + for query in join_criteria: + key = ifcopenshell.util.selector.get_element_value(layer, query) + print("got layer key", query, key) + if isinstance(key, (list, tuple)): + keys.extend(key) + else: + keys.append(key) + hash_keys = hash(tuple(keys)) if el.findall("{http://www.w3.org/2000/svg}path"): @@ -1289,7 +1427,8 @@ class CreateDrawing(bpy.types.Operator): for path in el.findall("{http://www.w3.org/2000/svg}path"): for subpath in path.attrib["d"].split("M")[1:]: subpath_co = "M" + subpath.strip(" Z") - coords = [[float(o) for o in co[1:].split(",")] for co in subpath_co.split()] + # Round due to inaccuracies from Blender meshes and bisection + coords = [[round(float(o), 3) for o in co[1:].split(",")] for co in subpath_co.split()] if subpath.strip().lower().endswith("z"): coords.append(coords[0]) if len(coords) > 2 and coords[0] == coords[-1]: diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 86fb187db8..92f234f71d 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -2112,6 +2112,7 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def bisect_mesh(cls, obj: bpy.types.Object, camera: bpy.types.Object) -> tuple[list[Vector], list[list[int]]]: + # TODO consolidate with other bisect functions camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world plane_co = camera_matrix.translation plane_no = camera_matrix.col[2].xyz @@ -2121,10 +2122,38 @@ class Drawing(bonsai.core.tool.Drawing): return cls.bisect_mesh_with_plane(obj, plane_co, plane_no, global_offset=global_offset) + @classmethod + def bisect_bmesh(cls, obj, bm, geom, camera): + # TODO consolidate with other bisect functions + camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world + plane_co = camera_matrix.translation + plane_no = camera_matrix.col[2].xyz + + global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start + + # Run the bisect operation + results = bmesh.ops.bisect_plane(bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no) + + vert_map = {} + verts = [] + edges = [] + i = 0 + for geom in results["geom_cut"]: + if isinstance(geom, bmesh.types.BMVert): + verts.append(tuple((obj.matrix_world @ geom.co) + global_offset)) + vert_map[geom.index] = i + i += 1 + else: + # It seems as though edges always appear after verts + edges.append([vert_map[v.index] for v in geom.verts]) + + return verts, edges + @classmethod def bisect_mesh_with_plane( cls, obj: bpy.types.Object, plane_co: Vector, plane_no: Vector, global_offset: Optional[Vector] = None ) -> tuple[list[Vector], list[list[int]]]: + # TODO consolidate with other bisect functions if global_offset is None: global_offset = Vector() @@ -2152,6 +2181,16 @@ class Drawing(bonsai.core.tool.Drawing): return verts, edges + @classmethod + def get_extrusion_vector(cls, wall): + if body := ifcopenshell.util.representation.get_representation(wall, "Model", "Body", "MODEL_VIEW"): + for item in ifcopenshell.util.representation.resolve_representation(body).Items: + while item.is_a("IfcBooleanResult"): + item = item.FirstOperand + if item.is_a("IfcExtrudedAreaSolid"): + return Vector(item.ExtrudedDirection.DirectionRatios) + return Vector([0.0, 0.0, 1.0]) + @classmethod def get_scale_ratio(cls, scale: str) -> float: numerator, denominator = scale.split("/") From 6e7c8ad640c6c0a33f41e745961ecfa65d1cbeca Mon Sep 17 00:00:00 2001 From: Sayanjyoti Das Date: Sun, 16 Mar 2025 17:32:31 +0530 Subject: [PATCH 390/476] Fix #6362 --- src/bonsai/bonsai/bim/module/cad/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/cad/operator.py b/src/bonsai/bonsai/bim/module/cad/operator.py index 78a0b887c8..3d8bd5234e 100644 --- a/src/bonsai/bonsai/bim/module/cad/operator.py +++ b/src/bonsai/bonsai/bim/module/cad/operator.py @@ -564,7 +564,7 @@ class AddIfcCircle(bpy.types.Operator): def has_selected_existing_circle(self, context: bpy.types.Context) -> bool: obj = self.obj - bm = bmesh.from_edit_mesh(self, mesh) + bm = bmesh.from_edit_mesh(self.mesh) verts = [v for v in bm.verts if v.select and not v.hide] if len(verts) != 2: return False From 37ec2bf0e430c9b39953057ab87b969f5a204d47 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sun, 16 Mar 2025 20:04:04 -0500 Subject: [PATCH 391/476] add bonsai_commit_date to error report --- src/bonsai/bonsai/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/__init__.py b/src/bonsai/bonsai/__init__.py index be5b497145..2b0fbfc25b 100644 --- a/src/bonsai/bonsai/__init__.py +++ b/src/bonsai/bonsai/__init__.py @@ -96,6 +96,7 @@ def get_debug_info(): "blender_version": bpy.app.version_string, "bonsai_version": bbim_version, "bonsai_commit_hash": get_last_commit_hash(), + "bonsai_commit_date": last_commit_date, "last_actions": last_actions, "last_error": last_error, } @@ -218,6 +219,7 @@ if IN_BLENDER: path = Path(__file__).resolve().parent repo = git.Repo(str(path), search_parent_directories=True) last_commit_hash = repo.head.object.hexsha + last_commit_date = repo.head.object.committed_datetime.strftime("%Y-%m-%d %H:%M:%S") except: pass From 210257de8ebe675563b9c7ee902b719300415568 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 11:57:21 +0500 Subject: [PATCH 392/476] black . --- src/bonsai/bonsai/bim/module/model/slab.py | 2 +- src/bonsai/bonsai/bim/module/model/wall.py | 2 +- src/bonsai/bonsai/bim/module/sequence/operator.py | 1 + src/bonsai/bonsai/bim/operator.py | 6 +++--- src/bonsai/bonsai/tool/collector.py | 1 - src/bonsai/bonsai/tool/geometry.py | 8 +++++++- src/bonsai/bonsai/tool/snap.py | 13 ++++++++++--- 7 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index ad553bbfc7..78418971f9 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -322,7 +322,7 @@ class DumbSlabPlaner: abs(existing_x_angle) < (pi / 2) and direction_ratios.z < 0 ): # The extrusion direction is negative. If the layer_parameter is set to positive, - # then the we change the extrusion direction. And the offset direction should remain positive + # then the we change the extrusion direction. And the offset direction should remain positive # for either direction sense, so we change it. offset_direction *= -1 if layer_params["direction_sense"] == "POSITIVE": diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index ed3e32702e..49811c8cbd 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -343,7 +343,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): ): # The extrusion direction is negative. If the layer_parameter is set to positive, # then the we change the extrusion direction. - # then the we change the extrusion direction. And the offset direction should remain positive + # then the we change the extrusion direction. And the offset direction should remain positive # for either direction sense, so we change it. offset_direction *= -1 if layer_params["direction_sense"] == "POSITIVE": diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index b7730f2b93..5eccfd04a8 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -654,6 +654,7 @@ class DisableEditingWorkCalendar(bpy.types.Operator): core.disable_editing_work_calendar(tool.Sequence) return {"FINISHED"} + class ImportCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.import_csv" bl_label = "Import CSV" diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index bcb31264c5..54dc26617a 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -678,13 +678,13 @@ class BIM_OT_add_section_plane(bpy.types.Operator): continue # In EEVEE rendering engine, `blend_mode` is deprecated and replaced by `surface_render_method` # https://developer.blender.org/docs/release_notes/4.2/eevee_migration/#materials - if (hasattr(material, "surface_render_method")): + if hasattr(material, "surface_render_method"): material.surface_render_method = "DITHERED" else: material.blend_method = "HASHED" - + # TODO: Find an alternative to `shadow_method` for EEVEE engine - if (hasattr(material, "shadow_method")): + if hasattr(material, "shadow_method"): material.shadow_method = "HASHED" material_output = tool.Blender.get_material_node(material, "OUTPUT_MATERIAL", {"is_active_output": True}) diff --git a/src/bonsai/bonsai/tool/collector.py b/src/bonsai/bonsai/tool/collector.py index 55bcd75f05..fdef8c9c71 100644 --- a/src/bonsai/bonsai/tool/collector.py +++ b/src/bonsai/bonsai/tool/collector.py @@ -47,7 +47,6 @@ class Collector(bonsai.core.tool.Collector): if element.is_a("IfcSlab"): tool.Geometry.lock_rotation(obj, x=True) - if element.is_a("IfcGridAxis"): if tool.Geometry.is_locked(element): tool.Geometry.lock_object(obj) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index cca73f3fb4..9f3a5fee30 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -178,7 +178,13 @@ class Geometry(bonsai.core.tool.Geometry): obj.lock_scale = (False, False, False) @classmethod - def lock_rotation(cls, obj: bpy.types.Object, x: bool=False, y: bool=False, z: bool=False,) -> None: + def lock_rotation( + cls, + obj: bpy.types.Object, + x: bool = False, + y: bool = False, + z: bool = False, + ) -> None: obj.lock_rotation = (x, y, z) @classmethod diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 4915f93bf3..f1b3f04464 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -245,9 +245,14 @@ class Snap(bonsai.core.tool.Snap): # Then it sorts them to get the shortest first intersections = [] if snap_point["type"] == "Face": - face_normal = snap_point["object"].rotation_euler.to_matrix() @ snap_point["object"].data.polygons[snap_point["face_index"]].normal + face_normal = ( + snap_point["object"].rotation_euler.to_matrix() + @ snap_point["object"].data.polygons[snap_point["face_index"]].normal + ) if face_normal.z == 0: - intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point["point"], face_normal.normalized())) + intersections.append( + tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point["point"], face_normal.normalized()) + ) if not intersections: x_axis = tool.Polyline.use_transform_orientations(Vector((1, 0, 0))) y_axis = tool.Polyline.use_transform_orientations(Vector((0, 1, 0))) @@ -358,7 +363,9 @@ class Snap(bonsai.core.tool.Snap): face_index = result[2] if hit is not None: # Wireframes - if snap_obj.type in {"EMPTY", "CURVE"} or (snap_obj.type == "MESH" and len(snap_obj.data.polygons) == 0): + if snap_obj.type in {"EMPTY", "CURVE"} or ( + snap_obj.type == "MESH" and len(snap_obj.data.polygons) == 0 + ): snap_points = tool.Raycast.ray_cast_by_proximity(context, event, snap_obj) if snap_points: for point in snap_points: From 0d1298aac17553f09618de2764241039b3df12b5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 11:56:11 +0500 Subject: [PATCH 393/476] Fix #6367 ahh, no idea where I picked up this string during refactoring --- src/bonsai/bonsai/bim/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/handler.py b/src/bonsai/bonsai/bim/handler.py index 2f46d51e66..88e785194f 100644 --- a/src/bonsai/bonsai/bim/handler.py +++ b/src/bonsai/bonsai/bim/handler.py @@ -287,7 +287,7 @@ def get_user(ifc: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None def viewport_shading_changed_callback(area: bpy.types.Area) -> None: shading = area.spaces.active.shading.type if shading == "RENDERED": - tool.Style.get_style_props().active_style_type = "Internal" + tool.Style.get_style_props().active_style_type = "External" def subscribe_to_viewport_shading_changes(): From 16ba10f9576af10c13c469efe8dd1a4a4e14b7d4 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 12:02:21 +0500 Subject: [PATCH 394/476] Fix UI error when selected object has no material #6371 (ba37754) --- src/bonsai/bonsai/bim/module/material/data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index daaba6ed07..1ecbaaf65f 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -238,8 +238,10 @@ class ObjectMaterialData: return results @classmethod - def set_items(cls): + def set_items(cls) -> list[dict[str, Any]]: results = [] + if cls.material is None: + return results if cls.material: items = [] if cls.material.is_a("IfcMaterialLayerSetUsage"): From d8592a2a9537d04a59b3a7a02340232c48360a02 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 13:51:03 +0500 Subject: [PATCH 395/476] get_rectangular_perimeter to use get_x again #6364 (4260313) Just restoring original behaviour before 4260313 when get_rectangular_perimeter prioritized x dimension. --- src/bonsai/bonsai/bim/module/qto/calculator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index 7501d11821..9f79d64b11 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -291,7 +291,8 @@ def get_space_net_perimeter(obj: bpy.types.Object) -> float: def get_rectangular_perimeter(obj: bpy.types.Object) -> float: - length = get_length(obj, main_axis="x") + """Get object perimeter in XZ plane.""" + length = get_x(obj) height = get_height(obj) return (length + height) * 2 From 9af00d4e72c8af40e94dc4fd05fa9bf03d2df7da Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 13:29:50 +0500 Subject: [PATCH 396/476] typing --- .../bonsai/bim/module/qto/calculator.py | 67 +++++++++++++++---- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index 9f79d64b11..f0bf8db390 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -29,6 +29,7 @@ from mathutils.bvhtree import BVHTree from shapely.geometry import Polygon from shapely.ops import unary_union from typing import Literal, Union, Optional +from typing_extensions import assert_never AxisType = Literal["x", "y", "z"] @@ -51,6 +52,7 @@ def get_z(o: bpy.types.Object) -> float: def get_units(o: bpy.types.Object, vg_index: int) -> int: + assert isinstance(o.data, bpy.types.Mesh) return len([v for v in o.data.vertices if vg_index in [g.group for g in v.groups]]) @@ -72,11 +74,12 @@ def get_length(o: bpy.types.Object, vg_index: Optional[int] = None) -> float: x = get_x(o) y = get_y(o) z = get_z(o) - if get_object_main_axis(o) == "x": + main_axis_guess = get_object_main_axis(o) + if main_axis_guess == "x": return max(x, y) - if get_object_main_axis(o) == "z": + elif main_axis_guess == "z": return max(z, x) - if get_object_main_axis(o) == "y": + elif main_axis_guess == "y": return max(y, z) length = 0 @@ -114,7 +117,9 @@ def get_gross_stair_area(obj: bpy.types.Object) -> float: def get_parametric_axis(obj: bpy.types.Object) -> Literal["AXIS2", "AXIS3", None]: - relating_type = ifcopenshell.util.element.get_type(tool.Ifc.get_entity(obj)) + element = tool.Ifc.get_entity(obj) + assert element + relating_type = ifcopenshell.util.element.get_type(element) if relating_type: parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric") if parametric: @@ -137,6 +142,8 @@ def get_covering_gross_area(obj: bpy.types.Object) -> float: return get_gross_side_area(obj) elif parametrix_axis == "AXIS3": return get_gross_footprint_area(obj) + else: + assert_never(parametrix_axis) def get_covering_net_area(obj: bpy.types.Object) -> float: @@ -147,6 +154,8 @@ def get_covering_net_area(obj: bpy.types.Object) -> float: return get_net_side_area(obj) elif parametrix_axis == "AXIS3": return get_net_footprint_area(obj) + else: + assert_never(parametrix_axis) def get_covering_width(obj: bpy.types.Object) -> float: @@ -157,6 +166,8 @@ def get_covering_width(obj: bpy.types.Object) -> float: return get_width(obj) elif parametrix_axis == "AXIS3": return get_height(obj) + else: + assert_never(parametrix_axis) def get_width(o: bpy.types.Object) -> float: @@ -225,6 +236,7 @@ def get_finish_floor_height(obj: bpy.types.Object) -> float: space_min_z_value = get_min_global_z(obj) element = tool.Ifc.get_entity(obj) + assert element decompositions = ifcopenshell.util.element.get_decomposition(element) flooring_max_z_value = space_min_z_value for decomposition in decompositions: @@ -233,6 +245,7 @@ def get_finish_floor_height(obj: bpy.types.Object) -> float: and ifcopenshell.util.element.get_predefined_type(decomposition) == "FLOORING" ): flooring_obj = tool.Ifc.get_object(decomposition) + assert isinstance(flooring_obj, bpy.types.Object) flooring_z_value = get_max_global_z(flooring_obj) if flooring_z_value > space_min_z_value: flooring_max_z_value = flooring_z_value @@ -245,6 +258,7 @@ def get_ceiling_height(obj: bpy.types.Object) -> float: space_max_z_value = get_max_global_z(obj) element = tool.Ifc.get_entity(obj) + assert element decompositions = ifcopenshell.util.element.get_decomposition(element) ceiling_min_z_value = space_max_z_value for decomposition in decompositions: @@ -253,6 +267,7 @@ def get_ceiling_height(obj: bpy.types.Object) -> float: and ifcopenshell.util.element.get_predefined_type(decomposition) == "CEILING" ): ceiling_obj = tool.Ifc.get_object(decomposition) + assert isinstance(ceiling_obj, bpy.types.Object) ceiling_z_value = get_min_global_z(ceiling_obj) if ceiling_z_value < space_max_z_value: ceiling_min_z_value = ceiling_z_value @@ -300,6 +315,7 @@ def get_rectangular_perimeter(obj: bpy.types.Object) -> float: def get_lowest_polygons(o: bpy.types.Object) -> list[bpy.types.MeshPolygon]: lowest_polygons = [] lowest_z = None + assert isinstance(o.data, bpy.types.Mesh) for polygon in o.data.polygons: z = round(polygon.center[2], 3) if lowest_z is None: @@ -317,6 +333,7 @@ def get_lowest_polygons(o: bpy.types.Object) -> list[bpy.types.MeshPolygon]: def get_highest_polygons(o: bpy.types.Object) -> list[bpy.types.MeshPolygon]: highest_polygons = [] highest_z = None + assert isinstance(o.data, bpy.types.Mesh) for polygon in o.data.polygons: z = round(polygon.center[2], 3) if highest_z is None: @@ -332,10 +349,12 @@ def get_highest_polygons(o: bpy.types.Object) -> list[bpy.types.MeshPolygon]: def get_edge_key_distance(obj: bpy.types.Object, edge_key: tuple[int, int]) -> float: + assert isinstance(obj.data, bpy.types.Mesh) return (obj.data.vertices[edge_key[1]].co - obj.data.vertices[edge_key[0]].co).length def get_edge_distance(obj: bpy.types.Object, edge: bpy.types.MeshEdge) -> float: + assert isinstance(obj.data, bpy.types.Mesh) return (obj.data.vertices[edge.vertices[1]].co - obj.data.vertices[edge.vertices[0]].co).length @@ -350,6 +369,7 @@ def get_net_floor_area(obj: bpy.types.Object) -> float: decomposition_type = decomposition.get_info()["type"] if decomposition_type == "IfcColumn" or decomposition_type == "IfcColumn": decomposition_obj = tool.Ifc.get_object(decomposition) + assert isinstance(decomposition_obj, bpy.types.Object) net_footprint_obj_area = get_net_footprint_area(decomposition_obj) total_net_floor_area -= net_footprint_obj_area @@ -368,6 +388,7 @@ def get_gross_ceiling_area(obj: bpy.types.Object) -> float: decomposition_predefined_type = ifcopenshell.util.element.get_predefined_type(decomposition) if decomposition_class == "IfcCovering" and decomposition_predefined_type == "CEILING": decomposition_obj = tool.Ifc.get_object(decomposition) + assert isinstance(decomposition_obj, bpy.types.Object) total_gross_ceiling_area += get_gross_footprint_area(decomposition_obj) return total_gross_ceiling_area @@ -385,10 +406,12 @@ def get_net_ceiling_area(obj: bpy.types.Object) -> float: decomposition_predefined_type = ifcopenshell.util.element.get_predefined_type(decomposition) if decomposition_class == "IfcCovering" and decomposition_predefined_type == "CEILING": decomposition_obj = tool.Ifc.get_object(decomposition) + assert isinstance(decomposition_obj, bpy.types.Object) total_net_ceiling_area += get_net_footprint_area(decomposition_obj) if decomposition_class == "IfcWall" or decomposition_class == "IfcColumn": decomposition_obj = tool.Ifc.get_object(decomposition) + assert isinstance(decomposition_obj, bpy.types.Object) total_net_ceiling_area -= get_net_roofprint_area(decomposition_obj) return total_net_ceiling_area @@ -405,6 +428,7 @@ def get_space_net_volume(obj: bpy.types.Object) -> float: decomposition_type = decomposition.get_info()["type"] if decomposition_type == "IfcWall" or decomposition_type == "IfcColumn": decomposition_obj = tool.Ifc.get_object(decomposition) + assert isinstance(decomposition_obj, bpy.types.Object) total_space_net_volume -= get_net_volume(decomposition_obj) return total_space_net_volume @@ -493,6 +517,7 @@ def get_gross_surface_area(o: bpy.types.Object, vg_index: Optional[int] = None) return area area = 0 + assert isinstance(o.data, bpy.types.Mesh) vertices_in_vg = [v.index for v in o.data.vertices if vg_index in [g.group for g in v.groups]] for polygon in o.data.polygons: if is_polygon_in_vg(polygon, vertices_in_vg): @@ -501,6 +526,7 @@ def get_gross_surface_area(o: bpy.types.Object, vg_index: Optional[int] = None) def get_net_surface_area(obj: bpy.types.Object) -> float: + assert isinstance(obj.data, bpy.types.Mesh) return get_mesh_area(obj.data) @@ -511,7 +537,7 @@ def get_mesh_area(mesh: bpy.types.Mesh) -> float: return area -def is_polygon_in_vg(polygon: bpy.types.MeshPolygon, vertices_in_vg: list[bpy.types.MeshVertex]) -> bool: +def is_polygon_in_vg(polygon: bpy.types.MeshPolygon, vertices_in_vg: list[int]) -> bool: for v in polygon.vertices: if v not in vertices_in_vg: return False @@ -553,6 +579,7 @@ def has_openings(obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]: def get_obj_decompositions(obj: bpy.types.Object) -> set[ifcopenshell.entity_instance]: element = tool.Ifc.get_entity(obj) + assert element decompositions = ifcopenshell.util.element.get_decomposition(element) return decompositions @@ -591,6 +618,7 @@ def get_opening_type(opening: bpy.types.Object, obj: bpy.types.Object) -> Litera :param blender-object obj: blender object :return string: "OPENING" or "RECESS" """ + assert isinstance(opening.data, bpy.types.Mesh) polygons = opening.data.polygons ray_intersections = 0 @@ -687,21 +715,25 @@ def get_lateral_area( y_axis = [0, 1, 0] z_axis = [0, 0, 1] - if get_object_main_axis(obj) == "x" or main_axis == "x": - main_axis = x_axis + main_axis_guess = get_object_main_axis(obj) + if main_axis_guess == "x" or main_axis == "x": + main_axis_v = x_axis side_axis = y_axis top_axis = z_axis - elif get_object_main_axis(obj) == "z": - main_axis = z_axis + elif main_axis_guess == "z": + main_axis_v = z_axis side_axis = x_axis top_axis = y_axis - elif get_object_main_axis(obj) == "y": - main_axis = y_axis + elif main_axis_guess == "y": + main_axis_v = y_axis side_axis = z_axis top_axis = x_axis + else: + assert_never(main_axis_guess) area = 0 total_opening_area = 0 if subtract_openings else get_opening_area(obj, angle_z1=angle_z1, angle_z2=angle_z2) + assert isinstance(obj.data, bpy.types.Mesh) polygons = obj.data.polygons for polygon in polygons: @@ -709,7 +741,7 @@ def get_lateral_area( if angle_to_top_axis < angle_z1 or angle_to_top_axis > angle_z2: continue if exclude_end_areas: - angle_to_main_axis = math.degrees(polygon.normal.rotation_difference(Vector(main_axis)).angle) + angle_to_main_axis = math.degrees(polygon.normal.rotation_difference(Vector(main_axis_v)).angle) if angle_to_main_axis < 45 or angle_to_main_axis > 135: continue if exclude_side_areas: @@ -779,6 +811,7 @@ def get_gross_top_area(obj: bpy.types.Object, angle: float = 45) -> float: entity = ifc.by_guid(opening_id) open_obj = tool.Ifc.get_object(entity) + assert isinstance(open_obj, bpy.types.Object) opening_area += get_net_top_area(open_obj, angle=angle) else: continue @@ -911,6 +944,7 @@ def get_AABB_object(obj: bpy.types.Object) -> bpy.types.Object: ifc_id = tool.Blender.get_ifc_definition_id(obj) aabb_mesh = bpy.data.meshes.new(f"OBB_{ifc_id}") + assert isinstance(obj.data, bpy.types.Mesh) x = [v.co.x for v in obj.data.vertices] y = [v.co.y for v in obj.data.vertices] z = [v.co.z for v in obj.data.vertices] @@ -976,6 +1010,7 @@ def get_bisected_obj( ifc_id = tool.Blender.get_ifc_definition_id(obj) bis_obj = obj.copy() + assert isinstance(obj.data, bpy.types.Mesh) bis_obj.data = obj.data.copy() bis_obj.name = f"Bisected_{ifc_id}" @@ -1033,6 +1068,7 @@ def get_touching_objects(obj: bpy.types.Object, class_filter: list[str]) -> list obj.rotation_euler[1] += math.radians(0.001) bpy.context.evaluated_depsgraph_get().update() + assert isinstance(obj.data, bpy.types.Mesh) obj_mesh = bmesh.new() obj_mesh.from_mesh(obj.data) obj_mesh.transform(obj.matrix_world) @@ -1045,11 +1081,14 @@ def get_touching_objects(obj: bpy.types.Object, class_filter: list[str]) -> list for f in class_filter: filtered_objects += ifc.by_type(f) + blender_o = None for o in filtered_objects: blender_o = tool.Ifc.get_object(o) if blender_o == obj: continue o_mesh = bmesh.new() + assert isinstance(blender_o, bpy.types.Object) + assert isinstance(blender_o.data, bpy.types.Mesh) try: o_mesh.from_mesh(blender_o.data) except: @@ -1062,6 +1101,7 @@ def get_touching_objects(obj: bpy.types.Object, class_filter: list[str]) -> list touching_objects.append(blender_o) # return the objects to their original states + assert isinstance(blender_o, bpy.types.Object) blender_o.rotation_euler[0] -= math.radians(0.001) blender_o.rotation_euler[1] -= math.radians(0.001) bpy.context.evaluated_depsgraph_get().update() @@ -1079,6 +1119,8 @@ def get_contact_area(object1: bpy.types.Object, object2: bpy.types.Object) -> fl # list of tuples, each tuple containing the index of the polygon in object1 and object2 that are touching total_area = 0 + assert isinstance(object1.data, bpy.types.Mesh) + assert isinstance(object2.data, bpy.types.Mesh) for poly1 in object1.data.polygons: for poly2 in object2.data.polygons: total_area += get_intersection_between_polygons(object1, poly1, object2, poly2) @@ -1144,6 +1186,7 @@ def create_shapely_polygon(obj: bpy.types.Object, polygon: bpy.types.MeshPolygon """ polygon_tuples = [] odata = obj.data + assert isinstance(odata, bpy.types.Mesh) for loop_index in polygon.loop_indices: loop = odata.loops[loop_index] coords = obj.matrix_world @ odata.vertices[loop.vertex_index].co From 55be5cc289710e413c54d38a89e7b82448a3b10e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 14:44:02 +0500 Subject: [PATCH 397/476] get_length, getget_object_main_axis - simplify 1) get_object_main_axis - replace branching with just max between dimensions. There is a small caveat that previously for `x,y,z=1,2,2` it would return 'x' but now it returns 'y', but I'm not sure if 'x' was really intended here. 2) get_length - current implementation does the same as get_linear_length Ping @maxfb87 just in case. --- .../bonsai/bim/module/qto/calculator.py | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index f0bf8db390..87e527dc72 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -71,16 +71,7 @@ def get_linear_length(o: bpy.types.Object) -> float: def get_length(o: bpy.types.Object, vg_index: Optional[int] = None) -> float: """Calculate the object length trying to guess the main axis.""" if vg_index is None: - x = get_x(o) - y = get_y(o) - z = get_z(o) - main_axis_guess = get_object_main_axis(o) - if main_axis_guess == "x": - return max(x, y) - elif main_axis_guess == "z": - return max(z, x) - elif main_axis_guess == "y": - return max(y, z) + return get_linear_length(o) length = 0 assert isinstance(o.data, bpy.types.Mesh) @@ -1223,21 +1214,18 @@ def get_bmesh_from_mesh(mesh: bpy.types.Mesh) -> bmesh.types.BMesh: def get_object_main_axis(o: bpy.types.Object) -> AxisType: """_summary_: Returns the main object axis. Useful for profile-defined objects. + Main axis is the axis with the largest dimension. + :param blender-object o: Blender Object :return str: main axis x or y or z """ - x = get_x(o) - y = get_y(o) - z = get_z(o) - if x >= y and x > z: - return "x" - if y > z and y > x: - return "y" - if z > x and z > y: - return "z" - else: - return "x" + axes: list[tuple[AxisType, float]] = [ + ("x", get_x(o)), + ("y", get_y(o)), + ("z", get_z(o)), + ] + return max(axes, key=lambda x: x[1])[0] def is_opening_horizontal(o: bpy.types.Object) -> bool: From a8ff6ecf14561c83f3672cadc7333668fa39ab3d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 15:09:25 +0500 Subject: [PATCH 398/476] Fix object materials UI #6375 (ba37754) --- src/bonsai/bonsai/bim/module/material/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/material/data.py b/src/bonsai/bonsai/bim/module/material/data.py index 1ecbaaf65f..45c994dd6d 100644 --- a/src/bonsai/bonsai/bim/module/material/data.py +++ b/src/bonsai/bonsai/bim/module/material/data.py @@ -300,7 +300,7 @@ class ObjectMaterialData: else: data["material"] = item.Material.Name or "Unnamed" results.append(data) - should_reverse = cls.material.DirectionSense == "POSITIVE" + should_reverse = cls.material.is_a("IfcMaterialLayerSetUsage") and cls.material.DirectionSense == "POSITIVE" last_i = len(results) - 1 for i, result in enumerate(results): result["index"] = i From cf3a554b15ff915f02f1d3c0a4a131eab8886e14 Mon Sep 17 00:00:00 2001 From: "Sayan J. Das" Date: Mon, 17 Mar 2025 16:47:42 +0530 Subject: [PATCH 399/476] Fix #5809: Add safety check during file.write (#6292) Co-authored-by: theseyan --- src/ifcopenshell-python/ifcopenshell/file.py | 4 ++-- src/ifcwrap/IfcParseWrapper.i | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index 328d98b27f..10193baa76 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -675,6 +675,7 @@ class file: """ path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) + if format == None: format = ifcopenshell.guess_format(path) if format == ".ifcXML": @@ -690,8 +691,7 @@ class file: if format == ".ifcZIP": return self.write(path, ".ifc", zipped=True) self.wrapped_data.write(str(path)) - if not path.exists(): - raise PermissionError(f"Failed to write to '{path}', check folder permissions.") + if zipped: unzipped_path = path.with_suffix(format) path.rename(unzipped_path) diff --git a/src/ifcwrap/IfcParseWrapper.i b/src/ifcwrap/IfcParseWrapper.i index b7af4036bf..2400e12677 100644 --- a/src/ifcwrap/IfcParseWrapper.i +++ b/src/ifcwrap/IfcParseWrapper.i @@ -142,6 +142,9 @@ static IfcUtil::ArgumentType helper_fn_attribute_type(const IfcUtil::IfcBaseClas void write(const std::string& fn) { std::ofstream f(IfcUtil::path::from_utf8(fn).c_str()); + if (!f.good()) { + throw std::runtime_error("Failed to write to path: '" + fn + "', check folder and file permissions."); + } f << (*$self); } From 102de327377c6502186d58c00b23d45a339e7a0f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 15:47:22 +0500 Subject: [PATCH 400/476] Fix UI errors in case of uppercase unit names (7af6727) IfcConversionBasedUnit's Name is case insenstive and it was producing UI errors. Noticed working with file from Revit Ryan attached in #6374 Traceback: Traceback (most recent call last): File "\bonsai\bim\module\pset\ui.py", line 456, in poll ObjectMaterialData.load() File "\bonsai\bim\module\material\data.py", line 167, in load cls.data["total_thickness"] = cls.total_thickness() ^^^^^^^^^^^^^^^^^^^^^ File "\bonsai\bim\module\material\data.py", line 331, in total_thickness return format_distance(thickness, precision=precision, suppress_zero_inches=True, in_unit_length=True) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "\bonsai\bim\module\drawing\helper.py", line 157, in format_distance unit_length = unit_length_mapping[unit_length] ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^ KeyError: 'FOOT' --- src/bonsai/bonsai/bim/module/drawing/helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index ac0b4fbcd8..8c26a17cad 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -143,12 +143,12 @@ def format_distance( unit_scale = 1 if length_unit := ifcopenshell.util.unit.get_project_unit(tool.Ifc.get(), "LENGTHUNIT"): unit_system = "METRIC" if length_unit.Name == "METRE" else "IMPERIAL" - unit_length = length_unit.Name + unit_length = length_unit.Name.upper() if hasattr(length_unit, "Prefix") and length_unit.Prefix: unit_length = length_unit.Prefix + length_unit.Name unit_length_mapping = { - "foot": "FEET", - "inch": "INCHES", + "FOOT": "FEET", + "INCH": "INCHES", "METRE": "METERS", "DECIMETRE": "DECIMETERS", "CENTIMETRE": "CENTIMETERS", From 778a839e5f2085fae9105f052c878675c0965414 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 16:33:32 +0500 Subject: [PATCH 401/476] get_shape_aspects to consider element's type #6374 See diagram in https://github.com/IfcOpenShell/IfcOpenShell/issues/5839#issuecomment-2661296683 when IfcShapeAspect assigned to IfcRepresentationMap instead of being assigned to IfcProductDefinitionShape directly. --- .../ifcopenshell/util/element.py | 14 ++++++++++++-- .../test/util/test_element.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 3f9dc7e40f..85cc192ceb 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -591,10 +591,16 @@ def get_types(type: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_in return [] -def get_shape_aspects(element: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]: +def get_shape_aspects( + element: ifcopenshell.entity_instance, + should_inherit: bool = True, +) -> list[ifcopenshell.entity_instance]: """Get element's shape aspects. :param element: IfcProduct or IfcTypeProduct. + :param should_inherit: If True, the shape aspects of the element's type will be considered. + Useful in cases when IfcShapeAspects are assigned to the type's IfcRepresentationMap + instead of the element's IfcProductDefinitionShape. :return: The associated shape aspects of the element. Example: @@ -607,7 +613,11 @@ def get_shape_aspects(element: ifcopenshell.entity_instance) -> list[ifcopenshel # IfcProduct if (representation := getattr(element, "Representation", ...)) != ...: - return representation.HasShapeAspects + shape_aspects: list[ifcopenshell.entity_instance] = [] + if should_inherit and (element_type := get_type(element)): + shape_aspects.extend(get_shape_aspects(element_type)) + shape_aspects.extend(representation.HasShapeAspects) + return shape_aspects if element.file.schema == "IFC2X3": return [] diff --git a/src/ifcopenshell-python/test/util/test_element.py b/src/ifcopenshell-python/test/util/test_element.py index f40cb295da..54e4db0c58 100644 --- a/src/ifcopenshell-python/test/util/test_element.py +++ b/src/ifcopenshell-python/test/util/test_element.py @@ -387,6 +387,25 @@ class TestGetShapeAspects(test.bootstrap.IFC4): shape_aspect.PartOfProductDefinitionShape = product_shape assert tuple(subject.get_shape_aspects(element)) == (shape_aspect,) + def test_getting_the_shape_aspects_of_a_product_with_inheritance(self): + element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") + element_type = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWallType") + ifcopenshell.api.type.assign_type(self.file, related_objects=[element], relating_type=element_type) + + # Setup type shape aspect. + type_shape_aspect = self.file.create_entity("IfcShapeAspect") + representation_map = self.file.create_entity("IfcRepresentationMap") + element_type.RepresentationMaps = (representation_map,) + type_shape_aspect.PartOfProductDefinitionShape = representation_map + + # Setup occurrence shape aspect. + occurrence_shape_aspect = self.file.create_entity("IfcShapeAspect") + product_shape = self.file.create_entity("IfcProductDefinitionShape") + element.Representation = product_shape + occurrence_shape_aspect.PartOfProductDefinitionShape = product_shape + + assert subject.get_shape_aspects(element) == [type_shape_aspect, occurrence_shape_aspect] + class TestGetMaterial(test.bootstrap.IFC4): def test_getting_the_material_of_a_product(self): From e67753eb4f4af6dddd4c1a447d1aa1d7fd4f562d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Mon, 17 Mar 2025 23:05:07 +1100 Subject: [PATCH 402/476] See #1227. Clear cache when activating drawings. --- src/bonsai/bonsai/bim/module/drawing/decoration.py | 3 +++ src/bonsai/bonsai/bim/module/drawing/operator.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 1aa6345793..dca510ad1b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -1595,6 +1595,9 @@ class CutDecorator: @classmethod def install(cls, context): + DecoratorData.cut_cache.clear() + DecoratorData.slice_cache.clear() + DecoratorData.fill_cache.clear() if cls.installed: cls.uninstall() handler = cls() diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 754b262ba8..74ecc04f8f 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -2180,7 +2180,6 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase): @classmethod def poll(cls, context): - props = tool.Drawing.get_document_props() if not tool.Drawing.get_active_drawing_item(): cls.poll_message_set("No drawing selected.") return False From 9c5b1a04ff59d004485aa01570b98651c285ee24 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 18 Mar 2025 00:14:56 +1100 Subject: [PATCH 403/476] Fix #6372. See #1227. More reliable layer slicing. Always slice in in 2D, not 3D, where possible. --- .../bonsai/bim/module/drawing/decoration.py | 80 ++++++++++--------- .../bonsai/bim/module/drawing/operator.py | 27 ++++--- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index dca510ad1b..2077f25f62 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -1726,15 +1726,33 @@ class CutDecorator: bm_original = bmesh.new() bm_original.from_mesh(mesh) + # Slice our mesh into a 2D drawing cut (2D is always easier) + camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world + global_offset = context.scene.camera.matrix_world.col[2].xyz * -context.scene.camera.data.clip_start + plane_co = camera_matrix.translation + plane_no = camera_matrix.col[2].xyz + geom = bm_original.verts[:] + bm_original.edges[:] + bm_original.faces[:] + bmesh.ops.bisect_plane( + bm_original, + geom=geom, + dist=0.0001, + plane_co=plane_co, + plane_no=plane_no, + clear_outer=True, + clear_inner=True, + ) + bmesh.ops.remove_doubles(bm_original, verts=bm_original.verts, dist=0.000001) + bmesh.ops.triangle_fill(bm_original, use_dissolve=True, edges=bm_original.edges) + if not (material := ifcopenshell.util.element.get_material(element)): - verts, tris = self.bisect_mesh_tris(obj, bm_original, context.scene.camera) + verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera) DecoratorData.fill_cache[element_id].setdefault(self.fallback_colour, []).append((verts, tris)) return if material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialLayerSetUsage"): # Constituents, lists, and item styles not supported yet material = ifcopenshell.util.element.get_materials(element)[0] - verts, tris = self.bisect_mesh_tris(obj, bm_original, context.scene.camera) + verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera) colour = self.get_material_colour(material) DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) return @@ -1754,7 +1772,7 @@ class CutDecorator: if len(layer_set.MaterialLayers) == 1: material = layer_set.MaterialLayers[0].Material - verts, tris = self.bisect_mesh_tris(obj, bm_original, context.scene.camera) + verts, tris = self.get_bmesh_tris(obj, bm_original, context.scene.camera) colour = self.get_material_colour(material) DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) return @@ -1778,41 +1796,42 @@ class CutDecorator: no = tool.Drawing.get_extrusion_vector(element).normalized() no = Vector([1.0, 0.0, 0.0]) no *= sense_factor - slice_plane_geom = [] last_i = len(layer_set.MaterialLayers) - 1 + + vert_map = {} + verts = [] + edges = [] + j = 0 + for i, layer in enumerate(layer_set.MaterialLayers): prev_co = co.copy() co += no * layer.LayerThickness * self.unit_scale if i != last_i: - bisect_geom = bmesh.ops.bisect_plane( + bisect = bmesh.ops.bisect_plane( bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=no ) - slice_plane_geom.extend(bisect_geom["geom_cut"]) - edges = [g for g in bisect_geom["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] - fill = bmesh.ops.edgenet_fill(bm, edges=edges) - slice_plane_geom.extend(fill["faces"]) + for geom in bisect["geom_cut"]: + if isinstance(geom, bmesh.types.BMVert): + verts.append(tuple((obj.matrix_world @ geom.co) + global_offset)) + vert_map[geom.index] = j + j += 1 + else: + # It seems as though edges always appear after verts + edges.append([vert_map[v.index] for v in geom.verts]) colour = self.get_material_colour(layer.Material) bm_fill = bm_original.copy() if i != last_i: geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] - bisect = bmesh.ops.bisect_plane( - bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True - ) - edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] - bmesh.ops.edgenet_fill(bm_fill, edges=edges) + bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True) if i != 0: geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] - bisect = bmesh.ops.bisect_plane( - bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True - ) - edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] - bmesh.ops.edgenet_fill(bm_fill, edges=edges) + bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True) - verts, tris = self.bisect_mesh_tris(obj, bm_fill, context.scene.camera) - DecoratorData.fill_cache[element_id].setdefault(colour, []).append((verts, tris)) + DecoratorData.fill_cache[element_id].setdefault(colour, []).append( + self.get_bmesh_tris(obj, bm_fill, context.scene.camera) + ) - verts, edges = tool.Drawing.bisect_bmesh(obj, bm, slice_plane_geom, context.scene.camera) DecoratorData.slice_cache[element.id()] = (verts, edges) bm_original.free() @@ -1826,21 +1845,9 @@ class CutDecorator: colour = styles[0].SurfaceColour return (colour.Red, colour.Green, colour.Blue, 1) - def bisect_mesh_tris(self, obj, bm, camera): - camera_matrix = obj.matrix_world.inverted() @ camera.matrix_world - plane_co = camera_matrix.translation - plane_no = camera_matrix.col[2].xyz - + def get_bmesh_tris(self, obj, bm, camera): global_offset = camera.matrix_world.col[2].xyz * -camera.data.clip_start - - geom = bm.verts[:] + bm.edges[:] + bm.faces[:] - bmesh.ops.bisect_plane( - bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no, clear_inner=True, clear_outer=True - ) - bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-6) - fill = bmesh.ops.edgenet_fill(bm, edges=bm.edges) - triangulate = bmesh.ops.triangulate(bm, faces=fill["faces"]) - + triangulate = bmesh.ops.triangulate(bm, faces=bm.faces) vert_map = {} verts = [] tris = [] @@ -1856,7 +1863,6 @@ class CutDecorator: tri.append(i) i += 1 tris.append(tri) - return verts, tris diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 74ecc04f8f..8dca73b3e4 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -599,6 +599,17 @@ class CreateDrawing(bpy.types.Operator): bm = bmesh.new() bm.from_mesh(mesh) + # Slice our mesh into a 2D drawing cut (2D is always easier) + camera_matrix = obj.matrix_world.inverted() @ context.scene.camera.matrix_world + plane_co = camera_matrix.translation + plane_no = camera_matrix.col[2].xyz + geom = bm.verts[:] + bm.edges[:] + bm.faces[:] + bmesh.ops.bisect_plane( + bm, geom=geom, dist=0.0001, plane_co=plane_co, plane_no=plane_no, clear_outer=True, clear_inner=True + ) + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001) + bmesh.ops.triangle_fill(bm, use_dissolve=True, edges=bm.edges) + prev_co = None if not usage: sense_factor = 1 # Assume the extrusion vector points in the direction sense @@ -625,21 +636,17 @@ class CreateDrawing(bpy.types.Operator): bm_fill = bm.copy() if i != last_i: geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] - bisect = bmesh.ops.bisect_plane( - bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True - ) - edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] - bmesh.ops.edgenet_fill(bm_fill, edges=edges) + bmesh.ops.bisect_plane(bm_fill, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True) if i != 0: geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] - bisect = bmesh.ops.bisect_plane( + bmesh.ops.bisect_plane( bm_fill, geom=geom, dist=0.0001, plane_co=prev_co, plane_no=no, clear_inner=True ) - edges = [g for g in bisect["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] - bmesh.ops.edgenet_fill(bm_fill, edges=edges) - geom = bm_fill.verts[:] + bm_fill.edges[:] + bm_fill.faces[:] - verts, edges = tool.Drawing.bisect_bmesh(obj, bm_fill, geom, context.scene.camera) + bm_fill.verts.ensure_lookup_table() + bm_fill.edges.ensure_lookup_table() + verts = [tuple(obj.matrix_world @ v.co) for v in bm_fill.verts] + edges = [[v.index for v in e.verts] for e in bm_fill.edges] g = etree.SubElement(root, "{http://www.w3.org/2000/svg}g") g.attrib["{http://www.ifcopenshell.org/ns}guid"] = element.GlobalId From 73a83c6d4e6b2b78f0972e35d8ff713ce4be569e Mon Sep 17 00:00:00 2001 From: Jesse Roodhorst <160035772+jes-r@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:28:03 +0100 Subject: [PATCH 404/476] Issue #5130 fix + sheet all expand and contract (#6379) --- .../bonsai/bim/module/drawing/operator.py | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 8dca73b3e4..c580b3459a 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3298,14 +3298,22 @@ class DisableEditingDrawings(bpy.types.Operator, tool.Ifc.Operator): class ExpandTargetView(bpy.types.Operator): bl_idname = "bim.expand_target_view" bl_label = "Expand Target View" - bl_description = "Show views in this category" + bl_description = "\nSHIFT+CLICK to expand all view categories " bl_options = {"REGISTER", "UNDO"} target_view: bpy.props.StringProperty() - + expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + # Expanding all categories on shift+click. + # Make sure to use SKIP_SAVE on property, otherwise it might get stuck (copied from #4771). + if event.type == "LEFTMOUSE" and event.shift: + self.expand_all = True + return self.execute(context) + def execute(self, context): props = tool.Drawing.get_document_props() - for drawing in [d for d in props.drawings if d.target_view == self.target_view]: + for drawing in [d for d in props.drawings if self.expand_all or d.target_view == self.target_view]: drawing.is_expanded = True core.load_drawings(tool.Drawing) return {"FINISHED"} @@ -3314,14 +3322,22 @@ class ExpandTargetView(bpy.types.Operator): class ContractTargetView(bpy.types.Operator): bl_idname = "bim.contract_target_view" bl_label = "Contract Target View" - bl_description = "Hide views in this category" + bl_description = "\n\nSHIFT+CLICK to hide all view categories" bl_options = {"REGISTER", "UNDO"} target_view: bpy.props.StringProperty() + contract_all: bpy.props.BoolProperty(name="Contract All", default=False, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + # Contracting all categories on shift+click. + # Make sure to use SKIP_SAVE on property, otherwise it might get stuck (copied from #4771). + if event.type == "LEFTMOUSE" and event.shift: + self.contract_all = True + return self.execute(context) def execute(self, context): props = tool.Drawing.get_document_props() - for drawing in [d for d in props.drawings if d.target_view == self.target_view]: + for drawing in [d for d in props.drawings if self.contract_all or d.target_view == self.target_view]: drawing.is_expanded = False core.load_drawings(tool.Drawing) return {"FINISHED"} @@ -3330,14 +3346,20 @@ class ContractTargetView(bpy.types.Operator): class ExpandSheet(bpy.types.Operator): bl_idname = "bim.expand_sheet" bl_label = "Expand Sheet" - bl_description = "Show views, schedules, references etc\nplaced on this sheet" - + bl_description = "Show views, schedules, references etc\nplaced on this sheet.\n\nShift+click to expand all sheets." bl_options = {"REGISTER", "UNDO"} + sheet: bpy.props.IntProperty() + expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + if event.type == "LEFTMOUSE" and event.shift: + self.expand_all = True + return self.execute(context) def execute(self, context): props = tool.Drawing.get_document_props() - for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]: + for sheet in [s for s in props.sheets if self.expand_all or s.ifc_definition_id == self.sheet]: sheet.is_expanded = True core.load_sheets(tool.Drawing) return {"FINISHED"} @@ -3346,14 +3368,20 @@ class ExpandSheet(bpy.types.Operator): class ContractSheet(bpy.types.Operator): bl_idname = "bim.contract_sheet" bl_label = "Contract Sheet" - bl_description = "Hide views, schedules, references etc\nplaced on this sheet" - + bl_description = "Hide views, schedules, references etc\nplaced on this sheet.\n\nShift+click to contract all sheets." bl_options = {"REGISTER", "UNDO"} + sheet: bpy.props.IntProperty() + expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"}) + + def invoke(self, context, event): + if event.type == "LEFTMOUSE" and event.shift: + self.expand_all = True + return self.execute(context) def execute(self, context): props = tool.Drawing.get_document_props() - for sheet in [s for s in props.sheets if s.ifc_definition_id == self.sheet]: + for sheet in [s for s in props.sheets if self.expand_all or s.ifc_definition_id == self.sheet]: sheet.is_expanded = False core.load_sheets(tool.Drawing) return {"FINISHED"} From 1f43f7f807ee5a40b44ba6f6b04287920c00c5ce Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 16:38:58 +0500 Subject: [PATCH 405/476] black . --- src/bonsai/bonsai/bim/module/drawing/operator.py | 12 +++++++----- src/ifcopenshell-python/ifcopenshell/file.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index c580b3459a..ba1f593096 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -3303,14 +3303,14 @@ class ExpandTargetView(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} target_view: bpy.props.StringProperty() expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"}) - + def invoke(self, context, event): # Expanding all categories on shift+click. # Make sure to use SKIP_SAVE on property, otherwise it might get stuck (copied from #4771). if event.type == "LEFTMOUSE" and event.shift: self.expand_all = True return self.execute(context) - + def execute(self, context): props = tool.Drawing.get_document_props() for drawing in [d for d in props.drawings if self.expand_all or d.target_view == self.target_view]: @@ -3348,7 +3348,7 @@ class ExpandSheet(bpy.types.Operator): bl_label = "Expand Sheet" bl_description = "Show views, schedules, references etc\nplaced on this sheet.\n\nShift+click to expand all sheets." bl_options = {"REGISTER", "UNDO"} - + sheet: bpy.props.IntProperty() expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"}) @@ -3368,9 +3368,11 @@ class ExpandSheet(bpy.types.Operator): class ContractSheet(bpy.types.Operator): bl_idname = "bim.contract_sheet" bl_label = "Contract Sheet" - bl_description = "Hide views, schedules, references etc\nplaced on this sheet.\n\nShift+click to contract all sheets." + bl_description = ( + "Hide views, schedules, references etc\nplaced on this sheet.\n\nShift+click to contract all sheets." + ) bl_options = {"REGISTER", "UNDO"} - + sheet: bpy.props.IntProperty() expand_all: bpy.props.BoolProperty(name="Expand All", default=False, options={"SKIP_SAVE"}) diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index 10193baa76..68d939a046 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -691,7 +691,7 @@ class file: if format == ".ifcZIP": return self.write(path, ".ifc", zipped=True) self.wrapped_data.write(str(path)) - + if zipped: unzipped_path = path.with_suffix(format) path.rename(unzipped_path) From 0b4deee73fae841e03a9afdf10b608c59e7fa2ed Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 15:08:25 +0500 Subject: [PATCH 406/476] typing --- .../bonsai/bim/module/drawing/helper.py | 1 + src/bonsai/bonsai/bim/module/geometry/data.py | 4 +++- src/bonsai/bonsai/bim/module/geometry/prop.py | 7 +++---- src/bonsai/bonsai/bim/module/material/ui.py | 4 ++-- src/bonsai/bonsai/bim/module/pset/ui.py | 20 +++++++++++-------- src/bonsai/bonsai/tool/pset.py | 6 +++++- 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index 8c26a17cad..cbd98cfe5a 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -20,6 +20,7 @@ import bpy import math import mathutils.geometry import ifcopenshell +import ifcopenshell.util.unit import bonsai.tool as tool from mathutils import Vector from typing import Union diff --git a/src/bonsai/bonsai/bim/module/geometry/data.py b/src/bonsai/bonsai/bim/module/geometry/data.py index 2ba25df702..2522615885 100644 --- a/src/bonsai/bonsai/bim/module/geometry/data.py +++ b/src/bonsai/bonsai/bim/module/geometry/data.py @@ -157,15 +157,17 @@ class RepresentationsData: if not cls.data["representations"]: return [] obj = tool.Geometry.get_active_or_representation_obj() + assert obj if not obj.data: return [] element = tool.Ifc.get_entity(obj) + assert element base_representation = tool.Geometry.get_active_representation(obj) if not base_representation: return [] # Maybe in profile editing mode # shape aspects matching context of the active representation - matching_shape_aspects = [] + matching_shape_aspects: list[ifcopenshell.entity_instance] = [] for shape_aspect in ifcopenshell.util.element.get_shape_aspects(element): matching_representation = tool.Geometry.get_shape_aspect_representation(shape_aspect, base_representation) if matching_representation: diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py index 7b80e9b040..6e67aca973 100644 --- a/src/bonsai/bonsai/bim/module/geometry/prop.py +++ b/src/bonsai/bonsai/bim/module/geometry/prop.py @@ -121,7 +121,7 @@ def get_layers_no_active(self, context): return LayersData.data["layers_enum_no_active"] -def update_shape_aspect(self, context): +def update_shape_aspect(self: "BIMObjectGeometryProperties", context: bpy.types.Context) -> None: shape_aspect_id = self.representation_item_shape_aspect attrs = self.shape_aspect_attrs @@ -250,9 +250,8 @@ class BIMObjectGeometryProperties(PropertyGroup): representation_item_layer: EnumProperty(items=get_layers, name="Representation Item's Layer") @property - def active_item(self): - if 0 <= self.active_item_index < len(self.items): - return self.items[self.active_item_index] + def active_item(self) -> Union[RepresentationItem, None]: + return tool.Blender.get_active_uilist_element(self.items, self.active_item_index) if TYPE_CHECKING: contexts: str diff --git a/src/bonsai/bonsai/bim/module/material/ui.py b/src/bonsai/bonsai/bim/module/material/ui.py index 6ca1d659ed..1b49a4aeee 100644 --- a/src/bonsai/bonsai/bim/module/material/ui.py +++ b/src/bonsai/bonsai/bim/module/material/ui.py @@ -24,7 +24,7 @@ from bpy.types import Panel, UIList from bonsai.bim.helper import draw_attributes from bonsai.bim.helper import prop_with_search from bonsai.bim.module.material.data import MaterialsData, ObjectMaterialData -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from bonsai.bim.module.material.prop import Material, BIMMaterialProperties @@ -289,7 +289,7 @@ class BIM_PT_object_material(Panel): row = box.row() prop_with_search(row, self.mprops, "profiles", icon="ITALIC", text="Profile") - def draw_read_only_set_item_ui(self, box, set_item): + def draw_read_only_set_item_ui(self, box: bpy.types.UILayout, set_item: dict[str, Any]) -> None: if ObjectMaterialData.data["material_class"] == "IfcMaterialList": row = box.row(align=True) row.label(text="IfcMaterial", icon="LAYER_ACTIVE") diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py index 5bc6de9b6f..62fd6366c9 100644 --- a/src/bonsai/bonsai/bim/module/pset/ui.py +++ b/src/bonsai/bonsai/bim/module/pset/ui.py @@ -237,7 +237,8 @@ class BIM_PT_object_psets(Panel): def draw_header(self, context): row = self.layout.row(align=True) row.label(text="") # empty text occupies the left of the row - row.prop(context.scene.GlobalPsetProperties, "pset_filter", text="", icon="VIEWZOOM") + global_props = tool.Pset.get_global_pset_props() + row.prop(global_props, "pset_filter", text="", icon="VIEWZOOM") @classmethod def poll(cls, context): @@ -261,6 +262,7 @@ class BIM_PT_object_psets(Panel): op.obj = context.active_object.name op.obj_type = "Object" + global_props = tool.Pset.get_global_pset_props() if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "PSET": draw_psetqto_ui( context, @@ -269,7 +271,7 @@ class BIM_PT_object_psets(Panel): props, self.layout, "Object", - filter_keyword=context.scene.GlobalPsetProperties.pset_filter, + filter_keyword=global_props.pset_filter, ) if ObjectPsetsData.data["psets"]: @@ -285,7 +287,7 @@ class BIM_PT_object_psets(Panel): props, self.layout, "Object", - filter_keyword=context.scene.GlobalPsetProperties.pset_filter, + filter_keyword=global_props.pset_filter, ) if ObjectPsetsData.data["inherited_psets"]: @@ -299,7 +301,7 @@ class BIM_PT_object_psets(Panel): self.layout, "Object", allow_removing=False, - filter_keyword=context.scene.GlobalPsetProperties.pset_filter, + filter_keyword=global_props.pset_filter, ) @@ -315,7 +317,8 @@ class BIM_PT_object_qtos(Panel): def draw_header(self, context): row = self.layout.row(align=True) row.label(text="") # empty text occupies the left of the row - row.prop(context.scene.GlobalPsetProperties, "qto_filter", text="", icon="VIEWZOOM") + global_props = tool.Pset.get_global_pset_props() + row.prop(global_props, "qto_filter", text="", icon="VIEWZOOM") @classmethod def poll(cls, context): @@ -339,6 +342,7 @@ class BIM_PT_object_qtos(Panel): op.obj = context.active_object.name op.obj_type = "Object" + global_props = tool.Pset.get_global_pset_props() if not props.active_pset_id and props.active_pset_name and props.active_pset_type == "QTO": draw_psetqto_ui( context, @@ -347,7 +351,7 @@ class BIM_PT_object_qtos(Panel): props, self.layout, "Object", - filter_keyword=context.scene.GlobalPsetProperties.qto_filter, + filter_keyword=global_props.qto_filter, ) if ObjectQtosData.data["qtos"]: @@ -363,7 +367,7 @@ class BIM_PT_object_qtos(Panel): props, self.layout, "Object", - filter_keyword=context.scene.GlobalPsetProperties.qto_filter, + filter_keyword=global_props.qto_filter, ) if ObjectQtosData.data["inherited_qsets"]: @@ -377,7 +381,7 @@ class BIM_PT_object_qtos(Panel): self.layout, "Object", allow_removing=False, - filter_keyword=context.scene.GlobalPsetProperties.qto_filter, + filter_keyword=global_props.qto_filter, ) layout = self.layout qtoprops = tool.Qto.get_qto_props() diff --git a/src/bonsai/bonsai/tool/pset.py b/src/bonsai/bonsai/tool/pset.py index 39e4b0d721..d979fdb1ac 100644 --- a/src/bonsai/bonsai/tool/pset.py +++ b/src/bonsai/bonsai/tool/pset.py @@ -31,12 +31,16 @@ from typing_extensions import assert_never if TYPE_CHECKING: - from bonsai.bim.module.pset.prop import PsetProperties + from bonsai.bim.module.pset.prop import PsetProperties, GlobalPsetProperties class Pset(bonsai.core.tool.Pset): PSET_TYPE = Literal["PSET", "QTO"] + @classmethod + def get_global_pset_props(cls) -> GlobalPsetProperties: + return bpy.context.scene.GlobalPsetProperties + @classmethod def get_element_pset( cls, element: ifcopenshell.entity_instance, pset_name: str From 5109168533009e56a72a1632dd0a71a7f2c714e5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 17:47:21 +0500 Subject: [PATCH 407/476] quantification fallback - small clarification --- src/bonsai/bonsai/bim/module/qto/prop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/qto/prop.py b/src/bonsai/bonsai/bim/module/qto/prop.py index e632bb2915..57e04d4cc1 100644 --- a/src/bonsai/bonsai/bim/module/qto/prop.py +++ b/src/bonsai/bonsai/bim/module/qto/prop.py @@ -93,7 +93,7 @@ class BIMQtoProperties(PropertyGroup): name="Fallback To Other Calculators", description=( "If currently selected calculator does not support quantification " - "of some class/property, to try other available calculators." + "of some class/quantity set, to try other available calculators." ), default=False, ) From 0abe6329d626bcedc237bad3cc7083074538f0b5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 17 Mar 2025 17:59:00 +0500 Subject: [PATCH 408/476] Quantification to consider Pset_ProfileMechanical.MassPerLength #6344 --- .../bonsai/bim/module/qto/calculator.py | 36 +++++++++++++-- src/bonsai/bonsai/tool/geometry.py | 10 ++--- src/ifc5d/ifc5d/qto.py | 44 +++++++++++++++++-- .../ifcopenshell/util/element.py | 26 ++++++++++- 4 files changed, 103 insertions(+), 13 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/qto/calculator.py b/src/bonsai/bonsai/bim/module/qto/calculator.py index 87e527dc72..264a8fbf6b 100644 --- a/src/bonsai/bonsai/bim/module/qto/calculator.py +++ b/src/bonsai/bonsai/bim/module/qto/calculator.py @@ -24,6 +24,7 @@ import bonsai.tool as tool import ifcopenshell import ifcopenshell.geom import ifcopenshell.util.element +import ifc5d.qto from mathutils import Vector, Matrix from mathutils.bvhtree import BVHTree from shapely.geometry import Polygon @@ -565,7 +566,7 @@ def has_openings(obj: bpy.types.Object) -> list[ifcopenshell.entity_instance]: element = tool.Ifc.get_entity(obj) if not element: return [] - return [o for o in tool.Geometry.get_openings(element)] + return [o for o in ifcopenshell.util.element.get_openings(element)] def get_obj_decompositions(obj: bpy.types.Object) -> set[ifcopenshell.entity_instance]: @@ -576,20 +577,42 @@ def get_obj_decompositions(obj: bpy.types.Object) -> set[ifcopenshell.entity_ins def get_gross_weight(obj: bpy.types.Object) -> Union[float, None]: - """Get gross weight of the object (based on gross volume and Pset_MaterialCommon.MassDensity)""" + """Get gross weight of the object. + + Based on gross volume and Pset_MaterialCommon.MassDensity + or Pset_ProfileMechanical.MassPerLength and extrusion depth if it's profile based. + """ + + weight = get_profile_obj_weight(obj) + if weight is not None: + return weight + obj_mass_density = get_obj_mass_density(obj) if not obj_mass_density: return + gross_volume = get_gross_volume(obj) gross_weight = obj_mass_density * gross_volume return gross_weight def get_net_weight(obj: bpy.types.Object) -> Union[float, None]: - """Get net weight of the object (based on net volume and Pset_MaterialCommon.MassDensity)""" + """Get net weight of the object. + + Based on Pset_ProfileMechanical.MassPerLength and extrusion depth + (for profile based objects, though objects with openings are not supported) + or object's net volume and Pset_MaterialCommon.MassDensity. + """ + + if not has_openings(obj): + weight = get_profile_obj_weight(obj) + if weight is not None: + return weight + obj_mass_density = get_obj_mass_density(obj) if not obj_mass_density: return + net_volume = get_net_volume(obj) net_weight = obj_mass_density * net_volume return net_weight @@ -602,6 +625,13 @@ def get_obj_mass_density(obj: bpy.types.Object) -> Union[float, None]: return ifcopenshell.util.element.get_element_mass_density(entity) +def get_profile_obj_weight(obj: bpy.types.Object) -> Union[float, None]: + element = tool.Ifc.get_entity(obj) + assert element + weight = ifc5d.qto.IfcOpenShell.get_weight_profile_based(element) + return weight + + def get_opening_type(opening: bpy.types.Object, obj: bpy.types.Object) -> Literal["OPENING", "RECESS"]: """_summary_: Returns the opening type - OPENING / RECESS diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 9f3a5fee30..2975bbdbcb 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1660,15 +1660,13 @@ class Geometry(bonsai.core.tool.Geometry): Use `.RelatedOpeningElement` to get the opening element. """ - for element_rel in getattr(element, "HasOpenings", ()): - yield element_rel - - if aggregate := ifcopenshell.util.element.get_aggregate(element): - yield from cls.get_openings(aggregate) + # TODO: replace everywhere with util method. + return ifcopenshell.util.element.get_openings(element) @classmethod def has_openings(cls, element: ifcopenshell.entity_instance) -> bool: - return bool(next(cls.get_openings(element), False)) + # TODO: replace everywhere with util method. + return ifcopenshell.util.element.has_openings(element) @classmethod def get_elements_by_representation( diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py index a71aa32942..6856d78fba 100644 --- a/src/ifc5d/ifc5d/qto.py +++ b/src/ifc5d/ifc5d/qto.py @@ -238,7 +238,9 @@ class IfcOpenShell(QtoCalculator): "get_weight": Function( "IfcMassMeasure", "Weight", - "The weight of the object based on it's volume and material density (from Pset_MaterialCommon.MassDensity).", + "The weight of the object based on it's length and Pset_ProfileMechanical.MassPerLength " + "(for profile based objects, though objects with openings are not supported for net calculations)" + "or it's volume and material density (from Pset_MaterialCommon.MassDensity).", ), } @@ -308,7 +310,8 @@ class IfcOpenShell(QtoCalculator): if value is None: continue elif formula == "get_weight": - value = cls.get_weight(element, geometry) + calculation_type = "GROSS" if iterator.settings is cls.gross_settings else "NET" + value = cls.get_weight(element, geometry, calculation_type) if value is None: continue else: @@ -367,7 +370,10 @@ class IfcOpenShell(QtoCalculator): @classmethod def get_weight( - cls, element: ifcopenshell.entity_instance, geometry: ifcopenshell.geom.ShapeType + cls, + element: ifcopenshell.entity_instance, + geometry: ifcopenshell.geom.ShapeType, + calculation_type: Literal["GROSS", "NET"], ) -> Union[float, None]: """Get element's weight. @@ -376,12 +382,44 @@ class IfcOpenShell(QtoCalculator): or ``None`` if mass density calculation for this element is not supported. """ + if calculation_type == "gross" or not ifcopenshell.util.element.has_openings(element): + weight = cls.get_weight_profile_based(element) + if weight is not None: + return weight + density = ifcopenshell.util.element.get_element_mass_density(element) if density is None: return volume = ifcopenshell.util.shape.get_volume(geometry) return volume * density + @classmethod + def get_weight_profile_based(cls, element: ifcopenshell.entity_instance) -> Union[float, None]: + """Get weight of the profile based element. + + :return: A float weight value if calculation was successful + or ``None`` if it's either not profile based object + or it's not supported. + """ + representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") + if not representation: + return None + items = representation.Items + if not all(item.is_a("IfcExtrudedAreaSolid") for item in items): + return None + mass = 0.0 + for item in items: + profile = item.SweptArea + # TODO: there are also bunch of other similar props we will need to consider in the future. + # Examples: + # - Pset_CableSegmentTypeBusBarSegment.MassPerLength + # - Pset_CableCarrierSegmentTypeCatenaryWire.MassPerLength + mass_per_length = ifcopenshell.util.element.get_pset(profile, "Pset_ProfileMechanical", "MassPerLength") + if not isinstance(mass_per_length, float): + return None + mass += mass_per_length * item.Depth + return mass + class Blender(QtoCalculator): """Calculates geometry based on currently loaded Blender objects.""" diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 85cc192ceb..4f21aa8a98 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -20,7 +20,7 @@ import ifcopenshell import ifcopenshell.guid import ifcopenshell.util.element import ifcopenshell.util.representation -from typing import Any, Callable, Optional, Union, Literal, overload, Sequence +from typing import Any, Callable, Optional, Union, Literal, overload, Sequence, Generator from collections import namedtuple @@ -1720,3 +1720,27 @@ def has_property(product: ifcopenshell.entity_instance, property_name: str) -> b return True qtos = get_psets(product, qtos_only=True) return any(property_name in quantities.keys() for quantities in qtos.values()) + + +def get_openings(element: ifcopenshell.entity_instance) -> Generator[ifcopenshell.entity_instance, None, None]: + """Get element openings as IfcRelVoidsElements. + + Use `.RelatedOpeningElement` to get the opening element. + + :param element: IfcElement. + :return: Generator of IfcRelVoidsElements. + """ + for element_rel in getattr(element, "HasOpenings", ()): + yield element_rel + + if aggregate := get_aggregate(element): + yield from get_openings(aggregate) + + +def has_openings(element: ifcopenshell.entity_instance) -> bool: + """Check if the element has openings. + + :param element: IfcElement. + :return: True if element has openings. + """ + return bool(next(get_openings(element), False)) From 6cf5cc002e50ab13dd0e700be25934c7dffb9b10 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 18 Mar 2025 01:06:44 +1100 Subject: [PATCH 409/476] Fix #6318. Fix warnings when decorator cannot convert 3D to 2D screen space location. --- .../bonsai/bim/module/drawing/decoration.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/decoration.py b/src/bonsai/bonsai/bim/module/drawing/decoration.py index 2077f25f62..19480b6a0d 100644 --- a/src/bonsai/bonsai/bim/module/drawing/decoration.py +++ b/src/bonsai/bonsai/bim/module/drawing/decoration.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . -import os import gpu import bpy import blf @@ -29,7 +28,7 @@ import ifcopenshell.util.unit import bonsai.tool as tool import bonsai.bim.module.drawing.helper as helper from pathlib import Path -from math import pi, sin, cos, tan, acos, atan, degrees, radians, ceil +from math import pi, sin, cos, acos, atan, degrees, radians from bpy.types import SpaceView3D from mathutils import Vector, Matrix from bpy_extras.view3d_utils import location_3d_to_region_2d @@ -595,7 +594,8 @@ class BaseDecorator: region3d = context.region_data text_dir = self.get_annotation_direction(context, obj) - pos = location_3d_to_region_2d(region, region3d, text_world_position) + if not (pos := location_3d_to_region_2d(region, region3d, text_world_position)): + return props = tool.Drawing.get_text_props(obj) text_data = DecoratorData.get_ifc_text_data(obj) if props.is_editing: @@ -728,6 +728,8 @@ class DimensionDecorator(BaseDecorator): v1 = Vector(vertices[i1]) p0 = location_3d_to_region_2d(region, region3d, v0) p1 = location_3d_to_region_2d(region, region3d, v1) + if not p0 or not p1: + return text_dir = p1 - p0 if text_dir.length < 1: continue @@ -878,6 +880,8 @@ class AngleDecorator(BaseDecorator): # calculate angle position p0, p1, p2 = [location_3d_to_region_2d(region, region3d, p) for p in vertices[i0 : i1 + 2]] + if not p0 or not p1 or not p2: + continue edge0 = p0 - p1 edge1 = p2 - p1 radius = min(edge0.length_squared, edge1.length_squared) ** 0.5 @@ -942,6 +946,8 @@ class RadiusDecorator(BaseDecorator): spline_points = self.get_spline_points(obj) p0, p1 = [location_3d_to_region_2d(region, region3d, p) for p in self.get_spline_points(obj)] + if not p0 or not p1: + return element = tool.Ifc.get_entity(obj) description = element.Description dimension_data = DecoratorData.get_dimension_data(obj) @@ -974,7 +980,8 @@ class FallDecorator(BaseDecorator): region = context.region region3d = context.region_data dir = Vector((1, 0)) - pos = location_3d_to_region_2d(region, region3d, self.get_spline_end(obj)) + if not (pos := location_3d_to_region_2d(region, region3d, self.get_spline_end(obj))): + return spline = obj.data.splines[0] spline_points = spline.bezier_points if spline.bezier_points else spline.points @@ -1165,7 +1172,6 @@ class PlanLevelDecorator(BaseDecorator): self.draw_labels(context, obj, self.get_splines(obj)) def draw_labels(self, context, obj, splines): - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) region = context.region region3d = context.region_data @@ -1175,6 +1181,8 @@ class PlanLevelDecorator(BaseDecorator): for verts in splines: p0, p1 = [location_3d_to_region_2d(region, region3d, v) for v in verts[:2]] + if not p0 or not p1: + continue text_dir = p1 - p0 if text_dir.length < 1: continue @@ -1441,8 +1449,10 @@ class GridDecorator(BaseDecorator): p1 = location_3d_to_region_2d(region, region3d, v1) dir = Vector((1, 0)) text = obj.name.split("/")[1].split(".")[0] - self.draw_label(context, text, p0, dir, vcenter=True, gap=0) - self.draw_label(context, text, p1, dir, vcenter=True, gap=0) + if p0: + self.draw_label(context, text, p0, dir, vcenter=True, gap=0) + if p1: + self.draw_label(context, text, p1, dir, vcenter=True, gap=0) class ElevationDecorator(BaseDecorator): From c176b9d607cac401a93b3b02880041ada0e945d8 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 18 Mar 2025 01:14:06 +1100 Subject: [PATCH 410/476] See #1227. Fix bug where unjoin walls didn't recheck the position. --- src/bonsai/bonsai/core/model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/core/model.py b/src/bonsai/bonsai/core/model.py index 897b1af632..1621585065 100644 --- a/src/bonsai/bonsai/core/model.py +++ b/src/bonsai/bonsai/core/model.py @@ -25,6 +25,8 @@ def unjoin_walls(ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, if not (element := ifc.get_entity(obj)) or model.get_usage_type(element) != "LAYER2": continue geometry.clear_scale(obj) + if ifc.is_moved(obj): + geometry.run_edit_object_placement(obj=obj) joiner.unjoin(obj) From 09a43f13395ff23b237b0429a21e28fa6b190fda Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:12:24 -0700 Subject: [PATCH 411/476] get distance along alignment from station New alignment api function to get the horizontal distance along an alignment from a station value --- .../ifcopenshell/api/alignment/__init__.py | 1 + .../alignment/distance_along_from_station.py | 56 +++++++++++++++++++ .../test_distance_along_from_station.py | 55 ++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 src/ifcopenshell-python/ifcopenshell/api/alignment/distance_along_from_station.py create mode 100644 src/ifcopenshell-python/test/api/alignment/test_distance_along_from_station.py diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py index 1098c23fe6..e32853f601 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py @@ -51,6 +51,7 @@ from .create_alignment_from_csv import create_alignment_from_csv from .create_horizontal_alignment_by_pi_method import create_horizontal_alignment_by_pi_method from .create_geometric_representation import create_geometric_representation from .create_vertical_alignment_by_pi_method import create_vertical_alignment_by_pi_method +from .distance_along_from_station import distance_along_from_station from .get_alignment_layouts import get_alignment_layouts from .get_axis_subcontext import get_axis_subcontext from .get_basis_curve import get_basis_curve diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/distance_along_from_station.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/distance_along_from_station.py new file mode 100644 index 0000000000..8bfbb8f03c --- /dev/null +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/distance_along_from_station.py @@ -0,0 +1,56 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import ifcopenshell +import ifcopenshell.api.alignment +import ifcopenshell.api.nest +import ifcopenshell.guid +from ifcopenshell import entity_instance + + +def distance_along_from_station(file: ifcopenshell.file, alignment: entity_instance, station: float) -> float: + """ + Given a station, returns the distance along the horizontal alignment. + + If the alignment does not have stationing defined with an IfcReferent, the start of the alignment is assumed + to be at station 0.0. That is, the station is the distance along. + + .. note:: The current implementation does not account for station equations and assumes stationing is increasing along the alignment. + + :param alignment: the alignment + :param station: station value + :return: distance along the horizontal alignment + + Example: + + .. code:: python + + alignment = model.by_type("IfcAlignment")[0] # alignment with start station 1+00.00 + dist_along = ifcopenshell.api.alignment.distance_along_from_station(model,alignment=alignment,station=200.0) + print(dist_along) # 100.00 + """ + + start_station = 0.0 + components = ifcopenshell.util.element.get_components(alignment) + for c in components: + if c.is_a("IfcReferent") and ifcopenshell.util.element.get_predefined_type(c) == "STATION": + start_station = ifcopenshell.util.element.get_pset(c, name="Pset_Stationing", prop="Station") + break + + dist_along = station - start_station + return dist_along diff --git a/src/ifcopenshell-python/test/api/alignment/test_distance_along_from_station.py b/src/ifcopenshell-python/test/api/alignment/test_distance_along_from_station.py new file mode 100644 index 0000000000..fa42765c56 --- /dev/null +++ b/src/ifcopenshell-python/test/api/alignment/test_distance_along_from_station.py @@ -0,0 +1,55 @@ +# IfcOpenShell - IFC toolkit and geometry engine +# Copyright (C) 2025 Thomas Krijnen +# +# 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 . + +import pytest +import ifcopenshell.api.alignment +import ifcopenshell.api.context + + +def test_add_stationing_to_alignment(): + file = ifcopenshell.file(schema="IFC4X3_ADD2") + project = file.createIfcProject(Name="Test") + geometric_representation_context = ifcopenshell.api.context.add_context(file, context_type="Model") + axis_model_representation_subcontext = ifcopenshell.api.context.add_context( + file, + context_type="Model", + context_identifier="Axis", + target_view="MODEL_VIEW", + parent=geometric_representation_context, + ) + + coordinates = [(500.0, 2500.0), (3340.0, 660.0), (4340.0, 5000.0), (7600.0, 4560.0), (8480.0, 2010.0)] + radii = [(1000.0), (1250.0), (950.0)] + vpoints = [(0.0, 100.0), (2000.0, 135.0), (5000.0, 105.0), (7400.0, 153.0), (9800.0, 105.0), (12800.0, 90.0)] + lengths = [(1600.0), (1200.0), (2000.0), (800.0)] + + alignment = ifcopenshell.api.alignment.create_alignment_by_pi_method( + file, "TestAlignment", coordinates, radii, vpoints, lengths + ) + + # test alignment without stationing referent + assert ifcopenshell.api.alignment.distance_along_from_station(file, alignment, 500.0) == pytest.approx(500.0) + + # add stationing referent + ifcopenshell.api.alignment.add_stationing_to_alignment(file, alignment, 10000.0) + + # Station 138+83.96 + assert ifcopenshell.api.alignment.distance_along_from_station(file, alignment, 13883.96) == pytest.approx(3883.96) + + # Station 175+25.36 + assert ifcopenshell.api.alignment.distance_along_from_station(file, alignment, 17525.36) == pytest.approx(7525.36) From ad03a0c8d9e5cb12d7eca89c345b2e38661102d2 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 18 Mar 2025 12:48:48 +0500 Subject: [PATCH 412/476] debugging docs - add section about debugging addon --- .../docs/guides/development/debugging.rst | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/bonsai/docs/guides/development/debugging.rst b/src/bonsai/docs/guides/development/debugging.rst index 4cecc46fef..a74331e1ec 100644 --- a/src/bonsai/docs/guides/development/debugging.rst +++ b/src/bonsai/docs/guides/development/debugging.rst @@ -5,6 +5,13 @@ This is a mini-guide to setting up an IDE and configuring it to debug Bonsai more easily. It is currently specific to this writers own system (Ubuntu) but this can be expanded by others. +.. warning:: + Attaching debugger to Blender may significantly decrease performance due to + the debugger overhead. + +VSCode Extension +-------------- + 1. **Install VSCode/VSCodium**: This will be system specific. I used the available snap package. @@ -125,4 +132,20 @@ this can be expanded by others. If you get to this point, congratulations! You will now be 1000% more effective when troubleshooting issues, and able to make many more contributions, fixes -and patches. \ No newline at end of file +and patches. + +Blender Addon + VS Code Debugger +------------------------------ + +Setting up debugging with Blender Addon is a bit simpler as it doesn't require Blender to be started in a special way +and debugger can be always attached later when it's needed. + +1. Install Hextant Python Debugger Blender addon from `official repository `_. + +2. Open IfcOpenShell repository in VS Code and setup configuration for attaching debugger `per the instructions `_. + +3. Start Debug Server in Blender (Blender -> System -> Start Debug Server) + +4. VS Code -> Run and Debug -> "Python Debugger: Remote Attach" in dropdown -> Start Debugging + +5. Now debugger is attached to Blender. You can set breakpoints in VS Code and use Debug Console. From 8def5ec30b69bb64376a91c361727bb7b9c08bce Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Mar 2025 12:27:53 +0500 Subject: [PATCH 413/476] Use explicit `bl_ui_utils.layout` import to fix #6389 Also related - https://github.com/nutti/fake-bpy-module/issues/352 --- src/bonsai/bonsai/bim/module/model/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/ui.py b/src/bonsai/bonsai/bim/module/model/ui.py index 9154e0dff6..dec2f1f91a 100644 --- a/src/bonsai/bonsai/bim/module/model/ui.py +++ b/src/bonsai/bonsai/bim/module/model/ui.py @@ -17,7 +17,7 @@ # along with Bonsai. If not, see . import bpy -import bl_ui_utils +import bl_ui_utils.layout import bonsai.bim import bonsai.tool as tool from bpy.types import Panel, Menu From d868d8ceac8f058a63c76fee1b884eb7c359e791 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Mar 2025 13:55:40 +0500 Subject: [PATCH 414/476] Fix missing default value for `last_commit_date` (37ec2bf) #6389 Which is needed when Bonsai is not using git --- src/bonsai/bonsai/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bonsai/bonsai/__init__.py b/src/bonsai/bonsai/__init__.py index 2b0fbfc25b..11cb6208d4 100644 --- a/src/bonsai/bonsai/__init__.py +++ b/src/bonsai/bonsai/__init__.py @@ -40,6 +40,7 @@ from typing import Union, Any, Generator last_commit_hash = "8888888" +last_commit_date = None def get_last_commit_hash() -> Union[str, None]: From d912012f76ebaeafb5f48080be86e1447587dc4d Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Mar 2025 13:59:55 +0500 Subject: [PATCH 415/476] Bonsai makefile - fill last_commit_date Also .isoformat(), so string would match exactly with the one git command provides. --- src/bonsai/Makefile | 2 ++ src/bonsai/bonsai/__init__.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile index 90867ff0ec..8933f14290 100644 --- a/src/bonsai/Makefile +++ b/src/bonsai/Makefile @@ -46,6 +46,7 @@ VERSION_MINOR:=$(shell cat '../../VERSION' | cut -d '.' -f 2) VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3) VERSION_DATE:=$(shell date '+%y%m%d') LAST_COMMIT_HASH:=$(shell git rev-parse HEAD) +LAST_COMMIT_DATE:=$(shell git show -s --format=%cI) PYVERSION:=py310 PYPI_IMP:=cp @@ -254,6 +255,7 @@ ifeq ($(IS_STABLE), TRUE) else $(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml $(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py + $(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py $(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml endif diff --git a/src/bonsai/bonsai/__init__.py b/src/bonsai/bonsai/__init__.py index 11cb6208d4..6ea48885b3 100644 --- a/src/bonsai/bonsai/__init__.py +++ b/src/bonsai/bonsai/__init__.py @@ -40,7 +40,7 @@ from typing import Union, Any, Generator last_commit_hash = "8888888" -last_commit_date = None +last_commit_date = "9999999" def get_last_commit_hash() -> Union[str, None]: @@ -52,6 +52,12 @@ def get_last_commit_hash() -> Union[str, None]: return last_commit_hash[:7] +def get_last_commit_date() -> Union[str, None]: + if last_commit_date == str(9_999999): + return None + return last_commit_date + + # Accessed from bonsai extension: bbim_semver: dict[str, Any] = {} @@ -97,7 +103,7 @@ def get_debug_info(): "blender_version": bpy.app.version_string, "bonsai_version": bbim_version, "bonsai_commit_hash": get_last_commit_hash(), - "bonsai_commit_date": last_commit_date, + "bonsai_commit_date": get_last_commit_date(), "last_actions": last_actions, "last_error": last_error, } @@ -220,7 +226,7 @@ if IN_BLENDER: path = Path(__file__).resolve().parent repo = git.Repo(str(path), search_parent_directories=True) last_commit_hash = repo.head.object.hexsha - last_commit_date = repo.head.object.committed_datetime.strftime("%Y-%m-%d %H:%M:%S") + last_commit_date = repo.head.object.committed_datetime.isoformat() except: pass From 26b55216ca64032c7cc3726885ea714fe1ad2fde Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 19 Mar 2025 11:40:54 +0100 Subject: [PATCH 416/476] Normalize model-rotation quaternion --- src/ifcgeom/mapping/mapping.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcgeom/mapping/mapping.cpp b/src/ifcgeom/mapping/mapping.cpp index 666ff43dc2..04f8db4589 100644 --- a/src/ifcgeom/mapping/mapping.cpp +++ b/src/ifcgeom/mapping/mapping.cpp @@ -874,7 +874,7 @@ void mapping::initialize_units_() { if (settings_.get().has()) { auto vs = settings_.get().get(); if (vs.size() == 4) { - auto m3 = Eigen::Quaterniond(vs[0], vs[1], vs[2], vs[3]).matrix(); + auto m3 = Eigen::Quaterniond(vs[0], vs[1], vs[2], vs[3]).normalized().matrix(); Eigen::Matrix4d m4 = Eigen::Matrix4d::Identity(); m4 << m3; offset_and_rotation_ *= m4; From e658b00768f23e990b2b2d39454c7757547db399 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 19 Mar 2025 11:42:11 +0100 Subject: [PATCH 417/476] Reinstate old bespoke model-offset/rotation parsing in IfcConvert #6290 --- src/ifcconvert/IfcConvert.cpp | 46 +++++++++++++++++++++++++++++++- src/ifcgeom/ConversionSettings.h | 15 ++++++----- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp index 52bb79128b..bd0c9110a2 100644 --- a/src/ifcconvert/IfcConvert.cpp +++ b/src/ifcconvert/IfcConvert.cpp @@ -298,6 +298,10 @@ int main(int argc, char** argv) { "Can take several minutes on large models.") ("center-model-geometry", "Centers the elements by applying the center point of all mesh vertices as an offset.") + ("model-offset", po::value(&offset_str), + "Applies an arbitrary offset of form 'x;y;z' to all placements.") + ("model-rotation", po::value(&rotation_str), + "Applies an arbitrary quaternion rotation of form 'x;y;z;w' to all placements.") ("include", po::value(&include_filter)->multitoken(), "Specifies that the instances that match a specific filtering criteria are to be included in the geometrical output:\n" "1) 'entities': the following list of types should be included. SVG output defaults " @@ -448,6 +452,8 @@ int main(int argc, char** argv) { const bool center_model = vmap.count("center-model") != 0; const bool center_model_geometry = vmap.count("center-model-geometry") != 0; + const bool model_offset = vmap.count("model-offset") != 0; + const bool model_rotation = vmap.count("model-rotation") != 0; if (!quiet || vmap.count("version")) { print_version(); @@ -924,6 +930,44 @@ int main(int argc, char** argv) { } else { Logger::SetOutput(quiet ? nullptr : &cout_, vcounter.count > 1 ? &cout_ : &log_stream); } + + if (model_rotation) { + std::vector rotation(4); + int n = 0; + if (sscanf(rotation_str.c_str(), "%lf;%lf;%lf;%lf %n", &rotation[0], &rotation[1], &rotation[2], &rotation[3], &n) != 4 || n != rotation_str.size()) { + cerr_ << "[Error] Invalid use of --model-rotation\n"; + IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename)); + print_options(serializer_options); + return EXIT_FAILURE; + } + + std::stringstream msg; + msg << "Using model rotation (" << rotation[0] << "," << rotation[1] << "," << rotation[2] << "," << rotation[3] << ")"; + Logger::Notice(msg.str()); + + geometry_settings.get().value = rotation; + } + + if (model_offset && (center_model || center_model_geometry)) { + Logger::Notice("--model-offset ignored with --center-model or --center-model-geometry"); + } + + if (model_offset && !(center_model || center_model_geometry)) { + std::vector offset(3); + int n = 0; + if (sscanf(offset_str.c_str(), "%lf;%lf;%lf %n", &offset[0], &offset[1], &offset[2], &n) != 3 || n != offset_str.size()) { + cerr_ << "[Error] Invalid use of --model-offset\n"; + IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename)); + print_options(serializer_options); + return EXIT_FAILURE; + } + + std::stringstream msg; + msg << std::setprecision(std::numeric_limits::max_digits10) << "Using model offset (" << offset[0] << "," << offset[1] << "," << offset[2] << ")"; + Logger::Notice(msg.str()); + + geometry_settings.get().value = offset; + } if (is_tesselated && (center_model || center_model_geometry)) { std::vector offset(3); @@ -957,7 +1001,7 @@ int main(int argc, char** argv) { offset[2] = -center(2); std::stringstream msg; - msg << std::setprecision (std::numeric_limits< double >::max_digits10) << "Using model offset (" << offset[0] << "," << offset[1] << "," << offset[2] << ")"; + msg << std::setprecision (std::numeric_limits::max_digits10) << "Using model offset (" << offset[0] << "," << offset[1] << "," << offset[2] << ")"; Logger::Notice(msg.str()); geometry_settings.get().value = offset; diff --git a/src/ifcgeom/ConversionSettings.h b/src/ifcgeom/ConversionSettings.h index 8902a8de26..d509cc27fc 100644 --- a/src/ifcgeom/ConversionSettings.h +++ b/src/ifcgeom/ConversionSettings.h @@ -47,7 +47,9 @@ namespace ifcopenshell { // boost program options does not seem to handle optional types, so in case // of vector settings we need to strip away the optional and detect argument presence // with !vector::empty() - std::conditional_t>, T, boost::optional> value; + // tfk: we no longer do this because negative values can not be passed like this as boost confuses them with options + // std::conditional_t>, T, boost::optional> value; + boost::optional value; SettingBase() {} @@ -64,14 +66,15 @@ namespace ifcopenshell { value.emplace(); desc.add_options()(Derived::name, apply_default(po::bool_switch(&*value)), Derived::description); } else if constexpr (std::is_same_v>) { - desc.add_options()(Derived::name, apply_default(po::value(&value)->multitoken()), Derived::description); + // these options have to be supplied manually in IfcConvert.cpp + // desc.add_options()(Derived::name, apply_default(po::value(&value)->multitoken()), Derived::description); } else { desc.add_options()(Derived::name, apply_default(po::value(&value)), Derived::description); } } T get() const { - if constexpr (std::is_same_v>) { + if constexpr (false && std::is_same_v>) { return value; } else { if (value) { @@ -85,7 +88,7 @@ namespace ifcopenshell { } bool has() const { - if constexpr (std::is_same_v>) { + if constexpr (false && std::is_same_v>) { return !value.empty(); } else { // @todo this is not reliable, better use vmap[...].defaulted() @@ -391,12 +394,12 @@ namespace ifcopenshell { struct ModelOffset : public SettingBase> { static constexpr const char* const name = "model-offset"; - static constexpr const char* const description = "Applies an arbitrary offset of form 'x,y,z' to all placements."; + static constexpr const char* const description = "Applies an arbitrary offset of form x,y,z to all placements."; }; struct ModelRotation : public SettingBase> { static constexpr const char* const name = "model-rotation"; - static constexpr const char* const description = "Applies an arbitrary quaternion rotation of form 'x,y,z,w' to all placements."; + static constexpr const char* const description = "Applies an arbitrary quaternion rotation of form x,y,z,w to all placements."; }; enum TriangulationMethod { From fa3c57b01660ae6a39dd1467607c8bee98f230da Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Wed, 19 Mar 2025 11:42:43 +0100 Subject: [PATCH 418/476] Implement readable setting type retrieval --- src/ifcgeom/ConversionSettings.h | 89 ++++++++++++++++++++++++++++++-- src/ifcwrap/IfcGeomWrapper.i | 6 +++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/ifcgeom/ConversionSettings.h b/src/ifcgeom/ConversionSettings.h index d509cc27fc..a82be36b05 100644 --- a/src/ifcgeom/ConversionSettings.h +++ b/src/ifcgeom/ConversionSettings.h @@ -422,6 +422,68 @@ namespace ifcopenshell { static constexpr bool defaultvalue = false; }; } + + namespace impl { + template + struct readable_name { + static constexpr const char* name = "Unknown Type"; + }; + + template <> + struct readable_name { + static constexpr const char* name = "bool"; + }; + + template <> + struct readable_name { + static constexpr const char* name = "int"; + }; + + template <> + struct readable_name { + static constexpr const char* name = "double"; + }; + + template <> + struct readable_name { + static constexpr const char* name = "std::string"; + }; + + template <> + struct readable_name> { + static constexpr const char* name = "std::set"; + }; + + template <> + struct readable_name> { + static constexpr const char* name = "std::set"; + }; + + template <> + struct readable_name> { + static constexpr const char* name = "std::vector"; + }; + + template <> + struct readable_name { + static constexpr const char* name = "IteratorOutputOptions"; + }; + + template <> + struct readable_name { + static constexpr const char* name = "FunctionStepMethod"; + }; + + template <> + struct readable_name { + static constexpr const char* name = "OutputDimensionalityTypes"; + }; + + template <> + struct readable_name { + static constexpr const char* name = "TriangulationMethod"; + }; + } template class IFC_GEOM_API SettingsContainer { @@ -450,17 +512,34 @@ namespace ifcopenshell { } } + template + std::string get_type_(const std::string& name) const { + if (std::tuple_element_t::name == name) { + return impl::readable_name::base_type>::name; + } + if constexpr (Index + 1 < std::tuple_size_v) { + return get_type_(name); + } else { + throw std::runtime_error("Setting not available"); + } + } + template void set_option_(const std::string& name, const value_variant_t& val) { if (std::tuple_element_t::name == name) { if constexpr (std::is_enum_v::base_type>) { - if (val.which() == 1) { - auto val_as_enum = (typename std::tuple_element_t::base_type) boost::get(val); + if (auto* val_ptr = boost::get(&val)) { + auto val_as_enum = (typename std::tuple_element_t::base_type) *val_ptr; std::get(settings).value = val_as_enum; return; } } - std::get(settings).value = boost::get::base_type>(val); + try { + std::get(settings).value = boost::get::base_type>(val); + } catch (const boost::bad_get&) { + std::string ty = impl::readable_name::base_type>::name; + throw std::runtime_error("Expected a value of type <" + ty + "> for setting '" + name + "'"); + } } else if constexpr (Index + 1 < std::tuple_size_v) { set_option_(name, val); } else { @@ -505,6 +584,10 @@ namespace ifcopenshell { set_option_<0>(name, val); } + std::string get_type(const std::string& name) { + return get_type_<0>(name); + } + std::vector setting_names() const { std::vector r; get_setting_names_<0>(r); diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index c0dd62c04f..35168dc969 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -394,6 +394,9 @@ assign_matrix_access(revolve); std::vector setting_names() { return $self->setting_names(); } + std::string get_type(const std::string& name) { + return $self->get_type(name); + } } %extend ifcopenshell::geometry::SerializerSettings { @@ -418,6 +421,9 @@ assign_matrix_access(revolve); std::vector setting_names() { return $self->setting_names(); } + std::string get_type(const std::string& name) { + return $self->get_type(name); + } } #ifdef IFOPSH_WITH_OPENCASCADE From 3c777a0b85176f3975e35874e506559a702b0a44 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Mar 2025 16:35:36 +0500 Subject: [PATCH 419/476] Blender 4.4 - fix broken UI lists #6396 Example error: ```python RuntimeError: could not create instance of BIM_UL_containers_manager to call callback function 'filter_items' 2025-03-19:16:13:14,001 ERROR [log.py:69] Uncaught exception RuntimeError: could not create instance of BIM_UL_containers_manager to call callback function 'draw_item' ``` --- src/bonsai/bonsai/bim/module/cost/ui.py | 10 ++++------ src/bonsai/bonsai/bim/module/spatial/ui.py | 6 ++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/cost/ui.py b/src/bonsai/bonsai/bim/module/cost/ui.py index 387998e9ac..e8df647071 100644 --- a/src/bonsai/bonsai/bim/module/cost/ui.py +++ b/src/bonsai/bonsai/bim/module/cost/ui.py @@ -708,18 +708,16 @@ class BIM_UL_cost_items_trait: class BIM_UL_cost_items(BIM_UL_cost_items_trait, UIList): - def __init__(self): - self.contract_operator = "bim.contract_cost_item" - self.expand_operator = "bim.expand_cost_item" + contract_operator = "bim.contract_cost_item" + expand_operator = "bim.expand_cost_item" class BIM_UL_cost_item_rates(BIM_UL_cost_items_trait, UIList): # A schedule of rates UIList is identical to a regular cost items UIList but # we want a separate UIList instance so that you can browse both lists # independently in Blender. So we use a trait. - def __init__(self): - self.contract_operator = "bim.contract_cost_item_rate" - self.expand_operator = "bim.expand_cost_item_rate" + contract_operator = "bim.contract_cost_item_rate" + expand_operator = "bim.expand_cost_item_rate" def draw_quantity_column(self, layout, cost_item): self.draw_uom_column(layout, cost_item) diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py index da20a63d7c..42b5a3cadc 100644 --- a/src/bonsai/bonsai/bim/module/spatial/ui.py +++ b/src/bonsai/bonsai/bim/module/spatial/ui.py @@ -264,7 +264,8 @@ class BIM_UL_containers_manager(UIList): "IfcRoadPart": "MOD_FLUID", } - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.use_filter_show = True def draw_item( @@ -338,7 +339,8 @@ class BIM_UL_containers_manager(UIList): class BIM_UL_elements(UIList): - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.use_filter_show = True def draw_toggle(self, row: bpy.types.UILayout, is_expanded: bool, index: int): From 53fd1284b53f4dca85b00d8536b5b54e1e2a30fa Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 20 Mar 2025 10:42:27 +0100 Subject: [PATCH 420/476] Update wheels #6402 --- src/pyodide/demo-app/index.html | 2 +- src/pyodide/demo-app/wheels | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyodide/demo-app/index.html b/src/pyodide/demo-app/index.html index 555b83bb99..781e3c5872 100644 --- a/src/pyodide/demo-app/index.html +++ b/src/pyodide/demo-app/index.html @@ -76,7 +76,7 @@ const micropip = pyodide.pyimport("micropip"); await micropip.install("typing-extensions"); document.querySelector("#status2").innerHTML = "Loading IfcOpenShell"; - await micropip.install("wheels/ifcopenshell-0.8.1+latest-cp312-cp312-emscripten_3_1_58_wasm32.whl"); + await micropip.install("wheels/ifcopenshell-0.8.2+d50e806-cp312-cp312-emscripten_3_1_58_wasm32.whl"); document.body.className = ''; diff --git a/src/pyodide/demo-app/wheels b/src/pyodide/demo-app/wheels index 33b437e5fd..6b5bfb4bdc 160000 --- a/src/pyodide/demo-app/wheels +++ b/src/pyodide/demo-app/wheels @@ -1 +1 @@ -Subproject commit 33b437e5fd5425e606f34aff602c42034ff5e6dc +Subproject commit 6b5bfb4bdc364f859643a624bd69bf9f471d45c0 From 2bc230919e83fd4e59175db2464efb8e7424aac3 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 20 Mar 2025 10:44:08 +0100 Subject: [PATCH 421/476] Update publish-pyodide-demo-app.yml --- .github/workflows/publish-pyodide-demo-app.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish-pyodide-demo-app.yml b/.github/workflows/publish-pyodide-demo-app.yml index 59fcf5c9f5..0c453528a6 100644 --- a/.github/workflows/publish-pyodide-demo-app.yml +++ b/.github/workflows/publish-pyodide-demo-app.yml @@ -9,6 +9,8 @@ on: paths: - 'src/pyodide/**' - '.github/workflows/publish-pyodide-demo-app.yml' + branches: + - v0.8.0 jobs: activate: From 8bdf8369b50b937558c5ae2c23eb2bde0a2df414 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 20 Mar 2025 10:49:31 +0100 Subject: [PATCH 422/476] pyodide demo: install shapely --- src/pyodide/demo-app/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyodide/demo-app/index.html b/src/pyodide/demo-app/index.html index 781e3c5872..63ec0f0e65 100644 --- a/src/pyodide/demo-app/index.html +++ b/src/pyodide/demo-app/index.html @@ -73,6 +73,7 @@ document.querySelector("#status2").innerHTML = "Loading dependencies"; await pyodide.loadPackage("micropip"); await pyodide.loadPackage("numpy"); + await pyodide.loadPackage("shapely"); const micropip = pyodide.pyimport("micropip"); await micropip.install("typing-extensions"); document.querySelector("#status2").innerHTML = "Loading IfcOpenShell"; From 2a2bfea87066b04cb7a6a203752627192f4a71b4 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 20 Mar 2025 11:47:14 +0100 Subject: [PATCH 423/476] Read settings from argparser --- .../ifcopenshell/geom/main.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index 508aa449e4..27e231cf37 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -225,6 +225,55 @@ class settings_mixin: else: raise AttributeError("'Settings' object has no attribute '%s'" % k) + def build_parser(self, parser) -> None: + """ + Accepts an argparse.ArgumentParser object, enumerates the settings in this container and + adds argument parser rules for each. + """ + type_factories = { + "bool": bool, + "int": int, + "double": float, + "std::string": str, + "std::set": lambda s: list(map(int, s.split(";"))), + "std::set": lambda s: s.split(";"), + "std::vector": lambda s: list(map(float, s.split(";"))), + "IteratorOutputOptions": int, + "FunctionStepMethod": int, + "OutputDimensionalityTypes": int, + "TriangulationMethod": int, + } + for nm in self.setting_names(): + if nm == "use-python-opencascade": + ty == "bool" + else: + ty = self.get_type(nm) + if ty == "bool": + group = parser.add_mutually_exclusive_group() + group.add_argument( + f"--{nm}", + dest=nm, + action="store_true", + ) + group.add_argument( + f"--no-{nm}", + dest=nm, + action="store_false", + ) + parser.set_defaults(**{nm: None}) + else: + parser.add_argument(f"--{nm}", dest=nm, type=type_factories[ty]) + + def apply_namespace(self, namespace) -> None: + """ + Accepts an argparse.Namespace object, enumerates over the values in this namespace and + writes them to the settings when available + """ + names = set(self.setting_names()) + for k, v in namespace._get_kwargs(): + if k in names and v is not None: + self.set(k, v) + class serializer_settings(settings_mixin, ifcopenshell_wrapper.SerializerSettings): pass From f8f36cf2f0745efa51177d8dcd9141f4c396f197 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Mar 2025 14:21:15 +0500 Subject: [PATCH 424/476] typing --- src/bonsai/bonsai/tool/cad.py | 6 ++++-- src/ifcsverchok/helper.py | 11 +++++++++-- src/ifcsverchok/nodes/ifc/create_entity.py | 1 + src/ifcsverchok/nodes/ifc/create_file.py | 3 ++- src/ifcsverchok/nodes/ifc/create_project.py | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/tool/cad.py b/src/bonsai/bonsai/tool/cad.py index 8b2b34dae2..62c2b854ab 100644 --- a/src/bonsai/bonsai/tool/cad.py +++ b/src/bonsai/bonsai/tool/cad.py @@ -36,7 +36,7 @@ import bmesh import mathutils.geometry from mathutils import Vector, Matrix, geometry import itertools -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union if TYPE_CHECKING: from bonsai.bim.module.cad.prop import BIMCadProperties @@ -170,7 +170,9 @@ class Cad: return geometry.intersect_line_plane(v1, v2, plane_co, plane_no) @classmethod - def intersect_edges(cls, edge1, edge2): + def intersect_edges( + cls, edge1: tuple[Vector, Vector], edge2: tuple[Vector, Vector] + ) -> Union[tuple[Vector, Vector], None]: """ > takes 2 tuples, each tuple contains 2 vectors - prepares input for sending to intersect_line_line diff --git a/src/ifcsverchok/helper.py b/src/ifcsverchok/helper.py index edcd723836..61b2b5de45 100644 --- a/src/ifcsverchok/helper.py +++ b/src/ifcsverchok/helper.py @@ -17,13 +17,17 @@ # along with IfcSverchok. If not, see . import bpy +import ifcopenshell from sverchok.data_structure import zip_long_repeat +from typing import Any -ifc_files = {} +ifc_files: dict[str, ifcopenshell.file] = {} class SvIfcCore: - def process(self): + sv_input_names: list[str] + + def process(self) -> None: sv_inputs_nested = [] for name in self.sv_input_names: sv_inputs_nested.append(self.inputs[name].sv_get()) @@ -31,3 +35,6 @@ class SvIfcCore: for sv_input in zip_long_repeat(*sv_input_nested): sv_input = list(sv_input) self.process_ifc(*sv_input) + + def process_ifc(self, *args: Any, **kwargs: Any) -> None: + raise NotImplementedError diff --git a/src/ifcsverchok/nodes/ifc/create_entity.py b/src/ifcsverchok/nodes/ifc/create_entity.py index f17b02a3c0..463154486c 100644 --- a/src/ifcsverchok/nodes/ifc/create_entity.py +++ b/src/ifcsverchok/nodes/ifc/create_entity.py @@ -19,6 +19,7 @@ import bpy from mathutils import Matrix import ifcopenshell +import ifcopenshell.api import ifcsverchok.helper from ifcsverchok.ifcstore import SvIfcStore diff --git a/src/ifcsverchok/nodes/ifc/create_file.py b/src/ifcsverchok/nodes/ifc/create_file.py index f57c9825ed..85794206bf 100644 --- a/src/ifcsverchok/nodes/ifc/create_file.py +++ b/src/ifcsverchok/nodes/ifc/create_file.py @@ -34,6 +34,7 @@ class SvIfcCreateFileRefresh(bpy.types.Operator): has_baked: bpy.props.BoolProperty(name="Has Baked", default=False) def execute(self, context): + node: SvIfcCreateFile node = bpy.data.node_groups[self.tree_name].nodes[self.node_name] node.process() return {"FINISHED"} @@ -55,7 +56,7 @@ class SvIfcCreateFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.S self.sv_input_names = ["schema"] super().process() - def process_ifc(self, schema): + def process_ifc(self, schema: str) -> None: guid = ifcopenshell.guid.new() ifcsverchok.helper.ifc_files[guid] = ifcopenshell.file(schema=schema) self.outputs["file"].sv_set([[ifcsverchok.helper.ifc_files[guid]]]) diff --git a/src/ifcsverchok/nodes/ifc/create_project.py b/src/ifcsverchok/nodes/ifc/create_project.py index 3048f7f936..b8f6b3177f 100644 --- a/src/ifcsverchok/nodes/ifc/create_project.py +++ b/src/ifcsverchok/nodes/ifc/create_project.py @@ -52,7 +52,7 @@ class SvIfcCreateProject(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helpe project_name = self.inputs["project_name"].sv_get()[0][0] self.process_ifc(file, project_name) - def process_ifc(self, file, project_name): + def process_ifc(self, file: ifcopenshell.file, project_name: str) -> None: # create project project = ifcopenshell.api.run("root.create_entity", file, ifc_class="IfcProject", name=str(project_name)) lengthunit = ifcopenshell.api.run("unit.add_si_unit", file, unit_type="LENGTHUNIT") From 5f8fc1ba8c7934b09b2eb7cad44427d369a8d7fa Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Mar 2025 18:49:49 +0500 Subject: [PATCH 425/476] node.sv_ifc_tooltip - hide title --- src/ifcsverchok/nodes/ifc/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ifcsverchok/nodes/ifc/api.py b/src/ifcsverchok/nodes/ifc/api.py index 7429c8ac58..a4d1f5f452 100644 --- a/src/ifcsverchok/nodes/ifc/api.py +++ b/src/ifcsverchok/nodes/ifc/api.py @@ -37,8 +37,9 @@ def update_usecase(self, context): class SvIfcTooltip(bpy.types.Operator): bl_idname = "node.sv_ifc_tooltip" - bl_label = "IFC Info" + bl_label = "" tooltip: bpy.props.StringProperty() + bl_options = {"INTERNAL"} @classmethod def description(cls, context, properties): From f10ebe0166c89dc776bcb9a49079fdc62e094b9a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Mar 2025 18:52:07 +0500 Subject: [PATCH 426/476] ifcsverchok - remove debug print --- src/ifcsverchok/ifcstore.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ifcsverchok/ifcstore.py b/src/ifcsverchok/ifcstore.py index e8c167bb52..9b036fb9ce 100644 --- a/src/ifcsverchok/ifcstore.py +++ b/src/ifcsverchok/ifcstore.py @@ -79,7 +79,6 @@ class SvIfcStore: # TODO change units to imperial pass model = ifcopenshell.util.representation.get_context(file, context="Model") - print("model: ", model) context = ifcopenshell.api.run( "context.add_context", file, From 1feb00678c6027dde64b9a94d407f9e239263a42 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 19 Mar 2025 19:02:54 +0500 Subject: [PATCH 427/476] node.sv_ifc_create_file_refresh - fix LB Out bl_label --- src/ifcsverchok/nodes/ifc/create_file.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ifcsverchok/nodes/ifc/create_file.py b/src/ifcsverchok/nodes/ifc/create_file.py index 85794206bf..d1671f73cb 100644 --- a/src/ifcsverchok/nodes/ifc/create_file.py +++ b/src/ifcsverchok/nodes/ifc/create_file.py @@ -27,7 +27,8 @@ from sverchok.data_structure import updateNode class SvIfcCreateFileRefresh(bpy.types.Operator): bl_idname = "node.sv_ifc_create_file_refresh" - bl_label = "LB Out" + bl_label = "File Refresh" + bl_description = "Create new IFC file." tree_name: StringProperty(default="") node_name: StringProperty(default="") From 4d5a3ec633f8d4ee043f9320f40697bbc13896cc Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Thu, 20 Mar 2025 09:34:52 +0500 Subject: [PATCH 428/476] ifcsverchok - ifc.write_file_panel to filter .ifc files --- src/ifcsverchok/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ifcsverchok/__init__.py b/src/ifcsverchok/__init__.py index d1608a0d72..e30a6db7ce 100644 --- a/src/ifcsverchok/__init__.py +++ b/src/ifcsverchok/__init__.py @@ -180,6 +180,7 @@ class IFC_Sv_write_file(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} bl_description = "File path to write to." filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) node_group: bpy.props.StringProperty(default="") force_mode: bpy.props.BoolProperty(default=False) From 58a5f881e637125755d2f5c3cc7555eb42b45957 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 20 Mar 2025 13:57:30 +0100 Subject: [PATCH 429/476] Retrieve length unit info from underlying mapping in iterator #6355 --- src/ifcgeom/Iterator.h | 10 ++++------ src/ifcgeom/abstract_mapping.h | 1 + src/ifcgeom/mapping/mapping.h | 1 + 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ifcgeom/Iterator.h b/src/ifcgeom/Iterator.h index 78c6ddc532..e6e9dd42b0 100644 --- a/src/ifcgeom/Iterator.h +++ b/src/ifcgeom/Iterator.h @@ -150,10 +150,6 @@ namespace IfcGeom { int done; int total; - // @todo these appear uninitialized? - std::string unit_name_; - double unit_magnitude_; - ifcopenshell::geometry::taxonomy::point3 bounds_min_; ifcopenshell::geometry::taxonomy::point3 bounds_max_; @@ -166,8 +162,8 @@ namespace IfcGeom { public: void set_cache(GeometrySerializer* cache) { cache_ = cache; } - const std::string& unit_name() const { return unit_name_; } - double unit_magnitude() const { return unit_magnitude_; } + const std::string& unit_name() const { return converter_->mapping()->get_length_unit_name(); } + double unit_magnitude() const { return converter_->mapping()->get_length_unit(); } // Check if error occurred during iterator initialization or iteration over elements. bool had_error_processing_elements() const { return had_error_processing_elements_; } @@ -903,6 +899,8 @@ namespace IfcGeom { for (auto& p : all_processed_elements_) { delete p; } + + delete converter_; } }; } diff --git a/src/ifcgeom/abstract_mapping.h b/src/ifcgeom/abstract_mapping.h index c8174d5363..c8ba61e1b4 100644 --- a/src/ifcgeom/abstract_mapping.h +++ b/src/ifcgeom/abstract_mapping.h @@ -45,6 +45,7 @@ namespace geometry { virtual const IfcUtil::IfcBaseEntity* get_product_type(const IfcUtil::IfcBaseEntity*) = 0; virtual const IfcUtil::IfcBaseEntity* get_single_material_association(const IfcUtil::IfcBaseEntity*) = 0; virtual double get_length_unit() const = 0; + virtual const std::string& get_length_unit_name() const = 0; virtual IfcUtil::IfcBaseEntity* representation_of(const IfcUtil::IfcBaseEntity* product) = 0; const Settings& settings() const { return settings_; } diff --git a/src/ifcgeom/mapping/mapping.h b/src/ifcgeom/mapping/mapping.h index 61e8b4d7b6..054e0a7efc 100644 --- a/src/ifcgeom/mapping/mapping.h +++ b/src/ifcgeom/mapping/mapping.h @@ -86,6 +86,7 @@ namespace geometry { virtual std::map get_layers(IfcUtil::IfcBaseEntity*); virtual void initialize_settings(); virtual double get_length_unit() const { return length_unit_; } + virtual const std::string& get_length_unit_name() const { return length_unit_name_; } virtual aggregate_of_instance::ptr find_openings(const IfcUtil::IfcBaseEntity*); virtual IfcUtil::IfcBaseEntity* representation_of(const IfcUtil::IfcBaseEntity* product); From 35fe79dfb230350110d203ab26c20487fed0a397 Mon Sep 17 00:00:00 2001 From: Sebastian Friston Date: Wed, 12 Mar 2025 15:44:14 +0000 Subject: [PATCH 430/476] ISSUE #6328 fix in dispatch_token to avoid missing unknown in logicals --- src/ifcparse/IfcFile.cpp | 4 +++- src/serializers/schema_dependent/XmlSerializer.cpp | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ifcparse/IfcFile.cpp b/src/ifcparse/IfcFile.cpp index 14892b96ff..7c964d3fda 100644 --- a/src/ifcparse/IfcFile.cpp +++ b/src/ifcparse/IfcFile.cpp @@ -57,8 +57,10 @@ namespace { void dispatch_token(int instance_id, int attribute_id, IfcParse::Token t, IfcParse::declaration* decl, Fn fn) { if (t.type == IfcParse::Token_BINARY) { fn(IfcParse::TokenFunc::asBinary(t)); - } else if (t.type == IfcParse::Token_BOOL) { + } else if (IfcParse::TokenFunc::isBool(t)) { fn(IfcParse::TokenFunc::asBool(t)); + } else if (IfcParse::TokenFunc::isLogical(t)) { + fn(IfcParse::TokenFunc::asLogical(t)); } else if (t.type == IfcParse::Token_ENUMERATION) { auto& s = IfcParse::TokenFunc::asStringRef(t); if (decl && decl->as_enumeration_type()) { diff --git a/src/serializers/schema_dependent/XmlSerializer.cpp b/src/serializers/schema_dependent/XmlSerializer.cpp index 3aefa71d7a..508c6e6060 100644 --- a/src/serializers/schema_dependent/XmlSerializer.cpp +++ b/src/serializers/schema_dependent/XmlSerializer.cpp @@ -82,9 +82,10 @@ boost::optional format_attribute(ifcopenshell::geometry::abstract_m } switch(argument_type) { - case IfcUtil::Argument_BOOL: { - const bool b = argument; - value = b ? "true" : "false"; + case IfcUtil::Argument_BOOL: + case IfcUtil::Argument_LOGICAL:{ + const boost::logic::tribool b = argument; + value = b.value == boost::logic::tribool::indeterminate_value ? "unknown" : b ? "true" : "false"; break; } case IfcUtil::Argument_DOUBLE: { const double d = argument; From 4c2844292e2fc16d8d7e85b3fdb76a6be9e8428a Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 20 Mar 2025 16:13:56 +0100 Subject: [PATCH 431/476] Apply skew in infra lofts #6386 --- src/ifcgeom/infra_sweep_helper.cpp | 33 +++++++++++++++---- src/ifcgeom/infra_sweep_helper.h | 1 + .../mapping/IfcSectionedSolidHorizontal.cpp | 16 ++++++++- src/ifcgeom/mapping/IfcSectionedSurface.cpp | 16 ++++++++- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/ifcgeom/infra_sweep_helper.cpp b/src/ifcgeom/infra_sweep_helper.cpp index 7559d4c9d6..f3f79d4c18 100644 --- a/src/ifcgeom/infra_sweep_helper.cpp +++ b/src/ifcgeom/infra_sweep_helper.cpp @@ -63,31 +63,37 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, auto relative_dist_along = (dist_along - *profile_index) / (*(profile_index + 1) - *profile_index); const auto& profile_a = cross_sections[std::distance(longitudes.begin(), profile_index)].section_geometry; const auto& offset_a = cross_sections[std::distance(longitudes.begin(), profile_index)].offset; + const auto& rotation_a = cross_sections[std::distance(longitudes.begin(), profile_index)].rotation; taxonomy::geom_item::ptr interpolated = nullptr; // Only interpolate if: // - there is a profile ahead of us, and - // - we're not exactly at the location of the current profile or whether there is an offset involved. + // - we're not exactly at the location of the current profile or whether there is an offset involved bool should_interpolate = (profile_index + 1 < longitudes.end()) && - (relative_dist_along >= 1.e-9 || offset_a.cwiseAbs().maxCoeff() > 0.); + (relative_dist_along >= 1.e-9 || offset_a.cwiseAbs().maxCoeff() > 0. || rotation_a); + + boost::optional interpolated_rotation; if (should_interpolate) { taxonomy::geom_item::ptr profile_b; Eigen::Vector3d offset_b; + boost::optional rotation_b; if ((profile_index + 1 < longitudes.end())) { profile_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].section_geometry; offset_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].offset; + rotation_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].rotation; } else { profile_b = profile_a; offset_b = offset_a; + rotation_b = rotation_a; } // Only interpolate if the profiles are different or either of the offsets is non-zero bool should_interpolate2 = (profile_a->instance != profile_b->instance) || - (offset_a.cwiseAbs().maxCoeff() > 0. || offset_b.cwiseAbs().maxCoeff() > 0.); + (offset_a.cwiseAbs().maxCoeff() > 0. || offset_b.cwiseAbs().maxCoeff() > 0. || rotation_b); if (should_interpolate2) { @@ -130,6 +136,13 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, } auto interpolated_offset = lerp(offset_a, offset_b, relative_dist_along); + if (rotation_a && rotation_b) { + // @todo we don't support an overridden rotation on only one of the placements + // in which case we would need to lerp with the rotation component below in m4b. + interpolated_rotation = lerp(*rotation_a, *rotation_b, relative_dist_along); + } else { + Logger::Error("Direction vectors on cross section placements only supported when used consistently"); + } taxonomy::loop::ptr w1, w2; taxonomy::edge::ptr e1, e2; for (auto tmp_ : boost::combine(loops_a, loops_b)) { @@ -149,6 +162,7 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, auto& p2 = boost::get(e2->start); auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval(); + // auto p4 = (interpolated_rotation * p3).eval(); points.push_back(taxonomy::make(p3)); } if (!points.empty()) { @@ -173,9 +187,16 @@ taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, }*/ Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity(); - m4b.col(0).head<3>() = m4.col(1).head<3>().normalized(); - m4b.col(1).head<3>() = m4.col(2).head<3>().normalized(); - m4b.col(2).head<3>() = m4.col(0).head<3>().normalized(); + if (interpolated_rotation) { + // direction vectors on the linear placement overwrite the placement otherwise inferred from the tangent + m4b.col(0).head<3>() = interpolated_rotation->col(1); + m4b.col(1).head<3>() = interpolated_rotation->col(2); + m4b.col(2).head<3>() = interpolated_rotation->col(0); + } else { + m4b.col(0).head<3>() = m4.col(1).head<3>().normalized(); + m4b.col(1).head<3>() = m4.col(2).head<3>().normalized(); + m4b.col(2).head<3>() = m4.col(0).head<3>().normalized(); + } m4b.col(3).head<3>() = m4.col(3).head<3>(); if (interpolated) { diff --git a/src/ifcgeom/infra_sweep_helper.h b/src/ifcgeom/infra_sweep_helper.h index 351206dd03..bacc4bd4f0 100644 --- a/src/ifcgeom/infra_sweep_helper.h +++ b/src/ifcgeom/infra_sweep_helper.h @@ -12,6 +12,7 @@ namespace ifcopenshell { double dist_along; taxonomy::geom_item::ptr section_geometry; Eigen::Vector3d offset; + boost::optional rotation; bool operator <(const cross_section& other) const { return dist_along < other.dist_along; diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp index 640b5a24f0..fe5b31c56f 100644 --- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp @@ -49,6 +49,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in // The longitudes determine the range of the sweep and the offsets are interpolated in between // sweep segments. std::vector profile_offsets; + std::vector> profile_rotations; std::vector longitudes; for (auto& cs : *css) { @@ -69,6 +70,19 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in ); profile_offsets.push_back(po); + + boost::optional rot; + if (csp->Axis() && csp->RefDirection()) { + rot = taxonomy::matrix4( + Eigen::Vector3d(0, 0, 0), + taxonomy::cast(map(csp->Axis()))->ccomponents(), + taxonomy::cast(map(csp->RefDirection()))->ccomponents()).ccomponents().block<3,3>(0,0); + } else if (csp->Axis()) { + rot = taxonomy::matrix4( + Eigen::Vector3d(0, 0, 0), + taxonomy::cast(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0); + } + profile_rotations.push_back(rot); } if (faces.size() != profile_offsets.size()) { Logger::Warning("Expected CrossSections and CrossSectionPositions to be equal length, but got " + std::to_string(faces.size()) + " and " + std::to_string(profile_offsets.size()) + " respectively", inst); @@ -80,7 +94,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in } for (size_t i = 0; i < faces.size(); ++i) { - cross_sections.push_back({ longitudes[i], faces[i], profile_offsets[i] }); + cross_sections.push_back({ longitudes[i], faces[i], profile_offsets[i], profile_rotations[i]}); } #else return nullptr; diff --git a/src/ifcgeom/mapping/IfcSectionedSurface.cpp b/src/ifcgeom/mapping/IfcSectionedSurface.cpp index 3edcde7946..4948ddd0a0 100644 --- a/src/ifcgeom/mapping/IfcSectionedSurface.cpp +++ b/src/ifcgeom/mapping/IfcSectionedSurface.cpp @@ -49,6 +49,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) { // The longitudes determine the range of the sweep and the offsets are interpolated in between // sweep segments. std::vector profile_offsets; + std::vector> profile_rotations; std::vector longitudes; for (auto& cs : *css) { @@ -69,6 +70,19 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) { ); profile_offsets.push_back(po); + + boost::optional rot; + if (csp->Axis() && csp->RefDirection()) { + rot = taxonomy::matrix4( + Eigen::Vector3d(0, 0, 0), + taxonomy::cast(map(csp->Axis()))->ccomponents(), + taxonomy::cast(map(csp->RefDirection()))->ccomponents()).ccomponents().block<3, 3>(0, 0); + } else if (csp->Axis()) { + rot = taxonomy::matrix4( + Eigen::Vector3d(0, 0, 0), + taxonomy::cast(map(csp->Axis()))->ccomponents()).ccomponents().block<3, 3>(0, 0); + } + profile_rotations.push_back(rot); } #else return nullptr; @@ -83,7 +97,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) { } for (size_t i = 0; i < faces.size(); ++i) { - cross_sections.push_back({ longitudes[i], faces[i], profile_offsets[i] }); + cross_sections.push_back({ longitudes[i], faces[i], profile_offsets[i], profile_rotations[i] }); } } From 9c4ec34de3d88ed4cbd5257309cf0f046c500e3f Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Thu, 20 Mar 2025 20:57:47 +0100 Subject: [PATCH 432/476] Some form of circle handling in loop_to_function_item_upgrade_impl() #6381 --- src/ifcgeom/taxonomy.cpp | 81 ++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp index 6fc7288dda..4d74a6b4e5 100644 --- a/src/ifcgeom/taxonomy.cpp +++ b/src/ifcgeom/taxonomy.cpp @@ -799,6 +799,25 @@ boost::optional ifcopenshell::geometry::taxonomy::curve_to_face_upgra return face_; } +namespace { + // @todo eliminate redundancy with cgal kernel + void evaluate_curve(const circle::ptr& c, double u, point3& p) { + Eigen::Vector4d xy{ c->radius * std::cos(u), c->radius * std::sin(u), 0, 1. }; + p.components() = (c->matrix->ccomponents() * xy).head<3>(); + } + + // @todo eliminate redundancy with cgal kernel + void evaluate_curve_d1(const circle::ptr& c, double u, direction3& p) { + Eigen::Vector4d xy{ -std::sin(u), cos(u), 0, 0. }; + p.components() = (c->matrix->ccomponents() * xy).head<3>(); + } + + double project_onto_curve(const circle::ptr& c, const point3& p) { + Eigen::Vector2d xy = (c->matrix->ccomponents().inverse() * p.ccomponents().homogeneous()).head<2>(); + return std::atan2(xy(1), xy(0)); + } +} + boost::optional ifcopenshell::geometry::taxonomy::loop_to_function_item_upgrade_impl(ptr item) { boost::optional fi_; @@ -811,23 +830,53 @@ boost::optional ifcopenshell::geometry::taxonomy::loop_to_fu piecewise_function::spans_t spans; spans.reserve(loop_->children.size()); for (auto& edge_ : loop_->children) { - // the edge could be an arc or trimmed circle in the case of IfcIndexPolyCurve - support for this isn't implemented yet - if (edge_->basis) { - Logger::Message(Logger::Severity::LOG_NOTICE, "Shape of basis curve ignored - edge is treated as a straight line edge"); - } + if (edge_->basis && edge_->basis->kind() == CIRCLE) { + const circle::ptr circ = std::static_pointer_cast(edge_->basis); - const auto& s = boost::get(edge_->start)->ccomponents(); - const auto& e = boost::get(edge_->end)->ccomponents(); - Eigen::Vector3d v = e - s; - auto l = v.norm(); // the norm of a vector is a measure of its length - v.normalize(); // normalize the vector so that it is a unit direction vector - std::function fn = [s, v](double u) { - Eigen::Vector3d o(s + u * v), axis(0, 0, 1), refDirection(v); - auto Y = axis.cross(refDirection).normalized(); - axis = refDirection.cross(Y).normalized(); - return make(o, axis, refDirection)->components(); - }; - spans.emplace_back(taxonomy::make(l, fn)); + auto* s_pnt = boost::get(&edge_->start); + auto* e_pnt = boost::get(&edge_->end); + auto* s_param = boost::get(&edge_->start); + auto* e_param = boost::get(&edge_->end); + + if (!s_pnt && !s_param) { + return boost::none; + } + if (!e_pnt && !e_param) { + return boost::none; + } + + double s = s_pnt ? project_onto_curve(circ, **s_pnt) : *s_param; + double e = e_pnt ? project_onto_curve(circ, **e_pnt) : *e_param; + + auto l = std::fabs(s - e) * circ->radius; + std::function fn = [circ, s](double u) { + point3 P; + direction3 d; + evaluate_curve(circ, u / circ->radius + s, P); + evaluate_curve_d1(circ, u / circ->radius + s, d); + return matrix4(P.ccomponents(), circ->matrix->ccomponents().col(2).head<3>(), d.ccomponents()).components(); + }; + spans.emplace_back(taxonomy::make(l, fn)); + } else if (edge_->start.which() == 1 && edge_->end.which() == 1) { + if (edge_->basis && edge_->basis->kind() != LINE) { + Logger::Message(Logger::Severity::LOG_WARNING, "Basis curve not supported - edge is treated as a straight line edge"); + } + const auto& s = boost::get(edge_->start)->ccomponents(); + const auto& e = boost::get(edge_->end)->ccomponents(); + Eigen::Vector3d v = e - s; + auto l = v.norm(); // the norm of a vector is a measure of its length + v.normalize(); // normalize the vector so that it is a unit direction vector + std::function fn = [s, v](double u) { + Eigen::Vector3d o(s + u * v), axis(0, 0, 1), refDirection(v); + auto Y = axis.cross(refDirection).normalized(); + axis = refDirection.cross(Y).normalized(); + return make(o, axis, refDirection)->components(); + }; + spans.emplace_back(taxonomy::make(l, fn)); + } else { + Logger::Message(Logger::Severity::LOG_ERROR, "Basis curve not supported"); + return boost::none; + } } fi_ = make(0.0,spans); loop_->fi = fi_; From 8e1e0aec79e82d4876f7c4feebb89e4b7612f9e5 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 21 Mar 2025 16:30:56 +1100 Subject: [PATCH 433/476] Fix #6404. See #1227. Fix IFC2X3 wall creation with new wall engine. --- .../ifcopenshell/api/geometry/regenerate_wall_representation.py | 2 +- src/ifcopenshell-python/ifcopenshell/util/representation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py index 0b748d7e0c..43bb761f81 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py @@ -548,7 +548,7 @@ class Regenerator: material = ifcopenshell.util.element.get_material(wall, should_skip_usage=True) if not material or not material.is_a("IfcMaterialLayerSet"): return [] - return [PrioritisedLayer(l.Priority or 0, l.LayerThickness) for l in material.MaterialLayers] + return [PrioritisedLayer(getattr(l, "Priority", 0) or 0, l.LayerThickness) for l in material.MaterialLayers] def combine_layers(self, layers, override_priorities): results = [] diff --git a/src/ifcopenshell-python/ifcopenshell/util/representation.py b/src/ifcopenshell-python/ifcopenshell/util/representation.py index f5233005af..a628ea6c88 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/representation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/representation.py @@ -480,7 +480,7 @@ def get_reference_line(wall: ifcopenshell.entity_instance, fallback_length: floa if axis := ifcopenshell.util.representation.get_representation(wall, "Plan", "Axis", "GRAPH_VIEW"): for item in ifcopenshell.util.representation.resolve_representation(axis).Items: if item.is_a("IfcPolyline"): - points = item.Points + points = [p[0] for p in item.Points] elif item.is_a("IfcIndexedPolyCurve"): points = item.Points.CoordList else: From 963ba5fc302fa1d092ae83286072785b266b750a Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 21 Mar 2025 16:45:29 +1100 Subject: [PATCH 434/476] See #6404. See #1227. Create axis context if it does not exist for walls. --- src/bonsai/bonsai/bim/module/model/wall.py | 2 +- .../api/geometry/regenerate_wall_representation.py | 10 ++++++++++ .../ifcopenshell/util/representation.py | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 49811c8cbd..2ab37af2c4 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -1170,7 +1170,7 @@ class DumbWallJoiner: ifcopenshell.util.element.replace_element(old_rep, rep) ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_rep) else: - ifcopenshell.api.geometry.assign_representation(self.file, product=wall, representation=rep) + ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), product=wall, representation=rep) def extend(self, wall1, target): if tool.Ifc.is_moved(wall1): diff --git a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py index 43bb761f81..ae2a885d36 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/geometry/regenerate_wall_representation.py @@ -73,6 +73,9 @@ def regenerate_wall_representation( 0.0)). This is a logical, consistent, and useful placement coordinate (especially for apps that can pivot using this point). + All this functionality relies on the Plan/Axis/GRAPH_VIEW representation + context. It will be created if it does not exist. + :param wall: The IfcWall for the representation, only Model/Body/MODEL_VIEW type of representations are currently supported. :param length: If the wall doesn't have an axis length, this is the default @@ -94,6 +97,13 @@ class Regenerator: self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(file) self.is_angled = False + if not self.axis: + if not (plan := ifcopenshell.util.representation.get_context(file, "Plan")): + plan = ifcopenshell.api.context.add_context(file, context_type="Plan") + self.axis = ifcopenshell.api.context.add_context( + file, context_type="Plan", context_identifier="Axis", target_view="GRAPH_VIEW", parent=plan + ) + def regenerate(self, wall, length=1.0, height=1.0, angle=None): print("-" * 100) print(wall) diff --git a/src/ifcopenshell-python/ifcopenshell/util/representation.py b/src/ifcopenshell-python/ifcopenshell/util/representation.py index a628ea6c88..ca79c4ec88 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/representation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/representation.py @@ -19,6 +19,7 @@ import numpy as np import numpy.typing as npt import ifcopenshell +import ifcopenshell.util.shape import ifcopenshell.util.placement from typing import Optional, Union, TypedDict, Literal, Generator, Sequence @@ -488,4 +489,13 @@ def get_reference_line(wall: ifcopenshell.entity_instance, fallback_length: floa if points[0][0] < points[1][0]: # An axis always goes in the +X direction return [np.array(points[0]), np.array(points[1])] return [np.array(points[1]), np.array(points[0])] + elif extrusions := ifcopenshell.util.shape.get_base_extrusions(wall): + for item in extrusions: + if item.is_a("IfcPolyline"): + x = [p[0][0] for p in item.Points] + elif item.is_a("IfcIndexedPolyCurve"): + x = [p[0] for p in item.Points.CoordList] + else: + continue + return [np.array((min(x), 0.0)), np.array((max(x), 0.0))] return [np.array((0.0, 0.0)), np.array((fallback_length, 0.0))] From 3231752fefe26437cadecfa9e47237097fdda2fa Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 21 Mar 2025 16:46:08 +1100 Subject: [PATCH 435/476] Minor fix to clipping planes if there is no camera --- src/bonsai/bonsai/bim/module/project/operator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 5e9b392720..c9bcdcac89 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -51,7 +51,6 @@ from bonsai.bim.ifc import IfcStore from bonsai.bim.ui import IFCFileSelector from bonsai.bim import import_ifc from bonsai.bim import export_ifc -from collections import defaultdict from math import radians from pathlib import Path from collections import defaultdict @@ -63,7 +62,7 @@ from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneD from bonsai.bim.module.project.prop import BreadcrumbType from bonsai.bim.module.model.decorator import PolylineDecorator, FaceAreaDecorator from bonsai.bim.module.model.polyline import PolylineOperator -from typing import Union, TYPE_CHECKING, Literal, get_args +from typing import Union, TYPE_CHECKING, get_args if TYPE_CHECKING: from bonsai.bim.module.project.prop import Link @@ -2316,7 +2315,7 @@ class RefreshClippingPlanes(bpy.types.Operator): should_refresh = True break - if context.scene.camera.visible_get() and tool.Ifc.get_entity(context.scene.camera): + if (camera := context.scene.camera) and camera.visible_get() and tool.Ifc.get_entity(camera): camera = context.scene.camera else: camera = None From ea84f829af4b816f514465533eee268cf89fc1fc Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 21 Mar 2025 16:46:42 +1100 Subject: [PATCH 436/476] The experimental cutting tool now supports a bisect mode for batch cuts --- src/bonsai/bonsai/bim/module/misc/operator.py | 18 ++++- src/bonsai/bonsai/bim/module/misc/ui.py | 5 +- src/bonsai/bonsai/tool/misc.py | 73 ++++++++++++++++++- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index a1ff930408..7d2907f11f 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -125,15 +125,25 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator): "Will unassign element from a type if type has a representation." ) bl_options = {"REGISTER", "UNDO"} + mode: bpy.props.StringProperty() @classmethod def poll(cls, context): - return context.selected_objects and tool.Ifc.get() + return context.selected_objects def _execute(self, context): cutter = context.active_object objs = [o for o in context.selected_objects if o != cutter] + if not tool.Ifc.get(): + if self.mode == "BOOLEAN": + tool.Misc.boolean_objects_with_cutter(objs, cutter) + elif self.mode == "BISECT": + tool.Misc.bisect_objects_with_cutter(objs, cutter) + for obj in objs: + bpy.data.objects.remove(obj) + return + objs_to_cut = [] # Splitting only works on meshes for obj in objs: @@ -169,7 +179,11 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator): objs_to_cut.append(obj) - new_objs = tool.Misc.split_objects_with_cutter(objs_to_cut, cutter) + if self.mode == "BOOLEAN": + new_objs = tool.Misc.boolean_objects_with_cutter(objs_to_cut, cutter) + elif self.mode == "BISECT": + new_objs = tool.Misc.bisect_objects_with_cutter(objs_to_cut, cutter) + for obj in new_objs: bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj) bpy.ops.bim.update_representation(obj=obj.name) diff --git a/src/bonsai/bonsai/bim/module/misc/ui.py b/src/bonsai/bonsai/bim/module/misc/ui.py index 7e33017619..c009b720e4 100644 --- a/src/bonsai/bonsai/bim/module/misc/ui.py +++ b/src/bonsai/bonsai/bim/module/misc/ui.py @@ -39,8 +39,9 @@ class BIM_PT_misc_utilities(bpy.types.Panel): row = layout.split(factor=0.2, align=True) row.prop(props, "total_storeys", text="") row.operator("bim.resize_to_storey").total_storeys = props.total_storeys - row = layout.row() - row.operator("bim.split_along_edge") + row = layout.row(align=True) + row.operator("bim.split_along_edge", text="Split Along Edge").mode = "BOOLEAN" + row.operator("bim.split_along_edge", text="Bisect At Faces").mode = "BISECT" row = layout.row() row.operator("bim.get_connected_system_elements") row = layout.row() diff --git a/src/bonsai/bonsai/tool/misc.py b/src/bonsai/bonsai/tool/misc.py index db43a14c5d..db99dd89d2 100644 --- a/src/bonsai/bonsai/tool/misc.py +++ b/src/bonsai/bonsai/tool/misc.py @@ -99,7 +99,7 @@ class Misc(bonsai.core.tool.Misc): bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) @classmethod - def split_objects_with_cutter( + def boolean_objects_with_cutter( cls, objs: list[bpy.types.Object], cutter: bpy.types.Object ) -> list[bpy.types.Object]: cutter_mesh = cutter.data @@ -144,3 +144,74 @@ class Misc(bonsai.core.tool.Misc): bm.free() bm_flipped.free() return new_objs + + @classmethod + def bisect_objects_with_cutter( + cls, objs: list[bpy.types.Object], cutter: bpy.types.Object + ) -> list[bpy.types.Object]: + cutter_mesh = cutter.data + assert isinstance(cutter_mesh, bpy.types.Mesh) + + bm = bmesh.new() + bm.from_mesh(cutter_mesh) + bm.faces.ensure_lookup_table() + + planes = [] + for face in bm.faces: + no = face.normal.to_4d() + no.w = 0.0 + no = cutter.matrix_world @ no + co = cutter.matrix_world @ face.verts[0].co + planes.append((co, no)) + + bm.free() + + new_objs = [] + for obj in objs: + if not isinstance(obj.data, bpy.types.Mesh) or obj == cutter: + continue + matrix_i = obj.matrix_world.inverted() + bm_obj = bmesh.new() + bm_obj.from_mesh(obj.data) + + bms = [bm_obj.copy()] + for co, no in planes: + co = matrix_i @ co + no = (matrix_i @ no).to_3d() + new_bms = [] + for bm in bms: + bm1 = bm.copy() + bm2 = bm.copy() + geom = bm1.verts[:] + bm1.edges[:] + bm1.faces[:] + bisect1 = bmesh.ops.bisect_plane( + bm1, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_inner=True + ) + if not bisect1["geom"] or not [g for g in bisect1["geom"] if isinstance(g, bmesh.types.BMFace)]: + new_bms.append(bm) + continue + edges = [g for g in bisect1["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + bmesh.ops.triangle_fill(bm1, use_dissolve=True, edges=edges) + geom = bm2.verts[:] + bm2.edges[:] + bm2.faces[:] + bisect2 = bmesh.ops.bisect_plane( + bm2, geom=geom, dist=0.0001, plane_co=co, plane_no=no, clear_outer=True + ) + if not bisect2["geom"] or not [g for g in bisect2["geom"] if isinstance(g, bmesh.types.BMFace)]: + new_bms.append(bm) + continue + edges = [g for g in bisect2["geom_cut"] if isinstance(g, bmesh.types.BMEdge)] + bmesh.ops.triangle_fill(bm2, use_dissolve=True, edges=edges) + + new_bms.append(bm1) + new_bms.append(bm2) + bms = new_bms + + for bm in bms: + mesh = obj.data + new_obj = obj.copy() + new_obj.data = mesh.copy() + bm.to_mesh(new_obj.data) + for collection in obj.users_collection: + collection.objects.link(new_obj) + new_objs.append(new_obj) + + return new_objs From f2283e5895b983c870d3cc212b9da4a0ff87b927 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Mar 2025 12:02:49 +0500 Subject: [PATCH 437/476] Fix Blender 4.4 errors starting some operators #6399 #6396 Ping @brunoperdigao just in case. --- src/bonsai/bonsai/bim/module/model/product.py | 5 +++-- src/bonsai/bonsai/bim/module/model/profile.py | 5 +++-- src/bonsai/bonsai/bim/module/model/slab.py | 8 +++++--- src/bonsai/bonsai/bim/module/model/wall.py | 8 +++++--- src/bonsai/bonsai/bim/module/project/operator.py | 16 ++++++++++------ 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/product.py b/src/bonsai/bonsai/bim/module/model/product.py index 12bae7ae18..c53af0665b 100644 --- a/src/bonsai/bonsai/bim/module/model/product.py +++ b/src/bonsai/bonsai/bim/module/model/product.py @@ -164,8 +164,9 @@ class DrawOccurrence(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): def poll(cls, context): return context.space_data.type == "VIEW_3D" - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + bpy.types.Operator.__init__(self, *args, **kwargs) + PolylineOperator.__init__(self) def create_occurrence(self, context, event): if not self.relating_type: diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index 424c56f5e9..40e3ab9992 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -1126,8 +1126,9 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato def poll(cls, context): return context.space_data.type == "VIEW_3D" - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + bpy.types.Operator.__init__(self, *args, **kwargs) + PolylineOperator.__init__(self) self.input_options = ["D", "A", "X", "Y", "Z"] self.input_ui = tool.Polyline.create_input_ui(input_options=self.input_options) self.relating_type = None diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index 78418971f9..1f89f27c00 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -864,7 +864,8 @@ class AddSlabFromWall(bpy.types.Operator, tool.Ifc.Operator): def poll(cls, context): return context.space_data.type == "VIEW_3D" - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.relating_type = None props = tool.Model.get_model_props() relating_type_id = props.relating_type_id @@ -895,8 +896,9 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): def poll(cls, context): return context.space_data.type == "VIEW_3D" - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + bpy.types.Operator.__init__(self, *args, **kwargs) + PolylineOperator.__init__(self) self.relating_type = None props = tool.Model.get_model_props() relating_type_id = props.relating_type_id diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 2ab37af2c4..7363844d9a 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -404,7 +404,8 @@ class AddWallsFromSlab(bpy.types.Operator, tool.Ifc.Operator): def poll(cls, context): return context.space_data.type == "VIEW_3D" - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.relating_type = None props = tool.Model.get_model_props() relating_type_id = props.relating_type_id @@ -437,8 +438,9 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): def poll(cls, context): return context.space_data.type == "VIEW_3D" - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + bpy.types.Operator.__init__(self, *args, **kwargs) + PolylineOperator.__init__(self) self.relating_type = None props = tool.Model.get_model_props() relating_type_id = props.relating_type_id diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index c9bcdcac89..52333a5120 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2190,7 +2190,8 @@ class EnableCulling(bpy.types.Operator): bl_label = "Enable Culling" bl_options = {"REGISTER"} - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.last_view_corners = None self.total_mousemoves = 0 self.cullable_objects = [] @@ -2296,7 +2297,8 @@ class RefreshClippingPlanes(bpy.types.Operator): bl_label = "Refresh Clipping Planes" bl_options = {"REGISTER"} - def __init__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.total_planes = 0 self.camera = None @@ -2605,8 +2607,9 @@ class MeasureTool(bpy.types.Operator, PolylineOperator): def poll(cls, context): return context.space_data.type == "VIEW_3D" - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + bpy.types.Operator.__init__(self, *args, **kwargs) + PolylineOperator.__init__(self) self.input_options = ["D", "A", "X", "Y", "Z"] self.input_ui = tool.Polyline.create_input_ui(input_options=self.input_options) @@ -2699,8 +2702,9 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator): def poll(cls, context): return context.space_data.type == "VIEW_3D" - def __init__(self): - super().__init__() + def __init__(self, *args, **kwargs): + bpy.types.Operator.__init__(self, *args, **kwargs) + PolylineOperator.__init__(self) self.input_options = ["AREA"] self.input_ui = tool.Polyline.create_input_ui(input_options=self.input_options) self.clicked_faces = [] From e327932f4ef4318e18b1cf63549d2f6854c54379 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 21 Mar 2025 13:48:59 +0100 Subject: [PATCH 438/476] Fixes to HierarchyHelper #6365 --- src/ifcparse/IfcHierarchyHelper.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ifcparse/IfcHierarchyHelper.h b/src/ifcparse/IfcHierarchyHelper.h index 4f427a527a..c303942dd5 100644 --- a/src/ifcparse/IfcHierarchyHelper.h +++ b/src/ifcparse/IfcHierarchyHelper.h @@ -411,9 +411,9 @@ class IFC_PARSE_API IfcHierarchyHelper : public IfcParse::IfcFile { bool found = false; for (typename Schema::IfcRelDefinesByType::list::it i = li->begin(); i != li->end(); ++i) { typename Schema::IfcRelDefinesByType* rel = *i; - if (rel->RelatingType() == related_object) { + if (rel->RelatingType() == relating_object) { typename Schema::IfcObject::list::ptr objects = rel->RelatedObjects(); - objects->push((typename Schema::IfcObject*)related_object); + objects->push(addEntity(related_object)->template as()); rel->setRelatedObjects(objects); found = true; break; @@ -427,8 +427,8 @@ class IFC_PARSE_API IfcHierarchyHelper : public IfcParse::IfcFile { owner_hist = addOwnerHistory(); } typename Schema::IfcObject::list::ptr related_objects(new aggregate_of()); - related_objects->push((typename Schema::IfcObject*)related_object); - typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, related_object->template as()); + related_objects->push(related_object->template as()); + typename Schema::IfcRelDefinesByType* t = new typename Schema::IfcRelDefinesByType(IfcParse::IfcGlobalId(), owner_hist, boost::none, boost::none, related_objects, relating_object->template as()); addEntity(t); } @@ -440,7 +440,7 @@ class IFC_PARSE_API IfcHierarchyHelper : public IfcParse::IfcFile { try { if (get_parent_of_relation(rel) == relating_object) { aggregate_of_instance::ptr products = get_children_of_relation(rel); - products->push(related_object); + products->push(addEntity(related_object)); set_children_of_relation(rel, products); found = true; break; From 6880e31a663b850e3dbe1f10b4176988b1696190 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 21 Mar 2025 14:35:23 +0100 Subject: [PATCH 439/476] Add ColladaSerializer to python bindings --- src/ifcopenshell-python/ifcopenshell/geom/main.py | 5 ++++- src/ifcwrap/IfcGeomWrapper.i | 1 + src/ifcwrap/IfcPython.i | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index 27e231cf37..01d5b4bf3f 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -641,7 +641,10 @@ class serializers: hdf5 = ifcopenshell_wrapper.HdfSerializer except: pass - + try: + collada = ifcopenshell_wrapper.ColladaSerializer + except: + pass # ttl is always available since it doesn't depend on any C++ libraries, # just people might be using an outdated binary if hasattr(ifcopenshell_wrapper, "TtlWktSerializer"): diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i index 35168dc969..16dc6e1081 100644 --- a/src/ifcwrap/IfcGeomWrapper.i +++ b/src/ifcwrap/IfcGeomWrapper.i @@ -260,6 +260,7 @@ namespace { %include "../serializers/SvgSerializer.h" %include "../serializers/HdfSerializer.h" %include "../serializers/WavefrontObjSerializer.h" +%include "../serializers/ColladaSerializer.h" %include "../serializers/XmlSerializer.h" %include "../serializers/GltfSerializer.h" %include "../serializers/TtlWktSerializer.h" diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i index 196416586c..7140481c2b 100644 --- a/src/ifcwrap/IfcPython.i +++ b/src/ifcwrap/IfcPython.i @@ -186,6 +186,7 @@ #include "../serializers/SvgSerializer.h" #include "../serializers/WavefrontObjSerializer.h" + #include "../serializers/ColladaSerializer.h" #include "../serializers/HdfSerializer.h" #ifdef HAS_SCHEMA_2x3 @@ -282,6 +283,7 @@ constexpr bool is_std_vector_vector_v = is_std_vector_vector::value; #include "../serializers/SvgSerializer.h" #include "../serializers/WavefrontObjSerializer.h" + #include "../serializers/ColladaSerializer.h" #include "../serializers/HdfSerializer.h" #include "../serializers/XmlSerializer.h" #include "../serializers/GltfSerializer.h" From 45c45d68629a385302be5c318acb4e77c82f6f18 Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Fri, 21 Mar 2025 14:35:47 +0100 Subject: [PATCH 440/476] ifcopenshell.geom.serializers.guess_from_extension() --- .../ifcopenshell/geom/main.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index 01d5b4bf3f..07d267c06f 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -655,3 +655,22 @@ class serializers: ) -> ifcopenshell_wrapper.SvgSerializer: out_filename = transform_string(out_filename) return ifcopenshell_wrapper.TtlWktSerializer(out_filename, geometry_settings, settings) + + @classmethod + def guess_from_extension(cls, filepath: str): + ext = filepath.split(".")[-1] + mapping = { + "glb": "gltf", + "hdf": "hdf5", + "h5": "hdf5", + "hdf5": "hdf5", + "obj": "obj", + "svg": "svg", + "ttl": "ttl", + "xml": "xml", + "dae": "collada", + } + serializer_name = mapping.get(ext) + if not serializer_name: + raise ValueError(f"No serializer available for .{ext} file") + return getattr(cls, serializer_name) From 544b61b95af10c17851cbdf328a27d84e56048cd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Mar 2025 14:03:02 +0500 Subject: [PATCH 441/476] remove debug print --- src/ifcsverchok/nodes/ifc/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ifcsverchok/nodes/ifc/api.py b/src/ifcsverchok/nodes/ifc/api.py index a4d1f5f452..40abd6587a 100644 --- a/src/ifcsverchok/nodes/ifc/api.py +++ b/src/ifcsverchok/nodes/ifc/api.py @@ -29,7 +29,6 @@ logger = logging.getLogger("sverchok.ifc") def update_usecase(self, context): - print("API - running update usecase!") module_usecase = self.get_module_usecase() if module_usecase: self.generate_node(*module_usecase) @@ -64,7 +63,6 @@ class SvIfcApi(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCor op.tooltip = self.tooltip def process(self): - print("process") module_usecase = self.get_module_usecase() if module_usecase: self.sv_input_names = [i.name for i in self.inputs] From a567af370bec17700ccae7a11016e0e1d9684fbc Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Mar 2025 12:29:30 +0500 Subject: [PATCH 442/476] ifcsverchok - fix missing socket tooltips apparently `tooltip` property doesn't exist but `description` does --- src/ifcsverchok/nodes/ifc/create_project.py | 4 ++-- .../nodes/ifc/quick_project_setup.py | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ifcsverchok/nodes/ifc/create_project.py b/src/ifcsverchok/nodes/ifc/create_project.py index b8f6b3177f..21650767a5 100644 --- a/src/ifcsverchok/nodes/ifc/create_project.py +++ b/src/ifcsverchok/nodes/ifc/create_project.py @@ -29,9 +29,9 @@ class SvIfcCreateProject(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helpe def sv_init(self, context): input_socket = self.inputs.new("SvStringsSocket", "file") - input_socket.tooltip = "ifc file to add the project to" + input_socket.description = "ifc file to add the project to" input_socket = self.inputs.new("SvStringsSocket", "project_name") - input_socket.tooltip = "Project name" + input_socket.description = "Project name" self.outputs.new("SvVerticesSocket", "file") def draw_buttons(self, context, layout): diff --git a/src/ifcsverchok/nodes/ifc/quick_project_setup.py b/src/ifcsverchok/nodes/ifc/quick_project_setup.py index 696d6d6515..6381a47b17 100644 --- a/src/ifcsverchok/nodes/ifc/quick_project_setup.py +++ b/src/ifcsverchok/nodes/ifc/quick_project_setup.py @@ -38,25 +38,25 @@ class SvIfcQuickProjectSetup(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.h def sv_init(self, context): input_socket = self.inputs.new("SvStringsSocket", "filename") - input_socket.tooltip = "Ifc file name" + input_socket.description = "Ifc file name" input_socket = self.inputs.new("SvStringsSocket", "timestring") - input_socket.tooltip = "Timestring, default = current time" + input_socket.description = "Timestring, default = current time" input_socket = self.inputs.new("SvStringsSocket", "organization") - input_socket.tooltip = "Organization" + input_socket.description = "Organization" input_socket = self.inputs.new("SvStringsSocket", "creator") - input_socket.tooltip = "creator" + input_socket.description = "creator" input_socket = self.inputs.new("SvStringsSocket", "schema_identifier") - input_socket.tooltip = "Schema, default = 'IFC4'" + input_socket.description = "Schema, default = 'IFC4'" input_socket = self.inputs.new("SvStringsSocket", "application_version") - input_socket.tooltip = "Application version" + input_socket.description = "Application version" input_socket = self.inputs.new("SvStringsSocket", "timestamp") - input_socket.tooltip = "Timestamp, default = current time" + input_socket.description = "Timestamp, default = current time" input_socket = self.inputs.new("SvStringsSocket", "application") - input_socket.tooltip = "Application, default = 'IfcOpenShell'" + input_socket.description = "Application, default = 'IfcOpenShell'" input_socket = self.inputs.new("SvStringsSocket", "project_globalid") - input_socket.tooltip = "Project GlobalId" + input_socket.description = "Project GlobalId" input_socket = self.inputs.new("SvStringsSocket", "project_name") - input_socket.tooltip = "Project name" + input_socket.description = "Project name" self.outputs.new("SvVerticesSocket", "file") def draw_buttons(self, context, layout): From e075c32a5a7ebe652e19ff58e1f6bbe2203a72d4 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Mar 2025 12:53:13 +0500 Subject: [PATCH 443/476] typing --- .../ifcopenshell/api/__init__.py | 2 +- src/ifcsverchok/__init__.py | 3 ++ src/ifcsverchok/ifcstore.py | 5 +-- src/ifcsverchok/nodes/ifc/add.py | 6 ++-- src/ifcsverchok/nodes/ifc/add_pset.py | 20 +++++++----- .../nodes/ifc/add_spatial_element.py | 1 - src/ifcsverchok/nodes/ifc/api.py | 9 +++--- src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py | 26 +++++++++------- src/ifcsverchok/nodes/ifc/by_guid.py | 1 + src/ifcsverchok/nodes/ifc/by_query.py | 2 +- src/ifcsverchok/nodes/ifc/create_entity.py | 31 +++++++++---------- src/ifcsverchok/nodes/ifc/create_project.py | 1 - src/ifcsverchok/nodes/ifc/create_shape.py | 1 + .../nodes/ifc/quick_project_setup.py | 1 - src/ifcsverchok/nodes/ifc/read_file.py | 2 +- src/ifcsverchok/nodes/ifc/remove.py | 11 ++++++- .../nodes/ifc/select_blender_objects.py | 17 +++++----- src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py | 25 ++++++--------- src/ifcsverchok/nodes/ifc/write_file.py | 3 +- 19 files changed, 90 insertions(+), 77 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index 31e2cff06e..7f798d06da 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -163,7 +163,7 @@ def remove_all_listeners(): post_listeners.clear() -def extract_docs(module, usecase): +def extract_docs(module: str, usecase: str) -> dict[str, Any]: import typing import collections diff --git a/src/ifcsverchok/__init__.py b/src/ifcsverchok/__init__.py index e30a6db7ce..748fc53d86 100644 --- a/src/ifcsverchok/__init__.py +++ b/src/ifcsverchok/__init__.py @@ -161,9 +161,12 @@ class IFC_Sv_UpdateCurrent(bpy.types.Operator): # infra-related spatial structure elements, such as IfcBridge. # https://github.com/IfcOpenShell/IfcOpenShell/pull/2576#discussion_r1016261407 def execute(self, context): + import sverchok.node_tree + self.file = SvIfcStore.purge() node_tree = context.space_data.node_tree if node_tree: + assert isinstance(node_tree, sverchok.node_tree.SverchCustomTree) if self.force_mode or node_tree.sv_process: try: bpy.context.window.cursor_set("WAIT") diff --git a/src/ifcsverchok/ifcstore.py b/src/ifcsverchok/ifcstore.py index 9b036fb9ce..d89ff83830 100644 --- a/src/ifcsverchok/ifcstore.py +++ b/src/ifcsverchok/ifcstore.py @@ -21,7 +21,7 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.representation from ifcopenshell import template -from typing import Union +from typing import Union, Any class SvIfcStore: @@ -30,7 +30,8 @@ class SvIfcStore: schema = None cache = None cache_path = None - id_map = {} + id_map: dict[str, Any] = {} + """Mapping `{node_id: Any}`""" guid_map = {} deleted_ids = set() edited_objs = set() diff --git a/src/ifcsverchok/nodes/ifc/add.py b/src/ifcsverchok/nodes/ifc/add.py index ec312f1de0..418ced8e7e 100644 --- a/src/ifcsverchok/nodes/ifc/add.py +++ b/src/ifcsverchok/nodes/ifc/add.py @@ -38,13 +38,13 @@ class SvIfcAdd(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCor def process(self): self.sv_input_names = ["file", "entity"] - self.file_out = [] - self.entity_out = [] + self.file_out: list[ifcopenshell.file] = [] + self.entity_out: list[ifcopenshell.entity_instance] = [] super().process() self.outputs["file"].sv_set([self.file_out]) self.outputs["entity"].sv_set([self.entity_out]) - def process_ifc(self, file, entity): + def process_ifc(self, file: ifcopenshell.file, entity: ifcopenshell.entity_instance) -> None: self.entity_out.append(file.add(entity)) self.file_out.append(file) diff --git a/src/ifcsverchok/nodes/ifc/add_pset.py b/src/ifcsverchok/nodes/ifc/add_pset.py index 73f5e24d37..683014d27e 100644 --- a/src/ifcsverchok/nodes/ifc/add_pset.py +++ b/src/ifcsverchok/nodes/ifc/add_pset.py @@ -25,6 +25,8 @@ from ifcsverchok.ifcstore import SvIfcStore import bpy import json import ifcopenshell +import ifcopenshell.api +import ifcopenshell.api.pset from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode @@ -80,12 +82,13 @@ class SvIfcAddPset(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIf self.outputs["Entity"].sv_set([element]) - def create(self, name, properties, elements): + def create( + self, name: str, properties: str, elements: list[ifcopenshell.entity_instance] + ) -> list[ifcopenshell.entity_instance]: results = [] for element in elements: - result = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name=name) - ifcopenshell.api.run( - "pset.edit_pset", + result = ifcopenshell.api.pset.add_pset(self.file, product=element, name=name) + ifcopenshell.api.pset.edit_pset( self.file, pset=result, properties=json.loads(properties), @@ -94,13 +97,14 @@ class SvIfcAddPset(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIf results.append(result) return results - def edit(self, name, properties, elements): + def edit( + self, name: str, properties: str, elements: list[ifcopenshell.entity_instance] + ) -> list[ifcopenshell.entity_instance]: result_ids = SvIfcStore.id_map[self.node_id] - results = [] + results: list[ifcopenshell.entity_instance] = [] for result_id in result_ids: result = self.file.by_id(result_id) - ifcopenshell.api.run( - "pset.edit_pset", + ifcopenshell.api.pset.edit_pset( self.file, pset=result, name=name, diff --git a/src/ifcsverchok/nodes/ifc/add_spatial_element.py b/src/ifcsverchok/nodes/ifc/add_spatial_element.py index 8015b4168c..77cb1fba3f 100644 --- a/src/ifcsverchok/nodes/ifc/add_spatial_element.py +++ b/src/ifcsverchok/nodes/ifc/add_spatial_element.py @@ -221,7 +221,6 @@ class SvIfcAddSpatialElement(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.h try: del SvIfcStore.id_map[self.node_id] del self.node_dict[hash(self)] - # print('Node was deleted') except KeyError or AttributeError: pass diff --git a/src/ifcsverchok/nodes/ifc/api.py b/src/ifcsverchok/nodes/ifc/api.py index 40abd6587a..3e6954952d 100644 --- a/src/ifcsverchok/nodes/ifc/api.py +++ b/src/ifcsverchok/nodes/ifc/api.py @@ -24,11 +24,12 @@ from bpy.props import StringProperty, EnumProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode import logging +from typing import Union, Any logger = logging.getLogger("sverchok.ifc") -def update_usecase(self, context): +def update_usecase(self: "SvIfcApi", context: bpy.types.Context) -> None: module_usecase = self.get_module_usecase() if module_usecase: self.generate_node(*module_usecase) @@ -68,12 +69,12 @@ class SvIfcApi(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCor self.sv_input_names = [i.name for i in self.inputs] super().process() - def get_module_usecase(self): + def get_module_usecase(self) -> Union[list[str], None]: usecase = self.inputs["usecase"].sv_get()[0][0] if usecase: return usecase.split(".") - def generate_node(self, module, usecase): + def generate_node(self, module: str, usecase: str) -> None: try: node_data = ifcopenshell.api.extract_docs(module, usecase) except: @@ -92,7 +93,7 @@ class SvIfcApi(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCor self.tooltip = f"{name}: {data['description']}\n" self.tooltip = self.tooltip.strip() - def process_ifc(self, usecase, *setting_values): + def process_ifc(self, usecase: Union[str, None], *setting_values: Any) -> None: if usecase: settings = dict(zip(self.sv_input_names[1:], setting_values)) settings = {k: v for k, v in settings.items() if v != ""} diff --git a/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py b/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py index 1c6f37644d..13a31e485d 100644 --- a/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py +++ b/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py @@ -23,6 +23,8 @@ import bpy import ifcopenshell import ifcsverchok.helper import ifcopenshell.api +import ifcopenshell.api.geometry +import ifcopenshell.util.representation from ifcsverchok.ifcstore import SvIfcStore import bonsai.tool as tool import bonsai.core.geometry as core @@ -40,6 +42,7 @@ from sverchok.data_structure import zip_long_repeat, node_id from sverchok.core.socket_data import sv_get_socket from itertools import chain, cycle +from mathutils import Matrix class SvIfcBMeshToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): @@ -162,9 +165,9 @@ class SvIfcBMeshToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.help self.outputs["Representations"].sv_set(representations) self.outputs["Locations"].sv_set(locations) - def create(self, blender_objects): - representations_ids = [] - locations = [] + def create(self, blender_objects: list[bpy.types.Object]) -> tuple[list[list[list[int]]], list[list[list[Matrix]]]]: + representations_ids: list[list[list[int]]] = [] + locations: list[list[list[Matrix]]] = [] context = self.get_context() for blender_object in blender_objects: bpy.context.view_layer.objects.active = blender_object @@ -183,8 +186,8 @@ class SvIfcBMeshToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.help bpy.ops.mesh.separate( type="LOOSE" ) # This isn't a great solution bc it creates new objects in the scene, thus changing the users model - representations_ids_obj = [] - locations_obj = [] + representations_ids_obj: list[list[int]] = [] + locations_obj: list[list[Matrix]] = [] try: bpy.ops.object.mode_set(mode="OBJECT") except Exception as e: @@ -192,16 +195,16 @@ class SvIfcBMeshToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.help pass for obj in bpy.context.selected_objects: if blender_object.type == "MESH": - representation = ifcopenshell.api.run( - "geometry.add_representation", + representation = ifcopenshell.api.geometry.add_representation( self.file, - should_run_listeners=False, blender_object=obj, geometry=obj.data, context=context, + should_run_listeners=False, ) if not representation: raise Exception("Couldn't create representation. Possibly wrong context.") + assert isinstance(representation, ifcopenshell.entity_instance) representations_ids_obj.append([representation.id()]) locations_obj.append([obj.matrix_world]) @@ -215,7 +218,7 @@ class SvIfcBMeshToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.help bpy.ops.object.select_all(action="DESELECT") return representations_ids, locations - def edit(self): + def edit(self) -> None: if "Representations" not in SvIfcStore.id_map[self.node_id]: return for obj in SvIfcStore.id_map[self.node_id]["Representations"]: @@ -229,7 +232,7 @@ class SvIfcBMeshToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.help del SvIfcStore.id_map[self.node_id]["Locations"] return - def get_context(self): + def get_context(self) -> ifcopenshell.entity_instance: context = ifcopenshell.util.representation.get_context( self.file, self.context_type, self.context_identifier, self.target_view ) @@ -248,7 +251,7 @@ class SvIfcBMeshToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.help SvIfcStore.id_map.setdefault(self.node_id, {}).setdefault("Contexts", []).append(context.id()) return context - def sv_free(self): + def sv_free(self) -> None: try: self.file = SvIfcStore.get_file() if "Representations" in SvIfcStore.id_map[self.node_id]: @@ -273,7 +276,6 @@ class SvIfcBMeshToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.help SvIfcStore.id_map[self.node_id]["Contexts"].remove(context_id) del SvIfcStore.id_map[self.node_id] del self.node_dict[hash(self)] - # print('Node was deleted') except KeyError or AttributeError: pass diff --git a/src/ifcsverchok/nodes/ifc/by_guid.py b/src/ifcsverchok/nodes/ifc/by_guid.py index 65c5196fad..c5012b88fe 100644 --- a/src/ifcsverchok/nodes/ifc/by_guid.py +++ b/src/ifcsverchok/nodes/ifc/by_guid.py @@ -33,6 +33,7 @@ class SvIfcByGuid(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfc n_id: StringProperty(default="") guid: StringProperty(name="Guid(s)", update=updateNode) id_iter = itertools.count() + guids: list[str] def sv_init(self, context): self.inputs.new("SvStringsSocket", "guid").prop_name = "guid" diff --git a/src/ifcsverchok/nodes/ifc/by_query.py b/src/ifcsverchok/nodes/ifc/by_query.py index 703de5dd8d..95f9ca654f 100644 --- a/src/ifcsverchok/nodes/ifc/by_query.py +++ b/src/ifcsverchok/nodes/ifc/by_query.py @@ -42,7 +42,7 @@ class SvIfcByQuery(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIf self.sv_input_names = ["query"] super().process() - def process_ifc(self, query): + def process_ifc(self, query: str) -> None: selector = ifcopenshell.util.selector.Selector() self.outputs["Entity"].sv_set([selector.parse(self.file, query)]) diff --git a/src/ifcsverchok/nodes/ifc/create_entity.py b/src/ifcsverchok/nodes/ifc/create_entity.py index 463154486c..d5794fa12e 100644 --- a/src/ifcsverchok/nodes/ifc/create_entity.py +++ b/src/ifcsverchok/nodes/ifc/create_entity.py @@ -20,6 +20,9 @@ import bpy from mathutils import Matrix import ifcopenshell import ifcopenshell.api +import ifcopenshell.api.geometry +import ifcopenshell.api.root +import ifcopenshell.util.schema import ifcsverchok.helper from ifcsverchok.ifcstore import SvIfcStore @@ -149,21 +152,20 @@ class SvIfcCreateEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper self.outputs["Entities"].sv_set(entities) - def create(self, index=None): - entities_ids = [] + def create(self, index=None) -> list[list[int]]: + entities_ids: list[list[int]] = [] iterator1 = range(len(self.names)) if index is not None: iterator1 = [index[0]] for i in iterator1: group = self.names[i] - group_entities_ids = [] + group_entities_ids: list[int] = [] iterator2 = range(len(group)) if index is not None: iterator2 = [index[1]] for j in iterator2: try: - entity = ifcopenshell.api.run( - "root.create_entity", + entity = ifcopenshell.api.root.create_entity( self.file, ifc_class=self.ifc_class, name=self.names[i][j], @@ -172,8 +174,7 @@ class SvIfcCreateEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper try: for repr in self.representations[i][j]: if repr: - ifcopenshell.api.run( - "geometry.assign_representation", + ifcopenshell.api.geometry.assign_representation( self.file, product=entity, representation=repr, @@ -183,8 +184,7 @@ class SvIfcCreateEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper try: for loc in self.locations[i][j]: if isinstance(loc, Matrix): - ifcopenshell.api.run( - "geometry.edit_object_placement", + ifcopenshell.api.geometry.edit_object_placement( self.file, product=entity, matrix=loc, @@ -198,11 +198,11 @@ class SvIfcCreateEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper entities_ids.append(group_entities_ids) return entities_ids - def edit(self): - entities_ids = [] + def edit(self) -> list[list[int]]: + entities_ids: list[list[int]] = [] id_map_copy = SvIfcStore.id_map[self.node_id].copy() for i, group in enumerate(self.names): - group_entities_ids = [] + group_entities_ids: list[int] = [] for j, _ in enumerate(group): try: step_id = id_map_copy[i][j] @@ -219,8 +219,7 @@ class SvIfcCreateEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper if repr and repr.is_a("IfcProductDefinitionShape"): entity.Representation = repr elif repr: - ifcopenshell.api.run( - "geometry.assign_representation", + ifcopenshell.api.geometry.assign_representation( self.file, product=entity, representation=repr, @@ -230,8 +229,7 @@ class SvIfcCreateEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper try: for loc in self.locations[i][j]: if isinstance(loc, Matrix): - ifcopenshell.api.run( - "geometry.edit_object_placement", + ifcopenshell.api.geometry.edit_object_placement( self.file, product=entity, matrix=loc, @@ -263,7 +261,6 @@ class SvIfcCreateEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper try: del SvIfcStore.id_map[self.node_id] del self.node_dict[hash(self)] - # print('Node was deleted') except KeyError or AttributeError: pass diff --git a/src/ifcsverchok/nodes/ifc/create_project.py b/src/ifcsverchok/nodes/ifc/create_project.py index 21650767a5..c302021ec7 100644 --- a/src/ifcsverchok/nodes/ifc/create_project.py +++ b/src/ifcsverchok/nodes/ifc/create_project.py @@ -38,7 +38,6 @@ class SvIfcCreateProject(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helpe op = layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( "Adds project, unit and context to IFC file" ) - # op.tooltip = self.tooltip def process(self): # file diff --git a/src/ifcsverchok/nodes/ifc/create_shape.py b/src/ifcsverchok/nodes/ifc/create_shape.py index 2795ef2dcd..381fc1e11c 100644 --- a/src/ifcsverchok/nodes/ifc/create_shape.py +++ b/src/ifcsverchok/nodes/ifc/create_shape.py @@ -19,6 +19,7 @@ import bpy import logging import ifcopenshell +import ifcopenshell.geom import ifcsverchok.helper from ifcsverchok.ifcstore import SvIfcStore import bonsai.bim.import_ifc diff --git a/src/ifcsverchok/nodes/ifc/quick_project_setup.py b/src/ifcsverchok/nodes/ifc/quick_project_setup.py index 6381a47b17..b06b07d97d 100644 --- a/src/ifcsverchok/nodes/ifc/quick_project_setup.py +++ b/src/ifcsverchok/nodes/ifc/quick_project_setup.py @@ -63,7 +63,6 @@ class SvIfcQuickProjectSetup(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.h op = layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( "Quick Project Setup: creates Ifc file and sets up a basic project" ) - # op.tooltip = self.tooltip def process(self): self.sv_input_names = [i.name for i in self.inputs] diff --git a/src/ifcsverchok/nodes/ifc/read_file.py b/src/ifcsverchok/nodes/ifc/read_file.py index a6b39763d0..6e2dbfb495 100644 --- a/src/ifcsverchok/nodes/ifc/read_file.py +++ b/src/ifcsverchok/nodes/ifc/read_file.py @@ -38,7 +38,7 @@ class SvIfcReadFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvI self.sv_input_names = ["path"] super().process() - def process_ifc(self, path): + def process_ifc(self, path: str) -> None: guid = ifcopenshell.guid.new() ifcsverchok.helper.ifc_files[guid] = ifcopenshell.open(path) self.outputs["file"].sv_set([[ifcsverchok.helper.ifc_files[guid]]]) diff --git a/src/ifcsverchok/nodes/ifc/remove.py b/src/ifcsverchok/nodes/ifc/remove.py index 58c2d8a5c3..b7af0f4ccc 100644 --- a/src/ifcsverchok/nodes/ifc/remove.py +++ b/src/ifcsverchok/nodes/ifc/remove.py @@ -22,6 +22,7 @@ import ifcsverchok.helper from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode +from typing import Union class SvIfcRemove(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): @@ -36,12 +37,20 @@ class SvIfcRemove(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfc self.outputs.new("SvStringsSocket", "file") def process(self): + file: ifcopenshell.file file = self.inputs["file"].sv_get()[0][0] self.new_file = ifcopenshell.file.from_string(file.wrapped_data.to_string()) self.remove_entity(self.inputs["entity"].sv_get()) self.outputs["file"].sv_set([[self.new_file]]) - def remove_entity(self, entity): + def remove_entity( + self, + entity: Union[ + list[list[ifcopenshell.entity_instance]], + list[ifcopenshell.entity_instance], + ifcopenshell.entity_instance, + ], + ) -> None: if isinstance(entity, (tuple, list)): for e in entity: self.remove_entity(e) diff --git a/src/ifcsverchok/nodes/ifc/select_blender_objects.py b/src/ifcsverchok/nodes/ifc/select_blender_objects.py index 66bd27a4a0..ef88f225ac 100644 --- a/src/ifcsverchok/nodes/ifc/select_blender_objects.py +++ b/src/ifcsverchok/nodes/ifc/select_blender_objects.py @@ -19,11 +19,11 @@ import bpy import ifcopenshell import ifcopenshell.util.selector +import bonsai.tool as tool import ifcsverchok.helper from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode -from bonsai.bim.ifc import IfcStore class SvIfcSelectBlenderObjectsRefresh(bpy.types.Operator): @@ -35,6 +35,7 @@ class SvIfcSelectBlenderObjectsRefresh(bpy.types.Operator): node_name: StringProperty(default="") def execute(self, context): + node: SvIfcSelectBlenderObjects node = bpy.data.node_groups[self.tree_name].nodes[self.node_name] node.process() return {"FINISHED"} @@ -43,7 +44,9 @@ class SvIfcSelectBlenderObjectsRefresh(bpy.types.Operator): class SvIfcSelectBlenderObjects(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcSelectBlenderObjects" bl_label = "IFC Select Blender Objects" + bl_description = "Select Blender objects based on IFC entities." file: StringProperty(name="file", update=updateNode) + # TODO: never used. query: StringProperty(name="query", update=updateNode) def sv_init(self, context): @@ -54,18 +57,18 @@ class SvIfcSelectBlenderObjects(bpy.types.Node, SverchCustomTreeNode, ifcsvercho layout, "node.sv_ifc_select_blender_objects_refresh", icon="FILE_REFRESH", text="Refresh" ) - def process(self): - self.file = IfcStore.get_file() + def process(self) -> None: self.sv_input_names = ["entities"] - self.guids = [] + self.guids: list[str] = [] super().process() for obj in bpy.context.visible_objects: - if not obj.BIMObjectProperties.ifc_definition_id: + element = tool.Ifc.get_entity(obj) + if not element: continue - if self.file.by_id(obj.BIMObjectProperties.ifc_definition_id).GlobalId in self.guids: + if getattr(element, "GlobalId", None) in self.guids: obj.select_set(True) - def process_ifc(self, entities): + def process_ifc(self, entities: ifcopenshell.entity_instance) -> None: self.guids.append(entities.GlobalId) diff --git a/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py b/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py index a2b092aa79..f5cf2f3dd0 100644 --- a/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py +++ b/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py @@ -16,15 +16,14 @@ # You should have received a copy of the GNU General Public License # along with IfcSverchok. If not, see . -from copy import deepcopy import bpy import ifcopenshell import ifcsverchok.helper import ifcopenshell.api +import ifcopenshell.api.context +import ifcopenshell.api.geometry import ifcopenshell.util.representation from ifcsverchok.ifcstore import SvIfcStore -import bonsai.tool as tool -import bonsai.core.geometry as core from bpy.props import StringProperty, EnumProperty, IntProperty, FloatVectorProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode, ensure_min_nesting @@ -142,8 +141,7 @@ class SvIfcSverchokToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.h for obj in geo_data: representations_ids_obj = [] for item in obj: - representation = ifcopenshell.api.run( - "geometry.add_mesh_representation", + representation = ifcopenshell.api.geometry.add_mesh_representation( self.file, should_run_listeners=False, context=self.context, @@ -165,8 +163,7 @@ class SvIfcSverchokToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.h return for obj in SvIfcStore.id_map[self.node_id]["Representations"]: for step_id in obj: - ifcopenshell.api.run( - "geometry.remove_representation", + ifcopenshell.api.geometry.remove_representation( self.file, representation=self.file.by_id(step_id[0]), ) @@ -180,9 +177,8 @@ class SvIfcSverchokToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.h if not context: parent = ifcopenshell.util.representation.get_context(self.file, self.context_type) if not parent: - parent = ifcopenshell.api.run("context.add_context", self.file, context_type=self.context_type) - context = ifcopenshell.api.run( - "context.add_context", + parent = ifcopenshell.api.context.add_context(self.file, context_type=self.context_type) + context = ifcopenshell.api.context.add_context( self.file, context_type=self.context_type, context_identifier=self.context_identifier, @@ -197,8 +193,7 @@ class SvIfcSverchokToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.h self.file = SvIfcStore.get_file() if "Representations" in SvIfcStore.id_map[self.node_id]: for step_id in SvIfcStore.id_map[self.node_id]["Representations"]: - ifcopenshell.api.run( - "geometry.remove_representation", + ifcopenshell.api.geometry.remove_representation( self.file, representation=self.file.by_id(step_id[0]), ) @@ -209,15 +204,13 @@ class SvIfcSverchokToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.h if not self.file.get_inverse(context): if self.file.by_id(context_id).ParentContext: parent = self.file.by_id(context_id).ParentContext - ifcopenshell.api.run("context.remove_context", self.file, context=context) + ifcopenshell.api.context.remove_context(self.file, context=context) if parent: if not self.file.get_inverse(parent): - ifcopenshell.api.run("context.remove_context", self.file, context=parent) - # print("Removed context with step ID: ", context_id) + ifcopenshell.api.context.remove_context(self.file, context=parent) SvIfcStore.id_map[self.node_id]["Contexts"].remove(context_id) del SvIfcStore.id_map[self.node_id] del self.node_dict[hash(self)] - # print('Node was deleted') except KeyError or AttributeError: pass diff --git a/src/ifcsverchok/nodes/ifc/write_file.py b/src/ifcsverchok/nodes/ifc/write_file.py index b9f03c6e1a..e798bf19d2 100644 --- a/src/ifcsverchok/nodes/ifc/write_file.py +++ b/src/ifcsverchok/nodes/ifc/write_file.py @@ -77,7 +77,8 @@ class SvIfcWriteFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.Sv file.write(path) self.outputs["output"].sv_set(f"File written successfully to: {path}.") - def ensure_hirarchy(self, file): + def ensure_hirarchy(self, file: ifcopenshell.file) -> None: + # TODO: same code as ifc.write_file_panel? elements_in_buildings = [] if not 0 <= 0 < len(file.by_type("IfcBuilding")): my_building = ifcopenshell.api.run("root.create_entity", file, ifc_class="IfcBuilding", name="My Building") From ee2b836b84facb6026bfd387588bec24b68bde41 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Mar 2025 18:35:19 +0500 Subject: [PATCH 444/476] ifcsverchok - add/update node descriptions --- src/ifcsverchok/__init__.py | 7 +++++-- src/ifcsverchok/nodes/ifc/add.py | 1 + src/ifcsverchok/nodes/ifc/add_pset.py | 1 + src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py | 3 ++- src/ifcsverchok/nodes/ifc/by_guid.py | 1 + src/ifcsverchok/nodes/ifc/by_id.py | 12 ++++++++++-- src/ifcsverchok/nodes/ifc/create_file.py | 1 + src/ifcsverchok/nodes/ifc/read_file.py | 1 + src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py | 3 ++- src/ifcsverchok/nodes/ifc/write_file.py | 2 +- 10 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/ifcsverchok/__init__.py b/src/ifcsverchok/__init__.py index 748fc53d86..8263c73576 100644 --- a/src/ifcsverchok/__init__.py +++ b/src/ifcsverchok/__init__.py @@ -147,7 +147,10 @@ from ifcsverchok.ifcstore import SvIfcStore class IFC_Sv_UpdateCurrent(bpy.types.Operator): - """Update current Sverchok node tree""" + """Update current Sverchok node tree. + + Will reset transient IFC file. + """ bl_idname = "ifc.sverchok_update_current" bl_label = "Update Current Node Tree" @@ -181,7 +184,7 @@ class IFC_Sv_write_file(bpy.types.Operator): bl_idname = "ifc.write_file_panel" bl_label = "Write File" bl_options = {"REGISTER", "UNDO"} - bl_description = "File path to write to." + bl_description = "Save transient IFC file to the provided path." filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) node_group: bpy.props.StringProperty(default="") diff --git a/src/ifcsverchok/nodes/ifc/add.py b/src/ifcsverchok/nodes/ifc/add.py index 418ced8e7e..dbb8b84dcc 100644 --- a/src/ifcsverchok/nodes/ifc/add.py +++ b/src/ifcsverchok/nodes/ifc/add.py @@ -27,6 +27,7 @@ from sverchok.data_structure import updateNode class SvIfcAdd(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcAdd" bl_label = "IFC Add" + bl_description = "Add an entity to the provided IFC file." file: StringProperty(name="file", update=updateNode) entity: StringProperty(name="entity", update=updateNode) diff --git a/src/ifcsverchok/nodes/ifc/add_pset.py b/src/ifcsverchok/nodes/ifc/add_pset.py index 683014d27e..70b92b5f17 100644 --- a/src/ifcsverchok/nodes/ifc/add_pset.py +++ b/src/ifcsverchok/nodes/ifc/add_pset.py @@ -36,6 +36,7 @@ from sverchok.data_structure import updateNode, flatten_data class SvIfcAddPset(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcAddPset" bl_label = "IFC Add Pset" + bl_description = "Add/edit a property set for the provided element ids in the transient IFC file." Name: StringProperty( name="Name", description="Name of the property set. Eg. Pset_WallCommon.", diff --git a/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py b/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py index 13a31e485d..6e8cb3df05 100644 --- a/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py +++ b/src/ifcsverchok/nodes/ifc/bmesh_to_ifc.py @@ -48,7 +48,8 @@ from mathutils import Matrix class SvIfcBMeshToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): """ Triggers: BMesh to Ifc Repr - Tooltip: Blender mesh to Ifc Shape Representation + Tooltip: Add Blender mesh objects as IfcShapeRepresentations + to the transient IFC file. """ bl_idname = "SvIfcBMeshToIfcRepr" diff --git a/src/ifcsverchok/nodes/ifc/by_guid.py b/src/ifcsverchok/nodes/ifc/by_guid.py index c5012b88fe..6957d55331 100644 --- a/src/ifcsverchok/nodes/ifc/by_guid.py +++ b/src/ifcsverchok/nodes/ifc/by_guid.py @@ -30,6 +30,7 @@ from sverchok.data_structure import updateNode, flatten_data class SvIfcByGuid(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcByGuid" bl_label = "IFC By Guid" + bl_description = "Get IFC elements by guid from the transient IFC file." n_id: StringProperty(default="") guid: StringProperty(name="Guid(s)", update=updateNode) id_iter = itertools.count() diff --git a/src/ifcsverchok/nodes/ifc/by_id.py b/src/ifcsverchok/nodes/ifc/by_id.py index b42060fcf5..e985f4d0e6 100644 --- a/src/ifcsverchok/nodes/ifc/by_id.py +++ b/src/ifcsverchok/nodes/ifc/by_id.py @@ -19,6 +19,7 @@ import bpy import ifcsverchok.helper +import ifcsverchok.helper as helper from ifcsverchok.ifcstore import SvIfcStore from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode @@ -28,14 +29,21 @@ from sverchok.data_structure import updateNode, flatten_data class SvIfcById(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcById" bl_label = "IFC By Id" + bl_description = "Get IFC elements by step id from the transient IFC file." id: StringProperty( name="Id(s)", update=updateNode, ) def sv_init(self, context): - self.inputs.new("SvStringsSocket", "id").prop_name = "id" - self.outputs.new("SvStringsSocket", "Entities") + helper.create_socket(self.inputs, "id", description="Step ids.", data_type="list[list[str]]", prop_name="id") + helper.create_socket( + self.outputs, + "Entities", + description="Entities.", + data_type="list[ifcopenshell.entity_instance]", + prop_name="Entities", + ) def draw_buttons(self, context, layout): layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( diff --git a/src/ifcsverchok/nodes/ifc/create_file.py b/src/ifcsverchok/nodes/ifc/create_file.py index d1671f73cb..f15128d711 100644 --- a/src/ifcsverchok/nodes/ifc/create_file.py +++ b/src/ifcsverchok/nodes/ifc/create_file.py @@ -44,6 +44,7 @@ class SvIfcCreateFileRefresh(bpy.types.Operator): class SvIfcCreateFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcCreateFile" bl_label = "IFC Create File" + bl_description = "Create a new IFC file." schema: StringProperty(name="schema", update=updateNode, default="IFC4") def sv_init(self, context): diff --git a/src/ifcsverchok/nodes/ifc/read_file.py b/src/ifcsverchok/nodes/ifc/read_file.py index 6e2dbfb495..2a772bd787 100644 --- a/src/ifcsverchok/nodes/ifc/read_file.py +++ b/src/ifcsverchok/nodes/ifc/read_file.py @@ -28,6 +28,7 @@ from sverchok.data_structure import updateNode class SvIfcReadFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): bl_idname = "SvIfcReadFile" bl_label = "IFC Read File" + bl_description = "Read an IFC file from the provided path." path: StringProperty(name="path", update=updateNode) def sv_init(self, context): diff --git a/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py b/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py index f5cf2f3dd0..f9a6701432 100644 --- a/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py +++ b/src/ifcsverchok/nodes/ifc/sverchok_to_ifc.py @@ -32,7 +32,8 @@ from sverchok.data_structure import updateNode, ensure_min_nesting class SvIfcSverchokToIfcRepr(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): """ Triggers: Sv to Ifc Repr - Tooltip: Sverchok geometry to Ifc Shape Representation + Tooltip: Add Sverchok geometry as IfcShapeRepresentations + to the transient IFC file. """ bl_idname = "SvIfcSverchokToIfcRepr" diff --git a/src/ifcsverchok/nodes/ifc/write_file.py b/src/ifcsverchok/nodes/ifc/write_file.py index e798bf19d2..aac5afbe31 100644 --- a/src/ifcsverchok/nodes/ifc/write_file.py +++ b/src/ifcsverchok/nodes/ifc/write_file.py @@ -31,7 +31,7 @@ from sverchok.data_structure import updateNode, flatten_data class SvIfcWriteFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCore): """ Triggers: Ifc write to file - Tooltip: Write active Sverchok Ifc file to path + Tooltip: Write transient Ifc file to path """ def refresh_node_local(self, context): From a98a7ac8569506d6b23dc0ee01901a62c5c4d665 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Mar 2025 18:36:50 +0500 Subject: [PATCH 445/476] ifcsverchok - add some sockets descriptions, specify types --- src/ifcsverchok/helper.py | 35 ++++++++++++++++++- src/ifcsverchok/nodes/ifc/add.py | 25 ++++++++++--- src/ifcsverchok/nodes/ifc/add_pset.py | 27 +++++++++++--- src/ifcsverchok/nodes/ifc/api.py | 18 ++++++++-- src/ifcsverchok/nodes/ifc/by_guid.py | 9 +++-- src/ifcsverchok/nodes/ifc/create_entity.py | 3 +- src/ifcsverchok/nodes/ifc/create_file.py | 14 ++++++-- src/ifcsverchok/nodes/ifc/create_project.py | 16 ++++++--- .../nodes/ifc/quick_project_setup.py | 10 +++++- src/ifcsverchok/nodes/ifc/read_file.py | 9 +++-- src/ifcsverchok/nodes/ifc/remove.py | 24 +++++++++++-- .../nodes/ifc/select_blender_objects.py | 9 ++++- src/ifcsverchok/nodes/ifc/write_file.py | 21 +++++++++-- 13 files changed, 188 insertions(+), 32 deletions(-) diff --git a/src/ifcsverchok/helper.py b/src/ifcsverchok/helper.py index 61b2b5de45..748afe1081 100644 --- a/src/ifcsverchok/helper.py +++ b/src/ifcsverchok/helper.py @@ -18,8 +18,11 @@ import bpy import ifcopenshell +import sverchok.core.sockets from sverchok.data_structure import zip_long_repeat -from typing import Any +from typing import Any, Union, TypeVar + +T_socket = TypeVar("T_socket", bound=sverchok.core.sockets.SvSocketCommon) ifc_files: dict[str, ifcopenshell.file] = {} @@ -28,6 +31,14 @@ class SvIfcCore: sv_input_names: list[str] def process(self) -> None: + """Process inputs from `self.sv_input_names` and call `process_ifc()`. + + For `process()` inputs are supposed to be double nested. + E.g. file input should have type `list[list[ifcopenshell.file]]`. + + Similarly, outputs should also be double nested, + so they can be easily passed as inputs to other nodes. + """ sv_inputs_nested = [] for name in self.sv_input_names: sv_inputs_nested.append(self.inputs[name].sv_get()) @@ -38,3 +49,25 @@ class SvIfcCore: def process_ifc(self, *args: Any, **kwargs: Any) -> None: raise NotImplementedError + + +def create_socket( + inputs_or_outputs: Union[bpy.types.NodeInputs, bpy.types.NodeOutputs], + name: str, + *, + description: str = "", + data_type: str = "", + prop_name: str = "", + socket_type: type[T_socket] = sverchok.core.sockets.SvStringsSocket, +) -> T_socket: + socket = inputs_or_outputs.new(socket_type.bl_idname, name) + assert isinstance(socket, socket_type) + + if data_type: + data_type = f"Type: `{data_type}`." + description = "\n\n".join(filter(None, [description, data_type])) + socket.description = description + + if prop_name: + socket.prop_name = prop_name + return socket diff --git a/src/ifcsverchok/nodes/ifc/add.py b/src/ifcsverchok/nodes/ifc/add.py index dbb8b84dcc..5aab0da1f1 100644 --- a/src/ifcsverchok/nodes/ifc/add.py +++ b/src/ifcsverchok/nodes/ifc/add.py @@ -19,6 +19,7 @@ import bpy import ifcopenshell import ifcsverchok.helper +import ifcsverchok.helper as helper from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode @@ -32,10 +33,26 @@ class SvIfcAdd(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCor entity: StringProperty(name="entity", update=updateNode) def sv_init(self, context): - self.inputs.new("SvStringsSocket", "file").prop_name = "file" - self.inputs.new("SvStringsSocket", "entity").prop_name = "entity" - self.outputs.new("SvStringsSocket", "file") - self.outputs.new("SvStringsSocket", "entity") + helper.create_socket( + self.inputs, + "file", + description="File to add entity to.", + data_type="list[list[ifcopenshell.file]]", + prop_name="file", + ) + helper.create_socket( + self.inputs, + "entity", + description="Entity to add to file.", + data_type="list[list[ifcopenshell.entity_instance]]", + prop_name="entity", + ) + helper.create_socket( + self.outputs, "file", description="File with added entity.", data_type="list[list[ifcopenshell.file]]" + ) + helper.create_socket( + self.outputs, "entity", description="Added entity.", data_type="list[list[ifcopenshell.entity_instance]]" + ) def process(self): self.sv_input_names = ["file", "entity"] diff --git a/src/ifcsverchok/nodes/ifc/add_pset.py b/src/ifcsverchok/nodes/ifc/add_pset.py index 70b92b5f17..4e6f0d7674 100644 --- a/src/ifcsverchok/nodes/ifc/add_pset.py +++ b/src/ifcsverchok/nodes/ifc/add_pset.py @@ -19,10 +19,11 @@ import bpy import ifcopenshell +import sverchok.core.sockets import ifcsverchok.helper +import ifcsverchok.helper as helper from ifcsverchok.ifcstore import SvIfcStore -import bpy import json import ifcopenshell import ifcopenshell.api @@ -52,10 +53,26 @@ class SvIfcAddPset(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIf Elements: StringProperty(name="Element Ids", update=updateNode) def sv_init(self, context): - self.inputs.new("SvStringsSocket", "Name").prop_name = "Name" - self.inputs.new("SvTextSocket", "Properties").prop_name = "Properties" - self.inputs.new("SvStringsSocket", "Elements").prop_name = "Elements" - self.outputs.new("SvStringsSocket", "Entity") + helper.create_socket( + self.inputs, "Name", description="Name of the property set.", data_type="list[list[str]]", prop_name="Name" + ) + helper.create_socket( + self.inputs, + "Properties", + description="Properties in a JSON format.", + data_type="list[list[str]]", + prop_name="Properties", + socket_type=sverchok.core.sockets.SvTextSocket, + ) + helper.create_socket( + self.inputs, "Elements", description="Element Ids.", data_type="list[list[str]]", prop_name="Elements" + ) + helper.create_socket( + self.outputs, + "Entity", + description="Added/edited psets.", + data_type="list[list[ifcopenshell.entity_instance]]]", + ) def draw_buttons(self, context, layout): layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( diff --git a/src/ifcsverchok/nodes/ifc/api.py b/src/ifcsverchok/nodes/ifc/api.py index 3e6954952d..af0b8766dd 100644 --- a/src/ifcsverchok/nodes/ifc/api.py +++ b/src/ifcsverchok/nodes/ifc/api.py @@ -19,7 +19,9 @@ import bpy import ifcopenshell import ifcopenshell.api +import sverchok.core.sockets import ifcsverchok.helper +import ifcsverchok.helper as helper from bpy.props import StringProperty, EnumProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode @@ -56,8 +58,20 @@ class SvIfcApi(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfcCor usecase: StringProperty(name="Usecase", update=update_usecase) def sv_init(self, context): - self.inputs.new("SvStringsSocket", "usecase").prop_name = "usecase" - self.outputs.new("SvVerticesSocket", "file") + helper.create_socket( + self.inputs, + "usecase", + description="Usecase to run.", + data_type="list[list[str]]", + prop_name="usecase", + ) + helper.create_socket( + self.outputs, + "file", + description="Use case output.", + data_type="list[Any]", + socket_type=sverchok.core.sockets.SvVerticesSocket, + ) def draw_buttons(self, context, layout): op = layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False) diff --git a/src/ifcsverchok/nodes/ifc/by_guid.py b/src/ifcsverchok/nodes/ifc/by_guid.py index 6957d55331..43bea96c84 100644 --- a/src/ifcsverchok/nodes/ifc/by_guid.py +++ b/src/ifcsverchok/nodes/ifc/by_guid.py @@ -21,6 +21,7 @@ import itertools import bpy import ifcopenshell import ifcsverchok.helper +import ifcsverchok.helper as helper from ifcsverchok.ifcstore import SvIfcStore from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode @@ -37,8 +38,12 @@ class SvIfcByGuid(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfc guids: list[str] def sv_init(self, context): - self.inputs.new("SvStringsSocket", "guid").prop_name = "guid" - self.outputs.new("SvStringsSocket", "Entities") + helper.create_socket( + self.inputs, "guid", description="Entities guids.", data_type="list[list[str]]", prop_name="guid" + ) + helper.create_socket( + self.outputs, "Entities", description="Entities", data_type="list[list[ifcopenshell.entity_instance]]" + ) def draw_buttons(self, context, layout): layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( diff --git a/src/ifcsverchok/nodes/ifc/create_entity.py b/src/ifcsverchok/nodes/ifc/create_entity.py index d5794fa12e..0bfb88b786 100644 --- a/src/ifcsverchok/nodes/ifc/create_entity.py +++ b/src/ifcsverchok/nodes/ifc/create_entity.py @@ -24,6 +24,7 @@ import ifcopenshell.api.geometry import ifcopenshell.api.root import ifcopenshell.util.schema import ifcsverchok.helper +import ifcsverchok.helper as helper from ifcsverchok.ifcstore import SvIfcStore from bpy.props import StringProperty, BoolProperty @@ -79,7 +80,7 @@ class SvIfcCreateEntity(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper self.inputs.new("SvStringsSocket", "Representations").prop_name = "Representations" self.inputs.new("SvMatrixSocket", "Locations").is_mandatory = False # self.inputs.new("SvStringsSocket", "Properties").prop_name = "Properties" - self.outputs.new("SvStringsSocket", "Entities") + helper.create_socket(self.outputs, "Entities", description="Created entities ids.", data_type="list[list[int]]") self.node_dict[hash(self)] = {} def draw_buttons(self, context, layout): diff --git a/src/ifcsverchok/nodes/ifc/create_file.py b/src/ifcsverchok/nodes/ifc/create_file.py index f15128d711..50a77a5198 100644 --- a/src/ifcsverchok/nodes/ifc/create_file.py +++ b/src/ifcsverchok/nodes/ifc/create_file.py @@ -20,6 +20,8 @@ import bpy import ifcopenshell import ifcopenshell.guid import ifcsverchok.helper +import ifcsverchok.helper as helper +import sverchok.core.sockets from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode @@ -48,8 +50,16 @@ class SvIfcCreateFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.S schema: StringProperty(name="schema", update=updateNode, default="IFC4") def sv_init(self, context): - self.inputs.new("SvStringsSocket", "schema").prop_name = "schema" - self.outputs.new("SvVerticesSocket", "file") + helper.create_socket( + self.inputs, "schema", description="IFC schema to use.", data_type="str", prop_name="schema" + ) + helper.create_socket( + self.outputs, + "file", + description="Opened IFC file.", + data_type="list[list[ifcopenshell.file]]", + socket_type=sverchok.core.sockets.SvVerticesSocket, + ) def draw_buttons(self, context, layout): self.wrapper_tracked_ui_draw_op(layout, "node.sv_ifc_create_file_refresh", icon="FILE_REFRESH", text="Refresh") diff --git a/src/ifcsverchok/nodes/ifc/create_project.py b/src/ifcsverchok/nodes/ifc/create_project.py index c302021ec7..d228267d88 100644 --- a/src/ifcsverchok/nodes/ifc/create_project.py +++ b/src/ifcsverchok/nodes/ifc/create_project.py @@ -20,6 +20,7 @@ import bpy import ifcopenshell import ifcopenshell.api import ifcsverchok.helper +import ifcsverchok.helper as helper from sverchok.node_tree import SverchCustomTreeNode @@ -28,11 +29,16 @@ class SvIfcCreateProject(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helpe bl_label = "IFC Create Project" def sv_init(self, context): - input_socket = self.inputs.new("SvStringsSocket", "file") - input_socket.description = "ifc file to add the project to" - input_socket = self.inputs.new("SvStringsSocket", "project_name") - input_socket.description = "Project name" - self.outputs.new("SvVerticesSocket", "file") + helper.create_socket( + self.inputs, "file", description="IFC file to add the project to", data_type="list[list[ifcopenshell.file]]" + ) + helper.create_socket(self.inputs, "project_name", description="Project name", data_type="list[list[str]]") + helper.create_socket( + self.outputs, + "file", + description="IFC file with the project added", + data_type="list[list[ifcopenshell.file]]", + ) def draw_buttons(self, context, layout): op = layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( diff --git a/src/ifcsverchok/nodes/ifc/quick_project_setup.py b/src/ifcsverchok/nodes/ifc/quick_project_setup.py index b06b07d97d..49c621d8ba 100644 --- a/src/ifcsverchok/nodes/ifc/quick_project_setup.py +++ b/src/ifcsverchok/nodes/ifc/quick_project_setup.py @@ -20,7 +20,9 @@ from email.mime import application import bpy import ifcopenshell +import sverchok.core.sockets import ifcsverchok.helper +import ifcsverchok.helper as helper from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode @@ -57,7 +59,13 @@ class SvIfcQuickProjectSetup(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.h input_socket.description = "Project GlobalId" input_socket = self.inputs.new("SvStringsSocket", "project_name") input_socket.description = "Project name" - self.outputs.new("SvVerticesSocket", "file") + helper.create_socket( + self.outputs, + "file", + description="New IFC file with the project added.", + data_type="list[list[ifcopenshell.file]]", + socket_type=sverchok.core.sockets.SvVerticesSocket, + ) def draw_buttons(self, context, layout): op = layout.operator("node.sv_ifc_tooltip", text="", icon="QUESTION", emboss=False).tooltip = ( diff --git a/src/ifcsverchok/nodes/ifc/read_file.py b/src/ifcsverchok/nodes/ifc/read_file.py index 2a772bd787..ab754d7278 100644 --- a/src/ifcsverchok/nodes/ifc/read_file.py +++ b/src/ifcsverchok/nodes/ifc/read_file.py @@ -20,6 +20,7 @@ import bpy import ifcopenshell import ifcopenshell.guid import ifcsverchok.helper +import ifcsverchok.helper as helper from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode @@ -32,8 +33,12 @@ class SvIfcReadFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvI path: StringProperty(name="path", update=updateNode) def sv_init(self, context): - self.inputs.new("SvStringsSocket", "path").prop_name = "path" - self.outputs.new("SvVerticesSocket", "file") + helper.create_socket( + self.inputs, "path", description="Path to IFC file.", data_type="list[list[str]]", prop_name="path" + ) + helper.create_socket( + self.outputs, "file", description="Opened IFC file.", data_type="list[list[ifcopenshell.file]]" + ) def process(self): self.sv_input_names = ["path"] diff --git a/src/ifcsverchok/nodes/ifc/remove.py b/src/ifcsverchok/nodes/ifc/remove.py index b7af0f4ccc..2e76d28896 100644 --- a/src/ifcsverchok/nodes/ifc/remove.py +++ b/src/ifcsverchok/nodes/ifc/remove.py @@ -19,6 +19,7 @@ import bpy import ifcopenshell import ifcsverchok.helper +import ifcsverchok.helper as helper from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode @@ -32,9 +33,26 @@ class SvIfcRemove(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.SvIfc entity: StringProperty(name="entity", update=updateNode) def sv_init(self, context): - self.inputs.new("SvStringsSocket", "file").prop_name = "file" - self.inputs.new("SvStringsSocket", "entity").prop_name = "entity" - self.outputs.new("SvStringsSocket", "file") + helper.create_socket( + self.inputs, + "file", + description="IFC file to remove entity from.", + data_type="list[list[ifcopenshell.file]]", + prop_name="file", + ) + helper.create_socket( + self.inputs, + "entity", + description="Entity to remove from IFC file.", + data_type="list[list[ifcopenshell.entity_instance]]", + prop_name="entity", + ) + helper.create_socket( + self.outputs, + "file", + description="New IFC file with the entity removed.", + data_type="list[list[ifcopenshell.file]]", + ) def process(self): file: ifcopenshell.file diff --git a/src/ifcsverchok/nodes/ifc/select_blender_objects.py b/src/ifcsverchok/nodes/ifc/select_blender_objects.py index ef88f225ac..5067db7ccf 100644 --- a/src/ifcsverchok/nodes/ifc/select_blender_objects.py +++ b/src/ifcsverchok/nodes/ifc/select_blender_objects.py @@ -21,6 +21,7 @@ import ifcopenshell import ifcopenshell.util.selector import bonsai.tool as tool import ifcsverchok.helper +import ifcsverchok.helper as helper from bpy.props import StringProperty from sverchok.node_tree import SverchCustomTreeNode from sverchok.data_structure import updateNode @@ -50,7 +51,13 @@ class SvIfcSelectBlenderObjects(bpy.types.Node, SverchCustomTreeNode, ifcsvercho query: StringProperty(name="query", update=updateNode) def sv_init(self, context): - self.inputs.new("SvStringsSocket", "entities").prop_name = "entities" + helper.create_socket( + self.inputs, + "entities", + description="IFC entities to select Bonsai Blender objects for (selects only objects by matching GlobalId).", + data_type="list[list[ifcopenshell.entity_instance]]", + prop_name="entities", + ) def draw_buttons(self, context, layout): self.wrapper_tracked_ui_draw_op( diff --git a/src/ifcsverchok/nodes/ifc/write_file.py b/src/ifcsverchok/nodes/ifc/write_file.py index aac5afbe31..b277ccadf3 100644 --- a/src/ifcsverchok/nodes/ifc/write_file.py +++ b/src/ifcsverchok/nodes/ifc/write_file.py @@ -22,6 +22,7 @@ import ifcopenshell import ifcopenshell.api import ifcopenshell.util.element import ifcsverchok.helper +import ifcsverchok.helper as helper from ifcsverchok.ifcstore import SvIfcStore from bpy.props import StringProperty, BoolProperty from sverchok.node_tree import SverchCustomTreeNode @@ -39,7 +40,9 @@ class SvIfcWriteFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.Sv self.process() self.refresh_local = False - refresh_local: BoolProperty(name="Write", description="Write to file", update=refresh_node_local) + refresh_local: BoolProperty( + name="Write", description="Write to file when changed to True.", update=refresh_node_local + ) bl_idname = "SvIfcWriteFile" bl_label = "IFC Write File" @@ -50,8 +53,20 @@ class SvIfcWriteFile(bpy.types.Node, SverchCustomTreeNode, ifcsverchok.helper.Sv ) def sv_init(self, context): - self.inputs.new("SvStringsSocket", "path").prop_name = "path" - self.outputs.new("SvStringsSocket", "output") + helper.create_socket( + self.inputs, + "path", + description="File path to write to. Can be relative.", + data_type="str", + prop_name="path", + ) + helper.create_socket( + self.outputs, + "output", + description="Node result output message.", + data_type="str", + prop_name="output", + ) def draw_buttons(self, context, layout): row = layout.row(align=True) From b5c062415d252f315335c295add421fb8c724ced Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 21 Mar 2025 18:38:14 +0500 Subject: [PATCH 446/476] ifcsverchok.helper - add debug get_selected_nodes method --- src/ifcsverchok/helper.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ifcsverchok/helper.py b/src/ifcsverchok/helper.py index 748afe1081..411809e025 100644 --- a/src/ifcsverchok/helper.py +++ b/src/ifcsverchok/helper.py @@ -51,6 +51,21 @@ class SvIfcCore: raise NotImplementedError +def get_selected_nodes() -> list[bpy.types.Node]: + """Get nodes selected in the currently opened editor. + + Mainly for debugging. + """ + screen = bpy.context.screen + assert screen + area = next(a for a in screen.areas if a.type == "NODE_EDITOR") + space = area.spaces.active + assert isinstance(space, bpy.types.SpaceNodeEditor) + node_tree = space.node_tree + assert node_tree + return [n for n in node_tree.nodes if n.select] + + def create_socket( inputs_or_outputs: Union[bpy.types.NodeInputs, bpy.types.NodeOutputs], name: str, From c80c1e9e15d461739c4be470c19e207e08ad2ce7 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Fri, 21 Mar 2025 10:34:33 -0500 Subject: [PATCH 447/476] fix #5709 - ensure REFLECTED_PLAN_VIEW camera has (-1,-1,-1) scale. --- src/bonsai/bonsai/bim/module/geometry/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index 14f201e2f1..f6ecfd2cf8 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -19,6 +19,7 @@ import bpy from . import ui, prop, operator from bpy.app.handlers import persistent +import ifcopenshell.util.element classes = ( operator.AddCurvelikeItem, @@ -103,8 +104,13 @@ def block_scale(scene: bpy.types.Scene) -> None: if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active): if isinstance(obj, bpy.types.Object) and tool.Blender.get_ifc_definition_id(obj): - if obj.scale != (1, 1, 1): - obj.scale = (1, 1, 1) + if obj.type == 'CAMERA': + camera = tool.Ifc.get_entity(obj) + if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW": + obj.scale = (-1, -1, -1) + else: + if obj.scale != (1, 1, 1): + obj.scale = (1, 1, 1) elif isinstance(obj, bpy.types.Mesh) and tool.Geometry.get_mesh_props(obj).ifc_definition_id: if obj.scale != (1, 1, 1): obj.scale = (1, 1, 1) From 25b7003d2d15326dcc760cb3a1e8a6fa2c2092dd Mon Sep 17 00:00:00 2001 From: sebjf Date: Fri, 21 Mar 2025 17:52:40 +0000 Subject: [PATCH 448/476] Issue 6385 - Delete IfcGeom::Elements as soon as possible (#6411) * ISSUE #6385 Delete iterator elements as soon as they are moved past * ISSUE #6385 added loop to destructor to clean up initialised elements not yet disposed of by the iterator --- src/ifcgeom/Iterator.h | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/ifcgeom/Iterator.h b/src/ifcgeom/Iterator.h index e6e9dd42b0..199566bc14 100644 --- a/src/ifcgeom/Iterator.h +++ b/src/ifcgeom/Iterator.h @@ -663,6 +663,10 @@ namespace IfcGeom { /// Use get() to retrieve the created geometry. const IfcUtil::IfcBaseClass* next() { using std::chrono::high_resolution_clock; + + delete *task_result_iterator_; + delete *native_task_result_iterator_; + if (num_threads_ != 1) { if (!wait_for_element()) { Logger::SetProduct(boost::none); @@ -885,19 +889,16 @@ namespace IfcGeom { init_future_.wait(); } } - - if (settings_.get().get() != ifcopenshell::geometry::settings::NATIVE) { - for (auto& p : all_processed_native_elements_) { - delete p; - } - } for (auto& k : kernel_pool) { delete k; } - - for (auto& p : all_processed_elements_) { - delete p; + + if (task_result_ptr_initialized) { + while (task_result_iterator_ != --all_processed_elements_.end()) { + delete *task_result_iterator_++; + delete *native_task_result_iterator_++; + } } delete converter_; From 775106d0a0c6ba030812fab1cf5c1ec63ea2234b Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Fri, 21 Mar 2025 23:37:30 +0000 Subject: [PATCH 449/476] Place drawings on sheets When adding a drawing to a sheet, place to the right of the last drawing if it fits on the sheet - otherwise start a new row below all existing drawings. This means that new drawings are no longer just piled up on top of each other at the top-left, but it does mean that new rows of drawings are added below the title box when the sheet is full. --- .../bonsai/bim/module/drawing/sheeter.py | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/sheeter.py b/src/bonsai/bonsai/bim/module/drawing/sheeter.py index c5f274610c..93c42bbae5 100644 --- a/src/bonsai/bonsai/bim/module/drawing/sheeter.py +++ b/src/bonsai/bonsai/bim/module/drawing/sheeter.py @@ -32,6 +32,7 @@ from mathutils import Vector import re VIEW_TITLE_OFFSET_Y = 5 +DRAWING_PADDING = 10 DEFAULT_POSITION = Vector((30, 30)) SVG = "{http://www.w3.org/2000/svg}" XLINK = "{http://www.w3.org/1999/xlink}" @@ -113,13 +114,15 @@ class SheetBuilder: view_width = self.convert_to_mm(view_root.attrib.get("width")) view_height = self.convert_to_mm(view_root.attrib.get("height")) + x, y = self.next_drawing_location(layout_root, view_width) + # add background if os.path.isfile(underlay_path): background = ET.SubElement(view, "image") background.attrib["data-type"] = "background" background.attrib["xlink:href"] = os.path.relpath(underlay_path, layout_dir) - background.attrib["x"] = str(DEFAULT_POSITION.x) - background.attrib["y"] = str(DEFAULT_POSITION.y) + background.attrib["x"] = str(x) + background.attrib["y"] = str(y) background.attrib["width"] = str(view_width) background.attrib["height"] = str(view_height) @@ -128,16 +131,49 @@ class SheetBuilder: foreground = ET.SubElement(view, "image") foreground.attrib["data-type"] = "foreground" foreground.attrib["xlink:href"] = os.path.relpath(drawing_path, layout_dir) - foreground.attrib["x"] = str(DEFAULT_POSITION.x) - foreground.attrib["y"] = str(DEFAULT_POSITION.y) + foreground.attrib["x"] = str(x) + foreground.attrib["y"] = str(y) foreground.attrib["width"] = str(view_width) foreground.attrib["height"] = str(view_height) - self.add_view_title( - DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir - ) + self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir) layout_tree.write(layout_path) + def next_drawing_location(self, layout_root: ET.Element, next_width: float) -> list: + titleblocks = layout_root.findall(f'{SVG}g[@data-type="titleblock"]') + drawings = layout_root.findall(f'{SVG}g[@data-type="drawing"]') + + # how wide is the title block frame + try: + titleblock_width = self.convert_to_mm(titleblocks[0][0].attrib.get("width")) + except (IndexError, AttributeError): + titleblock_width = 840.0 + + # where does the last drawing finish + try: + last = drawings[-1][0] + last_width = self.convert_to_mm(last.attrib.get("width")) + last_x = self.convert_to_mm(last.attrib.get("x")) + last_y = self.convert_to_mm(last.attrib.get("y")) + except (IndexError, AttributeError): + return [DEFAULT_POSITION.x, DEFAULT_POSITION.y] + + # check if the new drawing fits in the current row + if last_x + last_width + DRAWING_PADDING + next_width + DEFAULT_POSITION.x < titleblock_width: + return [last_x + last_width + DRAWING_PADDING, last_y] + + # start a new row, find the y + for drawing in drawings: + for image in drawing: + try: + image_y = self.convert_to_mm(image.attrib.get("y")) + image_height = self.convert_to_mm(image.attrib.get("height")) + except AttributeError: + return [DEFAULT_POSITION.x, DEFAULT_POSITION.y] + if image_y + image_height + DRAWING_PADDING > last_y: + last_y = image_y + image_height + DRAWING_PADDING + return [DEFAULT_POSITION.x, last_y] + def update_sheet_drawing_sizes(self, sheet: ifcopenshell.entity_instance) -> None: ET.register_namespace("", "http://www.w3.org/2000/svg") @@ -221,6 +257,8 @@ class SheetBuilder: view_width = self.convert_to_mm(view_root.attrib.get("width")) view_height = self.convert_to_mm(view_root.attrib.get("height")) + x, y = self.next_drawing_location(layout_root, view_width) + view = ET.SubElement(layout_root, "g") view.attrib["data-id"] = str(reference.id()) view.attrib["data-type"] = document.Scope.lower() @@ -229,14 +267,12 @@ class SheetBuilder: foreground = ET.SubElement(view, "image") foreground.attrib["data-type"] = "content" foreground.attrib["xlink:href"] = os.path.relpath(view_path, layout_dir) - foreground.attrib["x"] = str(DEFAULT_POSITION.x) - foreground.attrib["y"] = str(DEFAULT_POSITION.y) + foreground.attrib["x"] = str(x) + foreground.attrib["y"] = str(y) foreground.attrib["width"] = str(view_width) foreground.attrib["height"] = str(view_height) - self.add_view_title( - DEFAULT_POSITION.x, view_height + DEFAULT_POSITION.y + VIEW_TITLE_OFFSET_Y, view, layout_dir - ) + self.add_view_title(x, view_height + y + VIEW_TITLE_OFFSET_Y, view, layout_dir) layout_tree.write(layout_path) def add_view_title(self, x: float, y: float, parent: ET.Element, layout_dir: str) -> None: From 5830573cda281a4c169609202fd78ea27ca9976c Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sat, 22 Mar 2025 09:01:54 +0000 Subject: [PATCH 450/476] Create transform orientation slots (#6215) Setting the default container switches to an appropriate orientation slot if the orientation of this container isn't global. eg. if a building is rotated, selecting to work in a storey will set the orientation to the building orientation. Closes #6128 --- .../bonsai/bim/module/spatial/operator.py | 1 + src/bonsai/bonsai/core/spatial.py | 4 ++ src/bonsai/bonsai/tool/spatial.py | 55 +++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 9d134729f3..61fc4863b2 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -334,6 +334,7 @@ class SetDefaultContainer(bpy.types.Operator): def execute(self, context): core.set_default_container(tool.Spatial, container=tool.Ifc.get().by_id(self.container)) + core.set_orientation_slot(tool.Spatial, container=tool.Ifc.get().by_id(self.container)) return {"FINISHED"} diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index d6097484bf..241e0a3df2 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -121,6 +121,10 @@ def import_spatial_decomposition(spatial: tool.Spatial) -> None: spatial.import_spatial_decomposition() +def set_orientation_slot(spatial: tool.Spatial, container: ifcopenshell.entity_instance) -> None: + spatial.create_orientation_slot(container) + + def contract_container(spatial: tool.Spatial, container: ifcopenshell.entity_instance) -> None: spatial.contract_container(container) spatial.import_spatial_decomposition() diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 4e6f8dc457..181a493c7c 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -38,6 +38,7 @@ import bonsai.core.geometry import bonsai.core.unit import bonsai.tool as tool import json +import numpy as np from math import pi from mathutils import Vector, Matrix from shapely import Polygon @@ -500,6 +501,60 @@ class Spatial(bonsai.core.tool.Spatial): for child in children or []: cls.import_spatial_element(child, level_index + 1) + @classmethod + def create_orientation_slot(cls, container: ifcopenshell.entity_instance) -> None: + active_slot = bpy.context.scene.transform_orientation_slots[0] + placement = container.ObjectPlacement + combined_matrix = ifcopenshell.util.placement.get_local_placement(placement)[:3, :3] + + if np.allclose(combined_matrix, np.eye(3), atol=1e-6): + # this spatial element has global orientation + active_slot.type = "GLOBAL" + return + elif ( + hasattr(container, "Decomposes") + and container.Decomposes + and hasattr(container.Decomposes[0].RelatingObject, "ObjectPlacement") + ): + # this spatial element is part of a decomposition + parent_placement = container.Decomposes[0].RelatingObject.ObjectPlacement + parent_matrix = ifcopenshell.util.placement.get_local_placement(parent_placement)[:3, :3] + if np.allclose(combined_matrix, parent_matrix, atol=1e-6): + # this spatial element has the same orientation as its parent + cls.create_orientation_slot(container=container.Decomposes[0].RelatingObject) + return + + # this spatial element has a unique orientation + orientation_name = container.is_a() + "/" + container.Name + + # stash selected objects + active_object = bpy.context.view_layer.objects.active + selected_objects = list(bpy.context.view_layer.objects.selected) + + # bpy.ops.transform.create_orientation() requires a dummy object + bpy.ops.object.empty_add(type="PLAIN_AXES") + bpy.ops.transform.create_orientation(name=orientation_name, overwrite=True) + active_slot.type = orientation_name + active_slot.custom_orientation.matrix = np.linalg.inv(combined_matrix) + + # delete dummy object + bpy.ops.object.delete() + + # reinstate selected objects + for obj in selected_objects: + obj.select_set(True) + if active_object: + bpy.context.view_layer.objects.active = active_object + + @classmethod + def edit_container_attributes(cls, entity: ifcopenshell.entity_instance) -> None: + # TODO + obj = tool.Ifc.get_object(entity) + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + name = bpy.context.scene.BIMSpatialDecompositionProperties.container_name + if name != entity.Name: + cls.edit_container_name(entity, name) + @classmethod def edit_container_name(cls, container: ifcopenshell.entity_instance, name: str) -> None: tool.Ifc.run("attribute.edit_attributes", product=container, attributes={"Name": name}) From 75fe548bb0fa399a4f9c69f6bb4265e8686aaedc Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sat, 22 Mar 2025 10:17:41 +0000 Subject: [PATCH 451/476] Remove zombie method reintroduced in 5830573 edit_container_attributes() was deleted in e79a85a, but somehow got dragged back in --- src/bonsai/bonsai/tool/spatial.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 181a493c7c..93d0478f4a 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -546,15 +546,6 @@ class Spatial(bonsai.core.tool.Spatial): if active_object: bpy.context.view_layer.objects.active = active_object - @classmethod - def edit_container_attributes(cls, entity: ifcopenshell.entity_instance) -> None: - # TODO - obj = tool.Ifc.get_object(entity) - bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) - name = bpy.context.scene.BIMSpatialDecompositionProperties.container_name - if name != entity.Name: - cls.edit_container_name(entity, name) - @classmethod def edit_container_name(cls, container: ifcopenshell.entity_instance, name: str) -> None: tool.Ifc.run("attribute.edit_attributes", product=container, attributes={"Name": name}) From 328fb8bc0e633d7ada8ddbee76813d81b0cdaf59 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 22 Mar 2025 09:11:41 -0500 Subject: [PATCH 452/476] fix #3731 - round to the nearest foot --- src/bonsai/bonsai/bim/module/drawing/helper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index cbd98cfe5a..eb63b05a05 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -272,6 +272,8 @@ def format_distance( tx_dist += str(frac) + "/" + str(base) if add_inches or frac: tx_dist += '"' + if precision == "12": + tx_dist = str(feet) + "'" else: fmt = "%1.3f" sq_feet = round(value * toInches / inPerFoot, 4) From 9ee947b73047edd71767492b93c8056e155479f9 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Sat, 22 Mar 2025 09:53:15 -0500 Subject: [PATCH 453/476] a better implementation for #3731 - to round to the nearest foot. --- src/bonsai/bonsai/bim/module/drawing/helper.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index eb63b05a05..b1706cffa7 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -202,6 +202,7 @@ def format_distance( base = int(precision) decInches = value * toInches + decFeet = decInches/12 # Separate ft and inches # Unless Inches are the specified Length Unit or unit_fraction is False @@ -272,8 +273,8 @@ def format_distance( tx_dist += str(frac) + "/" + str(base) if add_inches or frac: tx_dist += '"' - if precision == "12": - tx_dist = str(feet) + "'" + if precision == "12" and unit_system == "IMPERIAL": + tx_dist = str(round(decFeet)) + "'" else: fmt = "%1.3f" sq_feet = round(value * toInches / inPerFoot, 4) From f7163b8db838a947987d5cbc275e02f63d45bfdf Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Sat, 22 Mar 2025 19:27:50 +0100 Subject: [PATCH 454/476] schema -> schema_identifier #6414 --- src/ifcpatch/ifcpatch/recipes/ExtractElements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py index e49f1b0af4..e2dfb13866 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py @@ -55,7 +55,7 @@ class Patcher: def patch(self): self.contained_ins: dict[str, set[ifcopenshell.entity_instance]] = {} self.aggregates: dict[str, set[ifcopenshell.entity_instance]] = {} - self.new = ifcopenshell.file(schema=self.file.wrapped_data.schema) + self.new = ifcopenshell.file(schema=self.file.schema_identifier) self.owner_history = None self.reuse_identities: dict[int, ifcopenshell.entity_instance] = {} for owner_history in self.file.by_type("IfcOwnerHistory"): From 212da8b6d6c625520f6cb5acabd24bd4d66cc1a0 Mon Sep 17 00:00:00 2001 From: Massimo Fabbro Date: Tue, 18 Mar 2025 06:10:26 +0100 Subject: [PATCH 455/476] fix #6271 Fix wrong length calculation with Blender Engine for IfcDoor and IfcWindows --- src/ifc5d/ifc5d/IFC4QtoBaseQuantitiesBlender.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ifc5d/ifc5d/IFC4QtoBaseQuantitiesBlender.json b/src/ifc5d/ifc5d/IFC4QtoBaseQuantitiesBlender.json index ac0acbe67e..73bc99dc3b 100644 --- a/src/ifc5d/ifc5d/IFC4QtoBaseQuantitiesBlender.json +++ b/src/ifc5d/ifc5d/IFC4QtoBaseQuantitiesBlender.json @@ -220,7 +220,7 @@ "Area": "get_net_side_area", "Height": "get_height", "Perimeter": "get_rectangular_perimeter", - "Width": "get_length" + "Width": "get_x" } }, "IfcDuctFitting": { @@ -629,7 +629,7 @@ "Area": "get_net_side_area", "Height": "get_height", "Perimeter": "get_rectangular_perimeter", - "Width": "get_length" + "Width": "get_x" } } } From 51eef295c82e2a1fdd0976d8e2dcca72396d3b8d Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Mar 2025 21:12:46 +1100 Subject: [PATCH 456/476] Fix models with crazy contexts (which kills the iterator which is context based) --- src/bonsai/bonsai/bim/import_ifc.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 14fa8a43ff..30c51e6310 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -300,6 +300,20 @@ class IfcImporter: bpy.context.window_manager.progress_end() def process_context_filter(self) -> None: + contexts = self.file.by_type("IfcGeometricRepresentationContext") + if len(contexts) > 100: # Probably something strange happening. Encountered from Revizto. + print("Warning! Excessive contexts were found and merged where applicable.") + uniques = {} + i = 0 + for element in contexts: + data = "-".join([str(a) for a in element]) + if unique := uniques.get(data, None): + ifcopenshell.util.element.replace_element(element, unique) + self.file.remove(element) + i += 1 + else: + uniques[data] = element + print(f"Replaced {i} IfcGeometricRepresentationContext") tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(self.file) tool.Loader.settings.context_settings = tool.Loader.create_settings() tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True) From f1138d1aac9ce8be974a3ffb08e66e2f04c78781 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Mar 2025 21:55:57 +1100 Subject: [PATCH 457/476] Fix regression in point cloud loading in refactoring the new item mode. --- src/bonsai/bonsai/bim/import_ifc.py | 7 ++--- src/bonsai/bonsai/tool/loader.py | 15 +++++---- src/bonsai/test/tool/test_loader.py | 48 +++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index 30c51e6310..d1c158dc8e 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -761,12 +761,9 @@ class IfcImporter: def create_pointclouds(self, products: set[ifcopenshell.entity_instance]) -> set[ifcopenshell.entity_instance]: result = set() for product in products: - representation = self.get_pointcloud_representation(product) - if representation is not None: - pointcloud = self.create_pointcloud(product, representation) - if pointcloud is not None: + if representation := self.get_pointcloud_representation(product): + if pointcloud := self.create_pointcloud(product, representation): result.add(pointcloud) - return result def create_pointcloud( diff --git a/src/bonsai/bonsai/tool/loader.py b/src/bonsai/bonsai/tool/loader.py index 91cfbc4629..0e8dee0305 100644 --- a/src/bonsai/bonsai/tool/loader.py +++ b/src/bonsai/bonsai/tool/loader.py @@ -766,16 +766,15 @@ class Loader(bonsai.core.tool.Loader): coords = None if item.is_a("IfcCartesianPointList3D"): # PointCloud.c coords = np.array(item.CoordList) - vertex_list.extend(Vector(list(coordinates)) * unit_scale for coordinates in item.CoordList) # Is it ever used? In IFC4+ PointCloud is requiring 3D list, before IFC4 there were no coord lists at all. elif item.is_a("IfcCartesianPointList2D"): - vertex_list.extend(Vector(list(coordinates)).to_3d() * unit_scale for coordinates in item.CoordList) - elif item.is_a("IfcPoint"): # Point - if item.is_a("IfcCartesianPoint"): - vertex_list.append(Vector(list(item.Coordinates)) * unit_scale) - else: - # TODO: implement non cartesian point vertices. - continue + coords = np.array(item.CoordList) + coords = np.column_stack((coords, np.zeros(coords.shape[0]))) + elif item.is_a("IfcCartesianPoint"): # Point + coord = np.array(item.Coordinates) + if len(coord) == 2: + coord = np.append(coord, (0.0,)) + coords = np.array((coord,)) else: assert False assert coords is not None diff --git a/src/bonsai/test/tool/test_loader.py b/src/bonsai/test/tool/test_loader.py index 30ef9e22b9..094fa8648f 100644 --- a/src/bonsai/test/tool/test_loader.py +++ b/src/bonsai/test/tool/test_loader.py @@ -549,3 +549,51 @@ class TestSetupActiveBsddClassification(NewFile): def test_set_load_and_set_active_bsdd_ifc4x3(self): self.run_test("IFC4X3") + + +class TestCreatePointCloudMesh(NewFile): + def test_cartesian_point_list_3d(self): + bpy.ops.bim.create_project() + ifc_file = tool.Ifc.get() + coords = ((1., 2., 3.), (4., 5., 6.)) + item = ifc_file.createIfcCartesianPointList3D(coords) + rep = ifc_file.createIfcShapeRepresentation(Items=[item]) + mesh = subject.create_point_cloud_mesh(rep) + assert len(mesh.vertices) == 2 + verts = np.array([v.co for v in mesh.vertices]) + assert np.allclose(verts, np.array([np.array(c) for c in coords])) + + def test_cartesian_point_list_2d(self): + bpy.ops.bim.create_project() + ifc_file = tool.Ifc.get() + coords = ((1., 2.), (4., 5.)) + coords3d = ((1., 2., 0.), (4., 5., 0.)) + item = ifc_file.createIfcCartesianPointList2D(coords) + rep = ifc_file.createIfcShapeRepresentation(Items=[item]) + mesh = subject.create_point_cloud_mesh(rep) + assert len(mesh.vertices) == 2 + verts = np.array([v.co for v in mesh.vertices]) + assert np.allclose(verts, np.array([np.array(c) for c in coords3d])) + + def test_point_3d(self): + bpy.ops.bim.create_project() + ifc_file = tool.Ifc.get() + coords = ((1., 2., 0.),) + item = ifc_file.createIfcCartesianPoint(Coordinates=coords[0]) + rep = ifc_file.createIfcShapeRepresentation(Items=[item]) + mesh = subject.create_point_cloud_mesh(rep) + assert len(mesh.vertices) == 1 + verts = np.array([v.co for v in mesh.vertices]) + assert np.allclose(verts, np.array([np.array(c) for c in coords])) + + def test_point_2d(self): + bpy.ops.bim.create_project() + ifc_file = tool.Ifc.get() + coords = ((1., 2.),) + coords3d = ((1., 2., 0.),) + item = ifc_file.createIfcCartesianPoint(Coordinates=coords[0]) + rep = ifc_file.createIfcShapeRepresentation(Items=[item]) + mesh = subject.create_point_cloud_mesh(rep) + assert len(mesh.vertices) == 1 + verts = np.array([v.co for v in mesh.vertices]) + assert np.allclose(verts, np.array([np.array(c) for c in coords3d])) From 26b21ddcb3eeef4632b412471d15e5f45c925026 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Mar 2025 22:16:42 +1100 Subject: [PATCH 458/476] Fix #6414. Allow extracting elements from non-standard IFC schemas. --- src/ifcpatch/ifcpatch/recipes/ExtractElements.py | 2 +- src/ifcpatch/test/test_ExtractElements.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py index e2dfb13866..0218d68a30 100644 --- a/src/ifcpatch/ifcpatch/recipes/ExtractElements.py +++ b/src/ifcpatch/ifcpatch/recipes/ExtractElements.py @@ -55,7 +55,7 @@ class Patcher: def patch(self): self.contained_ins: dict[str, set[ifcopenshell.entity_instance]] = {} self.aggregates: dict[str, set[ifcopenshell.entity_instance]] = {} - self.new = ifcopenshell.file(schema=self.file.schema_identifier) + self.new = ifcopenshell.file(schema_version=self.file.schema_version) self.owner_history = None self.reuse_identities: dict[int, ifcopenshell.entity_instance] = {} for owner_history in self.file.by_type("IfcOwnerHistory"): diff --git a/src/ifcpatch/test/test_ExtractElements.py b/src/ifcpatch/test/test_ExtractElements.py index 10b55bad09..97f416ae3f 100644 --- a/src/ifcpatch/test/test_ExtractElements.py +++ b/src/ifcpatch/test/test_ExtractElements.py @@ -75,6 +75,14 @@ class TestExtractElements(test.bootstrap.IFC4): assert output.by_type("IfcWall") assert not output.by_type("IfcSlab") + def test_extracting_non_standard_schema_version(self): + self.file = ifcopenshell.file(schema_version=(4, 3, 0, 0)) + project = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcProject") + wall = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") + output = ifcpatch.execute({"file": self.file, "recipe": "ExtractElements", "arguments": ["IfcWall"]}) + assert output.by_type("IfcProject")[0].GlobalId == project.GlobalId + assert output.by_type("IfcWall")[0].GlobalId == wall.GlobalId + class TestExtractElementsIFC2X3(test.bootstrap.IFC2X3, TestExtractElements): pass From f43e666583ddae351c3aa9d047b4a509e62566c8 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sun, 23 Mar 2025 12:07:41 +0000 Subject: [PATCH 459/476] git colourisation fixes Fix bug where uncommitted changes were not fully colourised in the same way as diffs. Also try and catch more changes, eg. highlight if a Products Type has changed. --- src/bonsai/bonsai/core/ifcgit.py | 10 +++++++--- src/bonsai/bonsai/tool/ifcgit.py | 30 +++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/bonsai/bonsai/core/ifcgit.py b/src/bonsai/bonsai/core/ifcgit.py index 731e3751f2..404bd8df9b 100644 --- a/src/bonsai/bonsai/core/ifcgit.py +++ b/src/bonsai/bonsai/core/ifcgit.py @@ -99,15 +99,19 @@ def colourise_revision(ifcgit: tool.IfcGit) -> None: step_ids = ifcgit.get_revisions_step_ids() if not step_ids: return - modified_shape_object_step_ids = ifcgit.get_modified_shape_object_step_ids(step_ids) - final_step_ids = ifcgit.update_step_ids(step_ids, modified_shape_object_step_ids) + modified_step_ids = ifcgit.get_modified_step_ids(step_ids) + final_step_ids = ifcgit.update_step_ids(step_ids, modified_step_ids) ifcgit.colourise(final_step_ids) def colourise_uncommitted(ifcgit: tool.IfcGit, ifc: tool.Ifc, repo: git.Repo) -> None: path_ifc = ifc.get_path() step_ids = ifcgit.ifc_diff_ids(repo, None, "HEAD", path_ifc) - ifcgit.colourise(step_ids) + if not step_ids: + return + modified_step_ids = ifcgit.get_modified_step_ids(step_ids) + final_step_ids = ifcgit.update_step_ids(step_ids, modified_step_ids) + ifcgit.colourise(final_step_ids) def switch_revision(ifcgit: tool.IfcGit, ifc: tool.Ifc) -> None: diff --git a/src/bonsai/bonsai/tool/ifcgit.py b/src/bonsai/bonsai/tool/ifcgit.py index e306271ebd..1fe721d2f7 100644 --- a/src/bonsai/bonsai/tool/ifcgit.py +++ b/src/bonsai/bonsai/tool/ifcgit.py @@ -370,24 +370,36 @@ class IfcGit: return step_ids @classmethod - def get_modified_shape_object_step_ids(cls, step_ids: STEP_IDS) -> STEP_IDS: + def get_modified_step_ids(cls, step_ids: STEP_IDS) -> STEP_IDS: model = tool.Ifc.get() - modified_shape_object_step_ids = {"modified": []} + modified_step_ids = {"modified": set()} - for step_id in step_ids["modified"]: - if model.by_id(step_id).is_a() == "IfcProductDefinitionShape": - product = model.by_id(step_id).ShapeOfProduct[0] - modified_shape_object_step_ids["modified"].append(product.id()) + for step_id in step_ids["modified"] | step_ids["added"]: + try: + entity = model.by_id(step_id) + except: + continue + if entity.is_a("IfcProductDefinitionShape"): + for product in entity.ShapeOfProduct: + modified_step_ids["modified"].add(product.id()) + elif entity.is_a("IfcObjectPlacement"): + for product in entity.PlacesObject: + modified_step_ids["modified"].add(product.id()) + elif entity.is_a("IfcTypeProduct") and entity.Types: + for related_object in entity.Types[0].RelatedObjects: + modified_step_ids["modified"].add(related_object.id()) - return modified_shape_object_step_ids + return modified_step_ids @classmethod - def update_step_ids(cls, step_ids: STEP_IDS, modified_shape_object_step_ids: STEP_IDS) -> STEP_IDS: + def update_step_ids(cls, step_ids: STEP_IDS, modified_step_ids: STEP_IDS) -> STEP_IDS: final_step_ids = {} final_step_ids["added"] = step_ids["added"] final_step_ids["removed"] = step_ids["removed"] - final_step_ids["modified"] = step_ids["modified"].union(modified_shape_object_step_ids["modified"]) + final_step_ids["modified"] = ( + step_ids["modified"].union(modified_step_ids["modified"]).difference(step_ids["added"]) + ) return final_step_ids @classmethod From b269a806eb79efa291a0f905e6859e8a053aa075 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Mar 2025 23:31:04 +1100 Subject: [PATCH 460/476] Improve description for dev setup Be more dogmatic in the setup to prevent confusion. --- .../docs/guides/development/installation.rst | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/bonsai/docs/guides/development/installation.rst b/src/bonsai/docs/guides/development/installation.rst index 3992c50701..b66c8d63b6 100644 --- a/src/bonsai/docs/guides/development/installation.rst +++ b/src/bonsai/docs/guides/development/installation.rst @@ -83,7 +83,7 @@ that they are typically updated every day. To install the **Unstable** version: .. warning:: Make sure the extension you install has ``raw.githubusercontent.com`` as - it's "Repository" (not ``extensions.blender.org``). + the "Repository" (not ``extensions.blender.org``). .. image:: images/unstable-repo.png @@ -143,19 +143,14 @@ and install. Live development environment ---------------------------- -One option for developers who want to actively develop from source is to follow -the instructions from :ref:`guides/development/installation:Bundling for Blender`. However, -creating a build, uninstalling the old add-on, and installing a new build is a -slow process. Although it works, it is very slow, so we do not recommend it. +First, install using the :ref:`guides/development/installation:Unstable +installation` method. This will provide all compiled dependencies for you out +of the box. -A more rapid approach is to follow the -:ref:`guides/development/installation:Unstable installation` method, as this -provides all dependencies for you out of the box. - -Once you've done this, you can replace certain Python files that tend to be -updated frequently with those from the Git repository. We're going to use -symbolic links, so we can code in our Git repository, and see the changes in -our Blender installation (you will need to restart Blender to see changes). +Once you've done this, we'll replace the installed Python files with those from +our Git repository. We're going to use symbolic links, so we can code in our +Git repository, and see the changes in our Blender installation (you will need +to restart Blender to see changes). For Linux or Mac: @@ -163,9 +158,8 @@ For Linux or Mac: :language: bash :caption: dev_environment.sh -Or, if you're on Windows, you can use the batch script below. You need to run -it as an administrator. Before running it follow the instructions descibed -in the `rem` tags. +For Windows, run this batch script as an administrator. Before running it +follow the instructions descibed in the `rem` tags. .. literalinclude:: ../../../scripts/installation/dev_environment.bat :language: bat @@ -174,17 +168,17 @@ in the `rem` tags. After you modify your code in the Git repository, you will need to restart Blender for the changes to take effect. +Note that this only links Python code to the Git repository. If there are any +major changes such as new dependencies or newly compiled C++ code, you will +need to make the updates manually. This is relatively rare. Reviewing the +`Makefile history +`__, +is one quick way to see if a dependency has changed. + If there are changes to the IfcOpenShell binaries, you may replace the two ``*ifcopenshell_wrapper*`` files with new ones downloaded from the automated `IfcOpenShell builds directory `__. -The downside with this approach is that if a new dependency is added, or a -compiled dependency has changed (that is not available via the build -directory), or the build system changes, you'll need to fix your setup -manually. But this is relatively rare. Reviewing the Makefile history, `here -`__, -is one quick way to see if a dependency has changed. - .. seealso:: There is a `useful Blender Addon From 384a74734e3d3b1af502a911f2107411d99d206e Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Mar 2025 23:36:19 +1100 Subject: [PATCH 461/476] Fix regression in split along edge Aren't tests great --- src/bonsai/bonsai/bim/module/misc/operator.py | 2 +- src/bonsai/test/bim/feature/misc.feature | 25 +++++++++++++------ src/bonsai/test/bim/test_feature.py | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/misc/operator.py b/src/bonsai/bonsai/bim/module/misc/operator.py index 7d2907f11f..5f4ddfa650 100644 --- a/src/bonsai/bonsai/bim/module/misc/operator.py +++ b/src/bonsai/bonsai/bim/module/misc/operator.py @@ -125,7 +125,7 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator): "Will unassign element from a type if type has a representation." ) bl_options = {"REGISTER", "UNDO"} - mode: bpy.props.StringProperty() + mode: bpy.props.StringProperty(default="BOOLEAN") @classmethod def poll(cls, context): diff --git a/src/bonsai/test/bim/feature/misc.feature b/src/bonsai/test/bim/feature/misc.feature index d418ee3ea5..153b4ff48c 100644 --- a/src/bonsai/test/bim/feature/misc.feature +++ b/src/bonsai/test/bim/feature/misc.feature @@ -31,7 +31,7 @@ Scenario: Resize to storey When I press "bim.resize_to_storey(total_storeys=1)" Then nothing happens -Scenario: Split along edge +Scenario: Split along edge - boolean mode Given an empty IFC project And I add a cube And the object "Cube" is selected @@ -41,13 +41,24 @@ Scenario: Split along edge And I add a plane of size "4" at "0,0,0" And the object "IfcWall/Cube" is selected And additionally the object "Plane" is selected - When I press "bim.split_along_edge" + And I look at the "Miscellaneous" panel + When I click "Split Along Edge" Then the object "IfcWall/Cube" is an "IfcWall" And the object "IfcWall/Cube.001" is an "IfcWall" -Scenario: Enabling and disabling IFC Sverchok +Scenario: Split along edge - bisect mode Given an empty IFC project - And I press "preferences.addon_enable(module="sverchok")" - And I press "preferences.addon_enable(module="ifcsverchok")" - And I press "preferences.addon_disable(module="sverchok")" - And I press "preferences.addon_disable(module="ifcsverchok")" + And I add a cube + And the object "Cube" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElement" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWall" + And I press "bim.assign_class" + And I add a plane of size "4" at "0,0,0" + And the object "IfcWall/Cube" is selected + And additionally the object "Plane" is selected + And I look at the "Miscellaneous" panel + When I click "Bisect At Faces" + Then the object "IfcWall/Cube" is an "IfcWall" + And the object "IfcWall/Cube.001" is an "IfcWall" + + diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 4a37caa2e1..a094f94d3d 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -73,7 +73,7 @@ class PanelSpy: return self def __call__(self, *args, **kwargs): - if self.spied_attr in ("row", "column", "box", "separator", "menu", "operator_menu_enum"): + if self.spied_attr in ("row", "column", "box", "separator", "menu", "operator_menu_enum", "split"): return self elif self.spied_attr == "template_list": listtype_name, list_id, dataptr, propname, active_dataptr, active_propname = args From 58c7976a5e7e6613ba2c245631737d28e826de7b Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 23 Mar 2025 23:55:15 +1100 Subject: [PATCH 462/476] See #1227. Fix regression where new wall system didn't preserve existing dimensions when regenerating geometry for an object that previously didn't have a layerset --- src/bonsai/test/bim/feature/type.feature | 39 ++++++++++++------- .../ifcopenshell/util/representation.py | 14 ++++--- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/bonsai/test/bim/feature/type.feature b/src/bonsai/test/bim/feature/type.feature index c32f7a8cfb..b85b9162c5 100644 --- a/src/bonsai/test/bim/feature/type.feature +++ b/src/bonsai/test/bim/feature/type.feature @@ -10,16 +10,6 @@ Scenario: Add type - adding via manual class assignment When I press "bim.assign_class" Then nothing happens -Scenario: Add type - add from empty template - Given an empty IFC project - And I press "bim.launch_type_manager" - And I set "scene.BIMModelProperties.type_class" to "IfcWallType" - And I set "scene.BIMModelProperties.type_predefined_type" to "SOLIDWALL" - And I set "scene.BIMModelProperties.type_template" to "EMPTY" - When I press "bim.add_type" - Then the object "IfcWallType/TYPEX" is an "IfcWallType" - And the object "IfcWallType/TYPEX" has no data - Scenario: Enable editing type Given an empty IFC project And I add a cube @@ -73,7 +63,7 @@ Scenario: Assign type - assign to a type with representation maps And I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')" Then the object "IfcWall/Cube" has a "MappedRepresentation" representation of "Model/Body/MODEL_VIEW" -Scenario: Assign type - assign to a type with a material layer set, which automatically recreates the shape +Scenario: Assign type - assign to a type with a material layer set, which automatically recreates a mesh and resets dimensions Given an empty IFC project And I add a cube And the object "Cube" is selected @@ -92,7 +82,28 @@ Scenario: Assign type - assign to a type with a material layer set, which automa And I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')" Then the object "IfcWall/Cube" has a "SweptSolid" representation of "Model/Body/MODEL_VIEW" And the object "IfcWall/Cube" has a "100" thick layered material containing the material "Default" - And the object "IfcWall/Cube" dimensions are "2,.1,3" + And the object "IfcWall/Cube" dimensions are "1,.1,1" + +Scenario: Assign type - assign to a type with a material layer set, which automatically recreates a solid (preserving parameters) + Given an empty IFC project + And I trigger "Add Element" + And I set the "Definition" property to "IfcElement" + And I set the "Class" property to "IfcWall" + And I set the "Representation" property to "Custom Extruded Solid" + And I click "OK" + And I add an empty + And the object "Empty" is selected + And I set "scene.BIMRootProperties.ifc_product" to "IfcElementType" + And I set "scene.BIMRootProperties.ifc_class" to "IfcWallType" + And I press "bim.assign_class" + And I press "bim.add_material()" + And I set "active_object.BIMObjectMaterialProperties.material_type" to "IfcMaterialLayerSet" + And I press "bim.assign_material" + When the variable "type" is "{ifc}.by_type('IfcWallType')[0].id()" + And I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Unnamed')" + Then the object "IfcWall/Unnamed" has a "SweptSolid" representation of "Model/Body/MODEL_VIEW" + And the object "IfcWall/Unnamed" has a "100" thick layered material containing the material "Default" + And the object "IfcWall/Unnamed" dimensions are ".5,.1,.5" Scenario: Assign type - assign to a different type with a material layer set Given an empty IFC project @@ -127,10 +138,10 @@ Scenario: Assign type - assign to a different type with a material layer set When I press "bim.assign_type(relating_type={type}, related_object='IfcWall/Cube')" Then the object "IfcWall/Cube" has a "SweptSolid" representation of "Model/Body/MODEL_VIEW" And the object "IfcWall/Cube" has a "100" thick layered material containing the material "Default" - And the object "IfcWall/Cube" dimensions are "2,.1,3" + And the object "IfcWall/Cube" dimensions are "1,.1,1" When I press "bim.assign_type(relating_type={type2}, related_object='IfcWall/Cube')" Then the object "IfcWall/Cube" has a "200" thick layered material containing the material "Default" - And the object "IfcWall/Cube" dimensions are "2,.2,3" + And the object "IfcWall/Cube" dimensions are "1,.2,1" Scenario: Assign type - assign to a type with a material profile set Given an empty IFC project diff --git a/src/ifcopenshell-python/ifcopenshell/util/representation.py b/src/ifcopenshell-python/ifcopenshell/util/representation.py index ca79c4ec88..94ec3f8579 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/representation.py +++ b/src/ifcopenshell-python/ifcopenshell/util/representation.py @@ -490,11 +490,15 @@ def get_reference_line(wall: ifcopenshell.entity_instance, fallback_length: floa return [np.array(points[0]), np.array(points[1])] return [np.array(points[1]), np.array(points[0])] elif extrusions := ifcopenshell.util.shape.get_base_extrusions(wall): - for item in extrusions: - if item.is_a("IfcPolyline"): - x = [p[0][0] for p in item.Points] - elif item.is_a("IfcIndexedPolyCurve"): - x = [p[0] for p in item.Points.CoordList] + for extrusion in extrusions: + profile = extrusion.SweptArea + curve = getattr(profile, "OuterCurve", None) + if not curve: + continue + elif curve.is_a("IfcPolyline"): + x = [p[0][0] for p in curve.Points] + elif curve.is_a("IfcIndexedPolyCurve"): + x = [p[0] for p in curve.Points.CoordList] else: continue return [np.array((min(x), 0.0)), np.array((max(x), 0.0))] From b63d9090a115be5d6d0817e393e6edab874ae931 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Mar 2025 11:36:36 +0500 Subject: [PATCH 463/476] black . --- src/bonsai/bonsai/bim/module/drawing/helper.py | 2 +- src/bonsai/bonsai/bim/module/geometry/__init__.py | 2 +- src/bonsai/test/tool/test_loader.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/helper.py b/src/bonsai/bonsai/bim/module/drawing/helper.py index b1706cffa7..91149b639d 100644 --- a/src/bonsai/bonsai/bim/module/drawing/helper.py +++ b/src/bonsai/bonsai/bim/module/drawing/helper.py @@ -202,7 +202,7 @@ def format_distance( base = int(precision) decInches = value * toInches - decFeet = decInches/12 + decFeet = decInches / 12 # Separate ft and inches # Unless Inches are the specified Length Unit or unit_fraction is False diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py index f6ecfd2cf8..47dcaeddce 100644 --- a/src/bonsai/bonsai/bim/module/geometry/__init__.py +++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py @@ -104,7 +104,7 @@ def block_scale(scene: bpy.types.Scene) -> None: if obj := (getattr(bpy.context, "active_object", None) or bpy.context.view_layer.objects.active): if isinstance(obj, bpy.types.Object) and tool.Blender.get_ifc_definition_id(obj): - if obj.type == 'CAMERA': + if obj.type == "CAMERA": camera = tool.Ifc.get_entity(obj) if ifcopenshell.util.element.get_pset(camera, "EPset_Drawing", "TargetView") == "REFLECTED_PLAN_VIEW": obj.scale = (-1, -1, -1) diff --git a/src/bonsai/test/tool/test_loader.py b/src/bonsai/test/tool/test_loader.py index 094fa8648f..abb3473245 100644 --- a/src/bonsai/test/tool/test_loader.py +++ b/src/bonsai/test/tool/test_loader.py @@ -555,7 +555,7 @@ class TestCreatePointCloudMesh(NewFile): def test_cartesian_point_list_3d(self): bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() - coords = ((1., 2., 3.), (4., 5., 6.)) + coords = ((1.0, 2.0, 3.0), (4.0, 5.0, 6.0)) item = ifc_file.createIfcCartesianPointList3D(coords) rep = ifc_file.createIfcShapeRepresentation(Items=[item]) mesh = subject.create_point_cloud_mesh(rep) @@ -566,8 +566,8 @@ class TestCreatePointCloudMesh(NewFile): def test_cartesian_point_list_2d(self): bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() - coords = ((1., 2.), (4., 5.)) - coords3d = ((1., 2., 0.), (4., 5., 0.)) + coords = ((1.0, 2.0), (4.0, 5.0)) + coords3d = ((1.0, 2.0, 0.0), (4.0, 5.0, 0.0)) item = ifc_file.createIfcCartesianPointList2D(coords) rep = ifc_file.createIfcShapeRepresentation(Items=[item]) mesh = subject.create_point_cloud_mesh(rep) @@ -578,7 +578,7 @@ class TestCreatePointCloudMesh(NewFile): def test_point_3d(self): bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() - coords = ((1., 2., 0.),) + coords = ((1.0, 2.0, 0.0),) item = ifc_file.createIfcCartesianPoint(Coordinates=coords[0]) rep = ifc_file.createIfcShapeRepresentation(Items=[item]) mesh = subject.create_point_cloud_mesh(rep) @@ -589,8 +589,8 @@ class TestCreatePointCloudMesh(NewFile): def test_point_2d(self): bpy.ops.bim.create_project() ifc_file = tool.Ifc.get() - coords = ((1., 2.),) - coords3d = ((1., 2., 0.),) + coords = ((1.0, 2.0),) + coords3d = ((1.0, 2.0, 0.0),) item = ifc_file.createIfcCartesianPoint(Coordinates=coords[0]) rep = ifc_file.createIfcShapeRepresentation(Items=[item]) mesh = subject.create_point_cloud_mesh(rep) From c84f8e55a6734dfc98d5dc6d1592451134ab2be5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Mar 2025 11:31:30 +0500 Subject: [PATCH 464/476] Fix typo --- src/bonsai/docs/guides/development/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/docs/guides/development/installation.rst b/src/bonsai/docs/guides/development/installation.rst index b66c8d63b6..a471c456ee 100644 --- a/src/bonsai/docs/guides/development/installation.rst +++ b/src/bonsai/docs/guides/development/installation.rst @@ -159,7 +159,7 @@ For Linux or Mac: :caption: dev_environment.sh For Windows, run this batch script as an administrator. Before running it -follow the instructions descibed in the `rem` tags. +follow the instructions described in the `rem` tags. .. literalinclude:: ../../../scripts/installation/dev_environment.bat :language: bat From 595ee3fa470887e8e95b19e63820711924b96359 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Mar 2025 11:33:10 +0500 Subject: [PATCH 465/476] Provide descriptions to all operators that use `fileselect_add` #6420 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I guess all it takes a persistent Blender crash to fill up operators descriptions 😅 --- src/bonsai/bonsai/bim/module/bcf/operator.py | 5 +++++ src/bonsai/bonsai/bim/module/brick/operator.py | 2 ++ src/bonsai/bonsai/bim/module/clash/operator.py | 2 ++ src/bonsai/bonsai/bim/module/classification/operator.py | 1 + src/bonsai/bonsai/bim/module/csv/operator.py | 3 +++ src/bonsai/bonsai/bim/module/diff/operator.py | 4 ++++ src/bonsai/bonsai/bim/module/drawing/operator.py | 1 + src/bonsai/bonsai/bim/module/fm/operator.py | 3 +++ src/bonsai/bonsai/bim/module/georeference/operator.py | 2 +- src/bonsai/bonsai/bim/module/model/sverchok_modifier.py | 2 ++ src/bonsai/bonsai/bim/module/patch/operator.py | 2 ++ src/bonsai/bonsai/bim/module/project/operator.py | 2 ++ src/bonsai/bonsai/bim/module/style/operator.py | 2 ++ src/bonsai/bonsai/bim/module/tester/operator.py | 1 + 14 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index 0c145d98f8..df07babe69 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -66,6 +66,7 @@ class NewBcfProject(bpy.types.Operator): class LoadBcfProject(bpy.types.Operator): bl_idname = "bim.load_bcf_project" bl_label = "Load BCF Project" + bl_description = "Load the BCF file." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"}) filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"}) @@ -344,6 +345,7 @@ class EditBcfTopic(bpy.types.Operator): class SaveBcfProject(bpy.types.Operator): bl_idname = "bim.save_bcf_project" bl_label = "Save BCF Project" + bl_description = "Save active BCF project by the provided filepath." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"}) @@ -1537,6 +1539,7 @@ class OpenBcfReferenceLink(bpy.types.Operator): class SelectBcfHeaderFile(bpy.types.Operator): bl_idname = "bim.select_bcf_header_file" bl_label = "Select BCF Header File" + bl_description = "Select filepath for BCF header reference." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"}) @@ -1555,6 +1558,7 @@ class SelectBcfHeaderFile(bpy.types.Operator): class SelectBcfBimSnippetReference(bpy.types.Operator): bl_idname = "bim.select_bcf_bim_snippet_reference" bl_label = "Select BCF BIM Snippet Reference" + bl_description = "Select filepath for BCF snippet reference." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -1572,6 +1576,7 @@ class SelectBcfBimSnippetReference(bpy.types.Operator): class SelectBcfDocumentReference(bpy.types.Operator): bl_idname = "bim.select_bcf_document_reference" bl_label = "Select BCF Document Reference" + bl_description = "Select filepath for BCF document reference." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") diff --git a/src/bonsai/bonsai/bim/module/brick/operator.py b/src/bonsai/bonsai/bim/module/brick/operator.py index 522241a88e..e9b20a215d 100644 --- a/src/bonsai/bonsai/bim/module/brick/operator.py +++ b/src/bonsai/bonsai/bim/module/brick/operator.py @@ -243,6 +243,8 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator): class SerializeBrick(bpy.types.Operator): bl_idname = "bim.serialize_brick" bl_label = "Serialize Brick" + # Prevents crash on Blender 4.4.0. + bl_description = "Save active Brick project by the provided filepath." filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"}) diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index 3fd4e3b4de..a3e43c72d6 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -173,6 +173,7 @@ class SelectClashSource(bpy.types.Operator): class SelectClashResults(bpy.types.Operator): bl_idname = "bim.select_clash_results" bl_label = "Select Clash Results" + bl_description = "Select filepath for clash results." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -188,6 +189,7 @@ class SelectClashResults(bpy.types.Operator): class SelectSmartGroupedClashesPath(bpy.types.Operator): bl_idname = "bim.select_smart_grouped_clashes_path" bl_label = "Select Smart-Grouped Clashes Path" + bl_description = "Select filepath for smart-grouped clashes." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index ab3fc08658..a2b19fcae1 100644 --- a/src/bonsai/bonsai/bim/module/classification/operator.py +++ b/src/bonsai/bonsai/bim/module/classification/operator.py @@ -31,6 +31,7 @@ from bonsai.bim.ifc import IfcStore class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.load_classification_library" bl_label = "Load Classification Library" + bl_description = "Load classification library from the provided filepath." filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index cd9e784bf2..18667381d6 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -185,6 +185,7 @@ class ExportCsvAttributes(bpy.types.Operator): class ExportIfcCsv(bpy.types.Operator): bl_idname = "bim.export_ifccsv" bl_label = "Export IFC" + bl_description = "Export IFC data as a spreadsheet." filename_ext = ".csv" filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -290,6 +291,7 @@ class ExportIfcCsv(bpy.types.Operator): class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.import_ifccsv" bl_label = "Import to IFC" + bl_description = "Import IFC data from a spreadsheet." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -340,6 +342,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): class SelectCsvIfcFile(bpy.types.Operator): bl_idname = "bim.select_csv_ifc_file" bl_label = "Select CSV IFC File" + bl_description = "Select IFC file for spreadsheet import/export." bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) diff --git a/src/bonsai/bonsai/bim/module/diff/operator.py b/src/bonsai/bonsai/bim/module/diff/operator.py index 4c243bc28b..516b26d9ad 100644 --- a/src/bonsai/bonsai/bim/module/diff/operator.py +++ b/src/bonsai/bonsai/bim/module/diff/operator.py @@ -30,6 +30,7 @@ from bonsai.bim.ifc import IfcStore class SelectDiffJsonFile(bpy.types.Operator): bl_idname = "bim.select_diff_json_file" bl_label = "Select Diff JSON File" + bl_description = "Select filepath for IFC diff results." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) @@ -104,6 +105,7 @@ class VisualiseDiff(bpy.types.Operator): class SelectDiffOldFile(bpy.types.Operator): bl_idname = "bim.select_diff_old_file" bl_label = "Select Diff Old File" + bl_description = "Select filepath for an old IFC file to compare." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) @@ -120,6 +122,7 @@ class SelectDiffOldFile(bpy.types.Operator): class SelectDiffNewFile(bpy.types.Operator): bl_idname = "bim.select_diff_new_file" bl_label = "Select Diff New File" + bl_description = "Select filepath for a new IFC file to compare." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) @@ -136,6 +139,7 @@ class SelectDiffNewFile(bpy.types.Operator): class ExecuteIfcDiff(bpy.types.Operator): bl_idname = "bim.execute_ifc_diff" bl_label = "Execute IFC Diff" + bl_description = "Compare two IFC files and save a json diff report by the provided filepath." filename_ext = ".json" filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index ba1f593096..0cc32e5d07 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -2216,6 +2216,7 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase): return True +# TODO: not exposed to the UI. class SelectDocIfcFile(bpy.types.Operator): bl_idname = "bim.select_doc_ifc_file" bl_label = "Select Documentation IFC File" diff --git a/src/bonsai/bonsai/bim/module/fm/operator.py b/src/bonsai/bonsai/bim/module/fm/operator.py index 6776c27e14..c78f70dee4 100644 --- a/src/bonsai/bonsai/bim/module/fm/operator.py +++ b/src/bonsai/bonsai/bim/module/fm/operator.py @@ -29,6 +29,7 @@ import bonsai.tool as tool class ExecuteIfcFM(bpy.types.Operator): bl_idname = "bim.execute_ifcfm" bl_label = "Execute IfcFM" + bl_description = "Export IfcFM data as a spreadsheet." file_format: bpy.props.StringProperty() filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -85,6 +86,7 @@ class ExecuteIfcFM(bpy.types.Operator): class SelectFMSpreadsheetFiles(bpy.types.Operator): bl_idname = "bim.select_fm_spreadsheet_files" bl_label = "Select FM Spreadsheet Files" + bl_description = "Select FM spreadsheets to merge." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -107,6 +109,7 @@ class SelectFMSpreadsheetFiles(bpy.types.Operator): class ExecuteIfcFMFederate(bpy.types.Operator): bl_idname = "bim.execute_ifcfm_federate" bl_label = "Merge IfcFM SpreadSheets" + bl_description = "Merge added IfcFM spreadsheets." filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") diff --git a/src/bonsai/bonsai/bim/module/georeference/operator.py b/src/bonsai/bonsai/bim/module/georeference/operator.py index 94307113e0..edfa9a9054 100644 --- a/src/bonsai/bonsai/bim/module/georeference/operator.py +++ b/src/bonsai/bonsai/bim/module/georeference/operator.py @@ -91,7 +91,7 @@ class ImportPlot(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.import_plot" bl_label = "Import Plot" bl_options = {"REGISTER", "UNDO"} - bl_description = "Import plot" + bl_description = "Import plot from a csv file." filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) diff --git a/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py b/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py index 5624797752..149a309baa 100644 --- a/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py +++ b/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py @@ -184,6 +184,7 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator): class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.import_sverchok_graph" bl_label = "Import Sverchok Graph" + bl_description = "Import Sverchok graph from a json file." bl_options = {"REGISTER"} filepath: bpy.props.StringProperty( @@ -226,6 +227,7 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.export_sverchok_graph" bl_label = "Export Sverchok Graph" + bl_description = "Export Sverchok graph to a json file." bl_options = {"REGISTER"} filepath: bpy.props.StringProperty( diff --git a/src/bonsai/bonsai/bim/module/patch/operator.py b/src/bonsai/bonsai/bim/module/patch/operator.py index 0c2c868f7a..bbd521ffd4 100644 --- a/src/bonsai/bonsai/bim/module/patch/operator.py +++ b/src/bonsai/bonsai/bim/module/patch/operator.py @@ -34,6 +34,7 @@ if TYPE_CHECKING: class SelectIfcPatchInput(bpy.types.Operator): bl_idname = "bim.select_ifc_patch_input" bl_label = "Select IFC Patch Input" + bl_description = "Select filepath for IFC patch input." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifcZIP;*.ifcXML", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -51,6 +52,7 @@ class SelectIfcPatchInput(bpy.types.Operator): class SelectIfcPatchOutput(bpy.types.Operator): bl_idname = "bim.select_ifc_patch_output" bl_label = "Select IFC Patch Output" + bl_description = "Select filepath for IFC patch output." bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filepath: bpy.props.StringProperty(subtype="FILE_PATH") diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 52333a5120..936a532d1e 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -1547,6 +1547,8 @@ class SelectLinkHandle(bpy.types.Operator): class ExportIFC(bpy.types.Operator): bl_idname = "bim.save_project" bl_label = "Save IFC" + # Prevents crash on Blender 4.4.0. + bl_description = "Save active IFC file by the provided filepath." bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"}) diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index 0ead9b6751..ea20f25be0 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -276,6 +276,7 @@ class SetAssetMaterialToExternalStyle(bpy.types.Operator): class BrowseExternalStyle(bpy.types.Operator): bl_idname = "bim.browse_external_style" bl_label = "Browse External Style" + bl_description = "Select filepath for an external style." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty( @@ -540,6 +541,7 @@ class SelectByStyle(bpy.types.Operator): class ChooseTextureMapPath(bpy.types.Operator): bl_idname = "bim.choose_texture_map_path" bl_label = "Choose Texture Map Path" + bl_description = "Select filepath for a texture map." bl_options = {"REGISTER", "UNDO", "INTERNAL"} texture_map_index: bpy.props.IntProperty(default=-1, options={"SKIP_SAVE"}) diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index a438bdcfb5..f653404d78 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -189,6 +189,7 @@ class SelectFailedEntities(bpy.types.Operator): class ExportBcf(bpy.types.Operator): bl_idname = "bim.export_bcf" bl_label = "Export BCF" + bl_description = "Save ifctester BCF report by the provided filepath." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.bcf", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") From 7a55e837f766099c6b954a3fb494fe7244662ce1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Mar 2025 12:30:58 +0500 Subject: [PATCH 466/476] typing --- src/bonsai/bonsai/bim/import_ifc.py | 2 +- .../bonsai/bim/module/alignment/operator.py | 4 +- .../bonsai/bim/module/clash/decorator.py | 8 +- .../bonsai/bim/module/clash/operator.py | 75 ++++++++++++------- src/bonsai/bonsai/bim/module/clash/prop.py | 65 +++++++++++++--- src/bonsai/bonsai/bim/module/clash/ui.py | 7 +- src/bonsai/bonsai/bim/module/csv/operator.py | 31 ++++---- src/bonsai/bonsai/bim/module/csv/prop.py | 32 ++++++++ src/bonsai/bonsai/bim/module/csv/ui.py | 4 +- src/bonsai/bonsai/bim/module/diff/data.py | 2 +- src/bonsai/bonsai/bim/module/diff/operator.py | 17 +++-- src/bonsai/bonsai/bim/module/diff/prop.py | 20 ++++- src/bonsai/bonsai/bim/module/diff/ui.py | 4 +- .../bonsai/bim/module/search/operator.py | 2 +- src/bonsai/bonsai/tool/blender.py | 10 +++ src/bonsai/bonsai/tool/clash.py | 44 +++++++---- src/bonsai/bonsai/tool/search.py | 11 ++- .../create_geometric_representation.py | 1 + 18 files changed, 239 insertions(+), 100 deletions(-) diff --git a/src/bonsai/bonsai/bim/import_ifc.py b/src/bonsai/bonsai/bim/import_ifc.py index d1c158dc8e..5c36163207 100644 --- a/src/bonsai/bonsai/bim/import_ifc.py +++ b/src/bonsai/bonsai/bim/import_ifc.py @@ -1170,7 +1170,7 @@ class IfcImportSettings: @staticmethod def factory(context=None, input_file=None, logger=None): - scene_diff = bpy.context.scene.DiffProperties + scene_diff = tool.Blender.get_diff_props() props = tool.Project.get_project_props() settings = IfcImportSettings() settings.input_file = input_file diff --git a/src/bonsai/bonsai/bim/module/alignment/operator.py b/src/bonsai/bonsai/bim/module/alignment/operator.py index 1fcbdd52c8..fe38ceea40 100644 --- a/src/bonsai/bonsai/bim/module/alignment/operator.py +++ b/src/bonsai/bonsai/bim/module/alignment/operator.py @@ -31,6 +31,8 @@ import isodate import bonsai.core.sequence as core import bonsai.tool as tool import bonsai.bim.module.sequence.helper as helper +import ifcopenshell.api.spatial +import ifcopenshell.geom import ifcopenshell.util.sequence import ifcopenshell.util.selector from datetime import datetime @@ -59,8 +61,6 @@ class ImportAlignmentCSV(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): return True def _execute(self, context): - import ifcopenshell.api.alignment - self.file = tool.Ifc.get() start = time.time() alignment = ifcopenshell.api.alignment.create_alignment_from_csv(self.file, self.filepath) diff --git a/src/bonsai/bonsai/bim/module/clash/decorator.py b/src/bonsai/bonsai/bim/module/clash/decorator.py index f23b8962c6..764c6e5232 100644 --- a/src/bonsai/bonsai/bim/module/clash/decorator.py +++ b/src/bonsai/bonsai/bim/module/clash/decorator.py @@ -60,8 +60,9 @@ class ClashDecorator: unselected_elements_color = self.addon_prefs.decorator_color_unselected special_elements_color = self.addon_prefs.decorator_color_special - text = context.scene.BIMClashProperties.active_clash_text - p = context.scene.BIMClashProperties.p1.lerp(context.scene.BIMClashProperties.p2, 0.5) + props = tool.Clash.get_clash_props() + text = props.active_clash_text + p = props.p1.lerp(props.p2, 0.5) font_id = 0 blf.size(font_id, 12) @@ -92,7 +93,8 @@ class ClashDecorator: # general shader self.shader = gpu.shader.from_builtin("UNIFORM_COLOR") - selected_vertices = [context.scene.BIMClashProperties.p1, context.scene.BIMClashProperties.p2] + props = tool.Clash.get_clash_props() + selected_vertices = [props.p1, props.p2] selected_edges = [] if selected_vertices[0] != selected_vertices[1]: selected_edges = [[0, 1]] diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index a3e43c72d6..3b19612c98 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -69,9 +69,10 @@ class ImportClashSets(bpy.types.Operator): def execute(self, context): tool.Clash.load_clash_sets(self.filepath) - context.scene.BIMClashProperties.clash_sets.clear() + props = tool.Clash.get_clash_props() + props.clash_sets.clear() for clash_set in tool.Clash.get_clash_sets(): - new = context.scene.BIMClashProperties.clash_sets.add() + new = props.clash_sets.add() new.name = clash_set["name"] new.mode = clash_set["mode"] if new.mode == "intersection": @@ -106,7 +107,8 @@ class AddClashSet(bpy.types.Operator): bl_description = "Add a clash set" def execute(self, context): - new = context.scene.BIMClashProperties.clash_sets.add() + props = tool.Clash.get_clash_props() + new = props.clash_sets.add() new.name = "New Clash Set" return {"FINISHED"} @@ -119,7 +121,8 @@ class RemoveClashSet(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - context.scene.BIMClashProperties.clash_sets.remove(self.index) + props = tool.Clash.get_clash_props() + props.clash_sets.remove(self.index) return {"FINISHED"} @@ -131,7 +134,8 @@ class AddClashSource(bpy.types.Operator): group: bpy.props.StringProperty() def execute(self, context): - clash_set = context.scene.BIMClashProperties.active_clash_set + props = tool.Clash.get_clash_props() + clash_set = props.active_clash_set source = getattr(clash_set, self.group).add() return {"FINISHED"} @@ -145,7 +149,8 @@ class RemoveClashSource(bpy.types.Operator): group: bpy.props.StringProperty() def execute(self, context): - clash_set = context.scene.BIMClashProperties.active_clash_set + props = tool.Clash.get_clash_props() + clash_set = props.active_clash_set getattr(clash_set, self.group).remove(self.index) return {"FINISHED"} @@ -161,7 +166,8 @@ class SelectClashSource(bpy.types.Operator): group: bpy.props.StringProperty() def execute(self, context): - clash_set = context.scene.BIMClashProperties.active_clash_set + props = tool.Clash.get_clash_props() + clash_set = props.active_clash_set getattr(clash_set, self.group)[self.index].name = self.filepath return {"FINISHED"} @@ -178,7 +184,8 @@ class SelectClashResults(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - context.scene.BIMClashProperties.clash_results_path = self.filepath + props = tool.Clash.get_clash_props() + props.clash_results_path = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -194,7 +201,8 @@ class SelectSmartGroupedClashesPath(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - context.scene.BIMClashProperties.smart_grouped_clashes_path = self.filepath + props = tool.Clash.get_clash_props() + props.smart_grouped_clashes_path = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -218,7 +226,7 @@ class ExecuteIfcClash(bpy.types.Operator): def execute(self, context): from ifcclash import ifcclash - self.props = context.scene.BIMClashProperties + self.props = tool.Clash.get_clash_props() _, extension = os.path.splitext(self.filepath) if extension != ".bcf": @@ -306,7 +314,9 @@ class SelectIfcClashResults(bpy.types.Operator): self.filepath = bpy.path.ensure_ext(self.filepath, ".json") with open(self.filepath) as f: clash_sets = json.load(f) - clash_set_name = context.scene.BIMClashProperties.active_clash_set.name + clash_props = tool.Clash.get_clash_props() + assert clash_props.active_clash_set + clash_set_name = clash_props.active_clash_set.name global_ids = [] for clash_set in clash_sets: if clash_set["name"] != clash_set_name: @@ -358,7 +368,7 @@ class SelectClash(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - self.props = context.scene.BIMClashProperties + self.props = tool.Clash.get_clash_props() clash_set = tool.Clash.get_clash_set(self.props.active_clash_set.name) active_clash = self.props.active_clash clash = tool.Clash.get_clash(clash_set, active_clash.a_global_id, active_clash.b_global_id) @@ -392,13 +402,15 @@ class SmartClashGroup(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.BIMClashProperties.clash_results_path + props = tool.Clash.get_clash_props() + return bool(props.clash_results_path) def execute(self, context): from ifcclash import ifcclash settings = ifcclash.ClashSettings() - self.filepath = bpy.path.ensure_ext(context.scene.BIMClashProperties.clash_results_path, ".json") + props = tool.Clash.get_clash_props() + self.filepath = bpy.path.ensure_ext(props.clash_results_path, ".json") settings.output = self.filepath settings.logger = logging.getLogger("Clash") settings.logger.setLevel(logging.DEBUG) @@ -408,19 +420,18 @@ class SmartClashGroup(bpy.types.Operator): clash_sets = json.load(f) # execute the smart grouping - save_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") - smart_grouped_clashes = ifc_clasher.smart_group_clashes( - clash_sets, context.scene.BIMClashProperties.smart_clash_grouping_max_distance - ) + save_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json") + smart_grouped_clashes = ifc_clasher.smart_group_clashes(clash_sets, props.smart_clash_grouping_max_distance) # save smart_groups to json with open(save_path, "w") as f: f.write(json.dumps(smart_grouped_clashes)) - clash_set_name = context.scene.BIMClashProperties.active_clash_set.name + assert props.active_clash_set + clash_set_name = props.active_clash_set.name # Reset the list of smart_clash_groups for the UI - context.scene.BIMClashProperties.smart_clash_groups.clear() + props.smart_clash_groups.clear() for clash_set, smart_groups in smart_grouped_clashes.items(): # Only select the clashes that correspond to the actively selected IFC Clash Set @@ -428,7 +439,7 @@ class SmartClashGroup(bpy.types.Operator): continue else: for smart_group, global_id_pairs in smart_groups[0].items(): - new_group = context.scene.BIMClashProperties.smart_clash_groups.add() + new_group = props.smart_clash_groups.add() new_group.number = f"{smart_group}" for pair in global_id_pairs: @@ -446,18 +457,21 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.BIMClashProperties.active_clash_set + props = tool.Clash.get_clash_props() + return bool(props.active_clash_set) def execute(self, context): - smart_groups_path = bpy.path.ensure_ext(context.scene.BIMClashProperties.smart_grouped_clashes_path, ".json") + props = tool.Clash.get_clash_props() + smart_groups_path = bpy.path.ensure_ext(props.smart_grouped_clashes_path, ".json") - clash_set_name = context.scene.BIMClashProperties.active_clash_set.name + assert props.active_clash_set + clash_set_name = props.active_clash_set.name with open(smart_groups_path) as f: smart_grouped_clashes = json.load(f) # Reset the list of smart_clash_groups for the UI - context.scene.BIMClashProperties.smart_clash_groups.clear() + props.smart_clash_groups.clear() for clash_set, smart_groups in smart_grouped_clashes.items(): # Only select the clashes that correspond to the actively selected IFC Clash Set @@ -465,7 +479,7 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): continue else: for smart_group, global_id_pairs in smart_groups[0].items(): - new_group = context.scene.BIMClashProperties.smart_clash_groups.add() + new_group = props.smart_clash_groups.add() new_group.number = f"{smart_group}" for pair in global_id_pairs: for guid in pair: @@ -482,11 +496,14 @@ class SelectSmartGroup(bpy.types.Operator): @classmethod def poll(cls, context): - return tool.Ifc.get() and context.visible_objects and context.scene.BIMClashProperties.active_smart_group + props = tool.Clash.get_clash_props() + return tool.Ifc.get() and context.visible_objects and props.active_smart_group def execute(self, context): - selected_smart_group = context.scene.BIMClashProperties.active_smart_group - products = [] + props = tool.Clash.get_clash_props() + selected_smart_group = props.active_smart_group + assert selected_smart_group + products: list[ifcopenshell.entity_instance] = [] for global_id in selected_smart_group.global_ids: try: products.append(tool.Ifc.get().by_guid(global_id.guid)) diff --git a/src/bonsai/bonsai/bim/module/clash/prop.py b/src/bonsai/bonsai/bim/module/clash/prop.py index b112964fe4..88b13a0e25 100644 --- a/src/bonsai/bonsai/bim/module/clash/prop.py +++ b/src/bonsai/bonsai/bim/module/clash/prop.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool from bonsai.bim.prop import StrProperty, Attribute, BIMFilterGroup from bpy.types import PropertyGroup from bpy.props import ( @@ -29,6 +30,8 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from mathutils import Vector +from typing import TYPE_CHECKING, Literal, Union class ClashSource(PropertyGroup): @@ -43,6 +46,10 @@ class ClashSource(PropertyGroup): name="Mode", ) + if TYPE_CHECKING: + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + mode: Literal["a", "i", "e"] + class Clash(PropertyGroup): a_global_id: StringProperty(name="A") @@ -51,9 +58,15 @@ class Clash(PropertyGroup): b_name: StringProperty(name="B Name") status: BoolProperty(name="Status", default=False) + if TYPE_CHECKING: + a_global_id: str + b_global_id: str + a_name: str + b_name: str + status: bool + class ClashSet(PropertyGroup): - name: StringProperty(name="Name") mode: EnumProperty( items=[ ( @@ -76,11 +89,25 @@ class ClashSet(PropertyGroup): b: CollectionProperty(name="Group B", type=ClashSource) clashes: CollectionProperty(name="Clashes", type=Clash) + if TYPE_CHECKING: + mode: Literal["intersection", "collision", "clearance"] + tolerance: float + clearance: float + allow_touching: bool + check_all: bool + a: bpy.types.bpy_prop_collection_idprop[ClashSource] + b: bpy.types.bpy_prop_collection_idprop[ClashSource] + clashes: bpy.types.bpy_prop_collection_idprop[Clash] + class SmartClashGroup(PropertyGroup): number: StringProperty(name="Number") global_ids: CollectionProperty(name="GlobalIDs", type=StrProperty) + if TYPE_CHECKING: + number: str + global_ids: bpy.types.bpy_prop_collection_idprop[StrProperty] + class BIMClashProperties(PropertyGroup): blender_clash_set_a: CollectionProperty(name="Blender Clash Set A", type=StrProperty) @@ -107,17 +134,33 @@ class BIMClashProperties(PropertyGroup): subtype="FILE_PATH", ) - @property - def active_clash_set(self): - if self.active_clash_set_index < len(self.clash_sets): - return self.clash_sets[self.active_clash_set_index] + if TYPE_CHECKING: + blender_clash_set_a: bpy.types.bpy_prop_collection_idprop[StrProperty] + blender_clash_set_b: bpy.types.bpy_prop_collection_idprop[StrProperty] + clash_sets: bpy.types.bpy_prop_collection_idprop[ClashSet] + should_create_clash_snapshots: bool + clash_results_path: str + smart_grouped_clashes_path: str + active_clash_set_index: int + active_clash_index: int + smart_clash_groups: bpy.types.bpy_prop_collection_idprop[SmartClashGroup] + active_smart_group_index: int + smart_clash_grouping_max_distance: int + p1: Vector + p2: Vector + active_clash_text: str + export_path: str @property - def active_smart_group(self): - if self.active_smart_group_index < len(self.smart_clash_groups): - return self.smart_clash_groups[self.active_smart_group_index] + def active_clash_set(self) -> Union[ClashSet, None]: + return tool.Blender.get_active_uilist_element(self.clash_sets, self.active_clash_set_index) @property - def active_clash(self): - if self.active_clash_index < len(self.active_clash_set.clashes): - return self.active_clash_set.clashes[self.active_clash_index] + def active_smart_group(self) -> Union[SmartClashGroup, None]: + return tool.Blender.get_active_uilist_element(self.smart_clash_groups, self.active_smart_group_index) + + @property + def active_clash(self) -> Union[Clash, None]: + if not (clash_set := self.active_clash_set): + return None + return tool.Blender.get_active_uilist_element(clash_set.clashes, self.active_clash_index) diff --git a/src/bonsai/bonsai/bim/module/clash/ui.py b/src/bonsai/bonsai/bim/module/clash/ui.py index 2a95b54ab3..3943f21f04 100644 --- a/src/bonsai/bonsai/bim/module/clash/ui.py +++ b/src/bonsai/bonsai/bim/module/clash/ui.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import bpy +import bonsai.tool as tool import bonsai.bim.helper from bpy.types import Panel from bonsai.bim.module.clash.data import ClashData @@ -36,9 +37,7 @@ class BIM_PT_ifcclash(Panel): ClashData.load() layout = self.layout - - scene = context.scene - props = scene.BIMClashProperties + props = tool.Clash.get_clash_props() row = layout.row(align=True) row.operator("bim.add_clash_set") @@ -157,7 +156,7 @@ class BIM_PT_smart_clash_manager(Panel): def draw(self, context): layout = self.layout - props = context.scene.BIMClashProperties + props = tool.Clash.get_clash_props() row = layout.row() layout.label(text="Select clash results to group:") diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index 18667381d6..77c58e54ad 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -42,7 +42,8 @@ class AddCsvAttribute(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - attribute = context.scene.CsvProperties.csv_attributes.add() + props = tool.Blender.get_csv_props() + props.csv_attributes.add() return {"FINISHED"} @@ -53,7 +54,8 @@ class RemoveCsvAttribute(bpy.types.Operator): index: bpy.props.IntProperty() def execute(self, context): - context.scene.CsvProperties.csv_attributes.remove(self.index) + props = tool.Blender.get_csv_props() + props.csv_attributes.remove(self.index) return {"FINISHED"} @@ -64,7 +66,8 @@ class RemoveAllCsvAttributes(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} def execute(self, context): - context.scene.CsvProperties.csv_attributes.clear() + props = tool.Blender.get_csv_props() + props.csv_attributes.clear() return {"FINISHED"} @@ -76,8 +79,9 @@ class ReorderCsvAttribute(bpy.types.Operator): new_index: bpy.props.IntProperty() def execute(self, context): - old = context.scene.CsvProperties.csv_attributes[self.old_index] - new = context.scene.CsvProperties.csv_attributes[self.new_index] + props = tool.Blender.get_csv_props() + old = props.csv_attributes[self.old_index] + new = props.csv_attributes[self.new_index] props = ["name", "header", "sort", "group", "varies_value", "summary", "formatting"] for prop in props: value = getattr(new, prop) @@ -95,7 +99,7 @@ class ImportCsvAttributes(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() data = json.load(open(self.filepath)) tool.Search.import_filter_query(data["query"], props.filter_groups) @@ -136,7 +140,7 @@ class ExportCsvAttributes(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() settings = {} for prop in [ @@ -191,14 +195,14 @@ class ExportIfcCsv(bpy.types.Operator): @classmethod def poll(cls, context): - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() if not props.should_load_from_memory and not props.csv_ifc_file: cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.") return False return True def invoke(self, context, event): - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() if props.format == "web": return self.execute(context) self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}") @@ -209,7 +213,7 @@ class ExportIfcCsv(bpy.types.Operator): def execute(self, context): import ifccsv - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() self.filepath = bpy.path.ensure_ext(self.filepath, f".{props.format}") if props.should_load_from_memory: ifc_file = tool.Ifc.get() @@ -298,7 +302,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): @classmethod def poll(cls, context): - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() if not props.should_load_from_memory and not props.csv_ifc_file: cls.poll_message_set("Select an IFC file or use 'load from memory' if it's loaded in Bonsai.") return False @@ -313,7 +317,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): import ifccsv - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() if props.should_load_from_memory: ifc_file = tool.Ifc.get() else: @@ -349,7 +353,8 @@ class SelectCsvIfcFile(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): - context.scene.CsvProperties.csv_ifc_file = self.filepath + props = tool.Blender.get_csv_props() + props.csv_ifc_file = self.filepath return {"FINISHED"} def invoke(self, context, event): diff --git a/src/bonsai/bonsai/bim/module/csv/prop.py b/src/bonsai/bonsai/bim/module/csv/prop.py index 9426eea5a5..f66ee75c51 100644 --- a/src/bonsai/bonsai/bim/module/csv/prop.py +++ b/src/bonsai/bonsai/bim/module/csv/prop.py @@ -29,6 +29,7 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Literal class CsvAttribute(PropertyGroup): @@ -59,6 +60,14 @@ class CsvAttribute(PropertyGroup): ) formatting: StringProperty(default="{{value}}", name="Formatting") + if TYPE_CHECKING: + header: str + sort: Literal["NONE", "ASC", "DESC"] + group: Literal["NONE", "GROUP", "CONCAT", "VARIES", "SUM", "AVERAGE", "MIN", "MAX"] + varies_value: str + summary: Literal["NONE", "SUM", "AVERAGE", "MIN", "MAX"] + formatting: str + class CsvProperties(PropertyGroup): csv_ifc_file: StringProperty(default="", name="IFC File") @@ -104,3 +113,26 @@ class CsvProperties(PropertyGroup): name="Load from Memory", description="Use IFC file currently loaded in Bonsai", ) + + if TYPE_CHECKING: + csv_ifc_file: str + ifc_selector: str + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + csv_attributes: bpy.types.bpy_prop_collection_idprop[CsvAttribute] + should_generate_svg: bool + should_preserve_existing: bool + include_global_id: bool + null_value: str + empty_value: str + true_value: str + false_value: str + concat_value: str + csv_delimiter: Literal["NONE", "ASC", "DESC"] + format: Literal["csv", "xlsx", "ods", "web"] + csv_custom_delimiter: str + should_show_settings: bool + should_show_sort: bool + should_show_group: bool + should_show_summary: bool + should_show_formatting: bool + should_load_from_memory: bool diff --git a/src/bonsai/bonsai/bim/module/csv/ui.py b/src/bonsai/bonsai/bim/module/csv/ui.py index 6aa41177b0..ec46867fd9 100644 --- a/src/bonsai/bonsai/bim/module/csv/ui.py +++ b/src/bonsai/bonsai/bim/module/csv/ui.py @@ -33,9 +33,7 @@ class BIM_PT_ifccsv(Panel): def draw(self, context): layout = self.layout - - scene = context.scene - props = scene.CsvProperties + props = tool.Blender.get_csv_props() if tool.Ifc.get(): row = layout.row(align=True) diff --git a/src/bonsai/bonsai/bim/module/diff/data.py b/src/bonsai/bonsai/bim/module/diff/data.py index c8fda6850c..ac3e545f43 100644 --- a/src/bonsai/bonsai/bim/module/diff/data.py +++ b/src/bonsai/bonsai/bim/module/diff/data.py @@ -43,7 +43,7 @@ class DiffData: @classmethod def diff_json(cls): - props = bpy.context.scene.DiffProperties + props = tool.Blender.get_diff_props() if not props.diff_json_file: cls.diff = None return diff --git a/src/bonsai/bonsai/bim/module/diff/operator.py b/src/bonsai/bonsai/bim/module/diff/operator.py index 516b26d9ad..a5c097db51 100644 --- a/src/bonsai/bonsai/bim/module/diff/operator.py +++ b/src/bonsai/bonsai/bim/module/diff/operator.py @@ -36,7 +36,8 @@ class SelectDiffJsonFile(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) def execute(self, context): - context.scene.DiffProperties.diff_json_file = self.filepath + props = tool.Blender.get_diff_props() + props.diff_json_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -51,7 +52,8 @@ class VisualiseDiff(bpy.types.Operator): def execute(self, context): ifc_file = tool.Ifc.get() - with open(context.scene.DiffProperties.diff_json_file, "r") as file: + props = tool.Blender.get_diff_props() + with open(props.diff_json_file, "r") as file: diff = json.load(file) for obj in context.visible_objects: obj.color = (1.0, 1.0, 1.0, 1.0) @@ -111,7 +113,8 @@ class SelectDiffOldFile(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) def execute(self, context): - context.scene.DiffProperties.old_file = self.filepath + props = tool.Blender.get_diff_props() + props.old_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -128,7 +131,8 @@ class SelectDiffNewFile(bpy.types.Operator): filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) def execute(self, context): - context.scene.DiffProperties.new_file = self.filepath + props = tool.Blender.get_diff_props() + props.new_file = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -153,7 +157,7 @@ class ExecuteIfcDiff(bpy.types.Operator): def execute(self, context): import ifcdiff - self.props = context.scene.DiffProperties + self.props = tool.Blender.get_diff_props() if tool.Ifc.get(): if self.props.active_file == "NONE": @@ -242,7 +246,8 @@ class SelectDiffObjects(bpy.types.Operator): def execute(self, context): ifc_file = tool.Ifc.get() - with open(context.scene.DiffProperties.diff_json_file, "r") as file: + props = tool.Blender.get_diff_props() + with open(props.diff_json_file, "r") as file: diff = json.load(file) for obj in context.visible_objects: obj.select_set(False) diff --git a/src/bonsai/bonsai/bim/module/diff/prop.py b/src/bonsai/bonsai/bim/module/diff/prop.py index 3f09b39900..61da124a7c 100644 --- a/src/bonsai/bonsai/bim/module/diff/prop.py +++ b/src/bonsai/bonsai/bim/module/diff/prop.py @@ -30,18 +30,25 @@ from bpy.props import ( FloatVectorProperty, CollectionProperty, ) +from typing import TYPE_CHECKING, Literal, get_args -def update_diff_json_file(self, context): +def update_diff_json_file(self: "DiffProperties", context: bpy.types.Context) -> None: DiffData.data["diff_json"] = DiffData.diff_json() +RelationshipType = Literal["type", "property", "container", "aggregate", "classification"] + + class Relationships(PropertyGroup): relationship: EnumProperty( name="Relationship", - items=[(r, r.capitalize(), r) for r in ["type", "property", "container", "aggregate", "classification"]], + items=[(r, r.capitalize(), r) for r in get_args(RelationshipType)], ) + if TYPE_CHECKING: + relationship: RelationshipType + class DiffProperties(PropertyGroup): diff_json_file: StringProperty(default="", name="JSON Output", update=update_diff_json_file) @@ -59,3 +66,12 @@ class DiffProperties(PropertyGroup): name="Active File", default="NEW", ) + + if TYPE_CHECKING: + diff_json_file: str + old_file: str + new_file: str + diff_relationships: bpy.types.bpy_prop_collection_idprop[Relationships] + filter_groups: bpy.types.bpy_prop_collection_idprop[BIMFilterGroup] + should_load_changed_elements: bool + active_file: Literal["NONE", "OLD", "NEW"] diff --git a/src/bonsai/bonsai/bim/module/diff/ui.py b/src/bonsai/bonsai/bim/module/diff/ui.py index 34e66df0f5..91e0a6b48e 100644 --- a/src/bonsai/bonsai/bim/module/diff/ui.py +++ b/src/bonsai/bonsai/bim/module/diff/ui.py @@ -37,9 +37,7 @@ class BIM_PT_diff(Panel): layout = self.layout layout.use_property_split = True - - scene = context.scene - props = scene.DiffProperties + props = tool.Blender.get_diff_props() layout.label(text="IFC Diff Setup:") diff --git a/src/bonsai/bonsai/bim/module/search/operator.py b/src/bonsai/bonsai/bim/module/search/operator.py index 32d3729d58..e1090b6cf1 100644 --- a/src/bonsai/bonsai/bim/module/search/operator.py +++ b/src/bonsai/bonsai/bim/module/search/operator.py @@ -155,7 +155,7 @@ class Search(Operator): def execute(self, context): if self.property_group == "CsvProperties": - props = context.scene.CsvProperties + props = tool.Blender.get_csv_props() elif self.property_group == "BIMSearchProperties": props = tool.Search.get_search_props() else: diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index 297b3eb4df..d5269a405f 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -43,6 +43,8 @@ from typing_extensions import assert_never if TYPE_CHECKING: from bonsai.bim.prop import BIMProperties, BIMObjectProperties + from bonsai.bim.module.csv.prop import CsvProperties + from bonsai.bim.module.diff.prop import DiffProperties T = TypeVar("T") @@ -1600,6 +1602,14 @@ class Blender(bonsai.core.tool.Blender): dct = {cls.bl_idname: cls.ifc_element_type for cls in (BimTool.__subclasses__())} return types.MappingProxyType(dct) + @classmethod + def get_csv_props(cls) -> CsvProperties: + return bpy.context.scene.CsvProperties + + @classmethod + def get_diff_props(cls) -> DiffProperties: + return bpy.context.scene.DiffProperties + @classmethod def get_bim_props(cls, scene: Optional[bpy.types.Scene] = None) -> BIMProperties: if scene is None: diff --git a/src/bonsai/bonsai/tool/clash.py b/src/bonsai/bonsai/tool/clash.py index 57b7ffd4a8..2ec7ccb75d 100644 --- a/src/bonsai/bonsai/tool/clash.py +++ b/src/bonsai/bonsai/tool/clash.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Bonsai. If not, see . +from __future__ import annotations import os import bpy import json @@ -25,14 +26,23 @@ import bonsai.tool as tool from contextlib import contextmanager from mathutils import Vector from ifcclash import ifcclash +from typing import TYPE_CHECKING, Union + +if TYPE_CHECKING: + from bonsai.bim.module.clash.prop import BIMClashProperties class Clash(bonsai.core.tool.Clash): + @classmethod + def get_clash_props(cls) -> BIMClashProperties: + return bpy.context.scene.BIMClashProperties + @classmethod def export_clash_sets(cls) -> list[ifcclash.ClashSet]: - clash_sets = [] - for clash_set in bpy.context.scene.BIMClashProperties.clash_sets: + clash_sets: list[ifcclash.ClashSet] = [] + props = cls.get_clash_props() + for clash_set in props.clash_sets: a = [] b = [] for ab in ["a", "b"]: @@ -46,7 +56,7 @@ class Clash(bonsai.core.tool.Clash): a.append(clash_source) elif ab == "b": b.append(clash_source) - clash_set_data = {"name": clash_set.name, "mode": clash_set.mode, "a": a, "b": b} + clash_set_data = ifcclash.ClashSet(name=clash_set.name, mode=clash_set.mode, a=a, b=b) if clash_set.mode == "intersection": clash_set_data["tolerance"] = clash_set.tolerance clash_set_data["check_all"] = clash_set.check_all @@ -55,33 +65,37 @@ class Clash(bonsai.core.tool.Clash): elif clash_set.mode == "clearance": clash_set_data["clearance"] = clash_set.clearance clash_set_data["check_all"] = clash_set.check_all - clash_sets.append(clash_set_data) + clash_sets.append(ifcclash.ClashSet(**clash_set_data)) return clash_sets @classmethod - def get_clash(cls, clash_set, a_global_id, b_global_id): + def get_clash( + cls, clash_set: ifcclash.ClashSet, a_global_id: str, b_global_id: str + ) -> Union[ifcclash.ClashResult, None]: clashes = clash_set.get("clashes", None) if not clashes: return return clashes.get(f"{a_global_id}-{b_global_id}", None) @classmethod - def get_clash_set(cls, name): + def get_clash_set(cls, name: str) -> Union[ifcclash.ClashSet, None]: for clash_set in ClashStore.clash_sets: if clash_set["name"] == name: return clash_set @classmethod - def get_clash_sets(cls): + def get_clash_sets(cls) -> list[ifcclash.ClashSet]: return ClashStore.clash_sets @classmethod - def import_active_clashes(cls): - clash_set = bpy.context.scene.BIMClashProperties.active_clash_set + def import_active_clashes(cls) -> None: + props = cls.get_clash_props() + clash_set = props.active_clash_set if not clash_set: return clash_set.clashes.clear() result = tool.Clash.get_clash_set(clash_set.name) + assert result is not None for clash in sorted(result.get("clashes", {}).values(), key=lambda x: x["distance"]): blender_clash = clash_set.clashes.add() blender_clash.a_global_id = clash["a_global_id"] @@ -91,17 +105,19 @@ class Clash(bonsai.core.tool.Clash): blender_clash.status = False if not "status" in clash.keys() else clash["status"] @classmethod - def load_clash_sets(cls, fn): + def load_clash_sets(cls, fn: str) -> None: with open(fn) as f: ClashStore.clash_sets = json.load(f) @classmethod - def look_at(cls, target, location): + def look_at(cls, target: Vector, location: Vector) -> None: camera_location = location area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") region = next(region for region in area.regions if region.type == "WINDOW") space = next(space for space in area.spaces if space.type == "VIEW_3D") override = {"area": area, "region": region, "space_data": space} + assert isinstance(space, bpy.types.SpaceView3D) + assert space.region_3d space.region_3d.view_location = target space.region_3d.view_rotation = Vector((camera_location - target)).to_track_quat("Z", "Y") space.region_3d.view_distance = (camera_location - target).length @@ -109,10 +125,8 @@ class Clash(bonsai.core.tool.Clash): class ClashStore: - clash_sets = None - path = None + clash_sets: list[ifcclash.ClashSet] = [] @staticmethod def purge(): - ClashStore.clash_sets = None - ClashStore.path = None + ClashStore.clash_sets = [] diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index a48e1404b6..635b1c01dd 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -43,22 +43,21 @@ class Search(bonsai.core.tool.Search): return json.loads(group.Description)["query"] @classmethod - def get_filter_groups(cls, module: str) -> bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]: + def get_filter_groups(cls, module: FilterModule) -> bpy.types.bpy_prop_collection_idprop[BIMFilterGroup]: if module == "search": return cls.get_search_props().filter_groups elif module == "csv": - return bpy.context.scene.CsvProperties.filter_groups + return tool.Blender.get_csv_props().filter_groups elif module == "diff": - return bpy.context.scene.DiffProperties.filter_groups + return tool.Blender.get_diff_props().filter_groups elif module == "drawing_include": return bpy.context.scene.camera.data.BIMCameraProperties.include_filter_groups elif module == "drawing_exclude": return bpy.context.scene.camera.data.BIMCameraProperties.exclude_filter_groups elif module.startswith("clash"): _, clash_set_index, ab, clash_source_index = module.split("_") - return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[ - int(clash_source_index) - ].filter_groups + props = tool.Clash.get_clash_props() + return getattr(props.clash_sets[int(clash_set_index)], ab)[int(clash_source_index)].filter_groups assert False, f"Unsupported module: {module}" @classmethod diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py index 86353c9e82..c15fac39a4 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_geometric_representation.py @@ -18,6 +18,7 @@ import ifcopenshell import ifcopenshell.api.alignment +import ifcopenshell.api.geometry from ifcopenshell import entity_instance import math From 1600e00295dcba334cf340a157d3de5108736601 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Mar 2025 12:34:47 +0500 Subject: [PATCH 467/476] Fix clash smart groups selection (6660ee3) It was using prop `guid` which doesn't exist , so `by_guid(global_id.guid)` would always fail with some exception. --- src/bonsai/bonsai/bim/module/clash/operator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index 3b19612c98..39621a3c4e 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -484,7 +484,7 @@ class LoadSmartGroupsForActiveClashSet(bpy.types.Operator): for pair in global_id_pairs: for guid in pair: new_global_id = new_group.global_ids.add() - new_global_id.guid = guid + new_global_id.name = guid return {"FINISHED"} @@ -500,14 +500,15 @@ class SelectSmartGroup(bpy.types.Operator): return tool.Ifc.get() and context.visible_objects and props.active_smart_group def execute(self, context): + ifc_file = tool.Ifc.get() props = tool.Clash.get_clash_props() selected_smart_group = props.active_smart_group assert selected_smart_group products: list[ifcopenshell.entity_instance] = [] for global_id in selected_smart_group.global_ids: try: - products.append(tool.Ifc.get().by_guid(global_id.guid)) - except: + products.append(ifc_file.by_guid(global_id.name)) + except RuntimeError: continue tool.Spatial.select_products(products, unhide=True) context_override = tool.Blender.get_viewport_context() From 2f34f997ddf636113abc1324a0067093e0fc7409 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Mar 2025 15:58:33 +0500 Subject: [PATCH 468/476] Fix api.alignment missing __all__ --- .../ifcopenshell/api/alignment/__init__.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py index e32853f601..bb127bc568 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/__init__.py @@ -69,3 +69,34 @@ from .remove_last_segment import remove_last_segment from .remove_zero_length_segment import remove_zero_length_segment from .update_curve_segment_transition_code import update_curve_segment_transition_code from .util import * + +__all__ = [ + "add_segment_to_curve", + "add_segment_to_layout", + "add_stationing_to_alignment", + "add_vertical_alignment_by_pi_method", + "add_vertical_alignment", + "add_zero_length_segment", + "create_alignment_by_pi_method", + "create_alignment_from_csv", + "create_horizontal_alignment_by_pi_method", + "create_geometric_representation", + "create_vertical_alignment_by_pi_method", + "distance_along_from_station", + "get_alignment_layouts", + "get_axis_subcontext", + "get_basis_curve", + "get_child_alignments", + "get_curve", + "get_parent_alignment", + "has_zero_length_segment", + "map_alignment_segments", + "map_alignment_segment", + "map_alignment_horizontal_segment", + "map_alignment_vertical_segment", + "map_alignment_cant_segment", + "name_segments", + "remove_last_segment", + "remove_zero_length_segment", + "update_curve_segment_transition_code", +] From 4f091af935f89a85103a7070201a8c5538da17b7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Mar 2025 16:12:28 +0500 Subject: [PATCH 469/476] bim.export_ifccsv - clarify description and title --- src/bonsai/bonsai/bim/module/csv/operator.py | 7 +++++++ src/bonsai/bonsai/bim/module/csv/ui.py | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index 77c58e54ad..bbb9da89f7 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -201,6 +201,13 @@ class ExportIfcCsv(bpy.types.Operator): return False return True + @classmethod + def description(cls, context, properties): + props = tool.Blender.get_csv_props() + if props.format == "web": + return "Open Web UI for spreadsheet data export." + return f"Export IFC data as a spreadsheet by the provided filepath in '{props.format}' format ." + def invoke(self, context, event): props = tool.Blender.get_csv_props() if props.format == "web": diff --git a/src/bonsai/bonsai/bim/module/csv/ui.py b/src/bonsai/bonsai/bim/module/csv/ui.py index ec46867fd9..bc994a0815 100644 --- a/src/bonsai/bonsai/bim/module/csv/ui.py +++ b/src/bonsai/bonsai/bim/module/csv/ui.py @@ -124,5 +124,8 @@ class BIM_PT_ifccsv(Panel): row.operator("bim.remove_csv_attribute", icon="X", text="").index = index row = layout.row(align=True) - row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper()) + if props.format == "web": + row.operator("bim.export_ifccsv", icon="EXPORT", text="Open Web UI") + else: + row.operator("bim.export_ifccsv", icon="EXPORT", text="Export IFC to " + props.format.upper()) row.operator("bim.import_ifccsv", icon="IMPORT") From 4594f596c27c7f1e513f74a21f3c7eade9af78ee Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 24 Mar 2025 16:30:53 +0500 Subject: [PATCH 470/476] Reuse ImportHelper/ExportHelper everywhere possible for consistency --- src/bonsai/bonsai/bim/module/bcf/operator.py | 36 ++++--------- .../bonsai/bim/module/brick/operator.py | 15 ++---- .../bonsai/bim/module/clash/operator.py | 52 +++++-------------- .../bim/module/classification/operator.py | 8 +-- src/bonsai/bonsai/bim/module/cost/operator.py | 8 ++- src/bonsai/bonsai/bim/module/csv/operator.py | 39 ++++---------- .../bonsai/bim/module/debug/operator.py | 12 ++--- src/bonsai/bonsai/bim/module/diff/operator.py | 34 +++--------- .../bonsai/bim/module/drawing/operator.py | 35 +++---------- src/bonsai/bonsai/bim/module/fm/operator.py | 22 +++----- .../bim/module/georeference/operator.py | 11 ++-- .../bim/module/model/sverchok_modifier.py | 23 ++------ .../bonsai/bim/module/patch/operator.py | 17 ++---- .../bonsai/bim/module/project/operator.py | 42 +++++---------- .../bonsai/bim/module/sequence/operator.py | 6 +-- .../bonsai/bim/module/style/operator.py | 19 ++----- .../bonsai/bim/module/tester/operator.py | 9 ++-- src/bonsai/bonsai/bim/operator.py | 36 ++++--------- src/ifcsverchok/__init__.py | 9 ++-- 19 files changed, 118 insertions(+), 315 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/bcf/operator.py b/src/bonsai/bonsai/bim/module/bcf/operator.py index df07babe69..b8daa24e00 100644 --- a/src/bonsai/bonsai/bim/module/bcf/operator.py +++ b/src/bonsai/bonsai/bim/module/bcf/operator.py @@ -42,6 +42,7 @@ import ifcopenshell.util.unit import bonsai.tool as tool import bonsai.bim.module.bcf.prop as bcf_prop import bonsai.bim.module.bcf.bcfstore as bcfstore +from bpy_extras.io_utils import ImportHelper, ExportHelper from pathlib import Path from math import radians, degrees, atan, tan, cos, sin from mathutils import Vector, Matrix, Euler, geometry @@ -63,13 +64,14 @@ class NewBcfProject(bpy.types.Operator): return {"FINISHED"} -class LoadBcfProject(bpy.types.Operator): +class LoadBcfProject(bpy.types.Operator, ImportHelper): bl_idname = "bim.load_bcf_project" bl_label = "Load BCF Project" bl_description = "Load the BCF file." bl_options = {"REGISTER", "UNDO"} filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"}) filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"}) + filename_ext = ".bcf" def execute(self, context): # Operator is also used when new project is created by not yet saved. @@ -109,10 +111,6 @@ class LoadBcfProject(bpy.types.Operator): self.report({"INFO"}, f"BCF Project '{Path(self.filepath).name}' is loaded.") return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class UnloadBcfProject(bpy.types.Operator): bl_idname = "bim.unload_bcf_project" @@ -342,7 +340,7 @@ class EditBcfTopic(bpy.types.Operator): return {"FINISHED"} -class SaveBcfProject(bpy.types.Operator): +class SaveBcfProject(bpy.types.Operator, ExportHelper): bl_idname = "bim.save_bcf_project" bl_label = "Save BCF Project" bl_description = "Save active BCF project by the provided filepath." @@ -350,6 +348,7 @@ class SaveBcfProject(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.bcf;*.bcfzip", options={"HIDDEN"}) save_current_bcf: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"}) + filename_ext = ".bcf" def execute(self, context): bcfxml = bcfstore.BcfStore.get_bcfxml() @@ -366,8 +365,7 @@ class SaveBcfProject(bpy.types.Operator): self.filepath = str(path) return self.execute(context) - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) class AddBcfTopic(bpy.types.Operator): @@ -1536,13 +1534,13 @@ class OpenBcfReferenceLink(bpy.types.Operator): return {"FINISHED"} -class SelectBcfHeaderFile(bpy.types.Operator): +class SelectBcfHeaderFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_bcf_header_file" bl_label = "Select BCF Header File" bl_description = "Select filepath for BCF header reference." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"}) + filename_ext = ".ifc" def execute(self, context): if self.filepath: @@ -1550,17 +1548,12 @@ class SelectBcfHeaderFile(bpy.types.Operator): props.file_reference = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectBcfBimSnippetReference(bpy.types.Operator): +class SelectBcfBimSnippetReference(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_bcf_bim_snippet_reference" bl_label = "Select BCF BIM Snippet Reference" bl_description = "Select filepath for BCF snippet reference." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): if self.filepath: @@ -1568,17 +1561,12 @@ class SelectBcfBimSnippetReference(bpy.types.Operator): props.bim_snippet_reference = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectBcfDocumentReference(bpy.types.Operator): +class SelectBcfDocumentReference(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_bcf_document_reference" bl_label = "Select BCF Document Reference" bl_description = "Select filepath for BCF document reference." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): if self.filepath: @@ -1586,10 +1574,6 @@ class SelectBcfDocumentReference(bpy.types.Operator): props.document_reference = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class LoadBcfHeaderIfcFile(bpy.types.Operator): bl_idname = "bim.load_bcf_header_ifc_file" diff --git a/src/bonsai/bonsai/bim/module/brick/operator.py b/src/bonsai/bonsai/bim/module/brick/operator.py index e9b20a215d..bb4f5f3277 100644 --- a/src/bonsai/bonsai/bim/module/brick/operator.py +++ b/src/bonsai/bonsai/bim/module/brick/operator.py @@ -22,17 +22,18 @@ import ifcopenshell.api import bonsai.tool as tool import bonsai.core.brick as core import bonsai.bim.handler +from bpy_extras.io_utils import ImportHelper, ExportHelper from bonsai.bim.ifc import IfcStore from bonsai.tool.brick import BrickStore -class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator): +class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.load_brick_project" bl_label = "Load Brickschema Project" bl_options = {"REGISTER", "UNDO"} bl_description = "Load in a Brick project from a file" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ttl", options={"HIDDEN"}) + filename_ext = ".ttl" def _execute(self, context): if os.path.exists(self.filepath) and "ttl" in os.path.splitext(self.filepath)[1].lower(): @@ -41,10 +42,6 @@ class LoadBrickProject(bpy.types.Operator, tool.Ifc.Operator): else: self.report({"ERROR"}, f"Failed to load {self.filepath}") - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class ViewBrickClass(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.view_brick_class" @@ -240,7 +237,7 @@ class RemoveBrick(bpy.types.Operator, tool.Ifc.Operator): ) -class SerializeBrick(bpy.types.Operator): +class SerializeBrick(bpy.types.Operator, ExportHelper): bl_idname = "bim.serialize_brick" bl_label = "Serialize Brick" # Prevents crash on Blender 4.4.0. @@ -251,9 +248,7 @@ class SerializeBrick(bpy.types.Operator): def invoke(self, context, event): if self.should_save_as or not BrickStore.path: - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) else: return self.execute(context) diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index 39621a3c4e..83fed588d2 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -24,26 +24,20 @@ import logging import numpy as np import ifcopenshell import bonsai.tool as tool +from bpy_extras.io_utils import ExportHelper, ImportHelper from math import radians from mathutils import Matrix, Vector from bonsai.bim.ifc import IfcStore from bonsai.bim.module.clash.decorator import ClashDecorator -class ExportClashSets(bpy.types.Operator): +class ExportClashSets(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_clash_sets" bl_label = "Export Clash Sets" bl_description = "Export clash sets to a selected file" filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - def execute(self, context): self.filepath = bpy.path.ensure_ext(self.filepath, ".json") clash_sets = tool.Clash.export_clash_sets() @@ -52,20 +46,17 @@ class ExportClashSets(bpy.types.Operator): return {"FINISHED"} -class ImportClashSets(bpy.types.Operator): +class ImportClashSets(bpy.types.Operator, ImportHelper): bl_idname = "bim.import_clash_sets" bl_label = "Import Clash Sets" bl_options = {"REGISTER", "UNDO"} bl_description = "Import clash sets from a selected file" filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) def invoke(self, context, event): self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def execute(self, context): tool.Clash.load_clash_sets(self.filepath) @@ -155,15 +146,15 @@ class RemoveClashSource(bpy.types.Operator): return {"FINISHED"} -class SelectClashSource(bpy.types.Operator): +class SelectClashSource(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_clash_source" bl_label = "Select Clash Source" bl_options = {"REGISTER", "UNDO"} bl_description = "Select an IFC file to add as a clash source" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) index: bpy.props.IntProperty() group: bpy.props.StringProperty() + filename_ext = ".ifc" def execute(self, context): props = tool.Clash.get_clash_props() @@ -171,46 +162,32 @@ class SelectClashSource(bpy.types.Operator): getattr(clash_set, self.group)[self.index].name = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectClashResults(bpy.types.Operator): +class SelectClashResults(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_clash_results" bl_label = "Select Clash Results" bl_description = "Select filepath for clash results." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = tool.Clash.get_clash_props() props.clash_results_path = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectSmartGroupedClashesPath(bpy.types.Operator): +class SelectSmartGroupedClashesPath(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_smart_grouped_clashes_path" bl_label = "Select Smart-Grouped Clashes Path" bl_description = "Select filepath for smart-grouped clashes." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = tool.Clash.get_clash_props() props.smart_grouped_clashes_path = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class ExecuteIfcClash(bpy.types.Operator): +class ExecuteIfcClash(bpy.types.Operator, ExportHelper): bl_idname = "bim.execute_ifc_clash" bl_label = "Execute IFC Clash" bl_description = "Execute clash detection and save the information to a .bcf or .json file" @@ -220,8 +197,7 @@ class ExecuteIfcClash(bpy.types.Operator): def invoke(self, context, event): if self.filepath: return self.execute(context) - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def execute(self, context): from ifcclash import ifcclash @@ -294,19 +270,17 @@ class ExecuteIfcClash(bpy.types.Operator): return {"FINISHED"} -class SelectIfcClashResults(bpy.types.Operator): +class SelectIfcClashResults(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_ifc_clash_results" bl_label = "Select IFC Clash Results" bl_options = {"REGISTER", "UNDO"} bl_description = "Select the clashing IFC geometry stored in a file" + filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def invoke(self, context, event): self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def execute(self, context): # TODO refactor into new clash results system diff --git a/src/bonsai/bonsai/bim/module/classification/operator.py b/src/bonsai/bonsai/bim/module/classification/operator.py index a2b19fcae1..ba9f9d1928 100644 --- a/src/bonsai/bonsai/bim/module/classification/operator.py +++ b/src/bonsai/bonsai/bim/module/classification/operator.py @@ -25,20 +25,16 @@ import ifcopenshell.util.classification import ifcopenshell.util.element import bonsai.tool as tool import bonsai.bim.helper +from bpy_extras.io_utils import ImportHelper from bonsai.bim.ifc import IfcStore -class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator): +class LoadClassificationLibrary(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.load_classification_library" bl_label = "Load Classification Library" bl_description = "Load classification library from the provided filepath." filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} def _execute(self, context): IfcStore.classification_file = ifcopenshell.open(self.filepath) diff --git a/src/bonsai/bonsai/bim/module/cost/operator.py b/src/bonsai/bonsai/bim/module/cost/operator.py index ead79ba57a..3cbe9b4d68 100644 --- a/src/bonsai/bonsai/bim/module/cost/operator.py +++ b/src/bonsai/bonsai/bim/module/cost/operator.py @@ -21,7 +21,7 @@ import bpy import ifcopenshell.api import bonsai.tool as tool -from bpy_extras.io_utils import ImportHelper +from bpy_extras.io_utils import ImportHelper, ExportHelper import bonsai.tool as tool import bonsai.core.cost as core from typing import get_args, TYPE_CHECKING @@ -678,7 +678,7 @@ class CalculateCostItemResourceValue(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} -class ExportCostSchedules(bpy.types.Operator): +class ExportCostSchedules(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_cost_schedules" bl_label = "Export Cost Schedule" bl_options = {"REGISTER", "UNDO"} @@ -701,9 +701,7 @@ class ExportCostSchedules(bpy.types.Operator): return {"FINISHED"} def invoke(self, context, event): - wm = context.window_manager - wm.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def draw(self, context): self.layout.label(text="Choose a format") diff --git a/src/bonsai/bonsai/bim/module/csv/operator.py b/src/bonsai/bonsai/bim/module/csv/operator.py index bbb9da89f7..34a296fe6f 100644 --- a/src/bonsai/bonsai/bim/module/csv/operator.py +++ b/src/bonsai/bonsai/bim/module/csv/operator.py @@ -27,6 +27,7 @@ import ifcopenshell import ifcopenshell.util.selector import bonsai.tool as tool import bonsai.bim.module.drawing.scheduler as scheduler +from bpy_extras.io_utils import ExportHelper, ImportHelper from bonsai.bim.handler import refresh_ui_data from typing import TYPE_CHECKING from collections import Counter @@ -90,13 +91,13 @@ class ReorderCsvAttribute(bpy.types.Operator): return {"FINISHED"} -class ImportCsvAttributes(bpy.types.Operator): +class ImportCsvAttributes(bpy.types.Operator, ImportHelper): bl_idname = "bim.import_csv_attributes" bl_label = "Load CSV Settings" bl_description = "Import a json template for CSV export" bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filename_ext = ".json" def execute(self, context): props = tool.Blender.get_csv_props() @@ -125,19 +126,14 @@ class ImportCsvAttributes(bpy.types.Operator): setattr(new, prop, attribute[prop]) return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class ExportCsvAttributes(bpy.types.Operator): +class ExportCsvAttributes(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_csv_attributes" bl_label = "Save CSV Settings" bl_options = {"REGISTER", "UNDO"} bl_description = "Save a json template for CSV export" filename_ext = ".json" filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = tool.Blender.get_csv_props() @@ -179,14 +175,8 @@ class ExportCsvAttributes(bpy.types.Operator): return {"FINISHED"} - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class ExportIfcCsv(bpy.types.Operator): +class ExportIfcCsv(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_ifccsv" bl_label = "Export IFC" bl_description = "Export IFC data as a spreadsheet." @@ -212,10 +202,7 @@ class ExportIfcCsv(bpy.types.Operator): props = tool.Blender.get_csv_props() if props.format == "web": return self.execute(context) - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def execute(self, context): import ifccsv @@ -299,13 +286,12 @@ class ExportIfcCsv(bpy.types.Operator): ] -class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): +class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.import_ifccsv" bl_label = "Import to IFC" bl_description = "Import IFC data from a spreadsheet." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") @classmethod def poll(cls, context): @@ -317,9 +303,7 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): def invoke(self, context, event): self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".csv") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def _execute(self, context): import ifccsv @@ -350,20 +334,15 @@ class ImportIfcCsv(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} -class SelectCsvIfcFile(bpy.types.Operator): +class SelectCsvIfcFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_csv_ifc_file" bl_label = "Select CSV IFC File" bl_description = "Select IFC file for spreadsheet import/export." bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = tool.Blender.get_csv_props() props.csv_ifc_file = self.filepath return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index c68bedbeb0..0b1c59211a 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -36,6 +36,7 @@ import bonsai.core.profile import bonsai.core.type import bonsai.bim.handler import bonsai.bim.import_ifc as import_ifc +from bpy_extras.io_utils import ImportHelper, ExportHelper from pathlib import Path from bonsai import get_debug_info, format_debug_info from bonsai.bim.ifc import IfcStore @@ -446,12 +447,11 @@ class ParseExpress(bpy.types.Operator): return {"FINISHED"} -class SelectExpressFile(bpy.types.Operator): +class SelectExpressFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_express_file" bl_label = "Select Express File" bl_options = {"REGISTER", "UNDO"} bl_description = "Select an IFC EXPRESS definition" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.exp", options={"HIDDEN"}) def execute(self, context): @@ -460,10 +460,6 @@ class SelectExpressFile(bpy.types.Operator): props.express_file = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class PurgeHdf5Cache(bpy.types.Operator): bl_idname = "bim.purge_hdf5_cache" @@ -523,7 +519,7 @@ class PrintUnusedElementStats(bpy.types.Operator): return {"FINISHED"} -class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator): +class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator, ExportHelper): bl_idname = "bim.purge_unused_elements_by_class" bl_label = "Purge Unused Elements By Class" bl_description = ( @@ -537,7 +533,7 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator): def invoke(self, context, event): if event.type == "LEFTMOUSE" and event.alt: - context.window_manager.fileselect_add(self) + return ExportHelper.invoke(self, context, event) return self.execute(context) @classmethod diff --git a/src/bonsai/bonsai/bim/module/diff/operator.py b/src/bonsai/bonsai/bim/module/diff/operator.py index a5c097db51..954e43ac1e 100644 --- a/src/bonsai/bonsai/bim/module/diff/operator.py +++ b/src/bonsai/bonsai/bim/module/diff/operator.py @@ -24,26 +24,23 @@ import ifcopenshell import bonsai.bim.handler import bonsai.bim.import_ifc import bonsai.tool as tool +from bpy_extras.io_utils import ImportHelper, ExportHelper from bonsai.bim.ifc import IfcStore -class SelectDiffJsonFile(bpy.types.Operator): +class SelectDiffJsonFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_diff_json_file" bl_label = "Select Diff JSON File" bl_description = "Select filepath for IFC diff results." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) + filename_ext = ".json" def execute(self, context): props = tool.Blender.get_diff_props() props.diff_json_file = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class VisualiseDiff(bpy.types.Operator): bl_idname = "bim.visualise_diff" @@ -104,56 +101,41 @@ class VisualiseDiff(bpy.types.Operator): return {"FINISHED"} -class SelectDiffOldFile(bpy.types.Operator): +class SelectDiffOldFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_diff_old_file" bl_label = "Select Diff Old File" bl_description = "Select filepath for an old IFC file to compare." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) + filename_ext = ".ifc" def execute(self, context): props = tool.Blender.get_diff_props() props.old_file = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectDiffNewFile(bpy.types.Operator): +class SelectDiffNewFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_diff_new_file" bl_label = "Select Diff New File" bl_description = "Select filepath for a new IFC file to compare." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) + filename_ext = ".ifc" def execute(self, context): props = tool.Blender.get_diff_props() props.new_file = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class ExecuteIfcDiff(bpy.types.Operator): +class ExecuteIfcDiff(bpy.types.Operator, ExportHelper): bl_idname = "bim.execute_ifc_diff" bl_label = "Execute IFC Diff" bl_description = "Compare two IFC files and save a json diff report by the provided filepath." filename_ext = ".json" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) - def invoke(self, context, event): - self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".json") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} - def execute(self, context): import ifcdiff diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 0cc32e5d07..56a59506d0 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -43,6 +43,7 @@ import bonsai.bim.module.drawing.svgwriter as svgwriter import bonsai.bim.module.drawing.annotation as annotation import bonsai.bim.module.drawing.sheeter as sheeter import bonsai.bim.export_ifc +from bpy_extras.io_utils import ImportHelper from bonsai.bim.module.drawing.decoration import CutDecorator from bonsai.bim.module.drawing.data import DecoratorData, DrawingsData from typing import NamedTuple, List, Union, Optional, Literal @@ -2217,12 +2218,12 @@ class ActivateDrawingFromSheet(bpy.types.Operator, ActivateDrawingBase): # TODO: not exposed to the UI. -class SelectDocIfcFile(bpy.types.Operator): +class SelectDocIfcFile(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_doc_ifc_file" bl_label = "Select Documentation IFC File" bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filename_ext = ".ifc" index: bpy.props.IntProperty() def execute(self, context): @@ -2230,10 +2231,6 @@ class SelectDocIfcFile(bpy.types.Operator): props.ifc_files[self.index].name = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class ResizeText(bpy.types.Operator): bl_idname = "bim.resize_text" @@ -2612,13 +2609,12 @@ class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator): core.remove_sheet(tool.Ifc, tool.Drawing, sheet=tool.Ifc.get().by_id(self.sheet)) -class AddSchedule(bpy.types.Operator, tool.Ifc.Operator): +class AddSchedule(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.add_schedule" bl_label = "Add Schedule" bl_options = {"REGISTER", "UNDO"} bl_description = "Add an .ods, .xls or .xlsx file as a schedule" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ods;*.xls;*.xlsx", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) @@ -2626,10 +2622,6 @@ class AddSchedule(bpy.types.Operator, tool.Ifc.Operator): filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) core.add_document(tool.Ifc, tool.Drawing, "SCHEDULE", uri=filepath) - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class RemoveSchedule(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_schedule" @@ -2799,24 +2791,20 @@ class AddReferenceToSheet(bpy.types.Operator, tool.Ifc.Operator): tool.Drawing.import_sheets() -class AddReference(bpy.types.Operator, tool.Ifc.Operator): +class AddReference(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.add_reference" bl_label = "Add Reference" bl_description = "Import a .svg file to the project as a reference" - bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filter_glob: bpy.props.StringProperty(default="*.svg", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) + filename_ext = ".svg" def _execute(self, context): filepath = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) core.add_document(tool.Ifc, tool.Drawing, "REFERENCE", uri=filepath) - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class RemoveReference(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.remove_reference" @@ -3445,7 +3433,7 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): bpy.ops.bim.activate_drawing(drawing=element.id(), should_view_from_camera=False) -class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator): +class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.add_reference_image" bl_label = "Add Reference Image" bl_description = "Add or import reference image to the IFC project" @@ -3453,9 +3441,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator): bl_options = {"REGISTER", "UNDO"} use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True) - filepath: bpy.props.StringProperty( - name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH" - ) filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) @@ -3483,10 +3468,6 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator): layout.prop(self, "override_existing_image") layout.prop(self, "use_existing_object_by_name") - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - def _execute(self, context): abs_path = Path(self.filepath).absolute().resolve() image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path)) diff --git a/src/bonsai/bonsai/bim/module/fm/operator.py b/src/bonsai/bonsai/bim/module/fm/operator.py index c78f70dee4..fac40439c2 100644 --- a/src/bonsai/bonsai/bim/module/fm/operator.py +++ b/src/bonsai/bonsai/bim/module/fm/operator.py @@ -24,15 +24,15 @@ import logging import tempfile import ifcopenshell import bonsai.tool as tool +from bpy_extras.io_utils import ExportHelper, ImportHelper -class ExecuteIfcFM(bpy.types.Operator): +class ExecuteIfcFM(bpy.types.Operator, ExportHelper): bl_idname = "bim.execute_ifcfm" bl_label = "Execute IfcFM" bl_description = "Export IfcFM data as a spreadsheet." file_format: bpy.props.StringProperty() filter_glob: bpy.props.StringProperty(default="*.csv;*.ods;*.xlsx", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") @classmethod def poll(cls, context): @@ -45,9 +45,7 @@ class ExecuteIfcFM(bpy.types.Operator): def invoke(self, context, event): props = context.scene.BIMFMProperties self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def execute(self, context): props = context.scene.BIMFMProperties @@ -83,13 +81,12 @@ class ExecuteIfcFM(bpy.types.Operator): return {"FINISHED"} -class SelectFMSpreadsheetFiles(bpy.types.Operator): +class SelectFMSpreadsheetFiles(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_fm_spreadsheet_files" bl_label = "Select FM Spreadsheet Files" bl_description = "Select FM spreadsheets to merge." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement) def execute(self, context): @@ -101,17 +98,12 @@ class SelectFMSpreadsheetFiles(bpy.types.Operator): new.name = os.path.join(dirname, f.name) return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class ExecuteIfcFMFederate(bpy.types.Operator): +class ExecuteIfcFMFederate(bpy.types.Operator, ExportHelper): bl_idname = "bim.execute_ifcfm_federate" bl_label = "Merge IfcFM SpreadSheets" bl_description = "Merge added IfcFM spreadsheets." filter_glob: bpy.props.StringProperty(default="*.ods;*.xlsx", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") @classmethod def poll(cls, context): @@ -124,9 +116,7 @@ class ExecuteIfcFMFederate(bpy.types.Operator): def invoke(self, context, event): props = context.scene.BIMFMProperties self.filepath = bpy.path.ensure_ext(bpy.data.filepath, f".{props.format}") - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def execute(self, context): props = context.scene.BIMFMProperties diff --git a/src/bonsai/bonsai/bim/module/georeference/operator.py b/src/bonsai/bonsai/bim/module/georeference/operator.py index edfa9a9054..7d8f98dd6c 100644 --- a/src/bonsai/bonsai/bim/module/georeference/operator.py +++ b/src/bonsai/bonsai/bim/module/georeference/operator.py @@ -20,6 +20,7 @@ import bpy import bonsai.tool as tool import bonsai.core.georeference as core +from bpy_extras.io_utils import ImportHelper from bonsai.bim.module.georeference.decorator import GeoreferenceDecorator @@ -87,22 +88,18 @@ class GetCursorLocation(bpy.types.Operator, tool.Ifc.Operator): core.get_cursor_location(tool.Georeference) -class ImportPlot(bpy.types.Operator, tool.Ifc.Operator): +class ImportPlot(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.import_plot" bl_label = "Import Plot" bl_options = {"REGISTER", "UNDO"} bl_description = "Import plot from a csv file." - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"}) + filename_ext = ".csv" - def execute(self, context): + def _execute(self, context): core.import_plot(tool.Georeference, filepath=self.filepath) return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class EnableEditingWCS(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.enable_editing_wcs" diff --git a/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py b/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py index 149a309baa..315b16a98c 100644 --- a/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py +++ b/src/bonsai/bonsai/bim/module/model/sverchok_modifier.py @@ -25,6 +25,7 @@ import bonsai.tool as tool import json import zipfile import os.path +from bpy_extras.io_utils import ImportHelper, ExportHelper def update_sverchok_modifier(context): @@ -181,17 +182,14 @@ class UpdateDataFromSverchok(bpy.types.Operator, tool.Ifc.Operator): # used code from Sverchok's SvNodeTreeImporter licensed under GPL v3 # the code was changed to work with ifc sverchok modifier # removed the part that was relying on node graph to be opened at execution -class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): +class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.import_sverchok_graph" bl_label = "Import Sverchok Graph" bl_description = "Import Sverchok graph from a json file." bl_options = {"REGISTER"} - filepath: bpy.props.StringProperty( - name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH" - ) - filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) + filename_ext = ".json" def _execute(self, context): import sverchok @@ -210,10 +208,6 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): props.node_group = node_group return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - def draw(self, context): col = self.layout.column() col.label(text="Destination tree to import JSON:") @@ -224,17 +218,14 @@ class ImportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): # used code from Sverchok's SvNodeTreeExporter licensed under GPL v3 # the code was changed to work with ifc sverchok modifier # removed the part that was relying on node graph to be opened at execution -class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): +class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator, ExportHelper): bl_idname = "bim.export_sverchok_graph" bl_label = "Export Sverchok Graph" bl_description = "Export Sverchok graph to a json file." bl_options = {"REGISTER"} - filepath: bpy.props.StringProperty( - name="File Path", description="Filepath used for exporting to", maxlen=1024, default="", subtype="FILE_PATH" - ) - filter_glob: bpy.props.StringProperty(default="*.json", options={"HIDDEN"}) + filename_ext = ".json" compact: bpy.props.BoolProperty(default=True, description="Compact representation of the JSON file") compress: bpy.props.BoolProperty() @@ -281,10 +272,6 @@ class ExportSverchokGraph(bpy.types.Operator, tool.Ifc.Operator): return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - def draw(self, context): graph_name = context.active_object.BIMSverchokProperties.node_group.name self.layout.label(text=f'Save node tree "{graph_name}" into json:') diff --git a/src/bonsai/bonsai/bim/module/patch/operator.py b/src/bonsai/bonsai/bim/module/patch/operator.py index bbd521ffd4..690f66a079 100644 --- a/src/bonsai/bonsai/bim/module/patch/operator.py +++ b/src/bonsai/bonsai/bim/module/patch/operator.py @@ -24,6 +24,7 @@ import ifcpatch import bonsai.tool as tool import bonsai.core.patch as core import bonsai.bim.handler +from bpy_extras.io_utils import ImportHelper, ExportHelper from pathlib import Path from typing import cast, TYPE_CHECKING @@ -31,41 +32,33 @@ if TYPE_CHECKING: from bonsai.bim.prop import AttributeDataType -class SelectIfcPatchInput(bpy.types.Operator): +class SelectIfcPatchInput(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_ifc_patch_input" bl_label = "Select IFC Patch Input" bl_description = "Select filepath for IFC patch input." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifcZIP;*.ifcXML", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filename_ext = ".ifc" def execute(self, context): props = tool.Patch.get_patch_props() props.ifc_patch_input = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class SelectIfcPatchOutput(bpy.types.Operator): +class SelectIfcPatchOutput(bpy.types.Operator, ExportHelper): bl_idname = "bim.select_ifc_patch_output" bl_label = "Select IFC Patch Output" bl_description = "Select filepath for IFC patch output." bl_options = {"REGISTER", "UNDO"} + filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifcZIP;*.ifcXML", options={"HIDDEN"}) filename_ext = ".ifc" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") def execute(self, context): props = tool.Patch.get_patch_props() props.ifc_patch_output = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class ExecuteIfcPatch(bpy.types.Operator): bl_idname = "bim.execute_ifc_patch" diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 936a532d1e..68180a5e21 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -47,6 +47,7 @@ import bonsai.bim.helper import bonsai.bim.schema import bonsai.tool as tool import bonsai.core.project as core +from bpy_extras.io_utils import ExportHelper, ImportHelper from bonsai.bim.ifc import IfcStore from bonsai.bim.ui import IFCFileSelector from bonsai.bim import import_ifc @@ -152,12 +153,11 @@ class CreateProject(bpy.types.Operator): IfcStore.file = data["file"] -class SelectLibraryFile(bpy.types.Operator, IFCFileSelector): +class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper): bl_idname = "bim.select_library_file" bl_label = "Select Library File" bl_options = {"REGISTER", "UNDO"} bl_description = "Select an IFC file that can be used as a library" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) append_all: bpy.props.BoolProperty(default=False) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) @@ -192,10 +192,6 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector): ProjectLibraryData.load() return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - def rollback(self, data): if data["old_filepath"]: IfcStore.library_path = data["old_filepath"] @@ -872,7 +868,7 @@ class DisableEditingHeader(bpy.types.Operator): return {"FINISHED"} -class LoadProject(bpy.types.Operator, IFCFileSelector): +class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper): bl_idname = "bim.load_project" bl_label = "Load Project" bl_options = {"REGISTER", "UNDO"} @@ -903,6 +899,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): default=False, ) use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) + filename_ext = ".ifc" @classmethod def description(cls, context, properties): @@ -1014,8 +1011,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector): def invoke(self, context, event): if self.filepath: return self.execute(context) - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def draw(self, context): if self.use_relative_path: @@ -1175,12 +1171,12 @@ class ToggleFilterCategories(bpy.types.Operator): return {"FINISHED"} -class LinkIfc(bpy.types.Operator): +class LinkIfc(bpy.types.Operator, ImportHelper): bl_idname = "bim.link_ifc" bl_label = "Link IFC" bl_options = {"REGISTER", "UNDO"} bl_description = "Reference in a read-only IFC model in the background" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + files: bpy.props.CollectionProperty(name="Files", type=bpy.types.OperatorFileListElement) directory: bpy.props.StringProperty(subtype="DIR_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) @@ -1190,6 +1186,7 @@ class LinkIfc(bpy.types.Operator): default=False, ) use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) + filename_ext = ".ifc" if TYPE_CHECKING: filepath: str @@ -1235,10 +1232,6 @@ class LinkIfc(bpy.types.Operator): print(f"Finished linking {len(files)} IFCs", time.time() - start) return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class UnlinkIfc(bpy.types.Operator): bl_idname = "bim.unlink_ifc" @@ -1544,7 +1537,7 @@ class SelectLinkHandle(bpy.types.Operator): return {"FINISHED"} -class ExportIFC(bpy.types.Operator): +class ExportIFC(bpy.types.Operator, ExportHelper): bl_idname = "bim.save_project" bl_label = "Save IFC" # Prevents crash on Blender 4.4.0. @@ -1552,7 +1545,6 @@ class ExportIFC(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} filename_ext = ".ifc" filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcjson", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version") json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False) should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"}) @@ -1583,15 +1575,8 @@ class ExportIFC(bpy.types.Operator): if (filepath := props.ifc_file) and not self.should_save_as: self.filepath = str(tool.Blender.ensure_blender_path_is_abs(Path(filepath))) return self.execute(context) - if not self.filepath: - if bpy.data.is_saved: - self.filepath = Path(bpy.data.filepath).with_suffix(".ifc").__str__() - else: - self.filepath = "untitled.ifc" - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ExportHelper.invoke(self, context, event) def execute(self, context): project_props = tool.Project.get_project_props() @@ -1668,12 +1653,11 @@ class ExportIFC(bpy.types.Operator): return "Save the IFC file. Will save both .IFC/.BLEND files if synced together" -class LoadLinkedProject(bpy.types.Operator): +class LoadLinkedProject(bpy.types.Operator, ImportHelper): bl_idname = "bim.load_linked_project" bl_label = "Load Project For Viewing Only" bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty() file: ifcopenshell.file meshes: dict[str, bpy.types.Mesh] @@ -1682,9 +1666,7 @@ class LoadLinkedProject(bpy.types.Operator): def invoke(self, context, event): # Invoke is for debugging purposes, users are not intended to use this method really. - WindowManager = context.window_manager - WindowManager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def execute(self, context): import ifcpatch diff --git a/src/bonsai/bonsai/bim/module/sequence/operator.py b/src/bonsai/bonsai/bim/module/sequence/operator.py index 5eccfd04a8..98e7ae6bce 100644 --- a/src/bonsai/bonsai/bim/module/sequence/operator.py +++ b/src/bonsai/bonsai/bim/module/sequence/operator.py @@ -32,7 +32,7 @@ import ifcopenshell.util.sequence import ifcopenshell.util.selector from datetime import datetime from dateutil import parser, relativedelta -from bpy_extras.io_utils import ImportHelper +from bpy_extras.io_utils import ImportHelper, ExportHelper from typing import get_args, TYPE_CHECKING from typing_extensions import assert_never @@ -794,7 +794,7 @@ class ImportMSP(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): self.report({"INFO"}, "Import finished in {:.2f} seconds".format(time.time() - start)) -class ExportMSP(bpy.types.Operator, ImportHelper): +class ExportMSP(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_msp" bl_label = "Export MSP" bl_options = {"REGISTER", "UNDO"} @@ -827,7 +827,7 @@ class ExportMSP(bpy.types.Operator, ImportHelper): return {"FINISHED"} -class ExportP6(bpy.types.Operator, ImportHelper): +class ExportP6(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_p6" bl_label = "Export P6" bl_options = {"REGISTER", "UNDO"} diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py index ea20f25be0..1a9a43f141 100644 --- a/src/bonsai/bonsai/bim/module/style/operator.py +++ b/src/bonsai/bonsai/bim/module/style/operator.py @@ -25,6 +25,7 @@ import ifcopenshell.api import ifcopenshell.api.style import ifcopenshell.util.representation import ifcopenshell.util.unit +from bpy_extras.io_utils import ImportHelper from pathlib import Path from mathutils import Vector from typing import Any, Union @@ -273,16 +274,12 @@ class SetAssetMaterialToExternalStyle(bpy.types.Operator): return {"FINISHED"} -class BrowseExternalStyle(bpy.types.Operator): +class BrowseExternalStyle(bpy.types.Operator, ImportHelper): bl_idname = "bim.browse_external_style" bl_label = "Browse External Style" bl_description = "Select filepath for an external style." bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty( - name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH" - ) - filter_glob: bpy.props.StringProperty( default="*.blend", options={"HIDDEN"}, @@ -357,8 +354,7 @@ class BrowseExternalStyle(bpy.types.Operator): if data_block in data_blocks: self.data_block = data_block - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) def draw(self, context): layout = self.layout @@ -538,7 +534,7 @@ class SelectByStyle(bpy.types.Operator): return {"FINISHED"} -class ChooseTextureMapPath(bpy.types.Operator): +class ChooseTextureMapPath(bpy.types.Operator, ImportHelper): bl_idname = "bim.choose_texture_map_path" bl_label = "Choose Texture Map Path" bl_description = "Select filepath for a texture map." @@ -548,9 +544,6 @@ class ChooseTextureMapPath(bpy.types.Operator): use_relative_path: bpy.props.BoolProperty( name="Use Relative Path", description="Save path relative to IFC file", default=True ) - filepath: bpy.props.StringProperty( - name="File Path", description="Filepath used to import from", maxlen=1024, default="", subtype="FILE_PATH" - ) filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"}) @@ -563,10 +556,6 @@ class ChooseTextureMapPath(bpy.types.Operator): layout.label(text="Save the .ifc file first ") layout.label(text="to use relative paths.") - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - def execute(self, context): if self.texture_map_index < 0: self.report({"ERROR"}, "Provide a texture map index") diff --git a/src/bonsai/bonsai/bim/module/tester/operator.py b/src/bonsai/bonsai/bim/module/tester/operator.py index f653404d78..f1341f0225 100644 --- a/src/bonsai/bonsai/bim/module/tester/operator.py +++ b/src/bonsai/bonsai/bim/module/tester/operator.py @@ -28,6 +28,7 @@ import ifctester.reporter import ifcopenshell import bonsai.tool as tool import bonsai.bim.handler +from bpy_extras.io_utils import ExportHelper from pathlib import Path from typing import Union @@ -186,13 +187,13 @@ class SelectFailedEntities(bpy.types.Operator): return {"FINISHED"} -class ExportBcf(bpy.types.Operator): +class ExportBcf(bpy.types.Operator, ExportHelper): bl_idname = "bim.export_bcf" bl_label = "Export BCF" bl_description = "Save ifctester BCF report by the provided filepath." bl_options = {"REGISTER", "UNDO"} filter_glob: bpy.props.StringProperty(default="*.bcf", options={"HIDDEN"}) - filepath: bpy.props.StringProperty(subtype="FILE_PATH") + filename_ext = ".bcf" def execute(self, context): bcf_reporter = ifctester.reporter.Bcf(tool.Tester.specs) @@ -200,7 +201,3 @@ class ExportBcf(bpy.types.Operator): bcf_reporter.to_file(self.filepath) self.report({"INFO"}, "Finished exporting!") return {"FINISHED"} - - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 54dc26617a..52bd2cf42c 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -31,6 +31,7 @@ import ifcopenshell import bonsai.bim import bonsai.tool as tool import bonsai.bim.handler +from bpy_extras.io_utils import ImportHelper from bonsai.bim import import_ifc from bonsai.bim.prop import StrProperty from bonsai.bim.ui import IFCFileSelector @@ -145,12 +146,11 @@ class CloseBlendWarning(bpy.types.Operator): return context.window_manager.invoke_props_dialog(self) -class SelectURIAttribute(bpy.types.Operator): +class SelectURIAttribute(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_uri_attribute" bl_label = "Select URI Attribute" bl_options = {"REGISTER", "UNDO"} bl_description = "Select a local file" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") data_path: bpy.props.StringProperty(name="Data Path") use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) @@ -174,18 +174,13 @@ class SelectURIAttribute(bpy.types.Operator): attribute.string_value = tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path) return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - -class BIM_OT_multiple_file_selector(bpy.types.Operator): +class BIM_OT_multiple_file_selector(bpy.types.Operator, ImportHelper): """Open Blender's file explorer to select one or multiple files.""" bl_idname = "bim.multiple_file_selector" bl_label = "Select File(s)" bl_options = {"REGISTER", "UNDO"} - filepath: bpy.props.StringProperty(subtype="FILE_PATH") files: bpy.props.CollectionProperty(name="File Path", type=bpy.types.OperatorFileListElement) filter_glob: bpy.props.StringProperty(default="*", options={"HIDDEN"}) filepath: bpy.props.StringProperty(subtype="FILE_PATH") @@ -206,18 +201,17 @@ class BIM_OT_multiple_file_selector(bpy.types.Operator): def invoke(self, context, event): self.file_props = context.file_props - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) -class SelectIfcFile(bpy.types.Operator, IFCFileSelector): +class SelectIfcFile(bpy.types.Operator, IFCFileSelector, ImportHelper): bl_idname = "bim.select_ifc_file" bl_label = "Select IFC File" bl_options = {"REGISTER", "UNDO"} bl_description = f"Select a different IFC file.\n{tool.Blender.operator_invoke_filepath_hotkeys_description}" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}) use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False) + filename_ext = ".ifc" def execute(self, context): if self.is_existing_ifc_file(): @@ -233,17 +227,14 @@ class SelectIfcFile(bpy.types.Operator, IFCFileSelector): res = tool.Blender.operator_invoke_filepath_hotkeys(self, context, event, filepath) if res is not None: return res - - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) -class SelectDir(bpy.types.Operator): +class SelectDir(bpy.types.Operator, ImportHelper): bl_idname = "bim.select_dir" bl_label = "Select Directory" bl_options = {"REGISTER", "UNDO"} bl_description = "Open a file browser to choose the directory" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") data_path: bpy.props.StringProperty(name="Data Path") def execute(self, context): @@ -262,8 +253,7 @@ class SelectDir(bpy.types.Operator): return {"FINISHED"} def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} + return ImportHelper.invoke(self, context, event) class FileAssociate(bpy.types.Operator): @@ -768,13 +758,13 @@ class BIM_OT_remove_section_plane(bpy.types.Operator): bpy.ops.object.delete() -class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator): +class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator, ImportHelper): bl_idname = "bim.reload_ifc_file" bl_label = "Reload IFC File" bl_options = {"REGISTER", "UNDO"} bl_description = "Reload an updated IFC file" - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) + filename_ext = ".ifc" def _execute(self, context): import ifcdiff @@ -842,10 +832,6 @@ class ReloadIfcFile(bpy.types.Operator, tool.Ifc.Operator): bim_props.ifc_file = self.filepath return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class AddIfcFile(bpy.types.Operator): bl_idname = "bim.add_ifc_file" diff --git a/src/ifcsverchok/__init__.py b/src/ifcsverchok/__init__.py index 8263c73576..97fe04c5e0 100644 --- a/src/ifcsverchok/__init__.py +++ b/src/ifcsverchok/__init__.py @@ -32,6 +32,7 @@ import importlib import logging import types import bpy +from bpy_extras.io_utils import ExportHelper logger = logging.getLogger("sverchok.ifc") @@ -180,15 +181,15 @@ class IFC_Sv_UpdateCurrent(bpy.types.Operator): return {"FINISHED"} -class IFC_Sv_write_file(bpy.types.Operator): +class IFC_Sv_write_file(bpy.types.Operator, ExportHelper): bl_idname = "ifc.write_file_panel" bl_label = "Write File" bl_options = {"REGISTER", "UNDO"} bl_description = "Save transient IFC file to the provided path." - filepath: bpy.props.StringProperty(subtype="FILE_PATH") filter_glob: bpy.props.StringProperty(default="*.ifc", options={"HIDDEN"}) node_group: bpy.props.StringProperty(default="") force_mode: bpy.props.BoolProperty(default=False) + filename_ext = ".ifc" @classmethod def poll(cls, context): @@ -264,10 +265,6 @@ class IFC_Sv_write_file(bpy.types.Operator): self.report({"INFO"}, f"File written to: {self.filepath}") return {"FINISHED"} - def invoke(self, context, event): - context.window_manager.fileselect_add(self) - return {"RUNNING_MODAL"} - class IFC_PT_write_file_panel(bpy.types.Panel): bl_idname = "IFC_PT_write_file_panel" From c6e3ac4155dac0b1aa4d6d30c17d634d0b8c362a Mon Sep 17 00:00:00 2001 From: shmoodyyy Date: Sun, 23 Mar 2025 14:42:12 +0100 Subject: [PATCH 471/476] fix: memory leak from missing deletes of raw heap allocations; big issue within `create_shape()` python lib --- src/ifcgeom/AbstractKernel.cpp | 2 ++ src/ifcgeom/Converter.cpp | 10 ++++++++++ src/ifcgeom/Converter.h | 2 +- src/ifcgeom/kernels/opencascade/boolean_utils.cpp | 2 ++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/ifcgeom/AbstractKernel.cpp b/src/ifcgeom/AbstractKernel.cpp index c5ecbc0684..9954b86531 100644 --- a/src/ifcgeom/AbstractKernel.cpp +++ b/src/ifcgeom/AbstractKernel.cpp @@ -206,6 +206,8 @@ ifcopenshell::geometry::kernels::AbstractKernel* ifcopenshell::geometry::kernels } #endif if (kernels.size() != n + 1) { + for (auto k : kernels) + delete k; throw IfcParse::IfcException("Invalid hybrid kernel " + geometry_library); } } diff --git a/src/ifcgeom/Converter.cpp b/src/ifcgeom/Converter.cpp index 9324e84329..15c06aef47 100644 --- a/src/ifcgeom/Converter.cpp +++ b/src/ifcgeom/Converter.cpp @@ -13,6 +13,16 @@ ifcopenshell::geometry::Converter::Converter(const std::string& geometry_library settings_ = mapping_->settings(); } +ifcopenshell::geometry::Converter::~Converter() +{ + if (kernel_ != nullptr) { + delete kernel_; + } + if (mapping_ != nullptr) { + delete mapping_; + } +} + namespace { void substitute_with_box_based_on_density(IfcGeom::ConversionResults& items, double& density) { int nv = 0; diff --git a/src/ifcgeom/Converter.h b/src/ifcgeom/Converter.h index 900faf827a..f0dcc72515 100644 --- a/src/ifcgeom/Converter.h +++ b/src/ifcgeom/Converter.h @@ -28,7 +28,7 @@ namespace ifcopenshell { namespace geometry { Converter(const std::string& geometry_library, IfcParse::IfcFile* file, ifcopenshell::geometry::Settings& settings); - ~Converter() {} + ~Converter(); ifcopenshell::geometry::abstract_mapping* mapping() const { return mapping_; } diff --git a/src/ifcgeom/kernels/opencascade/boolean_utils.cpp b/src/ifcgeom/kernels/opencascade/boolean_utils.cpp index 14efa8f65e..05d983f22d 100644 --- a/src/ifcgeom/kernels/opencascade/boolean_utils.cpp +++ b/src/ifcgeom/kernels/opencascade/boolean_utils.cpp @@ -978,6 +978,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To if (b.Extent() == 0) { Logger::Warning("No other operands remaining, using first operand"); result = a; + delete builder; return true; } @@ -1130,6 +1131,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To } else { Logger::Notice("Processed fully in 2D"); result = mp.Shape(); + delete builder; return true; } } else { From dd53f9b49432904fecca361c594765d8bafee15c Mon Sep 17 00:00:00 2001 From: shmoodyyy Date: Sun, 23 Mar 2025 14:45:08 +0100 Subject: [PATCH 472/476] style: keep tab indenting consistency --- src/ifcgeom/AbstractKernel.cpp | 4 ++-- src/ifcgeom/Converter.cpp | 12 ++++++------ src/ifcgeom/kernels/opencascade/boolean_utils.cpp | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ifcgeom/AbstractKernel.cpp b/src/ifcgeom/AbstractKernel.cpp index 9954b86531..96b5617b8e 100644 --- a/src/ifcgeom/AbstractKernel.cpp +++ b/src/ifcgeom/AbstractKernel.cpp @@ -206,8 +206,8 @@ ifcopenshell::geometry::kernels::AbstractKernel* ifcopenshell::geometry::kernels } #endif if (kernels.size() != n + 1) { - for (auto k : kernels) - delete k; + for (auto k : kernels) + delete k; throw IfcParse::IfcException("Invalid hybrid kernel " + geometry_library); } } diff --git a/src/ifcgeom/Converter.cpp b/src/ifcgeom/Converter.cpp index 15c06aef47..9c6739292d 100644 --- a/src/ifcgeom/Converter.cpp +++ b/src/ifcgeom/Converter.cpp @@ -15,12 +15,12 @@ ifcopenshell::geometry::Converter::Converter(const std::string& geometry_library ifcopenshell::geometry::Converter::~Converter() { - if (kernel_ != nullptr) { - delete kernel_; - } - if (mapping_ != nullptr) { - delete mapping_; - } + if (kernel_ != nullptr) { + delete kernel_; + } + if (mapping_ != nullptr) { + delete mapping_; + } } namespace { diff --git a/src/ifcgeom/kernels/opencascade/boolean_utils.cpp b/src/ifcgeom/kernels/opencascade/boolean_utils.cpp index 05d983f22d..2308ff869c 100644 --- a/src/ifcgeom/kernels/opencascade/boolean_utils.cpp +++ b/src/ifcgeom/kernels/opencascade/boolean_utils.cpp @@ -978,7 +978,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To if (b.Extent() == 0) { Logger::Warning("No other operands remaining, using first operand"); result = a; - delete builder; + delete builder; return true; } @@ -1131,7 +1131,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To } else { Logger::Notice("Processed fully in 2D"); result = mp.Shape(); - delete builder; + delete builder; return true; } } else { From eec8c88e6f8909081705609d38d52210715410cc Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Mon, 24 Mar 2025 14:09:00 +0100 Subject: [PATCH 473/476] Unique ptrs #6417 --- src/ifcgeom/AbstractKernel.cpp | 22 +++++++++---------- .../kernels/opencascade/boolean_utils.cpp | 11 ++++------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/ifcgeom/AbstractKernel.cpp b/src/ifcgeom/AbstractKernel.cpp index 96b5617b8e..80b7eb63c9 100644 --- a/src/ifcgeom/AbstractKernel.cpp +++ b/src/ifcgeom/AbstractKernel.cpp @@ -75,12 +75,12 @@ bool is_valid_for_kernel(const ifcopenshell::geometry::kernels::AbstractKernel* } class HybridKernel : public ifcopenshell::geometry::kernels::AbstractKernel { - std::vector kernels_; + std::vector> kernels_; ifcopenshell::geometry::abstract_mapping* mapping_; public: - HybridKernel(const std::string& name, IfcParse::IfcFile* file, Settings& settings, std::vector kernels) + HybridKernel(const std::string& name, IfcParse::IfcFile* file, Settings& settings, std::vector>&& kernels) : AbstractKernel(name, settings) - , kernels_(kernels) + , kernels_(std::move(kernels)) , mapping_(ifcopenshell::geometry::impl::mapping_implementations().construct(file, settings)) {} virtual bool convert(const taxonomy::ptr item, IfcGeom::ConversionResults& rs) { @@ -88,7 +88,7 @@ public: bool has_openings = ops && ops->size(); for (auto& k : kernels_) { #ifdef IFOPSH_WITH_CGAL - if (has_openings && dynamic_cast(k)) { + if (has_openings && dynamic_cast(k.get())) { // @todo this would fail later on in the find_openings() call, because we have a // SimpleCgalShape which cannot be used on a kernel that supports booleans. // @todo 1 implement the translation between various conversion result shapes @@ -138,7 +138,7 @@ public: for (auto& k : kernels_) { bool is_valid = true; for (auto& s : entity_shapes) { - if (!is_valid_for_kernel(k, s)) { + if (!is_valid_for_kernel(k.get(), s)) { is_valid = false; break; } @@ -179,7 +179,7 @@ ifcopenshell::geometry::kernels::AbstractKernel* ifcopenshell::geometry::kernels if (geometry_library_lower.rfind("hybrid-", 0) == 0) { geometry_library_lower = geometry_library_lower.substr(strlen("hybrid")); - std::vector kernels; + std::vector> kernels; while (!geometry_library_lower.empty()) { if (geometry_library_lower.find("-", 0) == 0) { geometry_library_lower = geometry_library_lower.substr(strlen("-")); @@ -189,25 +189,23 @@ ifcopenshell::geometry::kernels::AbstractKernel* ifcopenshell::geometry::kernels auto n = kernels.size(); #ifdef IFOPSH_WITH_OPENCASCADE if (geometry_library_lower.find("opencascade", 0) == 0) { - kernels.push_back(new IfcGeom::OpenCascadeKernel(conv_settings)); + kernels.emplace_back(new IfcGeom::OpenCascadeKernel(conv_settings)); geometry_library_lower = geometry_library_lower.substr(strlen("opencascade")); } #endif #ifdef IFOPSH_WITH_CGAL if (geometry_library_lower.find("cgal-simple", 0) == 0) { - kernels.push_back(new SimpleCgalKernel(conv_settings)); + kernels.emplace_back(new SimpleCgalKernel(conv_settings)); geometry_library_lower = geometry_library_lower.substr(strlen("cgal-simple")); } if (geometry_library_lower.find("cgal", 0) == 0) { - kernels.push_back(new CgalKernel(conv_settings)); + kernels.emplace_back(new CgalKernel(conv_settings)); geometry_library_lower = geometry_library_lower.substr(strlen("cgal")); } #endif if (kernels.size() != n + 1) { - for (auto k : kernels) - delete k; throw IfcParse::IfcException("Invalid hybrid kernel " + geometry_library); } } @@ -217,7 +215,7 @@ ifcopenshell::geometry::kernels::AbstractKernel* ifcopenshell::geometry::kernels } if (!kernels.empty()) { - return new HybridKernel(geometry_library, file, conv_settings, kernels); + return new HybridKernel(geometry_library, file, conv_settings, std::move(kernels)); } } diff --git a/src/ifcgeom/kernels/opencascade/boolean_utils.cpp b/src/ifcgeom/kernels/opencascade/boolean_utils.cpp index 2308ff869c..15e94bff05 100644 --- a/src/ifcgeom/kernels/opencascade/boolean_utils.cpp +++ b/src/ifcgeom/kernels/opencascade/boolean_utils.cpp @@ -929,11 +929,11 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To bool is_2d = count(a, TopAbs_FACE) > 0 && count(a, TopAbs_SHELL) == 0; bool success = false; - BRepAlgoAPI_BooleanOperation* builder; + std::unique_ptr builder; TopTools_ListOfShape b_tmp; if (op == BOPAlgo_CUT) { - builder = new BRepAlgoAPI_Cut(); + builder.reset(new BRepAlgoAPI_Cut()); if (do_subtraction_eliminate_disjoint_bbox) { PERF("boolean subtraction: eliminate disjoint bbox"); @@ -968,9 +968,9 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To } } else if (op == BOPAlgo_COMMON) { - builder = new BRepAlgoAPI_Common(); + builder.reset(new BRepAlgoAPI_Common()); } else if (op == BOPAlgo_FUSE) { - builder = new BRepAlgoAPI_Fuse(); + builder.reset(new BRepAlgoAPI_Fuse()); } else { return false; } @@ -978,7 +978,6 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To if (b.Extent() == 0) { Logger::Warning("No other operands remaining, using first operand"); result = a; - delete builder; return true; } @@ -1131,7 +1130,6 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To } else { Logger::Notice("Processed fully in 2D"); result = mp.Shape(); - delete builder; return true; } } else { @@ -1431,7 +1429,6 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To Logger::Notice(str_str); } } - delete builder; if (!success) { if (allow_retry) { return boolean_operation(settings, a, b, op, result, new_fuzziness); From 9311f50206acef37d65f62a5c635f5ebb6406ae1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 25 Mar 2025 11:53:44 +0500 Subject: [PATCH 474/476] Fix for 4594f596c2 Traceback: ``` Error: Python: Traceback (most recent call last): File "\bonsai\bim\module\project\operator.py", line 127, in execute self._execute(context) File "\bonsai\bim\module\project\operator.py", line 143, in _execute core.create_project( File "\bonsai\core\project.py", line 108, in create_project project.append_all_types_from_template(template) File "\bonsai\tool\project.py", line 55, in append_all_types_from_template bpy.ops.bim.select_library_file(filepath=filepath.__str__()) File "\Blender\4.4\scripts\modules\bpy\ops.py", line 109, in __call__ ret = _op_call(self.idname_py(), kw) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: Converting py args to operator properties:: keyword "filepath" unrecognized ``` --- src/bonsai/bonsai/bim/ui.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 4223706b8a..a6e8d2b25f 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -39,8 +39,10 @@ from typing import Optional, TYPE_CHECKING class IFCFileSelector: - filepath: str - use_relative_path: bool + # Avoid overriding blender prop annotations at runtime. + if TYPE_CHECKING: + filepath: str + use_relative_path: bool def is_existing_ifc_file(self, filepath: Optional[str] = None) -> bool: """Check if file path exists and if it's an IFC file. From 3744b81ccd8ed1e6d4aa491d82d36452212995de Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Tue, 25 Mar 2025 10:15:24 +0100 Subject: [PATCH 475/476] Only accept finite floats #6409 --- src/ifcparse/IfcParse.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/ifcparse/IfcParse.cpp b/src/ifcparse/IfcParse.cpp index d708f6152e..2eec8ea867 100644 --- a/src/ifcparse/IfcParse.cpp +++ b/src/ifcparse/IfcParse.cpp @@ -1130,6 +1130,23 @@ class apply_individual_instance_visitor { template void IfcUtil::IfcBaseClass::set_attribute_value(size_t i, const T& t) { + if constexpr (std::is_same_v, double>) { + if (!std::isfinite(t)) { + throw IfcParse::IfcException("Only finite values are allowed"); + } + } + if constexpr (std::is_same_v, std::vector>) { + if (std::any_of(t.begin(), t.end(), [](double d) { return !std::isfinite(d); })) { + throw IfcParse::IfcException("Only finite values are allowed"); + } + } + if constexpr (std::is_same_v, std::vector>>) { + for (auto& tt : t) { + if (std::any_of(tt.begin(), tt.end(), [](double d) { return !std::isfinite(d); })) { + throw IfcParse::IfcException("Only finite values are allowed"); + } + } + } auto current_attribute = data_.get_attribute_value(i); if (file_ != nullptr) { From bd92821b2ef35983fb900b6510c485d3d4cee71e Mon Sep 17 00:00:00 2001 From: Thomas Krijnen Date: Tue, 25 Mar 2025 11:44:46 +0100 Subject: [PATCH 476/476] Add file offset to instance reference errors so that they are picked up by ifcopenshell.validate --- src/ifcparse/IfcFile.cpp | 9 +-------- src/ifcparse/IfcFile.h | 10 +++++++++- src/ifcparse/IfcParse.cpp | 12 ++++++------ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/ifcparse/IfcFile.cpp b/src/ifcparse/IfcFile.cpp index 7c964d3fda..bd312d51cb 100644 --- a/src/ifcparse/IfcFile.cpp +++ b/src/ifcparse/IfcFile.cpp @@ -46,13 +46,6 @@ namespace { template constexpr bool is_type_in_variant_v = is_type_in_variant::value; - struct InstanceReference { - int v; - operator int() const { - return v; - } - }; - template void dispatch_token(int instance_id, int attribute_id, IfcParse::Token t, IfcParse::declaration* decl, Fn fn) { if (t.type == IfcParse::Token_BINARY) { @@ -75,7 +68,7 @@ namespace { } else if (t.type == IfcParse::Token_FLOAT) { fn(IfcParse::TokenFunc::asFloat(t)); } else if (t.type == IfcParse::Token_IDENTIFIER) { - fn(IfcParse::reference_or_simple_type{ InstanceReference{ IfcParse::TokenFunc::asIdentifier(t) } }); + fn(IfcParse::reference_or_simple_type{ IfcParse::InstanceReference{ IfcParse::TokenFunc::asIdentifier(t), t.startPos } }); } else if (t.type == IfcParse::Token_INT) { fn(IfcParse::TokenFunc::asInt(t)); } else if (t.type == IfcParse::Token_STRING) { diff --git a/src/ifcparse/IfcFile.h b/src/ifcparse/IfcFile.h index a61c9f94b7..acfe6f4c89 100644 --- a/src/ifcparse/IfcFile.h +++ b/src/ifcparse/IfcFile.h @@ -66,7 +66,15 @@ class IFC_PARSE_API file_open_status { } }; -typedef boost::variant reference_or_simple_type; +struct InstanceReference { + int v; + size_t file_offset; + operator int() const { + return v; + } +}; + +typedef boost::variant reference_or_simple_type; typedef std::list, std::vector>>>> unresolved_references; struct parse_context { diff --git a/src/ifcparse/IfcParse.cpp b/src/ifcparse/IfcParse.cpp index 2eec8ea867..021b9a1099 100644 --- a/src/ifcparse/IfcParse.cpp +++ b/src/ifcparse/IfcParse.cpp @@ -1423,10 +1423,10 @@ void IfcFile::initialize_(IfcParse::IfcSpfStream* s) { const auto& ref = p.first.name_; const auto& refattr = p.first.index_; if (auto* v = boost::get(&p.second)) { - if (auto* name = boost::get(v)) { + if (auto* name = boost::get(v)) { entity_by_id_t::const_iterator it = byid_.find(*name); if (it == byid_.end()) { - Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found"); + Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found at offset " + std::to_string(name->file_offset)); } else { byid_[p.first.name_]->data().storage_.set(p.first.index_, it->second); } @@ -1437,10 +1437,10 @@ void IfcFile::initialize_(IfcParse::IfcSpfStream* s) { aggregate_of_instance::ptr instances(new aggregate_of_instance); instances->reserve(v->size()); for (const auto& vi : *v) { - if (auto* name = boost::get(&vi)) { + if (auto* name = boost::get(&vi)) { entity_by_id_t::const_iterator it = byid_.find(*name); if (it == byid_.end()) { - Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found"); + Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found at offset " + std::to_string(name->file_offset)); } else { instances->push(it->second); } @@ -1454,10 +1454,10 @@ void IfcFile::initialize_(IfcParse::IfcSpfStream* s) { for (const auto& vi : *v) { std::vector inner; for (const auto& vii : vi) { - if (auto* name = boost::get(&vii)) { + if (auto* name = boost::get(&vii)) { entity_by_id_t::const_iterator it = byid_.find(*name); if (it == byid_.end()) { - Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found"); + Logger::Error("Instance reference #" + std::to_string(*name) + " used by instance #" + std::to_string(ref) + " at attribute index " + std::to_string(refattr) + " not found at offset " + std::to_string(name->file_offset)); } else { inner.push_back(it->second); }