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": "FACE",
|
||||
"face_verts": [tuple(v) for v in verts_w],
|
||||
"face_normal_world": (normal_w.x, normal_w.y, normal_w.z),
|
||||
})
|
||||
return candidates
|
||||
|
||||
@staticmethod
|
||||
def _pt_in_obj_bbox(obj, pt_world, tol=1e-3):
|
||||
"""Return True if pt_world is inside obj's world-space bounding box."""
|
||||
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.
|
||||
|
||||
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:
|
||||
local_pt = obj.matrix_world.inverted() @ pt_world
|
||||
except Exception:
|
||||
@@ -5848,7 +5855,7 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
||||
return (
|
||||
min(xs) - tol <= local_pt.x <= max(xs) + 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]":
|
||||
@@ -5878,6 +5885,13 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
||||
if hit_pt is None or not self.objs_2d_bbox:
|
||||
return None
|
||||
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
|
||||
for obj, _bbox2d in self.objs_2d_bbox:
|
||||
if extra_count >= 4:
|
||||
@@ -5889,8 +5903,10 @@ class DrawParametricDimension(bpy.types.Operator, PolylineOperator, tool.Ifc.Ope
|
||||
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:
|
||||
sx0, sx1, sy0, sy1 = _bbox2d
|
||||
_SCREEN_TOL = 30
|
||||
if not (sx0 - _SCREEN_TOL <= mx <= sx1 + _SCREEN_TOL and
|
||||
sy0 - _SCREEN_TOL <= my <= sy1 + _SCREEN_TOL):
|
||||
continue
|
||||
face_cands = self._snap_on_coplanar_faces(obj, hit_pt)
|
||||
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)
|
||||
if not extra_elem or not hasattr(extra_elem, "GlobalId"):
|
||||
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
|
||||
extra_ptr = obj.as_pointer()
|
||||
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)
|
||||
if snap_kind == "EDGE" and candidate.get("profile_x_m") is not None:
|
||||
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))
|
||||
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"),
|
||||
})
|
||||
elif cand.get("snap") == "FACE":
|
||||
fv = cand.get("face_verts", [])
|
||||
_snap_draw_data.update({
|
||||
"type": "FACE",
|
||||
"face_verts": cand.get("face_verts", []),
|
||||
"face_verts": fv,
|
||||
"snap_world": cand.get("snap_world"),
|
||||
})
|
||||
elif cand.get("snap") == "EDGE":
|
||||
|
||||
Reference in New Issue
Block a user