@vgai/editor 0.2.0 → 0.4.0-canary.20260715.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/assets/axe-C7YBtOuM.js +30 -0
- package/dist/assets/browserAll-DDtGGE48.js +1 -0
- package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
- package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
- package/dist/assets/game-BdTHR2_X.js +1 -0
- package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
- package/dist/assets/game-CcHfYxjP.js +1 -0
- package/dist/assets/game-Cqzj_lpD.js +1 -0
- package/dist/assets/game-DSZdeCHu.js +1 -0
- package/dist/assets/game-Dch9sAPc.js +1 -0
- package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
- package/dist/assets/game-loop-DMIgSMV-.js +1 -0
- package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
- package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
- package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
- package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
- package/dist/assets/index-CQEfi217.js +15412 -0
- package/dist/assets/index-Dvi-V0NE.css +1 -0
- package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
- package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
- package/dist/assets/react-18-C9eHn8x1.js +1 -0
- package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
- package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
- package/dist/index.html +15 -13
- package/dist/ingest/racing-game/vgai.game.json +5 -4
- package/dist/ingest/simcity/vgai.game.json +5 -4
- package/dist/ingest/tanks/vgai.game.json +5 -4
- package/dist/project-scripts.bundle.js +3917 -0
- package/dist/scenes/untitled.vscn.json +2 -0
- package/dist-server/packaged.mjs +7697 -3435
- package/package.json +31 -4
- package/scripts/probe-reporter.ts +349 -0
- package/scripts/run-probe.ts +2260 -0
- package/server/asset-catalog-audit.ts +268 -0
- package/server/asset-catalog-v2.ts +146 -0
- package/server/asset-library-routes.ts +276 -58
- package/server/canonical-path.ts +1 -1
- package/server/dev.ts +148 -44
- package/server/editor-server.ts +888 -61
- package/server/editor-sse.ts +115 -4
- package/server/harness-chat-service.ts +796 -0
- package/server/local-asset-catalog.ts +157 -22
- package/server/packaged.ts +51 -21
- package/server/process-shutdown.ts +136 -0
- package/server/prod.ts +9 -6
- package/server/project-hmr-files.ts +41 -0
- package/server/project-operations.ts +343 -0
- package/server/project-optimize-deps-entries.ts +4 -4
- package/server/project-output-writer.ts +156 -0
- package/server/project-validation.ts +140 -0
- package/server/project-view.ts +8 -50
- package/server/server-utils.ts +75 -10
- package/server/session-registry.ts +12 -0
- package/src/EditorContext.tsx +107 -3
- package/src/action-registry.ts +115 -17
- package/src/adapter-resolver.ts +213 -478
- package/src/asset-events.ts +64 -1
- package/src/asset-preview.ts +906 -0
- package/src/asset-selection.ts +121 -0
- package/src/asset-workflow/asset-capabilities.ts +259 -0
- package/src/asset-workflow/asset-editor-errors.ts +46 -0
- package/src/asset-workflow/asset-import-pipeline.ts +176 -0
- package/src/asset-workflow/asset-library-query.ts +107 -0
- package/src/asset-workflow/asset-types.ts +192 -0
- package/src/asset-workflow/asset-workflow-quality.ts +65 -0
- package/src/asset-workflow/import-contract.ts +113 -0
- package/src/asset-workflow/model-conversion.ts +126 -0
- package/src/asset-workflow/model-inspection.ts +185 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +153 -0
- package/src/asset-workflow/project-asset-operations.ts +119 -0
- package/src/asset-workflow/thumbnail-system.ts +172 -0
- package/src/authoring/active-systems.ts +15 -0
- package/src/authoring/boundary-authoring-adapter.ts +16 -5
- package/src/authoring/composite-authoring-adapter.ts +305 -124
- package/src/authoring/design-time-layers.ts +300 -117
- package/src/authoring/edit-mode-authoring.ts +278 -293
- package/src/authoring/embed-only-adapter.ts +4 -2
- package/src/authoring/first-party-hierarchy-menu.ts +7 -2
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
- package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
- package/src/authoring/first-party-inspector/register.tsx +13 -2
- package/src/authoring/ingest-authoring-adapter.ts +142 -45
- package/src/authoring/mount-failure-report.ts +5 -5
- package/src/authoring/provenance.ts +41 -0
- package/src/authoring/react-dom-authoring-adapter.ts +92 -92
- package/src/authoring/react-inspector-section.tsx +489 -51
- package/src/authoring/react-world-authoring-adapter.ts +541 -306
- package/src/authoring/story-overlay.ts +1 -1
- package/src/authoring/story-types.ts +11 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
- package/src/authoring/viewport-tool-context.ts +78 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +186 -0
- package/src/authoring/world-pan-state.ts +33 -17
- package/src/authoring/world-session-state.ts +1 -1
- package/src/authoring/world2d-authoring-adapter.ts +109 -52
- package/src/availability-tick.ts +56 -0
- package/src/browser-play-entry.ts +5 -5
- package/src/browser-project-scripts.ts +1 -0
- package/src/build-identity.ts +15 -0
- package/src/command-listener.ts +893 -19
- package/src/components/AlignToolbar.tsx +74 -63
- package/src/components/AppRoot.tsx +193 -26
- package/src/components/ApplicationMenus.tsx +272 -0
- package/src/components/AssetBrowser.tsx +1450 -293
- package/src/components/AssetEditorShell.tsx +141 -0
- package/src/components/AssetImportDetails.tsx +218 -0
- package/src/components/AssetLibraryPanel.tsx +14 -0
- package/src/components/CameraInfo.tsx +11 -20
- package/src/components/CenterDocuments.tsx +585 -0
- package/src/components/CommandPalette.tsx +75 -30
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +57 -112
- package/src/components/DebugConsolePanel.tsx +461 -0
- package/src/components/DebuggerPanel.tsx +165 -0
- package/src/components/DefaultEditorLayout.tsx +167 -231
- package/src/components/GameHierarchy.tsx +873 -290
- package/src/components/GamePerformanceOverlay.tsx +46 -0
- package/src/components/HarnessChatPanel.tsx +2352 -0
- package/src/components/HistoryPanel.tsx +226 -0
- package/src/components/Inspector.tsx +153 -99
- package/src/components/InspectorToolSection.tsx +6 -4
- package/src/components/MountFailureBanner.tsx +31 -48
- package/src/components/NetworkSection.tsx +25 -14
- package/src/components/OnlineAssetBrowser.tsx +794 -343
- package/src/components/OverlayReportBanner.tsx +38 -64
- package/src/components/PerformancePanel.tsx +206 -0
- package/src/components/PlayBar.tsx +93 -263
- package/src/components/PrefabSaveDialog.tsx +55 -127
- package/src/components/ProjectHeader.css +60 -0
- package/src/components/ProjectHeader.tsx +170 -105
- package/src/components/ProjectScreen.tsx +246 -184
- package/src/components/ReactCanvasControls.tsx +185 -0
- package/src/components/ResolutionPicker.tsx +85 -0
- package/src/components/RootActivateDialog.tsx +39 -68
- package/src/components/SaveDialog.tsx +55 -127
- package/src/components/SaveStatus.tsx +11 -29
- package/src/components/StartupLoadingScreen.tsx +96 -0
- package/src/components/StateWatchPanel.tsx +302 -0
- package/src/components/StatsOverlay.tsx +12 -5
- package/src/components/StatusBar.tsx +64 -0
- package/src/components/ThemeManagerDialog.css +89 -0
- package/src/components/ThemeManagerDialog.tsx +371 -0
- package/src/components/ToolHost.tsx +26 -15
- package/src/components/Toolbar.tsx +117 -214
- package/src/components/VgaiVoxelLogo.tsx +89 -0
- package/src/components/ViewportOverlay.tsx +97 -172
- package/src/components/ViewportPanel.tsx +130 -31
- package/src/components/WorkspaceDock.tsx +891 -0
- package/src/components/WorldSelectionOverlay.tsx +51 -63
- package/src/components/WorldTextEditor.tsx +9 -5
- package/src/components/asset-documents.tsx +390 -0
- package/src/components/asset-editor-persistence.ts +44 -0
- package/src/components/asset-selection-section.tsx +338 -0
- package/src/components/asset-viewers/AudioViewer.tsx +73 -21
- package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
- package/src/components/asset-viewers/ImageViewer.tsx +43 -11
- package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
- package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
- package/src/components/asset-viewers/ModelViewer.tsx +805 -121
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
- package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
- package/src/components/asset-workflow.css +506 -0
- package/src/components/build-documents.tsx +696 -0
- package/src/components/core-utilities.tsx +75 -0
- package/src/components/data-documents.tsx +364 -0
- package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
- package/src/components/debugger-panel-state.ts +57 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
- package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
- package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
- package/src/components/inspector-widgets/FontPicker.tsx +13 -21
- package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
- package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
- package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
- package/src/components/inspector-widgets/shared.tsx +90 -49
- package/src/components/inspectors/AnimationSection.tsx +39 -40
- package/src/components/inspectors/AudioSection.tsx +28 -39
- package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
- package/src/components/inspectors/CameraSection.tsx +14 -23
- package/src/components/inspectors/ComponentsSection.tsx +39 -43
- package/src/components/inspectors/JointsSection.tsx +34 -43
- package/src/components/inspectors/LightSection.tsx +90 -35
- package/src/components/inspectors/MaterialSection.tsx +137 -174
- package/src/components/inspectors/MeshSection.tsx +9 -17
- package/src/components/inspectors/NavigationSection.tsx +8 -15
- package/src/components/inspectors/ParticleSection.tsx +91 -76
- package/src/components/inspectors/PhysicsSection.tsx +42 -46
- package/src/components/inspectors/PostProcessingSection.tsx +28 -36
- package/src/components/inspectors/RenderingSection.tsx +12 -20
- package/src/components/inspectors/ShadowSection.tsx +7 -8
- package/src/components/inspectors/SplineSection.tsx +31 -42
- package/src/components/inspectors/TransformSection.tsx +14 -22
- package/src/components/patterns/Dialog.tsx +95 -0
- package/src/components/patterns/Fields.tsx +44 -0
- package/src/components/patterns/List.tsx +25 -0
- package/src/components/patterns/StateSurface.tsx +40 -0
- package/src/components/patterns/Surfaces.tsx +115 -0
- package/src/components/patterns/Tabs.tsx +80 -0
- package/src/components/patterns/Toolbar.tsx +51 -0
- package/src/components/patterns/Tree.tsx +72 -0
- package/src/components/primitives/Button.tsx +46 -0
- package/src/components/primitives/ColorInput.tsx +11 -47
- package/src/components/primitives/EditorIcon.tsx +99 -0
- package/src/components/primitives/FormControls.tsx +71 -0
- package/src/components/primitives/JsonInput.tsx +15 -2
- package/src/components/primitives/Layout.tsx +100 -0
- package/src/components/primitives/Menu.tsx +105 -0
- package/src/components/primitives/NumberInput.tsx +8 -35
- package/src/components/primitives/Panel.tsx +49 -44
- package/src/components/primitives/SectionHeader.tsx +10 -28
- package/src/components/primitives/Text.tsx +46 -0
- package/src/components/primitives/Tooltip.tsx +20 -12
- package/src/components/primitives/Vec3Input.tsx +3 -0
- package/src/components/primitives/banner-tones.ts +33 -0
- package/src/components/primitives/editor-icons.ts +102 -0
- package/src/components/primitives/panel-header-styles.ts +30 -0
- package/src/components/project-operation-documents.tsx +226 -0
- package/src/components/status-contributions.tsx +175 -0
- package/src/components/story-documents-register.ts +15 -0
- package/src/components/story-documents.tsx +1185 -0
- package/src/components/tool-documents.tsx +110 -0
- package/src/components/workspace-dock.css +293 -0
- package/src/components/workspace-history.ts +33 -0
- package/src/components/world-documents.tsx +145 -0
- package/src/components/xstate-documents.tsx +209 -0
- package/src/composite-screenshot.ts +248 -0
- package/src/design-system/index.ts +70 -0
- package/src/design-system-stories/Foundation.stories.tsx +183 -0
- package/src/design-system-stories/Patterns.stories.tsx +251 -0
- package/src/design-system-stories/Workspace.stories.tsx +538 -0
- package/src/doctor/report.ts +57 -13
- package/src/editor-api.ts +295 -31
- package/src/editor-compatibility.ts +200 -0
- package/src/editor-console.ts +35 -17
- package/src/editor-hotkeys.ts +85 -33
- package/src/editor-store.ts +538 -233
- package/src/editor-viewport.ts +160 -34
- package/src/entity-factory.ts +14 -3
- package/src/gizmo-registry.ts +21 -5
- package/src/harness-chat-editor-context.ts +37 -0
- package/src/harness-chat-indicator.ts +68 -0
- package/src/harness-chat-layout.ts +53 -0
- package/src/harness-chat-lifecycle.ts +170 -0
- package/src/harness-chat-reconnect.ts +85 -0
- package/src/harness-chat-types.ts +409 -0
- package/src/hierarchy-drop.ts +50 -0
- package/src/hierarchy-expansion-state.ts +100 -0
- package/src/hierarchy-menu-registry.ts +1 -1
- package/src/hierarchy-projection.ts +76 -0
- package/src/history/editor-session.ts +21 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-service.ts +904 -0
- package/src/history/json-history-resource.ts +165 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +266 -0
- package/src/history/project-root-history-backends.ts +53 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +108 -0
- package/src/history/source-history-backend.ts +374 -0
- package/src/history/types.ts +100 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hosted-example.ts +12 -2
- package/src/hotkeys.ts +89 -18
- package/src/ingest/discovery-react.ts +9 -10
- package/src/ingest/discovery.ts +4 -4
- package/src/ingest/discovery2d.ts +5 -5
- package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
- package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
- package/src/ingest/games/games-fps/vgai.game.json +3 -3
- package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
- package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
- package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
- package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
- package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
- package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
- package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
- package/src/ingest/types.ts +1 -1
- package/src/ingest-mode.ts +47 -52
- package/src/ingest-siblings.ts +23 -33
- package/src/initial-scene.ts +1 -2
- package/src/main.tsx +16 -8
- package/src/manifest-project.ts +10 -12
- package/src/model-thumbnail.ts +261 -26
- package/src/module-mode.ts +44 -50
- package/src/play-log-events.ts +26 -0
- package/src/play-mode.ts +495 -401
- package/src/play-scene-adoption.ts +23 -0
- package/src/playground/main.ts +68 -24
- package/src/playwright-shim.ts +473 -0
- package/src/project-manager.ts +90 -10
- package/src/project-operations.ts +112 -0
- package/src/project-story-discovery.ts +25 -0
- package/src/project-tool-discovery.ts +43 -0
- package/src/scene-serializer.ts +0 -6
- package/src/scene-sync.ts +7 -2
- package/src/script-loader.ts +3 -3
- package/src/skeleton-helper.ts +57 -0
- package/src/storage/seed.ts +68 -8
- package/src/stories/StoryPreviewMount.tsx +265 -0
- package/src/stories/action-stories.fixture.tsx +44 -0
- package/src/stories/button-stories.fixture.tsx +50 -0
- package/src/stories/compose-project-stories.ts +182 -0
- package/src/stories/loader-stories.fixture.tsx +53 -0
- package/src/stories/story-discovery.ts +130 -0
- package/src/stories/story-registry.ts +146 -0
- package/src/theme-css.ts +1049 -0
- package/src/theme-library.ts +238 -0
- package/src/theme-preference.ts +130 -0
- package/src/theme.ts +527 -0
- package/src/tool-loader.ts +80 -24
- package/src/ui-source/inspect.ts +27 -4
- package/src/ui-source/source-write-backend.ts +83 -22
- package/src/widgets/index.ts +9 -18
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-dock-controller.ts +64 -0
- package/src/workspace-document-ids.ts +5 -0
- package/src/workspace-document-registry.ts +367 -0
- package/src/workspace-state-persistence.ts +455 -0
- package/src/workspace-status-registry.ts +110 -0
- package/src/workspace-utility-commands.ts +49 -0
- package/src/workspace-utility-registry.ts +161 -0
- package/src/world-document-routing.ts +16 -0
- package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
- package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
- package/src/xstate-inspect/use-live-actor-state.ts +39 -0
- package/src/xstate-inspect/xstate-graph.ts +265 -0
- package/src/xstate-inspect/xstate-layout.ts +76 -0
- package/src/z-index.ts +26 -0
- package/template/.claude/skills/editor/SKILL.md +129 -90
- package/template/AGENTS.md +1110 -0
- package/template/CLAUDE.md +1 -512
- package/template/IDIOMS.md +50 -0
- package/template/check-idioms.ts +706 -0
- package/template/manifest-entry-modules-plugin.ts +53 -0
- package/template/package.json +13 -4
- package/template/public/scenes/default.scn2d.json +6 -0
- package/template/public/scenes/default.vscn.json +7 -1
- package/template/public/scenes/untitled.vscn.json +10 -0
- package/template/scripts/playtest.ts +40 -0
- package/template/server/loopback.ts +75 -0
- package/template/server/rooms/arena-room.ts +69 -1
- package/template/server/rooms/game-room.ts +68 -1
- package/template/src/data/README.md +9 -4
- package/template/src/data/data-ref.ts +35 -0
- package/template/src/data/tuning.schema.ts +1 -1
- package/template/src/main.ts +41 -62
- package/template/src/manifest-entry-modules.d.ts +3 -0
- package/template/src/operations/README.md +88 -0
- package/template/src/runtime/pixi-adapter.ts +24 -0
- package/template/src/scripts/components/data-spinner.ts +43 -2
- package/template/src/scripts/components/spin-lap-math.ts +27 -0
- package/template/src/tools/README.md +5 -3
- package/template/src/tools/example.tool.tsx +13 -7
- package/template/src/ui/Button.stories.tsx +66 -0
- package/template/src/ui/RebindPanel.tsx +2 -6
- package/template/src/ui/game.tsx +17 -0
- package/template/tests/acceptance/example.spec.ts +64 -0
- package/template/tests/logic/example.test.ts +31 -0
- package/template/tests/logic/room.test.ts +100 -0
- package/template/tests/tsconfig.json +30 -0
- package/template/validate-manifest.ts +5 -5
- package/template/validate-scenes.ts +50 -18
- package/template/vgai.game.json +20 -13
- package/template/vite.config.ts +77 -16
- package/vite-plugin-ui-oid.ts +261 -17
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
- package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
- package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
- package/dist/assets/browserAll-CEkWYlFk.js +0 -1
- package/dist/assets/create-runtime-DYIWr3va.js +0 -1
- package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
- package/dist/assets/game-8SDZPPgH.js +0 -1
- package/dist/assets/game-CevorxyV.js +0 -1
- package/dist/assets/game-Di7XlywD.js +0 -1
- package/dist/assets/game-DjnCaw8R.js +0 -1
- package/dist/assets/game-Q9A0aXSV.js +0 -1
- package/dist/assets/index-CPVV6pjw.js +0 -1
- package/dist/assets/index-CpOnYyeD.js +0 -213
- package/dist/assets/index-Dr8H_6Rl.js +0 -1
- package/dist/assets/index-_bCaipUk.js +0 -13650
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
- package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
- package/dist/assets/scene-query-BivAHhJu.js +0 -1
- package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
- package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
- package/src/authoring/scene-ui-layer.ts +0 -485
- package/src/authoring/ui-authoring-adapter.ts +0 -325
- package/src/authoring/ui-hierarchy-menu.ts +0 -40
- package/src/authoring/ui-inspector-sections.tsx +0 -156
- package/src/authoring/undo-timeline.ts +0 -145
- package/src/components/BottomPanel.tsx +0 -326
- package/src/components/BuildPanel.tsx +0 -455
- package/src/components/FolderTree.tsx +0 -179
- package/src/components/RightPanel.tsx +0 -166
- package/src/components/WorldContextMenu.tsx +0 -257
- package/src/components/asset-viewers/AssetViewer.tsx +0 -38
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
- package/src/scene-ui-mode/active-scene-ui.ts +0 -58
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
- package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
- package/src/ui-editor/inspector.tsx +0 -563
- package/src/ui-editor/node-ops.ts +0 -266
- package/src/ui-editor/overlay.tsx +0 -380
- package/src/ui-editor/palette.tsx +0 -114
- package/src/ui-editor/react-store.ts +0 -78
- package/src/ui-editor/timeline-panel.tsx +0 -443
- package/src/ui-editor/ui-edit-store.ts +0 -552
- package/src/ui-editor/ui-prefab.ts +0 -95
- package/template/project.json +0 -9
- package/template/src/scripts/main.ts +0 -94
- package/template/src/ui/DefaultHud.tsx +0 -40
- package/template/src/ui/game-state.tsx +0 -37
- package/template/src/ui/mount-default-hud.tsx +0 -19
- package/template/src/ui/mount-ui.ts +0 -15
- package/vite-plugin-react-world-provider.ts +0 -81
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import { gltfLoader } from '@engine/loader';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
|
|
4
|
+
export const ASSET_PREVIEW_VIEWS = ['front', 'right', 'top', 'perspective'] as const;
|
|
5
|
+
export type AssetPreviewView = (typeof ASSET_PREVIEW_VIEWS)[number];
|
|
6
|
+
export type AssetPreviewBackground = 'neutral' | 'transparent';
|
|
7
|
+
|
|
8
|
+
export interface AssetPreviewOptions {
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
background?: AssetPreviewBackground;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AssetPreviewImage {
|
|
15
|
+
view: AssetPreviewView;
|
|
16
|
+
base64: string;
|
|
17
|
+
mimeType: 'image/png';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AssetPreviewCapture {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
views: AssetPreviewImage[];
|
|
24
|
+
contactSheet: {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
base64: string;
|
|
28
|
+
mimeType: 'image/png';
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AssetPreviewFraming {
|
|
33
|
+
center: THREE.Vector3;
|
|
34
|
+
size: THREE.Vector3;
|
|
35
|
+
radius: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type MorphableObject = THREE.Object3D & {
|
|
39
|
+
morphTargetDictionary?: Record<string, number>;
|
|
40
|
+
morphTargetInfluences?: number[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const DEFAULT_SIZE = 512;
|
|
44
|
+
const MIN_SIZE = 64;
|
|
45
|
+
// Four views plus the 2x2 sheet cross the editor relay as base64 JSON. At
|
|
46
|
+
// 1024px even incompressible RGBA data remains below its 50 MB request limit.
|
|
47
|
+
const MAX_SIZE = 1024;
|
|
48
|
+
const PADDING = 1.2;
|
|
49
|
+
const NEUTRAL_COLOR = 0x20242a;
|
|
50
|
+
const EDITOR_LAYER = 31;
|
|
51
|
+
const MODEL_FETCH_TIMEOUT_MS = 25_000;
|
|
52
|
+
const MAX_MODEL_BYTES = 64 * 1024 * 1024;
|
|
53
|
+
|
|
54
|
+
const LABEL_GLYPHS: Record<string, readonly string[]> = {
|
|
55
|
+
F: ['11111', '10000', '10000', '11110', '10000', '10000', '10000'],
|
|
56
|
+
R: ['11110', '10001', '10001', '11110', '10100', '10010', '10001'],
|
|
57
|
+
O: ['01110', '10001', '10001', '10001', '10001', '10001', '01110'],
|
|
58
|
+
N: ['10001', '11001', '11001', '10101', '10011', '10011', '10001'],
|
|
59
|
+
T: ['11111', '00100', '00100', '00100', '00100', '00100', '00100'],
|
|
60
|
+
I: ['11111', '00100', '00100', '00100', '00100', '00100', '11111'],
|
|
61
|
+
G: ['01110', '10001', '10000', '10111', '10001', '10001', '01110'],
|
|
62
|
+
H: ['10001', '10001', '10001', '11111', '10001', '10001', '10001'],
|
|
63
|
+
P: ['11110', '10001', '10001', '11110', '10000', '10000', '10000'],
|
|
64
|
+
'3': ['11110', '00001', '00001', '01110', '00001', '00001', '11110'],
|
|
65
|
+
'/': ['00001', '00010', '00100', '00100', '01000', '10000', '10000'],
|
|
66
|
+
'4': ['00010', '00110', '01010', '10010', '11111', '00010', '00010'],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function checkedDimension(value: number | undefined): number {
|
|
70
|
+
const resolved = value ?? DEFAULT_SIZE;
|
|
71
|
+
if (!Number.isInteger(resolved) || resolved < MIN_SIZE || resolved > MAX_SIZE) {
|
|
72
|
+
throw new Error(`Asset preview dimensions must be integers from ${MIN_SIZE} to ${MAX_SIZE}.`);
|
|
73
|
+
}
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function pngBase64(canvas: HTMLCanvasElement): string {
|
|
78
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
79
|
+
const comma = dataUrl.indexOf(',');
|
|
80
|
+
return comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a render-only snapshot of an authored subtree. The source remains the
|
|
85
|
+
* live Object3D truth: it is never reparented, hidden, normalized, or edited.
|
|
86
|
+
* Skinned, morphed, and instanced render semantics are preserved while the
|
|
87
|
+
* snapshot shares immutable geometry/material resources.
|
|
88
|
+
*/
|
|
89
|
+
export function createAssetPreviewSnapshot(source: THREE.Object3D): THREE.Object3D {
|
|
90
|
+
// A scene entity can legally reference bones owned above/outside its selected
|
|
91
|
+
// root (for example a visual child attached beneath an entity shell). Such a
|
|
92
|
+
// partial skeleton cannot be rebound honestly inside an isolated clone, so
|
|
93
|
+
// it takes the explicit bind-geometry fallback below.
|
|
94
|
+
const sourceNodes = new Set<THREE.Object3D>();
|
|
95
|
+
let hasExternalBones = false;
|
|
96
|
+
source.traverse((object) => {
|
|
97
|
+
sourceNodes.add(object);
|
|
98
|
+
});
|
|
99
|
+
source.traverse((object) => {
|
|
100
|
+
const skinned = object as THREE.SkinnedMesh;
|
|
101
|
+
if (!skinned.isSkinnedMesh) return;
|
|
102
|
+
if (skinned.skeleton.bones.some((bone) => !bone || !sourceNodes.has(bone))) {
|
|
103
|
+
hasExternalBones = true;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (hasExternalBones) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
'Asset preview selection references bones outside its hierarchy. Select the complete skeleton root.',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const snapshot = cloneRenderableHierarchy(source);
|
|
112
|
+
|
|
113
|
+
// Preserve the selected root's authored world-space presentation even when
|
|
114
|
+
// it lives below transformed parents. Descendant local transforms are kept.
|
|
115
|
+
// Keep the exact composed matrix. Decomposing a rotated, non-uniformly
|
|
116
|
+
// scaled ancestor chain into TRS would discard shear and change geometry.
|
|
117
|
+
const exactWorldMatrix = objectWorldMatrix(source);
|
|
118
|
+
snapshot.matrix.copy(exactWorldMatrix);
|
|
119
|
+
snapshot.matrix.decompose(snapshot.position, snapshot.quaternion, snapshot.scale);
|
|
120
|
+
snapshot.matrixAutoUpdate = false;
|
|
121
|
+
snapshot.matrixWorld.copy(exactWorldMatrix);
|
|
122
|
+
snapshot.matrixWorldAutoUpdate = source.matrixWorldAutoUpdate;
|
|
123
|
+
// Isolation intentionally reveals a selected root that is hidden in the
|
|
124
|
+
// scene; descendant visibility remains authored and the source is untouched.
|
|
125
|
+
snapshot.visible = true;
|
|
126
|
+
|
|
127
|
+
// Editor-only helpers are presentation siblings/children, never asset data.
|
|
128
|
+
const helpers: THREE.Object3D[] = [];
|
|
129
|
+
snapshot.traverse((object) => {
|
|
130
|
+
if (object !== snapshot && object.userData['editorHelper']) helpers.push(object);
|
|
131
|
+
});
|
|
132
|
+
for (const helper of helpers) helper.parent?.remove(helper);
|
|
133
|
+
|
|
134
|
+
return snapshot;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function objectWorldMatrix(object: THREE.Object3D): THREE.Matrix4 {
|
|
138
|
+
const chain: THREE.Object3D[] = [];
|
|
139
|
+
let current: THREE.Object3D | null = object;
|
|
140
|
+
while (current) {
|
|
141
|
+
chain.push(current);
|
|
142
|
+
current = current.parent;
|
|
143
|
+
}
|
|
144
|
+
const manualBoundary = chain.findIndex((node) => !node.matrixWorldAutoUpdate);
|
|
145
|
+
const world =
|
|
146
|
+
manualBoundary >= 0 ? chain[manualBoundary]!.matrixWorld.clone() : new THREE.Matrix4();
|
|
147
|
+
const local = new THREE.Matrix4();
|
|
148
|
+
const firstLocal = manualBoundary >= 0 ? manualBoundary - 1 : chain.length - 1;
|
|
149
|
+
for (let index = firstLocal; index >= 0; index--) {
|
|
150
|
+
const node = chain[index]!;
|
|
151
|
+
if (node.matrixAutoUpdate) local.compose(node.position, node.quaternion, node.scale);
|
|
152
|
+
else local.copy(node.matrix);
|
|
153
|
+
world.multiply(local);
|
|
154
|
+
}
|
|
155
|
+
return world;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Clone a complete authored hierarchy without copying editor-only helpers or
|
|
160
|
+
* arbitrary userData. Object3D.clone JSON-serializes userData, which is unsafe
|
|
161
|
+
* for live AnimationMixer references, while SkeletonUtils.clone first invokes
|
|
162
|
+
* that same recursive clone. This direct render clone keeps Three.js object
|
|
163
|
+
* semantics and remaps complete internal skeletons after building the tree.
|
|
164
|
+
*/
|
|
165
|
+
function cloneRenderableHierarchy(source: THREE.Object3D): THREE.Object3D {
|
|
166
|
+
const cloneBySource = new Map<THREE.Object3D, THREE.Object3D>();
|
|
167
|
+
const skinnedPairs: Array<[THREE.SkinnedMesh, THREE.SkinnedMesh]> = [];
|
|
168
|
+
|
|
169
|
+
const cloneNode = (object: THREE.Object3D): THREE.Object3D => {
|
|
170
|
+
const clone = createRenderClone(object);
|
|
171
|
+
cloneBySource.set(object, clone);
|
|
172
|
+
copyObjectPresentation(object, clone);
|
|
173
|
+
if ((object as THREE.SkinnedMesh).isSkinnedMesh) {
|
|
174
|
+
skinnedPairs.push([object as THREE.SkinnedMesh, clone as THREE.SkinnedMesh]);
|
|
175
|
+
}
|
|
176
|
+
const sourceLod = object as THREE.LOD;
|
|
177
|
+
if (sourceLod.isLOD) {
|
|
178
|
+
const cloneLod = clone as THREE.LOD;
|
|
179
|
+
// A deterministic still preview freezes auto-updating LODs to one level.
|
|
180
|
+
// Manually controlled LODs retain their exact authored visibility, which
|
|
181
|
+
// may intentionally contain zero or several visible levels.
|
|
182
|
+
const visibleLevels = sourceLod.levels.filter((level) => level.object.visible);
|
|
183
|
+
const activeSourceLevel = visibleLevels.length === 1 ? visibleLevels[0] : sourceLod.levels[0];
|
|
184
|
+
cloneLod.autoUpdate = false;
|
|
185
|
+
const levelObjects = new Set(sourceLod.levels.map((level) => level.object));
|
|
186
|
+
for (const level of sourceLod.levels) {
|
|
187
|
+
if (isEditorPresentation(level.object)) continue;
|
|
188
|
+
const levelClone = cloneNode(level.object);
|
|
189
|
+
levelClone.visible = sourceLod.autoUpdate
|
|
190
|
+
? level === activeSourceLevel
|
|
191
|
+
: level.object.visible;
|
|
192
|
+
cloneLod.addLevel(levelClone, level.distance, level.hysteresis);
|
|
193
|
+
}
|
|
194
|
+
for (const child of object.children) {
|
|
195
|
+
if (!levelObjects.has(child) && !isEditorPresentation(child)) clone.add(cloneNode(child));
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
for (const child of object.children) {
|
|
199
|
+
if (!isEditorPresentation(child)) clone.add(cloneNode(child));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return clone;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const root = cloneNode(source);
|
|
206
|
+
for (const [original, clone] of skinnedPairs) {
|
|
207
|
+
const skeleton = original.skeleton.clone();
|
|
208
|
+
skeleton.bones = original.skeleton.bones.map((bone) => {
|
|
209
|
+
const mapped = cloneBySource.get(bone);
|
|
210
|
+
if (!(mapped as THREE.Bone | undefined)?.isBone) {
|
|
211
|
+
throw new Error(`Asset preview could not map skeleton bone: ${bone.name || bone.uuid}`);
|
|
212
|
+
}
|
|
213
|
+
return mapped as THREE.Bone;
|
|
214
|
+
});
|
|
215
|
+
clone.skeleton = skeleton;
|
|
216
|
+
clone.bindMode = original.bindMode;
|
|
217
|
+
clone.bindMatrix.copy(original.bindMatrix);
|
|
218
|
+
clone.bindMatrixInverse.copy(original.bindMatrixInverse);
|
|
219
|
+
}
|
|
220
|
+
return root;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function createRenderClone(object: THREE.Object3D): THREE.Object3D {
|
|
224
|
+
const skinned = object as THREE.SkinnedMesh;
|
|
225
|
+
if (skinned.isSkinnedMesh) return new THREE.SkinnedMesh(skinned.geometry, skinned.material);
|
|
226
|
+
const instanced = object as THREE.InstancedMesh;
|
|
227
|
+
if (instanced.isInstancedMesh) {
|
|
228
|
+
const clone = new THREE.InstancedMesh(instanced.geometry, instanced.material, instanced.count);
|
|
229
|
+
clone.instanceMatrix.copy(instanced.instanceMatrix);
|
|
230
|
+
clone.instanceColor = instanced.instanceColor
|
|
231
|
+
? (instanced.instanceColor.clone() as THREE.InstancedBufferAttribute)
|
|
232
|
+
: null;
|
|
233
|
+
clone.morphTexture = instanced.morphTexture;
|
|
234
|
+
return clone;
|
|
235
|
+
}
|
|
236
|
+
const mesh = object as THREE.Mesh;
|
|
237
|
+
if (mesh.isMesh) return new THREE.Mesh(mesh.geometry, mesh.material);
|
|
238
|
+
const line = object as THREE.Line;
|
|
239
|
+
if ((object as THREE.LineSegments).isLineSegments) {
|
|
240
|
+
return new THREE.LineSegments(line.geometry, line.material);
|
|
241
|
+
}
|
|
242
|
+
if ((object as THREE.LineLoop).isLineLoop)
|
|
243
|
+
return new THREE.LineLoop(line.geometry, line.material);
|
|
244
|
+
if (line.isLine) return new THREE.Line(line.geometry, line.material);
|
|
245
|
+
const points = object as THREE.Points;
|
|
246
|
+
if (points.isPoints) return new THREE.Points(points.geometry, points.material);
|
|
247
|
+
const sprite = object as THREE.Sprite;
|
|
248
|
+
if (sprite.isSprite) return new THREE.Sprite(sprite.material);
|
|
249
|
+
if ((object as THREE.LOD).isLOD) return new THREE.LOD();
|
|
250
|
+
if ((object as THREE.Bone).isBone) return new THREE.Bone();
|
|
251
|
+
if ((object as THREE.Group).isGroup) return new THREE.Group();
|
|
252
|
+
return new THREE.Object3D();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function copyObjectPresentation(source: THREE.Object3D, clone: THREE.Object3D): void {
|
|
256
|
+
clone.name = source.name;
|
|
257
|
+
clone.up.copy(source.up);
|
|
258
|
+
clone.position.copy(source.position);
|
|
259
|
+
clone.quaternion.copy(source.quaternion);
|
|
260
|
+
clone.scale.copy(source.scale);
|
|
261
|
+
clone.matrix.copy(source.matrix);
|
|
262
|
+
clone.matrixWorld.copy(source.matrixWorld);
|
|
263
|
+
clone.matrixAutoUpdate = source.matrixAutoUpdate;
|
|
264
|
+
clone.matrixWorldAutoUpdate = source.matrixWorldAutoUpdate;
|
|
265
|
+
clone.visible = source.visible;
|
|
266
|
+
clone.castShadow = source.castShadow;
|
|
267
|
+
clone.receiveShadow = source.receiveShadow;
|
|
268
|
+
// Separate preview renderers must not populate boundingSphere on geometry
|
|
269
|
+
// shared read-only with the live source. The deterministic cameras already
|
|
270
|
+
// frame the snapshot, so clone-only culling is unnecessary.
|
|
271
|
+
clone.frustumCulled = false;
|
|
272
|
+
clone.renderOrder = source.renderOrder;
|
|
273
|
+
// Asset Editor is a presentation scene with layer-0 cameras. Normalize only
|
|
274
|
+
// the clone so authored custom-layer content is visible without changing the
|
|
275
|
+
// live entity's layer assignment.
|
|
276
|
+
clone.layers.set(0);
|
|
277
|
+
clone.animations = [...source.animations];
|
|
278
|
+
|
|
279
|
+
const sourceMorph = source as MorphableObject;
|
|
280
|
+
const cloneMorph = clone as MorphableObject;
|
|
281
|
+
if (sourceMorph.morphTargetInfluences) {
|
|
282
|
+
if (sourceMorph.morphTargetDictionary) {
|
|
283
|
+
cloneMorph.morphTargetDictionary = { ...sourceMorph.morphTargetDictionary };
|
|
284
|
+
}
|
|
285
|
+
cloneMorph.morphTargetInfluences = [...sourceMorph.morphTargetInfluences];
|
|
286
|
+
}
|
|
287
|
+
const sourceSprite = source as THREE.Sprite;
|
|
288
|
+
if (sourceSprite.isSprite) (clone as THREE.Sprite).center.copy(sourceSprite.center);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isEditorPresentation(object: THREE.Object3D): boolean {
|
|
292
|
+
const editorLayerMask = 1 << EDITOR_LAYER;
|
|
293
|
+
return Boolean(
|
|
294
|
+
object.userData['editorHelper'] ||
|
|
295
|
+
object.userData['editorIcon'] ||
|
|
296
|
+
(object.layers.mask & editorLayerMask) !== 0,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function disposeAssetPreviewSnapshot(root: THREE.Object3D): void {
|
|
301
|
+
const skeletons = new Set<THREE.Skeleton>();
|
|
302
|
+
root.traverse((object) => {
|
|
303
|
+
const skinned = object as THREE.SkinnedMesh;
|
|
304
|
+
if (skinned.isSkinnedMesh) skeletons.add(skinned.skeleton);
|
|
305
|
+
});
|
|
306
|
+
for (const skeleton of skeletons) skeleton.dispose();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function measureAssetPreview(
|
|
310
|
+
object: THREE.Object3D,
|
|
311
|
+
view: AssetPreviewView = 'front',
|
|
312
|
+
): AssetPreviewFraming {
|
|
313
|
+
object.updateWorldMatrix(true, true);
|
|
314
|
+
const box = readOnlyRenderableBounds(object, view);
|
|
315
|
+
if (box.isEmpty()) throw new Error('Asset preview source has no renderable bounds.');
|
|
316
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
317
|
+
const size = box.getSize(new THREE.Vector3());
|
|
318
|
+
return {
|
|
319
|
+
center,
|
|
320
|
+
size,
|
|
321
|
+
radius: Math.max(size.length() / 2, 0.001),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Read exact renderable vertices without populating shared geometry bounds. */
|
|
326
|
+
function readOnlyRenderableBounds(root: THREE.Object3D, view: AssetPreviewView): THREE.Box3 {
|
|
327
|
+
const box = new THREE.Box3();
|
|
328
|
+
const vertex = new THREE.Vector3();
|
|
329
|
+
const instanceMatrix = new THREE.Matrix4();
|
|
330
|
+
const basis = assetViewBasis(view);
|
|
331
|
+
const visit = (object: THREE.Object3D): void => {
|
|
332
|
+
if (!object.visible) return;
|
|
333
|
+
const lod = object as THREE.LOD;
|
|
334
|
+
if (lod.isLOD && lod.levels.length > 0) {
|
|
335
|
+
const levelObjects = new Set(lod.levels.map((level) => level.object));
|
|
336
|
+
for (const level of lod.levels) visit(level.object);
|
|
337
|
+
for (const child of object.children) {
|
|
338
|
+
if (!levelObjects.has(child)) visit(child);
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const sprite = object as THREE.Sprite;
|
|
343
|
+
if (sprite.isSprite) {
|
|
344
|
+
if (sprite.material.visible) {
|
|
345
|
+
if (view === 'perspective' && !sprite.material.sizeAttenuation) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
'Asset preview cannot deterministically frame a perspective Sprite with sizeAttenuation disabled.',
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const anchor = new THREE.Vector3().setFromMatrixPosition(sprite.matrixWorld);
|
|
351
|
+
const scale = new THREE.Vector3().setFromMatrixScale(sprite.matrixWorld);
|
|
352
|
+
const cosine = Math.cos(sprite.material.rotation);
|
|
353
|
+
const sine = Math.sin(sprite.material.rotation);
|
|
354
|
+
for (const x of [-sprite.center.x, 1 - sprite.center.x]) {
|
|
355
|
+
for (const y of [-sprite.center.y, 1 - sprite.center.y]) {
|
|
356
|
+
const scaledX = x * Math.abs(scale.x);
|
|
357
|
+
const scaledY = y * Math.abs(scale.y);
|
|
358
|
+
const rotatedX = cosine * scaledX - sine * scaledY;
|
|
359
|
+
const rotatedY = sine * scaledX + cosine * scaledY;
|
|
360
|
+
box.expandByPoint(
|
|
361
|
+
anchor
|
|
362
|
+
.clone()
|
|
363
|
+
.addScaledVector(basis.right, rotatedX)
|
|
364
|
+
.addScaledVector(basis.up, rotatedY),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Sprite's internal unit quad is shader-billboarded above; treating that
|
|
371
|
+
// private geometry as an ordinary world-space mesh would double-count it.
|
|
372
|
+
const geometry = sprite.isSprite
|
|
373
|
+
? undefined
|
|
374
|
+
: (object as THREE.Mesh | THREE.Line | THREE.Points).geometry;
|
|
375
|
+
if (geometry) {
|
|
376
|
+
const position = geometry.getAttribute('position');
|
|
377
|
+
if (position) {
|
|
378
|
+
const renderedIndices = collectRenderedVertexIndices(object, geometry, position.count);
|
|
379
|
+
const instanced = object as THREE.InstancedMesh;
|
|
380
|
+
if (instanced.isInstancedMesh) {
|
|
381
|
+
const morphProbe = new THREE.Mesh(instanced.geometry, instanced.material);
|
|
382
|
+
for (let instance = 0; instance < instanced.count; instance++) {
|
|
383
|
+
instanced.getMatrixAt(instance, instanceMatrix);
|
|
384
|
+
if (instanced.morphTexture) instanced.getMorphAt(instance, morphProbe);
|
|
385
|
+
for (const index of renderedIndices) {
|
|
386
|
+
morphProbe.getVertexPosition(index, vertex);
|
|
387
|
+
vertex.applyMatrix4(instanceMatrix).applyMatrix4(instanced.matrixWorld);
|
|
388
|
+
box.expandByPoint(vertex);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
const mesh = object as THREE.Mesh;
|
|
393
|
+
for (const index of renderedIndices) {
|
|
394
|
+
if (mesh.isMesh) mesh.getVertexPosition(index, vertex);
|
|
395
|
+
else getNonMeshVertexPosition(object as MorphableObject, geometry, index, vertex);
|
|
396
|
+
vertex.applyMatrix4(object.matrixWorld);
|
|
397
|
+
box.expandByPoint(vertex);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const child of object.children) visit(child);
|
|
403
|
+
};
|
|
404
|
+
visit(root);
|
|
405
|
+
return box;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function collectRenderedVertexIndices(
|
|
409
|
+
object: THREE.Object3D,
|
|
410
|
+
geometry: THREE.BufferGeometry,
|
|
411
|
+
positionCount: number,
|
|
412
|
+
): number[] {
|
|
413
|
+
const material = (object as THREE.Mesh | THREE.Line | THREE.Points).material;
|
|
414
|
+
if (!material) return [];
|
|
415
|
+
const elementCount = geometry.index?.count ?? positionCount;
|
|
416
|
+
const drawStart = Math.max(0, geometry.drawRange.start);
|
|
417
|
+
const drawEnd = Math.min(
|
|
418
|
+
elementCount,
|
|
419
|
+
Number.isFinite(geometry.drawRange.count) ? drawStart + geometry.drawRange.count : elementCount,
|
|
420
|
+
);
|
|
421
|
+
const ranges: Array<[number, number]> = [];
|
|
422
|
+
if (Array.isArray(material)) {
|
|
423
|
+
for (const group of geometry.groups) {
|
|
424
|
+
const materialIndex = group.materialIndex ?? 0;
|
|
425
|
+
if (!material[materialIndex]?.visible) continue;
|
|
426
|
+
const start = Math.max(drawStart, group.start);
|
|
427
|
+
const end = Math.min(drawEnd, group.start + group.count);
|
|
428
|
+
if (end > start) ranges.push([start, end]);
|
|
429
|
+
}
|
|
430
|
+
} else if (material.visible && drawEnd > drawStart) {
|
|
431
|
+
ranges.push([drawStart, drawEnd]);
|
|
432
|
+
}
|
|
433
|
+
const indices: number[] = [];
|
|
434
|
+
for (const [start, end] of ranges) {
|
|
435
|
+
const count = assembledElementCount(object, end - start);
|
|
436
|
+
for (let element = start; element < start + count; element++) {
|
|
437
|
+
indices.push(geometry.index ? geometry.index.getX(element) : element);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return indices;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function assembledElementCount(object: THREE.Object3D, count: number): number {
|
|
444
|
+
if ((object as THREE.Mesh).isMesh) return count - (count % 3);
|
|
445
|
+
if ((object as THREE.LineSegments).isLineSegments) return count - (count % 2);
|
|
446
|
+
if ((object as THREE.Line).isLine) return count >= 2 ? count : 0;
|
|
447
|
+
return count;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function getNonMeshVertexPosition(
|
|
451
|
+
object: MorphableObject,
|
|
452
|
+
geometry: THREE.BufferGeometry,
|
|
453
|
+
index: number,
|
|
454
|
+
target: THREE.Vector3,
|
|
455
|
+
): THREE.Vector3 {
|
|
456
|
+
const position = geometry.getAttribute('position');
|
|
457
|
+
target.fromBufferAttribute(position, index);
|
|
458
|
+
const morphPositions = geometry.morphAttributes['position'];
|
|
459
|
+
const influences = object.morphTargetInfluences;
|
|
460
|
+
if (!morphPositions || !influences) return target;
|
|
461
|
+
const base = target.clone();
|
|
462
|
+
const delta = new THREE.Vector3();
|
|
463
|
+
const sample = new THREE.Vector3();
|
|
464
|
+
for (let morphIndex = 0; morphIndex < morphPositions.length; morphIndex++) {
|
|
465
|
+
const influence = influences[morphIndex] ?? 0;
|
|
466
|
+
if (influence === 0) continue;
|
|
467
|
+
sample.fromBufferAttribute(morphPositions[morphIndex]!, index);
|
|
468
|
+
if (!geometry.morphTargetsRelative) sample.sub(base);
|
|
469
|
+
delta.addScaledVector(sample, influence);
|
|
470
|
+
}
|
|
471
|
+
return target.add(delta);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function orthographicCamera(
|
|
475
|
+
view: Exclude<AssetPreviewView, 'perspective'>,
|
|
476
|
+
framing: AssetPreviewFraming,
|
|
477
|
+
aspect: number,
|
|
478
|
+
): THREE.OrthographicCamera {
|
|
479
|
+
const { center, size, radius } = framing;
|
|
480
|
+
let horizontal = size.x;
|
|
481
|
+
let vertical = size.y;
|
|
482
|
+
const basis = assetViewBasis(view);
|
|
483
|
+
const { direction, up } = basis;
|
|
484
|
+
|
|
485
|
+
if (view === 'right') {
|
|
486
|
+
horizontal = size.z;
|
|
487
|
+
} else {
|
|
488
|
+
if (view === 'top') {
|
|
489
|
+
horizontal = size.x;
|
|
490
|
+
vertical = size.z;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const halfHeight = Math.max(vertical / 2, horizontal / (2 * aspect), 0.001) * PADDING;
|
|
495
|
+
const halfWidth = halfHeight * aspect;
|
|
496
|
+
const camera = new THREE.OrthographicCamera(
|
|
497
|
+
-halfWidth,
|
|
498
|
+
halfWidth,
|
|
499
|
+
halfHeight,
|
|
500
|
+
-halfHeight,
|
|
501
|
+
0.01,
|
|
502
|
+
radius * 10 + 10,
|
|
503
|
+
);
|
|
504
|
+
camera.position.copy(center).addScaledVector(direction, radius * 3 + 1);
|
|
505
|
+
camera.up.copy(up);
|
|
506
|
+
camera.lookAt(center);
|
|
507
|
+
camera.updateProjectionMatrix();
|
|
508
|
+
camera.updateMatrixWorld(true);
|
|
509
|
+
return camera;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function perspectiveCamera(framing: AssetPreviewFraming, aspect: number): THREE.PerspectiveCamera {
|
|
513
|
+
const camera = new THREE.PerspectiveCamera(35, aspect, 0.01, framing.radius * 20 + 20);
|
|
514
|
+
const halfVerticalFov = THREE.MathUtils.degToRad(camera.fov / 2);
|
|
515
|
+
const halfHorizontalFov = Math.atan(Math.tan(halfVerticalFov) * aspect);
|
|
516
|
+
const { direction: viewDirection, right, up } = assetViewBasis('perspective');
|
|
517
|
+
const half = framing.size.clone().multiplyScalar(0.5);
|
|
518
|
+
let distance = 0;
|
|
519
|
+
for (const x of [-1, 1]) {
|
|
520
|
+
for (const y of [-1, 1]) {
|
|
521
|
+
for (const z of [-1, 1]) {
|
|
522
|
+
const offset = new THREE.Vector3(x * half.x, y * half.y, z * half.z);
|
|
523
|
+
const towardCamera = offset.dot(viewDirection);
|
|
524
|
+
distance = Math.max(
|
|
525
|
+
distance,
|
|
526
|
+
towardCamera + (Math.abs(offset.dot(right)) * PADDING) / Math.tan(halfHorizontalFov),
|
|
527
|
+
towardCamera + (Math.abs(offset.dot(up)) * PADDING) / Math.tan(halfVerticalFov),
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
camera.position.copy(framing.center).addScaledVector(viewDirection, Math.max(distance, 0.01));
|
|
533
|
+
camera.lookAt(framing.center);
|
|
534
|
+
camera.updateProjectionMatrix();
|
|
535
|
+
camera.updateMatrixWorld(true);
|
|
536
|
+
return camera;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function assetViewBasis(view: AssetPreviewView): {
|
|
540
|
+
direction: THREE.Vector3;
|
|
541
|
+
right: THREE.Vector3;
|
|
542
|
+
up: THREE.Vector3;
|
|
543
|
+
} {
|
|
544
|
+
const direction =
|
|
545
|
+
view === 'front'
|
|
546
|
+
? new THREE.Vector3(0, 0, 1)
|
|
547
|
+
: view === 'right'
|
|
548
|
+
? new THREE.Vector3(1, 0, 0)
|
|
549
|
+
: view === 'top'
|
|
550
|
+
? new THREE.Vector3(0, 1, 0)
|
|
551
|
+
: new THREE.Vector3(1, 0.72, 1).normalize();
|
|
552
|
+
const provisionalUp = view === 'top' ? new THREE.Vector3(0, 0, -1) : new THREE.Vector3(0, 1, 0);
|
|
553
|
+
const right = provisionalUp.clone().cross(direction).normalize();
|
|
554
|
+
const up = direction.clone().cross(right).normalize();
|
|
555
|
+
return { direction, right, up };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function createAssetPreviewCamera(
|
|
559
|
+
view: AssetPreviewView,
|
|
560
|
+
framing: AssetPreviewFraming,
|
|
561
|
+
aspect: number,
|
|
562
|
+
): THREE.Camera {
|
|
563
|
+
return view === 'perspective'
|
|
564
|
+
? perspectiveCamera(framing, aspect)
|
|
565
|
+
: orthographicCamera(view, framing, aspect);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function createPreviewScene(snapshot: THREE.Object3D, framing: AssetPreviewFraming): THREE.Scene {
|
|
569
|
+
const scene = new THREE.Scene();
|
|
570
|
+
scene.add(snapshot);
|
|
571
|
+
scene.add(new THREE.HemisphereLight(0xffffff, 0x354052, 1.8));
|
|
572
|
+
const key = new THREE.DirectionalLight(0xffffff, 2.2);
|
|
573
|
+
key.position
|
|
574
|
+
.copy(framing.center)
|
|
575
|
+
.addScaledVector(new THREE.Vector3(1, 1.5, 1), framing.radius * 3);
|
|
576
|
+
scene.add(key);
|
|
577
|
+
const fill = new THREE.DirectionalLight(0xbfd7ff, 0.8);
|
|
578
|
+
fill.position
|
|
579
|
+
.copy(framing.center)
|
|
580
|
+
.addScaledVector(new THREE.Vector3(-1, 0.5, -0.7), framing.radius * 2);
|
|
581
|
+
scene.add(fill);
|
|
582
|
+
return scene;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function drawBitmapLabel(
|
|
586
|
+
context: CanvasRenderingContext2D,
|
|
587
|
+
label: string,
|
|
588
|
+
x: number,
|
|
589
|
+
y: number,
|
|
590
|
+
cellWidth: number,
|
|
591
|
+
cellHeight: number,
|
|
592
|
+
): void {
|
|
593
|
+
const scale = Math.max(1, Math.floor(Math.min(cellWidth, cellHeight) / 128));
|
|
594
|
+
const padding = scale * 3;
|
|
595
|
+
const advance = scale * 6;
|
|
596
|
+
const width = label.length * advance - scale + padding * 2;
|
|
597
|
+
const height = scale * 7 + padding * 2;
|
|
598
|
+
context.fillStyle = 'rgba(0, 0, 0, 0.62)';
|
|
599
|
+
context.fillRect(x, y, width, height);
|
|
600
|
+
context.fillStyle = '#ffffff';
|
|
601
|
+
for (let glyphIndex = 0; glyphIndex < label.length; glyphIndex++) {
|
|
602
|
+
const glyph = LABEL_GLYPHS[label[glyphIndex]!]!;
|
|
603
|
+
for (let row = 0; row < glyph.length; row++) {
|
|
604
|
+
for (let column = 0; column < glyph[row]!.length; column++) {
|
|
605
|
+
if (glyph[row]![column] === '1') {
|
|
606
|
+
context.fillRect(
|
|
607
|
+
x + padding + glyphIndex * advance + column * scale,
|
|
608
|
+
y + padding + row * scale,
|
|
609
|
+
scale,
|
|
610
|
+
scale,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function captureObjectAssetPreview(
|
|
619
|
+
source: THREE.Object3D,
|
|
620
|
+
options: AssetPreviewOptions = {},
|
|
621
|
+
): AssetPreviewCapture {
|
|
622
|
+
const width = checkedDimension(options.width);
|
|
623
|
+
const height = checkedDimension(options.height);
|
|
624
|
+
const background = options.background ?? 'neutral';
|
|
625
|
+
const snapshot = createAssetPreviewSnapshot(source);
|
|
626
|
+
let renderer: THREE.WebGLRenderer | null = null;
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const framing = measureAssetPreview(snapshot, 'perspective');
|
|
630
|
+
const scene = createPreviewScene(snapshot, framing);
|
|
631
|
+
const activeRenderer = new THREE.WebGLRenderer({
|
|
632
|
+
antialias: true,
|
|
633
|
+
alpha: true,
|
|
634
|
+
preserveDrawingBuffer: true,
|
|
635
|
+
});
|
|
636
|
+
renderer = activeRenderer;
|
|
637
|
+
activeRenderer.setPixelRatio(1);
|
|
638
|
+
activeRenderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
639
|
+
activeRenderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
640
|
+
activeRenderer.toneMappingExposure = 1;
|
|
641
|
+
activeRenderer.setClearColor(NEUTRAL_COLOR, background === 'transparent' ? 0 : 1);
|
|
642
|
+
const cameras = new Map<AssetPreviewView, THREE.Camera>();
|
|
643
|
+
for (const view of ASSET_PREVIEW_VIEWS) {
|
|
644
|
+
cameras.set(
|
|
645
|
+
view,
|
|
646
|
+
createAssetPreviewCamera(view, measureAssetPreview(snapshot, view), width / height),
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const views: AssetPreviewImage[] = [];
|
|
651
|
+
const sheetWidth = width * 2;
|
|
652
|
+
const sheetHeight = height * 2;
|
|
653
|
+
const sheetCanvas = document.createElement('canvas');
|
|
654
|
+
sheetCanvas.width = sheetWidth;
|
|
655
|
+
sheetCanvas.height = sheetHeight;
|
|
656
|
+
const sheetContext = sheetCanvas.getContext('2d');
|
|
657
|
+
if (!sheetContext) throw new Error('Unable to create the asset preview contact sheet.');
|
|
658
|
+
activeRenderer.setSize(width, height, false);
|
|
659
|
+
ASSET_PREVIEW_VIEWS.forEach((view, index) => {
|
|
660
|
+
activeRenderer.render(scene, cameras.get(view)!);
|
|
661
|
+
views.push({ view, base64: pngBase64(activeRenderer.domElement), mimeType: 'image/png' });
|
|
662
|
+
const column = index % 2;
|
|
663
|
+
const rowFromTop = Math.floor(index / 2);
|
|
664
|
+
const x = column * width;
|
|
665
|
+
const y = rowFromTop * height;
|
|
666
|
+
sheetContext.drawImage(activeRenderer.domElement, x, y, width, height);
|
|
667
|
+
const label = view === 'perspective' ? '3/4' : view.toUpperCase();
|
|
668
|
+
drawBitmapLabel(sheetContext, label, x, y, width, height);
|
|
669
|
+
});
|
|
670
|
+
const contactSheet = {
|
|
671
|
+
width: sheetWidth,
|
|
672
|
+
height: sheetHeight,
|
|
673
|
+
base64: pngBase64(sheetCanvas),
|
|
674
|
+
mimeType: 'image/png' as const,
|
|
675
|
+
};
|
|
676
|
+
return { width, height, views, contactSheet };
|
|
677
|
+
} finally {
|
|
678
|
+
renderer?.dispose();
|
|
679
|
+
renderer?.forceContextLoss();
|
|
680
|
+
disposeAssetPreviewSnapshot(snapshot);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function projectModelUrl(assetPath: string): URL {
|
|
685
|
+
if (
|
|
686
|
+
!assetPath.startsWith('/') ||
|
|
687
|
+
assetPath.startsWith('//') ||
|
|
688
|
+
assetPath.includes('\\') ||
|
|
689
|
+
assetPath.includes('?') ||
|
|
690
|
+
assetPath.includes('#')
|
|
691
|
+
) {
|
|
692
|
+
throw new Error('Asset Editor model paths must be absolute project-root .glb or .gltf paths.');
|
|
693
|
+
}
|
|
694
|
+
let decodedPath: string;
|
|
695
|
+
try {
|
|
696
|
+
decodedPath = decodeURIComponent(assetPath);
|
|
697
|
+
} catch {
|
|
698
|
+
throw new Error('Asset Editor model path contains invalid URL encoding.');
|
|
699
|
+
}
|
|
700
|
+
if (decodedPath.split('/').some((segment) => segment === '.' || segment === '..')) {
|
|
701
|
+
throw new Error('Asset Editor model paths cannot traverse outside the project root.');
|
|
702
|
+
}
|
|
703
|
+
const projectOrigin = globalThis.location?.origin ?? 'http://localhost';
|
|
704
|
+
const url = new URL(assetPath, projectOrigin);
|
|
705
|
+
if (url.origin !== projectOrigin || !url.pathname.toLowerCase().match(/\.gl(?:b|tf)$/)) {
|
|
706
|
+
throw new Error('Asset Editor loads bounded, same-origin project .glb or .gltf files only.');
|
|
707
|
+
}
|
|
708
|
+
return url;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function boundedModelBytes(
|
|
712
|
+
url: URL,
|
|
713
|
+
signal: AbortSignal,
|
|
714
|
+
maxBytes = MAX_MODEL_BYTES,
|
|
715
|
+
): Promise<ArrayBuffer> {
|
|
716
|
+
const response = await fetch(url, { signal });
|
|
717
|
+
if (!response.ok) {
|
|
718
|
+
throw new Error(`Unable to load project model (${response.status} ${response.statusText}).`);
|
|
719
|
+
}
|
|
720
|
+
const declaredLength = Number(response.headers.get('content-length'));
|
|
721
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
722
|
+
throw new Error(
|
|
723
|
+
`Asset Editor project models must be ${MAX_MODEL_BYTES / 1024 / 1024} MiB or less.`,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
if (!response.body) {
|
|
727
|
+
const bytes = await response.arrayBuffer();
|
|
728
|
+
if (bytes.byteLength > maxBytes) {
|
|
729
|
+
throw new Error(
|
|
730
|
+
`Asset Editor project models must be ${MAX_MODEL_BYTES / 1024 / 1024} MiB or less.`,
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
return bytes;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const reader = response.body.getReader();
|
|
737
|
+
const chunks: Uint8Array[] = [];
|
|
738
|
+
let total = 0;
|
|
739
|
+
try {
|
|
740
|
+
while (true) {
|
|
741
|
+
const { done, value } = await reader.read();
|
|
742
|
+
if (done) break;
|
|
743
|
+
total += value.byteLength;
|
|
744
|
+
if (total > maxBytes) {
|
|
745
|
+
await reader.cancel();
|
|
746
|
+
throw new Error(
|
|
747
|
+
`Asset Editor project models must be ${MAX_MODEL_BYTES / 1024 / 1024} MiB or less.`,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
chunks.push(value);
|
|
751
|
+
}
|
|
752
|
+
} finally {
|
|
753
|
+
reader.releaseLock();
|
|
754
|
+
}
|
|
755
|
+
const combined = new Uint8Array(total);
|
|
756
|
+
let offset = 0;
|
|
757
|
+
for (const chunk of chunks) {
|
|
758
|
+
combined.set(chunk, offset);
|
|
759
|
+
offset += chunk.byteLength;
|
|
760
|
+
}
|
|
761
|
+
return combined.buffer;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function parseGltfData(
|
|
765
|
+
data: ArrayBuffer | string,
|
|
766
|
+
sourceUrl: URL,
|
|
767
|
+
signal: AbortSignal,
|
|
768
|
+
): Promise<THREE.Object3D> {
|
|
769
|
+
return new Promise((resolve, reject) => {
|
|
770
|
+
let settled = false;
|
|
771
|
+
const finish = (callback: () => void) => {
|
|
772
|
+
if (settled) return false;
|
|
773
|
+
settled = true;
|
|
774
|
+
signal.removeEventListener('abort', abort);
|
|
775
|
+
callback();
|
|
776
|
+
return true;
|
|
777
|
+
};
|
|
778
|
+
const abort = () => finish(() => reject(signal.reason));
|
|
779
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
780
|
+
if (signal.aborted) {
|
|
781
|
+
abort();
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
gltfLoader.parse(
|
|
785
|
+
data,
|
|
786
|
+
new URL('.', sourceUrl).href,
|
|
787
|
+
(gltf) => {
|
|
788
|
+
if (!finish(() => resolve(gltf.scene))) {
|
|
789
|
+
disposeOwnedModel(gltf.scene);
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
(error) => finish(() => reject(error)),
|
|
793
|
+
);
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
type ExternalGltfResource = { uri?: unknown; mimeType?: unknown };
|
|
798
|
+
|
|
799
|
+
function projectDependencyUrl(uri: string, sourceUrl: URL): URL {
|
|
800
|
+
if (uri.includes('\\') || uri.includes('?') || uri.includes('#')) {
|
|
801
|
+
throw new Error('Asset Editor .gltf dependencies must be plain same-origin project paths.');
|
|
802
|
+
}
|
|
803
|
+
const url = new URL(uri, sourceUrl);
|
|
804
|
+
if (url.origin !== sourceUrl.origin) {
|
|
805
|
+
throw new Error('Asset Editor .gltf dependencies must stay on the project origin.');
|
|
806
|
+
}
|
|
807
|
+
return url;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function dependencyMimeType(resource: ExternalGltfResource, url: URL): string {
|
|
811
|
+
if (typeof resource.mimeType === 'string') return resource.mimeType;
|
|
812
|
+
const extension = url.pathname.split('.').pop()?.toLowerCase();
|
|
813
|
+
if (extension === 'png') return 'image/png';
|
|
814
|
+
if (extension === 'jpg' || extension === 'jpeg') return 'image/jpeg';
|
|
815
|
+
if (extension === 'webp') return 'image/webp';
|
|
816
|
+
return 'application/octet-stream';
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async function inlineExternalGltfResources(
|
|
820
|
+
bytes: ArrayBuffer,
|
|
821
|
+
sourceUrl: URL,
|
|
822
|
+
signal: AbortSignal,
|
|
823
|
+
): Promise<string> {
|
|
824
|
+
let document: Record<string, unknown>;
|
|
825
|
+
try {
|
|
826
|
+
document = JSON.parse(new TextDecoder().decode(bytes)) as Record<string, unknown>;
|
|
827
|
+
} catch {
|
|
828
|
+
throw new Error('Asset Editor could not parse the project .gltf JSON document.');
|
|
829
|
+
}
|
|
830
|
+
const resources = [
|
|
831
|
+
...(Array.isArray(document['buffers']) ? (document['buffers'] as ExternalGltfResource[]) : []),
|
|
832
|
+
...(Array.isArray(document['images']) ? (document['images'] as ExternalGltfResource[]) : []),
|
|
833
|
+
];
|
|
834
|
+
let totalBytes = bytes.byteLength;
|
|
835
|
+
for (const resource of resources) {
|
|
836
|
+
if (typeof resource.uri !== 'string' || resource.uri.startsWith('data:')) continue;
|
|
837
|
+
const dependencyUrl = projectDependencyUrl(resource.uri, sourceUrl);
|
|
838
|
+
const dependency = await boundedModelBytes(dependencyUrl, signal, MAX_MODEL_BYTES - totalBytes);
|
|
839
|
+
totalBytes += dependency.byteLength;
|
|
840
|
+
const dependencyBytes = new Uint8Array(dependency);
|
|
841
|
+
let binary = '';
|
|
842
|
+
for (let offset = 0; offset < dependencyBytes.length; offset += 32_768) {
|
|
843
|
+
binary += String.fromCharCode(...dependencyBytes.subarray(offset, offset + 32_768));
|
|
844
|
+
}
|
|
845
|
+
resource.uri = `data:${dependencyMimeType(resource, dependencyUrl)};base64,${btoa(binary)}`;
|
|
846
|
+
}
|
|
847
|
+
return JSON.stringify(document);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export async function loadProjectAssetModel(
|
|
851
|
+
assetPath: string,
|
|
852
|
+
externalSignal?: AbortSignal,
|
|
853
|
+
): Promise<THREE.Object3D> {
|
|
854
|
+
const url = projectModelUrl(assetPath);
|
|
855
|
+
const controller = new AbortController();
|
|
856
|
+
const abortFromCaller = () => controller.abort(externalSignal?.reason);
|
|
857
|
+
if (externalSignal?.aborted) abortFromCaller();
|
|
858
|
+
else externalSignal?.addEventListener('abort', abortFromCaller, { once: true });
|
|
859
|
+
const timeout = globalThis.setTimeout(
|
|
860
|
+
() =>
|
|
861
|
+
controller.abort(new Error('Asset Editor project model load timed out after 25 seconds.')),
|
|
862
|
+
MODEL_FETCH_TIMEOUT_MS,
|
|
863
|
+
);
|
|
864
|
+
try {
|
|
865
|
+
const bytes = await boundedModelBytes(url, controller.signal);
|
|
866
|
+
controller.signal.throwIfAborted();
|
|
867
|
+
if (url.pathname.toLowerCase().endsWith('.glb')) {
|
|
868
|
+
return await parseGltfData(bytes, url, controller.signal);
|
|
869
|
+
}
|
|
870
|
+
const json = await inlineExternalGltfResources(bytes, url, controller.signal);
|
|
871
|
+
return await parseGltfData(json, url, controller.signal);
|
|
872
|
+
} finally {
|
|
873
|
+
globalThis.clearTimeout(timeout);
|
|
874
|
+
externalSignal?.removeEventListener('abort', abortFromCaller);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function disposeOwnedModel(root: THREE.Object3D): void {
|
|
879
|
+
disposeAssetPreviewSnapshot(root);
|
|
880
|
+
root.traverse((object) => {
|
|
881
|
+
const instanced = object as THREE.InstancedMesh;
|
|
882
|
+
if (instanced.isInstancedMesh) instanced.morphTexture?.dispose();
|
|
883
|
+
const mesh = object as THREE.Mesh;
|
|
884
|
+
mesh.geometry?.dispose();
|
|
885
|
+
if (!mesh.material) return;
|
|
886
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
887
|
+
for (const material of materials) {
|
|
888
|
+
for (const value of Object.values(material)) {
|
|
889
|
+
if (value instanceof THREE.Texture) value.dispose();
|
|
890
|
+
}
|
|
891
|
+
material.dispose();
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export async function captureModelAssetPreview(
|
|
897
|
+
assetPath: string,
|
|
898
|
+
options: AssetPreviewOptions = {},
|
|
899
|
+
): Promise<AssetPreviewCapture> {
|
|
900
|
+
const model = await loadProjectAssetModel(assetPath);
|
|
901
|
+
try {
|
|
902
|
+
return captureObjectAssetPreview(model, options);
|
|
903
|
+
} finally {
|
|
904
|
+
disposeOwnedModel(model);
|
|
905
|
+
}
|
|
906
|
+
}
|