mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 22:50:21 +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)
|
recalculate_pi_geometry(props)
|
||||||
rebuild_display_rows(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):
|
class CIVIL_OT_recalculate_pis(Operator, tool.Ifc.Operator):
|
||||||
"""Recalculate PI geometry and update IFC/visualization"""
|
"""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)
|
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)])
|
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:
|
if not has_real_segments:
|
||||||
# Use existing alignment - add segments to it
|
# Use existing alignment - add segments to it
|
||||||
|
|||||||
@@ -540,47 +540,86 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if ifc_context:
|
if ifc_context:
|
||||||
ifc_context = tool.Ifc.get().by_id(ifc_context)
|
ifc_context = tool.Ifc.get().by_id(ifc_context)
|
||||||
|
|
||||||
if representation_template in (
|
alignment_templates = {
|
||||||
"EMPTY",
|
|
||||||
"ALIGNMENT_HORIZONTAL",
|
"ALIGNMENT_HORIZONTAL",
|
||||||
"ALIGNMENT_GRADIENT",
|
"ALIGNMENT_GRADIENT",
|
||||||
"ALIGNMENT_CANT",
|
"ALIGNMENT_CANT",
|
||||||
"ALIGNMENT_POLYLINE_3D",
|
"ALIGNMENT_POLYLINE_3D",
|
||||||
"ALIGNMENT_POLYLINE_2D",
|
"ALIGNMENT_POLYLINE_2D",
|
||||||
"LAYERSET_AXIS2",
|
}
|
||||||
"LAYERSET_AXIS3",
|
|
||||||
"PROFILESET",
|
if representation_template in alignment_templates:
|
||||||
) or representation_template.startswith("FLOW_SEGMENT_"):
|
import ifcopenshell.api.alignment as align_api
|
||||||
mesh = None
|
from ifcopenshell.api.alignment._create_polyline_representation import (
|
||||||
elif representation_template == "OBJ" and not props.representation_obj:
|
_create_polyline_representation,
|
||||||
mesh = None
|
)
|
||||||
|
|
||||||
|
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:
|
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 = bpy.data.objects.new(props.ifc_class[3:], mesh)
|
||||||
obj.name = props.name or "Unnamed"
|
obj.name = props.name or "Unnamed"
|
||||||
obj.location = bpy.context.scene.cursor.location
|
obj.location = bpy.context.scene.cursor.location
|
||||||
element = core.assign_class(
|
element = core.assign_class(
|
||||||
tool.Ifc,
|
tool.Ifc,
|
||||||
tool.Collector,
|
tool.Collector,
|
||||||
tool.Root,
|
tool.Root,
|
||||||
obj=obj,
|
obj=obj,
|
||||||
ifc_class=props.ifc_class,
|
ifc_class=props.ifc_class,
|
||||||
predefined_type=predefined_type,
|
predefined_type=predefined_type,
|
||||||
should_add_representation=False,
|
should_add_representation=False,
|
||||||
)
|
)
|
||||||
element.Description = props.description or None
|
element.Description = props.description or None
|
||||||
|
|
||||||
if representation_template == "EMPTY":
|
if representation_template == "EMPTY" or representation_template in alignment_templates:
|
||||||
pass
|
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:
|
elif not ifc_context:
|
||||||
pass
|
pass
|
||||||
elif representation_template == "OBJ" and props.representation_obj:
|
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)
|
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||||
tool.Blender.set_active_object(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):
|
def draw(self, context):
|
||||||
props = tool.Root.get_root_props()
|
props = tool.Root.get_root_props()
|
||||||
self.layout.use_property_split = True
|
self.layout.use_property_split = True
|
||||||
|
|||||||
@@ -388,68 +388,6 @@ 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
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -634,6 +572,8 @@ class Alignment:
|
|||||||
obj = bpy.data.objects.new(tool.Loader.get_name(curve_segment), mesh)
|
obj = bpy.data.objects.new(tool.Loader.get_name(curve_segment), mesh)
|
||||||
tool.Ifc.link(curve_segment, obj)
|
tool.Ifc.link(curve_segment, obj)
|
||||||
tool.Collector.assign(obj)
|
tool.Collector.assign(obj)
|
||||||
|
if parent_obj:
|
||||||
|
obj.parent = parent_obj
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -123,8 +123,7 @@ def add_zero_length_segment(file: ifcopenshell.file, layout: entity_instance, in
|
|||||||
if layout.is_a("IfcSegmentedReferenceCurve"):
|
if layout.is_a("IfcSegmentedReferenceCurve"):
|
||||||
ifcopenshell.api.alignment.add_zero_length_segment(file, layout.BaseCurve)
|
ifcopenshell.api.alignment.add_zero_length_segment(file, layout.BaseCurve)
|
||||||
elif layout.is_a("IfcGradientCurve"):
|
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:
|
else:
|
||||||
zero_length_curve_segment = None
|
zero_length_curve_segment = None
|
||||||
|
|||||||
@@ -52,6 +52,5 @@ 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)
|
||||||
if layout_nest: # None when no segments exist yet (by design of get_alignment_segment_nest)
|
for segment in layout_nest.RelatedObjects:
|
||||||
for segment in layout_nest.RelatedObjects:
|
_add_segment_to_curve(file, segment, curve)
|
||||||
_add_segment_to_curve(file, segment, curve)
|
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ class IfcHeaderExtractor:
|
|||||||
data = HeaderMetadata()
|
data = HeaderMetadata()
|
||||||
max_lines_to_parse = 50
|
max_lines_to_parse = 50
|
||||||
for _ in range(max_lines_to_parse):
|
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):
|
if isinstance(line, bytes):
|
||||||
line = line.decode("utf-8")
|
line = line.decode("utf-8")
|
||||||
if line.startswith("FILE_DESCRIPTION"):
|
if line.startswith("FILE_DESCRIPTION"):
|
||||||
|
|||||||
Reference in New Issue
Block a user