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 <noreply@anthropic.com>
This commit is contained in:
Ryan Schultz
2026-08-04 14:21:50 -05:00
parent 8c5d33f9e5
commit a27c9ef01c
4 changed files with 78 additions and 11 deletions
+26 -5
View File
@@ -2818,8 +2818,23 @@ class DimensionLinePositionWidget(types.GizmoGroup):
# Helpers # Helpers
@staticmethod @staticmethod
def _offset_dir(obj: bpy.types.Object) -> "Vector | None": def _cam_dir() -> "Vector | None":
"""World-space unit direction perpendicular to the dimension line and world_Z.""" """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: if not obj.data or not hasattr(obj.data, "splines") or not obj.data.splines:
return None return None
spline = obj.data.splines[0] spline = obj.data.splines[0]
@@ -2831,8 +2846,10 @@ class DimensionLinePositionWidget(types.GizmoGroup):
if dim.length < 1e-10: if dim.length < 1e-10:
return None return None
dim.normalize() dim.normalize()
world_z = Vector((0.0, 0.0, 1.0)) cam_view = cls._cam_dir()
od = world_z.cross(dim) 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: if od.length < 1e-6:
od = Vector((1.0, 0.0, 0.0)).cross(dim) od = Vector((1.0, 0.0, 0.0)).cross(dim)
if od.length < 1e-6: if od.length < 1e-6:
@@ -2920,7 +2937,11 @@ class DimensionLinePositionWidget(types.GizmoGroup):
except Exception: except Exception:
pass 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: if resolved_pts:
_update_blender_curve(element, resolved_pts) _update_blender_curve(element, resolved_pts)
tool.Blender.update_viewport() tool.Blender.update_viewport()
@@ -113,6 +113,12 @@ def regenerate_dims_for_layer(file, layer) -> None:
geom_settings = ifcopenshell.geom.settings() geom_settings = ifcopenshell.geom.settings()
geom_settings.set("APPLY_DEFAULT_MATERIALS", False) 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: for ann_id in annotation_ids:
try: try:
annotation = file.by_id(ann_id) annotation = file.by_id(ann_id)
@@ -143,6 +149,7 @@ def regenerate_dims_for_layer(file, layer) -> None:
settings=geom_settings, settings=geom_settings,
shape_cache=_dim_shape_cache, shape_cache=_dim_shape_cache,
placement_override=placement_override, placement_override=placement_override,
camera_dir=cam_dir_tuple,
) )
if resolved_pts: if resolved_pts:
_update_blender_curve(annotation, resolved_pts) _update_blender_curve(annotation, resolved_pts)
@@ -6397,10 +6397,15 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
placement_override[elem.id()] = np.array(elem_obj.matrix_world) placement_override[elem.id()] = np.array(elem_obj.matrix_world)
except Exception: except Exception:
pass 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( resolved_pts = drawing_api.regenerate_dimension(
file, annotation, file, annotation,
shape_cache=getattr(self, "_shape_cache", None), shape_cache=getattr(self, "_shape_cache", None),
placement_override=placement_override, placement_override=placement_override,
camera_dir=_cam_dir_tuple,
) )
if resolved_pts: if resolved_pts:
_update_blender_curve(annotation, 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: if _is_elevation:
_update_elevation_marker_z(file, annotation, shape_cache=shape_cache, placement_override=placement_override) _update_elevation_marker_z(file, annotation, shape_cache=shape_cache, placement_override=placement_override)
else: else:
import bpy as _bpy
import ifcopenshell.api.drawing as drawing_api 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( resolved_pts = drawing_api.regenerate_dimension(
file, file,
annotation, annotation,
shape_cache=shape_cache, shape_cache=shape_cache,
placement_override=placement_override, placement_override=placement_override,
camera_dir=_cam_dir_tuple,
) )
if resolved_pts: if resolved_pts:
_update_blender_curve(annotation, 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 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 updated = 0
for annotation in candidates: for annotation in candidates:
pset = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension") pset = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
@@ -7998,6 +8014,7 @@ class RegenerateDimensions(bpy.types.Operator, tool.Ifc.Operator):
settings=geom_settings, settings=geom_settings,
shape_cache=shape_cache, shape_cache=shape_cache,
placement_override=placement_override, placement_override=placement_override,
camera_dir=_cam_dir_tuple,
) )
if not resolved_pts: if not resolved_pts:
continue continue
+28 -6
View File
@@ -1135,13 +1135,27 @@ def _get_line_position(self) -> float:
m = math.sqrt(dx * dx + dy * dy + dz * dz) m = math.sqrt(dx * dx + dy * dy + dz * dz)
if m > 1e-10: if m > 1e-10:
ddx, ddy, ddz = dx / m, dy / m, dz / m ddx, ddy, ddz = dx / m, dy / m, dz / m
# cross(world_Z=(0,0,1), dim_dir) = (-ddy, ddx, 0) cam = _bpy.context.scene.camera
ox, oy, oz = -ddy, ddx, 0.0 cam_is_plan = True
om = math.sqrt(ox * ox + oy * oy) 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: if om > 1e-6:
od = (ox / om, oy / om, 0.0) od = (ox / om, oy / om, oz / om)
pt = anchors[0]["pt"] 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: except Exception:
pass pass
return 0.0 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 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: for obj, element, pset_data in targets:
pset_entity = file.by_id(pset_data["id"]) pset_entity = file.by_id(pset_data["id"])
ifcopenshell.api.pset.edit_pset(file, pset=pset_entity, properties={"LinePosition": value}) 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: except Exception:
pass 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: if resolved_pts:
_update_blender_curve(element, resolved_pts) _update_blender_curve(element, resolved_pts)