docs(arch): codify the unified snapping/modifier convention + enforce in review
The snapping model (Shift = cycle mode, Alt = force/free, mode-driven reads via isGridSnapActive/isMagneticSnapActive/isAngleSnapActive, snapProfile-declared context) lived only in code and the plan; tools.md still preached the legacy "Shift = bypass snapping". Close the drift so the architecture review refuses tool changes that revert to the old pattern: - tools.md: replace the held-Shift-bypass manipulation policy with the unified mode-driven model + the single snap read path. - interaction-scope.md: new "Snapping mode & modifiers" section (contexts, read path, modifiers, the chip-needs-a-scope rule) + a Rules bullet + the known-legacy MEP movers (migrate-on-touch) incl. the dual-path constraint (a bespoke mover must not open a `moving` scope — it re-mounts the generic mover via useMovingNode). - review-architecture skill: add interaction-scope.md to the reads and a new "F. Interaction scope, snapping & modifiers" checklist — new shiftKey-bypass, ungated grid step, missing snapProfile, a new useEditor interaction flag, or a bespoke mover opening a moving scope are blockers; touching the legacy MEP movers forces migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1de1923e47
commit
f0206dc6eb
+24
-21
@@ -68,27 +68,30 @@ export function MyTool() {
|
||||
- The offset must be cleared on tool unmount, cancel, *and* commit — both `mesh.position.set(0, 0, 0)` and `useLiveTransforms.clear(id)`.
|
||||
- The tool must not generate or mutate geometry in this path — only transform writes. Geometry generation still belongs in a core system.
|
||||
- **No business logic in tools** — delegate geometry/constraint rules to core systems.
|
||||
- **Guided manipulation is the default.** Placement, move, rotate, resize, endpoint drag,
|
||||
and handle drag should behave as guided building mode: they help the user build quickly
|
||||
with fewer mistakes through grid/object snapping, canonical angle increments,
|
||||
alignment guides, and distance feedback. Holding Shift is the standard live bypass for
|
||||
those constraints: while Shift is held, tools should commit the raw pointer/angle
|
||||
proposal instead of applying sticky snap or angle corrections. Passive measurement
|
||||
guides may remain visible only when they do not alter the proposal. If an interaction
|
||||
cannot use Shift because of an established shortcut or topology rule, document the
|
||||
opt-out in its manipulation policy and explain the replacement behavior.
|
||||
- **Constraints and guides can be decoupled.** When a stronger constraint owns the
|
||||
proposal, such as a wall segment's 15° angle lock, the tool may still publish passive
|
||||
dashed alignment/proximity guides as long as it does not apply the guide snap delta.
|
||||
Use this for chained wall segments: users keep the fast constrained draft, but still see
|
||||
proximity feedback for later points. Shift remains the hard bypass for both correction
|
||||
and guide feedback.
|
||||
- **Help must mirror manipulation policy.** The shortcut dialog and floating helper panel
|
||||
are part of the interaction contract. Static shortcut docs should describe guided
|
||||
building as the default and Shift as the live bypass. Floating help should be contextual
|
||||
when enough state exists: Select mode can derive direct move, direct rotate,
|
||||
multi-select, and Shift-bypass tips from the selected nodes and active modifiers; active
|
||||
tools can highlight the Shift bypass row while the modifier is held.
|
||||
- **Snapping is mode-driven, not a held-Shift bypass.** Placement, move, rotate, resize,
|
||||
endpoint drag, and handle drag are guided building — grid/object snapping, canonical angle
|
||||
increments, alignment guides, distance feedback — but the active behaviour is an explicit,
|
||||
always-visible, **per-context** mode (the contextual HUD chip), not a hidden held key:
|
||||
- **Shift (tap)** cycles the snapping mode for the active context (`wall` grid/lines/angles/off ·
|
||||
`item` lines/grid/off · `polygon` grid/lines/off — one persisted mode per context).
|
||||
- **Alt (hold)** is force / free: commit the raw cursor past snap *and* past an invalid /
|
||||
colliding drop. It is the only momentary "bypass" key (plus the vertical-riser carve-out for MEP runs).
|
||||
- **Ctrl (tap)** cycles the grid step.
|
||||
- Read snapping through the single path — `isGridSnapActive()` / `isMagneticSnapActive()` /
|
||||
`isAngleSnapActive()` (`store/use-editor`), which resolve the active mode from the interaction
|
||||
scope via `getActiveSnapContext()`. **Never** read `event.shiftKey` / `event.nativeEvent.shiftKey` /
|
||||
`modifiers.shiftKey` to bypass snapping, and never apply a grid step that isn't gated on
|
||||
`isGridSnapActive()` (`const step = isGridSnapActive() ? gridSnapStep : 0`). A snappable kind declares
|
||||
`NodeDefinition.snapProfile` (`'item' | 'structural'`) so its context, mode-set, and chip fall out
|
||||
with no per-kind switch. See [interaction-scope](interaction-scope.md) § "Snapping mode & modifiers"
|
||||
and `lib/snapping-mode.ts`.
|
||||
- **Constraints and guides can be decoupled.** When a stronger constraint owns the proposal —
|
||||
a wall segment's 45° lock while in `angles` mode — the tool may still publish passive dashed
|
||||
alignment/proximity guides as long as it does not apply the guide snap delta. Use this for chained
|
||||
wall segments: users keep the fast constrained draft but still see proximity feedback for later points.
|
||||
- **Help mirrors the model.** The shortcut dialog and the contextual HUD are part of the interaction
|
||||
contract: they describe the always-visible mode chip + `Alt` = force, **not** a hidden Shift bypass.
|
||||
The HUD is driven by the active interaction scope, so it shows only the current context's controls.
|
||||
- **Preview geometry is local** — transient meshes shown while a tool is active live in the tool component, not in the scene store.
|
||||
- **Clean up on unmount** — remove any pending/incomplete nodes *and* any live transforms/mesh offsets when the tool unmounts.
|
||||
- **Tools must not import from `@pascal-app/viewer`** — use the scene store and core hooks only. `sceneRegistry` is exported from `@pascal-app/core` and is the allowed door into the Three.js graph for the narrow purposes above.
|
||||
|
||||
Reference in New Issue
Block a user