diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index c4b7cd0ef6..546ae1a5b1 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -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: