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"),
]
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")])
+24 -2
View File
@@ -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)
+62
View File
@@ -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
# =========================================================================
@@ -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)