- Working assembly multi :)
This commit is contained in:
@@ -1334,25 +1334,6 @@ class OCCRenderer(Renderer):
|
||||
|
||||
return []
|
||||
|
||||
def _project_to_screen(self, p3d: Tuple[float, float, float]) -> Optional[Tuple[int, int]]:
|
||||
"""Project a 3D world point to (x, y) screen pixel.
|
||||
|
||||
Uses OCC's ``V3d_View.Convert`` (world → view coords). Returns None
|
||||
if the projection fails (e.g. behind the camera).
|
||||
"""
|
||||
if self._view is None:
|
||||
return None
|
||||
try:
|
||||
# OCC's Convert returns the window pixel coordinates.
|
||||
xpix = self._view.Convert(float(p3d[0]), float(p3d[1]), float(p3d[2]))
|
||||
# Some OCP builds return a tuple (x, y); others return two values.
|
||||
if isinstance(xpix, (tuple, list)) and len(xpix) == 2:
|
||||
return (int(xpix[0]), int(xpix[1]))
|
||||
return None
|
||||
except Exception:
|
||||
# Fall back to ConvertWithProj or ProjTexte if Convert is unavailable.
|
||||
return None
|
||||
|
||||
def probe_snap_candidates(
|
||||
self, x: int, y: int, radius: int = 30,
|
||||
) -> List[Dict[str, Any]]:
|
||||
@@ -1429,8 +1410,8 @@ class OCCRenderer(Renderer):
|
||||
results.sort(key=lambda c: (c.get("screen", (x, y))[0] - x) ** 2 + (c.get("screen", (x, y))[1] - y) ** 2)
|
||||
return results
|
||||
|
||||
def highlight_snap(self, position, color=None, size=3.0) -> Optional[str]:
|
||||
"""Show a small marker sphere at *position* as a snap indicator.
|
||||
def highlight_snap(self, position, color=None, size=6.0) -> Optional[str]:
|
||||
"""Show a marker sphere at *position* as a snap indicator.
|
||||
|
||||
Returns an object id that can be removed later.
|
||||
The *size* is auto-scaled by camera distance so the marker stays
|
||||
@@ -1454,7 +1435,7 @@ class OCCRenderer(Renderer):
|
||||
self._view.Update()
|
||||
# Track this as a temporary object; use a synthetic id.
|
||||
oid = f"__snap_{id(ais)}"
|
||||
self._objects[oid] = _RenderObject(oid, ais, None, None)
|
||||
self._objects[oid] = OCCRenderObject(obj_id=oid, ais_shape=ais, ais_type="snap")
|
||||
return oid
|
||||
except Exception as exc:
|
||||
logger.debug(f"highlight_snap failed: {exc}")
|
||||
@@ -1596,13 +1577,13 @@ class OCCRenderer(Renderer):
|
||||
):
|
||||
continue
|
||||
cc = default_colors.get(cand.get("type", ""), (0.7, 0.7, 0.7))
|
||||
_make_sphere(cpos, cc, 1.4 * gizmo_scale) # dim, small
|
||||
_make_sphere(cpos, cc, 2.8 * gizmo_scale) # dim, small
|
||||
|
||||
# ── 1. Bright primary marker (sphere) ──
|
||||
_make_sphere(position, gizmo_color, 2.8 * gizmo_scale)
|
||||
_make_sphere(position, gizmo_color, 5.6 * gizmo_scale)
|
||||
|
||||
# ── 2. Axis indicator lines (primary only) ──
|
||||
axis_length = 15.0 * gizmo_scale
|
||||
axis_length = 30.0 * gizmo_scale
|
||||
|
||||
def _make_axis_line(
|
||||
origin: Tuple[float, float, float],
|
||||
@@ -1690,6 +1671,334 @@ class OCCRenderer(Renderer):
|
||||
if self._view is not None:
|
||||
self._view.Update()
|
||||
|
||||
# ─── Selection mode control ───────────────────────────────────────────
|
||||
#
|
||||
# When connector gizmo mode is active, standard OCC face/edge/vertex
|
||||
# selection is deactivated so dynamic highlighting does not interfere
|
||||
# with the gizmo visuals. The geometric probing method below replaces
|
||||
# the selection-system-based probe.
|
||||
|
||||
def deactivate_selection_modes(self) -> None:
|
||||
"""Deactivate OCC face/edge/vertex selection on every tracked AIS shape.
|
||||
|
||||
Used when entering connector gizmo mode so that standard dynamic
|
||||
highlighting (MoveTo) does not interfere with the gizmo visuals.
|
||||
Call :meth:`activate_selection_modes` to restore.
|
||||
"""
|
||||
if self._context is None:
|
||||
return
|
||||
from OCP.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
|
||||
from OCP.AIS import AIS_Shape
|
||||
for robj in self._objects.values():
|
||||
if robj.ais_shape is not None:
|
||||
for topo in (TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE):
|
||||
mode = AIS_Shape.SelectionMode_s(topo)
|
||||
try:
|
||||
self._context.Deactivate(robj.ais_shape, mode)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Selection modes deactivated for all AIS shapes")
|
||||
|
||||
def activate_selection_modes(self) -> None:
|
||||
"""Re-activate OCC face/edge/vertex selection on every tracked AIS shape.
|
||||
|
||||
Called when exiting connector gizmo mode to restore normal
|
||||
dynamic highlighting and face-pick behaviour.
|
||||
"""
|
||||
if self._context is None:
|
||||
return
|
||||
from OCP.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
|
||||
from OCP.AIS import AIS_Shape
|
||||
for robj in self._objects.values():
|
||||
if robj.ais_shape is not None:
|
||||
for topo in (TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE):
|
||||
mode = AIS_Shape.SelectionMode_s(topo)
|
||||
try:
|
||||
self._context.Activate(robj.ais_shape, mode)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("Selection modes re-activated for all AIS shapes")
|
||||
|
||||
# ─── Geometric snap probing (selection-system-independent) ────────────
|
||||
#
|
||||
# Walks every AIS shape's topology directly, projects each feature to
|
||||
# screen, and returns candidates within *radius* pixels of the cursor.
|
||||
# This replaces the MoveTo-based probe when selection modes are
|
||||
# deactivated (connector gizmo mode).
|
||||
|
||||
def _project_to_screen(self, p3d: Tuple[float, float, float]) -> Optional[Tuple[int, int]]:
|
||||
"""Project a 3D world point to (x, y) screen pixel.
|
||||
|
||||
Uses OCC's ``V3d_View.Convert`` (world → view coords). Returns None
|
||||
if the projection fails (e.g. behind the camera).
|
||||
"""
|
||||
if self._view is None:
|
||||
return None
|
||||
try:
|
||||
# OCC's Convert returns the window pixel coordinates.
|
||||
xpix = self._view.Convert(float(p3d[0]), float(p3d[1]), float(p3d[2]))
|
||||
# Some OCP builds return a tuple (x, y); others return two values.
|
||||
if isinstance(xpix, (tuple, list)) and len(xpix) == 2:
|
||||
return (int(xpix[0]), int(xpix[1]))
|
||||
return None
|
||||
except Exception:
|
||||
# Fall back to ConvertWithProj or ProjTexte if Convert is unavailable.
|
||||
return None
|
||||
|
||||
def probe_snap_candidates_geometric(
|
||||
self, x: int, y: int, radius: int = 30,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Probe snap candidates by iterating geometry directly (no selection system).
|
||||
|
||||
Uses a two-pass approach for performance:
|
||||
1. **Bounding-box pre-filter**: projects each shape's 3D bbox to screen;
|
||||
skips shapes whose screen bbox is far from the cursor.
|
||||
2. **Feature iteration**: for nearby shapes only, walks faces/edges/vertices,
|
||||
projects each feature to screen, and collects candidates within
|
||||
*radius* pixels.
|
||||
|
||||
This replaces ``probe_snap_candidates`` when selection modes are
|
||||
deactivated (connector gizmo mode).
|
||||
"""
|
||||
if self._view is None or self._context is None:
|
||||
return []
|
||||
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX
|
||||
from OCP.TopoDS import TopoDS
|
||||
from OCP.Bnd import Bnd_Box
|
||||
from OCP.BRepBndLib import BRepBndLib
|
||||
import numpy as np
|
||||
|
||||
candidates: Dict[Tuple[str, str, Tuple[int, int, int]], Dict[str, Any]] = {}
|
||||
# Expand the search radius for the bbox pre-filter so features near
|
||||
# the screen edge of a shape are not missed.
|
||||
margin = radius + 40
|
||||
|
||||
for robj in self._objects.values():
|
||||
if robj.ais_shape is None:
|
||||
continue
|
||||
try:
|
||||
shape = robj.ais_shape.Shape()
|
||||
except Exception:
|
||||
continue
|
||||
if shape is None:
|
||||
continue
|
||||
|
||||
# ── Pass 0: bounding-box pre-filter ──
|
||||
# Project the shape's 3D AABB to screen. If the cursor is
|
||||
# outside the screen bbox (with margin), skip this shape entirely.
|
||||
try:
|
||||
bbox = Bnd_Box()
|
||||
BRepBndLib.Add_s(shape, bbox)
|
||||
if bbox.IsVoid():
|
||||
continue
|
||||
bx0, by0, bz0, bx1, by1, bz1 = bbox.Get()
|
||||
# Project the 8 AABB corners to screen.
|
||||
corners = [
|
||||
(bx0, by0, bz0), (bx1, by0, bz0),
|
||||
(bx0, by1, bz0), (bx1, by1, bz0),
|
||||
(bx0, by0, bz1), (bx1, by0, bz1),
|
||||
(bx0, by1, bz1), (bx1, by1, bz1),
|
||||
]
|
||||
sx_min, sy_min = 99999, 99999
|
||||
sx_max, sy_max = -99999, -99999
|
||||
all_behind = True
|
||||
for c in corners:
|
||||
sp = self._project_to_screen(c)
|
||||
if sp is not None:
|
||||
all_behind = False
|
||||
sx_min = min(sx_min, sp[0])
|
||||
sy_min = min(sy_min, sp[1])
|
||||
sx_max = max(sx_max, sp[0])
|
||||
sy_max = max(sy_max, sp[1])
|
||||
if all_behind:
|
||||
continue
|
||||
# Check if cursor is within margin of the screen bbox.
|
||||
if (x < sx_min - margin or x > sx_max + margin or
|
||||
y < sy_min - margin or y > sy_max + margin):
|
||||
continue
|
||||
except Exception:
|
||||
pass # If bbox fails, fall through and try features.
|
||||
|
||||
# ── Pass 1: iterate only nearby shapes ──
|
||||
# --- Faces ---
|
||||
face_expl = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while face_expl.More():
|
||||
face = TopoDS.Face_s(face_expl.Current())
|
||||
infos = self._classify_detected_shape(face, robj.obj_id)
|
||||
for info in infos:
|
||||
pos = info.get("position") or (0.0, 0.0, 0.0)
|
||||
sp = self._project_to_screen(pos)
|
||||
if sp is None:
|
||||
continue
|
||||
dist2 = (sp[0] - x) ** 2 + (sp[1] - y) ** 2
|
||||
if dist2 <= radius * radius:
|
||||
key = (
|
||||
info.get("owner_obj_id", ""),
|
||||
info.get("type", ""),
|
||||
(round(pos[0], 1), round(pos[1], 1), round(pos[2], 1)),
|
||||
)
|
||||
if key not in candidates:
|
||||
info["screen"] = sp
|
||||
candidates[key] = info
|
||||
face_expl.Next()
|
||||
|
||||
# --- Edges ---
|
||||
edge_expl = TopExp_Explorer(shape, TopAbs_EDGE)
|
||||
while edge_expl.More():
|
||||
edge = TopoDS.Edge_s(edge_expl.Current())
|
||||
infos = self._classify_detected_shape(edge, robj.obj_id)
|
||||
for info in infos:
|
||||
pos = info.get("position") or (0.0, 0.0, 0.0)
|
||||
sp = self._project_to_screen(pos)
|
||||
if sp is None:
|
||||
continue
|
||||
dist2 = (sp[0] - x) ** 2 + (sp[1] - y) ** 2
|
||||
if dist2 <= radius * radius:
|
||||
key = (
|
||||
info.get("owner_obj_id", ""),
|
||||
info.get("type", ""),
|
||||
(round(pos[0], 1), round(pos[1], 1), round(pos[2], 1)),
|
||||
)
|
||||
if key not in candidates:
|
||||
info["screen"] = sp
|
||||
candidates[key] = info
|
||||
edge_expl.Next()
|
||||
|
||||
# --- Vertices ---
|
||||
vert_expl = TopExp_Explorer(shape, TopAbs_VERTEX)
|
||||
while vert_expl.More():
|
||||
vertex = TopoDS.Vertex_s(vert_expl.Current())
|
||||
infos = self._classify_detected_shape(vertex, robj.obj_id)
|
||||
for info in infos:
|
||||
pos = info.get("position") or (0.0, 0.0, 0.0)
|
||||
sp = self._project_to_screen(pos)
|
||||
if sp is None:
|
||||
continue
|
||||
dist2 = (sp[0] - x) ** 2 + (sp[1] - y) ** 2
|
||||
if dist2 <= radius * radius:
|
||||
key = (
|
||||
info.get("owner_obj_id", ""),
|
||||
info.get("type", ""),
|
||||
(round(pos[0], 1), round(pos[1], 1), round(pos[2], 1)),
|
||||
)
|
||||
if key not in candidates:
|
||||
info["screen"] = sp
|
||||
candidates[key] = info
|
||||
vert_expl.Next()
|
||||
|
||||
# Sort by screen-space distance to cursor, nearest first.
|
||||
results = list(candidates.values())
|
||||
results.sort(
|
||||
key=lambda c: (c.get("screen", (x, y))[0] - x) ** 2
|
||||
+ (c.get("screen", (x, y))[1] - y) ** 2
|
||||
)
|
||||
return results
|
||||
|
||||
def recognize_composite_features(
|
||||
self, candidates: List[Dict[str, Any]], x: int, y: int, radius: int = 30
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Enhance raw entity candidates with composite feature recognition.
|
||||
|
||||
Groups nearby entities and recognizes composite features like:
|
||||
* **hole** — cylindrical face (bolt/shaft insertion point)
|
||||
* **edge_loop** — circular edge loop (alignment target)
|
||||
* **meeting_edges** — vertex shared by two edges (corner constraint)
|
||||
* **mating_surface** — large planar face (assembly plane)
|
||||
|
||||
Each candidate gets additional fields:
|
||||
* ``feature_type`` — composite feature name (e.g. "hole", "edge_loop")
|
||||
* ``suggestion`` — human-readable snap suggestion
|
||||
* ``feature_data`` — dict with feature-specific info (radius, axis, etc.)
|
||||
"""
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
|
||||
# Group candidates by owner_obj_id.
|
||||
by_owner: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
||||
for c in candidates:
|
||||
owner = c.get("owner_obj_id", "")
|
||||
if owner:
|
||||
by_owner[owner].append(c)
|
||||
|
||||
enhanced: List[Dict[str, Any]] = []
|
||||
|
||||
for c in candidates:
|
||||
ec = dict(c) # copy
|
||||
etype = c.get("type", "")
|
||||
pos = c.get("position", (0, 0, 0))
|
||||
owner = c.get("owner_obj_id", "")
|
||||
|
||||
# ── Cylindrical face → hole / bolt insertion ──
|
||||
if etype == "cylindrical_face":
|
||||
ec["feature_type"] = "hole"
|
||||
ec["suggestion"] = "Bolt / shaft insertion point"
|
||||
ec["feature_data"] = {
|
||||
"axis": c.get("normal"),
|
||||
"radius": c.get("radius"),
|
||||
"center": pos,
|
||||
}
|
||||
enhanced.append(ec)
|
||||
continue
|
||||
|
||||
# ── Planar face → mating surface ──
|
||||
if etype == "planar_face":
|
||||
ec["feature_type"] = "mating_surface"
|
||||
ec["suggestion"] = "Assembly mating plane"
|
||||
ec["feature_data"] = {
|
||||
"normal": c.get("normal"),
|
||||
"center": pos,
|
||||
}
|
||||
enhanced.append(ec)
|
||||
continue
|
||||
|
||||
# ── Edge → check for circular edge loop ──
|
||||
if etype == "edge":
|
||||
# Look for other edges nearby that might form a loop.
|
||||
nearby_edges = [
|
||||
n for n in candidates
|
||||
if n.get("type") == "edge"
|
||||
and n.get("owner_obj_id") == owner
|
||||
and n is not c
|
||||
]
|
||||
# For now, mark as edge — loop detection is complex.
|
||||
ec["feature_type"] = "edge"
|
||||
ec["suggestion"] = "Edge midpoint snap"
|
||||
ec["feature_data"] = {
|
||||
"tangent": c.get("normal"),
|
||||
"midpoint": pos,
|
||||
}
|
||||
enhanced.append(ec)
|
||||
continue
|
||||
|
||||
# ── Vertex → check for meeting edges ──
|
||||
if etype == "vertex":
|
||||
# Look for edges that share this vertex (nearby edges).
|
||||
nearby_edges = [
|
||||
n for n in candidates
|
||||
if n.get("type") == "edge"
|
||||
and n.get("owner_obj_id") == owner
|
||||
]
|
||||
if len(nearby_edges) >= 2:
|
||||
ec["feature_type"] = "meeting_edges"
|
||||
ec["suggestion"] = "Corner constraint (vertex)"
|
||||
ec["feature_data"] = {
|
||||
"vertex": pos,
|
||||
"edge_count": len(nearby_edges),
|
||||
}
|
||||
else:
|
||||
ec["feature_type"] = "vertex"
|
||||
ec["suggestion"] = "Vertex snap"
|
||||
ec["feature_data"] = {"vertex": pos}
|
||||
enhanced.append(ec)
|
||||
continue
|
||||
|
||||
# Fallback: pass through unchanged.
|
||||
enhanced.append(ec)
|
||||
|
||||
return enhanced
|
||||
|
||||
# ─── Mouse / keyboard event forwarding ──────────────────────────────
|
||||
#
|
||||
# CAD-style navigation:
|
||||
|
||||
Reference in New Issue
Block a user