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 <noreply@anthropic.com>
This commit is contained in:
DesertSpringsCivil
2026-03-03 23:53:41 -07:00
parent 0b3967609c
commit e09ce1bcd8
4 changed files with 122 additions and 4 deletions
+33
View File
@@ -138,7 +138,40 @@ class IfcClassData:
("EMPTY", "No Geometry", "Start with an empty object"), ("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": 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 return templates
elif ifc_class in ("IfcWindowType", "IfcWindowStyle", "IfcWindow"): elif ifc_class in ("IfcWindowType", "IfcWindowStyle", "IfcWindow"):
templates.extend([None, ("WINDOW", "Window", "Parametric window")]) templates.extend([None, ("WINDOW", "Window", "Parametric window")])
+24 -2
View File
@@ -542,6 +542,11 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
if representation_template in ( if representation_template in (
"EMPTY", "EMPTY",
"ALIGNMENT_HORIZONTAL",
"ALIGNMENT_GRADIENT",
"ALIGNMENT_CANT",
"ALIGNMENT_POLYLINE_3D",
"ALIGNMENT_POLYLINE_2D",
"LAYERSET_AXIS2", "LAYERSET_AXIS2",
"LAYERSET_AXIS3", "LAYERSET_AXIS3",
"PROFILESET", "PROFILESET",
@@ -566,7 +571,17 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
) )
element.Description = props.description or None 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 pass
elif representation_template == "OBJ" and props.representation_obj: elif representation_template == "OBJ" and props.representation_obj:
obj.matrix_world = props.representation_obj.matrix_world.copy() 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": elif props.representation_template == "PROFILESET":
row = self.layout.row() row = self.layout.row()
prop_with_search(self.layout, props, "profile", text="Profile", should_click_ok=True) 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) prop_with_search(self.layout, props, "contexts", should_click_ok=True)
+62
View File
@@ -388,6 +388,68 @@ class Alignment:
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
align_api.layout_horizontal_alignment_by_pi_method(ifc_file, layout, hpoints, radii) 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 # Zero-Length Segment Utilities
# ========================================================================= # =========================================================================
@@ -52,5 +52,6 @@ def create_representation(
for layout in layouts: for layout in layouts:
curve = ifcopenshell.api.alignment.get_layout_curve(layout) curve = ifcopenshell.api.alignment.get_layout_curve(layout)
layout_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout) layout_nest = ifcopenshell.api.alignment.get_alignment_segment_nest(layout)
for segment in layout_nest.RelatedObjects: if layout_nest: # None when no segments exist yet (by design of get_alignment_segment_nest)
_add_segment_to_curve(file, segment, curve) for segment in layout_nest.RelatedObjects:
_add_segment_to_curve(file, segment, curve)