From d7b5ac1453d3115ac0c78fd092eb7e77fe0652a0 Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Thu, 28 May 2026 14:06:11 +0200 Subject: [PATCH] Add wall draft-resync helper + wire 6 mutation operators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/bonsai/bonsai/bim/module/model/wall.py | 36 +++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 104e521082..034def1b1e 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -22,6 +22,7 @@ import copy import math +from collections.abc import Iterable from math import atan2, cos, degrees, pi, sin 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 +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): bl_idname = "bim.unjoin_walls" bl_label = "Unjoin Walls" @@ -183,6 +208,7 @@ class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): _commit_pending_wall_edits_for_selection(context) 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): @@ -212,6 +238,7 @@ class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator): walls.append(obj) if slab and walls: core.extend_wall_to_slab(tool.Ifc, tool.Geometry, tool.Model, slab, walls) + _resync_walls_after_mutation(walls) else: 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(target_element, target_obj) + _resync_walls_after_mutation([target_obj, *objs]) else: 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() for obj in selected_objs: DumbWallJoiner().split(obj, context.scene.cursor.location) + _resync_walls_after_mutation(selected_objs) return {"FINISHED"} @@ -483,7 +512,11 @@ class MergeWall(bpy.types.Operator, tool.Ifc.Operator): active_obj = context.active_object assert active_obj 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"} @@ -2680,4 +2713,5 @@ class JoinWallsIntersection(bpy.types.Operator, tool.Ifc.Operator): except core.RequireTwoWallsError as e: self.report({"ERROR"}, str(e)) return {"CANCELLED"} + _resync_walls_after_mutation(tool.Blender.get_selected_objects()) return {"FINISHED"}