@volter/editor-game 0.5.67 → 0.5.69

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 (173) hide show
  1. package/NOTICE +7 -7
  2. package/contributions/autoplay.service.ts +1 -1
  3. package/contributions/build-player.document.tsx +95 -0
  4. package/contributions/build-profiles.document.tsx +1 -1
  5. package/contributions/build-progress.status.tsx +2 -2
  6. package/contributions/build.header.tsx +1 -1
  7. package/contributions/connection.status.tsx +1 -1
  8. package/contributions/coverage.service.ts +2 -2
  9. package/contributions/game-eval.command.ts +6 -6
  10. package/contributions/gameplay.command.ts +2 -2
  11. package/contributions/generations.status.tsx +1 -1
  12. package/contributions/ingest.service.ts +2 -2
  13. package/contributions/play.command.ts +6 -6
  14. package/contributions/react/react-inspector.service.ts +1 -1
  15. package/package.json +9 -8
  16. package/serving/index.ts +1 -1
  17. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  18. package/src/asset-budget/asset-budget-model.ts +14 -14
  19. package/src/asset-budget/optimize-apply.ts +8 -8
  20. package/src/bridge/call.ts +3 -3
  21. package/src/bridge/dispatch.ts +12 -12
  22. package/src/bridge/screenshot.ts +6 -6
  23. package/src/build/BuildProfilesPanel.tsx +19 -8
  24. package/src/build/build-session.ts +35 -0
  25. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +78 -5
  26. package/src/canvas/canvas-board/canvas-component-board.ts +1 -1
  27. package/src/canvas/canvas-design-mount.ts +1 -1
  28. package/src/coverage/live-project-verbs.ts +3 -3
  29. package/src/coverage/native-system-coverage.ts +3 -3
  30. package/src/coverage/root-coverage.ts +2 -2
  31. package/src/coverage/session-coverage.ts +2 -2
  32. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +2 -2
  33. package/src/game-document/DevicePresetPicker.tsx +6 -6
  34. package/src/game-document/GameCaptureFrameButton.tsx +1 -1
  35. package/src/game-document/GameDocument.tsx +2 -2
  36. package/src/game-document/GamePanel.tsx +9 -9
  37. package/src/game-document/InstanceInspectorPicker.tsx +1 -1
  38. package/src/game-document/device-preview.ts +4 -4
  39. package/src/game-document/physics-debug.ts +2 -2
  40. package/src/generation/GenerationActivity.tsx +22 -22
  41. package/src/generation/GenerationGallery.css +79 -79
  42. package/src/generation/generation-documents.tsx +25 -25
  43. package/src/generation/generation-presentation.ts +1 -1
  44. package/src/host/adapter-reach.ts +1 -1
  45. package/src/host/adapter-runtime-bindings.ts +5 -5
  46. package/src/host/after-paint.ts +3 -3
  47. package/src/host/api/configurations.ts +1 -1
  48. package/src/host/authoring/canvas-runtime-recognition.ts +2 -2
  49. package/src/host/authoring/contract-hierarchy-authoring.ts +6 -6
  50. package/src/host/authoring/contract-scenes-stories.ts +5 -5
  51. package/src/host/authoring/ingest-data-writer.ts +1 -1
  52. package/src/host/authoring/ingest-source-persistence.ts +2 -2
  53. package/src/host/authoring/mount-isolated-pixi-screen.ts +1 -1
  54. package/src/host/authoring/pixi-authoring-adapter.ts +206 -28
  55. package/src/host/authoring/pixi-creatable-kinds.ts +80 -0
  56. package/src/host/authoring/pixi-live-write-target.ts +6 -5
  57. package/src/host/authoring/pixi-source-write-target.ts +118 -20
  58. package/src/host/authoring/pixi-structure-history.ts +3 -3
  59. package/src/host/binding-resolver.ts +6 -6
  60. package/src/host/browser-transpile.ts +10 -10
  61. package/src/host/canvas-preview-frames.ts +1 -1
  62. package/src/host/components/CameraAuthoringOverlay.tsx +14 -14
  63. package/src/host/components/HeaderTelemetry.tsx +11 -11
  64. package/src/host/components/PixiIsolationSceneContent.tsx +3 -3
  65. package/src/host/components/ResolutionPicker.tsx +4 -4
  66. package/src/host/components/scene-document.tsx +3 -3
  67. package/src/host/components/world-root-stage-binding.tsx +1 -1
  68. package/src/host/components/world-root-stage.ts +1 -1
  69. package/src/host/coverage/authoring-read-probe.ts +1 -1
  70. package/src/host/coverage/capability-coverage.ts +22 -22
  71. package/src/host/coverage/game-contract-seam-evidence.ts +8 -8
  72. package/src/host/coverage/project-verb-coverage.ts +1 -1
  73. package/src/host/coverage/system-adapter-coverage.ts +19 -6
  74. package/src/host/game-location-guard.ts +3 -3
  75. package/src/host/game-module-access.ts +6 -6
  76. package/src/host/game-realm-page.ts +3 -3
  77. package/src/host/game-realm-reclaim.ts +1 -1
  78. package/src/host/gameplay-recording.ts +5 -5
  79. package/src/host/gated-globals.ts +13 -13
  80. package/src/host/ingest/registry.ts +14 -14
  81. package/src/host/play-control-hook.ts +1 -1
  82. package/src/host/playwright-shim.ts +2 -2
  83. package/src/host/projection/pixi.ts +2 -2
  84. package/src/host/react-mount-runtime.ts +1 -1
  85. package/src/host/roots/canvas-root.tsx +13 -1
  86. package/src/host/roots/r3f-root.tsx +1 -1
  87. package/src/host/scene-view-drawability.ts +1 -1
  88. package/src/host/sdk/tools.ts +1 -1
  89. package/src/host/served-bundle-runtime-modules.ts +2 -0
  90. package/src/host/staged-projects.ts +1 -1
  91. package/src/host/stories/story-pixi-preview.ts +1 -1
  92. package/src/host/stories/story-three-preview.ts +1 -1
  93. package/src/host/three-ingest-runtime.ts +1 -1
  94. package/src/ingest/active-ingest.ts +1 -1
  95. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  96. package/src/ingest/authoring/ingest-root-adapter.ts +11 -11
  97. package/src/ingest/capture-wait-report.ts +4 -4
  98. package/src/ingest/discovery-public-ingest.ts +4 -4
  99. package/src/ingest/dom-stub-mark.ts +1 -1
  100. package/src/ingest/entry-load.ts +1 -1
  101. package/src/ingest/game-contract-realm.ts +3 -3
  102. package/src/ingest/game-pointer-lock.ts +1 -1
  103. package/src/ingest/ingest-canvas-scene-document.tsx +3 -3
  104. package/src/ingest/ingest-evidence-hook.ts +6 -6
  105. package/src/ingest/ingest-frame-snapshot.ts +2 -2
  106. package/src/ingest/ingest-play-commands.ts +1 -1
  107. package/src/ingest/ingest-play-control.ts +11 -11
  108. package/src/ingest/ingest-render-debug.ts +5 -5
  109. package/src/ingest/ingest-siblings.ts +3 -3
  110. package/src/ingest/module-mode.ts +2 -2
  111. package/src/ingest/mount-canvas-ingest-root.ts +10 -10
  112. package/src/ingest/mount-coverage.ts +3 -3
  113. package/src/ingest/mount-dom-ingest-root.ts +7 -7
  114. package/src/ingest/mount-ingest-root.ts +10 -10
  115. package/src/ingest/mount-three-ingest-root.ts +5 -5
  116. package/src/ingest/resolve-three.ts +2 -2
  117. package/src/ingest/served-bundle.ts +6 -6
  118. package/src/ingest/served-html-boot.ts +8 -8
  119. package/src/ingest/surface-dom.ts +9 -9
  120. package/src/ingest/surface-three.ts +1 -1
  121. package/src/ingest/types.ts +1 -1
  122. package/src/ingest/unmount-ingest-root.ts +3 -3
  123. package/src/network/NetworkInspectorPanel.tsx +521 -54
  124. package/src/network/network-inspector-model.ts +6 -0
  125. package/src/play/play-boot-stall.ts +1 -1
  126. package/src/play/play-mode.ts +31 -30
  127. package/src/play/play-recording.ts +12 -12
  128. package/src/play-bar/PlayBar.tsx +8 -8
  129. package/src/play-bar/PlayerCountPicker.tsx +3 -3
  130. package/src/profiler/FrameDebuggerPanel.tsx +2 -2
  131. package/src/profiler/PerformancePanel.tsx +4 -4
  132. package/src/react/design-time-react-mount.ts +5 -5
  133. package/src/react/dom-authoring-adapter.ts +3 -3
  134. package/src/react/react-inspector-section.tsx +2 -2
  135. package/src/react/react-world-authoring-adapter.ts +20 -20
  136. package/src/react/story-documents/story-documents.tsx +4 -4
  137. package/src/react/ui-component-board.ts +1 -1
  138. package/src/runtime/adapter/audio-meter.ts +1 -1
  139. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +4 -4
  140. package/src/runtime/adapter/ingest/contract-system-adapters.ts +9 -9
  141. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +2 -2
  142. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +1 -1
  143. package/src/runtime/adapter/native-debug-module.ts +5 -5
  144. package/src/runtime/audio/pose-guard.ts +1 -1
  145. package/src/runtime/core/game-scoped-slot.ts +1 -1
  146. package/src/runtime/create-runtime.ts +15 -15
  147. package/src/runtime/debug-bridge.ts +25 -25
  148. package/src/runtime/debug-registry.ts +17 -17
  149. package/src/runtime/dev/instruments.ts +1 -1
  150. package/src/runtime/dev/performance-profiler.ts +2 -2
  151. package/src/runtime/dev/register-render-vitals.ts +2 -2
  152. package/src/runtime/dev/static-batch-advisor.ts +3 -3
  153. package/src/runtime/dev-build.ts +3 -3
  154. package/src/runtime/game.ts +5 -5
  155. package/src/runtime/input-router.ts +24 -11
  156. package/src/runtime/mount-manifest.ts +9 -9
  157. package/src/runtime/pixi/authoring.ts +1 -1
  158. package/src/runtime/pixi/ingest.ts +1 -1
  159. package/src/runtime/presentation.ts +1 -1
  160. package/src/runtime/render-control.ts +21 -21
  161. package/src/runtime/render-seed.ts +3 -3
  162. package/src/runtime/run-ticks-settled.ts +1 -1
  163. package/src/services/game-audio.ts +3 -3
  164. package/src/services/game-network.ts +125 -15
  165. package/src/three/authoring/model-asset-inspector-section.css +15 -15
  166. package/src/three/authoring/model-asset-inspector-section.tsx +7 -7
  167. package/src/three/authoring/r3f-design-session.ts +10 -10
  168. package/src/three/authoring/r3f-source-authoring-adapter.ts +5 -5
  169. package/src/three/authoring/three-authoring-adapter.ts +5 -4
  170. package/src/three/authoring/typed-three-inspector.ts +1 -1
  171. package/src/three/story-documents/three-story-documents.tsx +3 -3
  172. package/src/three/three-board/board-scene.ts +9 -9
  173. package/src/three/three-board/three-component-board.ts +1 -1
@@ -116,7 +116,7 @@ export const CAPTURE_GAP: CapabilityGap = {
116
116
  * it as a false alarm and stops trusting the instrument, which is the exact
117
117
  * failure this module's header forbids.
118
118
  *
119
- * MEASURED 2026-09-19 on a `--template game` scaffold, standalone `vgai edit`
119
+ * MEASURED 2026-09-19 on a `--template game` scaffold, standalone `volter-game-editor edit`
120
120
  * (it is NOT frame-specific): `hierarchy.roots()` passed, ten `hierarchy
121
121
  * .node()` calls passed, and the walk still failed — three of the default
122
122
  * scene's nodes report a `parentId` their listed parent does not claim. What
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Installs the loaded `vgai.adapter.ts` runtime bindings onto one mounted
2
+ * Installs the loaded `volter.adapter.ts` runtime bindings onto one mounted
3
3
  * Game. Game modules stay native: adapter closures close over app-owned stores
4
4
  * and functions, while this host-side file projects them onto the existing
5
5
  * session command/state/input doors.
@@ -188,7 +188,7 @@ function installObservations(
188
188
  function actionMap(binding: AdapterInputBinding, game: Game): Map<string, AdapterInputValueType> {
189
189
  const actions = binding.actions(game);
190
190
  if (!Array.isArray(actions)) {
191
- throw new Error('vgai.adapter.ts input.actions must return an array.');
191
+ throw new Error('volter.adapter.ts input.actions must return an array.');
192
192
  }
193
193
  const out = new Map<string, AdapterInputValueType>();
194
194
  for (const candidate of actions as readonly Partial<AdapterInputAction>[]) {
@@ -198,13 +198,13 @@ function actionMap(binding: AdapterInputBinding, game: Game): Map<string, Adapte
198
198
  !ADAPTER_INPUT_VALUE_TYPES.includes(candidate.valueType as AdapterInputValueType)
199
199
  ) {
200
200
  throw new Error(
201
- 'vgai.adapter.ts input.actions returned an invalid action; each action needs a non-empty ' +
201
+ 'volter.adapter.ts input.actions returned an invalid action; each action needs a non-empty ' +
202
202
  '`name` and a supported `valueType`.',
203
203
  );
204
204
  }
205
205
  if (out.has(candidate.name)) {
206
206
  throw new Error(
207
- `vgai.adapter.ts input.actions returned duplicate action "${candidate.name}".`,
207
+ `volter.adapter.ts input.actions returned duplicate action "${candidate.name}".`,
208
208
  );
209
209
  }
210
210
  out.set(candidate.name, candidate.valueType as AdapterInputValueType);
@@ -228,7 +228,7 @@ function valueMatches(type: AdapterInputValueType, value: AdapterInputValue): bo
228
228
  function installInput(game: Game, registry: DebugRegistry, binding: AdapterInputBinding): void {
229
229
  if (!game.world(binding.root)) {
230
230
  throw new Error(
231
- `vgai.adapter.ts input.root names "${binding.root}", which is not a mounted manifest root.`,
231
+ `volter.adapter.ts input.root names "${binding.root}", which is not a mounted manifest root.`,
232
232
  );
233
233
  }
234
234
 
@@ -4,13 +4,13 @@
4
4
  * READ THIS BEFORE WRITING `requestAnimationFrame` INTO A BOOT PATH.
5
5
  * **rAF does not fire in a hidden tab, ever** — not throttled, not late:
6
6
  * zero callbacks until the tab is foregrounded. And "hidden at boot" is the
7
- * ORDINARY case for this editor, not an edge: every `vgai edit` session rooted
7
+ * ORDINARY case for this editor, not an edge: every `volter-game-editor edit` session rooted
8
8
  * in an agent worktree opens its one tab behind the human's window
9
- * (`server/open-browser.ts` → `open -g`), and any `vgai edit` that fires while
9
+ * (`server/open-browser.ts` → `open -g`), and any `volter-game-editor edit` that fires while
10
10
  * the browser is not frontmost lands the same way. Measured twice in one hour:
11
11
  * an editor whose shell mount sat behind a bare double-rAF heartbeat for 20
12
12
  * minutes with the relay answering, `document.visibilityState: "hidden"`, and
13
- * zero console errors, while every `vgai play` refused with "the editor shell
13
+ * zero console errors, while every `volter-game-editor play` refused with "the editor shell
14
14
  * is not bound yet".
15
15
  *
16
16
  * A paint deferral is only ever an OPTIMISATION — let the committed chrome
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The client half of `/__editor/configurations` (`server/routes/run.ts`): the project's
3
3
  * declared run configurations and their runtime status, and the start/stop
4
- * verbs the transport's picker and `vgai run` share.
4
+ * verbs the transport's picker uses.
5
5
  */
6
6
 
7
7
  import { assertEditorServerResponse, editorServerJson } from '@volter/editor-sdk/kit/editor-server-response';
@@ -22,7 +22,7 @@ export interface PhaserRealmLike {
22
22
  export interface BabylonRealmLike {
23
23
  BABYLON?: { Engine?: { Instances?: BabylonEngineLike[] } };
24
24
  /** Host build seam for module-bundled Babylon, where no BABYLON global exists. */
25
- __vgaiBabylon?: {
25
+ __volterBabylon?: {
26
26
  engines?: BabylonEngineLike[];
27
27
  /** Optional boot promise published by a host-built classic bundle. */
28
28
  ready?: PromiseLike<unknown>;
@@ -39,7 +39,7 @@ export function findPhaserGame(realm: PhaserRealmLike | null): PhaserGameLike |
39
39
 
40
40
  /** Recognize Babylon from its public engine registry or the module-build host seam. */
41
41
  export function findBabylonEngine(realm: BabylonRealmLike | null): BabylonEngineLike | null {
42
- const bridged = realm?.__vgaiBabylon?.engines;
42
+ const bridged = realm?.__volterBabylon?.engines;
43
43
  const engines = Array.isArray(bridged) ? bridged : realm?.BABYLON?.Engine?.Instances;
44
44
  if (!Array.isArray(engines)) return null;
45
45
  return (
@@ -8,8 +8,8 @@
8
8
 
9
9
  import type { AuthoringAdapter, EditorNode, HierarchyProvider } from '@volter/editor-project/adapter';
10
10
  import type {
11
- VgaiGameHierarchyGroup,
12
- VgaiGameHierarchyProvider,
11
+ VolterGameHierarchyGroup,
12
+ VolterGameHierarchyProvider,
13
13
  } from '@volter/editor-project/adapter/ingest/game-contract';
14
14
  import { editorConsole } from '@volter/editor-sdk/kit/editor-console';
15
15
 
@@ -20,7 +20,7 @@ function groupNodeId(id: string): string {
20
20
  return `${GROUP_PREFIX}${encodeURIComponent(id)}`;
21
21
  }
22
22
 
23
- function groupNode(group: VgaiGameHierarchyGroup): EditorNode {
23
+ function groupNode(group: VolterGameHierarchyGroup): EditorNode {
24
24
  return {
25
25
  id: groupNodeId(group.id),
26
26
  label: group.label,
@@ -32,7 +32,7 @@ function groupNode(group: VgaiGameHierarchyGroup): EditorNode {
32
32
  };
33
33
  }
34
34
 
35
- function assertGroups(value: unknown): VgaiGameHierarchyGroup[] {
35
+ function assertGroups(value: unknown): VolterGameHierarchyGroup[] {
36
36
  if (!Array.isArray(value)) throw new Error('hierarchy() did not return an array');
37
37
  const ids = new Set<string>();
38
38
  for (const group of value) {
@@ -58,12 +58,12 @@ function assertGroups(value: unknown): VgaiGameHierarchyGroup[] {
58
58
  /** Identity-preserving when the game declares no hierarchy surface. */
59
59
  export function withContractHierarchy(
60
60
  base: AuthoringAdapter,
61
- provider: VgaiGameHierarchyProvider | undefined,
61
+ provider: VolterGameHierarchyProvider | undefined,
62
62
  ): AuthoringAdapter {
63
63
  if (!provider) return base;
64
64
 
65
65
  let warned = false;
66
- const groups = (): VgaiGameHierarchyGroup[] => {
66
+ const groups = (): VolterGameHierarchyGroup[] => {
67
67
  try {
68
68
  return assertGroups(provider());
69
69
  } catch (error) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Projects an ingested game's declared SCENES (`window.vgaiGame.scenes`, see
2
+ * Projects an ingested game's declared SCENES (`window.volterGame.scenes`, see
3
3
  * `@volter/editor-project/adapter/ingest/game-contract`) onto the ordinary
4
4
  * {@link StoriesProvider} the editor's story picker already reads — the sibling
5
5
  * of `contract-hierarchy-authoring.ts`, which does the same job for the
@@ -29,11 +29,11 @@ import type {
29
29
  LiveSceneTable,
30
30
  } from '@volter/editor-sdk/host';
31
31
  import type { StoriesProvider, StoryRef } from '@volter/editor-project/adapter';
32
- import type { VgaiGameContract, VgaiGameScene } from '@volter/editor-project/adapter/ingest/game-contract';
32
+ import type { VolterGameContract, VolterGameScene } from '@volter/editor-project/adapter/ingest/game-contract';
33
33
  import { readGameScenes } from '@volter/editor-project/adapter/ingest/game-contract';
34
34
  import { editorConsole } from '@volter/editor-sdk/kit/editor-console';
35
35
 
36
- function assertScenes(value: unknown): VgaiGameScene[] {
36
+ function assertScenes(value: unknown): VolterGameScene[] {
37
37
  if (!Array.isArray(value)) throw new Error('scenes.list() did not return an array');
38
38
  const ids = new Set<string>();
39
39
  for (const scene of value) {
@@ -111,7 +111,7 @@ export interface ContractScenesSeams {
111
111
  * bound: `readGameScenes` owns that shape check.
112
112
  */
113
113
  export function createContractScenesStories(
114
- contract: VgaiGameContract | null | undefined,
114
+ contract: VolterGameContract | null | undefined,
115
115
  seams: ContractScenesSeams = {},
116
116
  ): ContractScenesStories | null {
117
117
  const { scenes, malformed } = readGameScenes(contract);
@@ -121,7 +121,7 @@ export function createContractScenesStories(
121
121
  if (!scenes) return null;
122
122
 
123
123
  let warned = false;
124
- const list = (): VgaiGameScene[] => {
124
+ const list = (): VolterGameScene[] => {
125
125
  try {
126
126
  return assertScenes(scenes.list());
127
127
  } catch (error) {
@@ -53,7 +53,7 @@ import type { ChannelValue } from '@volter/editor-sdk/kit/creation-site-edit';
53
53
  * that carries none: that object is unanchored, with its reason, and its edits
54
54
  * stay live-only (the anti-shim rule).
55
55
  */
56
- export const DATA_RECORD_KEY = 'vgaiRecordIndex';
56
+ export const DATA_RECORD_KEY = 'volterRecordIndex';
57
57
 
58
58
  /** The record index a live object declares, or `null` when it declares none. */
59
59
  export function dataRecordIndexOf(object: unknown): number | null {
@@ -772,7 +772,7 @@ export class IngestSourcePersistence {
772
772
  const history = this.history!;
773
773
  const descriptor = history.registry.registerProject(
774
774
  'overlay',
775
- '.vgai/ingest-live-objects',
775
+ '.volter/ingest-live-objects',
776
776
  'Ingested game objects',
777
777
  );
778
778
  const driver: ResourceDriver = {
@@ -813,7 +813,7 @@ export class IngestSourcePersistence {
813
813
  /**
814
814
  * Report a refusal where the user can see it, once per gesture.
815
815
  *
816
- * A WARNING, not a log line, because of where each one goes: `vgai status`
816
+ * A WARNING, not a log line, because of where each one goes: `volter-game-editor status`
817
817
  * surfaces `sessionWarnings` and shows nothing at all for `log`. A refused
818
818
  * write was invisible to every door the product has — the user's edit did not
819
819
  * reach their file and the only explanation lived in a panel nobody could read
@@ -148,7 +148,7 @@ export async function mountIsolatedPixiScreen(
148
148
  preserveDrawingBuffer: true,
149
149
  });
150
150
  const canvas = app.canvas as HTMLCanvasElement;
151
- canvas.dataset['vgaiCanvasSceneSurface'] = 'true';
151
+ canvas.dataset['volterCanvasSceneSurface'] = 'true';
152
152
  canvas.style.position = 'absolute';
153
153
  canvas.style.inset = '0';
154
154
  canvas.style.width = '100%';
@@ -82,8 +82,10 @@ import {
82
82
  readContainerAuthoringLabel,
83
83
  } from './pixi-source-identity';
84
84
  import { CanvasStructureHistory } from './pixi-structure-history';
85
+ import { PIXI_CREATABLE_KINDS } from './pixi-creatable-kinds';
85
86
  import { fromNeutralTransform, toNeutralTransform } from './pixi-transform-channels';
86
87
  import { resolvesLiveOnly, runWritePipe } from '@volter/editor-sdk/kit/write-pipe';
88
+ import { editorHost } from '@volter/editor-sdk/host';
87
89
 
88
90
  /**
89
91
  * What a write target is handed at construction — the live view it edits
@@ -245,7 +247,7 @@ export interface PixiAuthoringOptions {
245
247
  * `pickable` at all — a stage with no mapped surface cannot answer where a
246
248
  * client point landed, and guessing one would fabricate a hit.
247
249
  *
248
- * When the element carries `data-vgai-stage-width`/`-height`, those are the
250
+ * When the element carries `data-volter-stage-width`/`-height`, those are the
249
251
  * stage's LOGICAL size and the mapping divides the measured rect by them —
250
252
  * which is what lets a design surface presented through the shared pan/zoom
251
253
  * camera (a CSS scale) pick correctly. Without them the mapping is 1:1,
@@ -302,12 +304,19 @@ export interface PixiAuthoringOptions {
302
304
  * `Texture.WHITE` and a size; a graphics gets a drawn rect): a node the author
303
305
  * cannot see is indistinguishable from a create that silently failed.
304
306
  */
305
- const CANVAS_CREATABLE_KINDS: readonly { kind: string; label: string }[] = [
306
- { kind: 'container', label: 'Container' },
307
- { kind: 'sprite', label: 'Sprite' },
308
- { kind: 'text', label: 'Text' },
309
- { kind: 'graphics', label: 'Graphics' },
310
- ];
307
+ /** The project-local section holding each canvas world's locked and grouped label paths. */
308
+ const EDIT_LOCKS_SECTION = 'canvasEditLocks';
309
+ type EditLocks = Record<string, { readonly locked: readonly string[]; readonly grouped: readonly string[] }>;
310
+
311
+ function readEditLocks(): EditLocks {
312
+ return editorHost().projectLocalState.read<EditLocks>(EDIT_LOCKS_SECTION) ?? {};
313
+ }
314
+
315
+ function writeEditLocks(world: string, locks: EditLocks[string]): void {
316
+ editorHost().projectLocalState.write(EDIT_LOCKS_SECTION, { ...readEditLocks(), [world]: locks });
317
+ }
318
+
319
+ const CANVAS_CREATABLE_KINDS = PIXI_CREATABLE_KINDS;
311
320
 
312
321
  /** What a canvas world can take from the asset browser: an image becomes a
313
322
  * Sprite. A model or an audio file has no display object to become here. */
@@ -404,6 +413,11 @@ function defaultLabelFor(kind: string): string {
404
413
  * `pixi` is THIS SURFACE's namespace ({@link PixiAuthoringOptions.pixi}): a
405
414
  * node built from another graph's classes is a foreign object in the world's
406
415
  * own display list, and the world's renderer is the one that has to draw it. */
416
+ /** A ParticleContainer, from whichever copy of Pixi made it: it adds particles, never children. */
417
+ function isParticleContainer(object: Container | null | undefined): boolean {
418
+ return !!object && typeof (object as { addParticle?: unknown }).addParticle === 'function';
419
+ }
420
+
407
421
  function createDisplayObject(pixi: CanvasPixiNamespace, kind: string): Container | null {
408
422
  switch (kind) {
409
423
  case 'container':
@@ -417,6 +431,31 @@ function createDisplayObject(pixi: CanvasPixiNamespace, kind: string): Container
417
431
  return new pixi.Text({ text: 'Text', style: { fill: 0xffffff, fontSize: 24 } });
418
432
  case 'graphics':
419
433
  return new pixi.Graphics().rect(0, 0, 100, 100).fill(0xffffff);
434
+ case 'animatedSprite': {
435
+ const animated = new pixi.AnimatedSprite([pixi.Texture.WHITE]);
436
+ animated.setSize(64, 64);
437
+ return animated;
438
+ }
439
+ case 'tilingSprite':
440
+ return new pixi.TilingSprite({ texture: pixi.Texture.WHITE, width: 100, height: 100 });
441
+ case 'nineSliceSprite':
442
+ return new pixi.NineSliceSprite({ texture: pixi.Texture.WHITE, width: 100, height: 100 });
443
+ case 'bitmapText':
444
+ return new pixi.BitmapText({ text: 'BitmapText', style: { fill: 0xffffff, fontSize: 24 } });
445
+ case 'htmlText':
446
+ return new pixi.HTMLText({ text: 'HTMLText', style: { fill: 0xffffff, fontSize: 24 } });
447
+ case 'meshPlane': {
448
+ const plane = new pixi.MeshPlane({ texture: pixi.Texture.WHITE });
449
+ plane.setSize(100, 100);
450
+ return plane;
451
+ }
452
+ case 'perspectiveMesh': {
453
+ const mesh = new pixi.PerspectiveMesh({ texture: pixi.Texture.WHITE });
454
+ mesh.setSize(100, 100);
455
+ return mesh;
456
+ }
457
+ case 'particleContainer':
458
+ return new pixi.ParticleContainer();
420
459
  default:
421
460
  return null;
422
461
  }
@@ -490,6 +529,35 @@ interface CanvasBoxEditSession {
490
529
  readonly id: string;
491
530
  readonly transform: Transform;
492
531
  readonly rect: { x: number; y: number; width: number; height: number };
532
+ /** The parent's world matrix's linear part (Pixi's a, b, c, d), when the node has a parent. */
533
+ readonly parentLinear?: { a: number; b: number; c: number; d: number };
534
+ }
535
+
536
+ /**
537
+ * The local turn, in degrees, that turns the node's own x axis by `degrees` on screen. Godot measures
538
+ * a rotate drag in the parent's space; a turn added straight to the local rotation goes backwards
539
+ * under a mirrored parent and by the wrong amount under an unevenly scaled one.
540
+ */
541
+ function localTurn(session: CanvasBoxEditSession, degrees: number): number {
542
+ const m = session.parentLinear;
543
+ if (!m) return degrees;
544
+ const det = m.a * m.d - m.b * m.c;
545
+ if (Math.abs(det) < 1e-9) return degrees;
546
+ const [, , z, w] = session.transform.rotation;
547
+ const r = 2 * Math.atan2(z, w);
548
+ // The node's x axis on screen, turned by the drag, taken back into the parent's space.
549
+ const vx = m.a * Math.cos(r) + m.c * Math.sin(r);
550
+ const vy = m.b * Math.cos(r) + m.d * Math.sin(r);
551
+ const t = (degrees * Math.PI) / 180;
552
+ const wx = vx * Math.cos(t) - vy * Math.sin(t);
553
+ const wy = vx * Math.sin(t) + vy * Math.cos(t);
554
+ const ux = (m.d * wx - m.c * wy) / det;
555
+ const uy = (-m.b * wx + m.a * wy) / det;
556
+ // A long drag passes half a turn: keep the answer beside the turn's own direction in that space.
557
+ const base = det < 0 ? -t : t;
558
+ const raw = Math.atan2(uy, ux) - r - base;
559
+ const wrapped = Math.atan2(Math.sin(raw), Math.cos(raw));
560
+ return ((base + wrapped) * 180) / Math.PI;
493
561
  }
494
562
 
495
563
  function boxPatchChannels(patch: Record<string, number>): TransformChannel[] {
@@ -538,7 +606,18 @@ function transformForBoxPatch(
538
606
  scale: [...session.transform.scale],
539
607
  };
540
608
  const rotate = patch['rotate'];
541
- if (rotate !== undefined) next.rotation = rotateTransformZ(session.transform, rotate);
609
+ if (rotate !== undefined) {
610
+ let turn = localTurn(session, rotate);
611
+ const step = patch['rotationStep'];
612
+ if (step !== undefined && step > 0) {
613
+ // Configure Snap's absolute step lands the node's own rotation on the step from the offset.
614
+ const [, , z, w] = session.transform.rotation;
615
+ const start = (2 * Math.atan2(z, w) * 180) / Math.PI;
616
+ const offset = patch['rotationOffset'] ?? 0;
617
+ turn = Math.round((start + turn - offset) / step) * step + offset - start;
618
+ }
619
+ next.rotation = rotateTransformZ(session.transform, turn);
620
+ }
542
621
  const width = patch['width'];
543
622
  if (width !== undefined && session.rect.width > 0) {
544
623
  next.scale[0] = session.transform.scale[0] * Math.max(0.0001, width / session.rect.width);
@@ -669,10 +748,17 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
669
748
  | undefined;
670
749
  private listeners = new Set<() => void>();
671
750
  private boxEditSessions = new Map<string, CanvasBoxEditSession>();
672
- private lockedIds = new Set<string>();
673
- /** Godot's Group: a click on any node inside a grouped one selects the group. Session-local, as
674
- * the lock is. */
675
- private groupedIds = new Set<string>();
751
+ /**
752
+ * Lock (a click passes through the node) and Godot's Group (a click inside a grouped node selects
753
+ * the group), held by each node's label path in this world so they survive the remount a source
754
+ * write causes and the next session. Kept per checkout, as Unity keeps scene pickability per
755
+ * user; Godot writes them into the scene file instead.
756
+ */
757
+ private lockedPaths = new Set<string>();
758
+ private groupedPaths = new Set<string>();
759
+ private readonly editLockWorld: string;
760
+ /** Each node's label path, cleared whenever the tree is re-projected. */
761
+ private pathKeys = new Map<string, string>();
676
762
 
677
763
  constructor(
678
764
  private readonly root: Container,
@@ -728,8 +814,10 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
728
814
  const hit = this.projector.pick(clientX, clientY, (id) => this.isPickLocked(id));
729
815
  return hit ? this.groupOf(hit) : hit;
730
816
  },
731
- candidates: (clientX, clientY) =>
732
- this.projector.candidates(clientX, clientY, (id) => this.isPickLocked(id)),
817
+ candidates: (clientX, clientY, options) =>
818
+ this.projector.candidates(clientX, clientY, (id) =>
819
+ options?.includeLocked ? false : this.isPickLocked(id),
820
+ ),
733
821
  };
734
822
  this.rects = {
735
823
  rect: (id) => this.projector.rect(id),
@@ -750,7 +838,46 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
750
838
  this.target.provenance.source === 'source-code' && this.target.persistence !== undefined,
751
839
  };
752
840
  this.target.bind({ a2d: this.a2d, store, journal: opts.journal, notify: () => this.notify() });
753
- this.structure = this.target.structure ?? this.liveStructure;
841
+ this.editLockWorld = opts.journal.id;
842
+ const saved = readEditLocks()[this.editLockWorld];
843
+ this.lockedPaths = new Set(saved?.locked ?? []);
844
+ this.groupedPaths = new Set(saved?.grouped ?? []);
845
+ const structure = this.target.structure ?? this.liveStructure;
846
+ // A placed creation reaches the structure in the new node's PARENT's own space: the target
847
+ // writes `x`/`y` as authored, and only this adapter knows the frame `rects` answer in.
848
+ // A ParticleContainer holds particles its code adds, never display objects: Pixi's `addChild`
849
+ // on one throws, so nothing is created or moved into it.
850
+ const holdsNoChildren = (parentId: string | null | undefined): boolean =>
851
+ !!parentId && isParticleContainer(this.projector.object(parentId));
852
+ const noChildren = 'a ParticleContainer holds particles added with addParticle, not display objects.';
853
+ this.structure = {
854
+ ...structure,
855
+ create: (kind, parentId, at) =>
856
+ holdsNoChildren(parentId)
857
+ ? { id: '', ack: this.refuseStructure(noChildren) }
858
+ : structure.create(kind, parentId, at ? this.pointInParent(parentId ?? null, at) : undefined),
859
+ ...(structure.reparent
860
+ ? {
861
+ reparent: (id: string, newParentId: string | null) =>
862
+ holdsNoChildren(newParentId) ? this.refuseStructure(noChildren) : structure.reparent!(id, newParentId),
863
+ }
864
+ : {}),
865
+ ...(structure.paste
866
+ ? {
867
+ paste: (parentId: string | null) =>
868
+ holdsNoChildren(parentId) ? this.refuseStructure(noChildren) : structure.paste!(parentId),
869
+ }
870
+ : {}),
871
+ ...(structure.canPaste
872
+ ? { canPaste: (parentId: string | null) => !holdsNoChildren(parentId) && structure.canPaste!(parentId) }
873
+ : {}),
874
+ ...(structure.creatableKinds
875
+ ? {
876
+ creatableKinds: (parentId: string | null) =>
877
+ holdsNoChildren(parentId) ? [] : structure.creatableKinds!(parentId),
878
+ }
879
+ : {}),
880
+ };
754
881
  this.assetDrop =
755
882
  this.target.assetDrop ??
756
883
  ({
@@ -851,6 +978,7 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
851
978
  queueMicrotask(() => {
852
979
  this.structureNotifyQueued = false;
853
980
  this.projector.reproject();
981
+ this.pathKeys.clear();
854
982
  this.watchStructure(this.root);
855
983
  this.target.onReindex();
856
984
  this.notify();
@@ -951,7 +1079,7 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
951
1079
  // degrade — a half-answer would be a menu item that silently does nothing.
952
1080
 
953
1081
  private readonly liveStructure: StructureProvider = {
954
- create: (kind, parentId) => this.createStructuralNode(kind, parentId ?? null),
1082
+ create: (kind, parentId, at) => this.createStructuralNode(kind, parentId ?? null, at),
955
1083
  remove: (id) => this.removeStructuralNodes([id]),
956
1084
  removeMany: (ids) => this.removeStructuralNodes(ids),
957
1085
  duplicate: (id) => this.duplicateStructuralNode(id),
@@ -967,7 +1095,7 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
967
1095
  private containerFor(parentId: string | null): Container | null {
968
1096
  if (!parentId) return this.root;
969
1097
  const object = this.projector.object(parentId);
970
- if (!object || typeof object.addChild !== 'function') return null;
1098
+ if (!object || typeof object.addChild !== 'function' || isParticleContainer(object)) return null;
971
1099
  return object;
972
1100
  }
973
1101
 
@@ -1015,6 +1143,7 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
1015
1143
  */
1016
1144
  private afterStructuralChange(): void {
1017
1145
  this.projector.reproject();
1146
+ this.pathKeys.clear();
1018
1147
  this.watchStructure(this.root);
1019
1148
  this.target.onReindex();
1020
1149
  this.notify();
@@ -1031,7 +1160,22 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
1031
1160
  * same turn. The live-only report is the other half of the answer and rides
1032
1161
  * back in `ack` (see `StructuralIdWrite`) instead of being fired `void`.
1033
1162
  */
1034
- private createStructuralNode(kind: string, parentId: string | null): StructuralIdWrite {
1163
+ /** A point in the `rects` frame, in the space of the container a new child of `parentId` joins. */
1164
+ private pointInParent(
1165
+ parentId: string | null,
1166
+ at: { readonly x: number; readonly y: number },
1167
+ ): { x: number; y: number } | undefined {
1168
+ const parent = this.containerFor(parentId);
1169
+ if (!parent || typeof parent.updateLocalTransform !== 'function') return undefined;
1170
+ const local = this.localToRootMatrix(parent).clone().invert().apply({ x: at.x, y: at.y });
1171
+ return { x: local.x, y: local.y };
1172
+ }
1173
+
1174
+ private createStructuralNode(
1175
+ kind: string,
1176
+ parentId: string | null,
1177
+ at?: { readonly x: number; readonly y: number },
1178
+ ): StructuralIdWrite {
1035
1179
  const parent = this.containerFor(parentId);
1036
1180
  if (!parent) {
1037
1181
  return {
@@ -1047,6 +1191,7 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
1047
1191
  };
1048
1192
  }
1049
1193
  node.label = defaultLabelFor(kind);
1194
+ if (at) node.position.set(at.x, at.y);
1050
1195
  const label = `Create ${node.label}`;
1051
1196
  this.structureHistory.track(parent);
1052
1197
  this.structureHistory.track(node);
@@ -1280,7 +1425,13 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
1280
1425
  this.boxEditSessions.delete(id);
1281
1426
  return;
1282
1427
  }
1283
- this.boxEditSessions.set(id, { id, rect, transform: this.transforms.get(id) });
1428
+ const parent = this.projector.object(id)?.parent?.worldTransform;
1429
+ this.boxEditSessions.set(id, {
1430
+ id,
1431
+ rect,
1432
+ transform: this.transforms.get(id),
1433
+ ...(parent ? { parentLinear: { a: parent.a, b: parent.b, c: parent.c, d: parent.d } } : {}),
1434
+ });
1284
1435
  this.transforms.beginEdit?.(id);
1285
1436
  },
1286
1437
  apply: (id, patch) => this.applyBoxEdit(id, patch),
@@ -1514,15 +1665,19 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
1514
1665
  ],
1515
1666
  get: (id, path) =>
1516
1667
  path === 'locked'
1517
- ? this.lockedIds.has(id)
1668
+ ? this.lockedPaths.has(this.pathKeyOf(id))
1518
1669
  : path === 'grouped'
1519
- ? this.groupedIds.has(id)
1670
+ ? this.groupedPaths.has(this.pathKeyOf(id))
1520
1671
  : this.target.get(id, path),
1521
1672
  set: (id, path, value) => {
1522
1673
  if (path === 'locked' || path === 'grouped') {
1523
- const set = path === 'locked' ? this.lockedIds : this.groupedIds;
1524
- if (value === true) set.add(id);
1525
- else set.delete(id);
1674
+ const set = path === 'locked' ? this.lockedPaths : this.groupedPaths;
1675
+ if (value === true) set.add(this.pathKeyOf(id));
1676
+ else set.delete(this.pathKeyOf(id));
1677
+ writeEditLocks(this.editLockWorld, {
1678
+ locked: [...this.lockedPaths],
1679
+ grouped: [...this.groupedPaths],
1680
+ });
1526
1681
  this.notify();
1527
1682
  return;
1528
1683
  }
@@ -1532,12 +1687,36 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
1532
1687
  remove: (id, path) => this.target.remove?.(id, path),
1533
1688
  };
1534
1689
 
1690
+ /** `id`'s label path from its world's root: each step the authored label and its place among
1691
+ * same-labelled siblings, so it holds across line edits and reloads (a rename drops it). */
1692
+ private pathKeyOf(id: string): string {
1693
+ const known = this.pathKeys.get(id);
1694
+ if (known !== undefined) return known;
1695
+ const labelOf = (nodeId: string): string => this.projector.object(nodeId)?.label ?? '';
1696
+ const segments: string[] = [];
1697
+ let current: string | null = id;
1698
+ while (current) {
1699
+ const node = this.projector.node(current);
1700
+ if (!node) break;
1701
+ const siblings = node.parentId
1702
+ ? (this.projector.node(node.parentId)?.childIds ?? [])
1703
+ : this.projector.roots().map((root) => root.id);
1704
+ const label = labelOf(current);
1705
+ const place = siblings.filter((sibling) => labelOf(sibling) === label).indexOf(current);
1706
+ segments.unshift(`${label}#${Math.max(0, place)}`);
1707
+ current = node.parentId ?? null;
1708
+ }
1709
+ const key = segments.join('/');
1710
+ this.pathKeys.set(id, key);
1711
+ return key;
1712
+ }
1713
+
1535
1714
  /** The outermost grouped node that contains `id`, or `id` itself when none does. */
1536
1715
  private groupOf(id: string): string {
1537
1716
  let group = id;
1538
1717
  let current: string | null = id;
1539
1718
  while (current) {
1540
- if (this.groupedIds.has(current)) group = current;
1719
+ if (this.groupedPaths.has(this.pathKeyOf(current))) group = current;
1541
1720
  current = this.projector.node(current)?.parentId ?? null;
1542
1721
  }
1543
1722
  return group;
@@ -1546,7 +1725,7 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
1546
1725
  private isPickLocked(id: string): boolean {
1547
1726
  let current: string | null = id;
1548
1727
  while (current) {
1549
- if (this.lockedIds.has(current)) return true;
1728
+ if (this.lockedPaths.has(this.pathKeyOf(current))) return true;
1550
1729
  current = this.projector.node(current)?.parentId ?? null;
1551
1730
  }
1552
1731
  return false;
@@ -1565,8 +1744,7 @@ export class PixiAuthoringAdapter implements AuthoringAdapter {
1565
1744
 
1566
1745
  dispose(): void {
1567
1746
  this.boxEditSessions.clear();
1568
- this.lockedIds.clear();
1569
- this.groupedIds.clear();
1747
+ this.pathKeys.clear();
1570
1748
  for (const object of this.watchedForStructure) {
1571
1749
  object.off('childAdded', this.onStructureChanged);
1572
1750
  object.off('childRemoved', this.onStructureChanged);