@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,93 @@
|
|
|
1
|
+
import { computeElementId, getElementId, NEEDLE_ID_KEY, occurrenceOfName } from "./id.js";
|
|
2
|
+
import type { GltfElement } from "./gltf.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Eager-compute half of the persistent-id contract:
|
|
6
|
+
* ids are computed at LOAD time against the pristine glTF JSON — the deterministic inputs
|
|
7
|
+
* (original names and their order) only exist pristinely then — and attached to the LIVE
|
|
8
|
+
* objects' userData. The source file is not touched; persistence happens on save, where
|
|
9
|
+
* the exporter writes userData back into extras.
|
|
10
|
+
*
|
|
11
|
+
* Already-stamped assets carry ids in extras (which the loader copies to userData); those
|
|
12
|
+
* are respected and always win over recomputation.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** subset of three GLTFLoader's `parser.associations` values that we consume */
|
|
16
|
+
export type GltfAssociation = {
|
|
17
|
+
nodes?: number;
|
|
18
|
+
materials?: number;
|
|
19
|
+
textures?: number;
|
|
20
|
+
cameras?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type UserDataHost = { userData?: Record<string, unknown> };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* association key → (glTF collection, element type) — in precedence order: the scene-graph
|
|
27
|
+
* identity (node) wins when one live object carries several associations (e.g. a Mesh
|
|
28
|
+
* created for a single-use glTF node). glTF `mesh` elements are deliberately NOT attached:
|
|
29
|
+
* they are shared data, and stamping a scene object with a mesh id would alias two
|
|
30
|
+
* identities (a mesh used by two nodes would give both objects the same id).
|
|
31
|
+
*/
|
|
32
|
+
const ASSOCIATION_MAPPING = [
|
|
33
|
+
["nodes", "node"],
|
|
34
|
+
["materials", "material"],
|
|
35
|
+
["textures", "texture"],
|
|
36
|
+
["cameras", "camera"],
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
export type AttachNeedleIdsResult = {
|
|
40
|
+
/** how many live objects received an id (already-stamped userData is left untouched) */
|
|
41
|
+
attached: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Attach needle-ids to the live objects of a loaded glTF.
|
|
46
|
+
* @param json the PRISTINE parsed glTF JSON (as read from the file, before any mutation)
|
|
47
|
+
* @param associations three GLTFLoader `parser.associations` (live object → element indices)
|
|
48
|
+
* @param scenes loaded scene objects, ordered like `json.scenes`
|
|
49
|
+
*/
|
|
50
|
+
export function attachNeedleIdsToLoadedGltf(
|
|
51
|
+
json: Record<string, unknown>,
|
|
52
|
+
associations: ReadonlyMap<object, GltfAssociation | undefined> | null | undefined,
|
|
53
|
+
scenes?: readonly (UserDataHost | null | undefined)[] | null,
|
|
54
|
+
): AttachNeedleIdsResult {
|
|
55
|
+
let attached = 0;
|
|
56
|
+
|
|
57
|
+
const attach = (target: UserDataHost, type: string, collection: string, index: number) => {
|
|
58
|
+
const userData = target.userData;
|
|
59
|
+
if (!userData || typeof userData !== "object") return;
|
|
60
|
+
if (typeof userData[NEEDLE_ID_KEY] === "string") return; // loader copied extras, or already attached
|
|
61
|
+
const elements = json[collection];
|
|
62
|
+
if (!Array.isArray(elements)) return;
|
|
63
|
+
const element = elements[index] as GltfElement | undefined;
|
|
64
|
+
if (!element || typeof element !== "object") return;
|
|
65
|
+
// occurrence-of-name (not index) is the id input — computed from the pristine
|
|
66
|
+
// collection so ids match what any other tool stamps for this file
|
|
67
|
+
const id = getElementId(element) ?? computeElementId(type, element.name, occurrenceOfName(elements, index));
|
|
68
|
+
userData[NEEDLE_ID_KEY] = id;
|
|
69
|
+
attached++;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (associations) {
|
|
73
|
+
for (const [object, association] of associations) {
|
|
74
|
+
if (!association || typeof object !== "object") continue;
|
|
75
|
+
for (const [key, type] of ASSOCIATION_MAPPING) {
|
|
76
|
+
const index = association[key];
|
|
77
|
+
if (typeof index === "number") {
|
|
78
|
+
attach(object as UserDataHost, type, key, index);
|
|
79
|
+
break; // precedence: first matching association identifies the object
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (scenes) {
|
|
86
|
+
for (let i = 0; i < scenes.length; i++) {
|
|
87
|
+
const scene = scenes[i];
|
|
88
|
+
if (scene) attach(scene, "scene", "scenes", i);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { attached };
|
|
93
|
+
}
|
package/src/resolve.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseAnchorValue } from "./anchor.js";
|
|
2
|
-
import { collectionKeyForType,
|
|
2
|
+
import { collectionKeyForType, splitNodePath, type GraphNode, type NeedleAddress } from "./address.js";
|
|
3
3
|
import { NEEDLE_ID_KEY } from "./id.js";
|
|
4
4
|
import { STAMPED_COLLECTIONS, type GltfJson } from "./gltf.js";
|
|
5
5
|
|
|
@@ -53,7 +53,7 @@ export function resolveInGltf(address: NeedleAddress, json: GltfJson, options?:
|
|
|
53
53
|
if (byId) return byId;
|
|
54
54
|
|
|
55
55
|
if (address.path !== undefined && (!address.type || address.type === "node")) {
|
|
56
|
-
const index = walkGltfNamePath(json,
|
|
56
|
+
const index = walkGltfNamePath(json, splitNodePath(address.path));
|
|
57
57
|
if (index !== null) {
|
|
58
58
|
const nodes = json["nodes"] as Array<Record<string, unknown>>;
|
|
59
59
|
return { type: "node", index, element: nodes[index] };
|
|
@@ -121,8 +121,9 @@ export function resolveInGraph(address: NeedleAddress, root: GraphNode, options?
|
|
|
121
121
|
|
|
122
122
|
if (address.path !== undefined) {
|
|
123
123
|
let current: GraphNode | null = root;
|
|
124
|
-
for (const segment of
|
|
125
|
-
|
|
124
|
+
for (const segment of splitNodePath(address.path)) {
|
|
125
|
+
const matches: GraphNode[] = current.children?.filter(c => (c.name ?? "") === segment.name) ?? [];
|
|
126
|
+
current = segment.occurrence !== undefined ? matches[segment.occurrence] ?? null : matches.length === 1 ? matches[0] : null;
|
|
126
127
|
if (!current) break;
|
|
127
128
|
}
|
|
128
129
|
if (current && current !== root) return current;
|
|
@@ -186,7 +187,7 @@ export type ReconcileCandidate = {
|
|
|
186
187
|
* path still matches. Callers decide thresholds and whether to re-anchor.
|
|
187
188
|
*/
|
|
188
189
|
export function reconcileInGraph(address: NeedleAddress, root: GraphNode): ReconcileCandidate[] {
|
|
189
|
-
const addressSegments = address.path !== undefined ?
|
|
190
|
+
const addressSegments = address.path !== undefined ? splitNodePath(address.path).map(segment => segment.name) : null;
|
|
190
191
|
if (!address.name && !addressSegments?.length) return [];
|
|
191
192
|
const targetName = address.name ?? addressSegments![addressSegments!.length - 1];
|
|
192
193
|
|
|
@@ -235,7 +236,7 @@ function findInCollection(json: GltfJson, key: string, predicate: (el: Record<st
|
|
|
235
236
|
return null;
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
function walkGltfNamePath(json: GltfJson, segments: string
|
|
239
|
+
function walkGltfNamePath(json: GltfJson, segments: Array<{ name: string; occurrence?: number }>): number | null {
|
|
239
240
|
const nodes = json["nodes"];
|
|
240
241
|
const scenes = json["scenes"];
|
|
241
242
|
if (!Array.isArray(nodes) || !Array.isArray(scenes)) return null;
|
|
@@ -243,7 +244,8 @@ function walkGltfNamePath(json: GltfJson, segments: string[]): number | null {
|
|
|
243
244
|
let candidates: number[] = scenes.flatMap(s => (Array.isArray(s?.nodes) ? s.nodes : []));
|
|
244
245
|
let match: number | null = null;
|
|
245
246
|
for (const segment of segments) {
|
|
246
|
-
|
|
247
|
+
const matches = [...new Set(candidates)].filter(i => (nodes[i]?.name ?? "") === segment.name);
|
|
248
|
+
match = segment.occurrence !== undefined ? matches[segment.occurrence] ?? null : matches.length === 1 ? matches[0] : null;
|
|
247
249
|
if (match === null) return null;
|
|
248
250
|
const children = nodes[match]?.children;
|
|
249
251
|
candidates = Array.isArray(children) ? children : [];
|