@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,891 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CompositeAuthoringAdapter — merges N per-world {@link AuthoringAdapter}s into ONE
|
|
3
|
+
* authoring tree so the editor's hierarchy lists EVERY mounted world's entities and
|
|
4
|
+
* selection/inspection routes to the right backend. This is the
|
|
5
|
+
* editor-multi-root-selection seam, generalized to N-ary (T6.1 slice 3,
|
|
6
|
+
* docs/MULTI-WORLD-DESIGN.md §1.F — GAME-ROOT-DESIGN §6.4's v1 scope, verbatim):
|
|
7
|
+
* one top-level GROUP NODE per world (`world:<id> (<kind>)`), the child adapter's
|
|
8
|
+
* own roots nested beneath it. Routing is by GROUP-NODE OWNERSHIP (which child
|
|
9
|
+
* adapter's tree actually contains a given node id) — not the old 2-child `w2d:`
|
|
10
|
+
* id-prefix sniff, which cannot generalize past exactly one 2D surface.
|
|
11
|
+
*
|
|
12
|
+
* The editor still talks to a single {@link AuthoringAdapter}; selection +
|
|
13
|
+
* inspection work in every world; the 3D transform gizmo stays threejs-only (a
|
|
14
|
+
* viewport concern, outside this adapter); play/pause stays game-level (T7.6).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
AssetDropProvider,
|
|
19
|
+
AuthoringAdapter,
|
|
20
|
+
AuthoringCapabilities,
|
|
21
|
+
ComponentsProvider,
|
|
22
|
+
EditorNode,
|
|
23
|
+
FileMapProvider,
|
|
24
|
+
HierarchyProvider,
|
|
25
|
+
InspectorProvider,
|
|
26
|
+
PersistenceProvider,
|
|
27
|
+
PropertyDescriptor,
|
|
28
|
+
RootGroup,
|
|
29
|
+
RootGroupsProvider,
|
|
30
|
+
SelectionProvider,
|
|
31
|
+
StoriesProvider,
|
|
32
|
+
StoryRef,
|
|
33
|
+
StructureProvider,
|
|
34
|
+
Transform,
|
|
35
|
+
TransformOwner,
|
|
36
|
+
TransformProvider,
|
|
37
|
+
} from '@engine/adapter';
|
|
38
|
+
import type * as THREE from 'three';
|
|
39
|
+
import { editorConsole } from '../editor-console';
|
|
40
|
+
|
|
41
|
+
/** One child world's authoring adapter, ordered as given to the constructor. */
|
|
42
|
+
export interface CompositeChild {
|
|
43
|
+
/** Manifest world id (T3.1) — becomes the group node id `world:<worldId>`. */
|
|
44
|
+
readonly worldId: string;
|
|
45
|
+
/** World render substrate kind — shown in the group node's label. */
|
|
46
|
+
readonly kind: string;
|
|
47
|
+
readonly adapter: AuthoringAdapter;
|
|
48
|
+
/**
|
|
49
|
+
* D12 (B4) — the world's INSTALL-TIME manifest zOrder (`world.zOrder ?? 0`),
|
|
50
|
+
* independent of this child's position in the constructor's array. Absent
|
|
51
|
+
* (every pre-B4 call site) ⇒ `childAdapters()` falls back to the array
|
|
52
|
+
* index, today's behavior unchanged. See `childAdapters()`'s own doc
|
|
53
|
+
* comment for why this stopped being "array order doubles as z-order" and
|
|
54
|
+
* what "install-time" means here.
|
|
55
|
+
*/
|
|
56
|
+
readonly zOrder?: number;
|
|
57
|
+
/**
|
|
58
|
+
* C3 (docs/unified-world-editor/26-unified-editor-spec.md §4 C3) — nest
|
|
59
|
+
* this child's group node UNDER another child's group node instead of at
|
|
60
|
+
* the top level (directly under the synthetic `game` root, or as a bare
|
|
61
|
+
* top-level root when there is no `gameRoot`). Used by the scene-UI child
|
|
62
|
+
* (`edit-mode-authoring.ts`'s UI child, `kind: 'scene-ui'`) to appear
|
|
63
|
+
* "under its owning world" in the ONE hierarchy, per the C3 spec text.
|
|
64
|
+
* Absent ⇒ today's flat top-level shape, unchanged.
|
|
65
|
+
*/
|
|
66
|
+
readonly parentWorldId?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const GROUP_PREFIX = 'world:';
|
|
70
|
+
const GAME_ROOT_ID = 'game';
|
|
71
|
+
|
|
72
|
+
function groupNodeId(worldId: string): string {
|
|
73
|
+
return `${GROUP_PREFIX}${worldId}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** True for a synthetic group-node id this Composite itself manufactures (never
|
|
77
|
+
* produced by a child adapter — the `world:` prefix is reserved for this use). */
|
|
78
|
+
function isGroupNodeId(id: string): boolean {
|
|
79
|
+
return id.startsWith(GROUP_PREFIX);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A4 (D8) — the game root's manifest-backed property surface, implemented by
|
|
84
|
+
* `edit-mode-authoring.ts`'s `ManifestAuthoring` over the RAW `vgai.game.json`
|
|
85
|
+
* (read-modify-write, never round-tripped through Zod). Defined here (not
|
|
86
|
+
* imported from `edit-mode-authoring.ts`) to avoid a circular import — that
|
|
87
|
+
* module already imports `CompositeAuthoringAdapter`.
|
|
88
|
+
*/
|
|
89
|
+
export interface GameRootManifestProvider {
|
|
90
|
+
/** True while no `vgai.game.json` exists on disk yet (a synthesized
|
|
91
|
+
* in-memory manifest) — edits buffer in memory instead of writing. */
|
|
92
|
+
readonly synthesized: boolean;
|
|
93
|
+
getGameName(): string;
|
|
94
|
+
setGameName(name: string): void;
|
|
95
|
+
getWorldZOrder(worldId: string): number;
|
|
96
|
+
setWorldZOrder(worldId: string, value: number): void;
|
|
97
|
+
getWorldPausable(worldId: string): boolean;
|
|
98
|
+
setWorldPausable(worldId: string, value: boolean): void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** OR every child's capabilities together — a feature is offered if ANY
|
|
102
|
+
* child supports it (the editor gates per-node via the routed adapter
|
|
103
|
+
* anyway). Factored out of the constructor (B1) so {@link
|
|
104
|
+
* CompositeAuthoringAdapter.replaceChild} can recompute it after an
|
|
105
|
+
* in-place child swap without duplicating the reduce. */
|
|
106
|
+
function computeCapabilities(children: ReadonlyArray<CompositeChild>): AuthoringCapabilities {
|
|
107
|
+
return children.reduce<AuthoringCapabilities>(
|
|
108
|
+
(acc, c) => ({
|
|
109
|
+
transform: acc.transform || c.adapter.capabilities.transform,
|
|
110
|
+
material: acc.material || c.adapter.capabilities.material,
|
|
111
|
+
inspectorFields: acc.inspectorFields || c.adapter.capabilities.inspectorFields,
|
|
112
|
+
create: acc.create || c.adapter.capabilities.create,
|
|
113
|
+
delete: acc.delete || c.adapter.capabilities.delete,
|
|
114
|
+
reparent: acc.reparent || c.adapter.capabilities.reparent,
|
|
115
|
+
persist: acc.persist || c.adapter.capabilities.persist,
|
|
116
|
+
}),
|
|
117
|
+
{
|
|
118
|
+
transform: false,
|
|
119
|
+
material: false,
|
|
120
|
+
inspectorFields: false,
|
|
121
|
+
create: false,
|
|
122
|
+
delete: false,
|
|
123
|
+
reparent: false,
|
|
124
|
+
persist: false,
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class CompositeAuthoringAdapter implements AuthoringAdapter {
|
|
130
|
+
/** NOT `readonly` (interface-level `readonly` is a consumer contract, not a
|
|
131
|
+
* ban on internal reassignment) — B1's {@link replaceChild} recomputes
|
|
132
|
+
* this after an in-place child swap (e.g. a world's design-time layer
|
|
133
|
+
* mounting successfully upgrades its Boundary adapter to a live one). */
|
|
134
|
+
capabilities: AuthoringCapabilities;
|
|
135
|
+
/**
|
|
136
|
+
* NOT `ReadonlyArray` (B1): {@link replaceChild} mutates ONE element of
|
|
137
|
+
* this array in place, preserving both the array's own identity-adjacent
|
|
138
|
+
* invariants (length, index/z-order) and — critically — THIS composite's
|
|
139
|
+
* OWN object identity, which `ViewportPanel.tsx`'s teardown-identity
|
|
140
|
+
* capture, `exitEditModeAuthoring`'s installed-instance check, and the
|
|
141
|
+
* `markEditModeOverride` brand all key off. A REBUILT composite (a new
|
|
142
|
+
* `CompositeAuthoringAdapter` instance) would silently break all three —
|
|
143
|
+
* see this class's own doc comment and `edit-mode-authoring.ts`'s. Copied
|
|
144
|
+
* (`[...children]`) out of the constructor's `ReadonlyArray` parameter so
|
|
145
|
+
* mutating it here never aliases a caller's own array.
|
|
146
|
+
*/
|
|
147
|
+
private readonly children: CompositeChild[];
|
|
148
|
+
/** A4 (D8) — present only when the caller wants a synthetic game-root node
|
|
149
|
+
* wrapping every world group (edit-mode's manifest-disclosing composite);
|
|
150
|
+
* absent for every other composite construction (play-mode's
|
|
151
|
+
* `installMultiWorldAuthoring`), which keeps `hierarchy.roots()` returning
|
|
152
|
+
* world group nodes directly, UNCHANGED from before A4. */
|
|
153
|
+
private readonly gameRoot: GameRootManifestProvider | null;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* N-ary (T6.1 slice 3): an ordered `{ worldId, kind, adapter }[]` — one group
|
|
157
|
+
* node per entry, in array order.
|
|
158
|
+
*
|
|
159
|
+
* The 2-positional-arg legacy overload (`new CompositeAuthoringAdapter(world3d,
|
|
160
|
+
* world2d)`, normalizing to synthetic `'world3d'`/`'world2d'` ids) is REMOVED
|
|
161
|
+
* this slice (T6.1 slice 4, docs/MULTI-WORLD-DESIGN.md §1.H): its only
|
|
162
|
+
* production call site was `world2d-mode.ts`'s dev-console-only hook, deleted
|
|
163
|
+
* with the rest of the world2d silo. Every production caller now goes through
|
|
164
|
+
* the N-ary array constructor (`play-mode.ts`'s `installMultiWorldAuthoring`).
|
|
165
|
+
*
|
|
166
|
+
* `gameRoot` (A4, D8) is an opt-in second argument: `edit-mode-authoring.ts`
|
|
167
|
+
* passes a `ManifestAuthoring` instance to wrap every world group under one
|
|
168
|
+
* synthetic `game` root; every other caller omits it and gets today's
|
|
169
|
+
* shape (world groups as roots) byte-for-byte unchanged.
|
|
170
|
+
*/
|
|
171
|
+
constructor(children: ReadonlyArray<CompositeChild>, gameRoot?: GameRootManifestProvider) {
|
|
172
|
+
this.children = [...children];
|
|
173
|
+
this.gameRoot = gameRoot ?? null;
|
|
174
|
+
if (children.length === 0) {
|
|
175
|
+
throw new Error('CompositeAuthoringAdapter: at least one child world is required.');
|
|
176
|
+
}
|
|
177
|
+
this.capabilities = computeCapabilities(this.children);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* B1 — swap `worldId`'s child adapter IN PLACE (the design-time layer
|
|
182
|
+
* mount's "adapter upgrade": a world's read-only `BoundaryAuthoringAdapter`
|
|
183
|
+
* is replaced by a live one — react: `ReactWorldAuthoringAdapter` — once its
|
|
184
|
+
* layer mounts successfully; a layer that fails/throws rebuilds the
|
|
185
|
+
* Boundary with the caught reason, also through this method). Mutates the
|
|
186
|
+
* existing children array (preserving array order — z-order/`childAdapters()`
|
|
187
|
+
* index is unaffected) and recomputes `capabilities`, rather than
|
|
188
|
+
* constructing a new `CompositeAuthoringAdapter` — a rebuild would break
|
|
189
|
+
* `ViewportPanel.tsx`'s captured teardown identity, `exitEditModeAuthoring`'s
|
|
190
|
+
* installed-instance check, and the `markEditModeOverride` brand, all of
|
|
191
|
+
* which key off THIS instance's identity (see the class doc comment).
|
|
192
|
+
*
|
|
193
|
+
* No-op (loud warn, #18 discipline) when `worldId` doesn't name an existing
|
|
194
|
+
* child — never silently routes to the wrong world or grows the array.
|
|
195
|
+
* Callers are responsible for their own side effects around the swap
|
|
196
|
+
* (undo-timeline registration for a newly-live adapter,
|
|
197
|
+
* `store.notifyIngestEdit()` so panels re-render against the new child) —
|
|
198
|
+
* this method only owns the swap + capability recompute.
|
|
199
|
+
*/
|
|
200
|
+
replaceChild(worldId: string, adapter: AuthoringAdapter): void {
|
|
201
|
+
const index = this.children.findIndex((c) => c.worldId === worldId);
|
|
202
|
+
if (index === -1) {
|
|
203
|
+
editorConsole.warn(
|
|
204
|
+
`[CompositeAuthoringAdapter] replaceChild: no child with worldId "${worldId}" — ignoring.`,
|
|
205
|
+
'authoring',
|
|
206
|
+
);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// D12 (B4) — carry the outgoing child's `zOrder` forward too (not just
|
|
210
|
+
// `kind`): a swap must not silently reset a world's install-time zOrder
|
|
211
|
+
// back to "no override" (array-index fallback). Built conditionally (not
|
|
212
|
+
// `{ ..., zOrder }`) because `zOrder` is an OPTIONAL property under this
|
|
213
|
+
// repo's `exactOptionalPropertyTypes` — an explicit `zOrder: undefined`
|
|
214
|
+
// is not the same as an absent property to the type checker.
|
|
215
|
+
const { kind, zOrder } = this.children[index]!;
|
|
216
|
+
this.children[index] =
|
|
217
|
+
zOrder === undefined ? { worldId, kind, adapter } : { worldId, kind, adapter, zOrder };
|
|
218
|
+
this.capabilities = computeCapabilities(this.children);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The group node id for a given child (exposed for callers building
|
|
222
|
+
* post-construction selection/expansion state — e.g. tests, e2e hooks). */
|
|
223
|
+
groupNodeId(worldId: string): string {
|
|
224
|
+
return groupNodeId(worldId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The child that owns `id` — by ACTUAL ownership (its hierarchy resolves the
|
|
228
|
+
* id), not by any prefix convention. `null` for a group-node id or an id no
|
|
229
|
+
* child recognizes. O(children) per call; fine at editor-UI scale, and avoids
|
|
230
|
+
* a cache that could go stale as a live tree adds/removes nodes. */
|
|
231
|
+
private findOwnerChild(id: string): CompositeChild | null {
|
|
232
|
+
if (isGroupNodeId(id)) return null;
|
|
233
|
+
for (const child of this.children) {
|
|
234
|
+
if (child.adapter.hierarchy.node(id)) return child;
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Route by ownership, falling back to the FIRST child for an unrecognized id
|
|
240
|
+
* (matches the old 2-child adapter's "everything else is world3d" floor —
|
|
241
|
+
* callers that pass a bogus id get inert/default responses, not a throw). */
|
|
242
|
+
private route(id: string): AuthoringAdapter {
|
|
243
|
+
return this.findOwnerChild(id)?.adapter ?? this.children[0]!.adapter;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** PUBLIC ownership query (T0 — A3's inspector dispatch and B4's pick routing
|
|
247
|
+
* both need it): the `worldId` of the child that owns `id`, or `null` for a
|
|
248
|
+
* group-node id or an id no child recognizes. */
|
|
249
|
+
ownerOf(id: string): string | null {
|
|
250
|
+
return this.findOwnerChild(id)?.worldId ?? null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* PUBLIC read-only view of the wrapped children (T0), ordered as constructed.
|
|
255
|
+
*
|
|
256
|
+
* `zOrder` is EACH CHILD'S OWN `CompositeChild.zOrder` (the world's
|
|
257
|
+
* install-time manifest zOrder) when supplied, falling back to the array
|
|
258
|
+
* index otherwise. Array order is NOT z-order (D12/B4 correction of this
|
|
259
|
+
* doc comment's former claim): children are built in MANIFEST DECLARATION
|
|
260
|
+
* order (`edit-mode-authoring.ts`), not stacking order — paint/pick order is
|
|
261
|
+
* `stackOrder` over each child's manifest zOrder (mirroring
|
|
262
|
+
* `create-runtime.ts`'s real stacking pass and `design-time-layers.ts`'s
|
|
263
|
+
* layer z-index assignment), which is exactly why this field exists.
|
|
264
|
+
* "Install-time" — the zOrder captured when this composite/child was built,
|
|
265
|
+
* not a live subscription: a world's zOrder edited afterward (the generic
|
|
266
|
+
* inspector's `zOrder` property, `A4`/D8) does NOT retroactively reorder an
|
|
267
|
+
* already-mounted design-time layer stack or an already-computed pick walk
|
|
268
|
+
* order this call returns; both settle to the new order the next time the
|
|
269
|
+
* composite/layers are rebuilt. Callers that need the LIVE manifest value
|
|
270
|
+
* (e.g. `GameHierarchy.tsx`'s zOrder badge) read it from the manifest
|
|
271
|
+
* surface directly instead of trusting this field alone — see
|
|
272
|
+
* `compositeGroupBadge`'s own comment.
|
|
273
|
+
*/
|
|
274
|
+
childAdapters(): ReadonlyArray<{
|
|
275
|
+
worldId: string;
|
|
276
|
+
adapter: AuthoringAdapter;
|
|
277
|
+
kind: string;
|
|
278
|
+
zOrder: number;
|
|
279
|
+
}> {
|
|
280
|
+
return this.children.map((c, index) => ({
|
|
281
|
+
worldId: c.worldId,
|
|
282
|
+
adapter: c.adapter,
|
|
283
|
+
kind: c.kind,
|
|
284
|
+
zOrder: c.zOrder ?? index,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** PUBLIC (A4, D8) — the synthetic game-root node's id when this composite
|
|
289
|
+
* was built with a `gameRoot` manifest surface, else `null`. Mirrors
|
|
290
|
+
* `groupNodeId()`'s "ask the adapter, don't hardcode the id" precedent —
|
|
291
|
+
* the shell (`GameHierarchy.tsx`) reads this instead of the literal
|
|
292
|
+
* string, so it never needs to know the reserved id itself. */
|
|
293
|
+
get gameRootId(): string | null {
|
|
294
|
+
return this.gameRoot ? GAME_ROOT_ID : null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* C3 — a child declared with `parentWorldId` nests its group node UNDER
|
|
299
|
+
* that parent's group node instead of at the top level: the parent's
|
|
300
|
+
* `parentId` resolution below picks up the nested group id automatically
|
|
301
|
+
* (`this.children.find(...).parentWorldId`), and this method also appends
|
|
302
|
+
* every child NESTED under `worldId` (in declaration order) to the END of
|
|
303
|
+
* `childIds`, after `worldId`'s own adapter roots — so e.g. a `scene-ui`
|
|
304
|
+
* child appears as a trailing "ui (scene-ui)" row under its owning world.
|
|
305
|
+
*/
|
|
306
|
+
private groupNode(worldId: string, kind: string, childRoots: EditorNode[]): EditorNode {
|
|
307
|
+
const self = this.children.find((c) => c.worldId === worldId);
|
|
308
|
+
const parentId = self?.parentWorldId
|
|
309
|
+
? groupNodeId(self.parentWorldId)
|
|
310
|
+
: this.gameRoot
|
|
311
|
+
? GAME_ROOT_ID
|
|
312
|
+
: null;
|
|
313
|
+
const nestedGroupIds = this.children
|
|
314
|
+
.filter((c) => c.parentWorldId === worldId)
|
|
315
|
+
.map((c) => groupNodeId(c.worldId));
|
|
316
|
+
return {
|
|
317
|
+
id: groupNodeId(worldId),
|
|
318
|
+
label: `world:${worldId} (${kind})`,
|
|
319
|
+
kind: 'group',
|
|
320
|
+
parentId,
|
|
321
|
+
childIds: [...childRoots.map((r) => r.id), ...nestedGroupIds],
|
|
322
|
+
flags: { runtimeOnly: true },
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** C3 — every child NOT nested under another (`parentWorldId` unset): the
|
|
327
|
+
* top-level group set, either wrapped under the synthetic `game` root or
|
|
328
|
+
* returned directly (see callers). A nested child is reached only via its
|
|
329
|
+
* parent's `childIds` (`groupNode` above), never listed here too — a node
|
|
330
|
+
* in two parents' `childIds` would render twice / cycle the row walk. */
|
|
331
|
+
private topLevelChildren(): CompositeChild[] {
|
|
332
|
+
return this.children.filter((c) => !c.parentWorldId);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A4 (D8) — the synthetic `game` root wrapping every TOP-LEVEL world group
|
|
336
|
+
* node (C3: a nested child, e.g. a scene-UI world, is reached through its
|
|
337
|
+
* parent's own `childIds` instead — see `topLevelChildren`). Only ever
|
|
338
|
+
* constructed when `this.gameRoot` is set. */
|
|
339
|
+
private gameRootNode(): EditorNode {
|
|
340
|
+
return {
|
|
341
|
+
id: GAME_ROOT_ID,
|
|
342
|
+
label: this.gameRoot?.getGameName() || 'Game',
|
|
343
|
+
kind: 'group',
|
|
344
|
+
parentId: null,
|
|
345
|
+
childIds: this.topLevelChildren().map((c) => groupNodeId(c.worldId)),
|
|
346
|
+
flags: { runtimeOnly: true },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
readonly hierarchy: HierarchyProvider = {
|
|
351
|
+
roots: (): EditorNode[] => {
|
|
352
|
+
// A4 (D8) — wrap under a synthetic `game` root ONLY when a manifest
|
|
353
|
+
// surface was supplied at construction (edit-mode's composite); every
|
|
354
|
+
// other composite (play-mode) returns world groups as roots directly,
|
|
355
|
+
// exactly as before A4. Either way, only TOP-LEVEL groups are roots
|
|
356
|
+
// (C3) — a nested child (e.g. scene-UI) is reached via its parent's
|
|
357
|
+
// own `childIds`, never listed as a root of its own.
|
|
358
|
+
if (this.gameRoot) return [this.gameRootNode()];
|
|
359
|
+
return this.topLevelChildren().map((c) =>
|
|
360
|
+
this.groupNode(c.worldId, c.kind, c.adapter.hierarchy.roots()),
|
|
361
|
+
);
|
|
362
|
+
},
|
|
363
|
+
node: (id) => {
|
|
364
|
+
if (id === GAME_ROOT_ID) {
|
|
365
|
+
return this.gameRoot ? this.gameRootNode() : null;
|
|
366
|
+
}
|
|
367
|
+
if (isGroupNodeId(id)) {
|
|
368
|
+
const worldId = id.slice(GROUP_PREFIX.length);
|
|
369
|
+
const child = this.children.find((c) => c.worldId === worldId);
|
|
370
|
+
if (!child) return null;
|
|
371
|
+
return this.groupNode(child.worldId, child.kind, child.adapter.hierarchy.roots());
|
|
372
|
+
}
|
|
373
|
+
const owner = this.findOwnerChild(id);
|
|
374
|
+
if (!owner) return null;
|
|
375
|
+
const node = owner.adapter.hierarchy.node(id);
|
|
376
|
+
if (!node) return null;
|
|
377
|
+
// A child's OWN root has `parentId: null` in its adapter's tree — rewrite
|
|
378
|
+
// it to point at this world's group node so the merged tree is one
|
|
379
|
+
// connected forest instead of the child roots looking parentless again.
|
|
380
|
+
return node.parentId === null ? { ...node, parentId: groupNodeId(owner.worldId) } : node;
|
|
381
|
+
},
|
|
382
|
+
object3D: (id) =>
|
|
383
|
+
id === GAME_ROOT_ID || isGroupNodeId(id) ? null : this.route(id).hierarchy.object3D(id),
|
|
384
|
+
idForObject3D: (o: THREE.Object3D) => {
|
|
385
|
+
for (const child of this.children) {
|
|
386
|
+
const id = child.adapter.hierarchy.idForObject3D(o);
|
|
387
|
+
if (id) return id;
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
readonly selection: SelectionProvider = {
|
|
394
|
+
// Union of every child's own selection.
|
|
395
|
+
get: () => {
|
|
396
|
+
const seen = new Set<string>();
|
|
397
|
+
for (const child of this.children) {
|
|
398
|
+
for (const id of child.adapter.selection?.get() ?? []) seen.add(id);
|
|
399
|
+
}
|
|
400
|
+
return [...seen];
|
|
401
|
+
},
|
|
402
|
+
set: (ids) => {
|
|
403
|
+
// A group-node id (or the game root) is not a real entity in any
|
|
404
|
+
// child's tree — route it specially rather than forwarding a bogus id
|
|
405
|
+
// nobody owns. Route each REAL id to its owning child.
|
|
406
|
+
//
|
|
407
|
+
// Every existing adapter (first-party/ingest/world2d) delegates
|
|
408
|
+
// selection to the SAME editor-global `EditorStore` — `selectMultiple`
|
|
409
|
+
// REPLACES the store's whole selected-id set, it does not merge. So
|
|
410
|
+
// calling `.set([])` on every non-owning child (the naive "clear the
|
|
411
|
+
// rest" approach) would call `selectMultiple` a second time on the
|
|
412
|
+
// SAME shared store and immediately clear out whatever the owning
|
|
413
|
+
// child's `.set()` call just wrote — a single click could never
|
|
414
|
+
// actually select anything. Only forward to children that own at
|
|
415
|
+
// least one of `ids`.
|
|
416
|
+
//
|
|
417
|
+
// A4 (D8): when NONE do (an empty selection, OR a selection that is
|
|
418
|
+
// PURELY a synthetic id), forward to exactly ONE child (any child
|
|
419
|
+
// sharing the backing store reaches every other child too) — but
|
|
420
|
+
// forward the SYNTHETIC ID ITSELF (not an empty clear) when one was
|
|
421
|
+
// given. This is why `RightPanel.tsx`'s `[...store.selectedEntityIds][0]`
|
|
422
|
+
// (and `Inspector.tsx`'s identical read) can show the game root's/
|
|
423
|
+
// a world group's own properties: every stock adapter shares the SAME
|
|
424
|
+
// `EditorStore`, so writing `'game'`/`'world:<id>'` into any one
|
|
425
|
+
// child's selection lands in that one shared `selectedEntityIds` set,
|
|
426
|
+
// which both those raw-store reads AND this composite's OWN `get()`
|
|
427
|
+
// (via the owning-adapter's `selection.get()`) then see identically —
|
|
428
|
+
// no separate synthetic-id bookkeeping needed on this class at all.
|
|
429
|
+
const byChild = new Map<AuthoringAdapter, string[]>();
|
|
430
|
+
let synthetic: string | null = null;
|
|
431
|
+
for (const id of ids) {
|
|
432
|
+
if (isGroupNodeId(id) || id === this.gameRootId) {
|
|
433
|
+
synthetic = id; // last one wins — only used when NOTHING else claims byChild
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const owner = this.findOwnerChild(id);
|
|
437
|
+
if (!owner) continue;
|
|
438
|
+
const arr = byChild.get(owner.adapter);
|
|
439
|
+
if (arr) arr.push(id);
|
|
440
|
+
else byChild.set(owner.adapter, [id]);
|
|
441
|
+
}
|
|
442
|
+
if (byChild.size === 0) {
|
|
443
|
+
this.children[0]!.adapter.selection?.set(synthetic ? [synthetic] : []);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
for (const [adapter, childIds] of byChild) adapter.selection?.set(childIds);
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
readonly transforms: TransformProvider = {
|
|
451
|
+
get: (id): Transform =>
|
|
452
|
+
this.route(id).transforms?.get(id) ?? {
|
|
453
|
+
position: [0, 0, 0],
|
|
454
|
+
rotation: [0, 0, 0, 1],
|
|
455
|
+
scale: [1, 1, 1],
|
|
456
|
+
},
|
|
457
|
+
owner: (id): TransformOwner => this.route(id).transforms?.owner(id) ?? 'editor',
|
|
458
|
+
beginEdit: (id) => this.route(id).transforms?.beginEdit(id),
|
|
459
|
+
apply: (id, t) => this.route(id).transforms?.apply(id, t),
|
|
460
|
+
endEdit: (id) => this.route(id).transforms?.endEdit(id),
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* A4 (D8) — the game root's `name` and each world group's `zOrder`/
|
|
465
|
+
* `pausable`/`kind` are a MANIFEST concept, not something any child
|
|
466
|
+
* adapter's own inspector knows about — intercepted here, BEFORE `route()`,
|
|
467
|
+
* so a real per-world adapter is never asked about an id it doesn't own.
|
|
468
|
+
* `null` when `id` isn't the game root or a group-node id (or there is no
|
|
469
|
+
* `gameRoot` manifest surface at all), letting the caller fall through to
|
|
470
|
+
* the ordinary routed behavior unchanged.
|
|
471
|
+
*/
|
|
472
|
+
private gameRootProperties(id: string): PropertyDescriptor[] | null {
|
|
473
|
+
if (!this.gameRoot) return null;
|
|
474
|
+
if (id === GAME_ROOT_ID) {
|
|
475
|
+
return [{ path: 'name', label: 'Name', type: 'string' }];
|
|
476
|
+
}
|
|
477
|
+
if (isGroupNodeId(id) && this.children.some((c) => groupNodeId(c.worldId) === id)) {
|
|
478
|
+
return [
|
|
479
|
+
{ path: 'kind', label: 'Kind', type: 'string', readonly: true },
|
|
480
|
+
{ path: 'zOrder', label: 'Z Order', type: 'number' },
|
|
481
|
+
{ path: 'pausable', label: 'Pausable', type: 'boolean' },
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
readonly inspector: InspectorProvider = {
|
|
488
|
+
properties: (id): PropertyDescriptor[] =>
|
|
489
|
+
this.gameRootProperties(id) ?? this.route(id).inspector?.properties(id) ?? [],
|
|
490
|
+
get: (id, path) => {
|
|
491
|
+
if (this.gameRoot) {
|
|
492
|
+
if (id === GAME_ROOT_ID) {
|
|
493
|
+
return path === 'name' ? this.gameRoot.getGameName() : undefined;
|
|
494
|
+
}
|
|
495
|
+
const worldId = isGroupNodeId(id) ? id.slice(GROUP_PREFIX.length) : null;
|
|
496
|
+
const child = worldId ? this.children.find((c) => c.worldId === worldId) : undefined;
|
|
497
|
+
if (child) {
|
|
498
|
+
if (path === 'kind') return child.kind;
|
|
499
|
+
if (path === 'zOrder') return this.gameRoot.getWorldZOrder(worldId!);
|
|
500
|
+
if (path === 'pausable') return this.gameRoot.getWorldPausable(worldId!);
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return this.route(id).inspector?.get(id, path);
|
|
505
|
+
},
|
|
506
|
+
set: (id, path, value) => {
|
|
507
|
+
if (this.gameRoot) {
|
|
508
|
+
if (id === GAME_ROOT_ID) {
|
|
509
|
+
if (path === 'name') this.gameRoot.setGameName(String(value));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const worldId = isGroupNodeId(id) ? id.slice(GROUP_PREFIX.length) : null;
|
|
513
|
+
const child = worldId ? this.children.find((c) => c.worldId === worldId) : undefined;
|
|
514
|
+
if (child) {
|
|
515
|
+
if (path === 'zOrder') this.gameRoot.setWorldZOrder(worldId!, Number(value));
|
|
516
|
+
else if (path === 'pausable') this.gameRoot.setWorldPausable(worldId!, Boolean(value));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
this.route(id).inspector?.set(id, path, value);
|
|
521
|
+
},
|
|
522
|
+
remove: (id, path) => {
|
|
523
|
+
// U2 — game-root/world-group synthetic props have no removable override;
|
|
524
|
+
// everything else routes to the owning child (which optional-omits
|
|
525
|
+
// `remove` unless it's the react-world adapter).
|
|
526
|
+
if (this.gameRoot && (id === GAME_ROOT_ID || isGroupNodeId(id))) return;
|
|
527
|
+
this.route(id).inspector?.remove?.(id, path);
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* A2 (map §5) — resolve a `create`/`creatableKinds` target: a group-node id
|
|
533
|
+
* (`world:<w>`) targets that world's OWN root (forwarded parentId
|
|
534
|
+
* `undefined`); a real id targets its owning child (forwarded as-is);
|
|
535
|
+
* `null`/`undefined` (no parent — the toolbar's root-level "Add") targets
|
|
536
|
+
* the FIRST child that actually has a structure provider — in edit mode
|
|
537
|
+
* that is the one focused, live `VgaiSceneAuthoringAdapter`; every other
|
|
538
|
+
* child is a read-only `BoundaryAuthoringAdapter` with no `structure` at
|
|
539
|
+
* all. `null` when nothing can serve the request (never silently routed to
|
|
540
|
+
* the wrong world — #18).
|
|
541
|
+
*/
|
|
542
|
+
private resolveStructureTarget(
|
|
543
|
+
parentId: string | null | undefined,
|
|
544
|
+
): { child: CompositeChild; forwardParentId: string | undefined } | null {
|
|
545
|
+
if (parentId != null && isGroupNodeId(parentId)) {
|
|
546
|
+
const worldId = parentId.slice(GROUP_PREFIX.length);
|
|
547
|
+
const child = this.children.find((c) => c.worldId === worldId);
|
|
548
|
+
return child ? { child, forwardParentId: undefined } : null;
|
|
549
|
+
}
|
|
550
|
+
if (parentId != null) {
|
|
551
|
+
const child = this.findOwnerChild(parentId);
|
|
552
|
+
return child ? { child, forwardParentId: parentId } : null;
|
|
553
|
+
}
|
|
554
|
+
const child = this.children.find((c) => c.adapter.structure);
|
|
555
|
+
return child ? { child, forwardParentId: undefined } : null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* A2 (map §5) — structural pass-through, routed by ownership. Never a
|
|
560
|
+
* silent no-op when a request can't be served: `create`/`remove`/
|
|
561
|
+
* `duplicate` log loudly via `editorConsole.warn` and return an honest
|
|
562
|
+
* empty/unchanged result (#18) rather than pretending to route to the
|
|
563
|
+
* wrong world. `reorder` is the one exception — it mirrors the base
|
|
564
|
+
* contract's OWN "absent means no reorder UI" convention, so a missing
|
|
565
|
+
* `reorder` on the owning child is a silent no-op, not a warning.
|
|
566
|
+
*/
|
|
567
|
+
readonly structure: StructureProvider = {
|
|
568
|
+
create: (kind, parentId) => {
|
|
569
|
+
const target = this.resolveStructureTarget(parentId ?? null);
|
|
570
|
+
if (!target?.child.adapter.structure) {
|
|
571
|
+
editorConsole.warn(
|
|
572
|
+
`[CompositeAuthoringAdapter] create: no owning child with a structure ` +
|
|
573
|
+
`provider for parent "${parentId ?? '(root)'}"`,
|
|
574
|
+
'authoring',
|
|
575
|
+
);
|
|
576
|
+
return '';
|
|
577
|
+
}
|
|
578
|
+
return target.child.adapter.structure.create(kind, target.forwardParentId);
|
|
579
|
+
},
|
|
580
|
+
remove: (id) => {
|
|
581
|
+
if (isGroupNodeId(id)) return; // a whole world isn't removable through this seam
|
|
582
|
+
const owner = this.findOwnerChild(id);
|
|
583
|
+
if (!owner?.adapter.structure) {
|
|
584
|
+
editorConsole.warn(
|
|
585
|
+
`[CompositeAuthoringAdapter] remove: no owner/structure for id "${id}"`,
|
|
586
|
+
'authoring',
|
|
587
|
+
);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// Propagate the owning child's return value (a react-world child
|
|
591
|
+
// returns an awaitable) so `deleteSelection` can still serialize a
|
|
592
|
+
// multi-delete THROUGH the composite the same way it does for a
|
|
593
|
+
// directly-active override adapter (see that adapter's own `remove`
|
|
594
|
+
// doc comment).
|
|
595
|
+
return owner.adapter.structure.remove(id);
|
|
596
|
+
},
|
|
597
|
+
duplicate: (id) => {
|
|
598
|
+
if (isGroupNodeId(id)) return id; // no-op — nothing to duplicate
|
|
599
|
+
const owner = this.findOwnerChild(id);
|
|
600
|
+
if (!owner?.adapter.structure) {
|
|
601
|
+
editorConsole.warn(
|
|
602
|
+
`[CompositeAuthoringAdapter] duplicate: no owner/structure for id "${id}"`,
|
|
603
|
+
'authoring',
|
|
604
|
+
);
|
|
605
|
+
return id;
|
|
606
|
+
}
|
|
607
|
+
return owner.adapter.structure.duplicate(id);
|
|
608
|
+
},
|
|
609
|
+
reparent: (id, newParentId) => {
|
|
610
|
+
const owner = this.findOwnerChild(id);
|
|
611
|
+
if (!owner?.adapter.structure) {
|
|
612
|
+
editorConsole.warn(
|
|
613
|
+
`[CompositeAuthoringAdapter] reparent: no owner/structure for id "${id}"`,
|
|
614
|
+
'authoring',
|
|
615
|
+
);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (newParentId === null || newParentId === groupNodeId(owner.worldId)) {
|
|
619
|
+
owner.adapter.structure.reparent(id, null); // -> a root of its own world
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const newOwner = this.findOwnerChild(newParentId);
|
|
623
|
+
if (!newOwner || newOwner.worldId !== owner.worldId) {
|
|
624
|
+
editorConsole.warn(
|
|
625
|
+
`[CompositeAuthoringAdapter] reparent: refusing to move "${id}" across worlds ` +
|
|
626
|
+
`(target parent "${newParentId}" is not in world "${owner.worldId}")`,
|
|
627
|
+
'authoring',
|
|
628
|
+
);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
owner.adapter.structure.reparent(id, newParentId);
|
|
632
|
+
},
|
|
633
|
+
reorder: (id, beforeSiblingId) => {
|
|
634
|
+
if (isGroupNodeId(id)) return;
|
|
635
|
+
const owner = this.findOwnerChild(id);
|
|
636
|
+
if (!owner) return;
|
|
637
|
+
// Cross-world guard: a non-null sibling anchor MUST belong to the same
|
|
638
|
+
// child as `id`. Otherwise (e.g. a drag onto another world's row whose
|
|
639
|
+
// reparent was already refused) the child maps the foreign sibling id to
|
|
640
|
+
// "append to end" and silently moves the entity within its own list on an
|
|
641
|
+
// operation the composite just rejected. A null anchor (move to end) is
|
|
642
|
+
// always valid.
|
|
643
|
+
if (beforeSiblingId !== null && this.findOwnerChild(beforeSiblingId) !== owner) return;
|
|
644
|
+
owner.adapter.structure?.reorder?.(id, beforeSiblingId);
|
|
645
|
+
},
|
|
646
|
+
creatableKinds: (parentId) => {
|
|
647
|
+
const target = this.resolveStructureTarget(parentId);
|
|
648
|
+
return (
|
|
649
|
+
target?.child.adapter.structure?.creatableKinds?.(target.forwardParentId ?? null) ?? []
|
|
650
|
+
);
|
|
651
|
+
},
|
|
652
|
+
// D3.a (spec 27 §6) — forward wrap/unwrap to the owning child, same
|
|
653
|
+
// owner-lookup + loud-warn-on-miss shape as `duplicate`/`remove` above
|
|
654
|
+
// (unlike `reorder`, which mirrors the base contract's OWN "absent means
|
|
655
|
+
// no UI" silent-no-op convention — wrap/unwrap follow the LOUD group
|
|
656
|
+
// instead since they're triggered from an explicit, always-visible menu
|
|
657
|
+
// item, same as duplicate/delete).
|
|
658
|
+
wrap: (id, wrapperTag) => {
|
|
659
|
+
if (isGroupNodeId(id)) return;
|
|
660
|
+
const owner = this.findOwnerChild(id);
|
|
661
|
+
if (!owner?.adapter.structure?.wrap) {
|
|
662
|
+
editorConsole.warn(
|
|
663
|
+
`[CompositeAuthoringAdapter] wrap: no owner/structure.wrap for id "${id}"`,
|
|
664
|
+
'authoring',
|
|
665
|
+
);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
owner.adapter.structure.wrap(id, wrapperTag);
|
|
669
|
+
},
|
|
670
|
+
unwrap: (id) => {
|
|
671
|
+
if (isGroupNodeId(id)) return;
|
|
672
|
+
const owner = this.findOwnerChild(id);
|
|
673
|
+
if (!owner?.adapter.structure?.unwrap) {
|
|
674
|
+
editorConsole.warn(
|
|
675
|
+
`[CompositeAuthoringAdapter] unwrap: no owner/structure.unwrap for id "${id}"`,
|
|
676
|
+
'authoring',
|
|
677
|
+
);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
owner.adapter.structure.unwrap(id);
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* A2 (map §5) — asset-drop pass-through. FIXES the pre-existing mis-route: a
|
|
686
|
+
* group-node id (`world:<w>`) used to fall through `route()`'s "unknown id ->
|
|
687
|
+
* first child" floor, so dropping an asset onto a world's OWN group row
|
|
688
|
+
* silently targeted whatever child happened to be FIRST in the array,
|
|
689
|
+
* regardless of which world's row was actually under the cursor. Translate
|
|
690
|
+
* the group-node id to that world's child directly, forwarding `''` (the
|
|
691
|
+
* shell/`VgaiSceneAuthoringAdapter` root-drop sentinel — see that adapter's
|
|
692
|
+
* `assetDrop` doc comment) as the node id, since the synthetic `world:<w>`
|
|
693
|
+
* string means nothing to the child's own hierarchy.
|
|
694
|
+
*/
|
|
695
|
+
private resolveAssetDropTarget(nodeId: string): { adapter: AuthoringAdapter; forwardId: string } {
|
|
696
|
+
if (isGroupNodeId(nodeId)) {
|
|
697
|
+
const worldId = nodeId.slice(GROUP_PREFIX.length);
|
|
698
|
+
const child = this.children.find((c) => c.worldId === worldId);
|
|
699
|
+
return child
|
|
700
|
+
? { adapter: child.adapter, forwardId: '' }
|
|
701
|
+
: { adapter: this.children[0]!.adapter, forwardId: '' };
|
|
702
|
+
}
|
|
703
|
+
const owner = this.findOwnerChild(nodeId);
|
|
704
|
+
// Unknown real id: same "first child" floor `route()` uses elsewhere, forwarding
|
|
705
|
+
// the id unchanged (parity with the pre-existing bogus-id behavior).
|
|
706
|
+
return owner
|
|
707
|
+
? { adapter: owner.adapter, forwardId: nodeId }
|
|
708
|
+
: { adapter: this.children[0]!.adapter, forwardId: nodeId };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* D4 (B2) — resolves a `stories` call THE SAME WAY `resolveAssetDropTarget`
|
|
713
|
+
* (above) resolves an asset drop: a group-node id (`world:<w>`) used to
|
|
714
|
+
* fall through `route()`'s "unknown id -> first child" floor, so
|
|
715
|
+
* `storiesFor('world:hud')` silently asked whatever child happened to be
|
|
716
|
+
* FIRST in the array (never the actual owner), meaning the story picker
|
|
717
|
+
* could NEVER appear on the world's own group row — the one place B2's
|
|
718
|
+
* shell (`Inspector.tsx`) actually calls it. Translated to `<w>`'s own
|
|
719
|
+
* child adapter. Unlike the asset-drop translation, the node id is
|
|
720
|
+
* forwarded UNCHANGED (not blanked to `''`) — B2's `ReactWorldAuthoringAdapter.stories`
|
|
721
|
+
* is world-level and ignores `nodeId` entirely (see that class's doc
|
|
722
|
+
* comment), but a future per-node catalog (B3) will want the real id, so
|
|
723
|
+
* there is no sentinel to invent here.
|
|
724
|
+
*/
|
|
725
|
+
private resolveStoriesTarget(nodeId: string): { adapter: AuthoringAdapter; forwardId: string } {
|
|
726
|
+
if (isGroupNodeId(nodeId)) {
|
|
727
|
+
const worldId = nodeId.slice(GROUP_PREFIX.length);
|
|
728
|
+
const child = this.children.find((c) => c.worldId === worldId);
|
|
729
|
+
return child
|
|
730
|
+
? { adapter: child.adapter, forwardId: nodeId }
|
|
731
|
+
: { adapter: this.children[0]!.adapter, forwardId: nodeId };
|
|
732
|
+
}
|
|
733
|
+
const owner = this.findOwnerChild(nodeId);
|
|
734
|
+
return owner
|
|
735
|
+
? { adapter: owner.adapter, forwardId: nodeId }
|
|
736
|
+
: { adapter: this.children[0]!.adapter, forwardId: nodeId };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* D8 — union of every child's own {@link RootGroup}s, each group id prefixed
|
|
741
|
+
* `<worldId>:` so groups from different worlds never collide. `activate`
|
|
742
|
+
* strips the prefix back off to find the owning child and the child's own
|
|
743
|
+
* (unprefixed) group id.
|
|
744
|
+
*/
|
|
745
|
+
readonly rootGroups: RootGroupsProvider = {
|
|
746
|
+
groups: (): RootGroup[] =>
|
|
747
|
+
this.children.flatMap(
|
|
748
|
+
(c) =>
|
|
749
|
+
c.adapter.rootGroups?.groups().map((g) => ({ ...g, id: `${c.worldId}:${g.id}` })) ?? [],
|
|
750
|
+
),
|
|
751
|
+
activate: (groupId, rootId) => {
|
|
752
|
+
const sep = groupId.indexOf(':');
|
|
753
|
+
if (sep < 0) return undefined;
|
|
754
|
+
const worldId = groupId.slice(0, sep);
|
|
755
|
+
const childGroupId = groupId.slice(sep + 1);
|
|
756
|
+
const child = this.children.find((c) => c.worldId === worldId);
|
|
757
|
+
return child?.adapter.rootGroups?.activate(childGroupId, rootId);
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
/** First NON-NULL child answer, in child order. */
|
|
762
|
+
readonly files: FileMapProvider = {
|
|
763
|
+
rootForFile: (path) => {
|
|
764
|
+
for (const child of this.children) {
|
|
765
|
+
const id = child.adapter.files?.rootForFile(path);
|
|
766
|
+
if (id) return id;
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
},
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
/** D4 — storybook stories, routed by node ownership (group-node ids
|
|
773
|
+
* translated — see `resolveStoriesTarget` above, the same fix
|
|
774
|
+
* `resolveAssetDropTarget` applied for asset drops). A node whose owning
|
|
775
|
+
* adapter has no `stories` provider reports/does nothing (empty list, null
|
|
776
|
+
* active, no-op apply/isolate) rather than throwing. */
|
|
777
|
+
readonly stories: StoriesProvider = {
|
|
778
|
+
storiesFor: (nodeId): StoryRef[] => {
|
|
779
|
+
const { adapter, forwardId } = this.resolveStoriesTarget(nodeId);
|
|
780
|
+
return adapter.stories?.storiesFor(forwardId) ?? [];
|
|
781
|
+
},
|
|
782
|
+
active: (nodeId) => {
|
|
783
|
+
const { adapter, forwardId } = this.resolveStoriesTarget(nodeId);
|
|
784
|
+
return adapter.stories?.active(forwardId) ?? null;
|
|
785
|
+
},
|
|
786
|
+
apply: (nodeId, storyId) => {
|
|
787
|
+
const { adapter, forwardId } = this.resolveStoriesTarget(nodeId);
|
|
788
|
+
adapter.stories?.apply(forwardId, storyId);
|
|
789
|
+
},
|
|
790
|
+
isolate: (nodeId, storyId) => {
|
|
791
|
+
// A null nodeId (exit isolation) has no owner to route by — forward the
|
|
792
|
+
// exit to every child that implements `isolate` so nothing is left stuck
|
|
793
|
+
// in an isolated state.
|
|
794
|
+
if (nodeId === null) {
|
|
795
|
+
for (const child of this.children) child.adapter.stories?.isolate?.(null, storyId);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const { adapter, forwardId } = this.resolveStoriesTarget(nodeId);
|
|
799
|
+
adapter.stories?.isolate?.(forwardId, storyId);
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
/** D6 (S-C) — GameComponent management, routed by node ownership. */
|
|
804
|
+
readonly components: ComponentsProvider = {
|
|
805
|
+
available: (nodeId): string[] => this.route(nodeId).components?.available(nodeId) ?? [],
|
|
806
|
+
list: (nodeId): { type: string }[] => this.route(nodeId).components?.list(nodeId) ?? [],
|
|
807
|
+
add: (nodeId, type) => this.route(nodeId).components?.add(nodeId, type),
|
|
808
|
+
remove: (nodeId, type) => this.route(nodeId).components?.remove(nodeId, type),
|
|
809
|
+
config: (nodeId, type) => this.route(nodeId).components?.config?.(nodeId, type) ?? null,
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
/** Asset drop, routed by node ownership (group-node ids translated — see
|
|
813
|
+
* `resolveAssetDropTarget` above). */
|
|
814
|
+
readonly assetDrop: AssetDropProvider = {
|
|
815
|
+
accepts: (nodeId, assetPath) => {
|
|
816
|
+
const target = this.resolveAssetDropTarget(nodeId);
|
|
817
|
+
return target.adapter.assetDrop?.accepts(target.forwardId, assetPath) ?? false;
|
|
818
|
+
},
|
|
819
|
+
drop: (nodeId, assetPath) => {
|
|
820
|
+
const target = this.resolveAssetDropTarget(nodeId);
|
|
821
|
+
target.adapter.assetDrop?.drop(target.forwardId, assetPath);
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// NOTE: `pickable` is deliberately NOT implemented here — layered viewport
|
|
826
|
+
// picking (D12) across children is shell policy (B4), not something this
|
|
827
|
+
// composite merges. Callers needing per-layer pick should go through
|
|
828
|
+
// `childAdapters()` and pick each child's own `pickable` themselves.
|
|
829
|
+
|
|
830
|
+
subscribe(listener: () => void): () => void {
|
|
831
|
+
const unsubs = this.children
|
|
832
|
+
.map((c) => c.adapter.subscribe?.(listener))
|
|
833
|
+
.filter((u): u is () => void => !!u);
|
|
834
|
+
return () => {
|
|
835
|
+
for (const u of unsubs) u();
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Persistence (T3.2 slice 3, generalized 2→N unchanged in semantics): `isDirty`
|
|
841
|
+
* = OR of children that actually have a provider (a child without one, e.g. an
|
|
842
|
+
* embed-only adapter, is simply skipped — it has nothing to be dirty about);
|
|
843
|
+
* `save()` saves the dirty children SEQUENTIALLY, logging loudly per-child on
|
|
844
|
+
* failure without aborting the rest; `destination` joins child destinations
|
|
845
|
+
* with `' + '`.
|
|
846
|
+
*
|
|
847
|
+
* No own undo: the global timeline (`undo-timeline.ts`) already dispatches
|
|
848
|
+
* per-adapter, and each child that supports undo registers ITSELF — wrapping
|
|
849
|
+
* one in a Composite changes nothing about that registration.
|
|
850
|
+
*/
|
|
851
|
+
get persistence(): PersistenceProvider {
|
|
852
|
+
// Always a REAL (never `undefined`) provider — see composite-authoring-adapter's
|
|
853
|
+
// 2-child precedent / `ephemeral-persistence.ts` for the same pattern.
|
|
854
|
+
const persistable = this.children.filter(
|
|
855
|
+
(
|
|
856
|
+
c,
|
|
857
|
+
): c is CompositeChild & {
|
|
858
|
+
adapter: AuthoringAdapter & { persistence: PersistenceProvider };
|
|
859
|
+
} => c.adapter.capabilities.persist && !!c.adapter.persistence,
|
|
860
|
+
);
|
|
861
|
+
return {
|
|
862
|
+
isDirty: () => persistable.some((c) => c.adapter.persistence.isDirty()),
|
|
863
|
+
save: async () => {
|
|
864
|
+
for (const c of persistable) {
|
|
865
|
+
const p = c.adapter.persistence;
|
|
866
|
+
if (!p.isDirty()) continue;
|
|
867
|
+
try {
|
|
868
|
+
await p.save();
|
|
869
|
+
} catch (err) {
|
|
870
|
+
console.error(
|
|
871
|
+
`[CompositeAuthoringAdapter] save failed for world "${c.worldId}" ` +
|
|
872
|
+
`(destination: ${p.destination}) — continuing with remaining worlds:`,
|
|
873
|
+
err,
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
// A labeled record keyed by world id — the N-ary generalization of the
|
|
879
|
+
// 2-child `{world3d, world2d}` shape (§4/§6.5); report if a different
|
|
880
|
+
// shape (e.g. a flat merge) is actually wanted.
|
|
881
|
+
serialize: () =>
|
|
882
|
+
Object.fromEntries(
|
|
883
|
+
this.children.map((c) => [c.worldId, c.adapter.persistence?.serialize()]),
|
|
884
|
+
),
|
|
885
|
+
destination:
|
|
886
|
+
persistable.length > 0
|
|
887
|
+
? persistable.map((c) => c.adapter.persistence.destination).join(' + ')
|
|
888
|
+
: '(no persistable child)',
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
}
|