From 5db955d40c7c7026f07e47171570f61627381b3e Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Fri, 3 Jul 2026 11:30:31 +0100 Subject: [PATCH] Apply link matrix when serialising linked drawings Linked IFC files were included in SVG output but without their world transform, causing geometry to appear at wrong coordinates. Falls back to no transform if the link cache is unavailable. Generated with the assistance of an AI coding tool. --- .../bonsai/bim/module/drawing/operator.py | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index d9431c356e..28f5a1781a 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -57,7 +57,7 @@ import shapely from bpy_extras.image_utils import load_image from bpy_extras.io_utils import ImportHelper from lxml import etree -from mathutils import Color, Vector +from mathutils import Color, Matrix, Vector import bonsai.bim.export_ifc import bonsai.bim.handler @@ -602,6 +602,7 @@ class CreateDrawing(bpy.types.Operator): context_type: Literal["body", "annotation"], drawing_elements: set[ifcopenshell.entity_instance], target_view: str, + link_matrix: Optional[Matrix] = None, ) -> None: drawing_elements = drawing_elements.copy() contexts_: list[list[int]] = getattr(contexts, context_type) @@ -613,9 +614,19 @@ class CreateDrawing(bpy.types.Operator): geom_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) geom_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE) - if ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view: + is_plan = ifc.by_id(context[0]).ContextType == "Plan" and "PLAN_VIEW" in target_view + z_offset = (0.002 if target_view == "PLAN_VIEW" else -0.002) if is_plan else 0.0 + + if link_matrix is not None: + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc) + t = link_matrix.to_translation() + offset = (t.x / unit_scale, t.y / unit_scale, t.z / unit_scale + z_offset) + geom_settings.set("model-offset", offset) + q = link_matrix.to_quaternion() + geom_settings.set("model-rotation", (q.x, q.y, q.z, q.w)) + elif z_offset: # A 2mm Z offset to combat Z-fighting in plan or RCPs - geom_settings.set("model-offset", (0.0, 0.0, 0.002 if target_view == "PLAN_VIEW" else -0.002)) + geom_settings.set("model-offset", (0.0, 0.0, z_offset)) geom_settings.set("context-ids", context) it = ifcopenshell.geom.iterator( @@ -923,11 +934,16 @@ class CreateDrawing(bpy.types.Operator): bim_props = tool.Blender.get_bim_props() prefs = tool.Blender.get_addon_preferences() - files = {bim_props.ifc_file: tool.Ifc.get()} + # Map ifc_path → (ifc_file, link_matrix); main file has no link_matrix (None) + files: dict[str, tuple[ifcopenshell.file, Optional[Matrix]]] = {bim_props.ifc_file: (tool.Ifc.get(), None)} props = tool.Project.get_project_props() for link in props.get_loaded_links_for_drawings(): - files[link.filepath] = self.get_linked_file(link) + try: + link_matrix = tool.Project.calculate_link_matrix(link) + except Exception: + link_matrix = None + files[link.filepath] = (self.get_linked_file(link), link_matrix) target_view = ifcopenshell.util.element.get_psets(self.camera_element)["EPset_Drawing"]["TargetView"] self.setup_serialiser(target_view) @@ -935,7 +951,7 @@ class CreateDrawing(bpy.types.Operator): tree = ifcopenshell.geom.tree() tree.enable_face_styles(True) - for ifc_path, ifc in files.items(): + for ifc_path, (ifc, link_matrix) in files.items(): # 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() @@ -949,8 +965,8 @@ class CreateDrawing(bpy.types.Operator): # A drawing prioritises a target view context first, followed by a model view context as a fallback. # Specifically for PLAN_VIEW and REFLECTED_PLAN_VIEW, any Plan context is also prioritised. contexts = self.get_linework_contexts(ifc, target_view) - self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view) - self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view) + self.serialize_contexts_elements(ifc, tree, contexts, "body", drawing_elements, target_view, link_matrix) + self.serialize_contexts_elements(ifc, tree, contexts, "annotation", drawing_elements, target_view, link_matrix) if tool.Ifc.get() == ifc and self.camera_element not in drawing_elements: with profile("Camera element"):