@vgai/editor 0.2.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/LICENSE +661 -0
- package/README.md +35 -0
- package/dist/a/b/c/test-nested.json +1 -0
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +1 -0
- package/dist/assets/SelectionBox-BXsv_6aH.js +1 -0
- package/dist/assets/browser-project-scripts-DOru4L1x.js +2 -0
- package/dist/assets/browserAll-CEkWYlFk.js +1 -0
- package/dist/assets/collision-world-DniMB0qq.glb +0 -0
- package/dist/assets/create-runtime-DYIWr3va.js +1 -0
- package/dist/assets/data-asset-Bb9sIM7i.js +5 -0
- package/dist/assets/esbuild-BHljloGq.wasm +0 -0
- package/dist/assets/fake-three-BMTQ9rl_.js +1 -0
- package/dist/assets/game-8SDZPPgH.js +1 -0
- package/dist/assets/game-BI111o5f.js +10 -0
- package/dist/assets/game-CevorxyV.js +1 -0
- package/dist/assets/game-Di1H0gFj.js +1 -0
- package/dist/assets/game-Di7XlywD.js +1 -0
- package/dist/assets/game-DjnCaw8R.js +1 -0
- package/dist/assets/game-Q9A0aXSV.js +1 -0
- package/dist/assets/game-pt_1lQLv.js +1 -0
- package/dist/assets/game-source-CvidN7AB.js +1 -0
- package/dist/assets/game-xsOho_wg.js +83 -0
- package/dist/assets/index-Bp5cecD6.js +12 -0
- package/dist/assets/index-CPVV6pjw.js +1 -0
- package/dist/assets/index-CpOnYyeD.js +213 -0
- package/dist/assets/index-Cs8xwv3e.js +1 -0
- package/dist/assets/index-DL7RcQ0n.js +27 -0
- package/dist/assets/index-Dr8H_6Rl.js +1 -0
- package/dist/assets/index-_bCaipUk.js +13650 -0
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +1 -0
- package/dist/assets/mount-manifest-Bhk0ohsl.js +1 -0
- package/dist/assets/recast-navigation.wasm-compat-CvP4UGK6.js +1 -0
- package/dist/assets/scene-query-BivAHhJu.js +1 -0
- package/dist/assets/stats.module--VATS4Kh.js +1 -0
- package/dist/assets/webworkerAll-BLrH0F6F.js +1 -0
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +1 -0
- package/dist/assets/xstate.esm-BgUD33nl.js +3 -0
- package/dist/index.html +37 -0
- package/dist/ingest/racing-game/UPSTREAM.md +68 -0
- package/dist/ingest/racing-game/a/Inter-Black.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Black.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-BlackItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-BlackItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Bold.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Bold.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-BoldItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-BoldItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBold.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBold.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLight.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLight.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Italic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Italic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Light.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Light.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-LightItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-LightItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Medium.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Medium.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-MediumItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-MediumItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Regular.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Regular.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBold.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBold.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Thin.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Thin.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ThinItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ThinItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-italic.var.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-roman.var.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter.var.woff2 +0 -0
- package/dist/ingest/racing-game/a/index.css +1 -0
- package/dist/ingest/racing-game/images/bronze.png +0 -0
- package/dist/ingest/racing-game/images/gold.png +0 -0
- package/dist/ingest/racing-game/images/silver.png +0 -0
- package/dist/ingest/racing-game/index.html +15 -0
- package/dist/ingest/racing-game/models/chassis-draco.glb +0 -0
- package/dist/ingest/racing-game/models/track-draco.glb +0 -0
- package/dist/ingest/racing-game/models/wheel-draco.glb +0 -0
- package/dist/ingest/racing-game/rg-entry.js +1 -0
- package/dist/ingest/racing-game/sounds/accelerate.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/boost.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/crash.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/engine.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/honk.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/tire-brake.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/train.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/water.mp3 +0 -0
- package/dist/ingest/racing-game/textures/cursor.svg +3 -0
- package/dist/ingest/racing-game/textures/dikhololo_night_1k.hdr +0 -0
- package/dist/ingest/racing-game/textures/heightmap_1024.png +0 -0
- package/dist/ingest/racing-game/textures/mask.svg +11 -0
- package/dist/ingest/racing-game/textures/no_sound.png +0 -0
- package/dist/ingest/racing-game/three-r139.module.js +50617 -0
- package/dist/ingest/racing-game/vendor.js +179 -0
- package/dist/ingest/racing-game/vgai.game.json +20 -0
- package/dist/ingest/simcity/UPSTREAM.md +65 -0
- package/dist/ingest/simcity/favicon.ico +0 -0
- package/dist/ingest/simcity/fonts/Bebas-Regular.otf +0 -0
- package/dist/ingest/simcity/fonts/Digitalt.otf +0 -0
- package/dist/ingest/simcity/fonts/Stanberry.ttf +0 -0
- package/dist/ingest/simcity/icons/bulldozer-color.png +0 -0
- package/dist/ingest/simcity/icons/bulldozer.png +0 -0
- package/dist/ingest/simcity/icons/calendar.png +0 -0
- package/dist/ingest/simcity/icons/factory-color.png +0 -0
- package/dist/ingest/simcity/icons/factory.png +0 -0
- package/dist/ingest/simcity/icons/house-color.png +0 -0
- package/dist/ingest/simcity/icons/house.png +0 -0
- package/dist/ingest/simcity/icons/job.png +0 -0
- package/dist/ingest/simcity/icons/office.png +0 -0
- package/dist/ingest/simcity/icons/pause-color.png +0 -0
- package/dist/ingest/simcity/icons/pause.png +0 -0
- package/dist/ingest/simcity/icons/person.png +0 -0
- package/dist/ingest/simcity/icons/play-color.png +0 -0
- package/dist/ingest/simcity/icons/play.png +0 -0
- package/dist/ingest/simcity/icons/power-color.png +0 -0
- package/dist/ingest/simcity/icons/power-line-color.png +0 -0
- package/dist/ingest/simcity/icons/power.png +0 -0
- package/dist/ingest/simcity/icons/road-color.png +0 -0
- package/dist/ingest/simcity/icons/road.png +0 -0
- package/dist/ingest/simcity/icons/select-color.png +0 -0
- package/dist/ingest/simcity/icons/select.png +0 -0
- package/dist/ingest/simcity/icons/store-color.png +0 -0
- package/dist/ingest/simcity/index.html +94 -0
- package/dist/ingest/simcity/index.js +58 -0
- package/dist/ingest/simcity/main.css +233 -0
- package/dist/ingest/simcity/models/armored-truck.glb +0 -0
- package/dist/ingest/simcity/models/building-block-4floor-corner.glb +0 -0
- package/dist/ingest/simcity/models/building-block-4floor-short.glb +0 -0
- package/dist/ingest/simcity/models/building-block-5floor.glb +0 -0
- package/dist/ingest/simcity/models/building-burger-joint.glb +0 -0
- package/dist/ingest/simcity/models/building-cafe.glb +0 -0
- package/dist/ingest/simcity/models/building-casino.glb +0 -0
- package/dist/ingest/simcity/models/building-cinema.glb +0 -0
- package/dist/ingest/simcity/models/building-house-block-big.glb +0 -0
- package/dist/ingest/simcity/models/building-house-family-large.glb +0 -0
- package/dist/ingest/simcity/models/building-house-family-small.glb +0 -0
- package/dist/ingest/simcity/models/building-office-balcony.glb +0 -0
- package/dist/ingest/simcity/models/building-office-big.glb +0 -0
- package/dist/ingest/simcity/models/building-office-pyramid.glb +0 -0
- package/dist/ingest/simcity/models/building-office-tall.glb +0 -0
- package/dist/ingest/simcity/models/building-office.glb +0 -0
- package/dist/ingest/simcity/models/building-restaurant.glb +0 -0
- package/dist/ingest/simcity/models/building-skyscraper.glb +0 -0
- package/dist/ingest/simcity/models/car-ambulance-pickup.glb +0 -0
- package/dist/ingest/simcity/models/car-baywatch.glb +0 -0
- package/dist/ingest/simcity/models/car-hippie-van.glb +0 -0
- package/dist/ingest/simcity/models/car-passenger-race.glb +0 -0
- package/dist/ingest/simcity/models/car-passenger.glb +0 -0
- package/dist/ingest/simcity/models/car-police.glb +0 -0
- package/dist/ingest/simcity/models/car-taxi.glb +0 -0
- package/dist/ingest/simcity/models/car-tow-truck.glb +0 -0
- package/dist/ingest/simcity/models/car-truck-dump.glb +0 -0
- package/dist/ingest/simcity/models/car-veteran.glb +0 -0
- package/dist/ingest/simcity/models/construction-small.glb +0 -0
- package/dist/ingest/simcity/models/data-center.glb +0 -0
- package/dist/ingest/simcity/models/industry-factory-old.glb +0 -0
- package/dist/ingest/simcity/models/industry-factory.glb +0 -0
- package/dist/ingest/simcity/models/industry-refinery.glb +0 -0
- package/dist/ingest/simcity/models/industry-warehouse.glb +0 -0
- package/dist/ingest/simcity/models/power_line_pole_modified.glb +0 -0
- package/dist/ingest/simcity/models/tile-plain_grass.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-curve.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-end.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-intersection-t.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-intersection.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-straight.glb +0 -0
- package/dist/ingest/simcity/models/truck.glb +0 -0
- package/dist/ingest/simcity/statusIcons/no-power.png +0 -0
- package/dist/ingest/simcity/statusIcons/no-road-access.png +0 -0
- package/dist/ingest/simcity/textures/base.png +0 -0
- package/dist/ingest/simcity/textures/grid.png +0 -0
- package/dist/ingest/simcity/textures/specular.png +0 -0
- package/dist/ingest/simcity/three-r155.module.js +51466 -0
- package/dist/ingest/simcity/vgai.game.json +20 -0
- package/dist/ingest/tanks/UPSTREAM.md +69 -0
- package/dist/ingest/tanks/index.html +214 -0
- package/dist/ingest/tanks/models/pixelTank.fbx +0 -0
- package/dist/ingest/tanks/tanks-entry.js +92 -0
- package/dist/ingest/tanks/three-r170.module.js +54571 -0
- package/dist/ingest/tanks/vgai.game.json +20 -0
- package/dist/inputmaps/default.inputmap.json +41 -0
- package/dist/jsm/libs/draco/gltf/draco_decoder.js +33 -0
- package/dist/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
- package/dist/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
- package/dist/scenes/untitled.vscn.json +29 -0
- package/dist/textures/waternormals.jpg +0 -0
- package/dist-server/packaged.mjs +18155 -0
- package/package.json +36 -0
- package/server/asset-library-routes.ts +833 -0
- package/server/canonical-path.ts +50 -0
- package/server/cloud-asset-catalog.ts +101 -0
- package/server/dev.ts +350 -0
- package/server/editor-server.ts +1537 -0
- package/server/editor-sse.ts +73 -0
- package/server/game-globals-shadow.ts +58 -0
- package/server/local-asset-catalog.ts +163 -0
- package/server/packaged.ts +571 -0
- package/server/prod.ts +75 -0
- package/server/project-optimize-deps-entries.ts +149 -0
- package/server/project-view.ts +65 -0
- package/server/server-utils.ts +430 -0
- package/server/session-registry.ts +93 -0
- package/src/EditorContext.tsx +87 -0
- package/src/achieved-tier.ts +126 -0
- package/src/action-registry.ts +268 -0
- package/src/adapter-resolver.ts +1858 -0
- package/src/asset-events.ts +100 -0
- package/src/authoring/active-adapter.ts +98 -0
- package/src/authoring/active-systems.ts +50 -0
- package/src/authoring/boundary-authoring-adapter.ts +178 -0
- package/src/authoring/composite-authoring-adapter.ts +891 -0
- package/src/authoring/design-time-layers.ts +776 -0
- package/src/authoring/edit-mode-authoring.ts +643 -0
- package/src/authoring/embed-only-adapter.ts +57 -0
- package/src/authoring/ephemeral-persistence.ts +26 -0
- package/src/authoring/eyedropper-session.ts +69 -0
- package/src/authoring/first-party-hierarchy-menu.ts +74 -0
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +118 -0
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +550 -0
- package/src/authoring/first-party-inspector/HeaderSection.tsx +329 -0
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +44 -0
- package/src/authoring/first-party-inspector/register.tsx +108 -0
- package/src/authoring/ingest-authoring-adapter.ts +475 -0
- package/src/authoring/ingest-capture.ts +94 -0
- package/src/authoring/ingest-game-adapter.ts +291 -0
- package/src/authoring/ingest-iframe-adapter.ts +38 -0
- package/src/authoring/ingest-iframe-reachable-adapter.ts +401 -0
- package/src/authoring/ingest-overlay.ts +193 -0
- package/src/authoring/layered-pick.ts +87 -0
- package/src/authoring/mount-failure-report.ts +112 -0
- package/src/authoring/overlay-report.ts +52 -0
- package/src/authoring/project-scenes.ts +111 -0
- package/src/authoring/react-dom-authoring-adapter.ts +795 -0
- package/src/authoring/react-inspector-section.tsx +1041 -0
- package/src/authoring/react-world-authoring-adapter.ts +2400 -0
- package/src/authoring/root-activation.ts +61 -0
- package/src/authoring/scene-ui-layer.ts +485 -0
- package/src/authoring/story-overlay.ts +48 -0
- package/src/authoring/ui-authoring-adapter.ts +325 -0
- package/src/authoring/ui-hierarchy-menu.ts +40 -0
- package/src/authoring/ui-inspector-sections.tsx +156 -0
- package/src/authoring/undo-timeline.ts +145 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +746 -0
- package/src/authoring/viewport-pick-context.ts +31 -0
- package/src/authoring/viewport-raycast.ts +80 -0
- package/src/authoring/world-overlay.ts +76 -0
- package/src/authoring/world-pan-state.ts +115 -0
- package/src/authoring/world-session-state.ts +102 -0
- package/src/authoring/world2d-authoring-adapter.ts +313 -0
- package/src/browser-external-watch.ts +98 -0
- package/src/browser-play-entry.ts +112 -0
- package/src/browser-project-scripts.ts +112 -0
- package/src/browser-transpile.ts +246 -0
- package/src/browser-watch-reload.ts +100 -0
- package/src/command-listener.ts +326 -0
- package/src/components/AlignToolbar.tsx +189 -0
- package/src/components/AppRoot.tsx +73 -0
- package/src/components/AssetBrowser.tsx +502 -0
- package/src/components/BottomPanel.tsx +326 -0
- package/src/components/BuildPanel.tsx +455 -0
- package/src/components/CameraInfo.tsx +56 -0
- package/src/components/CommandPalette.tsx +265 -0
- package/src/components/ConsolePanel.tsx +272 -0
- package/src/components/DataPanel.tsx +1268 -0
- package/src/components/DefaultEditorLayout.tsx +509 -0
- package/src/components/FolderTree.tsx +179 -0
- package/src/components/GameHierarchy.tsx +1349 -0
- package/src/components/Inspector.tsx +671 -0
- package/src/components/InspectorToolSection.tsx +74 -0
- package/src/components/MountFailureBanner.tsx +110 -0
- package/src/components/NetworkSection.tsx +109 -0
- package/src/components/OnlineAssetBrowser.tsx +448 -0
- package/src/components/OverlayReportBanner.tsx +122 -0
- package/src/components/PlayBar.tsx +356 -0
- package/src/components/PrefabSaveDialog.tsx +201 -0
- package/src/components/ProjectHeader.tsx +174 -0
- package/src/components/ProjectScreen.tsx +1004 -0
- package/src/components/RightPanel.tsx +166 -0
- package/src/components/RootActivateDialog.tsx +105 -0
- package/src/components/SaveDialog.tsx +197 -0
- package/src/components/SaveStatus.tsx +69 -0
- package/src/components/StatsOverlay.tsx +35 -0
- package/src/components/ToolHost.tsx +80 -0
- package/src/components/Toolbar.tsx +307 -0
- package/src/components/ViewportOverlay.tsx +234 -0
- package/src/components/ViewportPanel.tsx +369 -0
- package/src/components/WorldContextMenu.tsx +257 -0
- package/src/components/WorldSelectionOverlay.tsx +2003 -0
- package/src/components/WorldTextEditor.tsx +86 -0
- package/src/components/asset-viewers/AssetViewer.tsx +38 -0
- package/src/components/asset-viewers/AudioViewer.tsx +149 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +77 -0
- package/src/components/asset-viewers/ImageViewer.tsx +65 -0
- package/src/components/asset-viewers/InputMapViewer.tsx +206 -0
- package/src/components/asset-viewers/ModelViewer.tsx +180 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +236 -0
- package/src/components/data-csv.ts +301 -0
- package/src/components/entity-name-commit.ts +18 -0
- package/src/components/inspector-property-grouping.ts +65 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +188 -0
- package/src/components/inspector-widgets/BorderEditor.tsx +305 -0
- package/src/components/inspector-widgets/ColorPicker.tsx +548 -0
- package/src/components/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/components/inspector-widgets/FontPicker.tsx +196 -0
- package/src/components/inspector-widgets/GradientEditor.tsx +277 -0
- package/src/components/inspector-widgets/ScrubbableInput.tsx +152 -0
- package/src/components/inspector-widgets/ShadowEditor.tsx +332 -0
- package/src/components/inspector-widgets/color-utils.ts +202 -0
- package/src/components/inspector-widgets/gallery-main.tsx +167 -0
- package/src/components/inspector-widgets/index.ts +22 -0
- package/src/components/inspector-widgets/shared.tsx +135 -0
- package/src/components/inspectors/AnimationSection.tsx +143 -0
- package/src/components/inspectors/AudioSection.tsx +215 -0
- package/src/components/inspectors/BoneAttachmentSection.tsx +82 -0
- package/src/components/inspectors/CameraSection.tsx +105 -0
- package/src/components/inspectors/ComponentsSection.tsx +358 -0
- package/src/components/inspectors/JointsSection.tsx +255 -0
- package/src/components/inspectors/LightSection.tsx +116 -0
- package/src/components/inspectors/MaterialSection.tsx +1189 -0
- package/src/components/inspectors/MeshSection.tsx +99 -0
- package/src/components/inspectors/NavigationSection.tsx +58 -0
- package/src/components/inspectors/ParticleSection.tsx +681 -0
- package/src/components/inspectors/PhysicsSection.tsx +326 -0
- package/src/components/inspectors/PostProcessingSection.tsx +1290 -0
- package/src/components/inspectors/RenderingSection.tsx +109 -0
- package/src/components/inspectors/ShadowSection.tsx +84 -0
- package/src/components/inspectors/SplineSection.tsx +191 -0
- package/src/components/inspectors/TransformSection.tsx +191 -0
- package/src/components/inspectors/json-schema-fields.ts +281 -0
- package/src/components/inspectors/multi-edit-utils.ts +49 -0
- package/src/components/inspectors/zod-fields.ts +184 -0
- package/src/components/primitives/ColorInput.tsx +109 -0
- package/src/components/primitives/JsonInput.tsx +98 -0
- package/src/components/primitives/NumberInput.tsx +164 -0
- package/src/components/primitives/Panel.tsx +68 -0
- package/src/components/primitives/SectionHeader.tsx +45 -0
- package/src/components/primitives/Tooltip.tsx +82 -0
- package/src/components/primitives/Vec3Input.tsx +62 -0
- package/src/components/world-overlay-gestures.ts +1438 -0
- package/src/doctor/data-checks.ts +285 -0
- package/src/doctor/report.ts +386 -0
- package/src/editor-api.ts +700 -0
- package/src/editor-console.ts +194 -0
- package/src/editor-hotkeys.ts +441 -0
- package/src/editor-layers.ts +2 -0
- package/src/editor-mode.ts +28 -0
- package/src/editor-store.ts +2225 -0
- package/src/editor-viewport.ts +1924 -0
- package/src/entity-factory.ts +221 -0
- package/src/game-globals-prelude.ts +27 -0
- package/src/gated-globals.ts +131 -0
- package/src/generated/setup-by-example.generated.ts +53 -0
- package/src/gizmo-registry.ts +608 -0
- package/src/handle-system.ts +221 -0
- package/src/hierarchy-menu-registry.ts +70 -0
- package/src/hierarchy-rows.ts +73 -0
- package/src/hosted-example.ts +160 -0
- package/src/hotkeys.ts +142 -0
- package/src/ingest/discovery-react.ts +132 -0
- package/src/ingest/discovery.ts +205 -0
- package/src/ingest/discovery2d.ts +204 -0
- package/src/ingest/esm-entry-resolution.ts +102 -0
- package/src/ingest/games/bloom-composer/game.js +99 -0
- package/src/ingest/games/bloom-composer/vgai.game.json +21 -0
- package/src/ingest/games/embed-bundle/embed.html +10 -0
- package/src/ingest/games/embed-bundle/vgai.game.json +21 -0
- package/src/ingest/games/games-fps/assets/collision-world.glb +0 -0
- package/src/ingest/games/games-fps/game.js +479 -0
- package/src/ingest/games/games-fps/vgai.game.json +24 -0
- package/src/ingest/games/iframe-reachable/game-source.js +58 -0
- package/src/ingest/games/iframe-reachable/vgai.game.json +21 -0
- package/src/ingest/games/interactive-cubes/game.js +139 -0
- package/src/ingest/games/interactive-cubes/vgai.game.json +21 -0
- package/src/ingest/games/mismatched-three/fake-three.js +38 -0
- package/src/ingest/games/mismatched-three/game.js +10 -0
- package/src/ingest/games/mismatched-three/vgai.game.json +22 -0
- package/src/ingest/games/own-three-bundled/game.js +43 -0
- package/src/ingest/games/own-three-bundled/vgai.game.json +21 -0
- package/src/ingest/games/pointerlock-maze/game.js +288 -0
- package/src/ingest/games/pointerlock-maze/vgai.game.json +22 -0
- package/src/ingest/games-2d/_fixtures/first-party-scene.scn2d.json +124 -0
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +25 -0
- package/src/ingest/games-2d/bunnymark/game.ts +88 -0
- package/src/ingest/games-2d/bunnymark/vgai.game.json +21 -0
- package/src/ingest/games-2d/flappy/game.ts +148 -0
- package/src/ingest/games-2d/flappy/vgai.game.json +21 -0
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +25 -0
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +25 -0
- package/src/ingest/games-2d/tilemap-camera/game.ts +115 -0
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +21 -0
- package/src/ingest/games-react/react-rpg/entry.tsx +185 -0
- package/src/ingest/games-react/react-rpg/shims.d.ts +26 -0
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +129 -0
- package/src/ingest/games-react/react-rpg/vgai.game.json +21 -0
- package/src/ingest/host-namespace-registry.ts +196 -0
- package/src/ingest/types.ts +127 -0
- package/src/ingest-hierarchy-rows.ts +207 -0
- package/src/ingest-mode.ts +2092 -0
- package/src/ingest-siblings.ts +466 -0
- package/src/initial-scene.ts +194 -0
- package/src/inspector-section-registry.ts +117 -0
- package/src/interactive-edit-scope.ts +34 -0
- package/src/main.tsx +23 -0
- package/src/manifest-project.ts +43 -0
- package/src/model-thumbnail.ts +156 -0
- package/src/module-mode.ts +513 -0
- package/src/navmesh-handler.ts +94 -0
- package/src/packaged-runtime.ts +49 -0
- package/src/play-mode.ts +1148 -0
- package/src/playground/main.ts +206 -0
- package/src/project-manager.ts +173 -0
- package/src/scene-diff.ts +112 -0
- package/src/scene-serializer.ts +144 -0
- package/src/scene-sync.ts +587 -0
- package/src/scene-ui-mode/active-scene-ui.ts +58 -0
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +152 -0
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +29 -0
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +74 -0
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +49 -0
- package/src/scene-ui-mode/ui-prefab-io.ts +76 -0
- package/src/script-loader.ts +143 -0
- package/src/storage/fsa-storage.ts +55 -0
- package/src/storage/handle-storage.ts +150 -0
- package/src/storage/handle-store.ts +116 -0
- package/src/storage/http-storage.ts +137 -0
- package/src/storage/index.ts +147 -0
- package/src/storage/mem-storage.ts +135 -0
- package/src/storage/opfs-storage.ts +25 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/seed.ts +106 -0
- package/src/storage/types.ts +110 -0
- package/src/tool-loader.ts +291 -0
- package/src/ui-editor/inspector.tsx +563 -0
- package/src/ui-editor/node-ops.ts +266 -0
- package/src/ui-editor/overlay.tsx +380 -0
- package/src/ui-editor/palette.tsx +114 -0
- package/src/ui-editor/react-store.ts +78 -0
- package/src/ui-editor/timeline-panel.tsx +443 -0
- package/src/ui-editor/ui-edit-store.ts +552 -0
- package/src/ui-editor/ui-prefab.ts +95 -0
- package/src/ui-source/inspect.ts +544 -0
- package/src/ui-source/oid-transform.ts +133 -0
- package/src/ui-source/source-write-backend.ts +203 -0
- package/src/ui-source/tailwind-map.ts +174 -0
- package/src/ui-source/writer.ts +1175 -0
- package/src/widgets/index.ts +73 -0
- package/template/.claude/skills/editor/SKILL.md +252 -0
- package/template/CLAUDE.md +512 -0
- package/template/LICENSE +30 -0
- package/template/emit-schemas.ts +42 -0
- package/template/index.html +17 -0
- package/template/package.json +53 -0
- package/template/project.json +9 -0
- package/template/public/audio/ambient-arena.wav +0 -0
- package/template/public/audio/ambient-music.wav +0 -0
- package/template/public/audio/ambient-nature.wav +0 -0
- package/template/public/audio/ambient-wind.wav +0 -0
- package/template/public/inputmaps/default.inputmap.json +41 -0
- package/template/public/jsm/libs/draco/gltf/draco_decoder.js +33 -0
- package/template/public/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
- package/template/public/jsm/libs/draco/gltf/draco_encoder.js +33 -0
- package/template/public/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
- package/template/public/scenes/default.vscn.json +43 -0
- package/template/public/scenes/untitled.vscn.json +29 -0
- package/template/public/textures/waternormals.jpg +0 -0
- package/template/server/colyseus-setup.ts +85 -0
- package/template/server/main.ts +34 -0
- package/template/server/rooms/arena-room.ts +420 -0
- package/template/server/rooms/game-room.ts +162 -0
- package/template/server/rooms.ts +15 -0
- package/template/server/tsconfig.json +19 -0
- package/template/src/data/README.md +40 -0
- package/template/src/data/assets.ts +31 -0
- package/template/src/data/tuning.data.json +5 -0
- package/template/src/data/tuning.schema.json +21 -0
- package/template/src/data/tuning.schema.ts +34 -0
- package/template/src/data/tuning.ts +24 -0
- package/template/src/main.ts +157 -0
- package/template/src/scripts/components/data-spinner.ts +45 -0
- package/template/src/scripts/components/scene-camera.ts +40 -0
- package/template/src/scripts/main.ts +94 -0
- package/template/src/scripts/registry.ts +46 -0
- package/template/src/tools/README.md +14 -0
- package/template/src/tools/example.tool.tsx +91 -0
- package/template/src/tools/spin.tool.tsx +101 -0
- package/template/src/ui/Button.tsx +27 -0
- package/template/src/ui/DefaultHud.tsx +40 -0
- package/template/src/ui/KeyHint.tsx +25 -0
- package/template/src/ui/Panel.tsx +52 -0
- package/template/src/ui/RebindPanel.tsx +129 -0
- package/template/src/ui/Slider.tsx +28 -0
- package/template/src/ui/StatusDot.tsx +22 -0
- package/template/src/ui/Toggle.tsx +19 -0
- package/template/src/ui/game-state.tsx +37 -0
- package/template/src/ui/index.ts +7 -0
- package/template/src/ui/mount-default-hud.tsx +19 -0
- package/template/src/ui/mount-ui.ts +15 -0
- package/template/tsconfig.json +36 -0
- package/template/validate-asset-content.ts +78 -0
- package/template/validate-manifest.ts +55 -0
- package/template/validate-scenes.ts +211 -0
- package/template/vgai.game.json +21 -0
- package/template/vite.config.ts +128 -0
- package/vite-plugin-example-assets.ts +60 -0
- package/vite-plugin-game-globals.ts +46 -0
- package/vite-plugin-game-static.ts +71 -0
- package/vite-plugin-project-game-static.ts +84 -0
- package/vite-plugin-react-game-static.ts +98 -0
- package/vite-plugin-react-world-provider.ts +81 -0
- package/vite-plugin-react-world-runtime.ts +76 -0
- package/vite-plugin-ui-oid.ts +561 -0
|
@@ -0,0 +1,1924 @@
|
|
|
1
|
+
import { resolveUrl } from '@engine/loader';
|
|
2
|
+
import { tickCustomMaterials } from '@engine/scene/material-registry';
|
|
3
|
+
import type { SceneEntity } from '@engine/scene/scene-types';
|
|
4
|
+
import { getUserData, setUserData } from '@engine/scene/user-data';
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
7
|
+
import { TransformControls } from 'three/addons/controls/TransformControls.js';
|
|
8
|
+
import { BatchedRenderer } from 'three.quarks';
|
|
9
|
+
import { getActiveAuthoring } from './authoring/active-adapter';
|
|
10
|
+
import { pickTopmost } from './authoring/layered-pick';
|
|
11
|
+
import { setViewportPickContext } from './authoring/viewport-pick-context';
|
|
12
|
+
import { downloadOnlineAsset, getDownloadedAssetPath, getOnlineAssetFiles } from './editor-api';
|
|
13
|
+
import { EDITOR_LAYER } from './editor-layers';
|
|
14
|
+
import type { EditorStore } from './editor-store';
|
|
15
|
+
import { applyEntityGizmos } from './gizmo-registry';
|
|
16
|
+
import {
|
|
17
|
+
createHandleDefs,
|
|
18
|
+
createHandleMesh,
|
|
19
|
+
type HandleMesh,
|
|
20
|
+
scaleHandle,
|
|
21
|
+
setHandleHovered,
|
|
22
|
+
} from './handle-system';
|
|
23
|
+
|
|
24
|
+
/** A BoxHelper or LineSegments wireframe tagged with its source entity object. */
|
|
25
|
+
interface TaggedHelper extends THREE.BoxHelper {
|
|
26
|
+
isBoxHelper: boolean;
|
|
27
|
+
_entityObj?: THREE.Object3D;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** View directions indexed by click-target userData. */
|
|
31
|
+
const VC_DIRS: THREE.Vector3[] = [
|
|
32
|
+
/* 0 +X */ new THREE.Vector3(1, 0, 0),
|
|
33
|
+
/* 1 -X */ new THREE.Vector3(-1, 0, 0),
|
|
34
|
+
/* 2 +Y */ new THREE.Vector3(0, 1, 0),
|
|
35
|
+
/* 3 -Y */ new THREE.Vector3(0, -1, 0),
|
|
36
|
+
/* 4 +Z */ new THREE.Vector3(0, 0, 1),
|
|
37
|
+
/* 5 -Z */ new THREE.Vector3(0, 0, -1),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export class EditorViewport {
|
|
41
|
+
readonly camera: THREE.PerspectiveCamera;
|
|
42
|
+
readonly orbitControls: OrbitControls;
|
|
43
|
+
readonly transformControls: TransformControls;
|
|
44
|
+
readonly raycaster = (() => {
|
|
45
|
+
const r = new THREE.Raycaster();
|
|
46
|
+
r.layers.enableAll();
|
|
47
|
+
return r;
|
|
48
|
+
})();
|
|
49
|
+
readonly grid: THREE.GridHelper;
|
|
50
|
+
private _boxHelpers = new Map<string, THREE.BoxHelper>();
|
|
51
|
+
private _gizmoHelper: THREE.Object3D;
|
|
52
|
+
private _editorAmbient: THREE.AmbientLight;
|
|
53
|
+
private _editorDirLight: THREE.DirectionalLight;
|
|
54
|
+
|
|
55
|
+
private _scene: THREE.Scene;
|
|
56
|
+
/** Editor infrastructure objects that must move with the active scene. */
|
|
57
|
+
private _editorObjects: THREE.Object3D[] = [];
|
|
58
|
+
private _store: EditorStore;
|
|
59
|
+
private _objectMap = new Map<string, THREE.Object3D>();
|
|
60
|
+
private _canvas: HTMLCanvasElement;
|
|
61
|
+
private _pointerDownPos = new THREE.Vector2();
|
|
62
|
+
private _skipNextSync = false;
|
|
63
|
+
|
|
64
|
+
// -- Marquee select --
|
|
65
|
+
private _marqueeActive = false;
|
|
66
|
+
private _marqueeDiv: HTMLDivElement | null = null;
|
|
67
|
+
private _pointerDownButton = -1;
|
|
68
|
+
private _altDragOrbit = false;
|
|
69
|
+
|
|
70
|
+
// -- Orientation gizmo --
|
|
71
|
+
private _vcScene = new THREE.Scene();
|
|
72
|
+
private _vcCamera: THREE.OrthographicCamera;
|
|
73
|
+
private _vcClickTargets: THREE.Mesh[] = [];
|
|
74
|
+
private _vcSize = 110;
|
|
75
|
+
private _vcMarginRight = 16;
|
|
76
|
+
private _vcMarginTop = 36 + 16;
|
|
77
|
+
/** Scratch dir vector reused each frame by renderViewCube (avoids per-frame alloc). */
|
|
78
|
+
private _vcDir = new THREE.Vector3();
|
|
79
|
+
/** Scratch color reused by renderCameraPreview (avoids per-frame alloc). */
|
|
80
|
+
private _previewClearColor = new THREE.Color();
|
|
81
|
+
/** Last objectMap + visibility signature applied by syncFromStore's gizmo/helper
|
|
82
|
+
* passes — lets those O(scene) loops skip when their inputs are unchanged (e.g.
|
|
83
|
+
* during a transform drag, which notifies per frame but changes none of them). */
|
|
84
|
+
private _lastGizmoObjectMap: Map<string, THREE.Object3D> | null = null;
|
|
85
|
+
private _lastGizmoSig = '';
|
|
86
|
+
private _vcRaycaster = new THREE.Raycaster();
|
|
87
|
+
|
|
88
|
+
// -- Asset drop --
|
|
89
|
+
private _dropOverlay: HTMLDivElement | null = null;
|
|
90
|
+
private _groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
91
|
+
|
|
92
|
+
// -- Multi-drag: track primary object's transform at drag start --
|
|
93
|
+
private _dragStartPos = new THREE.Vector3();
|
|
94
|
+
private _dragStartQuat = new THREE.Quaternion();
|
|
95
|
+
private _dragStartScale = new THREE.Vector3();
|
|
96
|
+
private _otherDragObjects: {
|
|
97
|
+
obj: THREE.Object3D;
|
|
98
|
+
startPos: THREE.Vector3;
|
|
99
|
+
startQuat: THREE.Quaternion;
|
|
100
|
+
startScale: THREE.Vector3;
|
|
101
|
+
}[] = [];
|
|
102
|
+
private _allDragObjects: {
|
|
103
|
+
obj: THREE.Object3D;
|
|
104
|
+
startPos: THREE.Vector3;
|
|
105
|
+
startQuat: THREE.Quaternion;
|
|
106
|
+
startScale: THREE.Vector3;
|
|
107
|
+
}[] = [];
|
|
108
|
+
private _pivotDummy = new THREE.Object3D();
|
|
109
|
+
private _pivotPos = new THREE.Vector3();
|
|
110
|
+
|
|
111
|
+
// -- Surface snap --
|
|
112
|
+
private _surfaceSnapRaycaster = new THREE.Raycaster();
|
|
113
|
+
private _surfaceSnapIndicator: THREE.Mesh;
|
|
114
|
+
|
|
115
|
+
// -- Vertex snap --
|
|
116
|
+
private _vertexSnapTargets: THREE.Vector3[] = [];
|
|
117
|
+
private _vertexSnapIndicator!: THREE.Mesh;
|
|
118
|
+
private _vertexSnapThreshold = 20; // pixels
|
|
119
|
+
|
|
120
|
+
// -- Component handles (interactive gizmos for light range, audio distance, etc.) --
|
|
121
|
+
private _handles: HandleMesh[] = [];
|
|
122
|
+
private _handleRaycaster = new THREE.Raycaster();
|
|
123
|
+
private _handleDragActive = false;
|
|
124
|
+
private _activeHandle: HandleMesh | null = null;
|
|
125
|
+
private _handleDragPlane = new THREE.Plane();
|
|
126
|
+
private _handleDragOffset = new THREE.Vector3();
|
|
127
|
+
private _hoveredHandle: HandleMesh | null = null;
|
|
128
|
+
|
|
129
|
+
// -- Particles --
|
|
130
|
+
readonly batchedRenderer: BatchedRenderer;
|
|
131
|
+
|
|
132
|
+
// -- NavMesh helper (scene-root, toggled separately) --
|
|
133
|
+
private _navMeshHelper: THREE.Object3D | null = null;
|
|
134
|
+
|
|
135
|
+
// -- Snap-to-view animation --
|
|
136
|
+
private _snapAnimating = false;
|
|
137
|
+
private _snapStartTime = 0;
|
|
138
|
+
private _snapDuration = 400;
|
|
139
|
+
private _snapQ1 = new THREE.Quaternion();
|
|
140
|
+
private _snapQ2 = new THREE.Quaternion();
|
|
141
|
+
private _snapDist = 0;
|
|
142
|
+
|
|
143
|
+
// -- Fly camera (right-click + WASD) --
|
|
144
|
+
private _flyActive = false;
|
|
145
|
+
private _flyKeys = new Set<string>();
|
|
146
|
+
private _flySpeed = 5;
|
|
147
|
+
private _flyEuler = new THREE.Euler(0, 0, 0, 'YXZ');
|
|
148
|
+
private _rightMouseDown = false;
|
|
149
|
+
|
|
150
|
+
constructor(canvas: HTMLCanvasElement, scene: THREE.Scene, store: EditorStore) {
|
|
151
|
+
this._canvas = canvas;
|
|
152
|
+
this._scene = scene;
|
|
153
|
+
this._store = store;
|
|
154
|
+
|
|
155
|
+
// Camera — enableAll so editor sees both game content (layer 0) and editor objects (layer 31)
|
|
156
|
+
this.camera = new THREE.PerspectiveCamera(
|
|
157
|
+
60,
|
|
158
|
+
canvas.clientWidth / canvas.clientHeight,
|
|
159
|
+
0.1,
|
|
160
|
+
1000,
|
|
161
|
+
);
|
|
162
|
+
this.camera.position.set(10, 10, 10);
|
|
163
|
+
this.camera.lookAt(0, 0, 0);
|
|
164
|
+
this.camera.layers.enableAll();
|
|
165
|
+
|
|
166
|
+
// Grid
|
|
167
|
+
this.grid = new THREE.GridHelper(50, 50, 0x999999, 0x999999);
|
|
168
|
+
this.grid.layers.set(EDITOR_LAYER);
|
|
169
|
+
scene.add(this.grid);
|
|
170
|
+
|
|
171
|
+
// Editor lighting (on EDITOR_LAYER so game camera doesn't see them)
|
|
172
|
+
this._editorAmbient = new THREE.AmbientLight(0xffffff, 0.5);
|
|
173
|
+
this._editorAmbient.layers.set(EDITOR_LAYER);
|
|
174
|
+
scene.add(this._editorAmbient);
|
|
175
|
+
this._editorDirLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
|
176
|
+
this._editorDirLight.position.set(10, 20, 10);
|
|
177
|
+
this._editorDirLight.layers.set(EDITOR_LAYER);
|
|
178
|
+
scene.add(this._editorDirLight);
|
|
179
|
+
|
|
180
|
+
// Particle BatchedRenderer
|
|
181
|
+
this.batchedRenderer = new BatchedRenderer();
|
|
182
|
+
this.batchedRenderer.layers.set(EDITOR_LAYER);
|
|
183
|
+
scene.add(this.batchedRenderer);
|
|
184
|
+
|
|
185
|
+
// OrbitControls – Unity-style: right-drag = orbit, middle-drag = pan, scroll = zoom
|
|
186
|
+
this.orbitControls = new OrbitControls(this.camera, canvas);
|
|
187
|
+
this.orbitControls.enableDamping = true;
|
|
188
|
+
this.orbitControls.dampingFactor = 0.1;
|
|
189
|
+
this.orbitControls.mouseButtons = {
|
|
190
|
+
LEFT: -1 as THREE.MOUSE,
|
|
191
|
+
MIDDLE: THREE.MOUSE.PAN,
|
|
192
|
+
RIGHT: THREE.MOUSE.ROTATE,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// TransformControls
|
|
196
|
+
this.transformControls = new TransformControls(this.camera, canvas);
|
|
197
|
+
// Enable all layers on the internal raycaster so it can pick gizmo parts on EDITOR_LAYER
|
|
198
|
+
this.transformControls.getRaycaster().layers.enableAll();
|
|
199
|
+
this.transformControls.addEventListener('dragging-changed', (event) => {
|
|
200
|
+
const dragging = (event as unknown as { value: boolean }).value;
|
|
201
|
+
this.orbitControls.enabled = !dragging;
|
|
202
|
+
if (dragging) {
|
|
203
|
+
const gizmoObj = this.transformControls.object;
|
|
204
|
+
const primaryDragId =
|
|
205
|
+
(gizmoObj && (getUserData(gizmoObj, 'entityId') as string | undefined)) || '';
|
|
206
|
+
// Gizmo-commit routes through the AuthoringAdapter: beginEdit snapshots
|
|
207
|
+
// for undo and is the hook where a physics-owned object gets frozen for a
|
|
208
|
+
// stable edit (ingest / Phase E); endEdit commits at drag-end.
|
|
209
|
+
getActiveAuthoring(this._store).transforms?.beginEdit(primaryDragId);
|
|
210
|
+
if (gizmoObj) {
|
|
211
|
+
this._dragStartPos.copy(gizmoObj.position);
|
|
212
|
+
this._dragStartQuat.copy(gizmoObj.quaternion);
|
|
213
|
+
this._dragStartScale.copy(gizmoObj.scale);
|
|
214
|
+
this._otherDragObjects = [];
|
|
215
|
+
this._allDragObjects = [];
|
|
216
|
+
this._pivotPos.copy(gizmoObj.position);
|
|
217
|
+
const primaryId = getUserData(gizmoObj, 'entityId') as string | undefined;
|
|
218
|
+
if (!primaryId) {
|
|
219
|
+
// Gizmo on pivot dummy (median-point mode or per-entity pivot)
|
|
220
|
+
for (const id of this._store.selectedEntityIds) {
|
|
221
|
+
const obj = this._objectMap.get(id);
|
|
222
|
+
if (!obj) continue;
|
|
223
|
+
this._allDragObjects.push({
|
|
224
|
+
obj,
|
|
225
|
+
startPos: obj.position.clone(),
|
|
226
|
+
startQuat: obj.quaternion.clone(),
|
|
227
|
+
startScale: obj.scale.clone(),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
for (const id of this._store.selectedEntityIds) {
|
|
232
|
+
if (id === primaryId) continue;
|
|
233
|
+
const obj = this._objectMap.get(id);
|
|
234
|
+
if (!obj) continue;
|
|
235
|
+
this._otherDragObjects.push({
|
|
236
|
+
obj,
|
|
237
|
+
startPos: obj.position.clone(),
|
|
238
|
+
startQuat: obj.quaternion.clone(),
|
|
239
|
+
startScale: obj.scale.clone(),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
// Hide snap indicators when drag ends
|
|
246
|
+
this._surfaceSnapIndicator.visible = false;
|
|
247
|
+
this._vertexSnapIndicator.visible = false;
|
|
248
|
+
// After drag ends, re-sync for median-point pivot repositioning
|
|
249
|
+
this.syncFromStore();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
this.transformControls.addEventListener('objectChange', () => {
|
|
253
|
+
const gizmoObj = this.transformControls.object;
|
|
254
|
+
if (!gizmoObj) return;
|
|
255
|
+
const mode = this.transformControls.mode;
|
|
256
|
+
const isIndividual = this._store.pivotMode === 'individual-origins';
|
|
257
|
+
const isMedian = this._allDragObjects.length > 0;
|
|
258
|
+
const targets = isMedian ? this._allDragObjects : this._otherDragObjects;
|
|
259
|
+
if (targets.length > 0) {
|
|
260
|
+
if (mode === 'translate') {
|
|
261
|
+
const delta = gizmoObj.position.clone().sub(this._dragStartPos);
|
|
262
|
+
for (const { obj, startPos } of targets) {
|
|
263
|
+
obj.position.copy(startPos).add(delta);
|
|
264
|
+
}
|
|
265
|
+
} else if (mode === 'rotate') {
|
|
266
|
+
const deltaQuat = gizmoObj.quaternion
|
|
267
|
+
.clone()
|
|
268
|
+
.multiply(this._dragStartQuat.clone().invert());
|
|
269
|
+
if (isIndividual) {
|
|
270
|
+
// Individual origins: only rotate, no position orbiting
|
|
271
|
+
for (const { obj, startQuat } of targets) {
|
|
272
|
+
obj.quaternion.copy(deltaQuat).multiply(startQuat);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
// Active element / Median: orbit positions around pivot + rotate
|
|
276
|
+
for (const { obj, startPos, startQuat } of targets) {
|
|
277
|
+
const offset = startPos.clone().sub(this._pivotPos);
|
|
278
|
+
offset.applyQuaternion(deltaQuat);
|
|
279
|
+
obj.position.copy(this._pivotPos).add(offset);
|
|
280
|
+
obj.quaternion.copy(deltaQuat).multiply(startQuat);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} else if (mode === 'scale') {
|
|
284
|
+
const deltaScale = new THREE.Vector3(
|
|
285
|
+
this._dragStartScale.x !== 0 ? gizmoObj.scale.x / this._dragStartScale.x : 1,
|
|
286
|
+
this._dragStartScale.y !== 0 ? gizmoObj.scale.y / this._dragStartScale.y : 1,
|
|
287
|
+
this._dragStartScale.z !== 0 ? gizmoObj.scale.z / this._dragStartScale.z : 1,
|
|
288
|
+
);
|
|
289
|
+
if (isIndividual) {
|
|
290
|
+
// Individual origins: only scale, no position change
|
|
291
|
+
for (const { obj, startScale } of targets) {
|
|
292
|
+
obj.scale.copy(startScale).multiply(deltaScale);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
// Active element / Median: scale from pivot + scale objects
|
|
296
|
+
for (const { obj, startPos, startScale } of targets) {
|
|
297
|
+
const offset = startPos.clone().sub(this._pivotPos);
|
|
298
|
+
offset.multiply(deltaScale);
|
|
299
|
+
obj.position.copy(this._pivotPos).add(offset);
|
|
300
|
+
obj.scale.copy(startScale).multiply(deltaScale);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Vertex snap takes priority over surface snap
|
|
306
|
+
if (
|
|
307
|
+
mode === 'translate' &&
|
|
308
|
+
this._store.vertexSnapActive &&
|
|
309
|
+
this._vertexSnapTargets.length > 0
|
|
310
|
+
) {
|
|
311
|
+
this._applyVertexSnap(gizmoObj, targets);
|
|
312
|
+
} else if (mode === 'translate' && this._store.snapToSurface) {
|
|
313
|
+
this._applySurfaceSnap(gizmoObj, targets);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const helper of this._boxHelpers.values()) {
|
|
317
|
+
if ('update' in helper) (helper as THREE.BoxHelper).update();
|
|
318
|
+
}
|
|
319
|
+
this._flushGizmoToStoreLive();
|
|
320
|
+
});
|
|
321
|
+
this.transformControls.addEventListener('mouseUp', () => {
|
|
322
|
+
// Final flush (with skip flag so viewport doesn't rebuild)
|
|
323
|
+
this._flushGizmoToStore();
|
|
324
|
+
});
|
|
325
|
+
this._gizmoHelper = this.transformControls.getHelper();
|
|
326
|
+
this._gizmoHelper.traverse((child) => child.layers.set(EDITOR_LAYER));
|
|
327
|
+
scene.add(this._gizmoHelper);
|
|
328
|
+
|
|
329
|
+
// Pivot dummy for median-point mode (invisible, just a transform target)
|
|
330
|
+
this._pivotDummy.layers.set(EDITOR_LAYER);
|
|
331
|
+
scene.add(this._pivotDummy);
|
|
332
|
+
|
|
333
|
+
// Surface snap indicator (small ring shown at snap point)
|
|
334
|
+
this._surfaceSnapIndicator = new THREE.Mesh(
|
|
335
|
+
new THREE.RingGeometry(0.08, 0.12, 16),
|
|
336
|
+
new THREE.MeshBasicMaterial({ color: 0x00ff88, side: THREE.DoubleSide, depthTest: false }),
|
|
337
|
+
);
|
|
338
|
+
this._surfaceSnapIndicator.rotation.x = -Math.PI / 2;
|
|
339
|
+
this._surfaceSnapIndicator.visible = false;
|
|
340
|
+
this._surfaceSnapIndicator.renderOrder = 999;
|
|
341
|
+
this._surfaceSnapIndicator.layers.set(EDITOR_LAYER);
|
|
342
|
+
scene.add(this._surfaceSnapIndicator);
|
|
343
|
+
|
|
344
|
+
// Vertex snap indicator (small green sphere shown at snap target vertex)
|
|
345
|
+
this._vertexSnapIndicator = new THREE.Mesh(
|
|
346
|
+
new THREE.SphereGeometry(1, 8, 8),
|
|
347
|
+
new THREE.MeshBasicMaterial({ color: 0x00ffaa, depthTest: false }),
|
|
348
|
+
);
|
|
349
|
+
this._vertexSnapIndicator.visible = false;
|
|
350
|
+
this._vertexSnapIndicator.renderOrder = 999;
|
|
351
|
+
this._vertexSnapIndicator.layers.set(EDITOR_LAYER);
|
|
352
|
+
scene.add(this._vertexSnapIndicator);
|
|
353
|
+
|
|
354
|
+
// Patch gizmo: recolor handles to Godot axis colors and fix plane geometry.
|
|
355
|
+
this._patchGizmo(this._gizmoHelper);
|
|
356
|
+
|
|
357
|
+
// Orientation gizmo (axis arrows overlay)
|
|
358
|
+
this._vcCamera = new THREE.OrthographicCamera(-1.5, 1.5, 1.5, -1.5, 0.1, 10);
|
|
359
|
+
this._initOrientationGizmo();
|
|
360
|
+
|
|
361
|
+
// -- Fly camera input (right-click + WASD) --
|
|
362
|
+
window.addEventListener('keydown', this._onFlyKeyDown);
|
|
363
|
+
window.addEventListener('keyup', this._onFlyKeyUp);
|
|
364
|
+
canvas.addEventListener('pointerdown', this._onFlyPointerDown);
|
|
365
|
+
canvas.addEventListener('pointerup', this._onFlyPointerUp);
|
|
366
|
+
canvas.addEventListener('pointermove', this._onFlyPointerMove);
|
|
367
|
+
canvas.addEventListener('wheel', this._onFlyWheel, { passive: false });
|
|
368
|
+
|
|
369
|
+
// Alt+Left-drag = orbit (Unity-style), via capture phase so it fires before OrbitControls
|
|
370
|
+
canvas.addEventListener(
|
|
371
|
+
'pointerdown',
|
|
372
|
+
(e) => {
|
|
373
|
+
if (e.altKey && e.button === 0) {
|
|
374
|
+
this._altDragOrbit = true;
|
|
375
|
+
this.orbitControls.mouseButtons.LEFT = THREE.MOUSE.ROTATE;
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
{ capture: true },
|
|
379
|
+
);
|
|
380
|
+
canvas.addEventListener(
|
|
381
|
+
'pointerup',
|
|
382
|
+
() => {
|
|
383
|
+
if (this._altDragOrbit) {
|
|
384
|
+
this._altDragOrbit = false;
|
|
385
|
+
this.orbitControls.mouseButtons.LEFT = -1 as THREE.MOUSE;
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
{ capture: true },
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Click to select + marquee
|
|
392
|
+
canvas.addEventListener('pointerdown', this._onPointerDown);
|
|
393
|
+
canvas.addEventListener('pointermove', this._onPointerMove);
|
|
394
|
+
canvas.addEventListener('pointerup', this._onPointerUp);
|
|
395
|
+
|
|
396
|
+
// Asset drag-and-drop onto viewport
|
|
397
|
+
canvas.addEventListener('dragover', this._onDragOver);
|
|
398
|
+
canvas.addEventListener('dragleave', this._onDragLeave);
|
|
399
|
+
canvas.addEventListener('drop', this._onDrop);
|
|
400
|
+
|
|
401
|
+
// Collect editor infrastructure objects so they can be moved between scenes
|
|
402
|
+
this._editorObjects = [
|
|
403
|
+
this.grid,
|
|
404
|
+
this._editorAmbient,
|
|
405
|
+
this._editorDirLight,
|
|
406
|
+
this.batchedRenderer,
|
|
407
|
+
this._gizmoHelper,
|
|
408
|
+
this._pivotDummy,
|
|
409
|
+
this._surfaceSnapIndicator,
|
|
410
|
+
this._vertexSnapIndicator,
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
// D12 (B4) — publish the camera/canvas seam `VgaiSceneAuthoringAdapter.pickable`
|
|
414
|
+
// needs (it has no other way to reach them). Cleared in `dispose()`.
|
|
415
|
+
setViewportPickContext({ camera: this.camera, canvas: this._canvas });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Swap the scene the viewport renders. Moves editor infrastructure objects to the new scene. */
|
|
419
|
+
setScene(newScene: THREE.Scene): void {
|
|
420
|
+
if (newScene === this._scene) return;
|
|
421
|
+
// Move editor infrastructure to new scene
|
|
422
|
+
for (const obj of this._editorObjects) {
|
|
423
|
+
obj.removeFromParent();
|
|
424
|
+
newScene.add(obj);
|
|
425
|
+
}
|
|
426
|
+
// Move any active handles
|
|
427
|
+
for (const h of this._handles) {
|
|
428
|
+
h.removeFromParent();
|
|
429
|
+
newScene.add(h);
|
|
430
|
+
}
|
|
431
|
+
// Move box helpers
|
|
432
|
+
for (const helper of this._boxHelpers.values()) {
|
|
433
|
+
helper.removeFromParent();
|
|
434
|
+
newScene.add(helper);
|
|
435
|
+
}
|
|
436
|
+
this._scene = newScene;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
get currentScene(): THREE.Scene {
|
|
440
|
+
return this._scene;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
get objectMap(): Map<string, THREE.Object3D> {
|
|
444
|
+
return this._objectMap;
|
|
445
|
+
}
|
|
446
|
+
set objectMap(map: Map<string, THREE.Object3D>) {
|
|
447
|
+
this._objectMap = map;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Attach gizmo to entity by id. */
|
|
451
|
+
attach(id: string): void {
|
|
452
|
+
const obj = this._objectMap.get(id);
|
|
453
|
+
if (!obj) return;
|
|
454
|
+
this.transformControls.attach(obj);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Detach gizmo. */
|
|
458
|
+
detach(): void {
|
|
459
|
+
this.transformControls.detach();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Clear and rebuild box helpers for all selected entities. */
|
|
463
|
+
private _syncBoxHelpers(): void {
|
|
464
|
+
// Remove old + dispose geometry/materials on rebuild (LK6).
|
|
465
|
+
for (const helper of this._boxHelpers.values()) {
|
|
466
|
+
this._scene.remove(helper);
|
|
467
|
+
helper.geometry.dispose();
|
|
468
|
+
(helper.material as THREE.Material).dispose();
|
|
469
|
+
}
|
|
470
|
+
this._boxHelpers.clear();
|
|
471
|
+
|
|
472
|
+
const MIN = 0.5;
|
|
473
|
+
const color = new THREE.Color(0x579eff);
|
|
474
|
+
|
|
475
|
+
// Add new
|
|
476
|
+
for (const id of this._store.selectedEntityIds) {
|
|
477
|
+
const obj = this._objectMap.get(id);
|
|
478
|
+
if (!obj) continue;
|
|
479
|
+
|
|
480
|
+
// Check if the entity has enough non-helper geometry for BoxHelper to work.
|
|
481
|
+
// Cameras, lights, and audio sources only have editorHelper children
|
|
482
|
+
// whose geometry BoxHelper ignores or produces a degenerate box.
|
|
483
|
+
let hasVisibleGeometry = false;
|
|
484
|
+
obj.traverse((child) => {
|
|
485
|
+
if (getUserData(child, 'editorHelper')) return;
|
|
486
|
+
if ((child as THREE.Mesh).isMesh || ((child as THREE.Line).isLine && child !== obj)) {
|
|
487
|
+
hasVisibleGeometry = true;
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
if (hasVisibleGeometry) {
|
|
492
|
+
const helper = new THREE.BoxHelper(obj, 0x579eff);
|
|
493
|
+
helper.layers.set(EDITOR_LAYER);
|
|
494
|
+
this._scene.add(helper);
|
|
495
|
+
this._boxHelpers.set(id, helper);
|
|
496
|
+
} else {
|
|
497
|
+
// Create a fixed-size wireframe cube for non-mesh entities
|
|
498
|
+
const geo = new THREE.BoxGeometry(MIN, MIN, MIN);
|
|
499
|
+
const edges = new THREE.EdgesGeometry(geo);
|
|
500
|
+
geo.dispose();
|
|
501
|
+
const mat = new THREE.LineBasicMaterial({ color, toneMapped: false });
|
|
502
|
+
const wire = new THREE.LineSegments(edges, mat) as unknown as TaggedHelper;
|
|
503
|
+
wire.layers.set(EDITOR_LAYER);
|
|
504
|
+
obj.getWorldPosition(wire.position);
|
|
505
|
+
// Tag so we can update/dispose like BoxHelper
|
|
506
|
+
wire.isBoxHelper = true;
|
|
507
|
+
wire._entityObj = obj;
|
|
508
|
+
this._scene.add(wire);
|
|
509
|
+
this._boxHelpers.set(id, wire);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Sync mode/space/snap from store. */
|
|
515
|
+
syncFromStore(): void {
|
|
516
|
+
// Ensure all gizmo children stay on EDITOR_LAYER (TransformControls rebuilds internally)
|
|
517
|
+
this._gizmoHelper.traverse((child) => child.layers.set(EDITOR_LAYER));
|
|
518
|
+
|
|
519
|
+
const mode = this._store.transformMode;
|
|
520
|
+
if (this.transformControls.mode !== mode) {
|
|
521
|
+
this.transformControls.setMode(mode);
|
|
522
|
+
}
|
|
523
|
+
this.transformControls.setSpace(this._store.transformSpace);
|
|
524
|
+
const snap = this._store.snapEnabled;
|
|
525
|
+
const vals = this._store.snapValues;
|
|
526
|
+
this.transformControls.setTranslationSnap(snap ? vals.translate : null);
|
|
527
|
+
this.transformControls.setRotationSnap(snap ? THREE.MathUtils.degToRad(vals.rotate) : null);
|
|
528
|
+
this.transformControls.setScaleSnap(snap ? vals.scale : null);
|
|
529
|
+
|
|
530
|
+
// Grid visibility
|
|
531
|
+
this.grid.visible = this._store.showGrid;
|
|
532
|
+
|
|
533
|
+
// The two passes below (apply gizmos + per-type helper/icon visibility) are
|
|
534
|
+
// idempotent and O(total scene nodes). Their only inputs are the objectMap
|
|
535
|
+
// membership and the visibility toggles — none of which change during a
|
|
536
|
+
// transform drag/scrub (which still fires syncFromStore every frame). Gate
|
|
537
|
+
// them on a cheap signature so a drag doesn't re-walk the whole graph 60×/s.
|
|
538
|
+
const vis = this._store.helperVisibility;
|
|
539
|
+
const gizmoSig = `${this._objectMap.size}|${this._store.showHelpers}|${JSON.stringify(vis)}`;
|
|
540
|
+
if (this._objectMap !== this._lastGizmoObjectMap || gizmoSig !== this._lastGizmoSig) {
|
|
541
|
+
this._lastGizmoObjectMap = this._objectMap;
|
|
542
|
+
this._lastGizmoSig = gizmoSig;
|
|
543
|
+
|
|
544
|
+
// Ensure gizmos (icon billboards + wireframe helpers) exist on all objects.
|
|
545
|
+
// Idempotent — needed for game scene objects created by the runtime without gizmos.
|
|
546
|
+
for (const obj of this._objectMap.values()) {
|
|
547
|
+
applyEntityGizmos(obj);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Helper + icon visibility — per-type filtering
|
|
551
|
+
for (const obj of this._objectMap.values()) {
|
|
552
|
+
obj.traverse((child) => {
|
|
553
|
+
if (getUserData(child, 'editorIcon')) {
|
|
554
|
+
// Icon billboards follow the global helpers toggle
|
|
555
|
+
child.visible = this._store.showHelpers;
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (!getUserData(child, 'editorHelper')) return;
|
|
559
|
+
const type = getUserData(child, 'editorHelperType') as string | undefined;
|
|
560
|
+
if (type && type in vis) {
|
|
561
|
+
child.visible = vis[type as keyof typeof vis];
|
|
562
|
+
} else {
|
|
563
|
+
child.visible = this._store.showHelpers;
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
// NavMesh helper is a scene-root object, toggle separately
|
|
568
|
+
if (this._navMeshHelper) {
|
|
569
|
+
this._navMeshHelper.visible = vis.navmesh;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Attach gizmo based on pivot mode
|
|
574
|
+
const selectedId = this._store.selectedEntityId;
|
|
575
|
+
if (selectedId) {
|
|
576
|
+
if (!this.transformControls.dragging) {
|
|
577
|
+
if (this._store.pivotMode === 'median-point' && this._store.selectedEntityIds.size > 1) {
|
|
578
|
+
const median = new THREE.Vector3();
|
|
579
|
+
let count = 0;
|
|
580
|
+
for (const id of this._store.selectedEntityIds) {
|
|
581
|
+
const obj = this._objectMap.get(id);
|
|
582
|
+
if (obj) {
|
|
583
|
+
median.add(obj.position);
|
|
584
|
+
count++;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (count > 0) median.divideScalar(count);
|
|
588
|
+
this._pivotDummy.position.copy(median);
|
|
589
|
+
this._pivotDummy.quaternion.identity();
|
|
590
|
+
this._pivotDummy.scale.set(1, 1, 1);
|
|
591
|
+
this.transformControls.attach(this._pivotDummy);
|
|
592
|
+
} else {
|
|
593
|
+
// Check for per-entity pivot
|
|
594
|
+
const obj = this._objectMap.get(selectedId);
|
|
595
|
+
const entityPivot = getUserData(obj, 'pivot') as [number, number, number] | undefined;
|
|
596
|
+
const hasPivot =
|
|
597
|
+
entityPivot && (entityPivot[0] !== 0 || entityPivot[1] !== 0 || entityPivot[2] !== 0);
|
|
598
|
+
|
|
599
|
+
if (hasPivot && obj) {
|
|
600
|
+
// Position dummy at pivot's world location
|
|
601
|
+
const pivotLocal = new THREE.Vector3(entityPivot[0], entityPivot[1], entityPivot[2]);
|
|
602
|
+
pivotLocal.applyQuaternion(obj.quaternion);
|
|
603
|
+
this._pivotDummy.position.copy(obj.position).add(pivotLocal);
|
|
604
|
+
this._pivotDummy.quaternion.copy(obj.quaternion);
|
|
605
|
+
this._pivotDummy.scale.copy(obj.scale);
|
|
606
|
+
this.transformControls.attach(this._pivotDummy);
|
|
607
|
+
} else {
|
|
608
|
+
this.attach(selectedId);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
this.detach();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Box helpers for all selected
|
|
617
|
+
this._syncBoxHelpers();
|
|
618
|
+
|
|
619
|
+
// Component handles for selected entities
|
|
620
|
+
if (!this._handleDragActive) {
|
|
621
|
+
this._syncHandles();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Re-add editor infrastructure objects to the scene after play mode
|
|
627
|
+
* has cleared all scene children. Call this after loadDocument restores
|
|
628
|
+
* entities so the grid, lights, gizmos, and particle renderer are present.
|
|
629
|
+
*/
|
|
630
|
+
/** Move all editor gizmos from the editor scene into a target scene (e.g. the live game scene). */
|
|
631
|
+
moveGizmosTo(target: THREE.Scene): void {
|
|
632
|
+
const objs = this._gizmoObjects();
|
|
633
|
+
for (const obj of objs) target.add(obj);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Move all editor gizmos back to the editor scene. */
|
|
637
|
+
restoreGizmos(): void {
|
|
638
|
+
const objs = this._gizmoObjects();
|
|
639
|
+
for (const obj of objs) this._scene.add(obj);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** All editor-layer objects that should travel with the gizmos. */
|
|
643
|
+
private _gizmoObjects(): THREE.Object3D[] {
|
|
644
|
+
return [
|
|
645
|
+
this.grid,
|
|
646
|
+
this._editorAmbient,
|
|
647
|
+
this._editorDirLight,
|
|
648
|
+
this.batchedRenderer,
|
|
649
|
+
this._gizmoHelper,
|
|
650
|
+
this._pivotDummy,
|
|
651
|
+
this._surfaceSnapIndicator,
|
|
652
|
+
];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Focus camera on a given object. */
|
|
656
|
+
focusOn(obj: THREE.Object3D): void {
|
|
657
|
+
const box = new THREE.Box3().setFromObject(obj);
|
|
658
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
659
|
+
const size = box.getSize(new THREE.Vector3()).length();
|
|
660
|
+
// Ensure a minimum zoom distance for entities without visible geometry
|
|
661
|
+
const distance = Math.max(size * 2, 3);
|
|
662
|
+
|
|
663
|
+
const dir = this.camera.position.clone().sub(this.orbitControls.target).normalize();
|
|
664
|
+
this.camera.position.copy(center).add(dir.multiplyScalar(distance));
|
|
665
|
+
this.orbitControls.target.copy(center);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Activate vertex snap: collect target vertices from non-selected entities. */
|
|
669
|
+
activateVertexSnap(): void {
|
|
670
|
+
this._vertexSnapTargets = this._collectVertices(this._store.selectedEntityIds);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Deactivate vertex snap: clear collected vertices and hide indicator. */
|
|
674
|
+
deactivateVertexSnap(): void {
|
|
675
|
+
this._vertexSnapTargets = [];
|
|
676
|
+
this._vertexSnapIndicator.visible = false;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Focus camera on multiple objects. */
|
|
680
|
+
focusOnMultiple(objects: THREE.Object3D[]): void {
|
|
681
|
+
if (objects.length === 0) return;
|
|
682
|
+
if (objects.length === 1) {
|
|
683
|
+
this.focusOn(objects[0]!);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const box = new THREE.Box3();
|
|
687
|
+
for (const obj of objects) box.expandByObject(obj);
|
|
688
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
689
|
+
const size = box.getSize(new THREE.Vector3()).length();
|
|
690
|
+
const distance = size * 2;
|
|
691
|
+
|
|
692
|
+
const dir = this.camera.position.clone().sub(this.orbitControls.target).normalize();
|
|
693
|
+
this.camera.position.copy(center).add(dir.multiplyScalar(distance));
|
|
694
|
+
this.orbitControls.target.copy(center);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Set or clear the navmesh debug helper (managed by editor main). */
|
|
698
|
+
setNavMeshHelper(helper: THREE.Object3D | null): void {
|
|
699
|
+
if (this._navMeshHelper) {
|
|
700
|
+
this._scene.remove(this._navMeshHelper);
|
|
701
|
+
}
|
|
702
|
+
this._navMeshHelper = helper;
|
|
703
|
+
if (helper) {
|
|
704
|
+
setUserData(helper, 'editorHelper', true);
|
|
705
|
+
setUserData(helper, 'editorHelperType', 'navmesh');
|
|
706
|
+
this._scene.add(helper);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Snap camera to a preset view direction, preserving current zoom distance. */
|
|
711
|
+
setViewPreset(preset: 'top' | 'front' | 'right' | 'perspective'): void {
|
|
712
|
+
const target = this.orbitControls.target.clone();
|
|
713
|
+
const distance = this.camera.position.distanceTo(target);
|
|
714
|
+
|
|
715
|
+
const dir = new THREE.Vector3();
|
|
716
|
+
switch (preset) {
|
|
717
|
+
case 'top':
|
|
718
|
+
dir.set(0, 1, 0);
|
|
719
|
+
break;
|
|
720
|
+
case 'front':
|
|
721
|
+
dir.set(0, 0, 1);
|
|
722
|
+
break;
|
|
723
|
+
case 'right':
|
|
724
|
+
dir.set(1, 0, 0);
|
|
725
|
+
break;
|
|
726
|
+
case 'perspective':
|
|
727
|
+
dir.set(1, 1, 1).normalize();
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
this.camera.position.copy(target).add(dir.multiplyScalar(distance));
|
|
732
|
+
this.camera.lookAt(target);
|
|
733
|
+
this.orbitControls.update();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Move the camera to an arbitrary position/target/fov pose — the general
|
|
738
|
+
* case `setViewPreset`/`focusOn` don't cover (an exact xyz position, not a
|
|
739
|
+
* preset direction or a computed frame-to-fit). Backs the `set-camera`
|
|
740
|
+
* relay command (`editor.viewport.camera.set`'s free-pose mode).
|
|
741
|
+
*/
|
|
742
|
+
setPose(
|
|
743
|
+
position: { x: number; y: number; z: number },
|
|
744
|
+
target: { x: number; y: number; z: number },
|
|
745
|
+
fov?: number,
|
|
746
|
+
): void {
|
|
747
|
+
this.camera.position.set(position.x, position.y, position.z);
|
|
748
|
+
this.orbitControls.target.set(target.x, target.y, target.z);
|
|
749
|
+
this.camera.lookAt(this.orbitControls.target);
|
|
750
|
+
if (fov !== undefined) {
|
|
751
|
+
this.camera.fov = fov;
|
|
752
|
+
this.camera.updateProjectionMatrix();
|
|
753
|
+
}
|
|
754
|
+
this.orbitControls.update();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Handle resize. */
|
|
758
|
+
resize(width: number, height: number): void {
|
|
759
|
+
this.camera.aspect = Math.max(1, width) / Math.max(1, height);
|
|
760
|
+
this.camera.updateProjectionMatrix();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
update(dt = 0): void {
|
|
764
|
+
// Animate custom (shader) materials in edit mode — same tick the runtime
|
|
765
|
+
// preRender phase runs, so authored shaders preview live in the viewport.
|
|
766
|
+
tickCustomMaterials(this._scene, dt);
|
|
767
|
+
|
|
768
|
+
// Fly camera movement (before orbit so it takes priority)
|
|
769
|
+
this._updateFlyCamera(dt);
|
|
770
|
+
|
|
771
|
+
this.orbitControls.update();
|
|
772
|
+
|
|
773
|
+
// Snap-to-view animation (runs after orbit update so we have final say on position)
|
|
774
|
+
if (this._snapAnimating) {
|
|
775
|
+
const elapsed = performance.now() - this._snapStartTime;
|
|
776
|
+
let t = Math.min(elapsed / this._snapDuration, 1);
|
|
777
|
+
// easeInOutCubic
|
|
778
|
+
t = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
779
|
+
|
|
780
|
+
const qt = new THREE.Quaternion().slerpQuaternions(this._snapQ1, this._snapQ2, t);
|
|
781
|
+
const dir = new THREE.Vector3(0, 0, 1).applyQuaternion(qt);
|
|
782
|
+
this.camera.position.copy(this.orbitControls.target).add(dir.multiplyScalar(this._snapDist));
|
|
783
|
+
this.camera.lookAt(this.orbitControls.target);
|
|
784
|
+
|
|
785
|
+
if (t >= 1) this._snapAnimating = false;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
for (const helper of this._boxHelpers.values()) {
|
|
789
|
+
const tagged = helper as TaggedHelper;
|
|
790
|
+
if (tagged._entityObj) {
|
|
791
|
+
// Fixed-size wireframe cube — follow the entity's world position
|
|
792
|
+
tagged._entityObj.getWorldPosition(helper.position);
|
|
793
|
+
} else {
|
|
794
|
+
helper.update();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Keep handles at constant screen size
|
|
799
|
+
for (const handle of this._handles) {
|
|
800
|
+
scaleHandle(handle, this.camera);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** Render the orientation gizmo overlaid in the top-right corner. */
|
|
805
|
+
renderViewCube(renderer: THREE.WebGLRenderer): void {
|
|
806
|
+
const w = renderer.domElement.clientWidth;
|
|
807
|
+
const h = renderer.domElement.clientHeight;
|
|
808
|
+
const size = this._vcSize;
|
|
809
|
+
|
|
810
|
+
// Sync gizmo camera with main camera orientation (reuse scratch — runs every frame)
|
|
811
|
+
const dir = this._vcDir.subVectors(this.camera.position, this.orbitControls.target).normalize();
|
|
812
|
+
this._vcCamera.position.copy(dir.multiplyScalar(4));
|
|
813
|
+
this._vcCamera.up.copy(this.camera.up);
|
|
814
|
+
this._vcCamera.lookAt(0, 0, 0);
|
|
815
|
+
|
|
816
|
+
// GL viewport coords (origin bottom-left)
|
|
817
|
+
const px = w - size - this._vcMarginRight;
|
|
818
|
+
const py = h - size - this._vcMarginTop;
|
|
819
|
+
|
|
820
|
+
renderer.setScissorTest(true);
|
|
821
|
+
renderer.setViewport(px, py, size, size);
|
|
822
|
+
renderer.setScissor(px, py, size, size);
|
|
823
|
+
// Only clear depth so main scene shows through; disable autoClear so
|
|
824
|
+
// renderer.render() doesn't wipe the color buffer.
|
|
825
|
+
renderer.clear(false, true, false);
|
|
826
|
+
renderer.autoClear = false;
|
|
827
|
+
renderer.render(this._vcScene, this._vcCamera);
|
|
828
|
+
renderer.autoClear = true;
|
|
829
|
+
|
|
830
|
+
renderer.setScissorTest(false);
|
|
831
|
+
renderer.setViewport(0, 0, w, h);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Render a small camera preview in the bottom-right corner if a camera entity is selected. */
|
|
835
|
+
renderCameraPreview(renderer: THREE.WebGLRenderer): void {
|
|
836
|
+
const selectedEntity = this._store.selectedEntity;
|
|
837
|
+
if (!selectedEntity?.camera) return;
|
|
838
|
+
|
|
839
|
+
const entityObj = this._objectMap.get(selectedEntity.id!);
|
|
840
|
+
if (!entityObj) return;
|
|
841
|
+
const entityCamera = getUserData(entityObj, '_camera') as THREE.Camera | undefined;
|
|
842
|
+
if (!entityCamera) return;
|
|
843
|
+
|
|
844
|
+
const w = renderer.domElement.clientWidth;
|
|
845
|
+
const h = renderer.domElement.clientHeight;
|
|
846
|
+
const camW = selectedEntity.camera.width ?? 1920;
|
|
847
|
+
const camH = selectedEntity.camera.height ?? 1080;
|
|
848
|
+
const pw = Math.floor(w * 0.25);
|
|
849
|
+
const ph = Math.floor(pw * (camH / camW));
|
|
850
|
+
const marginRight = 12;
|
|
851
|
+
const marginBottom = 26 + 12;
|
|
852
|
+
const border = 2;
|
|
853
|
+
|
|
854
|
+
// Hide editor-only objects for the preview
|
|
855
|
+
this.grid.visible = false;
|
|
856
|
+
this._gizmoHelper.visible = false;
|
|
857
|
+
for (const helper of this._boxHelpers.values()) helper.visible = false;
|
|
858
|
+
// Hide the frustum helper lines so they don't appear in the camera's own view
|
|
859
|
+
const hiddenHelpers: THREE.Object3D[] = [];
|
|
860
|
+
entityObj.traverse((child) => {
|
|
861
|
+
if (child instanceof THREE.LineSegments && child.visible) {
|
|
862
|
+
child.visible = false;
|
|
863
|
+
hiddenHelpers.push(child);
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// Save and override clear color for border
|
|
868
|
+
const prevClearColor = renderer.getClearColor(this._previewClearColor);
|
|
869
|
+
const prevClearAlpha = renderer.getClearAlpha();
|
|
870
|
+
|
|
871
|
+
renderer.setScissorTest(true);
|
|
872
|
+
|
|
873
|
+
// Draw border area
|
|
874
|
+
const px = w - pw - marginRight;
|
|
875
|
+
const py = marginBottom;
|
|
876
|
+
renderer.setViewport(px - border, py - border, pw + border * 2, ph + border * 2);
|
|
877
|
+
renderer.setScissor(px - border, py - border, pw + border * 2, ph + border * 2);
|
|
878
|
+
renderer.setClearColor(0x666666, 1);
|
|
879
|
+
renderer.clear();
|
|
880
|
+
|
|
881
|
+
// Render preview
|
|
882
|
+
renderer.setViewport(px, py, pw, ph);
|
|
883
|
+
renderer.setScissor(px, py, pw, ph);
|
|
884
|
+
renderer.render(this._scene, entityCamera);
|
|
885
|
+
|
|
886
|
+
// Restore
|
|
887
|
+
renderer.setScissorTest(false);
|
|
888
|
+
renderer.setViewport(0, 0, w, h);
|
|
889
|
+
renderer.setClearColor(prevClearColor, prevClearAlpha);
|
|
890
|
+
|
|
891
|
+
// Restore visibility
|
|
892
|
+
this.grid.visible = true;
|
|
893
|
+
this._gizmoHelper.visible = true;
|
|
894
|
+
for (const helper of this._boxHelpers.values()) helper.visible = true;
|
|
895
|
+
for (const helper of hiddenHelpers) helper.visible = true;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** True when the fly camera is active (right-click + movement key held). */
|
|
899
|
+
get isFlying(): boolean {
|
|
900
|
+
return this._flyActive;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
dispose(): void {
|
|
904
|
+
setViewportPickContext(null);
|
|
905
|
+
window.removeEventListener('keydown', this._onFlyKeyDown);
|
|
906
|
+
window.removeEventListener('keyup', this._onFlyKeyUp);
|
|
907
|
+
this._canvas.removeEventListener('pointerdown', this._onFlyPointerDown);
|
|
908
|
+
this._canvas.removeEventListener('pointerup', this._onFlyPointerUp);
|
|
909
|
+
this._canvas.removeEventListener('pointermove', this._onFlyPointerMove);
|
|
910
|
+
this._canvas.removeEventListener('wheel', this._onFlyWheel);
|
|
911
|
+
this._canvas.removeEventListener('pointerdown', this._onPointerDown);
|
|
912
|
+
this._canvas.removeEventListener('pointermove', this._onPointerMove);
|
|
913
|
+
this._canvas.removeEventListener('pointerup', this._onPointerUp);
|
|
914
|
+
this._canvas.removeEventListener('dragover', this._onDragOver);
|
|
915
|
+
this._canvas.removeEventListener('dragleave', this._onDragLeave);
|
|
916
|
+
this._canvas.removeEventListener('drop', this._onDrop);
|
|
917
|
+
this._hideDropOverlay();
|
|
918
|
+
for (const h of this._handles) {
|
|
919
|
+
this._scene.remove(h);
|
|
920
|
+
(h.material as THREE.Material).dispose();
|
|
921
|
+
}
|
|
922
|
+
this._handles = [];
|
|
923
|
+
for (const helper of this._boxHelpers.values()) {
|
|
924
|
+
this._scene.remove(helper);
|
|
925
|
+
helper.geometry.dispose();
|
|
926
|
+
(helper.material as THREE.Material).dispose();
|
|
927
|
+
}
|
|
928
|
+
this._boxHelpers.clear();
|
|
929
|
+
this.orbitControls.dispose();
|
|
930
|
+
this.transformControls.dispose();
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// --- Private ---
|
|
934
|
+
|
|
935
|
+
/** Build the orientation gizmo: colored axis arrows with cone tips, labels, and click targets. */
|
|
936
|
+
private _initOrientationGizmo(): void {
|
|
937
|
+
this._vcScene.add(new THREE.AmbientLight(0xffffff, 0.8));
|
|
938
|
+
const light = new THREE.DirectionalLight(0xffffff, 0.5);
|
|
939
|
+
light.position.set(1, 2, 3);
|
|
940
|
+
this._vcScene.add(light);
|
|
941
|
+
|
|
942
|
+
// Godot axis colors
|
|
943
|
+
const colors = [
|
|
944
|
+
new THREE.Color(0.96, 0.2, 0.32), // X
|
|
945
|
+
new THREE.Color(0.53, 0.84, 0.01), // Y
|
|
946
|
+
new THREE.Color(0.16, 0.55, 0.96), // Z
|
|
947
|
+
];
|
|
948
|
+
const axes = [
|
|
949
|
+
new THREE.Vector3(1, 0, 0),
|
|
950
|
+
new THREE.Vector3(0, 1, 0),
|
|
951
|
+
new THREE.Vector3(0, 0, 1),
|
|
952
|
+
];
|
|
953
|
+
const names = ['X', 'Y', 'Z'];
|
|
954
|
+
const shaft = 0.9;
|
|
955
|
+
const tip = 0.15;
|
|
956
|
+
const targetGeo = new THREE.SphereGeometry(0.2, 8, 8);
|
|
957
|
+
const targetMat = new THREE.MeshBasicMaterial({ visible: false });
|
|
958
|
+
|
|
959
|
+
for (let i = 0; i < 3; i++) {
|
|
960
|
+
const c = colors[i]!;
|
|
961
|
+
const a = axes[i]!;
|
|
962
|
+
|
|
963
|
+
// --- Positive axis ---
|
|
964
|
+
|
|
965
|
+
// Shaft line
|
|
966
|
+
this._vcScene.add(
|
|
967
|
+
new THREE.Line(
|
|
968
|
+
new THREE.BufferGeometry().setFromPoints([
|
|
969
|
+
new THREE.Vector3(),
|
|
970
|
+
a.clone().multiplyScalar(shaft),
|
|
971
|
+
]),
|
|
972
|
+
new THREE.LineBasicMaterial({ color: c }),
|
|
973
|
+
),
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
// Cone arrowhead
|
|
977
|
+
const cone = new THREE.Mesh(
|
|
978
|
+
new THREE.ConeGeometry(0.055, tip, 8),
|
|
979
|
+
new THREE.MeshBasicMaterial({ color: c }),
|
|
980
|
+
);
|
|
981
|
+
cone.position.copy(a.clone().multiplyScalar(shaft + tip / 2));
|
|
982
|
+
if (i === 0) cone.rotation.z = -Math.PI / 2;
|
|
983
|
+
if (i === 2) cone.rotation.x = Math.PI / 2;
|
|
984
|
+
this._vcScene.add(cone);
|
|
985
|
+
|
|
986
|
+
// Label sprite
|
|
987
|
+
this._vcScene.add(
|
|
988
|
+
this._makeAxisLabel(names[i]!, c, a.clone().multiplyScalar(shaft + tip + 0.15)),
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
// Click target (invisible sphere)
|
|
992
|
+
const posTarget = new THREE.Mesh(targetGeo, targetMat);
|
|
993
|
+
posTarget.position.copy(a.clone().multiplyScalar(shaft * 0.6));
|
|
994
|
+
setUserData(posTarget, 'vcDirIdx', i * 2); // 0=+X, 2=+Y, 4=+Z
|
|
995
|
+
this._vcScene.add(posTarget);
|
|
996
|
+
this._vcClickTargets.push(posTarget);
|
|
997
|
+
|
|
998
|
+
// --- Negative axis ---
|
|
999
|
+
|
|
1000
|
+
// Faint line for negative direction
|
|
1001
|
+
this._vcScene.add(
|
|
1002
|
+
new THREE.Line(
|
|
1003
|
+
new THREE.BufferGeometry().setFromPoints([
|
|
1004
|
+
new THREE.Vector3(),
|
|
1005
|
+
a.clone().multiplyScalar(-0.45),
|
|
1006
|
+
]),
|
|
1007
|
+
new THREE.LineBasicMaterial({ color: c, transparent: true, opacity: 0.3 }),
|
|
1008
|
+
),
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
// Small dot at negative end
|
|
1012
|
+
const negDot = new THREE.Mesh(
|
|
1013
|
+
new THREE.SphereGeometry(0.06, 8, 8),
|
|
1014
|
+
new THREE.MeshBasicMaterial({ color: c, transparent: true, opacity: 0.4 }),
|
|
1015
|
+
);
|
|
1016
|
+
negDot.position.copy(a.clone().multiplyScalar(-0.5));
|
|
1017
|
+
this._vcScene.add(negDot);
|
|
1018
|
+
|
|
1019
|
+
// Click target for negative axis
|
|
1020
|
+
const negTarget = new THREE.Mesh(targetGeo, targetMat);
|
|
1021
|
+
negTarget.position.copy(a.clone().multiplyScalar(-0.45));
|
|
1022
|
+
setUserData(negTarget, 'vcDirIdx', i * 2 + 1); // 1=-X, 3=-Y, 5=-Z
|
|
1023
|
+
this._vcScene.add(negTarget);
|
|
1024
|
+
this._vcClickTargets.push(negTarget);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Center dot
|
|
1028
|
+
this._vcScene.add(
|
|
1029
|
+
new THREE.Mesh(
|
|
1030
|
+
new THREE.SphereGeometry(0.05, 8, 8),
|
|
1031
|
+
new THREE.MeshBasicMaterial({ color: 0x888888 }),
|
|
1032
|
+
),
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/** Create a sprite with a single axis letter. */
|
|
1037
|
+
private _makeAxisLabel(text: string, color: THREE.Color, position: THREE.Vector3): THREE.Sprite {
|
|
1038
|
+
const size = 64;
|
|
1039
|
+
const c = document.createElement('canvas');
|
|
1040
|
+
c.width = size;
|
|
1041
|
+
c.height = size;
|
|
1042
|
+
const ctx = c.getContext('2d')!;
|
|
1043
|
+
ctx.fillStyle = `#${color.getHexString()}`;
|
|
1044
|
+
ctx.font = 'bold 44px system-ui, -apple-system, sans-serif';
|
|
1045
|
+
ctx.textAlign = 'center';
|
|
1046
|
+
ctx.textBaseline = 'middle';
|
|
1047
|
+
ctx.fillText(text, size / 2, size / 2);
|
|
1048
|
+
|
|
1049
|
+
const texture = new THREE.CanvasTexture(c);
|
|
1050
|
+
const mat = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false });
|
|
1051
|
+
const sprite = new THREE.Sprite(mat);
|
|
1052
|
+
sprite.position.copy(position);
|
|
1053
|
+
sprite.scale.set(0.25, 0.25, 0.25);
|
|
1054
|
+
return sprite;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** Returns true if the click was inside the gizmo area (consumed). */
|
|
1058
|
+
private _handleViewCubeClick(e: PointerEvent): boolean {
|
|
1059
|
+
const rect = this._canvas.getBoundingClientRect();
|
|
1060
|
+
const x = e.clientX - rect.left;
|
|
1061
|
+
const y = e.clientY - rect.top;
|
|
1062
|
+
const w = rect.width;
|
|
1063
|
+
|
|
1064
|
+
// Screen-space gizmo rect
|
|
1065
|
+
const vcLeft = w - this._vcSize - this._vcMarginRight;
|
|
1066
|
+
const vcTop = this._vcMarginTop;
|
|
1067
|
+
|
|
1068
|
+
if (x < vcLeft || x > vcLeft + this._vcSize || y < vcTop || y > vcTop + this._vcSize) {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Convert to NDC for the gizmo camera
|
|
1073
|
+
const ndcX = ((x - vcLeft) / this._vcSize) * 2 - 1;
|
|
1074
|
+
const ndcY = -(((y - vcTop) / this._vcSize) * 2 - 1);
|
|
1075
|
+
|
|
1076
|
+
this._vcRaycaster.setFromCamera(new THREE.Vector2(ndcX, ndcY), this._vcCamera);
|
|
1077
|
+
const hits = this._vcRaycaster.intersectObjects(this._vcClickTargets);
|
|
1078
|
+
if (hits.length === 0) return true; // In gizmo area but missed — still consume
|
|
1079
|
+
|
|
1080
|
+
const dirIdx = getUserData(hits[0]!.object, 'vcDirIdx') as number;
|
|
1081
|
+
const targetDir = VC_DIRS[dirIdx]!;
|
|
1082
|
+
|
|
1083
|
+
// Set up slerp animation
|
|
1084
|
+
const startDir = new THREE.Vector3()
|
|
1085
|
+
.subVectors(this.camera.position, this.orbitControls.target)
|
|
1086
|
+
.normalize();
|
|
1087
|
+
|
|
1088
|
+
this._snapDist = this.camera.position.distanceTo(this.orbitControls.target);
|
|
1089
|
+
this._snapQ1.setFromUnitVectors(new THREE.Vector3(0, 0, 1), startDir);
|
|
1090
|
+
this._snapQ2.setFromUnitVectors(new THREE.Vector3(0, 0, 1), targetDir);
|
|
1091
|
+
this._snapStartTime = performance.now();
|
|
1092
|
+
this._snapAnimating = true;
|
|
1093
|
+
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** Live flush during drag — updates inspector without undo or scene rebuild. */
|
|
1098
|
+
private _flushGizmoToStoreLive(): void {
|
|
1099
|
+
this._skipNextSync = true;
|
|
1100
|
+
// Live transform edits flow through the AuthoringAdapter (the gizmo already
|
|
1101
|
+
// moved the Object3D; apply syncs the descriptor without pushing undo).
|
|
1102
|
+
const transforms = getActiveAuthoring(this._store).transforms;
|
|
1103
|
+
const applyLive = (o: THREE.Object3D): void => {
|
|
1104
|
+
const id = getUserData(o, 'entityId') as string | undefined;
|
|
1105
|
+
if (!id) return;
|
|
1106
|
+
transforms?.apply(id, {
|
|
1107
|
+
position: o.position.toArray() as [number, number, number],
|
|
1108
|
+
rotation: o.quaternion.toArray() as [number, number, number, number],
|
|
1109
|
+
scale: o.scale.toArray() as [number, number, number],
|
|
1110
|
+
});
|
|
1111
|
+
};
|
|
1112
|
+
// Primary (if it's a real entity, not pivot dummy)
|
|
1113
|
+
const obj = this.transformControls.object;
|
|
1114
|
+
if (obj) applyLive(obj);
|
|
1115
|
+
// Other selected objects (active-element / individual-origins)
|
|
1116
|
+
for (const { obj: other } of this._otherDragObjects) applyLive(other);
|
|
1117
|
+
// All selected objects (median-point mode — gizmo is on dummy)
|
|
1118
|
+
for (const { obj: mo } of this._allDragObjects) applyLive(mo);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Surface snap: raycast downward from the primary object to find
|
|
1123
|
+
* a surface and adjust Y positions accordingly.
|
|
1124
|
+
*/
|
|
1125
|
+
private _applySurfaceSnap(
|
|
1126
|
+
gizmoObj: THREE.Object3D,
|
|
1127
|
+
targets: {
|
|
1128
|
+
obj: THREE.Object3D;
|
|
1129
|
+
startPos: THREE.Vector3;
|
|
1130
|
+
startQuat: THREE.Quaternion;
|
|
1131
|
+
startScale: THREE.Vector3;
|
|
1132
|
+
}[],
|
|
1133
|
+
): void {
|
|
1134
|
+
// Collect all dragged entity IDs to exclude from raycast
|
|
1135
|
+
const draggedIds = new Set<string>();
|
|
1136
|
+
const gizmoId = getUserData(gizmoObj, 'entityId') as string | undefined;
|
|
1137
|
+
if (gizmoId) draggedIds.add(gizmoId);
|
|
1138
|
+
for (const { obj } of targets) {
|
|
1139
|
+
const id = getUserData(obj, 'entityId') as string | undefined;
|
|
1140
|
+
if (id) draggedIds.add(id);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Collect scene objects that are NOT being dragged
|
|
1144
|
+
const sceneTargets: THREE.Object3D[] = [];
|
|
1145
|
+
for (const [id, obj] of this._objectMap) {
|
|
1146
|
+
if (!draggedIds.has(id)) sceneTargets.push(obj);
|
|
1147
|
+
}
|
|
1148
|
+
// Also include the grid as a snap target
|
|
1149
|
+
sceneTargets.push(this.grid);
|
|
1150
|
+
|
|
1151
|
+
if (sceneTargets.length === 0) {
|
|
1152
|
+
this._surfaceSnapIndicator.visible = false;
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Raycast downward from the gizmo object position
|
|
1157
|
+
const origin = gizmoObj.position.clone();
|
|
1158
|
+
origin.y += 50; // Start high above
|
|
1159
|
+
this._surfaceSnapRaycaster.set(origin, new THREE.Vector3(0, -1, 0));
|
|
1160
|
+
this._surfaceSnapRaycaster.far = 200;
|
|
1161
|
+
|
|
1162
|
+
const hits = this._surfaceSnapRaycaster.intersectObjects(sceneTargets, true);
|
|
1163
|
+
if (hits.length > 0) {
|
|
1164
|
+
const hitPoint = hits[0]!.point;
|
|
1165
|
+
const snapY = hitPoint.y;
|
|
1166
|
+
const deltaY = snapY - gizmoObj.position.y;
|
|
1167
|
+
|
|
1168
|
+
gizmoObj.position.y = snapY;
|
|
1169
|
+
for (const { obj } of targets) {
|
|
1170
|
+
obj.position.y += deltaY;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Show snap indicator at hit point
|
|
1174
|
+
this._surfaceSnapIndicator.position.copy(hitPoint);
|
|
1175
|
+
this._surfaceSnapIndicator.position.y += 0.01; // Slight offset to avoid z-fighting
|
|
1176
|
+
// Orient ring to match surface normal
|
|
1177
|
+
const normal = hits[0]!.face?.normal;
|
|
1178
|
+
if (normal) {
|
|
1179
|
+
const worldNormal = normal.clone().transformDirection(hits[0]!.object.matrixWorld);
|
|
1180
|
+
this._surfaceSnapIndicator.quaternion.setFromUnitVectors(
|
|
1181
|
+
new THREE.Vector3(0, 1, 0),
|
|
1182
|
+
worldNormal,
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
this._surfaceSnapIndicator.visible = true;
|
|
1186
|
+
} else {
|
|
1187
|
+
this._surfaceSnapIndicator.visible = false;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Collect world-space vertex positions from entity meshes.
|
|
1193
|
+
* Excludes entities in `excludeIds` and editor helper children.
|
|
1194
|
+
*/
|
|
1195
|
+
private _collectVertices(excludeIds: ReadonlySet<string>): THREE.Vector3[] {
|
|
1196
|
+
const vertices: THREE.Vector3[] = [];
|
|
1197
|
+
const MAX_PER_MESH = 1000;
|
|
1198
|
+
for (const [id, obj] of this._objectMap) {
|
|
1199
|
+
if (excludeIds.has(id)) continue;
|
|
1200
|
+
obj.traverse((child) => {
|
|
1201
|
+
if (getUserData(child, 'editorHelper')) return;
|
|
1202
|
+
const mesh = child as THREE.Mesh;
|
|
1203
|
+
if (!mesh.isMesh || !mesh.geometry) return;
|
|
1204
|
+
const posAttr = mesh.geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
1205
|
+
if (!posAttr) return;
|
|
1206
|
+
const count = Math.min(posAttr.count, MAX_PER_MESH);
|
|
1207
|
+
const worldMatrix = mesh.matrixWorld;
|
|
1208
|
+
for (let i = 0; i < count; i++) {
|
|
1209
|
+
const v = new THREE.Vector3(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i));
|
|
1210
|
+
v.applyMatrix4(worldMatrix);
|
|
1211
|
+
vertices.push(v);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
return vertices;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Vertex snap: snap the gizmo object to the nearest vertex on non-selected geometry.
|
|
1220
|
+
*/
|
|
1221
|
+
private _applyVertexSnap(
|
|
1222
|
+
gizmoObj: THREE.Object3D,
|
|
1223
|
+
targets: { obj: THREE.Object3D; startPos: THREE.Vector3 }[],
|
|
1224
|
+
): void {
|
|
1225
|
+
const entityPos = gizmoObj.position;
|
|
1226
|
+
const projected = entityPos.clone().project(this.camera);
|
|
1227
|
+
const rect = this._canvas.getBoundingClientRect();
|
|
1228
|
+
const entityScreenX = ((projected.x + 1) / 2) * rect.width;
|
|
1229
|
+
const entityScreenY = ((-projected.y + 1) / 2) * rect.height;
|
|
1230
|
+
|
|
1231
|
+
let bestDist = this._vertexSnapThreshold;
|
|
1232
|
+
let bestVertex: THREE.Vector3 | null = null;
|
|
1233
|
+
|
|
1234
|
+
for (const vert of this._vertexSnapTargets) {
|
|
1235
|
+
const vs = vert.clone().project(this.camera);
|
|
1236
|
+
// Skip vertices behind camera
|
|
1237
|
+
if (vs.z > 1) continue;
|
|
1238
|
+
const vertScreenX = ((vs.x + 1) / 2) * rect.width;
|
|
1239
|
+
const vertScreenY = ((-vs.y + 1) / 2) * rect.height;
|
|
1240
|
+
const dx = entityScreenX - vertScreenX;
|
|
1241
|
+
const dy = entityScreenY - vertScreenY;
|
|
1242
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1243
|
+
if (dist < bestDist) {
|
|
1244
|
+
bestDist = dist;
|
|
1245
|
+
bestVertex = vert;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (bestVertex) {
|
|
1250
|
+
const delta = bestVertex.clone().sub(entityPos);
|
|
1251
|
+
gizmoObj.position.copy(bestVertex);
|
|
1252
|
+
for (const { obj } of targets) {
|
|
1253
|
+
obj.position.add(delta);
|
|
1254
|
+
}
|
|
1255
|
+
this._vertexSnapIndicator.position.copy(bestVertex);
|
|
1256
|
+
this._vertexSnapIndicator.visible = true;
|
|
1257
|
+
// Constant screen size for indicator
|
|
1258
|
+
const dist = bestVertex.distanceTo(this.camera.position);
|
|
1259
|
+
this._vertexSnapIndicator.scale.setScalar(dist * 0.008);
|
|
1260
|
+
} else {
|
|
1261
|
+
this._vertexSnapIndicator.visible = false;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/** Final flush on drag end — no undo push (already pushed on drag start). */
|
|
1266
|
+
private _flushGizmoToStore(): void {
|
|
1267
|
+
this._flushGizmoToStoreLive();
|
|
1268
|
+
this._otherDragObjects = [];
|
|
1269
|
+
this._allDragObjects = [];
|
|
1270
|
+
// Persist the final transform + mark dirty (ED2). A mid-drag autosave may
|
|
1271
|
+
// have already fired and cleared dirty; this re-schedules the final write.
|
|
1272
|
+
getActiveAuthoring(this._store).transforms?.endEdit('');
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/** Check if we should skip a sync (gizmo already moved the object). */
|
|
1276
|
+
consumeSkipSync(): boolean {
|
|
1277
|
+
if (this._skipNextSync) {
|
|
1278
|
+
this._skipNextSync = false;
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/** Recolor gizmo handles to Godot axis colors & shift plane geometry to meet at center. */
|
|
1285
|
+
private _patchGizmo(gizmoHelper: THREE.Object3D): void {
|
|
1286
|
+
// Godot axis colors: Color(0.96,0.20,0.32), Color(0.53,0.84,0.01), Color(0.16,0.55,0.96)
|
|
1287
|
+
const axisX = new THREE.Color(0.96, 0.2, 0.32);
|
|
1288
|
+
const axisY = new THREE.Color(0.53, 0.84, 0.01);
|
|
1289
|
+
const axisZ = new THREE.Color(0.16, 0.55, 0.96);
|
|
1290
|
+
|
|
1291
|
+
// Handle color: axis handles use axis color, plane handles use normal-axis color
|
|
1292
|
+
const colorMap: Record<string, THREE.Color> = {
|
|
1293
|
+
X: axisX,
|
|
1294
|
+
Y: axisY,
|
|
1295
|
+
Z: axisZ,
|
|
1296
|
+
XY: axisZ,
|
|
1297
|
+
YZ: axisX,
|
|
1298
|
+
XZ: axisY,
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
// Plane geometry offsets to bring inner corners to object center
|
|
1302
|
+
const planeOffsets: Record<string, [number, number, number]> = {
|
|
1303
|
+
XY: [-0.075, -0.075, 0],
|
|
1304
|
+
YZ: [0, -0.075, -0.075],
|
|
1305
|
+
XZ: [-0.075, 0, -0.075],
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
gizmoHelper.traverse((child) => {
|
|
1309
|
+
const mesh = child as THREE.Mesh;
|
|
1310
|
+
|
|
1311
|
+
// Recolor handle materials
|
|
1312
|
+
const newColor = colorMap[child.name];
|
|
1313
|
+
if (newColor && mesh.material) {
|
|
1314
|
+
const mat = mesh.material as THREE.MeshBasicMaterial | THREE.LineBasicMaterial;
|
|
1315
|
+
if (mat.color) mat.color.copy(newColor);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Shift plane handle geometry
|
|
1319
|
+
const offset = planeOffsets[child.name];
|
|
1320
|
+
if (offset && mesh.geometry) {
|
|
1321
|
+
mesh.geometry.translate(offset[0], offset[1], offset[2]);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/** Raycast against component handles and return the hit handle, or null. */
|
|
1327
|
+
private _raycastHandle(e: PointerEvent): HandleMesh | null {
|
|
1328
|
+
if (this._handles.length === 0) return null;
|
|
1329
|
+
const rect = this._canvas.getBoundingClientRect();
|
|
1330
|
+
const mouse = new THREE.Vector2(
|
|
1331
|
+
((e.clientX - rect.left) / rect.width) * 2 - 1,
|
|
1332
|
+
-((e.clientY - rect.top) / rect.height) * 2 + 1,
|
|
1333
|
+
);
|
|
1334
|
+
this._handleRaycaster.setFromCamera(mouse, this.camera);
|
|
1335
|
+
const intersects = this._handleRaycaster.intersectObjects(this._handles);
|
|
1336
|
+
return intersects.length > 0 ? (intersects[0]!.object as HandleMesh) : null;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/** Rebuild component handles for the current selection. */
|
|
1340
|
+
private _syncHandles(): void {
|
|
1341
|
+
// Remove old handles + dispose their materials (LK6). The geometry is a
|
|
1342
|
+
// shared module-level sphere, so it must NOT be disposed.
|
|
1343
|
+
for (const h of this._handles) {
|
|
1344
|
+
this._scene.remove(h);
|
|
1345
|
+
(h.material as THREE.Material).dispose();
|
|
1346
|
+
}
|
|
1347
|
+
this._handles = [];
|
|
1348
|
+
|
|
1349
|
+
// Create new handles for selected entities
|
|
1350
|
+
for (const id of this._store.selectedEntityIds) {
|
|
1351
|
+
const obj = this._objectMap.get(id);
|
|
1352
|
+
const entity = getUserData(obj, 'entity') as SceneEntity | undefined;
|
|
1353
|
+
if (!obj || !entity) continue;
|
|
1354
|
+
|
|
1355
|
+
const defs = createHandleDefs(entity);
|
|
1356
|
+
for (const def of defs) {
|
|
1357
|
+
const mesh = createHandleMesh(def);
|
|
1358
|
+
mesh.position.copy(def.getWorldPosition(obj, entity));
|
|
1359
|
+
scaleHandle(mesh, this.camera);
|
|
1360
|
+
this._scene.add(mesh);
|
|
1361
|
+
this._handles.push(mesh);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/** Project entity center to screen coords. */
|
|
1367
|
+
private _projectToScreen(obj: THREE.Object3D): THREE.Vector2 {
|
|
1368
|
+
const pos = new THREE.Vector3();
|
|
1369
|
+
obj.getWorldPosition(pos);
|
|
1370
|
+
pos.project(this.camera);
|
|
1371
|
+
const rect = this._canvas.getBoundingClientRect();
|
|
1372
|
+
return new THREE.Vector2(
|
|
1373
|
+
((pos.x + 1) / 2) * rect.width + rect.left,
|
|
1374
|
+
((-pos.y + 1) / 2) * rect.height + rect.top,
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// -- Fly camera handlers --
|
|
1379
|
+
|
|
1380
|
+
private static _FLY_KEYS = new Set(['w', 'a', 's', 'd', 'q', 'e']);
|
|
1381
|
+
|
|
1382
|
+
private _enterFlyMode(): void {
|
|
1383
|
+
if (this._flyActive) return;
|
|
1384
|
+
this._flyActive = true;
|
|
1385
|
+
this._snapAnimating = false; // cancel any view snap
|
|
1386
|
+
this.orbitControls.enabled = false;
|
|
1387
|
+
// Sync euler from current camera orientation
|
|
1388
|
+
this._flyEuler.setFromQuaternion(this.camera.quaternion, 'YXZ');
|
|
1389
|
+
this._canvas.requestPointerLock?.();
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
private _exitFlyMode(): void {
|
|
1393
|
+
if (!this._flyActive) return;
|
|
1394
|
+
this._flyActive = false;
|
|
1395
|
+
this._flyKeys.clear();
|
|
1396
|
+
this.orbitControls.enabled = true;
|
|
1397
|
+
document.exitPointerLock();
|
|
1398
|
+
// Sync orbit target to a point in front of the camera
|
|
1399
|
+
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion);
|
|
1400
|
+
const dist = this.camera.position.distanceTo(this.orbitControls.target);
|
|
1401
|
+
this.orbitControls.target
|
|
1402
|
+
.copy(this.camera.position)
|
|
1403
|
+
.add(forward.multiplyScalar(Math.max(dist, 5)));
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/** Apply fly camera movement each frame. Called from update(). */
|
|
1407
|
+
private _updateFlyCamera(dt: number): void {
|
|
1408
|
+
if (!this._flyActive || this._flyKeys.size === 0) return;
|
|
1409
|
+
|
|
1410
|
+
const speed = this._flySpeed * (this._flyKeys.has('shift') ? 3 : 1);
|
|
1411
|
+
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(this.camera.quaternion);
|
|
1412
|
+
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(this.camera.quaternion);
|
|
1413
|
+
const move = new THREE.Vector3();
|
|
1414
|
+
|
|
1415
|
+
if (this._flyKeys.has('w')) move.add(forward);
|
|
1416
|
+
if (this._flyKeys.has('s')) move.sub(forward);
|
|
1417
|
+
if (this._flyKeys.has('d')) move.add(right);
|
|
1418
|
+
if (this._flyKeys.has('a')) move.sub(right);
|
|
1419
|
+
if (this._flyKeys.has('e')) move.y += 1;
|
|
1420
|
+
if (this._flyKeys.has('q')) move.y -= 1;
|
|
1421
|
+
|
|
1422
|
+
if (move.lengthSq() > 0) {
|
|
1423
|
+
move.normalize().multiplyScalar(speed * dt);
|
|
1424
|
+
this.camera.position.add(move);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
private _onFlyKeyDown = (e: KeyboardEvent): void => {
|
|
1429
|
+
if (!this._rightMouseDown) return;
|
|
1430
|
+
// Skip when typing in form inputs
|
|
1431
|
+
if (
|
|
1432
|
+
e.target instanceof HTMLInputElement ||
|
|
1433
|
+
e.target instanceof HTMLTextAreaElement ||
|
|
1434
|
+
e.target instanceof HTMLSelectElement
|
|
1435
|
+
)
|
|
1436
|
+
return;
|
|
1437
|
+
|
|
1438
|
+
const key = e.key.toLowerCase();
|
|
1439
|
+
if (key === 'shift') {
|
|
1440
|
+
this._flyKeys.add('shift');
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (!EditorViewport._FLY_KEYS.has(key)) return;
|
|
1444
|
+
e.preventDefault();
|
|
1445
|
+
e.stopPropagation();
|
|
1446
|
+
this._flyKeys.add(key);
|
|
1447
|
+
if (!this._flyActive) this._enterFlyMode();
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
private _onFlyKeyUp = (e: KeyboardEvent): void => {
|
|
1451
|
+
const key = e.key.toLowerCase();
|
|
1452
|
+
if (key === 'shift') {
|
|
1453
|
+
this._flyKeys.delete('shift');
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
this._flyKeys.delete(key);
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
private _onFlyPointerDown = (e: PointerEvent): void => {
|
|
1460
|
+
if (e.button === 2) this._rightMouseDown = true;
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
private _onFlyPointerUp = (e: PointerEvent): void => {
|
|
1464
|
+
if (e.button === 2) {
|
|
1465
|
+
this._rightMouseDown = false;
|
|
1466
|
+
if (this._flyActive) this._exitFlyMode();
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
private _onFlyPointerMove = (e: PointerEvent): void => {
|
|
1471
|
+
if (!this._flyActive) return;
|
|
1472
|
+
const sensitivity = 0.002;
|
|
1473
|
+
this._flyEuler.y -= e.movementX * sensitivity;
|
|
1474
|
+
this._flyEuler.x -= e.movementY * sensitivity;
|
|
1475
|
+
// Clamp pitch to avoid gimbal flip
|
|
1476
|
+
this._flyEuler.x = Math.max(
|
|
1477
|
+
-Math.PI / 2 + 0.01,
|
|
1478
|
+
Math.min(Math.PI / 2 - 0.01, this._flyEuler.x),
|
|
1479
|
+
);
|
|
1480
|
+
this.camera.quaternion.setFromEuler(this._flyEuler);
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1483
|
+
private _onFlyWheel = (e: WheelEvent): void => {
|
|
1484
|
+
if (!this._flyActive) return;
|
|
1485
|
+
e.preventDefault();
|
|
1486
|
+
// Scroll adjusts fly speed (multiplicative, Godot-style)
|
|
1487
|
+
const factor = e.deltaY > 0 ? 0.85 : 1.18;
|
|
1488
|
+
this._flySpeed = Math.max(0.5, Math.min(100, this._flySpeed * factor));
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
private _onPointerDown = (e: PointerEvent): void => {
|
|
1492
|
+
this._pointerDownPos.set(e.clientX, e.clientY);
|
|
1493
|
+
this._pointerDownButton = e.button;
|
|
1494
|
+
this._marqueeActive = false;
|
|
1495
|
+
|
|
1496
|
+
// Check for handle click (left-button only)
|
|
1497
|
+
if (e.button === 0 && this._handles.length > 0 && !e.altKey) {
|
|
1498
|
+
const hit = this._raycastHandle(e);
|
|
1499
|
+
if (hit) {
|
|
1500
|
+
e.stopPropagation();
|
|
1501
|
+
this._handleDragActive = true;
|
|
1502
|
+
this._activeHandle = hit;
|
|
1503
|
+
this.orbitControls.enabled = false;
|
|
1504
|
+
this.transformControls.enabled = false;
|
|
1505
|
+
getActiveAuthoring(this._store).transforms?.beginEdit('');
|
|
1506
|
+
|
|
1507
|
+
// Compute constraint plane perpendicular to camera through handle position
|
|
1508
|
+
const camDir = this.camera.getWorldDirection(new THREE.Vector3());
|
|
1509
|
+
this._handleDragPlane.setFromNormalAndCoplanarPoint(camDir, hit.position);
|
|
1510
|
+
|
|
1511
|
+
// Compute offset so handle doesn't jump
|
|
1512
|
+
const rect = this._canvas.getBoundingClientRect();
|
|
1513
|
+
const mouse = new THREE.Vector2(
|
|
1514
|
+
((e.clientX - rect.left) / rect.width) * 2 - 1,
|
|
1515
|
+
-((e.clientY - rect.top) / rect.height) * 2 + 1,
|
|
1516
|
+
);
|
|
1517
|
+
this._handleRaycaster.setFromCamera(mouse, this.camera);
|
|
1518
|
+
const target = new THREE.Vector3();
|
|
1519
|
+
this._handleRaycaster.ray.intersectPlane(this._handleDragPlane, target);
|
|
1520
|
+
this._handleDragOffset.subVectors(hit.position, target);
|
|
1521
|
+
this._canvas.style.cursor = 'grabbing';
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
private _onPointerMove = (e: PointerEvent): void => {
|
|
1527
|
+
// Handle drag
|
|
1528
|
+
if (this._handleDragActive && this._activeHandle) {
|
|
1529
|
+
const rect = this._canvas.getBoundingClientRect();
|
|
1530
|
+
const mouse = new THREE.Vector2(
|
|
1531
|
+
((e.clientX - rect.left) / rect.width) * 2 - 1,
|
|
1532
|
+
-((e.clientY - rect.top) / rect.height) * 2 + 1,
|
|
1533
|
+
);
|
|
1534
|
+
this._handleRaycaster.setFromCamera(mouse, this.camera);
|
|
1535
|
+
const target = new THREE.Vector3();
|
|
1536
|
+
if (this._handleRaycaster.ray.intersectPlane(this._handleDragPlane, target)) {
|
|
1537
|
+
const newPos = target.add(this._handleDragOffset);
|
|
1538
|
+
const def = this._activeHandle.userData.handleDef;
|
|
1539
|
+
const entityObj = this._objectMap.get(def.entityId);
|
|
1540
|
+
const entity = getUserData(entityObj, 'entity') as SceneEntity | undefined;
|
|
1541
|
+
if (entityObj && entity) {
|
|
1542
|
+
def.onDrag(newPos, entityObj, entity);
|
|
1543
|
+
// Rebuild the entity helpers to reflect the new value
|
|
1544
|
+
this._store.rebuildEntityLive(def.entityId);
|
|
1545
|
+
// Re-sync handle positions (helpers were rebuilt)
|
|
1546
|
+
this._syncHandles();
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Handle hover detection (when not dragging anything)
|
|
1553
|
+
if (!(e.buttons & 1) && this._handles.length > 0) {
|
|
1554
|
+
const hit = this._raycastHandle(e);
|
|
1555
|
+
if (hit !== this._hoveredHandle) {
|
|
1556
|
+
if (this._hoveredHandle) setHandleHovered(this._hoveredHandle, false);
|
|
1557
|
+
this._hoveredHandle = hit;
|
|
1558
|
+
if (hit) {
|
|
1559
|
+
setHandleHovered(hit, true);
|
|
1560
|
+
this._canvas.style.cursor = 'grab';
|
|
1561
|
+
} else {
|
|
1562
|
+
this._canvas.style.cursor = '';
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// Only left button drag starts marquee (skip if Alt-orbiting)
|
|
1568
|
+
if (!(e.buttons & 1) || this._pointerDownButton !== 0) return;
|
|
1569
|
+
if (this.transformControls.dragging) return;
|
|
1570
|
+
if (this._altDragOrbit) return;
|
|
1571
|
+
|
|
1572
|
+
const dx = e.clientX - this._pointerDownPos.x;
|
|
1573
|
+
const dy = e.clientY - this._pointerDownPos.y;
|
|
1574
|
+
if (!this._marqueeActive && Math.sqrt(dx * dx + dy * dy) > 5) {
|
|
1575
|
+
// Check we're not dragging from the viewcube area
|
|
1576
|
+
const rect = this._canvas.getBoundingClientRect();
|
|
1577
|
+
const sx = this._pointerDownPos.x - rect.left;
|
|
1578
|
+
const sy = this._pointerDownPos.y - rect.top;
|
|
1579
|
+
const vcLeft = rect.width - this._vcSize - this._vcMarginRight;
|
|
1580
|
+
if (
|
|
1581
|
+
sx >= vcLeft &&
|
|
1582
|
+
sx <= vcLeft + this._vcSize &&
|
|
1583
|
+
sy >= this._vcMarginTop &&
|
|
1584
|
+
sy <= this._vcMarginTop + this._vcSize
|
|
1585
|
+
)
|
|
1586
|
+
return;
|
|
1587
|
+
|
|
1588
|
+
this._marqueeActive = true;
|
|
1589
|
+
// Disable orbit controls while marquee is active
|
|
1590
|
+
this.orbitControls.enabled = false;
|
|
1591
|
+
|
|
1592
|
+
if (!this._marqueeDiv) {
|
|
1593
|
+
this._marqueeDiv = document.createElement('div');
|
|
1594
|
+
Object.assign(this._marqueeDiv.style, {
|
|
1595
|
+
position: 'fixed',
|
|
1596
|
+
border: '1px solid #579EFF',
|
|
1597
|
+
background: 'rgba(87, 158, 255, 0.15)',
|
|
1598
|
+
pointerEvents: 'none',
|
|
1599
|
+
zIndex: '999',
|
|
1600
|
+
});
|
|
1601
|
+
document.body.appendChild(this._marqueeDiv);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if (this._marqueeActive && this._marqueeDiv) {
|
|
1606
|
+
const left = Math.min(this._pointerDownPos.x, e.clientX);
|
|
1607
|
+
const top = Math.min(this._pointerDownPos.y, e.clientY);
|
|
1608
|
+
const w = Math.abs(e.clientX - this._pointerDownPos.x);
|
|
1609
|
+
const h = Math.abs(e.clientY - this._pointerDownPos.y);
|
|
1610
|
+
Object.assign(this._marqueeDiv.style, {
|
|
1611
|
+
left: `${left}px`,
|
|
1612
|
+
top: `${top}px`,
|
|
1613
|
+
width: `${w}px`,
|
|
1614
|
+
height: `${h}px`,
|
|
1615
|
+
display: 'block',
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
private _onPointerUp = (e: PointerEvent): void => {
|
|
1621
|
+
// Handle drag end
|
|
1622
|
+
if (this._handleDragActive) {
|
|
1623
|
+
this._handleDragActive = false;
|
|
1624
|
+
this._activeHandle = null;
|
|
1625
|
+
this.orbitControls.enabled = true;
|
|
1626
|
+
this.transformControls.enabled = true;
|
|
1627
|
+
this._canvas.style.cursor = '';
|
|
1628
|
+
// Persist the final handle-driven edit + mark dirty (ED2).
|
|
1629
|
+
getActiveAuthoring(this._store).transforms?.endEdit('');
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// Handle marquee select. D12 (B4) scope note: marquee stays THREEJS-ONLY —
|
|
1634
|
+
// it only ever scans `this._objectMap` (the focused threejs world's live
|
|
1635
|
+
// objects), never the composite's other (react/pixi) layered children. A
|
|
1636
|
+
// layered/rect-based marquee across every visible layer is out of scope
|
|
1637
|
+
// for this task (D12) — single-click picking is layered (below), marquee
|
|
1638
|
+
// is not.
|
|
1639
|
+
if (this._marqueeActive) {
|
|
1640
|
+
this._marqueeActive = false;
|
|
1641
|
+
this.orbitControls.enabled = true;
|
|
1642
|
+
if (this._marqueeDiv) {
|
|
1643
|
+
this._marqueeDiv.style.display = 'none';
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Find all entities whose screen projection falls within the marquee
|
|
1647
|
+
const left = Math.min(this._pointerDownPos.x, e.clientX);
|
|
1648
|
+
const right = Math.max(this._pointerDownPos.x, e.clientX);
|
|
1649
|
+
const top = Math.min(this._pointerDownPos.y, e.clientY);
|
|
1650
|
+
const bottom = Math.max(this._pointerDownPos.y, e.clientY);
|
|
1651
|
+
|
|
1652
|
+
const hitIds: string[] = [];
|
|
1653
|
+
for (const [id, obj] of this._objectMap) {
|
|
1654
|
+
// Skip locked entities in marquee selection
|
|
1655
|
+
if (this._store.isEntityLocked(id)) continue;
|
|
1656
|
+
const screen = this._projectToScreen(obj);
|
|
1657
|
+
if (screen.x >= left && screen.x <= right && screen.y >= top && screen.y <= bottom) {
|
|
1658
|
+
hitIds.push(id);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (e.shiftKey) {
|
|
1663
|
+
// Add to existing selection (additive modifier — SelectionProvider
|
|
1664
|
+
// extension is Phase-F; first-party store op for now).
|
|
1665
|
+
for (const id of hitIds) this._store.addToSelection(id);
|
|
1666
|
+
} else {
|
|
1667
|
+
getActiveAuthoring(this._store).selection?.set(hitIds);
|
|
1668
|
+
}
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// Only left-click triggers selection (right-drag = orbit, not select)
|
|
1673
|
+
if (e.button !== 0) return;
|
|
1674
|
+
|
|
1675
|
+
// Only treat as click if pointer didn't move much (not a drag)
|
|
1676
|
+
const dx = e.clientX - this._pointerDownPos.x;
|
|
1677
|
+
const dy = e.clientY - this._pointerDownPos.y;
|
|
1678
|
+
if (Math.sqrt(dx * dx + dy * dy) > 5) return;
|
|
1679
|
+
|
|
1680
|
+
// Don't interfere with gizmo clicks
|
|
1681
|
+
if (this.transformControls.dragging) return;
|
|
1682
|
+
|
|
1683
|
+
// Check orientation gizmo click first
|
|
1684
|
+
if (this._handleViewCubeClick(e)) return;
|
|
1685
|
+
|
|
1686
|
+
// D12 (B4) — topmost-first layered pick: the composite's non-threejs
|
|
1687
|
+
// children (react/pixi design-time layers) are tried topmost-first before
|
|
1688
|
+
// falling through to the threejs raycast (now `VgaiSceneAuthoringAdapter
|
|
1689
|
+
// .pickable`) — see `pickTopmost`'s doc comment. Note: an INTERACTIVE
|
|
1690
|
+
// layer (session `interactive` toggle, B1) is mutually exclusive with this
|
|
1691
|
+
// walk running at all, by construction — its `pointer-events:auto` DOM
|
|
1692
|
+
// intercepts the event before it ever reaches this canvas listener, so
|
|
1693
|
+
// there is no special-case here for it.
|
|
1694
|
+
const entityId = pickTopmost(this._store, e.clientX, e.clientY);
|
|
1695
|
+
|
|
1696
|
+
if (e.shiftKey && entityId) {
|
|
1697
|
+
this._store.addToSelection(entityId);
|
|
1698
|
+
} else if ((e.metaKey || e.ctrlKey) && entityId) {
|
|
1699
|
+
this._store.toggleSelection(entityId);
|
|
1700
|
+
} else {
|
|
1701
|
+
getActiveAuthoring(this._store).selection?.set(entityId ? [entityId] : []);
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
// --- Asset drag-and-drop onto viewport ---
|
|
1706
|
+
|
|
1707
|
+
/** Raycast from screen coordinates to the ground plane (y=0). Returns world position or null. */
|
|
1708
|
+
private _raycastGroundPlane(e: DragEvent): THREE.Vector3 | null {
|
|
1709
|
+
const rect = this._canvas.getBoundingClientRect();
|
|
1710
|
+
const mouse = new THREE.Vector2(
|
|
1711
|
+
((e.clientX - rect.left) / rect.width) * 2 - 1,
|
|
1712
|
+
-((e.clientY - rect.top) / rect.height) * 2 + 1,
|
|
1713
|
+
);
|
|
1714
|
+
this.raycaster.setFromCamera(mouse, this.camera);
|
|
1715
|
+
const target = new THREE.Vector3();
|
|
1716
|
+
const hit = this.raycaster.ray.intersectPlane(this._groundPlane, target);
|
|
1717
|
+
return hit;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
private _showDropOverlay(): void {
|
|
1721
|
+
if (this._dropOverlay) return;
|
|
1722
|
+
const overlay = document.createElement('div');
|
|
1723
|
+
Object.assign(overlay.style, {
|
|
1724
|
+
position: 'fixed',
|
|
1725
|
+
inset: '0',
|
|
1726
|
+
pointerEvents: 'none',
|
|
1727
|
+
border: '2px dashed #579EFF',
|
|
1728
|
+
background: 'rgba(87, 158, 255, 0.08)',
|
|
1729
|
+
zIndex: '5',
|
|
1730
|
+
});
|
|
1731
|
+
document.body.appendChild(overlay);
|
|
1732
|
+
this._dropOverlay = overlay;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
private _hideDropOverlay(): void {
|
|
1736
|
+
if (this._dropOverlay) {
|
|
1737
|
+
this._dropOverlay.remove();
|
|
1738
|
+
this._dropOverlay = null;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
private _onDragOver = (e: DragEvent): void => {
|
|
1743
|
+
if (!e.dataTransfer?.types.includes('application/x-editor-asset')) return;
|
|
1744
|
+
e.preventDefault();
|
|
1745
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
1746
|
+
this._showDropOverlay();
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
private _onDragLeave = (_e: DragEvent): void => {
|
|
1750
|
+
this._hideDropOverlay();
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
private _onDrop = (e: DragEvent): void => {
|
|
1754
|
+
this._hideDropOverlay();
|
|
1755
|
+
const raw = e.dataTransfer?.getData('application/x-editor-asset');
|
|
1756
|
+
if (!raw) return;
|
|
1757
|
+
e.preventDefault();
|
|
1758
|
+
|
|
1759
|
+
let data: {
|
|
1760
|
+
path: string;
|
|
1761
|
+
kind: string;
|
|
1762
|
+
name: string;
|
|
1763
|
+
online?: {
|
|
1764
|
+
source: string;
|
|
1765
|
+
id: string;
|
|
1766
|
+
name: string;
|
|
1767
|
+
url?: string;
|
|
1768
|
+
format?: string;
|
|
1769
|
+
includes?: { relativePath: string; url: string; size: number }[];
|
|
1770
|
+
};
|
|
1771
|
+
};
|
|
1772
|
+
try {
|
|
1773
|
+
data = JSON.parse(raw);
|
|
1774
|
+
} catch {
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// Find drop position via ground-plane raycast, fallback to origin
|
|
1779
|
+
const worldPos = this._raycastGroundPlane(e);
|
|
1780
|
+
const pos: [number, number, number] = worldPos
|
|
1781
|
+
? [Math.round(worldPos.x * 100) / 100, 0, Math.round(worldPos.z * 100) / 100]
|
|
1782
|
+
: [0, 0, 0];
|
|
1783
|
+
|
|
1784
|
+
// Online asset: check cache first, otherwise show placeholder and download
|
|
1785
|
+
if (data.online) {
|
|
1786
|
+
const online = data.online;
|
|
1787
|
+
const baseName = online.name.replace(/\.[^.]+$/, '');
|
|
1788
|
+
|
|
1789
|
+
// Already downloaded? Create entity instantly, no placeholder needed.
|
|
1790
|
+
const cachedPath = getDownloadedAssetPath(online.source, online.name);
|
|
1791
|
+
if (cachedPath) {
|
|
1792
|
+
this._store.addEntity(null, {
|
|
1793
|
+
id: crypto.randomUUID(),
|
|
1794
|
+
name: baseName,
|
|
1795
|
+
mesh: { type: 'gltf', src: cachedPath },
|
|
1796
|
+
transform: { position: pos },
|
|
1797
|
+
});
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
const placeholderId = crypto.randomUUID();
|
|
1802
|
+
|
|
1803
|
+
// Create a visible placeholder box at the drop position
|
|
1804
|
+
this._store.addEntity(null, {
|
|
1805
|
+
id: placeholderId,
|
|
1806
|
+
name: `${baseName} (downloading...)`,
|
|
1807
|
+
mesh: { type: 'box' },
|
|
1808
|
+
transform: { position: pos, scale: [0.5, 0.5, 0.5] },
|
|
1809
|
+
material: { type: 'standard', color: '#579EFF', opacity: 0.4, transparent: true },
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
const resolveEntity = (localPath: string) => {
|
|
1813
|
+
// Replace placeholder with the real GLTF entity
|
|
1814
|
+
this._store.removeEntity(placeholderId);
|
|
1815
|
+
this._store.addEntity(null, {
|
|
1816
|
+
id: crypto.randomUUID(),
|
|
1817
|
+
name: baseName,
|
|
1818
|
+
mesh: { type: 'gltf', src: `/${localPath}` },
|
|
1819
|
+
transform: { position: pos },
|
|
1820
|
+
});
|
|
1821
|
+
};
|
|
1822
|
+
|
|
1823
|
+
const doDownload = (dlParams: Parameters<typeof downloadOnlineAsset>[0]) => {
|
|
1824
|
+
downloadOnlineAsset(dlParams)
|
|
1825
|
+
.then((result) => {
|
|
1826
|
+
if (result.ok && result.path) {
|
|
1827
|
+
resolveEntity(result.path);
|
|
1828
|
+
} else {
|
|
1829
|
+
// Download failed — update name to indicate error
|
|
1830
|
+
this._store.updateEntity(placeholderId, {
|
|
1831
|
+
name: `${baseName} (download failed)`,
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
})
|
|
1835
|
+
.catch(() => {
|
|
1836
|
+
// Browser mode (no Node dev server) throws instead of resolving
|
|
1837
|
+
// { ok: false } — same "download failed" placeholder update as
|
|
1838
|
+
// the resolved-failure branch above, no unhandled rejection.
|
|
1839
|
+
this._store.updateEntity(placeholderId, {
|
|
1840
|
+
name: `${baseName} (download failed)`,
|
|
1841
|
+
});
|
|
1842
|
+
});
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
if (online.url) {
|
|
1846
|
+
const dlParams: Parameters<typeof downloadOnlineAsset>[0] = {
|
|
1847
|
+
source: online.source,
|
|
1848
|
+
id: online.id,
|
|
1849
|
+
name: online.name,
|
|
1850
|
+
url: online.url,
|
|
1851
|
+
format: online.format ?? 'gltf',
|
|
1852
|
+
};
|
|
1853
|
+
if (online.includes) dlParams.includes = online.includes;
|
|
1854
|
+
doDownload(dlParams);
|
|
1855
|
+
} else {
|
|
1856
|
+
// Fetch file options, pick first, then download
|
|
1857
|
+
getOnlineAssetFiles(online.source, online.id).then((files) => {
|
|
1858
|
+
const file = files[0];
|
|
1859
|
+
if (!file) {
|
|
1860
|
+
this._store.updateEntity(placeholderId, {
|
|
1861
|
+
name: `${baseName} (no files available)`,
|
|
1862
|
+
});
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const params: Parameters<typeof downloadOnlineAsset>[0] = {
|
|
1866
|
+
source: online.source,
|
|
1867
|
+
id: online.id,
|
|
1868
|
+
name: online.name,
|
|
1869
|
+
url: file.url,
|
|
1870
|
+
format: file.format,
|
|
1871
|
+
};
|
|
1872
|
+
if (file.includes) params.includes = file.includes;
|
|
1873
|
+
doDownload(params);
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
const baseName = data.name.replace(/\.[^.]+$/, '');
|
|
1880
|
+
|
|
1881
|
+
if (data.kind === 'model') {
|
|
1882
|
+
this._store.addEntity(null, {
|
|
1883
|
+
id: crypto.randomUUID(),
|
|
1884
|
+
name: baseName,
|
|
1885
|
+
mesh: { type: 'gltf', src: data.path },
|
|
1886
|
+
transform: { position: pos },
|
|
1887
|
+
});
|
|
1888
|
+
} else if (data.kind === 'image') {
|
|
1889
|
+
this._store.addEntity(null, {
|
|
1890
|
+
id: crypto.randomUUID(),
|
|
1891
|
+
name: baseName,
|
|
1892
|
+
mesh: { type: 'plane', args: [2, 2] },
|
|
1893
|
+
material: { type: 'standard', map: data.path },
|
|
1894
|
+
transform: {
|
|
1895
|
+
position: [pos[0], pos[1] + 1, pos[2]],
|
|
1896
|
+
rotation: [0, 0, 0, 1],
|
|
1897
|
+
},
|
|
1898
|
+
});
|
|
1899
|
+
} else if (data.kind === 'scene') {
|
|
1900
|
+
fetch(resolveUrl(data.path))
|
|
1901
|
+
.then((r) => r.json())
|
|
1902
|
+
.then((doc: { entities?: SceneEntity[] }) => {
|
|
1903
|
+
if (doc.entities) {
|
|
1904
|
+
for (const entity of doc.entities) {
|
|
1905
|
+
const e: SceneEntity = { ...entity, id: crypto.randomUUID() };
|
|
1906
|
+
// Offset root entities to drop position
|
|
1907
|
+
if (!e.transform) e.transform = {};
|
|
1908
|
+
const ep = e.transform.position ?? [0, 0, 0];
|
|
1909
|
+
e.transform.position = [ep[0] + pos[0], ep[1] + pos[1], ep[2] + pos[2]];
|
|
1910
|
+
this._store.addEntity(null, e);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
} else if (data.kind === 'audio') {
|
|
1915
|
+
const baseName = data.name.replace(/\.[^.]+$/, '');
|
|
1916
|
+
this._store.addEntity(null, {
|
|
1917
|
+
id: crypto.randomUUID(),
|
|
1918
|
+
name: baseName,
|
|
1919
|
+
audio: { src: data.path, spatial: true, volume: 1 },
|
|
1920
|
+
transform: { position: pos },
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
}
|