Files
DndGamePlayer/app/main/materials/materialsOverlayStore.ts
T
Ivan Fontosh 2979d06f1c feat: multi-material overlays, presentation guides, and release prebuilds
Align control/presentation with presentation screen rect and darkness z-order; sync window titles and session window cleanup. Pack Win/Mac/Linux with npmRebuild disabled and release-native-prep for classic-level and sharp.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 11:28:18 +08:00

229 lines
7.5 KiB
TypeScript

import {
clampMaterialsLayout,
DEFAULT_MATERIALS_OVERLAY_LAYOUT,
defaultLegendLayoutForMaterial,
type MaterialId,
type MaterialsOverlayEvent,
type MaterialsOverlayLayout,
type MaterialsOverlayState,
type MaterialsZoomTool,
zoomMaterialsLayoutAt,
} from '../../shared/types';
function emptyState(): MaterialsOverlayState {
return {
revision: 1,
activeMaterialIds: [],
layouts: {},
legendLayouts: {},
focusMaterialId: null,
zoomTool: null,
};
}
function initialLayout(rotationDeg?: number): MaterialsOverlayLayout {
return clampMaterialsLayout({
...DEFAULT_MATERIALS_OVERLAY_LAYOUT,
rotationDeg: rotationDeg ?? 0,
});
}
function layoutFor(state: MaterialsOverlayState, materialId: MaterialId): MaterialsOverlayLayout {
return state.layouts[materialId] ?? { ...DEFAULT_MATERIALS_OVERLAY_LAYOUT };
}
function legendLayoutFor(state: MaterialsOverlayState, materialId: MaterialId): MaterialsOverlayLayout {
return state.legendLayouts[materialId] ?? defaultLegendLayoutForMaterial();
}
export class MaterialsOverlayStore {
private state: MaterialsOverlayState = emptyState();
getState(): MaterialsOverlayState {
return this.state;
}
clear(): MaterialsOverlayState {
if (this.state.activeMaterialIds.length === 0 && this.state.zoomTool === null) {
return this.state;
}
this.state = {
revision: this.state.revision + 1,
activeMaterialIds: [],
layouts: {},
legendLayouts: {},
focusMaterialId: null,
zoomTool: null,
};
return this.state;
}
dispatch(event: MaterialsOverlayEvent): MaterialsOverlayState {
switch (event.kind) {
case 'hide':
return this.clear();
case 'show': {
if (this.state.activeMaterialIds.includes(event.materialId)) {
this.state = {
...this.state,
revision: this.state.revision + 1,
focusMaterialId: event.materialId,
};
return this.state;
}
this.state = {
revision: this.state.revision + 1,
activeMaterialIds: [...this.state.activeMaterialIds, event.materialId],
layouts: {
...this.state.layouts,
[event.materialId]: initialLayout(event.rotationDeg),
},
legendLayouts: {
...this.state.legendLayouts,
[event.materialId]: defaultLegendLayoutForMaterial(),
},
focusMaterialId: event.materialId,
zoomTool: this.state.zoomTool,
};
return this.state;
}
case 'toggle': {
if (this.state.activeMaterialIds.includes(event.materialId)) {
const activeMaterialIds = this.state.activeMaterialIds.filter((id) => id !== event.materialId);
const layouts = { ...this.state.layouts };
const legendLayouts = { ...this.state.legendLayouts };
delete layouts[event.materialId];
delete legendLayouts[event.materialId];
const focusMaterialId =
this.state.focusMaterialId === event.materialId
? (activeMaterialIds[activeMaterialIds.length - 1] ?? null)
: this.state.focusMaterialId;
this.state = {
revision: this.state.revision + 1,
activeMaterialIds,
layouts,
legendLayouts,
focusMaterialId,
zoomTool: activeMaterialIds.length === 0 ? null : this.state.zoomTool,
};
return this.state;
}
this.state = {
revision: this.state.revision + 1,
activeMaterialIds: [...this.state.activeMaterialIds, event.materialId],
layouts: {
...this.state.layouts,
[event.materialId]: initialLayout(event.rotationDeg),
},
legendLayouts: {
...this.state.legendLayouts,
[event.materialId]: defaultLegendLayoutForMaterial(),
},
focusMaterialId: event.materialId,
zoomTool: this.state.zoomTool,
};
return this.state;
}
case 'layout.set': {
if (!this.state.activeMaterialIds.includes(event.materialId)) return this.state;
this.state = {
...this.state,
revision: this.state.revision + 1,
focusMaterialId: event.materialId,
layouts: {
...this.state.layouts,
[event.materialId]: clampMaterialsLayout({
...layoutFor(this.state, event.materialId),
...event.layout,
}),
},
};
return this.state;
}
case 'legendLayout.set': {
if (!this.state.activeMaterialIds.includes(event.materialId)) return this.state;
this.state = {
...this.state,
revision: this.state.revision + 1,
focusMaterialId: event.materialId,
legendLayouts: {
...this.state.legendLayouts,
[event.materialId]: clampMaterialsLayout({
...legendLayoutFor(this.state, event.materialId),
...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.activeMaterialIds.length === 0 || !this.state.zoomTool) return this.state;
const targetId =
(event.materialId && this.state.activeMaterialIds.includes(event.materialId)
? event.materialId
: null) ??
this.state.focusMaterialId ??
this.state.activeMaterialIds[this.state.activeMaterialIds.length - 1] ??
null;
if (!targetId) return this.state;
const factor = this.state.zoomTool === 'zoomIn' ? 1.25 : 1 / 1.25;
const layout: MaterialsOverlayLayout = zoomMaterialsLayoutAt(
layoutFor(this.state, targetId),
event.nx,
event.ny,
factor,
);
this.state = {
...this.state,
revision: this.state.revision + 1,
focusMaterialId: targetId,
layouts: {
...this.state.layouts,
[targetId]: layout,
},
};
return this.state;
}
default:
return this.state;
}
}
ensureMaterialStillExists(materialIds: ReadonlySet<MaterialId>): MaterialsOverlayState {
const activeMaterialIds = this.state.activeMaterialIds.filter((id) => materialIds.has(id));
if (activeMaterialIds.length === this.state.activeMaterialIds.length) return this.state;
if (activeMaterialIds.length === 0) return this.clear();
const layouts: Record<string, MaterialsOverlayLayout> = {};
const legendLayouts: Record<string, MaterialsOverlayLayout> = {};
for (const id of activeMaterialIds) {
const layout = this.state.layouts[id];
if (layout) layouts[id] = layout;
const legend = this.state.legendLayouts[id];
if (legend) legendLayouts[id] = legend;
}
const focusMaterialId =
this.state.focusMaterialId && activeMaterialIds.includes(this.state.focusMaterialId)
? this.state.focusMaterialId
: (activeMaterialIds[activeMaterialIds.length - 1] ?? null);
this.state = {
revision: this.state.revision + 1,
activeMaterialIds,
layouts,
legendLayouts,
focusMaterialId,
zoomTool: this.state.zoomTool,
};
return this.state;
}
}