diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index b7f287c0d6..183d9d32e6 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -391,9 +391,9 @@ class DisconnectElements(_CommitWallDraftsFirstMixin, bpy.types.Operator, tool.I # without rebuilding the source walls' miter cuts. Deleting the corner # wall is the supported teardown, which cascades back to the source # walls via the connection-cleanup handler. - either_is_fillet = tool.Parametric.is_fillet_corner_wall( - elem_a - ) or tool.Parametric.is_fillet_corner_wall(elem_b) + either_is_fillet = tool.Parametric.is_fillet_corner_wall(elem_a) or tool.Parametric.is_fillet_corner_wall( + elem_b + ) if either_is_fillet and any(k == "path" for _, k in rels): self.report( {"INFO"}, diff --git a/src/bonsai/test/bim/module/model/test_disconnect_elements.py b/src/bonsai/test/bim/module/model/test_disconnect_elements.py index 4aeb7ffbad..da668e604a 100644 --- a/src/bonsai/test/bim/module/model/test_disconnect_elements.py +++ b/src/bonsai/test/bim/module/model/test_disconnect_elements.py @@ -231,7 +231,9 @@ def test_disconnect_dispatches_one_call_per_rel(): return_value=[(rel1, "path"), (rel2, "element-top")], ), patch("bonsai.bim.module.model.wall.bonsai.core.connection.disconnect_rel") as dispatch, patch( "bonsai.bim.module.model.wall.tool.Ifc.get_object", return_value=Mock() - ), patch("bonsai.bim.module.model.wall._resync_walls_after_mutation"), patch( + ), patch( + "bonsai.bim.module.model.wall._resync_walls_after_mutation" + ), patch( "bonsai.bim.module.model.wall.tool.Parametric.is_fillet_corner_wall", return_value=False ): DisconnectElements._perform(op, context=MagicMock()) @@ -239,9 +241,7 @@ def test_disconnect_dispatches_one_call_per_rel(): assert dispatch.call_count == 2 # Both rels dispatch with elem=elem_a, partner=elem_b regardless of orientation # — orient_element_top inside disconnect_rel recovers the wall/slab roles. - for call, expected_rel, expected_kind in zip( - dispatch.call_args_list, [rel1, rel2], ["path", "element-top"] - ): + for call, expected_rel, expected_kind in zip(dispatch.call_args_list, [rel1, rel2], ["path", "element-top"]): kw = call.kwargs assert kw["rel"] is expected_rel assert kw["kind"] == expected_kind @@ -271,7 +271,9 @@ def test_disconnect_resyncs_path_objs_once_for_path_kind(): ), patch( "bonsai.bim.module.model.wall.tool.Ifc.get_object", side_effect=lambda e: {elem_a: obj_a, elem_b: obj_b}[e], - ), patch("bonsai.bim.module.model.wall.bonsai.core.connection.disconnect_rel"), patch( + ), patch( + "bonsai.bim.module.model.wall.bonsai.core.connection.disconnect_rel" + ), patch( "bonsai.bim.module.model.wall._resync_walls_after_mutation" ) as resync, patch( "bonsai.bim.module.model.wall.tool.Parametric.is_fillet_corner_wall", return_value=False @@ -298,7 +300,9 @@ def test_disconnect_skips_resync_for_non_path_kind(): "bonsai.bim.module.model.wall.tool.Connection.find_rels", return_value=[(rel, "element-top")] ), patch("bonsai.bim.module.model.wall.tool.Ifc.get_object", return_value=Mock()), patch( "bonsai.bim.module.model.wall.bonsai.core.connection.disconnect_rel" - ), patch("bonsai.bim.module.model.wall._resync_walls_after_mutation") as resync, patch( + ), patch( + "bonsai.bim.module.model.wall._resync_walls_after_mutation" + ) as resync, patch( "bonsai.bim.module.model.wall.tool.Parametric.is_fillet_corner_wall", return_value=False ): DisconnectElements._perform(op, context=MagicMock()) @@ -328,7 +332,9 @@ def test_disconnect_gizmo_direction_symmetry(): "bonsai.bim.module.model.wall.tool.Connection.find_rels", return_value=[(rel, "element-top")] ), patch("bonsai.bim.module.model.wall.tool.Ifc.get_object", return_value=Mock()), patch( "bonsai.bim.module.model.wall.bonsai.core.connection.disconnect_rel" - ) as dispatch, patch("bonsai.bim.module.model.wall._resync_walls_after_mutation"), patch( + ) as dispatch, patch( + "bonsai.bim.module.model.wall._resync_walls_after_mutation" + ), patch( "bonsai.bim.module.model.wall.tool.Parametric.is_fillet_corner_wall", return_value=False ): DisconnectElements._perform(op, context=MagicMock()) @@ -409,7 +415,9 @@ def test_disconnect_refuses_path_kind_when_either_side_is_fillet(): ), patch( "bonsai.bim.module.model.wall.tool.Parametric.is_fillet_corner_wall", side_effect=lambda e: e is fillet, - ), patch("bonsai.bim.module.model.wall.bonsai.core.connection.disconnect_rel") as dispatch: + ), patch( + "bonsai.bim.module.model.wall.bonsai.core.connection.disconnect_rel" + ) as dispatch: DisconnectElements._perform(op, context=MagicMock()) dispatch.assert_not_called() @@ -437,9 +445,13 @@ def test_disconnect_allows_slab_kind_even_when_wall_is_fillet(): ), patch( "bonsai.bim.module.model.wall.tool.Parametric.is_fillet_corner_wall", side_effect=lambda e: e is fillet, - ), patch("bonsai.bim.module.model.wall.tool.Ifc.get_object", return_value=Mock()), patch( + ), patch( + "bonsai.bim.module.model.wall.tool.Ifc.get_object", return_value=Mock() + ), patch( "bonsai.bim.module.model.wall.bonsai.core.connection.disconnect_rel" - ) as dispatch, patch("bonsai.bim.module.model.wall._resync_walls_after_mutation"): + ) as dispatch, patch( + "bonsai.bim.module.model.wall._resync_walls_after_mutation" + ): DisconnectElements._perform(op, context=MagicMock()) dispatch.assert_called_once() diff --git a/src/bonsai/test/bim/module/model/test_wall_props_resync_on_dim_change.py b/src/bonsai/test/bim/module/model/test_wall_props_resync_on_dim_change.py index f9f05fb936..68a95782fd 100644 --- a/src/bonsai/test/bim/module/model/test_wall_props_resync_on_dim_change.py +++ b/src/bonsai/test/bim/module/model/test_wall_props_resync_on_dim_change.py @@ -40,18 +40,25 @@ def _execute_source(operator_cls): def test_change_extrusion_depth_resyncs_wall_props(): + """Height mutation must re-prime ``BIMWallProperties.height`` so + gizmo icons positioned from ``props.height`` track the post-mutation + wall top in the same redraw.""" from bonsai.bim.module.model.wall import ChangeExtrusionDepth assert "_resync_walls_after_mutation" in _execute_source(ChangeExtrusionDepth) def test_change_extrusion_x_angle_resyncs_wall_props(): + """Slope mutation must re-prime ``BIMWallProperties.x_angle`` so + slope-driven gizmo positions track the new angle.""" from bonsai.bim.module.model.wall import ChangeExtrusionXAngle assert "_resync_walls_after_mutation" in _execute_source(ChangeExtrusionXAngle) def test_change_layer_length_resyncs_wall_props(): + """Length mutation must re-prime ``BIMWallProperties.length`` so + horizontal gizmo X positions track the new axis extent.""" from bonsai.bim.module.model.wall import ChangeLayerLength assert "_resync_walls_after_mutation" in _execute_source(ChangeLayerLength) diff --git a/src/bonsai/test/bim/module/model/test_wall_split_filled_opening.py b/src/bonsai/test/bim/module/model/test_wall_split_filled_opening.py index 9bbfcbb88a..499ce2509f 100644 --- a/src/bonsai/test/bim/module/model/test_wall_split_filled_opening.py +++ b/src/bonsai/test/bim/module/model/test_wall_split_filled_opening.py @@ -22,9 +22,9 @@ 1. Side classification reads the opening's axis-projected midpoint, not the filling's ``matrix_world.translation``. The filling origin is - flip-fragile — ``flip_object`` rotates the filler 180° + translates so - the bbox stays visually in place, which would mis-classify a flipped - door centred over the cut. + flip-fragile — flipping rotates the filler 180° + translates so the + bbox stays visually in place, which would mis-classify a flipped door + centred over the cut. 2. When the void straddles the cut and the filling moves to element2, the void copy for element1 is taken from the ORIGINAL opening (whose ``ObjectPlacement`` still references element1), not the rebound @@ -44,23 +44,24 @@ def _split_source(): def test_side_classification_uses_opening_midpoint_not_filling_origin(): + """Side classification must read the opening's axis-projected + midpoint, not the filling's world translation — the latter shifts + under flipping and would mis-classify a flipped door centred over + the cut.""" source = _split_source() assert "opening_midpoint" in source - # The pre-fix code projected the filling's world translation onto the - # axis to classify; that path must be gone. assert "filling_obj.matrix_world.translation" not in source def test_void_copy_reads_from_original_opening_before_remove(): + """When the filling moves to element2 and the void straddles the + cut, element1's pure-void copy must come from the original opening + BEFORE the cleanup that destroys it — the rebound ``new_opening`` + references element2's frame and would shift the void to element1's + origin in element2's local coords.""" source = _split_source() - # Locate the "filling moves to element2" branch via the opening - # midpoint check; the void-copy and the trailing remove_feature both - # live inside this branch, after the prior unfilled-opening loops. branch_start = source.index("if opening_midpoint > cut_percentage:") branch = source[branch_start:] add_idx = branch.index("_add_void_copy(element1, opening)") remove_idx = branch.index("feature.remove_feature(tool.Ifc.get(), feature=opening)") - # Read-from-original is the whole point — the rebound ``new_opening`` - # references element2's frame and would shift the void to element1's - # origin in element2's local coords. assert add_idx < remove_idx