mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
fix: Route IfcAlignment creation through align_api.create()
Addresses Rick Brice's PR review feedback on #7785. Root cause: the AddElement dialog was manually stitching together IfcAlignment layout containers and calling create_representation(), bypassing the validated construction sequence in align_api.create(). This left IfcGradientCurve.BaseCurve potentially None and skipped zero-length segment and stationing referent setup. Changes: - root/operator.py: alignment templates now call align_api.create() directly (HORIZONTAL, GRADIENT, CANT) or _create_polyline_representation (POLYLINE_2D/3D), then manually link the result to the Blender object via tool.Ifc.link + tool.Collector.assign. The old generic core.assign_class path is retained for all non-alignment templates unchanged. - tool/alignment.py: remove create_representation_structure() — superseded by the operator changes above. - create_representation.py: revert the if layout_nest: guard; with align_api.create() as the entry point the zero-length segment always exists before create_representation is called. - add_zero_length_segment.py: revert the BaseCurve None guard; the root cause (gradient curve created without a base curve) no longer occurs. - alignment/operator.py: guard _create_geometric_representation call so it only runs when no curve representation exists yet; auto-invoke create_alignment_by_pi after PI picker finishes if >=2 PIs are defined. - util/file.py: fix StopIteration on short IFC template files (next(ifc_file) → next(ifc_file, None) with break). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -583,6 +583,10 @@ class CIVIL_OT_pick_pi_from_viewport(bpy.types.Operator, PolylineOperator, tool.
|
||||
recalculate_pi_geometry(props)
|
||||
rebuild_display_rows(props)
|
||||
|
||||
# If invoked from alignment creation flow, auto-create IFC segments
|
||||
if props.active_alignment_id != 0 and len(props.pis) >= 2:
|
||||
bpy.ops.civil.create_alignment_by_pi()
|
||||
|
||||
|
||||
class CIVIL_OT_recalculate_pis(Operator, tool.Ifc.Operator):
|
||||
"""Recalculate PI geometry and update IFC/visualization"""
|
||||
@@ -738,7 +742,8 @@ class CIVIL_OT_create_alignment_by_pi(Operator, tool.Ifc.Operator):
|
||||
segments = ifcopenshell.api.alignment.get_layout_segments(h_layout)
|
||||
has_real_segments = bool([s for s in segments if not tool.Alignment.is_zero_length_segment(s)])
|
||||
|
||||
ifcopenshell.api.alignment._create_geometric_representation(tool.Ifc.get(), existing_alignment)
|
||||
if not ifcopenshell.api.alignment.get_curve(existing_alignment):
|
||||
ifcopenshell.api.alignment._create_geometric_representation(tool.Ifc.get(), existing_alignment)
|
||||
|
||||
if not has_real_segments:
|
||||
# Use existing alignment - add segments to it
|
||||
|
||||
@@ -540,47 +540,86 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if ifc_context:
|
||||
ifc_context = tool.Ifc.get().by_id(ifc_context)
|
||||
|
||||
if representation_template in (
|
||||
"EMPTY",
|
||||
alignment_templates = {
|
||||
"ALIGNMENT_HORIZONTAL",
|
||||
"ALIGNMENT_GRADIENT",
|
||||
"ALIGNMENT_CANT",
|
||||
"ALIGNMENT_POLYLINE_3D",
|
||||
"ALIGNMENT_POLYLINE_2D",
|
||||
"LAYERSET_AXIS2",
|
||||
"LAYERSET_AXIS3",
|
||||
"PROFILESET",
|
||||
) or representation_template.startswith("FLOW_SEGMENT_"):
|
||||
mesh = None
|
||||
elif representation_template == "OBJ" and not props.representation_obj:
|
||||
mesh = None
|
||||
}
|
||||
|
||||
if representation_template in alignment_templates:
|
||||
import ifcopenshell.api.alignment as align_api
|
||||
from ifcopenshell.api.alignment._create_polyline_representation import (
|
||||
_create_polyline_representation,
|
||||
)
|
||||
|
||||
ifc_file = tool.Ifc.get()
|
||||
alignment_name = props.name or "Unnamed"
|
||||
|
||||
if representation_template == "ALIGNMENT_HORIZONTAL":
|
||||
element = align_api.create(ifc_file, alignment_name, include_vertical=False, include_cant=False)
|
||||
elif representation_template == "ALIGNMENT_GRADIENT":
|
||||
element = align_api.create(ifc_file, alignment_name, include_vertical=True, include_cant=False)
|
||||
elif representation_template == "ALIGNMENT_CANT":
|
||||
element = align_api.create(ifc_file, alignment_name, include_vertical=True, include_cant=True)
|
||||
elif representation_template == "ALIGNMENT_POLYLINE_3D":
|
||||
element = ifc_file.createIfcAlignment(GlobalId=ifcopenshell.guid.new(), Name=alignment_name)
|
||||
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, element, pts)
|
||||
project = ifc_file.by_type("IfcProject")
|
||||
if project:
|
||||
ifcopenshell.api.aggregate.assign_object(ifc_file, products=[element], relating_object=project[0])
|
||||
elif representation_template == "ALIGNMENT_POLYLINE_2D":
|
||||
element = ifc_file.createIfcAlignment(GlobalId=ifcopenshell.guid.new(), Name=alignment_name)
|
||||
pts = [
|
||||
ifc_file.createIfcCartesianPoint(Coordinates=(0.0, 0.0)),
|
||||
ifc_file.createIfcCartesianPoint(Coordinates=(1.0, 0.0)),
|
||||
]
|
||||
_create_polyline_representation(ifc_file, element, pts)
|
||||
project = ifc_file.by_type("IfcProject")
|
||||
if project:
|
||||
ifcopenshell.api.aggregate.assign_object(ifc_file, products=[element], relating_object=project[0])
|
||||
|
||||
element.Description = props.description or None
|
||||
obj = bpy.data.objects.new(props.ifc_class[3:], None)
|
||||
obj.name = alignment_name
|
||||
obj.location = bpy.context.scene.cursor.location
|
||||
tool.Root.set_object_name(obj, element)
|
||||
tool.Ifc.link(element, obj)
|
||||
tool.Collector.assign(obj)
|
||||
else:
|
||||
mesh = bpy.data.meshes.new("Mesh")
|
||||
if representation_template in (
|
||||
"EMPTY",
|
||||
"LAYERSET_AXIS2",
|
||||
"LAYERSET_AXIS3",
|
||||
"PROFILESET",
|
||||
) or representation_template.startswith("FLOW_SEGMENT_"):
|
||||
mesh = None
|
||||
elif representation_template == "OBJ" and not props.representation_obj:
|
||||
mesh = None
|
||||
else:
|
||||
mesh = bpy.data.meshes.new("Mesh")
|
||||
|
||||
obj = bpy.data.objects.new(props.ifc_class[3:], mesh)
|
||||
obj.name = props.name or "Unnamed"
|
||||
obj.location = bpy.context.scene.cursor.location
|
||||
element = core.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Root,
|
||||
obj=obj,
|
||||
ifc_class=props.ifc_class,
|
||||
predefined_type=predefined_type,
|
||||
should_add_representation=False,
|
||||
)
|
||||
element.Description = props.description or None
|
||||
obj = bpy.data.objects.new(props.ifc_class[3:], mesh)
|
||||
obj.name = props.name or "Unnamed"
|
||||
obj.location = bpy.context.scene.cursor.location
|
||||
element = core.assign_class(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Root,
|
||||
obj=obj,
|
||||
ifc_class=props.ifc_class,
|
||||
predefined_type=predefined_type,
|
||||
should_add_representation=False,
|
||||
)
|
||||
element.Description = props.description or None
|
||||
|
||||
if representation_template == "EMPTY":
|
||||
if representation_template == "EMPTY" or representation_template in alignment_templates:
|
||||
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:
|
||||
@@ -849,6 +888,13 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
tool.Blender.set_active_object(obj)
|
||||
|
||||
# After alignment creation, auto-invoke PI picker for layout-based templates
|
||||
if props.ifc_class == "IfcAlignment" and representation_template.startswith("ALIGNMENT_") and representation_template not in ("ALIGNMENT_POLYLINE_2D", "ALIGNMENT_POLYLINE_3D"):
|
||||
civil_props = context.scene.CivilAlignmentProperties
|
||||
civil_props.active_alignment_id = element.id()
|
||||
civil_props.active_alignment_name = element.Name or "Unnamed"
|
||||
bpy.ops.civil.pick_pi_from_viewport("INVOKE_DEFAULT")
|
||||
|
||||
def draw(self, context):
|
||||
props = tool.Root.get_root_props()
|
||||
self.layout.use_property_split = True
|
||||
|
||||
@@ -388,68 +388,6 @@ 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
|
||||
# =========================================================================
|
||||
@@ -634,6 +572,8 @@ class Alignment:
|
||||
obj = bpy.data.objects.new(tool.Loader.get_name(curve_segment), mesh)
|
||||
tool.Ifc.link(curve_segment, obj)
|
||||
tool.Collector.assign(obj)
|
||||
if parent_obj:
|
||||
obj.parent = parent_obj
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -123,8 +123,7 @@ def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, in
|
||||
if layout.is_a("IfcSegmentedReferenceCurve"):
|
||||
ifcopenshell.api.alignment.add_zero_length_segment(file, layout.BaseCurve)
|
||||
elif layout.is_a("IfcGradientCurve"):
|
||||
if layout.BaseCurve is not None:
|
||||
ifcopenshell.api.alignment.add_zero_length_segment(file, layout.BaseCurve)
|
||||
ifcopenshell.api.alignment.add_zero_length_segment(file, layout.BaseCurve)
|
||||
|
||||
else:
|
||||
zero_length_curve_segment = None
|
||||
|
||||
@@ -52,6 +52,5 @@ 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)
|
||||
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)
|
||||
for segment in layout_nest.RelatedObjects:
|
||||
_add_segment_to_curve(file, segment, curve)
|
||||
|
||||
@@ -91,7 +91,9 @@ class IfcHeaderExtractor:
|
||||
data = HeaderMetadata()
|
||||
max_lines_to_parse = 50
|
||||
for _ in range(max_lines_to_parse):
|
||||
line = next(ifc_file)
|
||||
line = next(ifc_file, None)
|
||||
if line is None:
|
||||
break
|
||||
if isinstance(line, bytes):
|
||||
line = line.decode("utf-8")
|
||||
if line.startswith("FILE_DESCRIPTION"):
|
||||
|
||||
Reference in New Issue
Block a user