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): 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 = {}; const legendLayouts: Record = {}; 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; } }