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"