Fix wall-split filled-opening classification + void copy

Two bugs in DumbWallJoiner.split's filled-opening branch:

1. Side classification read filling_obj.matrix_world.translation —
   flip-fragile because flip_object rotates the filler 180° + translates
   so the bbox stays visually in place, moving the door origin to the
   opposite bbox corner. A flipped door centred over the cut could be
   classified on the wrong side. Switch to the opening's axis-projected
   midpoint, which the unfilled-opening loop already uses.

2. When the void straddles the cut and the filling moves to element2,
   the void copy for element1 was taken from the rebound new_opening
   whose PlacementRelTo had been swapped to element2 — the new void on
   element1 then sat in element2's local frame. Reorder so the copy
   reads from the original opening (still hosted by element1) before
   remove_feature destroys it.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-15 11:41:01 +02:00
parent 7dcf415ef1
commit bbe437adc8
2 changed files with 82 additions and 10 deletions
+16 -10
View File
@@ -1619,13 +1619,16 @@ class DumbWallJoiner:
r.RelatedOpeningElement for r in list(element1.HasOpenings) if r.RelatedOpeningElement.HasFillings
]:
rel = opening.HasFillings[0]
filling = rel.RelatedBuildingElement
filling_obj = tool.Ifc.get_object(filling)
filling_location = filling_obj.matrix_world.translation
_, filling_position = mathutils.geometry.intersect_point_line(filling_location.to_2d(), *axis_world_2d)
min_t, max_t = _opening_axis_extent(opening, axis_world_2d, unit_scale)
# Use the opening's axis-projected midpoint to classify the side.
# The filling's ``matrix_world.translation`` is flip-fragile —
# ``flip_object`` rotates 180° + translates so the bbox stays
# visually in place, moving the door origin to the opposite
# corner, which would mis-classify a flipped door centred over
# the cut.
opening_midpoint = (min_t + max_t) / 2
void_straddles = min_t < cut_percentage < max_t
if filling_position > cut_percentage:
if opening_midpoint > cut_percentage:
# The filling should be moved from element1 to element2.
new_opening = ifcopenshell.api.root.copy_class(tool.Ifc.get(), product=opening)
new_opening.VoidsElements[0].RelatingBuildingElement = element2
@@ -1640,13 +1643,16 @@ class DumbWallJoiner:
rel.RelatingOpeningElement = new_opening
# Remove the old opening
ifcopenshell.api.feature.remove_feature(tool.Ifc.get(), feature=opening)
if void_straddles:
# Filling moved to element2, but void straddles — add a
# pure-void copy back to element1 so its body still gets cut.
_add_void_copy(element1, new_opening)
# pure-void copy back to element1. Read from the original
# ``opening`` whose ObjectPlacement still references
# element1; ``new_opening`` was rebound to element2 and
# would copy element2's frame instead.
_add_void_copy(element1, opening)
# Remove the old opening
ifcopenshell.api.feature.remove_feature(tool.Ifc.get(), feature=opening)
elif void_straddles:
# Filling stays on element1, but void straddles — add a pure-void
# copy to element2 so its body gets cut.
@@ -0,0 +1,66 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2026
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
#
# This file was generated with the assistance of an AI coding tool.
"""Pins two contracts in ``DumbWallJoiner.split``'s filled-opening branch:
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.
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
``new_opening`` (whose ``PlacementRelTo`` was swapped to element2)."""
import inspect
import pytest
pytestmark = pytest.mark.wall
def _split_source():
from bonsai.bim.module.model.wall import DumbWallJoiner
return inspect.getsource(DumbWallJoiner.split)
def test_side_classification_uses_opening_midpoint_not_filling_origin():
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():
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