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: 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) 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 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) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index ee94fbc310..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 @@ -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/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 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") 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"