From 83a351b1c75783f5832741aedb6caffec773211a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 18 Feb 2025 13:37:53 +0500 Subject: [PATCH 01/79] 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 02/79] 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 03/79] 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 04/79] 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 05/79] 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 06/79] 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 07/79] 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 08/79] 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 09/79] 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 10/79] 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 11/79] 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 12/79] 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 13/79] 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 14/79] 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 15/79] 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 16/79] 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 17/79] 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 18/79] 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 19/79] 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 20/79] 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 21/79] 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 22/79] 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 23/79] 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 24/79] 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 25/79] 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 26/79] 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 27/79] 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 28/79] 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 29/79] 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 30/79] 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 31/79] 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 32/79] 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 33/79] 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 34/79] 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 35/79] 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 36/79] 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 37/79] 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 38/79] 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 39/79] 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 40/79] 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 41/79] 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 42/79] 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 43/79] 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 44/79] 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 45/79] 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 46/79] 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 47/79] 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 48/79] 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 49/79] 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 50/79] 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 51/79] 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 52/79] 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 53/79] 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 54/79] 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 55/79] 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 56/79] 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 57/79] 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 58/79] 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 59/79] 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 60/79] 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 61/79] 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 62/79] 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 63/79] 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 64/79] 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 65/79] 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 66/79] 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 67/79] 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 68/79] 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 69/79] 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 70/79] 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 71/79] 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 72/79] 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 73/79] 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 74/79] 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 75/79] 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 76/79] 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 77/79] 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 78/79] 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 79/79] 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: |