Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/exhibits/gradient-levels/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,9 @@ ported onto the plinth.
`GradientLevelsReadout` extends the shared `PanelReadout` base
(`scaffold/ui/PanelReadout.ts`) which contributes the cluster-shared
THREE.Group + boot-cloak + per-frame yaw `faceCamera` + dark
`MeshBasicMaterial` back-plate quad.
`MeshBasicMaterial` back-plate slab (front face flush with the original
PlaneGeometry position, extruded behind by `READOUT_PANEL_DEPTH` so
yaw-billboard motion reads as a solid screen turning — #270).

Per parent plan #225 §3.5 v3 lock (option-c), the back-plate is a
child of the readout's group, inheriting the yaw-billboard
Expand Down
7 changes: 5 additions & 2 deletions src/exhibits/quadrics/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,8 +574,11 @@ enforces zero references in `.ts` files repo-wide.
(`scaffold/ui/PanelReadout.ts`) which contributes (1) the THREE.Group
construction + boot-cloak, (2) the per-frame yaw-only `faceCamera`
billboard (formerly duplicated identical-by-copy across the four
cluster readouts), and (3) a dark `MeshBasicMaterial` back-plate quad
sized to the readout's worst-case content + padding.
cluster readouts), and (3) a dark `MeshBasicMaterial` back-plate slab
sized to the readout's worst-case content + padding (front face flush
with the original PlaneGeometry position, extruded behind by
`READOUT_PANEL_DEPTH` so yaw-billboard motion reads as a solid screen
turning, not a flat decal sliding — #270).

Per parent plan #225 §3.5 v3 lock (option-c), the back-plate sits as
a child of the readout's group, so it inherits the yaw-billboard
Expand Down
4 changes: 3 additions & 1 deletion src/exhibits/saddle-extrema/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,9 @@ ported onto the plinth.
`SaddleExtremaReadout` extends the shared `PanelReadout` base
(`scaffold/ui/PanelReadout.ts`) which contributes the cluster-shared
THREE.Group + boot-cloak + per-frame yaw `faceCamera` + dark
`MeshBasicMaterial` back-plate quad.
`MeshBasicMaterial` back-plate slab (front face flush with the original
PlaneGeometry position, extruded behind by `READOUT_PANEL_DEPTH` so
yaw-billboard motion reads as a solid screen turning — #270).

Per parent plan #225 §3.5 v3 lock (option-c), the back-plate is a
child of the readout's group, inheriting the yaw-billboard
Expand Down
4 changes: 3 additions & 1 deletion src/exhibits/tangent-planes/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,9 @@ value. The pre-plinth mid-air `2.75` constant was deleted at PR2
`TangentPlaneReadout` extends the shared `PanelReadout` base
(`scaffold/ui/PanelReadout.ts`) which contributes the cluster-shared
THREE.Group + boot-cloak + per-frame yaw `faceCamera` + dark
`MeshBasicMaterial` back-plate quad.
`MeshBasicMaterial` back-plate slab (front face flush with the original
PlaneGeometry position, extruded behind by `READOUT_PANEL_DEPTH` so
yaw-billboard motion reads as a solid screen turning — #270).

Per parent plan #225 §3.5 v3 lock (option-c), the back-plate is a
child of the readout's group, inheriting the yaw-billboard
Expand Down
36 changes: 26 additions & 10 deletions src/scaffold/ui/PanelReadout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@
// written every frame. Per parent-plan §3.5 v3 lock (option-c),
// the back-plate inherits this rotation transitively as a child
// of `group`, so panel + text yaw-billboard together.
// 3. Back-plate quad construction + dispose — a dark MeshBasicMaterial
// PlaneGeometry sized to the subclass-supplied worst-case text
// bounds + padding. Subclass calls createPanel(dims) once during
// its ctor after laying out text children; subclass's dispose()
// chains disposePanel() after disposing text children.
// 3. Back-plate slab construction + dispose — a dark MeshBasicMaterial
// BoxGeometry sized to the subclass-supplied worst-case text
// bounds + padding, with the front face flush at the subclass's
// requested z and the slab extruded behind it by
// READOUT_PANEL_DEPTH (#270 — gives the slab enough depth that
// yaw-billboard motion reads as a solid screen turning, not a
// flat decal sliding). Subclass calls createPanel(dims) once
// during its ctor after laying out text children; subclass's
// dispose() chains disposePanel() after disposing text children.
//
// What this base does NOT do:
// - The text children themselves (subclass-specific layouts).
Expand All @@ -29,7 +33,10 @@
// jitter.

import * as THREE from 'three';
import { READOUT_PANEL_COLOR_RGB } from './readoutTokens';
import {
READOUT_PANEL_COLOR_RGB,
READOUT_PANEL_DEPTH,
} from './readoutTokens';

export interface PanelReadoutPanelDimensions {
/** Half-width of the back-plate quad in group-local meters. */
Expand All @@ -39,7 +46,10 @@ export interface PanelReadoutPanelDimensions {
/** Group-local (x, y) center of the back-plate. Defaults to (0, 0).
* Used when text is offset from group origin. */
readonly center?: readonly [number, number];
/** Recess in group-local +Z. Defaults to -0.001 m (text in front). */
/** Front-face z (group-local +Z). The slab is extruded BEHIND this
* by READOUT_PANEL_DEPTH, so subclasses think in terms of "where
* does the screen surface sit" — the depth direction is internal.
* Defaults to -0.001 m (text in front of the screen surface). */
readonly localZ?: number;
}

Expand All @@ -50,7 +60,7 @@ export abstract class PanelReadout {
readonly group: THREE.Group;

private panel: THREE.Mesh<
THREE.PlaneGeometry,
THREE.BoxGeometry,
THREE.MeshBasicMaterial
> | null = null;
private readonly camWorld = new THREE.Vector3();
Expand All @@ -72,9 +82,10 @@ export abstract class PanelReadout {
if (this.panel !== null) {
throw new Error('PanelReadout.createPanel: already created');
}
const geometry = new THREE.PlaneGeometry(
const geometry = new THREE.BoxGeometry(
dims.halfWidth * 2,
dims.halfHeight * 2,
READOUT_PANEL_DEPTH,
);
const material = new THREE.MeshBasicMaterial({
color: new THREE.Color(...READOUT_PANEL_COLOR_RGB),
Expand All @@ -86,7 +97,12 @@ export abstract class PanelReadout {
// Defensive against three.js's render-order-vs-scene-graph subtlety.
mesh.renderOrder = -1;
const [cx, cy] = dims.center ?? [0, 0];
mesh.position.set(cx, cy, dims.localZ ?? -0.001);
// dims.localZ is the SCREEN SURFACE z (front face). The slab's
// center sits half-a-depth behind, so the geometry extends BEHIND
// the screen surface — front-face position is unchanged from the
// old PlaneGeometry contract; text stays in front.
const frontZ = dims.localZ ?? -0.001;
mesh.position.set(cx, cy, frontZ - READOUT_PANEL_DEPTH / 2);
this.group.add(mesh);
this.panel = mesh;
}
Expand Down
15 changes: 15 additions & 0 deletions src/scaffold/ui/readoutTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,18 @@ export const READOUT_SYNC_INTERVAL_MS = 33;
// component. Immutable tuple per feedback_threejs_token_exports_
// immutable — produce a fresh THREE.Color in each consumer.
export const READOUT_PANEL_COLOR_RGB = [0.08, 0.08, 0.1] as const;

// Back-plate depth — Z-extrusion BEHIND the front face (away from the
// viewer, into the plinth) so the front face stays at the original
// plane position and text-vs-panel ordering is unchanged. Gives the
// back-plate enough physical presence that yaw-billboard motion reads
// as a solid screen turning, not a flat decal sliding (per #270 smoke
// verdict on #252 / PR #269: panel-as-flat-decal-that-tracks-you).
// Round 1 = 8mm; smoke verdict "getting there but not strong enough."
// Round 2 = 12mm; smoke verdict "right track but still not strong
// enough." Round 3 = 14mm (current). Bracket narrows to [12mm, 16mm];
// if 14mm still subtle, next try 16mm (with attention to plinth-top
// clipping at extreme yaw — that's the hard ceiling). If 14mm
// overshoots, back to 13mm. Tune one dial per round; smoke on
// Cloudflare PR preview.
export const READOUT_PANEL_DEPTH = 0.014;
19 changes: 14 additions & 5 deletions test/scaffold/ui/PanelReadout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import {
READOUT_FONT_SIZE,
READOUT_PANEL_COLOR_RGB,
READOUT_PANEL_DEPTH,
} from '@/scaffold/ui/readoutTokens';
import { EquationReadout } from '@/exhibits/quadrics/EquationReadout';
import { TangentPlaneReadout } from '@/exhibits/tangent-planes/TangentPlaneReadout';
Expand Down Expand Up @@ -80,19 +81,26 @@ describe('PanelReadout', () => {
const child = r.group.children[0];
expect(child).toBeInstanceOf(THREE.Mesh);
const mesh = child as THREE.Mesh<
THREE.PlaneGeometry,
THREE.BoxGeometry,
THREE.MeshBasicMaterial
>;
expect(mesh.geometry).toBeInstanceOf(THREE.PlaneGeometry);
expect(mesh.geometry).toBeInstanceOf(THREE.BoxGeometry);
expect(mesh.geometry.parameters.width).toBeCloseTo(0.2, 6);
expect(mesh.geometry.parameters.height).toBeCloseTo(0.1, 6);
expect(mesh.geometry.parameters.depth).toBeCloseTo(
READOUT_PANEL_DEPTH,
6,
);
expect(mesh.material).toBeInstanceOf(THREE.MeshBasicMaterial);
const expectedColor = new THREE.Color(...READOUT_PANEL_COLOR_RGB);
expect(mesh.material.color.equals(expectedColor)).toBe(true);
expect(mesh.renderOrder).toBe(-1);
expect(mesh.position.x).toBeCloseTo(0, 6);
expect(mesh.position.y).toBeCloseTo(0, 6);
expect(mesh.position.z).toBeCloseTo(-0.001, 6);
// Box center sits half-a-depth behind the requested front-face z
// (default -0.001) so the screen surface stays where the old
// PlaneGeometry quad lived; the depth extends away from the viewer.
expect(mesh.position.z).toBeCloseTo(-0.001 - READOUT_PANEL_DEPTH / 2, 6);
});

it('honors center and localZ overrides', () => {
Expand All @@ -106,7 +114,8 @@ describe('PanelReadout', () => {
const mesh = r.group.children[0] as THREE.Mesh;
expect(mesh.position.x).toBeCloseTo(0.03, 6);
expect(mesh.position.y).toBeCloseTo(-0.02, 6);
expect(mesh.position.z).toBeCloseTo(-0.005, 6);
// localZ is the front-face z; box center sits depth/2 behind it.
expect(mesh.position.z).toBeCloseTo(-0.005 - READOUT_PANEL_DEPTH / 2, 6);
});

it('throws on a second call (single-shot guard)', () => {
Expand Down Expand Up @@ -152,7 +161,7 @@ describe('PanelReadout', () => {
const r = new TestReadout();
r.makePanel({ halfWidth: 0.1, halfHeight: 0.05 });
const mesh = r.group.children[0] as THREE.Mesh<
THREE.PlaneGeometry,
THREE.BoxGeometry,
THREE.MeshBasicMaterial
>;
const geomSpy = vi.spyOn(mesh.geometry, 'dispose');
Expand Down
Loading