@needle-tools/addressing 0.3.4 → 0.4.0-1791982227.dramatic-tempura.8b3a85e

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 (58) hide show
  1. package/Readme.md +151 -70
  2. package/dist/address.d.ts +5 -0
  3. package/dist/address.js +25 -3
  4. package/dist/document/assets.d.ts +27 -0
  5. package/dist/document/assets.js +75 -0
  6. package/dist/document/collectionSelector.d.ts +9 -0
  7. package/dist/document/collectionSelector.js +29 -0
  8. package/dist/document/evaluate.d.ts +86 -0
  9. package/dist/document/evaluate.js +1082 -0
  10. package/dist/document/index.d.ts +11 -0
  11. package/dist/document/index.js +11 -0
  12. package/dist/document/inspection.d.ts +30 -0
  13. package/dist/document/inspection.js +130 -0
  14. package/dist/document/loadedDocument.d.ts +18 -0
  15. package/dist/document/loadedDocument.js +35 -0
  16. package/dist/document/references.d.ts +7 -0
  17. package/dist/document/references.js +36 -0
  18. package/dist/document/runtime.d.ts +119 -0
  19. package/dist/document/runtime.js +1 -0
  20. package/dist/document/types.d.ts +240 -0
  21. package/dist/document/types.js +43 -0
  22. package/dist/document/validation.d.ts +36 -0
  23. package/dist/document/validation.js +222 -0
  24. package/dist/document/valueCodecs.d.ts +20 -0
  25. package/dist/document/valueCodecs.js +3 -0
  26. package/dist/document/write.d.ts +31 -0
  27. package/dist/document/write.js +131 -0
  28. package/dist/id.d.ts +2 -0
  29. package/dist/id.js +4 -0
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.js +2 -1
  32. package/dist/loadedGltf.d.ts +32 -0
  33. package/dist/loadedGltf.js +62 -0
  34. package/dist/resolve.js +8 -6
  35. package/package.json +10 -2
  36. package/src/address.ts +169 -144
  37. package/src/anchor.ts +71 -71
  38. package/src/document/assets.ts +77 -0
  39. package/src/document/collectionSelector.ts +37 -0
  40. package/src/document/evaluate.ts +1111 -0
  41. package/src/document/index.ts +11 -0
  42. package/src/document/inspection.ts +123 -0
  43. package/src/document/loadedDocument.ts +48 -0
  44. package/src/document/references.ts +41 -0
  45. package/src/document/runtime.ts +113 -0
  46. package/src/document/schema.json +293 -0
  47. package/src/document/types.ts +265 -0
  48. package/src/document/validation.ts +226 -0
  49. package/src/document/valueCodecs.ts +23 -0
  50. package/src/document/write.ts +132 -0
  51. package/src/gltf.ts +33 -33
  52. package/src/hash.ts +58 -58
  53. package/src/id.ts +206 -201
  54. package/src/index.ts +49 -46
  55. package/src/loadedGltf.ts +93 -0
  56. package/src/resolve.ts +319 -317
  57. package/src/target.ts +126 -126
  58. package/src/uuid.ts +48 -48
package/Readme.md CHANGED
@@ -1,70 +1,151 @@
1
- # @needle-tools/addressing
2
-
3
- Persistent addressing for glTF assets and three.js scene graphs: deterministic
4
- `needle-id` computation, the `#needle` name-anchor scheme, and address resolution.
5
-
6
- Zero runtime dependencies, no three.js import (scene graphs are accessed structurally),
7
- usable in browsers, node, and workers.
8
-
9
- ## Ids
10
-
11
- Every glTF root-collection element (nodes, scenes, animations, cameras, materials,
12
- meshes, skins, textures, images, samplers) can carry a UUID in `extras["needle-id"]`:
13
-
14
- - **Anchored** — the element name ends with `#needle` (case-insensitive): the id derives
15
- only from the text before the marker → it **survives any content change** as long as
16
- the name stays. Spaces and underscores in the value are treated as equal (loaders
17
- sanitize node names), so `Door Frame #needle` and `Door_Frame_#needle` share one id.
18
- - **Unanchored**: the id derives from the element's type, name, and its occurrence among
19
- same-named elements → uniquely named elements keep their ids across reorders and
20
- content updates; duplicates disambiguate by position within their name group; unnamed
21
- elements are positional.
22
-
23
- `ensureIds(gltfJson)` stamps a parsed glTF idempotently — existing ids are never
24
- overwritten, and any tool stamping the same element mints the same id.
25
-
26
- `ensureIds(gltfJson, { baseHash })` optionally salts unanchored ids with a caller-supplied
27
- identity (e.g. a stable asset id), scoping them to that identity.
28
-
29
- ## Addresses
30
-
31
- An address captures every fallback at creation time, while the target is known —
32
- `{ id, anchor?, type?, name?, path? }`. Resolution order is strict (path: "/"-separated segments, JSON-Pointer `~0`/`~1` escaping):
33
-
34
- 1. `id` — exact match (`extras` in glTF JSON, `userData` in live scene graphs)
35
- 2. `anchor` — survives asset updates
36
- 3. `path` — best-effort structural match
37
- 4. **reconciliation** — never automatic: `reconcileInGraph` returns ranked candidates
38
- with confidence + reasons; the caller decides thresholds and whether to re-anchor.
39
-
40
- ## API
41
-
42
- ```ts
43
- import {
44
- ensureIds, computeElementId, computeFileHash, // write side
45
- createGltfElementAddress, createObjectAddress, // address records
46
- resolveInGltf, resolveInGraph, reconcileInGraph, // resolve side
47
- parseAnchorValue, randomUuid, NEEDLE_ID_KEY,
48
- } from "@needle-tools/addressing";
49
-
50
- // stamp a parsed glTF (idempotent, never overwrites existing ids)
51
- const { fileHash, assigned } = ensureIds(gltfJson);
52
-
53
- // capture an address while the target is known
54
- const address = createObjectAddress(sceneRoot, targetObject);
55
-
56
- // later / elsewhere: resolve — or reconcile when the asset changed
57
- const hit = resolveInGraph(address, sceneRoot);
58
- const candidates = hit ? [] : reconcileInGraph(address, sceneRoot);
59
- ```
60
-
61
- Out of scope by design: GLB container parsing (this package takes parsed JSON), file
62
- I/O, and any composition/override semantics.
63
-
64
- ## Development
65
-
66
- ```
67
- npm install
68
- npm test # vitest — includes golden-fixture id tests
69
- npm run build # tsc → dist/
70
- ```
1
+ # @needle-tools/addressing
2
+
3
+ Persistent addressing for glTF assets and three.js scene graphs: deterministic
4
+ `needle-id` computation, the `#needle` name-anchor scheme, and address resolution.
5
+
6
+ Zero runtime dependencies, no three.js import (scene graphs are accessed structurally),
7
+ usable in browsers, node, and workers.
8
+
9
+ ## Project documents
10
+
11
+ `@needle-tools/addressing/document` exports the document types, evaluator,
12
+ single-def/over application, optional inspection index, and schema validator.
13
+ The JSON Schema is available at `@needle-tools/addressing/document/schema.json`.
14
+ The document format is unchanged by this package split.
15
+
16
+ The shared writer exports `writePropertyOver`, `captureDocumentValue`,
17
+ `documentOverKey`, and `mergeDocumentOvers`. These encode property edits, retain
18
+ addresses, remove net-undo edits, and merge saved overs. Hosts still supply node
19
+ reference encoding, live-target checks, asset assembly, and component serialization.
20
+ Non-finite numbers and unsupported values produce a writer issue instead of being
21
+ silently changed by JSON serialization.
22
+
23
+ `DocumentAssetAssembly` registers deduplicated asset sources and shared material
24
+ or HDR entities in the writer's asset/reference tables. Material assignments reuse
25
+ saved entity identities by source and material ID, even when asset keys change.
26
+ Hosts supply the captured entity IDs and resolve asset ownership; the assembly
27
+ contains no engine or editor dependencies.
28
+
29
+ The evaluator accepts a `DocumentRuntime` host. It owns asset caching, reference
30
+ placement, derived glTF IDs, target resolution, defs, property and parent overs,
31
+ entity sharing, and issue reporting. The host supplies loading, object creation,
32
+ cloning, and lifecycle scheduling. Needle Engine provides its own adapter;
33
+ plain three.js can use the same evaluator:
34
+
35
+ ```ts
36
+ import { Color, Object3D, Scene } from "three";
37
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
38
+ import { evaluateProjectDocument, type DocumentRuntime } from "@needle-tools/addressing/document";
39
+
40
+ const scene = new Scene();
41
+ const runtime: DocumentRuntime = {
42
+ scene,
43
+ renderer, // your existing renderer, for $renderer opinions
44
+ createNode: () => new Object3D(),
45
+ createColor: value => new Color().fromArray(value),
46
+ parse: (data, src) => new GLTFLoader().parseAsync(data, new URL(".", new URL(src, documentUrl)).href),
47
+ clone: node => (node as Object3D).clone(),
48
+ // Plain objects have no component lifecycle or template registrations.
49
+ parkTemplate() {},
50
+ activateInstance() {},
51
+ disposeTemplate() {},
52
+ schedule: work => work(),
53
+ loadEnvironment, // your existing HDR/PMREM loader: (url, options) => Promise<Texture | null>
54
+ };
55
+ const result = await evaluateProjectDocument(runtime, document, { baseUrl: documentUrl });
56
+ await result.completed;
57
+ ```
58
+
59
+ Supply `createDefContext` and the component access hooks if the host supports
60
+ components. Without them, component defs are reported as unsupported; node defs
61
+ and ordinary three.js properties still work. A skeletal or component-aware host
62
+ must supply its matching clone implementation. The default addressing entry point
63
+ does not import the document evaluator or any three.js runtime.
64
+
65
+ ## Ids
66
+
67
+ Every glTF root-collection element (nodes, scenes, animations, cameras, materials,
68
+ meshes, skins, textures, images, samplers) can carry a UUID in `extras["needle-id"]`:
69
+
70
+ - **Anchored** — the element name ends with `#needle` (case-insensitive): the id derives
71
+ only from the text before the marker → it **survives any content change** as long as
72
+ the name stays. Spaces and underscores in the value are treated as equal (loaders
73
+ sanitize node names), so `Door Frame #needle` and `Door_Frame_#needle` share one id.
74
+ - **Unanchored**: the id derives from the element's type, name, and its occurrence among
75
+ same-named elements → uniquely named elements keep their ids across reorders and
76
+ content updates; duplicates disambiguate by position within their name group; unnamed
77
+ elements are positional.
78
+
79
+ `ensureIds(gltfJson)` stamps a parsed glTF idempotently — existing ids are never
80
+ overwritten, and any tool stamping the same element mints the same id.
81
+
82
+ `ensureIds(gltfJson, { baseHash })` optionally salts unanchored ids with a caller-supplied
83
+ identity (e.g. a stable asset id), scoping them to that identity.
84
+
85
+ ## Addresses
86
+
87
+ An address captures every fallback at creation time, while the target is known —
88
+ `{ id, anchor?, type?, name?, path? }`. Resolution order is strict (path: "/"-separated segments, JSON-Pointer `~0`/`~1` escaping):
89
+
90
+ 1. `id` — exact match (`extras` in glTF JSON, `userData` in live scene graphs)
91
+ 2. `anchor` — survives asset updates
92
+ 3. `path` — best-effort structural match
93
+
94
+ Node paths qualify same-named siblings with `~2n` (zero-based occurrence):
95
+ `Cube~21` names the second `Cube`. Literal tildes remain escaped as `~0`.
96
+ Unqualified ambiguous paths fail to resolve rather than picking the first child.
97
+ These paths assume the original sibling order; use authored anchors or persistent
98
+ IDs when the source scene changes.
99
+ 4. **reconciliation** — never automatic: `reconcileInGraph` returns ranked candidates
100
+ with confidence + reasons; the caller decides thresholds and whether to re-anchor.
101
+
102
+ ## API
103
+
104
+ ```ts
105
+ import {
106
+ ensureIds, computeElementId, computeFileHash, // write side
107
+ createGltfElementAddress, createObjectAddress, // address records
108
+ resolveInGltf, resolveInGraph, reconcileInGraph, // resolve side
109
+ parseAnchorValue, randomUuid, NEEDLE_ID_KEY,
110
+ } from "@needle-tools/addressing";
111
+
112
+ // stamp a parsed glTF (idempotent, never overwrites existing ids)
113
+ const { fileHash, assigned } = ensureIds(gltfJson);
114
+
115
+ // capture an address while the target is known
116
+ const address = createObjectAddress(sceneRoot, targetObject);
117
+
118
+ // later / elsewhere: resolve — or reconcile when the asset changed
119
+ const hit = resolveInGraph(address, sceneRoot);
120
+ const candidates = hit ? [] : reconcileInGraph(address, sceneRoot);
121
+ ```
122
+
123
+ GLB parsing and file I/O belong to the host. The `/document` entry point provides
124
+ document composition through a host-supplied `DocumentRuntime`.
125
+
126
+ ## Loaded documents
127
+
128
+ ```ts
129
+ import { loadProjectDocument } from '@needle-tools/addressing/document';
130
+
131
+ const loaded = await loadProjectDocument(runtime, documentJson, { baseUrl: documentUrl });
132
+ const root = loaded.resolve({ ref: 'placed-reference-id' });
133
+ const target = root && loaded.addressOf(root);
134
+ ```
135
+
136
+ The result contains a frozen copy of the document JSON, the scene `root`, placed
137
+ `references`, per-reference `models`, evaluation `issues`, and `resolve`/`addressOf`.
138
+ It waits for scheduled composition and address indexing before returning. Reference
139
+ IDs distinguish separate placements of the same asset. Resolution uses the existing
140
+ document inspection index, including reference roots and reparented objects.
141
+
142
+ The host owns fetching, cancellation, resource disposal, rendering, and script
143
+ lifecycle. No editor or renderer implementation is imported by this API.
144
+
145
+ ## Development
146
+
147
+ ```
148
+ npm install
149
+ npm test # vitest — includes golden-fixture id tests
150
+ npm run build # tsc → dist/
151
+ ```
package/dist/address.d.ts CHANGED
@@ -23,6 +23,7 @@ export type NeedleAddress = {
23
23
  * {@link splitPath}. Empty SEGMENTS are load-bearing (positional walk over
24
24
  * unnamed nodes), so `""` is a valid one-segment path — test `!== undefined`,
25
25
  * never truthiness. ABSENT means "no path rung".
26
+ * A `~2n` suffix selects the zero-based occurrence among same-named siblings.
26
27
  */
27
28
  path?: string;
28
29
  };
@@ -44,6 +45,10 @@ export type GraphNode = {
44
45
  export declare function createGltfElementAddress(json: GltfJson, type: StampedElementType | string, index: number): NeedleAddress | null;
45
46
  /** Build an address for a live scene-graph object under `root`. */
46
47
  export declare function createObjectAddress(root: GraphNode, target: GraphNode): NeedleAddress | null;
48
+ export declare function splitNodePath(path: string): Array<{
49
+ name: string;
50
+ occurrence?: number;
51
+ }>;
47
52
  export declare function collectionKeyForType(type: string): string;
48
53
  /** path from root (exclusive) to target (inclusive), or null if target is not under root */
49
54
  export declare function findPath(root: GraphNode, target: GraphNode): GraphNode[] | null;
package/dist/address.js CHANGED
@@ -45,7 +45,7 @@ export function createGltfElementAddress(json, type, index) {
45
45
  if (type === "node") {
46
46
  const segments = gltfNodeNamePath(json, index);
47
47
  if (segments)
48
- address.path = joinPath(segments);
48
+ address.path = segments.join("/");
49
49
  }
50
50
  return address;
51
51
  }
@@ -64,9 +64,28 @@ export function createObjectAddress(root, target) {
64
64
  // empty name dropped — see createGltfElementAddress; path segments stay verbatim
65
65
  if (target.name)
66
66
  address.name = target.name;
67
- address.path = joinPath(nodes.map(n => n.name ?? ""));
67
+ let parent = root;
68
+ address.path = nodes.map(node => {
69
+ const siblings = parent.children ?? [];
70
+ parent = node;
71
+ return nodePathSegment(node.name ?? "", siblings, node);
72
+ }).join("/");
68
73
  return address;
69
74
  }
75
+ /** `~2n` selects a same-named sibling; literal tildes were already escaped as `~0`. */
76
+ function nodePathSegment(name, siblings, node) {
77
+ const matches = siblings.filter(sibling => (sibling.name ?? "") === name);
78
+ const escaped = encodePathSegment(name);
79
+ return matches.length > 1 ? `${escaped}~2${matches.indexOf(node)}` : escaped;
80
+ }
81
+ export function splitNodePath(path) {
82
+ return path.split("/").map(segment => {
83
+ const qualifier = /~2(0|[1-9][0-9]*)$/.exec(segment);
84
+ return qualifier
85
+ ? { name: decodePathSegment(segment.slice(0, qualifier.index)), occurrence: Number(qualifier[1]) }
86
+ : { name: decodePathSegment(segment) };
87
+ });
88
+ }
70
89
  export function collectionKeyForType(type) {
71
90
  // glTF collection keys are the pluralized type names; only "mesh" is irregular
72
91
  return type === "mesh" ? "meshes" : `${type}s`;
@@ -90,7 +109,10 @@ function gltfNodeNamePath(json, nodeIndex) {
90
109
  if (seen.has(current))
91
110
  return null; // malformed (cyclic) input
92
111
  seen.add(current);
93
- path.unshift(typeof nodes[current]?.name === "string" ? nodes[current].name : "");
112
+ const parent = parents.get(current);
113
+ const siblingIndices = parent !== undefined ? nodes[parent]?.children ?? []
114
+ : [...new Set((Array.isArray(json.scenes) ? json.scenes : []).flatMap(scene => scene.nodes ?? []))];
115
+ path.unshift(nodePathSegment(typeof nodes[current]?.name === "string" ? nodes[current].name : "", siblingIndices.map(i => nodes[i]), nodes[current]));
94
116
  current = parents.get(current);
95
117
  }
96
118
  return path;
@@ -0,0 +1,27 @@
1
+ import type { ProjectDocument, ProjectDocumentAsset, ProjectDocumentReference } from "./types.js";
2
+ /** Asset keys and shared resource identities for one document assembly. */
3
+ export declare class DocumentAssetAssembly {
4
+ private readonly assets;
5
+ private readonly references;
6
+ private readonly assetBySrc;
7
+ private readonly referenceById;
8
+ private readonly materialBySource;
9
+ private readonly baseMaterialBySource;
10
+ constructor(assets: Record<string, ProjectDocumentAsset>, references: ProjectDocumentReference[], base?: ProjectDocument | null);
11
+ addAsset(input: {
12
+ src: string;
13
+ name?: string;
14
+ kind: string;
15
+ identity?: {
16
+ id: string;
17
+ provider?: string;
18
+ };
19
+ }): string;
20
+ materialEntity(asset: string, material: {
21
+ id: string;
22
+ name?: string;
23
+ }, entityId: string): string;
24
+ environmentEntity(asset: string, entityId: string): string;
25
+ private addReference;
26
+ private materialKey;
27
+ }
@@ -0,0 +1,75 @@
1
+ /** Asset keys and shared resource identities for one document assembly. */
2
+ export class DocumentAssetAssembly {
3
+ constructor(assets, references, base) {
4
+ this.assets = assets;
5
+ this.references = references;
6
+ this.assetBySrc = new Map();
7
+ this.referenceById = new Map();
8
+ this.materialBySource = new Map();
9
+ this.baseMaterialBySource = new Map();
10
+ for (const [key, asset] of Object.entries(assets))
11
+ this.assetBySrc.set(asset.src, key);
12
+ for (const reference of references) {
13
+ this.referenceById.set(reference.id, reference);
14
+ const key = this.materialKey(reference, assets);
15
+ if (key)
16
+ this.materialBySource.set(key, reference);
17
+ }
18
+ for (const reference of base?.references ?? []) {
19
+ const key = this.materialKey(reference, base?.assets ?? {});
20
+ if (key)
21
+ this.baseMaterialBySource.set(key, reference);
22
+ }
23
+ }
24
+ addAsset(input) {
25
+ const existing = this.assetBySrc.get(input.src);
26
+ if (existing !== undefined)
27
+ return existing;
28
+ const filename = input.src.split("?")[0].split("/").pop() || "material";
29
+ const dot = filename.lastIndexOf(".");
30
+ const stem = input.name || (dot > 0 ? filename.substring(0, dot) : filename);
31
+ let key = stem;
32
+ let n = 2;
33
+ while (key in this.assets)
34
+ key = `${stem}-${n++}`;
35
+ this.assets[key] = { src: input.src, kind: input.kind, ...(input.identity ? { asset: { ...input.identity } } : {}) };
36
+ this.assetBySrc.set(input.src, key);
37
+ return key;
38
+ }
39
+ materialEntity(asset, material, entityId) {
40
+ if (!this.assets[asset])
41
+ throw new Error(`Unknown document asset: ${asset}`);
42
+ const key = JSON.stringify([this.assets[asset].src, material.id]);
43
+ const existing = this.materialBySource.get(key);
44
+ if (existing)
45
+ return existing.id;
46
+ const base = this.baseMaterialBySource.get(key);
47
+ const reference = base ? { ...base, asset } : {
48
+ id: entityId, asset, address: { type: "material", id: material.id, ...(material.name ? { name: material.name } : {}) },
49
+ };
50
+ this.addReference(reference);
51
+ this.materialBySource.set(key, reference);
52
+ return reference.id;
53
+ }
54
+ environmentEntity(asset, entityId) {
55
+ if (!this.assets[asset])
56
+ throw new Error(`Unknown document asset: ${asset}`);
57
+ this.addReference({ id: entityId, asset, address: { type: "hdr" } });
58
+ return entityId;
59
+ }
60
+ addReference(reference) {
61
+ const existing = this.referenceById.get(reference.id);
62
+ if (existing) {
63
+ if (existing.asset !== reference.asset || existing.address?.type !== reference.address?.type || existing.address?.id !== reference.address?.id) {
64
+ throw new Error(`Document reference id '${reference.id}' already identifies another resource`);
65
+ }
66
+ return;
67
+ }
68
+ this.references.push(reference);
69
+ this.referenceById.set(reference.id, reference);
70
+ }
71
+ materialKey(reference, assets) {
72
+ return reference.address?.type === "material" && reference.address.id && assets[reference.asset]
73
+ ? JSON.stringify([assets[reference.asset].src, reference.address.id]) : null;
74
+ }
75
+ }
@@ -0,0 +1,9 @@
1
+ export interface DocumentCollectionSelectorOptions {
2
+ matchesComponentType?(component: object, name: string): boolean;
3
+ matchesComponentGuid?(component: object, guid: string): boolean;
4
+ deriveComponentGuid?(referenceId: string, fileGuid: string): string;
5
+ referenceId?: string;
6
+ report?(message: string): void;
7
+ }
8
+ /** Resolve an index or field qualifier; ambiguous identities never select an arbitrary member. */
9
+ export declare function resolveDocumentCollectionElement(collection: readonly unknown[], selector: string, options?: DocumentCollectionSelectorOptions): unknown;
@@ -0,0 +1,29 @@
1
+ /** Resolve an index or field qualifier; ambiguous identities never select an arbitrary member. */
2
+ export function resolveDocumentCollectionElement(collection, selector, options = {}) {
3
+ if (/^\d+$/.test(selector))
4
+ return collection[Number(selector)];
5
+ const qualifier = /^([A-Za-z_][A-Za-z0-9_-]*):([\s\S]*)$/.exec(selector);
6
+ if (!qualifier) {
7
+ options.report?.(`"${selector}" is neither an index nor a <field>:<value> qualifier`);
8
+ return undefined;
9
+ }
10
+ const [, field, value] = qualifier;
11
+ const elements = collection.filter((element) => element !== null && typeof element === "object");
12
+ let matches = elements.filter(element => {
13
+ if (field === "type")
14
+ return options.matchesComponentType?.(element, value) ?? element.constructor?.name === value;
15
+ if (field === "guid")
16
+ return options.matchesComponentGuid?.(element, value)
17
+ ?? (element.guid === value || element.needle_editor_guid === value);
18
+ return element[field] === value;
19
+ });
20
+ if (!matches.length && field === "guid" && options.referenceId && options.deriveComponentGuid) {
21
+ const derived = options.deriveComponentGuid(options.referenceId, value);
22
+ matches = elements.filter(element => element.guid === derived);
23
+ }
24
+ if (matches.length > 1) {
25
+ options.report?.(`"${selector}" matched ${matches.length} elements ? the document must name one unambiguously`);
26
+ return undefined;
27
+ }
28
+ return matches[0];
29
+ }
@@ -0,0 +1,86 @@
1
+ import { type NeedleAddress } from "../index.js";
2
+ import { type ProjectDocument, type ProjectDocumentTarget, type ProjectEvaluationIssue, type ProjectEvaluationResult, type EvaluateProjectDocumentOptions } from "./types.js";
3
+ import type { DocumentNode as Object3D, DocumentModel as Model, DocumentRuntime as Context, DocumentDefContext as SerializationContext, DocumentExtensionProvider } from "./runtime.js";
4
+ import type { ProjectDocumentDef } from './types.js';
5
+ /** Controls reapplying component data to a scene that is already loaded. */
6
+ export interface ApplyDefOptions {
7
+ /** Restore components without creating nodes or resetting their transforms. */
8
+ readonly componentsOnly?: boolean;
9
+ /** Resolve addresses against the current scene at the moment they are used. */
10
+ readonly resolveTarget?: (target: ProjectDocumentTarget) => Object3D | null;
11
+ }
12
+ /** Finds the existing node that owns a definition's components without creating or editing it. */
13
+ export declare function resolveDefComponentHost(def: ProjectDocumentDef, resolveTarget: NonNullable<ApplyDefOptions['resolveTarget']>): Object3D | null;
14
+ /** Shared asset/entity preparation; does not place nodes into the host scene. */
15
+ export declare function createDocumentAssetLoader(context: Pick<Context, "parse" | "parkTemplate" | "loadEnvironment">, document: ProjectDocument, options: EvaluateProjectDocumentOptions | undefined, issue: (arc: ProjectEvaluationIssue["arc"], message: string) => void): {
16
+ loadParsedAsset: (assetId: string, arc?: ProjectEvaluationIssue["arc"]) => Promise<Model | null>;
17
+ entities: Map<string, Promise<object | null>>;
18
+ };
19
+ /**
20
+ * Evaluate a project document into the given context. One-shot; never throws for
21
+ * content issues (they are collected in the result), only for a non-document input.
22
+ *
23
+ * Order (plans/project-format-v0.md): load + place references (document order) →
24
+ * mounts → [pre-setup phase:] asset component deserialization (the engine's own
25
+ * queue, FIFO) → defs → overs. Resolution is scoped to the target reference's
26
+ * subtree; misses are reported and skipped (exact resolution only in v0).
27
+ */
28
+ export declare function evaluateProjectDocument(context: Context, document: ProjectDocument, options?: EvaluateProjectDocumentOptions): Promise<ProjectEvaluationResult>;
29
+ export declare function splitPath(path: string): string[];
30
+ /**
31
+ * The serialization context ONE def application (or a whole document's worth)
32
+ * deserializes through: rooted at the COMPOSITION root so `{guid}` references in
33
+ * def data resolve across the whole composed result (cross-reference wiring).
34
+ * `baseUrl`: URL-string component fields (e.g. a tracked image's remote object) go
35
+ * through the EXISTING AssetReference.getOrCreate deserialization — it needs a
36
+ * source id to resolve document-relative urls against (no new loading path).
37
+ */
38
+ export declare function createDefSerializationContext(context: Context, root: Object3D, baseUrl?: string): SerializationContext;
39
+ /**
40
+ * Apply ONE def to the live composition — create/adopt the node, then
41
+ * instantiate/adopt its components. Exported as the single-arc primitive:
42
+ * document evaluation loops over it, and the op-bridge APPLICATOR applies a
43
+ * creation op's carried def through exactly this function (one implementation,
44
+ * plans/operation-bridge.md fixture 2).
45
+ */
46
+ export declare function applyDef(def: NonNullable<ProjectDocument["defs"]>[number], root: Object3D, references: Map<string, Object3D>, issue: (arc: "def", message: string) => void, serializationContext: SerializationContext, providers?: ProjectDocument["extensions"], options?: ApplyDefOptions): void;
47
+ /**
48
+ * The shared state a sequence of over applications rides — constructed once per
49
+ * document evaluation AND once per op-bridge apply session (the applicator,
50
+ * plans/operation-bridge.md fixture 2):
51
+ * - async entity chaining: entity-targeted overs and entity-ref values await the
52
+ * entity's instantiation; overs touching the same entity keep their order among
53
+ * themselves by chaining on the entity's tail promise. Non-async overs apply
54
+ * synchronously (pre-awake); `settled()` awaits the async remainder.
55
+ * - deactivated subtrees ("active": false → removeFromParent), per scope ref:
56
+ * materials are RESOURCES, not graph nodes — a material used only by a
57
+ * deactivated mesh must stay addressable (doc order legally deactivates first
58
+ * and patches after), so material resolution searches these detached roots too.
59
+ */
60
+ export type OverApplyState = {
61
+ /** Recovery resolves against the already-open document rather than rebuilding its references. */
62
+ resolveValueReference?: (target: ProjectDocumentTarget) => Object3D | null;
63
+ /** Decoders for versioned extension values; missing readers never raw-assign payloads. */
64
+ extensions?: ReadonlyMap<string, DocumentExtensionProvider>;
65
+ /** Bind original-scene addresses before any rename, removal or reparent changes their paths. */
66
+ sceneTargets?: ReadonlyMap<ProjectDocumentTarget, Object3D | null>;
67
+ readonly entities: Map<string, Promise<object | null>>;
68
+ readonly deactivatedRoots: Map<string | undefined, Object3D[]>;
69
+ chainOnEntity(refId: string, work: (entity: object | null) => void): void;
70
+ settled(): Promise<void>;
71
+ };
72
+ export declare function createOverApplyState(entities: Map<string, Promise<object | null>>): OverApplyState;
73
+ /**
74
+ * Apply ONE over to the live composition — the single-arc primitive: document
75
+ * evaluation loops over it, and the op-bridge APPLICATOR applies a remote
76
+ * property/assign/deactivation op through exactly this function (one
77
+ * implementation, plans/operation-bridge.md fixture 2).
78
+ */
79
+ export declare function applyOver(context: Context, over: NonNullable<ProjectDocument["overs"]>[number], root: Object3D, references: Map<string, Object3D>, state: OverApplyState, issue: (arc: "over", message: string) => void): void;
80
+ /**
81
+ * Material entity resolution incl. MATERIAL-ONLY glbs (a promoted material
82
+ * library entry: materials but no scene/meshes). The loader never
83
+ * instantiates an unreferenced material, so a scene traversal finds nothing —
84
+ * pull the materials from the parser instead. Exported for tests.
85
+ */
86
+ export declare function resolveMaterialEntity(model: Model, address: NeedleAddress): Promise<object | null>;