From 2cbb48e2ef2c20fdf487b9e5b11e02d2da502359 Mon Sep 17 00:00:00 2001 From: Richard Brice <37087370+RickBrice@users.noreply.github.com> Date: Sun, 13 Sep 2026 11:26:26 -0700 Subject: [PATCH] Improves interactive editing of horizontal alignment --- .../bonsai/bim/module/alignment/prop.py | 19 +++++++++++++++++-- src/bonsai/bonsai/tool/alignment.py | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/alignment/prop.py b/src/bonsai/bonsai/bim/module/alignment/prop.py index 7e6d3d37a1..a3691a9da3 100644 --- a/src/bonsai/bonsai/bim/module/alignment/prop.py +++ b/src/bonsai/bonsai/bim/module/alignment/prop.py @@ -39,14 +39,28 @@ def _on_vertical_visibility_update(self, context): VerticalProfileDecorator.tag_redraw() +# Blender requires a dynamic EnumProperty callback to keep a reference to the +# items it returns — the strings are read by the C/RNA layer after the Python +# call returns, and if the list is only local to the function it can be +# garbage-collected before that happens. Without this cache, the dropdown can +# resolve its stored index against a stale/freed items list — e.g. right +# after adding and drawing a new alignment — leaving the dropdown (and the +# segment table, which reads its value) showing a different alignment than +# the one actually active in the viewport. See bpy.props.EnumProperty docs. +_alignment_enum_items_cache: list[tuple[str, str, str]] = [] + + def _alignment_enum_items(self, context): """Dynamic items: all top-level IfcAlignment entities in the current file.""" import bonsai.tool as tool + global _alignment_enum_items_cache + items = [("0", "— select alignment —", "")] ifc_file = tool.Ifc.get() if not ifc_file: - return items + _alignment_enum_items_cache = items + return _alignment_enum_items_cache try: for a in ifc_file.by_type("IfcAlignment"): # Skip child alignments (used in multi-vertical template) @@ -59,7 +73,8 @@ def _alignment_enum_items(self, context): items.append((str(a.id()), label, "")) except Exception: pass - return items + _alignment_enum_items_cache = items + return _alignment_enum_items_cache def _on_active_alignment_update(self, context): diff --git a/src/bonsai/bonsai/tool/alignment.py b/src/bonsai/bonsai/tool/alignment.py index 6e8febbc5f..0932e3aa7f 100644 --- a/src/bonsai/bonsai/tool/alignment.py +++ b/src/bonsai/bonsai/tool/alignment.py @@ -1057,6 +1057,23 @@ class Alignment: @classmethod def get_active_alignment(cls) -> ifcopenshell.entity_instance | None: if obj := tool.Blender.get_active_object(): + # PI curve marker empties (see PICurveMarkerProperties) are never + # IFC-linked — they're transient viewport helpers — so they need + # their own lookup via the alignment_id they were tagged with, + # rather than falling through to tool.Ifc.get_entity() below. + # Without this, selecting a marker to press Apply Curve leaves + # this returning None, and the Alignments tab's dropdown/segment + # table (which sync from this) revert to "select an alignment". + marker = obj.bonsai_pi_curve_marker + if marker.is_pi_marker: + ifc_file = tool.Ifc.get() + if not ifc_file: + return None + try: + alignment = ifc_file.by_id(marker.alignment_id) + except RuntimeError: + return None + return cls._get_top_level_alignment(alignment) if alignment.is_a("IfcAlignment") else None element = tool.Ifc.get_entity(obj) if not element: return None