@needle-tools/addressing 0.3.4 → 0.4.0
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/Readme.md +62 -0
- package/dist/address.d.ts +5 -0
- package/dist/address.js +25 -3
- package/dist/document/assets.d.ts +27 -0
- package/dist/document/assets.js +75 -0
- package/dist/document/collectionSelector.d.ts +9 -0
- package/dist/document/collectionSelector.js +29 -0
- package/dist/document/evaluate.d.ts +72 -0
- package/dist/document/evaluate.js +971 -0
- package/dist/document/index.d.ts +8 -0
- package/dist/document/index.js +8 -0
- package/dist/document/inspection.d.ts +30 -0
- package/dist/document/inspection.js +130 -0
- package/dist/document/runtime.d.ts +71 -0
- package/dist/document/runtime.js +1 -0
- package/dist/document/types.d.ts +225 -0
- package/dist/document/types.js +43 -0
- package/dist/document/validation.d.ts +36 -0
- package/dist/document/validation.js +219 -0
- package/dist/document/write.d.ts +31 -0
- package/dist/document/write.js +106 -0
- package/dist/id.d.ts +2 -0
- package/dist/id.js +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/loadedGltf.d.ts +32 -0
- package/dist/loadedGltf.js +62 -0
- package/dist/resolve.js +8 -6
- package/package.json +9 -1
- package/src/address.ts +28 -3
- package/src/document/assets.ts +77 -0
- package/src/document/collectionSelector.ts +37 -0
- package/src/document/evaluate.ts +998 -0
- package/src/document/index.ts +8 -0
- package/src/document/inspection.ts +123 -0
- package/src/document/runtime.ts +66 -0
- package/src/document/schema.json +263 -0
- package/src/document/types.ts +249 -0
- package/src/document/validation.ts +222 -0
- package/src/document/write.ts +112 -0
- package/src/id.ts +5 -0
- package/src/index.ts +3 -0
- package/src/loadedGltf.ts +93 -0
- package/src/resolve.ts +9 -7
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./runtime.js";
|
|
3
|
+
export * from "./evaluate.js";
|
|
4
|
+
export * from "./inspection.js";
|
|
5
|
+
export * from "./validation.js";
|
|
6
|
+
export * from "./write.js";
|
|
7
|
+
export * from "./assets.js";
|
|
8
|
+
export * from "./collectionSelector.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./runtime.js";
|
|
3
|
+
export * from "./evaluate.js";
|
|
4
|
+
export * from "./inspection.js";
|
|
5
|
+
export * from "./validation.js";
|
|
6
|
+
export * from "./write.js";
|
|
7
|
+
export * from "./assets.js";
|
|
8
|
+
export * from "./collectionSelector.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type GraphNode, type NeedleAddress } from "../index.js";
|
|
2
|
+
export type ProjectInspectionTarget = {
|
|
3
|
+
ref: string;
|
|
4
|
+
address?: NeedleAddress;
|
|
5
|
+
pathPrefix?: string;
|
|
6
|
+
};
|
|
7
|
+
type Node = GraphNode & {
|
|
8
|
+
parent?: Node | null;
|
|
9
|
+
isObject3D?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/** Reference membership is captured at placement, before parent overs move nodes. */
|
|
12
|
+
export declare class ProjectDocumentInspection {
|
|
13
|
+
private readonly scene;
|
|
14
|
+
private readonly scopes;
|
|
15
|
+
private readonly targets;
|
|
16
|
+
ready: Promise<void>;
|
|
17
|
+
constructor(scene: Node);
|
|
18
|
+
registerReference(ref: string, root: object, elements?: Iterable<object>, referenceRoots?: ReadonlySet<object>): void;
|
|
19
|
+
resolve(target: ProjectInspectionTarget): object | null;
|
|
20
|
+
targetFor(object: object): ProjectInspectionTarget | null;
|
|
21
|
+
private inScene;
|
|
22
|
+
}
|
|
23
|
+
/** Opt in before loading a document; ordinary playback allocates no inspection index. */
|
|
24
|
+
export declare function enableProjectDocumentInspection(): {
|
|
25
|
+
dispose(): void;
|
|
26
|
+
};
|
|
27
|
+
export declare function createProjectDocumentInspection(scene: Node): ProjectDocumentInspection | undefined;
|
|
28
|
+
export declare function getProjectDocumentInspection(context: object): ProjectDocumentInspection | undefined;
|
|
29
|
+
export declare function registerProjectDocumentInspection(context: object, inspection: ProjectDocumentInspection): void;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { resolveDocumentCollectionElement } from "./collectionSelector.js";
|
|
2
|
+
import { joinPath, parseAnchorValue, resolveInCollection, resolveInGraph, splitPath } from "../index.js";
|
|
3
|
+
/** Reference membership is captured at placement, before parent overs move nodes. */
|
|
4
|
+
export class ProjectDocumentInspection {
|
|
5
|
+
constructor(scene) {
|
|
6
|
+
this.scene = scene;
|
|
7
|
+
this.scopes = new Map();
|
|
8
|
+
this.targets = new WeakMap();
|
|
9
|
+
this.ready = Promise.resolve();
|
|
10
|
+
}
|
|
11
|
+
registerReference(ref, root, elements = [], referenceRoots) {
|
|
12
|
+
const nodes = new Map();
|
|
13
|
+
const scope = { root: new WeakRef(root), nodes, elements: [] };
|
|
14
|
+
const capture = (node, path) => {
|
|
15
|
+
const weak = new WeakRef(node);
|
|
16
|
+
const graph = {
|
|
17
|
+
get name() { return weak.deref()?.name; },
|
|
18
|
+
get userData() { return weak.deref()?.userData; },
|
|
19
|
+
children: node.children?.filter(child => !referenceRoots?.has(child))
|
|
20
|
+
.map(child => capture(child, [...path, child.name ?? ""])),
|
|
21
|
+
};
|
|
22
|
+
nodes.set(graph, weak);
|
|
23
|
+
const id = node.userData?.["needle-id"];
|
|
24
|
+
const anchor = parseAnchorValue(node.name);
|
|
25
|
+
const address = node === root ? undefined : {
|
|
26
|
+
type: "node", path: joinPath(path), ...(typeof id === "string" ? { id } : {}), ...(anchor ? { anchor } : {}),
|
|
27
|
+
};
|
|
28
|
+
if (node === root || address)
|
|
29
|
+
this.targets.set(node, { ref, ...(address ? { address } : {}) });
|
|
30
|
+
return graph;
|
|
31
|
+
};
|
|
32
|
+
if (root.isObject3D)
|
|
33
|
+
scope.graph = capture(root, []);
|
|
34
|
+
else
|
|
35
|
+
this.targets.set(root, { ref });
|
|
36
|
+
for (const element of elements) {
|
|
37
|
+
const value = element;
|
|
38
|
+
const type = value.isMaterial ? "material" : value.isTexture ? "texture" : value.isAnimationClip ? "animation" : null;
|
|
39
|
+
if (!type)
|
|
40
|
+
continue;
|
|
41
|
+
scope.elements.push(new WeakRef(element));
|
|
42
|
+
const id = value.userData?.["needle-id"];
|
|
43
|
+
if (typeof id === "string")
|
|
44
|
+
this.targets.set(element, { ref, address: { type, id, name: value.name } });
|
|
45
|
+
}
|
|
46
|
+
this.scopes.set(ref, scope);
|
|
47
|
+
}
|
|
48
|
+
resolve(target) {
|
|
49
|
+
const scope = this.scopes.get(target.ref);
|
|
50
|
+
let object = target.ref === "$scene" ? this.scene : scope?.root.deref() ?? null;
|
|
51
|
+
if (target.address) {
|
|
52
|
+
const address = target.address;
|
|
53
|
+
if (address.type && address.type !== "node") {
|
|
54
|
+
const elements = scope?.elements.flatMap(weak => {
|
|
55
|
+
const value = weak.deref();
|
|
56
|
+
const matches = address.type === "material" ? value?.isMaterial : address.type === "texture" ? value?.isTexture : address.type === "animation" ? value?.isAnimationClip : false;
|
|
57
|
+
return value && matches ? [value] : [];
|
|
58
|
+
}) ?? [];
|
|
59
|
+
object = resolveInCollection(address, elements);
|
|
60
|
+
}
|
|
61
|
+
else if (scope?.graph) {
|
|
62
|
+
const graph = resolveInGraph(address, scope.graph);
|
|
63
|
+
object = graph ? scope.nodes.get(graph)?.deref() ?? null : null;
|
|
64
|
+
if (object && !this.inScene(object))
|
|
65
|
+
object = null;
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
object = target.ref === "$scene" ? resolveInGraph(address, this.scene) : null;
|
|
69
|
+
}
|
|
70
|
+
if (!object)
|
|
71
|
+
return null;
|
|
72
|
+
if (object.isObject3D && !this.inScene(object))
|
|
73
|
+
return null;
|
|
74
|
+
if (target.pathPrefix) {
|
|
75
|
+
const parts = splitPath(target.pathPrefix.replace(/\/$/, ""));
|
|
76
|
+
for (const part of parts) {
|
|
77
|
+
if (part === "components")
|
|
78
|
+
object = object.userData?.components ?? null;
|
|
79
|
+
else if (Array.isArray(object) && part.startsWith("guid:")) {
|
|
80
|
+
const guid = part.slice(5);
|
|
81
|
+
object = resolveDocumentCollectionElement(object, `guid:${guid}`) ?? null;
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
return null;
|
|
85
|
+
if (!object)
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return object;
|
|
90
|
+
}
|
|
91
|
+
targetFor(object) {
|
|
92
|
+
if (object === this.scene)
|
|
93
|
+
return { ref: "$scene" };
|
|
94
|
+
const component = object;
|
|
95
|
+
const guid = component.needle_editor_guid || component.guid;
|
|
96
|
+
if (component.gameObject && guid) {
|
|
97
|
+
const host = this.targetFor(component.gameObject);
|
|
98
|
+
return host ? { ...host, pathPrefix: `components/guid:${guid}/` } : null;
|
|
99
|
+
}
|
|
100
|
+
const target = this.targets.get(object);
|
|
101
|
+
return target && this.resolve(target) === object ? structuredClone(target) : null;
|
|
102
|
+
}
|
|
103
|
+
inScene(object) {
|
|
104
|
+
// Liveness only; reference ownership comes from the placement scope above.
|
|
105
|
+
for (let node = object; node; node = node.parent)
|
|
106
|
+
if (node === this.scene)
|
|
107
|
+
return true;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const inspections = new WeakMap();
|
|
112
|
+
let observers = 0;
|
|
113
|
+
/** Opt in before loading a document; ordinary playback allocates no inspection index. */
|
|
114
|
+
export function enableProjectDocumentInspection() {
|
|
115
|
+
observers++;
|
|
116
|
+
let disposed = false;
|
|
117
|
+
return { dispose() { if (!disposed) {
|
|
118
|
+
disposed = true;
|
|
119
|
+
observers--;
|
|
120
|
+
} } };
|
|
121
|
+
}
|
|
122
|
+
export function createProjectDocumentInspection(scene) {
|
|
123
|
+
return observers ? new ProjectDocumentInspection(scene) : undefined;
|
|
124
|
+
}
|
|
125
|
+
export function getProjectDocumentInspection(context) {
|
|
126
|
+
return inspections.get(context);
|
|
127
|
+
}
|
|
128
|
+
export function registerProjectDocumentInspection(context, inspection) {
|
|
129
|
+
inspections.set(context, inspection);
|
|
130
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { GraphNode } from "../address.js";
|
|
2
|
+
import type { GltfAssociation } from "../loadedGltf.js";
|
|
3
|
+
import type { ProjectEvaluationIssue } from "./types.js";
|
|
4
|
+
export interface DocumentVector {
|
|
5
|
+
fromArray(value: number[]): unknown;
|
|
6
|
+
toArray(): number[];
|
|
7
|
+
}
|
|
8
|
+
/** The scene operations used by composition, independent of a three.js build. */
|
|
9
|
+
export interface DocumentNode extends GraphNode {
|
|
10
|
+
name: string;
|
|
11
|
+
isObject3D: boolean;
|
|
12
|
+
userData: Record<string, unknown>;
|
|
13
|
+
parent: DocumentNode | null;
|
|
14
|
+
children: DocumentNode[];
|
|
15
|
+
position: DocumentVector;
|
|
16
|
+
quaternion: DocumentVector;
|
|
17
|
+
scale: DocumentVector;
|
|
18
|
+
add(...nodes: DocumentNode[]): unknown;
|
|
19
|
+
removeFromParent(): unknown;
|
|
20
|
+
traverse(visitor: (node: DocumentNode) => void): void;
|
|
21
|
+
}
|
|
22
|
+
export interface DocumentModel {
|
|
23
|
+
scene?: DocumentNode;
|
|
24
|
+
scenes?: DocumentNode[];
|
|
25
|
+
animations?: object[];
|
|
26
|
+
parser?: {
|
|
27
|
+
json?: Record<string, unknown> & {
|
|
28
|
+
materials?: Array<{
|
|
29
|
+
name?: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
associations?: Map<object, GltfAssociation>;
|
|
33
|
+
getDependency?(type: string, index: number): Promise<unknown>;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export type DocumentIssueReporter = (arc: ProjectEvaluationIssue["arc"], message: string) => void;
|
|
37
|
+
export interface DocumentDefContext {
|
|
38
|
+
scene?: DocumentNode;
|
|
39
|
+
createNode(): DocumentNode;
|
|
40
|
+
applyComponent?(node: DocumentNode, data: {
|
|
41
|
+
name: string;
|
|
42
|
+
} & Record<string, unknown>, issue: (arc: "def", message: string) => void): void;
|
|
43
|
+
}
|
|
44
|
+
/** Loading and component lifecycle belong to the host; document rules do not. */
|
|
45
|
+
export interface DocumentRuntime {
|
|
46
|
+
scene: DocumentNode;
|
|
47
|
+
renderer: object;
|
|
48
|
+
createNode(): DocumentNode;
|
|
49
|
+
createColor(value: number[]): object;
|
|
50
|
+
parse(data: ArrayBuffer | string, src: string, options?: {
|
|
51
|
+
resolveUrl(url: string): string;
|
|
52
|
+
}): Promise<DocumentModel | null>;
|
|
53
|
+
instantiate(template: DocumentNode, referenceId: string): DocumentNode | null;
|
|
54
|
+
parkTemplate(node: DocumentNode): void;
|
|
55
|
+
activateInstance(node: DocumentNode): void;
|
|
56
|
+
disposeTemplate(node: DocumentNode): void;
|
|
57
|
+
/** Run after template deserialization, before any instance activates its components. */
|
|
58
|
+
schedule(work: () => void): void;
|
|
59
|
+
loadEnvironment(url: string, options: {
|
|
60
|
+
format?: "pmrem";
|
|
61
|
+
name?: string;
|
|
62
|
+
}): Promise<object | null>;
|
|
63
|
+
createDefContext?(root: DocumentNode, baseUrl?: string): DocumentDefContext;
|
|
64
|
+
getComponents?(node: DocumentNode): object[];
|
|
65
|
+
matchesComponentType?(component: object, name: string): boolean;
|
|
66
|
+
matchesComponentGuid?(component: object, guid: string): boolean;
|
|
67
|
+
deriveComponentGuid?(referenceId: string, fileGuid: string): string;
|
|
68
|
+
removeComponent?(component: object, owner: DocumentNode | null): void;
|
|
69
|
+
/** Identity used by the optional inspection registry; defaults to the runtime itself. */
|
|
70
|
+
inspectionOwner?: object;
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { NeedleAddress } from "../address.js";
|
|
2
|
+
import type { DocumentNode, DocumentModel } from "./runtime.js";
|
|
3
|
+
/** the version discriminator key every project document must carry */
|
|
4
|
+
export declare const PROJECT_DOCUMENT_KEY = "needle:project";
|
|
5
|
+
export type ProjectDocumentAsset = {
|
|
6
|
+
/** where the asset lives — resolved against `EvaluateProjectDocumentOptions.baseUrl` */
|
|
7
|
+
src: string;
|
|
8
|
+
/**
|
|
9
|
+
* Optional PROVIDER IDENTITY for assets whose resources (textures, buffers) live at
|
|
10
|
+
* provider-specific urls that `src` alone can't reach (e.g. PolyHaven). The host recovers
|
|
11
|
+
* the provider's resolver from this identity at load time (see
|
|
12
|
+
* `EvaluateProjectDocumentOptions.resolveAssetUrl`). Self-contained assets omit it and
|
|
13
|
+
* load by `src`. The engine ignores it; only the host (editor) reads it.
|
|
14
|
+
*/
|
|
15
|
+
asset?: {
|
|
16
|
+
id: string;
|
|
17
|
+
provider?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Optional SEMANTIC KIND, decided at USE time by the writer ("model", "material",
|
|
21
|
+
* "hdr", "texture", "image", "video", "audio", "code", "text") — the same file can
|
|
22
|
+
* be a texture on a material slot or a plain image in UI, and downstream
|
|
23
|
+
* processing (compression at publish) follows the use, not the extension. The
|
|
24
|
+
* engine ignores it; only hosts/tooling read it.
|
|
25
|
+
*/
|
|
26
|
+
kind?: string;
|
|
27
|
+
/**
|
|
28
|
+
* INFORMATIONAL: the server `ETag` observed when this document captured the
|
|
29
|
+
* asset — lets a future reader detect "the upstream file changed since".
|
|
30
|
+
* Never used for loading decisions; the engine ignores it.
|
|
31
|
+
*/
|
|
32
|
+
etag?: string;
|
|
33
|
+
};
|
|
34
|
+
export type ProjectDocumentReference = {
|
|
35
|
+
/** project-local id of this reference — the resolution scope (or entity handle) for targets */
|
|
36
|
+
id: string;
|
|
37
|
+
/** key into the document's `assets` map */
|
|
38
|
+
asset: string;
|
|
39
|
+
/**
|
|
40
|
+
* ELEMENT REFERENCE discriminator: when present, this reference instantiates the
|
|
41
|
+
* addressed element INSIDE the asset (e.g. a material) into the composition's ENTITY
|
|
42
|
+
* POOL instead of placing a scene subtree. Entity references are first-class composed
|
|
43
|
+
* entities: overs can target them (`{ ref: <id>, path: "map/repeat" }`) and values can
|
|
44
|
+
* point at them (`{ ref: <id> }` — e.g. a mesh's material assignment). N consumers of
|
|
45
|
+
* the same entity share ONE runtime instance by construction (USD: a Material-prim
|
|
46
|
+
* reference; assignment ≈ material:binding).
|
|
47
|
+
*/
|
|
48
|
+
address?: NeedleAddress;
|
|
49
|
+
/** @deprecated (2026-08-03, read-only shim — remove soon) replaced by parent OVERS
|
|
50
|
+
* (`{ target: { ref, path: "parent" }, value: { ref | "$scene", address?, index? } }`)
|
|
51
|
+
* days after shipping; only unreleased documents from that week carry this field.
|
|
52
|
+
* The writer no longer emits it; the evaluator still honors it at placement. */
|
|
53
|
+
parent?: {
|
|
54
|
+
ref: string;
|
|
55
|
+
address?: NeedleAddress;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/** A host-owned root that document opinions compose onto instead of instantiating. */
|
|
59
|
+
export type ProjectDocumentBinding = {
|
|
60
|
+
/** Document-local reference used by targets. `$`-prefixed ids are reserved. */
|
|
61
|
+
id: string;
|
|
62
|
+
/** The kind of host root required by this document. */
|
|
63
|
+
kind: "scene";
|
|
64
|
+
/** Stable structural identity used by the host to match this root before applying edits. */
|
|
65
|
+
signature: string;
|
|
66
|
+
/** Informational label for diagnostics only; never used as identity. */
|
|
67
|
+
name?: string;
|
|
68
|
+
/** Existing asset-reference ids whose instances belong to this host root. */
|
|
69
|
+
references?: string[];
|
|
70
|
+
};
|
|
71
|
+
export type ProjectDocumentTarget = {
|
|
72
|
+
/** which placed reference the address resolves WITHIN; omitted → the composition root */
|
|
73
|
+
ref?: string;
|
|
74
|
+
/** fat address of the element inside the reference (needle-id / anchor / path) */
|
|
75
|
+
address?: NeedleAddress;
|
|
76
|
+
/**
|
|
77
|
+
* Property path within the resolved element, "/"-separated.
|
|
78
|
+
*
|
|
79
|
+
* A segment landing on a COLLECTION may be a `<field>:<value>` qualifier instead of an
|
|
80
|
+
* index — `components/type:DragControls/enabled`, `components/guid:6f1f…/enabled` —
|
|
81
|
+
* so a document never depends on array order (`userData.components` reorders whenever
|
|
82
|
+
* a component is added or removed). A bare numeric segment on an array is still an
|
|
83
|
+
* index, but it is now an explicit opt-in rather than the only option.
|
|
84
|
+
* `components` is a synthesized segment on a node: the live layout
|
|
85
|
+
* (`userData.components`) stays an implementation detail.
|
|
86
|
+
*
|
|
87
|
+
* Reserved LAST segment: `active` — with `value: false` it removes whatever the rest
|
|
88
|
+
* of the path resolved to (the node itself, or a component) from the composed result.
|
|
89
|
+
*/
|
|
90
|
+
path?: string;
|
|
91
|
+
};
|
|
92
|
+
export type ProjectDocumentDef = {
|
|
93
|
+
target: ProjectDocumentTarget;
|
|
94
|
+
/**
|
|
95
|
+
* When present, the def CREATES a new node (USD-proper: def defines prims) under the
|
|
96
|
+
* resolved target (composition root when the target is empty) and the components
|
|
97
|
+
* apply to IT — e.g. the runtime start camera. Without `create`, components apply to
|
|
98
|
+
* the resolved existing node.
|
|
99
|
+
*/
|
|
100
|
+
create?: {
|
|
101
|
+
id?: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
position?: number[];
|
|
104
|
+
quaternion?: number[];
|
|
105
|
+
scale?: number[];
|
|
106
|
+
/** Recreate a hierarchy duplicate from its stable source and seed. */
|
|
107
|
+
copy?: {
|
|
108
|
+
source: ProjectDocumentTarget;
|
|
109
|
+
seed: string;
|
|
110
|
+
reference?: boolean;
|
|
111
|
+
} & ({
|
|
112
|
+
batch: string;
|
|
113
|
+
index: number;
|
|
114
|
+
} | {
|
|
115
|
+
batch?: never;
|
|
116
|
+
index?: never;
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
/** VERBATIM NEEDLE_components wire entries ({ name, guid, ...fields }) */
|
|
120
|
+
components?: Array<{
|
|
121
|
+
name: string;
|
|
122
|
+
} & Record<string, unknown>>;
|
|
123
|
+
};
|
|
124
|
+
export type ProjectDocumentOver = {
|
|
125
|
+
target: ProjectDocumentTarget;
|
|
126
|
+
value: unknown;
|
|
127
|
+
};
|
|
128
|
+
export type ProjectDocument = {
|
|
129
|
+
[PROJECT_DOCUMENT_KEY]: string;
|
|
130
|
+
/** layer metadata (USD's "layer metadata" concept — e.g. `name`); free-form */
|
|
131
|
+
metadata?: Record<string, unknown>;
|
|
132
|
+
/** named remote origins for `remote://<name>/<path>` srcs — the scheme
|
|
133
|
+
* routes, the AUTHORITY names the remote, this table supplies the base.
|
|
134
|
+
* Resolved per DOCUMENT (a nested document resolves with ITS table), so
|
|
135
|
+
* the document works wherever it travels. Semantics mirror the editor's
|
|
136
|
+
* remoteSrc.ts — keep the two in sync. */
|
|
137
|
+
remotes?: Record<string, string>;
|
|
138
|
+
assets?: Record<string, ProjectDocumentAsset>;
|
|
139
|
+
/** Required host-owned roots. The host resolves these before evaluation. */
|
|
140
|
+
bindings?: ProjectDocumentBinding[];
|
|
141
|
+
references?: ProjectDocumentReference[];
|
|
142
|
+
defs?: ProjectDocumentDef[];
|
|
143
|
+
overs?: ProjectDocumentOver[];
|
|
144
|
+
/** unknown arcs are ignored by evaluation and must be preserved by writers */
|
|
145
|
+
[arc: string]: unknown;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Resolve a `remote://name/path` src via the document's remotes table
|
|
149
|
+
* (`overrides` win for known names — the host's per-environment repoint, e.g.
|
|
150
|
+
* the editor's dev proxy). Non-remote srcs pass through. An UNKNOWN remote
|
|
151
|
+
* name resolves to null — the caller reports it; guessing a base would load
|
|
152
|
+
* the wrong bytes silently. MIRRORS the editor's remoteSrc.ts resolveRemoteSrc.
|
|
153
|
+
*/
|
|
154
|
+
export declare function resolveDocumentRemoteSrc(src: string, remotes: Record<string, string> | undefined, overrides?: Record<string, string>): string | null;
|
|
155
|
+
/** structural check for the version discriminator — the opener/sniffing entry point */
|
|
156
|
+
export declare function isProjectDocument(value: unknown): value is ProjectDocument;
|
|
157
|
+
export type ProjectEvaluationIssue = {
|
|
158
|
+
arc: "reference" | "def" | "over";
|
|
159
|
+
message: string;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Report evaluation issues to the console — in PRODUCTION too. Every issue
|
|
163
|
+
* means the document asked for something the runtime could not provide (a
|
|
164
|
+
* dropped def, an unresolved over, a failed asset), i.e. the loaded scene
|
|
165
|
+
* silently differs from what the author saved. The author debugs on the
|
|
166
|
+
* deployed page: gating this behind dev/?debugproject cost a full day of
|
|
167
|
+
* blind hunting when a deployed scene lost a component with a clean console
|
|
168
|
+
* (2026-09-01). A healthy document reports nothing.
|
|
169
|
+
*/
|
|
170
|
+
export declare function reportProjectIssues(issues: readonly ProjectEvaluationIssue[], warn?: (message: string) => void): void;
|
|
171
|
+
export type ProjectEvaluationResult = {
|
|
172
|
+
/** the object all non-mounted references were added to */
|
|
173
|
+
root: DocumentNode;
|
|
174
|
+
/** Placed reference roots by reference id. Populated during the host's scheduled
|
|
175
|
+
* composition phase, after template deserialization; await `completed` first. */
|
|
176
|
+
references: Map<string, DocumentNode>;
|
|
177
|
+
/** per-reference Model views by reference id (`scene` is that reference's placed
|
|
178
|
+
* INSTANCE; parser etc. come from the shared template). Populated at `completed`,
|
|
179
|
+
* like `references`. */
|
|
180
|
+
models: Map<string, DocumentModel>;
|
|
181
|
+
/**
|
|
182
|
+
* misses and errors — an issue never aborts evaluation, the arc is skipped.
|
|
183
|
+
* Populated up to `completed` (defs/overs apply in the host's composition phase).
|
|
184
|
+
*/
|
|
185
|
+
issues: ProjectEvaluationIssue[];
|
|
186
|
+
/**
|
|
187
|
+
* Resolves once defs/overs have been applied. Hosts schedule composition after
|
|
188
|
+
* asset deserialization and before component activation. Needle's adapter uses
|
|
189
|
+
* its pre-setup phase, requiring a context tick; a vanilla host can run immediately.
|
|
190
|
+
*/
|
|
191
|
+
completed: Promise<void>;
|
|
192
|
+
};
|
|
193
|
+
export type EvaluateProjectDocumentOptions = {
|
|
194
|
+
/** Reuse existing roots as named resolution scopes. Bindings may supply external
|
|
195
|
+
* host roots that have no asset reference in the document. When supplied, every
|
|
196
|
+
* declared scene reference must also be bound; a missing binding is reported
|
|
197
|
+
* instead of loading another instance. */
|
|
198
|
+
referenceBindings?: ReadonlyMap<string, DocumentNode>;
|
|
199
|
+
/** where references are placed; defaults to `context.scene` */
|
|
200
|
+
parent?: DocumentNode;
|
|
201
|
+
/** resolves `assets.src` for the document — tests/tools provide in-memory data here */
|
|
202
|
+
resolveAsset?: (assetId: string, src: string) => Promise<ArrayBuffer | string>;
|
|
203
|
+
/** base for resolving relative `assets.src` urls with the default (fetch) resolver */
|
|
204
|
+
baseUrl?: string;
|
|
205
|
+
/** per-environment overrides for the document's `remotes` table (known
|
|
206
|
+
* names only — the editor maps needle-cloud to its dev proxy); the
|
|
207
|
+
* document's own table is the default */
|
|
208
|
+
remotes?: Record<string, string>;
|
|
209
|
+
/**
|
|
210
|
+
* Rewrites a resource url (texture / external buffer) encountered while parsing an
|
|
211
|
+
* asset — the asset service's `resolveUrl` re-entering at load time. Needed for provider
|
|
212
|
+
* assets (e.g. PolyHaven) whose textures live at urls that plain relative resolution
|
|
213
|
+
* against `assets.src` cannot reach. Passed to the host's parse operation;
|
|
214
|
+
* the editor opener supplies it from `IAssetsService` so this
|
|
215
|
+
* evaluator carries NO provider knowledge.
|
|
216
|
+
*/
|
|
217
|
+
resolveAssetUrl?: (assetId: string, url: string) => string;
|
|
218
|
+
/**
|
|
219
|
+
* AGGREGATE download progress across the document's assets: `loaded` bytes summed
|
|
220
|
+
* over all asset downloads, `total` summed over the known Content-Lengths (0 while
|
|
221
|
+
* nothing reported a length). Fired per received chunk — the loader chain adapts it
|
|
222
|
+
* to the ProgressEvent shape the loading UI expects.
|
|
223
|
+
*/
|
|
224
|
+
onProgress?: (loaded: number, total: number) => void;
|
|
225
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** the version discriminator key every project document must carry */
|
|
2
|
+
export const PROJECT_DOCUMENT_KEY = "needle:project";
|
|
3
|
+
const REMOTE_SRC_SCHEME = "remote://";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a `remote://name/path` src via the document's remotes table
|
|
6
|
+
* (`overrides` win for known names — the host's per-environment repoint, e.g.
|
|
7
|
+
* the editor's dev proxy). Non-remote srcs pass through. An UNKNOWN remote
|
|
8
|
+
* name resolves to null — the caller reports it; guessing a base would load
|
|
9
|
+
* the wrong bytes silently. MIRRORS the editor's remoteSrc.ts resolveRemoteSrc.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveDocumentRemoteSrc(src, remotes, overrides) {
|
|
12
|
+
if (!src.startsWith(REMOTE_SRC_SCHEME))
|
|
13
|
+
return src;
|
|
14
|
+
const rest = src.substring(REMOTE_SRC_SCHEME.length);
|
|
15
|
+
const slash = rest.indexOf("/");
|
|
16
|
+
if (slash <= 0)
|
|
17
|
+
return null;
|
|
18
|
+
const name = rest.substring(0, slash);
|
|
19
|
+
const path = rest.substring(slash); // keeps the leading "/"
|
|
20
|
+
const base = overrides?.[name] ?? remotes?.[name];
|
|
21
|
+
if (base === undefined)
|
|
22
|
+
return null;
|
|
23
|
+
// "" base = host-relative; trim a trailing slash otherwise
|
|
24
|
+
return (base.endsWith("/") ? base.substring(0, base.length - 1) : base) + path;
|
|
25
|
+
}
|
|
26
|
+
/** structural check for the version discriminator — the opener/sniffing entry point */
|
|
27
|
+
export function isProjectDocument(value) {
|
|
28
|
+
return !!value && typeof value === "object" && typeof value[PROJECT_DOCUMENT_KEY] === "string";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Report evaluation issues to the console — in PRODUCTION too. Every issue
|
|
32
|
+
* means the document asked for something the runtime could not provide (a
|
|
33
|
+
* dropped def, an unresolved over, a failed asset), i.e. the loaded scene
|
|
34
|
+
* silently differs from what the author saved. The author debugs on the
|
|
35
|
+
* deployed page: gating this behind dev/?debugproject cost a full day of
|
|
36
|
+
* blind hunting when a deployed scene lost a component with a clean console
|
|
37
|
+
* (2026-09-01). A healthy document reports nothing.
|
|
38
|
+
*/
|
|
39
|
+
export function reportProjectIssues(issues, warn = console.warn) {
|
|
40
|
+
for (const issue of issues) {
|
|
41
|
+
warn(`[needle:project] ${issue.arc}: ${issue.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural validation of Needle project documents against the JSON Schema in
|
|
3
|
+
* `schema.json` — the format's machine-readable spec.
|
|
4
|
+
*
|
|
5
|
+
* The schema is the SINGLE source of truth: this file interprets it, it does not
|
|
6
|
+
* restate it. That is the whole point — a hand-written second validator would drift
|
|
7
|
+
* from the schema exactly like the plan's example drifted from the types.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately dependency-free (a JSON Schema library is a lot of surface for the
|
|
10
|
+
* handful of keywords this schema uses), and STRICT about that: any keyword the
|
|
11
|
+
* schema uses and this interpreter does not implement THROWS. A validator that
|
|
12
|
+
* silently ignores keywords it doesn't know is worse than no validator — it reports
|
|
13
|
+
* "valid" for documents it never actually checked. If you extend the schema with a
|
|
14
|
+
* new keyword, this file fails loudly until you implement it.
|
|
15
|
+
*
|
|
16
|
+
* This validates SHAPE, not resolvability: whether an address actually hits a node in
|
|
17
|
+
* the loaded content is the evaluator's job (it reports issues and skips the arc).
|
|
18
|
+
*/
|
|
19
|
+
/** a JSON Schema node — `true`/`false` schema forms are not used by our schema */
|
|
20
|
+
type SchemaNode = Record<string, unknown>;
|
|
21
|
+
export type SchemaValidationError = {
|
|
22
|
+
/** "/"-separated path to the offending value, relative to the document root */
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly message: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Validate `value` against `schema`. Returns every error found (not just the first) —
|
|
28
|
+
* a writer regression usually shows up as several related failures and seeing them
|
|
29
|
+
* together is what makes the cause obvious.
|
|
30
|
+
*
|
|
31
|
+
* @throws if the schema uses a keyword this interpreter does not implement.
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateAgainstSchema(schema: SchemaNode, value: unknown): SchemaValidationError[];
|
|
34
|
+
/** one-line-per-error rendering for assertion messages */
|
|
35
|
+
export declare function formatSchemaErrors(errors: readonly SchemaValidationError[]): string;
|
|
36
|
+
export {};
|