Skip to content

feat(scaffold): bake slider thumb emblazon into sphere texture (#278) - #279

Merged
bradleypmartin merged 6 commits into
mainfrom
278-emblazoned-thumb-texture
May 28, 2026
Merged

feat(scaffold): bake slider thumb emblazon into sphere texture (#278)#279
bradleypmartin merged 6 commits into
mainfrom
278-emblazoned-thumb-texture

Conversation

@bradleypmartin

@bradleypmartin bradleypmartin commented May 28, 2026

Copy link
Copy Markdown
Contributor

Closes #278

Summary

Replaces the #276 / #277 planar Troika-Text-child billboard with a baked (symbol, color) canvas texture wired into the thumb sphere's material.map. The sphere itself yaw-billboards via thumb.quaternion, so the glyph rides the surface instead of reading as a decal floating in front. New scaffold module bakedSymbolTexture.ts exposes a refcounted (symbol, color) cache — 13 unique pairs across the 17 cluster sliders. Soft release semantics + idempotent Slider.dispose() prevent uncaught throws on lifecycle paths.

Plan + review trail

Plan: _private/plans/278-emblazoned-thumb-texture.md (v3, post second-Sonnet sanity).

  • v1 /roundtable-plan-review ran across Sonnet (oppositional-reviewer with filesystem access) + GPT-5.5 + DeepSeek V4 Pro. Verdicts: Sonnet REVISE, GPT REVISE, DeepSeek PROCEED.
  • Three convergent findings closed in v2: canvas-factory injection (was: broken prototype mutation under Node Vitest); soft release semantics + idempotent dispose (was: throws on legitimate lifecycle paths); explicit refcount-ladder test arithmetic.
  • Accepted singletons folded in v2: system-ui canvas font (was: hard-coded Apple-first stack with no Quest fallback story); mipmapped minification (was: shimmer hazard in VR); flipY doc + smoke item; empirical glyph y-offset; backing-store clear at refcount→0; one stale Text import deletion Sonnet caught.
  • Second-Sonnet sanity HOLD on v2 → v3 fix: existing un-disposed makeSlider() callsites under the v2 factory injection would have polluted cache state across tests. v3 adds _clearCacheForTests() helper wired into afterEach blocks in both Slider.test.ts and PointerMigration.test.ts.

Test plan

  • Vitest green locally: npm test — 573 passing across 42 files (+6 net new behavioral cases: 7 in bakedSymbolTexture.test.ts, 6 new in Slider.test.ts, −7 deleted feat(scaffold): emblazoned-label slider thumbs (solid spheres + per-slider symbol) #276 label cases).
  • npm run build clean (tsc --noEmit + vite build).
  • Cloudflare PR preview smoke (Quest 3S + pancake) — the primary verification surface, per project headset-smoke convention:
    • Quadrics squared (slider a → label , slider b, slider c) — glyphs read as baked into the sphere surface, not as a decal floating in front.
    • Quadrics linear (u/v/wx/y/z) + cross-sections (x₀/y₀/z₀) — subscript glyph renders cleanly via system-ui.
    • d-slider C (YELLOW) appears identically in both Squared and Linear racks; cache hit, not duplicate render.
    • Tangent-planes (θ/φ neutral gray), gradient-levels (θ/φ/k neutral gray), saddle-extrema (x/y VERMILLION/BLUISH_GREEN).
    • Glyphs render upright, not flipped or mirrored (especially and x₀ where orientation regressions would be most visible).
    • Glyph visual centering on target/x₀ not visibly shifted up/down on the sphere face. Tune GLYPH_VERTICAL_OFFSET_PX per its bracket if off.
    • Hover/grab affordance still reads (sphere brightens via emissive flip; glyph subtly tints — acceptable per plan §3.4).
    • No glyph roll on VR head-tilt (yaw-only convention preserved from feat(scaffold): emblazoned-label slider thumbs (solid spheres + per-slider symbol) #276).
    • renderer.info.memory.textures returns to pre-cluster-nav value after a full cluster cycle (refcount cleanup).
    • FPS overlay 72 Hz hold preserved.

Escapes (recorded in plan §6 if any fire post-smoke)

  • (a) tune GLYPH_FONT_SIZE_PX if glyph wraps too tightly on the sphere
  • (b) switch to SDF render-target bake if system-ui font coverage misses
  • (c) per-body-deeper-tint glyph color if white-on-yellow d-slider washes out
  • (d) non-emissive rim-ring affordance if glyph hover-tint reads as distracting
  • (e) revert to feat(scaffold): emblazoned-label slider thumbs (#276) #277 planar billboard if the texture-bake fails the "emblazoned, not decal" gate on both form factors

🤖 Generated with Claude Code

Replace the #276 / #277 planar Troika-Text-child billboard with a
baked (symbol, color) canvas texture wired into the thumb sphere's
material.map. The sphere itself yaw-billboards via thumb.quaternion,
so the glyph rides the surface instead of reading as a decal
floating in front. New scaffold module bakedSymbolTexture.ts
exposes a refcounted (symbol, color) cache — 13 unique pairs across
the 17 cluster sliders. Soft release semantics + idempotent
Slider.dispose() prevent uncaught throws on lifecycle paths.

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

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown

Preview deploy ready — commit 7c192c0a56f5e736c8f585e169faac35cb5f7045

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

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

bradleypmartin and others added 5 commits May 28, 2026 12:31
Three.js SphereGeometry's equirectangular UV places (U=0.5, V=0.5)
on the local +X surface point, not +Z. faceCamera writes a yaw
quaternion that points local +Z at the camera; without compensating
for the UV convention, the bare +Z side of the sphere faces the
user and the glyph ends up ~90° off to the side. One-line
sphereGeom.rotateY(-π/2) in the ctor moves the glyph 90° around
the sphere to where faceCamera expects it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pancake smoke surfaced two issues: (1) the texture's UV center
landed at sphere-local +X (not +Z), so the glyph faced ~90° off;
(2) per-frame yaw-billboarding the sphere read as "still a
billboard" — a uniform-colored sphere body has no parallax cue to
betray rotation, so the visual was indistinguishable from #277's
planar billboard. (1) was patched by sphereGeom.rotateY(-π/2).

This commit fixes (2) by dropping the per-frame faceCamera
entirely. The thumb gets a static rotation (about local +X by
-π/2) so the glyph centers along slider-local +Y, which under
the plinth slot's orientation='surface' transform is the
drafting-board surface normal in world. The glyph is now truly
fixed on the sphere surface — the user reads it from the natural
lean-over working pose, and it slides off / foreshortens as they
back away. Removes the faceCamera method + 5 scratch fields from
Slider.ts and the 4 per-frame dispatches in the cluster scenes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous fix centered the glyph on slider-local +Y, which I'd
incorrectly identified as the drafting-board normal. The slab is
actually modelled thin in slot-local Z (front face at z=0), so
slot-local +Z is perpendicular to the slab face — that's the
real outward normal. After the slot rotation (-tilt about +X),
+Z maps to world (0, sin(tilt), cos(tilt)) = up and toward user;
+Y maps to (0, cos(tilt), -sin(tilt)) = up and away from user,
which read in smoke as "eyes rolled back into the head."

Drop thumb.rotation.x = -π/2; with identity rotation the
glyph stays at mesh-local +Z = slider-local +Z, facing along
the slab's actual outward normal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 of the binary-search dial on GLYPH_FONT_SIZE_PX per
feedback_binary_search_visual_constants. Round 1 (160 px) read
in pancake smoke as "wraps a full hemisphere of the sphere" —
too distorted by the equirectangular UV curvature at that size.
Drop to 112 (0.7 x 160) so the glyph drifts over less of the
sphere surface and the curvature reads as a subtle wrap rather
than a hemisphere-wide stretch. Bracket tightens to [80, 144].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 3 of the GLYPH_FONT_SIZE_PX binary-search dial. Round 2
(112) still drifted over more of the sphere than wanted; Brad
asked for 90 ("0.5x original"), so taking 90. Bracket tightens
to [78, 108] as we converge.

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): bake slider thumb labels into the sphere texture (follow-up to #276)

1 participant