@volter/editor-game 0.5.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- package/src/xstate/xstate-layout.ts +76 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OID half of the canvas authoring surface's IDENTITY axis
|
|
3
|
+
* (`@volter/game-runtime/pixi/authoring`'s {@link CanvasIdentity}) — what a row is called
|
|
4
|
+
* when the display tree was authored as first-party `@pixi/react` TSX, and the
|
|
5
|
+
* resolution from such an id back to the source tag it addresses.
|
|
6
|
+
*
|
|
7
|
+
* Pure: no Pixi mount, no network, so both halves are unit-testable directly
|
|
8
|
+
* (`packages/editor/test/pixi-source-identity.test.ts`). The structural half of
|
|
9
|
+
* the same axis is `STRUCTURAL_CANVAS_IDENTITY`, in the engine beside the walk
|
|
10
|
+
* it parameterizes.
|
|
11
|
+
*
|
|
12
|
+
* ## Where the address comes from (MEASURED, not assumed)
|
|
13
|
+
*
|
|
14
|
+
* A canvas `.tsx` is stamped `data-oid` by the same transform every dom file
|
|
15
|
+
* gets (`ui-source/oid-transform.ts`'s `oidAttributeForSurface`: a `canvas`
|
|
16
|
+
* declared surface returns `data-oid`). `@pixi/react` hands every unrecognized
|
|
17
|
+
* prop to the Pixi class CONSTRUCTOR (`createInstance` → `new
|
|
18
|
+
* PixiComponent(pixiProps)`), and Pixi assigns its options onto the instance —
|
|
19
|
+
* so the stamp lands on the live `Container` under its own literal key,
|
|
20
|
+
* `container['data-oid']`.
|
|
21
|
+
*
|
|
22
|
+
* That is a fact about `@pixi/react`, not about this repo, and it is pinned by
|
|
23
|
+
* a real mount rather than reasoned about: `packages/editor/test/
|
|
24
|
+
* pixi-oid-stamp.test.tsx` mounts the real reconciler and asserts it. (The
|
|
25
|
+
* plausible-looking alternative — that the dash is PIERCED to `container.oid`
|
|
26
|
+
* the way fiber pierces `userData-oid` — is what this module was first written
|
|
27
|
+
* against, and the mount test is what caught it: every row silently fell back
|
|
28
|
+
* to a structural path.) {@link readContainerOid} is the ONE place that
|
|
29
|
+
* knows.
|
|
30
|
+
*
|
|
31
|
+
* Ids follow the three lane's `r3f:<worldId>:<oid>` precedent
|
|
32
|
+
* (`r3f-source-authoring-adapter.ts`) with a `pixi:` prefix, and they exist for
|
|
33
|
+
* that lane's reason: an oid signature is stable across a remount, so
|
|
34
|
+
* selection survives one.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { CanvasIdentity } from '@volter/game-runtime/pixi/authoring';
|
|
38
|
+
import type { Container } from 'pixi.js';
|
|
39
|
+
import { occurrenceId } from '@volter/editor-core/authoring/component-instance-root';
|
|
40
|
+
|
|
41
|
+
const PREFIX = 'pixi';
|
|
42
|
+
|
|
43
|
+
/** The live field the `data-oid` JSX prop becomes — see this module's header.
|
|
44
|
+
* Spelled once, here. */
|
|
45
|
+
const OID_FIELD = 'data-oid';
|
|
46
|
+
const INSTANCE_FIELD = 'data-authoring-instance';
|
|
47
|
+
const INSTANCE_LABEL_FIELD = 'data-authoring-label';
|
|
48
|
+
const INSTANCE_COMPONENT_FIELD = 'data-authoring-component';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The oid `@pixi/react` landed on a live display object, or `undefined`.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately structural (`typeof === 'string'`): a game's own container
|
|
54
|
+
* subclass may declare an unrelated `oid` field, and a non-string one is not
|
|
55
|
+
* an address.
|
|
56
|
+
*/
|
|
57
|
+
export function readContainerOid(object: unknown): string | undefined {
|
|
58
|
+
if (!object || typeof object !== 'object') return undefined;
|
|
59
|
+
const value = (object as Record<string, unknown>)[OID_FIELD];
|
|
60
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stringField(object: unknown, field: string): string | undefined {
|
|
64
|
+
if (!object || typeof object !== 'object') return undefined;
|
|
65
|
+
const value = (object as Record<string, unknown>)[field];
|
|
66
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function readContainerAuthoringInstance(object: unknown): string | undefined {
|
|
70
|
+
return stringField(object, INSTANCE_FIELD);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function readContainerAuthoringLabel(object: unknown): string | undefined {
|
|
74
|
+
return stringField(object, INSTANCE_LABEL_FIELD);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readContainerAuthoringComponent(object: unknown): string | undefined {
|
|
78
|
+
return stringField(object, INSTANCE_COMPONENT_FIELD);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The first native Pixi host emitted by a component instance. */
|
|
82
|
+
export function isCanvasComponentInstanceRoot(object: Container): boolean {
|
|
83
|
+
const instance = readContainerAuthoringInstance(object);
|
|
84
|
+
if (!instance) return false;
|
|
85
|
+
return readContainerAuthoringInstance(object.parent) !== instance;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Callsite OID for a component root, otherwise the element's own OID. */
|
|
89
|
+
export function authoringOidForContainer(object: Container): string | undefined {
|
|
90
|
+
return isCanvasComponentInstanceRoot(object)
|
|
91
|
+
? readContainerAuthoringInstance(object)
|
|
92
|
+
: readContainerOid(object);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `pixi:<worldId>:<oid>`, with `#n` for every occurrence past the first. */
|
|
96
|
+
export function pixiOidId(worldId: string, oid: string, occurrence = 0): string {
|
|
97
|
+
return occurrenceId(`${PREFIX}:${worldId}:${oid}`, occurrence);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The address for a node the source never stamped — an object the game built
|
|
101
|
+
* imperatively, or a library's internal child. Structural, so it is stable
|
|
102
|
+
* for as long as the tree's shape is. */
|
|
103
|
+
export function pixiPathId(worldId: string, path: readonly number[]): string {
|
|
104
|
+
return `${PREFIX}:${worldId}:@${path.join('.')}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The oid an id addresses, or `null` for a path id / an id minted for a
|
|
108
|
+
* DIFFERENT world. Never guesses. */
|
|
109
|
+
export function oidOfPixiId(worldId: string, id: string): string | null {
|
|
110
|
+
const head = `${PREFIX}:${worldId}:`;
|
|
111
|
+
if (!id.startsWith(head)) return null;
|
|
112
|
+
const rest = id.slice(head.length);
|
|
113
|
+
if (rest.startsWith('@')) return null;
|
|
114
|
+
const hash = rest.indexOf('#');
|
|
115
|
+
const oid = hash < 0 ? rest : rest.slice(0, hash);
|
|
116
|
+
return oid.length > 0 ? oid : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The identity a FIRST-PARTY canvas root supplies to the shared walk.
|
|
121
|
+
*
|
|
122
|
+
* A repeated callsite (`sim.grunts.map(…)` renders one JSX element ninety
|
|
123
|
+
* times) resolves ninety live objects to one oid; they are disambiguated with
|
|
124
|
+
* the same `#n` occurrence suffix the three lane uses, counted per walk — which
|
|
125
|
+
* is what {@link CanvasIdentity.beginWalk} exists for.
|
|
126
|
+
*/
|
|
127
|
+
export function createOidCanvasIdentity(worldId: string): CanvasIdentity {
|
|
128
|
+
let occurrences = new Map<string, number>();
|
|
129
|
+
return {
|
|
130
|
+
beginWalk: () => {
|
|
131
|
+
occurrences = new Map();
|
|
132
|
+
},
|
|
133
|
+
idFor: (object: Container, path: readonly number[]): string => {
|
|
134
|
+
const oid = authoringOidForContainer(object);
|
|
135
|
+
if (oid === undefined) return pixiPathId(worldId, path);
|
|
136
|
+
const n = occurrences.get(oid) ?? 0;
|
|
137
|
+
occurrences.set(oid, n + 1);
|
|
138
|
+
return pixiOidId(worldId, oid, n);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STILL-STAGE PRESENTATION for design-time Pixi mounts — the two pieces the
|
|
3
|
+
* canvas design layer (`canvas-design-mount.ts`) and the isolated screen mount
|
|
4
|
+
* (`mount-isolated-pixi-screen.ts`) each spelled for themselves (owner-directed
|
|
5
|
+
* overlap audit, 2026-08-22): framing authored bounds into the Scene camera,
|
|
6
|
+
* and preparing a camera-presented frame for capture on a hidden tab.
|
|
7
|
+
*
|
|
8
|
+
* Both mounts share one presentation model: the stage is STILL (no ticker —
|
|
9
|
+
* Edit's paused clock), the editor camera owns pan/zoom, and a capture must
|
|
10
|
+
* see the exact camera-presented frame rather than a raw-stage extraction.
|
|
11
|
+
* What stays per-mount is the render itself (whose renderer, whose stage,
|
|
12
|
+
* whose camera pose) — handed in as `renderScene`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { registerPresentedCanvasFrame } from '@volter/editor-core/canvas-preview-frames';
|
|
16
|
+
import type { RootViewController } from '@volter/editor-core/authoring/world-pan-state';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Frame authored world bounds in the independent Scene camera. Unlike an
|
|
20
|
+
* asset/UI preview, a scene editor may magnify small subjects: zoom is an
|
|
21
|
+
* inspection tool, not a claim about the runtime's pixel scale. `false` when
|
|
22
|
+
* the pane or the bounds are too small to frame yet (a sub-2px pane is a
|
|
23
|
+
* dock mid-layout, not a viewport).
|
|
24
|
+
*/
|
|
25
|
+
export function fitSceneView(
|
|
26
|
+
layer: HTMLElement,
|
|
27
|
+
bounds: { x: number; y: number; width: number; height: number },
|
|
28
|
+
view: RootViewController,
|
|
29
|
+
/** A floor on the zoom the fit may choose — the OPENING fit passes the zoom
|
|
30
|
+
* that frames the game's declared resolution, so a world whose floor dwarfs
|
|
31
|
+
* its actors still opens at the scale the player sees (see the mount). */
|
|
32
|
+
minZoom = 0.1,
|
|
33
|
+
): boolean {
|
|
34
|
+
const rect = layer.getBoundingClientRect();
|
|
35
|
+
if (rect.width < 2 || rect.height < 2) return false;
|
|
36
|
+
if (!(bounds.width > 0) || !(bounds.height > 0)) return false;
|
|
37
|
+
const zoom = Math.min(
|
|
38
|
+
4,
|
|
39
|
+
Math.max(
|
|
40
|
+
minZoom,
|
|
41
|
+
Math.min((rect.width - 96) / bounds.width, (rect.height - 96) / bounds.height),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
view.setView(
|
|
45
|
+
rect.width / 2 - (bounds.x + bounds.width / 2) * zoom,
|
|
46
|
+
rect.height / 2 - (bounds.y + bounds.height / 2) * zoom,
|
|
47
|
+
zoom,
|
|
48
|
+
);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A hidden editor tab has no rAF, but capture is still a first-class reader of
|
|
54
|
+
* the authoring surface. Register a presenter that prepares the exact
|
|
55
|
+
* camera-presented frame at that boundary instead of falling back to raw-stage
|
|
56
|
+
* extraction: `renderScene` is retried across event-loop turns because the
|
|
57
|
+
* first frames of a mount can race layout (`false` = not renderable yet).
|
|
58
|
+
* Returns the unregister function.
|
|
59
|
+
*/
|
|
60
|
+
export function registerStillFramePresenter(
|
|
61
|
+
canvas: HTMLCanvasElement,
|
|
62
|
+
renderScene: () => boolean,
|
|
63
|
+
): () => void {
|
|
64
|
+
return registerPresentedCanvasFrame(canvas, async () => {
|
|
65
|
+
for (let turn = 0; turn < 24; turn++) {
|
|
66
|
+
if (renderScene()) return null;
|
|
67
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE undo entry per STRUCTURAL op on a live canvas tree — the bracket
|
|
3
|
+
* `PixiAuthoringAdapter.structure` wraps every create/remove/duplicate/
|
|
4
|
+
* reparent/reorder in.
|
|
5
|
+
*
|
|
6
|
+
* It is the SAME history path a transform edit already takes (a
|
|
7
|
+
* {@link JsonHistoryResource} registered against `store.projectHistory`), not a
|
|
8
|
+
* second one: what differs is the STATE SHAPE, and that is why it is a resource
|
|
9
|
+
* of its own rather than a field bolted onto the write target's. The write
|
|
10
|
+
* target journals a node's VALUES (label, alpha, tint, transform) keyed by the
|
|
11
|
+
* adapter's structural-path id; a structural op changes what those ids even
|
|
12
|
+
* ARE, so a value snapshot cannot describe it. This resource journals
|
|
13
|
+
* PLACEMENTS instead — "which parent, at which index, in which local pose" —
|
|
14
|
+
* keyed by a uid stamped on the display object itself, which survives the
|
|
15
|
+
* re-walk that renames every id.
|
|
16
|
+
*
|
|
17
|
+
* WHY A UID AND A REGISTRY. A removed object must be resurrectable, so it is
|
|
18
|
+
* kept alive here (detached, never `destroy()`ed) and found again by uid; a
|
|
19
|
+
* JSON snapshot cannot hold a `Container` reference. The registry is the ONLY
|
|
20
|
+
* thing that makes undo-of-remove possible at all.
|
|
21
|
+
*
|
|
22
|
+
* IT IS DELIBERATELY EDIT-SCOPED. Only objects an editor op touched (plus the
|
|
23
|
+
* ancestors those ops named) are tracked, so a restore never reorders the
|
|
24
|
+
* thousands of sprites a RUNNING game adds and removes on its own every frame.
|
|
25
|
+
* For the same reason `conflictIdentity` narrows to the parent map: a game that
|
|
26
|
+
* appended one sprite of its own since the transaction was recorded has shifted
|
|
27
|
+
* every index without touching anything the editor authored, and hashing those
|
|
28
|
+
* indexes would make every undo fail preflight as a content-conflict — the
|
|
29
|
+
* failure the three lane paid for as #81.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { Container } from 'pixi.js';
|
|
33
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
34
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
35
|
+
import { type JournalSubject, JsonHistoryResource } from '../history/json-history-resource';
|
|
36
|
+
|
|
37
|
+
/** The stage root's stand-in uid: it is not one of the adapter's nodes, but it
|
|
38
|
+
* is a legal parent for every op. */
|
|
39
|
+
const ROOT_UID = '#root';
|
|
40
|
+
|
|
41
|
+
/** The uid stamp, on the display object itself — the same technique
|
|
42
|
+
* `AuthoringAdapter2D` uses for `__authId`, for the same reason (there is no
|
|
43
|
+
* side table that survives the object outliving one walk). */
|
|
44
|
+
interface StructureStamped {
|
|
45
|
+
__vgaiStructUid?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A node's own local pose, as plain numbers. Recorded because `reparent`
|
|
49
|
+
* rewrites it to preserve the world transform — undoing the move without
|
|
50
|
+
* undoing that rewrite would leave the node in the right parent at the wrong
|
|
51
|
+
* place. */
|
|
52
|
+
interface CanvasNodePose {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
rotation: number;
|
|
56
|
+
scaleX: number;
|
|
57
|
+
scaleY: number;
|
|
58
|
+
pivotX: number;
|
|
59
|
+
pivotY: number;
|
|
60
|
+
skewX: number;
|
|
61
|
+
skewY: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface CanvasPlacement {
|
|
65
|
+
/** The parent's uid, {@link ROOT_UID} for the stage root, or `null` when the
|
|
66
|
+
* node is detached (removed, or not yet added). */
|
|
67
|
+
parent: string | null;
|
|
68
|
+
/** Index among that parent's children; `-1` while detached. */
|
|
69
|
+
index: number;
|
|
70
|
+
pose?: CanvasNodePose;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface CanvasStructureState {
|
|
74
|
+
placements: Record<string, CanvasPlacement>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function poseOf(object: Container): CanvasNodePose | undefined {
|
|
78
|
+
// A tracked node is not guaranteed to be a real PixiJS display object (the
|
|
79
|
+
// same tolerance `getTransform` and the structure watcher already carry).
|
|
80
|
+
if (!object.position || !object.scale || !object.pivot || !object.skew) return undefined;
|
|
81
|
+
return {
|
|
82
|
+
x: object.position.x,
|
|
83
|
+
y: object.position.y,
|
|
84
|
+
rotation: object.rotation,
|
|
85
|
+
scaleX: object.scale.x,
|
|
86
|
+
scaleY: object.scale.y,
|
|
87
|
+
pivotX: object.pivot.x,
|
|
88
|
+
pivotY: object.pivot.y,
|
|
89
|
+
skewX: object.skew.x,
|
|
90
|
+
skewY: object.skew.y,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function applyPose(object: Container, pose: CanvasNodePose): void {
|
|
95
|
+
if (!object.position || !object.scale || !object.pivot || !object.skew) return;
|
|
96
|
+
object.position.set(pose.x, pose.y);
|
|
97
|
+
object.scale.set(pose.scaleX, pose.scaleY);
|
|
98
|
+
object.pivot.set(pose.pivotX, pose.pivotY);
|
|
99
|
+
object.skew.set(pose.skewX, pose.skewY);
|
|
100
|
+
object.rotation = pose.rotation;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class CanvasStructureHistory {
|
|
104
|
+
private readonly byUid = new Map<string, Container>();
|
|
105
|
+
private readonly resource: JsonHistoryResource<CanvasStructureState> | null;
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
private readonly root: Container,
|
|
109
|
+
store: EditorShellStore,
|
|
110
|
+
/** Whose journal this structure stack belongs to — the world's for a held
|
|
111
|
+
* surface, the run's for play (`history/json-history-resource.ts`). */
|
|
112
|
+
journal: JournalSubject,
|
|
113
|
+
/** Re-index + notify after a restore put objects back. */
|
|
114
|
+
private readonly onRestored: () => void,
|
|
115
|
+
) {
|
|
116
|
+
const history = store.projectHistory;
|
|
117
|
+
this.resource = history
|
|
118
|
+
? new JsonHistoryResource<CanvasStructureState>({
|
|
119
|
+
history,
|
|
120
|
+
kind: 'session-state',
|
|
121
|
+
scope: 'session',
|
|
122
|
+
subject: { ...journal, id: `${journal.id}/world-2d-structure` },
|
|
123
|
+
displayName: 'World 2D structure',
|
|
124
|
+
capture: () => this.capture(),
|
|
125
|
+
conflictIdentity: (state) =>
|
|
126
|
+
Object.fromEntries(
|
|
127
|
+
Object.entries(state.placements).map(([uid, placement]) => [uid, placement.parent]),
|
|
128
|
+
),
|
|
129
|
+
restore: async (state) => {
|
|
130
|
+
this.restore(state);
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
: null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Register a display object so its placement is journaled and so a detached
|
|
137
|
+
* one can be found again. Idempotent; returns the uid. */
|
|
138
|
+
track(object: Container): string {
|
|
139
|
+
return this.uidOf(object);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run ONE structural mutation as ONE undo entry.
|
|
144
|
+
*
|
|
145
|
+
* Every participant must be {@link track}ed BEFORE the call — including a
|
|
146
|
+
* node that does not exist in the tree yet, whose "before" placement is
|
|
147
|
+
* therefore `detached`, which is exactly what makes undo-of-create remove it.
|
|
148
|
+
*/
|
|
149
|
+
run<T>(label: string, mutate: () => T): T {
|
|
150
|
+
this.resource?.assertCanMutate();
|
|
151
|
+
const before = this.capture();
|
|
152
|
+
const result = mutate();
|
|
153
|
+
if (this.resource) {
|
|
154
|
+
// JOURNALED LOUDLY. A rejected `record(...)` means the op IS applied to
|
|
155
|
+
// the live tree and has NO history entry: the user sees the node they
|
|
156
|
+
// created/moved/deleted and Ctrl+Z does nothing, with nothing said. Same
|
|
157
|
+
// statement (and same wording) as the three lane's `recordHistory`
|
|
158
|
+
// (`three-authoring-adapter.ts`).
|
|
159
|
+
//
|
|
160
|
+
// WHY THE REPORT IS HERE AND NOT IN THE VERB'S ACK. Every structural verb
|
|
161
|
+
// funnels through this method, and two of them — `structure.create` and
|
|
162
|
+
// `structure.duplicate` — return a synchronous node id by contract, so no
|
|
163
|
+
// caller can await this write. One report at the funnel is the only place
|
|
164
|
+
// that covers all of them identically; the pipe's ack answers a different
|
|
165
|
+
// question (where the BYTES went — nowhere, for this lane) and cannot
|
|
166
|
+
// carry a journaling failure without claiming it was a persistence one.
|
|
167
|
+
void this.resource.record(label, before, this.capture()).catch((error: unknown) => {
|
|
168
|
+
editorConsole.error(
|
|
169
|
+
`[canvas] failed to journal "${label}" into project history — the structural edit is ` +
|
|
170
|
+
`applied but has no history entry (it cannot be undone): ${
|
|
171
|
+
error instanceof Error ? error.message : String(error)
|
|
172
|
+
}`,
|
|
173
|
+
'authoring',
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
dispose(): void {
|
|
181
|
+
this.resource?.dispose();
|
|
182
|
+
this.byUid.clear();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private uidOf(object: Container): string {
|
|
186
|
+
if (object === this.root) return ROOT_UID;
|
|
187
|
+
const stamped = object as Container & StructureStamped;
|
|
188
|
+
const existing = stamped.__vgaiStructUid;
|
|
189
|
+
if (existing && this.byUid.get(existing) === object) return existing;
|
|
190
|
+
const uid = existing ?? `s${crypto.randomUUID()}`;
|
|
191
|
+
stamped.__vgaiStructUid = uid;
|
|
192
|
+
this.byUid.set(uid, object);
|
|
193
|
+
return uid;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private capture(): CanvasStructureState {
|
|
197
|
+
const placements: Record<string, CanvasPlacement> = {};
|
|
198
|
+
// `uidOf` may register an ancestor mid-iteration; a Map iterator visits
|
|
199
|
+
// entries added during the walk, so those ancestors get placements of their
|
|
200
|
+
// own — bounded by the depth of the tree, and what makes a restore able to
|
|
201
|
+
// name the parent it is putting a node back under.
|
|
202
|
+
for (const [uid, object] of this.byUid) {
|
|
203
|
+
const parent = (object.parent as Container | null) ?? null;
|
|
204
|
+
const pose = poseOf(object);
|
|
205
|
+
placements[uid] = {
|
|
206
|
+
parent: parent ? this.uidOf(parent) : null,
|
|
207
|
+
index: parent ? (parent.children?.indexOf(object) ?? -1) : -1,
|
|
208
|
+
...(pose ? { pose } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return { placements };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private restore(state: CanvasStructureState): void {
|
|
215
|
+
const entries = Object.entries(state.placements);
|
|
216
|
+
// Detach first, so the indexes the attach pass reads are the ones the
|
|
217
|
+
// snapshot was taken against.
|
|
218
|
+
for (const [uid, placement] of entries) {
|
|
219
|
+
if (placement.parent !== null) continue;
|
|
220
|
+
const object = this.byUid.get(uid);
|
|
221
|
+
object?.parent?.removeChild(object);
|
|
222
|
+
}
|
|
223
|
+
for (const [uid, placement] of entries
|
|
224
|
+
.filter(([, placement]) => placement.parent !== null)
|
|
225
|
+
.sort((a, b) => a[1].index - b[1].index)) {
|
|
226
|
+
const object = this.byUid.get(uid);
|
|
227
|
+
if (!object) continue;
|
|
228
|
+
const parent =
|
|
229
|
+
placement.parent === ROOT_UID ? this.root : this.byUid.get(placement.parent as string);
|
|
230
|
+
if (!parent || typeof parent.addChildAt !== 'function') continue;
|
|
231
|
+
const capacity = parent.children.length - (object.parent === parent ? 1 : 0);
|
|
232
|
+
parent.addChildAt(object, Math.max(0, Math.min(placement.index, capacity)));
|
|
233
|
+
if (placement.pose) applyPose(object, placement.pose);
|
|
234
|
+
}
|
|
235
|
+
this.onRestored();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CANVAS surface's transform vocabulary, as pure functions.
|
|
3
|
+
*
|
|
4
|
+
* Pixi's transform is `x`, `y`, `rotation` (a scalar, radians, about the
|
|
5
|
+
* screen normal) and `scale` — there is no z and no quaternion. The editor's
|
|
6
|
+
* neutral {@link Transform} is 3D by construction, so the canvas adapter has to
|
|
7
|
+
* translate in both directions. It used to carry that `angleToQuat`/
|
|
8
|
+
* `quatToAngle` pair inline (extraction cited: `pixi-live-write-target.ts`'s
|
|
9
|
+
* own header records where the code it was lifted out of lived). This module
|
|
10
|
+
* is that pair plus the SOURCE half the write path needs, in one place, so the
|
|
11
|
+
* live and source targets can never disagree about what "rotation" means.
|
|
12
|
+
*
|
|
13
|
+
* ## Why the source half is pure, and lives here
|
|
14
|
+
*
|
|
15
|
+
* Deciding which `@pixi/react` PROP a transform channel writes is the whole
|
|
16
|
+
* literal-vs-dynamic contract of the canvas lane, and it is decidable from two
|
|
17
|
+
* inputs alone: the channel, and the JSX attributes already on the tag. No
|
|
18
|
+
* live tree, no network, no adapter. Keeping it here is what makes it testable
|
|
19
|
+
* without mounting Pixi (`packages/editor/test/pixi-transform-channels.test.ts`).
|
|
20
|
+
*
|
|
21
|
+
* ## The scale rule, stated once
|
|
22
|
+
*
|
|
23
|
+
* `<pixiContainer scale={0.5}>` is the shipped idiom (see any prefab under an
|
|
24
|
+
* example project's `prefabs/`), and Pixi's own setter takes a number or a
|
|
25
|
+
* `PointData` — never an array. So a UNIFORM scale writes the scalar back, and
|
|
26
|
+
* a NON-UNIFORM one is only expressible through `@pixi/react`'s dashed-prop
|
|
27
|
+
* piercing (`scale-x`/`scale-y`, which land on `container.scale.x`/`.y`). This
|
|
28
|
+
* module writes the dashed pair when the tag already authors it, and otherwise
|
|
29
|
+
* REFUSES a non-uniform scale in a sentence naming the pair — never a silently
|
|
30
|
+
* lossy uniform write, and never an array literal Pixi would reject at
|
|
31
|
+
* runtime.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { Transform2DValue } from '@volter/game-runtime/pixi/authoring';
|
|
35
|
+
import type { Transform } from '@volter/editor-project/adapter';
|
|
36
|
+
|
|
37
|
+
/** Quaternion (xyzw) about Z ⇄ scalar radians — the one mapping both canvas
|
|
38
|
+
* adapters use. */
|
|
39
|
+
export function angleToQuat(a: number): [number, number, number, number] {
|
|
40
|
+
return [0, 0, Math.sin(a / 2), Math.cos(a / 2)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function quatToAngle(q: readonly [number, number, number, number]): number {
|
|
44
|
+
return 2 * Math.atan2(q[2], q[3]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Pixi's own 2D transform → the editor's neutral 3D one (z pinned). */
|
|
48
|
+
export function toNeutralTransform(t: Transform2DValue): Transform {
|
|
49
|
+
return {
|
|
50
|
+
position: [t.position[0], t.position[1], 0],
|
|
51
|
+
rotation: angleToQuat(t.rotation),
|
|
52
|
+
scale: [t.scale[0], t.scale[1], 1],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** …and back. The z channel is DROPPED rather than folded in anywhere: a
|
|
57
|
+
* canvas world has no depth axis, so carrying one would fabricate a value. */
|
|
58
|
+
export function fromNeutralTransform(t: Transform): Transform2DValue {
|
|
59
|
+
return {
|
|
60
|
+
position: [t.position[0], t.position[1]],
|
|
61
|
+
rotation: quatToAngle(t.rotation),
|
|
62
|
+
scale: [t.scale[0], t.scale[1]],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The subset of `JsxAttrInfo` this planner reads — declared structurally so a
|
|
67
|
+
* test can hand it plain objects. */
|
|
68
|
+
export interface PixiAttr {
|
|
69
|
+
readonly name: string;
|
|
70
|
+
readonly rawValue: string;
|
|
71
|
+
readonly isExpression: boolean;
|
|
72
|
+
readonly isLiteral: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One `prop={value}` write the plan resolves to. `value` is SOURCE TEXT. */
|
|
76
|
+
export interface PixiPropWrite {
|
|
77
|
+
readonly prop: string;
|
|
78
|
+
readonly value: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type PixiChannelPlan =
|
|
82
|
+
| { readonly writable: true; readonly writes: readonly PixiPropWrite[] }
|
|
83
|
+
| { readonly writable: false; readonly reason: string };
|
|
84
|
+
|
|
85
|
+
const NUMBER_RE = /^-?\d+(\.\d+)?$/;
|
|
86
|
+
const EPS = 1e-6;
|
|
87
|
+
|
|
88
|
+
/** ≤4 decimals, no trailing zeros, no negative zero — the same source-number
|
|
89
|
+
* formatting the R3F writer uses. */
|
|
90
|
+
export function formatSourceNumber(n: number): string {
|
|
91
|
+
const rounded = Math.round(n * 1e4) / 1e4;
|
|
92
|
+
return Object.is(rounded, -0) ? '0' : String(rounded);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function attr(attrs: readonly PixiAttr[], name: string): PixiAttr | undefined {
|
|
96
|
+
return attrs.find((candidate) => candidate.name === name);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A prop is writable when it is ABSENT (the writer appends it) or authored as
|
|
101
|
+
* a literal. An expression-bound prop (`x={grunt.x}`) is the sacred refusal:
|
|
102
|
+
* the game computes it every frame, so writing a number there would be a
|
|
103
|
+
* two-truths divergence the next tick erases.
|
|
104
|
+
*/
|
|
105
|
+
function propRefusal(attrs: readonly PixiAttr[], prop: string): string | null {
|
|
106
|
+
const found = attr(attrs, prop);
|
|
107
|
+
if (!found) return null;
|
|
108
|
+
if (found.isExpression && !found.isLiteral) {
|
|
109
|
+
return `${prop} is controlled by the JSX expression {${found.rawValue}}.`;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Which props one transform channel writes, given what the tag already says.
|
|
116
|
+
*
|
|
117
|
+
* `next` is the DESIRED transform in Pixi's own vocabulary. Returns the exact
|
|
118
|
+
* `prop={value}` writes to make, or one sentence saying why the channel is not
|
|
119
|
+
* writable here.
|
|
120
|
+
*/
|
|
121
|
+
export function planChannelWrite(
|
|
122
|
+
channel: 'position' | 'rotation' | 'scale',
|
|
123
|
+
next: Transform2DValue,
|
|
124
|
+
attrs: readonly PixiAttr[],
|
|
125
|
+
): PixiChannelPlan {
|
|
126
|
+
if (channel === 'position') {
|
|
127
|
+
for (const prop of ['x', 'y'] as const) {
|
|
128
|
+
const refusal = propRefusal(attrs, prop);
|
|
129
|
+
if (refusal) return { writable: false, reason: refusal };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
writable: true,
|
|
133
|
+
writes: [
|
|
134
|
+
{ prop: 'x', value: formatSourceNumber(next.position[0]) },
|
|
135
|
+
{ prop: 'y', value: formatSourceNumber(next.position[1]) },
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (channel === 'rotation') {
|
|
141
|
+
const refusal = propRefusal(attrs, 'rotation');
|
|
142
|
+
if (refusal) return { writable: false, reason: refusal };
|
|
143
|
+
return {
|
|
144
|
+
writable: true,
|
|
145
|
+
writes: [{ prop: 'rotation', value: formatSourceNumber(next.rotation) }],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- scale (see this module's header for the rule) ---
|
|
150
|
+
const dashed = attr(attrs, 'scale-x') ?? attr(attrs, 'scale-y');
|
|
151
|
+
if (dashed) {
|
|
152
|
+
for (const prop of ['scale-x', 'scale-y'] as const) {
|
|
153
|
+
const refusal = propRefusal(attrs, prop);
|
|
154
|
+
if (refusal) return { writable: false, reason: refusal };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
writable: true,
|
|
158
|
+
writes: [
|
|
159
|
+
{ prop: 'scale-x', value: formatSourceNumber(next.scale[0]) },
|
|
160
|
+
{ prop: 'scale-y', value: formatSourceNumber(next.scale[1]) },
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const refusal = propRefusal(attrs, 'scale');
|
|
166
|
+
if (refusal) return { writable: false, reason: refusal };
|
|
167
|
+
const authored = attr(attrs, 'scale');
|
|
168
|
+
if (authored && !NUMBER_RE.test(authored.rawValue.trim())) {
|
|
169
|
+
return {
|
|
170
|
+
writable: false,
|
|
171
|
+
reason:
|
|
172
|
+
`scale is authored as {${authored.rawValue}} — this editor writes the scalar ` +
|
|
173
|
+
'shorthand or the `scale-x`/`scale-y` pair, not an object literal.',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const [sx, sy] = next.scale;
|
|
177
|
+
if (Math.abs(sx - sy) >= EPS) {
|
|
178
|
+
return {
|
|
179
|
+
writable: false,
|
|
180
|
+
reason:
|
|
181
|
+
'A non-uniform scale needs the `scale-x`/`scale-y` props — this tag authors the ' +
|
|
182
|
+
'uniform `scale` shorthand, which cannot hold two values.',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { writable: true, writes: [{ prop: 'scale', value: formatSourceNumber(sx) }] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Whether two 2D transforms differ enough to be worth a source write. */
|
|
189
|
+
export function transform2DChanged(
|
|
190
|
+
channel: 'position' | 'rotation' | 'scale',
|
|
191
|
+
before: Transform2DValue,
|
|
192
|
+
after: Transform2DValue,
|
|
193
|
+
): boolean {
|
|
194
|
+
if (channel === 'position') {
|
|
195
|
+
return (
|
|
196
|
+
Math.abs(before.position[0] - after.position[0]) >= EPS ||
|
|
197
|
+
Math.abs(before.position[1] - after.position[1]) >= EPS
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (channel === 'rotation') return Math.abs(before.rotation - after.rotation) >= EPS;
|
|
201
|
+
return (
|
|
202
|
+
Math.abs(before.scale[0] - after.scale[0]) >= EPS ||
|
|
203
|
+
Math.abs(before.scale[1] - after.scale[1]) >= EPS
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot selection handoff for UI remounts that rebuild the scene between
|
|
3
|
+
* the outgoing and incoming owners. Keys are weak so closing a store cannot
|
|
4
|
+
* leave editor state retained by the handoff.
|
|
5
|
+
*/
|
|
6
|
+
export class SelectionRemountHandoff<Key extends object> {
|
|
7
|
+
private readonly pending = new WeakMap<Key, readonly string[]>();
|
|
8
|
+
|
|
9
|
+
remember(key: Key, selection: Iterable<string>): void {
|
|
10
|
+
const ids = [...selection];
|
|
11
|
+
if (ids.length === 0) this.pending.delete(key);
|
|
12
|
+
else this.pending.set(key, ids);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
take(key: Key, currentSelection: Iterable<string>): readonly string[] {
|
|
16
|
+
const current = [...currentSelection];
|
|
17
|
+
const previous = this.pending.get(key) ?? [];
|
|
18
|
+
this.pending.delete(key);
|
|
19
|
+
// A selection made while the incoming owner was mounting is newer than
|
|
20
|
+
// the outgoing owner's handoff and must win.
|
|
21
|
+
return current.length > 0 ? current : previous;
|
|
22
|
+
}
|
|
23
|
+
}
|