@vgai/engine 0.5.41 → 0.5.44

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.
Files changed (129) hide show
  1. package/dist/adapter/adapter-module.d.ts +28 -29
  2. package/dist/adapter/adapter-module.d.ts.map +1 -1
  3. package/dist/adapter/adapter-module.js +11 -34
  4. package/dist/adapter/authoring-seam-contract.d.ts +10 -0
  5. package/dist/adapter/authoring-seam-contract.d.ts.map +1 -1
  6. package/dist/adapter/authoring-seam-contract.js +2 -0
  7. package/dist/adapter/authoring.d.ts +31 -0
  8. package/dist/adapter/authoring.d.ts.map +1 -1
  9. package/dist/adapter/finders/finder-result.d.ts +2 -2
  10. package/dist/adapter/finders/finder-result.d.ts.map +1 -1
  11. package/dist/adapter/finders/index.d.ts +15 -2
  12. package/dist/adapter/finders/index.d.ts.map +1 -1
  13. package/dist/adapter/finders/index.js +38 -10
  14. package/dist/adapter/finders/prefabs-from-stories.d.ts.map +1 -1
  15. package/dist/adapter/finders/registry.d.ts +45 -0
  16. package/dist/adapter/finders/registry.d.ts.map +1 -0
  17. package/dist/adapter/finders/registry.js +56 -0
  18. package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
  19. package/dist/adapter/ingest/scene-capture.js +50 -20
  20. package/dist/adapter/system-adapter.d.ts +3 -1
  21. package/dist/adapter/system-adapter.d.ts.map +1 -1
  22. package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
  23. package/dist/canvas-react/pixi-react-root-factory.js +21 -11
  24. package/dist/data/vite-plugin-data.d.ts +1 -1
  25. package/dist/data/vite-plugin-data.d.ts.map +1 -1
  26. package/dist/data/vite-plugin-data.js +2 -2
  27. package/dist/ecs/user-data.d.ts +6 -0
  28. package/dist/ecs/user-data.d.ts.map +1 -1
  29. package/dist/ecs/user-data.js +4 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/input/host-pointer.d.ts +18 -0
  34. package/dist/input/host-pointer.d.ts.map +1 -1
  35. package/dist/input/host-pointer.js +54 -5
  36. package/dist/input/input-manager.d.ts +83 -1
  37. package/dist/input/input-manager.d.ts.map +1 -1
  38. package/dist/input/input-manager.js +357 -8
  39. package/dist/manifest/configuration-kinds.d.ts +97 -0
  40. package/dist/manifest/configuration-kinds.d.ts.map +1 -0
  41. package/dist/manifest/configuration-kinds.js +148 -0
  42. package/dist/manifest/index.d.ts +2 -1
  43. package/dist/manifest/index.d.ts.map +1 -1
  44. package/dist/manifest/index.js +4 -3
  45. package/dist/manifest/kind-modules.d.ts +18 -0
  46. package/dist/manifest/kind-modules.d.ts.map +1 -0
  47. package/dist/manifest/kind-modules.js +58 -0
  48. package/dist/manifest/load-file.d.ts +3 -3
  49. package/dist/manifest/load-file.d.ts.map +1 -1
  50. package/dist/manifest/load-file.js +4 -4
  51. package/dist/manifest/load.d.ts +17 -12
  52. package/dist/manifest/load.d.ts.map +1 -1
  53. package/dist/manifest/load.js +29 -19
  54. package/dist/manifest/runtime-environment.d.ts +11 -0
  55. package/dist/manifest/runtime-environment.d.ts.map +1 -0
  56. package/dist/manifest/runtime-environment.js +44 -0
  57. package/dist/manifest/schema.d.ts +10 -9
  58. package/dist/manifest/schema.d.ts.map +1 -1
  59. package/dist/manifest/schema.js +32 -27
  60. package/dist/react/use-data.d.ts +1 -1
  61. package/dist/react/use-data.js +1 -1
  62. package/dist/render/environment-capture.d.ts +10 -1
  63. package/dist/render/environment-capture.d.ts.map +1 -1
  64. package/dist/render/environment-capture.js +25 -0
  65. package/dist/render/ibl-override-material.d.ts +11 -2
  66. package/dist/render/ibl-override-material.d.ts.map +1 -1
  67. package/dist/render/ibl-override-material.js +16 -2
  68. package/dist/render/matcap-texture.d.ts +5 -0
  69. package/dist/render/matcap-texture.d.ts.map +1 -0
  70. package/dist/render/matcap-texture.js +82 -0
  71. package/dist/render/soft-particle-depth.d.ts +14 -8
  72. package/dist/render/soft-particle-depth.d.ts.map +1 -1
  73. package/dist/render/soft-particle-depth.js +37 -14
  74. package/dist/render/viewport-shading.d.ts +11 -1
  75. package/dist/render/viewport-shading.d.ts.map +1 -1
  76. package/dist/render/viewport-shading.js +25 -0
  77. package/dist/runtime/create-runtime.js +6 -5
  78. package/dist/runtime/debug-bridge.d.ts.map +1 -1
  79. package/dist/runtime/debug-bridge.js +17 -3
  80. package/dist/runtime/mount-manifest.d.ts.map +1 -1
  81. package/dist/runtime/mount-manifest.js +11 -2
  82. package/dist/settings/schema.d.ts +99 -0
  83. package/dist/settings/schema.d.ts.map +1 -0
  84. package/dist/settings/schema.js +110 -0
  85. package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
  86. package/dist/world3d-react/r3f-root-factory.js +34 -36
  87. package/dist/world3d-react/rapier-physics-bridge.d.ts.map +1 -1
  88. package/dist/world3d-react/rapier-physics-bridge.js +21 -6
  89. package/dist-config/config.js +1 -1
  90. package/dist-config/config.js.map +2 -2
  91. package/package.json +2 -1
  92. package/schemas/engine-capabilities.json +4 -4
  93. package/schemas/vgai-project.schema.json +41 -36
  94. package/schemas/vgai-settings.schema.json +112 -0
  95. package/src/adapter/adapter-module.ts +33 -51
  96. package/src/adapter/authoring-seam-contract.ts +2 -0
  97. package/src/adapter/authoring.ts +31 -0
  98. package/src/adapter/finders/finder-result.ts +2 -2
  99. package/src/adapter/finders/index.ts +68 -12
  100. package/src/adapter/finders/prefabs-from-stories.ts +2 -2
  101. package/src/adapter/finders/registry.ts +95 -0
  102. package/src/adapter/finders/scenes-from-entrypoint-selection.ts +4 -4
  103. package/src/adapter/ingest/scene-capture.ts +48 -19
  104. package/src/adapter/system-adapter.ts +3 -1
  105. package/src/canvas-react/pixi-react-root-factory.tsx +21 -11
  106. package/src/data/vite-plugin-data.ts +4 -2
  107. package/src/ecs/user-data.ts +6 -0
  108. package/src/index.ts +1 -1
  109. package/src/input/host-pointer.ts +84 -5
  110. package/src/input/input-manager.ts +404 -7
  111. package/src/manifest/configuration-kinds.ts +241 -0
  112. package/src/manifest/index.ts +4 -3
  113. package/src/manifest/kind-modules.ts +58 -0
  114. package/src/manifest/load-file.ts +11 -5
  115. package/src/manifest/load.ts +53 -22
  116. package/src/manifest/runtime-environment.ts +48 -0
  117. package/src/manifest/schema.ts +39 -33
  118. package/src/react/use-data.ts +1 -1
  119. package/src/render/environment-capture.ts +30 -1
  120. package/src/render/ibl-override-material.ts +32 -2
  121. package/src/render/matcap-texture.ts +92 -0
  122. package/src/render/soft-particle-depth.ts +43 -14
  123. package/src/render/viewport-shading.ts +33 -1
  124. package/src/runtime/create-runtime.ts +6 -5
  125. package/src/runtime/debug-bridge.ts +21 -3
  126. package/src/runtime/mount-manifest.ts +13 -2
  127. package/src/settings/schema.ts +127 -0
  128. package/src/world3d-react/r3f-root-factory.tsx +45 -39
  129. package/src/world3d-react/rapier-physics-bridge.tsx +22 -8
@@ -491,42 +491,22 @@ export interface PrefabsFromStoriesParams {
491
491
  readonly finder: 'prefabsFromStories';
492
492
  }
493
493
 
494
- /** Every finder this engine ships, as the adapter's concrete SELECTION. */
495
- export type FinderSelection = ScenesFromEntrypointSelectionParams | PrefabsFromStoriesParams;
496
-
497
- /** The finder names, as a value — what the loader's dispatcher switches on. */
498
- export const FINDER_NAMES = ['scenesFromEntrypointSelection', 'prefabsFromStories'] as const;
499
- export type FinderName = (typeof FINDER_NAMES)[number];
500
-
501
- const ScenesFromEntrypointSelectionSchema = z
502
- .object({
503
- finder: z.literal('scenesFromEntrypointSelection'),
504
- regions: z
505
- .union([z.array(z.string().min(1)).min(1), z.literal(EXPORTED_COMPOSITION_REGIONS)])
506
- .describe('Region ids whose entrypoints carry the selection, or the rule form'),
507
- selection: z
508
- .string()
509
- .min(1)
510
- .optional()
511
- .describe(
512
- "Identifier of the entrypoint's module-level scene selection table; omit for a " +
513
- 'single-composition entrypoint',
514
- ),
515
- })
516
- .strict()
517
- .refine((value) => value.selection === undefined || Array.isArray(value.regions), {
518
- message:
519
- 'scenesFromEntrypointSelection: `selection` names an identifier in ONE entrypoint’s ' +
520
- `source, so it cannot be paired with the \`${EXPORTED_COMPOSITION_REGIONS}\` rule — ` +
521
- 'name the region explicitly (`regions: ["<rootId>"]`).',
522
- });
523
-
524
- const PrefabsFromStoriesSchema = z.object({ finder: z.literal('prefabsFromStories') }).strict();
494
+ /**
495
+ * A finder selection: the finder's registered NAME plus its own parameters.
496
+ * The set of finders is OPEN (ARCHITECTURE-CORE §The editor protocol,
497
+ * "Documents, not scenes"): the engine's own register in
498
+ * `adapter/finders/registry.ts`, a contribution registers more, and this
499
+ * module validates only the envelope — each finder's parameters are checked
500
+ * against ITS schema the moment the loader runs it, loudly and by name.
501
+ */
502
+ export type FinderSelection = {
503
+ readonly finder: string;
504
+ readonly [param: string]: unknown;
505
+ };
525
506
 
526
- const FinderSelectionSchema = z.union([
527
- ScenesFromEntrypointSelectionSchema,
528
- PrefabsFromStoriesSchema,
529
- ]);
507
+ const FinderSelectionSchema = z
508
+ .object({ finder: z.string().min(1).describe('Registered finder name') })
509
+ .passthrough();
530
510
 
531
511
  // ---------------------------------------------------------------------------
532
512
  // The scene table
@@ -614,11 +594,13 @@ const SceneSourceSchema = z
614
594
  * verb, differing only in instance site (ARCHITECTURE-CORE §Roots: "A scene is
615
595
  * a ROLE, not a kind … the same kind of thing as a prefab").
616
596
  */
617
- export interface SceneEntry {
597
+ export interface DocumentEntry {
618
598
  readonly id: string;
619
599
  readonly label: string;
620
600
  /** `scene` (swaps at the entrypoint's slot) or `prefab` (placed by a designer). */
621
- readonly kind: 'scene' | 'prefab';
601
+ /** OPEN: `scene` and `prefab` are the first two kinds; a page, a component,
602
+ * a model, a bake are kinds the same way. */
603
+ readonly kind: string;
622
604
  /** Owning region id; `null` for a project-scoped entry bound to no one root. */
623
605
  readonly region: string | null;
624
606
  /**
@@ -633,20 +615,20 @@ export interface SceneEntry {
633
615
  * infers setup from neighboring filenames or game globals. */
634
616
  readonly isolationSetup?: SceneSource;
635
617
  /** Which finder produced it; absent = the adapter stated it outright. */
636
- readonly finder?: FinderName;
618
+ readonly finder?: string;
637
619
  }
638
620
 
639
- const SceneEntrySchema = z
621
+ const DocumentEntrySchema = z
640
622
  .object({
641
623
  id: z.string().min(1),
642
624
  label: z.string().min(1),
643
- kind: z.enum(['scene', 'prefab']),
625
+ kind: z.string().min(1),
644
626
  region: z.string().min(1).nullable(),
645
627
  authorable: z.boolean(),
646
628
  reach: SceneReachSchema,
647
629
  source: SceneSourceSchema.optional(),
648
630
  isolationSetup: SceneSourceSchema.optional(),
649
- finder: z.enum(FINDER_NAMES).optional(),
631
+ finder: z.string().min(1).optional(),
650
632
  })
651
633
  .strict();
652
634
 
@@ -654,8 +636,8 @@ const SceneEntrySchema = z
654
636
  * The adapter's scene table: entries stated outright, finder selections the
655
637
  * host runs to produce the rest, and the entry open by default.
656
638
  */
657
- export interface AdapterSceneTable {
658
- readonly entries?: readonly SceneEntry[];
639
+ export interface AdapterDocumentTable {
640
+ readonly entries?: readonly DocumentEntry[];
659
641
  readonly find?: readonly FinderSelection[];
660
642
  /**
661
643
  * Id of the entry open by default. Omit to let the finder's own answer
@@ -665,9 +647,9 @@ export interface AdapterSceneTable {
665
647
  readonly default?: string;
666
648
  }
667
649
 
668
- const AdapterSceneTableSchema = z
650
+ const AdapterDocumentTableSchema = z
669
651
  .object({
670
- entries: z.array(SceneEntrySchema).optional(),
652
+ entries: z.array(DocumentEntrySchema).optional(),
671
653
  find: z.array(FinderSelectionSchema).optional(),
672
654
  default: z.string().min(1).optional(),
673
655
  })
@@ -815,7 +797,7 @@ export interface AdapterDefinition {
815
797
  * an explicit `regions` LIST carries its own and rejects these by name.
816
798
  */
817
799
  readonly regionIncludes: Readonly<Record<string, AdapterRegionOverlay>>;
818
- readonly scenes: AdapterSceneTable;
800
+ readonly documents: AdapterDocumentTable;
819
801
  readonly observation: readonly ObservationDeclaration[];
820
802
  readonly input?: AdapterInputBinding | undefined;
821
803
  }
@@ -826,7 +808,7 @@ export interface AdapterDefinition {
826
808
  export interface AdapterDefinitionInput {
827
809
  readonly regions?: RegionBinding;
828
810
  readonly regionIncludes?: Readonly<Record<string, AdapterRegionOverlay>>;
829
- readonly scenes?: AdapterSceneTable;
811
+ readonly documents?: AdapterDocumentTable;
830
812
  readonly observation?: readonly ObservationDeclaration[];
831
813
  readonly input?: AdapterInputBinding | undefined;
832
814
  }
@@ -847,7 +829,7 @@ export const AdapterDefinitionSchema = z
847
829
  .object({
848
830
  regions: RegionBindingSchema.optional(),
849
831
  regionIncludes: RegionIncludesSchema.optional(),
850
- scenes: AdapterSceneTableSchema.optional(),
832
+ documents: AdapterDocumentTableSchema.optional(),
851
833
  observation: z.array(ObservationDeclarationSchema).optional(),
852
834
  input: AdapterInputBindingSchema.optional(),
853
835
  })
@@ -885,7 +867,7 @@ export function defineAdapter(input: AdapterDefinitionInput = {}): AdapterDefini
885
867
  return Object.freeze({
886
868
  regions: input.regions ?? 'manifest-roots',
887
869
  regionIncludes: Object.freeze({ ...(input.regionIncludes ?? {}) }),
888
- scenes: Object.freeze({ ...(input.scenes ?? {}) }),
870
+ documents: Object.freeze({ ...(input.documents ?? {}) }),
889
871
  observation: Object.freeze([...(input.observation ?? [])]),
890
872
  ...(input.input ? { input: input.input } : {}),
891
873
  });
@@ -894,7 +876,7 @@ export function defineAdapter(input: AdapterDefinitionInput = {}): AdapterDefini
894
876
  /** Extra bindings a near-native game adds without restating the native ones. */
895
877
  export interface NativeAdapterOptions {
896
878
  /** Scene-table entries this game states outright, beside what the finders find. */
897
- readonly scenes?: readonly SceneEntry[];
879
+ readonly scenes?: readonly DocumentEntry[];
898
880
  /** Per-region `include`/`mounts` parameters for the derivation — see
899
881
  * {@link AdapterDefinition.regionIncludes}. */
900
882
  readonly regionIncludes?: Readonly<Record<string, AdapterRegionOverlay>>;
@@ -911,7 +893,7 @@ export function nativeAdapter(options: NativeAdapterOptions = {}): AdapterDefini
911
893
  return defineAdapter({
912
894
  regions: 'manifest-roots',
913
895
  ...(options.regionIncludes ? { regionIncludes: options.regionIncludes } : {}),
914
- scenes: {
896
+ documents: {
915
897
  ...(options.scenes ? { entries: options.scenes } : {}),
916
898
  find: [
917
899
  { finder: 'scenesFromEntrypointSelection', regions: EXPORTED_COMPOSITION_REGIONS },
@@ -57,6 +57,7 @@ export const AUTHORING_ADAPTER_SHAPE = defineSeamShape<AuthoringAdapter>()({
57
57
  export const HIERARCHY_PROVIDER_SHAPE = defineSeamShape<HierarchyProvider>()({
58
58
  roots: { optional: false, kind: 'function', required: 'operation' },
59
59
  node: { optional: false, kind: 'function', required: 'operation' },
60
+ crossSurfaceStructureSignature: { optional: true, kind: 'function', required: 'operation' },
60
61
  object3D: { optional: true, kind: 'function', required: 'operation' },
61
62
  idForObject3D: { optional: true, kind: 'function', required: 'effect' },
62
63
  });
@@ -82,6 +83,7 @@ export const INSPECTOR_PROVIDER_SHAPE = defineSeamShape<InspectorProvider>()({
82
83
  properties: { optional: false, kind: 'function', required: 'operation' },
83
84
  get: { optional: false, kind: 'function', required: 'operation' },
84
85
  set: { optional: false, kind: 'function', required: 'effect' },
86
+ preview: { optional: true, kind: 'function', required: 'effect' },
85
87
  editability: { optional: true, kind: 'function', required: 'operation' },
86
88
  remove: { optional: true, kind: 'function', required: 'round-trip' },
87
89
  });
@@ -116,6 +116,12 @@ export interface EditorNode {
116
116
  crossSurfaceParentId?: string;
117
117
  /** Source-owned sibling ordinal used only when native rows are joined across surfaces. */
118
118
  crossSurfaceOrder?: number;
119
+ /**
120
+ * Live source-owned label for the projected group containing this row's adapter root.
121
+ * Any row may carry it: a native surface can have no semantic root of its own when all of
122
+ * its authored rows are children of entities owned by another surface.
123
+ */
124
+ crossSurfaceGroupLabel?: string;
119
125
  }
120
126
 
121
127
  export interface PropertyDescriptor {
@@ -177,6 +183,15 @@ export interface PropertyDescriptor {
177
183
  export interface HierarchyProvider {
178
184
  roots(): EditorNode[];
179
185
  node(id: string): EditorNode | null;
186
+ /**
187
+ * OPTIONAL semantic-join revision. A composite hierarchy uses explicit
188
+ * `crossSurface*` fields to join native roots; this signature changes if and
189
+ * only if those fields, their semantic ancestry, or their order changed.
190
+ * Ordinary native-only child churn may leave it stable. `null` explicitly
191
+ * means this provider has no semantic participation. Omission means the
192
+ * composite must conservatively re-read the provider's whole hierarchy.
193
+ */
194
+ crossSurfaceStructureSignature?(): string | null;
180
195
  /**
181
196
  * OPTIONAL, THREE-SPECIFIC — the live `Object3D` for raycast/gizmo binding
182
197
  * (null for a node this adapter has no object for). A Pixi, DOM or React
@@ -331,6 +346,22 @@ export interface InspectorProvider {
331
346
  * performed by this provider, and the shell reports the live-only floor.
332
347
  */
333
348
  set(id: string, path: string, value: unknown): void | WriteAck | Promise<void | WriteAck>;
349
+ /**
350
+ * Optional — SHOW a value without writing it, for a control the user drags.
351
+ *
352
+ * The gizmo already has this pair ({@link TransformProvider.writeTransform}
353
+ * live, {@link TransformProvider.endEdit} persisting), and a colour picker
354
+ * needs the same shape: it streams values continuously while the pointer
355
+ * moves, and persisting each one is a write burst the absorb cycle cannot
356
+ * follow. So the control previews here on every change and calls
357
+ * {@link set} ONCE when the gesture ends.
358
+ *
359
+ * A preview NEVER persists and is never the truth: whatever the adapter
360
+ * mutates is discarded by the next remount unless a `set` follows. An
361
+ * adapter that cannot show a value without writing it simply omits this,
362
+ * and the control's preview is a no-op — the value still lands on commit.
363
+ */
364
+ preview?(id: string, path: string, value: unknown): void;
334
365
  /** Optional per-subject write preflight for shell affordances outside the
335
366
  * Inspector field grid (notably hierarchy eye/lock controls). The property
336
367
  * descriptor remains the Inspector's source of `readonly`; this gives other
@@ -8,11 +8,11 @@
8
8
  * different facts, and only the second is a defect.
9
9
  */
10
10
 
11
- import type { SceneEntry } from '../adapter-module';
11
+ import type { DocumentEntry } from '../adapter-module';
12
12
 
13
13
  export interface FinderResult {
14
14
  /** Scene-table entries this finder produced, in discovery order. */
15
- readonly entries: readonly SceneEntry[];
15
+ readonly entries: readonly DocumentEntry[];
16
16
  /**
17
17
  * The entry this finder's own answer makes the default, when its answer
18
18
  * settles it UNAMBIGUOUSLY (one candidate). A finder never picks between
@@ -19,9 +19,16 @@
19
19
  * is what keeps a game's `vgai.adapter.ts` free of finder implementations.
20
20
  */
21
21
 
22
- import type { FinderSelection } from '../adapter-module';
22
+ import { type ZodType, z } from 'zod';
23
+ import {
24
+ EXPORTED_COMPOSITION_REGIONS,
25
+ type FinderSelection,
26
+ type PrefabsFromStoriesParams,
27
+ type ScenesFromEntrypointSelectionParams,
28
+ } from '../adapter-module';
23
29
  import type { FinderResult } from './finder-result';
24
30
  import { type PrefabsFromStoriesInput, prefabsFromStories } from './prefabs-from-stories';
31
+ import { registerFinder, runRegisteredFinder } from './registry';
25
32
  import {
26
33
  type ScenesFromEntrypointSelectionInput,
27
34
  scenesFromEntrypointSelection,
@@ -34,6 +41,13 @@ export type {
34
41
  StoryRegistration,
35
42
  } from './prefabs-from-stories';
36
43
  export { prefabsFromStories } from './prefabs-from-stories';
44
+ export type { FinderContribution, FinderRegistration } from './registry';
45
+ export {
46
+ registerContributedFinder,
47
+ registeredFinderNames,
48
+ registerFinder,
49
+ runRegisteredFinder,
50
+ } from './registry';
37
51
  export type {
38
52
  EntrypointSource,
39
53
  ScenesFromEntrypointSelectionInput,
@@ -41,7 +55,20 @@ export type {
41
55
  export { scenesFromEntrypointSelection } from './scenes-from-entrypoint-selection';
42
56
 
43
57
  /** Everything any shipped finder can be handed, gathered once by the loader. */
44
- export type FinderInput = ScenesFromEntrypointSelectionInput & PrefabsFromStoriesInput;
58
+ /** A project source the host read for a selection's `include` globs. */
59
+ export interface ProjectSourceFile {
60
+ /** Project-relative path. */
61
+ readonly path: string;
62
+ readonly source: string;
63
+ }
64
+
65
+ export type FinderInput = ScenesFromEntrypointSelectionInput &
66
+ PrefabsFromStoriesInput & {
67
+ /** The sources matching every selection's `include` globs — empty when
68
+ * no selection declares any. A contributed finder reads the project
69
+ * through this and nothing else. */
70
+ readonly sources?: readonly ProjectSourceFile[];
71
+ };
45
72
 
46
73
  /**
47
74
  * Run one adapter-declared selection. The switch is exhaustive over
@@ -49,14 +76,43 @@ export type FinderInput = ScenesFromEntrypointSelectionInput & PrefabsFromStorie
49
76
  * here rather than silently contributing nothing.
50
77
  */
51
78
  export function runFinderSelection(selection: FinderSelection, input: FinderInput): FinderResult {
52
- switch (selection.finder) {
53
- case 'scenesFromEntrypointSelection':
54
- return scenesFromEntrypointSelection(selection, input);
55
- case 'prefabsFromStories':
56
- return prefabsFromStories(selection, input);
57
- default: {
58
- const unreachable: never = selection;
59
- throw new Error(`Unknown finder selection: ${JSON.stringify(unreachable)}`);
60
- }
61
- }
79
+ return runRegisteredFinder(selection, input);
62
80
  }
81
+
82
+ // The engine's own finders register here, schemas beside them: what an
83
+ // adapter may select by name is exactly what this registry holds.
84
+ const ScenesFromEntrypointSelectionSchema = z
85
+ .object({
86
+ finder: z.literal('scenesFromEntrypointSelection'),
87
+ regions: z
88
+ .union([z.array(z.string().min(1)).min(1), z.literal(EXPORTED_COMPOSITION_REGIONS)])
89
+ .describe('Region ids whose entrypoints carry the selection, or the rule form'),
90
+ selection: z
91
+ .string()
92
+ .min(1)
93
+ .optional()
94
+ .describe(
95
+ "Identifier of the entrypoint's module-level scene selection table; omit for a " +
96
+ 'single-composition entrypoint',
97
+ ),
98
+ })
99
+ .strict()
100
+ .refine((value) => value.selection === undefined || Array.isArray(value.regions), {
101
+ message:
102
+ 'scenesFromEntrypointSelection: `selection` names an identifier in ONE entrypoint’s ' +
103
+ `source, so it cannot be paired with the \`${EXPORTED_COMPOSITION_REGIONS}\` rule — ` +
104
+ 'name the region explicitly (`regions: ["<rootId>"]`).',
105
+ });
106
+ registerFinder<ScenesFromEntrypointSelectionParams, FinderInput>({
107
+ name: 'scenesFromEntrypointSelection',
108
+ schema:
109
+ ScenesFromEntrypointSelectionSchema as unknown as ZodType<ScenesFromEntrypointSelectionParams>,
110
+ run: scenesFromEntrypointSelection,
111
+ });
112
+ registerFinder<PrefabsFromStoriesParams, FinderInput>({
113
+ name: 'prefabsFromStories',
114
+ schema: z
115
+ .object({ finder: z.literal('prefabsFromStories') })
116
+ .strict() as unknown as ZodType<PrefabsFromStoriesParams>,
117
+ run: prefabsFromStories,
118
+ });
@@ -26,7 +26,7 @@
26
26
  * already produced and the component index it already has.
27
27
  */
28
28
 
29
- import type { PrefabsFromStoriesParams, SceneEntry } from '../adapter-module';
29
+ import type { DocumentEntry, PrefabsFromStoriesParams } from '../adapter-module';
30
30
  import type { FinderResult } from './finder-result';
31
31
 
32
32
  /** One composed portable-CSF story, as the host's story registry reports it. */
@@ -112,7 +112,7 @@ export function prefabsFromStories(
112
112
  _params: PrefabsFromStoriesParams,
113
113
  input: PrefabsFromStoriesInput,
114
114
  ): FinderResult {
115
- const entries: SceneEntry[] = [];
115
+ const entries: DocumentEntry[] = [];
116
116
  const notes: string[] = [];
117
117
  for (const [componentName, stories] of storiesByComponent(input.stories)) {
118
118
  const component = input.components?.find((candidate) => candidate.name === componentName);
@@ -0,0 +1,95 @@
1
+ /**
2
+ * THE FINDER REGISTRY — the open set of finders an adapter may SELECT by
3
+ * name (ARCHITECTURE-CORE §The editor protocol, "Documents, not scenes").
4
+ *
5
+ * A finder is a pure function from the project's gathered source to document
6
+ * entries, plus the Zod schema of its own parameters. The engine registers
7
+ * its two here (`./index.ts`); a contribution registers more through the
8
+ * same door. `adapter-module.ts` validates only the selection ENVELOPE; the
9
+ * parameters are checked against the finder's schema when the loader runs
10
+ * it — an unregistered name or a bad parameter refuses loudly, naming both.
11
+ */
12
+
13
+ import type { ZodType } from 'zod';
14
+ import type { FinderSelection } from '../adapter-module';
15
+ import type { FinderResult } from './finder-result';
16
+
17
+ /** What a registration's selection type must carry: the name. A finder's own
18
+ * params interface satisfies this without an index signature. */
19
+ export type FinderSelectionEnvelope = { readonly finder: string };
20
+
21
+ export interface FinderRegistration<
22
+ S extends FinderSelectionEnvelope = FinderSelection,
23
+ I = unknown,
24
+ > {
25
+ readonly name: S['finder'];
26
+ /** The selection's full shape, `finder` included; `.strict()` by the
27
+ * same rule as every other adapter table. */
28
+ readonly schema: ZodType<S>;
29
+ readonly run: (selection: S, input: I) => FinderResult;
30
+ }
31
+
32
+ const registry = new Map<string, FinderRegistration>();
33
+
34
+ /** Register a finder. A second registration under one name replaces the
35
+ * first (a module re-evaluated on save), never throws. */
36
+ export function registerFinder<S extends FinderSelectionEnvelope, I>(
37
+ registration: FinderRegistration<S, I>,
38
+ ): () => void {
39
+ const entry = registration as unknown as FinderRegistration;
40
+ registry.set(registration.name, entry);
41
+ return () => {
42
+ if (registry.get(registration.name) === entry) registry.delete(registration.name);
43
+ };
44
+ }
45
+
46
+ /**
47
+ * What a CONTRIBUTION exports to register a finder (ARCHITECTURE-CORE §The
48
+ * project model): a plain object a project module `src/contributions/<name>.finder.ts`
49
+ * exports as `finder`, handed by the HOST to {@link registerContributedFinder}.
50
+ * A selection may carry `include` globs; the host then supplies the matching
51
+ * project sources on `input.sources`, which is how a finder reads the
52
+ * project without a file system.
53
+ */
54
+ export type FinderContribution = FinderRegistration<FinderSelection, unknown>;
55
+
56
+ /** Register a contribution's finder after checking its shape by name. */
57
+ export function registerContributedFinder(value: unknown, source: string): () => void {
58
+ const record = (value ?? {}) as Partial<FinderContribution>;
59
+ const problems: string[] = [];
60
+ if (typeof record.name !== 'string' || record.name.length === 0)
61
+ problems.push('`name` must be a non-empty string');
62
+ const schema = record.schema as { safeParse?: unknown } | undefined;
63
+ if (!schema || typeof schema.safeParse !== 'function')
64
+ problems.push('`schema` must be a Zod schema');
65
+ if (typeof record.run !== 'function') problems.push('`run(selection, input)` must be a function');
66
+ if (problems.length > 0) throw new Error(`${source}: not a finder — ${problems.join('; ')}`);
67
+ return registerFinder(record as FinderContribution);
68
+ }
69
+
70
+ /** Every registered finder name, in registration order. */
71
+ export function registeredFinderNames(): readonly string[] {
72
+ return [...registry.keys()];
73
+ }
74
+
75
+ /** Validate a selection against its finder's schema and run it. */
76
+ export function runRegisteredFinder<I>(selection: FinderSelection, input: I): FinderResult {
77
+ const registration = registry.get(selection.finder);
78
+ if (!registration) {
79
+ const known = registeredFinderNames();
80
+ throw new Error(
81
+ `Unknown finder ${JSON.stringify(selection.finder)} — registered: ${
82
+ known.length > 0 ? known.map((n) => `"${n}"`).join(', ') : 'none'
83
+ }`,
84
+ );
85
+ }
86
+ const parsed = registration.schema.safeParse(selection);
87
+ if (!parsed.success) {
88
+ throw new Error(
89
+ `Finder ${JSON.stringify(selection.finder)}: ${parsed.error.issues
90
+ .map((issue) => `${issue.path.join('.') || '(selection)'}: ${issue.message}`)
91
+ .join('; ')}`,
92
+ );
93
+ }
94
+ return registration.run(parsed.data, input);
95
+ }
@@ -38,7 +38,7 @@
38
38
 
39
39
  import ts from 'typescript';
40
40
  import type {
41
- SceneEntry,
41
+ DocumentEntry,
42
42
  SceneSource,
43
43
  ScenesFromEntrypointSelectionParams,
44
44
  } from '../adapter-module';
@@ -263,7 +263,7 @@ function targetedEntrypoints(
263
263
  /** DEGENERATE: the entrypoint mounts one composition, and mounting the region
264
264
  * IS opening it. Labelled by the module's own default export — the game's own
265
265
  * word for this composition. */
266
- function degenerateEntry(entrypoint: EntrypointSource, sf: ts.SourceFile): SceneEntry {
266
+ function degenerateEntry(entrypoint: EntrypointSource, sf: ts.SourceFile): DocumentEntry {
267
267
  return {
268
268
  id: entrypoint.regionId,
269
269
  label: defaultExportName(sf) ?? basename(entrypoint.path),
@@ -286,7 +286,7 @@ function selectionEntries(
286
286
  selection: string,
287
287
  resolveModule: ScenesFromEntrypointSelectionInput['resolveModule'],
288
288
  notes: string[],
289
- ): { entries: SceneEntry[]; active: string | undefined } {
289
+ ): { entries: DocumentEntry[]; active: string | undefined } {
290
290
  const table = selectionTable(sf, selection);
291
291
  if (!table) {
292
292
  notes.push(
@@ -352,7 +352,7 @@ export function scenesFromEntrypointSelection(
352
352
  input: ScenesFromEntrypointSelectionInput,
353
353
  ): FinderResult {
354
354
  const notes: string[] = [];
355
- const entries: SceneEntry[] = [];
355
+ const entries: DocumentEntry[] = [];
356
356
  const active: string[] = [];
357
357
 
358
358
  for (const entrypoint of targetedEntrypoints(params, input.entrypoints, notes)) {
@@ -649,30 +649,59 @@ export function installSceneCapture(
649
649
  recordLoop: recordAnimationLoop,
650
650
  });
651
651
 
652
- // DOM-backed Three renderers (CSS3DRenderer is the first) use a normal
653
- // prototype method rather than WebGLRenderer's constructor assignment. Trap
654
- // only classes explicitly supplied by the host, and restore each byte-for-byte.
652
+ // DOM-backed Three renderers (CSS3DRenderer is the first): trap only classes
653
+ // explicitly supplied by the host, and restore each byte-for-byte. Two
654
+ // shapes exist across three revisions and both are met: an ordinary
655
+ // prototype `render` is wrapped in place; a CONSTRUCTOR-ASSIGNED `render`
656
+ // (r180's CSS3DRenderer does `this.render = function …` exactly like
657
+ // WebGLRenderer, so the prototype carries none) gets the same
658
+ // getter/setter trap the WebGLRenderer path uses — the constructor's
659
+ // assignment hits the setter, the getter hands back the observing wrapper.
660
+ // Skipping that shape silently was measured as "the game never rendered"
661
+ // on css3d_periodictable while 118 of its elements sat in the DOM.
655
662
  const additionalRendererRestores: Array<() => void> = [];
656
663
  for (const candidate of opts?.additionalRendererCtors ?? []) {
657
664
  const ctor = candidate as PrototypeRendererCtorLike;
658
665
  const rendererProto = ctor?.prototype;
659
- const priorRender = rendererProto?.render;
660
- if (!rendererProto || typeof priorRender !== 'function' || rendererProto === proto) continue;
661
- rendererProto.render = function (this: unknown, ...args: unknown[]) {
662
- observeRendererRender(this, args[0], args[1]);
663
- const hooks = renderPassHooks;
664
- if (hooks === null || !captured || this !== captured.renderer) {
665
- return priorRender.apply(this, args);
666
- }
667
- hooks.before();
668
- try {
669
- return priorRender.apply(this, args);
670
- } finally {
671
- hooks.after();
672
- }
673
- };
666
+ if (!rendererProto || rendererProto === proto) continue;
667
+ const priorRender = rendererProto.render;
668
+ if (typeof priorRender === 'function') {
669
+ rendererProto.render = function (this: unknown, ...args: unknown[]) {
670
+ observeRendererRender(this, args[0], args[1]);
671
+ const hooks = renderPassHooks;
672
+ if (hooks === null || !captured || this !== captured.renderer) {
673
+ return priorRender.apply(this, args);
674
+ }
675
+ hooks.before();
676
+ try {
677
+ return priorRender.apply(this, args);
678
+ } finally {
679
+ hooks.after();
680
+ }
681
+ };
682
+ additionalRendererRestores.push(() => {
683
+ rendererProto.render = priorRender;
684
+ });
685
+ continue;
686
+ }
687
+ const priorAddonDescriptor = Object.getOwnPropertyDescriptor(rendererProto, 'render');
688
+ Object.defineProperty(rendererProto, 'render', {
689
+ configurable: true,
690
+ set(this: Record<symbol, unknown>, fn: unknown) {
691
+ this[REAL] = fn;
692
+ },
693
+ get(this: Record<symbol, unknown>) {
694
+ const self = this;
695
+ return function render(this: unknown, ...args: unknown[]) {
696
+ observeRendererRender(self, args[0], args[1]);
697
+ return forwardRender(self, args);
698
+ };
699
+ },
700
+ });
674
701
  additionalRendererRestores.push(() => {
675
- rendererProto.render = priorRender;
702
+ if (priorAddonDescriptor)
703
+ Object.defineProperty(rendererProto, 'render', priorAddonDescriptor);
704
+ else delete (rendererProto as { render?: unknown }).render;
676
705
  });
677
706
  }
678
707
 
@@ -168,7 +168,9 @@ export interface NetPeer {
168
168
  * running. `null` from `getServerConfig` means no server is declared. */
169
169
  export interface NetServerConfig {
170
170
  endpoint: string;
171
- roomName: string;
171
+ /** Known once the game's own networking adapter reports it; in edit mode
172
+ * the declared `process` gives only the endpoint. */
173
+ roomName?: string;
172
174
  }
173
175
 
174
176
  /** The signed-in player's authored identity, used as the connect default. */