mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Fix coplanar face snap: correct bbox check, on-edge detection, and ForcePerpendicularToFace
- Switch FACE/LAYER mode nearby-object filtering from 3D bbox to 2D screen-space bbox (30px tolerance), fixing walls whose local Y extent doesn't contain the floor hit point (e.g. wall at Z=0 with mesh not quite reaching the floor level). - Also run _snap_on_coplanar_faces on hit_obj itself so the blue outline and IFC snap fire even when the cursor lands exactly on the wall/floor boundary (hit_obj IS the wall, previously skipped). - Store face_normal_world in coplanar face candidates and call build_anchor_from_hit in _build_ifc_anchor for snap=="FACE", so the anchor gets a proper FACE type with normal_local in addr. This enables ForcePerpendicularToFace and LinePosition to work for coplanar edge-on face snaps the same as directly-hit faces. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5831,12 +5831,19 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
"snap_world": (snapped_pt.x, snapped_pt.y, snapped_pt.z),
|
"snap_world": (snapped_pt.x, snapped_pt.y, snapped_pt.z),
|
||||||
"snap": "FACE",
|
"snap": "FACE",
|
||||||
"face_verts": [tuple(v) for v in verts_w],
|
"face_verts": [tuple(v) for v in verts_w],
|
||||||
|
"face_normal_world": (normal_w.x, normal_w.y, normal_w.z),
|
||||||
})
|
})
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _pt_in_obj_bbox(obj, pt_world, tol=1e-3):
|
def _pt_in_obj_bbox(obj, pt_world, tol=1e-3, tol_z=None):
|
||||||
"""Return True if pt_world is inside obj's world-space bounding box."""
|
"""Return True if pt_world is inside obj's world-space bounding box.
|
||||||
|
|
||||||
|
tol_z overrides tol for the Z axis — pass a large value to skip Z
|
||||||
|
checking when Z is already constrained by the caller (e.g. FACE mode).
|
||||||
|
"""
|
||||||
|
if tol_z is None:
|
||||||
|
tol_z = tol
|
||||||
try:
|
try:
|
||||||
local_pt = obj.matrix_world.inverted() @ pt_world
|
local_pt = obj.matrix_world.inverted() @ pt_world
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -5848,7 +5855,7 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
return (
|
return (
|
||||||
min(xs) - tol <= local_pt.x <= max(xs) + tol
|
min(xs) - tol <= local_pt.x <= max(xs) + tol
|
||||||
and min(ys) - tol <= local_pt.y <= max(ys) + tol
|
and min(ys) - tol <= local_pt.y <= max(ys) + tol
|
||||||
and min(zs) - tol <= local_pt.z <= max(zs) + tol
|
and min(zs) - tol_z <= local_pt.z <= max(zs) + tol_z
|
||||||
)
|
)
|
||||||
|
|
||||||
def _compute_ifc_snap_candidate(self, context, event) -> "Optional[dict]":
|
def _compute_ifc_snap_candidate(self, context, event) -> "Optional[dict]":
|
||||||
@@ -5878,6 +5885,13 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
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 = []
|
||||||
|
# Check hit_obj itself first: handles the case where the cursor
|
||||||
|
# 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):
|
||||||
|
hit_elem = tool.Ifc.get_entity(hit_obj)
|
||||||
|
if hit_elem and hasattr(hit_elem, "GlobalId"):
|
||||||
|
for c in self._snap_on_coplanar_faces(hit_obj, hit_pt):
|
||||||
|
nearby_cands.append((c, hit_elem, hit_obj))
|
||||||
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:
|
||||||
@@ -5889,8 +5903,10 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
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
|
||||||
in_bbox = self._pt_in_obj_bbox(obj, hit_pt)
|
sx0, sx1, sy0, sy1 = _bbox2d
|
||||||
if not in_bbox:
|
_SCREEN_TOL = 30
|
||||||
|
if not (sx0 - _SCREEN_TOL <= mx <= sx1 + _SCREEN_TOL and
|
||||||
|
sy0 - _SCREEN_TOL <= my <= sy1 + _SCREEN_TOL):
|
||||||
continue
|
continue
|
||||||
face_cands = self._snap_on_coplanar_faces(obj, hit_pt)
|
face_cands = self._snap_on_coplanar_faces(obj, hit_pt)
|
||||||
nearby_cands.extend((c, extra_elem, obj) for c in face_cands)
|
nearby_cands.extend((c, extra_elem, obj) for c in face_cands)
|
||||||
@@ -5950,7 +5966,8 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
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
|
||||||
if not self._pt_in_obj_bbox(obj, hit_pt):
|
_sx0, _sx1, _sy0, _sy1 = _bbox2d
|
||||||
|
if not (_sx0 - 30 <= mx <= _sx1 + 30 and _sy0 - 30 <= my <= _sy1 + 30):
|
||||||
continue
|
continue
|
||||||
extra_ptr = obj.as_pointer()
|
extra_ptr = obj.as_pointer()
|
||||||
if extra_ptr not in self._snap_cand_multi_cache:
|
if extra_ptr not in self._snap_cand_multi_cache:
|
||||||
@@ -6045,6 +6062,11 @@ 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 == "FACE":
|
||||||
|
hit_location = candidate.get("snap_world", (0.0, 0.0, 0.0))
|
||||||
|
hit_normal = candidate.get("face_normal_world")
|
||||||
|
if hit_normal:
|
||||||
|
return drawing_api.build_anchor_from_hit(file, element, hit_location, hit_normal)
|
||||||
pt = candidate.get("snap_world", (0.0, 0.0, 0.0))
|
pt = candidate.get("snap_world", (0.0, 0.0, 0.0))
|
||||||
return drawing_api.make_world_anchor(list(pt))
|
return drawing_api.make_world_anchor(list(pt))
|
||||||
|
|
||||||
@@ -6061,9 +6083,10 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
|||||||
"snap_world": cand.get("snap_world"),
|
"snap_world": cand.get("snap_world"),
|
||||||
})
|
})
|
||||||
elif cand.get("snap") == "FACE":
|
elif cand.get("snap") == "FACE":
|
||||||
|
fv = cand.get("face_verts", [])
|
||||||
_snap_draw_data.update({
|
_snap_draw_data.update({
|
||||||
"type": "FACE",
|
"type": "FACE",
|
||||||
"face_verts": cand.get("face_verts", []),
|
"face_verts": fv,
|
||||||
"snap_world": cand.get("snap_world"),
|
"snap_world": cand.get("snap_world"),
|
||||||
})
|
})
|
||||||
elif cand.get("snap") == "EDGE":
|
elif cand.get("snap") == "EDGE":
|
||||||
|
|||||||
Reference in New Issue
Block a user