mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-18 14:31:39 +00:00
Fix void mirroring on assign_type path
Two bugs fixed in TrueMirrorElements for tessellated elements with IfcExtrudedAreaSolid voids on the assign_inverted_type path: 1. Sync element IFC placement before computing opening positions so the geometry engine uses the correct element-local frame when applying boolean voids (fixes void appearing at wrong element-local offset). 2. Replace builder.mirror for IfcExtrudedAreaSolid with direct coordinate negation to avoid IFC entity aliasing that corrupted profile vertices in the translate→mirror→translate sequence. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -794,7 +794,21 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
_, R_quat, _ = obj.matrix_world.decompose()
|
_, R_quat, _ = obj.matrix_world.decompose()
|
||||||
R_elem_new = np.array(R_quat.to_matrix())
|
R_elem_new = np.array(R_quat.to_matrix())
|
||||||
frame_change = np.linalg.inv(R_elem_new) @ R_elem_old
|
frame_change = np.linalg.inv(R_elem_new) @ R_elem_old
|
||||||
self._apply_opening_mirror(element, mirror_axes, M_slab_before, opening_placements_before, frame_change)
|
# Sync element IFC placement to the post-reflection Blender position NOW, before
|
||||||
|
# computing opening positions. The geometry engine uses the element's IFC placement
|
||||||
|
# to convert opening world-space positions to element-local positions when applying
|
||||||
|
# boolean voids. If the placement is stale the void lands at the wrong offset.
|
||||||
|
# Calling edit_object_placement here also makes the depsgraph handler skip a
|
||||||
|
# redundant second call (record_object_position marks it up-to-date).
|
||||||
|
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj, apply_scale=False)
|
||||||
|
M_slab_new = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||||
|
self._apply_opening_mirror(element, mirror_axes, M_slab_before, opening_placements_before, frame_change, M_slab_new)
|
||||||
|
for rel in element.HasOpenings:
|
||||||
|
opening = rel.RelatedOpeningElement
|
||||||
|
tool.Geometry.clear_cache(opening)
|
||||||
|
opening_obj = tool.Ifc.get_object(opening)
|
||||||
|
if opening_obj:
|
||||||
|
tool.Geometry.reload_representation(opening_obj)
|
||||||
|
|
||||||
# bonsai does not automatically switch to the representation that should be active in the given context
|
# bonsai does not automatically switch to the representation that should be active in the given context
|
||||||
# when switching to a type that was previously viewed in another context (e.g. plan view),
|
# when switching to a type that was previously viewed in another context (e.g. plan view),
|
||||||
@@ -806,14 +820,17 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
representation=ifcopenshell.util.representation.get_representation(element, active_context),
|
representation=ifcopenshell.util.representation.get_representation(element, active_context),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _apply_opening_mirror(self, element, mirror_axes, M_slab_before, opening_placements_before, frame_change):
|
def _apply_opening_mirror(self, element, mirror_axes, M_slab_before, opening_placements_before, frame_change, M_slab_new=None):
|
||||||
"""Mirror IfcOpeningElement placements in element-local space.
|
"""Mirror IfcOpeningElement placements and geometry in element-local space.
|
||||||
|
|
||||||
frame_change = inv(R_elem_new) @ R_elem_old accounts for any element rotation that will
|
frame_change = inv(R_elem_new) @ R_elem_old accounts for any element rotation change
|
||||||
be applied after save (e.g. 180° Z for Y-mirror via assign_inverted_type). Pass np.eye(3)
|
(e.g. 180° Z for Y-mirror via assign_inverted_type). Pass np.eye(3) when the element
|
||||||
when no element rotation changes (LAYER3 / invert_representation path).
|
rotation does not change (LAYER3 / invert_representation path).
|
||||||
|
|
||||||
|
M_slab_new: element IFC world matrix after the mirror. If None, M_slab_before is used
|
||||||
|
(correct for the LAYER3 path where the element placement stays the same).
|
||||||
"""
|
"""
|
||||||
# Mirror normal in element's OLD local space
|
M_slab_anchor = M_slab_new if M_slab_new is not None else M_slab_before
|
||||||
N_local = np.zeros(3)
|
N_local = np.zeros(3)
|
||||||
for i, flip in enumerate(mirror_axes[:3]):
|
for i, flip in enumerate(mirror_axes[:3]):
|
||||||
if flip > 0.0:
|
if flip > 0.0:
|
||||||
@@ -830,33 +847,70 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
M_rel = np.linalg.inv(M_slab_before) @ M_abs_old
|
M_rel = np.linalg.inv(M_slab_before) @ M_abs_old
|
||||||
M_rel_new = M_rel.copy()
|
M_rel_new = M_rel.copy()
|
||||||
|
|
||||||
# Mirror then re-express translation in new element frame
|
|
||||||
t = M_rel[:3, 3].copy()
|
t = M_rel[:3, 3].copy()
|
||||||
t_refl = t - 2 * np.dot(t, N_local) * N_local
|
t_refl = t - 2 * np.dot(t, N_local) * N_local
|
||||||
M_rel_new[:3, 3] = frame_change @ t_refl
|
M_rel_new[:3, 3] = frame_change @ t_refl
|
||||||
|
|
||||||
# Mirror rotation by conjugation: H@R@H keeps det=+1 and gives the correct mirrored rotation.
|
# Mirror rotation by conjugation: H@R@H keeps det=+1 and gives the correct mirrored
|
||||||
# Direct Householder on columns yields det=-1; IFC's Y=Z×X normalization then introduces
|
# rotation. Direct Householder on columns yields det=-1; IFC's Y=Z×X normalization
|
||||||
# a spurious 180°Z error (R_z(π-θ) instead of R_z(-θ)). Conjugation avoids this.
|
# then introduces a spurious 180°Z error. Conjugation avoids this.
|
||||||
H = np.eye(3) - 2 * np.outer(N_local, N_local)
|
H = np.eye(3) - 2 * np.outer(N_local, N_local)
|
||||||
R_mirrored = H @ M_rel[:3, :3] @ H
|
R_mirrored = H @ M_rel[:3, :3] @ H
|
||||||
M_rel_new[:3, :3] = frame_change @ R_mirrored
|
M_rel_new[:3, :3] = frame_change @ R_mirrored
|
||||||
|
|
||||||
M_abs_new = M_slab_before @ M_rel_new
|
M_abs_new = M_slab_anchor @ M_rel_new
|
||||||
ifcopenshell.api.geometry.edit_object_placement(
|
ifcopenshell.api.geometry.edit_object_placement(
|
||||||
tool.Ifc.get(), product=opening, matrix=M_abs_new, is_si=False
|
tool.Ifc.get(), product=opening, matrix=M_abs_new, is_si=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mirror the opening's own representation geometry (local vertices/profile).
|
if not opening.Representation:
|
||||||
# edit_object_placement only moves the frame; the local shape must also be
|
continue
|
||||||
# mirrored so the void is the correct mirror image. Blender reload is skipped
|
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||||
# here because invert_representation / reload_representation recreates the
|
mirror_axes_2d = mirror_axes[:2]
|
||||||
# opening objects when the host element reloads.
|
num_flipped = sum(1 for v in mirror_axes_2d if v > 0.0)
|
||||||
if opening.Representation:
|
for rep in opening.Representation.Representations:
|
||||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
for item in rep.Items:
|
||||||
mirror_axes_2d = mirror_axes[:2]
|
if item.is_a("IfcExtrudedAreaSolid"):
|
||||||
for rep in opening.Representation.Representations:
|
# Bypass builder.mirror for IfcExtrudedAreaSolid — its translate→mirror→
|
||||||
for item in rep.Items:
|
# translate sequence can corrupt profile coordinates via IFC entity aliasing
|
||||||
|
# when Position.Location shares an IfcCartesianPoint with a profile vertex.
|
||||||
|
# Direct negation avoids all aliasing. (Position assumed to have default /
|
||||||
|
# identity orientation in practice.)
|
||||||
|
pos = list(item.Position.Location.Coordinates)
|
||||||
|
for i, flip in enumerate(mirror_axes_2d[: len(pos)]):
|
||||||
|
if flip > 0.0:
|
||||||
|
pos[i] = -pos[i]
|
||||||
|
item.Position.Location.Coordinates = tuple(pos)
|
||||||
|
|
||||||
|
profile = item.SweptArea
|
||||||
|
for curve in [getattr(profile, "OuterCurve", None)]:
|
||||||
|
if curve is None:
|
||||||
|
continue
|
||||||
|
coords = builder.get_polyline_coords(curve)
|
||||||
|
for i, flip in enumerate(mirror_axes_2d[: coords.shape[1]]):
|
||||||
|
if flip > 0.0:
|
||||||
|
coords[:, i] = -coords[:, i]
|
||||||
|
if num_flipped % 2 == 1:
|
||||||
|
coords = coords[::-1]
|
||||||
|
builder.set_polyline_coords(curve, coords)
|
||||||
|
for inner in getattr(profile, "InnerCurves", None) or []:
|
||||||
|
coords = builder.get_polyline_coords(inner)
|
||||||
|
for i, flip in enumerate(mirror_axes_2d[: coords.shape[1]]):
|
||||||
|
if flip > 0.0:
|
||||||
|
coords[:, i] = -coords[:, i]
|
||||||
|
if num_flipped % 2 == 1:
|
||||||
|
coords = coords[::-1]
|
||||||
|
builder.set_polyline_coords(inner, coords)
|
||||||
|
|
||||||
|
placement_mat = ifcopenshell.util.placement.get_axis2placement(item.Position)[:3, :3]
|
||||||
|
dir_local = np.array(item.ExtrudedDirection.DirectionRatios)
|
||||||
|
dir_opening = placement_mat @ dir_local
|
||||||
|
for i, flip in enumerate(mirror_axes_2d):
|
||||||
|
if flip > 0.0:
|
||||||
|
dir_opening[i] = -dir_opening[i]
|
||||||
|
dir_local_new = np.linalg.inv(placement_mat) @ dir_opening
|
||||||
|
item.ExtrudedDirection.DirectionRatios = tuple(float(v) for v in dir_local_new)
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
builder.mirror(item, mirror_axes_2d, create_copy=False)
|
builder.mirror(item, mirror_axes_2d, create_copy=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -908,7 +962,10 @@ class TrueMirrorElements(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
for sub in item.MappingSource.MappedRepresentation.Items:
|
for sub in item.MappingSource.MappedRepresentation.Items:
|
||||||
mirror_item(sub)
|
mirror_item(sub)
|
||||||
else:
|
else:
|
||||||
builder.mirror(item, mirror_axes_2d, create_copy=False)
|
try:
|
||||||
|
builder.mirror(item, mirror_axes_2d, create_copy=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if element.is_a("IfcProduct"):
|
if element.is_a("IfcProduct"):
|
||||||
if not element.Representation:
|
if not element.Representation:
|
||||||
|
|||||||
Reference in New Issue
Block a user