@hraness/slopcamera 3.2.4
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/LICENSE +21 -0
- package/NOTICE.md +60 -0
- package/PRIVACY.md +46 -0
- package/README.md +311 -0
- package/SECURITY.md +21 -0
- package/apps/desktop/README.md +966 -0
- package/apps/desktop/analysis/protocol.ts +223 -0
- package/apps/desktop/application/capability-binding.ts +594 -0
- package/apps/desktop/application/context.ts +97 -0
- package/apps/desktop/application/creative-iteration.ts +1385 -0
- package/apps/desktop/application/default-registry.ts +146 -0
- package/apps/desktop/application/directing-contract.ts +218 -0
- package/apps/desktop/application/directing-plan.ts +178 -0
- package/apps/desktop/application/errors.ts +18 -0
- package/apps/desktop/application/gateway-port.ts +867 -0
- package/apps/desktop/application/html-overlay-browser-runtime.ts +687 -0
- package/apps/desktop/application/html-overlay-integrity.ts +337 -0
- package/apps/desktop/application/html-overlay-renderer.ts +45 -0
- package/apps/desktop/application/index.ts +13 -0
- package/apps/desktop/application/operation-completion-checkpoint.ts +333 -0
- package/apps/desktop/application/operation-effects.ts +94 -0
- package/apps/desktop/application/operation.ts +224 -0
- package/apps/desktop/application/operations/analysis/faces.ts +244 -0
- package/apps/desktop/application/operations/analysis/music.ts +248 -0
- package/apps/desktop/application/operations/analysis/project-auto-zooms.ts +302 -0
- package/apps/desktop/application/operations/analysis/project-inactivity.ts +381 -0
- package/apps/desktop/application/operations/analysis/scenes.ts +272 -0
- package/apps/desktop/application/operations/analysis/shared.ts +202 -0
- package/apps/desktop/application/operations/derive/edit-batch.ts +967 -0
- package/apps/desktop/application/operations/derive/follow-faces.ts +454 -0
- package/apps/desktop/application/operations/edit/bind-candidate-revision.ts +91 -0
- package/apps/desktop/application/operations/edit/create-candidate-revision.ts +377 -0
- package/apps/desktop/application/operations/edit/create-revision.ts +255 -0
- package/apps/desktop/application/operations/edit/freeze-revision.ts +182 -0
- package/apps/desktop/application/operations/gateway/image.ts +41 -0
- package/apps/desktop/application/operations/gateway/index.ts +17 -0
- package/apps/desktop/application/operations/gateway/shared.ts +122 -0
- package/apps/desktop/application/operations/gateway/speech.ts +41 -0
- package/apps/desktop/application/operations/gateway/transcription.ts +42 -0
- package/apps/desktop/application/operations/gateway/video.ts +41 -0
- package/apps/desktop/application/operations/index.ts +31 -0
- package/apps/desktop/application/operations/iteration/create-candidate.ts +240 -0
- package/apps/desktop/application/operations/iteration/create-matrix.ts +117 -0
- package/apps/desktop/application/operations/iteration/select.ts +155 -0
- package/apps/desktop/application/operations/iteration/shared.ts +250 -0
- package/apps/desktop/application/operations/media/audio-effects.ts +376 -0
- package/apps/desktop/application/operations/media/binding.ts +79 -0
- package/apps/desktop/application/operations/media/capabilities.ts +112 -0
- package/apps/desktop/application/operations/media/color-grade.ts +343 -0
- package/apps/desktop/application/operations/media/html-overlay.ts +802 -0
- package/apps/desktop/application/operations/media/index.ts +8 -0
- package/apps/desktop/application/operations/media/ingest.ts +303 -0
- package/apps/desktop/application/operations/media/overlay.ts +729 -0
- package/apps/desktop/application/operations/media/shared.ts +845 -0
- package/apps/desktop/application/operations/media/transform-platform.ts +120 -0
- package/apps/desktop/application/operations/project/commit-edits.ts +2054 -0
- package/apps/desktop/application/operations/project/promote-selection.ts +495 -0
- package/apps/desktop/application/operations/project/snapshot.ts +95 -0
- package/apps/desktop/application/operations/recording/index.ts +5 -0
- package/apps/desktop/application/operations/recording/pause.ts +34 -0
- package/apps/desktop/application/operations/recording/resume.ts +34 -0
- package/apps/desktop/application/operations/recording/shared.ts +199 -0
- package/apps/desktop/application/operations/recording/start.ts +169 -0
- package/apps/desktop/application/operations/recording/stop.ts +34 -0
- package/apps/desktop/application/operations/render/bind-candidate-output.ts +324 -0
- package/apps/desktop/application/operations/render/materialize-selection.ts +322 -0
- package/apps/desktop/application/operations/render/project-caption-plan.ts +287 -0
- package/apps/desktop/application/operations/render/project-plan.ts +939 -0
- package/apps/desktop/application/operations/render/project-spatial-receipt.ts +70 -0
- package/apps/desktop/application/operations/render/project.ts +1922 -0
- package/apps/desktop/application/operations/shared.ts +67 -0
- package/apps/desktop/application/operations/slopcamera-portable.ts +1188 -0
- package/apps/desktop/application/operations/slopcamera-visuals.ts +784 -0
- package/apps/desktop/application/operations/spatial-project.ts +209 -0
- package/apps/desktop/application/operations/spatial-render.ts +415 -0
- package/apps/desktop/application/operations/spatial-scene.ts +97 -0
- package/apps/desktop/application/operations/studio.ts +143 -0
- package/apps/desktop/application/output-publication-lease.ts +163 -0
- package/apps/desktop/application/project-publication-lease.ts +233 -0
- package/apps/desktop/application/project-store.ts +196 -0
- package/apps/desktop/application/receipts.ts +533 -0
- package/apps/desktop/application/registry.ts +182 -0
- package/apps/desktop/application/spatial-assets.ts +675 -0
- package/apps/desktop/application/spatial-compositor-cadence.ts +23 -0
- package/apps/desktop/application/spatial-project-authority.ts +35 -0
- package/apps/desktop/application/spatial-project-lease.ts +126 -0
- package/apps/desktop/application/spatial-project-render-preparation.ts +225 -0
- package/apps/desktop/application/spatial-project-store.ts +286 -0
- package/apps/desktop/application/spatial-render-projection.ts +201 -0
- package/apps/desktop/application/spatial-render.ts +677 -0
- package/apps/desktop/application/spatial-spz.ts +44 -0
- package/apps/desktop/application/spatial-world-import.ts +135 -0
- package/apps/desktop/application/spatial-world-provenance.ts +46 -0
- package/apps/desktop/application/studio-port.ts +58 -0
- package/apps/desktop/application/verified-receipt-reconciliation.ts +1574 -0
- package/apps/desktop/capture/protocol.ts +689 -0
- package/apps/desktop/cli/analyzer.ts +406 -0
- package/apps/desktop/cli/args.ts +3181 -0
- package/apps/desktop/cli/asset-ingest.ts +747 -0
- package/apps/desktop/cli/atomic-render-effects.ts +210 -0
- package/apps/desktop/cli/atomic-render-platform.ts +205 -0
- package/apps/desktop/cli/atomic-render.ts +50 -0
- package/apps/desktop/cli/audio-analysis.ts +252 -0
- package/apps/desktop/cli/bounded-map.ts +39 -0
- package/apps/desktop/cli/bundle-service.ts +256 -0
- package/apps/desktop/cli/capabilities.ts +212 -0
- package/apps/desktop/cli/capture-bundle.ts +1117 -0
- package/apps/desktop/cli/command-host-resources.ts +251 -0
- package/apps/desktop/cli/commands.ts +6836 -0
- package/apps/desktop/cli/directing-blob.ts +291 -0
- package/apps/desktop/cli/directing-media.ts +517 -0
- package/apps/desktop/cli/directing-quote.ts +89 -0
- package/apps/desktop/cli/directing-service.ts +288 -0
- package/apps/desktop/cli/emoji-assets.ts +538 -0
- package/apps/desktop/cli/errors.ts +29 -0
- package/apps/desktop/cli/face-analysis-service.ts +940 -0
- package/apps/desktop/cli/filter-script.ts +76 -0
- package/apps/desktop/cli/gateway-application-port.ts +1142 -0
- package/apps/desktop/cli/gateway-credential.ts +81 -0
- package/apps/desktop/cli/gateway-media-artifacts.ts +723 -0
- package/apps/desktop/cli/gateway-media-catalog.ts +922 -0
- package/apps/desktop/cli/gateway-media-service.ts +3106 -0
- package/apps/desktop/cli/gateway-media-signature.ts +122 -0
- package/apps/desktop/cli/gateway-provider-options.ts +562 -0
- package/apps/desktop/cli/gateway-scene-provider.ts +391 -0
- package/apps/desktop/cli/help.ts +501 -0
- package/apps/desktop/cli/html-overlay-gpu.ts +97 -0
- package/apps/desktop/cli/html-overlay-renderer.ts +2478 -0
- package/apps/desktop/cli/io.ts +290 -0
- package/apps/desktop/cli/main.ts +123 -0
- package/apps/desktop/cli/media-effects-service.ts +885 -0
- package/apps/desktop/cli/media-ingest-model.ts +364 -0
- package/apps/desktop/cli/media-ingest-platform.ts +227 -0
- package/apps/desktop/cli/media-ingest-program.ts +200 -0
- package/apps/desktop/cli/media-ingest.ts +30 -0
- package/apps/desktop/cli/music-analysis-service.ts +434 -0
- package/apps/desktop/cli/mutation-lock.ts +530 -0
- package/apps/desktop/cli/native-assets.d.ts +12 -0
- package/apps/desktop/cli/native-media-runtime.macos.ts +7 -0
- package/apps/desktop/cli/overlay-playback.ts +177 -0
- package/apps/desktop/cli/paths.ts +265 -0
- package/apps/desktop/cli/portable-surface.ts +242 -0
- package/apps/desktop/cli/project-camera-receipt.ts +112 -0
- package/apps/desktop/cli/project-face-camera.ts +394 -0
- package/apps/desktop/cli/project-inactivity-service.ts +552 -0
- package/apps/desktop/cli/project-media-integrity.ts +121 -0
- package/apps/desktop/cli/project-renderer.ts +1526 -0
- package/apps/desktop/cli/project-service.ts +519 -0
- package/apps/desktop/cli/project-state-transaction.ts +520 -0
- package/apps/desktop/cli/query-limits.ts +1 -0
- package/apps/desktop/cli/recording-controller.ts +1567 -0
- package/apps/desktop/cli/recording-daemon.ts +931 -0
- package/apps/desktop/cli/recording-ref.ts +56 -0
- package/apps/desktop/cli/renamed-environment.ts +6 -0
- package/apps/desktop/cli/renderer.ts +1439 -0
- package/apps/desktop/cli/run-cli-test-helper.ts +60 -0
- package/apps/desktop/cli/scene-analysis-service.ts +879 -0
- package/apps/desktop/cli/spatial-compositor-verifier.ts +213 -0
- package/apps/desktop/cli/spatial-project-service.ts +90 -0
- package/apps/desktop/cli/spatial-scene-service.ts +105 -0
- package/apps/desktop/cli/spatial-world-service.ts +39 -0
- package/apps/desktop/cli/speech-analysis-service.ts +682 -0
- package/apps/desktop/cli/studio-assemble.ts +58 -0
- package/apps/desktop/cli/studio-command.ts +60 -0
- package/apps/desktop/cli/studio-custody.ts +60 -0
- package/apps/desktop/cli/studio-encode.ts +216 -0
- package/apps/desktop/cli/studio-exr.ts +52 -0
- package/apps/desktop/cli/studio-files.ts +179 -0
- package/apps/desktop/cli/studio-output-validation.ts +162 -0
- package/apps/desktop/cli/studio-process.ts +145 -0
- package/apps/desktop/cli/studio-runtime.ts +77 -0
- package/apps/desktop/cli/studio-scaffold.ts +70 -0
- package/apps/desktop/cli/studio-service.ts +301 -0
- package/apps/desktop/cli/studio-spatial-asset.ts +248 -0
- package/apps/desktop/cli/studio-template-names.ts +3 -0
- package/apps/desktop/cli/studio-workflow.ts +19 -0
- package/apps/desktop/cli/time.ts +25 -0
- package/apps/desktop/cli/workflow-code.ts +584 -0
- package/apps/desktop/cli/workflow-runs.ts +576 -0
- package/apps/desktop/code/advanced.ts +139 -0
- package/apps/desktop/code/application-node-planner.ts +1288 -0
- package/apps/desktop/code/compiler.ts +329 -0
- package/apps/desktop/code/contracts.ts +13 -0
- package/apps/desktop/code/creative-recipes.ts +94 -0
- package/apps/desktop/code/define-workflow.ts +115 -0
- package/apps/desktop/code/editing.ts +284 -0
- package/apps/desktop/code/file-candidate-provenance.ts +420 -0
- package/apps/desktop/code/file-candidate.ts +69 -0
- package/apps/desktop/code/graph-builder.ts +10 -0
- package/apps/desktop/code/host-resource-policy.ts +55 -0
- package/apps/desktop/code/plan-contracts.ts +159 -0
- package/apps/desktop/code/planning.ts +361 -0
- package/apps/desktop/code/public.ts +134 -0
- package/apps/desktop/code/reference-projection.ts +62 -0
- package/apps/desktop/code/run-contracts.ts +466 -0
- package/apps/desktop/code/run-store.ts +2478 -0
- package/apps/desktop/code/runtime-identity.ts +328 -0
- package/apps/desktop/code/scheduler.ts +3067 -0
- package/apps/desktop/code/semantic-builder.ts +1436 -0
- package/apps/desktop/code/source-bundle.ts +1641 -0
- package/apps/desktop/code/source-typecheck.ts +211 -0
- package/apps/desktop/code/testing.ts +79 -0
- package/apps/desktop/code/worker-client.ts +1675 -0
- package/apps/desktop/code/worker-effects.ts +141 -0
- package/apps/desktop/code/worker-entry.ts +435 -0
- package/apps/desktop/code/worker-lease-guardian.ts +179 -0
- package/apps/desktop/code/worker-process-identity.ts +484 -0
- package/apps/desktop/code/worker-protocol.ts +211 -0
- package/apps/desktop/code/workflow-effects.ts +132 -0
- package/apps/desktop/contracts/analysis.ts +949 -0
- package/apps/desktop/contracts/edit.ts +576 -0
- package/apps/desktop/contracts/index.ts +9 -0
- package/apps/desktop/contracts/media-effects.ts +139 -0
- package/apps/desktop/contracts/project-render.ts +644 -0
- package/apps/desktop/contracts/project.ts +740 -0
- package/apps/desktop/contracts/recording.ts +1434 -0
- package/apps/desktop/contracts/render.ts +334 -0
- package/apps/desktop/contracts/runtime.ts +155 -0
- package/apps/desktop/contracts/spatial-compositor.ts +58 -0
- package/apps/desktop/contracts/spatial-project.ts +159 -0
- package/apps/desktop/contracts/spatial-world.ts +48 -0
- package/apps/desktop/contracts/video-effects.ts +120 -0
- package/apps/desktop/core/alignment-apply.ts +400 -0
- package/apps/desktop/core/audio-alignment.ts +466 -0
- package/apps/desktop/core/auto-zoom.ts +209 -0
- package/apps/desktop/core/canonical-json.ts +14 -0
- package/apps/desktop/core/emoji.ts +210 -0
- package/apps/desktop/core/events.ts +409 -0
- package/apps/desktop/core/face-framing.ts +533 -0
- package/apps/desktop/core/face-tracks.ts +326 -0
- package/apps/desktop/core/inactivity.ts +137 -0
- package/apps/desktop/core/index.ts +30 -0
- package/apps/desktop/core/intervals.ts +123 -0
- package/apps/desktop/core/montage.ts +236 -0
- package/apps/desktop/core/music-analysis.ts +518 -0
- package/apps/desktop/core/plan.ts +244 -0
- package/apps/desktop/core/project-auto-zoom.ts +75 -0
- package/apps/desktop/core/project-camera-spatial.ts +676 -0
- package/apps/desktop/core/project-camera.ts +403 -0
- package/apps/desktop/core/project-captions.ts +888 -0
- package/apps/desktop/core/project-inactivity.ts +125 -0
- package/apps/desktop/core/project-metadata.ts +927 -0
- package/apps/desktop/core/project-plan.ts +375 -0
- package/apps/desktop/core/project-render-plan.ts +303 -0
- package/apps/desktop/core/project-time.ts +469 -0
- package/apps/desktop/core/render-plan.ts +442 -0
- package/apps/desktop/core/scene-cache.ts +174 -0
- package/apps/desktop/core/scene-sampling.ts +448 -0
- package/apps/desktop/core/spatial-compositor.ts +22 -0
- package/apps/desktop/core/spatial-durability.ts +99 -0
- package/apps/desktop/core/spatial-project.ts +169 -0
- package/apps/desktop/core/spatial-shot-clock.ts +32 -0
- package/apps/desktop/core/speech-analysis.ts +333 -0
- package/apps/desktop/core/storage.ts +970 -0
- package/apps/desktop/core/time-map.ts +91 -0
- package/apps/desktop/core/time.ts +61 -0
- package/apps/desktop/core/video-effects.ts +660 -0
- package/apps/desktop/dist/cli/NebulaSans-Bold-26se8aek.otf +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Book-5ax05zvn.woff2 +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Book-8cenzchw.otf +0 -0
- package/apps/desktop/dist/cli/main.js +5086 -0
- package/apps/desktop/html-overlay/catalog.ts +171 -0
- package/apps/desktop/html-overlay/contracts.ts +372 -0
- package/apps/desktop/html-overlay/execution-profile.ts +77 -0
- package/apps/desktop/html-overlay/index.ts +98 -0
- package/apps/desktop/html-overlay/libraries.ts +263 -0
- package/apps/desktop/html-overlay/random.ts +49 -0
- package/apps/desktop/html-overlay/runtime.ts +813 -0
- package/apps/desktop/html-overlay/scaffolds.ts +792 -0
- package/apps/desktop/html-overlay/spark-runtime.ts +88 -0
- package/apps/desktop/html-overlay/spatial-splat-runtime.ts +60 -0
- package/apps/desktop/html-overlay/spatial.ts +720 -0
- package/apps/desktop/studio/assets/README.md +39 -0
- package/apps/desktop/studio/assets/contracts.ts +129 -0
- package/apps/desktop/studio/assets/network.ts +64 -0
- package/apps/desktop/studio/assets/poly-haven.ts +148 -0
- package/apps/desktop/studio/drivers/blender_driver.py +466 -0
- package/apps/desktop/studio/drivers/cadquery_driver.py +176 -0
- package/apps/desktop/studio/education/driver.py +308 -0
- package/apps/desktop/studio/education/lesson.py +180 -0
- package/apps/desktop/studio/education/toolkit.py +249 -0
- package/apps/desktop/workflows/chaptered-demo.ts +48 -0
- package/apps/desktop/workflows/creative-iteration.ts +107 -0
- package/apps/desktop/workflows/creative-selection.ts +92 -0
- package/apps/desktop/workflows/fragments.ts +486 -0
- package/apps/desktop/workflows/index.ts +93 -0
- package/apps/desktop/workflows/polished-screen-demo.ts +128 -0
- package/apps/desktop/workflows/scene-direction.ts +12 -0
- package/apps/desktop/workflows/social-variants.ts +75 -0
- package/apps/desktop/workflows/talking-head-cleanup.ts +53 -0
- package/dist/NebulaSans-Bold-26se8aek.otf +0 -0
- package/dist/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
- package/dist/NebulaSans-Book-5ax05zvn.woff2 +0 -0
- package/dist/NebulaSans-Book-8cenzchw.otf +0 -0
- package/dist/cli.js +479 -0
- package/dist/code/advanced.js +270 -0
- package/dist/code/index.js +2240 -0
- package/dist/generate.js +24 -0
- package/dist/host-resources.js +26 -0
- package/dist/index-42zsesc1.js +2257 -0
- package/dist/index-7308egqr.js +48 -0
- package/dist/index-8txs6fkn.js +712 -0
- package/dist/index-mcy8z0br.js +192 -0
- package/dist/index-p63wavx0.js +2029 -0
- package/dist/index-r7gdhmsp.js +424 -0
- package/dist/index-sh6xbav6.js +1219 -0
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index-z7239b4h.js +2363 -0
- package/dist/index-zfeg9hmw.js +2013 -0
- package/dist/index.js +241 -0
- package/dist/operations.js +29 -0
- package/dist/skill-install-dh0ntqba.js +10 -0
- package/dist/vectorize/worker.js +142 -0
- package/dist/workflow.js +16 -0
- package/docs/README.md +38 -0
- package/docs/studio.md +215 -0
- package/examples/studio/blender/README.md +26 -0
- package/examples/studio/blender/character.py +152 -0
- package/examples/studio/blender/cloth.py +101 -0
- package/examples/studio/blender/color_chart.py +29 -0
- package/examples/studio/blender/fluid.py +78 -0
- package/examples/studio/blender/import_model.py +74 -0
- package/examples/studio/blender/product.py +53 -0
- package/examples/studio/blender/shaded_street.py +481 -0
- package/examples/studio/blender/studio_scene.py +147 -0
- package/examples/studio/cadquery/README.md +13 -0
- package/examples/studio/cadquery/bracket.py +36 -0
- package/examples/studio/cadquery/import_step.py +12 -0
- package/examples/studio/education/README.md +46 -0
- package/examples/studio/education/lesson.json +99 -0
- package/examples/studio/education/scene.py +7 -0
- package/examples/studio/hybrid-scene.ts +96 -0
- package/examples/studio/native-workflow.ts +14 -0
- package/examples/studio/vgpu/README.md +64 -0
- package/examples/studio/vgpu/heat-field.mjs +290 -0
- package/package.json +304 -0
- package/packages/scene/src/index.ts +1 -0
- package/packages/scene/src/scene.ts +215 -0
- package/schema/diagram.schema.json +334 -0
- package/skills/slopcamera/SKILL.md +45 -0
- package/skills/slopcamera/agents/openai.yaml +4 -0
- package/skills/slopcamera/references/customization.md +77 -0
- package/skills/slopcamera/references/diagrams.md +153 -0
- package/skills/slopcamera/references/directed-scenes.md +42 -0
- package/skills/slopcamera/references/directing-video.md +18 -0
- package/skills/slopcamera/references/educational-video.md +26 -0
- package/skills/slopcamera/references/gateway-media.md +134 -0
- package/skills/slopcamera/references/install.md +62 -0
- package/skills/slopcamera/references/native-studio.md +31 -0
- package/skills/slopcamera/references/reference-led-3d.md +140 -0
- package/skills/slopcamera/references/rubber-stamp-examples/poster-example-1.jpg +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/poster-example-2.jpg +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-1.png +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-2.png +0 -0
- package/skills/slopcamera/references/rubber-stamp-field-notes.md +144 -0
- package/skills/slopcamera/references/vectorization.md +29 -0
- package/skills/slopcamera/references/video-projects.md +187 -0
- package/skills/slopcamera/references/visual-communication.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +45 -0
- package/skills/slopcamera/scripts/compose-rubber-stamp-field-note.ts +229 -0
- package/src/artifacts.ts +106 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +21 -0
- package/src/assets.d.ts +9 -0
- package/src/cli.ts +601 -0
- package/src/cloud-errors.ts +26 -0
- package/src/code/advanced.ts +10 -0
- package/src/code/boundary.ts +21 -0
- package/src/code/canonical-json.ts +129 -0
- package/src/code/compiler.ts +953 -0
- package/src/code/contracts.ts +576 -0
- package/src/code/define-workflow.ts +334 -0
- package/src/code/errors.ts +41 -0
- package/src/code/graph-builder.ts +945 -0
- package/src/code/index.ts +121 -0
- package/src/code/json-snapshot.ts +899 -0
- package/src/code/json-utf8.ts +88 -0
- package/src/code/portable-builder.ts +136 -0
- package/src/code/projection.ts +378 -0
- package/src/code/public-operations.ts +450 -0
- package/src/code/runtime.ts +580 -0
- package/src/code/sha256.ts +36 -0
- package/src/config.ts +180 -0
- package/src/desktop.ts +253 -0
- package/src/fs.ts +10 -0
- package/src/generate.ts +704 -0
- package/src/host-resource-posix.ts +161 -0
- package/src/host-resources.ts +1749 -0
- package/src/icons.ts +58 -0
- package/src/index.ts +200 -0
- package/src/label-layout.ts +130 -0
- package/src/layout.ts +413 -0
- package/src/lint.ts +127 -0
- package/src/mcp/boundary.ts +354 -0
- package/src/mcp/index.ts +33 -0
- package/src/mcp/server.ts +272 -0
- package/src/mcp/tools.ts +963 -0
- package/src/mcp/types.ts +71 -0
- package/src/operations.ts +775 -0
- package/src/parse.ts +861 -0
- package/src/process-environment.ts +17 -0
- package/src/render.ts +437 -0
- package/src/skill-install.ts +49 -0
- package/src/spatial-scene/authoring.ts +27 -0
- package/src/spatial-scene/camera-track.ts +83 -0
- package/src/spatial-scene/contracts.ts +277 -0
- package/src/spatial-scene/evaluate.ts +145 -0
- package/src/spatial-scene/gltf.ts +558 -0
- package/src/spatial-scene/identity.ts +260 -0
- package/src/spatial-scene/index.ts +10 -0
- package/src/spatial-scene/inspect.ts +68 -0
- package/src/spatial-scene/math.ts +280 -0
- package/src/spatial-scene/patch.ts +131 -0
- package/src/spatial-scene/time.ts +53 -0
- package/src/studio/contracts.ts +129 -0
- package/src/studio/index.ts +2 -0
- package/src/studio/plan.ts +156 -0
- package/src/studio/shared.ts +41 -0
- package/src/theme.ts +68 -0
- package/src/tldr.ts +474 -0
- package/src/types.ts +212 -0
- package/src/vectorize/archive.ts +101 -0
- package/src/vectorize/command.ts +797 -0
- package/src/vectorize/index.ts +27 -0
- package/src/vectorize/limits.ts +61 -0
- package/src/vectorize/metrics.ts +351 -0
- package/src/vectorize/pixels.ts +323 -0
- package/src/vectorize/supervisor.ts +339 -0
- package/src/vectorize/svg.ts +188 -0
- package/src/vectorize/tool.ts +542 -0
- package/src/vectorize/types.ts +113 -0
- package/src/vectorize/vectorize.ts +634 -0
- package/src/vectorize/worker-protocol.ts +40 -0
- package/src/vectorize/worker.ts +209 -0
- package/src/version.ts +1 -0
- package/src/workflow.ts +409 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, mkdtemp, open, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { Resvg } from "@resvg/resvg-js";
|
|
7
|
+
import sharp from "sharp";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { createBoundedJsonSnapshot } from "../../../src/code/json-snapshot";
|
|
11
|
+
import { parseDiagramSpec } from "../../../src/parse";
|
|
12
|
+
import { renderSvg } from "../../../src/render";
|
|
13
|
+
import { builtInIcons } from "../../../src/icons";
|
|
14
|
+
import {
|
|
15
|
+
EvaluatedSpatialSceneSchema, type EvaluatedSpatialScene, type SpatialAssetManifest, type SpatialEntity,
|
|
16
|
+
} from "../../../src/spatial-scene/contracts";
|
|
17
|
+
import { spatialAssetClosureDigests } from "../../../src/spatial-scene/identity";
|
|
18
|
+
import { evaluateSpatialGlb, parseSpatialGlb, type SpatialGlbModel } from "../../../src/spatial-scene/gltf";
|
|
19
|
+
import { canonicalJson, canonicalJsonSha256 } from "../core/canonical-json";
|
|
20
|
+
import { SpatialWorldImportManifestSchema, SPATIAL_SPLAT_LIMITS } from "../contracts/spatial-world";
|
|
21
|
+
import { inspectSpatialSpz } from "./spatial-spz";
|
|
22
|
+
import { WorldLabsProvenanceSchema } from "./spatial-world-provenance";
|
|
23
|
+
import {
|
|
24
|
+
PreparedSpatialAssetSchema, SPATIAL_OVERLAY_LIMITS, SpatialOverlayCapabilityError, spatialTextRasterContentSha256,
|
|
25
|
+
spatialGeometryContentSha256, spatialVideoRasterContentSha256,
|
|
26
|
+
type PreparedSpatialAsset,
|
|
27
|
+
} from "../html-overlay/spatial";
|
|
28
|
+
import type { ApplicationProcessRunner } from "./context";
|
|
29
|
+
import type { BoundHtmlOverlayResource } from "./html-overlay-renderer";
|
|
30
|
+
|
|
31
|
+
export const SPATIAL_ASSET_PREPARATION_LIMITS = Object.freeze({
|
|
32
|
+
sourceBytes: 256 * 1024 * 1024,
|
|
33
|
+
outputBytes: 256 * 1024 * 1024,
|
|
34
|
+
rasterPixels: 33_554_432,
|
|
35
|
+
dimension: 8_192,
|
|
36
|
+
fontBytes: 16 * 1024 * 1024,
|
|
37
|
+
svgBytes: 1_048_576,
|
|
38
|
+
probeBytes: 8 * 1024 * 1024,
|
|
39
|
+
videoFrames: 216_000,
|
|
40
|
+
nativeTimeoutMs: 120_000,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export interface SpatialVerifiedAsset {
|
|
44
|
+
readonly manifest: SpatialAssetManifest;
|
|
45
|
+
readonly manifestSha256: string;
|
|
46
|
+
/** Verified immutable in-memory copy. Native decoders receive its private snapshot path. */
|
|
47
|
+
readonly bytes: Uint8Array;
|
|
48
|
+
}
|
|
49
|
+
export interface SpatialGeometryPreparationRequest {
|
|
50
|
+
readonly asset: SpatialVerifiedAsset;
|
|
51
|
+
readonly dependencies: readonly SpatialVerifiedAsset[];
|
|
52
|
+
readonly entity: Extract<SpatialEntity, { kind: "mesh" }>;
|
|
53
|
+
readonly timeUs: number;
|
|
54
|
+
}
|
|
55
|
+
export interface SpatialGeometryPreparationResult {
|
|
56
|
+
readonly geometry: Extract<PreparedSpatialAsset, { kind: "geometry" }>;
|
|
57
|
+
/** Exact resource names referenced by geometry textures; bytes are independently decoded below. */
|
|
58
|
+
readonly images: readonly { readonly resourceName: string; readonly bytes: Uint8Array; readonly opaque: boolean }[];
|
|
59
|
+
readonly profile: string;
|
|
60
|
+
}
|
|
61
|
+
export interface SpatialAssetPreparationPorts {
|
|
62
|
+
readonly runner: ApplicationProcessRunner;
|
|
63
|
+
readonly ffmpegCommand?: string;
|
|
64
|
+
readonly ffprobeCommand?: string;
|
|
65
|
+
readonly prepareGeometry?: (request: SpatialGeometryPreparationRequest, signal: AbortSignal) => Promise<SpatialGeometryPreparationResult>;
|
|
66
|
+
}
|
|
67
|
+
export interface SpatialAssetPreparationInput {
|
|
68
|
+
readonly snapshots: readonly EvaluatedSpatialScene[];
|
|
69
|
+
readonly exactSceneTimesUs?: readonly { readonly numerator: string; readonly denominator: string }[];
|
|
70
|
+
/** Adapter-owned contained source root and private temporary workspace parent. */
|
|
71
|
+
readonly assetRoot: string;
|
|
72
|
+
readonly workspaceParent: string;
|
|
73
|
+
}
|
|
74
|
+
export interface PreparedSpatialAssets {
|
|
75
|
+
readonly preparedAssets: readonly PreparedSpatialAsset[];
|
|
76
|
+
readonly resources: readonly BoundHtmlOverlayResource[];
|
|
77
|
+
/** Private verified source copies, valid only while the consumer callback is active. */
|
|
78
|
+
readonly sources: readonly { readonly manifest: SpatialAssetManifest; readonly manifestSha256: string; readonly absolutePath: string }[];
|
|
79
|
+
readonly receipt: Readonly<{
|
|
80
|
+
readonly kind: "slopcamera.spatial-asset-preparation";
|
|
81
|
+
readonly schemaVersion: 1;
|
|
82
|
+
readonly sourceManifests: Readonly<Record<string, string>>;
|
|
83
|
+
readonly preparedSha256: string;
|
|
84
|
+
readonly sourceBytes: number;
|
|
85
|
+
readonly outputBytes: number;
|
|
86
|
+
readonly profiles: readonly string[];
|
|
87
|
+
}>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const inputSchema = z.strictObject({
|
|
91
|
+
snapshots: z.array(EvaluatedSpatialSceneSchema).min(1).max(SPATIAL_OVERLAY_LIMITS.frames),
|
|
92
|
+
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(),
|
|
93
|
+
assetRoot: z.string().min(1).max(4_096), workspaceParent: z.string().min(1).max(4_096),
|
|
94
|
+
});
|
|
95
|
+
const digest = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
|
|
96
|
+
function capability(name: string, message: string): never { throw new SpatialOverlayCapabilityError(name, message); }
|
|
97
|
+
function aborted(signal: AbortSignal): void { if (signal.aborted) throw signal.reason instanceof Error ? signal.reason : new Error("Spatial asset preparation was cancelled."); }
|
|
98
|
+
function utf8(bytes: Uint8Array): string { return new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
|
|
99
|
+
function xml(text: string): string { return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); }
|
|
100
|
+
|
|
101
|
+
async function physicalRoot(path: string): Promise<string> {
|
|
102
|
+
if (!isAbsolute(path)) throw new RangeError("Spatial asset roots must be absolute adapter-owned paths.");
|
|
103
|
+
const resolved = await realpath(path);
|
|
104
|
+
if (resolved !== path || !(await lstat(path)).isDirectory()) throw new RangeError("Spatial asset roots must be physical directories without symlinks.");
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readVerifiedAsset(root: string, manifest: SpatialAssetManifest, signal: AbortSignal): Promise<Uint8Array> {
|
|
109
|
+
const path = join(root, manifest.payload.path);
|
|
110
|
+
if (relative(root, path).startsWith(`..${sep}`) || relative(root, path) === "..") throw new RangeError("Asset escaped its source root.");
|
|
111
|
+
const chain: { path: string; dev: number; ino: number }[] = [];
|
|
112
|
+
let current = root;
|
|
113
|
+
for (const part of ["", ...manifest.payload.path.split("/").slice(0, -1)]) {
|
|
114
|
+
if (part !== "") current = join(current, part);
|
|
115
|
+
const stat = await lstat(current);
|
|
116
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new RangeError("Asset parent directories must be physical.");
|
|
117
|
+
chain.push({ path: current, dev: stat.dev, ino: stat.ino });
|
|
118
|
+
}
|
|
119
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
120
|
+
try {
|
|
121
|
+
aborted(signal);
|
|
122
|
+
const before = await handle.stat();
|
|
123
|
+
if (!before.isFile() || before.size !== manifest.payload.bytes) throw new RangeError("Asset payload size or file kind differs from its immutable manifest.");
|
|
124
|
+
const bytes = new Uint8Array(before.size);
|
|
125
|
+
let offset = 0;
|
|
126
|
+
while (offset < bytes.length) {
|
|
127
|
+
aborted(signal);
|
|
128
|
+
const result = await handle.read(bytes, offset, Math.min(1024 * 1024, bytes.length - offset), offset);
|
|
129
|
+
if (result.bytesRead === 0) throw new RangeError("Asset changed during snapshot capture.");
|
|
130
|
+
offset += result.bytesRead;
|
|
131
|
+
}
|
|
132
|
+
const after = await handle.stat();
|
|
133
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
|
|
134
|
+
|| await realpath(path) !== path || digest(bytes) !== manifest.payload.sha256) throw new RangeError("Asset changed or does not match its immutable payload digest.");
|
|
135
|
+
for (const parent of chain) {
|
|
136
|
+
const stat = await lstat(parent.path);
|
|
137
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== parent.dev || stat.ino !== parent.ino) throw new RangeError("Asset directory identity changed during capture.");
|
|
138
|
+
}
|
|
139
|
+
return bytes;
|
|
140
|
+
} finally { await handle.close(); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Shape-only SVG is rebuilt from an explicit vocabulary before native decoding. */
|
|
144
|
+
export function inertSpatialSvg(source: string): string {
|
|
145
|
+
if (new TextEncoder().encode(source).length > SPATIAL_ASSET_PREPARATION_LIMITS.svgBytes || /[&\u0000]|<!|<\?/u.test(source)) {
|
|
146
|
+
capability("inert-svg", "SVG must be bounded shape-only markup without entities, declarations, or processing instructions.");
|
|
147
|
+
}
|
|
148
|
+
const tags = new Set(["svg", "g", "path", "rect", "circle", "ellipse", "line", "polyline", "polygon"]);
|
|
149
|
+
const numbers = new Set(["width", "height", "x", "y", "x1", "y1", "x2", "y2", "cx", "cy", "r", "rx", "ry", "stroke-width", "stroke-miterlimit", "opacity", "fill-opacity", "stroke-opacity"]);
|
|
150
|
+
const stack: string[] = [];
|
|
151
|
+
let index = 0, count = 0, roots = 0;
|
|
152
|
+
const output: string[] = [];
|
|
153
|
+
const tokens = /<[^>]*>/gu;
|
|
154
|
+
for (const match of source.matchAll(tokens)) {
|
|
155
|
+
if (source.slice(index, match.index).trim() !== "") capability("inert-svg", "SVG text content requires a declared-font adapter.");
|
|
156
|
+
index = match.index + match[0].length;
|
|
157
|
+
if (++count > 20_000) throw new RangeError("SVG exceeds the element budget.");
|
|
158
|
+
const token = /^<(\/?)([A-Za-z]+)([\s\S]*?)(\/?)>$/u.exec(match[0]);
|
|
159
|
+
if (token === null || !tags.has(token[2]!)) capability("inert-svg", "SVG contains an unsupported element.");
|
|
160
|
+
const closing = token[1] === "/", name = token[2]!, tail = token[3]!, selfClosing = token[4] === "/";
|
|
161
|
+
if (closing) {
|
|
162
|
+
if (tail.trim() !== "" || selfClosing || stack.pop() !== name) throw new RangeError("SVG has mismatched closing elements.");
|
|
163
|
+
output.push(`</${name}>`); continue;
|
|
164
|
+
}
|
|
165
|
+
if (stack.length === 0 && (name !== "svg" || ++roots > 1)) throw new RangeError("SVG requires exactly one root element.");
|
|
166
|
+
if (stack.length >= 32 || (name === "svg" && stack.length !== 0)) throw new RangeError("Nested SVG or excessive nesting is unsupported.");
|
|
167
|
+
const attributes: string[] = [], seen = new Set<string>();
|
|
168
|
+
let consumed = 0;
|
|
169
|
+
for (const attribute of tail.matchAll(/\s+([A-Za-z][A-Za-z0-9-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/gu)) {
|
|
170
|
+
if (tail.slice(consumed, attribute.index).trim() !== "") throw new RangeError("SVG attributes must be explicitly quoted.");
|
|
171
|
+
consumed = attribute.index + attribute[0].length;
|
|
172
|
+
const key = attribute[1]!, value = attribute[2] ?? attribute[3]!;
|
|
173
|
+
if (seen.has(key) || /[<>]/u.test(value) || value.length > 100_000) throw new RangeError("SVG attributes must be unique and bounded.");
|
|
174
|
+
seen.add(key);
|
|
175
|
+
let allowed = false;
|
|
176
|
+
if (numbers.has(key)) allowed = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/u.test(value) && Number.isFinite(Number(value)) && Math.abs(Number(value)) <= 1e6;
|
|
177
|
+
else if (key === "xmlns") allowed = name === "svg" && value === "http://www.w3.org/2000/svg";
|
|
178
|
+
else if (key === "viewBox") allowed = /^[-+0-9.eE,\s]+$/u.test(value) && value.trim().split(/[\s,]+/u).length === 4 && value.trim().split(/[\s,]+/u).every(part => Number.isFinite(Number(part)) && Math.abs(Number(part)) <= 1e6);
|
|
179
|
+
else if (key === "d") allowed = name === "path" && /^[MmZzLlHhVvCcSsQqTtAa0-9eE+,.\s-]+$/u.test(value);
|
|
180
|
+
else if (key === "points") allowed = /^[0-9eE+,.\s-]+$/u.test(value);
|
|
181
|
+
else if (key === "transform") allowed = /^(?:(?:matrix|translate|scale|rotate|skewX|skewY)\([-+0-9.eE,\s]+\)\s*)+$/u.test(value);
|
|
182
|
+
else if (key === "fill" || key === "stroke") allowed = /^(?:none|#[a-fA-F0-9]{3}|#[a-fA-F0-9]{6}|#[a-fA-F0-9]{8})$/u.test(value);
|
|
183
|
+
else if (key === "fill-rule") allowed = value === "evenodd" || value === "nonzero";
|
|
184
|
+
else if (key === "stroke-linecap") allowed = ["butt", "round", "square"].includes(value);
|
|
185
|
+
else if (key === "stroke-linejoin") allowed = ["miter", "round", "bevel"].includes(value);
|
|
186
|
+
if (allowed && (numbers.has(key) || key === "viewBox" || key === "d" || key === "points" || key === "transform")) {
|
|
187
|
+
for (const numeric of value.matchAll(/[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?/gu)) {
|
|
188
|
+
if (!Number.isFinite(Number(numeric[0])) || Math.abs(Number(numeric[0])) > 1e6) allowed = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!allowed) capability("inert-svg", `SVG attribute ${key} is outside the shape-only profile.`);
|
|
192
|
+
attributes.push(`${key}="${value}"`);
|
|
193
|
+
}
|
|
194
|
+
if (tail.slice(consumed).trim() !== "") throw new RangeError("SVG has unparsed attribute content.");
|
|
195
|
+
output.push(`<${name}${attributes.length === 0 ? "" : ` ${attributes.join(" ")}`}${selfClosing ? "/" : ""}>`);
|
|
196
|
+
if (!selfClosing) stack.push(name);
|
|
197
|
+
}
|
|
198
|
+
if (source.slice(index).trim() !== "" || stack.length !== 0 || roots !== 1) throw new RangeError("SVG is incomplete or has non-element content.");
|
|
199
|
+
return output.join("");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function dimensions(width: number, height: number): void {
|
|
203
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > 8_192 || height > 8_192 || width * height > SPATIAL_ASSET_PREPARATION_LIMITS.rasterPixels) {
|
|
204
|
+
throw new RangeError("Prepared raster exceeds its dimension or pixel budget.");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function raster(bytes: Uint8Array, opaque = false): Promise<{ bytes: Uint8Array; width: number; height: number; alpha: "opaque" | "straight" }> {
|
|
208
|
+
const image = sharp(bytes, { limitInputPixels: SPATIAL_ASSET_PREPARATION_LIMITS.rasterPixels, failOn: "warning" });
|
|
209
|
+
const metadata = await image.metadata();
|
|
210
|
+
if ((metadata.format !== "png" && metadata.format !== "jpeg") || metadata.pages !== undefined && metadata.pages !== 1
|
|
211
|
+
|| metadata.orientation !== undefined && metadata.orientation !== 1 || metadata.depth !== "uchar" || metadata.icc !== undefined) {
|
|
212
|
+
capability("sdr-raster", "Raster inputs require single-frame unrotated 8-bit PNG/JPEG without unqualified ICC profiles.");
|
|
213
|
+
}
|
|
214
|
+
if (metadata.width === undefined || metadata.height === undefined) throw new RangeError("Raster has no dimensions.");
|
|
215
|
+
dimensions(metadata.width, metadata.height);
|
|
216
|
+
const pipeline = image.toColourspace("srgb");
|
|
217
|
+
const result = await (opaque ? pipeline.removeAlpha() : pipeline).png().toBuffer({ resolveWithObject: true });
|
|
218
|
+
return { bytes: result.data, width: result.info.width, height: result.info.height, alpha: opaque || !metadata.hasAlpha ? "opaque" : "straight" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface FontInfo { readonly family: string; readonly hasGlyph: (codepoint: number) => boolean }
|
|
222
|
+
/** Bounded OpenType cmap coverage qualification. No font discovery or fallback. */
|
|
223
|
+
export function spatialOpenTypeFont(bytes: Uint8Array, declaredFamily: string): FontInfo {
|
|
224
|
+
if (bytes.length < 12 || bytes.length > SPATIAL_ASSET_PREPARATION_LIMITS.fontBytes) throw new RangeError("OpenType font size is invalid.");
|
|
225
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
226
|
+
const u16 = (at: number) => { if (at < 0 || at + 2 > bytes.length) throw new RangeError("Font table is truncated."); return view.getUint16(at); };
|
|
227
|
+
const u32 = (at: number) => { if (at < 0 || at + 4 > bytes.length) throw new RangeError("Font table is truncated."); return view.getUint32(at); };
|
|
228
|
+
if (u32(0) !== 0x4f54544f && u32(0) !== 0x00010000) capability("font-format", "Only standalone OpenType/TrueType sfnt data is qualified; WOFF2 requires a separate decoder.");
|
|
229
|
+
const count = u16(4);
|
|
230
|
+
if (count < 1 || count > 128 || 12 + count * 16 > bytes.length) throw new RangeError("Font table directory is invalid.");
|
|
231
|
+
const tables = new Map<string, { offset: number; length: number }>();
|
|
232
|
+
for (let i = 0; i < count; i++) {
|
|
233
|
+
const at = 12 + i * 16, name = String.fromCharCode(...bytes.subarray(at, at + 4));
|
|
234
|
+
const offset = u32(at + 8), length = u32(at + 12);
|
|
235
|
+
if (offset + length > bytes.length || tables.has(name)) throw new RangeError("Font table extent or identity is invalid.");
|
|
236
|
+
tables.set(name, { offset, length });
|
|
237
|
+
}
|
|
238
|
+
const name = tables.get("name"), cmap = tables.get("cmap"), maxp = tables.get("maxp");
|
|
239
|
+
if (name === undefined || cmap === undefined || maxp === undefined || maxp.length < 6) throw new RangeError("Font requires name, maxp, and Unicode cmap tables.");
|
|
240
|
+
const glyphCount = u16(maxp.offset + 4);
|
|
241
|
+
const nameCount = u16(name.offset + 2), storage = name.offset + u16(name.offset + 4), families = new Set<string>();
|
|
242
|
+
if (nameCount > 1_024 || 6 + nameCount * 12 > name.length) throw new RangeError("Font names exceed their table bounds.");
|
|
243
|
+
for (let i = 0; i < nameCount; i++) {
|
|
244
|
+
const at = name.offset + 6 + i * 12, platform = u16(at), id = u16(at + 6), length = u16(at + 8), start = storage + u16(at + 10);
|
|
245
|
+
if (start + length > name.offset + name.length) throw new RangeError("Font name exceeds its table bounds.");
|
|
246
|
+
if ((id === 1 || id === 16) && (platform === 0 || platform === 3) && length % 2 === 0) {
|
|
247
|
+
let family = "";
|
|
248
|
+
for (let offset = start; offset < start + length; offset += 2) family += String.fromCharCode(u16(offset));
|
|
249
|
+
families.add(family);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (!families.has(declaredFamily)) throw new RangeError("Declared font family does not match the exact OpenType family.");
|
|
253
|
+
const cmapCount = u16(cmap.offset + 2), subtables: { at: number; length: number; format: number }[] = [];
|
|
254
|
+
if (cmapCount > 256 || 4 + cmapCount * 8 > cmap.length) throw new RangeError("Font cmap directory exceeds its bounds.");
|
|
255
|
+
for (let i = 0; i < cmapCount; i++) {
|
|
256
|
+
const entry = cmap.offset + 4 + i * 8, platform = u16(entry), encoding = u16(entry + 2), at = cmap.offset + u32(entry + 4);
|
|
257
|
+
if (platform !== 0 && !(platform === 3 && (encoding === 1 || encoding === 10))) continue;
|
|
258
|
+
const format = u16(at);
|
|
259
|
+
if (format !== 4 && format !== 12) continue;
|
|
260
|
+
const length = format === 4 ? u16(at + 2) : u32(at + 4);
|
|
261
|
+
if (at < cmap.offset || at + length > cmap.offset + cmap.length || length < (format === 4 ? 16 : 16)) throw new RangeError("Unicode cmap exceeds its table bounds.");
|
|
262
|
+
if (format === 4 && (u16(at + 6) % 2 !== 0 || 16 + 8 * (u16(at + 6) / 2) > length)) throw new RangeError("cmap format 4 segments exceed their table.");
|
|
263
|
+
if (format === 12 && (u32(at + 12) > 100_000 || 16 + u32(at + 12) * 12 > length)) throw new RangeError("cmap format 12 groups exceed their table.");
|
|
264
|
+
const segments = format === 12 ? u32(at + 12) : u16(at + 6) / 2;
|
|
265
|
+
let previous = -1;
|
|
266
|
+
for (let i = 0; i < segments; i++) {
|
|
267
|
+
const start = format === 12 ? u32(at + 16 + i * 12) : u16(at + 16 + segments * 2 + i * 2);
|
|
268
|
+
const end = format === 12 ? u32(at + 20 + i * 12) : u16(at + 14 + i * 2);
|
|
269
|
+
if (start > end || start <= previous || end > 0x10ffff) throw new RangeError("Unicode cmap ranges must be ordered and disjoint.");
|
|
270
|
+
previous = end;
|
|
271
|
+
}
|
|
272
|
+
subtables.push({ at, length, format });
|
|
273
|
+
}
|
|
274
|
+
if (subtables.length === 0) capability("font-cmap", "Font requires a qualified Unicode cmap format 4 or 12.");
|
|
275
|
+
return Object.freeze({ family: declaredFamily, hasGlyph: (codepoint: number): boolean => {
|
|
276
|
+
if (!Number.isInteger(codepoint) || codepoint < 0 || codepoint > 0x10ffff) return false;
|
|
277
|
+
for (const table of subtables) {
|
|
278
|
+
const at = table.at;
|
|
279
|
+
if (table.format === 12) {
|
|
280
|
+
const groups = u32(at + 12);
|
|
281
|
+
let low = 0, high = groups - 1;
|
|
282
|
+
while (low <= high) {
|
|
283
|
+
const i = Math.floor((low + high) / 2);
|
|
284
|
+
const group = at + 16 + i * 12, start = u32(group), end = u32(group + 4);
|
|
285
|
+
if (codepoint < start) high = i - 1;
|
|
286
|
+
else if (codepoint > end) low = i + 1;
|
|
287
|
+
else { const glyph = u32(group + 8) + codepoint - start; if (glyph > 0 && glyph < glyphCount) return true; break; }
|
|
288
|
+
}
|
|
289
|
+
} else if (codepoint <= 0xffff) {
|
|
290
|
+
const segments = u16(at + 6) / 2, ends = at + 14, starts = ends + segments * 2 + 2, deltas = starts + segments * 2, offsets = deltas + segments * 2;
|
|
291
|
+
let low = 0, high = segments - 1;
|
|
292
|
+
while (low <= high) {
|
|
293
|
+
const i = Math.floor((low + high) / 2);
|
|
294
|
+
const start = u16(starts + i * 2), end = u16(ends + i * 2);
|
|
295
|
+
if (codepoint < start) { high = i - 1; continue; }
|
|
296
|
+
if (codepoint > end) { low = i + 1; continue; }
|
|
297
|
+
const delta = u16(deltas + i * 2), offset = u16(offsets + i * 2);
|
|
298
|
+
if (offset === 0) { const glyph = (codepoint + delta) % 65_536; if (glyph > 0 && glyph < glyphCount) return true; }
|
|
299
|
+
else {
|
|
300
|
+
const glyphAt = offsets + i * 2 + offset + (codepoint - start) * 2;
|
|
301
|
+
if (glyphAt + 2 > at + table.length) throw new RangeError("Font glyph lookup escapes cmap table.");
|
|
302
|
+
const glyph = u16(glyphAt);
|
|
303
|
+
if (glyph !== 0 && (glyph + delta) % 65_536 > 0 && (glyph + delta) % 65_536 < glyphCount) return true;
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
} });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function glyphCoverage(text: string, fonts: readonly FontInfo[]): void {
|
|
314
|
+
for (const character of text) {
|
|
315
|
+
if (character === "\n" || character === "\r" || character === "\t") continue;
|
|
316
|
+
const point = character.codePointAt(0)!;
|
|
317
|
+
if (point < 32 || !fonts.some(font => font.hasGlyph(point))) capability("font-glyph", `Declared fonts do not cover codepoint U+${point.toString(16).toUpperCase()}.`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const probeSchema = z.object({
|
|
322
|
+
streams: z.array(z.object({ width: z.number().int(), height: z.number().int(), pix_fmt: z.string(), avg_frame_rate: z.string(), time_base: z.string().max(40),
|
|
323
|
+
color_transfer: z.string().optional(), color_primaries: z.string().optional(), color_space: z.string().optional() })).length(1),
|
|
324
|
+
frames: z.array(z.object({ best_effort_timestamp: z.number().int().safe().min(0), best_effort_timestamp_time: z.string(), duration: z.number().int().safe().min(1).optional(), pkt_duration: z.number().int().safe().min(1).optional() })).min(1).max(SPATIAL_ASSET_PREPARATION_LIMITS.videoFrames),
|
|
325
|
+
});
|
|
326
|
+
function decimalUs(value: string): number {
|
|
327
|
+
if (!/^-?\d+(?:\.\d{1,9})?$/u.test(value)) throw new RangeError("Video frame timestamp is not a bounded decimal.");
|
|
328
|
+
const negative = value.startsWith("-"), [whole, fractional = ""] = (negative ? value.slice(1) : value).split(".");
|
|
329
|
+
const nanos = BigInt(whole!) * 1_000_000_000n + BigInt(fractional.padEnd(9, "0"));
|
|
330
|
+
const result = Number((nanos + 500n) / 1_000n) * (negative ? -1 : 1);
|
|
331
|
+
if (!Number.isSafeInteger(result)) throw new RangeError("Video timestamp exceeds the integer clock.");
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
export function spatialVideoTimeUs(entity: Extract<SpatialEntity, { kind: "video" }>, sceneTimeUs: number, durationUs: number): number {
|
|
335
|
+
if (!Number.isSafeInteger(sceneTimeUs) || sceneTimeUs < 0 || !Number.isSafeInteger(durationUs) || durationUs < 1) throw new RangeError("Video clocks must be nonnegative integer microseconds.");
|
|
336
|
+
const time = entity.sourceOffsetUs + sceneTimeUs;
|
|
337
|
+
if (entity.playback === "loop") return time % durationUs;
|
|
338
|
+
if (entity.playback === "freeze") return Math.min(time, durationUs - 1);
|
|
339
|
+
if (time >= durationUs) throw new RangeError("Video once playback lies outside its half-open source duration.");
|
|
340
|
+
return time;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Owns every temporary/native input until the consumer and all native calls settle. */
|
|
344
|
+
export async function withPreparedSpatialAssets<Result>(
|
|
345
|
+
input: unknown, ports: SpatialAssetPreparationPorts, signal: AbortSignal,
|
|
346
|
+
consume: (prepared: PreparedSpatialAssets) => Promise<Result>,
|
|
347
|
+
): Promise<Result> {
|
|
348
|
+
const request = inputSchema.parse(createBoundedJsonSnapshot(input, SPATIAL_OVERLAY_LIMITS.requestBytes, "Spatial asset preparation request", { maximumDepth: 48, maximumValues: SPATIAL_OVERLAY_LIMITS.requestValues }).value);
|
|
349
|
+
if (request.exactSceneTimesUs !== undefined && request.exactSceneTimesUs.length !== request.snapshots.length) throw new RangeError("Exact scene times must correspond one-to-one to snapshots.");
|
|
350
|
+
const exactTimes = request.snapshots.map((snapshot, index) => {
|
|
351
|
+
const exact = request.exactSceneTimesUs?.[index] ?? { numerator: String(snapshot.timeUs), denominator: "1" };
|
|
352
|
+
const n = BigInt(exact.numerator), d = BigInt(exact.denominator);
|
|
353
|
+
if (Number((2n * n + d) / (2n * d)) !== snapshot.timeUs) throw new RangeError("Exact scene time must round to its snapshot clock.");
|
|
354
|
+
let a = n, b = d;
|
|
355
|
+
while (b !== 0n) { const remainder = a % b; a = b; b = remainder; }
|
|
356
|
+
return { n: n / a, d: d / a };
|
|
357
|
+
});
|
|
358
|
+
aborted(signal);
|
|
359
|
+
const assetRoot = await physicalRoot(request.assetRoot), parent = await physicalRoot(request.workspaceParent);
|
|
360
|
+
const workspace = await mkdtemp(join(parent, ".slopcamera-spatial-assets-"));
|
|
361
|
+
const verified = new Map<string, SpatialVerifiedAsset>(), paths = new Map<string, string>(), resources = new Map<string, BoundHtmlOverlayResource>();
|
|
362
|
+
const preparedAssets = new Map<string, PreparedSpatialAsset>(), profiles = new Set<string>();
|
|
363
|
+
let sourceBytes = 0, outputBytes = 0;
|
|
364
|
+
const publishGeometry = async (geometry: Extract<PreparedSpatialAsset, { kind: "geometry" }>) => {
|
|
365
|
+
const captured = createBoundedJsonSnapshot(geometry.primitives, SPATIAL_OVERLAY_LIMITS.requestBytes, "Prepared geometry resource", {
|
|
366
|
+
maximumDepth: 48, maximumValues: SPATIAL_OVERLAY_LIMITS.requestValues,
|
|
367
|
+
});
|
|
368
|
+
const bytes = new TextEncoder().encode(canonicalJson(captured.value));
|
|
369
|
+
const sha256 = digest(bytes), name = `geometry-${sha256.slice(0, 40)}`;
|
|
370
|
+
const resource = { name, sha256, bytes: bytes.byteLength, mediaType: "application/json", urlPath: `${name}.json`, transport: "fetch" as const };
|
|
371
|
+
const existing = resources.get(name);
|
|
372
|
+
if (existing !== undefined) {
|
|
373
|
+
if (existing.sha256 !== sha256 || existing.bytes !== bytes.byteLength || existing.mediaType !== resource.mediaType) throw new RangeError("Prepared geometry resource name is bound to different bytes.");
|
|
374
|
+
} else {
|
|
375
|
+
if (resources.size >= 64 || outputBytes + bytes.byteLength > SPATIAL_ASSET_PREPARATION_LIMITS.outputBytes) throw new RangeError("Prepared geometry resources exceed the output budget.");
|
|
376
|
+
const path = join(workspace, `${sha256}.json`);
|
|
377
|
+
aborted(signal);
|
|
378
|
+
await writeFile(path, bytes, { flag: "wx", mode: 0o600 });
|
|
379
|
+
aborted(signal);
|
|
380
|
+
resources.set(name, { ...resource, absolutePath: path }); outputBytes += bytes.byteLength;
|
|
381
|
+
}
|
|
382
|
+
return PreparedSpatialAssetSchema.parse({ ...geometry, resource });
|
|
383
|
+
};
|
|
384
|
+
const publishRaster = async (image: Awaited<ReturnType<typeof raster>>, preferredName?: string) => {
|
|
385
|
+
const sha256 = digest(image.bytes), name = preferredName ?? `image-${sha256.slice(0, 40)}`;
|
|
386
|
+
const existing = resources.get(name);
|
|
387
|
+
if (existing !== undefined) {
|
|
388
|
+
if (existing.sha256 !== sha256) throw new RangeError("Prepared resource name is bound to different bytes.");
|
|
389
|
+
return { resource: { name, sha256, bytes: existing.bytes, mediaType: "image/png", urlPath: existing.urlPath }, width: image.width, height: image.height, alpha: image.alpha };
|
|
390
|
+
}
|
|
391
|
+
if (resources.size >= 64 || outputBytes + image.bytes.length > SPATIAL_ASSET_PREPARATION_LIMITS.outputBytes) throw new RangeError("Prepared resources exceed the output budget.");
|
|
392
|
+
outputBytes += image.bytes.length;
|
|
393
|
+
const filename = `${sha256}.png`, path = join(workspace, filename);
|
|
394
|
+
await writeFile(path, image.bytes, { flag: "wx", mode: 0o600 }).catch(async error => {
|
|
395
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST") || digest(await readFile(path)) !== sha256) throw error;
|
|
396
|
+
});
|
|
397
|
+
const resource = { name, sha256, bytes: image.bytes.length, mediaType: "image/png", urlPath: `${name}.png` };
|
|
398
|
+
resources.set(name, { ...resource, absolutePath: path });
|
|
399
|
+
return { resource, width: image.width, height: image.height, alpha: image.alpha };
|
|
400
|
+
};
|
|
401
|
+
const native = async (argv: readonly [string, ...string[]], maxOutputBytes = 16_384) => {
|
|
402
|
+
aborted(signal);
|
|
403
|
+
const result = await ports.runner.run(argv, { abortSignal: signal, timeoutMs: SPATIAL_ASSET_PREPARATION_LIMITS.nativeTimeoutMs, maxOutputBytes, stdin: "ignore" });
|
|
404
|
+
aborted(signal);
|
|
405
|
+
if (result.exitCode !== 0) throw new Error("Spatial native media preparation failed; no output was published.");
|
|
406
|
+
return result.stdout;
|
|
407
|
+
};
|
|
408
|
+
const videoProbes = new Map<string, { times: readonly number[]; timestamps: readonly string[]; pts: readonly number[]; timeBase: { numerator: string; denominator: string }; colorFilter: string }>(), decodedVideos = new Map<string, Awaited<ReturnType<typeof publishRaster>>>();
|
|
409
|
+
const glbModels = new Map<string, SpatialGlbModel>();
|
|
410
|
+
let failure: { error: unknown } | undefined;
|
|
411
|
+
let completed: { value: Result } | undefined;
|
|
412
|
+
try {
|
|
413
|
+
await mkdir(join(workspace, "source"), { mode: 0o700 });
|
|
414
|
+
for (const snapshot of request.snapshots) {
|
|
415
|
+
const closures = spatialAssetClosureDigests(snapshot.assets);
|
|
416
|
+
for (const manifest of snapshot.assets) {
|
|
417
|
+
const existing = verified.get(manifest.assetId);
|
|
418
|
+
if (existing !== undefined) {
|
|
419
|
+
if (existing.manifestSha256 !== closures[manifest.assetId]) throw new RangeError("One asset ID cannot have multiple interpretations within a batch.");
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
sourceBytes += manifest.payload.bytes;
|
|
423
|
+
if (sourceBytes > SPATIAL_ASSET_PREPARATION_LIMITS.sourceBytes) throw new RangeError("Spatial source assets exceed the aggregate byte budget.");
|
|
424
|
+
const bytes = await readVerifiedAsset(assetRoot, manifest, signal);
|
|
425
|
+
if (manifest.interpretation.kind === "metadata") {
|
|
426
|
+
if (bytes.byteLength > SPATIAL_SPLAT_LIMITS.metadataBytes) throw new RangeError("Retained metadata exceeds its byte bound.");
|
|
427
|
+
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
|
+
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);
|
|
432
|
+
}
|
|
433
|
+
const record = { manifest, manifestSha256: closures[manifest.assetId]!, bytes };
|
|
434
|
+
verified.set(manifest.assetId, record);
|
|
435
|
+
const path = join(workspace, "source", manifest.assetId);
|
|
436
|
+
await writeFile(path, bytes, { flag: "wx", mode: 0o600 }); paths.set(manifest.assetId, path);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const quantizedKeys = new Map<string, string>();
|
|
440
|
+
for (let snapshotIndex = 0; snapshotIndex < request.snapshots.length; snapshotIndex++) for (const entry of request.snapshots[snapshotIndex]!.entities) {
|
|
441
|
+
const snapshot = request.snapshots[snapshotIndex]!, exactTime = exactTimes[snapshotIndex]!;
|
|
442
|
+
aborted(signal);
|
|
443
|
+
const entity = entry.entity;
|
|
444
|
+
if (entity.kind === "group" || entity.kind === "light" || entity.kind === "mesh" && entity.geometry.kind !== "asset") continue;
|
|
445
|
+
const assetId = entity.kind === "text" ? entity.fontAssetId : entity.kind === "mesh" && entity.geometry.kind === "asset" ? entity.geometry.assetId : "assetId" in entity ? entity.assetId : "";
|
|
446
|
+
const asset = verified.get(assetId);
|
|
447
|
+
if (asset === undefined) throw new RangeError(`Missing manifest for spatial asset ${assetId}.`);
|
|
448
|
+
const interpretation = asset.manifest.interpretation;
|
|
449
|
+
if (entity.kind === "splat") {
|
|
450
|
+
if (interpretation.kind !== "splat" || interpretation.format !== "spz") capability("splat-format", "The qualified world profile accepts gzip SPZ v2/v3 only.");
|
|
451
|
+
const key = `${assetId}:${entity.entityId}:splat`;
|
|
452
|
+
if (preparedAssets.has(key)) continue;
|
|
453
|
+
const facts = await inspectSpatialSpz(asset.bytes, signal);
|
|
454
|
+
const name = `splat-${asset.manifestSha256.slice(0, 40)}`;
|
|
455
|
+
const resource = { name, sha256: asset.manifest.payload.sha256, bytes: asset.bytes.byteLength, mediaType: "application/octet-stream", urlPath: `${name}.spz` };
|
|
456
|
+
if (!resources.has(name)) {
|
|
457
|
+
if (resources.size >= 64 || outputBytes + asset.bytes.byteLength > SPATIAL_ASSET_PREPARATION_LIMITS.outputBytes) throw new RangeError("Prepared splat resources exceed their byte budget.");
|
|
458
|
+
resources.set(name, { ...resource, absolutePath: paths.get(assetId)! }); outputBytes += asset.bytes.byteLength;
|
|
459
|
+
}
|
|
460
|
+
preparedAssets.set(key, PreparedSpatialAssetSchema.parse({ kind: "splat", assetId, entityId: entity.entityId, assetManifestSha256: asset.manifestSha256, resource, facts }));
|
|
461
|
+
profiles.add("slopcamera.spz-v2-v3-spark-2.1.0-full-resolution-v1");
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (entity.kind === "mesh") {
|
|
465
|
+
if (entity.geometry.kind !== "asset") throw new RangeError("Expected asset mesh.");
|
|
466
|
+
if (interpretation.kind !== "gltf" || interpretation.format !== "glb") capability("gltf", "Asset meshes require the qualified self-contained GLB profile.");
|
|
467
|
+
const key = `${assetId}:${entity.entityId}:${entity.geometry.nodeIndex ?? "all"}:${entity.geometry.clip === undefined ? "static" : snapshot.timeUs}`;
|
|
468
|
+
if (preparedAssets.has(key)) continue;
|
|
469
|
+
if (ports.prepareGeometry !== undefined) {
|
|
470
|
+
const result = await ports.prepareGeometry({ asset, dependencies: asset.manifest.dependencies.map(id => verified.get(id)!), entity, timeUs: snapshot.timeUs }, signal);
|
|
471
|
+
aborted(signal);
|
|
472
|
+
const geometry = PreparedSpatialAssetSchema.parse(result.geometry);
|
|
473
|
+
if (geometry.kind !== "geometry" || geometry.assetId !== assetId || geometry.entityId !== entity.entityId || geometry.entityGeometrySha256 !== spatialGeometryContentSha256(entity) || geometry.assetManifestSha256 !== asset.manifestSha256 || geometry.nodeIndex !== entity.geometry.nodeIndex
|
|
474
|
+
|| geometry.timeUs !== (entity.geometry.clip === undefined ? null : snapshot.timeUs)) throw new RangeError("Geometry preparer returned an unrelated representation.");
|
|
475
|
+
const images = new Map<string, Awaited<ReturnType<typeof publishRaster>>>();
|
|
476
|
+
for (const image of result.images) {
|
|
477
|
+
if (images.has(image.resourceName)) throw new RangeError("Geometry image resource names must be unique.");
|
|
478
|
+
images.set(image.resourceName, await publishRaster(await raster(image.bytes, image.opaque), image.resourceName));
|
|
479
|
+
}
|
|
480
|
+
const used = new Set<string>();
|
|
481
|
+
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 } };
|
|
487
|
+
});
|
|
488
|
+
if (used.size !== images.size) throw new RangeError("Geometry preparer returned unused image bytes.");
|
|
489
|
+
const resolved = PreparedSpatialAssetSchema.parse({ ...geometry, primitives });
|
|
490
|
+
if (resolved.kind !== "geometry") throw new RangeError("Expected prepared geometry.");
|
|
491
|
+
preparedAssets.set(key, await publishGeometry(resolved)); profiles.add(result.profile);
|
|
492
|
+
} else {
|
|
493
|
+
if (asset.manifest.dependencies.length !== 0) capability("glb-dependencies", "The built-in GLB profile accepts only self-contained payloads.");
|
|
494
|
+
let model = glbModels.get(assetId);
|
|
495
|
+
if (model === undefined) { model = parseSpatialGlb(asset.bytes); glbModels.set(assetId, model); }
|
|
496
|
+
const geometry = evaluateSpatialGlb(model, { metersPerUnit: interpretation.metersPerUnit, sourceUp: interpretation.sourceUp, timeUs: snapshot.timeUs,
|
|
497
|
+
...(entity.geometry.nodeIndex === undefined ? {} : { nodeIndex: entity.geometry.nodeIndex }),
|
|
498
|
+
...(entity.geometry.materialMode === undefined ? {} : { materialMode: entity.geometry.materialMode }),
|
|
499
|
+
...(entity.geometry.clip === undefined ? {} : { clip: entity.geometry.clip }),
|
|
500
|
+
});
|
|
501
|
+
const primitives: unknown[] = [];
|
|
502
|
+
for (const primitive of geometry.primitives) {
|
|
503
|
+
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
|
+
}
|
|
510
|
+
primitives.push({ ...base,
|
|
511
|
+
...(sourceMaterial === undefined ? {} : { material: { kind: "standard", color: "#ffffff", opacity: sourceMaterial.alphaMode === "OPAQUE" ? 1 : sourceMaterial.baseColorLinear[3], metalness: sourceMaterial.metalness, roughness: sourceMaterial.roughness },
|
|
512
|
+
linearColor: sourceMaterial.baseColorLinear.slice(0, 3), doubleSided: sourceMaterial.doubleSided, alphaMode: sourceMaterial.alphaMode,
|
|
513
|
+
...(sourceMaterial.alphaMode === "MASK" ? { alphaCutoff: sourceMaterial.alphaCutoff } : {}) }),
|
|
514
|
+
...(texture === undefined ? {} : { texture }),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
const resolved = PreparedSpatialAssetSchema.parse({ kind: "geometry", assetId, entityId: entity.entityId,
|
|
518
|
+
entityGeometrySha256: spatialGeometryContentSha256(entity),
|
|
519
|
+
assetManifestSha256: asset.manifestSha256, timeUs: entity.geometry.clip === undefined ? null : snapshot.timeUs,
|
|
520
|
+
...(entity.geometry.nodeIndex === undefined ? {} : { nodeIndex: entity.geometry.nodeIndex }), primitives });
|
|
521
|
+
if (resolved.kind !== "geometry") throw new RangeError("Expected prepared geometry.");
|
|
522
|
+
preparedAssets.set(key, await publishGeometry(resolved));
|
|
523
|
+
profiles.add(geometry.profile);
|
|
524
|
+
}
|
|
525
|
+
profiles.add("slopcamera.prepared-geometry-canonical-json-resource-v1");
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const staticKey = `${assetId}:${entity.entityId}:static`, key = entity.kind === "video" ? `${assetId}:${entity.entityId}:${snapshot.timeUs}` : staticKey;
|
|
529
|
+
if (entity.kind === "video") {
|
|
530
|
+
const exactKey = `${exactTime.n}/${exactTime.d}`;
|
|
531
|
+
if (quantizedKeys.has(key) && quantizedKeys.get(key) !== exactKey) throw new RangeError("Different exact media samples collide on one quantized snapshot time.");
|
|
532
|
+
quantizedKeys.set(key, exactKey);
|
|
533
|
+
}
|
|
534
|
+
if (preparedAssets.has(key)) continue;
|
|
535
|
+
let output: Awaited<ReturnType<typeof publishRaster>>, sourceTimeUs: number | undefined;
|
|
536
|
+
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");
|
|
551
|
+
} else if (entity.kind === "video") {
|
|
552
|
+
if (interpretation.kind !== "video") throw new RangeError("Video entity requires a video asset interpretation.");
|
|
553
|
+
if (ports.ffmpegCommand === undefined || ports.ffprobeCommand === undefined) capability("video-decoder", "Video surfaces require bound FFmpeg and FFprobe capabilities.");
|
|
554
|
+
if (asset.bytes.length < 12 || String.fromCharCode(...asset.bytes.subarray(4, 8)) !== "ftyp") capability("video-container", "Only self-contained ISO-BMFF MP4/QuickTime video is qualified.");
|
|
555
|
+
let sourceN = exactTime.n + BigInt(entity.sourceOffsetUs) * exactTime.d;
|
|
556
|
+
const endN = BigInt(interpretation.durationUs) * exactTime.d;
|
|
557
|
+
if (entity.playback === "loop") sourceN %= endN;
|
|
558
|
+
else if (entity.playback === "freeze") sourceN = sourceN >= endN ? endN - exactTime.d : sourceN;
|
|
559
|
+
else if (sourceN >= endN) throw new RangeError("Video once playback lies outside its exact half-open source duration.");
|
|
560
|
+
sourceTimeUs = Number((2n * sourceN + exactTime.d) / (2n * exactTime.d));
|
|
561
|
+
let timeline = videoProbes.get(assetId);
|
|
562
|
+
if (timeline === undefined) {
|
|
563
|
+
const raw = await native([ports.ffprobeCommand, "-v", "error", "-protocol_whitelist", "file", "-format_whitelist", "mov", "-enable_drefs", "0", "-use_absolute_path", "0", "-select_streams", "v:0", "-show_streams", "-show_frames", "-show_entries", "stream=width,height,pix_fmt,avg_frame_rate,time_base,color_transfer,color_primaries,color_space:frame=best_effort_timestamp,best_effort_timestamp_time,duration,pkt_duration", "-of", "json", paths.get(assetId)!], SPATIAL_ASSET_PREPARATION_LIMITS.probeBytes);
|
|
564
|
+
const probe = probeSchema.parse(JSON.parse(raw) as unknown), stream = probe.streams[0]!;
|
|
565
|
+
if (stream.width !== interpretation.width || stream.height !== interpretation.height || !["yuv420p", "yuv422p", "yuv444p", "rgba", "bgra", "rgb24", "bgr24", "argb", "gbrp"].includes(stream.pix_fmt)
|
|
566
|
+
|| stream.color_transfer !== undefined && !["bt709", "iec61966-2-1", "unknown"].includes(stream.color_transfer)
|
|
567
|
+
|| stream.color_primaries !== undefined && !["bt709", "unknown"].includes(stream.color_primaries)
|
|
568
|
+
|| stream.color_space !== undefined && !["bt709", "gbr", "unknown"].includes(stream.color_space)) capability("video-sdr", "Video requires matching dimensions and qualified 8-bit SDR metadata.");
|
|
569
|
+
const rate = /^(\d+)\/(\d+)$/u.exec(stream.avg_frame_rate);
|
|
570
|
+
if (rate === null || BigInt(rate[1]!) * BigInt(interpretation.frameRate.denominator) !== BigInt(rate[2]!) * BigInt(interpretation.frameRate.numerator) || BigInt(rate[2]!) === 0n) throw new RangeError("Video frame rate differs from its immutable interpretation.");
|
|
571
|
+
let colorFilter = "format=rgba";
|
|
572
|
+
if (stream.pix_fmt.startsWith("yuv")) {
|
|
573
|
+
if (stream.color_transfer !== "bt709" || stream.color_primaries !== "bt709" || stream.color_space !== "bt709") capability("video-color", "YUV video requires explicit BT.709 primaries, transfer, and matrix before conversion to sRGB.");
|
|
574
|
+
colorFilter = "colorspace=space=bt709:trc=iec61966-2-1:primaries=bt709:range=pc:format=yuv444p,format=rgba";
|
|
575
|
+
} else if (stream.color_transfer !== undefined && stream.color_transfer !== "unknown" && stream.color_transfer !== "iec61966-2-1") capability("video-color", "RGB video must declare sRGB transfer or use the source manifest's untagged-sRGB interpretation.");
|
|
576
|
+
dimensions(stream.width, stream.height);
|
|
577
|
+
const timestamps = probe.frames.map(frame => frame.best_effort_timestamp_time), times = timestamps.map(decimalUs);
|
|
578
|
+
const base = /^([1-9]\d{0,15})\/([1-9]\d{0,15})$/u.exec(stream.time_base);
|
|
579
|
+
if (base === null) throw new RangeError("Video requires an exact positive stream time base.");
|
|
580
|
+
const timeBase = { numerator: base[1]!, denominator: base[2]! }, pts = probe.frames.map(frame => frame.best_effort_timestamp);
|
|
581
|
+
if (pts[0] !== 0 || pts.some((time, index) => index > 0 && time <= pts[index - 1]!)) throw new RangeError("Video requires strictly increasing presentation timestamps starting at zero.");
|
|
582
|
+
const final = probe.frames.at(-1)!, finalDuration = final.duration ?? final.pkt_duration;
|
|
583
|
+
if (finalDuration === undefined) throw new RangeError("Video final frame has no presentation duration.");
|
|
584
|
+
const end = BigInt(pts.at(-1)! + finalDuration) * BigInt(timeBase.numerator) * 1_000_000n, d = BigInt(timeBase.denominator);
|
|
585
|
+
if (Math.abs(Number((2n * end + d) / (2n * d)) - interpretation.durationUs) > 1) throw new RangeError("Video duration differs from complete presentation timestamp coverage.");
|
|
586
|
+
timeline = { times, timestamps, pts, timeBase, colorFilter }; videoProbes.set(assetId, timeline);
|
|
587
|
+
}
|
|
588
|
+
let frameIndex = 0;
|
|
589
|
+
while (frameIndex + 1 < timeline.times.length && BigInt(timeline.pts[frameIndex + 1]!) * BigInt(timeline.timeBase.numerator) * 1_000_000n * exactTime.d <= sourceN * BigInt(timeline.timeBase.denominator)) frameIndex++;
|
|
590
|
+
videoEvidence = { sourceFrameIndex: frameIndex, sourcePresentationTimeUs: timeline.times[frameIndex]!, sourceTimestamp: timeline.timestamps[frameIndex]!, sourcePts: timeline.pts[frameIndex]!, sourceTimeBase: timeline.timeBase, sourceExactTimeUs: { numerator: String(sourceN), denominator: String(exactTime.d) } };
|
|
591
|
+
const frameKey = `${assetId}:${frameIndex}`;
|
|
592
|
+
const previous = decodedVideos.get(frameKey);
|
|
593
|
+
if (previous !== undefined) output = previous;
|
|
594
|
+
else {
|
|
595
|
+
const path = join(workspace, `decode-${assetId}-${frameIndex}.png`);
|
|
596
|
+
await native([ports.ffmpegCommand, "-v", "error", "-nostdin", "-protocol_whitelist", "file", "-format_whitelist", "mov", "-enable_drefs", "0", "-use_absolute_path", "0", "-i", paths.get(assetId)!, "-map", "0:v:0", "-vf", `select=eq(n\\,${frameIndex}),${timeline.colorFilter}`, "-fps_mode", "passthrough", "-frames:v", "1", "-an", "-sn", "-dn", "-f", "image2", "-n", path]);
|
|
597
|
+
const stat = await lstat(path);
|
|
598
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size < 1 || stat.size > 128 * 1024 * 1024) throw new RangeError("Decoded video frame is not bounded physical image data.");
|
|
599
|
+
const decoded = await raster(await readFile(path), interpretation.alpha === "opaque");
|
|
600
|
+
if (decoded.width !== interpretation.width || decoded.height !== interpretation.height) throw new RangeError("Decoded video frame changed dimensions.");
|
|
601
|
+
output = await publishRaster(decoded); decodedVideos.set(frameKey, output); await rm(path);
|
|
602
|
+
}
|
|
603
|
+
profiles.add("iso-bmff-absolute-presentation-frame-sdr-v1");
|
|
604
|
+
} else {
|
|
605
|
+
const fontAssets = entity.kind === "text" ? [asset] : asset.manifest.dependencies.map(id => verified.get(id)!).filter(value => value.manifest.interpretation.kind === "font");
|
|
606
|
+
if (entity.kind === "text" && interpretation.kind !== "font" || entity.kind === "diagram" && interpretation.kind !== "diagram") throw new RangeError("Text/diagram entity has an incompatible asset interpretation.");
|
|
607
|
+
if (fontAssets.length === 0) capability("declared-font", "Text and diagrams require exact declared font assets; system fallback is disabled.");
|
|
608
|
+
const fonts = fontAssets.map(font => {
|
|
609
|
+
const info = font.manifest.interpretation;
|
|
610
|
+
if (info.kind !== "font" || info.format !== "otf") capability("font-format", "Only declared OTF fonts are qualified for text rasterization.");
|
|
611
|
+
return spatialOpenTypeFont(font.bytes, info.family);
|
|
612
|
+
});
|
|
613
|
+
const family = fonts[0]!.family, fontFiles = fontAssets.map(font => paths.get(font.manifest.assetId)!);
|
|
614
|
+
let svg: string;
|
|
615
|
+
if (entity.kind === "text") {
|
|
616
|
+
glyphCoverage(entity.text, fonts);
|
|
617
|
+
const scaleX = entity.placement.kind === "world" ? 256 : entity.placement.units === "normalized" ? snapshot.camera.projection.width : 1;
|
|
618
|
+
const scaleY = entity.placement.kind === "world" ? 256 : entity.placement.units === "normalized" ? snapshot.camera.projection.height : 1;
|
|
619
|
+
const width = Math.ceil(entity.width * scaleX), size = entity.fontSize * scaleY, lines = entity.text.split(/\r?\n/u);
|
|
620
|
+
const height = Math.max(1, Math.ceil(size * 1.5 * lines.length)); dimensions(width, height);
|
|
621
|
+
const measuredSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width + size * 2}" height="${height + size * 2}">${lines.map((line, index) => `<text x="${size}" y="${size * (2.1 + 1.5 * index)}" font-family="${xml(family)}" font-size="${size}" fill="#ffffff">${xml(line)}</text>`).join("")}</svg>`;
|
|
622
|
+
const measured = new Resvg(measuredSvg, { font: { loadSystemFonts: false, fontFiles, defaultFontFamily: family } }).getBBox();
|
|
623
|
+
if (measured !== undefined && measured.width > width + 0.01) capability("text-layout", "Text exceeds its explicit width; add authored line breaks or adjust size/width.");
|
|
624
|
+
const x = entity.align === "left" ? 0 : entity.align === "center" ? width / 2 : width;
|
|
625
|
+
const anchor = entity.align === "left" ? "start" : entity.align === "center" ? "middle" : "end";
|
|
626
|
+
svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${lines.map((line, index) => `<text x="${x}" y="${size * (1.1 + 1.5 * index)}" font-family="${xml(family)}" font-size="${size}" fill="#ffffff" text-anchor="${anchor}">${xml(line)}</text>`).join("")}</svg>`;
|
|
627
|
+
} else {
|
|
628
|
+
if (interpretation.kind !== "diagram") throw new RangeError("Expected diagram interpretation.");
|
|
629
|
+
const spec = parseDiagramSpec(JSON.parse(utf8(asset.bytes)) as unknown);
|
|
630
|
+
for (const shape of spec.shapes) if ("icon" in shape && shape.icon !== undefined && builtInIcons[shape.icon] === undefined) capability("diagram-icon", "Diagram references an icon outside the closed built-in vocabulary.");
|
|
631
|
+
dimensions(spec.canvas.width, spec.canvas.height);
|
|
632
|
+
const sourceText: string[] = [];
|
|
633
|
+
const collect = (value: unknown): void => {
|
|
634
|
+
if (Array.isArray(value)) { for (const item of value) collect(item); }
|
|
635
|
+
else if (value !== null && typeof value === "object") for (const [key, child] of Object.entries(value)) {
|
|
636
|
+
if ((key === "text" || key === "label") && typeof child === "string") sourceText.push(child); else collect(child);
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
collect(spec); glyphCoverage(sourceText.join("\n"), fonts);
|
|
640
|
+
svg = (await renderSvg(spec, interpretation.theme, { icons: builtInIcons, font: { family, monoFamily: family, files: fontFiles.map(path => ({ path, embed: false })) } })).svg;
|
|
641
|
+
}
|
|
642
|
+
const resvg = new Resvg(svg, { font: { loadSystemFonts: false, fontFiles, defaultFontFamily: family, monospaceFamily: family, sansSerifFamily: family, serifFamily: family } });
|
|
643
|
+
dimensions(resvg.width, resvg.height);
|
|
644
|
+
if (resvg.imagesToResolve().length !== 0) throw new RangeError("Text/diagram raster attempted an undeclared external image.");
|
|
645
|
+
if (entity.kind === "text") {
|
|
646
|
+
const bounds = resvg.innerBBox();
|
|
647
|
+
if (bounds !== undefined && (bounds.x < -0.01 || bounds.y < -0.01 || bounds.x + bounds.width > resvg.width + 0.01 || bounds.y + bounds.height > resvg.height + 0.01)) capability("text-layout", "Text exceeds its explicit width; add authored line breaks or adjust size/width.");
|
|
648
|
+
}
|
|
649
|
+
output = await publishRaster(await raster(resvg.render().asPng())); profiles.add("declared-otf-no-system-font-resvg-2.6.2");
|
|
650
|
+
}
|
|
651
|
+
preparedAssets.set(key, PreparedSpatialAssetSchema.parse({ kind: "raster", assetId, assetManifestSha256: asset.manifestSha256, entityId: entity.entityId,
|
|
652
|
+
timeUs: entity.kind === "video" ? snapshot.timeUs : null, ...output,
|
|
653
|
+
...(sourceTimeUs === undefined ? {} : { sourceTimeUs }),
|
|
654
|
+
...(videoEvidence === undefined ? {} : videoEvidence),
|
|
655
|
+
...(entity.kind === "text" ? { entityContentSha256: spatialTextRasterContentSha256(entity) } : {}),
|
|
656
|
+
...(entity.kind === "video" ? { entityContentSha256: spatialVideoRasterContentSha256(entity) } : {}),
|
|
657
|
+
}));
|
|
658
|
+
}
|
|
659
|
+
aborted(signal);
|
|
660
|
+
const prepared = [...preparedAssets.values()];
|
|
661
|
+
completed = { value: await consume(Object.freeze({ preparedAssets: prepared, resources: [...resources.values()],
|
|
662
|
+
sources: [...verified].map(([id, asset]) => ({ manifest: asset.manifest, manifestSha256: asset.manifestSha256, absolutePath: paths.get(id)! })),
|
|
663
|
+
receipt: Object.freeze({
|
|
664
|
+
kind: "slopcamera.spatial-asset-preparation", schemaVersion: 1, sourceManifests: Object.fromEntries([...verified].map(([id, asset]) => [id, asset.manifestSha256])),
|
|
665
|
+
preparedSha256: canonicalJsonSha256(prepared), sourceBytes, outputBytes, profiles: [...profiles].sort(),
|
|
666
|
+
}) })) };
|
|
667
|
+
} catch (error) { failure = { error }; }
|
|
668
|
+
finally {
|
|
669
|
+
try { await rm(workspace, { recursive: true, force: true }); }
|
|
670
|
+
catch (cleanupError) { failure = { error: failure === undefined ? cleanupError : new AggregateError([failure.error, cleanupError], "Spatial preparation and workspace cleanup both failed.") }; }
|
|
671
|
+
}
|
|
672
|
+
if (failure !== undefined) throw failure.error;
|
|
673
|
+
if (completed === undefined) throw new Error("Spatial asset consumer did not settle with a result.");
|
|
674
|
+
return completed.value;
|
|
675
|
+
}
|