@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,509 @@
|
|
|
1
|
+
import { resolveUrl } from '@engine/loader';
|
|
2
|
+
import { parsePrefabFile, parseSceneFile } from '@engine/scene/parse';
|
|
3
|
+
import type { PrefabFile, SceneFile } from '@engine/scene/scene-types';
|
|
4
|
+
import { use, useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
6
|
+
import { connectAssetEvents, connectSceneEvents } from '../asset-events';
|
|
7
|
+
import { getActiveAuthoring, hasAuthoringOverride } from '../authoring/active-adapter';
|
|
8
|
+
import {
|
|
9
|
+
findOwningRootGroupId,
|
|
10
|
+
ROOT_ACTIVATION_REQUEST_EVENT,
|
|
11
|
+
type RootActivationRequest,
|
|
12
|
+
requestRootActivation,
|
|
13
|
+
} from '../authoring/root-activation';
|
|
14
|
+
import { watchExternalSceneChanges } from '../browser-external-watch';
|
|
15
|
+
import { useEditorInit, useEditorStore } from '../EditorContext';
|
|
16
|
+
import { readProjectTextFile, saveFile } from '../editor-api';
|
|
17
|
+
import { editorConsole } from '../editor-console';
|
|
18
|
+
import { getEditorMode } from '../editor-mode';
|
|
19
|
+
import { resizeGame, setGameContainer } from '../play-mode';
|
|
20
|
+
import { SceneUIPalettePanel } from '../scene-ui-mode/scene-ui-palette-panel';
|
|
21
|
+
import { UIPrefabDialogHost } from '../scene-ui-mode/ui-prefab-dialog-host';
|
|
22
|
+
import { getStorageBackend } from '../storage';
|
|
23
|
+
import { BottomPanel } from './BottomPanel';
|
|
24
|
+
import { GameHierarchy } from './GameHierarchy';
|
|
25
|
+
import { MountFailureBanner } from './MountFailureBanner';
|
|
26
|
+
import { NetworkSection } from './NetworkSection';
|
|
27
|
+
import { PlayBar, type Resolution } from './PlayBar';
|
|
28
|
+
import { PrefabSaveDialog } from './PrefabSaveDialog';
|
|
29
|
+
import { ProjectHeader } from './ProjectHeader';
|
|
30
|
+
import { RightPanel } from './RightPanel';
|
|
31
|
+
import { RootActivateDialog } from './RootActivateDialog';
|
|
32
|
+
import { ViewportPanel } from './ViewportPanel';
|
|
33
|
+
|
|
34
|
+
/** Load a scene from a URL into the store. */
|
|
35
|
+
export async function openSceneFromAsset(
|
|
36
|
+
store: { loadDocument: (doc: SceneFile, path: string) => void },
|
|
37
|
+
assetPath: string,
|
|
38
|
+
): Promise<boolean> {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(resolveUrl(assetPath));
|
|
41
|
+
if (!res.ok) return false;
|
|
42
|
+
const doc = parseSceneFile(await res.json());
|
|
43
|
+
const savePath = assetPath.replace(/^\//, '');
|
|
44
|
+
store.loadDocument(doc, savePath);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Save a MaterialFile to disk via the editor server API. */
|
|
52
|
+
export async function saveMaterialToServer(
|
|
53
|
+
material: import('@engine/scene/scene-types').MaterialFile,
|
|
54
|
+
path: string,
|
|
55
|
+
): Promise<boolean> {
|
|
56
|
+
const content = JSON.stringify(material, null, 2);
|
|
57
|
+
return saveFile(path, content);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Save a PrefabFile to disk via the editor server API. */
|
|
61
|
+
export async function savePrefabToServer(prefab: PrefabFile, path: string): Promise<boolean> {
|
|
62
|
+
const content = JSON.stringify(prefab, null, 2);
|
|
63
|
+
return saveFile(path, content);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Load a prefab from a URL into the store for editing. */
|
|
67
|
+
export async function openPrefabFromAsset(
|
|
68
|
+
store: { loadPrefab: (prefab: PrefabFile, path: string) => void },
|
|
69
|
+
assetPath: string,
|
|
70
|
+
): Promise<boolean> {
|
|
71
|
+
try {
|
|
72
|
+
const text = await readProjectTextFile(assetPath);
|
|
73
|
+
if (text === null) return false;
|
|
74
|
+
const prefab = parsePrefabFile(JSON.parse(text));
|
|
75
|
+
const savePath = assetPath.replace(/^\//, '');
|
|
76
|
+
store.loadPrefab(prefab, savePath);
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Fetch a PrefabFile (for unpack operations) — storage-first so a just-saved prefab resolves. */
|
|
84
|
+
export async function fetchPrefabFile(url: string): Promise<PrefabFile | null> {
|
|
85
|
+
try {
|
|
86
|
+
const text = await readProjectTextFile(url);
|
|
87
|
+
if (text === null) return null;
|
|
88
|
+
return parsePrefabFile(JSON.parse(text));
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Game panel — a container div that play-mode.ts fills with a canvas. */
|
|
95
|
+
function GamePanel({ style }: { style?: React.CSSProperties }) {
|
|
96
|
+
const store = useEditorStore();
|
|
97
|
+
const outerRef = useRef<HTMLDivElement>(null);
|
|
98
|
+
const [resolution, setResolution] = useState<Resolution>({
|
|
99
|
+
label: 'Fill',
|
|
100
|
+
width: null,
|
|
101
|
+
height: null,
|
|
102
|
+
});
|
|
103
|
+
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
104
|
+
|
|
105
|
+
// Callback ref so setGameContainer is called even when the element swaps (fill ↔ device)
|
|
106
|
+
const prevRef = useRef<HTMLDivElement | null>(null);
|
|
107
|
+
const canvasRef = useCallback((el: HTMLDivElement | null) => {
|
|
108
|
+
if (el === prevRef.current) return;
|
|
109
|
+
// Move game canvas to new container
|
|
110
|
+
if (prevRef.current && el) {
|
|
111
|
+
while (prevRef.current.firstChild) el.appendChild(prevRef.current.firstChild);
|
|
112
|
+
}
|
|
113
|
+
prevRef.current = el;
|
|
114
|
+
setGameContainer(el);
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
// Track outer container size for scale calculation
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const el = outerRef.current;
|
|
120
|
+
if (!el) return;
|
|
121
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
122
|
+
if (!entry) return;
|
|
123
|
+
setContainerSize({ width: entry.contentRect.width, height: entry.contentRect.height });
|
|
124
|
+
});
|
|
125
|
+
observer.observe(el);
|
|
126
|
+
return () => observer.disconnect();
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
// Resize the game renderer when resolution or container size changes
|
|
130
|
+
const isFill = resolution.width === null;
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (isFill) {
|
|
133
|
+
// Fill mode: resize to match available container space
|
|
134
|
+
if (containerSize.width > 0 && containerSize.height > 0) {
|
|
135
|
+
resizeGame(containerSize.width, containerSize.height);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
resizeGame(resolution.width!, resolution.height!);
|
|
139
|
+
}
|
|
140
|
+
}, [isFill, resolution.width, resolution.height, containerSize.width, containerSize.height]);
|
|
141
|
+
|
|
142
|
+
// Scale: fit the device resolution into the available space, never upscale
|
|
143
|
+
let scale = 1;
|
|
144
|
+
if (!isFill && containerSize.width > 0 && containerSize.height > 0) {
|
|
145
|
+
const scaleX = containerSize.width / resolution.width!;
|
|
146
|
+
const scaleY = containerSize.height / resolution.height!;
|
|
147
|
+
scale = Math.min(scaleX, scaleY, 1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div style={{ overflow: 'hidden', position: 'relative', flexDirection: 'column', ...style }}>
|
|
152
|
+
<div
|
|
153
|
+
ref={outerRef}
|
|
154
|
+
style={{
|
|
155
|
+
flex: 1,
|
|
156
|
+
overflow: 'hidden',
|
|
157
|
+
display: 'flex',
|
|
158
|
+
alignItems: 'center',
|
|
159
|
+
justifyContent: 'center',
|
|
160
|
+
position: 'relative',
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
{isFill ? (
|
|
164
|
+
<div ref={canvasRef} aria-hidden="true" style={{ position: 'absolute', inset: 0 }} />
|
|
165
|
+
) : (
|
|
166
|
+
/* Wrapper sized to the scaled dimensions so flexbox centering works.
|
|
167
|
+
The inner canvasRef is full device resolution, CSS-scaled down. */
|
|
168
|
+
<div
|
|
169
|
+
style={{
|
|
170
|
+
width: resolution.width! * scale,
|
|
171
|
+
height: resolution.height! * scale,
|
|
172
|
+
overflow: 'hidden',
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<div
|
|
176
|
+
ref={canvasRef}
|
|
177
|
+
aria-hidden="true"
|
|
178
|
+
style={{
|
|
179
|
+
width: resolution.width!,
|
|
180
|
+
height: resolution.height!,
|
|
181
|
+
transformOrigin: 'top left',
|
|
182
|
+
transform: `scale(${scale})`,
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{/* Same overlay ViewportPanel renders on the Scene tab (T-R1a #6) — the
|
|
188
|
+
Game tab is where play auto-switches to, so it must render here too,
|
|
189
|
+
not only on the Scene-tab viewport it would otherwise be hidden behind. */}
|
|
190
|
+
{store.playState !== 'stopped' && <NetworkSection />}
|
|
191
|
+
</div>
|
|
192
|
+
<PlayBar resolution={resolution} onResolutionChange={setResolution} scale={scale} />
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function ViewportTabBar({
|
|
198
|
+
active,
|
|
199
|
+
onChange,
|
|
200
|
+
showGame,
|
|
201
|
+
}: {
|
|
202
|
+
active: 'scene' | 'game';
|
|
203
|
+
onChange: (tab: 'scene' | 'game') => void;
|
|
204
|
+
showGame: boolean;
|
|
205
|
+
}) {
|
|
206
|
+
const tabStyle = (isActive: boolean): React.CSSProperties => ({
|
|
207
|
+
background: 'none',
|
|
208
|
+
border: 'none',
|
|
209
|
+
padding: '4px 8px',
|
|
210
|
+
cursor: 'pointer',
|
|
211
|
+
fontSize: 11,
|
|
212
|
+
fontWeight: 600,
|
|
213
|
+
textTransform: 'uppercase',
|
|
214
|
+
color: isActive ? '#c5c8ce' : '#666',
|
|
215
|
+
borderBottom: isActive ? '2px solid #579eff' : '2px solid transparent',
|
|
216
|
+
marginBottom: -1,
|
|
217
|
+
pointerEvents: 'auto',
|
|
218
|
+
});
|
|
219
|
+
return (
|
|
220
|
+
<div
|
|
221
|
+
style={{
|
|
222
|
+
display: 'flex',
|
|
223
|
+
gap: 2,
|
|
224
|
+
background: '#2c2c2c',
|
|
225
|
+
borderBottom: '1px solid #333',
|
|
226
|
+
flexShrink: 0,
|
|
227
|
+
pointerEvents: 'auto',
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<button type="button" style={tabStyle(active === 'scene')} onClick={() => onChange('scene')}>
|
|
231
|
+
Viewport
|
|
232
|
+
</button>
|
|
233
|
+
{showGame && (
|
|
234
|
+
<button type="button" style={tabStyle(active === 'game')} onClick={() => onChange('game')}>
|
|
235
|
+
Game
|
|
236
|
+
</button>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function DefaultEditorLayout({
|
|
243
|
+
onReturnToProjectScreen,
|
|
244
|
+
}: {
|
|
245
|
+
onReturnToProjectScreen?: (() => void) | undefined;
|
|
246
|
+
}) {
|
|
247
|
+
const store = useEditorStore();
|
|
248
|
+
use(useEditorInit()); // suspends until project is detected
|
|
249
|
+
|
|
250
|
+
// Subscribe to store changes
|
|
251
|
+
useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
252
|
+
const isPlaying = store.playState !== 'stopped';
|
|
253
|
+
const [prefabSaveState, setPrefabSaveState] = useState<{ entityId: string; name: string } | null>(
|
|
254
|
+
null,
|
|
255
|
+
);
|
|
256
|
+
// A4 (D8) — a root-activation request whose adapter was dirty, awaiting the
|
|
257
|
+
// user's Cancel/Save-&-Switch choice. `requestRootActivation` (shared by
|
|
258
|
+
// GameHierarchy's radio dblclick and `handleOpenScene` below) dispatches
|
|
259
|
+
// this via a window event instead of rendering anything itself (adapters —
|
|
260
|
+
// and the hierarchy panel driven purely by them — can't own a portal).
|
|
261
|
+
const [pendingRootActivation, setPendingRootActivation] = useState<RootActivationRequest | null>(
|
|
262
|
+
null,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
const handler = (e: Event) => {
|
|
267
|
+
setPendingRootActivation((e as CustomEvent<RootActivationRequest>).detail);
|
|
268
|
+
};
|
|
269
|
+
window.addEventListener(ROOT_ACTIVATION_REQUEST_EVENT, handler);
|
|
270
|
+
return () => window.removeEventListener(ROOT_ACTIVATION_REQUEST_EVENT, handler);
|
|
271
|
+
}, []);
|
|
272
|
+
|
|
273
|
+
// Listen for "save as prefab" events from the hierarchy context menu
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
const handler = (e: Event) => {
|
|
276
|
+
const detail = (e as CustomEvent).detail as { entityId: string; name: string };
|
|
277
|
+
setPrefabSaveState(detail);
|
|
278
|
+
};
|
|
279
|
+
window.addEventListener('editor:save-as-prefab', handler);
|
|
280
|
+
return () => window.removeEventListener('editor:save-as-prefab', handler);
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
const handlePrefabSaveConfirm = useCallback(
|
|
284
|
+
(path: string) => {
|
|
285
|
+
if (!prefabSaveState) return;
|
|
286
|
+
const prefab = store.extractAsPrefab(prefabSaveState.entityId);
|
|
287
|
+
if (!prefab) {
|
|
288
|
+
setPrefabSaveState(null);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
savePrefabToServer(prefab, path).then((ok) => {
|
|
292
|
+
if (ok) {
|
|
293
|
+
const servingPath = `/${path}`;
|
|
294
|
+
store.convertToPrefabInstance(prefabSaveState.entityId, servingPath);
|
|
295
|
+
}
|
|
296
|
+
setPrefabSaveState(null);
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
[store, prefabSaveState],
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// A4 (D8) — the SINGLE choke point for the asset-browser's scene/prefab
|
|
303
|
+
// double-click (and the `editor:open-asset` event below). A scene file
|
|
304
|
+
// that is one of the active adapter's OWN root-group members reroutes
|
|
305
|
+
// through the shared dirty-gated `requestRootActivation` (radio activation)
|
|
306
|
+
// instead of the legacy `openSceneFromAsset` — it must NOT go through both
|
|
307
|
+
// paths, or the two could disagree about which scene is "loaded". Prefabs,
|
|
308
|
+
// and any scene the active adapter doesn't map to a root of its own
|
|
309
|
+
// (`files.rootForFile` returns null — e.g. no adapter installed, or a
|
|
310
|
+
// scene living outside this project's own root-group tracking), keep the
|
|
311
|
+
// legacy path unchanged.
|
|
312
|
+
const handleOpenScene = useCallback(
|
|
313
|
+
(assetPath: string) => {
|
|
314
|
+
if (assetPath.endsWith('.prefab.json')) {
|
|
315
|
+
openPrefabFromAsset(store, assetPath);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const adapter = getActiveAuthoring(store);
|
|
319
|
+
const rootId = adapter.files?.rootForFile(assetPath) ?? null;
|
|
320
|
+
const groupId = rootId ? findOwningRootGroupId(adapter, rootId) : null;
|
|
321
|
+
if (rootId && groupId) {
|
|
322
|
+
requestRootActivation(adapter, groupId, rootId);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
openSceneFromAsset(store, assetPath);
|
|
326
|
+
},
|
|
327
|
+
[store],
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Listen for "open asset" events (e.g. "Open Prefab" from context menu)
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
const handler = (e: Event) => {
|
|
333
|
+
const detail = (e as CustomEvent).detail as { path: string };
|
|
334
|
+
if (detail.path) handleOpenScene(detail.path);
|
|
335
|
+
};
|
|
336
|
+
window.addEventListener('editor:open-asset', handler);
|
|
337
|
+
return () => window.removeEventListener('editor:open-asset', handler);
|
|
338
|
+
}, [handleOpenScene]);
|
|
339
|
+
|
|
340
|
+
// Subscribe to server-sent asset move events (file watcher)
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
return connectAssetEvents((event) => {
|
|
343
|
+
store.applyAssetMove(event.oldPath, event.newPath);
|
|
344
|
+
window.dispatchEvent(new CustomEvent('editor:assets-changed'));
|
|
345
|
+
});
|
|
346
|
+
}, [store]);
|
|
347
|
+
|
|
348
|
+
// Subscribe to server-sent scene file changes (live sync from disk). This channel
|
|
349
|
+
// is scoped to the currently-OPEN first-party `.vscn` (`eventPath === currentPath`
|
|
350
|
+
// below, `currentPath` = `store.savePath`) — the SSE transport itself is unchanged
|
|
351
|
+
// (design docs/PERSISTENCE-PROVIDER-DESIGN.md §5). When no override is active, the
|
|
352
|
+
// active adapter IS the first-party one, so applying the update is routed through
|
|
353
|
+
// its `persistence.applyExternal` (the existing `applyExternalUpdate` diff-apply,
|
|
354
|
+
// `event.content` forwarded as `rawContent` so own-echo detection is unaffected)
|
|
355
|
+
// instead of reaching into the store directly. When a non-first-party override
|
|
356
|
+
// (ingest overlay / UI / world2d / ephemeral) IS active, it has its OWN persisted
|
|
357
|
+
// artifact + reload contract, unrelated to this event — routing THIS doc through
|
|
358
|
+
// ITS `applyExternal` would feed a foreign `SceneFile` into e.g. an ingest
|
|
359
|
+
// overlay's apply, so fall back to `store.applyExternalUpdate` directly there,
|
|
360
|
+
// preserving its existing play-state-guarded queueing behavior unchanged.
|
|
361
|
+
const onSceneChanged = useCallback(
|
|
362
|
+
(event: { path: string; content: string }) => {
|
|
363
|
+
const currentPath = store.savePath;
|
|
364
|
+
if (!currentPath) return;
|
|
365
|
+
const eventPath = event.path.startsWith('/') ? event.path.slice(1) : event.path;
|
|
366
|
+
if (eventPath !== currentPath) return;
|
|
367
|
+
try {
|
|
368
|
+
const doc = parseSceneFile(JSON.parse(event.content));
|
|
369
|
+
if (hasAuthoringOverride()) {
|
|
370
|
+
store.applyExternalUpdate(doc, event.content);
|
|
371
|
+
} else {
|
|
372
|
+
getActiveAuthoring(store).persistence?.applyExternal?.(doc, event.content);
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
if (err instanceof Error) {
|
|
376
|
+
editorConsole.error(`Scene validation failed: ${err.message}`, 'scene');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
[store],
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
useEffect(() => connectSceneEvents(onSceneChanged), [onSceneChanged]);
|
|
384
|
+
|
|
385
|
+
// A4 — external-change detection for the open scene. A2's watch only sees the
|
|
386
|
+
// editor's OWN writes; a real on-disk (FSA) folder can be edited out-of-band
|
|
387
|
+
// (agent apply-diff, another editor, git). Poll it and deliver through the
|
|
388
|
+
// SAME handler — the store's own-echo guard drops self-writes, so only genuine
|
|
389
|
+
// external edits reload. OPFS is a private sandbox with no external writer, so
|
|
390
|
+
// this is scoped to the FSA backend; server mode keeps SSE.
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (getEditorMode() !== 'browser') return;
|
|
393
|
+
if (getStorageBackend().id !== 'fsa') return;
|
|
394
|
+
return watchExternalSceneChanges(() => store.savePath, onSceneChanged);
|
|
395
|
+
}, [store, onSceneChanged]);
|
|
396
|
+
|
|
397
|
+
const bottomEl = document.getElementById('editor-bottom');
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<>
|
|
401
|
+
<div
|
|
402
|
+
style={{
|
|
403
|
+
position: 'absolute',
|
|
404
|
+
top: 0,
|
|
405
|
+
left: 0,
|
|
406
|
+
width: '100%',
|
|
407
|
+
height: '100%',
|
|
408
|
+
display: 'flex',
|
|
409
|
+
flexDirection: 'column',
|
|
410
|
+
pointerEvents: 'none',
|
|
411
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
412
|
+
fontSize: '13px',
|
|
413
|
+
color: '#c5c8ce',
|
|
414
|
+
}}
|
|
415
|
+
>
|
|
416
|
+
<ProjectHeader onReturnToProjectScreen={onReturnToProjectScreen} />
|
|
417
|
+
{/* D-W3 (F9): rendered here, OUTSIDE `GameHierarchy` — a manifest world
|
|
418
|
+
that failed to mount entirely never installs an override, so a
|
|
419
|
+
banner living only INSIDE the hierarchy panel wouldn't help: this
|
|
420
|
+
one must be visible over the default Untitled scene the editor
|
|
421
|
+
falls back to in exactly that case. (`GameHierarchy` separately
|
|
422
|
+
renders `OverlayReportBanner` internally, unconditionally, for the
|
|
423
|
+
ingest/composite overlay-report case — see that component.) */}
|
|
424
|
+
<MountFailureBanner />
|
|
425
|
+
<div
|
|
426
|
+
style={{
|
|
427
|
+
display: 'flex',
|
|
428
|
+
flex: 1,
|
|
429
|
+
minHeight: 0,
|
|
430
|
+
}}
|
|
431
|
+
>
|
|
432
|
+
{/* A2 — ONE contract-only hierarchy panel drives every session shape
|
|
433
|
+
(bare single-scene edit, ingested game, multi-world composite):
|
|
434
|
+
`hasAuthoringOverride()` no longer selects between two panels. */}
|
|
435
|
+
<GameHierarchy />
|
|
436
|
+
{/* E1.a (spec-27 §7 E1) — the scene-UI palette (+ prefab
|
|
437
|
+
instantiate list), visible only while a scene-UI child is
|
|
438
|
+
installed (renders null otherwise — the component's own gate). */}
|
|
439
|
+
<SceneUIPalettePanel />
|
|
440
|
+
{/* Viewport + Game area — two independent panels, tab-switched */}
|
|
441
|
+
<div
|
|
442
|
+
style={{
|
|
443
|
+
flex: 1,
|
|
444
|
+
display: 'flex',
|
|
445
|
+
flexDirection: 'column',
|
|
446
|
+
position: 'relative',
|
|
447
|
+
pointerEvents: 'none',
|
|
448
|
+
}}
|
|
449
|
+
>
|
|
450
|
+
<ViewportTabBar
|
|
451
|
+
active={store.activeViewportTab}
|
|
452
|
+
onChange={(tab) => store.setActiveViewportTab(tab)}
|
|
453
|
+
showGame={isPlaying}
|
|
454
|
+
/>
|
|
455
|
+
{/* Viewport panel (editor) — all overlays are inside ViewportPanel */}
|
|
456
|
+
<div
|
|
457
|
+
style={{
|
|
458
|
+
flex: 1,
|
|
459
|
+
position: 'relative',
|
|
460
|
+
display: store.activeViewportTab === 'scene' ? 'block' : 'none',
|
|
461
|
+
}}
|
|
462
|
+
>
|
|
463
|
+
<ViewportPanel />
|
|
464
|
+
</div>
|
|
465
|
+
{/* Game panel — always mounted, hidden when not playing/active */}
|
|
466
|
+
<GamePanel
|
|
467
|
+
style={{
|
|
468
|
+
flex: 1,
|
|
469
|
+
position: 'relative',
|
|
470
|
+
display: isPlaying && store.activeViewportTab === 'game' ? 'flex' : 'none',
|
|
471
|
+
flexDirection: 'column',
|
|
472
|
+
pointerEvents: 'auto',
|
|
473
|
+
...(isPlaying && store.playState === 'playing'
|
|
474
|
+
? { outline: '2px solid #4ec9b0', outlineOffset: -2 }
|
|
475
|
+
: isPlaying
|
|
476
|
+
? { outline: '2px solid #dcdcaa', outlineOffset: -2 }
|
|
477
|
+
: {}),
|
|
478
|
+
}}
|
|
479
|
+
/>
|
|
480
|
+
</div>
|
|
481
|
+
<RightPanel />
|
|
482
|
+
</div>
|
|
483
|
+
{prefabSaveState && (
|
|
484
|
+
<PrefabSaveDialog
|
|
485
|
+
defaultName={prefabSaveState.name}
|
|
486
|
+
onSave={handlePrefabSaveConfirm}
|
|
487
|
+
onCancel={() => setPrefabSaveState(null)}
|
|
488
|
+
/>
|
|
489
|
+
)}
|
|
490
|
+
{/* E1.c (spec-27 §7 E1) — the scene-UI "Save as Prefab" dialog
|
|
491
|
+
(hierarchy context-menu, `ui-prefab-hierarchy-menu.ts`); a
|
|
492
|
+
self-contained host, renders nothing until that menu item fires. */}
|
|
493
|
+
<UIPrefabDialogHost />
|
|
494
|
+
{pendingRootActivation && (
|
|
495
|
+
<RootActivateDialog
|
|
496
|
+
label={pendingRootActivation.label}
|
|
497
|
+
onConfirm={() => {
|
|
498
|
+
const { groupId, rootId } = pendingRootActivation;
|
|
499
|
+
setPendingRootActivation(null);
|
|
500
|
+
void getActiveAuthoring(store).rootGroups?.activate(groupId, rootId);
|
|
501
|
+
}}
|
|
502
|
+
onCancel={() => setPendingRootActivation(null)}
|
|
503
|
+
/>
|
|
504
|
+
)}
|
|
505
|
+
</div>
|
|
506
|
+
{bottomEl && createPortal(<BottomPanel onOpenScene={handleOpenScene} />, bottomEl)}
|
|
507
|
+
</>
|
|
508
|
+
);
|
|
509
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { faChevronDown, faChevronRight, faFolder } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
3
|
+
import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
|
|
4
|
+
import { listAssets } from '../editor-api';
|
|
5
|
+
|
|
6
|
+
// --- Types ---
|
|
7
|
+
|
|
8
|
+
interface FolderTreeNode {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
root: string;
|
|
12
|
+
path: string;
|
|
13
|
+
children: FolderTreeNode[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface FolderTreeProps {
|
|
17
|
+
currentPath: string;
|
|
18
|
+
currentRoot: string;
|
|
19
|
+
onNavigate: (root: string, path: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface FolderRow {
|
|
23
|
+
node: FolderTreeNode;
|
|
24
|
+
depth: number;
|
|
25
|
+
hasChildren: boolean;
|
|
26
|
+
isOpen: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- Helpers ---
|
|
30
|
+
|
|
31
|
+
function makeNodeId(root: string, path: string): string {
|
|
32
|
+
return `${root}:${path}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fetchSubfolders(root: string, dir: string): Promise<FolderTreeNode[]> {
|
|
36
|
+
const entries = await listAssets(root, dir);
|
|
37
|
+
return entries
|
|
38
|
+
.filter((e) => e.type === 'directory')
|
|
39
|
+
.map((e) => {
|
|
40
|
+
const path = dir ? `${dir}/${e.name}` : e.name;
|
|
41
|
+
return {
|
|
42
|
+
id: makeNodeId(root, path),
|
|
43
|
+
name: e.name,
|
|
44
|
+
root,
|
|
45
|
+
path,
|
|
46
|
+
children: [],
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Recursively replace children of a target node by ID. */
|
|
52
|
+
function updateNodeChildren(
|
|
53
|
+
nodes: FolderTreeNode[],
|
|
54
|
+
targetId: string,
|
|
55
|
+
newChildren: FolderTreeNode[],
|
|
56
|
+
): FolderTreeNode[] {
|
|
57
|
+
return nodes.map((node) => {
|
|
58
|
+
if (node.id === targetId) return { ...node, children: newChildren };
|
|
59
|
+
if (node.children.length > 0) {
|
|
60
|
+
const updated = updateNodeChildren(node.children, targetId, newChildren);
|
|
61
|
+
if (updated !== node.children) return { ...node, children: updated };
|
|
62
|
+
}
|
|
63
|
+
return node;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function flattenFolderRows(nodes: FolderTreeNode[], openIds: Set<string>, depth = 0): FolderRow[] {
|
|
68
|
+
const rows: FolderRow[] = [];
|
|
69
|
+
for (const node of nodes) {
|
|
70
|
+
const isOpen = openIds.has(node.id);
|
|
71
|
+
// Show as having children if explicitly opened (children may be loading)
|
|
72
|
+
// or if children array is non-empty
|
|
73
|
+
const hasChildren = node.children.length > 0 || isOpen;
|
|
74
|
+
rows.push({ node, depth, hasChildren, isOpen });
|
|
75
|
+
if (isOpen && node.children.length > 0) {
|
|
76
|
+
rows.push(...flattenFolderRows(node.children, openIds, depth + 1));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return rows;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Main Component ---
|
|
83
|
+
|
|
84
|
+
export function FolderTree({ currentPath, currentRoot, onNavigate }: FolderTreeProps) {
|
|
85
|
+
const [treeData, setTreeData] = useState<FolderTreeNode[]>([]);
|
|
86
|
+
const openIdsRef = useRef(new Set<string>());
|
|
87
|
+
const loadedRef = useRef(new Set<string>());
|
|
88
|
+
const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
|
|
89
|
+
|
|
90
|
+
// Load top-level folders directly (no wrapper "public" node)
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
fetchSubfolders('public', '').then((topFolders) => {
|
|
93
|
+
setTreeData(topFolders);
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const loadChildren = useCallback((id: string, root: string, path: string) => {
|
|
98
|
+
if (loadedRef.current.has(id)) return;
|
|
99
|
+
loadedRef.current.add(id);
|
|
100
|
+
fetchSubfolders(root, path).then((children) => {
|
|
101
|
+
setTreeData((prev) => updateNodeChildren(prev, id, children));
|
|
102
|
+
});
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const selectedId = makeNodeId(currentRoot, currentPath);
|
|
106
|
+
const rows = flattenFolderRows(treeData, openIdsRef.current);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', paddingTop: 4 }}>
|
|
110
|
+
{rows.map((row) => {
|
|
111
|
+
const data = row.node;
|
|
112
|
+
const isActive = data.id === selectedId;
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
key={data.id}
|
|
117
|
+
style={{
|
|
118
|
+
display: 'flex',
|
|
119
|
+
alignItems: 'center',
|
|
120
|
+
height: 24,
|
|
121
|
+
gap: 4,
|
|
122
|
+
paddingLeft: row.depth * 14,
|
|
123
|
+
cursor: 'pointer',
|
|
124
|
+
background: isActive ? '#579EFF33' : 'transparent',
|
|
125
|
+
color: isActive ? '#fff' : '#c5c8ce',
|
|
126
|
+
borderRadius: 2,
|
|
127
|
+
whiteSpace: 'nowrap',
|
|
128
|
+
overflow: 'hidden',
|
|
129
|
+
fontSize: 12,
|
|
130
|
+
}}
|
|
131
|
+
onClick={() => {
|
|
132
|
+
onNavigate(data.root, data.path);
|
|
133
|
+
if (!row.isOpen) {
|
|
134
|
+
openIdsRef.current.add(data.id);
|
|
135
|
+
loadChildren(data.id, data.root, data.path);
|
|
136
|
+
forceUpdate();
|
|
137
|
+
}
|
|
138
|
+
}}
|
|
139
|
+
onMouseEnter={(e) => {
|
|
140
|
+
if (!isActive) (e.currentTarget as HTMLDivElement).style.background = '#333';
|
|
141
|
+
}}
|
|
142
|
+
onMouseLeave={(e) => {
|
|
143
|
+
if (!isActive) (e.currentTarget as HTMLDivElement).style.background = 'transparent';
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{/* Expand/collapse arrow */}
|
|
147
|
+
<span
|
|
148
|
+
onClick={(e) => {
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
const set = openIdsRef.current;
|
|
151
|
+
if (set.has(data.id)) {
|
|
152
|
+
set.delete(data.id);
|
|
153
|
+
} else {
|
|
154
|
+
set.add(data.id);
|
|
155
|
+
loadChildren(data.id, data.root, data.path);
|
|
156
|
+
}
|
|
157
|
+
forceUpdate();
|
|
158
|
+
}}
|
|
159
|
+
style={{
|
|
160
|
+
width: 10,
|
|
161
|
+
fontSize: 8,
|
|
162
|
+
textAlign: 'center',
|
|
163
|
+
flexShrink: 0,
|
|
164
|
+
color: '#8c8c8c',
|
|
165
|
+
userSelect: 'none',
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
<FontAwesomeIcon icon={row.isOpen ? faChevronDown : faChevronRight} />
|
|
169
|
+
</span>
|
|
170
|
+
<span style={{ fontSize: 12, color: '#e8a740' }}>
|
|
171
|
+
<FontAwesomeIcon icon={faFolder} />
|
|
172
|
+
</span>
|
|
173
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{data.name}</span>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
})}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|