Remove create alignment operator, refactor to use tool.Alignment.get_active_alignment

This commit is contained in:
Dion Moult
2026-02-25 11:03:06 +11:00
parent 95048ea2dd
commit cbdf1a193e
5 changed files with 67 additions and 146 deletions
@@ -36,7 +36,6 @@ classes = (
operator.SAIKEI_OT_recalculate_pis, operator.SAIKEI_OT_recalculate_pis,
operator.SAIKEI_OT_clear_pis, operator.SAIKEI_OT_clear_pis,
# Operators - Creation # Operators - Creation
operator.SAIKEI_OT_create_alignment,
operator.SAIKEI_OT_create_alignment_by_pi, operator.SAIKEI_OT_create_alignment_by_pi,
operator.SAIKEI_OT_import_alignment_csv, operator.SAIKEI_OT_import_alignment_csv,
# Operators - Stationing # Operators - Stationing
@@ -116,31 +116,6 @@ def poll_ifc4x3(cls, context):
return True return True
def get_alignment_by_id(ifc, alignment_id):
"""Safely get an alignment by ID, returning None if not found.
This handles the case where the IFC entity no longer exists
(e.g., after undo or external modification).
"""
if alignment_id == 0:
return None
try:
entity = ifc.by_id(alignment_id)
# Verify it's actually an alignment
if entity and entity.is_a("IfcAlignment"):
return entity
return None
except RuntimeError:
# Entity not found in IFC file
return None
def clear_invalid_alignment_reference(props):
"""Clear active alignment reference if it's invalid."""
props.active_alignment_id = 0
props.active_alignment_name = ""
def sync_pis_from_ifc(props): def sync_pis_from_ifc(props):
"""Sync PI Editor data from IFC alignment. """Sync PI Editor data from IFC alignment.
@@ -157,21 +132,14 @@ def sync_pis_from_ifc(props):
# No IFC file - clear everything # No IFC file - clear everything
props.pis.clear() props.pis.clear()
props.active_pi_index = 0 props.active_pi_index = 0
clear_invalid_alignment_reference(props)
rebuild_display_rows(props) rebuild_display_rows(props)
return False return False
if props.active_alignment_id == 0: alignment = tool.Alignment.get_active_alignment()
# No active alignment - just rebuild display if not alignment:
rebuild_display_rows(props)
return True
alignment = get_alignment_by_id(ifc, props.active_alignment_id)
if alignment is None:
# Alignment no longer exists - clear everything # Alignment no longer exists - clear everything
props.pis.clear() props.pis.clear()
props.active_pi_index = 0 props.active_pi_index = 0
clear_invalid_alignment_reference(props)
rebuild_display_rows(props) rebuild_display_rows(props)
return False return False
@@ -644,48 +612,40 @@ class SAIKEI_OT_recalculate_pis(Operator, tool.Ifc.Operator):
recalculate_pi_geometry(props) recalculate_pi_geometry(props)
# If there's an active alignment, update it in-place # If there's an active alignment, update it in-place
if props.active_alignment_id != 0: if alignment := tool.Alignment.get_active_alignment():
alignment = get_alignment_by_id(ifc, props.active_alignment_id) # Get horizontal layout for in-place editing
if alignment is None: h_layout = align_api.get_horizontal_layout(alignment)
# Alignment no longer exists (e.g., after undo) - clear reference if h_layout is None:
clear_invalid_alignment_reference(props) self.report({"ERROR"}, "Alignment has no horizontal layout")
self.report({"WARNING"}, "Active alignment no longer exists. Reference cleared.") return
return {"FINISHED"}
if alignment:
# Get horizontal layout for in-place editing
h_layout = align_api.get_horizontal_layout(alignment)
if h_layout is None:
self.report({"ERROR"}, "Alignment has no horizontal layout")
return {"CANCELLED"}
# Collect updated PI data # Collect updated PI data
hpoints = [[float(o) for o in tool.Georeference.enh2xyz((float(pi.e), float(pi.n), 0.), to_blender=False)[:2]] for pi in props.pis] hpoints = [[float(o) for o in tool.Georeference.enh2xyz((float(pi.e), float(pi.n), 0.), to_blender=False)[:2]] for pi in props.pis]
radii = [pi.radius for pi in props.pis[1:-1]] radii = [pi.radius for pi in props.pis[1:-1]]
# Remove Blender visualization for segments (not the whole hierarchy) # Remove Blender visualization for segments (not the whole hierarchy)
tool.Alignment.remove_layout_segment_objects(h_layout) tool.Alignment.remove_layout_segment_objects(h_layout)
# Clear existing IFC segments (preserves layout and zero-length terminator) # Clear existing IFC segments (preserves layout and zero-length terminator)
align_api.clear_layout_segments(ifc, h_layout) align_api.clear_layout_segments(ifc, h_layout)
# Add new segments with updated PI positions # Add new segments with updated PI positions
align_api.layout_horizontal_alignment_by_pi_method( align_api.layout_horizontal_alignment_by_pi_method(
ifc, h_layout, hpoints, radii ifc, h_layout, hpoints, radii
) )
# Refresh Blender visualization for new segments # Refresh Blender visualization for new segments
layout_obj = tool.Ifc.get_object(h_layout) layout_obj = tool.Ifc.get_object(h_layout)
if layout_obj: if layout_obj:
tool.Alignment.create_objects_for_layout_segments(h_layout, layout_obj) tool.Alignment.create_objects_for_layout_segments(h_layout, layout_obj)
# Alignment ID stays the same - no need to update props.active_alignment_id # Alignment ID stays the same - no need to update props.active_alignment_id
self.report({"INFO"}, f"Updated alignment '{alignment.Name}' with {len(hpoints)} PIs") self.report({"INFO"}, f"Updated alignment '{alignment.Name}' with {len(hpoints)} PIs")
return {"FINISHED"} return
# No active alignment - just report geometry recalculation # No active alignment - just report geometry recalculation
total_length = sum(pi.length_to_next for pi in props.pis) total_length = sum(pi.length_to_next for pi in props.pis)
self.report({"INFO"}, f"Recalculated {len(props.pis)} PIs, total length: {total_length:.2f}") self.report({"INFO"}, f"Recalculated {len(props.pis)} PIs, total length: {total_length:.2f}")
return {"FINISHED"}
class SAIKEI_OT_clear_pis(Operator, tool.Ifc.Operator): class SAIKEI_OT_clear_pis(Operator, tool.Ifc.Operator):
@@ -717,18 +677,12 @@ class SAIKEI_OT_clear_pis(Operator, tool.Ifc.Operator):
# If there's an active alignment, remove it entirely (Blender + IFC) # If there's an active alignment, remove it entirely (Blender + IFC)
# This ensures we don't leave the IFC in an inconsistent state # This ensures we don't leave the IFC in an inconsistent state
if props.active_alignment_id != 0: if alignment := tool.Alignment.get_active_alignment():
alignment = get_alignment_by_id(ifc, props.active_alignment_id) # Remove all Blender objects for this alignment
if alignment: removed_objects = tool.Alignment.remove_alignment_hierarchy(alignment)
# Remove all Blender objects for this alignment
removed_objects = tool.Alignment.remove_alignment_hierarchy(alignment)
# Remove the IFC alignment entity entirely # Remove the IFC alignment entity entirely
ifcopenshell.api.run("root.remove_product", ifc, product=alignment) ifcopenshell.api.run("root.remove_product", ifc, product=alignment)
# Clear the active alignment reference
props.active_alignment_id = 0
props.active_alignment_name = ""
# Clear the PI list in the UI # Clear the PI list in the UI
props.pis.clear() props.pis.clear()
@@ -742,7 +696,6 @@ class SAIKEI_OT_clear_pis(Operator, tool.Ifc.Operator):
self.report({"INFO"}, f"Cleared all PIs and removed {removed_objects} objects") self.report({"INFO"}, f"Cleared all PIs and removed {removed_objects} objects")
else: else:
self.report({"INFO"}, "Cleared all PIs") self.report({"INFO"}, "Cleared all PIs")
return {"FINISHED"}
# ============================================================================= # =============================================================================
@@ -750,41 +703,6 @@ class SAIKEI_OT_clear_pis(Operator, tool.Ifc.Operator):
# ============================================================================= # =============================================================================
class SAIKEI_OT_create_alignment(Operator, tool.Ifc.Operator):
"""Create a new IFC alignment"""
bl_idname = "saikei.create_alignment"
bl_label = "Create Alignment"
bl_description = "Create a new empty IFC alignment"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return poll_ifc4x3(cls, context)
def _execute(self, context):
ifc = tool.Ifc.get()
props = context.scene.SaikeiAlignmentProperties
alignment = ifcopenshell.api.alignment.create(
ifc,
name=props.new_alignment_name,
)
# Create full Blender hierarchy (alignment + layouts + segments)
obj = tool.Alignment.create_hierarchy_for_alignment(alignment)
# Update UI
props.active_alignment_name = props.new_alignment_name
props.active_alignment_id = alignment.id()
if obj:
self.report({"INFO"}, f"Created alignment: {props.new_alignment_name}")
else:
self.report({"WARNING"}, f"Created IFC alignment but could not create Blender object")
return {"FINISHED"}
class SAIKEI_OT_create_alignment_by_pi(Operator, tool.Ifc.Operator): class SAIKEI_OT_create_alignment_by_pi(Operator, tool.Ifc.Operator):
"""Create alignment using the PI (Point of Intersection) method""" """Create alignment using the PI (Point of Intersection) method"""
@@ -801,42 +719,44 @@ class SAIKEI_OT_create_alignment_by_pi(Operator, tool.Ifc.Operator):
if len(props.pis) < 2: if len(props.pis) < 2:
cls.poll_message_set("Need at least 2 PI points") cls.poll_message_set("Need at least 2 PI points")
return False return False
if not tool.Alignment.get_active_alignment():
cls.poll_message_set("Select an alignment to edit")
return False
return True return True
def _execute(self, context): def _execute(self, context):
props = context.scene.SaikeiAlignmentProperties props = context.scene.SaikeiAlignmentProperties
if not props.active_alignment_id:
return {"FINISHED"}
hpoints = [[float(o) for o in tool.Georeference.enh2xyz((float(pi.e), float(pi.n), 0.), to_blender=False)[:2]] for pi in props.pis] hpoints = [[float(o) for o in tool.Georeference.enh2xyz((float(pi.e), float(pi.n), 0.), to_blender=False)[:2]] for pi in props.pis]
radii = [pi.radius for pi in props.pis[1:-1]] radii = [pi.radius for pi in props.pis[1:-1]]
existing_alignment = get_alignment_by_id(tool.Ifc.get(), props.active_alignment_id) existing_alignment = tool.Alignment.get_active_alignment()
h_layout = ifcopenshell.api.alignment.get_horizontal_layout(existing_alignment) if not (h_layout := ifcopenshell.api.alignment.get_horizontal_layout(existing_alignment)):
if h_layout: return
# Check if horizontal layout is empty (only has zero-length terminal or no segments) # Check if horizontal layout is empty (only has zero-length terminal or no segments)
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)])
if not has_real_segments: ifcopenshell.api.alignment._create_geometric_representation(tool.Ifc.get(), existing_alignment)
# Use existing alignment - add segments to it
# Use safe wrapper to validate layout has parent alignment
tool.Alignment.safe_layout_horizontal_by_pi_method(tool.Ifc.get(), h_layout, hpoints, radii)
# Create/update Blender objects for the segments if not has_real_segments:
alignment_obj = tool.Ifc.get_object(existing_alignment) # Use existing alignment - add segments to it
h_layout_obj = tool.Ifc.get_object(h_layout) # Use safe wrapper to validate layout has parent alignment
tool.Alignment.safe_layout_horizontal_by_pi_method(tool.Ifc.get(), h_layout, hpoints, radii)
if not h_layout_obj and alignment_obj: # Create/update Blender objects for the segments
h_layout_obj = tool.Alignment.create_object_for_layout(h_layout, alignment_obj) alignment_obj = tool.Ifc.get_object(existing_alignment)
h_layout_obj = tool.Ifc.get_object(h_layout)
if h_layout_obj: if not h_layout_obj and alignment_obj:
tool.Alignment.create_objects_for_layout_segments(h_layout, h_layout_obj) h_layout_obj = tool.Alignment.create_object_for_layout(h_layout, alignment_obj)
self.report( if h_layout_obj:
{"INFO"}, f"Added {len(hpoints)} PIs to existing alignment '{existing_alignment.Name}'" tool.Alignment.create_objects_for_layout_segments(h_layout, h_layout_obj)
)
return {"FINISHED"} self.report(
{"INFO"}, f"Added {len(hpoints)} PIs to existing alignment '{existing_alignment.Name}'"
)
class SAIKEI_OT_import_alignment_csv(Operator, tool.Ifc.Operator, ImportHelper): class SAIKEI_OT_import_alignment_csv(Operator, tool.Ifc.Operator, ImportHelper):
@@ -923,9 +843,8 @@ class SAIKEI_OT_add_stationing_referent(Operator, tool.Ifc.Operator):
ifc = tool.Ifc.get() ifc = tool.Ifc.get()
props = context.scene.SaikeiAlignmentProperties props = context.scene.SaikeiAlignmentProperties
alignment = get_alignment_by_id(ifc, props.active_alignment_id) alignment = tool.Alignment.get_active_alignment()
if alignment is None: if alignment is None:
clear_invalid_alignment_reference(props)
self.report({"ERROR"}, "Alignment no longer exists. Reference cleared.") self.report({"ERROR"}, "Alignment no longer exists. Reference cleared.")
return {"CANCELLED"} return {"CANCELLED"}
@@ -950,7 +869,6 @@ class SAIKEI_OT_add_stationing_referent(Operator, tool.Ifc.Operator):
) )
self.report({"INFO"}, f"Added referent '{name}' at station {self.station}") self.report({"INFO"}, f"Added referent '{name}' at station {self.station}")
return {"FINISHED"}
def format_station(station_value): def format_station(station_value):
@@ -987,16 +905,14 @@ class SAIKEI_OT_name_segments(Operator, tool.Ifc.Operator):
ifc = tool.Ifc.get() ifc = tool.Ifc.get()
props = context.scene.SaikeiAlignmentProperties props = context.scene.SaikeiAlignmentProperties
alignment = get_alignment_by_id(ifc, props.active_alignment_id) alignment = tool.Alignment.get_active_alignment()
if alignment is None: if alignment is None:
clear_invalid_alignment_reference(props)
self.report({"ERROR"}, "Alignment no longer exists. Reference cleared.") self.report({"ERROR"}, "Alignment no longer exists. Reference cleared.")
return {"CANCELLED"} return {"CANCELLED"}
ifcopenshell.api.alignment.name_segments(ifc, alignment) ifcopenshell.api.alignment.name_segments(ifc, alignment)
self.report({"INFO"}, "Named alignment segments") self.report({"INFO"}, "Named alignment segments")
return {"FINISHED"}
# ============================================================================= # =============================================================================
@@ -1030,8 +946,7 @@ class SAIKEI_OT_enter_pi_edit_mode(Operator, tool.Ifc.Operator):
cls.poll_message_set("No alignment selected") cls.poll_message_set("No alignment selected")
return False return False
# Verify alignment still exists # Verify alignment still exists
ifc = tool.Ifc.get() alignment = tool.Alignment.get_active_alignment()
alignment = get_alignment_by_id(ifc, props.active_alignment_id)
if alignment is None: if alignment is None:
cls.poll_message_set("Selected alignment no longer exists") cls.poll_message_set("Selected alignment no longer exists")
return False return False
@@ -203,7 +203,6 @@ class SAIKEI_PT_alignment_creation(Panel):
# Creation operators # Creation operators
col = layout.column(align=True) col = layout.column(align=True)
col.operator("saikei.create_alignment", icon="ADD")
col.operator("saikei.create_alignment_by_pi", icon="CURVE_DATA") col.operator("saikei.create_alignment_by_pi", icon="CURVE_DATA")
+6
View File
@@ -1063,3 +1063,9 @@ class Alignment:
radii.append(radius) radii.append(radius)
return (hpoints, radii) return (hpoints, radii)
@classmethod
def get_active_alignment(cls) -> ifcopenshell.entity_instance | None:
if obj := tool.Blender.get_active_object():
if (element := tool.Ifc.get_entity(obj)) and element.is_a("IfcAlignment"):
return element
@@ -90,6 +90,7 @@ from .layout_vertical_alignment_by_pi_method import (
from .name_segments import name_segments from .name_segments import name_segments
from .segment_vertices import segment_vertices from .segment_vertices import segment_vertices
from .update_fallback_position import update_fallback_position from .update_fallback_position import update_fallback_position
from ._create_geometric_representation import _create_geometric_representation
from .util import * from .util import *
__all__ = [ __all__ = [
@@ -105,6 +106,7 @@ __all__ = [
"create_layout_segment", "create_layout_segment",
"create_representation", "create_representation",
"create_segment_representations", "create_segment_representations",
"_create_geometric_representation", # TODO I know I know
"distance_along_from_station", "distance_along_from_station",
"get_alignment", "get_alignment",
"get_alignment_layout_nest", "get_alignment_layout_nest",