@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,260 @@
|
|
|
1
|
+
import type { z } from "zod"
|
|
2
|
+
import { SlopcameraCodeError } from "../code/errors.js"
|
|
3
|
+
import { createBoundedJsonSnapshot, createBoundedJsonValueSnapshot, deepFreezeJson } from "../code/json-snapshot.js"
|
|
4
|
+
import {
|
|
5
|
+
SPATIAL_SCENE_LIMITS, SpatialAssetManifestSchema, SpatialEntitySchema,
|
|
6
|
+
SpatialGeneratorIdSchema, SpatialSceneV1Schema,
|
|
7
|
+
type SpatialAnimation, type SpatialAssetManifest, type SpatialEntity,
|
|
8
|
+
type SpatialOverride, type SpatialSceneV1,
|
|
9
|
+
} from "./contracts.js"
|
|
10
|
+
|
|
11
|
+
export class SpatialSceneError extends SlopcameraCodeError {
|
|
12
|
+
readonly path: string
|
|
13
|
+
constructor(code: "invalid-data" | "conflict" | "not-found", message: string, path = "scene") {
|
|
14
|
+
super(code, message, { path })
|
|
15
|
+
this.name = "SpatialSceneError"
|
|
16
|
+
this.path = path
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const limits = { maximumDepth: SPATIAL_SCENE_LIMITS.sourceDepth, maximumValues: SPATIAL_SCENE_LIMITS.sourceValues }
|
|
21
|
+
|
|
22
|
+
/** Foreign objects are captured without invoking getters before schema parsing. */
|
|
23
|
+
export function parseSpatialValue<Schema extends z.ZodType>(schema: Schema, input: unknown, name: string): z.infer<Schema> {
|
|
24
|
+
try {
|
|
25
|
+
const captured = createBoundedJsonValueSnapshot(input, SPATIAL_SCENE_LIMITS.sourceBytes, name, limits)
|
|
26
|
+
const parsed = schema.safeParse(captured.value)
|
|
27
|
+
if (!parsed.success) {
|
|
28
|
+
const issue = parsed.error.issues[0]
|
|
29
|
+
throw new SpatialSceneError("invalid-data", issue?.message ?? `Invalid ${name}.`, `${name}.${issue?.path.join(".") ?? ""}`)
|
|
30
|
+
}
|
|
31
|
+
return parsed.data
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof SpatialSceneError) throw error
|
|
34
|
+
throw new SpatialSceneError("invalid-data", error instanceof Error ? error.message : `Invalid ${name}.`, name)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function spatialValueSha256(input: unknown): string {
|
|
39
|
+
return createBoundedJsonSnapshot(input, SPATIAL_SCENE_LIMITS.sourceBytes, "spatial identity", limits).sha256
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Derived matrices add at most 22 scalar/container values per authored entity. */
|
|
43
|
+
export function spatialStateValueSha256(input: unknown): string {
|
|
44
|
+
return createBoundedJsonSnapshot(input, SPATIAL_SCENE_LIMITS.sourceBytes * 4, "spatial state identity", {
|
|
45
|
+
maximumDepth: SPATIAL_SCENE_LIMITS.sourceDepth + 4,
|
|
46
|
+
maximumValues: SPATIAL_SCENE_LIMITS.sourceValues + SPATIAL_SCENE_LIMITS.entities * 24,
|
|
47
|
+
}).sha256
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compare(a: string, b: string): number { return a < b ? -1 : a > b ? 1 : 0 }
|
|
51
|
+
export function sortSpatialBy<T>(items: readonly T[], id: (item: T) => string): T[] {
|
|
52
|
+
return [...items].sort((a, b) => compare(id(a), id(b)))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function unique<T>(items: readonly T[], id: (item: T) => string, path: string): Map<string, T> {
|
|
56
|
+
const result = new Map<string, T>()
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
const key = id(item)
|
|
59
|
+
if (result.has(key)) throw new SpatialSceneError("invalid-data", `Duplicate identity ${key}.`, path)
|
|
60
|
+
result.set(key, item)
|
|
61
|
+
}
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function requireReference<T>(map: ReadonlyMap<string, T>, id: string, path: string): T {
|
|
66
|
+
const result = map.get(id)
|
|
67
|
+
if (result === undefined) throw new SpatialSceneError("invalid-data", `Missing reference ${id}.`, path)
|
|
68
|
+
return result
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Iterative traversal bounds hierarchy and dependency checks without recursive stack use. */
|
|
72
|
+
export function spatialTopologicalIds(edges: ReadonlyMap<string, readonly string[]>, name: string): readonly string[] {
|
|
73
|
+
const done = new Set<string>()
|
|
74
|
+
const active = new Set<string>()
|
|
75
|
+
const result: string[] = []
|
|
76
|
+
for (const id of edges.keys()) {
|
|
77
|
+
const pending: { id: string; exit: boolean }[] = [{ id, exit: false }]
|
|
78
|
+
while (pending.length) {
|
|
79
|
+
const next = pending.pop()!
|
|
80
|
+
if (next.exit) { active.delete(next.id); done.add(next.id); result.push(next.id); continue }
|
|
81
|
+
if (done.has(next.id)) continue
|
|
82
|
+
if (active.has(next.id)) throw new SpatialSceneError("invalid-data", `Cycle at ${next.id}.`, name)
|
|
83
|
+
const dependencies = requireReference(edges, next.id, name)
|
|
84
|
+
active.add(next.id)
|
|
85
|
+
pending.push({ id: next.id, exit: true })
|
|
86
|
+
for (const dependency of dependencies) pending.push({ id: dependency, exit: false })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return Object.freeze(result)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function generatedSpatialEntityId(generatorId: string, key: string): string {
|
|
93
|
+
SpatialGeneratorIdSchema.parse(generatorId)
|
|
94
|
+
if (typeof key !== "string" || key.length < 1 || key.length > 256) throw new SpatialSceneError("invalid-data", "Generator keys must contain 1–256 characters.")
|
|
95
|
+
return `entity_${spatialValueSha256({ domain: "slopcamera.generated-entity.v1", generatorId, key })}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeEntity(entity: SpatialEntity): SpatialEntity {
|
|
99
|
+
if (entity.kind === "mesh") return { ...entity, material: { ...entity.material, color: entity.material.color.toLowerCase() } }
|
|
100
|
+
if (entity.kind === "text" || entity.kind === "light") return { ...entity, color: entity.color.toLowerCase() }
|
|
101
|
+
return entity
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeAsset(asset: SpatialAssetManifest): SpatialAssetManifest {
|
|
105
|
+
if (asset.interpretation.kind !== "video") return asset
|
|
106
|
+
const rate = asset.interpretation.frameRate
|
|
107
|
+
let divisor = rate.numerator, remainder = rate.denominator
|
|
108
|
+
while (remainder !== 0) { const next = divisor % remainder; divisor = remainder; remainder = next }
|
|
109
|
+
return { ...asset, interpretation: { ...asset.interpretation, frameRate: { numerator: rate.numerator / divisor, denominator: rate.denominator / divisor } } }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Hash retained authored entities before overrides or evaluation; no source is executed. */
|
|
113
|
+
export function spatialGeneratorOutputSha256(input: unknown): string {
|
|
114
|
+
const entities = parseSpatialValue(SpatialEntitySchema.array().max(SPATIAL_SCENE_LIMITS.entities), input, "generator output")
|
|
115
|
+
unique(entities, item => item.entityId, "generator output")
|
|
116
|
+
return spatialValueSha256({ domain: "slopcamera.generator-output.v1", entities: sortSpatialBy(entities.map(normalizeEntity), item => item.entityId) })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Locators and descriptive provenance are excluded; interpretation and dependency digests are bound. */
|
|
120
|
+
export function spatialAssetManifestSha256(input: unknown, dependencyDigests: Readonly<Record<string, string>> = {}): string {
|
|
121
|
+
const asset = normalizeAsset(parseSpatialValue(SpatialAssetManifestSchema, input, "asset manifest"))
|
|
122
|
+
const captured = createBoundedJsonValueSnapshot(dependencyDigests, SPATIAL_SCENE_LIMITS.sourceBytes, "dependency identities", limits).value
|
|
123
|
+
if (captured === null || Array.isArray(captured) || typeof captured !== "object") throw new SpatialSceneError("invalid-data", "Dependency digests must be an object.")
|
|
124
|
+
unique(asset.dependencies, item => item, "asset.dependencies")
|
|
125
|
+
const dependencies = [...asset.dependencies].sort(compare).map(assetId => {
|
|
126
|
+
const sha256 = (captured as Readonly<Record<string, unknown>>)[assetId]
|
|
127
|
+
if (typeof sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(sha256)) throw new SpatialSceneError("invalid-data", `Missing dependency digest for ${assetId}.`)
|
|
128
|
+
return { assetId, sha256 }
|
|
129
|
+
})
|
|
130
|
+
return spatialValueSha256({ domain: "slopcamera.asset-manifest.v1", payload: { sha256: asset.payload.sha256, bytes: asset.payload.bytes }, interpretation: asset.interpretation, dependencies })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function spatialAssetClosureDigests(assets: readonly SpatialAssetManifest[]): Readonly<Record<string, string>> {
|
|
134
|
+
const map = unique(assets, asset => asset.assetId, "assets")
|
|
135
|
+
const order = spatialTopologicalIds(new Map(assets.map(asset => [asset.assetId, asset.dependencies])), "asset dependencies")
|
|
136
|
+
const digests: Record<string, string> = Object.create(null) as Record<string, string>
|
|
137
|
+
for (const id of order) digests[id] = spatialAssetManifestSha256(map.get(id)!, digests)
|
|
138
|
+
return Object.freeze(digests)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function spatialPropertySupported(entity: SpatialEntity, property: SpatialOverride["property"] | SpatialAnimation["property"]): boolean {
|
|
142
|
+
if ((property === "color" || property === "opacity") && entity.kind === "mesh"
|
|
143
|
+
&& entity.geometry.kind === "asset" && entity.geometry.materialMode === "source") return false
|
|
144
|
+
if (property === "opacity") return ["mesh", "image", "video", "diagram"].includes(entity.kind)
|
|
145
|
+
if (property === "color") return ["mesh", "text", "light"].includes(entity.kind)
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** One property writer per interval; a transform override owns position, rotation and scale. */
|
|
150
|
+
export function validateSpatialOverrides(scene: SpatialSceneV1, overrides: readonly SpatialOverride[]): void {
|
|
151
|
+
const entities = new Map(scene.entities.map(entity => [entity.entityId, entity]))
|
|
152
|
+
const generators = new Map(scene.generators.map(generator => [generator.generatorId, generator]))
|
|
153
|
+
unique(overrides, override => `${override.entityId}:${override.property}`, "overrides")
|
|
154
|
+
for (const override of overrides) {
|
|
155
|
+
const entity = requireReference(entities, override.entityId, "overrides")
|
|
156
|
+
if (!spatialPropertySupported(entity, override.property)) throw new SpatialSceneError("conflict", `Entity ${entity.entityId} does not support ${override.property}.`, "overrides")
|
|
157
|
+
if (entity.origin.kind === "generated") {
|
|
158
|
+
const origin = entity.origin
|
|
159
|
+
const generator = requireReference(generators, origin.generatorId, "overrides")
|
|
160
|
+
if (!generator.editableKeys.some(item => item.key === origin.key && item.properties.includes(override.property))) {
|
|
161
|
+
throw new SpatialSceneError("conflict", `Undeclared override ${origin.key}.${override.property}.`, "overrides")
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const properties = override.property === "transform" ? ["position", "rotation", "scale"] : [override.property]
|
|
165
|
+
if (scene.animations.some(channel => channel.targetId === entity.entityId && properties.includes(channel.property))) {
|
|
166
|
+
throw new SpatialSceneError("conflict", `Animation and override both write ${entity.entityId}.${override.property}.`, "overrides")
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function parseSpatialScene(input: unknown): SpatialSceneV1 {
|
|
172
|
+
const parsed = parseSpatialValue(SpatialSceneV1Schema, input, "scene")
|
|
173
|
+
const scene: SpatialSceneV1 = {
|
|
174
|
+
...parsed,
|
|
175
|
+
entities: sortSpatialBy(parsed.entities.map(normalizeEntity), item => item.entityId),
|
|
176
|
+
cameras: sortSpatialBy(parsed.cameras, item => item.cameraId),
|
|
177
|
+
assets: sortSpatialBy(parsed.assets.map(normalizeAsset).map(asset => ({ ...asset, dependencies: [...asset.dependencies].sort(compare) })), item => item.assetId),
|
|
178
|
+
animations: sortSpatialBy(parsed.animations, item => item.channelId),
|
|
179
|
+
generators: sortSpatialBy(parsed.generators.map(generator => ({ ...generator, editableKeys: sortSpatialBy(generator.editableKeys.map(item => ({ ...item, properties: [...item.properties].sort(compare) })), item => item.key) })), item => item.generatorId),
|
|
180
|
+
overrides: sortSpatialBy(parsed.overrides.map(item => item.property === "color" ? { ...item, value: item.value.toLowerCase() } : item), item => `${item.entityId}:${item.property}`),
|
|
181
|
+
}
|
|
182
|
+
const entities = unique(scene.entities, entity => entity.entityId, "entities")
|
|
183
|
+
const cameras = unique(scene.cameras, camera => camera.cameraId, "cameras")
|
|
184
|
+
const assets = unique(scene.assets, asset => asset.assetId, "assets")
|
|
185
|
+
const generators = unique(scene.generators, generator => generator.generatorId, "generators")
|
|
186
|
+
unique(scene.animations, channel => channel.channelId, "animations")
|
|
187
|
+
unique(scene.animations, channel => `${channel.targetId}:${channel.property}`, "animation writers")
|
|
188
|
+
let payloadBytes = 0
|
|
189
|
+
for (const asset of scene.assets) {
|
|
190
|
+
unique(asset.dependencies, id => id, "asset dependencies")
|
|
191
|
+
payloadBytes += asset.payload.bytes
|
|
192
|
+
if (payloadBytes > 268_435_456) throw new SpatialSceneError("invalid-data", "Asset closure exceeds 256 MiB.", "assets")
|
|
193
|
+
if ((asset.interpretation.kind === "image" || asset.interpretation.kind === "video") && asset.interpretation.width * asset.interpretation.height > 33_554_432) throw new SpatialSceneError("invalid-data", "Asset exceeds the 32-megapixel limit.", "assets")
|
|
194
|
+
if (asset.interpretation.kind === "metadata" && asset.payload.bytes > 1_048_576) throw new SpatialSceneError("invalid-data", "Retained metadata exceeds one MiB.", "assets")
|
|
195
|
+
}
|
|
196
|
+
spatialTopologicalIds(new Map(scene.assets.map(asset => [asset.assetId, asset.dependencies])), "asset dependencies")
|
|
197
|
+
spatialTopologicalIds(new Map(scene.entities.map(entity => [entity.entityId, entity.parentId === null ? [] : [entity.parentId]])), "entity hierarchy")
|
|
198
|
+
const generatedKeys = new Set<string>()
|
|
199
|
+
for (const entity of scene.entities) {
|
|
200
|
+
if (entity.placement.kind === "view") requireReference(cameras, entity.placement.cameraId, "placement")
|
|
201
|
+
if (entity.parentId !== null) {
|
|
202
|
+
const parent = requireReference(entities, entity.parentId, "parent")
|
|
203
|
+
const a = parent.placement, b = entity.placement
|
|
204
|
+
if (a.kind !== b.kind || (a.kind === "view" && b.kind === "view" && (a.cameraId !== b.cameraId || a.units !== b.units))) throw new SpatialSceneError("invalid-data", "Parent and child must share their world or view coordinate domain.", "placement")
|
|
205
|
+
}
|
|
206
|
+
if (entity.origin.kind === "generated") {
|
|
207
|
+
const { generatorId, key } = entity.origin
|
|
208
|
+
requireReference(generators, generatorId, "origin")
|
|
209
|
+
if (entity.entityId !== generatedSpatialEntityId(generatorId, key)) throw new SpatialSceneError("invalid-data", "Generated entity identity must derive from its generator and stable key.", "origin")
|
|
210
|
+
const identity = `${generatorId}:${key}`
|
|
211
|
+
if (generatedKeys.has(identity)) throw new SpatialSceneError("invalid-data", "Duplicate generator output key.", "origin")
|
|
212
|
+
generatedKeys.add(identity)
|
|
213
|
+
}
|
|
214
|
+
const reference = entity.kind === "mesh" && entity.geometry.kind === "asset" ? { assetId: entity.geometry.assetId, kind: "gltf" }
|
|
215
|
+
: entity.kind === "text" ? { assetId: entity.fontAssetId, kind: "font" }
|
|
216
|
+
: "assetId" in entity ? { assetId: entity.assetId, kind: entity.kind } : undefined
|
|
217
|
+
if (reference) {
|
|
218
|
+
const asset = requireReference(assets, reference.assetId, "entity asset")
|
|
219
|
+
if (asset.interpretation.kind !== reference.kind) throw new SpatialSceneError("invalid-data", `Entity ${entity.entityId} requires a ${reference.kind} asset.`, "entity asset")
|
|
220
|
+
if (entity.kind === "video" && asset.interpretation.kind === "video" && entity.sourceOffsetUs >= asset.interpretation.durationUs) throw new SpatialSceneError("invalid-data", "Video source offset must precede its duration.", "sourceOffsetUs")
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const generator of scene.generators) {
|
|
224
|
+
unique(generator.editableKeys, item => item.key, "generator editable keys")
|
|
225
|
+
for (const editable of generator.editableKeys) {
|
|
226
|
+
unique(editable.properties, item => item, "generator editable properties")
|
|
227
|
+
const entity = entities.get(generatedSpatialEntityId(generator.generatorId, editable.key))
|
|
228
|
+
if (!entity) throw new SpatialSceneError("conflict", `Orphan editable key ${editable.key}.`, "generators")
|
|
229
|
+
if (editable.properties.some(property => !spatialPropertySupported(entity, property))) throw new SpatialSceneError("invalid-data", `Editable key ${editable.key} declares an unsupported property.`, "generators")
|
|
230
|
+
}
|
|
231
|
+
const retained = scene.entities.filter(entity => entity.origin.kind === "generated" && entity.origin.generatorId === generator.generatorId)
|
|
232
|
+
if (spatialGeneratorOutputSha256(retained) !== generator.outputSha256) throw new SpatialSceneError("conflict", "Retained generator output does not match its pinned digest.", "generators")
|
|
233
|
+
}
|
|
234
|
+
for (const channel of scene.animations) {
|
|
235
|
+
const entity = entities.get(channel.targetId)
|
|
236
|
+
if (entity === undefined) {
|
|
237
|
+
requireReference(cameras, channel.targetId, "animation target")
|
|
238
|
+
if (channel.property !== "position" && channel.property !== "rotation") throw new SpatialSceneError("invalid-data", "Camera animation supports only position and rotation.", "animations")
|
|
239
|
+
} else {
|
|
240
|
+
if (!spatialPropertySupported(entity, channel.property)) throw new SpatialSceneError("invalid-data", `Unsupported ${channel.property} animation.`, "animations")
|
|
241
|
+
if (entity.origin.kind === "generated") {
|
|
242
|
+
const origin = entity.origin
|
|
243
|
+
const control = channel.property === "opacity" ? "opacity" : "transform"
|
|
244
|
+
if (!generators.get(origin.generatorId)!.editableKeys.some(item => item.key === origin.key && item.properties.includes(control))) throw new SpatialSceneError("conflict", `Generated animation requires declared ${control} control.`, "animations")
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
let previous = -1
|
|
248
|
+
for (const key of channel.keys) {
|
|
249
|
+
if (key.timeUs <= previous || key.timeUs > scene.durationUs) throw new SpatialSceneError("invalid-data", "Animation keys must be strictly ordered within scene duration.", "animations")
|
|
250
|
+
previous = key.timeUs
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
validateSpatialOverrides(scene, scene.overrides)
|
|
254
|
+
// Freeze the normalized, validated authored document without any renderer or host effects.
|
|
255
|
+
return deepFreezeJson(scene)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function spatialSceneSha256(input: unknown): string {
|
|
259
|
+
return spatialValueSha256(parseSpatialScene(input))
|
|
260
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./contracts.js"
|
|
2
|
+
export * from "./identity.js"
|
|
3
|
+
export * from "./patch.js"
|
|
4
|
+
export * from "./inspect.js"
|
|
5
|
+
export * from "./evaluate.js"
|
|
6
|
+
export * from "./math.js"
|
|
7
|
+
export * from "./time.js"
|
|
8
|
+
export * from "./authoring.js"
|
|
9
|
+
export * from "./gltf.js"
|
|
10
|
+
export * from "./camera-track.js"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { deepFreezeJson } from "../code/json-snapshot.js"
|
|
2
|
+
import type { SpatialEntity, SpatialOverride, SpatialSceneV1 } from "./contracts.js"
|
|
3
|
+
import { evaluateSpatialScene } from "./evaluate.js"
|
|
4
|
+
import { parseSpatialScene, spatialAssetClosureDigests, spatialPropertySupported, spatialValueSha256 } from "./identity.js"
|
|
5
|
+
import { transformBounds, type Bounds, type Vec3 } from "./math.js"
|
|
6
|
+
|
|
7
|
+
export type SpatialInspectedBounds =
|
|
8
|
+
| { readonly status: "authored-enclosure"; readonly coordinateDomain: SpatialEntity["placement"]; readonly atTimeUs: 0; readonly bounds: Bounds }
|
|
9
|
+
| { readonly status: "unknown"; readonly reason: "requires-asset-decoding" | "requires-text-layout" | "no-surface" }
|
|
10
|
+
|
|
11
|
+
export interface SpatialSceneInspection {
|
|
12
|
+
readonly sceneId: string
|
|
13
|
+
readonly sceneSha256: string
|
|
14
|
+
readonly durationUs: number
|
|
15
|
+
readonly entities: readonly {
|
|
16
|
+
readonly entityId: string
|
|
17
|
+
readonly name: string
|
|
18
|
+
readonly kind: SpatialEntity["kind"]
|
|
19
|
+
readonly origin: SpatialEntity["origin"]
|
|
20
|
+
readonly parentId: string | null
|
|
21
|
+
readonly placement: SpatialEntity["placement"]
|
|
22
|
+
readonly editableControls: readonly SpatialOverride["property"][]
|
|
23
|
+
readonly animatedProperties: readonly string[]
|
|
24
|
+
readonly assetIds: readonly string[]
|
|
25
|
+
readonly bounds: SpatialInspectedBounds
|
|
26
|
+
}[]
|
|
27
|
+
readonly cameras: SpatialSceneV1["cameras"]
|
|
28
|
+
readonly assets: readonly { readonly assetId: string; readonly manifestSha256: string; readonly manifest: SpatialSceneV1["assets"][number] }[]
|
|
29
|
+
readonly generators: SpatialSceneV1["generators"]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function localBounds(entity: SpatialEntity): Bounds | SpatialInspectedBounds {
|
|
33
|
+
let half: Vec3
|
|
34
|
+
if (entity.kind === "mesh") {
|
|
35
|
+
switch (entity.geometry.kind) {
|
|
36
|
+
case "asset": return { status: "unknown", reason: "requires-asset-decoding" }
|
|
37
|
+
case "box": half = entity.geometry.size.map(value => value / 2) as unknown as Vec3; break
|
|
38
|
+
case "plane": half = [entity.geometry.width / 2, entity.geometry.height / 2, 0]; break
|
|
39
|
+
case "sphere": half = [entity.geometry.radius, entity.geometry.radius, entity.geometry.radius]; break
|
|
40
|
+
case "cylinder": half = [entity.geometry.radius, entity.geometry.height / 2, entity.geometry.radius]; break
|
|
41
|
+
}
|
|
42
|
+
} else if (entity.kind === "image" || entity.kind === "diagram" || entity.kind === "video") half = [entity.width / 2, entity.height / 2, 0]
|
|
43
|
+
else return { status: "unknown", reason: entity.kind === "text" ? "requires-text-layout" : entity.kind === "splat" ? "requires-asset-decoding" : "no-surface" }
|
|
44
|
+
return { min: [-half[0], -half[1], -half[2]], max: half }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Authored enclosures are labelled explicitly; inspection never decodes or invents imported bounds. */
|
|
48
|
+
export function inspectSpatialScene(input: unknown): SpatialSceneInspection {
|
|
49
|
+
const scene = parseSpatialScene(input)
|
|
50
|
+
const snapshot = evaluateSpatialScene(scene, { timeUs: 0, cameraId: scene.cameras[0]!.cameraId })
|
|
51
|
+
const digests = spatialAssetClosureDigests(scene.assets)
|
|
52
|
+
return deepFreezeJson({
|
|
53
|
+
sceneId: scene.sceneId, sceneSha256: spatialValueSha256(scene), durationUs: scene.durationUs,
|
|
54
|
+
entities: snapshot.entities.map(({ entity, worldMatrix }) => {
|
|
55
|
+
const origin = entity.origin
|
|
56
|
+
const declared = origin.kind === "generated"
|
|
57
|
+
? scene.generators.find(generator => generator.generatorId === origin.generatorId)!.editableKeys.find(item => item.key === origin.key)?.properties ?? []
|
|
58
|
+
: (["color", "opacity", "transform"] as const).filter(property => spatialPropertySupported(entity, property))
|
|
59
|
+
const animatedProperties = scene.animations.filter(channel => channel.targetId === entity.entityId).map(channel => channel.property)
|
|
60
|
+
const editableControls = declared.filter(property => !animatedProperties.some(animated => animated === property || (property === "transform" && ["position", "rotation", "scale"].includes(animated))))
|
|
61
|
+
const local = localBounds(entity)
|
|
62
|
+
const bounds: SpatialInspectedBounds = "status" in local ? local : { status: "authored-enclosure", coordinateDomain: entity.placement, atTimeUs: 0, bounds: transformBounds(worldMatrix, local) }
|
|
63
|
+
const assetIds = entity.kind === "mesh" && entity.geometry.kind === "asset" ? [entity.geometry.assetId] : entity.kind === "text" ? [entity.fontAssetId] : "assetId" in entity ? [entity.assetId] : []
|
|
64
|
+
return { entityId: entity.entityId, name: entity.name, kind: entity.kind, origin, parentId: entity.parentId, placement: entity.placement, editableControls, animatedProperties, assetIds, bounds }
|
|
65
|
+
}),
|
|
66
|
+
cameras: scene.cameras, assets: scene.assets.map(manifest => ({ assetId: manifest.assetId, manifestSha256: digests[manifest.assetId]!, manifest })), generators: scene.generators,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { SpatialCamera } from "./contracts.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Portable affine and calibrated-camera math.
|
|
5
|
+
* Right-handed meters, Y-up; cameras face local -Z. Column vectors, column-major
|
|
6
|
+
* matrices, quaternion [x,y,z,w]. Pixel origin is the top-left image boundary;
|
|
7
|
+
* pixel (i,j)'s center is [i + 0.5, j + 0.5]. No distortion or implicit resize.
|
|
8
|
+
*/
|
|
9
|
+
export type Vec2 = readonly [number, number];
|
|
10
|
+
export type Vec3 = readonly [number, number, number];
|
|
11
|
+
export type Quaternion = readonly [number, number, number, number];
|
|
12
|
+
export type Mat4 = readonly [
|
|
13
|
+
number, number, number, number,
|
|
14
|
+
number, number, number, number,
|
|
15
|
+
number, number, number, number,
|
|
16
|
+
number, number, number, number,
|
|
17
|
+
];
|
|
18
|
+
export type Transform = Readonly<{ position: Vec3; rotation: Quaternion; scale: Vec3 }>;
|
|
19
|
+
type View = Readonly<{ width: number; height: number; near: number; far: number }>;
|
|
20
|
+
export type Perspective = View & Readonly<{ kind: "perspective"; fx: number; fy: number; cx: number; cy: number }>;
|
|
21
|
+
export type Orthographic = View & Readonly<{ kind: "orthographic"; left: number; right: number; bottom: number; top: number }>;
|
|
22
|
+
export type Projection = Perspective | Orthographic;
|
|
23
|
+
export type Camera = Readonly<{ projection: Projection; cameraToWorld: Mat4 }>;
|
|
24
|
+
export type ProjectedPoint = Readonly<{ pixel: Vec2; depthMeters: number; insideImage: boolean; insideClip: boolean }>;
|
|
25
|
+
export type Ray = Readonly<{ origin: Vec3; direction: Vec3; nearDistanceMeters: number; farDistanceMeters: number }>;
|
|
26
|
+
export type Bounds = Readonly<{ min: Vec3; max: Vec3 }>;
|
|
27
|
+
|
|
28
|
+
// Explicit numerical guardrails for intermediate affine calculations.
|
|
29
|
+
export const MAX_ABS_COMPONENT = 1e12;
|
|
30
|
+
export const MAX_IMAGE_DIMENSION = 1_000_000;
|
|
31
|
+
const AFFINE_TOLERANCE = 1e-12;
|
|
32
|
+
const RIGID_TOLERANCE = 1e-8;
|
|
33
|
+
const MIN_RELATIVE_DETERMINANT = 1e-12;
|
|
34
|
+
|
|
35
|
+
function number(value: number, label: string): number {
|
|
36
|
+
if (!Number.isFinite(value) || Math.abs(value) > MAX_ABS_COMPONENT) {
|
|
37
|
+
throw new RangeError(`${label} must be finite with magnitude <= ${MAX_ABS_COMPONENT}`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function values(value: readonly number[], length: number, label: string): void {
|
|
43
|
+
if (!Array.isArray(value) || value.length !== length) throw new RangeError(`${label} must have ${length} components`);
|
|
44
|
+
for (let index = 0; index < length; index++) number(value[index]!, `${label}[${index}]`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function vec2(x: number, y: number): Vec2 {
|
|
48
|
+
return Object.freeze([number(x, "x"), number(y, "y")]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function vec3(x: number, y: number, z: number): Vec3 {
|
|
52
|
+
return Object.freeze([number(x, "x"), number(y, "y"), number(z, "z")]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function matrix(value: number[]): Mat4 {
|
|
56
|
+
values(value, 16, "matrix");
|
|
57
|
+
return Object.freeze(value) as unknown as Mat4;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function affine(value: Mat4): void {
|
|
61
|
+
values(value, 16, "transform");
|
|
62
|
+
if (Math.abs(value[3]) > AFFINE_TOLERANCE || Math.abs(value[7]) > AFFINE_TOLERANCE
|
|
63
|
+
|| Math.abs(value[11]) > AFFINE_TOLERANCE || Math.abs(value[15] - 1) > AFFINE_TOLERANCE) {
|
|
64
|
+
throw new RangeError("transform must be affine with bottom row [0,0,0,1]");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const IDENTITY_MATRIX: Mat4 = matrix([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
69
|
+
|
|
70
|
+
/** Normalizes nonzero input, without selecting an arbitrary quaternion sign. */
|
|
71
|
+
export function normalizeQuaternion(q: Quaternion): Quaternion {
|
|
72
|
+
values(q, 4, "quaternion");
|
|
73
|
+
const length = Math.hypot(...q);
|
|
74
|
+
if (length === 0) throw new RangeError("quaternion must be nonzero");
|
|
75
|
+
return Object.freeze([q[0] / length, q[1] / length, q[2] / length, q[3] / length]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** T * R * S: scale in local axes, rotate, then translate. Zero scale is allowed. */
|
|
79
|
+
export function composeTransform(transform: Transform): Mat4 {
|
|
80
|
+
values(transform.position, 3, "position");
|
|
81
|
+
values(transform.scale, 3, "scale");
|
|
82
|
+
const [x, y, z, w] = normalizeQuaternion(transform.rotation);
|
|
83
|
+
const [sx, sy, sz] = transform.scale;
|
|
84
|
+
return matrix([
|
|
85
|
+
(1 - 2 * (y*y + z*z)) * sx, 2 * (x*y + z*w) * sx, 2 * (x*z - y*w) * sx, 0,
|
|
86
|
+
2 * (x*y - z*w) * sy, (1 - 2 * (x*x + z*z)) * sy, 2 * (y*z + x*w) * sy, 0,
|
|
87
|
+
2 * (x*z + y*w) * sz, 2 * (y*z - x*w) * sz, (1 - 2 * (x*x + y*y)) * sz, 0,
|
|
88
|
+
...transform.position, 1,
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Parent * local; preserves affine shear introduced by a scaled parent. */
|
|
93
|
+
export function multiplyTransforms(parent: Mat4, local: Mat4): Mat4 {
|
|
94
|
+
affine(parent);
|
|
95
|
+
affine(local);
|
|
96
|
+
const output = new Array<number>(16).fill(0);
|
|
97
|
+
for (let col = 0; col < 4; col++) {
|
|
98
|
+
for (let row = 0; row < 3; row++) {
|
|
99
|
+
output[col * 4 + row] = parent[row]! * local[col * 4]!
|
|
100
|
+
+ parent[4 + row]! * local[col * 4 + 1]!
|
|
101
|
+
+ parent[8 + row]! * local[col * 4 + 2]!
|
|
102
|
+
+ (col === 3 ? parent[12 + row]! : 0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
output[15] = 1;
|
|
106
|
+
return matrix(output);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Inverts an affine transform including shear/reflection. Rejects singular and
|
|
111
|
+
* ill-conditioned bases: determinant after scaling by max basis entry <= 1e-12.
|
|
112
|
+
*/
|
|
113
|
+
export function invertTransform(transform: Mat4): Mat4 {
|
|
114
|
+
affine(transform);
|
|
115
|
+
const scale = Math.max(...[0, 1, 2, 4, 5, 6, 8, 9, 10].map((index) => Math.abs(transform[index]!)));
|
|
116
|
+
if (scale === 0) throw new RangeError("transform is singular");
|
|
117
|
+
const [a, b, c, d, e, f, g, h, i] = [0, 4, 8, 1, 5, 9, 2, 6, 10].map((index) => transform[index]! / scale) as [number, number, number, number, number, number, number, number, number];
|
|
118
|
+
const det = a*(e*i-f*h) - b*(d*i-f*g) + c*(d*h-e*g);
|
|
119
|
+
if (Math.abs(det) <= MIN_RELATIVE_DETERMINANT) throw new RangeError("transform is singular or ill-conditioned");
|
|
120
|
+
const factor = 1 / det / scale;
|
|
121
|
+
const r00 = (e*i-f*h)*factor, r01 = (c*h-b*i)*factor, r02 = (b*f-c*e)*factor;
|
|
122
|
+
const r10 = (f*g-d*i)*factor, r11 = (a*i-c*g)*factor, r12 = (c*d-a*f)*factor;
|
|
123
|
+
const r20 = (d*h-e*g)*factor, r21 = (b*g-a*h)*factor, r22 = (a*e-b*d)*factor;
|
|
124
|
+
const [tx, ty, tz] = [transform[12], transform[13], transform[14]];
|
|
125
|
+
return matrix([
|
|
126
|
+
r00, r10, r20, 0, r01, r11, r21, 0, r02, r12, r22, 0,
|
|
127
|
+
-(r00*tx+r01*ty+r02*tz), -(r10*tx+r11*ty+r12*tz), -(r20*tx+r21*ty+r22*tz), 1,
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function transformPoint(transform: Mat4, point: Vec3): Vec3 {
|
|
132
|
+
affine(transform);
|
|
133
|
+
values(point, 3, "point");
|
|
134
|
+
return apply(transform, point, true);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Transforms a displacement/direction; does not normalize it or transform normals. */
|
|
138
|
+
export function transformDirection(transform: Mat4, direction: Vec3): Vec3 {
|
|
139
|
+
affine(transform);
|
|
140
|
+
values(direction, 3, "direction");
|
|
141
|
+
return apply(transform, direction, false);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function apply(m: Mat4, v: Vec3, translate: boolean): Vec3 {
|
|
145
|
+
return vec3(
|
|
146
|
+
m[0]*v[0] + m[4]*v[1] + m[8]*v[2] + (translate ? m[12] : 0),
|
|
147
|
+
m[1]*v[0] + m[5]*v[1] + m[9]*v[2] + (translate ? m[13] : 0),
|
|
148
|
+
m[2]*v[0] + m[6]*v[1] + m[10]*v[2] + (translate ? m[14] : 0),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Shortest-arc interpolation; q and -q describe the same rotation. t in [0,1]. */
|
|
153
|
+
export function slerpQuaternion(from: Quaternion, to: Quaternion, t: number): Quaternion {
|
|
154
|
+
number(t, "t");
|
|
155
|
+
if (t < 0 || t > 1) throw new RangeError("t must be in [0,1]");
|
|
156
|
+
const a = normalizeQuaternion(from);
|
|
157
|
+
const normalizedTo = normalizeQuaternion(to);
|
|
158
|
+
let dot = a.reduce((sum, value, index) => sum + value * normalizedTo[index]!, 0);
|
|
159
|
+
const b = dot < 0 ? normalizedTo.map((value) => -value) : normalizedTo;
|
|
160
|
+
dot = Math.min(1, Math.max(0, Math.abs(dot)));
|
|
161
|
+
let left = 1 - t, right = t;
|
|
162
|
+
if (dot < 0.9995) {
|
|
163
|
+
const angle = Math.acos(dot), sine = Math.sin(angle);
|
|
164
|
+
left = Math.sin((1-t)*angle) / sine;
|
|
165
|
+
right = Math.sin(t*angle) / sine;
|
|
166
|
+
}
|
|
167
|
+
return normalizeQuaternion([
|
|
168
|
+
left*a[0] + right*b[0]!, left*a[1] + right*b[1]!,
|
|
169
|
+
left*a[2] + right*b[2]!, left*a[3] + right*b[3]!,
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function projection(p: Projection): void {
|
|
174
|
+
for (const value of [p.width, p.height]) {
|
|
175
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_IMAGE_DIMENSION) throw new RangeError("image dimensions must be positive bounded integers");
|
|
176
|
+
}
|
|
177
|
+
number(p.near, "near");
|
|
178
|
+
number(p.far, "far");
|
|
179
|
+
if (p.near <= 0 || p.far <= p.near) throw new RangeError("clipping requires 0 < near < far");
|
|
180
|
+
if (p.kind === "perspective") {
|
|
181
|
+
for (const key of ["fx", "fy", "cx", "cy"] as const) number(p[key], key);
|
|
182
|
+
if (p.fx <= 0 || p.fy <= 0) throw new RangeError("focal lengths in pixels must be positive");
|
|
183
|
+
} else if (p.kind === "orthographic") {
|
|
184
|
+
for (const key of ["left", "right", "bottom", "top"] as const) number(p[key], key);
|
|
185
|
+
if (p.left >= p.right || p.bottom >= p.top) throw new RangeError("orthographic extents must be ordered");
|
|
186
|
+
} else {
|
|
187
|
+
throw new RangeError("unsupported projection");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Camera scale/shear/reflection would change the meaning of axial-meter depth. */
|
|
192
|
+
function camera(camera: Camera): void {
|
|
193
|
+
projection(camera.projection);
|
|
194
|
+
const m = camera.cameraToWorld;
|
|
195
|
+
affine(m);
|
|
196
|
+
const columns: Vec3[] = [[m[0], m[1], m[2]], [m[4], m[5], m[6]], [m[8], m[9], m[10]]];
|
|
197
|
+
for (let i = 0; i < 3; i++) {
|
|
198
|
+
for (let j = i; j < 3; j++) {
|
|
199
|
+
const dot = columns[i]!.reduce((sum, value, index) => sum + value*columns[j]![index]!, 0);
|
|
200
|
+
if (Math.abs(dot - (i === j ? 1 : 0)) > RIGID_TOLERANCE) throw new RangeError("camera pose must be rigid without scale or shear");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const determinant = m[0]*(m[5]*m[10]-m[9]*m[6]) - m[4]*(m[1]*m[10]-m[9]*m[2]) + m[8]*(m[1]*m[6]-m[5]*m[2]);
|
|
204
|
+
if (determinant <= 0) throw new RangeError("camera pose must preserve handedness");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns null on/behind the camera plane. Positive-depth offscreen or clipped
|
|
209
|
+
* points remain projectable, with separate flags. Image bounds are half-open;
|
|
210
|
+
* near/far depth boundaries are inclusive. No rasterization epsilon is implied.
|
|
211
|
+
*/
|
|
212
|
+
export function projectPoint(view: Camera, worldPoint: Vec3): ProjectedPoint | null {
|
|
213
|
+
camera(view);
|
|
214
|
+
values(worldPoint, 3, "world point");
|
|
215
|
+
const local = apply(invertTransform(view.cameraToWorld), worldPoint, true);
|
|
216
|
+
const depthMeters = -local[2];
|
|
217
|
+
if (depthMeters <= 0) return null;
|
|
218
|
+
const p = view.projection;
|
|
219
|
+
const pixel = p.kind === "perspective"
|
|
220
|
+
? vec2(p.fx*local[0]/depthMeters+p.cx, p.cy-p.fy*local[1]/depthMeters)
|
|
221
|
+
: vec2((local[0]-p.left)/(p.right-p.left)*p.width, (p.top-local[1])/(p.top-p.bottom)*p.height);
|
|
222
|
+
return Object.freeze({ pixel, depthMeters,
|
|
223
|
+
insideImage: pixel[0] >= 0 && pixel[0] < p.width && pixel[1] >= 0 && pixel[1] < p.height,
|
|
224
|
+
insideClip: depthMeters >= p.near && depthMeters <= p.far });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Pixel and depth may lie outside image/clip bounds; depth must remain positive. */
|
|
228
|
+
export function unprojectPixel(view: Camera, pixel: Vec2, depthMeters: number): Vec3 {
|
|
229
|
+
camera(view);
|
|
230
|
+
values(pixel, 2, "pixel");
|
|
231
|
+
number(depthMeters, "depth");
|
|
232
|
+
if (depthMeters <= 0) throw new RangeError("axial depth must be positive");
|
|
233
|
+
const p = view.projection;
|
|
234
|
+
const local: Vec3 = p.kind === "perspective"
|
|
235
|
+
? [(pixel[0]-p.cx)/p.fx*depthMeters, (p.cy-pixel[1])/p.fy*depthMeters, -depthMeters]
|
|
236
|
+
: [p.left+pixel[0]/p.width*(p.right-p.left), p.top-pixel[1]/p.height*(p.top-p.bottom), -depthMeters];
|
|
237
|
+
values(local, 3, "unprojected local point");
|
|
238
|
+
return apply(view.cameraToWorld, local, true);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* A normalized world-space ray. Perspective origin is the optical center;
|
|
243
|
+
* orthographic origin is the corresponding point on the camera plane (depth 0).
|
|
244
|
+
* near/far distances are along the ray, NOT positive axial depth for perspective.
|
|
245
|
+
*/
|
|
246
|
+
export function pixelRay(view: Camera, pixel: Vec2): Ray {
|
|
247
|
+
camera(view);
|
|
248
|
+
values(pixel, 2, "pixel");
|
|
249
|
+
const p = view.projection;
|
|
250
|
+
const x = p.kind === "perspective" ? (pixel[0]-p.cx)/p.fx : p.left+pixel[0]/p.width*(p.right-p.left);
|
|
251
|
+
const y = p.kind === "perspective" ? (p.cy-pixel[1])/p.fy : p.top-pixel[1]/p.height*(p.top-p.bottom);
|
|
252
|
+
const length = p.kind === "perspective" ? Math.hypot(x, y, 1) : 1;
|
|
253
|
+
const origin = apply(view.cameraToWorld, p.kind === "perspective" ? [0, 0, 0] : vec3(x, y, 0), true);
|
|
254
|
+
const direction = apply(view.cameraToWorld, p.kind === "perspective" ? vec3(x/length, y/length, -1/length) : [0, 0, -1], false);
|
|
255
|
+
return Object.freeze({ origin, direction,
|
|
256
|
+
nearDistanceMeters: number(p.near*length, "near ray distance"),
|
|
257
|
+
farDistanceMeters: number(p.far*length, "far ray distance") });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** World-axis-aligned enclosure of all eight corners, including shear/reflection. */
|
|
261
|
+
export function transformBounds(transform: Mat4, bounds: Bounds): Bounds {
|
|
262
|
+
affine(transform);
|
|
263
|
+
values(bounds.min, 3, "bounds min");
|
|
264
|
+
values(bounds.max, 3, "bounds max");
|
|
265
|
+
if (bounds.min.some((value, index) => value > bounds.max[index]!)) throw new RangeError("bounds must have min <= max");
|
|
266
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
267
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
268
|
+
const point = apply(transform, [mask&1 ? bounds.max[0] : bounds.min[0], mask&2 ? bounds.max[1] : bounds.min[1], mask&4 ? bounds.max[2] : bounds.min[2]], true);
|
|
269
|
+
for (let index = 0; index < 3; index++) {
|
|
270
|
+
min[index] = Math.min(min[index]!, point[index]!);
|
|
271
|
+
max[index] = Math.max(max[index]!, point[index]!);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return Object.freeze({ min: vec3(min[0]!, min[1]!, min[2]!), max: vec3(max[0]!, max[1]!, max[2]!) });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Convert a validated authored camera pose to an explicit camera-to-world matrix. */
|
|
278
|
+
export function cameraMathView(camera: SpatialCamera): Camera {
|
|
279
|
+
return Object.freeze({ projection: camera.projection, cameraToWorld: composeTransform({ ...camera.pose, scale: [1, 1, 1] }) });
|
|
280
|
+
}
|