Skip to content

feat: world-space rotation axis rings - #60

Open
Teagan42 wants to merge 2 commits into
Formsmith746:mainfrom
constructorfleet:feature/rotation-axis-rings
Open

feat: world-space rotation axis rings#60
Teagan42 wants to merge 2 commits into
Formsmith746:mainfrom
constructorfleet:feature/rotation-axis-rings

Conversation

@Teagan42

Copy link
Copy Markdown

Summary

  • Replace the tiny corner rotate-handle icons with three world-space colored axis rings (Blender / three.js TransformControls style) as the primary rotation affordance on desktop.
  • Camera-facing bias tips near-edge-on rings up to ~10° so they stay hittable; opacities ease for hover / active-drag / camera-motion states.
  • Corner icons kept as a responsive fallback on touch / narrow viewports; a "Use classic rotate handles" preference forces the classic UI everywhere. URL ?rotationMode=classic|rings overrides both for tests.

Test plan

  • npm run typecheck
  • npm test (209 passing, includes 27 new tests in rotationMode.test.ts and rotationRings.test.ts)
  • Manual: select a shape on desktop → verify red/green/blue rings appear around it, dim during orbit, brighten on hover, drag rotates around the correct axis.
  • Manual: toggle "Use classic rotate handles" in Workspace Settings → corner icons return, rings hide.
  • Manual: resize window below 768px (or DevTools touch emulation) → corner icons appear as the fallback.
  • Manual: append ?rotationMode=classic / ?rotationMode=rings to the URL → override wins.

Spec: .scratch/rotation-handle-visibility/map.md

Adds three colored axis rings (R/G/B for X/Y/Z) around the current selection
so rotation handles are discoverable — the corner rotate icons are small and
low-contrast at 60% opacity, and users had trouble finding them.

Phase 1 is visuals-only: rings render, size to the selection's bounding
sphere (clamped to a 40–160 px screen-space range), fade during camera
motion, and tilt slightly toward the camera when within 15° of edge-on so
they never fully degenerate into a line. The corner icons remain the drag
target for now.

Also wires the mode-derivation infrastructure that the follow-up phase will
use to gate the corner icons: a "Use classic rotate handles" preference in
the workspace settings modal (off by default), a matchMedia hook that flips
to classic mode on touch or narrow viewports, and a ?rotationMode=classic|rings
URL override for tests and Storybook. Precedence: URL > preference > media.

Phase 2 (not in this commit) will add pointer-driven drag on the rings and
hide the corner icons on desktop under the default mode — those two changes
belong together so desktop users are never left without a way to rotate.

Design decisions and the reasoning behind them live in the wayfinder map
under .scratch/rotation-handle-visibility/.

Test plan:
- New unit tests (27) cover ring radius clamping, camera-facing bias math
  at threshold boundaries, opacity easing, target-opacity precedence, and
  mode-derivation precedence.
- Full suite: 38 files, 209 tests, all pass.
- Typecheck: clean.
Completes the rotation-handle-visibility spec. The axis rings from phase 1
are now the primary drag target on desktop; the corner rotate-handle icons
render only in classic mode or on touch / narrow viewports.

Wires ring meshes into the existing transform-handle infrastructure by
tagging them with the same `userData.transformHandle = "rotate"` pattern
that other 3D helper handles use. A ring hit produces a synthesized
handleKey like `rotate-ring-x`, which `rotationAxisForHandle` maps to the
matching axis via its existing `endsWith("-x" | "-z")` fallback — no new
codepath in the rotate branch.

`pickTransformHandle` gets a small relaxation: it still bails on
multi-selection for non-ring handles (which are bound to a specific shape
id), but rings are allowed for multi-select and default their id to the
first selected shape.

Hover: raycasts on pointermove when nothing else is grabbing the pointer
and updates a ref that feeds `syncRotationRings`. Cleared on pointer leave.

Active-drag dimming: derived in the animate loop from `transformRef`
(handleKey starts with `rotate-ring-`), so the non-dragged rings ease to
28% while the active one stays at full opacity.

Visibility on the meshes: had to mirror `group.visible` onto each ring
mesh because three.js's `Raycaster.intersectObjects` doesn't skip
descendants of an invisible parent — otherwise rings hidden in classic mode
would still be hit-testable.

TransformOverlay gains one prop: `hideCornerRotateHandles`. Wired to
`rotationMode === "rings"`.

Test plan:
- All 209 existing tests still pass; typecheck clean.
- Manual verification of drag feel is the next step (I can't run the app).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant