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
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:
parent
fcabaee59e
commit
9d3343bb31
6 changed files with 66 additions and 23 deletions
|
|
@ -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 })!;
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue