Fix FACE/VERTEX/EDGE snap for thin edge-on walls in DrawParametricDimension

Walls viewed edge-on in plan (2-7 px screen bbox) were never hit by
Blender's raycast, so all three snap modes silently returned nothing.

- FACE: remove has_coplanar_edge Z-gate; vertical faces are now
  snappable regardless of what elevation the native snap lands on
  (sub-floor surfaces at Z~-7.5m were blocking all candidates)
- All modes: replace hardcoded 30 px _FACE_THRESH_D2 with a
  per-candidate max_tol that matches the adaptive _SCREEN_TOL used
  for bbox inclusion (~98 px for 2 px-wide walls)
- VERTEX/EDGE/LAYER: remove early `if not hit_obj: return None`;
  all modes now search objs_2d_bbox with adaptive tolerance when the
  primary raycast misses
- Add _get_mesh_snap_candidates fallback for tessellated elements
  (IfcFacetedBrep etc.) where get_profile_snap_candidates returns []
- Add LOCAL_POINT anchor method (build_anchor_from_local_point +
  resolve_anchor handler) so mesh-derived anchors store element-local
  coords and follow the element through moves/rotations rather than
  becoming free-floating WORLD anchors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ryan Schultz
2026-06-14 11:20:13 -05:00
parent 38dc24336e
commit c92451e0fc
+154 -95
View File
@@ -5798,17 +5798,56 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
# IFC-native snap for LAYER / VERTEX / EDGE modes # IFC-native snap for LAYER / VERTEX / EDGE modes
@staticmethod @staticmethod
def _snap_on_coplanar_faces(obj, hit_pt_world, tol_z=1e-3): def _get_mesh_snap_candidates(obj, snap_mode):
"""Return FACE snap candidates for vertical mesh faces with an edge at hit_pt_world's Z. """Derive VERTEX or EDGE snap candidates directly from the Blender mesh.
Finds faces that are edge-on to the camera (perpendicular to the floor plane) and Fallback for tessellated IFC elements (IfcFacetedBrep, IfcTessellatedFaceSet,
whose bottom edge is coplanar with the hovered floor surface, then projects the etc.) that have no IfcExtrudedAreaSolid, causing get_profile_snap_candidates
hit point onto each such face's plane to get the snap position. to return []. Each candidate carries ``local_m`` (element-local coordinates
in metres) so _build_ifc_anchor can create a LOCAL_POINT anchor that follows
the element through moves and rotations.
"""
mx = obj.matrix_world
mesh = obj.data
candidates = []
if snap_mode == "VERTEX":
for vert in mesh.vertices:
wp = mx @ vert.co
candidates.append({
"type": "VERTEX", "snap": "VERTEX",
"snap_world": (wp.x, wp.y, wp.z),
"local_m": (vert.co.x, vert.co.y, vert.co.z),
})
elif snap_mode == "EDGE":
for edge in mesh.edges:
v0c = mesh.vertices[edge.vertices[0]].co
v1c = mesh.vertices[edge.vertices[1]].co
v0 = mx @ v0c
v1 = mx @ v1c
mid_w = ((v0.x + v1.x) * 0.5, (v0.y + v1.y) * 0.5, (v0.z + v1.z) * 0.5)
mid_l = ((v0c.x + v1c.x) * 0.5, (v0c.y + v1c.y) * 0.5, (v0c.z + v1c.z) * 0.5)
candidates.append({
"type": "EDGE", "snap": "EDGE",
"snap_world": mid_w,
"local_m": mid_l,
"v0": (v0.x, v0.y, v0.z),
"v1": (v1.x, v1.y, v1.z),
})
return candidates
@staticmethod
def _snap_on_coplanar_faces(obj, hit_pt_world, tol_z=1e-3):
"""Return FACE snap candidates by projecting hit_pt onto each vertical face plane.
Accepts any face with a near-horizontal normal (wall-like faces) regardless of
the face's Z elevation. The screen-space bbox filter on the caller side and the
final per-candidate screen-distance gate already prevent false positives, so no
Z-based filtering is needed here. (Z is also irrelevant for 2D annotation
projections.)
""" """
from mathutils import Vector from mathutils import Vector
mx = obj.matrix_world mx = obj.matrix_world
mesh = obj.data mesh = obj.data
target_z = float(hit_pt_world.z)
hit_pt = Vector(hit_pt_world) hit_pt = Vector(hit_pt_world)
candidates = [] candidates = []
for poly in mesh.polygons: for poly in mesh.polygons:
@@ -5817,12 +5856,6 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
continue continue
verts_w = [mx @ mesh.vertices[vi].co for vi in poly.vertices] verts_w = [mx @ mesh.vertices[vi].co for vi in poly.vertices]
n = len(verts_w) n = len(verts_w)
has_coplanar_edge = any(
abs(verts_w[i].z - target_z) <= tol_z and abs(verts_w[(i + 1) % n].z - target_z) <= tol_z
for i in range(n)
)
if not has_coplanar_edge:
continue
face_center_w = sum(verts_w, Vector((0.0, 0.0, 0.0))) / n face_center_w = sum(verts_w, Vector((0.0, 0.0, 0.0))) / n
dist = (hit_pt - face_center_w).dot(normal_w) dist = (hit_pt - face_center_w).dot(normal_w)
snapped_pt = hit_pt - normal_w * dist snapped_pt = hit_pt - normal_w * dist
@@ -5884,14 +5917,14 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
if self._snap_mode == "FACE": if self._snap_mode == "FACE":
if hit_pt is None or not self.objs_2d_bbox: if hit_pt is None or not self.objs_2d_bbox:
return None return None
nearby_cands = [] nearby_cands = [] # (cand, elem, obj, max_screen_tol)
# Check hit_obj itself first: handles the case where the cursor # Check hit_obj itself first: handles the case where the cursor
# lands exactly on the wall/edge boundary (hit_obj IS the wall). # lands exactly on the wall/edge boundary (hit_obj IS the wall).
if hit_obj and hit_obj.data and isinstance(hit_obj.data, bpy.types.Mesh): if hit_obj and hit_obj.data and isinstance(hit_obj.data, bpy.types.Mesh):
hit_elem = tool.Ifc.get_entity(hit_obj) hit_elem = tool.Ifc.get_entity(hit_obj)
if hit_elem and hasattr(hit_elem, "GlobalId"): if hit_elem and hasattr(hit_elem, "GlobalId"):
for c in self._snap_on_coplanar_faces(hit_obj, hit_pt): for c in self._snap_on_coplanar_faces(hit_obj, hit_pt):
nearby_cands.append((c, hit_elem, hit_obj)) nearby_cands.append((c, hit_elem, hit_obj, 30))
extra_count = 0 extra_count = 0
for obj, _bbox2d in self.objs_2d_bbox: for obj, _bbox2d in self.objs_2d_bbox:
if extra_count >= 4: if extra_count >= 4:
@@ -5904,22 +5937,22 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
if not extra_elem or not hasattr(extra_elem, "GlobalId"): if not extra_elem or not hasattr(extra_elem, "GlobalId"):
continue continue
sx0, sx1, sy0, sy1 = _bbox2d sx0, sx1, sy0, sy1 = _bbox2d
_SCREEN_TOL = 30 _SCREEN_TOL = max(30, 100 - min(sx1 - sx0, sy1 - sy0))
if not (sx0 - _SCREEN_TOL <= mx <= sx1 + _SCREEN_TOL and if not (sx0 - _SCREEN_TOL <= mx <= sx1 + _SCREEN_TOL and sy0 - _SCREEN_TOL <= my <= sy1 + _SCREEN_TOL):
sy0 - _SCREEN_TOL <= my <= sy1 + _SCREEN_TOL):
continue continue
face_cands = self._snap_on_coplanar_faces(obj, hit_pt) nearby_cands.extend(
nearby_cands.extend((c, extra_elem, obj) for c in face_cands) (c, extra_elem, obj, _SCREEN_TOL)
for c in self._snap_on_coplanar_faces(obj, hit_pt)
)
extra_count += 1 extra_count += 1
_FACE_THRESH_D2 = 30 * 30
best_cand, best_elem, best_obj, best_d2 = None, None, None, float("inf") best_cand, best_elem, best_obj, best_d2 = None, None, None, float("inf")
for cand, elem, obj in nearby_cands: for cand, elem, obj, max_tol in nearby_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"]))
if sp is None: if sp is None:
continue continue
d2 = (sp.x - mx) ** 2 + (sp.y - my) ** 2 d2 = (sp.x - mx) ** 2 + (sp.y - my) ** 2
if d2 < best_d2 and d2 < _FACE_THRESH_D2: if d2 < best_d2 and d2 < max_tol * max_tol:
best_d2 = d2 best_d2 = d2
best_cand = cand best_cand = cand
best_elem = elem best_elem = elem
@@ -5934,56 +5967,84 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# LAYER / VERTEX / EDGE modes # LAYER / VERTEX / EDGE modes
# ---------------------------------------------------------------- # ----------------------------------------------------------------
if not hit_obj: # Candidates are stored as (cand, elem, obj, max_screen_tol):
return None # - Direct-hit object → max_screen_tol = inf (no distance gate)
element = tool.Ifc.get_entity(hit_obj) # - Nearby objects found via screen bbox → max_screen_tol = _SCREEN_TOL
if not element or not hasattr(element, "GlobalId"): # This lets thin edge-on walls (2-7 px wide bbox, ~98 px tolerance) be
return None # included without relaxing the gate for normal objects.
file = tool.Ifc.get()
all_cands = [] # (cand, elem, obj, max_screen_tol)
# Recompute expensive candidate geometry only when the hovered object changes. # Build initial candidates from the directly-hit object (if any).
obj_ptr = hit_obj.as_pointer() # When hit_obj is None (thin walls are never the raycast target) we skip
if obj_ptr != self._snap_cand_obj_ptr: # this block and rely entirely on the screen-bbox search below.
file = tool.Ifc.get() if hit_obj:
placement_override = {element.id(): np.array(hit_obj.matrix_world)} element = tool.Ifc.get_entity(hit_obj)
if self._snap_mode == "LAYER": if element and hasattr(element, "GlobalId"):
self._snap_cand_cache = drawing_api.get_layer_snap_candidates(file, element, placement_override) obj_ptr = hit_obj.as_pointer()
else: if obj_ptr != self._snap_cand_obj_ptr:
self._snap_cand_cache = drawing_api.get_profile_snap_candidates(file, element, placement_override) placement_override = {element.id(): np.array(hit_obj.matrix_world)}
self._snap_cand_obj_ptr = obj_ptr 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
all_cands = [(c, element, hit_obj, float("inf")) for c in self._snap_cand_cache]
if self._snap_mode == "LAYER": # Extend with candidates from nearby objects using adaptive screen-bbox.
all_layer_cands = [(c, element, hit_obj) for c in self._snap_cand_cache] # Replaces the old 30-px hardcoded check (LAYER) and _pt_in_obj_bbox
if hit_pt is not None and self.objs_2d_bbox: # (VERTEX/EDGE), so thin edge-on walls are reachable in all modes.
file = tool.Ifc.get() if self.objs_2d_bbox:
extra_count = 0 extra_count = 0
for obj, _bbox2d in self.objs_2d_bbox: for obj, _bbox2d in self.objs_2d_bbox:
if extra_count >= 4: if extra_count >= 4:
break break
if obj is hit_obj: if obj is hit_obj:
continue continue
if obj.data is None or not isinstance(obj.data, bpy.types.Mesh): if obj.data is None or not isinstance(obj.data, bpy.types.Mesh):
continue continue
extra_elem = tool.Ifc.get_entity(obj) extra_elem = tool.Ifc.get_entity(obj)
if not extra_elem or not hasattr(extra_elem, "GlobalId"): if not extra_elem or not hasattr(extra_elem, "GlobalId"):
continue continue
_sx0, _sx1, _sy0, _sy1 = _bbox2d sx0, sx1, sy0, sy1 = _bbox2d
if not (_sx0 - 30 <= mx <= _sx1 + 30 and _sy0 - 30 <= my <= _sy1 + 30): _bbox_w = sx1 - sx0
continue _bbox_h = sy1 - sy0
extra_ptr = obj.as_pointer() _SCREEN_TOL = max(30, 100 - min(_bbox_w, _bbox_h))
if extra_ptr not in self._snap_cand_multi_cache: if not (sx0 - _SCREEN_TOL <= mx <= sx1 + _SCREEN_TOL and sy0 - _SCREEN_TOL <= my <= sy1 + _SCREEN_TOL):
placement_override = {extra_elem.id(): np.array(obj.matrix_world)} continue
extra_ptr = obj.as_pointer()
if extra_ptr not in self._snap_cand_multi_cache:
placement_override = {extra_elem.id(): np.array(obj.matrix_world)}
if self._snap_mode == "LAYER":
self._snap_cand_multi_cache[extra_ptr] = drawing_api.get_layer_snap_candidates( self._snap_cand_multi_cache[extra_ptr] = drawing_api.get_layer_snap_candidates(
file, extra_elem, placement_override file, extra_elem, placement_override
) )
all_layer_cands.extend((c, extra_elem, obj) for c in self._snap_cand_multi_cache[extra_ptr]) else:
extra_count += 1 self._snap_cand_multi_cache[extra_ptr] = drawing_api.get_profile_snap_candidates(
file, extra_elem, placement_override
)
ifc_cands = self._snap_cand_multi_cache[extra_ptr]
if ifc_cands:
all_cands.extend((c, extra_elem, obj, _SCREEN_TOL) for c in ifc_cands)
elif self._snap_mode in ("VERTEX", "EDGE"):
# Tessellated element with no IfcExtrudedAreaSolid: fall back to mesh
all_cands.extend(
(c, extra_elem, obj, _SCREEN_TOL)
for c in self._get_mesh_snap_candidates(obj, self._snap_mode)
)
extra_count += 1
if not all_cands:
return None
if self._snap_mode == "LAYER":
best_cand, best_elem, best_obj, best_d2 = None, None, None, float("inf") best_cand, best_elem, best_obj, best_d2 = None, None, None, float("inf")
for cand, elem, obj in all_layer_cands: for cand, elem, obj, max_tol in all_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"]))
if sp is None: if sp is None:
continue continue
d2 = (sp.x - mx) ** 2 + (sp.y - my) ** 2 d2 = (sp.x - mx) ** 2 + (sp.y - my) ** 2
if d2 < best_d2: if d2 < best_d2 and d2 < max_tol * max_tol:
best_d2 = d2 best_d2 = d2
best_cand = cand best_cand = cand
best_elem = elem best_elem = elem
@@ -5996,46 +6057,16 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
result["obj"] = best_obj result["obj"] = best_obj
return result return result
# VERTEX or EDGE — profile-based candidates. # VERTEX or EDGE
# Build a tagged list of (candidate, element, obj) so the best match from
# any object carries the right element reference into _build_ifc_anchor.
all_cands = [(c, element, hit_obj) for c in self._snap_cand_cache]
# Also query nearby objects whose 3D bbox contains the hit point.
if hit_pt is not None and self.objs_2d_bbox:
file = tool.Ifc.get()
extra_count = 0
for obj, _bbox2d in self.objs_2d_bbox:
if extra_count >= 4:
break
if obj is hit_obj:
continue
if obj.data is None or not isinstance(obj.data, bpy.types.Mesh):
continue
extra_elem = tool.Ifc.get_entity(obj)
if not extra_elem or not hasattr(extra_elem, "GlobalId"):
continue
in_bbox = self._pt_in_obj_bbox(obj, hit_pt)
if not in_bbox:
continue
extra_ptr = obj.as_pointer()
if extra_ptr not in self._snap_cand_multi_cache:
placement_override = {extra_elem.id(): np.array(obj.matrix_world)}
self._snap_cand_multi_cache[extra_ptr] = drawing_api.get_profile_snap_candidates(
file, extra_elem, placement_override
)
all_cands.extend((c, extra_elem, obj) for c in self._snap_cand_multi_cache[extra_ptr])
extra_count += 1
best_cand, best_elem, best_obj, best_d2 = None, None, None, float("inf") best_cand, best_elem, best_obj, best_d2 = None, None, None, float("inf")
for cand, elem, obj in all_cands: for cand, elem, obj, max_tol in all_cands:
if cand["type"] != self._snap_mode: if cand["type"] != self._snap_mode:
continue continue
sp = location_3d_to_region_2d(region, rv3d, Vector(cand["snap_world"])) sp = location_3d_to_region_2d(region, rv3d, Vector(cand["snap_world"]))
if sp is None: if sp is None:
continue continue
d2 = (sp.x - mx) ** 2 + (sp.y - my) ** 2 d2 = (sp.x - mx) ** 2 + (sp.y - my) ** 2
if d2 < best_d2: if d2 < best_d2 and d2 < max_tol * max_tol:
best_d2 = d2 best_d2 = d2
best_cand = cand best_cand = cand
best_elem = elem best_elem = elem
@@ -6062,6 +6093,10 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
return drawing_api.build_anchor_from_profile_vert(file, element, candidate) return drawing_api.build_anchor_from_profile_vert(file, element, candidate)
if snap_kind == "EDGE" and candidate.get("profile_x_m") is not None: if snap_kind == "EDGE" and candidate.get("profile_x_m") is not None:
return drawing_api.build_anchor_from_profile_edge(file, element, candidate) return drawing_api.build_anchor_from_profile_edge(file, element, candidate)
if snap_kind in ("VERTEX", "EDGE") and candidate.get("local_m") is not None:
return drawing_api.build_anchor_from_local_point(
element, snap_kind, candidate["snap_world"], candidate["local_m"]
)
if snap_kind == "FACE": if snap_kind == "FACE":
hit_location = candidate.get("snap_world", (0.0, 0.0, 0.0)) hit_location = candidate.get("snap_world", (0.0, 0.0, 0.0))
hit_normal = candidate.get("face_normal_world") hit_normal = candidate.get("face_normal_world")
@@ -6939,6 +6974,8 @@ class SetDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
# Scene-BVH pierce-through: O(log N) vs the previous O(N) per-object loop. # Scene-BVH pierce-through: O(log N) vs the previous O(N) per-object loop.
# Each iteration steps past the last hit surface to reach the next object. # Each iteration steps past the last hit surface to reach the next object.
_DBG_GUIDS = {"1kGw8dvBT2zgE3OsifqnY8", "3YfgKSYh971wjlK2f3vaxy"}
direct: list = [] direct: list = []
ray_hit_objs: set = set() # all IFC objects the ray passed through (any face) ray_hit_objs: set = set() # all IFC objects the ray passed through (any face)
ray_origin = Vector(origin) ray_origin = Vector(origin)
@@ -6952,13 +6989,20 @@ class SetDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
break break
ray_origin = loc_w + direction * _EPS ray_origin = loc_w + direction * _EPS
ifc_obj = getattr(hit_obj_eval, "original", hit_obj_eval) ifc_obj = getattr(hit_obj_eval, "original", hit_obj_eval)
_dbg_guid = getattr(tool.Ifc.get_entity(ifc_obj), "GlobalId", None)
if _dbg_guid in _DBG_GUIDS:
print(f"[dbg-ray] hit {_dbg_guid} obj={ifc_obj.name}")
if ifc_obj == self._annotation_obj: if ifc_obj == self._annotation_obj:
if _dbg_guid in _DBG_GUIDS: print(f"[dbg-ray] {_dbg_guid} SKIP: is annotation obj")
continue continue
if _is_hidden(ifc_obj): if _is_hidden(ifc_obj):
if _dbg_guid in _DBG_GUIDS: print(f"[dbg-ray] {_dbg_guid} SKIP: hidden h={ifc_obj.hide_get(view_layer=view_layer)} hv={ifc_obj.hide_viewport} vis={ifc_obj.visible_get()}")
continue continue
if ifc_obj.type != "MESH": if ifc_obj.type != "MESH":
if _dbg_guid in _DBG_GUIDS: print(f"[dbg-ray] {_dbg_guid} SKIP: type={ifc_obj.type}")
continue continue
if not tool.Ifc.get_entity(ifc_obj): if not tool.Ifc.get_entity(ifc_obj):
if _dbg_guid in _DBG_GUIDS: print(f"[dbg-ray] {_dbg_guid} SKIP: no IFC entity")
continue continue
ray_hit_objs.add(ifc_obj) # track even if face is non-perp ray_hit_objs.add(ifc_obj) # track even if face is non-perp
mx = ifc_obj.matrix_world mx = ifc_obj.matrix_world
@@ -6972,8 +7016,10 @@ class SetDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
else nrm_w.normalized() else nrm_w.normalized()
) )
if not _face_perp_ok(normal): if not _face_perp_ok(normal):
if _dbg_guid in _DBG_GUIDS: print(f"[dbg-ray] {_dbg_guid} SKIP: face not perp normal={normal} dot={abs(normal.dot(_face_cam_view)) if _face_cam_view else 'N/A'}")
continue continue
dist = (loc_w - origin).length dist = (loc_w - origin).length
if _dbg_guid in _DBG_GUIDS: print(f"[dbg-ray] {_dbg_guid} ACCEPTED dist={dist:.4f}")
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])
@@ -6997,19 +7043,26 @@ class SetDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
prox: list = [] prox: list = []
for ifc_obj in context.scene.objects: for ifc_obj in context.scene.objects:
_dbg_guid2 = getattr(tool.Ifc.get_entity(ifc_obj), "GlobalId", None)
_is_dbg = _dbg_guid2 in _DBG_GUIDS
if ifc_obj == self._annotation_obj: if ifc_obj == self._annotation_obj:
if _is_dbg: print(f"[dbg-prox] {_dbg_guid2} SKIP: is annotation obj")
continue continue
if _is_hidden(ifc_obj): if _is_hidden(ifc_obj):
if _is_dbg: print(f"[dbg-prox] {_dbg_guid2} SKIP: hidden")
continue continue
elem = tool.Ifc.get_entity(ifc_obj) elem = tool.Ifc.get_entity(ifc_obj)
if not elem: if not elem:
if _is_dbg: print(f"[dbg-prox] {ifc_obj.name} SKIP: no IFC entity")
continue continue
if ifc_obj.type != "MESH": if ifc_obj.type != "MESH":
if _is_dbg: print(f"[dbg-prox] {_dbg_guid2} SKIP: type={ifc_obj.type}")
continue continue
mx = ifc_obj.matrix_world mx = ifc_obj.matrix_world
try: try:
mx_inv = mx.inverted() mx_inv = mx.inverted()
except Exception: except Exception:
if _is_dbg: print(f"[dbg-prox] {_dbg_guid2} SKIP: matrix not invertible")
continue continue
bb_world = [mx @ Vector(c) for c in ifc_obj.bound_box] bb_world = [mx @ Vector(c) for c in ifc_obj.bound_box]
bb_proj = [_perp(v) for v in bb_world] bb_proj = [_perp(v) for v in bb_world]
@@ -7018,7 +7071,9 @@ class SetDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
sy = max(min(v.y for v in bb_proj) - op.y, 0.0, op.y - max(v.y for v in bb_proj)) sy = max(min(v.y for v in bb_proj) - op.y, 0.0, op.y - max(v.y for v in bb_proj))
sz = max(min(v.z for v in bb_proj) - op.z, 0.0, op.z - max(v.z for v in bb_proj)) sz = max(min(v.z for v in bb_proj) - op.z, 0.0, op.z - max(v.z for v in bb_proj))
perp_dist = _math.sqrt(sx * sx + sy * sy + sz * sz) perp_dist = _math.sqrt(sx * sx + sy * sy + sz * sz)
if _is_dbg: print(f"[dbg-prox] {_dbg_guid2} perp_dist={perp_dist:.4f} TOL={TOL}")
if perp_dist > TOL: if perp_dist > TOL:
if _is_dbg: print(f"[dbg-prox] {_dbg_guid2} SKIP: perp_dist too large")
continue continue
bb_ctr = sum((v for v in bb_world), Vector()) / 8 bb_ctr = sum((v for v in bb_world), Vector()) / 8
t = (bb_ctr - origin).dot(direction) t = (bb_ctr - origin).dot(direction)
@@ -7026,15 +7081,19 @@ class SetDimensionAnchor(bpy.types.Operator, tool.Ifc.Operator):
try: try:
found, loc_l, nrm_l, fi = ifc_obj.closest_point_on_mesh(mx_inv @ query_w, distance=100.0) found, loc_l, nrm_l, fi = ifc_obj.closest_point_on_mesh(mx_inv @ query_w, distance=100.0)
except RuntimeError: except RuntimeError:
if _is_dbg: print(f"[dbg-prox] {_dbg_guid2} SKIP: closest_point_on_mesh RuntimeError")
continue continue
if not found: if not found:
if _is_dbg: print(f"[dbg-prox] {_dbg_guid2} SKIP: closest_point_on_mesh not found")
continue continue
loc_w = mx @ loc_l loc_w = mx @ loc_l
if self._snap_mode != "FACE": if self._snap_mode != "FACE":
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 (mx.to_3x3() @ nrm_l).normalized()
if not _face_perp_ok(normal): if not _face_perp_ok(normal):
if _is_dbg: print(f"[dbg-prox] {_dbg_guid2} SKIP: face not perp normal={normal} dot={abs(normal.dot(_face_cam_view)) if _face_cam_view else 'N/A'}")
continue continue
if _is_dbg: print(f"[dbg-prox] {_dbg_guid2} ACCEPTED perp_dist={perp_dist:.4f} ray_hit={ifc_obj in ray_hit_objs}")
prox.append((perp_dist, ifc_obj, ifc_obj, mx, loc_w, normal, fi)) prox.append((perp_dist, ifc_obj, ifc_obj, mx, loc_w, normal, fi))
# Objects the ray directly passed through get priority over objects that # Objects the ray directly passed through get priority over objects that