@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,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reachable-three iframe host — the tier-2/3 middle case the founding docs flagged
|
|
3
|
+
* as the "named remaining edge" of bundled-game coverage (`cov-iframe-reachable`).
|
|
4
|
+
*
|
|
5
|
+
* A prebuilt bundle runs in its OWN iframe document (a separate realm), but — unlike
|
|
6
|
+
* the opaque embed floor — its `three` is REACHABLE: the host wires the iframe's
|
|
7
|
+
* `import 'three'` to a tiny blob module that re-exports `window.parent`'s host
|
|
8
|
+
* `three` namespace. Because the bundle therefore constructs HOST-realm `three`
|
|
9
|
+
* objects, the cross-realm canvas/document hazard that blocked naive sharing is
|
|
10
|
+
* sidestepped two ways:
|
|
11
|
+
* - every `Scene`/`Mesh`/`Camera` the bundle makes is a host-realm instance, so the
|
|
12
|
+
* editor's `instanceof THREE.*`, gizmo, raycast and inspector all work on them; and
|
|
13
|
+
* - the bundle creates + passes its OWN iframe canvas to `new WebGLRenderer({canvas})`,
|
|
14
|
+
* so the WebGL context lives in the iframe realm — no parent-document canvas adoption.
|
|
15
|
+
*
|
|
16
|
+
* The host's scene-capture trap (installed on the shared host `three`) then captures
|
|
17
|
+
* the bundle's renderer on its first frame exactly as in the shared-ESM path — the
|
|
18
|
+
* difference is purely WHERE the bundle's code runs (an iframe document) and HOW its
|
|
19
|
+
* `three` is resolved (importmap → blob re-export). The result is a real
|
|
20
|
+
* `IngestAuthoringAdapter` over the live `Object3D` tree: full in-realm editing.
|
|
21
|
+
*
|
|
22
|
+
* See `docs/ADAPTER-ARCHITECTURE.md` §2.3 (coverage table, reachable-three row).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { MountedGame } from '@engine/adapter';
|
|
26
|
+
import { installSceneCapture, type SceneCaptureHandle } from '@engine/adapter/ingest/scene-capture';
|
|
27
|
+
import * as THREE from 'three';
|
|
28
|
+
import type { EditorStore } from '../editor-store';
|
|
29
|
+
import { IngestAuthoringAdapter } from './ingest-authoring-adapter';
|
|
30
|
+
import type { IngestMount, MountIngestOptions } from './ingest-game-adapter';
|
|
31
|
+
|
|
32
|
+
/** Global key under which the host exposes its `three` namespace to the iframe realm. */
|
|
33
|
+
const HOST_THREE_KEY = '__VGAI_HOST_THREE';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a blob-module URL that re-exports the host `three` namespace from
|
|
37
|
+
* `window.parent[HOST_THREE_KEY]`. The iframe's importmap maps `three` to this URL,
|
|
38
|
+
* so the bundle's `import * as THREE from 'three'` yields the HOST instance.
|
|
39
|
+
*/
|
|
40
|
+
function makeHostThreeReexportUrl(): string {
|
|
41
|
+
// Only valid JS identifier exports can be re-bound by name; `default` is handled
|
|
42
|
+
// separately. (three's public API is all identifier-named named exports.)
|
|
43
|
+
const ident = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
44
|
+
const keys = Object.keys(THREE).filter((k) => k !== 'default' && ident.test(k));
|
|
45
|
+
const body =
|
|
46
|
+
`const T = window.parent[${JSON.stringify(HOST_THREE_KEY)}];\n` +
|
|
47
|
+
`export default T;\n` +
|
|
48
|
+
keys.map((k) => `export const ${k} = T[${JSON.stringify(k)}];`).join('\n');
|
|
49
|
+
return URL.createObjectURL(new Blob([body], { type: 'text/javascript' }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let _hostNsReexportSeq = 0;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* D-W1 (docs/WAVE4-FTUE-HARDENING-DESIGN.md) — the three-side twin of
|
|
56
|
+
* `ingest-iframe-2d.ts`'s `makeHostNamespaceReexportUrl`: build a blob module
|
|
57
|
+
* re-exporting an ARBITRARY host namespace (a manifest's `extraDeps` beyond
|
|
58
|
+
* `three` itself — e.g. `gsap`), for the iframe's importmap. Duplicated
|
|
59
|
+
* (rather than imported from the world2d module) to keep this file's own
|
|
60
|
+
* module boundary independent of the pixi-flavored ingest-iframe-2d stack —
|
|
61
|
+
* both are ~10-line, dependency-free functions over `window`/`Blob`/`URL`.
|
|
62
|
+
*/
|
|
63
|
+
function makeHostNamespaceReexportUrl(ns: Record<string, unknown>): string {
|
|
64
|
+
const key = `__vgaiHostThreeExtraDep_${_hostNsReexportSeq++}`;
|
|
65
|
+
(window as unknown as Record<string, unknown>)[key] = ns;
|
|
66
|
+
const ident = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
67
|
+
const keys = Object.keys(ns).filter((k) => k !== 'default' && ident.test(k));
|
|
68
|
+
const body =
|
|
69
|
+
`const T = window.parent[${JSON.stringify(key)}];\n` +
|
|
70
|
+
`export default (T && T.default !== undefined ? T.default : T);\n` +
|
|
71
|
+
keys.map((k) => `export const ${k} = T[${JSON.stringify(k)}];`).join('\n');
|
|
72
|
+
return URL.createObjectURL(new Blob([body], { type: 'text/javascript' }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* D-W1 — the three analog of pixi's `Assets.init` basePath wrap: a
|
|
77
|
+
* document.write iframe keeps the PARENT's `window.location`, so a relative
|
|
78
|
+
* asset request the bundle makes (through a `three` loader, which routes
|
|
79
|
+
* every URL through `DefaultLoadingManager`) would otherwise resolve against
|
|
80
|
+
* the parent editor page, not the served bundle directory. Absolute
|
|
81
|
+
* (has-a-scheme), root-relative (`/...`), `data:`, and `blob:` URLs already
|
|
82
|
+
* resolve correctly on their own and pass through unchanged; only bundle-
|
|
83
|
+
* relative requests get joined against `assetBaseUrl`. `assetBaseUrl` itself
|
|
84
|
+
* MUST already be absolute here (`new URL(url, assetBaseUrl)` requires an
|
|
85
|
+
* origin) — the ingest-mode.ts caller resolves a root-relative manifest
|
|
86
|
+
* value against the editor page's own URL BEFORE calling this (mirrors the
|
|
87
|
+
* pixi sibling's identical `assetBaseUrl` absolutization). Exported for
|
|
88
|
+
* direct unit testing (pure, no DOM).
|
|
89
|
+
*/
|
|
90
|
+
export function makeAssetUrlModifier(assetBaseUrl: string): (url: string) => string {
|
|
91
|
+
const base = assetBaseUrl.endsWith('/') ? assetBaseUrl : `${assetBaseUrl}/`;
|
|
92
|
+
const isAlreadyResolvable = /^([a-z][a-z0-9+.-]*:)|^\//i;
|
|
93
|
+
return (url: string) => {
|
|
94
|
+
if (isAlreadyResolvable.test(url)) return url;
|
|
95
|
+
return new URL(url, base).toString();
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Assemble the reachable-three iframe document (D-W1): factored into a pure,
|
|
101
|
+
* directly-unit-testable function so both mount paths below — the original
|
|
102
|
+
* inline-source mount and the new prebuilt-bundle mount — build their HTML
|
|
103
|
+
* through ONE seam, and a unit test can pin the exact assembled string
|
|
104
|
+
* (importmap/base/bodyHtml/script tag) without a real iframe/DOM. Byte-
|
|
105
|
+
* compatible with the original inline-source document by construction: that
|
|
106
|
+
* mount passes the SAME `style`/`imports`/inline-script shape it always
|
|
107
|
+
* built inline, just through this shared assembler now.
|
|
108
|
+
*/
|
|
109
|
+
export interface ReachableThreeDocumentOptions {
|
|
110
|
+
/** Bare-specifier -> URL importmap entries (MUST include `three`). */
|
|
111
|
+
imports: Record<string, string>;
|
|
112
|
+
/** `<base href>` for the iframe, when given. */
|
|
113
|
+
baseHref?: string | undefined;
|
|
114
|
+
/** Inline `<style>` block body (kept for the original inline-source mount's exact byte output). */
|
|
115
|
+
style?: string | undefined;
|
|
116
|
+
/** DOM injected into `<body>` before the script tag. */
|
|
117
|
+
bodyHtml?: string | undefined;
|
|
118
|
+
/** The game's own script — either an inline module body or a `<script type="module" src=...>`. */
|
|
119
|
+
script: { kind: 'inline'; source: string } | { kind: 'src'; url: string };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function assembleReachableThreeDocument(opts: ReachableThreeDocumentOptions): string {
|
|
123
|
+
// Split the closing script token so this module's own bundling never sees a
|
|
124
|
+
// literal `</script>` (defensive; matches the world2d iframe mounts' convention).
|
|
125
|
+
const close = `</${'scr'}ipt>`;
|
|
126
|
+
const baseTag = opts.baseHref ? `<base href="${opts.baseHref}">` : '';
|
|
127
|
+
const styleTag = opts.style ? `<style>${opts.style}</style>` : '';
|
|
128
|
+
const importmapTag = `<script type="importmap">${JSON.stringify({ imports: opts.imports })}${close}`;
|
|
129
|
+
const bodyHtml = opts.bodyHtml ?? '';
|
|
130
|
+
const scriptTag =
|
|
131
|
+
opts.script.kind === 'inline'
|
|
132
|
+
? `<script type="module">${opts.script.source}${close}`
|
|
133
|
+
: `<script type="module" src="${opts.script.url}">${close}`;
|
|
134
|
+
return (
|
|
135
|
+
`<!doctype html><html><head><meta charset="utf-8">${baseTag}${styleTag}${importmapTag}</head>` +
|
|
136
|
+
`<body>${bodyHtml}${scriptTag}</body></html>`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Mount a prebuilt bundle inside a same-origin iframe whose `three` is the host's,
|
|
142
|
+
* capture its live scene through the shared-`three` trap, and return an
|
|
143
|
+
* {@link IngestMount} with a real authoring adapter — identical contract to the
|
|
144
|
+
* shared-ESM {@link import('./ingest-game-adapter').mountIngestGame} path.
|
|
145
|
+
*/
|
|
146
|
+
export async function mountIngestGameIframeReachable(
|
|
147
|
+
store: EditorStore,
|
|
148
|
+
gameModuleSrc: string,
|
|
149
|
+
gameContainer: HTMLElement,
|
|
150
|
+
opts: MountIngestOptions = {},
|
|
151
|
+
): Promise<IngestMount> {
|
|
152
|
+
// Expose host `three` to the (same-origin) iframe realm.
|
|
153
|
+
(window as unknown as Record<string, unknown>)[HOST_THREE_KEY] = THREE;
|
|
154
|
+
const reexportUrl = makeHostThreeReexportUrl();
|
|
155
|
+
|
|
156
|
+
// Trap the host renderer prototype BEFORE the bundle constructs its renderer, so
|
|
157
|
+
// the bundle's (host-realm) renderer is captured and the editor's pre-existing one
|
|
158
|
+
// is not (its `render` is an own property set before the trap existed).
|
|
159
|
+
const capture: SceneCaptureHandle = installSceneCapture(THREE);
|
|
160
|
+
|
|
161
|
+
// Build a same-origin iframe and write the bundle document into it. Using
|
|
162
|
+
// document.write (not srcdoc) guarantees the about:blank doc inherits the parent
|
|
163
|
+
// origin, so `window.parent.__VGAI_HOST_THREE` and the blob: URL both resolve.
|
|
164
|
+
const iframe = document.createElement('iframe');
|
|
165
|
+
iframe.setAttribute('data-testid', 'ingest-reachable-frame');
|
|
166
|
+
iframe.style.cssText =
|
|
167
|
+
'position:absolute;inset:0;width:100%;height:100%;border:0;background:#000;';
|
|
168
|
+
gameContainer.appendChild(iframe);
|
|
169
|
+
|
|
170
|
+
const doc = iframe.contentDocument;
|
|
171
|
+
if (!doc) throw new Error('iframe contentDocument unavailable (cross-origin?)');
|
|
172
|
+
doc.open();
|
|
173
|
+
doc.write(
|
|
174
|
+
assembleReachableThreeDocument({
|
|
175
|
+
imports: { three: reexportUrl },
|
|
176
|
+
style: 'html,body{margin:0;height:100%;overflow:hidden;background:#000}canvas{display:block}',
|
|
177
|
+
script: { kind: 'inline', source: gameModuleSrc },
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
doc.close();
|
|
181
|
+
|
|
182
|
+
// The bundle renders on its first rAF; the host trap captures it.
|
|
183
|
+
const rt = await capture.waitForCapture(opts.captureTimeoutMs ?? 10_000);
|
|
184
|
+
|
|
185
|
+
const loop = {
|
|
186
|
+
pause: () => rt.renderer.setAnimationLoop(null),
|
|
187
|
+
resume: () => {
|
|
188
|
+
const cb = capture.getAnimationLoop(rt.renderer);
|
|
189
|
+
if (cb) rt.renderer.setAnimationLoop(cb);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const authoring = new IngestAuthoringAdapter(store, rt.scene, {
|
|
194
|
+
loop,
|
|
195
|
+
overlay: opts.overlay,
|
|
196
|
+
persist: opts.persist,
|
|
197
|
+
destination: opts.destination,
|
|
198
|
+
camera: rt.camera,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const cleanup = () => {
|
|
202
|
+
try {
|
|
203
|
+
rt.renderer.setAnimationLoop(null);
|
|
204
|
+
rt.renderer.dispose();
|
|
205
|
+
} catch {
|
|
206
|
+
/* ignore */
|
|
207
|
+
}
|
|
208
|
+
capture.uninstall();
|
|
209
|
+
URL.revokeObjectURL(reexportUrl);
|
|
210
|
+
iframe.remove();
|
|
211
|
+
delete (window as unknown as Record<string, unknown>)[HOST_THREE_KEY];
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const mounted: MountedGame = {
|
|
215
|
+
kind: 'threejs',
|
|
216
|
+
scene: rt.scene,
|
|
217
|
+
camera: rt.camera,
|
|
218
|
+
drivesOwnLoop: true,
|
|
219
|
+
setPaused: (paused) => (paused ? loop.pause() : loop.resume()),
|
|
220
|
+
resize: (w, h) => rt.renderer.setSize(w, h, false),
|
|
221
|
+
dispose: cleanup,
|
|
222
|
+
authoring,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return { game: mounted, authoring, hostEl: iframe, capture, scene: rt.scene };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* D-W1 (docs/WAVE4-FTUE-HARDENING-DESIGN.md) — options for
|
|
230
|
+
* {@link mountIngestGameIframeReachableBundle}, the prebuilt-EXTERNALIZED-
|
|
231
|
+
* bundle sibling of {@link mountIngestGameIframeReachable} above (which takes
|
|
232
|
+
* an INLINE module-source string). Mirrors pixi's
|
|
233
|
+
* `IframeReachableMultiOpts`/`mountIngestGame2DIframeReachableMulti`
|
|
234
|
+
* (`@engine/world2d/ingest-iframe-2d.ts`) field-for-field, minus the
|
|
235
|
+
* pixi-specific `pixiModuleUrl`/`inlineSrc` (three has no version-skew
|
|
236
|
+
* mechanism this wave — D-W7 — and this function is bundle-only, never
|
|
237
|
+
* inline).
|
|
238
|
+
*/
|
|
239
|
+
export interface IframeReachableBundleOpts extends MountIngestOptions {
|
|
240
|
+
/** Load the game bundle from this absolute URL (a built, externalized-`three` multi-file game). */
|
|
241
|
+
bundleUrl: string;
|
|
242
|
+
/** `<base href>` for the iframe so the game's relative asset URLs resolve. */
|
|
243
|
+
baseHref?: string | undefined;
|
|
244
|
+
/** DOM injected into the iframe body before the bundle's script boots. */
|
|
245
|
+
bodyHtml?: string | undefined;
|
|
246
|
+
/** Bare-specifier -> host module-namespace map for this game's OTHER externalized deps beyond `three` (already-resolved, D-Z3 `namespaces`). */
|
|
247
|
+
extraDeps?: Record<string, Record<string, unknown>> | undefined;
|
|
248
|
+
/** Bare-specifier -> VERBATIM URL importmap entries (the D-Z3 project-node_modules fallback, already-resolved). */
|
|
249
|
+
extraDepUrls?: Record<string, string> | undefined;
|
|
250
|
+
/** Host-side `DefaultLoadingManager.setURLModifier` basePath remap (already absolutized by the caller). */
|
|
251
|
+
assetBaseUrl?: string | undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Mount a PREBUILT, externalized-`three` bundle (its own `<script type="module"
|
|
256
|
+
* src=...>`, not an inline source string) inside the same same-origin
|
|
257
|
+
* reachable-three iframe recipe {@link mountIngestGameIframeReachable} uses:
|
|
258
|
+
* `installSceneCapture` trapped BEFORE the bundle's module executes (the
|
|
259
|
+
* importmap/capture setup lives in the SAME `document.write` call, so the
|
|
260
|
+
* deferred `type="module"` script only ever sees the trap already installed
|
|
261
|
+
* — verified against the existing inline path's identical ordering), an
|
|
262
|
+
* importmap resolving `three` (+ `extraDeps`) to host-namespace re-export
|
|
263
|
+
* blobs (or verbatim D-Z3 URLs), an optional `<base href>`/`bodyHtml`, and an
|
|
264
|
+
* `assetBaseUrl` asset adapter on the host `three`'s
|
|
265
|
+
* `DefaultLoadingManager.setURLModifier` (installed before the bundle runs,
|
|
266
|
+
* restored on dispose) — the three analog of pixi's `Assets.init` wrap.
|
|
267
|
+
*
|
|
268
|
+
* Capture-timeout handling is NOT the same as the inline-source mount above
|
|
269
|
+
* (that one still throws — this task keeps it byte-compatible on purpose):
|
|
270
|
+
* this bundle path DEGRADES to an embed-only `IngestMount` on a capture
|
|
271
|
+
* failure (three unreachable/mismatched/bundle never rendered in time),
|
|
272
|
+
* mirroring `mountIngestGame`'s own degrade shape — so
|
|
273
|
+
* `enterIngestModeWithGame`'s existing `mount.embedOnly` check (ingest-mode.ts)
|
|
274
|
+
* applies uniformly to this new route too (D-W1's explicit requirement).
|
|
275
|
+
*/
|
|
276
|
+
export async function mountIngestGameIframeReachableBundle(
|
|
277
|
+
store: EditorStore,
|
|
278
|
+
gameContainer: HTMLElement,
|
|
279
|
+
opts: IframeReachableBundleOpts,
|
|
280
|
+
): Promise<IngestMount> {
|
|
281
|
+
(window as unknown as Record<string, unknown>)[HOST_THREE_KEY] = THREE;
|
|
282
|
+
const created: string[] = [];
|
|
283
|
+
const threeReexportUrl = makeHostThreeReexportUrl();
|
|
284
|
+
created.push(threeReexportUrl);
|
|
285
|
+
|
|
286
|
+
const imports: Record<string, string> = { three: threeReexportUrl };
|
|
287
|
+
for (const [spec, ns] of Object.entries(opts.extraDeps ?? {})) {
|
|
288
|
+
const url = makeHostNamespaceReexportUrl(ns);
|
|
289
|
+
created.push(url);
|
|
290
|
+
imports[spec] = url;
|
|
291
|
+
}
|
|
292
|
+
// D-Z3 fallback: verbatim URL entries, no blob wrapping, merged in beside
|
|
293
|
+
// the host-namespace ones (mirrors `mountIngestGame2DIframeReachableMulti`'s
|
|
294
|
+
// identical merge).
|
|
295
|
+
for (const [spec, url] of Object.entries(opts.extraDepUrls ?? {})) imports[spec] = url;
|
|
296
|
+
|
|
297
|
+
// Trap the host renderer prototype BEFORE the bundle constructs its renderer
|
|
298
|
+
// (same ordering guarantee as the inline-source mount above).
|
|
299
|
+
const capture: SceneCaptureHandle = installSceneCapture(THREE);
|
|
300
|
+
|
|
301
|
+
// Host-side asset URL remap (asset adapter): install BEFORE the bundle
|
|
302
|
+
// runs, restore on dispose — pure host library configuration, never
|
|
303
|
+
// touches the game source.
|
|
304
|
+
let restoreUrlModifier: (() => void) | undefined;
|
|
305
|
+
if (opts.assetBaseUrl) {
|
|
306
|
+
THREE.DefaultLoadingManager.setURLModifier(makeAssetUrlModifier(opts.assetBaseUrl));
|
|
307
|
+
restoreUrlModifier = () => THREE.DefaultLoadingManager.setURLModifier((u) => u);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const iframe = document.createElement('iframe');
|
|
311
|
+
iframe.setAttribute('data-testid', 'ingest-reachable-frame');
|
|
312
|
+
iframe.style.cssText =
|
|
313
|
+
'position:absolute;inset:0;width:100%;height:100%;border:0;background:#000;';
|
|
314
|
+
gameContainer.appendChild(iframe);
|
|
315
|
+
|
|
316
|
+
const cleanupHost = () => {
|
|
317
|
+
capture.uninstall();
|
|
318
|
+
restoreUrlModifier?.();
|
|
319
|
+
for (const u of created) URL.revokeObjectURL(u);
|
|
320
|
+
iframe.remove();
|
|
321
|
+
delete (window as unknown as Record<string, unknown>)[HOST_THREE_KEY];
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const doc = iframe.contentDocument;
|
|
325
|
+
if (!doc) {
|
|
326
|
+
cleanupHost();
|
|
327
|
+
throw new Error('iframe contentDocument unavailable (cross-origin?)');
|
|
328
|
+
}
|
|
329
|
+
doc.open();
|
|
330
|
+
doc.write(
|
|
331
|
+
assembleReachableThreeDocument({
|
|
332
|
+
imports,
|
|
333
|
+
baseHref: opts.baseHref,
|
|
334
|
+
bodyHtml: opts.bodyHtml,
|
|
335
|
+
script: { kind: 'src', url: opts.bundleUrl },
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
doc.close();
|
|
339
|
+
|
|
340
|
+
let rt: Awaited<ReturnType<SceneCaptureHandle['waitForCapture']>>;
|
|
341
|
+
try {
|
|
342
|
+
rt = await capture.waitForCapture(opts.captureTimeoutMs ?? 10_000);
|
|
343
|
+
} catch {
|
|
344
|
+
// three unreachable / bundle failed to render a host-realm frame within
|
|
345
|
+
// the timeout — degrade to embed-only (mountIngestGame's own degrade
|
|
346
|
+
// shape) rather than throwing, so the caller's existing
|
|
347
|
+
// `mount.embedOnly` check takes over (D-W1).
|
|
348
|
+
return {
|
|
349
|
+
embedOnly: true,
|
|
350
|
+
hostEl: iframe,
|
|
351
|
+
capture,
|
|
352
|
+
game: {
|
|
353
|
+
kind: 'threejs',
|
|
354
|
+
scene: new THREE.Scene(),
|
|
355
|
+
camera: new THREE.PerspectiveCamera(),
|
|
356
|
+
drivesOwnLoop: true,
|
|
357
|
+
resize: () => {},
|
|
358
|
+
dispose: cleanupHost,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const loop = {
|
|
364
|
+
pause: () => rt.renderer.setAnimationLoop(null),
|
|
365
|
+
resume: () => {
|
|
366
|
+
const cb = capture.getAnimationLoop(rt.renderer);
|
|
367
|
+
if (cb) rt.renderer.setAnimationLoop(cb);
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const authoring = new IngestAuthoringAdapter(store, rt.scene, {
|
|
372
|
+
loop,
|
|
373
|
+
overlay: opts.overlay,
|
|
374
|
+
persist: opts.persist,
|
|
375
|
+
destination: opts.destination,
|
|
376
|
+
camera: rt.camera,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const cleanup = () => {
|
|
380
|
+
try {
|
|
381
|
+
rt.renderer.setAnimationLoop(null);
|
|
382
|
+
rt.renderer.dispose();
|
|
383
|
+
} catch {
|
|
384
|
+
/* ignore */
|
|
385
|
+
}
|
|
386
|
+
cleanupHost();
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const mounted: MountedGame = {
|
|
390
|
+
kind: 'threejs',
|
|
391
|
+
scene: rt.scene,
|
|
392
|
+
camera: rt.camera,
|
|
393
|
+
drivesOwnLoop: true,
|
|
394
|
+
setPaused: (paused) => (paused ? loop.pause() : loop.resume()),
|
|
395
|
+
resize: (w, h) => rt.renderer.setSize(w, h, false),
|
|
396
|
+
dispose: cleanup,
|
|
397
|
+
authoring,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
return { game: mounted, authoring, hostEl: iframe, capture, scene: rt.scene };
|
|
401
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingest overlay persistence — the honest serialize-back for an UNMODIFIED game.
|
|
3
|
+
*
|
|
4
|
+
* We never touch the game's source. Instead we persist the editor's edits beside
|
|
5
|
+
* it as an OVERLAY keyed by the object's stable structural-path id, and reapply it
|
|
6
|
+
* on the next ingest (after the game rebuilds its scene). This is the founding
|
|
7
|
+
* doc's "Overlay file" persistence mode (`docs/ADAPTER-ARCHITECTURE.md` §3.2,
|
|
8
|
+
* `docs/ADAPTER-VISION-ORIGINAL.md` "Patch File"): same `PersistenceProvider`
|
|
9
|
+
* interface as `.vscn`, different source of truth.
|
|
10
|
+
*
|
|
11
|
+
* The envelope TYPE + pure parsing (`OverlayFile`, `parseOverlayFile`, the
|
|
12
|
+
* legacy-flat-file upgrade) moved engine-side (T3.8, docs/DECISIONS-PENDING.md
|
|
13
|
+
* §D9) so the runtime ship-path applier (`@engine/adapter/ingest/overlay-applier.ts`)
|
|
14
|
+
* can share the SAME parsing rather than duplicating it — re-exported below
|
|
15
|
+
* unchanged so every existing import site here keeps working byte-for-byte.
|
|
16
|
+
*
|
|
17
|
+
* Storage transport (the "overlay unification" fix, docs/BACKBONE-TASKS.md's
|
|
18
|
+
* T3.3 close-out finding): `loadOverlay`/`saveOverlay` used to go straight
|
|
19
|
+
* through `getStorageBackend()`, which in server mode is `HttpStorage` —
|
|
20
|
+
* hard-rooted at `<projectRoot>/public/` — so the overlay actually landed at
|
|
21
|
+
* `<projectRoot>/public/.vgai/overlays/<id>.json`, contradicting `overlayPath()`'s
|
|
22
|
+
* "project-root-relative" doc comment and splitting the project into TWO
|
|
23
|
+
* `.vgai/` dirs (the editor-state/thumbnail/scaffold-baseline machinery, and
|
|
24
|
+
* `authoring/world-overlay.ts`'s manifest-world overlays, both use the
|
|
25
|
+
* project-root `.vgai/`). This module now follows the EXACT mode-branched
|
|
26
|
+
* pattern `world-overlay.ts` established (T6.1 §1.G):
|
|
27
|
+
* - browser mode (`getEditorMode() === 'browser'`): `getStorageBackend()`
|
|
28
|
+
* directly — OPFS/FSA already root at the TRUE project folder, no `public/`
|
|
29
|
+
* insertion exists there.
|
|
30
|
+
* - server mode: the dedicated `/__editor/vgai-file` GET/POST route pair
|
|
31
|
+
* (`editor-server.ts`), which reads/writes `<projectRoot>/.vgai/...`
|
|
32
|
+
* directly via `node:fs`, bypassing `HttpStorage`'s `public/` rooting.
|
|
33
|
+
* `overlayPath(gameId)` itself is UNCHANGED — it is also the ship path's
|
|
34
|
+
* default fetch URL (`overlay-applier.ts`).
|
|
35
|
+
*
|
|
36
|
+
* Back-compat read: `loadOverlay` tries the unified project-root location
|
|
37
|
+
* first, and — only if nothing is found there — falls back to the legacy
|
|
38
|
+
* `public/`-rooted location a pre-fix save would have written, so overlays
|
|
39
|
+
* saved before this fix keep loading. A hit on the legacy path logs one
|
|
40
|
+
* `console.warn` per `gameId` (never repeated) and is NEVER written back or
|
|
41
|
+
* deleted; the next `saveOverlay` for that id writes only the unified
|
|
42
|
+
* project-root location.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
emptyOverlayFile,
|
|
47
|
+
type IngestOverlay,
|
|
48
|
+
type IngestOverride,
|
|
49
|
+
OVERLAY_FORMAT_VERSION,
|
|
50
|
+
type OverlayFile,
|
|
51
|
+
overlayPath,
|
|
52
|
+
parseOverlayFile,
|
|
53
|
+
} from '@engine/adapter/ingest/overlay-file';
|
|
54
|
+
import { getEditorMode } from '../editor-mode';
|
|
55
|
+
import { getStorageBackend } from '../storage';
|
|
56
|
+
|
|
57
|
+
export type { IngestOverlay, IngestOverride, OverlayFile };
|
|
58
|
+
export { OVERLAY_FORMAT_VERSION, overlayPath };
|
|
59
|
+
|
|
60
|
+
const BASE = '/__editor';
|
|
61
|
+
|
|
62
|
+
/** `gameId`s we've already warned about finding only at the legacy location. */
|
|
63
|
+
const warnedLegacyIds = new Set<string>();
|
|
64
|
+
|
|
65
|
+
/** Read the unified project-root overlay location, or `undefined` if absent/unreadable. */
|
|
66
|
+
async function readProjectRoot(path: string): Promise<string | undefined> {
|
|
67
|
+
try {
|
|
68
|
+
if (getEditorMode() === 'browser') {
|
|
69
|
+
const backend = getStorageBackend();
|
|
70
|
+
if (!(await backend.exists(path))) return undefined;
|
|
71
|
+
return await backend.read(path);
|
|
72
|
+
}
|
|
73
|
+
const res = await fetch(`${BASE}/vgai-file?${new URLSearchParams({ path })}`);
|
|
74
|
+
if (!res.ok) return undefined;
|
|
75
|
+
return await res.text();
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read the legacy `public/`-rooted overlay location, or `undefined` if
|
|
83
|
+
* absent/unreadable. Server mode's `getStorageBackend()` IS `HttpStorage`
|
|
84
|
+
* (hard-rooted at `<projectRoot>/public/`), so the plain `overlayPath(gameId)`
|
|
85
|
+
* string already resolves there; browser mode's backend roots at the TRUE
|
|
86
|
+
* project folder, so the legacy file must be addressed with an explicit
|
|
87
|
+
* `public/` prefix.
|
|
88
|
+
*/
|
|
89
|
+
async function readLegacy(gameId: string): Promise<string | undefined> {
|
|
90
|
+
try {
|
|
91
|
+
const backend = getStorageBackend();
|
|
92
|
+
const legacyPath =
|
|
93
|
+
getEditorMode() === 'browser' ? `public/${overlayPath(gameId)}` : overlayPath(gameId);
|
|
94
|
+
if (!(await backend.exists(legacyPath))) return undefined;
|
|
95
|
+
return await backend.read(legacyPath);
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Load a game's saved overlay envelope, or an empty envelope if none exists /
|
|
103
|
+
* is unreadable anywhere. Always returns the {@link OverlayFile} shape —
|
|
104
|
+
* legacy flat-file content is transparently wrapped (`parseOverlayFile`,
|
|
105
|
+
* engine-side), whether found at the unified or the legacy location. Loads
|
|
106
|
+
* are read-only: a legacy hit is never rewritten or deleted here — only the
|
|
107
|
+
* next `saveOverlay` moves this `gameId` onto the unified location.
|
|
108
|
+
*/
|
|
109
|
+
export async function loadOverlay(gameId: string): Promise<OverlayFile> {
|
|
110
|
+
const path = overlayPath(gameId);
|
|
111
|
+
|
|
112
|
+
const primary = await readProjectRoot(path);
|
|
113
|
+
if (primary !== undefined) {
|
|
114
|
+
try {
|
|
115
|
+
return parseOverlayFile(JSON.parse(primary) as unknown);
|
|
116
|
+
} catch {
|
|
117
|
+
// Unparseable at the unified location — fall through and try legacy too.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const legacy = await readLegacy(gameId);
|
|
122
|
+
if (legacy !== undefined) {
|
|
123
|
+
try {
|
|
124
|
+
const parsed = parseOverlayFile(JSON.parse(legacy) as unknown);
|
|
125
|
+
if (!warnedLegacyIds.has(gameId)) {
|
|
126
|
+
warnedLegacyIds.add(gameId);
|
|
127
|
+
const legacyPath = getEditorMode() === 'browser' ? `public/${path}` : path;
|
|
128
|
+
console.warn(
|
|
129
|
+
`ingest-overlay: '${gameId}' overlay found only at the legacy location (${legacyPath}), ` +
|
|
130
|
+
`not the unified project-root location (${path}). The next save will write ${path}.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
} catch {
|
|
135
|
+
// Unparseable legacy content either — fall through to empty.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return emptyOverlayFile();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Persist a game's overlay — always as the D9 envelope (including the first
|
|
144
|
+
* save after loading a legacy flat file: the silent-upgrade path), and always
|
|
145
|
+
* to the unified project-root location (browser: `getStorageBackend()`
|
|
146
|
+
* directly; server: the dedicated `/__editor/vgai-file` POST route — never
|
|
147
|
+
* the legacy `public/`-rooted path, even if this `gameId`'s overlay was just
|
|
148
|
+
* loaded from there). `gameVersion` is the mounted game's manifest `version`
|
|
149
|
+
* (`ingest-mode.ts` threads it from either the manifest route's resolved
|
|
150
|
+
* manifest or the fixture route's `IngestGame.gameVersion`); omitted/`null`
|
|
151
|
+
* stamps `authoredAgainst.gameVersion: null` rather than fabricating one
|
|
152
|
+
* (anti-shim).
|
|
153
|
+
*
|
|
154
|
+
* `upstreamPin` (R5, docs/MASTER-ARCHITECTURE-REVIEW.md §4(c)) is the
|
|
155
|
+
* mounted game's `UPSTREAM.md` pin, when `ingest-mode.ts` could discover one
|
|
156
|
+
* for this session (see its `readUpstreamPinForActiveProject`). Deliberately
|
|
157
|
+
* OMITTED (not defaulted to `null`) when the caller doesn't pass it at all —
|
|
158
|
+
* that keeps every pre-R5 call site (and every existing test pinning the
|
|
159
|
+
* on-disk envelope shape byte-for-byte) writing the EXACT SAME JSON as
|
|
160
|
+
* before, with no `upstreamPin` key at all: that omission is itself the
|
|
161
|
+
* signal `./overlay-file.ts`'s `parseOverlayFile`/`overlay-report.ts` treat
|
|
162
|
+
* as "legacy, never checked" on a later load. Once a caller DOES pass a
|
|
163
|
+
* value (a string, or an explicit `null` for "looked, found nothing" —
|
|
164
|
+
* `ingest-mode.ts` always passes one of these two once it's R5-aware), the
|
|
165
|
+
* key is always written from then on.
|
|
166
|
+
*/
|
|
167
|
+
export async function saveOverlay(
|
|
168
|
+
gameId: string,
|
|
169
|
+
overlay: IngestOverlay,
|
|
170
|
+
gameVersion?: string | null,
|
|
171
|
+
upstreamPin?: string | null,
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
const authoredAgainst: OverlayFile['authoredAgainst'] = { gameVersion: gameVersion ?? null };
|
|
174
|
+
if (upstreamPin !== undefined) authoredAgainst.upstreamPin = upstreamPin;
|
|
175
|
+
const file: OverlayFile = {
|
|
176
|
+
formatVersion: OVERLAY_FORMAT_VERSION,
|
|
177
|
+
authoredAgainst,
|
|
178
|
+
overrides: overlay,
|
|
179
|
+
};
|
|
180
|
+
const path = overlayPath(gameId);
|
|
181
|
+
const content = JSON.stringify(file, null, 2);
|
|
182
|
+
|
|
183
|
+
if (getEditorMode() === 'browser') {
|
|
184
|
+
await getStorageBackend().write(path, content);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const res = await fetch(`${BASE}/vgai-file`, {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: { 'Content-Type': 'application/json' },
|
|
190
|
+
body: JSON.stringify({ path, content }),
|
|
191
|
+
});
|
|
192
|
+
if (!res.ok) throw new Error(`saveOverlay '${gameId}': HTTP ${res.status}`);
|
|
193
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layered viewport picking (B4, D12) — "click anything you can see; topmost
|
|
3
|
+
* visible layer wins." Shell POLICY over the composite's public seams, kept a
|
|
4
|
+
* separate module (rather than living inline in `editor-viewport.ts`) so it
|
|
5
|
+
* stays lean AND unit-testable with plain fake adapters — no real DOM/canvas
|
|
6
|
+
* needed.
|
|
7
|
+
*
|
|
8
|
+
* This module speaks ONLY the public contract: `CompositeAuthoringAdapter
|
|
9
|
+
* .childAdapters()` (T0), the `AuthoringAdapter.pickable` provider (D12,
|
|
10
|
+
* `@engine/adapter`), and `world-session-state.ts`'s session toggles. It never
|
|
11
|
+
* reaches into any one child adapter's own private, format-specific model (no
|
|
12
|
+
* concrete-world-shape vocabulary of any kind here) — `instanceof
|
|
13
|
+
* CompositeAuthoringAdapter` is the ONE sanctioned type check (that class's
|
|
14
|
+
* own doc comment blesses exactly this use, mirroring
|
|
15
|
+
* `authoring/first-party-inspector/is-first-party-selection.ts`'s identical
|
|
16
|
+
* precedent).
|
|
17
|
+
*
|
|
18
|
+
* Walk order: the composite's NON-threejs children (react/pixi design-time
|
|
19
|
+
* layers), topmost-first by (install-time) manifest zOrder — `stackOrder`
|
|
20
|
+
* sorts bottom-to-top, so `.reverse()` gives topmost-first while preserving
|
|
21
|
+
* its own tie-break rule. The focused threejs world is appended LAST
|
|
22
|
+
* (canvas-bottom) regardless of its manifest zOrder — it always renders
|
|
23
|
+
* beneath every positioned design-time layer (`design-time-layers.ts`'s own
|
|
24
|
+
* doc comment: "the threejs canvas keeps its existing slot"), so it is
|
|
25
|
+
* correct for it to lose every pick to a layer that also claims the point.
|
|
26
|
+
*
|
|
27
|
+
* A layer that is eye-hidden or pick-locked (`world-session-state.ts`, B1) is
|
|
28
|
+
* skipped ENTIRELY — its `pickable.pick` is never even called. This is a
|
|
29
|
+
* SEPARATE mechanism from a layer's `interactive` toggle: an interactive
|
|
30
|
+
* layer's DOM has `pointer-events: auto`, so a real pointer event lands on the
|
|
31
|
+
* layer's own element and this walk (triggered only by the CANVAS's own
|
|
32
|
+
* `pointerup` listener) never runs at all for that event — mutually exclusive
|
|
33
|
+
* by construction, no code needed here for it.
|
|
34
|
+
*
|
|
35
|
+
* When the active adapter is NOT a composite (single-world project, or a
|
|
36
|
+
* non-composite override), this degrades to that one adapter's own
|
|
37
|
+
* `pickable.pick` (or `null` if it doesn't have one) — today's single-world
|
|
38
|
+
* behavior, unchanged.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import type { AuthoringAdapter } from '@engine/adapter';
|
|
42
|
+
import { stackOrder } from '@engine/runtime/input-router';
|
|
43
|
+
import type { EditorStore } from '../editor-store';
|
|
44
|
+
import { getActiveAuthoring } from './active-adapter';
|
|
45
|
+
import { CompositeAuthoringAdapter } from './composite-authoring-adapter';
|
|
46
|
+
import { isWorldHidden, isWorldPickLocked } from './world-session-state';
|
|
47
|
+
|
|
48
|
+
/** Topmost-first pick across every visible, pick-unlocked layer of the active
|
|
49
|
+
* (possibly composite) authoring adapter. Returns the winning node id, or
|
|
50
|
+
* `null` when nothing under `(clientX, clientY)` claims the point. */
|
|
51
|
+
export function pickTopmost(store: EditorStore, clientX: number, clientY: number): string | null {
|
|
52
|
+
const active = getActiveAuthoring(store);
|
|
53
|
+
if (!(active instanceof CompositeAuthoringAdapter)) {
|
|
54
|
+
return active.pickable?.pick(clientX, clientY) ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const children = active.childAdapters();
|
|
58
|
+
const layered = children.filter((c) => c.kind !== 'threejs');
|
|
59
|
+
const threejs = children.filter((c) => c.kind === 'threejs');
|
|
60
|
+
|
|
61
|
+
// `stackOrder` wants `{id, zOrder}` (`ClaimEntry`) — reuse `worldId` as its
|
|
62
|
+
// `id` (this walk has no separate id vocabulary of its own).
|
|
63
|
+
const layeredClaims = layered.map((c) => ({
|
|
64
|
+
id: c.worldId,
|
|
65
|
+
zOrder: c.zOrder,
|
|
66
|
+
worldId: c.worldId,
|
|
67
|
+
adapter: c.adapter,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// Topmost-first: `stackOrder` is bottom-to-top (ascending zOrder, ties by
|
|
71
|
+
// original array order) — `.reverse()` flips it without disturbing its own
|
|
72
|
+
// tie-break rule (a later-declared same-zOrder layer still wins the tie,
|
|
73
|
+
// now by being checked FIRST instead of sorting last).
|
|
74
|
+
const order: Array<{ worldId: string; adapter: AuthoringAdapter }> = [
|
|
75
|
+
...stackOrder(layeredClaims).reverse(),
|
|
76
|
+
// The focused threejs world paints/picks LAST — canvas-bottom, regardless
|
|
77
|
+
// of its own manifest zOrder (see module doc comment).
|
|
78
|
+
...threejs,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const { worldId, adapter } of order) {
|
|
82
|
+
if (isWorldHidden(worldId) || isWorldPickLocked(worldId)) continue;
|
|
83
|
+
const hit = adapter.pickable?.pick(clientX, clientY);
|
|
84
|
+
if (hit) return hit;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|