mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 13:18:58 +00:00
In 2.5D with a backdrop image every floor switch rendered the card twice before the first frame. The paper key of the first-frame state (#654) held the space id, so each switch cleared the ready paper: the first render inserted the loading veil, updated() probed the computed card background with a temporary span and asked for a second full update, which removed the veil again. The colour itself never changed: it is the theme card background, and no space sets those variables. Locally this second pass was about 50 ms per warm switch on the large house. The paper under a backdrop is now resolved once per theme identity (dark mode, default and dark default theme, theme) and card mode. The state keeps the resolved paper of the current theme and mode beside the current paper, so a floor with a backdrop is ready in prepare() when that paper is known -- also after a drawn floor in between -- and the switch renders once: no veil, no probe, no second update. A drawn plan keeps its white paper without the DOM. Any change of the theme identity or the mode, also one made in Flat or in an editor, drops the kept paper, so the first backdrop floor after load, a theme change and a trip to an editor take the #654 path unchanged. isoPaperContext still takes the floor; it deliberately leaves it out of the identity. Witnesses: the #739 unit test is red on dev at "a floor switch shows no veil" and on a key-only variant (space dropped, no theme cache) at "drawn -> backdrop keeps the known theme paper"; the new smoke_iso_floor_switch is red on dev (2 updates, 1 colour probe and a veil insertion in every click task). The iso-paper-resolved-per-floor mutant puts the floor back into the theme identity. Issue: #739 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
107 lines
4.1 KiB
TypeScript
107 lines
4.1 KiB
TypeScript
import {
|
|
memoIsoLightFloorRooms, type IsoLightFloorMemo, type Rgb,
|
|
} from './iso-materials';
|
|
|
|
export interface IsoPaperContext {
|
|
readonly key: string;
|
|
/** Identity of the theme card background that an image-backed floor shows. */
|
|
readonly theme: string;
|
|
readonly imagePlan: boolean;
|
|
}
|
|
|
|
interface ThemeIdentity {
|
|
readonly darkMode?: unknown;
|
|
readonly default_theme?: unknown;
|
|
readonly default_dark_theme?: unknown;
|
|
readonly theme?: unknown;
|
|
}
|
|
|
|
/**
|
|
* All non-DOM inputs that can change the computed plan-paper colour. The floor
|
|
* the caller passes is deliberately not one of them (#739): no space sets the
|
|
* card background variables, so a floor switch keeps the paper of the theme
|
|
* and mode instead of resolving it again with a second update pass.
|
|
*/
|
|
export function isoPaperContext(
|
|
space: string, mode: string, imagePlan: boolean, themes?: ThemeIdentity,
|
|
): IsoPaperContext {
|
|
const theme = JSON.stringify([
|
|
mode, themes?.darkMode ?? null, themes?.default_theme ?? null,
|
|
themes?.default_dark_theme ?? null, themes?.theme ?? null,
|
|
]);
|
|
return { imagePlan, theme, key: JSON.stringify([imagePlan, theme]) };
|
|
}
|
|
|
|
/**
|
|
* #654 cold-start barrier and memo ownership. Lifecycle callers may invalidate
|
|
* here before render, but the theme resolver is invoked only after DOM commit.
|
|
*/
|
|
export class IsoFirstFrameState {
|
|
private runtimeFailed = false;
|
|
private paperRgb: Rgb = [255, 255, 255];
|
|
private paperContext = '';
|
|
private paperReady = false;
|
|
private floorMemo: IsoLightFloorMemo | null = null;
|
|
/** #739: the last resolved theme paper, kept across drawn and image floors. */
|
|
private themePaper: { readonly theme: string; readonly rgb: Rgb } | null = null;
|
|
|
|
public runtimeLoading(): void { this.runtimeFailed = false; }
|
|
public runtimeReady(): void { this.runtimeFailed = false; }
|
|
public runtimeFailure(): void { this.runtimeFailed = true; }
|
|
|
|
public prepare(desired: 'flat' | 'iso', context: IsoPaperContext): void {
|
|
// Only the current theme and mode are kept: any change of either — also one
|
|
// made in Flat or in an editor — resolves the paper again on the #654 path.
|
|
if (this.themePaper && this.themePaper.theme !== context.theme) this.themePaper = null;
|
|
if (context.key === this.paperContext) return;
|
|
this.paperReady = false;
|
|
if (desired !== 'iso') return;
|
|
if (!context.imagePlan) this.commitPaper(context.key, [255, 255, 255]);
|
|
// #739: a known theme paper is ready before the first render of the floor:
|
|
// no veil, no resolver, no second update pass.
|
|
else if (this.themePaper?.theme === context.theme) this.commitPaper(context.key, this.themePaper.rgb);
|
|
}
|
|
|
|
/** Returns true when a committed render must consume a newly resolved paper. */
|
|
public sync(
|
|
desired: 'flat' | 'iso', context: IsoPaperContext, resolveThemePaper: () => Rgb,
|
|
): boolean {
|
|
if (desired !== 'iso' || this.paperReady && context.key === this.paperContext) return false;
|
|
let rgb: Rgb = [255, 255, 255];
|
|
if (context.imagePlan) {
|
|
rgb = resolveThemePaper();
|
|
this.themePaper = { theme: context.theme, rgb };
|
|
}
|
|
this.commitPaper(context.key, rgb);
|
|
return true;
|
|
}
|
|
|
|
public pending(desired: 'flat' | 'iso', runtimeReady: boolean): boolean {
|
|
return desired === 'iso' && !this.runtimeFailed && (!runtimeReady || !this.paperReady);
|
|
}
|
|
|
|
public readiness(
|
|
desired: 'flat' | 'iso', effective: 'flat' | 'iso', runtimeReady: boolean,
|
|
): 'pending' | 'ready' | 'fallback' | null {
|
|
if (this.pending(desired, runtimeReady)) return 'pending';
|
|
if (effective === 'iso') return 'ready';
|
|
return desired === 'iso' && this.runtimeFailed ? 'fallback' : null;
|
|
}
|
|
|
|
public lightFloors(
|
|
fills: ReadonlyMap<string, { color: string; opacity: number } | null>,
|
|
): ReadonlySet<string> {
|
|
this.floorMemo = memoIsoLightFloorRooms(this.floorMemo, fills, this.paperRgb);
|
|
return this.floorMemo.rooms;
|
|
}
|
|
|
|
private commitPaper(key: string, rgb: Rgb): void {
|
|
const changed = !this.paperReady || this.paperContext !== key
|
|
|| rgb.some((channel, index) => channel !== this.paperRgb[index]);
|
|
this.paperContext = key;
|
|
this.paperRgb = rgb;
|
|
this.paperReady = true;
|
|
if (changed) this.floorMemo = null;
|
|
}
|
|
}
|