From 28eb2cd6281e7f78579a6bf3d3f274f2a50e2105 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Fri, 19 Apr 2024 08:51:55 +1000 Subject: [PATCH 1/8] Fix dateutil dependency --- src/ifctester/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifctester/pyproject.toml b/src/ifctester/pyproject.toml index d5adce1193..a4644df54c 100644 --- a/src/ifctester/pyproject.toml +++ b/src/ifctester/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", ] -dependencies = ["ifcopenshell", "dateutil", "xmlschema", "numpy"] +dependencies = ["ifcopenshell", "python-dateutil", "xmlschema", "numpy"] [project.urls] Homepage = "http://ifcopenshell.org" From 6ddf91fb527fefbeb089c3d51ef99ea75cddbaed Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Thu, 18 Apr 2024 21:08:46 -0500 Subject: [PATCH 2/8] =?UTF-8?q?bim.select=5Faggregate(select=5Fparts=3DTru?= =?UTF-8?q?e)=20-=20selects=20all=20the=20host=20aggreg=E2=80=A6=20(#4552)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bim.select_aggregate(select_parts=True) - selects all the host aggregates and all the parts and subaggregates within these host aggregates - bim.select_linked_aggregates(select_parts=False) - selects all the host aggregates that are linked - bim.select_linked_aggregates(select_parts=True) - selects all the host aggregates that are linked and all the parts and subaggregates within these host aggregates --- .../bim/module/aggregate/operator.py | 123 +++++++++++++----- .../blenderbim/bim/module/aggregate/ui.py | 5 +- 2 files changed, 92 insertions(+), 36 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py index 6d07550d7e..7c9b2d5fd1 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/operator.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/operator.py @@ -190,12 +190,17 @@ class BIM_OT_select_parts(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() - obj = bpy.data.objects.get(self.obj) or context.active_object - parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)) - parts_objs = set(tool.Ifc.get_object(part) for part in parts) - selectable_parts_objs = set(context.selectable_objects).intersection(parts_objs) - for selectable_part_obj in selectable_parts_objs: - selectable_part_obj.select_set(True) + # obj = bpy.data.objects.get(self.obj) or context.active_object + + for obj in context.selected_objects: + parts = ifcopenshell.util.element.get_parts(tool.Ifc.get_entity(obj)) + if parts: + parts_objs = set(tool.Ifc.get_object(part) for part in parts) + selectable_parts_objs = set(context.selectable_objects).intersection(parts_objs) + for selectable_part_obj in selectable_parts_objs: + selectable_part_obj.select_set(True) + else: + obj.select_set(False) return {"FINISHED"} @@ -217,18 +222,43 @@ class BIM_OT_select_aggregate(bpy.types.Operator): def execute(self, context): self.file = IfcStore.get_file() - obj = bpy.data.objects.get(self.obj) or context.active_object - aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj)) - aggregate_obj = tool.Ifc.get_object(aggregate) + + # obj = bpy.data.objects.get(self.obj) or context.active_object + # aggregate = ifcopenshell.util.element.get_aggregate(tool.Ifc.get_entity(obj)) + # aggregate_obj = tool.Ifc.get_object(aggregate) + + all_parts = [] + for obj in context.selected_objects: + element = tool.Ifc.get_entity(obj) + if element: + aggregate = ifcopenshell.util.element.get_aggregate(element) + if aggregate: + all_parts.append(aggregate) + obj.select_set(False) + else: + pass + if not element: + obj.select_set(False) + if self.select_parts: - bpy.ops.bim.select_parts(obj=aggregate_obj.name) - if aggregate_obj in context.selectable_objects: - tool.Blender.set_objects_selection( - context, - aggregate_obj, - [aggregate_obj], - clear_previous_selection=not self.select_parts, - ) + all_objs = [] + for part in all_parts: + if part.IsDecomposedBy: + for subpart in part.IsDecomposedBy[0].RelatedObjects: + all_parts.append(subpart) + all_objs.append(part) + + for element in all_objs: + obj = tool.Ifc.get_object(element) + if obj: + obj.select_set(True) + + else: + for aggregate_element in all_parts: + aggregate_obj = tool.Ifc.get_object(aggregate_element) + aggregate_obj.select_set(True) + bpy.context.view_layer.objects.active = aggregate_obj + return {"FINISHED"} @@ -292,28 +322,51 @@ class BIM_OT_select_linked_aggregates(bpy.types.Operator, Operator): bl_idname = "bim.select_linked_aggregates" bl_label = "Select linked aggregates" bl_options = {"REGISTER", "UNDO"} + select_parts: bpy.props.BoolProperty(default=False) + + @classmethod + def description(cls, context, properties): + if properties.select_parts: + return "Select all aggregates, subaggregates and all their parts" + else: + return "Select all aggregates" def _execute(self, context): - element = tool.Ifc.get_entity(bpy.context.active_object) - aggregate = ifcopenshell.util.element.get_aggregate(element) - if not aggregate: - return [] - if not element: - return [] - - linked_aggregate_group = [ - r.RelatingGroup - for r in getattr(aggregate, "HasAssignments", []) or [] - if r.is_a("IfcRelAssignsToGroup") - if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name - ] - + for obj in context.selected_objects: obj.select_set(False) + element = tool.Ifc.get_entity(obj) + aggregate = ifcopenshell.util.element.get_aggregate(element) + if not aggregate: + continue + if not element: + continue - for rel in linked_aggregate_group[0].IsGroupedBy or []: - for element in rel.RelatedObjects: - obj = tool.Ifc.get_object(element) - obj.select_set(True) + linked_aggregate_group = [ + r.RelatingGroup + for r in getattr(aggregate, "HasAssignments", []) or [] + if r.is_a("IfcRelAssignsToGroup") + if "BBIM_Linked_Aggregate" in r.RelatingGroup.Name + ] + + group_rel = linked_aggregate_group[0].IsGroupedBy or [] + for group_link in group_rel: + parts = list(group_link.RelatedObjects) + if self.select_parts: + parts_objs = [] + for part in parts: + if part.IsDecomposedBy: + for subpart in part.IsDecomposedBy[0].RelatedObjects: + parts.append(subpart) + parts_objs.append(part) + + for element in parts_objs: + obj = tool.Ifc.get_object(element) + if obj: + obj.select_set(True) + else: + for element in parts: + obj = tool.Ifc.get_object(element) + obj.select_set(True) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py index 41f4b5b98d..bc691e3908 100644 --- a/src/blenderbim/blenderbim/bim/module/aggregate/ui.py +++ b/src/blenderbim/blenderbim/bim/module/aggregate/ui.py @@ -139,7 +139,10 @@ class BIM_PT_linked_aggregate(Panel): if type(AggregateData.data['total_linked_aggregate']) is int: if AggregateData.data['total_linked_aggregate'] > 0: row.label(text=f"{AggregateData.data['total_linked_aggregate']} Linked Aggregates") - row.operator("bim.select_linked_aggregates", text="", icon="RESTRICT_SELECT_OFF") + op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_DATA_POINTCLOUD") + op.select_parts = False + op = row.operator("bim.select_linked_aggregates", text="", icon="OUTLINER_OB_POINTCLOUD") + op.select_parts = True row.operator("bim.refresh_linked_aggregate", text="", icon="FILE_REFRESH") op = row.operator("bim.break_link_to_other_aggregates", text="", icon="X") else: From 9da4215bac90056ac7b223fb4c91e8582b493b38 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 19 Apr 2024 16:05:33 +0500 Subject: [PATCH 3/8] ifcopenshell.util.selector.filter_elements - inherited predefined types mentioned by @theoryshaw on osarch - https://community.osarch.org/discussion/comment/20126/#Comment_20126 --- src/ifcopenshell-python/ifcopenshell/util/selector.py | 5 ++++- src/ifcopenshell-python/test/util/test_selector.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index ee94fbc310..dd48eeb599 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -524,7 +524,10 @@ class FacetTransformer(lark.Transformer): name = name.children[0].value def filter_function(element): - element_value = getattr(element, name, None) + if name == "PredefinedType": + element_value = ifcopenshell.util.element.get_predefined_type(element) + else: + element_value = getattr(element, name, None) return self.compare(element_value, comparison, value) self.elements = set(filter(filter_function, self.elements)) diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 571511d39f..78dbc7e946 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -165,6 +165,11 @@ class TestFilterElements(test.bootstrap.IFC4): element.Description = "Foobar" assert subject.filter_elements(self.file, "IfcWall, Name=Foo, Description=Foobar") == {element} + element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") + element_type.PredefinedType = "SOLIDWALL" + ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=element_type) + assert subject.filter_elements(self.file, "IfcWall, PredefinedType=SOLIDWALL") == {element} + def test_selecting_by_type(self): element = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWall") element_type = ifcopenshell.api.run("root.create_entity", self.file, ifc_class="IfcWallType") From 25edaa9b89aab418f4f590e6dba33edac0758012 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 19 Apr 2024 16:07:29 +0500 Subject: [PATCH 4/8] update broken url for ladybug tools temporarily use fork repo --- src/blenderbim/docs/users/other_addons.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/docs/users/other_addons.rst b/src/blenderbim/docs/users/other_addons.rst index 7ae799a6fb..e2b50e3cc4 100644 --- a/src/blenderbim/docs/users/other_addons.rst +++ b/src/blenderbim/docs/users/other_addons.rst @@ -36,7 +36,7 @@ Some of these add-ons are not shipped with Blender: import GIS data, grab elevation data from the web, and generate TINs from survey points and contours. - `Ladybug Tools for Blender - `__ - Ladybug Tools + `__ - Ladybug Tools is an extension of Sverchok for environmental analysis and building physics simulation. It allows analysis of solar, daylight, energy, and CFD. - `Topologic `__ - Perform spatial and topological From 447625bffd3cbb65ec729c2d2a4673488477df81 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 19 Apr 2024 16:18:46 +0500 Subject: [PATCH 5/8] fix failing test in util.selector.format with imperial_length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the error is below, not sure how I haven't noticed it before 🤔 assert subject.format('imperial_length(1, 1)') == "1'" > raise VisitError(tree.data, tree, e) E lark.exceptions.VisitError: Error trying to process rule "imperial_length": E E cannot access local variable 'output_unit' where it is not associated with a value --- src/ifcopenshell-python/ifcopenshell/util/selector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index dd48eeb599..15ba9e0762 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -233,7 +233,7 @@ class FormatTransformer(lark.Transformer): def imperial_length(self, args): if len(args) == 2: - input_unit = "foot" + input_unit, output_unit = "foot", "foot" value, precision = args else: value, precision, input_unit, output_unit = args From 757a010172f1c3061d3efe2b7740bb32f08a1f74 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 19 Apr 2024 16:18:57 +0500 Subject: [PATCH 6/8] typing --- src/ifcopenshell-python/ifcopenshell/util/unit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/unit.py b/src/ifcopenshell-python/ifcopenshell/util/unit.py index a218a9504f..102b7b0d61 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/unit.py +++ b/src/ifcopenshell-python/ifcopenshell/util/unit.py @@ -604,8 +604,8 @@ def format_length( decimal_places: int = 2, suppress_zero_inches=True, unit_system: Literal["metric", "imperial"] = "imperial", - input_unit="foot", - output_unit="foot", + input_unit: Literal["foot", "inch"] = "foot", + output_unit: Literal["foot", "inch"] = "foot", ) -> str: """Formats a length for readability and imperial formatting From 60a70f5236528eaad63a1bb9109044d04a108679 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 19 Apr 2024 17:11:56 +0500 Subject: [PATCH 7/8] support IfcAxis1Placement and warn on IfcAxis2PlacementLinear #4565 --- .../ifcopenshell/util/placement.py | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/placement.py b/src/ifcopenshell-python/ifcopenshell/util/placement.py index f05959677d..1bf35ed07b 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/placement.py +++ b/src/ifcopenshell-python/ifcopenshell/util/placement.py @@ -61,11 +61,22 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType: :return: A 4x4 numpy matrix :rtype: np.ndarray[np.ndarray[float]] """ - if placement.is_a("IfcAxis2Placement3D"): + ifc_class = placement.is_a() + if ifc_class in ("IfcAxis2Placement3D", "IfcAxis2PlacementLinear"): z = np.array(placement.Axis.DirectionRatios if placement.Axis else (0, 0, 1)) x = np.array(placement.RefDirection.DirectionRatios if placement.RefDirection else (1, 0, 0)) - o = placement.Location.Coordinates - elif placement.is_a("IfcAxis2Placement2D"): + location = placement.Location + if coordinates := getattr(location, "Coordinates", None): + o = coordinates + else: + ifc_class = location.is_a("IfcPointByDistanceExpression") + print( + f'WARNING. Placement location of type "{ifc_class}" ' + f'is not yet supported and placement {placement} may be placed incorrectly.' + ) + o = (0.0, 0.0, 0.0) + + elif ifc_class == "IfcAxis2Placement2D": z = np.array((0, 0, 1)) if placement.RefDirection: x = np.array(placement.RefDirection.DirectionRatios) @@ -73,6 +84,13 @@ def get_axis2placement(placement: ifcopenshell.entity_instance) -> MatrixType: else: x = np.array((1, 0, 0)) o = (*placement.Location.Coordinates, 0.0) + + elif ifc_class == "IfcAxis1Placement": + axis = placement.Axis + z = np.array(axis.DirectionRatios if axis else (0, 0, 1)) + x = np.array((1, 0, 0)) + o = placement.Location.Coordinates + return a2p(o, z, x) From 3ef823db239843bdd1104055f2229336d824cc9a Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sat, 20 Apr 2024 11:56:22 +0100 Subject: [PATCH 8/8] Regenerate missing sheets #4462 BlenderBIM will regenerate missing SVG drawings from the IFC on demand, now it attempts to regenerate missing SVG sheets too. ie. you can open a bare IFC file with configured drawings and sheets and continue working. Only drawings are recognised, spreadsheet and other SVG refs are not regenerated, all drawings are placed at the 30,30 pixel location. --- .../blenderbim/bim/module/drawing/operator.py | 1 + .../blenderbim/bim/module/drawing/svgwriter.py | 2 +- src/blenderbim/blenderbim/core/drawing.py | 8 ++++++++ src/blenderbim/blenderbim/tool/drawing.py | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/operator.py b/src/blenderbim/blenderbim/bim/module/drawing/operator.py index 1d5e751062..f11837dba4 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/operator.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/operator.py @@ -2373,6 +2373,7 @@ class LoadSheets(bpy.types.Operator, Operator): if not filepath.is_file(): sheet_name = f"{sheet_prop.identification} - {sheet_prop.name}" sheets_not_found.append(f'"{sheet_name}" - {document_uri}') + core.regenerate_sheet(tool.Drawing, sheet) if sheets_not_found: self.report({"ERROR"}, "Some sheets svg files are missing:\n" + "\n".join(sheets_not_found)) diff --git a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py index 736d8efbe5..21220f3971 100644 --- a/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py +++ b/src/blenderbim/blenderbim/bim/module/drawing/svgwriter.py @@ -93,7 +93,7 @@ class SvgWriter: def setup_drawing_resource_paths(self, element): pset = ifcopenshell.util.element.get_pset(element, "EPset_Drawing") - for resource in ("Stylesheet", "Markers", "Symbols", "Patterns"): + for resource in ("Stylesheet", "Markers", "Symbols", "Patterns", "ShadingStyles"): resource_path = pset.get(resource) if not resource_path: self.resource_paths[resource] = None diff --git a/src/blenderbim/blenderbim/core/drawing.py b/src/blenderbim/blenderbim/core/drawing.py index cccdc16ad1..36cec23daa 100644 --- a/src/blenderbim/blenderbim/core/drawing.py +++ b/src/blenderbim/blenderbim/core/drawing.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU General Public License # along with BlenderBIM Add-on. If not, see . +from pathlib import Path + def enable_editing_text(drawing, obj=None): drawing.enable_editing_text(obj) @@ -89,6 +91,12 @@ def add_sheet(ifc, drawing, titleblock=None): drawing.import_sheets() +def regenerate_sheet(drawing, sheet=None): + titleblock_uri = drawing.get_document_uri(sheet, "TITLEBLOCK") + drawing.create_svg_sheet(sheet, drawing.sanitise_filename(Path(titleblock_uri).stem)) + drawing.add_drawings(sheet) + + def open_sheet(drawing, sheet=None): drawing.open_layout_svg(drawing.get_document_uri(sheet, "LAYOUT")) diff --git a/src/blenderbim/blenderbim/tool/drawing.py b/src/blenderbim/blenderbim/tool/drawing.py index d1d7e9649a..a5d67e5aed 100644 --- a/src/blenderbim/blenderbim/tool/drawing.py +++ b/src/blenderbim/blenderbim/tool/drawing.py @@ -247,6 +247,21 @@ class Drawing(blenderbim.core.tool.Drawing): sheet_builder.create(uri, titleblock) return uri + @classmethod + def add_drawings(cls, sheet): + sheet_builder = sheeter.SheetBuilder() + sheet_builder.data_dir = bpy.context.scene.BIMProperties.data_dir + sheet_reference = None + drawing_names = [] + for reference in cls.get_document_references(sheet): + if reference.Description == "LAYOUT": + sheet_reference = reference + elif reference.Description == "DRAWING": + drawing_names.append(Path(reference.Location).stem) + for annotation in [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"]: + if annotation.Name in drawing_names: + sheet_builder.add_drawing(sheet_reference, annotation, sheet) + @classmethod def delete_collection(cls, collection): bpy.data.collections.remove(collection, do_unlink=True)