@flighthq/render 0.3.0 → 0.3.1-next.1041.35b950c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/renderer.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { getRegistryTableEntry, withRegistryTableEntry } from '@flighthq/registry/contract';
2
+ import { RegistryEntryState } from '@flighthq/types/contract';
1
3
  import { getRenderStateRuntime } from './renderState';
2
4
  // Mask renderers were retired (a mask is now a path ClipRegion realized by the backend clip hooks), so
3
5
  // there is no mask-renderer registry to copy — only the kind→renderer map and the clip hooks.
@@ -7,35 +9,55 @@ export function copyAllRenderersFromRenderState(target, source) {
7
9
  target.displayObjectClipHooks = source.displayObjectClipHooks;
8
10
  }
9
11
  export function copyRenderersFromRenderState(target, source) {
10
- getRenderStateRuntime(source).rendererMap.forEach((renderer, kind) => {
11
- registerRenderer(target, kind, renderer);
12
- });
12
+ const targetRuntime = getRenderStateRuntime(target);
13
+ const sourceTable = getRenderStateRuntime(source).registries.renderers;
14
+ const targetTable = targetRuntime.registries.renderers;
15
+ // A fresh derived pipeline can share the immutable source snapshot directly. A target that already
16
+ // has policy keeps its target-only registrations, matching the additive copy contract, while source
17
+ // bindings remain last-write-wins through registerRenderer.
18
+ if (targetTable.entries.size === 0) {
19
+ let registrationCount = 0;
20
+ for (const entry of sourceTable.entries.values()) {
21
+ if (entry.state === RegistryEntryState.Bound)
22
+ registrationCount++;
23
+ }
24
+ if (registrationCount === 0)
25
+ return;
26
+ targetRuntime.registries.renderers = sourceTable;
27
+ targetRuntime.rendererMapId = (targetRuntime.rendererMapId + registrationCount) >>> 0;
28
+ return;
29
+ }
30
+ for (const [kind, entry] of sourceTable.entries) {
31
+ if (entry.state === RegistryEntryState.Bound)
32
+ registerRenderer(target, kind, entry.value);
33
+ }
13
34
  }
14
- // Copies the backend-agnostic policy registries that participate in pipeline derivation. Maps are
15
- // cloned, never aliased: derived states start with a snapshot and diverge until an explicit re-copy.
35
+ // Copies the backend-agnostic policy registries that participate in pipeline derivation. Persistent
36
+ // tables share an immutable snapshot through distinct aggregates, so later replacements diverge until
37
+ // an explicit re-copy.
16
38
  export function copyRenderStateRegistrations(target, source) {
17
39
  const targetRuntime = getRenderStateRuntime(target);
18
40
  const sourceRuntime = getRenderStateRuntime(source);
19
- targetRuntime.colorAdjustmentResolver = sourceRuntime.colorAdjustmentResolver;
20
- const sourcePaddingRegistry = sourceRuntime.renderEffectPaddingResolverRegistry;
21
- targetRuntime.renderEffectPaddingResolverRegistry =
22
- sourcePaddingRegistry === null || sourcePaddingRegistry === undefined ? null : new Map(sourcePaddingRegistry);
23
- // The shape-command set is a base-runtime registry every backend replays through, so a pipeline
24
- // that inherits the renderers must inherit the commands too. Without it an offscreen state resolves
25
- // no handler for any command in a shape's stream and bakes an empty target.
26
- const sourceShapeCommands = sourceRuntime.canvasShapeCommandRegistry;
27
- targetRuntime.canvasShapeCommandRegistry =
28
- sourceShapeCommands === null || sourceShapeCommands === undefined ? null : new Map(sourceShapeCommands);
41
+ targetRuntime.registries.colorAdjustments = sourceRuntime.registries.colorAdjustments;
42
+ targetRuntime.registries.colorAdjustmentUnsupportedGuard = sourceRuntime.registries.colorAdjustmentUnsupportedGuard;
43
+ targetRuntime.registries.effectPaddingResolvers = sourceRuntime.registries.effectPaddingResolvers;
44
+ targetRuntime.registries.renderRootGuard = sourceRuntime.registries.renderRootGuard;
45
+ // The shape-command set is base policy every backend replays through, so a pipeline that inherits
46
+ // the renderers must inherit the commands too. Without it an offscreen state resolves no handler for
47
+ // any command in a shape's stream and bakes an empty target.
48
+ targetRuntime.registries.canvasShapeCommands = sourceRuntime.registries.canvasShapeCommands;
49
+ targetRuntime.registries.strokeTessellator = sourceRuntime.registries.strokeTessellator;
29
50
  }
30
51
  export function noopRendererData(_state, _source) {
31
52
  return null;
32
53
  }
33
54
  export function registerRenderer(state, kind, renderer) {
34
55
  const runtime = getRenderStateRuntime(state);
35
- if (runtime.rendererMap.get(kind) === renderer)
56
+ const table = runtime.registries.renderers;
57
+ if (getRegistryTableEntry(table, kind) === renderer)
36
58
  return;
59
+ runtime.registries.renderers = withRegistryTableEntry(table, kind, renderer);
37
60
  runtime.rendererMapId = (runtime.rendererMapId + 1) >>> 0;
38
- runtime.rendererMap.set(kind, renderer);
39
61
  }
40
62
  // Batch form of registerRenderer over a caller-supplied set of [kind, renderer] pairs. The registry
41
63
  // stays open and tree-shakable: only the renderers the caller references are pulled in — there is no
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEtD,uGAAuG;AACvG,8FAA8F;AAC9F,MAAM,UAAU,+BAA+B,CAAC,MAAmB,EAAE,MAAmB;IACtF,4BAA4B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,IAAI,MAAM,CAAC,sBAAsB,KAAK,IAAI;QAAE,MAAM,CAAC,sBAAsB,GAAG,MAAM,CAAC,sBAAsB,CAAC;AAC5G,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,MAAmB,EAAE,MAAmB;IACnF,qBAAqB,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE;QACnE,gBAAgB,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC;AAED,kGAAkG;AAClG,qGAAqG;AACrG,MAAM,UAAU,4BAA4B,CAAC,MAAmB,EAAE,MAAmB;IACnF,MAAM,aAAa,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;IACpD,MAAM,aAAa,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;IACpD,aAAa,CAAC,uBAAuB,GAAG,aAAa,CAAC,uBAAuB,CAAC;IAC9E,MAAM,qBAAqB,GAAG,aAAa,CAAC,mCAAmC,CAAC;IAChF,aAAa,CAAC,mCAAmC;QAC/C,qBAAqB,KAAK,IAAI,IAAI,qBAAqB,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAChH,gGAAgG;IAChG,oGAAoG;IACpG,4EAA4E;IAC5E,MAAM,mBAAmB,GAAG,aAAa,CAAC,0BAA0B,CAAC;IACrE,aAAa,CAAC,0BAA0B;QACtC,mBAAmB,KAAK,IAAI,IAAI,mBAAmB,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,mBAAmB,CAAC,CAAC;AAC5G,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAmB,EAAE,OAAmB;IACvE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAkB,EAAE,IAAU,EAAE,QAAkB;IACjF,MAAM,OAAO,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IAC7C,IAAI,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,QAAQ;QAAE,OAAO;IACvD,OAAO,CAAC,aAAa,GAAG,CAAC,OAAO,CAAC,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IAC1D,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,oGAAoG;AACpG,qGAAqG;AACrG,kFAAkF;AAClF,MAAM,UAAU,iBAAiB,CAAC,KAAkB,EAAE,OAAiD;IACrG,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,OAAO;QAAE,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;AAClF,CAAC"}
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/render",
3
- "version": "0.3.0",
3
+ "version": "0.3.1-next.1041.35b950c",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,16 +38,17 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/adjustments": "0.3.0",
42
- "@flighthq/color": "0.3.0",
43
- "@flighthq/entity": "0.3.0",
44
- "@flighthq/geometry": "0.3.0",
45
- "@flighthq/log": "0.3.0",
46
- "@flighthq/materials": "0.3.0",
47
- "@flighthq/mesh": "0.3.0",
48
- "@flighthq/node": "0.3.0",
49
- "@flighthq/signals": "0.3.0",
50
- "@flighthq/types": "0.3.0"
41
+ "@flighthq/adjustments": "0.3.1-next.1041.35b950c",
42
+ "@flighthq/color": "0.3.1-next.1041.35b950c",
43
+ "@flighthq/entity": "0.3.1-next.1041.35b950c",
44
+ "@flighthq/geometry": "0.3.1-next.1041.35b950c",
45
+ "@flighthq/log": "0.3.1-next.1041.35b950c",
46
+ "@flighthq/materials": "0.3.1-next.1041.35b950c",
47
+ "@flighthq/mesh": "0.3.1-next.1041.35b950c",
48
+ "@flighthq/node": "0.3.1-next.1041.35b950c",
49
+ "@flighthq/registry": "0.3.1-next.1041.35b950c",
50
+ "@flighthq/signals": "0.3.1-next.1041.35b950c",
51
+ "@flighthq/types": "0.3.1-next.1041.35b950c"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@flighthq/camera": "*",
@@ -2,7 +2,8 @@ import { createTintAdjustment } from '@flighthq/adjustments/contract';
2
2
  import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
3
3
  import { setNodeColorAdjustments } from '@flighthq/node/contract';
4
4
  import { createDisplayObject } from '@flighthq/scene2d/contract';
5
- import type { Adjustment, Renderable } from '@flighthq/types/contract';
5
+ import type { Adjustment, Renderable, RenderProxy, RenderState } from '@flighthq/types/contract';
6
+ import { RegistryEntryState } from '@flighthq/types/contract';
6
7
 
7
8
  import { areColorAdjustmentGuardsEnabled, enableColorAdjustmentGuards } from './enableColorAdjustmentGuards';
8
9
  import { enableColorAdjustments } from './enableColorAdjustments';
@@ -15,6 +16,15 @@ describe('areColorAdjustmentGuardsEnabled', () => {
15
16
  expect(areColorAdjustmentGuardsEnabled(state)).toBe(false);
16
17
  enableColorAdjustmentGuards(state);
17
18
  expect(areColorAdjustmentGuardsEnabled(state)).toBe(true);
19
+ const table = getRenderStateRuntime(state).registries.colorAdjustmentUnsupportedGuard;
20
+ expect(table).toMatchObject({
21
+ entry: { state: RegistryEntryState.Bound },
22
+ onMiss: 'Disabled',
23
+ registry: 'ColorAdjustmentUnsupportedGuard',
24
+ shape: 'slot',
25
+ });
26
+ enableColorAdjustmentGuards(state);
27
+ expect(getRenderStateRuntime(state).registries.colorAdjustmentUnsupportedGuard).toBe(table);
18
28
  });
19
29
  });
20
30
 
@@ -30,7 +40,7 @@ describe('enableColorAdjustmentGuards', () => {
30
40
  const sink = createMemoryLogSink(8);
31
41
  addLogSink(sink.sink);
32
42
  try {
33
- getRenderStateRuntime(state).colorAdjustmentResolver!(state, data);
43
+ resolveColorAdjustments(state, data);
34
44
  const entries = getMemoryLogSinkEntries(sink);
35
45
  expect(entries.length).toBe(1);
36
46
  expect(String((entries[0].data as Record<string, unknown>).message)).toContain('not inline-able');
@@ -49,10 +59,16 @@ describe('enableColorAdjustmentGuards', () => {
49
59
  const sink = createMemoryLogSink(8);
50
60
  addLogSink(sink.sink);
51
61
  try {
52
- getRenderStateRuntime(state).colorAdjustmentResolver!(state, data);
62
+ resolveColorAdjustments(state, data);
53
63
  expect(getMemoryLogSinkEntries(sink).length).toBe(0);
54
64
  } finally {
55
65
  removeLogSink(sink.sink);
56
66
  }
57
67
  });
58
68
  });
69
+
70
+ function resolveColorAdjustments(state: RenderState, data: RenderProxy): void {
71
+ const entry = getRenderStateRuntime(state).registries.colorAdjustments?.entry;
72
+ if (entry?.state !== RegistryEntryState.Bound) throw new Error('Color-adjustment resolver is not enabled');
73
+ entry.value(state, data);
74
+ }
@@ -7,6 +7,7 @@ import {
7
7
  import { addNodeChild, setNodeColorAdjustments } from '@flighthq/node/contract';
8
8
  import { createDisplayObject, getNode2DRuntime } from '@flighthq/scene2d/contract';
9
9
  import type { Renderable, RenderProxy, RenderState } from '@flighthq/types/contract';
10
+ import { RegistryEntryState } from '@flighthq/types/contract';
10
11
 
11
12
  import { areColorAdjustmentsEnabled, enableColorAdjustments } from './enableColorAdjustments';
12
13
  import { createRenderProxy, getRenderProxy2D, prepareScene2DRender } from './renderProxy';
@@ -22,6 +23,20 @@ describe('areColorAdjustmentsEnabled', () => {
22
23
  });
23
24
 
24
25
  describe('enableColorAdjustments', () => {
26
+ it('replaces the persistent resolver slot once and remains idempotent', () => {
27
+ const state = createRenderState();
28
+ const runtime = getRenderStateRuntime(state);
29
+ const empty = runtime.registries.colorAdjustments;
30
+
31
+ enableColorAdjustments(state);
32
+
33
+ const bound = runtime.registries.colorAdjustments!;
34
+ expect(bound).not.toBe(empty);
35
+ expect(bound.entry?.state).toBe(RegistryEntryState.Bound);
36
+ enableColorAdjustments(state);
37
+ expect(runtime.registries.colorAdjustments).toBe(bound);
38
+ });
39
+
25
40
  it('leaves color adjustments unresolved until the state opts in', () => {
26
41
  const state = createRenderState();
27
42
  const node = createDisplayObject();
@@ -229,5 +244,7 @@ function createEnabledRenderState(): RenderState {
229
244
  }
230
245
 
231
246
  function resolveColorAdjustments(state: RenderState, data: RenderProxy, parentData?: RenderProxy): void {
232
- getRenderStateRuntime(state).colorAdjustmentResolver!(state, data, parentData);
247
+ const entry = getRenderStateRuntime(state).registries.colorAdjustments?.entry;
248
+ if (entry?.state !== RegistryEntryState.Bound) throw new Error('Color-adjustment resolver is not enabled');
249
+ entry.value(state, data, parentData);
233
250
  }
@@ -1,5 +1,5 @@
1
- import type { Renderer, Scene2DKindUsage, SceneCoverageEntry } from '@flighthq/types/contract';
2
- import { RenderRegistry, SceneCoverage } from '@flighthq/types/contract';
1
+ import type { Renderer, Scene2DKindUsage, SceneCoverageCatalog, SceneCoverageEntry } from '@flighthq/types/contract';
2
+ import { RegistryEntryState, RenderRegistry, RequirementFacet, SceneCoverage } from '@flighthq/types/contract';
3
3
  import { describe, expect, it } from 'vitest';
4
4
 
5
5
  import { explainScene2DCoverage, hasScene2DCoverage } from './explainScene2DCoverage';
@@ -8,28 +8,75 @@ import { createRenderState } from './renderState';
8
8
  import { getRenderStateRuntime } from './renderState';
9
9
 
10
10
  const renderer: Renderer = { createData: () => null, submit: () => {} } as unknown as Renderer;
11
+ const coverageCatalog: SceneCoverageCatalog = [
12
+ {
13
+ kind: 'Shape',
14
+ registrations: [
15
+ { module: '@flighthq/scene2d', registrar: 'registerShapeRenderer' },
16
+ { module: '@flighthq/scene2d-canvas', registrar: 'registerCanvasShapeCommands' },
17
+ ],
18
+ registry: RenderRegistry.NodeRenderer,
19
+ },
20
+ {
21
+ kind: 'Sprite',
22
+ registrations: [{ module: '@flighthq/scene2d', registrar: 'registerSpriteRenderer' }],
23
+ registry: RenderRegistry.NodeRenderer,
24
+ },
25
+ {
26
+ kind: 'beginFill',
27
+ registrations: [{ module: '@flighthq/scene2d-canvas', registrar: 'registerCanvasShapeCommands' }],
28
+ registry: RenderRegistry.ShapeCommandHandler,
29
+ },
30
+ ];
11
31
 
12
32
  function usage(overrides: Partial<Scene2DKindUsage> = {}): Scene2DKindUsage {
13
33
  return { blendModes: [], materialKinds: [], nodeKinds: [], shapeCommandKeys: [], ...overrides };
14
34
  }
15
35
 
16
- function entries(state: ReturnType<typeof createRenderState>, u: Scene2DKindUsage): SceneCoverageEntry[] {
36
+ function entries(
37
+ state: ReturnType<typeof createRenderState>,
38
+ u: Scene2DKindUsage,
39
+ catalog: SceneCoverageCatalog = coverageCatalog,
40
+ ): SceneCoverageEntry[] {
17
41
  const out: SceneCoverageEntry[] = [];
18
- explainScene2DCoverage(out, state, u);
42
+ explainScene2DCoverage(out, state, u, catalog);
19
43
  return out;
20
44
  }
21
45
 
22
- // The registry is a plain Map on the base runtime; registering a real command means reaching through
46
+ // The registry lives on the base runtime; registering a real command means reaching through
23
47
  // scene2d-canvas, which render cannot depend on, so the test installs the binding directly.
24
48
  function wireShapeCommand(state: ReturnType<typeof createRenderState>, key: string): void {
25
49
  const runtime = getRenderStateRuntime(state);
26
- (runtime.canvasShapeCommandRegistry ??= new Map()).set(key, { key, draw: () => {} } as never);
50
+ runtime.registries.canvasShapeCommands = {
51
+ entries: new Map([[key, { state: RegistryEntryState.Bound, value: { key, draw: () => {} } as never }]]),
52
+ onMiss: 'Unregistered',
53
+ registry: 'CanvasShapeCommand',
54
+ shape: 'keyed',
55
+ };
27
56
  }
28
57
 
29
58
  describe('explainScene2DCoverage', () => {
30
- it('reports an unregistered node kind as Missing, since the node and its subtree never draw', () => {
59
+ it('reports an unregistered node kind with the primary catalog remedy', () => {
31
60
  expect(entries(createRenderState(), usage({ nodeKinds: ['Shape'] }))).toEqual([
32
- { coverage: SceneCoverage.Missing, kind: 'Shape', registry: RenderRegistry.NodeRenderer },
61
+ {
62
+ coverage: SceneCoverage.Unregistered,
63
+ facet: RequirementFacet.SceneNodeKind,
64
+ kind: 'Shape',
65
+ module: '@flighthq/scene2d',
66
+ registrar: 'registerShapeRenderer',
67
+ registry: RenderRegistry.NodeRenderer,
68
+ },
69
+ ]);
70
+ });
71
+
72
+ it('reports an uncatalogued node kind as Unavailable, with no remedy fields', () => {
73
+ expect(entries(createRenderState(), usage({ nodeKinds: ['Shape'] }), [])).toEqual([
74
+ {
75
+ coverage: SceneCoverage.Unavailable,
76
+ facet: RequirementFacet.SceneNodeKind,
77
+ kind: 'Shape',
78
+ registry: RenderRegistry.NodeRenderer,
79
+ },
33
80
  ]);
34
81
  });
35
82
 
@@ -37,14 +84,22 @@ describe('explainScene2DCoverage', () => {
37
84
  const state = createRenderState();
38
85
  registerRenderer(state, 'Shape', renderer);
39
86
  expect(entries(state, usage({ nodeKinds: ['Shape'] }))).toEqual([
40
- { coverage: SceneCoverage.Satisfied, kind: 'Shape', registry: RenderRegistry.NodeRenderer },
87
+ {
88
+ coverage: SceneCoverage.Satisfied,
89
+ facet: RequirementFacet.SceneNodeKind,
90
+ kind: 'Shape',
91
+ registry: RenderRegistry.NodeRenderer,
92
+ },
41
93
  ]);
42
94
  });
43
95
 
44
96
  it('reports an unhandled shape command key against the state that would replay it', () => {
45
97
  expect(entries(createRenderState(), usage({ shapeCommandKeys: ['beginFill'] }))).toContainEqual({
46
- coverage: SceneCoverage.Missing,
98
+ coverage: SceneCoverage.Unregistered,
99
+ facet: RequirementFacet.SceneShapeCommand,
47
100
  kind: 'beginFill',
101
+ module: '@flighthq/scene2d-canvas',
102
+ registrar: 'registerCanvasShapeCommands',
48
103
  registry: RenderRegistry.ShapeCommandHandler,
49
104
  });
50
105
  });
@@ -54,6 +109,7 @@ describe('explainScene2DCoverage', () => {
54
109
  wireShapeCommand(state, 'beginFill');
55
110
  expect(entries(state, usage({ shapeCommandKeys: ['beginFill'] }))).toContainEqual({
56
111
  coverage: SceneCoverage.Satisfied,
112
+ facet: RequirementFacet.SceneShapeCommand,
57
113
  kind: 'beginFill',
58
114
  registry: RenderRegistry.ShapeCommandHandler,
59
115
  });
@@ -64,7 +120,7 @@ describe('explainScene2DCoverage', () => {
64
120
  registerRenderer(state, 'Shape', renderer);
65
121
  const found = entries(state, usage({ nodeKinds: ['Shape', 'Sprite'], shapeCommandKeys: ['beginFill'] }));
66
122
  expect(found).toHaveLength(3);
67
- expect(found.filter((e) => e.coverage === SceneCoverage.Missing).map((e) => e.kind)).toEqual([
123
+ expect(found.filter((e) => e.coverage !== SceneCoverage.Satisfied).map((e) => e.kind)).toEqual([
68
124
  'Sprite',
69
125
  'beginFill',
70
126
  ]);
@@ -74,9 +130,9 @@ describe('explainScene2DCoverage', () => {
74
130
  const state = createRenderState();
75
131
  const u = usage({ nodeKinds: ['Shape'] });
76
132
  const out: SceneCoverageEntry[] = [];
77
- explainScene2DCoverage(out, state, u);
133
+ explainScene2DCoverage(out, state, u, coverageCatalog);
78
134
  const first = out.length;
79
- explainScene2DCoverage(out, state, u);
135
+ explainScene2DCoverage(out, state, u, coverageCatalog);
80
136
  expect(out).toHaveLength(first);
81
137
  });
82
138
  });
@@ -92,7 +148,7 @@ describe('hasScene2DCoverage', () => {
92
148
  wireShapeCommand(state, 'beginFill');
93
149
  const u = usage({ nodeKinds: ['Shape'], shapeCommandKeys: ['beginFill'] });
94
150
  const out: SceneCoverageEntry[] = [];
95
- explainScene2DCoverage(out, state, u);
151
+ explainScene2DCoverage(out, state, u, coverageCatalog);
96
152
  expect(out.every((e) => e.coverage === SceneCoverage.Satisfied)).toBe(true);
97
153
  expect(hasScene2DCoverage(state, u)).toBe(true);
98
154
  });
@@ -102,7 +158,7 @@ describe('hasScene2DCoverage', () => {
102
158
  registerRenderer(state, 'Shape', renderer);
103
159
  const u = usage({ nodeKinds: ['Shape', 'Sprite'] });
104
160
  const out: SceneCoverageEntry[] = [];
105
- explainScene2DCoverage(out, state, u);
161
+ explainScene2DCoverage(out, state, u, coverageCatalog);
106
162
  const gaps = out.filter((e) => e.coverage !== SceneCoverage.Satisfied);
107
163
  expect(hasScene2DCoverage(state, u)).toBe(gaps.length === 0);
108
164
  });
@@ -117,7 +117,7 @@ describe('registerRenderCacheRenderer', () => {
117
117
  const state = createRenderState();
118
118
  const renderer = { createData: () => null, submit: vi.fn() };
119
119
  registerRenderCacheRenderer(state, renderer as any);
120
- expect(getRenderStateRuntime(state).rendererMap.get(RenderCacheKind)).toBe(renderer);
120
+ expect(getRegistryTableEntry(getRenderStateRuntime(state).registries.renderers, RenderCacheKind)).toBe(renderer);
121
121
  });
122
122
  });
123
123
 
@@ -149,3 +149,4 @@ describe('useRenderCache', () => {
149
149
  expect(getRenderProxyAdapter(stateB, obj as any)).toBeNull();
150
150
  });
151
151
  });
152
+ import { getRegistryTableEntry } from '@flighthq/registry/contract';
@@ -14,6 +14,7 @@ import {
14
14
  import { createDisplayObject, setNode2DClip } from '@flighthq/scene2d/contract';
15
15
  import { createSprite } from '@flighthq/scene2d/contract';
16
16
  import type { ClipRegion, Node, RenderProxy, RenderProxy2D, RenderState } from '@flighthq/types/contract';
17
+ import { RegistryEntryState } from '@flighthq/types/contract';
17
18
 
18
19
  import { registerRenderer } from './renderer';
19
20
  import {
@@ -224,17 +225,23 @@ describe('getOrCreateRenderProxy2D', () => {
224
225
  expect(a).toBe(b);
225
226
  });
226
227
 
227
- it('syncs renderer when rendererMapId has changed', () => {
228
+ it('invalidates and syncs an existing proxy when the renderer table is replaced', () => {
228
229
  const state = createRenderState();
229
230
  const source = createSprite();
230
231
  const node = getOrCreateRenderProxy2D(state, source);
231
232
  expect(node.renderer).toBeNull();
232
233
 
233
234
  const renderer = makeRenderer();
235
+ const runtime = getRenderStateRuntime(state);
236
+ const tableBeforeRegistration = runtime.registries.renderers;
237
+ const idBeforeRegistration = runtime.rendererMapId;
234
238
  registerRenderer(state, source.kind, renderer as any);
239
+ expect(runtime.registries.renderers).not.toBe(tableBeforeRegistration);
240
+ expect(runtime.rendererMapId).toBe(idBeforeRegistration + 1);
235
241
  getOrCreateRenderProxy2D(state, source);
236
242
 
237
243
  expect(node.renderer).toBe(renderer);
244
+ expect(node.rendererMapId).toBe(runtime.rendererMapId);
238
245
  });
239
246
  });
240
247
 
@@ -583,7 +590,13 @@ describe('updateRenderProxy2D', () => {
583
590
  expect((resolvedData as RenderProxy2D).clipDepth).toBe(0);
584
591
  expect(resolvedData.lastChildrenId).toBe(-1);
585
592
  });
586
- getRenderStateRuntime(state).colorAdjustmentResolver = resolver;
593
+ const runtime = getRenderStateRuntime(state);
594
+ runtime.registries.colorAdjustments = {
595
+ entry: { state: RegistryEntryState.Bound, value: resolver },
596
+ onMiss: 'Disabled',
597
+ registry: 'ColorAdjustments',
598
+ shape: 'slot',
599
+ };
587
600
 
588
601
  updateRenderProxy2D(state, root, data, undefined);
589
602
 
@@ -1,8 +1,15 @@
1
1
  import { createMatrix } from '@flighthq/geometry/contract';
2
- import type { RenderState } from '@flighthq/types/contract';
3
- import { BlendMode, EntityRuntimeKey } from '@flighthq/types/contract';
2
+ import { createKeyedTable, createSlotTable } from '@flighthq/registry/contract';
3
+ import type { ColorAdjustmentUnsupportedGuard, RenderState } from '@flighthq/types/contract';
4
+ import { BlendMode, EntityRuntimeKey, RegistryEntryState } from '@flighthq/types/contract';
4
5
 
5
- import { createRenderState, createRenderStateRuntime, destroyRenderState, getRenderStateRuntime } from './renderState';
6
+ import {
7
+ createRenderState,
8
+ createRenderStateRuntime,
9
+ destroyRenderState,
10
+ getColorAdjustmentUnsupportedGuard,
11
+ getRenderStateRuntime,
12
+ } from './renderState';
6
13
 
7
14
  describe('createRenderState', () => {
8
15
  let state: RenderState;
@@ -30,7 +37,10 @@ describe('createRenderState', () => {
30
37
  expect(runtime.currentFrameId).toStrictEqual(0);
31
38
  expect(runtime.renderProxyMap).toStrictEqual(new WeakMap());
32
39
  expect(runtime.renderProxyAdapterMap).toStrictEqual(new WeakMap());
33
- expect(runtime.rendererMap).toStrictEqual(new Map());
40
+ expect(runtime.registries).toStrictEqual({
41
+ renderers: createKeyedTable('NodeRenderer', 'Unregistered'),
42
+ strokeTessellator: createSlotTable('StrokeTessellator', 'Rasterize'),
43
+ });
34
44
  expect(runtime.rendererMapId).toStrictEqual(0);
35
45
  expect(runtime.tempStack).toStrictEqual([]);
36
46
  });
@@ -69,11 +79,12 @@ describe('createRenderState', () => {
69
79
  describe('createRenderStateRuntime', () => {
70
80
  it('initializes the machinery fields', () => {
71
81
  const runtime = createRenderStateRuntime();
72
- expect(runtime.colorAdjustmentResolver).toBeNull();
73
82
  expect(runtime.currentFrameId).toStrictEqual(0);
74
83
  expect(runtime.renderProxyMap).toStrictEqual(new WeakMap());
75
84
  expect(runtime.renderProxyAdapterMap).toStrictEqual(new WeakMap());
76
- expect(runtime.rendererMap).toStrictEqual(new Map());
85
+ expect(runtime.registries.colorAdjustments).toBeUndefined();
86
+ expect(runtime.registries.renderers).toStrictEqual(createKeyedTable('NodeRenderer', 'Unregistered'));
87
+ expect(runtime.registries.strokeTessellator).toStrictEqual(createSlotTable('StrokeTessellator', 'Rasterize'));
77
88
  expect(runtime.rendererMapId).toStrictEqual(0);
78
89
  expect(runtime.tempStack).toStrictEqual([]);
79
90
  });
@@ -82,7 +93,8 @@ describe('createRenderStateRuntime', () => {
82
93
  const a = createRenderStateRuntime();
83
94
  const b = createRenderStateRuntime();
84
95
  expect(a).not.toBe(b);
85
- expect(a.rendererMap).not.toBe(b.rendererMap);
96
+ expect(a.registries.renderers).not.toBe(b.registries.renderers);
97
+ expect(a.registries.strokeTessellator).not.toBe(b.registries.strokeTessellator);
86
98
  });
87
99
  });
88
100
 
@@ -91,12 +103,39 @@ describe('destroyRenderState', () => {
91
103
  const state = createRenderState();
92
104
  const runtime = getRenderStateRuntime(state);
93
105
  runtime.tempStack.push({} as never);
94
- runtime.renderEffectPaddingResolverRegistry = new Map();
106
+ runtime.registries.effectPaddingResolvers = {
107
+ entries: new Map(),
108
+ onMiss: 'Zero',
109
+ registry: 'RenderEffectPaddingResolver',
110
+ shape: 'keyed',
111
+ };
95
112
 
96
113
  destroyRenderState(state);
97
114
 
98
115
  expect(runtime.tempStack).toHaveLength(0);
99
- expect(runtime.renderEffectPaddingResolverRegistry).toBeNull();
116
+ expect(runtime.registries.effectPaddingResolvers).toBeUndefined();
117
+ });
118
+ });
119
+
120
+ describe('getColorAdjustmentUnsupportedGuard', () => {
121
+ it('resolves only a bound guard entry', () => {
122
+ const state = createRenderState();
123
+ const guard: ColorAdjustmentUnsupportedGuard = vi.fn();
124
+ const runtime = getRenderStateRuntime(state);
125
+
126
+ expect(getColorAdjustmentUnsupportedGuard(state)).toBeNull();
127
+ runtime.registries.colorAdjustmentUnsupportedGuard = {
128
+ entry: { state: RegistryEntryState.Bound, value: guard },
129
+ onMiss: 'Disabled',
130
+ registry: 'ColorAdjustmentUnsupportedGuard',
131
+ shape: 'slot',
132
+ };
133
+ expect(getColorAdjustmentUnsupportedGuard(state)).toBe(guard);
134
+ runtime.registries.colorAdjustmentUnsupportedGuard = {
135
+ ...runtime.registries.colorAdjustmentUnsupportedGuard,
136
+ entry: { state: RegistryEntryState.Tombstoned },
137
+ };
138
+ expect(getColorAdjustmentUnsupportedGuard(state)).toBeNull();
100
139
  });
101
140
  });
102
141