mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
artcraft: move engine events + EngineStoreBridge into ui-pagescene
The Chromium-style EventListenerMap bus and the per-event-type bridge
both move into the lib. Class identity is single-sourced from
@storyteller/ui-pagescene so emit/subscribe lookups share the same
constructor key whether they're called from inside the lib or from
the artcraft host (e.g. PageEditor's grid-toggle GridVisibleChangedEvent).
The two apps/PageScene/engine files left in place become tiny re-export
shims pointing at the lib so the existing ~50+ importers (engine
internals, action dispatchers, PageEditor) keep working unchanged
during the staged migration -- they'll be deleted in a future commit
once their consumers move into the lib too.
Workspace plumbing:
- frontend/tsconfig.json: nx sync added pagescene to the root project
references list.
- pagescene/tsconfig.{json,lib.json}: nx sync trimmed the references
to only the libs the new code actually uses
(api-enums, model-list, common).
- node_modules/@storyteller/ui-pagescene needs to be a Windows
directory junction (mklink /J) for the workspace to resolve the
package; the prior bash `ln -s` produced a copy on Windows that
didn't reflect the lib's live state and caused misleading
"no exported member" errors.
Drive-by: nx sync also added a missing `gravatar` reference under
lightbox-modal -- unrelated cleanup riding along.
Verified: artcraft typecheck error count back to 124 baseline (no
new errors from this move).
This commit is contained in:
1 parent
bd18c3ec64
commit
cdb16ff9f4
12 files changed
+461
-488
No files matched your search
@@ -1,157 +1,8 @@
|
||||
// EngineStoreBridge — single boundary between the 3D engine and the
|
||||
// Zustand store. It is the ONLY file inside engine/ that imports
|
||||
// usePageSceneStore. Engine subsystems emit typed EngineEvents on the
|
||||
// bus; this bridge subscribes once per event class at construction and
|
||||
// translates each event into a one-line store mutation.
|
||||
//
|
||||
// One handler per event class — no instanceof chains. The bus routes
|
||||
// each emit to the matching handler set in O(1) (Map lookup), so the
|
||||
// hot per-frame events (CameraUpdatedEvent, etc.) avoid the linear
|
||||
// type-guard scan that the old switch-style had.
|
||||
|
||||
import { usePageSceneStore } from "../PageSceneStore";
|
||||
import { EngineEventBus } from "./events/EngineEventBus";
|
||||
import {
|
||||
AssetModalVisibilityChangedEvent,
|
||||
CameraAspectRatioChangedEvent,
|
||||
CameraUpdatedEvent,
|
||||
CamerasReplacedEvent,
|
||||
EditorLoaderEvent,
|
||||
EditorStateChangedEvent,
|
||||
EngineInitializedEvent,
|
||||
GridVisibleChangedEvent,
|
||||
InspectorPanelChangedEvent,
|
||||
ObjectAddedEvent,
|
||||
ObjectRemovedEvent,
|
||||
OutlinerItemLockToggledEvent,
|
||||
OutlinerItemVisibilityToggledEvent,
|
||||
OutlinerRefreshedEvent,
|
||||
OutlinerSelectedItemChangedEvent,
|
||||
PoseControlsVisibilityChangedEvent,
|
||||
PoseModeChangedEvent,
|
||||
SceneLoadedEvent,
|
||||
SceneResetEvent,
|
||||
SelectedCameraChangedEvent,
|
||||
SelectedModeChangedEvent,
|
||||
SelectionChangedEvent,
|
||||
TransformModeChangedEvent,
|
||||
TransformSpaceChangedEvent,
|
||||
} from "./events/EngineEvent";
|
||||
|
||||
export type EngineStoreBridgeDeps = {
|
||||
bus: EngineEventBus;
|
||||
};
|
||||
|
||||
export class EngineStoreBridge {
|
||||
private unsubscribeAll: () => void;
|
||||
|
||||
constructor(deps: EngineStoreBridgeDeps) {
|
||||
const { bus } = deps;
|
||||
const unsubs: Array<() => void> = [
|
||||
// selection / inspector
|
||||
bus.subscribe(SelectionChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setSelectedObject(e.selection),
|
||||
),
|
||||
bus.subscribe(InspectorPanelChangedEvent, (e) => {
|
||||
const store = usePageSceneStore.getState();
|
||||
if (e.panel) {
|
||||
store.updateObjectPanel(e.panel);
|
||||
store.showObjectPanel(e.panel);
|
||||
} else {
|
||||
store.hideObjectPanel();
|
||||
}
|
||||
}),
|
||||
|
||||
// outliner
|
||||
bus.subscribe(OutlinerRefreshedEvent, (e) =>
|
||||
usePageSceneStore.getState().setOutlinerItems(e.items),
|
||||
),
|
||||
bus.subscribe(OutlinerSelectedItemChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setOutlinerSelectedItem(e.item),
|
||||
),
|
||||
bus.subscribe(OutlinerItemLockToggledEvent, (e) =>
|
||||
usePageSceneStore.getState().toggleOutlinerLock(e.uuid),
|
||||
),
|
||||
bus.subscribe(OutlinerItemVisibilityToggledEvent, (e) =>
|
||||
usePageSceneStore.getState().toggleOutlinerVisibility(e.uuid),
|
||||
),
|
||||
|
||||
// scene tree
|
||||
bus.subscribe(ObjectAddedEvent, (e) => {
|
||||
const store = usePageSceneStore.getState();
|
||||
if (e.object.kind === "character") store.addCharacter(e.object);
|
||||
else if (e.object.kind === "shape") store.addShape(e.object);
|
||||
else store.addObject(e.object);
|
||||
}),
|
||||
bus.subscribe(ObjectRemovedEvent, (e) =>
|
||||
usePageSceneStore.getState().removeSceneObject(e.uuid),
|
||||
),
|
||||
bus.subscribe(SceneResetEvent, () =>
|
||||
usePageSceneStore.getState().resetScene(),
|
||||
),
|
||||
|
||||
// mode / overlays
|
||||
bus.subscribe(TransformModeChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setTransformMode(e.mode),
|
||||
),
|
||||
bus.subscribe(TransformSpaceChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setTransformSpace(e.space),
|
||||
),
|
||||
bus.subscribe(SelectedModeChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setSelectedMode(e.mode),
|
||||
),
|
||||
bus.subscribe(EditorStateChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setEditorState(e.state),
|
||||
),
|
||||
bus.subscribe(EditorLoaderEvent, (e) => {
|
||||
const store = usePageSceneStore.getState();
|
||||
if (e.visible) store.showEditorLoader(e.message);
|
||||
else store.hideEditorLoader();
|
||||
}),
|
||||
bus.subscribe(PoseControlsVisibilityChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setShowPoseControls(e.visible),
|
||||
),
|
||||
bus.subscribe(PoseModeChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setPoseMode(e.mode),
|
||||
),
|
||||
bus.subscribe(AssetModalVisibilityChangedEvent, (e) => {
|
||||
const store = usePageSceneStore.getState();
|
||||
store.setAssetModalVisible(e.visible);
|
||||
store.setAssetModalVisibleDuringDrag(e.visibleDuringDrag);
|
||||
}),
|
||||
bus.subscribe(GridVisibleChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setGridVisible(e.visible),
|
||||
),
|
||||
|
||||
// camera
|
||||
bus.subscribe(CameraAspectRatioChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setCameraAspectRatio(e.ratio),
|
||||
),
|
||||
bus.subscribe(CamerasReplacedEvent, (e) =>
|
||||
usePageSceneStore.setState({ cameras: e.cameras }),
|
||||
),
|
||||
bus.subscribe(SelectedCameraChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setSelectedCameraId(e.cameraId),
|
||||
),
|
||||
bus.subscribe(CameraUpdatedEvent, (e) =>
|
||||
usePageSceneStore.getState().updateCamera(e.cameraId, e.updates),
|
||||
),
|
||||
|
||||
// lifecycle
|
||||
bus.subscribe(EngineInitializedEvent, (e) =>
|
||||
usePageSceneStore.getState().setIs3DEditorInitialized(e.initialized),
|
||||
),
|
||||
bus.subscribe(SceneLoadedEvent, (e) =>
|
||||
usePageSceneStore.getState().setIs3DSceneLoaded(e.loaded),
|
||||
),
|
||||
];
|
||||
|
||||
this.unsubscribeAll = () => {
|
||||
for (const u of unsubs) u();
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
}
|
||||
// Re-export shim. Single source of truth lives in
|
||||
// @storyteller/ui-pagescene.
|
||||
export {
|
||||
EngineStoreBridge,
|
||||
} from "@storyteller/ui-pagescene";
|
||||
export type {
|
||||
EngineStoreBridgeDeps,
|
||||
} from "@storyteller/ui-pagescene";
|
||||
@@ -1,183 +1,4 @@
|
||||
// Typed event protocol for the 3D engine.
|
||||
//
|
||||
// Architecture (Command/polymorphism — same shape as engine/editor/
|
||||
// actions/UndoableAction): one class per event kind. Engine sites emit
|
||||
// `bus.emit(new SelectionChangedEvent(...))`; EngineStoreBridge is the
|
||||
// single subscriber that translates events to Zustand store mutations.
|
||||
// Adding a new event kind = one new class file here + one new branch
|
||||
// in EngineStoreBridge.handle. No other files change.
|
||||
//
|
||||
// Engine code never imports the Zustand store; emitting events is the
|
||||
// only way for the engine to push state changes outward.
|
||||
|
||||
import type { CameraAspectRatio, EditorStates } from "../../enums";
|
||||
import type {
|
||||
ObjectPanelObject,
|
||||
OutlinerItem,
|
||||
PoseMode,
|
||||
SceneObject,
|
||||
SelectedSceneObject,
|
||||
TransformMode,
|
||||
TransformSpace,
|
||||
} from "../../PageSceneStore";
|
||||
import type { Camera } from "@storyteller/common";
|
||||
|
||||
export abstract class EngineEvent {
|
||||
readonly timestamp: number = performance.now();
|
||||
}
|
||||
|
||||
// ─── lifecycle ────────────────────────────────────────────────────────
|
||||
|
||||
export class EngineInitializedEvent extends EngineEvent {
|
||||
constructor(readonly initialized: boolean) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class SceneLoadedEvent extends EngineEvent {
|
||||
constructor(readonly loaded: boolean) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class SceneResetEvent extends EngineEvent {}
|
||||
|
||||
// ─── selection / inspector ─────────────────────────────────────────────
|
||||
|
||||
export class SelectionChangedEvent extends EngineEvent {
|
||||
constructor(readonly selection: SelectedSceneObject | null) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class InspectorPanelChangedEvent extends EngineEvent {
|
||||
constructor(readonly panel: ObjectPanelObject | null) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── scene tree ────────────────────────────────────────────────────────
|
||||
|
||||
export class ObjectAddedEvent extends EngineEvent {
|
||||
constructor(readonly object: SceneObject) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectRemovedEvent extends EngineEvent {
|
||||
constructor(readonly uuid: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlinerRefreshedEvent extends EngineEvent {
|
||||
constructor(readonly items: OutlinerItem[]) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlinerSelectedItemChangedEvent extends EngineEvent {
|
||||
constructor(readonly item: OutlinerItem | null) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlinerItemLockToggledEvent extends EngineEvent {
|
||||
constructor(readonly uuid: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlinerItemVisibilityToggledEvent extends EngineEvent {
|
||||
constructor(readonly uuid: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── mode / transform / overlays ──────────────────────────────────────
|
||||
|
||||
export class TransformModeChangedEvent extends EngineEvent {
|
||||
constructor(readonly mode: TransformMode) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class TransformSpaceChangedEvent extends EngineEvent {
|
||||
constructor(readonly space: TransformSpace) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectedModeChangedEvent extends EngineEvent {
|
||||
constructor(readonly mode: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class EditorStateChangedEvent extends EngineEvent {
|
||||
constructor(readonly state: EditorStates) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class EditorLoaderEvent extends EngineEvent {
|
||||
constructor(readonly visible: boolean, readonly message?: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class PoseControlsVisibilityChangedEvent extends EngineEvent {
|
||||
constructor(readonly visible: boolean) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class PoseModeChangedEvent extends EngineEvent {
|
||||
constructor(readonly mode: PoseMode) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class AssetModalVisibilityChangedEvent extends EngineEvent {
|
||||
constructor(
|
||||
readonly visible: boolean,
|
||||
readonly visibleDuringDrag: boolean,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class GridVisibleChangedEvent extends EngineEvent {
|
||||
constructor(readonly visible: boolean) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── camera ────────────────────────────────────────────────────────────
|
||||
|
||||
export class CameraAspectRatioChangedEvent extends EngineEvent {
|
||||
constructor(readonly ratio: CameraAspectRatio) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class CamerasReplacedEvent extends EngineEvent {
|
||||
constructor(readonly cameras: Camera[]) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectedCameraChangedEvent extends EngineEvent {
|
||||
constructor(readonly cameraId: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class CameraUpdatedEvent extends EngineEvent {
|
||||
constructor(
|
||||
readonly cameraId: string,
|
||||
readonly updates: Partial<Camera>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
// Re-export shim. Single source of truth lives in
|
||||
// @storyteller/ui-pagescene; class identity is single-sourced so
|
||||
// emit/subscribe lookups share the same constructor key.
|
||||
export * from "@storyteller/ui-pagescene";
|
||||
@@ -1,50 +1,9 @@
|
||||
// Per-event-type pub/sub for EngineEvent. Mirrors Chromium's
|
||||
// EventListenerMap: each event class has its own listener set, and
|
||||
// emit() walks only that set — no global fan-out, no instanceof
|
||||
// chains in subscribers. We key by constructor reference instead of
|
||||
// the DOM's string `type` because identity is faster than string
|
||||
// hashing and we have no user-visible type strings to intern.
|
||||
//
|
||||
// emit() iterates the Set directly. ES Set iteration is well-defined
|
||||
// under concurrent add/delete (deleted entries skipped, added entries
|
||||
// visited later in the same iteration) — fine for our engine, since
|
||||
// no current subscriber self-modifies the listener set mid-dispatch.
|
||||
|
||||
import type { EngineEvent } from "./EngineEvent";
|
||||
|
||||
export type EngineEventCtor<T extends EngineEvent = EngineEvent> = new (
|
||||
...args: never[]
|
||||
) => T;
|
||||
|
||||
export type EngineEventListener<T extends EngineEvent = EngineEvent> = (
|
||||
event: T,
|
||||
) => void;
|
||||
|
||||
export class EngineEventBus {
|
||||
private byCtor = new Map<
|
||||
EngineEventCtor,
|
||||
Set<EngineEventListener<EngineEvent>>
|
||||
>();
|
||||
|
||||
subscribe<T extends EngineEvent>(
|
||||
ctor: EngineEventCtor<T>,
|
||||
listener: EngineEventListener<T>,
|
||||
): () => void {
|
||||
let set = this.byCtor.get(ctor);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
this.byCtor.set(ctor, set);
|
||||
}
|
||||
const erased = listener as EngineEventListener<EngineEvent>;
|
||||
set.add(erased);
|
||||
return () => {
|
||||
set!.delete(erased);
|
||||
};
|
||||
}
|
||||
|
||||
emit<T extends EngineEvent>(event: T): void {
|
||||
const set = this.byCtor.get(event.constructor as EngineEventCtor);
|
||||
if (!set) return;
|
||||
for (const fn of set) fn(event);
|
||||
}
|
||||
}
|
||||
// Re-export shim. Single source of truth lives in
|
||||
// @storyteller/ui-pagescene.
|
||||
export {
|
||||
EngineEventBus,
|
||||
} from "@storyteller/ui-pagescene";
|
||||
export type {
|
||||
EngineEventCtor,
|
||||
EngineEventListener,
|
||||
} from "@storyteller/ui-pagescene";
|
||||
@@ -14,6 +14,9 @@
|
||||
{
|
||||
"path": "../toaster"
|
||||
},
|
||||
{
|
||||
"path": "../gravatar"
|
||||
},
|
||||
{
|
||||
"path": "../../api"
|
||||
},
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
{
|
||||
"path": "../toaster/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../gravatar/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../api/tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -41,3 +41,42 @@ export type { XYZ, Simple3DVector } from "./lib/datastructures/common";
|
||||
|
||||
// Models (lib-internal types — only what's needed at the public boundary)
|
||||
export type { MediaItem, AudioMediaItem } from "./lib/models/assets";
|
||||
|
||||
// Engine event bus + event classes (host wrappers can emit
|
||||
// GridVisibleChangedEvent etc. through editor.bus to keep the
|
||||
// one-way write flow). Class identity is single-sourced from the
|
||||
// lib so emit/subscribe lookups share the same constructor key.
|
||||
export { EngineEventBus } from "./lib/engine/events/EngineEventBus";
|
||||
export { EngineStoreBridge } from "./lib/engine/EngineStoreBridge";
|
||||
export type { EngineStoreBridgeDeps } from "./lib/engine/EngineStoreBridge";
|
||||
export type {
|
||||
EngineEventCtor,
|
||||
EngineEventListener,
|
||||
} from "./lib/engine/events/EngineEventBus";
|
||||
export {
|
||||
EngineEvent,
|
||||
EngineInitializedEvent,
|
||||
SceneLoadedEvent,
|
||||
SceneResetEvent,
|
||||
SelectionChangedEvent,
|
||||
InspectorPanelChangedEvent,
|
||||
ObjectAddedEvent,
|
||||
ObjectRemovedEvent,
|
||||
OutlinerRefreshedEvent,
|
||||
OutlinerSelectedItemChangedEvent,
|
||||
OutlinerItemLockToggledEvent,
|
||||
OutlinerItemVisibilityToggledEvent,
|
||||
TransformModeChangedEvent,
|
||||
TransformSpaceChangedEvent,
|
||||
SelectedModeChangedEvent,
|
||||
EditorStateChangedEvent,
|
||||
EditorLoaderEvent,
|
||||
PoseControlsVisibilityChangedEvent,
|
||||
PoseModeChangedEvent,
|
||||
AssetModalVisibilityChangedEvent,
|
||||
GridVisibleChangedEvent,
|
||||
CameraAspectRatioChangedEvent,
|
||||
CamerasReplacedEvent,
|
||||
SelectedCameraChangedEvent,
|
||||
CameraUpdatedEvent,
|
||||
} from "./lib/engine/events/EngineEvent";
|
||||
@@ -0,0 +1,157 @@
|
||||
// EngineStoreBridge — single boundary between the 3D engine and the
|
||||
// Zustand store. It is the ONLY file inside engine/ that imports
|
||||
// usePageSceneStore. Engine subsystems emit typed EngineEvents on the
|
||||
// bus; this bridge subscribes once per event class at construction and
|
||||
// translates each event into a one-line store mutation.
|
||||
//
|
||||
// One handler per event class — no instanceof chains. The bus routes
|
||||
// each emit to the matching handler set in O(1) (Map lookup), so the
|
||||
// hot per-frame events (CameraUpdatedEvent, etc.) avoid the linear
|
||||
// type-guard scan that the old switch-style had.
|
||||
|
||||
import { usePageSceneStore } from "../PageSceneStore";
|
||||
import { EngineEventBus } from "./events/EngineEventBus";
|
||||
import {
|
||||
AssetModalVisibilityChangedEvent,
|
||||
CameraAspectRatioChangedEvent,
|
||||
CameraUpdatedEvent,
|
||||
CamerasReplacedEvent,
|
||||
EditorLoaderEvent,
|
||||
EditorStateChangedEvent,
|
||||
EngineInitializedEvent,
|
||||
GridVisibleChangedEvent,
|
||||
InspectorPanelChangedEvent,
|
||||
ObjectAddedEvent,
|
||||
ObjectRemovedEvent,
|
||||
OutlinerItemLockToggledEvent,
|
||||
OutlinerItemVisibilityToggledEvent,
|
||||
OutlinerRefreshedEvent,
|
||||
OutlinerSelectedItemChangedEvent,
|
||||
PoseControlsVisibilityChangedEvent,
|
||||
PoseModeChangedEvent,
|
||||
SceneLoadedEvent,
|
||||
SceneResetEvent,
|
||||
SelectedCameraChangedEvent,
|
||||
SelectedModeChangedEvent,
|
||||
SelectionChangedEvent,
|
||||
TransformModeChangedEvent,
|
||||
TransformSpaceChangedEvent,
|
||||
} from "./events/EngineEvent";
|
||||
|
||||
export type EngineStoreBridgeDeps = {
|
||||
bus: EngineEventBus;
|
||||
};
|
||||
|
||||
export class EngineStoreBridge {
|
||||
private unsubscribeAll: () => void;
|
||||
|
||||
constructor(deps: EngineStoreBridgeDeps) {
|
||||
const { bus } = deps;
|
||||
const unsubs: Array<() => void> = [
|
||||
// selection / inspector
|
||||
bus.subscribe(SelectionChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setSelectedObject(e.selection),
|
||||
),
|
||||
bus.subscribe(InspectorPanelChangedEvent, (e) => {
|
||||
const store = usePageSceneStore.getState();
|
||||
if (e.panel) {
|
||||
store.updateObjectPanel(e.panel);
|
||||
store.showObjectPanel(e.panel);
|
||||
} else {
|
||||
store.hideObjectPanel();
|
||||
}
|
||||
}),
|
||||
|
||||
// outliner
|
||||
bus.subscribe(OutlinerRefreshedEvent, (e) =>
|
||||
usePageSceneStore.getState().setOutlinerItems(e.items),
|
||||
),
|
||||
bus.subscribe(OutlinerSelectedItemChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setOutlinerSelectedItem(e.item),
|
||||
),
|
||||
bus.subscribe(OutlinerItemLockToggledEvent, (e) =>
|
||||
usePageSceneStore.getState().toggleOutlinerLock(e.uuid),
|
||||
),
|
||||
bus.subscribe(OutlinerItemVisibilityToggledEvent, (e) =>
|
||||
usePageSceneStore.getState().toggleOutlinerVisibility(e.uuid),
|
||||
),
|
||||
|
||||
// scene tree
|
||||
bus.subscribe(ObjectAddedEvent, (e) => {
|
||||
const store = usePageSceneStore.getState();
|
||||
if (e.object.kind === "character") store.addCharacter(e.object);
|
||||
else if (e.object.kind === "shape") store.addShape(e.object);
|
||||
else store.addObject(e.object);
|
||||
}),
|
||||
bus.subscribe(ObjectRemovedEvent, (e) =>
|
||||
usePageSceneStore.getState().removeSceneObject(e.uuid),
|
||||
),
|
||||
bus.subscribe(SceneResetEvent, () =>
|
||||
usePageSceneStore.getState().resetScene(),
|
||||
),
|
||||
|
||||
// mode / overlays
|
||||
bus.subscribe(TransformModeChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setTransformMode(e.mode),
|
||||
),
|
||||
bus.subscribe(TransformSpaceChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setTransformSpace(e.space),
|
||||
),
|
||||
bus.subscribe(SelectedModeChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setSelectedMode(e.mode),
|
||||
),
|
||||
bus.subscribe(EditorStateChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setEditorState(e.state),
|
||||
),
|
||||
bus.subscribe(EditorLoaderEvent, (e) => {
|
||||
const store = usePageSceneStore.getState();
|
||||
if (e.visible) store.showEditorLoader(e.message);
|
||||
else store.hideEditorLoader();
|
||||
}),
|
||||
bus.subscribe(PoseControlsVisibilityChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setShowPoseControls(e.visible),
|
||||
),
|
||||
bus.subscribe(PoseModeChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setPoseMode(e.mode),
|
||||
),
|
||||
bus.subscribe(AssetModalVisibilityChangedEvent, (e) => {
|
||||
const store = usePageSceneStore.getState();
|
||||
store.setAssetModalVisible(e.visible);
|
||||
store.setAssetModalVisibleDuringDrag(e.visibleDuringDrag);
|
||||
}),
|
||||
bus.subscribe(GridVisibleChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setGridVisible(e.visible),
|
||||
),
|
||||
|
||||
// camera
|
||||
bus.subscribe(CameraAspectRatioChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setCameraAspectRatio(e.ratio),
|
||||
),
|
||||
bus.subscribe(CamerasReplacedEvent, (e) =>
|
||||
usePageSceneStore.setState({ cameras: e.cameras }),
|
||||
),
|
||||
bus.subscribe(SelectedCameraChangedEvent, (e) =>
|
||||
usePageSceneStore.getState().setSelectedCameraId(e.cameraId),
|
||||
),
|
||||
bus.subscribe(CameraUpdatedEvent, (e) =>
|
||||
usePageSceneStore.getState().updateCamera(e.cameraId, e.updates),
|
||||
),
|
||||
|
||||
// lifecycle
|
||||
bus.subscribe(EngineInitializedEvent, (e) =>
|
||||
usePageSceneStore.getState().setIs3DEditorInitialized(e.initialized),
|
||||
),
|
||||
bus.subscribe(SceneLoadedEvent, (e) =>
|
||||
usePageSceneStore.getState().setIs3DSceneLoaded(e.loaded),
|
||||
),
|
||||
];
|
||||
|
||||
this.unsubscribeAll = () => {
|
||||
for (const u of unsubs) u();
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// Typed event protocol for the 3D engine.
|
||||
//
|
||||
// Architecture (Command/polymorphism — same shape as engine/editor/
|
||||
// actions/UndoableAction): one class per event kind. Engine sites emit
|
||||
// `bus.emit(new SelectionChangedEvent(...))`; EngineStoreBridge is the
|
||||
// single subscriber that translates events to Zustand store mutations.
|
||||
// Adding a new event kind = one new class file here + one new branch
|
||||
// in EngineStoreBridge.handle. No other files change.
|
||||
//
|
||||
// Engine code never imports the Zustand store; emitting events is the
|
||||
// only way for the engine to push state changes outward.
|
||||
|
||||
import type { CameraAspectRatio, EditorStates } from "../../enums";
|
||||
import type {
|
||||
ObjectPanelObject,
|
||||
OutlinerItem,
|
||||
PoseMode,
|
||||
SceneObject,
|
||||
SelectedSceneObject,
|
||||
TransformMode,
|
||||
TransformSpace,
|
||||
} from "../../PageSceneStore";
|
||||
import type { Camera } from "@storyteller/common";
|
||||
|
||||
export abstract class EngineEvent {
|
||||
readonly timestamp: number = performance.now();
|
||||
}
|
||||
|
||||
// ─── lifecycle ────────────────────────────────────────────────────────
|
||||
|
||||
export class EngineInitializedEvent extends EngineEvent {
|
||||
constructor(readonly initialized: boolean) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class SceneLoadedEvent extends EngineEvent {
|
||||
constructor(readonly loaded: boolean) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class SceneResetEvent extends EngineEvent {}
|
||||
|
||||
// ─── selection / inspector ─────────────────────────────────────────────
|
||||
|
||||
export class SelectionChangedEvent extends EngineEvent {
|
||||
constructor(readonly selection: SelectedSceneObject | null) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class InspectorPanelChangedEvent extends EngineEvent {
|
||||
constructor(readonly panel: ObjectPanelObject | null) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── scene tree ────────────────────────────────────────────────────────
|
||||
|
||||
export class ObjectAddedEvent extends EngineEvent {
|
||||
constructor(readonly object: SceneObject) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class ObjectRemovedEvent extends EngineEvent {
|
||||
constructor(readonly uuid: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlinerRefreshedEvent extends EngineEvent {
|
||||
constructor(readonly items: OutlinerItem[]) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlinerSelectedItemChangedEvent extends EngineEvent {
|
||||
constructor(readonly item: OutlinerItem | null) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlinerItemLockToggledEvent extends EngineEvent {
|
||||
constructor(readonly uuid: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlinerItemVisibilityToggledEvent extends EngineEvent {
|
||||
constructor(readonly uuid: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── mode / transform / overlays ──────────────────────────────────────
|
||||
|
||||
export class TransformModeChangedEvent extends EngineEvent {
|
||||
constructor(readonly mode: TransformMode) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class TransformSpaceChangedEvent extends EngineEvent {
|
||||
constructor(readonly space: TransformSpace) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectedModeChangedEvent extends EngineEvent {
|
||||
constructor(readonly mode: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class EditorStateChangedEvent extends EngineEvent {
|
||||
constructor(readonly state: EditorStates) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class EditorLoaderEvent extends EngineEvent {
|
||||
constructor(readonly visible: boolean, readonly message?: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class PoseControlsVisibilityChangedEvent extends EngineEvent {
|
||||
constructor(readonly visible: boolean) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class PoseModeChangedEvent extends EngineEvent {
|
||||
constructor(readonly mode: PoseMode) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class AssetModalVisibilityChangedEvent extends EngineEvent {
|
||||
constructor(
|
||||
readonly visible: boolean,
|
||||
readonly visibleDuringDrag: boolean,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class GridVisibleChangedEvent extends EngineEvent {
|
||||
constructor(readonly visible: boolean) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── camera ────────────────────────────────────────────────────────────
|
||||
|
||||
export class CameraAspectRatioChangedEvent extends EngineEvent {
|
||||
constructor(readonly ratio: CameraAspectRatio) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class CamerasReplacedEvent extends EngineEvent {
|
||||
constructor(readonly cameras: Camera[]) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectedCameraChangedEvent extends EngineEvent {
|
||||
constructor(readonly cameraId: string) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export class CameraUpdatedEvent extends EngineEvent {
|
||||
constructor(
|
||||
readonly cameraId: string,
|
||||
readonly updates: Partial<Camera>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Per-event-type pub/sub for EngineEvent. Mirrors Chromium's
|
||||
// EventListenerMap: each event class has its own listener set, and
|
||||
// emit() walks only that set — no global fan-out, no instanceof
|
||||
// chains in subscribers. We key by constructor reference instead of
|
||||
// the DOM's string `type` because identity is faster than string
|
||||
// hashing and we have no user-visible type strings to intern.
|
||||
//
|
||||
// emit() iterates the Set directly. ES Set iteration is well-defined
|
||||
// under concurrent add/delete (deleted entries skipped, added entries
|
||||
// visited later in the same iteration) — fine for our engine, since
|
||||
// no current subscriber self-modifies the listener set mid-dispatch.
|
||||
|
||||
import type { EngineEvent } from "./EngineEvent";
|
||||
|
||||
export type EngineEventCtor<T extends EngineEvent = EngineEvent> = new (
|
||||
...args: never[]
|
||||
) => T;
|
||||
|
||||
export type EngineEventListener<T extends EngineEvent = EngineEvent> = (
|
||||
event: T,
|
||||
) => void;
|
||||
|
||||
export class EngineEventBus {
|
||||
private byCtor = new Map<
|
||||
EngineEventCtor,
|
||||
Set<EngineEventListener<EngineEvent>>
|
||||
>();
|
||||
|
||||
subscribe<T extends EngineEvent>(
|
||||
ctor: EngineEventCtor<T>,
|
||||
listener: EngineEventListener<T>,
|
||||
): () => void {
|
||||
let set = this.byCtor.get(ctor);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
this.byCtor.set(ctor, set);
|
||||
}
|
||||
const erased = listener as EngineEventListener<EngineEvent>;
|
||||
set.add(erased);
|
||||
return () => {
|
||||
set!.delete(erased);
|
||||
};
|
||||
}
|
||||
|
||||
emit<T extends EngineEvent>(event: T): void {
|
||||
const set = this.byCtor.get(event.constructor as EngineEventCtor);
|
||||
if (!set) return;
|
||||
for (const fn of set) fn(event);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,6 @@
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../common"
|
||||
},
|
||||
{
|
||||
"path": "../../api"
|
||||
},
|
||||
{
|
||||
"path": "../../api-enums"
|
||||
},
|
||||
@@ -15,49 +9,7 @@
|
||||
"path": "../../model-list"
|
||||
},
|
||||
{
|
||||
"path": "../../soundboard"
|
||||
},
|
||||
{
|
||||
"path": "../promptbox"
|
||||
},
|
||||
{
|
||||
"path": "../popover"
|
||||
},
|
||||
{
|
||||
"path": "../loading"
|
||||
},
|
||||
{
|
||||
"path": "../toaster"
|
||||
},
|
||||
{
|
||||
"path": "../gallery-modal"
|
||||
},
|
||||
{
|
||||
"path": "../model-selector"
|
||||
},
|
||||
{
|
||||
"path": "../help-menu"
|
||||
},
|
||||
{
|
||||
"path": "../login-modal"
|
||||
},
|
||||
{
|
||||
"path": "../settings-modal"
|
||||
},
|
||||
{
|
||||
"path": "../modal"
|
||||
},
|
||||
{
|
||||
"path": "../tooltip"
|
||||
},
|
||||
{
|
||||
"path": "../button"
|
||||
},
|
||||
{
|
||||
"path": "../input"
|
||||
},
|
||||
{
|
||||
"path": "../slider-v2"
|
||||
"path": "../../common"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
|
||||
@@ -44,12 +44,6 @@
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../common/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../api/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../api-enums/tsconfig.lib.json"
|
||||
},
|
||||
@@ -57,49 +51,7 @@
|
||||
"path": "../../model-list/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../soundboard/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../promptbox/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../popover/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../loading/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../toaster/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../gallery-modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../model-selector/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../help-menu/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../login-modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../settings-modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../tooltip/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../button/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../input/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../slider-v2/tsconfig.lib.json"
|
||||
"path": "../../common/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -188,6 +188,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./libs/components/checkbox"
|
||||
},
|
||||
{
|
||||
"path": "./libs/components/pagescene"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user