- Working assembly multi :)
This commit is contained in:
@@ -66,6 +66,9 @@ class Viewer3DWidget(QWidget):
|
||||
self._connector_pick_mode: bool = False
|
||||
# Current snap highlight object id (for hover during connector mode).
|
||||
self._connector_snap_id: Optional[str] = None
|
||||
# Throttle connector hover probes to avoid UI lag on fast mouse moves.
|
||||
self._connector_last_hover_time: float = 0.0
|
||||
self._connector_hover_interval: float = 0.05 # 50 ms between probes
|
||||
# When True, left-click on a body activates assembly drag-to-move.
|
||||
self._assembly_move_mode: bool = False
|
||||
# State for ongoing assembly drag.
|
||||
@@ -315,10 +318,17 @@ class Viewer3DWidget(QWidget):
|
||||
def mouseMoveEvent(self, event):
|
||||
self._ensure_initialized()
|
||||
# In connector mode, show snap hover.
|
||||
# Selection modes are deactivated so we skip the idle MoveTo
|
||||
# (dynamic highlighting) — only the gizmo hover handler runs.
|
||||
if self._connector_pick_mode:
|
||||
self._handle_connector_hover(event)
|
||||
super().mouseMoveEvent(event)
|
||||
return
|
||||
# If connector mode was just exited (gizmo persists after pick),
|
||||
# clear any lingering gizmo on first mouse move.
|
||||
gizmo_objs = getattr(self._renderer, '_gizmo_objects', None)
|
||||
if self._connector_snap_id is not None or (gizmo_objs and len(gizmo_objs) > 0):
|
||||
self._clear_connector_snap()
|
||||
# In face-pick mode, keep dynamic highlighting.
|
||||
if self._pick_face_mode:
|
||||
if hasattr(self._renderer, "handle_mouse_move"):
|
||||
@@ -420,20 +430,38 @@ class Viewer3DWidget(QWidget):
|
||||
|
||||
# ─── Connector pick mode (assembly) ────────────────────────────────────
|
||||
|
||||
def set_connector_pick_mode(self, enabled: bool) -> None:
|
||||
def set_connector_pick_mode(self, enabled: bool, clear_gizmo: bool = True) -> None:
|
||||
"""Toggle connector pick mode for placing connection points.
|
||||
|
||||
When enabled, clicking an entity (face, edge, vertex, hole)
|
||||
on a body in the assembly view captures its position and
|
||||
direction as a connection point for the SolveSpace solver.
|
||||
|
||||
Entering connector mode deactivates standard OCC face/edge/vertex
|
||||
selection so dynamic highlighting does not clash with the gizmo
|
||||
visuals. Selection is re-activated on exit.
|
||||
|
||||
*clear_gizmo*: if False the gizmo marker is not cleared on exit,
|
||||
allowing it to persist until the next hover event (used after a
|
||||
successful pick so the user sees what was selected).
|
||||
"""
|
||||
self._connector_pick_mode = bool(enabled)
|
||||
if enabled:
|
||||
self.setCursor(Qt.CrossCursor)
|
||||
elif not self._pick_face_mode:
|
||||
self.unsetCursor()
|
||||
if not enabled:
|
||||
self._clear_connector_snap()
|
||||
# Disable standard OCC selection so gizmo visuals are not
|
||||
# interfered with by dynamic face highlighting.
|
||||
fn = getattr(self._renderer, "deactivate_selection_modes", None)
|
||||
if fn is not None:
|
||||
fn()
|
||||
else:
|
||||
if clear_gizmo:
|
||||
self._clear_connector_snap()
|
||||
# Restore standard OCC selection for face-pick / normal modes.
|
||||
fn = getattr(self._renderer, "activate_selection_modes", None)
|
||||
if fn is not None:
|
||||
fn()
|
||||
if not self._pick_face_mode:
|
||||
self.unsetCursor()
|
||||
|
||||
def is_connector_pick_mode(self) -> bool:
|
||||
return self._connector_pick_mode
|
||||
@@ -453,14 +481,20 @@ class Viewer3DWidget(QWidget):
|
||||
def _handle_connector_hover(self, event) -> None:
|
||||
"""Update the hover snap gizmo during connector pick mode.
|
||||
|
||||
Probes a small neighbourhood around the cursor for ALL nearby snap
|
||||
candidates (vertices, edge midpoints, face centres, hole openings)
|
||||
and renders a dim marker on each plus a bright primary on the nearest
|
||||
one — the general snap indicator. Clicking then selects the
|
||||
primary's position.
|
||||
Uses geometric probing (direct topology walk) which does not depend
|
||||
on OCC's selection system — this avoids clashing with the gizmo
|
||||
visuals since selection modes are deactivated in connector mode.
|
||||
Probes are throttled to at most once every 50 ms to avoid UI lag
|
||||
on fast mouse moves.
|
||||
"""
|
||||
import time
|
||||
now = time.monotonic()
|
||||
if now - self._connector_last_hover_time < self._connector_hover_interval:
|
||||
return # throttled — skip this mouse move
|
||||
self._connector_last_hover_time = now
|
||||
|
||||
self._ensure_initialized()
|
||||
probe = getattr(self._renderer, "probe_snap_candidates", None)
|
||||
probe = getattr(self._renderer, "probe_snap_candidates_geometric", None)
|
||||
pos = event.position().toPoint() if hasattr(event, "position") else event.pos()
|
||||
|
||||
if probe is not None:
|
||||
@@ -472,22 +506,42 @@ class Viewer3DWidget(QWidget):
|
||||
# Primary = the nearest candidate (probe sorts nearest-first).
|
||||
info = candidates[0]
|
||||
else:
|
||||
# Fall back to single-pixel pick on renderers without the probe.
|
||||
picker = getattr(self._renderer, "pick_entity", None)
|
||||
if picker is None:
|
||||
return
|
||||
info = picker(pos.x(), pos.y())
|
||||
candidates = [info] if info else []
|
||||
if info is None or info.get("owner_obj_id") is None:
|
||||
self._clear_connector_snap()
|
||||
self.connectorHover.emit(None)
|
||||
return
|
||||
# Fall back to the selection-system-based probe.
|
||||
probe2 = getattr(self._renderer, "probe_snap_candidates", None)
|
||||
if probe2 is not None:
|
||||
candidates = probe2(pos.x(), pos.y())
|
||||
if not candidates:
|
||||
self._clear_connector_snap()
|
||||
self.connectorHover.emit(None)
|
||||
return
|
||||
info = candidates[0]
|
||||
else:
|
||||
# Last resort: single-pixel pick.
|
||||
picker = getattr(self._renderer, "pick_entity", None)
|
||||
if picker is None:
|
||||
return
|
||||
info = picker(pos.x(), pos.y())
|
||||
candidates = [info] if info else []
|
||||
if info is None or info.get("owner_obj_id") is None:
|
||||
self._clear_connector_snap()
|
||||
self.connectorHover.emit(None)
|
||||
return
|
||||
|
||||
origin = info["position"]
|
||||
normal = info.get("normal")
|
||||
entity_type = info["type"]
|
||||
owner = info.get("owner_obj_id", "")
|
||||
|
||||
# ── Feature recognition ──
|
||||
# Enhance candidates with composite feature info (holes, edge loops, etc.)
|
||||
recognize = getattr(self._renderer, "recognize_composite_features", None)
|
||||
if recognize is not None and candidates:
|
||||
candidates = recognize(candidates, pos.x(), pos.y())
|
||||
info = candidates[0] # re-read primary after enhancement
|
||||
origin = info["position"]
|
||||
normal = info.get("normal")
|
||||
entity_type = info.get("type", info.get("feature_type", entity_type))
|
||||
|
||||
# Show smart entity gizmo — dim candidate markers + bright primary.
|
||||
self._clear_connector_snap()
|
||||
gizmo_fn = getattr(self._renderer, "show_entity_gizmo", None)
|
||||
@@ -513,31 +567,48 @@ class Viewer3DWidget(QWidget):
|
||||
c = colors.get(entity_type, (1.0, 0.6, 0.0))
|
||||
self._connector_snap_id = fn(origin, color=c, size=3.0)
|
||||
|
||||
self.connectorHover.emit({
|
||||
# Build payload with feature recognition info.
|
||||
payload = {
|
||||
"origin": origin,
|
||||
"normal": normal,
|
||||
"type": entity_type,
|
||||
"owner_obj_id": owner,
|
||||
})
|
||||
}
|
||||
# Attach feature info if available.
|
||||
if "feature_type" in info:
|
||||
payload["feature_type"] = info["feature_type"]
|
||||
if "suggestion" in info:
|
||||
payload["suggestion"] = info["suggestion"]
|
||||
if "feature_data" in info:
|
||||
payload["feature_data"] = info["feature_data"]
|
||||
|
||||
self.connectorHover.emit(payload)
|
||||
|
||||
def _handle_connector_pick(self, event) -> None:
|
||||
"""Detect an entity under the click and emit connectorPicked.
|
||||
|
||||
Uses the multi-pixel ``probe_snap_candidates`` so a click selects the
|
||||
Uses geometric probing (direct topology walk) so a click selects the
|
||||
PRIMARY (nearest) snap target — the same one the hover gizmo
|
||||
emphasised. Falls back to single-pixel ``pick_entity`` then to
|
||||
``pick_planar_face`` on renderers without the probe.
|
||||
emphasised. Falls back to selection-system probe, then single-pixel
|
||||
``pick_entity``, then ``pick_planar_face``.
|
||||
"""
|
||||
self._ensure_initialized()
|
||||
pos = event.position().toPoint() if hasattr(event, "position") else event.pos()
|
||||
info: Optional[Dict[str, Any]] = None
|
||||
|
||||
probe = getattr(self._renderer, "probe_snap_candidates", None)
|
||||
probe = getattr(self._renderer, "probe_snap_candidates_geometric", None)
|
||||
if probe is not None:
|
||||
candidates = probe(pos.x(), pos.y())
|
||||
if candidates:
|
||||
info = candidates[0] # nearest = primary
|
||||
|
||||
if info is None:
|
||||
probe2 = getattr(self._renderer, "probe_snap_candidates", None)
|
||||
if probe2 is not None:
|
||||
candidates = probe2(pos.x(), pos.y())
|
||||
if candidates:
|
||||
info = candidates[0]
|
||||
|
||||
if info is None:
|
||||
picker = getattr(self._renderer, "pick_entity", None)
|
||||
if picker is None:
|
||||
|
||||
Reference in New Issue
Block a user