@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,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UIAuthoringAdapter — drives the UI editor through the SAME format-neutral
|
|
3
|
+
* {@link AuthoringAdapter} contract the 3D editor uses (K6). It wraps a
|
|
4
|
+
* {@link UIEditStore} (the .vscn `ui` tree) and exposes hierarchy / selection /
|
|
5
|
+
* inspector / LAYOUT (K7, NOT transform — UI has no 3D gizmo) / structure /
|
|
6
|
+
* persistence / undo. `object3D` is always null (UI nodes are DOM, not Object3D);
|
|
7
|
+
* the editor's UI selection surface is the DOM overlay, parallel to the 3D viewport
|
|
8
|
+
* (architectural_decision §C).
|
|
9
|
+
*
|
|
10
|
+
* It is registered via `setActiveAuthoring` directly by the standalone harness
|
|
11
|
+
* (`ui-editor/harness.tsx`), and — as a `kind: 'scene-ui'` child of the edit-mode
|
|
12
|
+
* `CompositeAuthoringAdapter` — by `authoring/edit-mode-authoring.ts` (C3,
|
|
13
|
+
* docs/unified-world-editor/26-unified-editor-spec.md §4 C3): the panel-swap into
|
|
14
|
+
* the main editor this doc comment used to call "the remaining integration" has
|
|
15
|
+
* landed — a UI node is now a layer node under its owning world in the ONE
|
|
16
|
+
* hierarchy, and this adapter's `pickable` (below) drives B4 click-select for it.
|
|
17
|
+
*/
|
|
18
|
+
import type {
|
|
19
|
+
AuthoringAdapter,
|
|
20
|
+
AuthoringCapabilities,
|
|
21
|
+
EditorNode,
|
|
22
|
+
HierarchyProvider,
|
|
23
|
+
InspectorProvider,
|
|
24
|
+
LayoutProvider,
|
|
25
|
+
PersistenceProvider,
|
|
26
|
+
PickProvider,
|
|
27
|
+
PropertyDescriptor,
|
|
28
|
+
SelectionProvider,
|
|
29
|
+
StructureProvider,
|
|
30
|
+
} from '@engine/adapter';
|
|
31
|
+
import type { UINode } from '@engine/scene/schema';
|
|
32
|
+
import { UIAnimationController } from '@vgai/scene-ui/animation';
|
|
33
|
+
import { findNode, getNodePath, walk } from '../ui-editor/node-ops';
|
|
34
|
+
import { hitTest } from '../ui-editor/react-store';
|
|
35
|
+
import type { UIEditStore } from '../ui-editor/ui-edit-store';
|
|
36
|
+
import type { SourceWriteBackend } from '../ui-source/source-write-backend';
|
|
37
|
+
|
|
38
|
+
/** C3 — the shell's generic "Create ▸" kind list (`structure.creatableKinds`
|
|
39
|
+
* below): every kind `defaultNodeForKind` (`ui-edit-store.ts`) seeds a
|
|
40
|
+
* sensible default for. Containers (`box`/`panel`/`grid`) can themselves be
|
|
41
|
+
* a `create` target; kept intentionally smaller than the harness palette's
|
|
42
|
+
* full set (`ui-editor/palette.tsx`) — that richer picker stays harness-only. */
|
|
43
|
+
const CREATABLE_KINDS: ReadonlyArray<{ kind: string; label: string }> = [
|
|
44
|
+
{ kind: 'box', label: 'Box' },
|
|
45
|
+
{ kind: 'panel', label: 'Panel' },
|
|
46
|
+
{ kind: 'grid', label: 'Grid' },
|
|
47
|
+
{ kind: 'text', label: 'Text' },
|
|
48
|
+
{ kind: 'button', label: 'Button' },
|
|
49
|
+
{ kind: 'image', label: 'Image' },
|
|
50
|
+
{ kind: 'input', label: 'Input' },
|
|
51
|
+
{ kind: 'toggle', label: 'Toggle' },
|
|
52
|
+
{ kind: 'slider', label: 'Slider' },
|
|
53
|
+
{ kind: 'progress', label: 'Progress' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function toEditorNode(node: UINode, parentId: string | null): EditorNode {
|
|
57
|
+
return {
|
|
58
|
+
id: node.id as string,
|
|
59
|
+
label: node.name,
|
|
60
|
+
kind: node.kind,
|
|
61
|
+
parentId,
|
|
62
|
+
childIds: (node.children ?? []).map((c) => c.id as string),
|
|
63
|
+
flags: { locked: node.locked ?? false },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class UIAuthoringAdapter implements AuthoringAdapter {
|
|
68
|
+
readonly capabilities: AuthoringCapabilities;
|
|
69
|
+
/**
|
|
70
|
+
* T3.2 slice 3: the source-write flush seam (design §4). Absent ⇒ no dev-server
|
|
71
|
+
* backend in this session (a hosted/browser build) — see `../ui-source/source-write-backend.ts`.
|
|
72
|
+
*
|
|
73
|
+
* NOT folded into `capabilities.persist` (deliberately — reported as a premise
|
|
74
|
+
* mismatch in the T3.2 slice 3 report): `EditorStore._activePersistence()` gates
|
|
75
|
+
* ALL autosave for whichever adapter is active on `capabilities.persist`,
|
|
76
|
+
* including the SEPARATE, already-real ui-tree persistence path (`onCommit` →
|
|
77
|
+
* `EditorStore.setSceneUI` → the normal `.vscn` autosave, unconditional today).
|
|
78
|
+
* Flipping `capabilities.persist` off when no source-write backend exists would
|
|
79
|
+
* silently disable THAT working path too, in every hosted/no-dev-server build —
|
|
80
|
+
* not the intended effect. `capabilities.persist` therefore stays `true`
|
|
81
|
+
* (matching pre-slice-3 behavior); backend absence is instead surfaced honestly
|
|
82
|
+
* through `persistence.destination` below.
|
|
83
|
+
*/
|
|
84
|
+
private readonly writeBackend: SourceWriteBackend | undefined;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* C3 (D12/B4) — the live viewport DOM element to hit-test against,
|
|
88
|
+
* attached once the unified editor's scene-UI design-time layer mounts
|
|
89
|
+
* (`authoring/scene-ui-layer.ts`) and detached on its teardown. `null` in
|
|
90
|
+
* the standalone harness (which never attaches one) and briefly between
|
|
91
|
+
* composite-install and layer-mount — `pickable.pick` degrades to `null`
|
|
92
|
+
* either way, never throws.
|
|
93
|
+
*/
|
|
94
|
+
private _pickSurface: HTMLElement | null = null;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* D4 (spec27 §6 D4, layer-tree "lock" toggle) — SESSION-LOCAL, never
|
|
98
|
+
* persisted: `UINode` dropped its `locked` schema field as a dead field
|
|
99
|
+
* with no runtime reader (CLAUDE.md, `ui.ts` — same fate as `tags`), so a
|
|
100
|
+
* scene-UI node's lock state can't round-trip through `.vscn` either. See
|
|
101
|
+
* `ReactWorldAuthoringAdapter.lockedIds`'s doc comment for the identical
|
|
102
|
+
* reasoning. D4.R1 — now also read by `pickable.pick` below, so a locked
|
|
103
|
+
* node is skipped by canvas click/marquee pick, matching the first-party
|
|
104
|
+
* threejs raycast's own locked-skip behavior (`viewport-raycast.ts`).
|
|
105
|
+
*/
|
|
106
|
+
private readonly lockedIds = new Set<string>();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* E1.b (spec-27 §7 E1) — the ONE `UIAnimationController` shared by every
|
|
110
|
+
* consumer of this child's timeline: the design-time stage
|
|
111
|
+
* (`scene-ui-layer.ts`'s `SceneUILayerRoot`, live preview) AND the unified
|
|
112
|
+
* editor's keyframe timeline panel (`scene-ui-mode/scene-ui-timeline-tab.tsx`,
|
|
113
|
+
* playback/scrub controls). Owned here (not `useMemo`'d separately in each
|
|
114
|
+
* mount) so scrubbing the timeline moves the SAME preview the stage renders
|
|
115
|
+
* — the harness's `UIEditor.tsx` got this for free by constructing both in
|
|
116
|
+
* one component; the unified editor mounts the stage and the timeline as
|
|
117
|
+
* two independent React roots, so the shared instance has to live on the
|
|
118
|
+
* adapter both already hold a reference to.
|
|
119
|
+
*/
|
|
120
|
+
readonly anim = new UIAnimationController();
|
|
121
|
+
|
|
122
|
+
constructor(
|
|
123
|
+
private readonly store: UIEditStore,
|
|
124
|
+
opts: { writeBackend?: SourceWriteBackend } = {},
|
|
125
|
+
) {
|
|
126
|
+
this.writeBackend = opts.writeBackend;
|
|
127
|
+
this.capabilities = {
|
|
128
|
+
transform: false,
|
|
129
|
+
material: false,
|
|
130
|
+
inspectorFields: true,
|
|
131
|
+
create: true,
|
|
132
|
+
delete: true,
|
|
133
|
+
reparent: true,
|
|
134
|
+
persist: true,
|
|
135
|
+
layout: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** C3 (D12) — geometric hit-test over the attached pick surface, reusing
|
|
140
|
+
* the SAME `hitTest` primitive the harness's `SelectionOverlay` already
|
|
141
|
+
* uses for in-realm hover/click (`ui-editor/react-store.ts`) — the B4-
|
|
142
|
+
* blessed geometric `data-ui-id` rect test, not `elementFromPoint`.
|
|
143
|
+
*
|
|
144
|
+
* D4.R1 — a `lockedIds` member is SKIPPED by the hit-test (falls through
|
|
145
|
+
* to whatever unlocked element is behind it, or `null`), closing the gap
|
|
146
|
+
* where the layer-tree/overlay lock toggle gated undo/round-trip but
|
|
147
|
+
* nothing on the pick path — see `lockedIds`'s doc comment. */
|
|
148
|
+
readonly pickable: PickProvider = {
|
|
149
|
+
pick: (clientX, clientY) =>
|
|
150
|
+
this._pickSurface
|
|
151
|
+
? hitTest(this._pickSurface, clientX, clientY, (id) => this.lockedIds.has(id))
|
|
152
|
+
: null,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Attach/detach the live stage element `pickable.pick` hit-tests against
|
|
156
|
+
* (`scene-ui-layer.ts`'s mount/dispose). */
|
|
157
|
+
attachPickSurface(el: HTMLElement): void {
|
|
158
|
+
this._pickSurface = el;
|
|
159
|
+
}
|
|
160
|
+
detachPickSurface(): void {
|
|
161
|
+
this._pickSurface = null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- hierarchy ---
|
|
165
|
+
readonly hierarchy: HierarchyProvider = {
|
|
166
|
+
roots: () => this.store.roots.map((r) => toEditorNode(r, null)),
|
|
167
|
+
node: (id) => {
|
|
168
|
+
const loc = findNode(this.store.roots, id);
|
|
169
|
+
return loc ? toEditorNode(loc.node, loc.parent?.id ?? null) : null;
|
|
170
|
+
},
|
|
171
|
+
object3D: () => null, // UI nodes are DOM, not Object3D
|
|
172
|
+
idForObject3D: () => null,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// --- selection ---
|
|
176
|
+
readonly selection: SelectionProvider = {
|
|
177
|
+
get: () => this.store.selection,
|
|
178
|
+
set: (ids) => this.store.select(ids),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// --- inspector (schema-driven property descriptors) ---
|
|
182
|
+
readonly inspector: InspectorProvider = {
|
|
183
|
+
properties: (id) => this.describe(id),
|
|
184
|
+
get: (id, path) => {
|
|
185
|
+
// D4 — see `lockedIds`'s doc comment; `visible` is a real `UINode`
|
|
186
|
+
// field already reached generically via `getNodePath` below, so it
|
|
187
|
+
// needs no special case here.
|
|
188
|
+
if (path === 'locked') return this.lockedIds.has(id);
|
|
189
|
+
const loc = findNode(this.store.roots, id);
|
|
190
|
+
return loc ? getNodePath(loc.node, path) : undefined;
|
|
191
|
+
},
|
|
192
|
+
set: (id, path, value) => {
|
|
193
|
+
if (path === 'locked') {
|
|
194
|
+
if (value) this.lockedIds.add(id);
|
|
195
|
+
else this.lockedIds.delete(id);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.store.setProp(id, path, value);
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// --- layout (K7) ---
|
|
203
|
+
readonly layout: LayoutProvider = {
|
|
204
|
+
get: (id) => {
|
|
205
|
+
const loc = findNode(this.store.roots, id);
|
|
206
|
+
return (loc?.node.layout as Record<string, unknown> | undefined) ?? null;
|
|
207
|
+
},
|
|
208
|
+
beginEdit: () => this.store.beginEdit(),
|
|
209
|
+
apply: (id, patch) => {
|
|
210
|
+
for (const [k, v] of Object.entries(patch)) this.store.previewSetProp(id, `layout.${k}`, v);
|
|
211
|
+
},
|
|
212
|
+
endEdit: () => this.store.endEdit(),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// --- structure ---
|
|
216
|
+
readonly structure: StructureProvider = {
|
|
217
|
+
create: (kind, parentId) =>
|
|
218
|
+
this.store.addNode(parentId ?? (this.store.roots[0]?.id as string), kind as UINode['kind']),
|
|
219
|
+
remove: (id) => this.store.remove(id),
|
|
220
|
+
// D4.R2 — batch a multi-node delete into the store's ONE-commit
|
|
221
|
+
// `removeMany` (a single undo step for the whole selection) instead of
|
|
222
|
+
// this shell looping `remove(id)` N times (N undo steps). Cheap here:
|
|
223
|
+
// `UIEditStore`'s undo is an in-memory JSON-tree snapshot per commit, no
|
|
224
|
+
// async/network round trip to sequence — unlike the react-world
|
|
225
|
+
// adapter's whole-file source writes, there's no race to guard against.
|
|
226
|
+
removeMany: (ids) => this.store.removeMany(ids),
|
|
227
|
+
duplicate: (id) => this.store.duplicate(id) ?? id,
|
|
228
|
+
reparent: (id, newParentId) => {
|
|
229
|
+
if (newParentId) this.store.reparent(id, newParentId);
|
|
230
|
+
},
|
|
231
|
+
// C3 — drives the shell's generic "Create ▸" hierarchy-menu submenu
|
|
232
|
+
// (`GameHierarchy.tsx`), the SAME contract-only affordance every other
|
|
233
|
+
// adapter's rows already get; the richer drag/drop palette (K1) stays
|
|
234
|
+
// harness-only (`ui-editor/palette.tsx`, spec-27 E1 scope). Every kind
|
|
235
|
+
// `defaultNodeForKind` (`ui-edit-store.ts`) actually knows how to seed.
|
|
236
|
+
creatableKinds: () => [...CREATABLE_KINDS],
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// --- persistence ---
|
|
240
|
+
// De-stubbed (T3.2 slice 3, design §4) over the `SourceWriteBackend` seam. A class
|
|
241
|
+
// GETTER, not a field initializer — a field initializer runs before the
|
|
242
|
+
// constructor body assigns `this.writeBackend` (the slice-1 ordering bug; see
|
|
243
|
+
// `vgai-scene-authoring-adapter.ts` for the pattern).
|
|
244
|
+
//
|
|
245
|
+
// What "de-stubbed" means here, concretely: `SourceEditPanel` writes every source
|
|
246
|
+
// edit to disk IMMEDIATELY via fetch the instant it is committed (Enter/blur) —
|
|
247
|
+
// there is NO pending-write queue anywhere in this architecture. `save()`
|
|
248
|
+
// therefore has nothing to flush, backend or not; its honest implementation is a
|
|
249
|
+
// no-op (see `../ui-source/source-write-backend.ts`'s doc comment for the
|
|
250
|
+
// investigation). The real de-stub delivered here is `destination` truthfully
|
|
251
|
+
// reporting whether a backend even EXISTS in this session, instead of the old
|
|
252
|
+
// hard-coded "save() not yet wired" stub text.
|
|
253
|
+
get persistence(): PersistenceProvider {
|
|
254
|
+
const backend = this.writeBackend;
|
|
255
|
+
const store = this.store;
|
|
256
|
+
return {
|
|
257
|
+
isDirty: () => store.canUndo,
|
|
258
|
+
save: async () => {
|
|
259
|
+
// Nothing pending to flush (immediate-write architecture) — see doc
|
|
260
|
+
// comment above. The ui-tree's OWN persistence (layout/prop edits → the
|
|
261
|
+
// open scene) is a separate, unconditional path (`onCommit` →
|
|
262
|
+
// `EditorStore.setSceneUI`) that does not route through this method.
|
|
263
|
+
},
|
|
264
|
+
serialize: () => store.serialize(),
|
|
265
|
+
destination: backend
|
|
266
|
+
? 'component source (JSX, via /__ui-source — writes are immediate; nothing pending to flush)'
|
|
267
|
+
: 'component source (JSX) — no source-write backend in this session (hosted/no dev server)',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
undo(): void {
|
|
272
|
+
this.store.undo();
|
|
273
|
+
}
|
|
274
|
+
redo(): void {
|
|
275
|
+
this.store.redo();
|
|
276
|
+
}
|
|
277
|
+
subscribe(listener: () => void): () => void {
|
|
278
|
+
return this.store.subscribe(listener);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Build property descriptors for a node (inspector schema). */
|
|
282
|
+
private describe(id: string): PropertyDescriptor[] {
|
|
283
|
+
const loc = findNode(this.store.roots, id);
|
|
284
|
+
if (!loc) return [];
|
|
285
|
+
const node = loc.node;
|
|
286
|
+
const out: PropertyDescriptor[] = [
|
|
287
|
+
{ path: 'name', label: 'name', type: 'string' },
|
|
288
|
+
{ path: 'visible', label: 'visible', type: 'boolean' },
|
|
289
|
+
{ path: 'layout.width', label: 'width', type: 'number' },
|
|
290
|
+
{ path: 'layout.height', label: 'height', type: 'number' },
|
|
291
|
+
{ path: 'layout.gap', label: 'gap', type: 'number' },
|
|
292
|
+
{ path: 'style.backgroundColor', label: 'background', type: 'color' },
|
|
293
|
+
{ path: 'style.color', label: 'color', type: 'color' },
|
|
294
|
+
{ path: 'style.fontSize', label: 'fontSize', type: 'number' },
|
|
295
|
+
];
|
|
296
|
+
for (const k of Object.keys(node.props ?? {})) {
|
|
297
|
+
out.push({ path: `props.${k}`, label: k, type: 'json' });
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Every node id (for select-all). */
|
|
303
|
+
allIds(): string[] {
|
|
304
|
+
const ids: string[] = [];
|
|
305
|
+
walk(this.store.roots, (n) => ids.push(n.id as string));
|
|
306
|
+
return ids;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* C2 (docs/unified-world-editor/26-unified-editor-spec.md §4 C2) — the
|
|
311
|
+
* wrapped {@link UIEditStore}, exposed for this adapter's OWN inspector-
|
|
312
|
+
* section registrations (`authoring/ui-inspector-sections.tsx`) to read/
|
|
313
|
+
* write UI-tree state the narrow `AuthoringAdapter` contract can't express
|
|
314
|
+
* (multi-select batch writes via `setMany`, K9 mixed-value detection) — the
|
|
315
|
+
* same "adapter-owned module privilege" the first-party threejs sections
|
|
316
|
+
* use via `EditorContext`'s `useEditorStore()` (there is no equivalent
|
|
317
|
+
* React context here — a bare `UIAuthoringAdapter` is never wrapped in one).
|
|
318
|
+
* NOT part of the `AuthoringAdapter` interface — a
|
|
319
|
+
* concrete-class-only escape hatch, consumed only by that registration
|
|
320
|
+
* module (which already does `instanceof UIAuthoringAdapter` to match).
|
|
321
|
+
*/
|
|
322
|
+
get uiStore(): UIEditStore {
|
|
323
|
+
return this.store;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Add UI Canvas" hierarchy context-menu item (C3, docs/unified-world-editor/
|
|
3
|
+
* 26-unified-editor-spec.md §4 C3) — the first-canvas affordance that
|
|
4
|
+
* replaces the retired standalone UI-edit mode's demo-absent
|
|
5
|
+
* `defaultCanvasRoot()` seeding (no shim — Q3). Shown on a first-party
|
|
6
|
+
* scene-ROOT row while the open scene has no `ui` roots yet; seeds ONE empty
|
|
7
|
+
* canvas (design/24-scene-ui.md D10 — the same schema-minimal empty canvas
|
|
8
|
+
* project mode already seeds) and asks `ViewportPanel.tsx` to rebuild the
|
|
9
|
+
* edit-mode composite so the new `scene-ui` child appears (§3.1's install
|
|
10
|
+
* gate — `getSceneUI().length > 0` — flips the instant this commits).
|
|
11
|
+
*
|
|
12
|
+
* Registered through `hierarchy-menu-registry.ts`'s permanent adapter-module
|
|
13
|
+
* escape hatch (C2), the same seam `first-party-hierarchy-menu.ts` uses.
|
|
14
|
+
* Imported once for its side effect at boot (`main.tsx`) — NOT loaded by the
|
|
15
|
+
* standalone harness (`ui-editor/harness.tsx`), which has no `EditorStore`
|
|
16
|
+
* to seed and no composite to rebuild.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { registerHierarchyMenuItems } from '../hierarchy-menu-registry';
|
|
20
|
+
import { defaultCanvasRoot } from '../ui-editor/ui-edit-store';
|
|
21
|
+
import { EDIT_MODE_REBUILD_EVENT } from './edit-mode-authoring';
|
|
22
|
+
|
|
23
|
+
registerHierarchyMenuItems(
|
|
24
|
+
(ctx) => {
|
|
25
|
+
if (ctx.store.getSceneUI().length > 0) return false;
|
|
26
|
+
// A first-party scene ROOT row only (not every entity row) — one of the
|
|
27
|
+
// open document's own top-level entities (`store.entities` is the
|
|
28
|
+
// top-level `SceneEntity[]` tree, `EditorStore`'s own doc comment).
|
|
29
|
+
return ctx.store.entities.some((e) => e.id === ctx.nodeId);
|
|
30
|
+
},
|
|
31
|
+
(ctx) => [
|
|
32
|
+
{
|
|
33
|
+
label: 'Add UI Canvas',
|
|
34
|
+
action: () => {
|
|
35
|
+
ctx.store.setSceneUI([defaultCanvasRoot()]);
|
|
36
|
+
window.dispatchEvent(new CustomEvent(EDIT_MODE_REBUILD_EVENT));
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI adapter inspector-section registrations (C2/C3, docs/unified-world-
|
|
3
|
+
* editor/26-unified-editor-spec.md §4 C2/C3) — the UI adapter module's
|
|
4
|
+
* contribution to the shared `inspector-section-registry.ts` seam, the
|
|
5
|
+
* sibling of `authoring/first-party-inspector/register.tsx`. Wraps `ui-editor/
|
|
6
|
+
* inspector.tsx`'s extracted `LayoutSection`/`StyleSection`/`PropsSection`/
|
|
7
|
+
* `EventsSection` (moved out of the monolithic `UIInspector` body, testids
|
|
8
|
+
* unchanged) so `UIInspector` itself becomes a thin shell that renders
|
|
9
|
+
* whatever matches via `matchedInspectorSections` (see that file's own doc
|
|
10
|
+
* comment).
|
|
11
|
+
*
|
|
12
|
+
* Matched for a real UI node (`node !== null`) reachable through a
|
|
13
|
+
* `UIAuthoringAdapter` — {@link resolveUIAdapter} below resolves that EITHER
|
|
14
|
+
* WAY the adapter can present it: the ACTIVE adapter is the bare
|
|
15
|
+
* `UIAuthoringAdapter` itself (the pre-C3 `setActiveAuthoring(adapter)` shape
|
|
16
|
+
* — no live construction site remains post-E2's harness retirement, but the
|
|
17
|
+
* branch stays as the defensive fallback), OR — since C3 — the edit-mode
|
|
18
|
+
* `CompositeAuthoringAdapter`, whose `ownerOf(node.id)` names the UI CHILD
|
|
19
|
+
* that actually owns the node (mirrors `first-party-inspector/
|
|
20
|
+
* is-first-party-selection.ts`'s identical composite-resolution precedent).
|
|
21
|
+
* The naive pre-C3 `adapter instanceof UIAuthoringAdapter` check ALWAYS
|
|
22
|
+
* fails once the composite is installed (`Inspector.tsx` passes the
|
|
23
|
+
* COMPOSITE, never the child, as its `adapter` prop) — this is the fix.
|
|
24
|
+
*
|
|
25
|
+
* Props/Events additionally require a SINGLE selection, mirroring the
|
|
26
|
+
* pre-C2 `!multi` gate in `UIInspector`. The Layout registration carries
|
|
27
|
+
* `suppresses`/`panelName` (C3): under the ONE shell (`Inspector.tsx`), the
|
|
28
|
+
* generic `UIAuthoringAdapter.inspector.properties()` grid would otherwise
|
|
29
|
+
* render NEXT TO this richer Layout/Style pair — the bare-adapter path
|
|
30
|
+
* (`UIInspector`, which has no such built-ins) ignores both.
|
|
31
|
+
*
|
|
32
|
+
* Imported once for its side effect, by `main.tsx` (so `UIInspector`
|
|
33
|
+
* renders correctly for the unified editor's scene-UI child, C3).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { AuthoringAdapter, EditorNode } from '@engine/adapter';
|
|
37
|
+
import type { UINode } from '@engine/scene/schema';
|
|
38
|
+
import {
|
|
39
|
+
type InspectorSectionProps,
|
|
40
|
+
registerInspectorSections,
|
|
41
|
+
} from '../inspector-section-registry';
|
|
42
|
+
import {
|
|
43
|
+
EventsSection,
|
|
44
|
+
LayoutSection,
|
|
45
|
+
mixedValue,
|
|
46
|
+
PropsSection,
|
|
47
|
+
StyleSection,
|
|
48
|
+
} from '../ui-editor/inspector';
|
|
49
|
+
import { findNode } from '../ui-editor/node-ops';
|
|
50
|
+
import type { UIEditStore } from '../ui-editor/ui-edit-store';
|
|
51
|
+
import { CompositeAuthoringAdapter } from './composite-authoring-adapter';
|
|
52
|
+
import { UIAuthoringAdapter } from './ui-authoring-adapter';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* C3 — resolve the `UIAuthoringAdapter` that actually owns `nodeId`, whether
|
|
56
|
+
* `adapter` IS one (bare/harness path) or is the edit-mode composite wrapping
|
|
57
|
+
* one as a `kind: 'scene-ui'` child (unified-editor path). `null` when
|
|
58
|
+
* neither applies (a non-UI selection, or a composite with no UI child).
|
|
59
|
+
*/
|
|
60
|
+
function resolveUIAdapter(
|
|
61
|
+
nodeId: string | null,
|
|
62
|
+
adapter: AuthoringAdapter,
|
|
63
|
+
): UIAuthoringAdapter | null {
|
|
64
|
+
if (adapter instanceof UIAuthoringAdapter) return adapter;
|
|
65
|
+
if (adapter instanceof CompositeAuthoringAdapter && nodeId) {
|
|
66
|
+
const owner = adapter.ownerOf(nodeId);
|
|
67
|
+
const child = owner ? adapter.childAdapters().find((c) => c.worldId === owner) : undefined;
|
|
68
|
+
return child?.adapter instanceof UIAuthoringAdapter ? child.adapter : null;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const isUISelection = (node: EditorNode | null, adapter: AuthoringAdapter): boolean =>
|
|
74
|
+
node !== null && resolveUIAdapter(node.id, adapter) !== null;
|
|
75
|
+
|
|
76
|
+
const isSingleUISelection = (node: EditorNode | null, adapter: AuthoringAdapter): boolean => {
|
|
77
|
+
if (node === null) return false;
|
|
78
|
+
const ui = resolveUIAdapter(node.id, adapter);
|
|
79
|
+
return ui !== null && ui.uiStore.selection.length <= 1;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Shared "resolve the live node" prelude every wrapped section below needs
|
|
83
|
+
* — `null` when `nodeId` no longer resolves (selection changed mid-render;
|
|
84
|
+
* the shell simply skips the section). */
|
|
85
|
+
function resolveUINode(store: UIEditStore, nodeId: string | null): UINode | null {
|
|
86
|
+
const loc = nodeId ? findNode(store.roots, nodeId) : null;
|
|
87
|
+
return loc?.node ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeSetAndMixed(store: UIEditStore, ids: string[], nodeId: string) {
|
|
91
|
+
const set = (path: string, value: unknown): void => {
|
|
92
|
+
if (ids.length > 1) store.setPropMulti(ids, path, value);
|
|
93
|
+
else store.setProp(nodeId, path, value);
|
|
94
|
+
};
|
|
95
|
+
const mixed = (path: string): boolean => mixedValue(store, ids, path);
|
|
96
|
+
return { set, mixed };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
registerInspectorSections({
|
|
100
|
+
match: isUISelection,
|
|
101
|
+
Section: function WrappedLayoutSection({ adapter, nodeId }: InspectorSectionProps) {
|
|
102
|
+
const ui = resolveUIAdapter(nodeId, adapter);
|
|
103
|
+
if (!ui) return null;
|
|
104
|
+
const store = ui.uiStore;
|
|
105
|
+
const ids = store.selection;
|
|
106
|
+
const node = resolveUINode(store, nodeId);
|
|
107
|
+
if (!node) return null;
|
|
108
|
+
const { set, mixed } = makeSetAndMixed(store, ids, nodeId!);
|
|
109
|
+
return <LayoutSection store={store} ids={ids} node={node} set={set} mixed={mixed} />;
|
|
110
|
+
},
|
|
111
|
+
// C3 — under the unified editor's ONE inspector shell, suppress the
|
|
112
|
+
// generic descriptor grid `UIAuthoringAdapter.inspector.properties()`
|
|
113
|
+
// would otherwise render alongside this richer pair (§4 C3/R4). The
|
|
114
|
+
// bare-adapter (harness) path never renders the shell's generic grid at
|
|
115
|
+
// all, so `suppresses`/`panelName` are no-ops there.
|
|
116
|
+
suppresses: ['header', 'transform', 'properties'],
|
|
117
|
+
panelName: 'Inspector',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
registerInspectorSections({
|
|
121
|
+
match: isUISelection,
|
|
122
|
+
Section: function WrappedStyleSection({ adapter, nodeId }: InspectorSectionProps) {
|
|
123
|
+
const ui = resolveUIAdapter(nodeId, adapter);
|
|
124
|
+
if (!ui) return null;
|
|
125
|
+
const store = ui.uiStore;
|
|
126
|
+
const ids = store.selection;
|
|
127
|
+
const node = resolveUINode(store, nodeId);
|
|
128
|
+
if (!node) return null;
|
|
129
|
+
const { set, mixed } = makeSetAndMixed(store, ids, nodeId!);
|
|
130
|
+
return <StyleSection node={node} set={set} mixed={mixed} />;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
registerInspectorSections({
|
|
135
|
+
match: isSingleUISelection,
|
|
136
|
+
Section: function WrappedPropsSection({ adapter, nodeId }: InspectorSectionProps) {
|
|
137
|
+
const ui = resolveUIAdapter(nodeId, adapter);
|
|
138
|
+
if (!ui) return null;
|
|
139
|
+
const store = ui.uiStore;
|
|
140
|
+
const node = resolveUINode(store, nodeId);
|
|
141
|
+
if (!node) return null;
|
|
142
|
+
return <PropsSection store={store} node={node} id={nodeId!} />;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
registerInspectorSections({
|
|
147
|
+
match: isSingleUISelection,
|
|
148
|
+
Section: function WrappedEventsSection({ adapter, nodeId }: InspectorSectionProps) {
|
|
149
|
+
const ui = resolveUIAdapter(nodeId, adapter);
|
|
150
|
+
if (!ui) return null;
|
|
151
|
+
const store = ui.uiStore;
|
|
152
|
+
const node = resolveUINode(store, nodeId);
|
|
153
|
+
if (!node) return null;
|
|
154
|
+
return <EventsSection store={store} node={node} id={nodeId!} />;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor-global undo TIMELINE (T3.2 slice 2 — design
|
|
3
|
+
* `docs/PERSISTENCE-PROVIDER-DESIGN.md` §3/§7 slice 2).
|
|
4
|
+
*
|
|
5
|
+
* Today there are THREE independent undo stacks (the first-party `EditorStore`'s
|
|
6
|
+
* whole-doc snapshots, an `IngestAuthoringAdapter`'s overlay-backed closures, a
|
|
7
|
+
* `UIEditStore`'s tree snapshots) and nothing orders them against each other: a
|
|
8
|
+
* Ctrl+Z after "first-party edit → enter ingest → ingest edit" only ever undoes
|
|
9
|
+
* within whichever stack happens to be active. This module is the ONE shared
|
|
10
|
+
* timeline that fixes that: a bounded stack of `{ adapter }` ENTRIES — tags, not
|
|
11
|
+
* state. Each participating store keeps recording into its OWN stack exactly as
|
|
12
|
+
* it always has (this file never touches `_undoStack`/`undoStack`/`_undo`
|
|
13
|
+
* directly) and calls {@link notifyUndoableOp} with itself right after it pushes,
|
|
14
|
+
* so the entry's only job is to say "the next undo of THIS one goes here."
|
|
15
|
+
*
|
|
16
|
+
* Ctrl+Z pops the timeline and calls `entry.adapter.undo()`; Ctrl+Shift+Z /
|
|
17
|
+
* Cmd+Shift+Z / Ctrl+Y mirror with `redo()`. A brand-new op recorded ANYWHERE
|
|
18
|
+
* clears the global redo mirror (standard undo semantics), on top of whatever
|
|
19
|
+
* per-adapter redo-clearing each store already does for its own stack.
|
|
20
|
+
*
|
|
21
|
+
* Adapter identity — the choice this file makes and why: an entry holds the LIVE
|
|
22
|
+
* OBJECT REFERENCE that recorded it (the `EditorStore` instance for first-party,
|
|
23
|
+
* an `IngestAuthoringAdapter` instance, a `UIEditStore` instance — all three
|
|
24
|
+
* already expose `undo()`/`redo()` so no wrapper type is needed), checked against
|
|
25
|
+
* a `Set` of currently-registered instances at dispatch time. A synthesized
|
|
26
|
+
* string id (`'ingest'`, `'ui'`) was considered and rejected: a SECOND ingest/UI
|
|
27
|
+
* session reuses the same string, so a stale entry from a first, since-uninstalled
|
|
28
|
+
* session would resolve against the WRONG (new) live adapter instead of being
|
|
29
|
+
* dropped. Keying on the object itself sidesteps that — a new session is a new
|
|
30
|
+
* object, so an old entry can only ever match its OWN (registered) instance or
|
|
31
|
+
* fail the liveness check and get dropped, which is exactly the disposed-adapter
|
|
32
|
+
* rule below. (A `WeakRef` was the other option raised; rejected because
|
|
33
|
+
* "disposed" here is a LOGICAL state — the instance can still be strongly
|
|
34
|
+
* reachable elsewhere when its session ends — so liveness must be tracked by an
|
|
35
|
+
* explicit unregister call, not by GC.)
|
|
36
|
+
*
|
|
37
|
+
* Session lifetime: `registerUndoAdapter`/`unregisterUndoAdapter` are called at
|
|
38
|
+
* each store's construction/teardown (see `EditorStore`, `IngestAuthoringAdapter`,
|
|
39
|
+
* `UIEditStore`, `ingest-mode.ts`'s `exitIngestMode`, `edit-mode-authoring.ts`'s
|
|
40
|
+
* `exitEditModeAuthoring` for a scene-UI child's `UIEditStore`). An entry
|
|
41
|
+
* whose adapter has been unregistered since it was
|
|
42
|
+
* recorded is dropped (with a console warning) when popped; popping continues
|
|
43
|
+
* past dropped entries until a live one is found or the stack is empty — a
|
|
44
|
+
* session-scoped stack dies with its session, same rule the per-adapter stacks
|
|
45
|
+
* already had on their own.
|
|
46
|
+
*
|
|
47
|
+
* Cross-adapter transactions are OUT of scope (design §6): a gesture spanning two
|
|
48
|
+
* adapters records two entries and undoes in two Ctrl+Z presses.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/** Anything with its own undo/redo the timeline can dispatch to — `EditorStore`,
|
|
52
|
+
* `IngestAuthoringAdapter`, and `UIEditStore` all satisfy this structurally.
|
|
53
|
+
* A strict `false` return means "I could not perform this right now" (blocked
|
|
54
|
+
* during play, empty local stack, no scene bound) — the timeline then puts the
|
|
55
|
+
* entry BACK instead of consuming it, so a blocked Ctrl+Z doesn't silently eat
|
|
56
|
+
* an undo level. `void`/`true` both count as performed. */
|
|
57
|
+
export interface UndoAdapter {
|
|
58
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: deliberate contract (see doc comment) — legacy adapters return void ("performed"), refusal-aware ones return false; `boolean | undefined` would reject `(): void` implementers
|
|
59
|
+
undo(): boolean | void;
|
|
60
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: same contract as undo()
|
|
61
|
+
redo(): boolean | void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Matches the 50-deep bound every per-adapter stack already uses. */
|
|
65
|
+
const MAX_TIMELINE = 50;
|
|
66
|
+
|
|
67
|
+
/** Currently-registered (not disposed/uninstalled) adapters — see doc comment above. */
|
|
68
|
+
const live = new Set<UndoAdapter>();
|
|
69
|
+
|
|
70
|
+
let undoStack: UndoAdapter[] = [];
|
|
71
|
+
let redoStack: UndoAdapter[] = [];
|
|
72
|
+
|
|
73
|
+
/** Register a store/adapter as a valid dispatch target for entries tagging it. */
|
|
74
|
+
export function registerUndoAdapter(adapter: UndoAdapter): void {
|
|
75
|
+
live.add(adapter);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Uninstall a store/adapter (session end). Any timeline entries already recorded
|
|
80
|
+
* for it are NOT removed from the stacks — they are simply dropped (with a warn)
|
|
81
|
+
* the next time they would be popped, per the disposed-adapter rule.
|
|
82
|
+
*/
|
|
83
|
+
export function unregisterUndoAdapter(adapter: UndoAdapter): void {
|
|
84
|
+
live.delete(adapter);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Record a new undoable op that `adapter` just pushed onto its OWN stack. Clears
|
|
89
|
+
* the GLOBAL redo mirror (in addition to whatever per-adapter redo-clearing the
|
|
90
|
+
* caller already does for its own stack) — a new op anywhere invalidates redo.
|
|
91
|
+
*/
|
|
92
|
+
export function notifyUndoableOp(adapter: UndoAdapter): void {
|
|
93
|
+
undoStack.push(adapter);
|
|
94
|
+
if (undoStack.length > MAX_TIMELINE) undoStack.shift();
|
|
95
|
+
redoStack = [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function dispatch(from: UndoAdapter[], to: UndoAdapter[], op: 'undo' | 'redo'): void {
|
|
99
|
+
while (from.length > 0) {
|
|
100
|
+
const adapter = from.pop()!;
|
|
101
|
+
if (!live.has(adapter)) {
|
|
102
|
+
console.warn(
|
|
103
|
+
`[undo-timeline] dropping ${op} entry for a disposed/uninstalled adapter — ` +
|
|
104
|
+
'its session ended before this entry was reached',
|
|
105
|
+
);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (adapter[op]() === false) {
|
|
109
|
+
// The adapter is live but couldn't act (e.g. first-party undo blocked
|
|
110
|
+
// during play). Put the entry back and stop — falling through to an OLDER
|
|
111
|
+
// entry from another adapter would undo out of global order.
|
|
112
|
+
from.push(adapter);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
to.push(adapter);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Ctrl+Z / Cmd+Z. */
|
|
121
|
+
export function timelineUndo(): void {
|
|
122
|
+
dispatch(undoStack, redoStack, 'undo');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Ctrl+Shift+Z / Cmd+Shift+Z / Ctrl+Y. */
|
|
126
|
+
export function timelineRedo(): void {
|
|
127
|
+
dispatch(redoStack, undoStack, 'redo');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** True when the next Ctrl+Z has at least one (possibly dead) entry to try. */
|
|
131
|
+
export function timelineCanUndo(): boolean {
|
|
132
|
+
return undoStack.length > 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** True when the next redo has at least one (possibly dead) entry to try. */
|
|
136
|
+
export function timelineCanRedo(): boolean {
|
|
137
|
+
return redoStack.length > 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Test-only: wipe all registrations + stacks so test files don't bleed into each other. */
|
|
141
|
+
export function __resetUndoTimelineForTest(): void {
|
|
142
|
+
live.clear();
|
|
143
|
+
undoStack = [];
|
|
144
|
+
redoStack = [];
|
|
145
|
+
}
|