mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-27 02:31:09 +00:00
Add DriveDimensionLength operator to move anchored objects to target dimension
Clicking the near/far half of a parametric dimension segment opens a dialog pre-filled with the current segment length; entering a target value moves the corresponding anchored IFC element and regenerates the dimension curve. First click selects the dimension; second click triggers the dialog. Clears the cut-decorator cache after the move so section hatch updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,7 @@ classes = (
|
|||||||
operator.DrawParametricDimension,
|
operator.DrawParametricDimension,
|
||||||
operator.SetDimensionAnchor,
|
operator.SetDimensionAnchor,
|
||||||
operator.RegenerateDimensions,
|
operator.RegenerateDimensions,
|
||||||
|
operator.DriveDimensionLength,
|
||||||
operator.RemoveDimensionAnchor,
|
operator.RemoveDimensionAnchor,
|
||||||
operator.InsertDimensionAnchor,
|
operator.InsertDimensionAnchor,
|
||||||
operator.ClickNearestDimensionAnchor,
|
operator.ClickNearestDimensionAnchor,
|
||||||
@@ -183,8 +184,10 @@ classes = (
|
|||||||
gizmos.ExtrusionGuidesGizmo,
|
gizmos.ExtrusionGuidesGizmo,
|
||||||
gizmos.ExtrusionWidget,
|
gizmos.ExtrusionWidget,
|
||||||
gizmos.GizmoAnchorHandle,
|
gizmos.GizmoAnchorHandle,
|
||||||
|
gizmos.GizmoDriveDimLabel,
|
||||||
gizmos.DimensionAnchorWidget,
|
gizmos.DimensionAnchorWidget,
|
||||||
gizmos.DimensionLinePositionWidget,
|
gizmos.DimensionLinePositionWidget,
|
||||||
|
gizmos.DimensionDriveLabelWidget,
|
||||||
workspace.LaunchAnnotationTypeManager,
|
workspace.LaunchAnnotationTypeManager,
|
||||||
workspace.Hotkey,
|
workspace.Hotkey,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3022,6 +3022,76 @@ class DimensionLinePositionWidget(types.GizmoGroup):
|
|||||||
return scale_value
|
return scale_value
|
||||||
|
|
||||||
|
|
||||||
|
class DimensionDriveLabelWidget(types.GizmoGroup):
|
||||||
|
"""Pen-icon gizmos at each segment midpoint of the active parametric dimension.
|
||||||
|
|
||||||
|
Clicking a pen invokes ``bim.drive_dimension_length`` for that segment,
|
||||||
|
opening a dialog pre-filled with the current length.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bl_idname = "BIM_GGT_dimension_drive_label"
|
||||||
|
bl_label = "Dimension Drive Label"
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "WINDOW"
|
||||||
|
bl_options = {"3D", "PERSISTENT", "SHOW_MODAL_ALL"}
|
||||||
|
|
||||||
|
_DIM_TYPES = frozenset(("DIMENSION", "RADIUS", "DIAMETER", "ANGLE"))
|
||||||
|
_MAX_SEGMENTS = 15
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context: bpy.types.Context) -> bool:
|
||||||
|
if not tool.Ifc.get():
|
||||||
|
return False
|
||||||
|
obj = context.active_object
|
||||||
|
if not obj or obj.type != "CURVE":
|
||||||
|
return False
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element or not element.is_a("IfcAnnotation"):
|
||||||
|
return False
|
||||||
|
import ifcopenshell.util.element as _ue
|
||||||
|
if _ue.get_predefined_type(element) not in cls._DIM_TYPES:
|
||||||
|
return False
|
||||||
|
pset = _ue.get_pset(element, "BBIM_Dimension")
|
||||||
|
return bool(pset and pset.get("Anchors"))
|
||||||
|
|
||||||
|
def setup(self, context: bpy.types.Context) -> None:
|
||||||
|
self._labels: list = []
|
||||||
|
for _ in range(self._MAX_SEGMENTS):
|
||||||
|
gz = self.gizmos.new("BIM_GT_drive_dim_label")
|
||||||
|
gz.color = (0.9, 0.75, 0.1)
|
||||||
|
gz.color_highlight = (1.0, 0.95, 0.3)
|
||||||
|
gz.alpha = 0.85
|
||||||
|
gz.alpha_highlight = 1.0
|
||||||
|
gz.scale_basis = 0.18
|
||||||
|
gz.use_draw_modal = True
|
||||||
|
gz.hide = True
|
||||||
|
self._labels.append(gz)
|
||||||
|
|
||||||
|
def refresh(self, context: bpy.types.Context) -> None:
|
||||||
|
obj = context.active_object
|
||||||
|
if not obj or not obj.data or not getattr(obj.data, "splines", None) or not obj.data.splines:
|
||||||
|
for gz in self._labels:
|
||||||
|
gz.hide = True
|
||||||
|
return
|
||||||
|
|
||||||
|
spline = obj.data.splines[0]
|
||||||
|
pts = [obj.matrix_world @ p.co.to_3d() for p in spline.points]
|
||||||
|
n_segs = min(len(pts) - 1, self._MAX_SEGMENTS)
|
||||||
|
|
||||||
|
for i in range(n_segs):
|
||||||
|
gz = self._labels[i]
|
||||||
|
mid = (pts[i] + pts[i + 1]) * 0.5
|
||||||
|
gz.matrix_basis = Matrix.Translation(mid)
|
||||||
|
gz.segment_index = i
|
||||||
|
gz.hide = False
|
||||||
|
|
||||||
|
for i in range(n_segs, self._MAX_SEGMENTS):
|
||||||
|
self._labels[i].hide = True
|
||||||
|
|
||||||
|
def draw_prepare(self, context: bpy.types.Context) -> None:
|
||||||
|
self.refresh(context)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Core Gizmo Classes
|
# Core Gizmo Classes
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -3988,6 +4058,24 @@ class GizmoPen(StaticTrisGizmoMixin, bpy.types.Gizmo):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GizmoDriveDimLabel(bpy.types.Gizmo):
|
||||||
|
"""Visual-only pen icon at a parametric dimension segment midpoint.
|
||||||
|
|
||||||
|
No draw_select/invoke — click handling is done by ClickNearestDimensionAnchor,
|
||||||
|
which dispatches bim.drive_dimension_length on a plain LMB at a midpoint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bl_idname = "BIM_GT_drive_dim_label"
|
||||||
|
__slots__ = ("segment_index", "custom_shape")
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.segment_index = 0
|
||||||
|
self.custom_shape = self.new_custom_shape("TRIS", GizmoPen.tris)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
self.draw_custom_shape(self.custom_shape)
|
||||||
|
|
||||||
|
|
||||||
class GizmoValidate(StaticTrisGizmoMixin, bpy.types.Gizmo):
|
class GizmoValidate(StaticTrisGizmoMixin, bpy.types.Gizmo):
|
||||||
"""Validate/checkmark icon gizmo for confirming edits."""
|
"""Validate/checkmark icon gizmo for confirming edits."""
|
||||||
|
|
||||||
|
|||||||
@@ -8416,6 +8416,144 @@ def _find_curve_in_item(item: ifcopenshell.entity_instance) -> Optional[ifcopens
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DriveDimensionLength(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
|
"""Set the length of one segment of a parametric dimension by moving one end's element.
|
||||||
|
|
||||||
|
Click the near half of a segment to move the near-end element; click the far half to
|
||||||
|
move the far-end element. A dialog opens pre-filled with the current length — enter
|
||||||
|
the target value and confirm.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bl_idname = "bim.drive_dimension_length"
|
||||||
|
bl_label = "Drive Dimension Length"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
segment_index: bpy.props.IntProperty(default=0)
|
||||||
|
# 0 = move anchor[segment_index] (near end); 1 = move anchor[segment_index+1] (far end)
|
||||||
|
move_end: bpy.props.IntProperty(default=1)
|
||||||
|
target_length: bpy.props.FloatProperty(
|
||||||
|
name="Length",
|
||||||
|
description="Target length for this dimension segment",
|
||||||
|
subtype="DISTANCE",
|
||||||
|
unit="LENGTH",
|
||||||
|
min=0.001,
|
||||||
|
)
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
obj = context.active_object
|
||||||
|
if not obj:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
if not element:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Dimension")
|
||||||
|
if not pset or not pset.get("Anchors"):
|
||||||
|
return {"CANCELLED"}
|
||||||
|
try:
|
||||||
|
anchors = json.loads(pset["Anchors"])
|
||||||
|
except Exception:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
if self.segment_index + 1 >= len(anchors):
|
||||||
|
return {"CANCELLED"}
|
||||||
|
pt_a = anchors[self.segment_index].get("pt")
|
||||||
|
pt_b = anchors[self.segment_index + 1].get("pt")
|
||||||
|
if pt_a and pt_b:
|
||||||
|
import math as _math
|
||||||
|
dx, dy, dz = pt_b[0] - pt_a[0], pt_b[1] - pt_a[1], pt_b[2] - pt_a[2]
|
||||||
|
self.target_length = _math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
|
||||||
|
def _execute(self, context):
|
||||||
|
from mathutils import Vector
|
||||||
|
obj = context.active_object
|
||||||
|
if not obj:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
file = tool.Ifc.get()
|
||||||
|
annotation = tool.Ifc.get_entity(obj)
|
||||||
|
if not annotation:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
pset = ifcopenshell.util.element.get_pset(annotation, "BBIM_Dimension")
|
||||||
|
if not pset or not pset.get("Anchors"):
|
||||||
|
return {"CANCELLED"}
|
||||||
|
try:
|
||||||
|
anchors = json.loads(pset["Anchors"])
|
||||||
|
except Exception:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
idx_a, idx_b = self.segment_index, self.segment_index + 1
|
||||||
|
if idx_b >= len(anchors):
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
pt_a = anchors[idx_a].get("pt")
|
||||||
|
pt_b = anchors[idx_b].get("pt")
|
||||||
|
if not pt_a or not pt_b:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
seg = Vector(pt_b) - Vector(pt_a)
|
||||||
|
current_length = seg.length
|
||||||
|
if current_length < 1e-10:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
# move_end=1 → move far anchor (anchor[idx_b]) away from near anchor (fixed).
|
||||||
|
# move_end=0 → move near anchor (anchor[idx_a]) away from far anchor (fixed).
|
||||||
|
# The direction seg points from a→b; negating it gives the b→a direction.
|
||||||
|
if self.move_end == 0:
|
||||||
|
move_idx = idx_a
|
||||||
|
delta = -(self.target_length - current_length) * seg.normalized()
|
||||||
|
else:
|
||||||
|
move_idx = idx_b
|
||||||
|
delta = (self.target_length - current_length) * seg.normalized()
|
||||||
|
|
||||||
|
guid_b = anchors[move_idx].get("guid")
|
||||||
|
if not guid_b:
|
||||||
|
self.report({"WARNING"}, "That anchor has no IFC element — cannot move")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
try:
|
||||||
|
elem_b = file.by_guid(guid_b)
|
||||||
|
except Exception:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
elem_obj = tool.Ifc.get_object(elem_b)
|
||||||
|
if not elem_obj:
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
elem_obj.matrix_world.translation += delta
|
||||||
|
ifcopenshell.api.run(
|
||||||
|
"geometry.edit_object_placement",
|
||||||
|
file,
|
||||||
|
product=elem_b,
|
||||||
|
matrix=np.array(elem_obj.matrix_world),
|
||||||
|
)
|
||||||
|
|
||||||
|
placement_override: dict = {}
|
||||||
|
for a in anchors:
|
||||||
|
guid = a.get("guid")
|
||||||
|
if not guid:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
elem = file.by_guid(guid)
|
||||||
|
o = tool.Ifc.get_object(elem)
|
||||||
|
if o:
|
||||||
|
placement_override[elem.id()] = np.array(o.matrix_world)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import ifcopenshell.api.drawing as drawing_api
|
||||||
|
_cam = bpy.context.scene.camera
|
||||||
|
_cam_dir = None
|
||||||
|
if _cam:
|
||||||
|
_cam_dir = tuple((_cam.matrix_world.to_3x3() @ Vector((0, 0, -1))).normalized())
|
||||||
|
resolved_pts = drawing_api.regenerate_dimension(
|
||||||
|
file, annotation,
|
||||||
|
placement_override=placement_override,
|
||||||
|
camera_dir=_cam_dir,
|
||||||
|
)
|
||||||
|
if resolved_pts:
|
||||||
|
_update_blender_curve(annotation, resolved_pts)
|
||||||
|
if CutDecorator.installed:
|
||||||
|
CutDecorator.install(context)
|
||||||
|
tool.Blender.update_viewport()
|
||||||
|
|
||||||
|
|
||||||
class RemoveDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
|
class RemoveDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
"""Remove one anchor vertex from a parametric dimension.
|
"""Remove one anchor vertex from a parametric dimension.
|
||||||
|
|
||||||
@@ -8570,6 +8708,7 @@ class ClickNearestDimensionAnchor(bpy.types.Operator):
|
|||||||
"""Click handler for dimension anchor dots and segment midpoints.
|
"""Click handler for dimension anchor dots and segment midpoints.
|
||||||
|
|
||||||
Plain click near a dot → SetDimensionAnchor (re-anchor that vertex).
|
Plain click near a dot → SetDimensionAnchor (re-anchor that vertex).
|
||||||
|
Plain click near a segment midpoint → DriveDimensionLength (drive that segment to a typed length).
|
||||||
Alt+click near a dot → RemoveDimensionAnchor (delete that vertex).
|
Alt+click near a dot → RemoveDimensionAnchor (delete that vertex).
|
||||||
Ctrl+click near a dot or segment midpoint → InsertDimensionAnchor
|
Ctrl+click near a dot or segment midpoint → InsertDimensionAnchor
|
||||||
(insert a new vertex after that position and enter face-pick mode).
|
(insert a new vertex after that position and enter face-pick mode).
|
||||||
@@ -8584,6 +8723,12 @@ class ClickNearestDimensionAnchor(bpy.types.Operator):
|
|||||||
|
|
||||||
RADIUS_PX = 15
|
RADIUS_PX = 15
|
||||||
|
|
||||||
|
# Tracks which dimension objects have already been "first-clicked" (selected).
|
||||||
|
# A plain click on a segment line is only dispatched to DriveDimensionLength once
|
||||||
|
# the object's name appears here. Cleared whenever the user clicks away from all
|
||||||
|
# dimensions so the next click re-enters the "first click = select" state.
|
||||||
|
_activated: set = set()
|
||||||
|
|
||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
from bpy_extras.view3d_utils import location_3d_to_region_2d
|
||||||
|
|
||||||
@@ -8620,6 +8765,7 @@ class ClickNearestDimensionAnchor(bpy.types.Operator):
|
|||||||
best_dist_sq = float("inf")
|
best_dist_sq = float("inf")
|
||||||
# "DOT" = clicked on an anchor vertex; "MIDPOINT" = clicked between two anchors.
|
# "DOT" = clicked on an anchor vertex; "MIDPOINT" = clicked between two anchors.
|
||||||
best_hit_type = "DOT"
|
best_hit_type = "DOT"
|
||||||
|
best_move_end = 1 # only meaningful for MIDPOINT hits
|
||||||
|
|
||||||
for obj in context.scene.objects:
|
for obj in context.scene.objects:
|
||||||
if obj.type != "CURVE":
|
if obj.type != "CURVE":
|
||||||
@@ -8665,27 +8811,63 @@ class ClickNearestDimensionAnchor(bpy.types.Operator):
|
|||||||
best_obj = obj
|
best_obj = obj
|
||||||
best_hit_type = "DOT"
|
best_hit_type = "DOT"
|
||||||
|
|
||||||
# When Ctrl is held, also check segment midpoints so the user
|
# Check proximity to each segment line (full length, excluding dot zones).
|
||||||
# can insert between two anchors without clicking on a dot.
|
# Projects the cursor onto the segment; accepts clicks within RADIUS_PX
|
||||||
if event.ctrl:
|
# perpendicularly, but rejects the RADIUS_PX region around each endpoint
|
||||||
for i in range(len(screen_pts) - 1):
|
# so dot and segment hits never compete.
|
||||||
sp_a = screen_pts[i]
|
for i in range(len(screen_pts) - 1):
|
||||||
sp_b = screen_pts[i + 1]
|
sp_a = screen_pts[i]
|
||||||
if not sp_a or not sp_b:
|
sp_b = screen_pts[i + 1]
|
||||||
continue
|
if not sp_a or not sp_b:
|
||||||
mid_x = (sp_a.x + sp_b.x) / 2.0
|
continue
|
||||||
mid_y = (sp_a.y + sp_b.y) / 2.0
|
seg_dx = sp_b.x - sp_a.x
|
||||||
dx, dy = cx - mid_x, cy - mid_y
|
seg_dy = sp_b.y - sp_a.y
|
||||||
d2 = dx * dx + dy * dy
|
seg_len_sq = seg_dx * seg_dx + seg_dy * seg_dy
|
||||||
if d2 < r2 and d2 < best_dist_sq:
|
if seg_len_sq < 1e-6:
|
||||||
best_dist_sq = d2
|
continue
|
||||||
best_idx = i # insert_after = segment start
|
seg_len = seg_len_sq ** 0.5
|
||||||
best_obj = obj
|
# Parametric projection of cursor onto segment (0=sp_a, 1=sp_b).
|
||||||
best_hit_type = "MIDPOINT"
|
t = ((cx - sp_a.x) * seg_dx + (cy - sp_a.y) * seg_dy) / seg_len_sq
|
||||||
|
# Exclude the dot zones at each end.
|
||||||
|
dot_frac = self.RADIUS_PX / seg_len
|
||||||
|
if t < dot_frac or t > 1.0 - dot_frac:
|
||||||
|
continue
|
||||||
|
# Perpendicular distance from cursor to segment.
|
||||||
|
foot_x = sp_a.x + t * seg_dx
|
||||||
|
foot_y = sp_a.y + t * seg_dy
|
||||||
|
perp_d2 = (cx - foot_x) ** 2 + (cy - foot_y) ** 2
|
||||||
|
if perp_d2 < r2 and perp_d2 < best_dist_sq:
|
||||||
|
best_dist_sq = perp_d2
|
||||||
|
best_idx = i
|
||||||
|
best_obj = obj
|
||||||
|
best_hit_type = "MIDPOINT"
|
||||||
|
# t < 0.5 → cursor is in the near half → move near-end object.
|
||||||
|
best_move_end = 0 if t < 0.5 else 1
|
||||||
|
|
||||||
if best_obj is None:
|
if best_obj is None:
|
||||||
|
print(f"[ClickDim] no hit → clear _activated={ClickNearestDimensionAnchor._activated}")
|
||||||
|
ClickNearestDimensionAnchor._activated.clear()
|
||||||
return {"PASS_THROUGH"}
|
return {"PASS_THROUGH"}
|
||||||
|
|
||||||
|
print(f"[ClickDim] hit={best_hit_type} obj={best_obj.name!r} ctrl={event.ctrl} alt={event.alt} _activated={ClickNearestDimensionAnchor._activated}")
|
||||||
|
|
||||||
|
# For midpoint hits (drive-dimension): require a prior interaction with this
|
||||||
|
# dimension before opening the dialog. First click explicitly selects it
|
||||||
|
# (anchor dots appear) so the user has clear feedback before the second click.
|
||||||
|
if best_hit_type == "MIDPOINT" and not event.ctrl:
|
||||||
|
if best_obj.name not in ClickNearestDimensionAnchor._activated:
|
||||||
|
ClickNearestDimensionAnchor._activated.add(best_obj.name)
|
||||||
|
print(f"[ClickDim] first click → select {best_obj.name!r}")
|
||||||
|
for o in list(context.selected_objects):
|
||||||
|
o.select_set(False)
|
||||||
|
best_obj.select_set(True)
|
||||||
|
context.view_layer.objects.active = best_obj
|
||||||
|
return {"FINISHED"}
|
||||||
|
print(f"[ClickDim] MIDPOINT dispatch → drive_dimension_length")
|
||||||
|
|
||||||
|
# Any successful non-first-click interaction marks this dimension as activated.
|
||||||
|
ClickNearestDimensionAnchor._activated.add(best_obj.name)
|
||||||
|
|
||||||
for o in list(context.selected_objects):
|
for o in list(context.selected_objects):
|
||||||
o.select_set(False)
|
o.select_set(False)
|
||||||
best_obj.select_set(True)
|
best_obj.select_set(True)
|
||||||
@@ -8698,6 +8880,7 @@ class ClickNearestDimensionAnchor(bpy.types.Operator):
|
|||||||
self._alt = event.alt
|
self._alt = event.alt
|
||||||
self._ctrl = event.ctrl
|
self._ctrl = event.ctrl
|
||||||
self._hit_type = best_hit_type
|
self._hit_type = best_hit_type
|
||||||
|
self._move_end = best_move_end
|
||||||
context.window_manager.modal_handler_add(self)
|
context.window_manager.modal_handler_add(self)
|
||||||
return {"RUNNING_MODAL"}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
@@ -8710,6 +8893,14 @@ class ClickNearestDimensionAnchor(bpy.types.Operator):
|
|||||||
elif self._ctrl:
|
elif self._ctrl:
|
||||||
# Ctrl+click on a dot or midpoint → insert after that position.
|
# Ctrl+click on a dot or midpoint → insert after that position.
|
||||||
bpy.ops.bim.insert_dimension_anchor("INVOKE_DEFAULT", insert_after=self._best_idx)
|
bpy.ops.bim.insert_dimension_anchor("INVOKE_DEFAULT", insert_after=self._best_idx)
|
||||||
|
elif self._hit_type == "MIDPOINT":
|
||||||
|
# Plain click on a segment midpoint → drive that segment's length.
|
||||||
|
# Which end moves depends on which half of the segment was clicked.
|
||||||
|
bpy.ops.bim.drive_dimension_length(
|
||||||
|
"INVOKE_DEFAULT",
|
||||||
|
segment_index=self._best_idx,
|
||||||
|
move_end=self._move_end,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Plain click on a dot → re-anchor (existing behaviour).
|
# Plain click on a dot → re-anchor (existing behaviour).
|
||||||
from bonsai.bim.module.drawing.gizmos import set_active_anchor
|
from bonsai.bim.module.drawing.gizmos import set_active_anchor
|
||||||
|
|||||||
Reference in New Issue
Block a user