@hraness/slopcamera 3.2.7 → 3.3.1

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 (188) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +65 -20
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +6 -9
  6. package/apps/desktop/application/default-registry.ts +26 -10
  7. package/apps/desktop/application/operation.ts +14 -5
  8. package/apps/desktop/application/operations/index.ts +4 -1
  9. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  10. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  11. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  12. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  13. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  14. package/apps/desktop/application/registry.ts +2 -2
  15. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  16. package/apps/desktop/application/spatial-assets.ts +160 -31
  17. package/apps/desktop/application/spatial-render.ts +217 -20
  18. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  19. package/apps/desktop/application/spatial-review.ts +361 -0
  20. package/apps/desktop/application/spatial-spz.ts +48 -3
  21. package/apps/desktop/application/spatial-world-import.ts +26 -5
  22. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  23. package/apps/desktop/cli/args.ts +528 -97
  24. package/apps/desktop/cli/capability-manifest.ts +374 -0
  25. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  26. package/apps/desktop/cli/command-host-resources.ts +31 -2
  27. package/apps/desktop/cli/commands.ts +1215 -70
  28. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  29. package/apps/desktop/cli/help.ts +183 -23
  30. package/apps/desktop/cli/html-scene.ts +139 -28
  31. package/apps/desktop/cli/io.ts +2 -1
  32. package/apps/desktop/cli/main.ts +26 -50
  33. package/apps/desktop/cli/menubar.ts +244 -0
  34. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  35. package/apps/desktop/cli/portable-surface.ts +18 -5
  36. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  37. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  38. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  39. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  40. package/apps/desktop/cli/workflow-code.ts +1 -1
  41. package/apps/desktop/code/application-node-planner.ts +0 -47
  42. package/apps/desktop/code/host-source-layout.ts +46 -0
  43. package/apps/desktop/code/public.ts +0 -1
  44. package/apps/desktop/code/runtime-identity.ts +50 -8
  45. package/apps/desktop/code/semantic-builder.ts +24 -43
  46. package/apps/desktop/code/source-bundle.ts +23 -6
  47. package/apps/desktop/code/source-typecheck.ts +131 -43
  48. package/apps/desktop/code/worker-client.ts +14 -2
  49. package/apps/desktop/contracts/cinema.ts +736 -0
  50. package/apps/desktop/contracts/index.ts +1 -0
  51. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  52. package/apps/desktop/contracts/spatial-world.ts +26 -3
  53. package/apps/desktop/core/cinema-plan.ts +1320 -0
  54. package/apps/desktop/core/index.ts +1 -0
  55. package/apps/desktop/core/music-analysis.ts +124 -1
  56. package/apps/desktop/core/storage.ts +75 -0
  57. package/apps/desktop/dist/cli/main.js +737 -291
  58. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  59. package/apps/desktop/html-overlay/index.ts +9 -0
  60. package/apps/desktop/html-overlay/runtime.ts +2 -0
  61. package/apps/desktop/html-overlay/scene.ts +29 -0
  62. package/apps/desktop/html-overlay/spatial.ts +619 -57
  63. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  64. package/apps/desktop/workflows/index.ts +6 -0
  65. package/dist/cli.js +32 -434
  66. package/dist/code/advanced.js +1 -269
  67. package/dist/code/index.js +16 -2239
  68. package/dist/generate.js +1 -23
  69. package/dist/host-resources.js +1 -25
  70. package/dist/index-0bbesn4m.js +35 -0
  71. package/dist/index-296qhpf2.js +3 -0
  72. package/dist/index-3cxf2c58.js +3 -0
  73. package/dist/index-77fjfg9f.js +3 -0
  74. package/dist/index-80a36bc0.js +3 -0
  75. package/dist/index-a3vvs3rf.js +3 -0
  76. package/dist/index-b3geqczn.js +5 -0
  77. package/dist/index-cgpkyjqa.js +3 -0
  78. package/dist/index-f0ptv8dm.js +7 -0
  79. package/dist/index-grk47tgs.js +3 -0
  80. package/dist/index-qry58nj2.js +10 -0
  81. package/dist/index.js +1 -240
  82. package/dist/operations.js +1 -28
  83. package/dist/skill-install-gx7afxsd.js +2 -0
  84. package/dist/vectorize/worker.js +1 -141
  85. package/dist/workflow.js +1 -15
  86. package/docs/README.md +5 -1
  87. package/docs/studio.md +1 -1
  88. package/package.json +11 -24
  89. package/skills/slopcamera/SKILL.md +14 -1
  90. package/skills/slopcamera/references/diagrams.md +5 -14
  91. package/skills/slopcamera/references/directed-scenes.md +22 -2
  92. package/skills/slopcamera/references/gateway-media.md +15 -1
  93. package/skills/slopcamera/references/image-galleries.md +166 -0
  94. package/skills/slopcamera/references/install.md +2 -2
  95. package/skills/slopcamera/references/music-video.md +23 -3
  96. package/skills/slopcamera/references/parametric-design.md +14 -0
  97. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  98. package/skills/slopcamera/references/scene-building.md +32 -0
  99. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  100. package/skills/slopcamera/references/support.md +25 -0
  101. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  102. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  103. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  104. package/src/capability-manifest.ts +361 -0
  105. package/src/cli.ts +250 -102
  106. package/src/code/canonical-json.ts +9 -0
  107. package/src/generate.ts +2 -2
  108. package/src/icon.ts +1111 -0
  109. package/src/image-gallery.ts +1049 -0
  110. package/src/index.ts +11 -23
  111. package/src/mcp/boundary.ts +11 -11
  112. package/src/mcp/index.ts +3 -0
  113. package/src/mcp/server.ts +2 -1
  114. package/src/mcp/tools.ts +1642 -130
  115. package/src/mcp/types.ts +19 -0
  116. package/src/operations.ts +412 -0
  117. package/src/portable-capability-manifest.ts +67 -0
  118. package/src/scene-gallery.ts +184 -0
  119. package/src/spatial-scene/asset-admission.ts +183 -0
  120. package/src/spatial-scene/audit-rendered.ts +692 -0
  121. package/src/spatial-scene/audit.ts +617 -0
  122. package/src/spatial-scene/behavior-audit.ts +252 -0
  123. package/src/spatial-scene/behavior-bake.ts +233 -0
  124. package/src/spatial-scene/behavior-fns.ts +585 -0
  125. package/src/spatial-scene/behavior-gallery.ts +109 -0
  126. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  127. package/src/spatial-scene/behavior-trace.ts +325 -0
  128. package/src/spatial-scene/behavior.ts +618 -0
  129. package/src/spatial-scene/build.ts +632 -0
  130. package/src/spatial-scene/camera-rig.ts +125 -0
  131. package/src/spatial-scene/camera-track.ts +4 -2
  132. package/src/spatial-scene/character.ts +124 -0
  133. package/src/spatial-scene/contracts.ts +75 -7
  134. package/src/spatial-scene/design-templates.ts +351 -0
  135. package/src/spatial-scene/design.ts +327 -0
  136. package/src/spatial-scene/direction-compile.ts +584 -0
  137. package/src/spatial-scene/direction.ts +122 -0
  138. package/src/spatial-scene/effects.ts +149 -0
  139. package/src/spatial-scene/evaluate.ts +109 -29
  140. package/src/spatial-scene/gallery.ts +277 -0
  141. package/src/spatial-scene/generate.ts +239 -0
  142. package/src/spatial-scene/geometry-native.ts +165 -0
  143. package/src/spatial-scene/geometry.ts +1216 -0
  144. package/src/spatial-scene/gltf.ts +600 -77
  145. package/src/spatial-scene/identity.ts +55 -2
  146. package/src/spatial-scene/index.ts +36 -0
  147. package/src/spatial-scene/inspect.ts +35 -6
  148. package/src/spatial-scene/material-lighting.ts +609 -0
  149. package/src/spatial-scene/math.ts +21 -4
  150. package/src/spatial-scene/motion-evidence.ts +88 -0
  151. package/src/spatial-scene/parametric.ts +765 -0
  152. package/src/spatial-scene/particle-preparation.ts +183 -0
  153. package/src/spatial-scene/particle.ts +195 -0
  154. package/src/spatial-scene/patch.ts +109 -15
  155. package/src/spatial-scene/performance.ts +1613 -0
  156. package/src/spatial-scene/probe.ts +137 -0
  157. package/src/spatial-scene/recipe-pack.ts +86 -0
  158. package/src/spatial-scene/render-effects.ts +240 -0
  159. package/src/spatial-scene/review.ts +639 -0
  160. package/src/spatial-scene/simulation-bake.ts +206 -0
  161. package/src/spatial-scene/simulation.ts +187 -0
  162. package/src/spatial-scene/solve.ts +366 -0
  163. package/src/spatial-scene/temporal-audit.ts +354 -0
  164. package/src/support-completion.ts +16 -0
  165. package/src/support.ts +42 -0
  166. package/src/version.ts +1 -1
  167. package/apps/desktop/application/operations/recording/index.ts +0 -5
  168. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  169. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  170. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  171. package/apps/desktop/application/operations/recording/start.ts +0 -169
  172. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  173. package/apps/desktop/capture/protocol.ts +0 -689
  174. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  175. package/apps/desktop/cli/recording-controller.ts +0 -1567
  176. package/apps/desktop/cli/recording-daemon.ts +0 -931
  177. package/dist/index-42zsesc1.js +0 -2257
  178. package/dist/index-7308egqr.js +0 -48
  179. package/dist/index-8txs6fkn.js +0 -712
  180. package/dist/index-mcy8z0br.js +0 -192
  181. package/dist/index-p63wavx0.js +0 -2029
  182. package/dist/index-r7gdhmsp.js +0 -424
  183. package/dist/index-rvghzt7b.js +0 -2013
  184. package/dist/index-sh6xbav6.js +0 -1219
  185. package/dist/index-z1w83f81.js +0 -4
  186. package/dist/index-z7239b4h.js +0 -2363
  187. package/dist/skill-install-dh0ntqba.js +0 -10
  188. package/src/desktop.ts +0 -253
@@ -2,10 +2,35 @@ import { Readable } from "node:stream";
2
2
  import { finished } from "node:stream/promises";
3
3
  import { createGunzip } from "node:zlib";
4
4
 
5
+ import type { Bounds } from "../../../src/spatial-scene/math";
5
6
  import { SpatialSpzFactsSchema, SPATIAL_SPLAT_LIMITS, spatialSpzAllocationBounds, type SpatialSpzFacts } from "../contracts/spatial-world";
6
7
 
7
- /** Inspect the complete gzip stream without allocating its advertised output. No decoder/browser runs before admission. */
8
- export async function inspectSpatialSpz(input: Uint8Array, signal: AbortSignal): Promise<SpatialSpzFacts> {
8
+ export interface SpatialSpzInspection {
9
+ readonly facts: SpatialSpzFacts;
10
+ /**
11
+ * Model-space axis-aligned bounds decoded from the SPZ position section —
12
+ * the signed int24 fixed-point XYZ each splat stores before any scene
13
+ * normalization. This is the only honest splat enclosure available before a
14
+ * renderer decodes gaussian extent, so object-ID proxies derive from it.
15
+ */
16
+ readonly modelBounds: Bounds;
17
+ }
18
+
19
+ /** Decodes one signed little-endian int24 fixed-point coordinate. */
20
+ function spzPositionComponent(data: Uint8Array, offset: number, fractionalBits: number): number {
21
+ let raw = data[offset]! | (data[offset + 1]! << 8) | (data[offset + 2]! << 16);
22
+ if (raw >= 0x80_00_00) raw -= 0x1_00_00_00;
23
+ return raw / 2 ** fractionalBits;
24
+ }
25
+
26
+ /**
27
+ * Inspect the complete gzip stream without allocating its advertised output.
28
+ * No decoder/browser runs before admission. Besides structural facts, the
29
+ * returned inspection carries model-space bounds decoded from the position
30
+ * section — bytes [16, 16 + splats × 9) of the decompressed body — so object-ID
31
+ * proxies and geometric audits share one trusted enclosure.
32
+ */
33
+ export async function inspectSpatialSpz(input: Uint8Array, signal: AbortSignal): Promise<SpatialSpzInspection> {
9
34
  if (!(input instanceof Uint8Array) || input.byteLength < 18 || input.byteLength > SPATIAL_SPLAT_LIMITS.sourceBytes || input[0] !== 0x1f || input[1] !== 0x8b) throw new RangeError("The qualified splat profile requires bounded gzip SPZ v2/v3 bytes.");
10
35
  signal.throwIfAborted();
11
36
  const gunzip = createGunzip({ chunkSize: 16 * 1024 });
@@ -15,10 +40,12 @@ export async function inspectSpatialSpz(input: Uint8Array, signal: AbortSignal):
15
40
  source.pipe(gunzip);
16
41
  const header = new Uint8Array(16);
17
42
  let count = 0, facts: SpatialSpzFacts | undefined;
43
+ let positions: Uint8Array | undefined;
18
44
  try {
19
45
  for await (const chunkInput of gunzip) {
20
46
  signal.throwIfAborted();
21
47
  const chunk = chunkInput as Buffer;
48
+ const chunkStart = count;
22
49
  if (count < 16) header.set(chunk.subarray(0, Math.min(16 - count, chunk.byteLength)), count);
23
50
  count += chunk.byteLength;
24
51
  if (count > SPATIAL_SPLAT_LIMITS.decompressedBytes) throw new RangeError("SPZ decompression exceeds its byte budget.");
@@ -29,12 +56,30 @@ export async function inspectSpatialSpz(input: Uint8Array, signal: AbortSignal):
29
56
  if (flags === 1) throw new RangeError("Antialiased-training SPZ is not supported by the initial qualified Spark profile.");
30
57
  const decompressedBytes = 16 + splats * (9 + 1 + 3 + 3 + (version === 3 ? 4 : 3) + 3 * ((shDegree + 1) ** 2 - 1));
31
58
  facts = SpatialSpzFactsSchema.parse({ kind: "slopcamera.spz-admission", schemaVersion: 1, version, splats, shDegree, fractionalBits, antialiased: flags === 1, decompressedBytes, ...spatialSpzAllocationBounds(splats, input.byteLength, decompressedBytes) });
59
+ positions = new Uint8Array(facts.splats * 9);
32
60
  }
33
61
  if (facts !== undefined && count > facts.decompressedBytes) throw new RangeError("SPZ body exceeds the exact admitted attribute layout.");
62
+ if (positions !== undefined) {
63
+ // SPZ lays out splat sections back-to-back: every position lands in
64
+ // decompressed bytes [16, 16 + splats × 9) ahead of scales/rotations.
65
+ const windowEnd = 16 + positions.byteLength;
66
+ const copyStart = Math.max(chunkStart, 16), copyEnd = Math.min(count, windowEnd);
67
+ if (copyEnd > copyStart) positions.set(chunk.subarray(copyStart - chunkStart, copyEnd - chunkStart), copyStart - 16);
68
+ }
34
69
  }
35
70
  if (facts === undefined || count !== facts.decompressedBytes) throw new RangeError("SPZ attribute body is truncated or does not match its header.");
36
71
  signal.throwIfAborted();
37
- return Object.freeze(facts);
72
+ const positionBytes = positions ?? new Uint8Array(0);
73
+ if (positionBytes.byteLength !== facts.splats * 9) throw new RangeError("SPZ position section is truncated or does not match its header.");
74
+ const minimum = [Infinity, Infinity, Infinity] as [number, number, number], maximum = [-Infinity, -Infinity, -Infinity] as [number, number, number];
75
+ for (let index = 0; index < facts.splats; index++) {
76
+ for (const axis of [0, 1, 2] as const) {
77
+ const value = spzPositionComponent(positionBytes, index * 9 + axis * 3, facts.fractionalBits);
78
+ if (value < minimum[axis]) minimum[axis] = value;
79
+ if (value > maximum[axis]) maximum[axis] = value;
80
+ }
81
+ }
82
+ return Object.freeze({ facts: Object.freeze(facts), modelBounds: Object.freeze({ min: Object.freeze(minimum), max: Object.freeze(maximum) }) });
38
83
  } finally {
39
84
  signal.removeEventListener("abort", abort);
40
85
  const settled = [finished(source, { cleanup: true }), finished(gunzip, { cleanup: true })];
@@ -9,7 +9,8 @@ import { SpatialAssetManifestSchema, SpatialEntitySchema } from "../../../src/sp
9
9
  import { canonicalJson, canonicalJsonSha256 } from "../core/canonical-json";
10
10
  import { createNodeSpatialDurability } from "../core/spatial-durability";
11
11
  import { createNodeBundleFileSystem } from "../core/storage";
12
- import { SavedSpatialWorldImportInputSchema, SavedSpatialWorldImportOutputSchema, SpatialWorldImportManifestSchema, SPATIAL_SPLAT_LIMITS, type SavedSpatialWorldImportOutput } from "../contracts/spatial-world";
12
+ import { SavedSpatialWorldImportInputSchema, SavedSpatialWorldImportOutputSchema, SpatialWorldImportManifestSchema, SPATIAL_SPLAT_LIMITS, type SavedSpatialWorldImportOutput, type SpatialWorldSuggestedNormalization } from "../contracts/spatial-world";
13
+ import { extractWorldProviderMetadata } from "./spatial-world-metadata";
13
14
  import { inspectSpatialSpz } from "./spatial-spz";
14
15
  import { WorldLabsProvenanceSchema } from "./spatial-world-provenance";
15
16
 
@@ -59,7 +60,8 @@ export async function importSavedSpatialWorld(options: {
59
60
  }): Promise<SavedSpatialWorldImportOutput> {
60
61
  const input = SavedSpatialWorldImportInputSchema.parse(options.input);
61
62
  if (input.provenance.receipt !== undefined && input.provenance.receipt.bytes > SPATIAL_SPLAT_LIMITS.metadataBytes) throw new RangeError("World provenance receipt exceeds one MiB.");
62
- const sourceClosureBytes = input.splat.bytes + (input.collider?.bytes ?? 0) + (input.provenance.receipt?.bytes ?? 0);
63
+ if (input.providerMetadata !== undefined && input.providerMetadata.bytes > SPATIAL_SPLAT_LIMITS.metadataBytes) throw new RangeError("World provider metadata exceeds one MiB.");
64
+ const sourceClosureBytes = input.splat.bytes + (input.collider?.bytes ?? 0) + (input.provenance.receipt?.bytes ?? 0) + (input.providerMetadata?.bytes ?? 0);
63
65
  // Reserve the bounded import manifest as part of the returned asset closure
64
66
  // before reading/copying any payload, so a successful import is renderable
65
67
  // under the same 256 MiB source-asset ceiling.
@@ -67,7 +69,7 @@ export async function importSavedSpatialWorld(options: {
67
69
  options.signal.throwIfAborted();
68
70
  const sourceRoot = await physicalRoot(options.sourceRoot), destinationRoot = await physicalRoot(options.destinationRoot);
69
71
  const splatBytes = await readExact(sourceRoot, input.splat, options.signal);
70
- const facts = await inspectSpatialSpz(splatBytes, options.signal);
72
+ const { facts } = await inspectSpatialSpz(splatBytes, options.signal);
71
73
  const colliderBytes = input.collider === undefined ? undefined : await readExact(sourceRoot, input.collider, options.signal);
72
74
  if (colliderBytes !== undefined) parseSpatialGlb(colliderBytes);
73
75
  const providerBytes = input.provenance.receipt === undefined ? undefined : await readExact(sourceRoot, input.provenance.receipt, options.signal);
@@ -78,6 +80,19 @@ export async function importSavedSpatialWorld(options: {
78
80
  if (!matches(value.assets.splat, input.splat) || input.collider === undefined || !matches(value.assets.collider, input.collider)
79
81
  || (input.provenance.worldId !== undefined && value.world.worldId !== input.provenance.worldId)) throw new RangeError("World provider provenance does not match the retained world and exact SPZ/collider bytes.");
80
82
  }
83
+ let suggestedNormalization: SpatialWorldSuggestedNormalization | undefined;
84
+ let providerMetadataBytes: Uint8Array | undefined;
85
+ if (input.providerMetadata !== undefined) {
86
+ providerMetadataBytes = await readExact(sourceRoot, input.providerMetadata, options.signal).catch((error: unknown) => {
87
+ if (options.signal.aborted || error instanceof RangeError) throw error;
88
+ throw new RangeError("World provider metadata source could not be read as declared.", { cause: error });
89
+ });
90
+ let document: unknown;
91
+ try { document = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(providerMetadataBytes)); }
92
+ catch (error) { throw new RangeError("World provider metadata is not valid UTF-8 JSON.", { cause: error }); }
93
+ const captured = createBoundedJsonSnapshot(document, 256 * 1024, "World provider metadata", { maximumDepth: 16, maximumValues: 4096 });
94
+ suggestedNormalization = { ...extractWorldProviderMetadata(captured.value), artifactSha256: input.providerMetadata.sha256 };
95
+ }
81
96
  const fs = createNodeBundleFileSystem(destinationRoot), durability = createNodeSpatialDurability(destinationRoot);
82
97
  const published: Artifact[] = [];
83
98
  const attempted: Artifact[] = [];
@@ -98,11 +113,15 @@ export async function importSavedSpatialWorld(options: {
98
113
  try {
99
114
  const splat = await publishBytes(splatBytes, "spz"), collider = colliderBytes === undefined ? undefined : await publishBytes(colliderBytes, "glb");
100
115
  const receipt = providerBytes === undefined ? undefined : await publishBytes(providerBytes, "json");
116
+ // The provider metadata artifact is retained verbatim — the exact source
117
+ // bytes publish under their own content address, never canonicalized.
118
+ const providerMetadata = providerMetadataBytes === undefined ? undefined : await publishBytes(providerMetadataBytes, "json");
101
119
  const manifest = SpatialWorldImportManifestSchema.parse({ kind: "slopcamera.spatial-world-import", schemaVersion: 1,
102
120
  splat: { payload: splat, facts }, collider: collider === undefined ? null : { payload: collider, role: "approximate-collider", validation: "bounded-glb-structure-only" },
103
121
  identities: input.identities, normalization: input.normalization,
104
122
  provenance: { ...input.provenance, ...(receipt === undefined ? {} : { receipt }) },
105
- capabilities: { beauty: true, semanticIds: "authored-wrapper-only", depth: "unsupported", objectId: "unsupported", physics: collider === undefined ? "unavailable" : "unvalidated" },
123
+ ...(suggestedNormalization === undefined ? {} : { suggestedNormalization }),
124
+ capabilities: { beauty: true, semanticIds: "authored-wrapper-only", depth: "unsupported", objectId: "bounding-box-proxy", physics: collider === undefined ? "unavailable" : "unvalidated" },
106
125
  });
107
126
  const text = `${canonicalJson(manifest)}\n`, sha256 = createHash("sha256").update(text).digest("hex");
108
127
  const manifestArtifact = { path: `spatial/worlds/receipts/${sha256}.json`, sha256, bytes: Buffer.byteLength(text) };
@@ -115,10 +134,12 @@ export async function importSavedSpatialWorld(options: {
115
134
  const interpretation = { metersPerUnit: input.normalization.metersPerUnit, sourceUp: input.normalization.sourceUp };
116
135
  const manifestAssetId = `asset_world_manifest_${manifestArtifact.sha256}`;
117
136
  const receiptAssetId = receipt === undefined ? undefined : `asset_world_provider_${receipt.sha256}`;
137
+ const providerMetadataAssetId = providerMetadata === undefined ? undefined : `asset_world_provider_metadata_${providerMetadata.sha256}`;
118
138
  const assets = [
119
139
  ...(collider === undefined ? [] : [SpatialAssetManifestSchema.parse({ assetId: input.identities.colliderAssetId, payload: collider, interpretation: { kind: "gltf", format: "glb", ...interpretation }, dependencies: [], provenance })]),
120
- SpatialAssetManifestSchema.parse({ assetId: manifestAssetId, payload: manifestArtifact, interpretation: { kind: "metadata", format: "json", schema: "slopcamera.spatial-world-import" }, dependencies: receiptAssetId === undefined ? [] : [receiptAssetId], provenance }),
140
+ SpatialAssetManifestSchema.parse({ assetId: manifestAssetId, payload: manifestArtifact, interpretation: { kind: "metadata", format: "json", schema: "slopcamera.spatial-world-import" }, dependencies: [...(receiptAssetId === undefined ? [] : [receiptAssetId]), ...(providerMetadataAssetId === undefined ? [] : [providerMetadataAssetId])], provenance }),
121
141
  ...(receipt === undefined ? [] : [SpatialAssetManifestSchema.parse({ assetId: receiptAssetId, payload: receipt, interpretation: { kind: "metadata", format: "json", schema: "slopcamera.world-labs-provenance" }, dependencies: [], provenance })]),
142
+ ...(providerMetadata === undefined ? [] : [SpatialAssetManifestSchema.parse({ assetId: providerMetadataAssetId, payload: providerMetadata, interpretation: { kind: "metadata", format: "json", schema: "slopcamera.provider-metadata" }, dependencies: [], provenance })]),
122
143
  SpatialAssetManifestSchema.parse({ assetId: input.identities.assetId, payload: splat, interpretation: { kind: "splat", format: "spz", ...interpretation }, dependencies: [...(input.identities.colliderAssetId === undefined ? [] : [input.identities.colliderAssetId]), manifestAssetId], provenance }),
123
144
  ];
124
145
  if (new Set(assets.map(asset => asset.assetId)).size !== assets.length) throw new RangeError("World metadata asset IDs conflict with requested identities.");
@@ -0,0 +1,105 @@
1
+ import { z } from "zod";
2
+
3
+ import type { SpatialWorldSuggestedNormalization } from "../contracts/spatial-world";
4
+
5
+ // World Labs Marble `semantics_metadata` documents are not formally versioned.
6
+ // The documented fields are `metric_scale_factor` and `ground_plane_offset`
7
+ // (both number-or-null; the ground plane offset is defined on the Y axis of the
8
+ // metric-scaled frame). The documented carrier chain is
9
+ // `world.assets.splats.semantics_metadata`: a world document nests it under
10
+ // `assets.splats`, `world`/`response` envelopes wrap one world document, and a
11
+ // saved `assets` object carries it under `splats` directly. A bare document and
12
+ // a wrapping `semantics_metadata`/`semanticsMetadata` object are also read —
13
+ // including the camelCase spelling retained inside slopcamera.world-labs-
14
+ // provenance receipts. Recognized keys are validated strictly while unknown
15
+ // keys are ignored, so newer provider fields never break parsing. Within one
16
+ // scope the snake_case spelling wins, and an outer carrier wins over a more
17
+ // deeply nested one. Every extracted value is provider-declared, never
18
+ // verified.
19
+ const metricScaleFactor = z.number().finite().positive().max(1_000_000).nullable();
20
+ const groundPlaneOffset = z.number().finite().min(-1_000_000).max(1_000_000).nullable();
21
+ const axisLabel = z.string().min(1).max(16).nullable();
22
+ const recognizedScope = z.object({
23
+ metric_scale_factor: metricScaleFactor.optional(), metricScaleFactor: metricScaleFactor.optional(),
24
+ ground_plane_offset: groundPlaneOffset.optional(), groundPlaneOffset: groundPlaneOffset.optional(),
25
+ ground_plane_axis: axisLabel.optional(), groundPlaneAxis: axisLabel.optional(),
26
+ up_axis: axisLabel.optional(), upAxis: axisLabel.optional(),
27
+ });
28
+ // Each carrier level may hold the document under either documented spelling.
29
+ const splatCarrier = z.object({
30
+ semantics_metadata: recognizedScope.nullable().optional(),
31
+ semanticsMetadata: recognizedScope.nullable().optional(),
32
+ });
33
+ const assetsCarrier = z.object({ splats: splatCarrier.nullable().optional() });
34
+ const worldCarrier = z.object({ assets: assetsCarrier.nullable().optional() });
35
+ const documentSchema = recognizedScope.extend({
36
+ semantics_metadata: recognizedScope.nullable().optional(),
37
+ semanticsMetadata: recognizedScope.nullable().optional(),
38
+ splats: splatCarrier.nullable().optional(),
39
+ assets: assetsCarrier.nullable().optional(),
40
+ world: worldCarrier.nullable().optional(),
41
+ response: worldCarrier.nullable().optional(),
42
+ });
43
+ type RecognizedScope = z.infer<typeof recognizedScope>;
44
+ type SplatCarrier = z.infer<typeof splatCarrier>;
45
+ type Declared = SpatialWorldSuggestedNormalization["declared"];
46
+ type Suggested = SpatialWorldSuggestedNormalization["suggestion"];
47
+ export type WorldProviderMetadataSuggestion = Omit<SpatialWorldSuggestedNormalization, "artifactSha256">;
48
+
49
+ const positiveAxes = new Set(["x", "y", "z"]);
50
+ function positiveAxis(label: string | null | undefined): "x" | "y" | "z" | undefined {
51
+ if (label === null || label === undefined) return undefined;
52
+ const normalized = label.trim().toLowerCase().replace(/^\+/u, "");
53
+ return positiveAxes.has(normalized) ? normalized as "x" | "y" | "z" : undefined;
54
+ }
55
+
56
+ /** Scopes carried by one splat-level object: snake_case first, then camelCase. */
57
+ function carrierScopes(carrier: SplatCarrier | null | undefined): RecognizedScope[] {
58
+ if (carrier === null || carrier === undefined) return [];
59
+ return [
60
+ ...(carrier.semantics_metadata === null || carrier.semantics_metadata === undefined ? [] : [carrier.semantics_metadata]),
61
+ ...(carrier.semanticsMetadata === null || carrier.semanticsMetadata === undefined ? [] : [carrier.semanticsMetadata]),
62
+ ];
63
+ }
64
+
65
+ /** Extract the recognized provider-declared fields into an advisory suggestion record. */
66
+ export function extractWorldProviderMetadata(document: unknown): WorldProviderMetadataSuggestion {
67
+ const parsed = documentSchema.safeParse(document);
68
+ if (!parsed.success) throw new RangeError("World provider metadata is not a recognized semantics_metadata document.");
69
+ // Outer carriers win over deeper nesting; each carrier's snake_case spelling
70
+ // precedes its camelCase spelling — matching the bare-before-wrapped rule.
71
+ const scopes: RecognizedScope[] = [
72
+ parsed.data,
73
+ ...carrierScopes(parsed.data),
74
+ ...carrierScopes(parsed.data.splats),
75
+ ...carrierScopes(parsed.data.assets?.splats),
76
+ ...carrierScopes(parsed.data.world?.assets?.splats),
77
+ ...carrierScopes(parsed.data.response?.assets?.splats),
78
+ ];
79
+ const first = <T>(select: (scope: RecognizedScope) => T | undefined): T | undefined => {
80
+ for (const scope of scopes) {
81
+ const value = select(scope);
82
+ if (value !== undefined) return value;
83
+ }
84
+ return undefined;
85
+ };
86
+ const metricScaleFactorValue = first(scope => scope.metric_scale_factor !== undefined ? scope.metric_scale_factor : scope.metricScaleFactor);
87
+ const groundPlaneOffsetValue = first(scope => scope.ground_plane_offset !== undefined ? scope.ground_plane_offset : scope.groundPlaneOffset);
88
+ const groundPlaneAxisValue = first(scope => scope.ground_plane_axis !== undefined ? scope.ground_plane_axis : scope.groundPlaneAxis);
89
+ const upAxisValue = first(scope => scope.up_axis !== undefined ? scope.up_axis : scope.upAxis);
90
+ const declared: Declared = {};
91
+ if (metricScaleFactorValue !== undefined) declared.metricScaleFactor = metricScaleFactorValue;
92
+ if (groundPlaneOffsetValue !== undefined) declared.groundPlaneOffset = groundPlaneOffsetValue;
93
+ if (groundPlaneAxisValue !== undefined) declared.groundPlaneAxis = groundPlaneAxisValue;
94
+ if (upAxisValue !== undefined) declared.upAxis = upAxisValue;
95
+ const suggestion: Suggested = {};
96
+ if (typeof declared.metricScaleFactor === "number" && declared.metricScaleFactor >= 0.000001) suggestion.metersPerUnit = declared.metricScaleFactor;
97
+ const sourceUp = positiveAxis(declared.upAxis);
98
+ if (sourceUp !== undefined) suggestion.sourceUp = sourceUp;
99
+ if (typeof declared.groundPlaneOffset === "number") {
100
+ // Marble defines ground_plane_offset on the Y axis of the metric-scaled
101
+ // frame; a declared axis only overrides that when it names a known axis.
102
+ suggestion.groundPlane = { axis: positiveAxis(declared.groundPlaneAxis) ?? "y", offset: declared.groundPlaneOffset };
103
+ }
104
+ return { provider: "worldlabs-marble", schema: "semantics_metadata", status: "unverified-provider-declared", declared, suggestion };
105
+ }