Gate slab disconnect gizmos behind parametric edit lifecycle

Wires slabs into the parametric edit framework (tool.Parametric
.EDIT_TYPES) so the wall-slab disconnect UI gets ESC handling, red
cancel icon, mutual exclusion with other parametric edits, and
per-feature gizmo prefs — all from BaseParametricGizmoGroup — without
duplicating the lifecycle.

Adds:
- ParametricObject("slab") registry entry + tool.Parametric.is_slab
  predicate (any IfcSlab).
- BIMSlabProperties with is_editing flag; PointerProperty wired by
  the framework's register_object_properties.
- bim.enable_editing_slab / bim.finish_editing_slab /
  bim.cancel_editing_slab operators on tool.Ifc.Operator so they
  flow through tool.Parametric.run_bim_op cleanly. No IFC mutation
  — slab edit is a pure UI gate; finish and cancel share the body.
- tool.Model.get_slab_props accessor.
- GizmoSlabEdition inheriting BaseParametricGizmoGroup with the
  pen / validate / cancel triad. is_element_type narrows to
  IfcSlab with at least one wall clipped to its underside.

The disconnect-icon group GizmoSlabUnjoinWalls polls behind
_slab_connection_gizmo_poll_gate(require_editing=True), which now
reads is_editing through tool.Model.get_slab_props.

Drops the standalone GizmoSlabConnectionAccess + the
setup_pen_cancel_icons helper added earlier in this branch — both
superseded by the framework integration.

Also folds in the wall + multi-slab gizmo polish requested live:
- Wall side: stack the per-slab unjoin icons vertically (up to 5)
  so multi-slab connections each get a distinct clickable icon;
  hover-highlight reveals which slab will disconnect.
- GizmoPairDisconnect activates when 2 elements with an
  IfcRelConnectsElements(TOP) rel are selected, with the icon at
  the wall-slab connection world anchor.
- Wall-slab anchor moved from slab clip Z to wall top +
  WALL_SLAB_CONNECTION_Z_CLEARANCE so the disconnect icon perches
  above the extend-vertical / slope gizmo instead of overlapping.
- Shared _resolve_active_partner_pair helper for 2-selection
  gizmos; _slab_connection_gizmo_poll_gate added to
  _REQUIRED_CALLEES + GizmoSlabEdition added to the AST
  forward-compat allowlist.

Build note: wall.py's DisconnectElements._perform imports
bonsai.core.connection.disconnect_rel — that core module is being
added in a parallel-session commit. Until that lands the addon
import will fail.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-12 11:05:02 +02:00
parent a3593ed58b
commit c7d5d6c498
9 changed files with 452 additions and 74 deletions
@@ -26,6 +26,8 @@ Allow-list (gizmos intentionally outside the rule):
- ``GizmoWallEdition`` — single-object parametric edit gizmo. Its base
parametric poll already filters array children.
- ``GizmoSlabEdition`` — same as ``GizmoWallEdition`` (inherits
``BaseParametricGizmoGroup`` whose poll filters array children).
- ``GizmoWallFilletPreview`` — the preview-owner whose poll must fire
WHILE its own preview is active; routing it through the topology gate
would self-block it.
@@ -47,9 +49,11 @@ pytestmark = pytest.mark.model
# Wall gizmo groups intentionally outside the rule. Add a new entry only
# with the in-code reasoning above.
_ALLOWLIST = frozenset({"GizmoWallEdition", "GizmoWallFilletPreview"})
_ALLOWLIST = frozenset({"GizmoSlabEdition", "GizmoWallEdition", "GizmoWallFilletPreview"})
_REQUIRED_CALLEES = frozenset({"_wall_topology_gizmo_poll_gate", "any_selected_is_array_child"})
_REQUIRED_CALLEES = frozenset(
{"_wall_topology_gizmo_poll_gate", "_slab_connection_gizmo_poll_gate", "any_selected_is_array_child"}
)
def _wall_module_source():
@@ -178,28 +178,30 @@ def test_find_wall_slab_rel_returns_none_when_unconnected():
# ---------------------------------------------------------------------------
def test_wall_slab_connection_location_lifts_axis_mid_to_slab_underside():
"""The icon sits at the wall's axis midpoint X/Y lifted to the slab's
underside Z so it reads as a marker on the slab cut line."""
def test_wall_slab_connection_location_perches_above_wall_top():
"""Icon X/Y comes from the wall axis midpoint; Z from the wall's mesh
bbox top in world space plus WALL_SLAB_CONNECTION_Z_CLEARANCE so the
icon sits above the extend-vertical / slope gizmo at the wall top."""
wall_obj = Mock()
slab_obj = Mock()
slab_obj.matrix_world = Matrix.Translation(Vector((0.0, 0.0, 3.0)))
slab_obj.bound_box = [
(-1.0, -1.0, 0.0),
(1.0, -1.0, 0.0),
(-1.0, 1.0, 0.0),
(1.0, 1.0, 0.0),
(-1.0, -1.0, 0.2),
(1.0, -1.0, 0.2),
(-1.0, 1.0, 0.2),
(1.0, 1.0, 0.2),
wall_obj.matrix_world = Matrix.Identity(4)
wall_obj.bound_box = [
(-0.1, -0.1, 0.0),
(0.1, -0.1, 0.0),
(-0.1, 0.1, 0.0),
(0.1, 0.1, 0.0),
(-0.1, -0.1, 3.0),
(0.1, -0.1, 3.0),
(-0.1, 0.1, 3.0),
(0.1, 0.1, 3.0),
]
slab_obj = Mock()
ref_line = (Vector((1.0, 0.0, 0.0)), Vector((3.0, 0.0, 0.0)))
with patch.object(tool.Wall, "get_world_reference_line", return_value=ref_line):
loc = tool.Wall.wall_slab_connection_location_world(wall_obj, slab_obj)
assert loc == Vector((2.0, 0.0, 3.0))
expected_z = 3.0 + tool.Wall.WALL_SLAB_CONNECTION_Z_CLEARANCE
assert loc == Vector((2.0, 0.0, expected_z))
def test_wall_slab_connection_location_returns_none_for_axisless_wall():