- Working assembly multi :)

This commit is contained in:
bklronin
2026-07-11 21:29:58 +02:00
parent 2b2afbc479
commit d7e5929a13
5 changed files with 812 additions and 197 deletions
+98 -27
View File
@@ -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: