@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,795 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReactDomAuthoringAdapter — the editor's {@link AuthoringAdapter} for an
|
|
3
|
+
* INGESTED (unmodified) native-React world, wave 10 Slice A
|
|
4
|
+
* (`docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md` §1, D-K1..D-K5).
|
|
5
|
+
*
|
|
6
|
+
* Unlike {@link ReactWorldAuthoringAdapter} (the first-party react-WORLD
|
|
7
|
+
* adapter, T6.2 slice 2), an ingested react game's JSX is NEVER `data-oid`-
|
|
8
|
+
* stamped (three independent guards — see the design doc's §0) and is never
|
|
9
|
+
* written back to (the never-modify-game-source rule, `CLAUDE.md`). So this
|
|
10
|
+
* adapter cannot reuse `walkOidTree` (it would see an EMPTY tree — nothing
|
|
11
|
+
* is stamped) and cannot reuse the JSX write-back seam at all. It is
|
|
12
|
+
* structurally independent:
|
|
13
|
+
*
|
|
14
|
+
* - Hierarchy: an on-demand walk of every ELEMENT node under a live DOM
|
|
15
|
+
* root (snapshot per call — the DOM is live and may re-render, exactly
|
|
16
|
+
* like `walkOidTree`'s own "no cached/mirrored state" contract). Identity
|
|
17
|
+
* is a STRUCTURAL path, not an authored id: `'rdom:' + '/'-joined
|
|
18
|
+
* segments`, each segment `<tag>[k]` where `k` is the element's index
|
|
19
|
+
* among preceding same-tag element SIBLINGS (the DOM analog of the
|
|
20
|
+
* Object3D structural-path scheme other ingest adapters already use).
|
|
21
|
+
* Deterministic and stable while structure is stable; disambiguates
|
|
22
|
+
* same-tag siblings (`div[0]` vs `div[1]`). The root itself is not a
|
|
23
|
+
* node — its element children are the hierarchy roots.
|
|
24
|
+
* - Labels derive from real DOM only — tag, plus `#id` or the first CSS
|
|
25
|
+
* class when present — never a fabricated/authored name (anti-shim).
|
|
26
|
+
* - Persistence is overlay-only: edits write the live element's inline
|
|
27
|
+
* `style` DIRECTLY (a runtime override, gone on reload unless saved) and
|
|
28
|
+
* record the same write into a {@link ReactDomOverlay} document, which the
|
|
29
|
+
* caller persists via `saveWorldOverlay`/`loadWorldOverlay`
|
|
30
|
+
* (`./world-overlay.ts`) — the SAME `.vgai/overlays/<world-id>.json`
|
|
31
|
+
* convention every other manifest-world overlay uses. There is NO
|
|
32
|
+
* `SourceWriteBackend` import anywhere in this file and NO `/__ui-source`
|
|
33
|
+
* traffic of any kind, ever — this is the one hard invariant the design
|
|
34
|
+
* doc's §4 NOTs pin (AC-K7).
|
|
35
|
+
* - `object3D()`/`idForObject3D()` return `null` — these entities are DOM,
|
|
36
|
+
* not `Object3D` (no 3D gizmo binding, mirroring `ReactWorldAuthoringAdapter`).
|
|
37
|
+
*
|
|
38
|
+
* Inspector reads/writes reuse `ReactWorldAuthoringAdapter`'s pure style
|
|
39
|
+
* helpers (`styleProp`/`numericStyleValue`/`cssColorToHex` — exported for
|
|
40
|
+
* this purpose, D-K5) but declare their OWN `STYLE_PROPERTIES` list: this
|
|
41
|
+
* adapter's writes are not routed through the JSX writer's supported-property
|
|
42
|
+
* ceiling, so its list is free to diverge (kept identical today for
|
|
43
|
+
* predictability — nothing stops a later slice from growing it).
|
|
44
|
+
*
|
|
45
|
+
* Apply-on-mount (D-K3): a pre-loaded overlay's entries are applied to the
|
|
46
|
+
* live DOM right after construction. An id that no longer resolves (the
|
|
47
|
+
* mounted tree moved on since the overlay was authored) is NOT applied, is
|
|
48
|
+
* loudly `console.warn`-reported (the `overlay-report.ts` orphan spirit,
|
|
49
|
+
* without importing that Object3D-shaped apparatus), and is RETAINED in the
|
|
50
|
+
* document — user data is never silently dropped (AC-K4).
|
|
51
|
+
*
|
|
52
|
+
* Undo/redo (D-K3, AC-K6) is adapter-local (`PendingUndo`, mirroring
|
|
53
|
+
* `ReactWorldAuthoringAdapter`'s pattern) and self-registers with the
|
|
54
|
+
* editor-global undo timeline (`./undo-timeline.ts`) exactly like every
|
|
55
|
+
* other ingest/world adapter — so Ctrl+Z reaches it while it's the active
|
|
56
|
+
* session, and its entries are dropped (not misapplied) once the session
|
|
57
|
+
* ends (the route's teardown calls `unregisterUndoAdapter`).
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import type {
|
|
61
|
+
AuthoringAdapter,
|
|
62
|
+
AuthoringCapabilities,
|
|
63
|
+
BoxEditProvider,
|
|
64
|
+
ColorSampleProvider,
|
|
65
|
+
DOMRectLike,
|
|
66
|
+
EditorNode,
|
|
67
|
+
HierarchyProvider,
|
|
68
|
+
InspectorProvider,
|
|
69
|
+
PersistenceProvider,
|
|
70
|
+
PickProvider,
|
|
71
|
+
PropertyDescriptor,
|
|
72
|
+
RectProvider,
|
|
73
|
+
SelectionProvider,
|
|
74
|
+
} from '@engine/adapter';
|
|
75
|
+
import type { EditorStore } from '../editor-store';
|
|
76
|
+
import { browserOrInlineResolver, getComputedStyleValue } from '../ui-source/inspect';
|
|
77
|
+
import {
|
|
78
|
+
cssColorToHex,
|
|
79
|
+
mapBoxEditPatchKey,
|
|
80
|
+
numericStyleValue,
|
|
81
|
+
styleProp,
|
|
82
|
+
} from './react-world-authoring-adapter';
|
|
83
|
+
import { notifyUndoableOp, registerUndoAdapter } from './undo-timeline';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The minimal structural shape this adapter needs from a live DOM element —
|
|
87
|
+
* deliberately NOT `HTMLElement` (same testability reasoning as
|
|
88
|
+
* `OidElementLike` in `react-world-authoring-adapter.ts`: this repo's vitest
|
|
89
|
+
* environment for THIS adapter's own unit test is `node`, no real DOM). A
|
|
90
|
+
* real `Element`/`HTMLElement` satisfies this structurally (`style` is
|
|
91
|
+
* optional here for the same reason it is on `OidElementLike` — a plain
|
|
92
|
+
* `Element` has no `style` getter, only `HTMLElement`/`SVGElement` do, but
|
|
93
|
+
* TS treats an optional field as satisfied by its absence).
|
|
94
|
+
*/
|
|
95
|
+
export interface DomElementLike {
|
|
96
|
+
readonly tagName: string;
|
|
97
|
+
readonly children: ArrayLike<DomElementLike>;
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly className: string;
|
|
100
|
+
readonly style?: unknown;
|
|
101
|
+
/**
|
|
102
|
+
* T0 (spec 27 §2) — the element's live viewport rect, for `rects.rect`'s
|
|
103
|
+
* host-relative geometry below (see `OidElementLike.getBoundingClientRect`'s
|
|
104
|
+
* identical doc comment in `react-world-authoring-adapter.ts`). Optional so
|
|
105
|
+
* existing plain-object test fixtures keep type-checking unchanged — a node
|
|
106
|
+
* with no `getBoundingClientRect` simply has no rect (never a fabricated 0×0).
|
|
107
|
+
*/
|
|
108
|
+
getBoundingClientRect?(): {
|
|
109
|
+
left: number;
|
|
110
|
+
top: number;
|
|
111
|
+
right: number;
|
|
112
|
+
bottom: number;
|
|
113
|
+
width: number;
|
|
114
|
+
height: number;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** One structural-path node resolved from a live-DOM walk. */
|
|
119
|
+
interface DomNode {
|
|
120
|
+
/** `'rdom:' + '/'-joined <tag>[k] segments` — see the class doc comment. */
|
|
121
|
+
id: string;
|
|
122
|
+
tag: string;
|
|
123
|
+
el: DomElementLike;
|
|
124
|
+
parentId: string | null;
|
|
125
|
+
childIds: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface DomTree {
|
|
129
|
+
nodes: Map<string, DomNode>;
|
|
130
|
+
rootIds: string[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const ID_PREFIX = 'rdom:';
|
|
134
|
+
|
|
135
|
+
/** First whitespace-separated CSS class, or `undefined` if `className` is empty/absent. */
|
|
136
|
+
function firstClassName(className: string): string | undefined {
|
|
137
|
+
const trimmed = className.trim();
|
|
138
|
+
if (!trimmed) return undefined;
|
|
139
|
+
return trimmed.split(/\s+/)[0];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Real-DOM-only label (anti-shim): tag, `#id` when present, else `.firstClass` when present. */
|
|
143
|
+
function labelFor(el: DomElementLike, tag: string): string {
|
|
144
|
+
if (el.id) return `${tag}#${el.id}`;
|
|
145
|
+
const cls = firstClassName(el.className ?? '');
|
|
146
|
+
if (cls) return `${tag}.${cls}`;
|
|
147
|
+
return tag;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Walk a live DOM root's ELEMENT descendants into a {@link DomTree}, snapshot
|
|
152
|
+
* per call (D-K1: "the DOM is live and may re-render"). Structural-path id
|
|
153
|
+
* scheme: each segment is `<tag>[k]`, `k` = the element's index among
|
|
154
|
+
* preceding SAME-TAG element siblings under the SAME parent — computed with
|
|
155
|
+
* one `Map<tag, count>` per sibling group, reset at each recursion level.
|
|
156
|
+
*/
|
|
157
|
+
function walkDomTree(root: DomElementLike): DomTree {
|
|
158
|
+
const nodes = new Map<string, DomNode>();
|
|
159
|
+
const rootIds: string[] = [];
|
|
160
|
+
|
|
161
|
+
function visitChildren(
|
|
162
|
+
children: ArrayLike<DomElementLike>,
|
|
163
|
+
parentId: string | null,
|
|
164
|
+
parentPath: string,
|
|
165
|
+
): void {
|
|
166
|
+
const tagCounts = new Map<string, number>();
|
|
167
|
+
for (const el of Array.from(children)) {
|
|
168
|
+
const tag = el.tagName.toLowerCase();
|
|
169
|
+
const k = tagCounts.get(tag) ?? 0;
|
|
170
|
+
tagCounts.set(tag, k + 1);
|
|
171
|
+
const path = parentPath ? `${parentPath}/${tag}[${k}]` : `${tag}[${k}]`;
|
|
172
|
+
const id = `${ID_PREFIX}${path}`;
|
|
173
|
+
const node: DomNode = { id, tag, el, parentId, childIds: [] };
|
|
174
|
+
nodes.set(id, node);
|
|
175
|
+
if (parentId) nodes.get(parentId)?.childIds.push(id);
|
|
176
|
+
else rootIds.push(id);
|
|
177
|
+
visitChildren(el.children, id, path);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
visitChildren(root.children, null, '');
|
|
182
|
+
return { nodes, rootIds };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** A reasonably useful, curated inline-style property list — this adapter's
|
|
186
|
+
* OWN list (D-K2: not shared with `ReactWorldAuthoringAdapter`'s, though
|
|
187
|
+
* kept identical in shape today — this one is not bounded by a JSX writer's
|
|
188
|
+
* supported-property ceiling). */
|
|
189
|
+
const STYLE_PROPERTIES: ReadonlyArray<{
|
|
190
|
+
prop: string;
|
|
191
|
+
label: string;
|
|
192
|
+
type: PropertyDescriptor['type'];
|
|
193
|
+
options?: string[];
|
|
194
|
+
}> = [
|
|
195
|
+
{ prop: 'color', label: 'Color', type: 'color' },
|
|
196
|
+
{ prop: 'backgroundColor', label: 'Background', type: 'color' },
|
|
197
|
+
{ prop: 'borderColor', label: 'Border Color', type: 'color' },
|
|
198
|
+
{ prop: 'fontSize', label: 'Font Size', type: 'number' },
|
|
199
|
+
{ prop: 'width', label: 'Width', type: 'number' },
|
|
200
|
+
{ prop: 'height', label: 'Height', type: 'number' },
|
|
201
|
+
{ prop: 'padding', label: 'Padding', type: 'number' },
|
|
202
|
+
{ prop: 'margin', label: 'Margin', type: 'number' },
|
|
203
|
+
{ prop: 'borderRadius', label: 'Border Radius', type: 'number' },
|
|
204
|
+
{ prop: 'gap', label: 'Gap', type: 'number' },
|
|
205
|
+
{ prop: 'display', label: 'Display', type: 'enum', options: ['flex', 'block', 'none', 'grid'] },
|
|
206
|
+
{ prop: 'textAlign', label: 'Text Align', type: 'enum', options: ['left', 'center', 'right'] },
|
|
207
|
+
{ prop: 'flexDirection', label: 'Flex Direction', type: 'enum', options: ['row', 'column'] },
|
|
208
|
+
];
|
|
209
|
+
const STYLE_PATH_PREFIX = 'style.';
|
|
210
|
+
|
|
211
|
+
/** `prop -> declared type` — see `ReactWorldAuthoringAdapter`'s identical map
|
|
212
|
+
* for why `inspector.get` needs this (a real CSSOM value is always a
|
|
213
|
+
* unit-suffixed string / an `rgb(...)` string, never a bare number/hex). */
|
|
214
|
+
const STYLE_PROPERTY_TYPE: ReadonlyMap<string, PropertyDescriptor['type']> = new Map(
|
|
215
|
+
STYLE_PROPERTIES.map(({ prop, type }) => [prop, type]),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
/** structural id -> cssProp -> value (D-K3). Values are always strings — the
|
|
219
|
+
* same literal an inline style write / a saved overlay round-trips. */
|
|
220
|
+
export type ReactDomOverlay = Record<string, Record<string, string>>;
|
|
221
|
+
|
|
222
|
+
export interface ReactDomAuthoringOptions {
|
|
223
|
+
/** Pre-loaded overlay (from `loadWorldOverlay<ReactDomOverlay>`) — applied
|
|
224
|
+
* to the live DOM on construction (D-K3 apply-on-mount) and mutated
|
|
225
|
+
* in-place by every subsequent `inspector.set` (so `serialize()`/a later
|
|
226
|
+
* `save()` always reflects every write, including unresolved-at-mount-time
|
|
227
|
+
* ones from BEFORE this session, and new ones from THIS session). */
|
|
228
|
+
overlay: ReactDomOverlay;
|
|
229
|
+
/** `save()` flushes the current document through this — routes pass
|
|
230
|
+
* `(o) => saveWorldOverlay(worldId, o)`. */
|
|
231
|
+
persist: (overlay: ReactDomOverlay) => Promise<void>;
|
|
232
|
+
/** Human/agent-readable destination (`PersistenceProvider.destination`) —
|
|
233
|
+
* routes pass `worldOverlayPath(worldId)`. */
|
|
234
|
+
destination: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface PendingUndo {
|
|
238
|
+
undo: () => void;
|
|
239
|
+
redo: () => void;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export class ReactDomAuthoringAdapter implements AuthoringAdapter {
|
|
243
|
+
readonly capabilities: AuthoringCapabilities = {
|
|
244
|
+
transform: false, // no 3D gizmo — DOM has no Object3D pose
|
|
245
|
+
material: false,
|
|
246
|
+
inspectorFields: true,
|
|
247
|
+
create: false, // element insertion out of v1 scope
|
|
248
|
+
delete: false, // structural delete out of v1 scope (overlay-only, no structure provider)
|
|
249
|
+
reparent: false,
|
|
250
|
+
persist: true,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
private readonly doc: ReactDomOverlay;
|
|
254
|
+
private readonly persistFn: (overlay: ReactDomOverlay) => Promise<void>;
|
|
255
|
+
private readonly destinationPath: string;
|
|
256
|
+
private dirty = false;
|
|
257
|
+
private readonly undoStack: PendingUndo[] = [];
|
|
258
|
+
private readonly redoStack: PendingUndo[] = [];
|
|
259
|
+
/** Synchronous-burst cache for `snapshot()` — see that method's doc. */
|
|
260
|
+
private snapCache: DomTree | null = null;
|
|
261
|
+
private snapInvalidationScheduled = false;
|
|
262
|
+
/**
|
|
263
|
+
* T0 (spec 27 §4, B1) — the currently-open `boxEdit` begin/apply×N/end gesture, or
|
|
264
|
+
* `null` between gestures. Mirrors `ReactWorldAuthoringAdapter.boxEditSession` (see
|
|
265
|
+
* its doc comment for why `priorInline` exists) — `touched` here maps prop → the
|
|
266
|
+
* FINAL CSS-STRING value to write (this adapter's overlay route always stores CSS
|
|
267
|
+
* strings, never bare numbers, unlike the react-world source-write route).
|
|
268
|
+
*/
|
|
269
|
+
private boxEditSession: {
|
|
270
|
+
id: string;
|
|
271
|
+
touched: Map<string, string>;
|
|
272
|
+
priorInline: Map<string, string>;
|
|
273
|
+
} | null = null;
|
|
274
|
+
/** D4 (spec27 §6 D4, layer-tree "lock" toggle) — SESSION-LOCAL, never
|
|
275
|
+
* written to the overlay JSON: an ingested game's DOM has no persisted
|
|
276
|
+
* "locked" concept (its overlay only ever records style overrides). See
|
|
277
|
+
* `ReactWorldAuthoringAdapter.lockedIds`'s doc comment for the same
|
|
278
|
+
* reasoning, INCLUDING D4.R1 (now read by `pickable.pick` below and by
|
|
279
|
+
* the overlay's marquee candidate pool; NOT by `hierarchy`/`selection` —
|
|
280
|
+
* a locked node stays selectable/unlockable from the layer tree). */
|
|
281
|
+
private readonly lockedIds = new Set<string>();
|
|
282
|
+
|
|
283
|
+
constructor(
|
|
284
|
+
private readonly root: DomElementLike,
|
|
285
|
+
private readonly store: EditorStore,
|
|
286
|
+
opts: ReactDomAuthoringOptions,
|
|
287
|
+
) {
|
|
288
|
+
this.doc = opts.overlay;
|
|
289
|
+
this.persistFn = opts.persist;
|
|
290
|
+
this.destinationPath = opts.destination;
|
|
291
|
+
// This adapter IS an undo/redo target the editor-global timeline
|
|
292
|
+
// dispatches to (mirrors `ReactWorldAuthoringAdapter`'s identical call) —
|
|
293
|
+
// the caller (the ingest route's teardown) unregisters it on session end.
|
|
294
|
+
registerUndoAdapter(this);
|
|
295
|
+
this.applyOverlayOnMount();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* A live-DOM walk, memoized for the current SYNCHRONOUS burst only.
|
|
300
|
+
*
|
|
301
|
+
* `hierarchy.node(id)` is called once per node by the shell's row flattener
|
|
302
|
+
* (`flattenIngestRows`), and a large live react game (react-rpg's dungeon
|
|
303
|
+
* is ~1900 walked rows over a several-thousand-element DOM) turned that into
|
|
304
|
+
* an O(N²) re-walk-per-node — a ~9s synchronous block that hung the editor
|
|
305
|
+
* on selection (the hierarchy re-renders synchronously inside the click's
|
|
306
|
+
* discrete-event flush). JS is single-threaded, so the DOM cannot change
|
|
307
|
+
* mid-render: caching the walk for the current synchronous stack and
|
|
308
|
+
* invalidating it on the next microtask keeps the "the DOM is live, never
|
|
309
|
+
* stale across renders" contract (a subsequent render, after the game may
|
|
310
|
+
* have re-rendered, re-walks) while collapsing one render's N node() calls
|
|
311
|
+
* to ONE walk. Style READS stay live regardless — `inspector.get` reads
|
|
312
|
+
* `n.el.style` off the cached element ref, not a cached value. Structural
|
|
313
|
+
* mutations (none in v1 — this adapter has no `structure` provider) would
|
|
314
|
+
* land between ticks (the game's own re-render), which the microtask
|
|
315
|
+
* invalidation already covers.
|
|
316
|
+
*/
|
|
317
|
+
private snapshot(): DomTree {
|
|
318
|
+
if (this.snapCache) return this.snapCache;
|
|
319
|
+
const tree = walkDomTree(this.root);
|
|
320
|
+
this.snapCache = tree;
|
|
321
|
+
if (!this.snapInvalidationScheduled) {
|
|
322
|
+
this.snapInvalidationScheduled = true;
|
|
323
|
+
queueMicrotask(() => {
|
|
324
|
+
this.snapCache = null;
|
|
325
|
+
this.snapInvalidationScheduled = false;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return tree;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* D-L1 (wave 10 Slice B, docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md
|
|
333
|
+
* §2) — total ELEMENT nodes currently walked under the root: a fresh
|
|
334
|
+
* snapshot every call (same "the DOM is live and may re-render, never
|
|
335
|
+
* cached" discipline as `hierarchy.roots()`/`hierarchy.node()` above), not
|
|
336
|
+
* a count taken once at construction. Backs
|
|
337
|
+
* `__vgaiIngestReact.domEvidence().hierarchyNodeCount` — one of the four
|
|
338
|
+
* legs `vgai doctor`'s react-ingest MOUNTED bar requires
|
|
339
|
+
* (`clearsReactIngestMountedBar`, `packages/editor/src/doctor/report.ts`).
|
|
340
|
+
*/
|
|
341
|
+
nodeCount(): number {
|
|
342
|
+
return this.snapshot().nodes.size;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private toEditorNode(n: DomNode): EditorNode {
|
|
346
|
+
return {
|
|
347
|
+
id: n.id,
|
|
348
|
+
label: labelFor(n.el, n.tag),
|
|
349
|
+
kind: n.tag,
|
|
350
|
+
parentId: n.parentId,
|
|
351
|
+
childIds: n.childIds,
|
|
352
|
+
flags: { runtimeOnly: true },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* D-K3 apply-on-mount: apply every resolvable overlay entry to the live
|
|
358
|
+
* DOM; an id that doesn't resolve is loudly reported and left untouched
|
|
359
|
+
* (and stays in `this.doc` — it is never deleted here).
|
|
360
|
+
*/
|
|
361
|
+
private applyOverlayOnMount(): void {
|
|
362
|
+
const { nodes } = this.snapshot();
|
|
363
|
+
for (const [id, props] of Object.entries(this.doc)) {
|
|
364
|
+
const n = nodes.get(id);
|
|
365
|
+
if (!n) {
|
|
366
|
+
// biome-ignore lint/suspicious/noConsole: deliberate, greppable orphan report — mirrors overlay-report.ts's own console.warn
|
|
367
|
+
console.warn(
|
|
368
|
+
`[ReactDomAuthoringAdapter] overlay entry "${id}" does not resolve to any live ` +
|
|
369
|
+
'element on this mount — not applied, retained in the overlay document.',
|
|
370
|
+
);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
for (const [prop, value] of Object.entries(props)) {
|
|
374
|
+
this.applyStyleToElement(n.el, prop, value);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private applyStyleToElement(el: DomElementLike, prop: string, value: string): void {
|
|
380
|
+
if (!el.style) return;
|
|
381
|
+
(el.style as Record<string, unknown>)[prop] = value;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private recordOverlayEntry(id: string, prop: string, value: string): void {
|
|
385
|
+
const entry = this.doc[id] ?? {};
|
|
386
|
+
entry[prop] = value;
|
|
387
|
+
this.doc[id] = entry;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private removeOverlayEntry(id: string, prop: string): void {
|
|
391
|
+
const entry = this.doc[id];
|
|
392
|
+
if (!entry) return;
|
|
393
|
+
delete entry[prop];
|
|
394
|
+
if (Object.keys(entry).length === 0) delete this.doc[id];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
readonly hierarchy: HierarchyProvider = {
|
|
398
|
+
roots: () => {
|
|
399
|
+
const { nodes, rootIds } = this.snapshot();
|
|
400
|
+
return rootIds.map((id) => this.toEditorNode(nodes.get(id)!));
|
|
401
|
+
},
|
|
402
|
+
node: (id) => {
|
|
403
|
+
const n = this.snapshot().nodes.get(id);
|
|
404
|
+
return n ? this.toEditorNode(n) : null;
|
|
405
|
+
},
|
|
406
|
+
object3D: () => null, // DOM, not Object3D — no 3D gizmo binding
|
|
407
|
+
idForObject3D: () => null,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
readonly selection: SelectionProvider = {
|
|
411
|
+
get: () => [...this.store.selectedEntityIds],
|
|
412
|
+
set: (ids) => this.store.selectMultiple(ids),
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* A2 gap close-out (spec 27 §2, spec:237-254/spec:251-254) — GEOMETRIC rect
|
|
417
|
+
* hit-test over the live DOM tree, symmetric with
|
|
418
|
+
* `ReactWorldAuthoringAdapter.pickable` (see that provider's doc comment
|
|
419
|
+
* for the full rationale, identical here): NOT `document.elementFromPoint`
|
|
420
|
+
* (which skips a `pointer-events:none` design-time wrapper), smallest-area
|
|
421
|
+
* element containing the point wins (the deepest/most-specific node), ties
|
|
422
|
+
* broken by later tree-walk order (the topmost). Without this, an ingested
|
|
423
|
+
* react world's `rdom:`-id nodes are never `pickable`, so the shell's
|
|
424
|
+
* `pickTopmost` (`layered-pick.ts`) always falls through to `null` for this
|
|
425
|
+
* adapter — click-to-select through the viewport never resolves any node,
|
|
426
|
+
* even though `hierarchy`/`selection` already work from the panel.
|
|
427
|
+
*
|
|
428
|
+
* The winning id is the SAME structural `rdom:<tag>[k]` id
|
|
429
|
+
* `hierarchy`/`selection` already use — a hit routes straight into
|
|
430
|
+
* `composite.selection.set([id])` with no translation.
|
|
431
|
+
*/
|
|
432
|
+
readonly pickable: PickProvider = {
|
|
433
|
+
pick: (clientX, clientY) => {
|
|
434
|
+
const { nodes } = this.snapshot();
|
|
435
|
+
let bestId: string | null = null;
|
|
436
|
+
let bestArea = Number.POSITIVE_INFINITY;
|
|
437
|
+
let bestOrder = -1;
|
|
438
|
+
let order = -1;
|
|
439
|
+
for (const node of nodes.values()) {
|
|
440
|
+
order++;
|
|
441
|
+
// D4.R1 — a locked node is SKIPPED, not returned (falls through to
|
|
442
|
+
// whatever unlocked node is behind/around it) — mirrors
|
|
443
|
+
// `ReactWorldAuthoringAdapter.pickable`'s identical fix and the
|
|
444
|
+
// first-party `viewport-raycast.ts` locked-skip.
|
|
445
|
+
if (this.lockedIds.has(node.id)) continue;
|
|
446
|
+
const rect = node.el.getBoundingClientRect?.();
|
|
447
|
+
if (!rect) continue; // no live rect (test fixture, or unmounted) — never a candidate
|
|
448
|
+
if (
|
|
449
|
+
clientX < rect.left ||
|
|
450
|
+
clientX > rect.right ||
|
|
451
|
+
clientY < rect.top ||
|
|
452
|
+
clientY > rect.bottom
|
|
453
|
+
) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const area = rect.width * rect.height;
|
|
457
|
+
// smallest-area (deepest) wins; ties broken by later tree-order (topmost)
|
|
458
|
+
if (area < bestArea || (area === bestArea && order > bestOrder)) {
|
|
459
|
+
bestId = node.id;
|
|
460
|
+
bestArea = area;
|
|
461
|
+
bestOrder = order;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return bestId;
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
/** Host rect to subtract for {@link rects}' HOST-RELATIVE geometry — same
|
|
469
|
+
* reasoning as `ReactWorldAuthoringAdapter.hostRect` (this adapter's own
|
|
470
|
+
* `this.root` is symmetrically the ingested world's mounted DOM layer). */
|
|
471
|
+
private hostRect(): { left: number; top: number } {
|
|
472
|
+
const r = this.root.getBoundingClientRect?.();
|
|
473
|
+
return { left: r?.left ?? 0, top: r?.top ?? 0 };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private toHostRelative(r: {
|
|
477
|
+
left: number;
|
|
478
|
+
top: number;
|
|
479
|
+
width: number;
|
|
480
|
+
height: number;
|
|
481
|
+
}): DOMRectLike {
|
|
482
|
+
const host = this.hostRect();
|
|
483
|
+
return { x: r.left - host.left, y: r.top - host.top, width: r.width, height: r.height };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* T0 (spec 27 §2) — per-node screen geometry, HOST-RELATIVE (see
|
|
488
|
+
* {@link hostRect}'s doc comment), symmetric with
|
|
489
|
+
* `ReactWorldAuthoringAdapter.rects` so the overlay this feeds (Phase A3)
|
|
490
|
+
* treats both react adapters identically.
|
|
491
|
+
*/
|
|
492
|
+
readonly rects: RectProvider = {
|
|
493
|
+
rect: (id) => {
|
|
494
|
+
const n = this.snapshot().nodes.get(id);
|
|
495
|
+
const r = n?.el.getBoundingClientRect?.();
|
|
496
|
+
return r ? this.toHostRelative(r) : null;
|
|
497
|
+
},
|
|
498
|
+
contextRects: (id) => {
|
|
499
|
+
const { nodes } = this.snapshot();
|
|
500
|
+
const n = nodes.get(id);
|
|
501
|
+
if (!n) return {};
|
|
502
|
+
const parentNode = n.parentId ? nodes.get(n.parentId) : undefined;
|
|
503
|
+
const parentRect = parentNode?.el.getBoundingClientRect?.();
|
|
504
|
+
const siblingIds = (parentNode ? parentNode.childIds : this.snapshot().rootIds).filter(
|
|
505
|
+
(sid) => sid !== id,
|
|
506
|
+
);
|
|
507
|
+
const siblings = siblingIds
|
|
508
|
+
.map((sid) => nodes.get(sid)?.el.getBoundingClientRect?.())
|
|
509
|
+
.filter((r): r is NonNullable<typeof r> => r != null)
|
|
510
|
+
.map((r) => this.toHostRelative(r));
|
|
511
|
+
const el = n.el;
|
|
512
|
+
const rect = el.getBoundingClientRect?.();
|
|
513
|
+
let paddingBox: DOMRectLike | undefined;
|
|
514
|
+
if (rect) {
|
|
515
|
+
const bt =
|
|
516
|
+
numericStyleValue(getComputedStyleValue(el, 'borderTopWidth', browserOrInlineResolver)) ??
|
|
517
|
+
0;
|
|
518
|
+
const br =
|
|
519
|
+
numericStyleValue(
|
|
520
|
+
getComputedStyleValue(el, 'borderRightWidth', browserOrInlineResolver),
|
|
521
|
+
) ?? 0;
|
|
522
|
+
const bb =
|
|
523
|
+
numericStyleValue(
|
|
524
|
+
getComputedStyleValue(el, 'borderBottomWidth', browserOrInlineResolver),
|
|
525
|
+
) ?? 0;
|
|
526
|
+
const bl =
|
|
527
|
+
numericStyleValue(
|
|
528
|
+
getComputedStyleValue(el, 'borderLeftWidth', browserOrInlineResolver),
|
|
529
|
+
) ?? 0;
|
|
530
|
+
paddingBox = this.toHostRelative({
|
|
531
|
+
left: rect.left + bl,
|
|
532
|
+
top: rect.top + bt,
|
|
533
|
+
width: Math.max(0, rect.width - bl - br),
|
|
534
|
+
height: Math.max(0, rect.height - bt - bb),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
...(parentRect ? { parent: this.toHostRelative(parentRect) } : {}),
|
|
539
|
+
...(siblings.length ? { siblings } : {}),
|
|
540
|
+
...(paddingBox ? { paddingBox } : {}),
|
|
541
|
+
};
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* D3.d (spec 27 §6) — eyedropper FALLBACK color-sample path, symmetric with
|
|
547
|
+
* `ReactWorldAuthoringAdapter.colorSample` (see that provider's doc comment
|
|
548
|
+
* for the full rationale — raw/un-normalized values, hit-element first).
|
|
549
|
+
*/
|
|
550
|
+
readonly colorSample: ColorSampleProvider = {
|
|
551
|
+
backgroundChainAt: (clientX, clientY) => {
|
|
552
|
+
const hitId = this.pickable.pick(clientX, clientY);
|
|
553
|
+
if (!hitId) return null;
|
|
554
|
+
const { nodes } = this.snapshot();
|
|
555
|
+
const chain: string[] = [];
|
|
556
|
+
let cur: DomNode | undefined = nodes.get(hitId);
|
|
557
|
+
while (cur) {
|
|
558
|
+
chain.push(getComputedStyleValue(cur.el, 'backgroundColor', browserOrInlineResolver));
|
|
559
|
+
cur = cur.parentId ? nodes.get(cur.parentId) : undefined;
|
|
560
|
+
}
|
|
561
|
+
return chain;
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* T0 (spec 27 §4, B1) — same interface as `ReactWorldAuthoringAdapter.boxEdit`,
|
|
567
|
+
* routed to the OVERLAY-JSON instead of source (spec:305 — "Ingested adapter routes
|
|
568
|
+
* to its overlay-JSON instead of source, same interface"). `apply` is LIVE PREVIEW
|
|
569
|
+
* ONLY — it mutates the live element's inline style via `applyStyleToElement`
|
|
570
|
+
* WITHOUT touching `this.doc`/`dirty` (no overlay traffic, no spam). `end` commits
|
|
571
|
+
* every touched prop ONCE through {@link writeStyleEntry} (preserving the
|
|
572
|
+
* restore-BOTH-halves undo invariant, D-K2), composed into exactly ONE undo entry
|
|
573
|
+
* for the whole gesture (acceptance:310) even across multiple touched props.
|
|
574
|
+
*/
|
|
575
|
+
readonly boxEdit: BoxEditProvider = {
|
|
576
|
+
begin: (id) => {
|
|
577
|
+
this.boxEditSession = { id, touched: new Map(), priorInline: new Map() };
|
|
578
|
+
},
|
|
579
|
+
apply: (id, patch) => {
|
|
580
|
+
const session = this.boxEditSession;
|
|
581
|
+
if (!session || session.id !== id) return; // no open gesture for this id
|
|
582
|
+
const n = this.snapshot().nodes.get(id);
|
|
583
|
+
if (!n) return; // unresolved id — no-op (per contract)
|
|
584
|
+
const pos = getComputedStyleValue(n.el, 'position', browserOrInlineResolver);
|
|
585
|
+
const isPositioned = pos === 'absolute' || pos === 'fixed';
|
|
586
|
+
for (const [key, v] of Object.entries(patch)) {
|
|
587
|
+
const mapped = mapBoxEditPatchKey(key, isPositioned);
|
|
588
|
+
if (!mapped) {
|
|
589
|
+
console.warn(
|
|
590
|
+
`[ReactDomAuthoringAdapter] boxEdit: dropping patch key "${key}" for "${id}" — ` +
|
|
591
|
+
(key === 'x' || key === 'y'
|
|
592
|
+
? `node is not absolutely/fixed positioned (computed position: "${pos}"), ` +
|
|
593
|
+
'no left/top to move'
|
|
594
|
+
: 'unrecognized box-edit patch key'),
|
|
595
|
+
);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
// Lazily snapshot the TRUE pre-gesture inline value the first time THIS
|
|
599
|
+
// prop is touched in this gesture — before mutating it (see
|
|
600
|
+
// `boxEditSession`'s doc comment, mirroring the react-world adapter).
|
|
601
|
+
if (!session.priorInline.has(mapped.prop)) {
|
|
602
|
+
const prevRaw = styleProp(n.el.style, mapped.prop);
|
|
603
|
+
session.priorInline.set(mapped.prop, prevRaw == null ? '' : String(prevRaw));
|
|
604
|
+
}
|
|
605
|
+
const cssValue = mapped.cssValue(v);
|
|
606
|
+
this.applyStyleToElement(n.el, mapped.prop, cssValue);
|
|
607
|
+
session.touched.set(mapped.prop, cssValue); // this route always commits CSS strings
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
end: (id) => {
|
|
611
|
+
const session = this.boxEditSession;
|
|
612
|
+
this.boxEditSession = null;
|
|
613
|
+
if (!session || session.id !== id || session.touched.size === 0) return;
|
|
614
|
+
this.commitBoxEdit(id, session.touched, session.priorInline);
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* T0 (spec 27 §4, B1) — commit every prop touched by one `boxEdit` gesture, each
|
|
620
|
+
* through {@link writeStyleEntry} (preserving the restore-BOTH-halves undo
|
|
621
|
+
* invariant per prop), composed into exactly ONE undo entry (acceptance:310) whose
|
|
622
|
+
* inverse runs in REVERSE order and whose redo runs in gesture order.
|
|
623
|
+
*/
|
|
624
|
+
private commitBoxEdit(
|
|
625
|
+
id: string,
|
|
626
|
+
touched: Map<string, string>,
|
|
627
|
+
priorInline: Map<string, string>,
|
|
628
|
+
): void {
|
|
629
|
+
const entries: PendingUndo[] = [];
|
|
630
|
+
for (const [prop, value] of touched) {
|
|
631
|
+
const entry = this.writeStyleEntry(id, prop, value, priorInline.get(prop));
|
|
632
|
+
if (entry) entries.push(entry);
|
|
633
|
+
}
|
|
634
|
+
if (entries.length === 0) return;
|
|
635
|
+
this.pushUndo({
|
|
636
|
+
undo: () => {
|
|
637
|
+
for (let i = entries.length - 1; i >= 0; i--) entries[i]!.undo();
|
|
638
|
+
},
|
|
639
|
+
redo: () => {
|
|
640
|
+
for (const entry of entries) entry.redo();
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
readonly inspector: InspectorProvider = {
|
|
646
|
+
properties: (): PropertyDescriptor[] =>
|
|
647
|
+
STYLE_PROPERTIES.map(({ prop, label, type, options }) => ({
|
|
648
|
+
path: `${STYLE_PATH_PREFIX}${prop}`,
|
|
649
|
+
label,
|
|
650
|
+
type,
|
|
651
|
+
...(options ? { options } : {}),
|
|
652
|
+
})),
|
|
653
|
+
get: (id, path) => {
|
|
654
|
+
// D4 (spec27 §6 D4, layer-tree visibility/lock) — see
|
|
655
|
+
// `ReactWorldAuthoringAdapter`'s matching branch for the reasoning
|
|
656
|
+
// (`locked` session-local; `visible` sugar for the `visibility` inline
|
|
657
|
+
// style — this adapter writes synchronously, so no echo is needed).
|
|
658
|
+
if (path === 'locked') return this.lockedIds.has(id);
|
|
659
|
+
if (path === 'visible') {
|
|
660
|
+
const n = this.snapshot().nodes.get(id);
|
|
661
|
+
if (!n) return undefined;
|
|
662
|
+
return styleProp(n.el.style, 'visibility') !== 'hidden';
|
|
663
|
+
}
|
|
664
|
+
if (!path.startsWith(STYLE_PATH_PREFIX)) return undefined;
|
|
665
|
+
const prop = path.slice(STYLE_PATH_PREFIX.length);
|
|
666
|
+
const n = this.snapshot().nodes.get(id);
|
|
667
|
+
if (!n) return undefined;
|
|
668
|
+
const raw = styleProp(n.el.style, prop);
|
|
669
|
+
const declaredType = STYLE_PROPERTY_TYPE.get(prop);
|
|
670
|
+
if (declaredType === 'number') return numericStyleValue(raw);
|
|
671
|
+
if (declaredType === 'color') return cssColorToHex(raw);
|
|
672
|
+
return raw;
|
|
673
|
+
},
|
|
674
|
+
set: (id, path, value) => {
|
|
675
|
+
if (path === 'locked') {
|
|
676
|
+
if (value) this.lockedIds.add(id);
|
|
677
|
+
else this.lockedIds.delete(id);
|
|
678
|
+
this.store.notifyIngestEdit();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (path === 'visible') {
|
|
682
|
+
this.writeStyle(id, 'visibility', value ? 'visible' : 'hidden');
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (!path.startsWith(STYLE_PATH_PREFIX)) return;
|
|
686
|
+
const prop = path.slice(STYLE_PATH_PREFIX.length);
|
|
687
|
+
this.writeStyle(id, prop, String(value));
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* T0 (spec 27 §4, B1) — the write body extracted from {@link writeStyle}, RETURNING
|
|
693
|
+
* the resulting {@link PendingUndo} entry instead of pushing it, so a caller that
|
|
694
|
+
* writes MULTIPLE props for one logical gesture ({@link commitBoxEdit}) can compose
|
|
695
|
+
* them into a single undo entry itself. `writeStyle` (below) is now a one-line
|
|
696
|
+
* wrapper — identical behavior for its existing single-prop caller (`inspector.set`).
|
|
697
|
+
* `priorInlineOverride`, when given, is used as the captured PRE-gesture inline value
|
|
698
|
+
* instead of re-reading `n.el.style` — needed because `boxEdit.apply` already mutated
|
|
699
|
+
* the live inline style for preview before `end` gets here (see `boxEditSession`'s
|
|
700
|
+
* doc comment). D-K2's restore-BOTH-halves invariant (inline AND overlay) is
|
|
701
|
+
* unchanged — only WHERE `prevInline` comes from differs when an override is given.
|
|
702
|
+
*/
|
|
703
|
+
private writeStyleEntry(
|
|
704
|
+
id: string,
|
|
705
|
+
prop: string,
|
|
706
|
+
value: string,
|
|
707
|
+
priorInlineOverride?: string,
|
|
708
|
+
): PendingUndo | null {
|
|
709
|
+
const n = this.snapshot().nodes.get(id);
|
|
710
|
+
if (!n) return null;
|
|
711
|
+
const prevRaw =
|
|
712
|
+
priorInlineOverride !== undefined ? priorInlineOverride : styleProp(n.el.style, prop);
|
|
713
|
+
const prevInline = prevRaw == null ? '' : String(prevRaw);
|
|
714
|
+
const existingEntry = this.doc[id];
|
|
715
|
+
const hadOverlayEntry = existingEntry ? Object.hasOwn(existingEntry, prop) : false;
|
|
716
|
+
const prevOverlayValue = existingEntry?.[prop];
|
|
717
|
+
|
|
718
|
+
this.applyStyleToElement(n.el, prop, value);
|
|
719
|
+
this.recordOverlayEntry(id, prop, value);
|
|
720
|
+
this.dirty = true;
|
|
721
|
+
this.store.notifyIngestEdit();
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
undo: () => {
|
|
725
|
+
// Re-resolve rather than cache the element ref — the DOM is live and
|
|
726
|
+
// may have re-rendered since this entry was pushed (same discipline
|
|
727
|
+
// as `ReactWorldAuthoringAdapter.applyStyleWrite`).
|
|
728
|
+
const cur = this.snapshot().nodes.get(id);
|
|
729
|
+
if (cur) this.applyStyleToElement(cur.el, prop, prevInline);
|
|
730
|
+
if (hadOverlayEntry) this.recordOverlayEntry(id, prop, prevOverlayValue!);
|
|
731
|
+
else this.removeOverlayEntry(id, prop);
|
|
732
|
+
this.store.notifyIngestEdit();
|
|
733
|
+
},
|
|
734
|
+
redo: () => {
|
|
735
|
+
const cur = this.snapshot().nodes.get(id);
|
|
736
|
+
if (cur) this.applyStyleToElement(cur.el, prop, value);
|
|
737
|
+
this.recordOverlayEntry(id, prop, value);
|
|
738
|
+
this.store.notifyIngestEdit();
|
|
739
|
+
},
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* D-K2: mutate the live element's inline style AND the overlay document,
|
|
745
|
+
* mark dirty, push an adapter-local undo entry that restores BOTH the
|
|
746
|
+
* prior inline value and the prior overlay-entry state (present, absent,
|
|
747
|
+
* or a different value) — never just one half (AC-K6).
|
|
748
|
+
*/
|
|
749
|
+
private writeStyle(id: string, prop: string, value: string): void {
|
|
750
|
+
const entry = this.writeStyleEntry(id, prop, value);
|
|
751
|
+
if (entry) this.pushUndo(entry);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private pushUndo(entry: PendingUndo): void {
|
|
755
|
+
this.undoStack.push(entry);
|
|
756
|
+
this.redoStack.length = 0;
|
|
757
|
+
notifyUndoableOp(this);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
undo(): boolean {
|
|
761
|
+
const e = this.undoStack.pop();
|
|
762
|
+
if (!e) return false;
|
|
763
|
+
e.undo();
|
|
764
|
+
this.redoStack.push(e);
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
redo(): boolean {
|
|
769
|
+
const e = this.redoStack.pop();
|
|
770
|
+
if (!e) return false;
|
|
771
|
+
e.redo();
|
|
772
|
+
this.undoStack.push(e);
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
subscribe(listener: () => void): () => void {
|
|
777
|
+
return this.store.subscribe(listener);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// A class GETTER, not a field initializer — same reason as every other
|
|
781
|
+
// adapter here (`ui-authoring-adapter.ts`/`vgai-scene-authoring-adapter.ts`):
|
|
782
|
+
// a field initializer runs before the constructor body assigns
|
|
783
|
+
// `this.destinationPath`/`this.persistFn`.
|
|
784
|
+
get persistence(): PersistenceProvider {
|
|
785
|
+
return {
|
|
786
|
+
isDirty: () => this.dirty,
|
|
787
|
+
save: async () => {
|
|
788
|
+
await this.persistFn(this.doc);
|
|
789
|
+
this.dirty = false;
|
|
790
|
+
},
|
|
791
|
+
serialize: () => this.doc,
|
|
792
|
+
destination: this.destinationPath,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
}
|