@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,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DESIGN-TIME isolation of a Pixi screen CLASS — Edit mounts the piece
|
|
3
|
+
* on the same Scene camera first-party canvas Edit uses
|
|
4
|
+
* (`canvas-design-mount.ts`).
|
|
5
|
+
*
|
|
6
|
+
* The construct keeps its authored stage size. The viewport is a presentation
|
|
7
|
+
* surface: the editor camera frames it, wheel zooms, and the grid shows
|
|
8
|
+
* around it. The pane is never the game's output rectangle — that is how a
|
|
9
|
+
* letterboxed 1280×720 screen made zoom a no-op.
|
|
10
|
+
*
|
|
11
|
+
* `new Ctor()`, settle at rest. No ticker, no `show()` (GameScreen.show
|
|
12
|
+
* starts the match). The live ingest host is not used.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Application, Container } from 'pixi.js';
|
|
16
|
+
import { type CanvasPixiNamespace, resolveCanvasPixiForEditor } from '../canvas-entry-runtime';
|
|
17
|
+
import { createWithOwnedPixiTickerListeners } from './owned-pixi-ticker-listeners';
|
|
18
|
+
import { initializePixiIsolationAssets } from './pixi-isolation-assets';
|
|
19
|
+
import { fitSceneView, registerStillFramePresenter } from './pixi-still-presentation';
|
|
20
|
+
import type { RootViewController } from '@volter/editor-core/authoring/world-pan-state';
|
|
21
|
+
|
|
22
|
+
export type IsolatedPixiScreenCtor = (new () => IsolatedPixiScreen) & {
|
|
23
|
+
readonly assetBundles?: readonly string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type IsolatedPixiScreen = Container & {
|
|
27
|
+
prepare?(data: unknown): void;
|
|
28
|
+
resize?(width: number, height: number): void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const EMPTY_RESULT = { score: 0, popped: 0, combo: 0, powerups: 0, highscore: 0 };
|
|
32
|
+
|
|
33
|
+
type PoseNode = {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
alpha: number;
|
|
37
|
+
readonly children?: readonly PoseNode[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Isolation shows the CONSTRUCT at rest, not the intro tween and not the
|
|
42
|
+
* running match.
|
|
43
|
+
*
|
|
44
|
+
* `prepare` is the game's pre-show hook: Result fills labels; Title parks
|
|
45
|
+
* intro groups off-screen pending `show()`. Snapshot the constructed pose,
|
|
46
|
+
* let prepare write data, then restore pose and keep alpha 1. Never call
|
|
47
|
+
* `show()` — that starts the match / starts tweens.
|
|
48
|
+
*/
|
|
49
|
+
export function settleIsolatedScreen(screen: IsolatedPixiScreen): void {
|
|
50
|
+
const poses = snapshotPoses(screen as PoseNode);
|
|
51
|
+
screen.prepare?.(EMPTY_RESULT);
|
|
52
|
+
restorePoses(screen as PoseNode, poses);
|
|
53
|
+
screen.alpha = 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function snapshotPoses(node: PoseNode): Map<PoseNode, { x: number; y: number; alpha: number }> {
|
|
57
|
+
const out = new Map<PoseNode, { x: number; y: number; alpha: number }>();
|
|
58
|
+
const walk = (current: PoseNode) => {
|
|
59
|
+
out.set(current, { x: current.x, y: current.y, alpha: current.alpha });
|
|
60
|
+
for (const child of current.children ?? []) walk(child);
|
|
61
|
+
};
|
|
62
|
+
walk(node);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function restorePoses(
|
|
67
|
+
node: PoseNode,
|
|
68
|
+
poses: Map<PoseNode, { x: number; y: number; alpha: number }>,
|
|
69
|
+
): void {
|
|
70
|
+
const pose = poses.get(node);
|
|
71
|
+
if (pose) {
|
|
72
|
+
node.x = pose.x;
|
|
73
|
+
node.y = pose.y;
|
|
74
|
+
node.alpha = pose.alpha;
|
|
75
|
+
}
|
|
76
|
+
for (const child of node.children ?? []) restorePoses(child, poses);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Authored stage size — the construct's own units, never the pane.
|
|
81
|
+
*
|
|
82
|
+
* Height is the Bubbo HUD's design content height (925). Isolation used to
|
|
83
|
+
* resize to 720, so `windowHeight - content.height` went negative, the
|
|
84
|
+
* thin-screen Title sat above the stage, and opening-fit of `getBounds()`
|
|
85
|
+
* zoomed the camera out around that off-screen wordmark.
|
|
86
|
+
*/
|
|
87
|
+
export const ISOLATED_PIXI_STAGE = { width: 1280, height: 925 } as const;
|
|
88
|
+
|
|
89
|
+
/** The Scene camera frames this rectangle, not the AABB of every child. */
|
|
90
|
+
export function isolatedStageBounds(): {
|
|
91
|
+
x: number;
|
|
92
|
+
y: number;
|
|
93
|
+
width: number;
|
|
94
|
+
height: number;
|
|
95
|
+
} {
|
|
96
|
+
return { x: 0, y: 0, width: ISOLATED_PIXI_STAGE.width, height: ISOLATED_PIXI_STAGE.height };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface MountedIsolatedPixiScreen {
|
|
100
|
+
readonly canvas: HTMLCanvasElement;
|
|
101
|
+
readonly app: Application;
|
|
102
|
+
readonly screen: IsolatedPixiScreen;
|
|
103
|
+
/** The namespace this screen was constructed and rendered in — the caller's
|
|
104
|
+
* authoring adapter and preview take the SAME one. */
|
|
105
|
+
readonly pixi: CanvasPixiNamespace;
|
|
106
|
+
dispose(): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface MountIsolatedPixiScreenOptions {
|
|
110
|
+
readonly initAssets?: () => Promise<void>;
|
|
111
|
+
/** Project-relative identity of the module behind `initAssets`. */
|
|
112
|
+
readonly assetSourcePath?: string;
|
|
113
|
+
/** Adapter-declared prerequisite for constructing this isolated piece. */
|
|
114
|
+
readonly isolationSetup?: () => void | Promise<void>;
|
|
115
|
+
/** The document's independent Scene camera. Required: isolation is a
|
|
116
|
+
* Scene-camera piece, not a letterboxed output. */
|
|
117
|
+
readonly view: RootViewController;
|
|
118
|
+
/** Already-mounted document pane the viewport canvas draws into. */
|
|
119
|
+
readonly layer: HTMLElement;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Frame authored bounds in the Scene camera — the shared design-time fit
|
|
123
|
+
* (`pixi-still-presentation.ts`), re-exported under this mount's own name for
|
|
124
|
+
* its existing callers. */
|
|
125
|
+
export const fitIsolatedSceneView = fitSceneView;
|
|
126
|
+
|
|
127
|
+
export async function mountIsolatedPixiScreen(
|
|
128
|
+
Ctor: IsolatedPixiScreenCtor,
|
|
129
|
+
options: MountIsolatedPixiScreenOptions,
|
|
130
|
+
): Promise<MountedIsolatedPixiScreen> {
|
|
131
|
+
const { view, layer } = options;
|
|
132
|
+
const stage = ISOLATED_PIXI_STAGE;
|
|
133
|
+
// THE namespace: `Ctor` is one of the GAME's own classes, loaded through the
|
|
134
|
+
// project graph, so its `pixi.js` is the project's under the packaged
|
|
135
|
+
// runtime. Everything this mount builds around it — the Application that
|
|
136
|
+
// draws it, the `Assets` bundles it needs, the `Ticker.shared` whose
|
|
137
|
+
// constructor-time registrations are withheld, the presentation Matrix —
|
|
138
|
+
// has to be that same one. See `../../vite-plugin-module-doorways.ts`.
|
|
139
|
+
const pixi = await resolveCanvasPixiForEditor();
|
|
140
|
+
const app = new pixi.Application();
|
|
141
|
+
await app.init({
|
|
142
|
+
autoStart: false,
|
|
143
|
+
sharedTicker: false,
|
|
144
|
+
backgroundAlpha: 0,
|
|
145
|
+
width: stage.width,
|
|
146
|
+
height: stage.height,
|
|
147
|
+
antialias: true,
|
|
148
|
+
preserveDrawingBuffer: true,
|
|
149
|
+
});
|
|
150
|
+
const canvas = app.canvas as HTMLCanvasElement;
|
|
151
|
+
canvas.dataset['vgaiCanvasSceneSurface'] = 'true';
|
|
152
|
+
canvas.style.position = 'absolute';
|
|
153
|
+
canvas.style.inset = '0';
|
|
154
|
+
canvas.style.width = '100%';
|
|
155
|
+
canvas.style.height = '100%';
|
|
156
|
+
canvas.style.display = 'block';
|
|
157
|
+
layer.appendChild(canvas);
|
|
158
|
+
|
|
159
|
+
if (options.initAssets) {
|
|
160
|
+
await initializePixiIsolationAssets(
|
|
161
|
+
options.assetSourcePath ?? options.initAssets.name,
|
|
162
|
+
options.initAssets,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (Ctor.assetBundles && Ctor.assetBundles.length > 0) {
|
|
166
|
+
await pixi.Assets.loadBundle([...Ctor.assetBundles]);
|
|
167
|
+
}
|
|
168
|
+
await options.isolationSetup?.();
|
|
169
|
+
const ownedScreen = createWithOwnedPixiTickerListeners(() => {
|
|
170
|
+
const isolated = new Ctor();
|
|
171
|
+
// `prepare` is part of constructing the at-rest authoring document and may
|
|
172
|
+
// schedule delayed GSAP work (Bubbo's Porthole does). Keep it inside the
|
|
173
|
+
// owned-clock window so teardown can kill that work before Play begins.
|
|
174
|
+
settleIsolatedScreen(isolated);
|
|
175
|
+
return isolated;
|
|
176
|
+
}, pixi);
|
|
177
|
+
const screen = ownedScreen.value;
|
|
178
|
+
app.stage.addChild(screen);
|
|
179
|
+
// World size once. The pane is the camera, not a second resolution.
|
|
180
|
+
screen.resize?.(stage.width, stage.height);
|
|
181
|
+
|
|
182
|
+
const resizeRenderer = (): void => {
|
|
183
|
+
const rect = layer.getBoundingClientRect();
|
|
184
|
+
if (rect.width < 2 || rect.height < 2) return;
|
|
185
|
+
const renderer = app.renderer;
|
|
186
|
+
if (renderer.screen.width !== rect.width || renderer.screen.height !== rect.height) {
|
|
187
|
+
renderer.resize(rect.width, rect.height);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const renderScene = (): boolean => {
|
|
192
|
+
const renderer = app.renderer;
|
|
193
|
+
if (!renderer) return false;
|
|
194
|
+
resizeRenderer();
|
|
195
|
+
const pose = view.get();
|
|
196
|
+
renderer.render({
|
|
197
|
+
container: app.stage,
|
|
198
|
+
transform: new pixi.Matrix(pose.zoom, 0, 0, pose.zoom, pose.x, pose.y),
|
|
199
|
+
});
|
|
200
|
+
return true;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const unregisterPresented = registerStillFramePresenter(canvas, renderScene);
|
|
204
|
+
|
|
205
|
+
let frame = 0;
|
|
206
|
+
let disposed = false;
|
|
207
|
+
const draw = (): void => {
|
|
208
|
+
frame = requestAnimationFrame(draw);
|
|
209
|
+
if (disposed) return;
|
|
210
|
+
renderScene();
|
|
211
|
+
};
|
|
212
|
+
frame = requestAnimationFrame(draw);
|
|
213
|
+
|
|
214
|
+
const observer = new ResizeObserver(() => {
|
|
215
|
+
if (disposed) return;
|
|
216
|
+
renderScene();
|
|
217
|
+
});
|
|
218
|
+
observer.observe(layer);
|
|
219
|
+
|
|
220
|
+
let openingFit = 0;
|
|
221
|
+
let openingAttempts = 0;
|
|
222
|
+
let openingCancelled = false;
|
|
223
|
+
const stopOpeningOnNavigation = view.subscribe(() => {
|
|
224
|
+
openingCancelled = true;
|
|
225
|
+
});
|
|
226
|
+
const tryOpeningFit = (): void => {
|
|
227
|
+
if (openingCancelled || disposed || openingAttempts++ > 180) return;
|
|
228
|
+
if (fitIsolatedSceneView(layer, isolatedStageBounds(), view)) return;
|
|
229
|
+
openingFit = requestAnimationFrame(tryOpeningFit);
|
|
230
|
+
};
|
|
231
|
+
openingFit = requestAnimationFrame(tryOpeningFit);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
canvas,
|
|
235
|
+
app,
|
|
236
|
+
screen,
|
|
237
|
+
pixi,
|
|
238
|
+
dispose() {
|
|
239
|
+
disposed = true;
|
|
240
|
+
cancelAnimationFrame(frame);
|
|
241
|
+
cancelAnimationFrame(openingFit);
|
|
242
|
+
stopOpeningOnNavigation();
|
|
243
|
+
observer.disconnect();
|
|
244
|
+
unregisterPresented();
|
|
245
|
+
ownedScreen.dispose();
|
|
246
|
+
app.destroy(true, { children: true });
|
|
247
|
+
canvas.remove();
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AuthoringAdapter } from '@volter/editor-project/adapter';
|
|
2
|
+
import type * as THREE from 'three';
|
|
3
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
4
|
+
import { stampEntityId } from '@volter/editor-core/entity-object';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Selection is editor-global UI state. A mounted adapter may deliberately own
|
|
8
|
+
* selection; when it does not, connect its semantic ids to the shared editor
|
|
9
|
+
* selection rather than requiring project code to know about EditorShellStore.
|
|
10
|
+
*/
|
|
11
|
+
export function withStoreSelection(
|
|
12
|
+
authoring: AuthoringAdapter,
|
|
13
|
+
store: EditorShellStore,
|
|
14
|
+
): AuthoringAdapter {
|
|
15
|
+
if (authoring.selection) return authoring;
|
|
16
|
+
const selection = {
|
|
17
|
+
get: () => [...store.selectedEntityIds],
|
|
18
|
+
set: (ids: string[]) => store.selectMultiple(ids),
|
|
19
|
+
};
|
|
20
|
+
return new Proxy(authoring, {
|
|
21
|
+
get(target, property) {
|
|
22
|
+
if (property === 'selection') return selection;
|
|
23
|
+
const value = Reflect.get(target, property, target) as unknown;
|
|
24
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Make the adopted Three scene use the mounted adapter's own semantic ids.
|
|
31
|
+
* The store, viewport raycast, gizmo and adapter hierarchy must all address an
|
|
32
|
+
* object by the same id; a later generic live-scene walk preserves these tags.
|
|
33
|
+
*/
|
|
34
|
+
export function stampMountedAuthoringIds(scene: THREE.Scene, authoring: AuthoringAdapter): void {
|
|
35
|
+
const idForObject3D = authoring.hierarchy.idForObject3D;
|
|
36
|
+
if (!idForObject3D) return;
|
|
37
|
+
scene.traverse((object) => {
|
|
38
|
+
const id = idForObject3D(object);
|
|
39
|
+
if (id) stampEntityId(object, id);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Container, TickerCallback } from 'pixi.js';
|
|
2
|
+
import * as shellPixi from 'pixi.js';
|
|
3
|
+
import type { CanvasPixiNamespace } from '../canvas-entry-runtime';
|
|
4
|
+
import { currentGameRealmMountId, gameRealmForMountId } from '../gated-globals';
|
|
5
|
+
|
|
6
|
+
/** The part of a GSAP instance this reads: its global timeline's live work. */
|
|
7
|
+
interface GsapAnimation {
|
|
8
|
+
kill(): unknown;
|
|
9
|
+
}
|
|
10
|
+
interface GameGsap {
|
|
11
|
+
readonly globalTimeline: {
|
|
12
|
+
getChildren(nested: boolean, tweens: boolean, timelines: boolean): GsapAnimation[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* THE GAME'S OWN GSAP, when it has one. GSAP installs itself as `gsap` on the
|
|
18
|
+
* global its code runs against — the game realm's, or the page's — so the
|
|
19
|
+
* animations a game's widgets start are read from the instance the GAME
|
|
20
|
+
* loaded. The editor carries no GSAP of its own.
|
|
21
|
+
*/
|
|
22
|
+
function gameGsap(): GameGsap | null {
|
|
23
|
+
const mountId = currentGameRealmMountId();
|
|
24
|
+
const realmGlobal = mountId === null ? null : gameRealmForMountId(mountId).globalThis;
|
|
25
|
+
const candidate =
|
|
26
|
+
(realmGlobal as { gsap?: unknown } | null)?.gsap ?? (globalThis as { gsap?: unknown }).gsap;
|
|
27
|
+
const timeline = (candidate as Partial<GameGsap> | undefined)?.globalTimeline;
|
|
28
|
+
return typeof timeline?.getChildren === 'function' ? (candidate as GameGsap) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function liveGsapAnimations(gsap: GameGsap | null): GsapAnimation[] {
|
|
32
|
+
return gsap ? gsap.globalTimeline.getChildren(true, true, true) : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function newGsapAnimations(
|
|
36
|
+
gsap: GameGsap | null,
|
|
37
|
+
before: ReadonlySet<GsapAnimation>,
|
|
38
|
+
): GsapAnimation[] {
|
|
39
|
+
return liveGsapAnimations(gsap).filter((animation) => !before.has(animation));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Construct one short-lived Pixi object while withholding shared-ticker
|
|
44
|
+
* listeners installed synchronously by its constructor.
|
|
45
|
+
*
|
|
46
|
+
* `@pixi/ui` FancyButton installs an anonymous `Ticker.shared` callback for
|
|
47
|
+
* each animated button and exposes no matching disposer. That is harmless for
|
|
48
|
+
* the original page-lifetime game, but editor isolation deliberately mounts
|
|
49
|
+
* and destroys the same classes repeatedly. Registering the callback and only
|
|
50
|
+
* removing it at teardown is too late: while this preview is alive it advances
|
|
51
|
+
* the package-global tween group, including stale tweens from a preview React
|
|
52
|
+
* just destroyed. Edit is contractually static, so constructor-time ownership
|
|
53
|
+
* of a wall-clock shared ticker is withheld at the isolation boundary. Play
|
|
54
|
+
* never uses this helper and keeps the game's original clock unchanged.
|
|
55
|
+
*
|
|
56
|
+
* `pixi` is the namespace `create()` CONSTRUCTS IN, because `Ticker.shared` is
|
|
57
|
+
* a module-global: the shell's is a different clock from the project's, and
|
|
58
|
+
* withholding registrations on the wrong one withholds nothing at all — the
|
|
59
|
+
* `@pixi/ui` callbacks this exists to intercept install on whichever
|
|
60
|
+
* `Ticker.shared` the constructed class imported. Absent ⇒ this bundle's own
|
|
61
|
+
* copy (see `../../vite-plugin-module-doorways.ts`).
|
|
62
|
+
*/
|
|
63
|
+
export function createWithOwnedPixiTickerListeners<T extends Container>(
|
|
64
|
+
create: () => T,
|
|
65
|
+
pixi: CanvasPixiNamespace = shellPixi,
|
|
66
|
+
): {
|
|
67
|
+
readonly value: T;
|
|
68
|
+
dispose(): void;
|
|
69
|
+
} {
|
|
70
|
+
const ticker = pixi.Ticker.shared;
|
|
71
|
+
const originalAdd = ticker.add;
|
|
72
|
+
const gsap = gameGsap();
|
|
73
|
+
const animationsBefore = new Set(liveGsapAnimations(gsap));
|
|
74
|
+
ticker.add = ((_fn: TickerCallback<unknown>, _context?: unknown, _priority?: number) =>
|
|
75
|
+
ticker) as typeof ticker.add;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const value = create();
|
|
79
|
+
// Game interaction is not an authoring gesture. Besides keeping Edit
|
|
80
|
+
// static, disabling the isolated subtree prevents @pixi/ui hover handlers
|
|
81
|
+
// from enqueueing tweedle animations that would survive React teardown in
|
|
82
|
+
// the package-global group and crash when Play later advances it.
|
|
83
|
+
value.eventMode = 'none';
|
|
84
|
+
const ownedAnimations = newGsapAnimations(gsap, animationsBefore);
|
|
85
|
+
// A delayed GSAP call is already live once construction returns. Waiting
|
|
86
|
+
// until disposal lets it mutate the supposedly held document for its whole
|
|
87
|
+
// visible lifetime, so remove construction-owned work immediately.
|
|
88
|
+
for (const animation of ownedAnimations) animation.kill();
|
|
89
|
+
return { value, dispose: () => {} };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
for (const animation of newGsapAnimations(gsap, animationsBefore)) animation.kill();
|
|
92
|
+
throw error;
|
|
93
|
+
} finally {
|
|
94
|
+
ticker.add = originalAdd;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A structural authoring projection over a live Phaser 3 game.
|
|
3
|
+
*
|
|
4
|
+
* Phaser belongs behind the generic `canvas` surface: this adapter never
|
|
5
|
+
* imports Phaser and never asks the host to become a Phaser runtime. It reads
|
|
6
|
+
* the public shape of the game instance the editor's realm holds, projects
|
|
7
|
+
* active scenes and their display lists into ordinary EditorNodes, and writes
|
|
8
|
+
* the native object's own transform/visibility fields directly. The game
|
|
9
|
+
* remains the entity model; no mirror tree or editor scene is fabricated.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
AuthoringAdapter,
|
|
14
|
+
AuthoringCapabilities,
|
|
15
|
+
AuthoringProvenance,
|
|
16
|
+
EditorNode,
|
|
17
|
+
PropertyDescriptor,
|
|
18
|
+
Transform,
|
|
19
|
+
} from '@volter/editor-project/adapter';
|
|
20
|
+
import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
|
|
21
|
+
import { createEphemeralPersistence } from './ephemeral-persistence';
|
|
22
|
+
|
|
23
|
+
export interface PhaserGameObjectLike {
|
|
24
|
+
name?: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
active?: boolean;
|
|
27
|
+
visible?: boolean;
|
|
28
|
+
alpha?: number;
|
|
29
|
+
depth?: number;
|
|
30
|
+
x?: number;
|
|
31
|
+
y?: number;
|
|
32
|
+
rotation?: number;
|
|
33
|
+
scaleX?: number;
|
|
34
|
+
scaleY?: number;
|
|
35
|
+
list?: PhaserGameObjectLike[];
|
|
36
|
+
constructor?: { name?: string };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PhaserSceneLike {
|
|
40
|
+
sys?: { settings?: { key?: string; active?: boolean } };
|
|
41
|
+
children?: { list?: PhaserGameObjectLike[] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PhaserGameLike {
|
|
45
|
+
scene?: { getScenes?: (activeOnly?: boolean) => PhaserSceneLike[] };
|
|
46
|
+
canvas?: HTMLCanvasElement;
|
|
47
|
+
destroy?: (removeCanvas?: boolean, noReturn?: boolean) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type IndexedValue =
|
|
51
|
+
| { kind: 'scene'; scene: PhaserSceneLike; node: EditorNode }
|
|
52
|
+
| { kind: 'object'; object: PhaserGameObjectLike; node: EditorNode };
|
|
53
|
+
|
|
54
|
+
function angleToQuat(angle: number): [number, number, number, number] {
|
|
55
|
+
return [0, 0, Math.sin(angle / 2), Math.cos(angle / 2)];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function quatToAngle(q: [number, number, number, number]): number {
|
|
59
|
+
return 2 * Math.atan2(q[2], q[3]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cleanSegment(value: string): string {
|
|
63
|
+
return encodeURIComponent(value || 'unnamed');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function objectChildren(object: PhaserGameObjectLike): PhaserGameObjectLike[] {
|
|
67
|
+
return Array.isArray(object.list) ? object.list : [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class PhaserLiveAuthoringAdapter implements AuthoringAdapter {
|
|
71
|
+
readonly capabilities: AuthoringCapabilities = {
|
|
72
|
+
transform: true,
|
|
73
|
+
inspectorFields: true,
|
|
74
|
+
persist: false,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
readonly provenance: AuthoringProvenance = {
|
|
78
|
+
source: 'foreign',
|
|
79
|
+
label: 'live-only',
|
|
80
|
+
detail:
|
|
81
|
+
'Live Phaser scene/display lists projected directly — edits apply to native game objects for this session only.',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
readonly persistence = {
|
|
85
|
+
...createEphemeralPersistence(),
|
|
86
|
+
destination: 'live-only (not saved)',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
private readonly indexed = new Map<string, IndexedValue>();
|
|
90
|
+
private readonly listeners = new Set<() => void>();
|
|
91
|
+
|
|
92
|
+
constructor(
|
|
93
|
+
private readonly game: PhaserGameLike,
|
|
94
|
+
private readonly store: EditorShellStore,
|
|
95
|
+
) {}
|
|
96
|
+
|
|
97
|
+
refresh(): { scenes: number; objects: number } {
|
|
98
|
+
this.reindex();
|
|
99
|
+
let scenes = 0;
|
|
100
|
+
let objects = 0;
|
|
101
|
+
for (const value of this.indexed.values()) {
|
|
102
|
+
if (value.kind === 'scene') scenes++;
|
|
103
|
+
else objects++;
|
|
104
|
+
}
|
|
105
|
+
return { scenes, objects };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private scenes(): PhaserSceneLike[] {
|
|
109
|
+
const getScenes = this.game.scene?.getScenes;
|
|
110
|
+
if (typeof getScenes !== 'function') return [];
|
|
111
|
+
const active = getScenes.call(this.game.scene, true);
|
|
112
|
+
return Array.isArray(active) ? active : [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private reindex(): void {
|
|
116
|
+
this.indexed.clear();
|
|
117
|
+
for (const [sceneIndex, scene] of this.scenes().entries()) {
|
|
118
|
+
const sceneName = scene.sys?.settings?.key || `Scene ${sceneIndex + 1}`;
|
|
119
|
+
const sceneId = `phaser:scene:${sceneIndex}:${cleanSegment(sceneName)}`;
|
|
120
|
+
const roots = Array.isArray(scene.children?.list) ? scene.children.list : [];
|
|
121
|
+
const sceneNode: EditorNode = {
|
|
122
|
+
id: sceneId,
|
|
123
|
+
label: sceneName,
|
|
124
|
+
role: 'root',
|
|
125
|
+
kind: 'phaser-scene',
|
|
126
|
+
typeLabel: 'Phaser.Scene',
|
|
127
|
+
parentId: null,
|
|
128
|
+
childIds: [],
|
|
129
|
+
};
|
|
130
|
+
this.indexed.set(sceneId, { kind: 'scene', scene, node: sceneNode });
|
|
131
|
+
sceneNode.childIds = roots.map((object, index) =>
|
|
132
|
+
this.indexObject(object, sceneId, `${sceneIndex}/${index}`),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private indexObject(object: PhaserGameObjectLike, parentId: string, path: string): string {
|
|
138
|
+
const nativeType = object.type || object.constructor?.name || 'GameObject';
|
|
139
|
+
const label = object.name || nativeType;
|
|
140
|
+
// Structural path only: renaming a native object must not change the id
|
|
141
|
+
// the editor selection and undo surfaces use for it.
|
|
142
|
+
const id = `phaser:object:${path}`;
|
|
143
|
+
const node: EditorNode = {
|
|
144
|
+
id,
|
|
145
|
+
label,
|
|
146
|
+
role: 'entity',
|
|
147
|
+
kind: `phaser-${nativeType.toLowerCase()}`,
|
|
148
|
+
// Bundled Phaser classes are routinely minified (`c`, `a`, ...). Its
|
|
149
|
+
// public GameObject `type` is the stable player-facing type name.
|
|
150
|
+
typeLabel: nativeType,
|
|
151
|
+
parentId,
|
|
152
|
+
childIds: [],
|
|
153
|
+
};
|
|
154
|
+
this.indexed.set(id, { kind: 'object', object, node });
|
|
155
|
+
node.childIds = objectChildren(object).map((child, index) =>
|
|
156
|
+
this.indexObject(child, id, `${path}/${index}`),
|
|
157
|
+
);
|
|
158
|
+
return id;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
readonly hierarchy = {
|
|
162
|
+
roots: (): EditorNode[] => {
|
|
163
|
+
this.reindex();
|
|
164
|
+
return [...this.indexed.values()]
|
|
165
|
+
.filter((value) => value.kind === 'scene')
|
|
166
|
+
.map((value) => value.node);
|
|
167
|
+
},
|
|
168
|
+
node: (id: string): EditorNode | null => {
|
|
169
|
+
this.reindex();
|
|
170
|
+
return this.indexed.get(id)?.node ?? null;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
readonly selection = {
|
|
175
|
+
get: (): string[] => [...this.store.selectedEntityIds],
|
|
176
|
+
set: (ids: string[]): void => {
|
|
177
|
+
this.store.selectMultiple(ids);
|
|
178
|
+
this.notify();
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
readonly transforms = {
|
|
183
|
+
dimensions: (id: string): '2d' | null => {
|
|
184
|
+
this.reindex();
|
|
185
|
+
return this.indexed.get(id)?.kind === 'object' ? '2d' : null;
|
|
186
|
+
},
|
|
187
|
+
get: (id: string): Transform => {
|
|
188
|
+
this.reindex();
|
|
189
|
+
const value = this.indexed.get(id);
|
|
190
|
+
if (!value || value.kind !== 'object') {
|
|
191
|
+
return { position: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1] };
|
|
192
|
+
}
|
|
193
|
+
const object = value.object;
|
|
194
|
+
return {
|
|
195
|
+
position: [object.x ?? 0, object.y ?? 0, object.depth ?? 0],
|
|
196
|
+
rotation: angleToQuat(object.rotation ?? 0),
|
|
197
|
+
scale: [object.scaleX ?? 1, object.scaleY ?? 1, 1],
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
beginEdit: (_id: string): void => {},
|
|
201
|
+
apply: (id: string, transform: Transform): void => {
|
|
202
|
+
this.reindex();
|
|
203
|
+
const value = this.indexed.get(id);
|
|
204
|
+
if (!value || value.kind !== 'object') return;
|
|
205
|
+
value.object.x = transform.position[0];
|
|
206
|
+
value.object.y = transform.position[1];
|
|
207
|
+
value.object.depth = transform.position[2];
|
|
208
|
+
value.object.rotation = quatToAngle(transform.rotation);
|
|
209
|
+
value.object.scaleX = transform.scale[0];
|
|
210
|
+
value.object.scaleY = transform.scale[1];
|
|
211
|
+
this.notify();
|
|
212
|
+
},
|
|
213
|
+
endEdit: (_id: string): void => {},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
readonly inspector = {
|
|
217
|
+
properties: (id: string): PropertyDescriptor[] => {
|
|
218
|
+
this.reindex();
|
|
219
|
+
const value = this.indexed.get(id);
|
|
220
|
+
if (!value) return [];
|
|
221
|
+
if (value.kind === 'scene') {
|
|
222
|
+
return [{ path: 'key', label: 'Scene key', type: 'string', readonly: true }];
|
|
223
|
+
}
|
|
224
|
+
return [
|
|
225
|
+
{ path: 'type', label: 'Type', type: 'string', readonly: true },
|
|
226
|
+
{ path: 'name', label: 'Name', type: 'string' },
|
|
227
|
+
{ path: 'active', label: 'Active', type: 'boolean' },
|
|
228
|
+
{ path: 'visible', label: 'Visible', type: 'boolean' },
|
|
229
|
+
{ path: 'alpha', label: 'Alpha', type: 'number' },
|
|
230
|
+
{ path: 'depth', label: 'Depth', type: 'number' },
|
|
231
|
+
];
|
|
232
|
+
},
|
|
233
|
+
get: (id: string, path: string): unknown => {
|
|
234
|
+
this.reindex();
|
|
235
|
+
const value = this.indexed.get(id);
|
|
236
|
+
if (!value) return undefined;
|
|
237
|
+
if (value.kind === 'scene')
|
|
238
|
+
return path === 'key' ? value.scene.sys?.settings?.key : undefined;
|
|
239
|
+
if (path === 'type') return value.object.type || value.object.constructor?.name;
|
|
240
|
+
return value.object[path as keyof PhaserGameObjectLike];
|
|
241
|
+
},
|
|
242
|
+
set: (id: string, path: string, value: unknown): void => {
|
|
243
|
+
this.reindex();
|
|
244
|
+
const indexed = this.indexed.get(id);
|
|
245
|
+
if (!indexed || indexed.kind !== 'object' || path === 'type') return;
|
|
246
|
+
(indexed.object as unknown as Record<string, unknown>)[path] = value;
|
|
247
|
+
this.notify();
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
subscribe(listener: () => void): () => void {
|
|
252
|
+
this.listeners.add(listener);
|
|
253
|
+
return () => this.listeners.delete(listener);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
dispose(): void {
|
|
257
|
+
this.listeners.clear();
|
|
258
|
+
this.indexed.clear();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private notify(): void {
|
|
262
|
+
for (const listener of this.listeners) listener();
|
|
263
|
+
this.store.notifyIngestEdit();
|
|
264
|
+
}
|
|
265
|
+
}
|