From e09ce1bcd8f4b0c430014779de1fd8735aaf8a61 Mon Sep 17 00:00:00 2001 From: DesertSpringsCivil Date: Tue, 3 Mar 2026 23:53:41 -0700 Subject: [PATCH] feat: Add alignment representation templates to AddElement dialog Wire IfcAlignment creation into Bonsai's Add IFC Element flow with representation template options (Horizontal, Gradient, Cant, 3D/2D Polyline). Adds create_representation_structure() to tool.Alignment which creates layout containers, geometric representation, and polyline placeholders. Guards create_representation against empty layout nests. Co-Authored-By: Claude Opus 4.6 --- src/bonsai/bonsai/bim/module/root/data.py | 33 ++++++++++ src/bonsai/bonsai/bim/module/root/operator.py | 26 +++++++- src/bonsai/bonsai/tool/alignment.py | 62 +++++++++++++++++++ .../api/alignment/create_representation.py | 5 +- 4 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/root/data.py b/src/bonsai/bonsai/bim/module/root/data.py index d5a6d6aac2..8b627b822f 100644 --- a/src/bonsai/bonsai/bim/module/root/data.py +++ b/src/bonsai/bonsai/bim/module/root/data.py @@ -138,7 +138,40 @@ class IfcClassData: ("EMPTY", "No Geometry", "Start with an empty object"), ] + if ifc_class in ("IfcAlignmentHorizontal", "IfcAlignmentVertical", "IfcAlignmentCant"): + return templates # layout containers have no direct representation per IFC 4.3 spec + if ifc_class == "IfcAlignment": + templates.extend( + [ + None, + ( + "ALIGNMENT_HORIZONTAL", + "Horizontal Alignment (2D)", + "2D horizontal-only alignment (IfcCompositeCurve)", + ), + ( + "ALIGNMENT_GRADIENT", + "Gradient Curve (3D)", + "3D alignment with horizontal and vertical layouts (IfcGradientCurve)", + ), + ( + "ALIGNMENT_CANT", + "Segmented Reference Curve", + "3D alignment with horizontal, vertical, and cant layouts (IfcSegmentedReferenceCurve)", + ), + ( + "ALIGNMENT_POLYLINE_3D", + "3D Survey Polyline", + "3D alignment from survey data (IfcPolyline with 3D points)", + ), + ( + "ALIGNMENT_POLYLINE_2D", + "2D Planning Polyline", + "2D alignment for early planning phases (IfcPolyline with 2D points)", + ), + ] + ) return templates elif ifc_class in ("IfcWindowType", "IfcWindowStyle", "IfcWindow"): templates.extend([None, ("WINDOW", "Window", "Parametric window")]) diff --git a/src/bonsai/bonsai/bim/module/root/operator.py b/src/bonsai/bonsai/bim/module/root/operator.py index 2a074a7237..86ab6c93a1 100644 --- a/src/bonsai/bonsai/bim/module/root/operator.py +++ b/src/bonsai/bonsai/bim/module/root/operator.py @@ -542,6 +542,11 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): if representation_template in ( "EMPTY", + "ALIGNMENT_HORIZONTAL", + "ALIGNMENT_GRADIENT", + "ALIGNMENT_CANT", + "ALIGNMENT_POLYLINE_3D", + "ALIGNMENT_POLYLINE_2D", "LAYERSET_AXIS2", "LAYERSET_AXIS3", "PROFILESET", @@ -566,7 +571,17 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): ) element.Description = props.description or None - if representation_template == "EMTPY" or not ifc_context: + if representation_template == "EMPTY": + pass + elif representation_template in ( + "ALIGNMENT_HORIZONTAL", + "ALIGNMENT_GRADIENT", + "ALIGNMENT_CANT", + "ALIGNMENT_POLYLINE_3D", + "ALIGNMENT_POLYLINE_2D", + ): + tool.Alignment.create_representation_structure(element, representation_template) + elif not ifc_context: pass elif representation_template == "OBJ" and props.representation_obj: obj.matrix_world = props.representation_obj.matrix_world.copy() @@ -862,5 +877,12 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator): elif props.representation_template == "PROFILESET": row = self.layout.row() prop_with_search(self.layout, props, "profile", text="Profile", should_click_ok=True) - if props.representation_template != "EMPTY": + alignment_templates = { + "ALIGNMENT_HORIZONTAL", + "ALIGNMENT_GRADIENT", + "ALIGNMENT_CANT", + "ALIGNMENT_POLYLINE_3D", + "ALIGNMENT_POLYLINE_2D", + } + if props.representation_template != "EMPTY" and props.representation_template not in alignment_templates: prop_with_search(self.layout, props, "contexts", should_click_ok=True) diff --git a/src/bonsai/bonsai/tool/alignment.py b/src/bonsai/bonsai/tool/alignment.py index 0e771973e7..60d8d623df 100644 --- a/src/bonsai/bonsai/tool/alignment.py +++ b/src/bonsai/bonsai/tool/alignment.py @@ -388,6 +388,68 @@ class Alignment: ifc_file = tool.Ifc.get() align_api.layout_horizontal_alignment_by_pi_method(ifc_file, layout, hpoints, radii) + @classmethod + def create_representation_structure(cls, alignment: "ifcopenshell.entity_instance", template: str) -> None: + """Create semantic layout containers and geometric representation for an IfcAlignment. + + Called from the AddElement operator when the user selects a representation type + for a newly-created IfcAlignment. For layout-based templates (HORIZONTAL, GRADIENT, + CANT), the appropriate IfcAlignmentHorizontal / Vertical / Cant entities are nested to + the alignment and ifcopenshell.api.alignment.create_representation is invoked to build + the matching geometric curves. For polyline templates, an IfcPolyLine with two + placeholder origin points is created directly. + + Layout containers are IFC entities only — no Blender objects are created for them. + + Args: + alignment: The IfcAlignment entity that was just created by AddElement. + template: One of ALIGNMENT_HORIZONTAL, ALIGNMENT_GRADIENT, ALIGNMENT_CANT, + ALIGNMENT_POLYLINE_3D, or ALIGNMENT_POLYLINE_2D. + """ + import ifcopenshell.api.alignment as align_api + import ifcopenshell.api.nest + import ifcopenshell.guid + from ifcopenshell.api.alignment._create_polyline_representation import ( + _create_polyline_representation, + ) + + ifc_file = tool.Ifc.get() + + if template == "ALIGNMENT_HORIZONTAL": + h_layout = ifc_file.createIfcAlignmentHorizontal(GlobalId=ifcopenshell.guid.new()) + ifcopenshell.api.nest.assign_object(ifc_file, related_objects=[h_layout], relating_object=alignment) + align_api.create_representation(ifc_file, alignment) + + elif template == "ALIGNMENT_GRADIENT": + h_layout = ifc_file.createIfcAlignmentHorizontal(GlobalId=ifcopenshell.guid.new()) + ifcopenshell.api.nest.assign_object(ifc_file, related_objects=[h_layout], relating_object=alignment) + v_layout = ifc_file.createIfcAlignmentVertical(GlobalId=ifcopenshell.guid.new()) + ifcopenshell.api.nest.assign_object(ifc_file, related_objects=[v_layout], relating_object=alignment) + align_api.create_representation(ifc_file, alignment) + + elif template == "ALIGNMENT_CANT": + h_layout = ifc_file.createIfcAlignmentHorizontal(GlobalId=ifcopenshell.guid.new()) + ifcopenshell.api.nest.assign_object(ifc_file, related_objects=[h_layout], relating_object=alignment) + v_layout = ifc_file.createIfcAlignmentVertical(GlobalId=ifcopenshell.guid.new()) + ifcopenshell.api.nest.assign_object(ifc_file, related_objects=[v_layout], relating_object=alignment) + c_layout = ifc_file.createIfcAlignmentCant(GlobalId=ifcopenshell.guid.new()) + ifcopenshell.api.nest.assign_object(ifc_file, related_objects=[c_layout], relating_object=alignment) + align_api.create_representation(ifc_file, alignment) + + elif template == "ALIGNMENT_POLYLINE_3D": + pts = [ + ifc_file.createIfcCartesianPoint(Coordinates=(0.0, 0.0, 0.0)), + ifc_file.createIfcCartesianPoint(Coordinates=(1.0, 0.0, 0.0)), + ] + _create_polyline_representation(ifc_file, alignment, pts) + + elif template == "ALIGNMENT_POLYLINE_2D": + pts = [ + ifc_file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)), + ifc_file.createIfcCartesianPoint(Coordinates=(1.0, 0.0)), + ] + _create_polyline_representation(ifc_file, alignment, pts) + # ========================================================================= # Zero-Length Segment Utilities # ========================================================================= diff --git a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_representation.py b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_representation.py index 896108367f..67e911bfbc 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/alignment/create_representation.py +++ b/src/ifcopenshell-python/ifcopenshell/api/alignment/create_representation.py @@ -52,5 +52,6 @@ def create_representation( for layout in layouts: curve = ifcopenshell.api.alignment.get_layout_curve(layout) layout_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout) - for segment in layout_nest.RelatedObjects: - _add_segment_to_curve(file, segment, curve) + if layout_nest: # None when no segments exist yet (by design of get_alignment_segment_nest) + for segment in layout_nest.RelatedObjects: + _add_segment_to_curve(file, segment, curve)