Bonsai: re-orthonormalize item placement in Item Mode (#6996)

Entering and leaving Item Mode on an element with IfcExtrudedAreaSolid
items corrupts their orientation, and the corruption persists after save
and reopen.

get_axis2placement builds the item frame using the placement's RefDirection
as the X axis without projecting it onto the plane perpendicular to Axis.
IFC permits a non perpendicular RefDirection (only its projection is
significant), so a legal placement yields a sheared, non orthonormal
matrix. import_item assigns that to the Blender item object's matrix_world,
but a Blender object transform stores only location, rotation and scale and
cannot represent shear, so the next depsgraph evaluation recomposes it into
a wrong rotation. sync_item_positions then reads the corrupted matrix and
writes it back into IfcExtrudedAreaSolid.Position, permanently corrupting
the geometry.

Re-orthonormalize the frame right after get_axis2placement (keep Z, project
X onto the plane perpendicular to Z, recompute Y). This is the same
projection the geometry kernel already applies, so rendered geometry is
unchanged; it only removes the shear Blender cannot store. No-op when
RefDirection is already perpendicular or absent, guarded against the
degenerate parallel case.

Verified live (headless Blender, enter+leave+save over 50 solids): the
maximum change in the normalized extrusion axis goes from 1.026 (segments
flipping, the bug) to 7e-6 (identity within float noise), and the items are
no longer flagged as moved, so their Positions are left untouched.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Petru Conduraru
2026-07-11 15:49:36 +03:00
parent 256d5a63f1
commit 77d7d6dcdc
+17
View File
@@ -2248,6 +2248,23 @@ class Geometry(bonsai.core.tool.Geometry):
if position or not is_swept_area:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
position = ifcopenshell.util.placement.get_axis2placement(position)
# get_axis2placement uses the placement's RefDirection as the X axis as
# is, without projecting it onto the plane perpendicular to Axis. IFC
# allows RefDirection to be non perpendicular to Axis (only its
# projection matters), so such a legal placement yields a sheared, non
# orthonormal matrix. A Blender object transform can only store
# location, rotation and scale, so on the next depsgraph evaluation the
# shear is silently baked into a wrong rotation, which flips the item's
# orientation and, once written back by sync_item_positions, corrupts
# the saved IfcSweptAreaSolid. Re orthonormalise the frame (keeping Z,
# projecting X) so it matches the geometry kernel and survives Blender.
z_axis = position[:3, 2]
x_axis = position[:3, 0] - np.dot(position[:3, 0], z_axis) * z_axis
x_axis_length = np.linalg.norm(x_axis)
if x_axis_length > 1e-12:
x_axis /= x_axis_length
position[:3, 0] = x_axis
position[:3, 1] = np.cross(z_axis, x_axis)
position[:, 3][0:3] *= unit_scale
item_matrix = np.array(rep_obj.matrix_world.copy())
if cartesian_point_offset is not None: