fix: Address PR #7589 review items - prefix rename, cleanup, copyright

- Rename all saikei.* operator idnames to civil.* per Bonsai convention
- Rename SAIKEI_OT_*, SAIKEI_PT_*, SAIKEI_UL_* classes to CIVIL_* prefix
- Rename SaikeiAlignmentProperties -> CivilAlignmentProperties
- Rename saikei_* Blender object custom property keys to civil_*
- Remove IOS-version-compat try/except fallback in _get_segment_vertices_in_model_units()
  now that Rick's segment_vertices() API accepts IfcAlignmentSegment directly
- Remove try/except wrapper around get_alignment() - call directly
- Add Michael Yoder copyright to __init__.py and operator.py
- Fix misleading coordinate comment (IFC -> global easting/northing)
- Document props.pis coordinate system (global E/N) in AlignmentPI and operator comments

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DesertSpringsCivil
2026-03-03 13:39:19 -07:00
parent 9a7cd5b373
commit 0b3967609c
5 changed files with 104 additions and 111 deletions
@@ -1,5 +1,5 @@
# Bonsai - OpenBIM Blender Add-on # Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com> # Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2026 Michael Yoder <myoder@desertspringscivil.com>
# #
# This file is part of Bonsai. # This file is part of Bonsai.
# #
@@ -25,28 +25,28 @@ classes = (
# Property groups (must be registered before classes that use them) # Property groups (must be registered before classes that use them)
prop.AlignmentPI, prop.AlignmentPI,
prop.AlignmentDisplayRow, prop.AlignmentDisplayRow,
prop.SaikeiAlignmentProperties, prop.CivilAlignmentProperties,
# UILists # UILists
ui.SAIKEI_UL_alignment_pis, ui.CIVIL_UL_alignment_pis,
operator.ImportAlignmentCSV, operator.ImportAlignmentCSV,
# Operators - PI Management # Operators - PI Management
operator.SAIKEI_OT_add_pi, operator.CIVIL_OT_add_pi,
operator.SAIKEI_OT_remove_pi, operator.CIVIL_OT_remove_pi,
operator.SAIKEI_OT_pick_pi_from_viewport, operator.CIVIL_OT_pick_pi_from_viewport,
operator.SAIKEI_OT_recalculate_pis, operator.CIVIL_OT_recalculate_pis,
operator.SAIKEI_OT_clear_pis, operator.CIVIL_OT_clear_pis,
# Operators - Creation # Operators - Creation
operator.SAIKEI_OT_create_alignment_by_pi, operator.CIVIL_OT_create_alignment_by_pi,
operator.SAIKEI_OT_import_alignment_csv, operator.CIVIL_OT_import_alignment_csv,
# Operators - Stationing # Operators - Stationing
operator.SAIKEI_OT_add_stationing_referent, operator.CIVIL_OT_add_stationing_referent,
operator.SAIKEI_OT_name_segments, operator.CIVIL_OT_name_segments,
# Operators - PI Edit Mode # Operators - PI Edit Mode
operator.SAIKEI_OT_enter_pi_edit_mode, operator.CIVIL_OT_enter_pi_edit_mode,
# UI Panels (appear in Properties sidebar under CIVIL tab) # UI Panels (appear in Properties sidebar under CIVIL tab)
ui.SAIKEI_PT_alignment_creation, ui.CIVIL_PT_alignment_creation,
ui.SAIKEI_PT_pi_editor, ui.CIVIL_PT_pi_editor,
ui.SAIKEI_PT_alignment_stationing, ui.CIVIL_PT_alignment_stationing,
) )
@@ -55,10 +55,10 @@ def menu_func_import(self, context):
def register(): def register():
bpy.types.Scene.SaikeiAlignmentProperties = bpy.props.PointerProperty(type=prop.SaikeiAlignmentProperties) bpy.types.Scene.CivilAlignmentProperties = bpy.props.PointerProperty(type=prop.CivilAlignmentProperties)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister(): def unregister():
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
del bpy.types.Scene.SaikeiAlignmentProperties del bpy.types.Scene.CivilAlignmentProperties
@@ -1,5 +1,5 @@
# Bonsai - OpenBIM Blender Add-on # Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2022 Yassine Oualid <yassine@sigmadimensions.com> # Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>, 2022 Yassine Oualid <yassine@sigmadimensions.com>, 2026 Michael Yoder <myoder@desertspringscivil.com>
# #
# This file is part of Bonsai. # This file is part of Bonsai.
# #
@@ -190,7 +190,7 @@ def on_radius_changed(pi, context):
2. Rebuild of display_rows (Mid point becomes Curve segment) 2. Rebuild of display_rows (Mid point becomes Curve segment)
3. If an active alignment exists, regeneration of IFC entities 3. If an active alignment exists, regeneration of IFC entities
""" """
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
recalculate_pi_geometry(props) recalculate_pi_geometry(props)
# If there's an active alignment, trigger IFC regeneration # If there's an active alignment, trigger IFC regeneration
@@ -311,10 +311,10 @@ def rebuild_display_rows(props):
# ============================================================================= # =============================================================================
class SAIKEI_OT_add_pi(Operator): class CIVIL_OT_add_pi(Operator):
"""Add a new PI point to the list""" """Add a new PI point to the list"""
bl_idname = "saikei.add_pi" bl_idname = "civil.add_pi"
bl_label = "Add PI" bl_label = "Add PI"
bl_description = "Add a new PI (Point of Intersection) to the alignment" bl_description = "Add a new PI (Point of Intersection) to the alignment"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -324,7 +324,7 @@ class SAIKEI_OT_add_pi(Operator):
return poll_ifc4x3(cls, context) return poll_ifc4x3(cls, context)
def execute(self, context): def execute(self, context):
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
# Add new PI # Add new PI
pi = props.pis.add() pi = props.pis.add()
@@ -363,10 +363,10 @@ class SAIKEI_OT_add_pi(Operator):
return {"FINISHED"} return {"FINISHED"}
class SAIKEI_OT_remove_pi(Operator): class CIVIL_OT_remove_pi(Operator):
"""Remove the selected PI point""" """Remove the selected PI point"""
bl_idname = "saikei.remove_pi" bl_idname = "civil.remove_pi"
bl_label = "Remove PI" bl_label = "Remove PI"
bl_description = "Remove the selected PI from the alignment" bl_description = "Remove the selected PI from the alignment"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -375,7 +375,7 @@ class SAIKEI_OT_remove_pi(Operator):
def poll(cls, context): def poll(cls, context):
if not poll_ifc4x3(cls, context): if not poll_ifc4x3(cls, context):
return False return False
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
if len(props.pis) == 0: if len(props.pis) == 0:
cls.poll_message_set("No PIs to remove") cls.poll_message_set("No PIs to remove")
return False return False
@@ -389,7 +389,7 @@ class SAIKEI_OT_remove_pi(Operator):
return True return True
def execute(self, context): def execute(self, context):
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
# Get the PI index from the selected display row # Get the PI index from the selected display row
pi_index = -1 pi_index = -1
@@ -420,10 +420,10 @@ class SAIKEI_OT_remove_pi(Operator):
return {"FINISHED"} return {"FINISHED"}
class SAIKEI_OT_pick_pi_from_viewport(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): class CIVIL_OT_pick_pi_from_viewport(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
"""Add PI points by clicking in the 3D viewport using polyline tools""" """Add PI points by clicking in the 3D viewport using polyline tools"""
bl_idname = "saikei.pick_pi_from_viewport" bl_idname = "civil.pick_pi_from_viewport"
bl_label = "Pick PI from Viewport" bl_label = "Pick PI from Viewport"
bl_description = "Click in the viewport to add PI points with snapping and numeric input. RMB/Enter to finish, ESC to cancel." bl_description = "Click in the viewport to add PI points with snapping and numeric input. RMB/Enter to finish, ESC to cancel."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -554,7 +554,7 @@ class SAIKEI_OT_pick_pi_from_viewport(bpy.types.Operator, PolylineOperator, tool
Polyline points are in Blender coordinate space. This method converts Polyline points are in Blender coordinate space. This method converts
each point to IFC coordinate space before storing in props.pis. each point to IFC coordinate space before storing in props.pis.
""" """
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
polyline_props = tool.Model.get_polyline_props() polyline_props = tool.Model.get_polyline_props()
polyline_data = polyline_props.insertion_polyline polyline_data = polyline_props.insertion_polyline
if not polyline_data: if not polyline_data:
@@ -566,7 +566,7 @@ class SAIKEI_OT_pick_pi_from_viewport(bpy.types.Operator, PolylineOperator, tool
num_points = len(polyline_points) num_points = len(polyline_points)
for i, point in enumerate(polyline_points): for i, point in enumerate(polyline_points):
# Convert Blender space -> IFC easting/northing # Convert Blender space -> global easting/northing (props.pis stores global E/N coords)
ifc_coord = tool.Georeference.xyz2enh((point.x, point.y, 0.0)) ifc_coord = tool.Georeference.xyz2enh((point.x, point.y, 0.0))
pi = props.pis.add() pi = props.pis.add()
@@ -584,10 +584,10 @@ class SAIKEI_OT_pick_pi_from_viewport(bpy.types.Operator, PolylineOperator, tool
rebuild_display_rows(props) rebuild_display_rows(props)
class SAIKEI_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"""
bl_idname = "saikei.recalculate_pis" bl_idname = "civil.recalculate_pis"
bl_label = "Recalculate PIs" bl_label = "Recalculate PIs"
bl_description = "Recalculate geometry, update IFC segments, and refresh visualization" bl_description = "Recalculate geometry, update IFC segments, and refresh visualization"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -596,7 +596,7 @@ class SAIKEI_OT_recalculate_pis(Operator, tool.Ifc.Operator):
def poll(cls, context): def poll(cls, context):
if not poll_ifc4x3(cls, context): if not poll_ifc4x3(cls, context):
return False return False
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
if len(props.pis) < 2: if len(props.pis) < 2:
cls.poll_message_set("Need at least 2 PIs to recalculate") cls.poll_message_set("Need at least 2 PIs to recalculate")
return False return False
@@ -606,7 +606,7 @@ class SAIKEI_OT_recalculate_pis(Operator, tool.Ifc.Operator):
import ifcopenshell.api.alignment as align_api import ifcopenshell.api.alignment as align_api
ifc = tool.Ifc.get() ifc = tool.Ifc.get()
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
# Recalculate geometry in UI properties # Recalculate geometry in UI properties
recalculate_pi_geometry(props) recalculate_pi_geometry(props)
@@ -619,7 +619,7 @@ class SAIKEI_OT_recalculate_pis(Operator, tool.Ifc.Operator):
self.report({"ERROR"}, "Alignment has no horizontal layout") self.report({"ERROR"}, "Alignment has no horizontal layout")
return return
# Collect updated PI data # Convert global E/N coords (stored in props.pis) -> local IFC coords for the IfcOpenShell API
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]]
@@ -648,10 +648,10 @@ class SAIKEI_OT_recalculate_pis(Operator, tool.Ifc.Operator):
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}")
class SAIKEI_OT_clear_pis(Operator, tool.Ifc.Operator): class CIVIL_OT_clear_pis(Operator, tool.Ifc.Operator):
"""Clear all PI points and optionally remove visualization/IFC data""" """Clear all PI points and optionally remove visualization/IFC data"""
bl_idname = "saikei.clear_pis" bl_idname = "civil.clear_pis"
bl_label = "Clear All PIs" bl_label = "Clear All PIs"
bl_description = "Remove all PI points and clear segment visualization" bl_description = "Remove all PI points and clear segment visualization"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -660,7 +660,7 @@ class SAIKEI_OT_clear_pis(Operator, tool.Ifc.Operator):
def poll(cls, context): def poll(cls, context):
if not poll_ifc4x3(cls, context): if not poll_ifc4x3(cls, context):
return False return False
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
if len(props.pis) == 0: if len(props.pis) == 0:
cls.poll_message_set("No PIs to clear") cls.poll_message_set("No PIs to clear")
return False return False
@@ -671,7 +671,7 @@ class SAIKEI_OT_clear_pis(Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
ifc = tool.Ifc.get() ifc = tool.Ifc.get()
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
removed_objects = 0 removed_objects = 0
@@ -703,10 +703,10 @@ class SAIKEI_OT_clear_pis(Operator, tool.Ifc.Operator):
# ============================================================================= # =============================================================================
class SAIKEI_OT_create_alignment_by_pi(Operator, tool.Ifc.Operator): class CIVIL_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"""
bl_idname = "saikei.create_alignment_by_pi" bl_idname = "civil.create_alignment_by_pi"
bl_label = "Create by PI Method" bl_label = "Create by PI Method"
bl_description = "Create alignment using PI points and curve radii. If an active alignment exists with no segments, adds to it instead of creating new." bl_description = "Create alignment using PI points and curve radii. If an active alignment exists with no segments, adds to it instead of creating new."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -715,7 +715,7 @@ class SAIKEI_OT_create_alignment_by_pi(Operator, tool.Ifc.Operator):
def poll(cls, context): def poll(cls, context):
if not poll_ifc4x3(cls, context): if not poll_ifc4x3(cls, context):
return False return False
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
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
@@ -725,8 +725,9 @@ class SAIKEI_OT_create_alignment_by_pi(Operator, tool.Ifc.Operator):
return True return True
def _execute(self, context): def _execute(self, context):
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
# Convert global E/N coords (stored in props.pis) -> local IFC coords for the IfcOpenShell API
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]]
@@ -759,10 +760,10 @@ class SAIKEI_OT_create_alignment_by_pi(Operator, tool.Ifc.Operator):
) )
class SAIKEI_OT_import_alignment_csv(Operator, tool.Ifc.Operator, ImportHelper): class CIVIL_OT_import_alignment_csv(Operator, tool.Ifc.Operator, ImportHelper):
"""Import alignment from CSV file""" """Import alignment from CSV file"""
bl_idname = "saikei.import_alignment_csv" bl_idname = "civil.import_alignment_csv"
bl_label = "Import Alignment CSV" bl_label = "Import Alignment CSV"
bl_description = "Import alignment definition from a CSV file" bl_description = "Import alignment definition from a CSV file"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -776,7 +777,7 @@ class SAIKEI_OT_import_alignment_csv(Operator, tool.Ifc.Operator, ImportHelper):
def _execute(self, context): def _execute(self, context):
ifc = tool.Ifc.get() ifc = tool.Ifc.get()
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
alignment = ifcopenshell.api.alignment.create_from_csv(ifc, self.filepath) alignment = ifcopenshell.api.alignment.create_from_csv(ifc, self.filepath)
@@ -795,10 +796,10 @@ class SAIKEI_OT_import_alignment_csv(Operator, tool.Ifc.Operator, ImportHelper):
# ============================================================================= # =============================================================================
class SAIKEI_OT_add_stationing_referent(Operator, tool.Ifc.Operator): class CIVIL_OT_add_stationing_referent(Operator, tool.Ifc.Operator):
"""Add a stationing referent to the alignment""" """Add a stationing referent to the alignment"""
bl_idname = "saikei.add_stationing_referent" bl_idname = "civil.add_stationing_referent"
bl_label = "Add Stationing Referent" bl_label = "Add Stationing Referent"
bl_description = "Add an IfcReferent for stationing" bl_description = "Add an IfcReferent for stationing"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -819,7 +820,7 @@ class SAIKEI_OT_add_stationing_referent(Operator, tool.Ifc.Operator):
def poll(cls, context): def poll(cls, context):
if not poll_ifc4x3(cls, context): if not poll_ifc4x3(cls, context):
return False return False
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
if props.active_alignment_id == 0: if props.active_alignment_id == 0:
cls.poll_message_set("Select an alignment first") cls.poll_message_set("Select an alignment first")
return False return False
@@ -827,7 +828,7 @@ class SAIKEI_OT_add_stationing_referent(Operator, tool.Ifc.Operator):
def invoke(self, context, event): def invoke(self, context, event):
# Default station to start_station from props # Default station to start_station from props
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
self.station = props.start_station self.station = props.start_station
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
@@ -841,7 +842,7 @@ class SAIKEI_OT_add_stationing_referent(Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
ifc = tool.Ifc.get() ifc = tool.Ifc.get()
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
alignment = tool.Alignment.get_active_alignment() alignment = tool.Alignment.get_active_alignment()
if alignment is None: if alignment is None:
@@ -883,10 +884,10 @@ def format_station(station_value):
return f"{main}+{offset:05.2f}" return f"{main}+{offset:05.2f}"
class SAIKEI_OT_name_segments(Operator, tool.Ifc.Operator): class CIVIL_OT_name_segments(Operator, tool.Ifc.Operator):
"""Auto-name segments based on station values""" """Auto-name segments based on station values"""
bl_idname = "saikei.name_segments" bl_idname = "civil.name_segments"
bl_label = "Name Segments" bl_label = "Name Segments"
bl_description = "Automatically name segments with station-based labels" bl_description = "Automatically name segments with station-based labels"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -895,7 +896,7 @@ class SAIKEI_OT_name_segments(Operator, tool.Ifc.Operator):
def poll(cls, context): def poll(cls, context):
if not poll_ifc4x3(cls, context): if not poll_ifc4x3(cls, context):
return False return False
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
if props.active_alignment_id == 0: if props.active_alignment_id == 0:
cls.poll_message_set("Select an alignment first") cls.poll_message_set("Select an alignment first")
return False return False
@@ -903,7 +904,7 @@ class SAIKEI_OT_name_segments(Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
ifc = tool.Ifc.get() ifc = tool.Ifc.get()
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
alignment = tool.Alignment.get_active_alignment() alignment = tool.Alignment.get_active_alignment()
if alignment is None: if alignment is None:
@@ -920,10 +921,10 @@ class SAIKEI_OT_name_segments(Operator, tool.Ifc.Operator):
# ============================================================================= # =============================================================================
class SAIKEI_OT_enter_pi_edit_mode(Operator, tool.Ifc.Operator): class CIVIL_OT_enter_pi_edit_mode(Operator, tool.Ifc.Operator):
"""Enter PI editing mode - move PIs with G key, press Enter to apply or Escape to cancel""" """Enter PI editing mode - move PIs with G key, press Enter to apply or Escape to cancel"""
bl_idname = "saikei.enter_pi_edit_mode" bl_idname = "civil.enter_pi_edit_mode"
bl_label = "Edit PIs" bl_label = "Edit PIs"
bl_description = "Enter PI edit mode. Move PI points with G key. Press Enter to apply changes, Escape to cancel." bl_description = "Enter PI edit mode. Move PI points with G key. Press Enter to apply changes, Escape to cancel."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@@ -938,7 +939,7 @@ class SAIKEI_OT_enter_pi_edit_mode(Operator, tool.Ifc.Operator):
def poll(cls, context): def poll(cls, context):
if not poll_ifc4x3(cls, context): if not poll_ifc4x3(cls, context):
return False return False
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
if props.is_pi_edit_mode: if props.is_pi_edit_mode:
cls.poll_message_set("Already in PI edit mode") cls.poll_message_set("Already in PI edit mode")
return False return False
@@ -956,7 +957,7 @@ class SAIKEI_OT_enter_pi_edit_mode(Operator, tool.Ifc.Operator):
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE") return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
def _invoke(self, context, event): def _invoke(self, context, event):
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
self._alignment_id = props.active_alignment_id self._alignment_id = props.active_alignment_id
# Enter edit mode via core layer (validates and creates empties) # Enter edit mode via core layer (validates and creates empties)
@@ -999,7 +1000,7 @@ class SAIKEI_OT_enter_pi_edit_mode(Operator, tool.Ifc.Operator):
return IfcStore.execute_ifc_operator(self, context, event, method="MODAL") return IfcStore.execute_ifc_operator(self, context, event, method="MODAL")
def _modal(self, context, event): def _modal(self, context, event):
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
# Safety: check if empties still exist (handles undo edge case) # Safety: check if empties still exist (handles undo edge case)
if not self._empties_still_exist(): if not self._empties_still_exist():
@@ -1040,7 +1041,7 @@ class SAIKEI_OT_enter_pi_edit_mode(Operator, tool.Ifc.Operator):
def _cleanup_and_finish(self, context, apply: bool): def _cleanup_and_finish(self, context, apply: bool):
"""Exit edit mode, optionally applying changes.""" """Exit edit mode, optionally applying changes."""
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
try: try:
if apply: if apply:
@@ -50,9 +50,11 @@ class AlignmentPI(PropertyGroup):
- Interior PIs: Points where tangents intersect, optionally with curves - Interior PIs: Points where tangents intersect, optionally with curves
""" """
# Coordinates # Coordinates stored as global easting/northing (map coordinates).
e: StringProperty(name="E", description="Easting", default="0.0") # Coordinate flow: Blender coords -> xyz2enh() -> global E/N (stored here)
n: StringProperty(name="N", description="Northing", default="0.0") # global E/N -> enh2xyz(to_blender=False) -> local IFC coords (for IfcOpenShell API)
e: StringProperty(name="E", description="Easting (global map coordinates)", default="0.0")
n: StringProperty(name="N", description="Northing (global map coordinates)", default="0.0")
# PI Type # PI Type
pi_type: EnumProperty( pi_type: EnumProperty(
@@ -146,7 +148,7 @@ class AlignmentDisplayRow(PropertyGroup):
arc_length: FloatProperty(name="Arc Length", default=0.0, precision=2, unit="LENGTH") arc_length: FloatProperty(name="Arc Length", default=0.0, precision=2, unit="LENGTH")
class SaikeiAlignmentProperties(PropertyGroup): class CivilAlignmentProperties(PropertyGroup):
"""Properties for the alignment module""" """Properties for the alignment module"""
# Active alignment selection # Active alignment selection
+20 -20
View File
@@ -38,7 +38,7 @@ def is_ifc4x3():
# ============================================================================= # =============================================================================
class SAIKEI_UL_alignment_pis(UIList): class CIVIL_UL_alignment_pis(UIList):
"""UIList for displaying interleaved points and segments (Civil 3D style) """UIList for displaying interleaved points and segments (Civil 3D style)
Row types: Row types:
@@ -123,11 +123,11 @@ class SAIKEI_UL_alignment_pis(UIList):
# ============================================================================= # =============================================================================
class SAIKEI_PT_alignment_creation(Panel): class CIVIL_PT_alignment_creation(Panel):
"""Sub-panel for alignment creation tools""" """Sub-panel for alignment creation tools"""
bl_label = "Creation" bl_label = "Creation"
bl_idname = "SAIKEI_PT_alignment_creation" bl_idname = "CIVIL_PT_alignment_creation"
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
bl_context = "scene" bl_context = "scene"
@@ -140,7 +140,7 @@ class SAIKEI_PT_alignment_creation(Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
# New alignment properties # New alignment properties
box = layout.box() box = layout.box()
@@ -150,7 +150,7 @@ 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_by_pi", icon="CURVE_DATA") col.operator("civil.create_alignment_by_pi", icon="CURVE_DATA")
# ============================================================================= # =============================================================================
@@ -158,11 +158,11 @@ class SAIKEI_PT_alignment_creation(Panel):
# ============================================================================= # =============================================================================
class SAIKEI_PT_pi_editor(Panel): class CIVIL_PT_pi_editor(Panel):
"""Sub-panel for PI point table editor (Civil 3D style grid view)""" """Sub-panel for PI point table editor (Civil 3D style grid view)"""
bl_label = "PI Editor" bl_label = "PI Editor"
bl_idname = "SAIKEI_PT_pi_editor" bl_idname = "CIVIL_PT_pi_editor"
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
bl_context = "scene" bl_context = "scene"
@@ -175,7 +175,7 @@ class SAIKEI_PT_pi_editor(Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
# PI Edit Mode indicator # PI Edit Mode indicator
if props.is_pi_edit_mode: if props.is_pi_edit_mode:
@@ -193,7 +193,7 @@ class SAIKEI_PT_pi_editor(Panel):
if props.active_alignment_id != 0: if props.active_alignment_id != 0:
box = layout.box() box = layout.box()
box.label(text="Edit Alignment:", icon="EDITMODE_HLT") box.label(text="Edit Alignment:", icon="EDITMODE_HLT")
box.operator("saikei.enter_pi_edit_mode", icon="PIVOT_CURSOR", text="Edit PIs (G key)") box.operator("civil.enter_pi_edit_mode", icon="PIVOT_CURSOR", text="Edit PIs (G key)")
layout.separator() layout.separator()
# Header row with column labels # Header row with column labels
@@ -208,7 +208,7 @@ class SAIKEI_PT_pi_editor(Panel):
# Combined point/segment list (interleaved view) # Combined point/segment list (interleaved view)
row = layout.row() row = layout.row()
row.template_list( row.template_list(
"SAIKEI_UL_alignment_pis", "CIVIL_UL_alignment_pis",
"", "",
props, props,
"display_rows", "display_rows",
@@ -219,16 +219,16 @@ class SAIKEI_PT_pi_editor(Panel):
# Side buttons for list management # Side buttons for list management
col = row.column(align=True) col = row.column(align=True)
col.operator("saikei.add_pi", icon="ADD", text="") col.operator("civil.add_pi", icon="ADD", text="")
col.operator("saikei.remove_pi", icon="REMOVE", text="") col.operator("civil.remove_pi", icon="REMOVE", text="")
col.separator() col.separator()
col.operator("saikei.pick_pi_from_viewport", icon="EYEDROPPER", text="") col.operator("civil.pick_pi_from_viewport", icon="EYEDROPPER", text="")
# Bottom actions # Bottom actions
layout.separator() layout.separator()
row = layout.row(align=True) row = layout.row(align=True)
row.operator("saikei.recalculate_pis", icon="FILE_REFRESH", text="Recalculate") row.operator("civil.recalculate_pis", icon="FILE_REFRESH", text="Recalculate")
row.operator("saikei.clear_pis", icon="TRASH", text="Clear All") row.operator("civil.clear_pis", icon="TRASH", text="Clear All")
# ============================================================================= # =============================================================================
@@ -236,11 +236,11 @@ class SAIKEI_PT_pi_editor(Panel):
# ============================================================================= # =============================================================================
class SAIKEI_PT_alignment_stationing(Panel): class CIVIL_PT_alignment_stationing(Panel):
"""Sub-panel for stationing and referents""" """Sub-panel for stationing and referents"""
bl_label = "Stationing" bl_label = "Stationing"
bl_idname = "SAIKEI_PT_alignment_stationing" bl_idname = "CIVIL_PT_alignment_stationing"
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
bl_context = "scene" bl_context = "scene"
@@ -253,7 +253,7 @@ class SAIKEI_PT_alignment_stationing(Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
props = context.scene.SaikeiAlignmentProperties props = context.scene.CivilAlignmentProperties
# Station display options # Station display options
box = layout.box() box = layout.box()
@@ -265,5 +265,5 @@ class SAIKEI_PT_alignment_stationing(Panel):
# Stationing operators # Stationing operators
col = layout.column(align=True) col = layout.column(align=True)
col.operator("saikei.add_stationing_referent", icon="EMPTY_AXIS") col.operator("civil.add_stationing_referent", icon="EMPTY_AXIS")
col.operator("saikei.name_segments", icon="FONT_DATA") col.operator("civil.name_segments", icon="FONT_DATA")
+15 -25
View File
@@ -273,17 +273,10 @@ class Alignment:
return None return None
return (point[0] / unit_scale, point[1] / unit_scale) return (point[0] / unit_scale, point[1] / unit_scale)
try: result = align_api.segment_vertices(ifc_file, segment)
start, end, ti, ni = align_api.segment_vertices(ifc_file, segment) if result is None:
except Exception: return None
try: start, end, ti, ni = result
mapped = align_api.get_mapped_segments(segment)
curve_segment = mapped[0]
if curve_segment is None:
return None
start, end, ti, ni = align_api.segment_vertices(ifc_file, curve_segment)
except Exception:
return None
return (convert(start), convert(end), convert(ti), convert(ni)) return (convert(start), convert(end), convert(ti), convert(ni))
@@ -658,7 +651,7 @@ class Alignment:
the Blender UI properties. the Blender UI properties.
Args: Args:
props: The SaikeiAlignmentProperties PropertyGroup props: The CivilAlignmentProperties PropertyGroup
geometry_result: PIGeometryResult from core.alignment geometry_result: PIGeometryResult from core.alignment
""" """
pis = props.pis pis = props.pis
@@ -780,12 +773,9 @@ class Alignment:
Returns: Returns:
The parent IfcAlignment if found, None otherwise The parent IfcAlignment if found, None otherwise
""" """
try: import ifcopenshell.api.alignment as align_api
import ifcopenshell.api.alignment as align_api
return align_api.get_alignment(layout) return align_api.get_alignment(layout)
except Exception:
return None
@classmethod @classmethod
def safe_layout_horizontal_by_pi_method( def safe_layout_horizontal_by_pi_method(
@@ -970,11 +960,11 @@ class Alignment:
empty.location = blender_pos empty.location = blender_pos
# Tag with custom properties for identification # Tag with custom properties for identification
empty["saikei_is_pi_empty"] = True empty["civil_is_pi_empty"] = True
empty["saikei_pi_index"] = i empty["civil_pi_index"] = i
empty["saikei_pi_radius"] = pi["radius"] empty["civil_pi_radius"] = pi["radius"]
empty["saikei_alignment_id"] = alignment_id empty["civil_alignment_id"] = alignment_id
empty["saikei_pi_type"] = pi["pi_type"] empty["civil_pi_type"] = pi["pi_type"]
# Parent to alignment object # Parent to alignment object
empty.parent = alignment_obj empty.parent = alignment_obj
@@ -1002,11 +992,11 @@ class Alignment:
empties = [] empties = []
for obj in bpy.data.objects: for obj in bpy.data.objects:
if obj.get("saikei_is_pi_empty") and obj.get("saikei_alignment_id") == alignment_id: if obj.get("civil_is_pi_empty") and obj.get("civil_alignment_id") == alignment_id:
empties.append(obj) empties.append(obj)
# Sort by PI index # Sort by PI index
empties.sort(key=lambda e: e.get("saikei_pi_index", 0)) empties.sort(key=lambda e: e.get("civil_pi_index", 0))
return empties return empties
@@ -1059,7 +1049,7 @@ class Alignment:
# Collect radii for interior PIs only (not first or last) # Collect radii for interior PIs only (not first or last)
if 0 < i < len(empties) - 1: if 0 < i < len(empties) - 1:
radius = empty.get("saikei_pi_radius", 0.0) radius = empty.get("civil_pi_radius", 0.0)
radii.append(radius) radii.append(radius)
return (hpoints, radii) return (hpoints, radii)