Skip to content

feat(scaffold): production-value slider thumb composite (#256) - #275

Closed
bradleypmartin wants to merge 2 commits into
mainfrom
256-slider-thumb-visual
Closed

feat(scaffold): production-value slider thumb composite (#256)#275
bradleypmartin wants to merge 2 commits into
mainfrom
256-slider-thumb-visual

Conversation

@bradleypmartin

Copy link
Copy Markdown
Contributor

Closes #256

Summary

Replace the pre-#256 standalone arrow-* / sphere thumb meshes with a composite THREE.Group: an outer translucent axis-tinted sphere (the grab-affordance envelope) circumscribing an interior opaque bidirectional axis-arrow for the 10 quadrics axis-coefficient sliders. The 8 sphere-shaped sliders (quadrics d, saddle-extrema x/y, tangent-planes θ/φ, gradient-levels θ/φ/k) get a tinted translucent sphere with no interior. Plan + roundtable lineage: _private/plans/256-slider-thumb-visual.md (v3, post-second-Sonnet sanity check; folded 16 roundtable findings + 3 sanity-check residuals).

Mechanics:

  • New buildThumb(shape, r, baseColor) returns a ThumbBuild with hoverTargets, outerMaterial, applyOpacityBump, and a single dispose() for all composite GPU resources. Track disposal stays independent in Slider.dispose().
  • Outer sphere material: MeshStandardMaterial, transparent: true, opacity: 0.30 idle (0.50 hover, 0.70 grab for sphere thumbs via applyOpacityBump), side: THREE.FrontSide, depthWrite: false, renderOrder = 1 (matching the LOCKED_113 transparent-pass convention from SlicingPlane / TangentPlane / TaylorOverlay).
  • Interior arrow proportions shrunk so the tip extent fits inside the outer sphere with breathing room: shaftLength = 1.20r, shaftRadius = 0.18r, coneHeight = 0.35r, coneRadius = 0.28r (tip extent 0.95r).
  • userData.role markers: 'slider-thumb' on the Group, 'slider-thumb-outer' on the outer sphere mesh (stable test discovery; no positional indexing).

No scene consumers change — ThumbShape enum and per-scene SLIDER_CONFIG blocks untouched. Hit-test radius math (thumbRadius × grabRadiusMultiplier) unchanged.

Test plan

Automated (already green at push):

  • Vitest: 15 new test cases in test/scaffold/ui/Slider.test.ts covering composite structure, depth/render flags, hover-target wiring, opacity bump (sphere fires; arrow control case doesn't), dispose cleanup with track preservation, and per-vertex radial-containment envelope for arrow-{x,y,z}. All 575 project tests pass.
  • ESLint + tsc --noEmit + Vite production build all clean.

Headset smoke (Cloudflare PR preview):

Per-scene visual gates:

  • Quadrics squared / linear / cross-sections: thumbs a / b / c (+ u / v / w + x₀ / y₀ / z₀) show vermillion / bluish-green / sky-blue translucent spheres each with an interior axis-arrow visible through the body. Thumb d shows a yellow translucent sphere with no interior.
  • Tangent-planes θ / φ + gradient-levels θ / φ / k: neutral-gray translucent spheres, no interior.
  • Saddle-extrema x / y: VERMILLION / BLUISH_GREEN translucent spheres, no interior (axis-tinted per tokens.ts slider-tint rule).

Depth + transparency gates:

  • Background visible through translucent sphere (transparency working as designed — plinth, track, opaque surfaces behind the thumb show through at 0.30 opacity).
  • No back-face artifacts — side: THREE.FrontSide keeps the rear hemisphere undrawn.
  • No occlusion regression — outer sphere correctly hides behind a controller mesh / pointer passing in front of it.
  • No z-fight between outer body and interior arrow, or between outer body and any other transparent surface in the scene (renderOrder = 1 shared with SlicingPlane / TangentPlane / TaylorOverlay).

Hover/grab cue gates:

  • Hover any axis-coefficient slider: interior arrow lights up; outer envelope unchanged. Grab intensifies.
  • Hover any sphere-shaped slider (d / θ / φ / k / saddle x/y): outer sphere lights up AND opacity bumps to 0.50 on hover, 0.70 on grab. Cue clearly visible.

Legibility gates (critical for neutral-gray sphere thumbs):

  • At 1.0 m and 1.5 m viewing distances, the neutral-gray translucent sphere remains clearly distinguishable from the plinth at rest (no hover assistance). If it disappears against the plinth, bump SLIDER_THUMB_OUTER_OPACITY per the bracket-doc comment.
  • Hover/grab cue on neutral-gray spheres is visually unambiguous (alpha-attenuated emissive + opacity bump composes strongly enough). If it reads as too weak, bump _HOVERED / _GRABBED toward 0.65 / 0.85 per the bracket-doc comment.

Performance + regression:

  • Quest 3S 72 Hz hold preserved across all four scenes (net +10 meshes in quadrics, +0 in the three sister scenes).
  • ?fps=1 overlay still works.
  • SceneRack cluster nav, sliders / presets / section-tab interactions unchanged.

🤖 Generated with Claude Code

Replace the standalone `arrow-*` / `sphere` thumb meshes with a
composite Group: outer translucent axis-tinted sphere (the grab
affordance envelope) circumscribing an interior opaque axis-arrow
for `arrow-*` shapes; outer-only for `sphere`. Hover/grab emissive
lands on the interior for axis-coefficient sliders and on the
outer for sphere sliders, with a hover-state opacity bump on the
latter to compensate for alpha-attenuated emissive in standard
transparent blending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown

Preview deploy ready — commit 6ef02acf6b3b7b10466b4e8af768d20b786dffe0

https://geometer-pr-275.1bradley-martin1.workers.dev

Open this URL in the Quest browser to smoke-test in headset.

Three documentation fixes flagged by adversarial Sonnet review of
the parent commit. No runtime behavior change.

- Slider.ts: depthWrite: false comment incorrectly claimed the
  flag protects the interior arrow's visibility through the outer
  sphere. Interior is opaque so it draws in the opaque pass
  before the outer's transparent pass starts — independent of
  this flag. The flag's real purpose is preventing the outer's
  depth from occluding LATER transparent surfaces (other thumbs,
  TangentPlane, TaylorOverlay) drawn in the same transparent
  pass at the shared renderOrder = 1.
- Slider.test.ts: hittingRay docstring claimed the default
  makeSlider config snaps initial=1.0 to 0. With snapDetent=0.05
  and snapPoints=[0], |1.0 - 0| = 1.0 ≥ 0.05 so no snap fires.
  Tests using hittingRay() pass initial: 0 explicitly.
- Slider.test.ts: test 4 comment claimed tryGrab clears the
  hover bit. It doesn't — hovered stays true; the grabbed branch
  precedes the hovered branch in refreshThumbEmissive's if/else.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bradleypmartin

Copy link
Copy Markdown
Contributor Author

Closing as won't-do per design pivot.

Headset + pancake smoke surfaced (2026-05-28, Brad) that the translucent-sphere visual language doesn't read well at slider-thumb scale: the outer bubble reads as colored haze rather than as a definable object, and the interior axis-arrow gets visually muddied by the translucent envelope across both VR and pancake viewing distances. Expected to be even worse on mobile.

The technical implementation here is sound (and the roundtable / second-Sonnet / spar history caught real bugs along the way — those reviews paid off even though we're abandoning the visual). But the design hypothesis — that a translucent-grab-affordance circumscribing an axis-arrow would read as production-quality — failed in smoke.

New direction (will be tracked in a fresh issue + plan + PR):

  • Drop transparency on the slider bubbles entirely.
  • Return to solid axis-tinted spheres (same axis-color palette).
  • Remove the 3D bidirectional axis-arrow geometry from the arrow-* slider thumbs completely.
  • Emblazon a curved or planar label on the front of each thumb sphere indicating what the slider drives:
    • Squared-coefficients rack: / / / C (constant term)
    • Linear-terms rack: x / y / z
    • Cross-section rack: x₀ / y₀ / z₀
    • Tangent-planes / gradient-levels: θ / φ (+ k on gradient-levels)
    • Saddle-extrema: x / y

The plan doc for this PR (_private/plans/256-slider-thumb-visual.md v3) stays as a reference for "things we tried and what we learned" but doesn't get carried forward.

bradleypmartin added a commit that referenced this pull request May 28, 2026
Direction-pivot successor to #256 (closed won't-do, 2026-05-28) after
PR #275 closed unmerged following negative headset + pancake smoke on
the translucent-bubble approach (memory
`feedback_translucent_at_thumb_scale_fails`).

Replaces the four-value `ThumbShape` family (`'sphere' | 'arrow-x' |
'arrow-y' | 'arrow-z'`) with a single opaque axis-tinted sphere + a
Troika `Text` label child that yaw-billboards toward the camera each
frame. Drops the bidirectional 3D axis-arrow geometry; the textual
symbol now names each slider's role directly:

- quadrics squared coefficients: `x²` / `y²` / `z²`
- quadrics constant (shared across Squared + Linear via #140): `C`
- quadrics linear terms: `x` / `y` / `z`
- quadrics cross-sections: `x₀` / `y₀` / `z₀`
- tangent-planes: `θ` / `φ`
- gradient-levels: `θ` / `φ` / `k`
- saddle-extrema: `x` / `y`

New required `thumbLabel: string` option on `SliderOptions`.
Empty-string is the structural opt-out. The d-slider's `C` propagates
to both Squared and Linear racks via the existing #140 single-instance
mounting wiring — no per-rack code path needed.

`Slider.faceCamera(camera)` computes the desired world-frame yaw
quaternion (about world-Y, using the world-XZ-projected camera-facing
direction for both orientation AND position), then converts to
label-local space via the inverse parent quaternion. The world-frame
→ local-frame conversion is what makes yaw-only billboarding correct
under the plinth's ~20° X-tilt. Each cluster scene dispatches one
symmetric call per frame alongside the existing `s.update()` /
`s.updateHover()` ticks.

Vitest: 7 new behavioral cases covering label presence + persistence
through hover/grab, world-space billboard correctness under a 20°
plinth-tilt parent across 4 camera poses, empty-string opt-out, and
constructor-time placement outside the sphere envelope. The two
`makeSlider` test helpers (Slider.test.ts + PointerMigration.test.ts)
gain `thumbLabel: 'test'` for the required field. Troika is stubbed
with the established `vi.mock('troika-three-text', ...)` pattern
from TapButton.test.ts.

Plan + roundtable (Sonnet + GPT-5.5 + DeepSeek V4 Pro) + second-Sonnet
sanity check at `_private/plans/276-emblazoned-label-slider-thumbs.md`.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

feat(scaffold): production-value slider thumb visual — transparent sphere circumscribing axis-arrow interior

1 participant