Files
DndGamePlayer/app/main/materials/materialsOverlayStore.ts
T
Ivan Fontosh 02d73ddf81 feat: scene traps, material legends, and scene editor window
Add trap overlays with session state, material legend editor/panel, and a dedicated scene editor window wired through IPC and project persistence.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 16:29:43 +08:00

147 lines
4.1 KiB
TypeScript

import {
clampMaterialsLayout,
DEFAULT_MATERIALS_OVERLAY_LAYOUT,
type MaterialId,
type MaterialsOverlayEvent,
type MaterialsOverlayLayout,
type MaterialsOverlayState,
type MaterialsZoomTool,
zoomMaterialsLayoutAt,
} from '../../shared/types';
function emptyState(): MaterialsOverlayState {
return {
revision: 1,
activeMaterialId: null,
layout: { ...DEFAULT_MATERIALS_OVERLAY_LAYOUT },
legendLayout: { ...DEFAULT_MATERIALS_OVERLAY_LAYOUT, cx: 0.82, cy: 0.5, scale: 0.85 },
zoomTool: null,
};
}
function initialLayout(rotationDeg?: number): MaterialsOverlayLayout {
return clampMaterialsLayout({
...DEFAULT_MATERIALS_OVERLAY_LAYOUT,
rotationDeg: rotationDeg ?? 0,
});
}
function initialLegendLayout(): MaterialsOverlayLayout {
return clampMaterialsLayout({
...DEFAULT_MATERIALS_OVERLAY_LAYOUT,
cx: 0.82,
cy: 0.5,
scale: 0.85,
rotationDeg: 0,
});
}
export class MaterialsOverlayStore {
private state: MaterialsOverlayState = emptyState();
getState(): MaterialsOverlayState {
return this.state;
}
clear(): MaterialsOverlayState {
if (this.state.activeMaterialId === null && this.state.zoomTool === null) {
return this.state;
}
this.state = {
revision: this.state.revision + 1,
activeMaterialId: null,
layout: { ...DEFAULT_MATERIALS_OVERLAY_LAYOUT },
legendLayout: initialLegendLayout(),
zoomTool: null,
};
return this.state;
}
dispatch(event: MaterialsOverlayEvent): MaterialsOverlayState {
switch (event.kind) {
case 'hide':
return this.clear();
case 'show':
this.state = {
revision: this.state.revision + 1,
activeMaterialId: event.materialId,
layout: initialLayout(event.rotationDeg),
legendLayout: initialLegendLayout(),
zoomTool: this.state.zoomTool,
};
return this.state;
case 'toggle': {
if (this.state.activeMaterialId === event.materialId) {
return this.clear();
}
this.state = {
revision: this.state.revision + 1,
activeMaterialId: event.materialId,
layout: initialLayout(event.rotationDeg),
legendLayout: initialLegendLayout(),
zoomTool: this.state.zoomTool,
};
return this.state;
}
case 'layout.set': {
if (this.state.activeMaterialId === null) return this.state;
this.state = {
...this.state,
revision: this.state.revision + 1,
layout: clampMaterialsLayout({
...this.state.layout,
...event.layout,
}),
};
return this.state;
}
case 'legendLayout.set': {
if (this.state.activeMaterialId === null) return this.state;
this.state = {
...this.state,
revision: this.state.revision + 1,
legendLayout: clampMaterialsLayout({
...this.state.legendLayout,
...event.layout,
rotationDeg: 0,
}),
};
return this.state;
}
case 'zoomTool.set': {
const tool: MaterialsZoomTool = event.tool;
this.state = {
...this.state,
revision: this.state.revision + 1,
zoomTool: tool,
};
return this.state;
}
case 'zoomAt': {
if (this.state.activeMaterialId === null || !this.state.zoomTool) return this.state;
const factor = this.state.zoomTool === 'zoomIn' ? 1.25 : 1 / 1.25;
const layout: MaterialsOverlayLayout = zoomMaterialsLayoutAt(
this.state.layout,
event.nx,
event.ny,
factor,
);
this.state = {
...this.state,
revision: this.state.revision + 1,
layout,
};
return this.state;
}
default:
return this.state;
}
}
ensureMaterialStillExists(materialIds: ReadonlySet<MaterialId>): MaterialsOverlayState {
const active = this.state.activeMaterialId;
if (active === null || materialIds.has(active)) return this.state;
return this.clear();
}
}