Bonsai: fix clash pin drifting away when a false origin is active

ifcclash computes p1/p2 directly from the IFC file's true (SI metre)
coordinates. Every other imported object gets Bonsai's false origin /
georeferencing offset applied to its matrix_world on import, but the
clash pin in SelectClash assigned the raw ifcclash points straight to
the viewport, with no such conversion. Whenever a false origin was
active, the pin landed off by the entire offset (translation and
project-north rotation), which can be very large for real georeferenced
projects, matching the "shows up a very long ways away" symptom in #7178.

Live reproduction (synthetic IFC models, real ifcclash + real Bonsai
import pipeline in headless Blender) showed the raw unit-scale hypothesis
from our earlier comment on the issue does not hold: ifcclash already
returns proper SI metres regardless of project unit system (feet vs mm
projects gave identical, correct clash points for identical real-world
geometry). The false origin/georeferencing offset was the actual cause:
forcing a 50m offset moved the pin ~70m from the true intersection, and
adding a 30 degree project-north rotation on top moved it ~88m away.

Fix: convert ifcclash points through the same
false-origin/project-north transform already used for every other
displayed object (tool.Loader.apply_blender_offset_to_matrix_world),
via a new tool.Clash.convert_clash_point_to_blender() helper used by
SelectClash. Verified the fix brings the pin back to within the
existing measurement/approximation noise floor (~0.1-0.3m) of the true
intersection in both the translation-only and translation+rotation
cases, and leaves the false-origin-disabled case byte-for-byte
unchanged.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Petru Conduraru
2026-07-20 17:44:22 +03:00
parent 55a2430d71
commit e6489c2d9a
2 changed files with 25 additions and 3 deletions
@@ -428,10 +428,10 @@ class SelectClash(bpy.types.Operator):
tool.Spatial.select_products(products, unhide=True) tool.Spatial.select_products(products, unhide=True)
ClashDecorator.install(bpy.context) ClashDecorator.install(bpy.context)
target = Vector(clash["p1"]) target = tool.Clash.convert_clash_point_to_blender(clash["p1"])
tool.Clash.look_at(target, target + Vector((5, 5, 5))) tool.Clash.look_at(target, target + Vector((5, 5, 5)))
self.props.p1 = clash["p1"] self.props.p1 = target
self.props.p2 = clash["p2"] self.props.p2 = tool.Clash.convert_clash_point_to_blender(clash["p2"])
self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm" self.props.active_clash_text = clash["type"].title() + " " + str(round(clash["distance"] * 1000)) + "mm"
return {"FINISHED"} return {"FINISHED"}
+22
View File
@@ -22,6 +22,9 @@ import json
from typing import TYPE_CHECKING, Literal, Union from typing import TYPE_CHECKING, Literal, Union
import bpy import bpy
import ifcopenshell.util.geolocation
import ifcopenshell.util.unit
import numpy as np
from ifcclash import ifcclash from ifcclash import ifcclash
from ifcclash.ifcclash import ClashSource from ifcclash.ifcclash import ClashSource
from mathutils import Vector from mathutils import Vector
@@ -129,6 +132,25 @@ class Clash(bonsai.core.tool.Clash):
with open(fn) as f: with open(fn) as f:
ClashStore.clash_sets = json.load(f) ClashStore.clash_sets = json.load(f)
@classmethod
def convert_clash_point_to_blender(cls, point: Union[list[float], tuple[float, float, float]]) -> Vector:
"""Convert a raw ifcclash point into Bonsai's displayed viewport coordinates."""
props = tool.Georeference.get_georeference_props()
if not props.has_blender_offset:
return Vector(point)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
matrix = np.eye(4)
matrix[:3, 3] = point
matrix = ifcopenshell.util.geolocation.global2local(
matrix,
float(props.blender_offset_x) * unit_scale,
float(props.blender_offset_y) * unit_scale,
float(props.blender_offset_z) * unit_scale,
float(props.blender_x_axis_abscissa),
float(props.blender_x_axis_ordinate),
)
return Vector(matrix[:3, 3])
@classmethod @classmethod
def look_at(cls, target: Vector, location: Vector) -> None: def look_at(cls, target: Vector, location: Vector) -> None:
camera_location = location camera_location = location