@volter/sdk 0.0.0-stage → 0.5.204

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.
Files changed (523) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +20 -0
  3. package/README.md +38 -3
  4. package/package.json +510 -4
  5. package/src/account.ts +210 -0
  6. package/src/chrome.ts +88 -0
  7. package/src/client.ts +1646 -0
  8. package/src/commands.ts +66 -0
  9. package/src/contributions.ts +619 -0
  10. package/src/css-numeric-style.ts +97 -0
  11. package/src/document-probe.ts +282 -0
  12. package/src/editor-view.ts +225 -0
  13. package/src/extension.ts +40 -0
  14. package/src/generations.ts +178 -0
  15. package/src/host.ts +1157 -0
  16. package/src/http-transport.browser.ts +14 -0
  17. package/src/http-transport.node.ts +19 -0
  18. package/src/index.ts +131 -0
  19. package/src/kit/CapabilityCoverageSection.tsx +185 -0
  20. package/src/kit/account-client.ts +333 -0
  21. package/src/kit/action-registry.ts +317 -0
  22. package/src/kit/active-product.ts +76 -0
  23. package/src/kit/active-project.ts +155 -0
  24. package/src/kit/adapter-editor-config.ts +25 -0
  25. package/src/kit/adapter-module.ts +7 -0
  26. package/src/kit/adapter-observation.ts +49 -0
  27. package/src/kit/animation/animation-clock.ts +479 -0
  28. package/src/kit/animation/stage-transport.ts +385 -0
  29. package/src/kit/api/assets.ts +365 -0
  30. package/src/kit/api/project-open.ts +355 -0
  31. package/src/kit/api/project-source.ts +180 -0
  32. package/src/kit/api/project-state.ts +110 -0
  33. package/src/kit/api/relay.ts +270 -0
  34. package/src/kit/api/themes.ts +45 -0
  35. package/src/kit/api-asset-library-wire.ts +45 -0
  36. package/src/kit/api-base.ts +10 -0
  37. package/src/kit/api-build.ts +99 -0
  38. package/src/kit/api-git-wire.ts +56 -0
  39. package/src/kit/api-logs.ts +92 -0
  40. package/src/kit/api-project-identity.ts +74 -0
  41. package/src/kit/api-settings.ts +36 -0
  42. package/src/kit/api-worktrees.ts +205 -0
  43. package/src/kit/asset-capabilities.ts +344 -0
  44. package/src/kit/asset-compare-core.ts +171 -0
  45. package/src/kit/asset-editor-context.tsx +101 -0
  46. package/src/kit/asset-events.ts +96 -0
  47. package/src/kit/asset-inspector-actions.ts +87 -0
  48. package/src/kit/asset-selection-viewer-registry.ts +113 -0
  49. package/src/kit/asset-selection.ts +146 -0
  50. package/src/kit/asset-thumbnails.ts +25 -0
  51. package/src/kit/asset-viewers.ts +115 -0
  52. package/src/kit/asset-workflow/asset-import-jobs.ts +106 -0
  53. package/src/kit/asset-workflow/asset-ledger-backend.ts +126 -0
  54. package/src/kit/asset-workflow/asset-ledger.ts +156 -0
  55. package/src/kit/asset-workflow/asset-materialization-report.ts +140 -0
  56. package/src/kit/asset-workflow/asset-pack-manifest.ts +320 -0
  57. package/src/kit/asset-workflow/asset-types.ts +142 -0
  58. package/src/kit/asset-workflow/audio-preview-player.ts +193 -0
  59. package/src/kit/asset-workflow/audio-waveform.ts +22 -0
  60. package/src/kit/asset-workflow/cloud-asset-client.ts +263 -0
  61. package/src/kit/asset-workflow/hosted-asset-materialization.ts +236 -0
  62. package/src/kit/asset-workflow/image-view-scale.ts +31 -0
  63. package/src/kit/asset-workflow/import-contract.ts +124 -0
  64. package/src/kit/asset-workflow/ledger-write-lock.ts +244 -0
  65. package/src/kit/asset-workflow/pixi-spritesheet.ts +197 -0
  66. package/src/kit/asset-workflow/preview-resource-lifetime.ts +44 -0
  67. package/src/kit/asset-workflow/project-asset-commands.ts +20 -0
  68. package/src/kit/asset-workflow/project-content.ts +288 -0
  69. package/src/kit/asset-workflow/project-source-index.ts +545 -0
  70. package/src/kit/asset-workflow/thumbnail-system.ts +256 -0
  71. package/src/kit/authoring/active-adapter.ts +200 -0
  72. package/src/kit/authoring/active-systems.ts +422 -0
  73. package/src/kit/authoring/adapter-key.ts +18 -0
  74. package/src/kit/authoring/authoring-asset-url.ts +27 -0
  75. package/src/kit/authoring/bootstrap-state.ts +49 -0
  76. package/src/kit/authoring/boundary-authoring-adapter.ts +189 -0
  77. package/src/kit/authoring/canvas-scene-guides.ts +84 -0
  78. package/src/kit/authoring/composite-authoring-adapter.ts +2109 -0
  79. package/src/kit/authoring/consumer-actions.ts +531 -0
  80. package/src/kit/authoring/design-time-layers.ts +852 -0
  81. package/src/kit/authoring/design-time-mount-registry.ts +244 -0
  82. package/src/kit/authoring/edit-mode-authoring.ts +637 -0
  83. package/src/kit/authoring/empty-project-authoring.ts +22 -0
  84. package/src/kit/authoring/instance-source-menu.ts +135 -0
  85. package/src/kit/authoring/layered-pick.ts +185 -0
  86. package/src/kit/authoring/mounted-root-subjects.ts +146 -0
  87. package/src/kit/authoring/no-authoring-adapter.ts +59 -0
  88. package/src/kit/authoring/object3d-document-persistence.ts +122 -0
  89. package/src/kit/authoring/panel-authoring.ts +121 -0
  90. package/src/kit/authoring/project-authoring-session.ts +105 -0
  91. package/src/kit/authoring/provenance.ts +99 -0
  92. package/src/kit/authoring/react-canvas-navigation.ts +259 -0
  93. package/src/kit/authoring/react-design-canvas-style.ts +20 -0
  94. package/src/kit/authoring/react-story-board.ts +917 -0
  95. package/src/kit/authoring/selection-scope.ts +195 -0
  96. package/src/kit/authoring/shell-document-ops.ts +169 -0
  97. package/src/kit/authoring/story-board-chrome-fit.ts +107 -0
  98. package/src/kit/authoring/story-board-presentation.ts +111 -0
  99. package/src/kit/authoring/three-root.ts +67 -0
  100. package/src/kit/authoring/viewport-tool-context.ts +73 -0
  101. package/src/kit/authoring/viewport-tool-owner.ts +38 -0
  102. package/src/kit/authoring/world-session-state.ts +101 -0
  103. package/src/kit/authoring-seam-evidence.ts +300 -0
  104. package/src/kit/availability-tick.ts +66 -0
  105. package/src/kit/bitmap-label.ts +120 -0
  106. package/src/kit/boot-routing.ts +392 -0
  107. package/src/kit/breakpoint-state.ts +43 -0
  108. package/src/kit/build-identity.ts +16 -0
  109. package/src/kit/bytes-codec.ts +62 -0
  110. package/src/kit/cancellation-reason.ts +58 -0
  111. package/src/kit/canvas-frames.ts +88 -0
  112. package/src/kit/capture-camera-pose.ts +77 -0
  113. package/src/kit/capture-size.ts +88 -0
  114. package/src/kit/chrome-registry.ts +159 -0
  115. package/src/kit/chrome-slot-registry.ts +91 -0
  116. package/src/kit/collaboration-client.ts +264 -0
  117. package/src/kit/collaboration-presence.ts +41 -0
  118. package/src/kit/command-dispatch.ts +19 -0
  119. package/src/kit/command-listener.ts +2182 -0
  120. package/src/kit/command-registry.ts +71 -0
  121. package/src/kit/component-board-registry.ts +205 -0
  122. package/src/kit/component-states-registry.ts +199 -0
  123. package/src/kit/components/AlignToolbar.tsx +204 -0
  124. package/src/kit/components/ApplicationMenus.tsx +372 -0
  125. package/src/kit/components/AssetEditorShell.tsx +216 -0
  126. package/src/kit/components/AssetInspectorToolSection.tsx +124 -0
  127. package/src/kit/components/BoardRulers.tsx +354 -0
  128. package/src/kit/components/CanvasAddNodeDialogs.tsx +529 -0
  129. package/src/kit/components/CanvasSceneViewport.tsx +1195 -0
  130. package/src/kit/components/ChromeSlot.tsx +20 -0
  131. package/src/kit/components/CodeView.tsx +470 -0
  132. package/src/kit/components/CompactInspectorShell.tsx +39 -0
  133. package/src/kit/components/ConsolePanel.tsx +273 -0
  134. package/src/kit/components/GameplaySessionTimeline.tsx +295 -0
  135. package/src/kit/components/InspectionProjection.tsx +932 -0
  136. package/src/kit/components/Inspector.tsx +270 -0
  137. package/src/kit/components/InspectorCanvasPreview.tsx +35 -0
  138. package/src/kit/components/InspectorFieldsSection.tsx +290 -0
  139. package/src/kit/components/InspectorStoriesSection.tsx +92 -0
  140. package/src/kit/components/InspectorToolSection.tsx +96 -0
  141. package/src/kit/components/InspectorTransformSection.tsx +245 -0
  142. package/src/kit/components/LightExplorerPanel.tsx +433 -0
  143. package/src/kit/components/MediaProperties.tsx +145 -0
  144. package/src/kit/components/ProjectHeader.tsx +328 -0
  145. package/src/kit/components/ReactCanvasControls.tsx +358 -0
  146. package/src/kit/components/RootSelectionOverlay.tsx +3688 -0
  147. package/src/kit/components/RootTextEditor.tsx +79 -0
  148. package/src/kit/components/SaveStatus.tsx +70 -0
  149. package/src/kit/components/SurfaceCrashBoundary.tsx +105 -0
  150. package/src/kit/components/SurfaceStateOverlay.tsx +24 -0
  151. package/src/kit/components/ToolContributionSurfaces.tsx +49 -0
  152. package/src/kit/components/ToolHost.tsx +380 -0
  153. package/src/kit/components/Toolbar.tsx +811 -0
  154. package/src/kit/components/TransientHint.tsx +44 -0
  155. package/src/kit/components/VersionControlSection.tsx +470 -0
  156. package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
  157. package/src/kit/components/VolterLogo.tsx +18 -0
  158. package/src/kit/components/WorktreeSwitcher.tsx +712 -0
  159. package/src/kit/components/account-documents.tsx +1162 -0
  160. package/src/kit/components/asset-documents.tsx +794 -0
  161. package/src/kit/components/asset-editor-persistence.ts +216 -0
  162. package/src/kit/components/asset-selection-section.tsx +545 -0
  163. package/src/kit/components/asset-thumbnails.tsx +307 -0
  164. package/src/kit/components/asset-viewers/AudioViewer.tsx +201 -0
  165. package/src/kit/components/asset-viewers/GenericJsonViewer.tsx +102 -0
  166. package/src/kit/components/asset-viewers/ImageViewer.tsx +300 -0
  167. package/src/kit/components/asset-viewers/JsonAssetDocument.tsx +98 -0
  168. package/src/kit/components/asset-viewers/OnlineAssetDetail.tsx +426 -0
  169. package/src/kit/components/asset-viewers/SourceAssetViewer.tsx +356 -0
  170. package/src/kit/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
  171. package/src/kit/components/asset-viewers/VideoViewer.tsx +101 -0
  172. package/src/kit/components/asset-viewers/shader-source.ts +144 -0
  173. package/src/kit/components/board-guides.ts +150 -0
  174. package/src/kit/components/canvas-scene-hotkeys.ts +37 -0
  175. package/src/kit/components/canvas-temporary-pivot.ts +34 -0
  176. package/src/kit/components/core-utilities.tsx +94 -0
  177. package/src/kit/components/inspector-preview-section.tsx +223 -0
  178. package/src/kit/components/inspector-revert-label.ts +20 -0
  179. package/src/kit/components/inspector-selection.ts +42 -0
  180. package/src/kit/components/inspector-stories-gating.ts +171 -0
  181. package/src/kit/components/inspector-transform-subject.ts +11 -0
  182. package/src/kit/components/inspector-transform.ts +88 -0
  183. package/src/kit/components/kind-documents.tsx +544 -0
  184. package/src/kit/components/primitives/DraftColorInput.tsx +74 -0
  185. package/src/kit/components/project-tool-documents.tsx +402 -0
  186. package/src/kit/components/scene-documents.tsx +221 -0
  187. package/src/kit/components/status-contributions.tsx +407 -0
  188. package/src/kit/components/tool-documents.tsx +302 -0
  189. package/src/kit/components/tool-schema-form.tsx +262 -0
  190. package/src/kit/components/use-after-paint.ts +41 -0
  191. package/src/kit/components/use-project-image-assets.ts +86 -0
  192. package/src/kit/components/workspace-history.ts +32 -0
  193. package/src/kit/components/world-documents.tsx +570 -0
  194. package/src/kit/components/world-overlay-gestures.ts +1939 -0
  195. package/src/kit/composite-screenshot.ts +2238 -0
  196. package/src/kit/content-entry-source-registry.ts +184 -0
  197. package/src/kit/contribution-surfaces.ts +48 -0
  198. package/src/kit/coverage/canvas-reveal.ts +192 -0
  199. package/src/kit/coverage/design-time-surfaces.ts +101 -0
  200. package/src/kit/coverage/ontology-invariants.ts +466 -0
  201. package/src/kit/coverage/session-vitals.ts +503 -0
  202. package/src/kit/crash-null-boundary.ts +36 -0
  203. package/src/kit/creation-site-edit.ts +1491 -0
  204. package/src/kit/creation-site-registry.ts +160 -0
  205. package/src/kit/delegate-harness-registry.ts +134 -0
  206. package/src/kit/document-areas.ts +70 -0
  207. package/src/kit/document-context-registry.ts +193 -0
  208. package/src/kit/document-open-registry.ts +200 -0
  209. package/src/kit/document-play-extension.ts +221 -0
  210. package/src/kit/document-preview-source.ts +20 -0
  211. package/src/kit/document-renderer-session.ts +138 -0
  212. package/src/kit/document-stage-sessions.ts +26 -0
  213. package/src/kit/document-viewports.ts +120 -0
  214. package/src/kit/editor-api.ts +46 -0
  215. package/src/kit/editor-chrome-capture.ts +136 -0
  216. package/src/kit/editor-commands.ts +176 -0
  217. package/src/kit/editor-console.ts +580 -0
  218. package/src/kit/editor-current-view.ts +56 -0
  219. package/src/kit/editor-document-probe.ts +1168 -0
  220. package/src/kit/editor-git-client.ts +115 -0
  221. package/src/kit/editor-hotkeys.ts +728 -0
  222. package/src/kit/editor-lease-view.ts +39 -0
  223. package/src/kit/editor-lease.ts +415 -0
  224. package/src/kit/editor-mode.ts +19 -0
  225. package/src/kit/editor-notifications.ts +140 -0
  226. package/src/kit/editor-presence.ts +563 -0
  227. package/src/kit/editor-presentation-activity.ts +58 -0
  228. package/src/kit/editor-presentation-notice.ts +42 -0
  229. package/src/kit/editor-runtime.tsx +147 -0
  230. package/src/kit/editor-server-response.ts +86 -0
  231. package/src/kit/editor-session-attribution.ts +85 -0
  232. package/src/kit/editor-session-mode.ts +54 -0
  233. package/src/kit/editor-state-facets.ts +74 -0
  234. package/src/kit/editor-view-presentation.ts +777 -0
  235. package/src/kit/environment-images.ts +58 -0
  236. package/src/kit/eyedropper-session.ts +60 -0
  237. package/src/kit/files/file-provider.ts +62 -0
  238. package/src/kit/files/project-files.ts +270 -0
  239. package/src/kit/finders/index.ts +137 -0
  240. package/src/kit/finders/scenes-from-entrypoint-selection.ts +387 -0
  241. package/src/kit/frame/frame-parts.ts +30 -0
  242. package/src/kit/framed-document-capture.ts +34 -0
  243. package/src/kit/game-globals-prelude.ts +143 -0
  244. package/src/kit/game-surface-defaults.ts +33 -0
  245. package/src/kit/gameplay-dom-recording.ts +318 -0
  246. package/src/kit/gameplay-export-state.ts +14 -0
  247. package/src/kit/gameplay-replay.ts +417 -0
  248. package/src/kit/gameplay-session-time.ts +9 -0
  249. package/src/kit/gameplay-sessions.ts +204 -0
  250. package/src/kit/hierarchy-component-marks.ts +298 -0
  251. package/src/kit/hierarchy-internals.ts +197 -0
  252. package/src/kit/hierarchy-kind-icon.ts +217 -0
  253. package/src/kit/hierarchy-menu-registry.ts +67 -0
  254. package/src/kit/hierarchy-node-rows.ts +307 -0
  255. package/src/kit/hierarchy-panel-view.ts +280 -0
  256. package/src/kit/hierarchy-projection.ts +76 -0
  257. package/src/kit/hierarchy-row-media.ts +45 -0
  258. package/src/kit/hierarchy-row-model.ts +308 -0
  259. package/src/kit/hierarchy-rows.ts +11 -0
  260. package/src/kit/hierarchy-walk.ts +86 -0
  261. package/src/kit/history/editor-session.ts +25 -0
  262. package/src/kit/history/history-commands.ts +147 -0
  263. package/src/kit/history/history-delegate.ts +187 -0
  264. package/src/kit/history/history-limit-notices.ts +43 -0
  265. package/src/kit/history/history-service.ts +1189 -0
  266. package/src/kit/history/persistence-coordinator.ts +35 -0
  267. package/src/kit/history/project-file-history.ts +386 -0
  268. package/src/kit/history/project-root-history-backends.ts +139 -0
  269. package/src/kit/history/resource-registry.ts +209 -0
  270. package/src/kit/history/snapshot-store.ts +103 -0
  271. package/src/kit/history/source-history-backend.ts +546 -0
  272. package/src/kit/history-types.ts +124 -0
  273. package/src/kit/hmr-registration-group.ts +67 -0
  274. package/src/kit/hmr-stable-react-context.ts +23 -0
  275. package/src/kit/hotkeys.ts +190 -0
  276. package/src/kit/inference-diagnostics.ts +69 -0
  277. package/src/kit/initial-project.ts +80 -0
  278. package/src/kit/inspection/active-subject.ts +571 -0
  279. package/src/kit/inspection/active-surface.ts +142 -0
  280. package/src/kit/inspection/compose-subject.ts +1055 -0
  281. package/src/kit/inspection/compose.ts +7 -0
  282. package/src/kit/inspection/display.ts +171 -0
  283. package/src/kit/inspection/document-subject.ts +109 -0
  284. package/src/kit/inspection/game-subject.ts +85 -0
  285. package/src/kit/inspection/null-subject.ts +119 -0
  286. package/src/kit/inspection/serialize.ts +357 -0
  287. package/src/kit/inspection/use-active-inspection.ts +180 -0
  288. package/src/kit/inspection-model.ts +542 -0
  289. package/src/kit/inspection-node-media.ts +58 -0
  290. package/src/kit/inspector-presentation.ts +203 -0
  291. package/src/kit/inspector-property-grouping.ts +64 -0
  292. package/src/kit/inspector-section-registry.ts +221 -0
  293. package/src/kit/instance-source-actions.ts +163 -0
  294. package/src/kit/js-heap.ts +71 -0
  295. package/src/kit/key-actions.ts +91 -0
  296. package/src/kit/keymap-presets.ts +428 -0
  297. package/src/kit/layout-policy.ts +31 -0
  298. package/src/kit/light-explorer-model.ts +134 -0
  299. package/src/kit/live-canvas-frame.ts +55 -0
  300. package/src/kit/live-document.ts +296 -0
  301. package/src/kit/live-gesture-lock.ts +50 -0
  302. package/src/kit/live-seam-evidence.ts +11 -0
  303. package/src/kit/live-session-registry.ts +220 -0
  304. package/src/kit/live-transition.ts +391 -0
  305. package/src/kit/manifest-project.ts +107 -0
  306. package/src/kit/module-fetch-diagnosis.ts +192 -0
  307. package/src/kit/mount-failure-report.ts +154 -0
  308. package/src/kit/native-selection-style.ts +497 -0
  309. package/src/kit/object3d-document-write-policy.ts +137 -0
  310. package/src/kit/packaged-runtime.ts +108 -0
  311. package/src/kit/palettes/maya.palette.json +57 -0
  312. package/src/kit/palettes/substance.palette.json +57 -0
  313. package/src/kit/performance-profiler.ts +367 -0
  314. package/src/kit/performance-sources.ts +69 -0
  315. package/src/kit/photograph-notice.ts +141 -0
  316. package/src/kit/play-boot-phase.ts +166 -0
  317. package/src/kit/play-camera-flight.ts +35 -0
  318. package/src/kit/png-encode.worker.ts +26 -0
  319. package/src/kit/presentation-surface.ts +248 -0
  320. package/src/kit/product-command.ts +90 -0
  321. package/src/kit/project-adapter.ts +1140 -0
  322. package/src/kit/project-asset-refresh.ts +23 -0
  323. package/src/kit/project-asset-roots.ts +68 -0
  324. package/src/kit/project-local-state.ts +151 -0
  325. package/src/kit/project-manager.ts +243 -0
  326. package/src/kit/project-module-changes.ts +201 -0
  327. package/src/kit/project-module-split.ts +270 -0
  328. package/src/kit/project-play-layers.ts +25 -0
  329. package/src/kit/project-provenance.ts +115 -0
  330. package/src/kit/project-ready.ts +42 -0
  331. package/src/kit/project-shape.ts +68 -0
  332. package/src/kit/project-tools.ts +107 -0
  333. package/src/kit/projection-types.ts +44 -0
  334. package/src/kit/readiness.ts +113 -0
  335. package/src/kit/renderer-resource-counts.ts +27 -0
  336. package/src/kit/reported-play-state.ts +90 -0
  337. package/src/kit/resolve-contributed-command.ts +14 -0
  338. package/src/kit/resolve-relative-specifier.ts +33 -0
  339. package/src/kit/retained-document-states.ts +91 -0
  340. package/src/kit/scene-document-plan.ts +320 -0
  341. package/src/kit/scene-live-open.ts +210 -0
  342. package/src/kit/scoped-game-css.ts +152 -0
  343. package/src/kit/served-url.ts +5 -0
  344. package/src/kit/session-close.ts +17 -0
  345. package/src/kit/session-tombstone.ts +127 -0
  346. package/src/kit/settings/settings-provider.ts +82 -0
  347. package/src/kit/settings-store.ts +348 -0
  348. package/src/kit/shell-document-state.ts +27 -0
  349. package/src/kit/shell-store-door.ts +45 -0
  350. package/src/kit/shell-store.ts +722 -0
  351. package/src/kit/source-conflict.ts +122 -0
  352. package/src/kit/stage-context.ts +377 -0
  353. package/src/kit/stage-invalidation.ts +25 -0
  354. package/src/kit/stage-store-registry.ts +69 -0
  355. package/src/kit/startup-failure.ts +80 -0
  356. package/src/kit/state-report-deferral.ts +73 -0
  357. package/src/kit/storage/host-files-storage.ts +97 -0
  358. package/src/kit/storage/http-storage.ts +174 -0
  359. package/src/kit/storage/index.ts +75 -0
  360. package/src/kit/storage/mem-storage.ts +158 -0
  361. package/src/kit/storage/path-lock.ts +44 -0
  362. package/src/kit/storage/paths.ts +26 -0
  363. package/src/kit/storage-types.ts +127 -0
  364. package/src/kit/stories/StoryPreviewMount.tsx +306 -0
  365. package/src/kit/stories/compose-project-stories.ts +255 -0
  366. package/src/kit/stories/prefabs-finder.ts +54 -0
  367. package/src/kit/stories/prefabs-from-stories.ts +182 -0
  368. package/src/kit/stories/project-story-regions.ts +24 -0
  369. package/src/kit/stories/story-capture.ts +579 -0
  370. package/src/kit/stories/story-declared-medium.ts +126 -0
  371. package/src/kit/stories/story-discovery.ts +176 -0
  372. package/src/kit/stories/story-dom-runtime.ts +78 -0
  373. package/src/kit/stories/story-grouping.ts +111 -0
  374. package/src/kit/stories/story-mount-turn.ts +27 -0
  375. package/src/kit/stories/story-presentation.ts +215 -0
  376. package/src/kit/stories/story-preview-component.ts +7 -0
  377. package/src/kit/stories/story-registry.ts +530 -0
  378. package/src/kit/stories-scope.ts +35 -0
  379. package/src/kit/story-document-openers.ts +36 -0
  380. package/src/kit/story-thumbnails.ts +47 -0
  381. package/src/kit/surface-keyboard.ts +101 -0
  382. package/src/kit/surface-state.ts +135 -0
  383. package/src/kit/system-seam-evidence.ts +72 -0
  384. package/src/kit/tab-census.ts +202 -0
  385. package/src/kit/tab-lifecycle-client.ts +227 -0
  386. package/src/kit/theme-library.ts +897 -0
  387. package/src/kit/theme-preference.ts +429 -0
  388. package/src/kit/three-viewport-presentation.ts +23 -0
  389. package/src/kit/tool-contribution-play.ts +74 -0
  390. package/src/kit/tool-loader.ts +1918 -0
  391. package/src/kit/transform-mode-request.ts +66 -0
  392. package/src/kit/transient-hint.ts +78 -0
  393. package/src/kit/transport-strip.tsx +174 -0
  394. package/src/kit/ui-source/adapter-region-includes.ts +238 -0
  395. package/src/kit/ui-source/file-region-resolver.ts +302 -0
  396. package/src/kit/ui-source/inspect.ts +775 -0
  397. package/src/kit/ui-source/source-write-backend.ts +605 -0
  398. package/src/kit/ui-source/tier-source-write-backend.ts +279 -0
  399. package/src/kit/user-local-state.ts +105 -0
  400. package/src/kit/viewport-activation-timings.ts +840 -0
  401. package/src/kit/viewport-editor-controls.ts +22 -0
  402. package/src/kit/viewport-presentation.ts +668 -0
  403. package/src/kit/viewport-surface-status.tsx +55 -0
  404. package/src/kit/wait-until.ts +37 -0
  405. package/src/kit/worker-call-metrics.ts +166 -0
  406. package/src/kit/workspace-areas.ts +191 -0
  407. package/src/kit/workspace-aux-commands.ts +11 -0
  408. package/src/kit/workspace-available-documents.ts +142 -0
  409. package/src/kit/workspace-core-utilities.ts +31 -0
  410. package/src/kit/workspace-document-ids.ts +59 -0
  411. package/src/kit/workspace-document-registry.ts +624 -0
  412. package/src/kit/workspace-document-restore.ts +146 -0
  413. package/src/kit/workspace-host-commands.ts +141 -0
  414. package/src/kit/workspace-persistence-gate.ts +40 -0
  415. package/src/kit/workspace-play-utilities.ts +44 -0
  416. package/src/kit/workspace-presets.ts +446 -0
  417. package/src/kit/workspace-regions.ts +276 -0
  418. package/src/kit/workspace-static-panels.ts +73 -0
  419. package/src/kit/workspace-status-registry.ts +121 -0
  420. package/src/kit/workspace-storage.ts +35 -0
  421. package/src/kit/workspace-style.ts +226 -0
  422. package/src/kit/workspace-utility-commands.ts +74 -0
  423. package/src/kit/workspace-utility-registry.ts +263 -0
  424. package/src/kit/world-adoption-event.ts +23 -0
  425. package/src/kit/world-adoption.ts +115 -0
  426. package/src/kit/world-canvas-viewport-state.ts +35 -0
  427. package/src/kit/world-document-routing.ts +104 -0
  428. package/src/kit/world-pan-state.ts +198 -0
  429. package/src/kit/write-pipe.ts +173 -0
  430. package/src/layout-arrangements.ts +5 -0
  431. package/src/layouts.tsx +108 -0
  432. package/src/looks.ts +16 -0
  433. package/src/project/output-roots.ts +73 -0
  434. package/src/project/tab-census.ts +155 -0
  435. package/src/project-tool-catalog.ts +104 -0
  436. package/src/selection.tsx +107 -0
  437. package/src/services.ts +18 -0
  438. package/src/session/build-report.ts +22 -0
  439. package/src/session/collaboration-types.ts +262 -0
  440. package/src/session/command-table.ts +327 -0
  441. package/src/session/discovery.ts +100 -0
  442. package/src/session/editor-brand.ts +48 -0
  443. package/src/session/editor-compatibility.ts +329 -0
  444. package/src/session/editor-control-lifecycle.ts +68 -0
  445. package/src/session/editor-control-protocol.ts +5 -0
  446. package/src/session/entrypoint-selection-readers.ts +66 -0
  447. package/src/session/entrypoint-selection-source.ts +120 -0
  448. package/src/session/game-css-scope.ts +30 -0
  449. package/src/session/hosted-attachment.ts +225 -0
  450. package/src/session/limited-view.ts +82 -0
  451. package/src/session/product-create.ts +24 -0
  452. package/src/session/product-locator.ts +478 -0
  453. package/src/session/project-module-url.ts +242 -0
  454. package/src/session/project-serving.ts +164 -0
  455. package/src/session/project-upgrade.ts +669 -0
  456. package/src/session/registry-format.ts +210 -0
  457. package/src/session/relative-path-guard.ts +56 -0
  458. package/src/session/scoped-game-css.ts +461 -0
  459. package/src/session/source-glob.ts +15 -0
  460. package/src/session/tool-contribution-convention.ts +123 -0
  461. package/src/session/workbench-locator.ts +712 -0
  462. package/src/session.ts +41 -0
  463. package/src/share.ts +160 -0
  464. package/src/source-analysis.ts +28 -0
  465. package/src/source-authoring.ts +439 -0
  466. package/src/tools/errors.ts +91 -0
  467. package/src/tools/provider-execution.ts +70 -0
  468. package/src/tools/registry.ts +341 -0
  469. package/src/tools/types.ts +159 -0
  470. package/src/transport.ts +100 -0
  471. package/src/types.ts +1693 -0
  472. package/src/views.ts +164 -0
  473. package/src/widgets/design-system.ts +93 -0
  474. package/src/widgets/editor-appearance.ts +151 -0
  475. package/src/widgets/editor-material.ts +83 -0
  476. package/src/widgets/icon-set-registry.ts +105 -0
  477. package/src/widgets/index.ts +71 -0
  478. package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
  479. package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
  480. package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
  481. package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
  482. package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
  483. package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
  484. package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
  485. package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
  486. package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
  487. package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
  488. package/src/widgets/inspector-widgets/color-utils.ts +201 -0
  489. package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
  490. package/src/widgets/inspector-widgets/index.ts +25 -0
  491. package/src/widgets/inspector-widgets/shared.tsx +140 -0
  492. package/src/widgets/interactive-edit-scope.ts +33 -0
  493. package/src/widgets/patterns/Dialog.tsx +140 -0
  494. package/src/widgets/patterns/Fields.tsx +44 -0
  495. package/src/widgets/patterns/List.tsx +25 -0
  496. package/src/widgets/patterns/StateSurface.tsx +40 -0
  497. package/src/widgets/patterns/Surfaces.tsx +122 -0
  498. package/src/widgets/patterns/Tabs.tsx +80 -0
  499. package/src/widgets/patterns/Toolbar.tsx +72 -0
  500. package/src/widgets/patterns/Tree.tsx +72 -0
  501. package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
  502. package/src/widgets/primitives/Button.tsx +62 -0
  503. package/src/widgets/primitives/ColorInput.tsx +78 -0
  504. package/src/widgets/primitives/DraftTextInput.tsx +63 -0
  505. package/src/widgets/primitives/EditorIcon.tsx +157 -0
  506. package/src/widgets/primitives/FormControls.tsx +88 -0
  507. package/src/widgets/primitives/HoverPreview.tsx +96 -0
  508. package/src/widgets/primitives/JsonInput.tsx +113 -0
  509. package/src/widgets/primitives/Layout.tsx +100 -0
  510. package/src/widgets/primitives/Menu.tsx +161 -0
  511. package/src/widgets/primitives/NumberInput.tsx +169 -0
  512. package/src/widgets/primitives/Panel.tsx +80 -0
  513. package/src/widgets/primitives/SectionHeader.tsx +77 -0
  514. package/src/widgets/primitives/Text.tsx +54 -0
  515. package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
  516. package/src/widgets/primitives/Tooltip.tsx +204 -0
  517. package/src/widgets/primitives/Vec3Input.tsx +70 -0
  518. package/src/widgets/primitives/banner-tones.ts +32 -0
  519. package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
  520. package/src/widgets/primitives/editor-icons.ts +254 -0
  521. package/src/widgets/primitives/panel-header-styles.ts +42 -0
  522. package/src/widgets/theme.ts +2841 -0
  523. package/src/widgets/z-index.ts +25 -0
@@ -0,0 +1,1195 @@
1
+ import {
2
+ faBullseye,
3
+ faCheck,
4
+ faCrosshairs,
5
+ faHand,
6
+ faLayerGroup,
7
+ faListUl,
8
+ faLock,
9
+ faLockOpen,
10
+ faRulerCombined,
11
+ faMagnifyingGlassMinus,
12
+ faMagnifyingGlassPlus,
13
+ } from '@fortawesome/free-solid-svg-icons';
14
+ import {
15
+ AnchoredMenu,
16
+ Button,
17
+ EditorIcon,
18
+ FloatingToolbar,
19
+ IconButton,
20
+ MenuItem,
21
+ MenuSeparator,
22
+ MenuSubmenu,
23
+ Tooltip,
24
+ } from '@volter/sdk/widgets';
25
+ import type { AuthoringAdapter, DOMRectLike } from '@volter/project/adapter';
26
+ import {
27
+ type PointerEvent as ReactPointerEvent,
28
+ type RefObject,
29
+ useCallback,
30
+ useEffect,
31
+ useRef,
32
+ useState,
33
+ useSyncExternalStore,
34
+ } from 'react';
35
+ import { collectAllNodeIds } from '@volter/sdk/kit/authoring/active-adapter';
36
+ import { SCENE_MAX_ZOOM, SCENE_MIN_ZOOM } from '@volter/sdk/kit/authoring/react-canvas-navigation';
37
+ import {
38
+ addCanvasSceneGuide,
39
+ canvasSceneGuideRevision,
40
+ canvasSceneGuides,
41
+ clearCanvasSceneGuides,
42
+ moveCanvasSceneGuide,
43
+ removeCanvasSceneGuide,
44
+ subscribeCanvasSceneGuides,
45
+ } from '../authoring/canvas-scene-guides';
46
+ import type { RootViewController } from '@volter/sdk/kit/world-pan-state';
47
+ import { useEditorStore } from '@volter/sdk/kit/editor-runtime';
48
+ import {
49
+ bindViewPresentation,
50
+ setViewDrafting,
51
+ setViewGridVisible,
52
+ viewDrafting,
53
+ subscribeViewportPresentation,
54
+ viewGridVisible,
55
+ viewportPresentationVersion,
56
+ } from '@volter/sdk/kit/viewport-presentation';
57
+ import { getCurrentProject } from '@volter/sdk/kit/project-manager';
58
+ import { ToolStrip } from '@volter/sdk/kit/components/Toolbar';
59
+ import { bindCanvasSceneKeys } from './canvas-scene-hotkeys';
60
+ import { setTemporaryPivot } from './canvas-temporary-pivot';
61
+ import { markCanvasStageDocument } from '@volter/sdk/kit/stage-context';
62
+ import { TransientHintOverlay } from '@volter/sdk/kit/components/TransientHint';
63
+ import { ViewportPickMenu } from '@volter/sdk/kit/components/RootSelectionOverlay';
64
+ import {
65
+ EDGE_SNAP_THRESHOLD_PX,
66
+ frameForId,
67
+ rectFrame,
68
+ rectForId,
69
+ snapPivotPoint,
70
+ spatialHandlesForId,
71
+ } from '@volter/sdk/kit/components/world-overlay-gestures';
72
+ import { pickCandidates } from '@volter/sdk/kit/authoring/layered-pick';
73
+
74
+ interface Bounds {
75
+ x: number;
76
+ y: number;
77
+ width: number;
78
+ height: number;
79
+ }
80
+
81
+ /** Neutral spatial-workspace ground shared in spirit with the Three Scene.
82
+ * Unlike the component boards' media background, this is editor viewport
83
+ * dressing: native Pixi content renders transparently above it. */
84
+ export const CANVAS_SCENE_BACKGROUND = '#111214';
85
+
86
+ /** Keep the drafting grid legible without divorcing it from world units.
87
+ * Every visible interval is a power-of-two multiple/division of the base
88
+ * cell, so grid intersections never swim relative to authored points. */
89
+ function adaptiveGridWorldSpacing(zoom: number, base = 32): number {
90
+ let worldUnits = base > 0 ? base : 32;
91
+ while (worldUnits * zoom < 12) worldUnits *= 2;
92
+ while (worldUnits * zoom > 48) worldUnits /= 2;
93
+ return worldUnits;
94
+ }
95
+
96
+ function unionRects(rects: readonly DOMRectLike[]): Bounds | null {
97
+ if (rects.length === 0) return null;
98
+ let minX = Number.POSITIVE_INFINITY;
99
+ let minY = Number.POSITIVE_INFINITY;
100
+ let maxX = Number.NEGATIVE_INFINITY;
101
+ let maxY = Number.NEGATIVE_INFINITY;
102
+ for (const rect of rects) {
103
+ minX = Math.min(minX, rect.x);
104
+ minY = Math.min(minY, rect.y);
105
+ maxX = Math.max(maxX, rect.x + rect.width);
106
+ maxY = Math.max(maxY, rect.y + rect.height);
107
+ }
108
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
109
+ }
110
+
111
+ function frameBounds(
112
+ container: HTMLElement,
113
+ view: RootViewController,
114
+ bounds: Bounds | null,
115
+ ): void {
116
+ if (!bounds || !(bounds.width > 0) || !(bounds.height > 0)) return;
117
+ const availableWidth = Math.max(1, container.clientWidth - 96);
118
+ const availableHeight = Math.max(1, container.clientHeight - 96);
119
+ const zoom = Math.min(
120
+ 4,
121
+ Math.max(SCENE_MIN_ZOOM, Math.min(availableWidth / bounds.width, availableHeight / bounds.height)),
122
+ );
123
+ view.setView(
124
+ container.clientWidth / 2 - (bounds.x + bounds.width / 2) * zoom,
125
+ container.clientHeight / 2 - (bounds.y + bounds.height / 2) * zoom,
126
+ zoom,
127
+ );
128
+ }
129
+
130
+ /** Infinite XY drafting overlay for a native Pixi stage. The axes and grid
131
+ * follow the editor camera; neither exists in the game's source or runtime.
132
+ * It sits above source-owned backgrounds (which otherwise erase any useful
133
+ * drafting reference) and below selection/transform chrome. */
134
+ export function CanvasSceneBackdrop({ view, documentId }: { view: RootViewController; documentId: string }) {
135
+ const store = useEditorStore();
136
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
137
+ // The 2D scene's grid switch is its VIEW's, like every stage's (`kit/viewport-presentation`):
138
+ // bound under the host document's own id, so a door that toggles the active view's grid
139
+ // reaches this one, and two canvases never share a switch.
140
+ useEffect(() => bindViewPresentation(documentId, 'canvas'), [documentId]);
141
+ useSyncExternalStore(subscribeViewportPresentation, viewportPresentationVersion);
142
+ const showGrid = viewGridVisible(documentId);
143
+ const drafting = viewDrafting(documentId);
144
+ const resolution = getCurrentProject()?.config.resolution;
145
+ const pose = useSyncExternalStore(view.subscribe, view.get, view.get);
146
+ useSyncExternalStore(
147
+ useCallback((listener) => subscribeCanvasSceneGuides(view, listener), [view]),
148
+ useCallback(() => canvasSceneGuideRevision(view), [view]),
149
+ );
150
+ const guides = canvasSceneGuides(view);
151
+ // The grid is drawn at the snap step from its offset, as Godot's is: a snapped move lands on a
152
+ // drawn line (zoomed out far enough, every other line thins away by powers of two).
153
+ const grid = store.snap2D;
154
+ const minor = adaptiveGridWorldSpacing(pose.zoom, grid.step) * pose.zoom;
155
+ const major = minor * Math.max(1, Math.round(grid.primaryEvery));
156
+ const gridX = pose.x + grid.offsetX * pose.zoom;
157
+ const gridY = pose.y + grid.offsetY * pose.zoom;
158
+ return (
159
+ <div
160
+ data-testid="canvas-scene-backdrop"
161
+ style={{
162
+ position: 'absolute',
163
+ inset: 0,
164
+ zIndex: 2,
165
+ overflow: 'hidden',
166
+ pointerEvents: 'none',
167
+ }}
168
+ >
169
+ {showGrid && (!drafting.gridWhenSnapping || store.snapEnabled) && (
170
+ <div
171
+ data-testid="canvas-scene-grid"
172
+ style={{
173
+ position: 'absolute',
174
+ inset: 0,
175
+ backgroundImage:
176
+ 'linear-gradient(rgba(255,255,255,.075) 1px, transparent 1px),' +
177
+ 'linear-gradient(90deg, rgba(255,255,255,.075) 1px, transparent 1px),' +
178
+ 'linear-gradient(rgba(255,255,255,.14) 1px, transparent 1px),' +
179
+ 'linear-gradient(90deg, rgba(255,255,255,.14) 1px, transparent 1px)',
180
+ backgroundPosition: `${gridX}px ${gridY}px`,
181
+ backgroundSize: `${minor}px ${minor}px, ${minor}px ${minor}px, ${major}px ${major}px, ${major}px ${major}px`,
182
+ }}
183
+ />
184
+ )}
185
+ {drafting.viewport && resolution && resolution.width > 0 && resolution.height > 0 && (
186
+ // The game's viewport: the manifest's resolution from the origin, as Godot draws its
187
+ // project window size (View › Show Viewport).
188
+ <div
189
+ data-testid="canvas-scene-viewport-rect"
190
+ style={{
191
+ position: 'absolute',
192
+ left: pose.x,
193
+ top: pose.y,
194
+ width: resolution.width * pose.zoom,
195
+ height: resolution.height * pose.zoom,
196
+ boxSizing: 'border-box',
197
+ borderStyle: 'solid',
198
+ borderWidth: 1,
199
+ borderColor: 'rgba(160, 120, 255, .75)',
200
+ }}
201
+ />
202
+ )}
203
+ {drafting.origin && (
204
+ <>
205
+ <div
206
+ data-testid="canvas-scene-origin-y"
207
+ style={{
208
+ position: 'absolute',
209
+ left: 0,
210
+ right: 0,
211
+ top: pose.y,
212
+ height: 1,
213
+ background: 'rgba(238, 91, 91, .8)',
214
+ }}
215
+ />
216
+ <div
217
+ data-testid="canvas-scene-origin-x"
218
+ style={{
219
+ position: 'absolute',
220
+ top: 0,
221
+ bottom: 0,
222
+ left: pose.x,
223
+ width: 1,
224
+ background: 'rgba(87, 199, 126, .8)',
225
+ }}
226
+ />
227
+ <div
228
+ style={{
229
+ position: 'absolute',
230
+ left: pose.x + 6,
231
+ top: pose.y + 6,
232
+ color: 'rgba(255,255,255,.68)',
233
+ font: '10px ui-monospace, SFMono-Regular, Menlo, monospace',
234
+ }}
235
+ >
236
+ 0, 0
237
+ </div>
238
+ </>
239
+ )}
240
+ {drafting.guides &&
241
+ guides.map((guide) => (
242
+ <div
243
+ key={guide.id}
244
+ data-testid={`canvas-scene-guide-${guide.axis}`}
245
+ title={`${guide.axis.toUpperCase()} ${Math.round(guide.value * 100) / 100} · drag to move · right-click to remove`}
246
+ onContextMenu={(event) => {
247
+ event.preventDefault();
248
+ removeCanvasSceneGuide(view, guide.id);
249
+ }}
250
+ onPointerDown={(event) => {
251
+ event.preventDefault();
252
+ event.stopPropagation();
253
+ const host = event.currentTarget.parentElement?.getBoundingClientRect();
254
+ if (!host) return;
255
+ const move = (next: PointerEvent) => {
256
+ const screen =
257
+ guide.axis === 'x' ? next.clientX - host.left : next.clientY - host.top;
258
+ const pan = view.get();
259
+ moveCanvasSceneGuide(
260
+ view,
261
+ guide.id,
262
+ (screen - (guide.axis === 'x' ? pan.x : pan.y)) / pan.zoom,
263
+ );
264
+ };
265
+ const end = () => {
266
+ window.removeEventListener('pointermove', move);
267
+ window.removeEventListener('pointerup', end);
268
+ };
269
+ window.addEventListener('pointermove', move);
270
+ window.addEventListener('pointerup', end, { once: true });
271
+ }}
272
+ style={{
273
+ position: 'absolute',
274
+ pointerEvents: 'auto',
275
+ cursor: guide.axis === 'x' ? 'ew-resize' : 'ns-resize',
276
+ ...(guide.axis === 'x'
277
+ ? {
278
+ left: pose.x + guide.value * pose.zoom - 4,
279
+ top: 20,
280
+ bottom: 0,
281
+ width: 9,
282
+ borderLeft: '1px solid rgba(74, 180, 255, .95)',
283
+ }
284
+ : {
285
+ top: pose.y + guide.value * pose.zoom - 4,
286
+ left: 20,
287
+ right: 0,
288
+ height: 9,
289
+ borderTop: '1px solid rgba(74, 180, 255, .95)',
290
+ }),
291
+ }}
292
+ />
293
+ ))}
294
+ {drafting.rulers && <CanvasSceneRulers view={view} />}
295
+ </div>
296
+ );
297
+ }
298
+
299
+ function CanvasSceneRulers({ view }: { view: RootViewController }) {
300
+ const pose = useSyncExternalStore(view.subscribe, view.get, view.get);
301
+ const horizontalRef = useRef<HTMLButtonElement>(null);
302
+ const verticalRef = useRef<HTMLButtonElement>(null);
303
+ const [extent, setExtent] = useState({ width: 0, height: 0 });
304
+
305
+ useEffect(() => {
306
+ const update = () => {
307
+ const width = horizontalRef.current?.clientWidth ?? 0;
308
+ const height = verticalRef.current?.clientHeight ?? 0;
309
+ setExtent((current) =>
310
+ current.width === width && current.height === height ? current : { width, height },
311
+ );
312
+ };
313
+ update();
314
+ if (typeof ResizeObserver === 'undefined') return;
315
+ const observer = new ResizeObserver(update);
316
+ if (horizontalRef.current) observer.observe(horizontalRef.current);
317
+ if (verticalRef.current) observer.observe(verticalRef.current);
318
+ return () => observer.disconnect();
319
+ }, []);
320
+
321
+ const ticksFor = (axis: 'x' | 'y', length: number) => {
322
+ if (!(length > 0)) return [];
323
+ const pan = axis === 'x' ? pose.x : pose.y;
324
+ const worldStep = adaptiveGridWorldSpacing(pose.zoom);
325
+ const screenStep = worldStep * pose.zoom;
326
+ const rulerStart = 20;
327
+ const first = Math.ceil((rulerStart - pan) / screenStep);
328
+ const last = Math.floor((rulerStart + length - pan) / screenStep);
329
+ return Array.from({ length: Math.max(0, last - first + 1) }, (_, offset) => {
330
+ const index = first + offset;
331
+ return {
332
+ major: index % 4 === 0,
333
+ screen: pan + index * screenStep - rulerStart,
334
+ value: index * worldStep,
335
+ };
336
+ });
337
+ };
338
+ const horizontalTicks = ticksFor('x', extent.width);
339
+ const verticalTicks = ticksFor('y', extent.height);
340
+
341
+ const beginGuide = (axis: 'x' | 'y', event: ReactPointerEvent<HTMLButtonElement>): void => {
342
+ event.preventDefault();
343
+ event.stopPropagation();
344
+ const host = event.currentTarget.parentElement?.getBoundingClientRect();
345
+ if (!host) return;
346
+ const valueAt = (clientX: number, clientY: number): number => {
347
+ const pose = view.get();
348
+ const screen = axis === 'x' ? clientX - host.left : clientY - host.top;
349
+ return (screen - (axis === 'x' ? pose.x : pose.y)) / pose.zoom;
350
+ };
351
+ const id = addCanvasSceneGuide(view, axis, valueAt(event.clientX, event.clientY));
352
+ const move = (next: PointerEvent) =>
353
+ moveCanvasSceneGuide(view, id, valueAt(next.clientX, next.clientY));
354
+ const end = () => {
355
+ window.removeEventListener('pointermove', move);
356
+ window.removeEventListener('pointerup', end);
357
+ };
358
+ window.addEventListener('pointermove', move);
359
+ window.addEventListener('pointerup', end, { once: true });
360
+ };
361
+ const rulerBase = {
362
+ position: 'absolute' as const,
363
+ pointerEvents: 'auto' as const,
364
+ backgroundColor: 'rgba(24, 26, 30, .96)',
365
+ border: 0,
366
+ borderColor: 'rgba(255,255,255,.14)',
367
+ boxSizing: 'border-box' as const,
368
+ };
369
+ return (
370
+ <>
371
+ <button
372
+ ref={horizontalRef}
373
+ type="button"
374
+ data-testid="canvas-scene-ruler-x"
375
+ aria-label="Horizontal ruler; drag to create a horizontal guide"
376
+ onPointerDown={(event) => beginGuide('y', event)}
377
+ style={{
378
+ ...rulerBase,
379
+ left: 20,
380
+ right: 0,
381
+ top: 0,
382
+ height: 20,
383
+ borderBottomStyle: 'solid',
384
+ borderBottomWidth: 1,
385
+ cursor: 'row-resize',
386
+ padding: 0,
387
+ overflow: 'hidden',
388
+ }}
389
+ >
390
+ {horizontalTicks.map((tick) => (
391
+ <span
392
+ key={tick.value}
393
+ aria-hidden="true"
394
+ data-ruler-axis="x"
395
+ data-ruler-value={tick.value}
396
+ style={{
397
+ position: 'absolute',
398
+ left: tick.screen,
399
+ bottom: 0,
400
+ width: 1,
401
+ height: tick.major ? 8 : 4,
402
+ background: tick.major ? 'rgba(255,255,255,.5)' : 'rgba(255,255,255,.28)',
403
+ pointerEvents: 'none',
404
+ }}
405
+ >
406
+ {tick.major && (
407
+ <span
408
+ style={{
409
+ position: 'absolute',
410
+ left: 3,
411
+ bottom: 5,
412
+ color: 'rgba(255,255,255,.62)',
413
+ font: '9px ui-monospace, SFMono-Regular, Menlo, monospace',
414
+ lineHeight: 1,
415
+ whiteSpace: 'nowrap',
416
+ }}
417
+ >
418
+ {tick.value}
419
+ </span>
420
+ )}
421
+ </span>
422
+ ))}
423
+ </button>
424
+ <button
425
+ ref={verticalRef}
426
+ type="button"
427
+ data-testid="canvas-scene-ruler-y"
428
+ aria-label="Vertical ruler; drag to create a vertical guide"
429
+ onPointerDown={(event) => beginGuide('x', event)}
430
+ style={{
431
+ ...rulerBase,
432
+ top: 20,
433
+ bottom: 0,
434
+ left: 0,
435
+ width: 20,
436
+ borderRightStyle: 'solid',
437
+ borderRightWidth: 1,
438
+ cursor: 'col-resize',
439
+ padding: 0,
440
+ overflow: 'hidden',
441
+ }}
442
+ >
443
+ {verticalTicks.map((tick) => (
444
+ <span
445
+ key={tick.value}
446
+ aria-hidden="true"
447
+ data-ruler-axis="y"
448
+ data-ruler-value={tick.value}
449
+ style={{
450
+ position: 'absolute',
451
+ top: tick.screen,
452
+ right: 0,
453
+ width: tick.major ? 8 : 4,
454
+ height: 1,
455
+ background: tick.major ? 'rgba(255,255,255,.5)' : 'rgba(255,255,255,.28)',
456
+ pointerEvents: 'none',
457
+ }}
458
+ >
459
+ {tick.major && (
460
+ <span
461
+ style={{
462
+ position: 'absolute',
463
+ right: 4,
464
+ top: 3,
465
+ color: 'rgba(255,255,255,.62)',
466
+ font: '9px ui-monospace, SFMono-Regular, Menlo, monospace',
467
+ lineHeight: 1,
468
+ whiteSpace: 'nowrap',
469
+ writingMode: 'vertical-rl',
470
+ transform: 'rotate(180deg)',
471
+ }}
472
+ >
473
+ {tick.value}
474
+ </span>
475
+ )}
476
+ </span>
477
+ ))}
478
+ </button>
479
+ <div style={{ ...rulerBase, left: 0, top: 0, width: 20, height: 20 }} />
480
+ </>
481
+ );
482
+ }
483
+
484
+ /** Standard 2D scene controls: transform tools on the left, display/framing
485
+ * on the right, and zoom at the lower-right. All operate on the independent
486
+ * editor camera; none changes the game's runtime camera or resolution. */
487
+ export function CanvasSceneControls({
488
+ active,
489
+ adapter,
490
+ containerRef,
491
+ view,
492
+ documentId,
493
+ }: {
494
+ active: boolean;
495
+ adapter?: AuthoringAdapter;
496
+ containerRef: RefObject<HTMLDivElement | null>;
497
+ view: RootViewController;
498
+ documentId: string;
499
+ }) {
500
+ const store = useEditorStore();
501
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
502
+ useSyncExternalStore(subscribeViewportPresentation, viewportPresentationVersion);
503
+ const pose = useSyncExternalStore(view.subscribe, view.get, view.get);
504
+ // The mode is the view's, so it survives the remount a source write causes (Godot's stays on).
505
+ const [mode, setModeState] = useState<CanvasSceneMode | null>(() => sceneModes.get(view) ?? null);
506
+ const setMode = useCallback(
507
+ (next: CanvasSceneMode | null) => {
508
+ sceneModes.set(view, next);
509
+ setModeState(next);
510
+ },
511
+ [view],
512
+ );
513
+ // This document paints a canvas stage: the stage context says so, and the canvas keys key on it.
514
+ useEffect(() => markCanvasStageDocument(documentId), [documentId]);
515
+ // The tools' keys and Godot's Pan and Ruler keys, on this 2D view while it is the active one.
516
+ useEffect(() => {
517
+ if (!active) return;
518
+ return bindCanvasSceneKeys(
519
+ store,
520
+ () => setMode(null),
521
+ (mode) => setMode(sceneModes.get(view) === mode ? null : mode),
522
+ () => toggleCanvasGrid(documentId),
523
+ );
524
+ }, [active, store, setMode, view, documentId]);
525
+ // One radio group however a tool is picked: a transform tool chosen by its key (W, E, R, T)
526
+ // leaves List Select, Pivot, Pan and Ruler as its button does.
527
+ const transformMode = store.transformMode;
528
+ const lastTransformMode = useRef(transformMode);
529
+ useEffect(() => {
530
+ if (lastTransformMode.current === transformMode) return;
531
+ lastTransformMode.current = transformMode;
532
+ setMode(null);
533
+ }, [transformMode, setMode]);
534
+
535
+ const zoomAroundCenter = useCallback(
536
+ (requestedZoom: number) => {
537
+ const container = containerRef.current;
538
+ if (!container) return;
539
+ const zoom = Math.min(SCENE_MAX_ZOOM, Math.max(SCENE_MIN_ZOOM, requestedZoom));
540
+ const cx = container.clientWidth / 2;
541
+ const cy = container.clientHeight / 2;
542
+ const worldX = (cx - pose.x) / pose.zoom;
543
+ const worldY = (cy - pose.y) / pose.zoom;
544
+ view.setView(cx - worldX * zoom, cy - worldY * zoom, zoom);
545
+ },
546
+ [containerRef, pose, view],
547
+ );
548
+
549
+ const rectsFor = useCallback(
550
+ (ids: Iterable<string>): DOMRectLike[] => {
551
+ if (!adapter?.rects) return [];
552
+ const rects: DOMRectLike[] = [];
553
+ for (const id of ids) {
554
+ const rect = adapter.rects.rect(id);
555
+ if (rect && rect.width > 0 && rect.height > 0) rects.push(rect);
556
+ }
557
+ return rects;
558
+ },
559
+ [adapter],
560
+ );
561
+
562
+ const frameSelection = useCallback(() => {
563
+ const container = containerRef.current;
564
+ if (!container) return;
565
+ frameBounds(container, view, unionRects(rectsFor(store.selectedEntityIds)));
566
+ }, [containerRef, rectsFor, store, view]);
567
+
568
+ // The zoom widget's first button (Godot's Center View): the game's viewport rectangle, or the
569
+ // origin when the project declares no resolution, in the middle of the pane at this zoom.
570
+ const centerView = useCallback(() => {
571
+ const container = containerRef.current;
572
+ if (!container) return;
573
+ const now = view.get();
574
+ const resolution = getCurrentProject()?.config.resolution;
575
+ const cx = resolution ? resolution.width / 2 : 0;
576
+ const cy = resolution ? resolution.height / 2 : 0;
577
+ view.setView(container.clientWidth / 2 - cx * now.zoom, container.clientHeight / 2 - cy * now.zoom, now.zoom);
578
+ }, [containerRef, view]);
579
+
580
+ const centerSelection = useCallback(() => {
581
+ const container = containerRef.current;
582
+ const bounds = unionRects(rectsFor(store.selectedEntityIds));
583
+ if (!container || !bounds) return;
584
+ const now = view.get();
585
+ view.setView(
586
+ container.clientWidth / 2 - (bounds.x + bounds.width / 2) * now.zoom,
587
+ container.clientHeight / 2 - (bounds.y + bounds.height / 2) * now.zoom,
588
+ now.zoom,
589
+ );
590
+ }, [containerRef, rectsFor, store, view]);
591
+
592
+ const frameScene = useCallback(() => {
593
+ const container = containerRef.current;
594
+ if (!container || !adapter) return;
595
+ frameBounds(container, view, unionRects(rectsFor(collectAllNodeIds(adapter))));
596
+ }, [adapter, containerRef, rectsFor, view]);
597
+
598
+ // Focus is an editor action, not a Three-camera action. The native Canvas
599
+ // Scene consumes the same hierarchy/hotkey commands as the Three Scene,
600
+ // then resolves them through Pixi's own bounds and its independent 2D
601
+ // editor camera. An inactive document stays MOUNTED, so only the active
602
+ // Scene may respond.
603
+ useEffect(() => {
604
+ if (!active) return;
605
+ return store.onViewportAction((action) => {
606
+ if (action.type === 'focus-entity') {
607
+ const container = containerRef.current;
608
+ if (!container) return;
609
+ frameBounds(container, view, unionRects(rectsFor([action.id])));
610
+ } else if (action.type === 'focus-selection') {
611
+ frameSelection();
612
+ } else if (action.type === 'focus-scene') {
613
+ frameScene();
614
+ }
615
+ });
616
+ }, [active, containerRef, frameScene, frameSelection, rectsFor, store, view]);
617
+
618
+ return (
619
+ <>
620
+ {/* The editor's one transient-hint channel renders per SCENE surface
621
+ * (the world root's stage mounts it for the three viewport). Without it here a
622
+ * canvas world had no hint surface at all, so a refused write — the
623
+ * declared-prop transform refusal — could only speak in the Console,
624
+ * which a user watching the board does not read (pass 65). */}
625
+ <TransientHintOverlay />
626
+ {/* Godot's 2D modes are ONE radio group: a transform tool leaves List Select, Pivot, Pan and
627
+ Ruler, and while one of those is on no transform tool is lit. */}
628
+ <ToolStrip
629
+ dimensions="2d"
630
+ otherToolActive={mode !== null}
631
+ onToolArmed={() => setMode(null)}
632
+ // Godot's one row: Select, Move, Rotate, Scale, then List Select, Pivot, Pan and Ruler.
633
+ viewTools={
634
+ <>
635
+ <Tooltip text="List Select: click to list the selectable nodes there">
636
+ <IconButton
637
+ aria-label="List Select mode"
638
+ aria-pressed={mode === 'list'}
639
+ size="comfortable"
640
+ onClick={() => setMode(mode === 'list' ? null : 'list')}
641
+ >
642
+ <EditorIcon icon={faListUl} size="md" />
643
+ </IconButton>
644
+ </Tooltip>
645
+ <Tooltip text="Pivot: click or drag to put the selected node's pivot there">
646
+ <IconButton
647
+ aria-label="Pivot mode"
648
+ aria-pressed={mode === 'pivot'}
649
+ size="comfortable"
650
+ onClick={(event) => {
651
+ // Godot: Shift on this button puts the temporary pivot at the selection's centre.
652
+ if (event.shiftKey) {
653
+ const rects = adapter
654
+ ? [...store.selectedEntityIds]
655
+ .map((id) => adapter.rects?.rect(id))
656
+ .filter((rect): rect is DOMRectLike => rect != null)
657
+ : [];
658
+ const bounds = unionRects(rects);
659
+ if (bounds) {
660
+ setTemporaryPivot(view, {
661
+ x: bounds.x + bounds.width / 2,
662
+ y: bounds.y + bounds.height / 2,
663
+ });
664
+ }
665
+ setMode('pivot');
666
+ return;
667
+ }
668
+ setMode(mode === 'pivot' ? null : 'pivot');
669
+ }}
670
+ >
671
+ <EditorIcon icon={faBullseye} size="md" />
672
+ </IconButton>
673
+ </Tooltip>
674
+ <Tooltip text="Pan (Space-drag pans in any mode)">
675
+ <IconButton
676
+ aria-label="Pan mode"
677
+ aria-pressed={mode === 'pan'}
678
+ size="comfortable"
679
+ onClick={() => setMode(mode === 'pan' ? null : 'pan')}
680
+ >
681
+ <EditorIcon icon={faHand} size="md" />
682
+ </IconButton>
683
+ </Tooltip>
684
+ <Tooltip text="Ruler: drag to measure distance and angle">
685
+ <IconButton
686
+ aria-label="Ruler mode"
687
+ aria-pressed={mode === 'ruler'}
688
+ size="comfortable"
689
+ onClick={() => setMode(mode === 'ruler' ? null : 'ruler')}
690
+ >
691
+ <EditorIcon icon={faRulerCombined} size="md" />
692
+ </IconButton>
693
+ </Tooltip>
694
+ </>
695
+ }
696
+ // …and after snapping, Godot's Lock, Group and View menu, in the same row.
697
+ trailingTools={
698
+ <>
699
+ <CanvasSceneLockButton adapter={adapter} selected={[...store.selectedEntityIds]} />
700
+ <CanvasSceneGroupButton adapter={adapter} selected={[...store.selectedEntityIds]} />
701
+ <CanvasSceneViewMenu
702
+ documentId={documentId}
703
+ view={view}
704
+ hasSelection={store.selectedEntityIds.size > 0}
705
+ onCenterSelection={centerSelection}
706
+ onFrameSelection={frameSelection}
707
+ />
708
+ </>
709
+ }
710
+ />
711
+ <CanvasSceneEditGizmos adapter={adapter} view={view} documentId={documentId} />
712
+ {mode ? (
713
+ <CanvasSceneModeLayer
714
+ mode={mode}
715
+ view={view}
716
+ adapter={adapter}
717
+ containerRef={containerRef}
718
+ onExit={() => setMode(null)}
719
+ />
720
+ ) : null}
721
+ <FloatingToolbar
722
+ label="2D scene display"
723
+ className="volter-viewport-toolbar volter-viewport-toolbar-right"
724
+ data-volter-canvas-navigation-ignore="true"
725
+ >
726
+ <Button aria-label="Frame all" variant="ghost" size="comfortable" onClick={frameScene}>
727
+ Frame all
728
+ </Button>
729
+ </FloatingToolbar>
730
+ <FloatingToolbar
731
+ label="2D scene navigation"
732
+ className="volter-canvas-scene-navigation"
733
+ data-volter-canvas-navigation-ignore="true"
734
+ >
735
+ <IconButton
736
+ aria-label="Center view"
737
+ title="Center view: the game's viewport in the middle of the pane, at this zoom"
738
+ size="comfortable"
739
+ onClick={centerView}
740
+ >
741
+ <EditorIcon icon={faCrosshairs} size="md" />
742
+ </IconButton>
743
+ <IconButton
744
+ aria-label="Zoom out"
745
+ title="Zoom out"
746
+ size="comfortable"
747
+ onClick={() => zoomAroundCenter(pose.zoom / 1.2)}
748
+ >
749
+ <EditorIcon icon={faMagnifyingGlassMinus} size="md" />
750
+ </IconButton>
751
+ <Button
752
+ variant="ghost"
753
+ aria-label="Reset zoom to 100%"
754
+ title="Reset zoom to 100%"
755
+ size="comfortable"
756
+ onClick={() => zoomAroundCenter(1)}
757
+ >
758
+ {Math.round(pose.zoom * 100)}%
759
+ </Button>
760
+ <IconButton
761
+ aria-label="Zoom in"
762
+ title="Zoom in"
763
+ size="comfortable"
764
+ onClick={() => zoomAroundCenter(pose.zoom * 1.2)}
765
+ >
766
+ <EditorIcon icon={faMagnifyingGlassPlus} size="md" />
767
+ </IconButton>
768
+ </FloatingToolbar>
769
+ </>
770
+ );
771
+ }
772
+
773
+ /**
774
+ * THE 2D VIEW MENU — Godot's 2D View menu, the home of what the view draws and where it looks:
775
+ * the grid, rulers, guides, origin and the game's viewport rectangle (each the view's own switch,
776
+ * `overlays` in its presentation), then Center Selection, Frame Selection and Clear Guides.
777
+ */
778
+ /** Godot's lock and group gizmos: a mark at each locked or grouped node's corner, drawn over the
779
+ * scene and never in the way of a click. */
780
+ function CanvasSceneEditGizmos({
781
+ adapter,
782
+ view,
783
+ documentId,
784
+ }: {
785
+ adapter: AuthoringAdapter | undefined;
786
+ view: RootViewController;
787
+ documentId: string;
788
+ }) {
789
+ const pose = useSyncExternalStore(view.subscribe, view.get, view.get);
790
+ const subscribe = useCallback(
791
+ (listener: () => void) => adapter?.subscribe?.(listener) ?? (() => undefined),
792
+ [adapter],
793
+ );
794
+ const [revision, bump] = useState(0);
795
+ useEffect(() => subscribe(() => bump((n) => n + 1)), [subscribe]);
796
+ useSyncExternalStore(subscribeViewportPresentation, viewportPresentationVersion);
797
+ const drafting = viewDrafting(documentId);
798
+ if (!adapter?.inspector || !adapter.rects || (!drafting.lock && !drafting.group)) return null;
799
+ const marks = collectAllNodeIds(adapter).flatMap((id) => {
800
+ const locked = drafting.lock && adapter.inspector?.get(id, 'locked') === true;
801
+ const grouped = drafting.group && adapter.inspector?.get(id, 'grouped') === true;
802
+ const rect = locked || grouped ? adapter.rects?.rect(id) : null;
803
+ return rect ? [{ id, locked, grouped, x: pose.x + rect.x * pose.zoom, y: pose.y + rect.y * pose.zoom }] : [];
804
+ });
805
+ return (
806
+ <div
807
+ data-testid="canvas-scene-edit-gizmos"
808
+ data-revision={revision}
809
+ style={{ position: 'absolute', inset: 0, zIndex: 55, pointerEvents: 'none', overflow: 'hidden' }}
810
+ >
811
+ {marks.map((mark) => (
812
+ <div
813
+ key={mark.id}
814
+ data-testid="canvas-scene-edit-gizmo"
815
+ data-locked={mark.locked}
816
+ data-grouped={mark.grouped}
817
+ style={{ position: 'absolute', left: mark.x + 2, top: mark.y + 2, display: 'flex', gap: 2, color: '#fff' }}
818
+ >
819
+ {mark.locked ? <EditorIcon icon={faLock} size="xs" /> : null}
820
+ {mark.grouped ? <EditorIcon icon={faLayerGroup} size="xs" /> : null}
821
+ </div>
822
+ ))}
823
+ </div>
824
+ );
825
+ }
826
+
827
+ function CanvasSceneViewMenu({
828
+ documentId,
829
+ view,
830
+ hasSelection,
831
+ onCenterSelection,
832
+ onFrameSelection,
833
+ }: {
834
+ documentId: string;
835
+ view: RootViewController;
836
+ hasSelection: boolean;
837
+ onCenterSelection: () => void;
838
+ onFrameSelection: () => void;
839
+ }) {
840
+ const [open, setOpen] = useState(false);
841
+ const triggerRef = useRef<HTMLButtonElement>(null);
842
+ useSyncExternalStore(subscribeViewportPresentation, viewportPresentationVersion);
843
+ useSyncExternalStore(
844
+ useCallback((listener) => subscribeCanvasSceneGuides(view, listener), [view]),
845
+ useCallback(() => canvasSceneGuideRevision(view), [view]),
846
+ );
847
+ const store = useEditorStore();
848
+ useSyncExternalStore(store.subscribe, store.getSnapshot);
849
+ const drafting = viewDrafting(documentId);
850
+ const gridShown = viewGridVisible(documentId);
851
+ // Godot's Grid submenu: Show Grid, Show When Snapping, Hide Grid.
852
+ const gridStates: readonly { label: string; on: boolean; pick: () => void }[] = [
853
+ {
854
+ label: 'Show Grid',
855
+ on: gridShown && !drafting.gridWhenSnapping,
856
+ pick: () => {
857
+ setViewGridVisible(documentId, true);
858
+ setViewDrafting(documentId, { gridWhenSnapping: false });
859
+ },
860
+ },
861
+ {
862
+ label: 'Show Grid When Snapping',
863
+ on: gridShown && drafting.gridWhenSnapping,
864
+ pick: () => {
865
+ setViewGridVisible(documentId, true);
866
+ setViewDrafting(documentId, { gridWhenSnapping: true });
867
+ },
868
+ },
869
+ { label: 'Hide Grid', on: !gridShown, pick: () => setViewGridVisible(documentId, false) },
870
+ ];
871
+ const toggleGrid = (): void => toggleCanvasGrid(documentId);
872
+ const switches: readonly { label: string; on: boolean; toggle: () => void }[] = [
873
+ { label: 'Rulers', on: drafting.rulers, toggle: () => setViewDrafting(documentId, { rulers: !drafting.rulers }) },
874
+ { label: 'Guides', on: drafting.guides, toggle: () => setViewDrafting(documentId, { guides: !drafting.guides }) },
875
+ { label: 'Origin', on: drafting.origin, toggle: () => setViewDrafting(documentId, { origin: !drafting.origin }) },
876
+ { label: 'Viewport', on: drafting.viewport, toggle: () => setViewDrafting(documentId, { viewport: !drafting.viewport }) },
877
+ ];
878
+ const act = (run: () => void) => () => {
879
+ run();
880
+ setOpen(false);
881
+ };
882
+ return (
883
+ <>
884
+ <Button
885
+ ref={triggerRef}
886
+ aria-label="View"
887
+ aria-haspopup="menu"
888
+ aria-expanded={open}
889
+ variant="ghost"
890
+ size="comfortable"
891
+ onClick={() => setOpen(!open)}
892
+ >
893
+ View
894
+ </Button>
895
+ {open && (
896
+ <AnchoredMenu anchorRef={triggerRef} align="end" clamp aria-label="2D view" onDismiss={() => setOpen(false)}>
897
+ {/* Godot's View menu: a Grid submenu, the Show switches, a Gizmos submenu, then the view
898
+ verbs. */}
899
+ <MenuSubmenu label="Grid" data-testid="canvas-scene-grid-submenu">
900
+ {gridStates.map((entry) => (
901
+ <MenuItem key={entry.label} role="menuitemradio" aria-checked={entry.on} onSelect={act(entry.pick)}>
902
+ <span className="volter-menu-check">{entry.on && <EditorIcon icon={faCheck} size="xs" />}</span>
903
+ {entry.label}
904
+ </MenuItem>
905
+ ))}
906
+ <MenuSeparator />
907
+ <MenuItem data-testid="canvas-scene-toggle-grid" onSelect={act(toggleGrid)}>
908
+ <span className="volter-menu-check" />
909
+ Toggle Grid
910
+ </MenuItem>
911
+ </MenuSubmenu>
912
+ {switches.map((entry) => (
913
+ <MenuItem
914
+ key={entry.label}
915
+ role="menuitemcheckbox"
916
+ aria-checked={entry.on}
917
+ // Godot's menu closes on a checked item too (`hide_on_checkable_item_selection`).
918
+ onSelect={act(entry.toggle)}
919
+ >
920
+ <span className="volter-menu-check">{entry.on && <EditorIcon icon={faCheck} size="xs" />}</span>
921
+ {entry.label.startsWith('Show ') ? entry.label : `Show ${entry.label}`}
922
+ </MenuItem>
923
+ ))}
924
+ {/* Godot's Gizmos submenu, in its order: Position, Lock, Group, Transformation. */}
925
+ <MenuSubmenu label="Gizmos" data-testid="canvas-scene-gizmos-submenu">
926
+ {(
927
+ [
928
+ ['position', 'Position'],
929
+ ['lock', 'Lock'],
930
+ ['group', 'Group'],
931
+ ['transformation', 'Transformation'],
932
+ ] as const
933
+ ).map(([key, label]) => (
934
+ <MenuItem
935
+ key={key}
936
+ role="menuitemcheckbox"
937
+ aria-checked={drafting[key]}
938
+ onSelect={act(() => setViewDrafting(documentId, { [key]: !drafting[key] }))}
939
+ >
940
+ <span className="volter-menu-check">{drafting[key] && <EditorIcon icon={faCheck} size="xs" />}</span>
941
+ {label}
942
+ </MenuItem>
943
+ ))}
944
+ </MenuSubmenu>
945
+ <MenuSeparator />
946
+ <MenuItem disabled={!hasSelection} onSelect={act(onCenterSelection)}>
947
+ <span className="volter-menu-check" />
948
+ Center Selection
949
+ </MenuItem>
950
+ <MenuItem disabled={!hasSelection} onSelect={act(onFrameSelection)}>
951
+ <span className="volter-menu-check" />
952
+ Frame Selection
953
+ </MenuItem>
954
+ <MenuItem disabled={canvasSceneGuides(view).length === 0} onSelect={act(() => clearCanvasSceneGuides(view))}>
955
+ <span className="volter-menu-check" />
956
+ Clear Guides
957
+ </MenuItem>
958
+ </AnchoredMenu>
959
+ )}
960
+ </>
961
+ );
962
+ }
963
+
964
+ /**
965
+ * Lock the selected node against selection and movement — Godot's 2D toolbar Lock, a shortcut to
966
+ * the lock the hierarchy owns (`locked`, the same session-local flag its row toggles).
967
+ */
968
+ function CanvasSceneLockButton({ adapter, selected }: { adapter: AuthoringAdapter | undefined; selected: readonly string[] }) {
969
+ // Godot's Lock acts on every selected node: all locked, it unlocks them; else it locks them all.
970
+ const offered = selected.filter((id) => adapter?.inspector?.properties(id).some((p) => p.path === 'locked'));
971
+ const locked = offered.length > 0 && offered.every((id) => adapter?.inspector?.get(id, 'locked') === true);
972
+ const plural = offered.length > 1 ? 'nodes' : 'node';
973
+ const label = locked ? `Unlock selected ${plural}` : `Lock selected ${plural}`;
974
+ return (
975
+ <Tooltip text={offered.length > 0 ? label : 'Select a node to lock it'}>
976
+ <IconButton
977
+ aria-label={label}
978
+ aria-pressed={locked}
979
+ size="comfortable"
980
+ disabled={offered.length === 0 || !adapter?.inspector?.set}
981
+ onClick={() => {
982
+ for (const id of offered) adapter?.inspector?.set?.(id, 'locked', !locked);
983
+ }}
984
+ >
985
+ <EditorIcon icon={locked ? faLock : faLockOpen} size="md" />
986
+ </IconButton>
987
+ </Tooltip>
988
+ );
989
+ }
990
+
991
+ /**
992
+ * Godot's Pan and Ruler modes: while one is on, this layer over the scene takes the pointer. Pan
993
+ * drags the view with the primary button; Ruler draws the drag as a line and reads its length in
994
+ * world units and its angle. Escape leaves the mode.
995
+ */
996
+ /** Godot's Toggle Grid (measured on 4.7.1): from Show it goes to Show When Snapping, and from any
997
+ * other state (When Snapping, Hide) to Show — Hide → Show → When Snapping → Show. */
998
+ function toggleCanvasGrid(documentId: string): void {
999
+ const showing = viewGridVisible(documentId) && !viewDrafting(documentId).gridWhenSnapping;
1000
+ setViewGridVisible(documentId, true);
1001
+ setViewDrafting(documentId, { gridWhenSnapping: showing });
1002
+ }
1003
+
1004
+ /** Godot's 2D modes beside the transform tools: Pan, Ruler, List Select and Pivot. */
1005
+ type CanvasSceneMode = 'pan' | 'ruler' | 'list' | 'pivot';
1006
+ const sceneModes = new WeakMap<RootViewController, CanvasSceneMode | null>();
1007
+
1008
+ function CanvasSceneModeLayer({
1009
+ mode,
1010
+ view,
1011
+ adapter,
1012
+ containerRef,
1013
+ onExit,
1014
+ }: {
1015
+ mode: CanvasSceneMode;
1016
+ view: RootViewController;
1017
+ adapter: AuthoringAdapter | undefined;
1018
+ containerRef: RefObject<HTMLDivElement | null>;
1019
+ onExit: () => void;
1020
+ }) {
1021
+ const store = useEditorStore();
1022
+ const pose = useSyncExternalStore(view.subscribe, view.get, view.get);
1023
+ const [measure, setMeasure] = useState<{ from: { x: number; y: number }; to: { x: number; y: number } } | null>(null);
1024
+ const [pick, setPick] = useState<{ x: number; y: number; ids: readonly string[] } | null>(null);
1025
+ useEffect(() => {
1026
+ const onKey = (event: KeyboardEvent) => {
1027
+ if (event.key === 'Escape') {
1028
+ // Escape in Pivot mode lets go of the temporary pivot too.
1029
+ if (mode === 'pivot') setTemporaryPivot(view, null);
1030
+ onExit();
1031
+ }
1032
+ };
1033
+ window.addEventListener('keydown', onKey);
1034
+ return () => window.removeEventListener('keydown', onKey);
1035
+ }, [onExit]);
1036
+ const worldAt = (clientX: number, clientY: number) => {
1037
+ const box = containerRef.current?.getBoundingClientRect();
1038
+ const now = view.get();
1039
+ const x = clientX - (box?.left ?? 0);
1040
+ const y = clientY - (box?.top ?? 0);
1041
+ return { x: (x - now.x) / now.zoom, y: (y - now.y) / now.zoom };
1042
+ };
1043
+ /** A ruler point snaps as Godot's does (measured on 4.7.1: with grid snap on the point read
1044
+ * (1136, 552) on the 8 px grid where it read (1136, 549) off): the grid when the magnet is on,
1045
+ * then whole pixels under Use Pixel Snap. */
1046
+ const rulerPoint = (point: { x: number; y: number }): { x: number; y: number } => {
1047
+ const grid = store.snap2D;
1048
+ const onGrid = (value: number, offset: number): number =>
1049
+ store.snapEnabled && grid.step > 0 ? Math.round((value - offset) / grid.step) * grid.step + offset : value;
1050
+ const snapped = { x: onGrid(point.x, grid.offsetX), y: onGrid(point.y, grid.offsetY) };
1051
+ return grid.pixel ? { x: Math.round(snapped.x), y: Math.round(snapped.y) } : snapped;
1052
+ };
1053
+ /** Where the pivot goes for a point: snapped to its node's sides and centre as a dragged pivot is. */
1054
+ const pivotPoint = (id: string, point: { x: number; y: number }, invert: boolean) => {
1055
+ if (!adapter) return point;
1056
+ const rect = rectForId(adapter, id);
1057
+ return snapPivotPoint(point, frameForId(adapter, id) ?? (rect ? rectFrame(rect) : null), {
1058
+ invert,
1059
+ smart: store.smartSnap,
1060
+ gridOn: store.snapEnabled,
1061
+ grid: store.snap2D,
1062
+ threshold: EDGE_SNAP_THRESHOLD_PX / Math.max(view.get().zoom, 0.01),
1063
+ });
1064
+ };
1065
+ const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
1066
+ if (event.button !== 0) return;
1067
+ event.preventDefault();
1068
+ if (mode === 'list') {
1069
+ // Godot's List Select: every selectable node under the click, to choose from.
1070
+ const ids = pickCandidates(store, event.clientX, event.clientY, adapter ? { adapter } : {});
1071
+ setPick(ids.length > 0 ? { x: event.clientX, y: event.clientY, ids } : null);
1072
+ return;
1073
+ }
1074
+ if (mode === 'pivot' && event.shiftKey) {
1075
+ // Godot's Shift in Pivot mode: a temporary pivot rotation turns around, no node's own pivot
1076
+ // changed.
1077
+ setTemporaryPivot(view, rulerPoint(worldAt(event.clientX, event.clientY)));
1078
+ return;
1079
+ }
1080
+ if (mode === 'pivot') {
1081
+ const selected = store.selectedEntityIds;
1082
+ const id = selected.size === 1 && adapter ? [...selected][0]! : null;
1083
+ const provider = id && adapter ? spatialHandlesForId(adapter, id) : null;
1084
+ const handle = id
1085
+ ? provider
1086
+ ?.layers(id)
1087
+ .find((layer) => layer.category === 'origin')
1088
+ ?.handles.find((h) => h.writable)
1089
+ : undefined;
1090
+ if (!id || !provider || !handle) return;
1091
+ event.currentTarget.setPointerCapture(event.pointerId);
1092
+ const at = (next: { clientX: number; clientY: number; metaKey: boolean; ctrlKey: boolean }) => {
1093
+ const point = pivotPoint(id, worldAt(next.clientX, next.clientY), next.metaKey || next.ctrlKey);
1094
+ return [point.x, point.y, 0] as const;
1095
+ };
1096
+ provider.preview(id, handle.id, at(event));
1097
+ const move = (next: PointerEvent) => provider.preview(id, handle.id, at(next));
1098
+ const end = (last: PointerEvent) => {
1099
+ window.removeEventListener('pointermove', move);
1100
+ void provider.commit(id, handle.id, at(last));
1101
+ };
1102
+ window.addEventListener('pointermove', move);
1103
+ window.addEventListener('pointerup', end, { once: true });
1104
+ return;
1105
+ }
1106
+ event.currentTarget.setPointerCapture(event.pointerId);
1107
+ const start = { clientX: event.clientX, clientY: event.clientY, pose: view.get() };
1108
+ const from = worldAt(event.clientX, event.clientY);
1109
+ if (mode === 'ruler') setMeasure({ from: rulerPoint(from), to: rulerPoint(from) });
1110
+ const move = (next: PointerEvent) => {
1111
+ if (mode === 'pan') {
1112
+ view.setView(
1113
+ start.pose.x + next.clientX - start.clientX,
1114
+ start.pose.y + next.clientY - start.clientY,
1115
+ start.pose.zoom,
1116
+ );
1117
+ } else {
1118
+ setMeasure({ from: rulerPoint(from), to: rulerPoint(worldAt(next.clientX, next.clientY)) });
1119
+ }
1120
+ };
1121
+ const end = () => {
1122
+ window.removeEventListener('pointermove', move);
1123
+ window.removeEventListener('pointerup', end);
1124
+ };
1125
+ window.addEventListener('pointermove', move);
1126
+ window.addEventListener('pointerup', end, { once: true });
1127
+ };
1128
+ const screen = (point: { x: number; y: number }) => ({ x: pose.x + point.x * pose.zoom, y: pose.y + point.y * pose.zoom });
1129
+ const length = measure ? Math.hypot(measure.to.x - measure.from.x, measure.to.y - measure.from.y) : 0;
1130
+ const angle = measure ? (Math.atan2(measure.to.y - measure.from.y, measure.to.x - measure.from.x) * 180) / Math.PI : 0;
1131
+ const a = measure ? screen(measure.from) : null;
1132
+ const b = measure ? screen(measure.to) : null;
1133
+ return (
1134
+ <div
1135
+ data-testid={`canvas-scene-${mode}-layer`}
1136
+ data-volter-canvas-navigation-ignore="true"
1137
+ onPointerDown={onPointerDown}
1138
+ // Above the selection hit layer (z50), below the viewport toolbars.
1139
+ style={{ position: 'absolute', inset: 0, zIndex: 60, cursor: mode === 'pan' ? 'grab' : mode === 'list' ? 'context-menu' : 'crosshair' }}
1140
+ >
1141
+ {pick && adapter ? <ViewportPickMenu state={pick} adapter={adapter} onClose={() => setPick(null)} /> : null}
1142
+ {a && b ? (
1143
+ <>
1144
+ <svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
1145
+ <line x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke="#ff5fa2" strokeWidth={1.5} />
1146
+ </svg>
1147
+ <div
1148
+ data-testid="canvas-scene-ruler-reading"
1149
+ style={{
1150
+ position: 'absolute',
1151
+ left: (a.x + b.x) / 2 + 8,
1152
+ top: (a.y + b.y) / 2 + 8,
1153
+ padding: '2px 6px',
1154
+ borderRadius: 4,
1155
+ background: 'rgba(24, 26, 30, .92)',
1156
+ color: '#fff',
1157
+ font: '11px ui-monospace, SFMono-Regular, Menlo, monospace',
1158
+ pointerEvents: 'none',
1159
+ whiteSpace: 'nowrap',
1160
+ }}
1161
+ >
1162
+ {`${length.toFixed(1)} px · ${angle.toFixed(1)}° · Δ ${(measure!.to.x - measure!.from.x).toFixed(1)}, ${(measure!.to.y - measure!.from.y).toFixed(1)}`}
1163
+ </div>
1164
+ </>
1165
+ ) : null}
1166
+ </div>
1167
+ );
1168
+ }
1169
+
1170
+ /**
1171
+ * Group the selected node with its children — Godot's 2D toolbar Group: a click on any node inside
1172
+ * it then selects the group (`grouped`, beside `locked`; the hierarchy still reaches every child).
1173
+ */
1174
+ function CanvasSceneGroupButton({ adapter, selected }: { adapter: AuthoringAdapter | undefined; selected: readonly string[] }) {
1175
+ // Godot's Group acts on every selected node, as Lock does.
1176
+ const offered = selected.filter((id) => adapter?.inspector?.properties(id).some((p) => p.path === 'grouped'));
1177
+ const grouped = offered.length > 0 && offered.every((id) => adapter?.inspector?.get(id, 'grouped') === true);
1178
+ const plural = offered.length > 1 ? 'nodes' : 'node';
1179
+ const label = grouped ? `Ungroup selected ${plural}` : `Group selected ${plural} with their children`;
1180
+ return (
1181
+ <Tooltip text={offered.length > 0 ? label : 'Select a node to group it'}>
1182
+ <IconButton
1183
+ aria-label={label}
1184
+ aria-pressed={grouped}
1185
+ size="comfortable"
1186
+ disabled={offered.length === 0 || !adapter?.inspector?.set}
1187
+ onClick={() => {
1188
+ for (const id of offered) adapter?.inspector?.set?.(id, 'grouped', !grouped);
1189
+ }}
1190
+ >
1191
+ <EditorIcon icon={faLayerGroup} size="md" />
1192
+ </IconButton>
1193
+ </Tooltip>
1194
+ );
1195
+ }