@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.
Files changed (44) hide show
  1. package/Readme.md +62 -0
  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 +72 -0
  9. package/dist/document/evaluate.js +971 -0
  10. package/dist/document/index.d.ts +8 -0
  11. package/dist/document/index.js +8 -0
  12. package/dist/document/inspection.d.ts +30 -0
  13. package/dist/document/inspection.js +130 -0
  14. package/dist/document/runtime.d.ts +71 -0
  15. package/dist/document/runtime.js +1 -0
  16. package/dist/document/types.d.ts +225 -0
  17. package/dist/document/types.js +43 -0
  18. package/dist/document/validation.d.ts +36 -0
  19. package/dist/document/validation.js +219 -0
  20. package/dist/document/write.d.ts +31 -0
  21. package/dist/document/write.js +106 -0
  22. package/dist/id.d.ts +2 -0
  23. package/dist/id.js +4 -0
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +2 -1
  26. package/dist/loadedGltf.d.ts +32 -0
  27. package/dist/loadedGltf.js +62 -0
  28. package/dist/resolve.js +8 -6
  29. package/package.json +9 -1
  30. package/src/address.ts +28 -3
  31. package/src/document/assets.ts +77 -0
  32. package/src/document/collectionSelector.ts +37 -0
  33. package/src/document/evaluate.ts +998 -0
  34. package/src/document/index.ts +8 -0
  35. package/src/document/inspection.ts +123 -0
  36. package/src/document/runtime.ts +66 -0
  37. package/src/document/schema.json +263 -0
  38. package/src/document/types.ts +249 -0
  39. package/src/document/validation.ts +222 -0
  40. package/src/document/write.ts +112 -0
  41. package/src/id.ts +5 -0
  42. package/src/index.ts +3 -0
  43. package/src/loadedGltf.ts +93 -0
  44. 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, splitPath, type GraphNode, type NeedleAddress } from "./address.js";
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, splitPath(address.path));
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 splitPath(address.path)) {
125
- current = current.children?.find(c => (c.name ?? "") === segment) ?? null;
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 ? splitPath(address.path) : null;
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[]): number | null {
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
- match = candidates.find(i => ((nodes[i]?.name ?? "") === segment)) ?? null;
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 : [];