@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
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The display objects a canvas world creates, as its create dialog lists them: Pixi's own class
3
+ * tree (every one of them is a Container) and a line on what each is, from Pixi's API reference.
4
+ */
5
+
6
+ import type { CreatableKind } from '@volter/editor-project/adapter';
7
+
8
+ export const PIXI_CREATABLE_KINDS: readonly CreatableKind[] = [
9
+ {
10
+ kind: 'container',
11
+ label: 'Container',
12
+ description: 'A general-purpose display object that holds children, and transforms and draws them together.',
13
+ },
14
+ {
15
+ kind: 'sprite',
16
+ label: 'Sprite',
17
+ extends: 'container',
18
+ description: 'Draws one texture: an image, or a frame of a spritesheet.',
19
+ },
20
+ {
21
+ kind: 'animatedSprite',
22
+ label: 'AnimatedSprite',
23
+ extends: 'sprite',
24
+ description: 'Plays a list of textures as frames at an animation speed.',
25
+ },
26
+ {
27
+ kind: 'tilingSprite',
28
+ label: 'TilingSprite',
29
+ extends: 'container',
30
+ description: 'Repeats one texture across its width and height, scrollable by its tile position.',
31
+ },
32
+ {
33
+ kind: 'nineSliceSprite',
34
+ label: 'NineSliceSprite',
35
+ extends: 'container',
36
+ description: 'Stretches a texture without stretching its corners: the nine-slice panel of a UI.',
37
+ },
38
+ {
39
+ kind: 'text',
40
+ label: 'Text',
41
+ extends: 'container',
42
+ description: 'Draws a string with a text style (font, size, fill, wrapping).',
43
+ },
44
+ {
45
+ kind: 'bitmapText',
46
+ label: 'BitmapText',
47
+ extends: 'container',
48
+ description: 'Draws a string from a bitmap font: fast to change every frame.',
49
+ },
50
+ {
51
+ kind: 'htmlText',
52
+ label: 'HTMLText',
53
+ extends: 'container',
54
+ description: 'Draws a string of HTML and CSS markup.',
55
+ },
56
+ {
57
+ kind: 'graphics',
58
+ label: 'Graphics',
59
+ extends: 'container',
60
+ description: 'Draws vector shapes (rectangles, circles, paths) from its draw callback.',
61
+ },
62
+ {
63
+ kind: 'meshPlane',
64
+ label: 'MeshPlane',
65
+ extends: 'container',
66
+ description: 'A textured grid of vertices that can be bent and deformed.',
67
+ },
68
+ {
69
+ kind: 'perspectiveMesh',
70
+ label: 'PerspectiveMesh',
71
+ extends: 'meshPlane',
72
+ description: 'A texture drawn in perspective between four corner points.',
73
+ },
74
+ {
75
+ kind: 'particleContainer',
76
+ label: 'ParticleContainer',
77
+ extends: 'container',
78
+ description: 'Draws many lightweight particles in one batch, added from code with addParticle.',
79
+ },
80
+ ];
@@ -30,6 +30,7 @@
30
30
  * moved, and the persistence parameter is what this unit added.
31
31
  */
32
32
 
33
+ import { physicsOwnerOf } from '@volter/editor-project/adapter';
33
34
  import type {
34
35
  AuthoringAdapter2D,
35
36
  Overlay2D,
@@ -554,7 +555,7 @@ export function createLiveCanvasWriteTarget(
554
555
  * `literalsFor` is what a rendering one needs. A revert or an apply is a
555
556
  * deliberate act with somewhere to await, so it must not silently do nothing
556
557
  * merely because nothing happened to have described this node first (an agent
557
- * driving the provider through `vgai eval` never does).
558
+ * driving the provider through `volter-game-editor eval` never does).
558
559
  */
559
560
  const literalsNow = async (
560
561
  id: string,
@@ -807,7 +808,7 @@ export function createLiveCanvasWriteTarget(
807
808
  beginTransformEdit(id: string): void {
808
809
  historyResource?.assertCanMutate();
809
810
  const display = a2d.displayObject(id);
810
- if (display && physics.ownerOf(display) === 'physics') physics.freeze(display);
811
+ if (display && physicsOwnerOf(physics, display) === 'physics') physics.freeze(display);
811
812
  editStartState = captureHistoryState();
812
813
  editBaseline = persist ? (a2d.getTransform(id) ?? undefined) : undefined;
813
814
  editMovedOrigin = false;
@@ -816,7 +817,7 @@ export function createLiveCanvasWriteTarget(
816
817
  writeTransform(id: string, next: Transform2DValue): void {
817
818
  historyResource?.assertCanMutate();
818
819
  const display = a2d.displayObject(id);
819
- if (display && physics.ownerOf(display) === 'physics') {
820
+ if (display && physicsOwnerOf(physics, display) === 'physics') {
820
821
  physics.commit(display, next.position, next.rotation);
821
822
  }
822
823
  a2d.setTransform(id, next);
@@ -829,7 +830,7 @@ export function createLiveCanvasWriteTarget(
829
830
  // source write is attempted.
830
831
  endTransformEdit(id: string): void | Promise<WriteAck> {
831
832
  const display = a2d.displayObject(id);
832
- if (display && physics.ownerOf(display) === 'physics') physics.unfreeze(display);
833
+ if (display && physicsOwnerOf(physics, display) === 'physics') physics.unfreeze(display);
833
834
  const label = `Transform ${labelOf(id)}`;
834
835
  const before = editStartState;
835
836
  const baseline = editBaseline;
@@ -907,7 +908,7 @@ export function createLiveCanvasWriteTarget(
907
908
  * channel a refused transform does: they asked for source writes, so each
908
909
  * one that did not land matters individually. Unarmed, the first structural
909
910
  * edit of the session raises the warning that reaches the product's own
910
- * doors (`vgai status`, the session journal, one ack to clear) — a whole
911
+ * doors (`volter-game-editor status`, the session journal, one ack to clear) — a whole
911
912
  * class of this author's edits will not be saved, which is exactly what an
912
913
  * unresolved warning is for — and every later op logs, because repeating an
913
914
  * ack-requiring entry per created node would flood the set with the lane
@@ -42,6 +42,7 @@ import {
42
42
  transform2DChanged,
43
43
  } from './pixi-transform-channels';
44
44
  import { createStructWritePipe, type StructOpOptions } from './struct-write-pipe';
45
+ import { PIXI_CREATABLE_KINDS } from './pixi-creatable-kinds';
45
46
  import {
46
47
  clipboardOutcome,
47
48
  LIVE_ONLY_ACK,
@@ -102,6 +103,7 @@ const PANEL_OWNED_PROPS = new Set([
102
103
  'scale-y',
103
104
  'label',
104
105
  'visible',
106
+ 'skew',
105
107
  ]);
106
108
  /** Props the drop snippet writes itself. */
107
109
  const DROP_SNIPPET_PROPS = new Set(['x', 'y', 'name', 'children']);
@@ -112,16 +114,20 @@ const CREATE_SNIPPETS: Readonly<Record<string, string>> = {
112
114
  sprite: '<pixiSprite label="Sprite" />',
113
115
  text: '<pixiText label="Text" text="Text" />',
114
116
  graphics: '<pixiGraphics label="Graphics" />',
117
+ animatedSprite: '<pixiAnimatedSprite label="AnimatedSprite" textures={[Texture.WHITE]} width={64} height={64} />',
118
+ tilingSprite: '<pixiTilingSprite label="TilingSprite" texture={Texture.WHITE} width={100} height={100} />',
119
+ nineSliceSprite: '<pixiNineSliceSprite label="NineSliceSprite" texture={Texture.WHITE} width={100} height={100} />',
120
+ bitmapText: '<pixiBitmapText label="BitmapText" text="BitmapText" />',
121
+ htmlText: '<pixiHTMLText label="HTMLText" text="HTMLText" />',
122
+ meshPlane: '<pixiMeshPlane label="MeshPlane" texture={Texture.WHITE} width={100} height={100} />',
123
+ perspectiveMesh: '<pixiPerspectiveMesh label="PerspectiveMesh" texture={Texture.WHITE} width={100} height={100} />',
124
+ particleContainer: '<pixiParticleContainer label="ParticleContainer" />',
115
125
  };
116
- const CREATE_LABELS: Readonly<Record<string, string>> = {
117
- container: 'Container',
118
- sprite: 'Sprite',
119
- text: 'Text',
120
- graphics: 'Graphics',
121
- };
126
+ /** A kind whose snippet names Pixi's `Texture`, which the file must then import. */
127
+ const TEXTURE_SNIPPET_KINDS = new Set(['animatedSprite', 'tilingSprite', 'nineSliceSprite', 'meshPlane', 'perspectiveMesh']);
122
128
 
123
129
  function isHiddenProp(name: string): boolean {
124
- return HIDDEN_PROPS.has(name) || /^on[A-Z]/.test(name) || name.startsWith('__vgai');
130
+ return HIDDEN_PROPS.has(name) || /^on[A-Z]/.test(name) || name.startsWith('__volter');
125
131
  }
126
132
 
127
133
  /** Present a source identifier the way native engine inspectors present
@@ -217,6 +223,10 @@ export function createSourceCanvasWriteTarget(
217
223
  const dynamicPaths = new Set<string>();
218
224
  /** Pre-gesture 2D poses, one per id currently under a drag. */
219
225
  const editStarts = new Map<string, Transform2DValue>();
226
+ const pendingOrigins = new Map<
227
+ string,
228
+ { kind: 'pivot' | 'anchor'; value: readonly [number, number]; before: readonly [number, number] }
229
+ >();
220
230
  let commitQueue = Promise.resolve();
221
231
 
222
232
  const oidOf = (id: string): string | null => {
@@ -464,10 +474,16 @@ export function createSourceCanvasWriteTarget(
464
474
  const channels = (['position', 'rotation', 'scale'] as const).filter((channel) =>
465
475
  transform2DChanged(channel, before, after),
466
476
  );
467
- if (channels.length === 0) return false;
468
-
469
- const revert = (channel: TransformChannel): void => {
470
- if (channel === 'position') a2d.setTransform(id, { position: before.position });
477
+ // A moved origin (pivot or anchor) is written with the position that compensates it.
478
+ const origin = pendingOrigins.get(id);
479
+ pendingOrigins.delete(id);
480
+ if (channels.length === 0 && !origin) return false;
481
+
482
+ const revert = (channel: TransformChannel | 'origin'): void => {
483
+ if (channel === 'origin') {
484
+ const display = a2d.displayObject(id) as (Container & Record<string, unknown>) | null;
485
+ if (display && origin) display[origin.kind] = { x: origin.before[0], y: origin.before[1] };
486
+ } else if (channel === 'position') a2d.setTransform(id, { position: before.position });
471
487
  else if (channel === 'rotation') a2d.setTransform(id, { rotation: before.rotation });
472
488
  else a2d.setTransform(id, { scale: before.scale });
473
489
  notify();
@@ -495,7 +511,7 @@ export function createSourceCanvasWriteTarget(
495
511
  const instance = componentIdentityOf(id);
496
512
  const channelBlocked = (channel: TransformChannel): boolean =>
497
513
  componentOwnedChannel(id, channel, attrs, after);
498
- const writes: Array<{ channel: TransformChannel; prop: string; value: string }> = [];
514
+ const writes: Array<{ channel: TransformChannel | 'origin'; prop: string; value: string }> = [];
499
515
  for (const channel of channels) {
500
516
  if (channelBlocked(channel)) {
501
517
  dynamicPaths.add(`${id}|${channel}`);
@@ -532,6 +548,16 @@ export function createSourceCanvasWriteTarget(
532
548
  }
533
549
  for (const write of plan.writes) writes.push({ channel, ...write });
534
550
  }
551
+ if (origin) {
552
+ if (instance && channelBlocked('position')) revert('origin');
553
+ else {
554
+ writes.push({
555
+ channel: 'origin',
556
+ prop: origin.kind,
557
+ value: `{ x: ${formatSourceNumber(origin.value[0])}, y: ${formatSourceNumber(origin.value[1])} }`,
558
+ });
559
+ }
560
+ }
535
561
  if (writes.length === 0) return false;
536
562
 
537
563
  let persisted = false;
@@ -543,7 +569,8 @@ export function createSourceCanvasWriteTarget(
543
569
  // must not need a trip to the text editor.
544
570
  addIfMissing: true,
545
571
  // `scale={1.5}` becomes `scale={{ x, y }}` when a gesture makes it non-uniform.
546
- ...(write.prop === 'scale' ? { allowShapeUpgrade: true } : {}),
572
+ // The same for an origin authored as one number (`pivot={60}`).
573
+ ...(write.prop === 'scale' || write.channel === 'origin' ? { allowShapeUpgrade: true } : {}),
547
574
  });
548
575
  if (res.changed) {
549
576
  persisted = true;
@@ -595,6 +622,38 @@ export function createSourceCanvasWriteTarget(
595
622
 
596
623
  /** One prop edit. Answers whether a byte reached the entry TSX; every
597
624
  * refusal has already named itself on the console before it returns. */
625
+ /** Godot's Node2D Skew, one axis in degrees: Pixi's `skew` point in radians, written whole. */
626
+ const writeSkew = async (id: string, axis: 'x' | 'y', degrees: number): Promise<boolean> => {
627
+ const display = a2d.displayObject(id);
628
+ const oid = oidOf(id);
629
+ if (!display || !oid || !backend?.writeProp || !Number.isFinite(degrees)) return false;
630
+ const before = { x: display.skew.x, y: display.skew.y };
631
+ const radians = (degrees * Math.PI) / 180;
632
+ const next = { x: axis === 'x' ? radians : before.x, y: axis === 'y' ? radians : before.y };
633
+ display.skew.set(next.x, next.y);
634
+ notify();
635
+ const res = await backend.writeProp(
636
+ oid,
637
+ 'skew',
638
+ // Radians to six places, so a typed angle reads back as typed (four places lose 0.003°).
639
+ `{ x: ${String(Number(next.x.toFixed(6)))}, y: ${String(Number(next.y.toFixed(6)))} }`,
640
+ { addIfMissing: true, allowShapeUpgrade: true },
641
+ );
642
+ if (!res.changed && (res.dynamic || res.error)) {
643
+ display.skew.set(before.x, before.y);
644
+ notify();
645
+ // biome-ignore lint/suspicious/noConsole: a refused write must say why
646
+ console.warn(
647
+ `[canvas-source ${entryPath}] skew write refused for oid "${oid}": ` +
648
+ `${res.dynamic ? 'dynamic expression (guarded)' : res.error} — reverting.`,
649
+ );
650
+ return false;
651
+ }
652
+ await refreshSourceState();
653
+ notify();
654
+ return res.changed;
655
+ };
656
+
598
657
  const writeJsxProp = async (
599
658
  id: string,
600
659
  path: string,
@@ -1130,12 +1189,20 @@ export function createSourceCanvasWriteTarget(
1130
1189
  // The new element's oid is minted server-side on the remount, so the id
1131
1190
  // half is '' and the ack half is this creation's own piped write — handed
1132
1191
  // back rather than fired `void` (see `StructuralIdWrite`).
1133
- create: (kind, parentId) => {
1134
- const snippet = CREATE_SNIPPETS[kind];
1192
+ create: (kind, parentId, at) => {
1193
+ // `at` arrives in the new node's parent's own space (the adapter converts it).
1194
+ const base = CREATE_SNIPPETS[kind];
1195
+ const snippet =
1196
+ base && at
1197
+ ? base.replace(/\s*\/>$/, ` x={${formatSourceNumber(at.x)}} y={${formatSourceNumber(at.y)}} />`)
1198
+ : base;
1135
1199
  // Nothing was attempted (the palette is empty in both cases), so there is
1136
1200
  // no write to ack.
1137
1201
  if (!snippet || !backend?.writeStruct) return { id: '', ack: undefined };
1138
- if (parentId) return { id: '', ack: structOp(parentId, 'create', { snippet }) };
1202
+ const options = TEXTURE_SNIPPET_KINDS.has(kind)
1203
+ ? { snippet, ensureImport: { name: 'Texture', module: 'pixi.js' } }
1204
+ : { snippet };
1205
+ if (parentId) return { id: '', ack: structOp(parentId, 'create', options) };
1139
1206
  const lastRoot = a2d.roots().at(-1);
1140
1207
  if (!lastRoot) {
1141
1208
  return {
@@ -1145,15 +1212,14 @@ export function createSourceCanvasWriteTarget(
1145
1212
  ),
1146
1213
  };
1147
1214
  }
1148
- return { id: '', ack: structOp(lastRoot.id, 'create-sibling', { snippet }) };
1215
+ return { id: '', ack: structOp(lastRoot.id, 'create-sibling', options) };
1149
1216
  },
1150
1217
  creatableKinds: (parentId) => {
1151
1218
  if (!backend?.writeStruct) return [];
1152
1219
  if (parentId && !oidOf(parentId)) return [];
1153
1220
  if (!parentId && a2d.roots().length === 0) return [];
1154
- return Object.keys(CREATE_SNIPPETS).map((kind) => ({
1155
- kind,
1156
- label: CREATE_LABELS[kind] ?? kind,
1221
+ return PIXI_CREATABLE_KINDS.filter((entry) => CREATE_SNIPPETS[entry.kind] !== undefined).map((entry) => ({
1222
+ ...entry,
1157
1223
  }));
1158
1224
  },
1159
1225
  // Returns the promise (never `void structOp(…)`): `deleteSelection`
@@ -1469,6 +1535,21 @@ export function createSourceCanvasWriteTarget(
1469
1535
  if (!editStarts.has(id)) editStarts.set(id, read2D(id));
1470
1536
  },
1471
1537
 
1538
+ /**
1539
+ * Mid-gesture origin write — see {@link CanvasWriteTarget.writeOrigin}. The live object moves
1540
+ * now; the `pivot`/`anchor` prop is written at `endTransformEdit`, in the same gesture as the
1541
+ * position that compensates it.
1542
+ */
1543
+ writeOrigin(id: string, kind: 'pivot' | 'anchor', value: readonly [number, number]): void {
1544
+ const display = a2d.displayObject(id) as (Container & Record<string, unknown>) | null;
1545
+ const current = display?.[kind] as { x?: unknown; y?: unknown } | undefined;
1546
+ if (!display || typeof current?.x !== 'number' || typeof current.y !== 'number') return;
1547
+ const before = pendingOrigins.get(id)?.before ?? ([current.x, current.y] as const);
1548
+ display[kind] = { x: value[0], y: value[1] };
1549
+ pendingOrigins.set(id, { kind, value, before });
1550
+ notify();
1551
+ },
1552
+
1472
1553
  writeTransform(id: string, next: Transform2DValue): void {
1473
1554
  // The live tree moves first so the paused frame follows the gesture; the
1474
1555
  // source write happens once, at the end.
@@ -1535,6 +1616,13 @@ export function createSourceCanvasWriteTarget(
1535
1616
  { path: 'visible', label: 'Visible', type: 'boolean', group: 'Visibility' },
1536
1617
  ];
1537
1618
  const attrs = attrsOf(id) ?? [];
1619
+ // Godot's Node2D Skew, in its Transform group; a skew the source computes is shown, not edited.
1620
+ const skewAttr = attrs.find((attr) => attr.name === 'skew');
1621
+ const skewFixed = skewAttr ? !skewAttr.isLiteral : false;
1622
+ props.push(
1623
+ { path: 'skew.x', label: 'Skew X (deg)', type: 'number', group: 'Transform', readonly: skewFixed },
1624
+ { path: 'skew.y', label: 'Skew Y (deg)', type: 'number', group: 'Transform', readonly: skewFixed },
1625
+ );
1538
1626
  const declared = declaredPropsOf(id);
1539
1627
  const attrByName = new Map(attrs.map((attr) => [attr.name, attr]));
1540
1628
  const names = [
@@ -1573,6 +1661,9 @@ export function createSourceCanvasWriteTarget(
1573
1661
  if (echoed !== undefined) return echoed;
1574
1662
  if (path === 'name') return display.label ?? '';
1575
1663
  if (path === 'visible') return display.visible;
1664
+ if (path === 'skew.x' || path === 'skew.y') {
1665
+ return Number((((path === 'skew.x' ? display.skew.x : display.skew.y) * 180) / Math.PI).toFixed(4));
1666
+ }
1576
1667
  if (!path.startsWith('jsx.')) return undefined;
1577
1668
  return propValue(attrsOf(id), declaredPropsOf(id), path.slice(4));
1578
1669
  },
@@ -1581,6 +1672,13 @@ export function createSourceCanvasWriteTarget(
1581
1672
  // edit, awaited. A path this target owns no prop for performed no write and
1582
1673
  // returns nothing.
1583
1674
  set(id: string, path: string, value: unknown): void | Promise<WriteAck> {
1675
+ if (path === 'skew.x' || path === 'skew.y') {
1676
+ return piped(
1677
+ () => writeSkew(id, path === 'skew.x' ? 'x' : 'y', Number(value)),
1678
+ undefined,
1679
+ destinationOf(id),
1680
+ );
1681
+ }
1584
1682
  const prop =
1585
1683
  path === 'name'
1586
1684
  ? 'label'
@@ -42,7 +42,7 @@ const ROOT_UID = '#root';
42
42
  * `AuthoringAdapter2D` uses for `__authId`, for the same reason (there is no
43
43
  * side table that survives the object outliving one walk). */
44
44
  interface StructureStamped {
45
- __vgaiStructUid?: string;
45
+ __volterStructUid?: string;
46
46
  }
47
47
 
48
48
  /** A node's own local pose, as plain numbers. Recorded because `reparent`
@@ -185,10 +185,10 @@ export class CanvasStructureHistory {
185
185
  private uidOf(object: Container): string {
186
186
  if (object === this.root) return ROOT_UID;
187
187
  const stamped = object as Container & StructureStamped;
188
- const existing = stamped.__vgaiStructUid;
188
+ const existing = stamped.__volterStructUid;
189
189
  if (existing && this.byUid.get(existing) === object) return existing;
190
190
  const uid = existing ?? `s${crypto.randomUUID()}`;
191
- stamped.__vgaiStructUid = uid;
191
+ stamped.__volterStructUid = uid;
192
192
  this.byUid.set(uid, object);
193
193
  return uid;
194
194
  }
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * A game is a native program plus a declaration of itself; the editor is a
6
6
  * universal client of that declaration. This module is where the declaration
7
- * is READ: the manifest root, the project's parsed `vgai.adapter.ts`, and the
7
+ * is READ: the manifest root, the project's parsed `volter.adapter.ts`, and the
8
8
  * entry module's own static surface, resolved against ONE realm
9
9
  * (`realm-services.ts` — dev or packaged).
10
10
  *
@@ -85,7 +85,7 @@ export interface ResolveRootBindingOptions {
85
85
  * Resolve one manifest root against one realm.
86
86
  *
87
87
  * `adapterDef` is an INPUT, never something this function goes and fetches:
88
- * `vgai.adapter.ts` loading stays in `project-adapter.ts`, and the binding is
88
+ * `volter.adapter.ts` loading stays in `project-adapter.ts`, and the binding is
89
89
  * the hand-off's shape rather than a second loader.
90
90
  */
91
91
  export async function resolveRootBinding(
@@ -252,7 +252,7 @@ function rejectCapturedIngestAsRootAdapter(root: ResolvedAdapterRoot): never {
252
252
  export interface ResolveAllRootEntriesOptions {
253
253
  /**
254
254
  * Remount the named region's entrypoint at this swap-slot key. Only that
255
- * world's entry URL carries the override; siblings share `?vgai-mount=`
255
+ * world's entry URL carries the override; siblings share `?volter-mount=`
256
256
  * alone so the module graph stays one instance.
257
257
  */
258
258
  readonly selectionOverride?:
@@ -276,7 +276,7 @@ interface ResolvedComposition {
276
276
  * resolvers would give each root its own generation, which is the defect.
277
277
  *
278
278
  * The COMPOSITION level is likewise where the project's two ambient inputs are
279
- * fetched — the realm's services and the parsed `vgai.adapter.ts` — because
279
+ * fetched — the realm's services and the parsed `volter.adapter.ts` — because
280
280
  * both are properties of the open project rather than of any one root, and
281
281
  * asking once per composition is what keeps every root of a game bound to the
282
282
  * same declaration. `resolveRootBinding` itself still fetches neither: it is
@@ -302,7 +302,7 @@ async function resolveComposition(
302
302
  out.push({ world, resolved });
303
303
  }
304
304
  // Stringified because that is the form the id travels in everywhere else:
305
- // the entry url's `?vgai-mount=` value, and therefore the key
305
+ // the entry url's `?volter-mount=` value, and therefore the key
306
306
  // `gated-globals.ts` reads back off a module url.
307
307
  return { roots: out, mountId: String(epoch) };
308
308
  }
@@ -351,7 +351,7 @@ export async function resolveAllRoots(
351
351
  *
352
352
  * It also returns this composition's `mountId` — the identity the whole
353
353
  * isolation layer is keyed by. It is minted here (one epoch per composition),
354
- * rides every project module url as `?vgai-mount=`, and is what
354
+ * rides every project module url as `?volter-mount=`, and is what
355
355
  * `gated-globals.ts` resolves a realm and an input gate under.
356
356
  *
357
357
  * It does NOT return the entry modules' harvested `debug`/`systems` bindings.
@@ -174,7 +174,7 @@ export function registerRuntimeModules(modules: Record<string, ModuleNamespace>)
174
174
  }
175
175
  // Exposed on globalThis so shim modules (which run in their own module scope)
176
176
  // can reach the live namespaces by specifier.
177
- (globalThis as Record<string, unknown>)['__vgaiModules'] = Object.fromEntries(runtimeModules);
177
+ (globalThis as Record<string, unknown>)['__volterModules'] = Object.fromEntries(runtimeModules);
178
178
  }
179
179
 
180
180
  /**
@@ -259,7 +259,7 @@ function loadRuntimeModule(spec: string): Promise<void> {
259
259
 
260
260
  /**
261
261
  * The call-time half of a literal dynamic import in project code. The rewrite
262
- * turns `import('tone')` into `globalThis.__vgaiImportRuntimeModule('tone')`,
262
+ * turns `import('tone')` into `globalThis.__volterImportRuntimeModule('tone')`,
263
263
  * so the load — and the wall, if the browser editor does not ship it — happens
264
264
  * exactly when the project's own code executes the import, never earlier.
265
265
  */
@@ -267,7 +267,7 @@ export async function importRuntimeModule(spec: string): Promise<ModuleNamespace
267
267
  if (!runtimeModules.has(spec)) await loadRuntimeModule(spec);
268
268
  return runtimeModules.get(spec) as ModuleNamespace;
269
269
  }
270
- (globalThis as Record<string, unknown>)['__vgaiImportRuntimeModule'] = importRuntimeModule;
270
+ (globalThis as Record<string, unknown>)['__volterImportRuntimeModule'] = importRuntimeModule;
271
271
 
272
272
  /**
273
273
  * Bring every static bare import live, then rewrite by the lexer's own byte
@@ -304,7 +304,7 @@ export async function resolveAndRewriteBareImports(code: string): Promise<string
304
304
  out = `${out.slice(0, record.s)}${shimUrlFor(record.n)}${out.slice(record.e)}`;
305
305
  } else if (record.d >= 0) {
306
306
  // Literal dynamic import: swap the whole `import(...)` expression.
307
- out = `${out.slice(0, record.ss)}globalThis.__vgaiImportRuntimeModule(${JSON.stringify(
307
+ out = `${out.slice(0, record.ss)}globalThis.__volterImportRuntimeModule(${JSON.stringify(
308
308
  record.n,
309
309
  )})${out.slice(record.se)}`;
310
310
  }
@@ -379,7 +379,7 @@ function shimUrlFor(spec: string): string {
379
379
  // `registerRuntimeModules` user) — the same wall, said the same way.
380
380
  if (!ns) throw unregisteredRuntimeModules([spec]);
381
381
 
382
- const lines = [`const __m = globalThis.__vgaiModules[${JSON.stringify(spec)}];`];
382
+ const lines = [`const __m = globalThis.__volterModules[${JSON.stringify(spec)}];`];
383
383
  for (const key of Object.keys(ns)) {
384
384
  if (key === 'default' || !IDENT_RE.test(key) || RESERVED_WORDS.has(key)) continue;
385
385
  lines.push(`export const ${key} = __m[${JSON.stringify(key)}];`);
@@ -432,7 +432,7 @@ export async function transpileToModuleUrl(
432
432
 
433
433
  /**
434
434
  * Prepend the game-globals lexical shadow to project source — the browser-side
435
- * analogue of the dev server's `vgai-game-globals` Vite plugin
435
+ * analogue of the dev server's `volter-game-globals` Vite plugin
436
436
  * (`server/dev.ts` + `shouldShadowGameGlobals`). The server shadows per-module
437
437
  * because Vite transforms EVERY module indiscriminately and must be told which
438
438
  * ones are game code; the browser has no such firehose — {@link importFromSource}
@@ -541,7 +541,7 @@ export function loaderFor(path: string): esbuild.Loader {
541
541
  function resolveServedModulePath(importer: string, specifier: string): string {
542
542
  if (specifier.startsWith('/')) return specifier;
543
543
  if (/^(?:https?:|blob:|data:)/.test(specifier)) return specifier;
544
- const origin = globalThis.location?.origin ?? 'https://vgai.invalid';
544
+ const origin = globalThis.location?.origin ?? 'https://volter.invalid';
545
545
  const resolved = new URL(specifier, new URL(importer, origin));
546
546
  return resolved.origin === origin
547
547
  ? `${resolved.pathname}${resolved.search}${resolved.hash}`
@@ -572,7 +572,7 @@ export async function bundleServedModule(
572
572
  define: PROJECT_MODULE_DEFINES,
573
573
  plugins: [
574
574
  {
575
- name: 'vgai-served-module',
575
+ name: 'volter-served-module',
576
576
  setup(build) {
577
577
  build.onResolve({ filter: /.*/ }, (args) => {
578
578
  if (
@@ -586,9 +586,9 @@ export async function bundleServedModule(
586
586
  args.kind === 'entry-point'
587
587
  ? args.path
588
588
  : resolveServedModulePath(args.importer, args.path);
589
- return { path, namespace: 'vgai-served' };
589
+ return { path, namespace: 'volter-served' };
590
590
  });
591
- build.onLoad({ filter: /.*/, namespace: 'vgai-served' }, async (args) => {
591
+ build.onLoad({ filter: /.*/, namespace: 'volter-served' }, async (args) => {
592
592
  const response = await fetchImpl(args.path);
593
593
  if (!response.ok) {
594
594
  throw new Error(
@@ -17,7 +17,7 @@
17
17
  * five callers never had a story in hand (`authoring/canvas-design-mount.ts`,
18
18
  * `authoring/pixi-still-presentation.ts`, `components/world-root-stage.ts`,
19
19
  * `live-canvas-frame.ts`, and the ingest lane's canvas mount); moving it out is
20
- * the part of the CSF estate's exit to `@vgai/game` (WORK.md §The open-source
20
+ * the part of the CSF estate's exit to `@volter/game` (WORK.md §The open-source
21
21
  * launch, phase 1 unit 3) that does not wait on anything else.
22
22
  */
23
23
 
@@ -56,13 +56,13 @@ function CameraPreview({ previewRef }: CameraAuthoringOverlayProps) {
56
56
  : 16 / 9;
57
57
 
58
58
  return (
59
- <div className="vgai-camera-preview" data-testid="camera-preview">
60
- <Inline className="vgai-camera-preview-header" gap={4} align="center">
59
+ <div className="volter-camera-preview" data-testid="camera-preview">
60
+ <Inline className="volter-camera-preview-header" gap={4} align="center">
61
61
  <EditorIcon icon={faCamera} size="sm" />
62
62
  <Text
63
63
  variant="label"
64
64
  truncate
65
- className="vgai-camera-preview-title"
65
+ className="volter-camera-preview-title"
66
66
  title={`${preview.name} · ${preview.lens}`}
67
67
  >
68
68
  {preview.name}
@@ -77,7 +77,7 @@ function CameraPreview({ previewRef }: CameraAuthoringOverlayProps) {
77
77
  <EditorIcon icon={faThumbtack} size="sm" />
78
78
  </IconButton>
79
79
  </Tooltip>
80
- <div ref={menuRef} className="vgai-camera-preview-menu-anchor">
80
+ <div ref={menuRef} className="volter-camera-preview-menu-anchor">
81
81
  <Tooltip text="Camera actions">
82
82
  <IconButton
83
83
  aria-label="Camera actions"
@@ -89,7 +89,7 @@ function CameraPreview({ previewRef }: CameraAuthoringOverlayProps) {
89
89
  </IconButton>
90
90
  </Tooltip>
91
91
  {menuOpen ? (
92
- <EditorPopover className="vgai-camera-preview-menu">
92
+ <EditorPopover className="volter-camera-preview-menu">
93
93
  <Button
94
94
  variant="ghost"
95
95
  size="compact"
@@ -130,7 +130,7 @@ function CameraPreview({ previewRef }: CameraAuthoringOverlayProps) {
130
130
  </Inline>
131
131
  <div
132
132
  ref={previewRef}
133
- className="vgai-camera-preview-image"
133
+ className="volter-camera-preview-image"
134
134
  style={{ aspectRatio: String(Math.max(0.5, Math.min(3, aspect))) }}
135
135
  role="img"
136
136
  aria-label={`Live view through ${preview.name}`}
@@ -142,13 +142,13 @@ function CameraPreview({ previewRef }: CameraAuthoringOverlayProps) {
142
142
 
143
143
  function CompositionGuides() {
144
144
  return (
145
- <div className="vgai-camera-guides" aria-hidden="true">
146
- <div className="vgai-camera-guide vgai-camera-guide-v1" />
147
- <div className="vgai-camera-guide vgai-camera-guide-v2" />
148
- <div className="vgai-camera-guide vgai-camera-guide-h1" />
149
- <div className="vgai-camera-guide vgai-camera-guide-h2" />
150
- <div className="vgai-camera-safe-frame" />
151
- <div className="vgai-camera-center-mark" />
145
+ <div className="volter-camera-guides" aria-hidden="true">
146
+ <div className="volter-camera-guide volter-camera-guide-v1" />
147
+ <div className="volter-camera-guide volter-camera-guide-v2" />
148
+ <div className="volter-camera-guide volter-camera-guide-h1" />
149
+ <div className="volter-camera-guide volter-camera-guide-h2" />
150
+ <div className="volter-camera-safe-frame" />
151
+ <div className="volter-camera-center-mark" />
152
152
  </div>
153
153
  );
154
154
  }
@@ -163,7 +163,7 @@ function CameraViewBanner() {
163
163
  <EditorSurface
164
164
  variant="overlay"
165
165
  border
166
- className="vgai-camera-view-banner vgai-chrome-island vgai-glass-island"
166
+ className="volter-camera-view-banner volter-chrome-island volter-glass-island"
167
167
  data-testid="camera-view-banner"
168
168
  >
169
169
  <Inline gap={6} align="center">