From a27c9ef01c876a971b76c476b56fb2bb66561cc9 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Tue, 4 Aug 2026 14:21:50 -0500 Subject: [PATCH] Make LinePosition camera-relative in section/elevation views _get_line_offset_direction now uses cross(camera_dir, dim_dir) instead of cross(world_Z, dim_dir) when the camera is mostly horizontal (section or elevation view). This keeps the offset axis in the view plane so the DimensionLinePositionWidget gizmo drags the line visually up/down rather than in/out of the screen. Plan view behaviour (cross(world_Z, dim_dir)) is preserved unchanged so existing stored LinePosition values continue to work. camera_dir is threaded through regenerate_dimension(), _get_line_offset_direction(), and all callers: gizmos.py (_set_pos / _offset_dir), prop.py (_get/_set_line_position), handler.py (regenerate_dims_for_layer and depsgraph handler), and all three operator.py call sites (DrawParametricDimension, _do_write_anchor, RegenerateDimensions). Co-Authored-By: Claude Sonnet 4.6 --- .../bonsai/bim/module/drawing/gizmos.py | 31 ++++++++++++++--- .../bonsai/bim/module/drawing/handler.py | 7 ++++ .../bonsai/bim/module/drawing/operator.py | 17 ++++++++++ src/bonsai/bonsai/bim/module/drawing/prop.py | 34 +++++++++++++++---- 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/gizmos.py b/src/bonsai/bonsai/bim/module/drawing/gizmos.py index 1bf5733187..b56c84019c 100644 --- a/src/bonsai/bonsai/bim/module/drawing/gizmos.py +++ b/src/bonsai/bonsai/bim/module/drawing/gizmos.py @@ -2818,8 +2818,23 @@ class DimensionLinePositionWidget(types.GizmoGroup): # Helpers @staticmethod - def _offset_dir(obj: bpy.types.Object) -> "Vector | None": - """World-space unit direction perpendicular to the dimension line and world_Z.""" + def _cam_dir() -> "Vector | None": + """Scene camera forward direction, or None.""" + cam = bpy.context.scene.camera + if not cam: + return None + return (cam.matrix_world.to_3x3() @ Vector((0.0, 0.0, -1.0))).normalized() + + @classmethod + def _offset_dir(cls, obj: bpy.types.Object) -> "Vector | None": + """World-space direction perpendicular to the dimension line and in the view plane. + + Plan view (camera mostly vertical): cross(world_Z, dim_dir) — preserves + existing stored LinePosition values. + Section/elevation (camera mostly horizontal): cross(cam_forward, dim_dir) — + keeps the offset axis inside the view plane so the gizmo moves the line + visually sideways (up/down in section) rather than into/out of the screen. + """ if not obj.data or not hasattr(obj.data, "splines") or not obj.data.splines: return None spline = obj.data.splines[0] @@ -2831,8 +2846,10 @@ class DimensionLinePositionWidget(types.GizmoGroup): if dim.length < 1e-10: return None dim.normalize() - world_z = Vector((0.0, 0.0, 1.0)) - od = world_z.cross(dim) + cam_view = cls._cam_dir() + cam_is_plan = (cam_view is None) or abs(cam_view.z) > 0.7 + ref = Vector((0.0, 0.0, 1.0)) if cam_is_plan else cam_view + od = ref.cross(dim) if od.length < 1e-6: od = Vector((1.0, 0.0, 0.0)).cross(dim) if od.length < 1e-6: @@ -2920,7 +2937,11 @@ class DimensionLinePositionWidget(types.GizmoGroup): except Exception: pass - resolved_pts = drawing_api.regenerate_dimension(file, element, placement_override=placement_override) + cam_view = self._cam_dir() + cam_dir_tuple = tuple(cam_view) if cam_view is not None else None + resolved_pts = drawing_api.regenerate_dimension( + file, element, placement_override=placement_override, camera_dir=cam_dir_tuple + ) if resolved_pts: _update_blender_curve(element, resolved_pts) tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/bim/module/drawing/handler.py b/src/bonsai/bonsai/bim/module/drawing/handler.py index 793b16be97..0cd767c7ae 100644 --- a/src/bonsai/bonsai/bim/module/drawing/handler.py +++ b/src/bonsai/bonsai/bim/module/drawing/handler.py @@ -113,6 +113,12 @@ def regenerate_dims_for_layer(file, layer) -> None: geom_settings = ifcopenshell.geom.settings() geom_settings.set("APPLY_DEFAULT_MATERIALS", False) + cam = bpy.context.scene.camera + cam_dir_tuple = None + if cam: + from mathutils import Vector as _Vec + cam_dir_tuple = tuple((cam.matrix_world.to_3x3() @ _Vec((0, 0, -1))).normalized()) + for ann_id in annotation_ids: try: annotation = file.by_id(ann_id) @@ -143,6 +149,7 @@ def regenerate_dims_for_layer(file, layer) -> None: settings=geom_settings, shape_cache=_dim_shape_cache, placement_override=placement_override, + camera_dir=cam_dir_tuple, ) if resolved_pts: _update_blender_curve(annotation, resolved_pts) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 2905aff709..d2f1413109 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -6397,10 +6397,15 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope placement_override[elem.id()] = np.array(elem_obj.matrix_world) except Exception: pass + _cam = bpy.context.scene.camera + _cam_dir_tuple = None + if _cam: + _cam_dir_tuple = tuple((bpy.context.scene.camera.matrix_world.to_3x3() @ Vector((0, 0, -1))).normalized()) resolved_pts = drawing_api.regenerate_dimension( file, annotation, shape_cache=getattr(self, "_shape_cache", None), placement_override=placement_override, + camera_dir=_cam_dir_tuple, ) if resolved_pts: _update_blender_curve(annotation, resolved_pts) @@ -7669,12 +7674,18 @@ def _do_write_anchor(annotation, annotation_obj, new_anchor: dict, vertex_index: if _is_elevation: _update_elevation_marker_z(file, annotation, shape_cache=shape_cache, placement_override=placement_override) else: + import bpy as _bpy import ifcopenshell.api.drawing as drawing_api + _cam = _bpy.context.scene.camera + _cam_dir_tuple = None + if _cam: + _cam_dir_tuple = tuple((_cam.matrix_world.to_3x3() @ Vector((0, 0, -1))).normalized()) resolved_pts = drawing_api.regenerate_dimension( file, annotation, shape_cache=shape_cache, placement_override=placement_override, + camera_dir=_cam_dir_tuple, ) if resolved_pts: _update_blender_curve(annotation, resolved_pts) @@ -7944,6 +7955,11 @@ class RegenerateDimensions(bpy.types.Operator, tool.Ifc.Operator): from bonsai.bim.module.drawing.handler import _sync_dimension_anchors_to_curve + _cam = context.scene.camera + _cam_dir_tuple = None + if _cam: + _cam_dir_tuple = tuple((_cam.matrix_world.to_3x3() @ Vector((0, 0, -1))).normalized()) + updated = 0 for annotation in candidates: pset = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension") @@ -7998,6 +8014,7 @@ class RegenerateDimensions(bpy.types.Operator, tool.Ifc.Operator): settings=geom_settings, shape_cache=shape_cache, placement_override=placement_override, + camera_dir=_cam_dir_tuple, ) if not resolved_pts: continue diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index 01cbaa3c89..70fa4f08fb 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -1135,13 +1135,27 @@ def _get_line_position(self) -> float: m = math.sqrt(dx * dx + dy * dy + dz * dz) if m > 1e-10: ddx, ddy, ddz = dx / m, dy / m, dz / m - # cross(world_Z=(0,0,1), dim_dir) = (-ddy, ddx, 0) - ox, oy, oz = -ddy, ddx, 0.0 - om = math.sqrt(ox * ox + oy * oy) + cam = _bpy.context.scene.camera + cam_is_plan = True + cvx, cvy, cvz = 0.0, 0.0, 1.0 + if cam: + from mathutils import Vector as _Vec + cv = (cam.matrix_world.to_3x3() @ _Vec((0, 0, -1))).normalized() + cvx, cvy, cvz = cv.x, cv.y, cv.z + cam_is_plan = abs(cvz) > 0.7 + if cam_is_plan: + # cross(world_Z, dim_dir) + ox, oy, oz = -ddy, ddx, 0.0 + else: + # cross(cam_dir, dim_dir) + ox = cvy * ddz - cvz * ddy + oy = cvz * ddx - cvx * ddz + oz = cvx * ddy - cvy * ddx + om = math.sqrt(ox * ox + oy * oy + oz * oz) if om > 1e-6: - od = (ox / om, oy / om, 0.0) + od = (ox / om, oy / om, oz / om) pt = anchors[0]["pt"] - return float(pt[0] * od[0] + pt[1] * od[1]) + return float(pt[0] * od[0] + pt[1] * od[1] + pt[2] * od[2]) except Exception: pass return 0.0 @@ -1180,6 +1194,12 @@ def _set_line_position(self, value: float) -> None: from bonsai.bim.module.drawing.operator import _update_blender_curve + cam = _bpy.context.scene.camera + cam_dir_tuple = None + if cam: + from mathutils import Vector as _Vec + cam_dir_tuple = tuple((cam.matrix_world.to_3x3() @ _Vec((0, 0, -1))).normalized()) + for obj, element, pset_data in targets: pset_entity = file.by_id(pset_data["id"]) ifcopenshell.api.pset.edit_pset(file, pset=pset_entity, properties={"LinePosition": value}) @@ -1198,7 +1218,9 @@ def _set_line_position(self, value: float) -> None: except Exception: pass - resolved_pts = drawing_api.regenerate_dimension(file, element, placement_override=placement_override) + resolved_pts = drawing_api.regenerate_dimension( + file, element, placement_override=placement_override, camera_dir=cam_dir_tuple + ) if resolved_pts: _update_blender_curve(element, resolved_pts)