feat(scaffold): production-value slider thumb composite (#256) - #275
feat(scaffold): production-value slider thumb composite (#256)#275bradleypmartin wants to merge 2 commits into
Conversation
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>
|
Preview deploy ready — commit 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>
|
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):
The plan doc for this PR ( |
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>
Closes #256
Summary
Replace the pre-#256 standalone
arrow-*/spherethumb meshes with a compositeTHREE.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 8sphere-shaped sliders (quadricsd, 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:
buildThumb(shape, r, baseColor)returns aThumbBuildwithhoverTargets,outerMaterial,applyOpacityBump, and a singledispose()for all composite GPU resources. Track disposal stays independent inSlider.dispose().MeshStandardMaterial,transparent: true,opacity: 0.30idle (0.50hover,0.70grab forspherethumbs viaapplyOpacityBump),side: THREE.FrontSide,depthWrite: false,renderOrder = 1(matching the LOCKED_113 transparent-pass convention from SlicingPlane / TangentPlane / TaylorOverlay).shaftLength = 1.20r,shaftRadius = 0.18r,coneHeight = 0.35r,coneRadius = 0.28r(tip extent0.95r).userData.rolemarkers:'slider-thumb'on the Group,'slider-thumb-outer'on the outer sphere mesh (stable test discovery; no positional indexing).No scene consumers change —
ThumbShapeenum and per-sceneSLIDER_CONFIGblocks untouched. Hit-test radius math (thumbRadius × grabRadiusMultiplier) unchanged.Test plan
Automated (already green at push):
test/scaffold/ui/Slider.test.tscovering 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.tsc --noEmit+ Vite production build all clean.Headset smoke (Cloudflare PR preview):
Per-scene visual gates:
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. Thumbdshows a yellow translucent sphere with no interior.tokens.tsslider-tint rule).Depth + transparency gates:
side: THREE.FrontSidekeeps the rear hemisphere undrawn.renderOrder = 1shared with SlicingPlane / TangentPlane / TaylorOverlay).Hover/grab cue gates:
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):
SLIDER_THUMB_OUTER_OPACITYper the bracket-doc comment._HOVERED/_GRABBEDtoward 0.65 / 0.85 per the bracket-doc comment.Performance + regression:
?fps=1overlay still works.🤖 Generated with Claude Code