From e6489c2d9ae865abc84832cb9c15072904f16414 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Mon, 20 Jul 2026 17:44:22 +0300 Subject: [PATCH] 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. --- .../bonsai/bim/module/clash/operator.py | 6 ++--- src/bonsai/bonsai/tool/clash.py | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/clash/operator.py b/src/bonsai/bonsai/bim/module/clash/operator.py index ae5f622bbd..8062b348e9 100644 --- a/src/bonsai/bonsai/bim/module/clash/operator.py +++ b/src/bonsai/bonsai/bim/module/clash/operator.py @@ -428,10 +428,10 @@ class SelectClash(bpy.types.Operator): tool.Spatial.select_products(products, unhide=True) 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))) - self.props.p1 = clash["p1"] - self.props.p2 = clash["p2"] + self.props.p1 = target + 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" return {"FINISHED"} diff --git a/src/bonsai/bonsai/tool/clash.py b/src/bonsai/bonsai/tool/clash.py index fb64e82b49..f8a6f71bc8 100644 --- a/src/bonsai/bonsai/tool/clash.py +++ b/src/bonsai/bonsai/tool/clash.py @@ -22,6 +22,9 @@ import json from typing import TYPE_CHECKING, Literal, Union import bpy +import ifcopenshell.util.geolocation +import ifcopenshell.util.unit +import numpy as np from ifcclash import ifcclash from ifcclash.ifcclash import ClashSource from mathutils import Vector @@ -129,6 +132,25 @@ class Clash(bonsai.core.tool.Clash): with open(fn) as 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 def look_at(cls, target: Vector, location: Vector) -> None: camera_location = location