mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 06:58:56 +00:00
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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user