@hraness/slopcamera 3.2.8 → 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 (150) hide show
  1. package/README.md +29 -9
  2. package/apps/desktop/application/context.ts +6 -0
  3. package/apps/desktop/application/default-registry.ts +26 -0
  4. package/apps/desktop/application/operation.ts +14 -1
  5. package/apps/desktop/application/operations/index.ts +4 -0
  6. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  7. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  8. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  9. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  10. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  11. package/apps/desktop/application/registry.ts +2 -2
  12. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  13. package/apps/desktop/application/spatial-assets.ts +160 -31
  14. package/apps/desktop/application/spatial-render.ts +217 -20
  15. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  16. package/apps/desktop/application/spatial-review.ts +361 -0
  17. package/apps/desktop/application/spatial-spz.ts +48 -3
  18. package/apps/desktop/application/spatial-world-import.ts +26 -5
  19. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  20. package/apps/desktop/cli/args.ts +514 -11
  21. package/apps/desktop/cli/capability-manifest.ts +374 -0
  22. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  23. package/apps/desktop/cli/command-host-resources.ts +29 -1
  24. package/apps/desktop/cli/commands.ts +1188 -4
  25. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  26. package/apps/desktop/cli/help.ts +172 -6
  27. package/apps/desktop/cli/main.ts +3 -3
  28. package/apps/desktop/cli/portable-surface.ts +6 -1
  29. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  30. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  31. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  32. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  33. package/apps/desktop/cli/workflow-code.ts +1 -1
  34. package/apps/desktop/code/semantic-builder.ts +24 -0
  35. package/apps/desktop/contracts/cinema.ts +736 -0
  36. package/apps/desktop/contracts/index.ts +1 -0
  37. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  38. package/apps/desktop/contracts/spatial-world.ts +26 -3
  39. package/apps/desktop/core/cinema-plan.ts +1320 -0
  40. package/apps/desktop/core/index.ts +1 -0
  41. package/apps/desktop/core/storage.ts +75 -0
  42. package/apps/desktop/dist/cli/main.js +725 -273
  43. package/apps/desktop/html-overlay/spatial.ts +619 -57
  44. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  45. package/apps/desktop/workflows/index.ts +6 -0
  46. package/dist/cli.js +25 -429
  47. package/dist/code/advanced.js +1 -269
  48. package/dist/code/index.js +16 -2239
  49. package/dist/generate.js +1 -23
  50. package/dist/host-resources.js +1 -25
  51. package/dist/index-0bbesn4m.js +35 -0
  52. package/dist/index-296qhpf2.js +3 -0
  53. package/dist/index-3cxf2c58.js +3 -0
  54. package/dist/index-77fjfg9f.js +3 -0
  55. package/dist/index-80a36bc0.js +3 -0
  56. package/dist/index-a3vvs3rf.js +3 -0
  57. package/dist/index-b3geqczn.js +5 -0
  58. package/dist/index-cgpkyjqa.js +3 -0
  59. package/dist/index-f0ptv8dm.js +7 -0
  60. package/dist/index-grk47tgs.js +3 -0
  61. package/dist/index-qry58nj2.js +10 -0
  62. package/dist/index.js +1 -226
  63. package/dist/operations.js +1 -28
  64. package/dist/skill-install-gx7afxsd.js +2 -0
  65. package/dist/vectorize/worker.js +1 -141
  66. package/dist/workflow.js +1 -15
  67. package/docs/README.md +5 -1
  68. package/package.json +7 -5
  69. package/skills/slopcamera/SKILL.md +6 -1
  70. package/skills/slopcamera/references/directed-scenes.md +22 -2
  71. package/skills/slopcamera/references/gateway-media.md +15 -1
  72. package/skills/slopcamera/references/image-galleries.md +166 -0
  73. package/skills/slopcamera/references/install.md +1 -1
  74. package/skills/slopcamera/references/parametric-design.md +14 -0
  75. package/skills/slopcamera/references/scene-building.md +32 -0
  76. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  77. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  78. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  79. package/src/capability-manifest.ts +361 -0
  80. package/src/cli.ts +222 -4
  81. package/src/code/canonical-json.ts +9 -0
  82. package/src/generate.ts +2 -2
  83. package/src/icon.ts +1111 -0
  84. package/src/image-gallery.ts +1049 -0
  85. package/src/index.ts +11 -0
  86. package/src/mcp/boundary.ts +11 -11
  87. package/src/mcp/index.ts +3 -0
  88. package/src/mcp/server.ts +2 -1
  89. package/src/mcp/tools.ts +1642 -130
  90. package/src/mcp/types.ts +19 -0
  91. package/src/operations.ts +412 -0
  92. package/src/portable-capability-manifest.ts +67 -0
  93. package/src/scene-gallery.ts +184 -0
  94. package/src/spatial-scene/asset-admission.ts +183 -0
  95. package/src/spatial-scene/audit-rendered.ts +692 -0
  96. package/src/spatial-scene/audit.ts +617 -0
  97. package/src/spatial-scene/behavior-audit.ts +252 -0
  98. package/src/spatial-scene/behavior-bake.ts +233 -0
  99. package/src/spatial-scene/behavior-fns.ts +585 -0
  100. package/src/spatial-scene/behavior-gallery.ts +109 -0
  101. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  102. package/src/spatial-scene/behavior-trace.ts +325 -0
  103. package/src/spatial-scene/behavior.ts +618 -0
  104. package/src/spatial-scene/build.ts +632 -0
  105. package/src/spatial-scene/camera-rig.ts +125 -0
  106. package/src/spatial-scene/camera-track.ts +4 -2
  107. package/src/spatial-scene/character.ts +124 -0
  108. package/src/spatial-scene/contracts.ts +75 -7
  109. package/src/spatial-scene/design-templates.ts +351 -0
  110. package/src/spatial-scene/design.ts +327 -0
  111. package/src/spatial-scene/direction-compile.ts +584 -0
  112. package/src/spatial-scene/direction.ts +122 -0
  113. package/src/spatial-scene/effects.ts +149 -0
  114. package/src/spatial-scene/evaluate.ts +109 -29
  115. package/src/spatial-scene/gallery.ts +277 -0
  116. package/src/spatial-scene/generate.ts +239 -0
  117. package/src/spatial-scene/geometry-native.ts +165 -0
  118. package/src/spatial-scene/geometry.ts +1216 -0
  119. package/src/spatial-scene/gltf.ts +600 -77
  120. package/src/spatial-scene/identity.ts +55 -2
  121. package/src/spatial-scene/index.ts +36 -0
  122. package/src/spatial-scene/inspect.ts +35 -6
  123. package/src/spatial-scene/material-lighting.ts +609 -0
  124. package/src/spatial-scene/math.ts +21 -4
  125. package/src/spatial-scene/motion-evidence.ts +88 -0
  126. package/src/spatial-scene/parametric.ts +765 -0
  127. package/src/spatial-scene/particle-preparation.ts +183 -0
  128. package/src/spatial-scene/particle.ts +195 -0
  129. package/src/spatial-scene/patch.ts +109 -15
  130. package/src/spatial-scene/performance.ts +1613 -0
  131. package/src/spatial-scene/probe.ts +137 -0
  132. package/src/spatial-scene/recipe-pack.ts +86 -0
  133. package/src/spatial-scene/render-effects.ts +240 -0
  134. package/src/spatial-scene/review.ts +639 -0
  135. package/src/spatial-scene/simulation-bake.ts +206 -0
  136. package/src/spatial-scene/simulation.ts +187 -0
  137. package/src/spatial-scene/solve.ts +366 -0
  138. package/src/spatial-scene/temporal-audit.ts +354 -0
  139. package/src/version.ts +1 -1
  140. package/dist/index-42zsesc1.js +0 -2257
  141. package/dist/index-7131pg1c.js +0 -1820
  142. package/dist/index-7308egqr.js +0 -48
  143. package/dist/index-8txs6fkn.js +0 -712
  144. package/dist/index-fava6pge.js +0 -192
  145. package/dist/index-h1k0fnjq.js +0 -2363
  146. package/dist/index-r7gdhmsp.js +0 -424
  147. package/dist/index-sh6xbav6.js +0 -1219
  148. package/dist/index-z1w83f81.js +0 -4
  149. package/dist/index-zfnddgay.js +0 -2029
  150. package/dist/skill-install-dh0ntqba.js +0 -10
@@ -16,13 +16,17 @@ import {
16
16
  } from "../../../src/spatial-scene/contracts";
17
17
  import { spatialAssetClosureDigests } from "../../../src/spatial-scene/identity";
18
18
  import { evaluateSpatialGlb, parseSpatialGlb, type SpatialGlbModel } from "../../../src/spatial-scene/gltf";
19
+ import { transformBounds } from "../../../src/spatial-scene/math";
20
+ import { SpatialDerivationCandidateSchema, pbrMaterialMapAssetIds, type SpatialDerivationCandidate } from "../../../src/spatial-scene/material-lighting";
19
21
  import { canonicalJson, canonicalJsonSha256 } from "../core/canonical-json";
20
22
  import { SpatialWorldImportManifestSchema, SPATIAL_SPLAT_LIMITS } from "../contracts/spatial-world";
23
+ import { SpatialAssetFactsV1Schema } from "../contracts/spatial-asset";
21
24
  import { inspectSpatialSpz } from "./spatial-spz";
25
+ import { extractWorldProviderMetadata } from "./spatial-world-metadata";
22
26
  import { WorldLabsProvenanceSchema } from "./spatial-world-provenance";
23
27
  import {
24
28
  PreparedSpatialAssetSchema, SPATIAL_OVERLAY_LIMITS, SpatialOverlayCapabilityError, spatialTextRasterContentSha256,
25
- spatialGeometryContentSha256, spatialVideoRasterContentSha256,
29
+ spatialGeometryContentSha256, spatialSplatContentTransform, spatialVideoRasterContentSha256,
26
30
  type PreparedSpatialAsset,
27
31
  } from "../html-overlay/spatial";
28
32
  import type { ApplicationProcessRunner } from "./context";
@@ -67,6 +71,8 @@ export interface SpatialAssetPreparationPorts {
67
71
  export interface SpatialAssetPreparationInput {
68
72
  readonly snapshots: readonly EvaluatedSpatialScene[];
69
73
  readonly exactSceneTimesUs?: readonly { readonly numerator: string; readonly denominator: string }[];
74
+ /** Beauty lut-grade assets not bound by any entity; decoded like other image rasters. */
75
+ readonly lutAssetIds?: readonly string[];
70
76
  /** Adapter-owned contained source root and private temporary workspace parent. */
71
77
  readonly assetRoot: string;
72
78
  readonly workspaceParent: string;
@@ -90,9 +96,43 @@ export interface PreparedSpatialAssets {
90
96
  const inputSchema = z.strictObject({
91
97
  snapshots: z.array(EvaluatedSpatialSceneSchema).min(1).max(SPATIAL_OVERLAY_LIMITS.frames),
92
98
  exactSceneTimesUs: z.array(z.strictObject({ numerator: z.string().regex(/^(?:0|[1-9]\d{0,23})$/u), denominator: z.string().regex(/^[1-9]\d{0,8}$/u) })).min(1).max(SPATIAL_OVERLAY_LIMITS.frames).optional(),
99
+ lutAssetIds: z.array(z.string().min(1).max(256)).max(64).optional(),
93
100
  assetRoot: z.string().min(1).max(4_096), workspaceParent: z.string().min(1).max(4_096),
94
101
  });
95
102
  const digest = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
103
+ export interface SpatialMaterialDerivationReceipt {
104
+ readonly kind: "slopcamera.spatial-material-derivation";
105
+ readonly schemaVersion: 1;
106
+ readonly candidate: SpatialDerivationCandidate;
107
+ readonly sourceSha256: string;
108
+ readonly outputSha256: string;
109
+ readonly provenanceSha256: string;
110
+ readonly width: number;
111
+ readonly height: number;
112
+ readonly output: Uint8Array;
113
+ }
114
+
115
+ /** Deterministic, local-only raster derivation. Output is an opaque PNG bound to exact source bytes and candidate provenance. */
116
+ export async function deriveSpatialMaterialRaster(input: { readonly candidate: SpatialDerivationCandidate; readonly source: Uint8Array; readonly sourceSha256: string }): Promise<SpatialMaterialDerivationReceipt> {
117
+ const candidate = SpatialDerivationCandidateSchema.parse(input.candidate);
118
+ if (!(input.source instanceof Uint8Array) || input.source.byteLength < 1 || input.source.byteLength > SPATIAL_ASSET_PREPARATION_LIMITS.sourceBytes || digest(input.source) !== input.sourceSha256) throw new RangeError("Material derivation source bytes must match their exact bounded digest.");
119
+ const image = sharp(input.source, { failOn: "error", limitInputPixels: SPATIAL_ASSET_PREPARATION_LIMITS.rasterPixels, sequentialRead: true }).removeAlpha().toColourspace("b-w");
120
+ const metadata = await image.metadata();
121
+ if (metadata.width === undefined || metadata.height === undefined || metadata.width * metadata.height > SPATIAL_ASSET_PREPARATION_LIMITS.rasterPixels) throw new RangeError("Material derivation exceeds the decoded pixel budget.");
122
+ let output: Buffer;
123
+ if (candidate.method === "sobel-normal-from-height") {
124
+ const raw = await image.raw().toBuffer(); const rgba = Buffer.alloc(metadata.width * metadata.height * 4); const sample = (x: number, y: number) => raw[Math.max(0, Math.min(metadata.height - 1, y)) * metadata.width + Math.max(0, Math.min(metadata.width - 1, x))]! / 255;
125
+ for (let y = 0; y < metadata.height; y++) for (let x = 0; x < metadata.width; x++) { const gx = sample(x + 1, y) - sample(x - 1, y), gy = sample(x, y + 1) - sample(x, y - 1), length = Math.hypot(gx, gy, 1), at = (y * metadata.width + x) * 4; rgba[at] = Math.round((-.5 * gx / length + .5) * 255); rgba[at + 1] = Math.round((-.5 * gy / length + .5) * 255); rgba[at + 2] = Math.round((1 / length * .5 + .5) * 255); rgba[at + 3] = 255; }
126
+ output = await sharp(rgba, { raw: { width: metadata.width, height: metadata.height, channels: 4 } }).png({ compressionLevel: 9, adaptiveFiltering: false, palette: false }).toBuffer();
127
+ } else {
128
+ const raw = await image.raw().toBuffer();
129
+ if (candidate.method === "average-luminance-roughness") for (let index = 0; index < raw.length; index++) raw[index] = 255 - raw[index]!;
130
+ output = await sharp(raw, { raw: { width: metadata.width, height: metadata.height, channels: 1 } }).png({ compressionLevel: 9, adaptiveFiltering: false, palette: false }).toBuffer();
131
+ }
132
+ const outputBytes = Uint8Array.from(output), outputSha256 = digest(outputBytes);
133
+ const provenanceSha256 = canonicalJsonSha256({ domain: "slopcamera.spatial-material-derivation/v1", candidate, sourceSha256: input.sourceSha256, outputSha256, width: metadata.width, height: metadata.height });
134
+ return Object.freeze({ kind: "slopcamera.spatial-material-derivation", schemaVersion: 1, candidate, sourceSha256: input.sourceSha256, outputSha256, provenanceSha256, width: metadata.width, height: metadata.height, output: outputBytes });
135
+ }
96
136
  function capability(name: string, message: string): never { throw new SpatialOverlayCapabilityError(name, message); }
97
137
  function aborted(signal: AbortSignal): void { if (signal.aborted) throw signal.reason instanceof Error ? signal.reason : new Error("Spatial asset preparation was cancelled."); }
98
138
  function utf8(bytes: Uint8Array): string { return new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
@@ -341,9 +381,15 @@ export function spatialVideoTimeUs(entity: Extract<SpatialEntity, { kind: "video
341
381
  }
342
382
 
343
383
  /** Owns every temporary/native input until the consumer and all native calls settle. */
384
+ export interface SpatialAssetPreparationOptions {
385
+ /** When true, an SPZ that fails inspection is skipped rather than aborting preparation. */
386
+ readonly tolerateSplatDecodeFailure?: boolean;
387
+ }
388
+
344
389
  export async function withPreparedSpatialAssets<Result>(
345
390
  input: unknown, ports: SpatialAssetPreparationPorts, signal: AbortSignal,
346
391
  consume: (prepared: PreparedSpatialAssets) => Promise<Result>,
392
+ options: SpatialAssetPreparationOptions = {},
347
393
  ): Promise<Result> {
348
394
  const request = inputSchema.parse(createBoundedJsonSnapshot(input, SPATIAL_OVERLAY_LIMITS.requestBytes, "Spatial asset preparation request", { maximumDepth: 48, maximumValues: SPATIAL_OVERLAY_LIMITS.requestValues }).value);
349
395
  if (request.exactSceneTimesUs !== undefined && request.exactSceneTimesUs.length !== request.snapshots.length) throw new RangeError("Exact scene times must correspond one-to-one to snapshots.");
@@ -398,6 +444,24 @@ export async function withPreparedSpatialAssets<Result>(
398
444
  resources.set(name, { ...resource, absolutePath: path });
399
445
  return { resource, width: image.width, height: image.height, alpha: image.alpha };
400
446
  };
447
+ /** Verified image manifest → normalized PNG publication; shared by surfaces, environments, and material maps. */
448
+ const decodeImageRaster = async (asset: SpatialVerifiedAsset, label: string) => {
449
+ const interpretation = asset.manifest.interpretation;
450
+ if (interpretation.kind !== "image") throw new RangeError(`${label} requires an image asset interpretation.`);
451
+ let bytes = asset.bytes;
452
+ if (interpretation.mimeType === "image/png" && (bytes[0] !== 137 || bytes[1] !== 80 || bytes[2] !== 78 || bytes[3] !== 71)
453
+ || interpretation.mimeType === "image/jpeg" && (bytes[0] !== 255 || bytes[1] !== 216)) throw new RangeError("Image payload format differs from its declared MIME type.");
454
+ if (interpretation.mimeType === "image/svg+xml") {
455
+ const svg = inertSpatialSvg(utf8(bytes));
456
+ const resvg = new Resvg(svg, { font: { loadSystemFonts: false } });
457
+ dimensions(resvg.width, resvg.height);
458
+ bytes = resvg.render().asPng(); profiles.add("shape-only-svg-resvg-2.6.2");
459
+ }
460
+ const decoded = await raster(bytes, interpretation.alpha === "opaque");
461
+ if (decoded.width !== interpretation.width || decoded.height !== interpretation.height) throw new RangeError("Image dimensions differ from its interpretation.");
462
+ profiles.add("sdr-png-jpeg-sharp-0.35.3");
463
+ return publishRaster(decoded);
464
+ };
401
465
  const native = async (argv: readonly [string, ...string[]], maxOutputBytes = 16_384) => {
402
466
  aborted(signal);
403
467
  const result = await ports.runner.run(argv, { abortSignal: signal, timeoutMs: SPATIAL_ASSET_PREPARATION_LIMITS.nativeTimeoutMs, maxOutputBytes, stdin: "ignore" });
@@ -426,9 +490,18 @@ export async function withPreparedSpatialAssets<Result>(
426
490
  if (bytes.byteLength > SPATIAL_SPLAT_LIMITS.metadataBytes) throw new RangeError("Retained metadata exceeds its byte bound.");
427
491
  const captured = createBoundedJsonSnapshot(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)), SPATIAL_SPLAT_LIMITS.metadataBytes, "Retained world metadata", { maximumDepth: 24, maximumValues: 32_768 });
428
492
  const metadata = captured.value as Record<string, unknown>;
429
- if (metadata.kind !== manifest.interpretation.schema || metadata.schemaVersion !== 1) throw new RangeError("Retained metadata does not match its declared schema.");
430
- if (manifest.interpretation.schema === "slopcamera.spatial-world-import") SpatialWorldImportManifestSchema.parse(metadata);
431
- else WorldLabsProvenanceSchema.parse(metadata);
493
+ const schema = manifest.interpretation.schema;
494
+ if (schema === "slopcamera.provider-metadata") {
495
+ // Raw provider bytes are retained verbatim — no slopcamera
496
+ // envelope to match. A recognized semantics_metadata shape proves
497
+ // the declaration; unknown keys stay tolerated and ignored.
498
+ extractWorldProviderMetadata(metadata);
499
+ } else {
500
+ if (metadata.kind !== schema || metadata.schemaVersion !== 1) throw new RangeError("Retained metadata does not match its declared schema.");
501
+ if (schema === "slopcamera.spatial-world-import") SpatialWorldImportManifestSchema.parse(metadata);
502
+ else if (schema === "slopcamera.spatial-asset-facts") SpatialAssetFactsV1Schema.parse(metadata);
503
+ else WorldLabsProvenanceSchema.parse(metadata);
504
+ }
432
505
  }
433
506
  const record = { manifest, manifestSha256: closures[manifest.assetId]!, bytes };
434
507
  verified.set(manifest.assetId, record);
@@ -441,6 +514,31 @@ export async function withPreparedSpatialAssets<Result>(
441
514
  const snapshot = request.snapshots[snapshotIndex]!, exactTime = exactTimes[snapshotIndex]!;
442
515
  aborted(signal);
443
516
  const entity = entry.entity;
517
+ // A procedural mesh's material map is a secondary binding: the entity
518
+ // still has no primary asset, so it bypasses the per-entity asset path.
519
+ const materialMaps = entity.kind !== "mesh" ? [] : entity.material.kind === "pbr"
520
+ ? pbrMaterialMapAssetIds(entity.material) : entity.material.map === undefined ? [] : [entity.material.map];
521
+ if (entity.kind === "mesh" && entity.geometry.kind !== "asset" && materialMaps.length > 0) {
522
+ let expectedDimensions: readonly [number, number] | undefined;
523
+ for (const materialMap of materialMaps) {
524
+ const mapAsset = verified.get(materialMap);
525
+ if (mapAsset === undefined) throw new RangeError(`Missing manifest for spatial asset ${materialMap}.`);
526
+ const mapKey = `${materialMap}:${entity.entityId}:static`;
527
+ let prepared = preparedAssets.get(mapKey);
528
+ if (prepared === undefined) {
529
+ const output = await decodeImageRaster(mapAsset, `Material map on ${entity.entityId}`);
530
+ prepared = PreparedSpatialAssetSchema.parse({ kind: "raster", assetId: materialMap,
531
+ assetManifestSha256: mapAsset.manifestSha256, entityId: entity.entityId, timeUs: null, ...output });
532
+ preparedAssets.set(mapKey, prepared);
533
+ }
534
+ if (prepared.kind !== "raster") throw new RangeError("PBR maps require exact prepared rasters.");
535
+ const current = [prepared.width, prepared.height] as const;
536
+ if (expectedDimensions !== undefined && (current[0] !== expectedDimensions[0] || current[1] !== expectedDimensions[1])) {
537
+ throw new RangeError(`PBR maps on ${entity.entityId} must have identical decoded dimensions.`);
538
+ }
539
+ expectedDimensions = current;
540
+ }
541
+ }
444
542
  if (entity.kind === "group" || entity.kind === "light" || entity.kind === "mesh" && entity.geometry.kind !== "asset") continue;
445
543
  const assetId = entity.kind === "text" ? entity.fontAssetId : entity.kind === "mesh" && entity.geometry.kind === "asset" ? entity.geometry.assetId : "assetId" in entity ? entity.assetId : "";
446
544
  const asset = verified.get(assetId);
@@ -450,14 +548,26 @@ export async function withPreparedSpatialAssets<Result>(
450
548
  if (interpretation.kind !== "splat" || interpretation.format !== "spz") capability("splat-format", "The qualified world profile accepts gzip SPZ v2/v3 only.");
451
549
  const key = `${assetId}:${entity.entityId}:splat`;
452
550
  if (preparedAssets.has(key)) continue;
453
- const facts = await inspectSpatialSpz(asset.bytes, signal);
551
+ const inspected = await inspectSpatialSpz(asset.bytes, signal).catch((error: unknown) => {
552
+ if (options.tolerateSplatDecodeFailure) return undefined;
553
+ throw error;
554
+ });
555
+ // Audits may skip a splat that cannot be inspected: the rendered audit
556
+ // analyzer reports unsupported-kind when no bounds are supplied. Beauty
557
+ // rendering keeps the failure so invalid source does not render silently.
558
+ if (inspected === undefined) continue;
559
+ const { facts, modelBounds } = inspected;
560
+ // Entity-local enclosure: the same sourceUp→Y rotation and uniform
561
+ // metersPerUnit scale the renderer applies ahead of the entity
562
+ // transform, so the object-ID proxy and the geometric audit agree.
563
+ const bounds = transformBounds(spatialSplatContentTransform(interpretation), modelBounds);
454
564
  const name = `splat-${asset.manifestSha256.slice(0, 40)}`;
455
565
  const resource = { name, sha256: asset.manifest.payload.sha256, bytes: asset.bytes.byteLength, mediaType: "application/octet-stream", urlPath: `${name}.spz` };
456
566
  if (!resources.has(name)) {
457
567
  if (resources.size >= 64 || outputBytes + asset.bytes.byteLength > SPATIAL_ASSET_PREPARATION_LIMITS.outputBytes) throw new RangeError("Prepared splat resources exceed their byte budget.");
458
568
  resources.set(name, { ...resource, absolutePath: paths.get(assetId)! }); outputBytes += asset.bytes.byteLength;
459
569
  }
460
- preparedAssets.set(key, PreparedSpatialAssetSchema.parse({ kind: "splat", assetId, entityId: entity.entityId, assetManifestSha256: asset.manifestSha256, resource, facts }));
570
+ preparedAssets.set(key, PreparedSpatialAssetSchema.parse({ kind: "splat", assetId, entityId: entity.entityId, assetManifestSha256: asset.manifestSha256, resource, facts, bounds }));
461
571
  profiles.add("slopcamera.spz-v2-v3-spark-2.1.0-full-resolution-v1");
462
572
  continue;
463
573
  }
@@ -478,12 +588,18 @@ export async function withPreparedSpatialAssets<Result>(
478
588
  images.set(image.resourceName, await publishRaster(await raster(image.bytes, image.opaque), image.resourceName));
479
589
  }
480
590
  const used = new Set<string>();
591
+ const textureSlots = ["texture", "normalTexture", "metallicRoughnessTexture", "occlusionTexture", "emissiveTexture", "clearcoatTexture", "clearcoatRoughnessTexture", "clearcoatNormalTexture", "transmissionTexture", "sheenColorTexture", "sheenRoughnessTexture", "anisotropyTexture"] as const;
481
592
  const primitives = geometry.primitives.map(primitive => {
482
- if (primitive.texture === undefined) return primitive;
483
- const image = images.get(primitive.texture.resource.name);
484
- if (image === undefined) throw new RangeError("Geometry texture has no exact prepared image bytes.");
485
- used.add(primitive.texture.resource.name);
486
- return { ...primitive, texture: { ...primitive.texture, ...image } };
593
+ const resolved: Record<string, unknown> = { ...primitive };
594
+ for (const slot of textureSlots) {
595
+ const texture = primitive[slot];
596
+ if (texture === undefined) continue;
597
+ const image = images.get(texture.resource.name);
598
+ if (image === undefined) throw new RangeError("Geometry texture has no exact prepared image bytes.");
599
+ used.add(texture.resource.name);
600
+ resolved[slot] = { ...texture, ...image };
601
+ }
602
+ return resolved;
487
603
  });
488
604
  if (used.size !== images.size) throw new RangeError("Geometry preparer returned unused image bytes.");
489
605
  const resolved = PreparedSpatialAssetSchema.parse({ ...geometry, primitives });
@@ -497,21 +613,37 @@ export async function withPreparedSpatialAssets<Result>(
497
613
  ...(entity.geometry.nodeIndex === undefined ? {} : { nodeIndex: entity.geometry.nodeIndex }),
498
614
  ...(entity.geometry.materialMode === undefined ? {} : { materialMode: entity.geometry.materialMode }),
499
615
  ...(entity.geometry.clip === undefined ? {} : { clip: entity.geometry.clip }),
616
+ ...(entity.geometry.morphWeights === undefined ? {} : { morphWeights: entity.geometry.morphWeights }),
500
617
  });
618
+ const publishTexture = async (reference: { readonly imageIndex: number; readonly sampler: unknown; readonly transform?: { readonly offset: readonly [number, number]; readonly rotation: number; readonly scale: readonly [number, number] } } | undefined, opaque: boolean) => {
619
+ if (reference === undefined) return undefined;
620
+ const image = geometry.images.find(image => image.imageIndex === reference.imageIndex);
621
+ if (image === undefined) throw new RangeError("GLB texture image is missing.");
622
+ return { ...await publishRaster(await raster(image.bytes, opaque)), flipY: false, sampler: reference.sampler, ...(reference.transform === undefined ? {} : { uvTransform: reference.transform }) };
623
+ };
501
624
  const primitives: unknown[] = [];
502
625
  for (const primitive of geometry.primitives) {
503
626
  const { bounds: _bounds, material: sourceMaterial, ...base } = primitive;
504
- let texture: unknown;
505
- if (sourceMaterial?.baseColorTexture !== undefined) {
506
- const source = sourceMaterial.baseColorTexture, image = geometry.images.find(image => image.imageIndex === source.imageIndex);
507
- if (image === undefined) throw new RangeError("GLB texture image is missing.");
508
- texture = { ...await publishRaster(await raster(image.bytes, sourceMaterial.alphaMode === "OPAQUE")), flipY: false, sampler: source.sampler };
509
- }
627
+ const texture = await publishTexture(sourceMaterial?.baseColorTexture, sourceMaterial?.alphaMode === "OPAQUE");
628
+ const metallicRoughnessTexture = await publishTexture(sourceMaterial?.metallicRoughnessTexture, false);
629
+ const normalTexture = await publishTexture(sourceMaterial?.normalTexture, false);
630
+ const occlusionTexture = await publishTexture(sourceMaterial?.occlusionTexture, false);
631
+ const emissiveTexture = await publishTexture(sourceMaterial?.emissiveTexture, false);
632
+ const clearcoatTexture = await publishTexture(sourceMaterial?.clearcoat?.texture, false), clearcoatRoughnessTexture = await publishTexture(sourceMaterial?.clearcoat?.roughnessTexture, false), clearcoatNormalTexture = await publishTexture(sourceMaterial?.clearcoat?.normalTexture, false);
633
+ const transmissionTexture = await publishTexture(sourceMaterial?.transmission?.texture, false), sheenColorTexture = await publishTexture(sourceMaterial?.sheen?.colorTexture, false), sheenRoughnessTexture = await publishTexture(sourceMaterial?.sheen?.roughnessTexture, false), anisotropyTexture = await publishTexture(sourceMaterial?.anisotropy?.texture, false);
634
+ const physical = sourceMaterial === undefined ? undefined : { ...(sourceMaterial.emissiveLinear === undefined ? {} : { emissiveLinear: sourceMaterial.emissiveLinear }), ...(sourceMaterial.emissiveStrength === undefined ? {} : { emissiveStrength: sourceMaterial.emissiveStrength }), ...(sourceMaterial.clearcoat === undefined ? {} : { clearcoat: { factor: sourceMaterial.clearcoat.factor, roughness: sourceMaterial.clearcoat.roughness } }), ...(sourceMaterial.transmission === undefined ? {} : { transmission: sourceMaterial.transmission.factor }), ...(sourceMaterial.sheen === undefined ? {} : { sheen: { colorLinear: sourceMaterial.sheen.colorLinear, roughness: sourceMaterial.sheen.roughness } }), ...(sourceMaterial.anisotropy === undefined ? {} : { anisotropy: { strength: sourceMaterial.anisotropy.strength, rotation: sourceMaterial.anisotropy.rotation } }), ...(sourceMaterial.ior === undefined ? {} : { ior: sourceMaterial.ior }) };
510
635
  primitives.push({ ...base,
511
636
  ...(sourceMaterial === undefined ? {} : { material: { kind: "standard", color: "#ffffff", opacity: sourceMaterial.alphaMode === "OPAQUE" ? 1 : sourceMaterial.baseColorLinear[3], metalness: sourceMaterial.metalness, roughness: sourceMaterial.roughness },
512
637
  linearColor: sourceMaterial.baseColorLinear.slice(0, 3), doubleSided: sourceMaterial.doubleSided, alphaMode: sourceMaterial.alphaMode,
513
638
  ...(sourceMaterial.alphaMode === "MASK" ? { alphaCutoff: sourceMaterial.alphaCutoff } : {}) }),
514
639
  ...(texture === undefined ? {} : { texture }),
640
+ ...(metallicRoughnessTexture === undefined ? {} : { metallicRoughnessTexture }),
641
+ ...(normalTexture === undefined ? {} : { normalTexture, ...(sourceMaterial?.normalTexture?.scale === undefined ? {} : { normalScale: sourceMaterial.normalTexture.scale }) }),
642
+ ...(occlusionTexture === undefined ? {} : { occlusionTexture, ...(sourceMaterial?.occlusionTexture?.strength === undefined ? {} : { occlusionStrength: sourceMaterial.occlusionTexture.strength }) }),
643
+ ...(emissiveTexture === undefined ? {} : { emissiveTexture }),
644
+ ...(clearcoatTexture === undefined ? {} : { clearcoatTexture }), ...(clearcoatRoughnessTexture === undefined ? {} : { clearcoatRoughnessTexture }), ...(clearcoatNormalTexture === undefined ? {} : { clearcoatNormalTexture }),
645
+ ...(transmissionTexture === undefined ? {} : { transmissionTexture }), ...(sheenColorTexture === undefined ? {} : { sheenColorTexture }), ...(sheenRoughnessTexture === undefined ? {} : { sheenRoughnessTexture }), ...(anisotropyTexture === undefined ? {} : { anisotropyTexture }),
646
+ ...(physical === undefined ? {} : { physical }),
515
647
  });
516
648
  }
517
649
  const resolved = PreparedSpatialAssetSchema.parse({ kind: "geometry", assetId, entityId: entity.entityId,
@@ -534,20 +666,8 @@ export async function withPreparedSpatialAssets<Result>(
534
666
  if (preparedAssets.has(key)) continue;
535
667
  let output: Awaited<ReturnType<typeof publishRaster>>, sourceTimeUs: number | undefined;
536
668
  let videoEvidence: { sourceFrameIndex: number; sourcePresentationTimeUs: number; sourceTimestamp: string; sourcePts: number; sourceTimeBase: { numerator: string; denominator: string }; sourceExactTimeUs: { numerator: string; denominator: string } } | undefined;
537
- if (entity.kind === "image") {
538
- if (interpretation.kind !== "image") throw new RangeError("Image entity requires an image asset interpretation.");
539
- let bytes = asset.bytes;
540
- if (interpretation.mimeType === "image/png" && (bytes[0] !== 137 || bytes[1] !== 80 || bytes[2] !== 78 || bytes[3] !== 71)
541
- || interpretation.mimeType === "image/jpeg" && (bytes[0] !== 255 || bytes[1] !== 216)) throw new RangeError("Image payload format differs from its declared MIME type.");
542
- if (interpretation.mimeType === "image/svg+xml") {
543
- const svg = inertSpatialSvg(utf8(bytes));
544
- const resvg = new Resvg(svg, { font: { loadSystemFonts: false } });
545
- dimensions(resvg.width, resvg.height);
546
- bytes = resvg.render().asPng(); profiles.add("shape-only-svg-resvg-2.6.2");
547
- }
548
- const decoded = await raster(bytes, interpretation.alpha === "opaque");
549
- if (decoded.width !== interpretation.width || decoded.height !== interpretation.height) throw new RangeError("Image dimensions differ from its interpretation.");
550
- output = await publishRaster(decoded); profiles.add("sdr-png-jpeg-sharp-0.35.3");
669
+ if (entity.kind === "image" || entity.kind === "environment") {
670
+ output = await decodeImageRaster(asset, entity.kind === "image" ? "Image entity" : "Environment entity");
551
671
  } else if (entity.kind === "video") {
552
672
  if (interpretation.kind !== "video") throw new RangeError("Video entity requires a video asset interpretation.");
553
673
  if (ports.ffmpegCommand === undefined || ports.ffprobeCommand === undefined) capability("video-decoder", "Video surfaces require bound FFmpeg and FFprobe capabilities.");
@@ -656,6 +776,15 @@ export async function withPreparedSpatialAssets<Result>(
656
776
  ...(entity.kind === "video" ? { entityContentSha256: spatialVideoRasterContentSha256(entity) } : {}),
657
777
  }));
658
778
  }
779
+ for (const assetId of request.lutAssetIds ?? []) {
780
+ const key = `lut:${assetId}`;
781
+ if (preparedAssets.has(key)) continue;
782
+ const asset = verified.get(assetId);
783
+ if (asset === undefined) throw new RangeError(`Missing manifest for LUT asset ${assetId}.`);
784
+ const output = await decodeImageRaster(asset, `LUT asset ${assetId}`);
785
+ preparedAssets.set(key, PreparedSpatialAssetSchema.parse({ kind: "lut", assetId, assetManifestSha256: asset.manifestSha256, ...output }));
786
+ aborted(signal);
787
+ }
659
788
  aborted(signal);
660
789
  const prepared = [...preparedAssets.values()];
661
790
  completed = { value: await consume(Object.freeze({ preparedAssets: prepared, resources: [...resources.values()],