Add wall draft-resync helper + wire 6 mutation operators

After a one-shot wall IFC mutation (unjoin / split / merge / extend /
join-at-corner …) the always-visible gizmos on the OTHER side of the
join can be left reading stale ``BIMWallProperties`` — the IFC
geometry moved but the draft props that drive the gizmo handles still
point at the pre-mutation numbers, so a subsequent edit-mode enter
shows the wall at its old length / position.

* New ``_maybe_resync_wall_props_from_ifc(obj)``: re-primes a single
  wall's draft props from current IFC, with guards for non-walls,
  non-parametric walls, and walls in an active draft session (the
  draft is then the source of truth, not IFC). Must run from an
  operator ``_execute`` — ID writes from gizmo refresh raise.
* New ``_resync_walls_after_mutation(objs)``: iterates the above
  across a selection.
* Six existing mutation operators gain a resync call after their
  ``core.*`` / ``DumbWallJoiner`` mutation completes:
  UnjoinWalls, ExtendWallsToUnderside, ExtendWallsToWall, SplitWall,
  MergeWall, JoinWallsIntersection. MergeWall resyncs only the
  surviving wall — the active wall is the deletion target.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-05-28 14:06:11 +02:00
parent 1961cd905e
commit d7b5ac1453
+35 -1
View File
@@ -22,6 +22,7 @@
import copy import copy
import math import math
from collections.abc import Iterable
from math import atan2, cos, degrees, pi, sin from math import atan2, cos, degrees, pi, sin
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, Union, get_args from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, Union, get_args
@@ -167,6 +168,30 @@ def _read_wall_state_into_props(obj: bpy.types.Object, props: "BIMWallProperties
props.snap_offset_baseline = props.desired_offset_baseline props.snap_offset_baseline = props.desired_offset_baseline
def _maybe_resync_wall_props_from_ifc(obj: "bpy.types.Object | None") -> None:
"""Re-prime ``BIMWallProperties`` from current IFC after an IFC mutation, so
non-edit-mode gizmos read post-mutation coordinates. Must be called from an
operator's ``_execute`` — ID writes from ``GizmoGroup.refresh`` raise
``AttributeError: Writing to ID classes in this context is not allowed``.
No-op during a draft session; the draft is then the source of truth."""
if obj is None:
return
if _validate_wall_for_parametric_edit(obj) is not None:
return
props = tool.Model.get_wall_props(obj)
if props.is_editing:
return
_read_wall_state_into_props(obj, props)
def _resync_walls_after_mutation(objs: Iterable["bpy.types.Object | None"]) -> None:
"""Re-prime each wall's draft props after a one-shot IFC mutation. Safe to
call from operator ``_execute``: ID writes are allowed there, unlike gizmo
refresh."""
for obj in objs:
_maybe_resync_wall_props_from_ifc(obj)
class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator): class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unjoin_walls" bl_idname = "bim.unjoin_walls"
bl_label = "Unjoin Walls" bl_label = "Unjoin Walls"
@@ -183,6 +208,7 @@ class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context): def _execute(self, context):
_commit_pending_wall_edits_for_selection(context) _commit_pending_wall_edits_for_selection(context)
core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model) core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model)
_resync_walls_after_mutation(tool.Blender.get_selected_objects())
class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator): class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator):
@@ -212,6 +238,7 @@ class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator):
walls.append(obj) walls.append(obj)
if slab and walls: if slab and walls:
core.extend_wall_to_slab(tool.Ifc, tool.Geometry, tool.Model, slab, walls) core.extend_wall_to_slab(tool.Ifc, tool.Geometry, tool.Model, slab, walls)
_resync_walls_after_mutation(walls)
else: else:
self.report({"ERROR"}, "Please select at least one LAYER2 element and an active element") self.report({"ERROR"}, "Please select at least one LAYER2 element and an active element")
@@ -253,6 +280,7 @@ class ExtendWallsToWall(bpy.types.Operator, tool.Ifc.Operator):
) )
tool.Model.recreate_wall(element, obj) tool.Model.recreate_wall(element, obj)
tool.Model.recreate_wall(target_element, target_obj) tool.Model.recreate_wall(target_element, target_obj)
_resync_walls_after_mutation([target_obj, *objs])
else: else:
self.report({"ERROR"}, "Please select at least one LAYER2 element and one active LAYER2 element") self.report({"ERROR"}, "Please select at least one LAYER2 element and one active LAYER2 element")
@@ -455,6 +483,7 @@ class SplitWall(bpy.types.Operator, tool.Ifc.Operator):
selected_objs = tool.Model.get_selected_mesh_objects() selected_objs = tool.Model.get_selected_mesh_objects()
for obj in selected_objs: for obj in selected_objs:
DumbWallJoiner().split(obj, context.scene.cursor.location) DumbWallJoiner().split(obj, context.scene.cursor.location)
_resync_walls_after_mutation(selected_objs)
return {"FINISHED"} return {"FINISHED"}
@@ -483,7 +512,11 @@ class MergeWall(bpy.types.Operator, tool.Ifc.Operator):
active_obj = context.active_object active_obj = context.active_object
assert active_obj assert active_obj
selected_objs = tool.Model.get_selected_mesh_objects() selected_objs = tool.Model.get_selected_mesh_objects()
DumbWallJoiner().merge(next(o for o in selected_objs if o != active_obj), active_obj) # The merge deletes the second argument when the walls are collinear;
# only the first survives, so the resync targets the non-active wall.
surviving_obj = next(o for o in selected_objs if o != active_obj)
DumbWallJoiner().merge(surviving_obj, active_obj)
_maybe_resync_wall_props_from_ifc(surviving_obj)
return {"FINISHED"} return {"FINISHED"}
@@ -2680,4 +2713,5 @@ class JoinWallsIntersection(bpy.types.Operator, tool.Ifc.Operator):
except core.RequireTwoWallsError as e: except core.RequireTwoWallsError as e:
self.report({"ERROR"}, str(e)) self.report({"ERROR"}, str(e))
return {"CANCELLED"} return {"CANCELLED"}
_resync_walls_after_mutation(tool.Blender.get_selected_objects())
return {"FINISHED"} return {"FINISHED"}