mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 20:42:27 +00:00
Optimize DrawParametricDimension startup and MOUSEMOVE performance
- Remove clear_snap_objs() from PolylineOperator.invoke — BVH cache now persists across invocations; per-entry staleness is checked in create_snap_obj via matrix_world equality + vertex count, eliminating the ~11 s full rebuild on every Shift+A press. - Add _init_snapping_points() hook to PolylineOperator; DrawParametricDimension overrides it with a cheap plane-intersection placeholder, deferring full BVH detection to the first MOUSEMOVE. - Cache matrix_world in SnapObj and replace O(N_vertices) validation loop with O(1) matrix equality + single sample vertex check, cutting per-call create_snap_obj cost from 22-600 ms to <0.2 ms on cache hits. - Use scene-level BVH pierce-through in SetDimensionAnchor._compute_candidates instead of per-object ray_cast loop (O(log N) vs O(N_objects)). - Guard PolylineDecorator snap_mouse_point access against empty collection to prevent IndexError before first MOUSEMOVE populates the property. - Wrap closest_point_on_mesh in try/except RuntimeError in _update_snap_draw_data for annotation objects with no internal mesh data. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5614,6 +5614,8 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
self._snap_mode = "FACE"
|
self._snap_mode = "FACE"
|
||||||
self._ifc_snap_candidate = None
|
self._ifc_snap_candidate = None
|
||||||
self._draw_handler = None
|
self._draw_handler = None
|
||||||
|
self._snap_cand_obj_ptr: int = -1 # Blender object pointer for cached snap cands
|
||||||
|
self._snap_cand_cache: list = [] # cached get_layer/profile_snap_candidates result
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Snap → anchor bridge
|
# Snap → anchor bridge
|
||||||
@@ -5714,12 +5716,10 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
a = self._anchors[0] if self._anchors else None
|
a = self._anchors[0] if self._anchors else None
|
||||||
if not a or a.get("type") != "FACE":
|
if not a or a.get("type") != "FACE":
|
||||||
print(f"[perp] _update_perp_constraint: anchor type={a.get('type') if a else None} — need FACE, skipping")
|
|
||||||
return
|
return
|
||||||
addr = a.get("addr") or {}
|
addr = a.get("addr") or {}
|
||||||
pt = a.get("pt")
|
pt = a.get("pt")
|
||||||
if not pt:
|
if not pt:
|
||||||
print(f"[perp] _update_perp_constraint: missing pt")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
method = addr.get("method", "FACE_NORMAL")
|
method = addr.get("method", "FACE_NORMAL")
|
||||||
@@ -5728,7 +5728,6 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
if method == "LAYER_BOUNDARY":
|
if method == "LAYER_BOUNDARY":
|
||||||
# Derive the thickness-axis normal from the element's LayerSetDirection.
|
# Derive the thickness-axis normal from the element's LayerSetDirection.
|
||||||
if not guid:
|
if not guid:
|
||||||
print(f"[perp] _update_perp_constraint (LAYER): no guid")
|
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
file = tool.Ifc.get()
|
file = tool.Ifc.get()
|
||||||
@@ -5736,7 +5735,6 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
import ifcopenshell.util.element as _ifc_elem
|
import ifcopenshell.util.element as _ifc_elem
|
||||||
usage = _ifc_elem.get_material(element, should_inherit=True)
|
usage = _ifc_elem.get_material(element, should_inherit=True)
|
||||||
if not usage or not usage.is_a("IfcMaterialLayerSetUsage"):
|
if not usage or not usage.is_a("IfcMaterialLayerSetUsage"):
|
||||||
print(f"[perp] _update_perp_constraint (LAYER): no IfcMaterialLayerSetUsage")
|
|
||||||
return
|
return
|
||||||
axis = getattr(usage, "LayerSetDirection", None) or "AXIS2"
|
axis = getattr(usage, "LayerSetDirection", None) or "AXIS2"
|
||||||
if axis == "AXIS1":
|
if axis == "AXIS1":
|
||||||
@@ -5752,13 +5750,11 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
n: tuple = (nw.x, nw.y, nw.z)
|
n: tuple = (nw.x, nw.y, nw.z)
|
||||||
else:
|
else:
|
||||||
n = normal_local
|
n = normal_local
|
||||||
except Exception as exc:
|
except Exception:
|
||||||
print(f"[perp] _update_perp_constraint (LAYER): {exc}")
|
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
normal_local = addr.get("normal_local")
|
normal_local = addr.get("normal_local")
|
||||||
if not normal_local:
|
if not normal_local:
|
||||||
print(f"[perp] _update_perp_constraint: missing normal_local")
|
|
||||||
return
|
return
|
||||||
n = normal_local
|
n = normal_local
|
||||||
if guid:
|
if guid:
|
||||||
@@ -5770,18 +5766,14 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
nw = obj.matrix_world.to_3x3() @ Vector(normal_local)
|
nw = obj.matrix_world.to_3x3() @ Vector(normal_local)
|
||||||
nw.normalize()
|
nw.normalize()
|
||||||
n = (nw.x, nw.y, nw.z)
|
n = (nw.x, nw.y, nw.z)
|
||||||
else:
|
except Exception:
|
||||||
print(f"[perp] _update_perp_constraint: no Blender obj for guid={guid}, using normal_local as-is")
|
pass
|
||||||
except Exception as exc:
|
|
||||||
print(f"[perp] _update_perp_constraint: exception rotating normal: {exc}")
|
|
||||||
|
|
||||||
mag = math.sqrt(n[0] ** 2 + n[1] ** 2 + n[2] ** 2)
|
mag = math.sqrt(n[0] ** 2 + n[1] ** 2 + n[2] ** 2)
|
||||||
if mag < 1e-12:
|
if mag < 1e-12:
|
||||||
print(f"[perp] _update_perp_constraint: zero-length normal after rotation")
|
|
||||||
return
|
return
|
||||||
self._anchor0_normal = (n[0] / mag, n[1] / mag, n[2] / mag)
|
self._anchor0_normal = (n[0] / mag, n[1] / mag, n[2] / mag)
|
||||||
self._anchor0_pt = tuple(pt)
|
self._anchor0_pt = tuple(pt)
|
||||||
print(f"[perp] _update_perp_constraint: OK normal={[round(v,3) for v in self._anchor0_normal]} pt={[round(v,3) for v in self._anchor0_pt]}")
|
|
||||||
|
|
||||||
def _apply_perp_constraint(self) -> None:
|
def _apply_perp_constraint(self) -> None:
|
||||||
"""Project the current snap point onto the constraint line when active."""
|
"""Project the current snap point onto the constraint line when active."""
|
||||||
@@ -5799,7 +5791,6 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
n = self._anchor0_normal
|
n = self._anchor0_normal
|
||||||
t = (p.x - base[0]) * n[0] + (p.y - base[1]) * n[1] + (p.z - base[2]) * n[2]
|
t = (p.x - base[0]) * n[0] + (p.y - base[1]) * n[1] + (p.z - base[2]) * n[2]
|
||||||
constrained = Vector((base[0] + t * n[0], base[1] + t * n[1], base[2] + t * n[2]))
|
constrained = Vector((base[0] + t * n[0], base[1] + t * n[1], base[2] + t * n[2]))
|
||||||
print(f"[perp] _apply_perp_constraint: raw=({p.x:.3f},{p.y:.3f},{p.z:.3f}) t={t:.4f} constrained=({constrained.x:.3f},{constrained.y:.3f},{constrained.z:.3f})")
|
|
||||||
snap["point"] = constrained
|
snap["point"] = constrained
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -5823,13 +5814,23 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
region = context.region
|
region = context.region
|
||||||
rv3d = context.region_data
|
rv3d = context.region_data
|
||||||
mx, my = event.mouse_region_x, event.mouse_region_y
|
mx, my = event.mouse_region_x, event.mouse_region_y
|
||||||
file = tool.Ifc.get()
|
|
||||||
placement_override = {element.id(): np.array(hit_obj.matrix_world)}
|
# Recompute expensive candidate geometry only when the hovered object changes.
|
||||||
|
obj_ptr = hit_obj.as_pointer()
|
||||||
|
if obj_ptr != self._snap_cand_obj_ptr:
|
||||||
|
file = tool.Ifc.get()
|
||||||
|
placement_override = {element.id(): np.array(hit_obj.matrix_world)}
|
||||||
|
if self._snap_mode == "LAYER":
|
||||||
|
self._snap_cand_cache = drawing_api.get_layer_snap_candidates(file, element, placement_override)
|
||||||
|
else:
|
||||||
|
self._snap_cand_cache = drawing_api.get_profile_snap_candidates(file, element, placement_override)
|
||||||
|
self._snap_cand_obj_ptr = obj_ptr
|
||||||
|
cands = self._snap_cand_cache
|
||||||
|
|
||||||
|
if not cands:
|
||||||
|
return None
|
||||||
|
|
||||||
if self._snap_mode == "LAYER":
|
if self._snap_mode == "LAYER":
|
||||||
cands = drawing_api.get_layer_snap_candidates(file, element, placement_override)
|
|
||||||
if not cands:
|
|
||||||
return None
|
|
||||||
best_cand, best_d2 = None, float("inf")
|
best_cand, best_d2 = None, float("inf")
|
||||||
for cand in cands:
|
for cand in cands:
|
||||||
sp = location_3d_to_region_2d(region, rv3d, Vector(cand["snap_world"]))
|
sp = location_3d_to_region_2d(region, rv3d, Vector(cand["snap_world"]))
|
||||||
@@ -5848,9 +5849,6 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
# VERTEX or EDGE — profile-based candidates
|
# VERTEX or EDGE — profile-based candidates
|
||||||
cands = drawing_api.get_profile_snap_candidates(file, element, placement_override)
|
|
||||||
if not cands:
|
|
||||||
return None
|
|
||||||
cands_of_type = [c for c in cands if c["type"] == self._snap_mode]
|
cands_of_type = [c for c in cands if c["type"] == self._snap_mode]
|
||||||
if not cands_of_type:
|
if not cands_of_type:
|
||||||
return None
|
return None
|
||||||
@@ -5926,7 +5924,10 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
if face_index is None or face_index >= len(hit_obj.data.polygons):
|
if face_index is None or face_index >= len(hit_obj.data.polygons):
|
||||||
if pt_world is not None:
|
if pt_world is not None:
|
||||||
local_pt = hit_obj.matrix_world.inverted() @ pt_world
|
local_pt = hit_obj.matrix_world.inverted() @ pt_world
|
||||||
ok, _loc, _n, face_index = hit_obj.closest_point_on_mesh(local_pt)
|
try:
|
||||||
|
ok, _loc, _n, face_index = hit_obj.closest_point_on_mesh(local_pt)
|
||||||
|
except RuntimeError:
|
||||||
|
return
|
||||||
if not ok:
|
if not ok:
|
||||||
return
|
return
|
||||||
face_index = _prefer_perp_face_index(hit_obj, pt_world, face_index)
|
face_index = _prefer_perp_face_index(hit_obj, pt_world, face_index)
|
||||||
@@ -6073,12 +6074,15 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
cur = self._SNAP_MODES.index(self._snap_mode)
|
cur = self._SNAP_MODES.index(self._snap_mode)
|
||||||
self._snap_mode = self._SNAP_MODES[(cur + 1) % len(self._SNAP_MODES)]
|
self._snap_mode = self._SNAP_MODES[(cur + 1) % len(self._SNAP_MODES)]
|
||||||
self._ifc_snap_candidate = None
|
self._ifc_snap_candidate = None
|
||||||
|
self._snap_cand_obj_ptr = -1 # invalidate cache: LAYER vs VERTEX/EDGE differ
|
||||||
self._set_status(context)
|
self._set_status(context)
|
||||||
return {"RUNNING_MODAL"}
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
# For LAYER / VERTEX / EDGE modes, compute an IFC-native snap candidate and
|
# For LAYER / VERTEX / EDGE modes, compute an IFC-native snap candidate and
|
||||||
# override the polyline cursor position so the visual tracks the IFC point.
|
# override the polyline cursor position so the visual tracks the IFC point.
|
||||||
self._ifc_snap_candidate = self._compute_ifc_snap_candidate(context, event)
|
# Only recompute on mouse moves — key events don't change the hit object.
|
||||||
|
if event.type == "MOUSEMOVE":
|
||||||
|
self._ifc_snap_candidate = self._compute_ifc_snap_candidate(context, event)
|
||||||
if self._ifc_snap_candidate and self.snapping_points:
|
if self._ifc_snap_candidate and self.snapping_points:
|
||||||
wp = self._ifc_snap_candidate.get("snap_world")
|
wp = self._ifc_snap_candidate.get("snap_world")
|
||||||
if wp:
|
if wp:
|
||||||
@@ -6112,6 +6116,20 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
def invoke(self, context, event):
|
def invoke(self, context, event):
|
||||||
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
|
return IfcStore.execute_ifc_operator(self, context, event, method="INVOKE")
|
||||||
|
|
||||||
|
def _init_snapping_points(self, context, event):
|
||||||
|
"""Skip the full BVH snap at startup — use a plane-intersection placeholder.
|
||||||
|
|
||||||
|
handle_mouse_move populates snapping_points properly after the first few
|
||||||
|
MOUSEMOVE events, so this placeholder only needs to survive until then.
|
||||||
|
We must also populate snap_mouse_point (a Blender prop collection) because
|
||||||
|
calculate_distance_and_angle accesses it immediately after invoke.
|
||||||
|
"""
|
||||||
|
from mathutils import Vector
|
||||||
|
plane_pt = tool.Raycast.ray_cast_to_plane(context, event, Vector((0, 0, 0)), Vector((0, 0, 1)))
|
||||||
|
snap = {"type": "Plane", "point": plane_pt, "object": None, "group": "Plane", "distance": 10}
|
||||||
|
self.snapping_points = [snap]
|
||||||
|
tool.Snap.update_snapping_point(plane_pt, "Plane")
|
||||||
|
|
||||||
def _invoke(self, context, event):
|
def _invoke(self, context, event):
|
||||||
super().invoke(context, event)
|
super().invoke(context, event)
|
||||||
self._force_perpendicular = tool.Drawing.get_annotation_props().force_perpendicular_to_face
|
self._force_perpendicular = tool.Drawing.get_annotation_props().force_perpendicular_to_face
|
||||||
@@ -6285,7 +6303,7 @@ class SetDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
_draw_handler: object # SpaceView3D draw handler handle
|
_draw_handler: object # SpaceView3D draw handler handle
|
||||||
|
|
||||||
_VERTEX_PICK_RADIUS_PX = 20
|
_VERTEX_PICK_RADIUS_PX = 20
|
||||||
_HOVER_THROTTLE_PX_SQ = 25
|
_HOVER_THROTTLE_PX_SQ = 144 # 12 px — enough to feel responsive without per-pixel recompute
|
||||||
_SNAP_MODES = ("FACE", "LAYER", "EDGE", "VERTEX")
|
_SNAP_MODES = ("FACE", "LAYER", "EDGE", "VERTEX")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -6613,31 +6631,39 @@ class SetDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
from mathutils import Vector
|
from mathutils import Vector
|
||||||
|
|
||||||
origin, direction = self._unproject_coord(coord)
|
origin, direction = self._unproject_coord(coord)
|
||||||
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
|
|
||||||
direct: list = [] # (dist, ifc_obj, mesh_obj, mx, loc_w, normal, face_index)
|
# Scene-BVH pierce-through: O(log N) vs the previous O(N) per-object loop.
|
||||||
for ifc_obj in context.scene.objects:
|
# Each iteration steps past the last hit surface to reach the next object.
|
||||||
|
direct: list = []
|
||||||
|
ray_origin = Vector(origin)
|
||||||
|
_EPS = 1e-4
|
||||||
|
|
||||||
|
for _ in range(8):
|
||||||
|
result, loc_w, nrm_w, fi, hit_obj_eval, hit_mx = context.scene.ray_cast(
|
||||||
|
depsgraph, ray_origin, direction
|
||||||
|
)
|
||||||
|
if not result:
|
||||||
|
break
|
||||||
|
ray_origin = loc_w + direction * _EPS
|
||||||
|
ifc_obj = getattr(hit_obj_eval, "original", hit_obj_eval)
|
||||||
if ifc_obj == self._annotation_obj:
|
if ifc_obj == self._annotation_obj:
|
||||||
continue
|
continue
|
||||||
if not tool.Ifc.get_entity(ifc_obj):
|
if not ifc_obj.visible_get():
|
||||||
continue
|
continue
|
||||||
if ifc_obj.type != "MESH":
|
if ifc_obj.type != "MESH":
|
||||||
continue
|
continue
|
||||||
|
if not tool.Ifc.get_entity(ifc_obj):
|
||||||
|
continue
|
||||||
mx = ifc_obj.matrix_world
|
mx = ifc_obj.matrix_world
|
||||||
try:
|
|
||||||
mx_inv = mx.inverted()
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
ok, loc_l, nrm_l, fi = ifc_obj.ray_cast(
|
|
||||||
mx_inv @ origin, (mx_inv.to_3x3() @ direction).normalized()
|
|
||||||
)
|
|
||||||
if not ok:
|
|
||||||
continue
|
|
||||||
loc_w = mx @ loc_l
|
|
||||||
fi = _prefer_perp_face_index(ifc_obj, loc_w, fi, world_matrix=mx)
|
fi = _prefer_perp_face_index(ifc_obj, loc_w, fi, world_matrix=mx)
|
||||||
normal = (mx.to_3x3() @ ifc_obj.data.polygons[fi].normal).normalized() if fi is not None else (mx.to_3x3() @ nrm_l).normalized()
|
normal = (
|
||||||
|
(mx.to_3x3() @ ifc_obj.data.polygons[fi].normal).normalized()
|
||||||
|
if fi is not None
|
||||||
|
else nrm_w.normalized()
|
||||||
|
)
|
||||||
dist = (loc_w - origin).length
|
dist = (loc_w - origin).length
|
||||||
direct.append((dist, ifc_obj, ifc_obj, mx, loc_w, normal, fi))
|
direct.append((dist, ifc_obj, ifc_obj, mx, loc_w, normal, fi))
|
||||||
|
|
||||||
if direct:
|
if direct:
|
||||||
direct.sort(key=lambda c: c[0])
|
direct.sort(key=lambda c: c[0])
|
||||||
return [(o, m, mmx, l, n, f) for _, o, m, mmx, l, n, f in direct]
|
return [(o, m, mmx, l, n, f) for _, o, m, mmx, l, n, f in direct]
|
||||||
@@ -6652,6 +6678,8 @@ class SetDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
for ifc_obj in context.scene.objects:
|
for ifc_obj in context.scene.objects:
|
||||||
if ifc_obj == self._annotation_obj:
|
if ifc_obj == self._annotation_obj:
|
||||||
continue
|
continue
|
||||||
|
if not ifc_obj.visible_get():
|
||||||
|
continue
|
||||||
if not tool.Ifc.get_entity(ifc_obj):
|
if not tool.Ifc.get_entity(ifc_obj):
|
||||||
continue
|
continue
|
||||||
if ifc_obj.type != "MESH":
|
if ifc_obj.type != "MESH":
|
||||||
|
|||||||
@@ -796,6 +796,8 @@ class PolylineDecorator(tool.Blender.ViewportDecorator):
|
|||||||
rv3d = region.data
|
rv3d = region.data
|
||||||
|
|
||||||
polyline_props = tool.Model.get_polyline_props()
|
polyline_props = tool.Model.get_polyline_props()
|
||||||
|
if not polyline_props.snap_mouse_point:
|
||||||
|
return
|
||||||
snap_prop = polyline_props.snap_mouse_point[0]
|
snap_prop = polyline_props.snap_mouse_point[0]
|
||||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||||
|
|
||||||
@@ -863,6 +865,8 @@ class PolylineDecorator(tool.Blender.ViewportDecorator):
|
|||||||
gpu.state.point_size_set(6)
|
gpu.state.point_size_set(6)
|
||||||
|
|
||||||
polyline_props = tool.Model.get_polyline_props()
|
polyline_props = tool.Model.get_polyline_props()
|
||||||
|
if not polyline_props.snap_mouse_point:
|
||||||
|
return
|
||||||
snap_prop = polyline_props.snap_mouse_point[0]
|
snap_prop = polyline_props.snap_mouse_point[0]
|
||||||
# Point related to the mouse
|
# Point related to the mouse
|
||||||
mouse_point = [Vector((snap_prop.x, snap_prop.y, snap_prop.z))]
|
mouse_point = [Vector((snap_prop.x, snap_prop.y, snap_prop.z))]
|
||||||
|
|||||||
@@ -462,14 +462,26 @@ class PolylineOperator:
|
|||||||
self.tool_state.axis_method = None
|
self.tool_state.axis_method = None
|
||||||
self.tool_state.plane_method = None
|
self.tool_state.plane_method = None
|
||||||
self.tool_state.mode = "Mouse"
|
self.tool_state.mode = "Mouse"
|
||||||
tool.Raycast.clear_snap_objs()
|
# Do not call clear_snap_objs() here — create_snap_obj() validates stale
|
||||||
|
# entries per-object (vertex count + position check), so the BVH cache can
|
||||||
|
# safely persist across invocations. Clearing it caused an 11-second stall
|
||||||
|
# on every Shift+A because SnapObj rebuilds a pure-Python BVH tree.
|
||||||
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
||||||
for obj in self.visible_objs:
|
for obj in self.visible_objs:
|
||||||
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
||||||
self.objs_2d_bbox.append(bbox_2d)
|
self.objs_2d_bbox.append(bbox_2d)
|
||||||
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
|
self._init_snapping_points(context, event)
|
||||||
self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
|
|
||||||
tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
|
tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
|
||||||
|
|
||||||
tool.Blender.update_viewport()
|
tool.Blender.update_viewport()
|
||||||
context.window_manager.modal_handler_add(self)
|
context.window_manager.modal_handler_add(self)
|
||||||
|
|
||||||
|
def _init_snapping_points(self, context: bpy.types.Context, event: bpy.types.Event) -> None:
|
||||||
|
"""Populate self.snapping_points at operator start.
|
||||||
|
|
||||||
|
Override in subclasses to skip the full BVH snap detection when a cheap
|
||||||
|
placeholder is sufficient. The default runs the full detection pass.
|
||||||
|
"""
|
||||||
|
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
|
||||||
|
self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
|
||||||
|
|
||||||
|
|||||||
@@ -971,17 +971,25 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
return None
|
return None
|
||||||
for i, snap_obj in enumerate(cls.snap_objs):
|
for i, snap_obj in enumerate(cls.snap_objs):
|
||||||
if obj.name == snap_obj.obj.name:
|
if obj.name == snap_obj.obj.name:
|
||||||
# Handle objects modified while a modal operator is active.
|
# Fast O(1) invalidation: vertex count change (mesh edit) or
|
||||||
# Example: adding a door or window alters the wall geometry.
|
# world matrix change (object moved/rotated).
|
||||||
if len(obj.data.vertices) != len(snap_obj.verts_3d):
|
if len(obj.data.vertices) != len(snap_obj.verts_3d):
|
||||||
cls.snap_objs.pop(i)
|
cls.snap_objs.pop(i)
|
||||||
snap_obj = SnapObj(obj)
|
snap_obj = SnapObj(obj)
|
||||||
cls.snap_objs.append(snap_obj)
|
cls.snap_objs.append(snap_obj)
|
||||||
for v1, v2 in zip(obj.data.vertices, snap_obj.verts_3d):
|
return snap_obj
|
||||||
if (obj.matrix_world @ v1.co) != v2:
|
if obj.matrix_world != snap_obj.matrix_world:
|
||||||
|
cls.snap_objs.pop(i)
|
||||||
|
snap_obj = SnapObj(obj)
|
||||||
|
cls.snap_objs.append(snap_obj)
|
||||||
|
return snap_obj
|
||||||
|
# Sample one vertex to catch mesh edits that preserve vertex count.
|
||||||
|
if obj.data.vertices and snap_obj.verts_3d:
|
||||||
|
if (obj.matrix_world @ obj.data.vertices[0].co) != snap_obj.verts_3d[0]:
|
||||||
cls.snap_objs.pop(i)
|
cls.snap_objs.pop(i)
|
||||||
snap_obj = SnapObj(obj)
|
snap_obj = SnapObj(obj)
|
||||||
cls.snap_objs.append(snap_obj)
|
cls.snap_objs.append(snap_obj)
|
||||||
|
return snap_obj
|
||||||
return snap_obj
|
return snap_obj
|
||||||
snap_obj = SnapObj(obj)
|
snap_obj = SnapObj(obj)
|
||||||
cls.snap_objs.append(snap_obj)
|
cls.snap_objs.append(snap_obj)
|
||||||
@@ -1020,6 +1028,7 @@ class SnapObj:
|
|||||||
self.root = None
|
self.root = None
|
||||||
self._bvh_built = False
|
self._bvh_built = False
|
||||||
self.verts_3d = [obj.matrix_world @ v.co for v in obj.data.vertices]
|
self.verts_3d = [obj.matrix_world @ v.co for v in obj.data.vertices]
|
||||||
|
self.matrix_world = obj.matrix_world.copy()
|
||||||
self.snap_points = []
|
self.snap_points = []
|
||||||
|
|
||||||
def _ensure_bvh(self):
|
def _ensure_bvh(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user