nemesis/demo: tests follow the new numbers; Ridge retries a zero-size start
All checks were successful
Build pull request / Build pull request (pull_request) Successful in 4m12s

skyTheme: auto resolves to the clock's theme; the plate pins now assert the
chromeless shell (only composer, call bar, call view and plain messages are
plates). terrainWorld: ceilings for the 512/768 grids; still grid 256 -> 320 so
its quads stay within two height texels at the nearest ground (the test is
unchanged). skyPlate.css: drop the void-import placeholders. Ridge: a scene
started with no area no longer throws on drawImage or returns null; it builds on
the first resize that gives it one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Void 2026-11-10 13:53:22 +00:00
commit 9d3343bb31
6 changed files with 66 additions and 23 deletions

View file

@ -437,6 +437,30 @@ describe('createRidgeScene', () => {
expect(parent.children.some((c: any) => c === readout)).toBe(false);
});
it('a scene started at zero size paints nothing, then builds on the first non-zero resize', async () => {
const { parent, canvas } = host();
canvas.clientWidth = 0;
canvas.clientHeight = 0;
(window as any).innerWidth = 0;
(window as any).innerHeight = 0;
const s = createRidgeScene({ canvas, tier: 'full', sky: DAY });
expect(s).not.toBeNull();
expect(parent.children).toEqual([canvas]);
expect(draws).toHaveLength(0);
// Still zero: a resize does not paint either.
window.dispatchEvent(new Event('resize'));
await new Promise((r) => setTimeout(r, 300));
expect(parent.children).toEqual([canvas]);
(window as any).innerWidth = 400;
(window as any).innerHeight = 800;
window.dispatchEvent(new Event('resize'));
await new Promise((r) => setTimeout(r, 300));
expect(parent.children.slice(1)).toHaveLength(LAYERS.length);
expect(draws.filter((d) => d === 'sky')).toHaveLength(1);
s!.dispose();
expect(parent.children).toEqual([canvas]);
});
it('dispose cancels the loop, the animations and the layers', () => {
const { parent, canvas } = host();
const s = createRidgeScene({ canvas, tier: 'full', sky: DAY })!;

View file

@ -180,9 +180,18 @@ export function createRidgeScene(opts: RidgeOptions): Scene | null {
built = undefined;
};
/** Bake the sky, then the layers. Returns false when a 2D context is missing. */
/** True while the host has no area to paint (a scene started in a hidden or collapsed container). */
const hasNoArea = () => {
const { w, h, scale } = viewSize();
const size = layerSize(w, h, scale);
return size.period < 1 || size.height < 1 || Math.round(h * scale) < 1;
};
/** Bake the sky, then the layers. Returns false when a 2D context is missing or there is no area yet. */
const build = (): boolean => {
teardownLayers();
// A zero-size canvas throws on drawImage; wait for the first resize that gives the host an area.
if (hasNoArea()) return false;
const { w, h, scale } = viewSize();
const size = layerSize(w, h, scale);
canvas.width = size.period;
@ -343,7 +352,8 @@ export function createRidgeScene(opts: RidgeOptions): Scene | null {
raf = requestAnimationFrame(tick);
};
if (!build()) return null;
// No area yet is not a failure: the resize listener below builds once the host has one.
if (!build() && !hasNoArea()) return null;
if (probe) {
readout = document.createElement('div');
Object.assign(readout.style, {

View file

@ -1,12 +1,8 @@
import { globalStyle, style } from '@vanilla-extract/css';
import { color, config } from 'folds';
import { MessageBase, BubbleContent } from '../../components/message/layout/layout.css';
import { PageContent, PageNavContent } from '../../components/page/style.css';
import { Editor } from '../../components/editor/Editor.css';
import { MembersDrawer } from '../room/MembersDrawer.css';
import { CallStatus } from '../call-status/styles.css';
import { RoomViewFollowing } from '../room/RoomViewFollowing.css';
import { RoomViewTyping } from '../room/RoomViewTyping.css';
import { PLATE_TINT, SKY_PLATE_FALLBACK } from './skyPlate';
// Over the scene (html[data-scene]) the signed-in shell is clear (sceneShell.css.ts), so text
@ -26,11 +22,6 @@ const base = (recipe: () => string) => `.${recipe().split(' ')[0]}`;
// Meta, 2026-11-09: no chrome. Title bars, the room list, list pages, the drawer and the bars over the composer are bare
// ink on the sky (dusk is allowed to be low contrast: it is the crossover between the light and dark inks).
void PageNavContent;
void PageContent;
void MembersDrawer;
void RoomViewTyping;
void RoomViewFollowing;
export const plateRegions = [
`.${SkyPlate}`,
// The composer and the call status bar.

View file

@ -39,11 +39,17 @@ describe('skyThemeId', () => {
expect(skyThemeId('night')).toBe('midnight-sky');
});
it('picks none for auto, at any hour: auto never overrides a signed-in theme', () => {
it('picks the theme of the state auto resolves to, at any hour', () => {
[0, 6, 12, 18, 23].forEach((h) => {
setSkyClock(() => atStandardTime(h, 10));
expect(skyThemeId('auto')).toBeUndefined();
const id = skyThemeId('auto');
expect(id).toBeDefined();
expect(id).toBe(skyThemeId(getResolvedSkyState()));
});
setSkyClock(() => atStandardTime(12));
expect(skyThemeId('auto')).toBe('noonday-sky');
setSkyClock(() => atStandardTime(0));
expect(skyThemeId('auto')).toBe('midnight-sky');
setSkyClock();
});
@ -145,19 +151,26 @@ describe('regions that sit on the plate', () => {
// and link of that plate are pinned above; this pins who is on it.
const on = (cls: string) => expect(plateRegions).toContain(cls);
it('plates the composer, the members drawer, the call status bar and the call view', () => {
it('plates the composer, the call status bar and the call view', () => {
on(`.${Editor}`);
on(`.${MembersDrawer}`);
on(`.${CallStatus}`);
on(`.${SkyPlate}`);
});
it('plates the room list, list pages, typing and Following bars and every header', () => {
on(`.${PageNavContent}`);
on(`.${PageContent}`);
on(`.${RoomViewTyping}`);
on('#root header');
it('plates a message that is not a bubble, and nothing else of the signed-in shell', () => {
expect(plateRegions.some((r) => r.includes(':not(:has('))).toBe(true);
expect(plateRegions).toHaveLength(4);
});
// Meta, 2026-11-09: no chrome. These are bare ink on the sky, not plates.
it('leaves title bars, the room list, list pages, the drawer and the bars over the composer bare', () => {
[
`.${MembersDrawer}`,
`.${PageNavContent}`,
`.${PageContent}`,
`.${RoomViewTyping}`,
'#root header',
].forEach((cls) => expect(plateRegions).not.toContain(cls));
});
});

View file

@ -41,8 +41,13 @@ describe('grid', () => {
});
it('each tier fits a 32-bit index and the cheap tiers stay cheap', () => {
// 205k at 320 quads: a guess at what a phone carries (SwiftShader only, no phone frame time), kept as a ceiling so a bigger grid is a deliberate change.
expect(triangleCount(GRID.low.grid)).toBeLessThan(210000);
// 524k at 512 quads (low), 1.18M at 768 (full): Meta asked for denser terrain on 2026-11-10, up from 205k at 320. No
// phone frame time is measured (SwiftShader only); kept as a ceiling so a bigger grid is a deliberate change.
expect(triangleCount(GRID.low.grid)).toBeLessThan(540000);
expect(triangleCount(GRID.full.grid)).toBeLessThan(1200000);
(['still', 'low', 'full'] as const).forEach((t) =>
expect((GRID[t].grid + 1) ** 2).toBeLessThan(2 ** 32)
);
expect(GRID.full.grid).toBeGreaterThan(GRID.low.grid);
expect(GRID.still.tex).toBeLessThanOrEqual(GRID.low.tex);
});

View file

@ -11,7 +11,7 @@ export const TERRAIN = { span: 3000, hmax: 256 };
* under shading read from the height map shows as hard-edged facets and stepped crests.
*/
export const GRID = {
still: { grid: 256, tex: 1024 },
still: { grid: 320, tex: 1024 },
low: { grid: 512, tex: 1024 },
full: { grid: 768, tex: 2048 },
} as const;