import { MathUtils } from "three"; import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; import { defaultGraphData } from "../../components/SimulationDashboard/data/defaultGraphData"; import { Block, UIElement, ExtendedCSSProperties } from "../../types/exportedTypes"; interface SimulationDashboardStore { blocks: Block[]; selectedBlock: string | null; selectedElement: string | null; // Subscription management subscribe: (callback: () => void) => () => void; _notifySubscribers: () => void; saveBlocks: () => void; // Selection and hover management setSelectedBlock: (blockId: string | null) => void; setSelectedElement: (elementId: string | null) => void; // Block operations addBlock: (newBlock: Block) => void; removeBlock: (blockId: string) => void; updateBlock: (blockId: string, updates: Partial) => void; clearBlocks: () => void; setBlocks: (blocks: Block[]) => void; // Block styling and positioning updateBlockStyle: (blockId: string, newStyle: React.CSSProperties) => void; updateBlockSize: (blockId: string, size: Size) => void; updateBlockPosition: (blockId: string, position: Position) => void; updateBlockPositionType: (blockId: string, positionType: "relative" | "absolute" | "fixed") => void; updateBlockZIndex: (blockId: string, zIndex: number) => void; // Element operations addElement: (blockId: string, type: UIType, graphType?: GraphTypes) => void; removeElement: (blockId: string, elementId: string) => void; updateElement: (blockId: string, elementId: string, updates: Partial) => void; // Element styling and positioning updateElementStyle: (blockId: string, elementId: string, newStyle: ExtendedCSSProperties) => void; updateElementSize: (blockId: string, elementId: string, size: Size) => void; updateElementPosition: (blockId: string, elementId: string, position: Position) => void; updateElementPositionType: (blockId: string, elementId: string, positionType: "relative" | "absolute" | "fixed") => void; updateElementZIndex: (blockId: string, elementId: string, zIndex: number) => void; // Element data operations updateElementData: (blockId: string, elementId: string, updates: Partial) => void; updateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => void; updateGraphTitle: (blockId: string, elementId: string, title: string) => void; updateGraphType: (blockId: string, elementId: string, graphType: GraphTypes) => void; // Element swapping swapElements: (blockId: string, elementId1: string, elementId2: string) => void; // Peek operations (get updated value without setting state) peekAddBlock: () => Block[]; peekRemoveBlock: (blockId: string) => Block[]; peekUpdateBlock: (blockId: string, updates: Partial) => Block[]; peekUpdateBlockStyle: (blockId: string, newStyle: React.CSSProperties) => Block[]; peekUpdateBlockSize: (blockId: string, size: Size) => Block[]; peekUpdateBlockPosition: (blockId: string, position: Position) => Block[]; peekUpdateBlockPositionType: (blockId: string, positionType: "relative" | "absolute" | "fixed") => Block[]; peekUpdateBlockZIndex: (blockId: string, zIndex: number) => Block[]; peekAddElement: (blockId: string, type: UIType, graphType?: GraphTypes, dataType?: DataType) => Block[]; peekRemoveElement: (blockId: string, elementId: string) => Block[]; peekUpdateElement: (blockId: string, elementId: string, updates: Partial) => Block[]; peekUpdateElementStyle: (blockId: string, elementId: string, newStyle: ExtendedCSSProperties) => Block[]; peekUpdateElementSize: (blockId: string, elementId: string, size: Size) => Block[]; peekUpdateElementPosition: (blockId: string, elementId: string, position: Position) => Block[]; peekUpdateElementPositionType: (blockId: string, elementId: string, positionType: "relative" | "absolute" | "fixed") => Block[]; peekUpdateElementZIndex: (blockId: string, elementId: string, zIndex: number) => Block[]; peekUpdateElementData: (blockId: string, elementId: string, updates: Partial) => Block[]; peekUpdateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => Block[]; peekUpdateGraphTitle: (blockId: string, elementId: string, title: string) => Block[]; peekUpdateGraphType: (blockId: string, elementId: string, graphType: GraphTypes) => Block[]; peekUpdateDataType: (blockId: string, elementId: string, dataType: "single-machine" | "multiple-machine") => Block[]; peekUpdateCommonValue: (blockId: string, elementId: string, commonValue: string) => Block[]; peekUpdateDataValue: (blockId: string, elementId: string, dataValue: string | string[]) => Block[]; peekUpdateDataSource: (blockId: string, elementId: string, dataSource: string | string[]) => Block[]; peekSwapElements: (blockId: string, elementId1: string, elementId2: string) => Block[]; // Helper functions getBlockById: (blockId: string) => Block | undefined; getElementById: (blockId: string, elementId: string) => UIElement | undefined; getSelectedBlock: () => Block | undefined; getSelectedElement: () => UIElement | undefined; hasBlock: (blockId: string) => boolean; hasElement: (blockId: string, elementId: string) => boolean; } const subscribers = new Set<() => void>(); // Helper function to deep clone blocks const cloneBlocks = (blocks: Block[]): Block[] => { return JSON.parse(JSON.stringify(blocks)); }; export const createSimulationDashboardStore = () => { return create()( immer((set, get) => ({ blocks: [], selectedBlock: null, selectedElement: null, subscribe: (callback: () => void) => { subscribers.add(callback); return () => { subscribers.delete(callback); }; }, _notifySubscribers: () => { subscribers.forEach((callback) => { try { callback(); } catch (error) { console.error("Error in store subscriber:", error); } }); }, saveBlocks: () => { get()._notifySubscribers(); }, // Selection and hover management setSelectedBlock: (blockId) => { set((state) => { state.selectedBlock = blockId; if (blockId) { state.selectedElement = null; } }); }, setSelectedElement: (elementId) => { set((state) => { state.selectedElement = elementId; }); }, // Block operations addBlock: (newBlock) => { set((state) => { state.blocks.push(newBlock); }); }, removeBlock: (blockId) => { set((state) => { state.blocks = state.blocks.filter((block) => block.blockUuid !== blockId); if (state.selectedBlock === blockId) { state.selectedBlock = null; } }); }, updateBlock: (blockId, updates) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { Object.assign(block, updates); } }); }, clearBlocks: () => { set((state) => { state.blocks = []; state.selectedBlock = null; state.selectedElement = null; }); }, setBlocks: (blocks) => { set((state) => { state.blocks = blocks; }); }, // Block styling and positioning updateBlockStyle: (blockId, newStyle) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { block.style = { ...block.style, ...newStyle }; } }); }, updateBlockSize: (blockId, size) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { block.size = size; } }); }, updateBlockPosition: (blockId, position) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { block.position = position; } }); }, updateBlockPositionType: (blockId, positionType) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { block.positionType = positionType; } }); }, updateBlockZIndex: (blockId, zIndex) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { block.zIndex = zIndex; } }); }, // Element operations addElement: (blockId, type, graphType, dataType?: DataType) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (!block) return; let newElement: UIElement; const commonProps = { elementUuid: MathUtils.generateUUID(), positionType: "relative" as const, position: { x: 0, y: 0 }, zIndex: 1, data: { key: MathUtils.generateUUID(), dataSource: "static" as const, staticValue: "", label: undefined, }, }; switch (type) { case "label-value": newElement = { ...commonProps, type: "label-value", title: "Label Value", dataSource: "", dataValue: "", style: { color: "#ffffff", fontSize: 14, textAlign: "left" as const, display: "flex", flexDirection: "column", gap: "4px", alignItems: "flex-start", justifyContent: "center", labelColor: "#ffffff", valueColor: "#ffffff", }, data: { ...commonProps.data, staticValue: "Value", label: "Label", }, size: { width: 200, height: 60 }, }; break; case "graph": const baseGraphProps = { ...commonProps, type: "graph" as const, graphType: graphType, graphTitle: "Graph Title", style: { width: "100%", height: "100%", minHeight: "120px", color: "#ffffff", fontSize: 14, textAlign: "left" as const, backgroundColor: "rgba(0, 0, 0, 0.2)", borderRadius: "4px", padding: "8px", }, graphData: defaultGraphData, size: { width: 400, height: 200 }, }; if (dataType === "multiple-machine") { newElement = { ...baseGraphProps, dataType: "multiple-machine" as const, title: "Multi Machine Chart", dataSource: [], commonValue: "", }; } else { newElement = { ...baseGraphProps, dataType: "single-machine" as const, title: "Single Machine Chart", dataSource: "", dataValue: [], }; } break; case "text": newElement = { ...commonProps, type: "text", style: { color: "#ffffff", fontSize: 14, textAlign: "left" as const, }, data: { ...commonProps.data, staticValue: "Text", }, size: { width: 200, height: 40 }, }; break; case "icon": newElement = { ...commonProps, type: "icon", style: { color: "#ffffff", fontSize: 14, textAlign: "center" as const, }, size: { width: 40, height: 40 }, }; break; default: return; } block.elements.push(newElement); }); }, removeElement: (blockId, elementId) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { block.elements = block.elements.filter((el) => el.elementUuid !== elementId); if (state.selectedElement === elementId) { state.selectedElement = null; } } }); }, updateElement: (blockId, elementId, updates) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { Object.assign(element, updates); } } }); }, // Element styling and positioning updateElementStyle: (blockId, elementId, newStyle) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.style = { ...element.style, ...newStyle }; } } }); }, updateElementSize: (blockId, elementId, size) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.size = size; } } }); }, updateElementPosition: (blockId, elementId, position) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.position = position; } } }); }, updateElementPositionType: (blockId, elementId, positionType) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.positionType = positionType; } } }); }, updateElementZIndex: (blockId, elementId, zIndex) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.zIndex = zIndex; } } }); }, // Element data operations updateElementData: (blockId, elementId, updates) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element?.data) { element.data = { ...element.data, ...updates }; } } }); }, updateGraphData: (blockId, elementId, newData) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.graphData = newData; } } }); }, updateGraphTitle: (blockId, elementId, title) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.graphTitle = title; } } }); }, updateGraphType: (blockId, elementId, graphType) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element && element.type === "graph") { element.graphType = graphType; element.graphTitle = `${graphType.charAt(0).toUpperCase() + graphType.slice(1)} Chart`; element.graphData = defaultGraphData; } } }); }, // Element swapping swapElements: (blockId, elementId1, elementId2) => { set((state) => { const block = state.blocks.find((b) => b.blockUuid === blockId); if (block) { block.elements = block.elements.map((el) => { if (el.elementUuid === elementId1) { const targetElement = block.elements.find((e) => e.elementUuid === elementId2); return targetElement ? { ...targetElement, elementUuid: elementId1 } : el; } if (el.elementUuid === elementId2) { const sourceElement = block.elements.find((e) => e.elementUuid === elementId1); return sourceElement ? { ...sourceElement, elementUuid: elementId2 } : el; } return el; }); } }); }, // Peek operations - return updated blocks without setting state peekAddBlock: () => { const blocks = cloneBlocks(get().blocks); const newBlock: Block = { blockUuid: MathUtils.generateUUID(), style: { backgroundColor: "rgba(50, 50, 50, 0.8)", backdropFilter: "blur(10px)", padding: 10, borderRadius: 8, border: "1px solid rgba(255, 255, 255, 0.1)", position: "relative", minHeight: "200px", minWidth: "300px", }, elements: [], zIndex: 1, size: { width: 400, height: 300 }, position: { x: 0, y: 0 }, positionType: "relative", }; blocks.push(newBlock); return blocks; }, peekRemoveBlock: (blockId) => { const blocks = cloneBlocks(get().blocks); return blocks.filter((block) => block.blockUuid !== blockId); }, peekUpdateBlock: (blockId, updates) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { Object.assign(block, updates); } return blocks; }, peekUpdateBlockStyle: (blockId, newStyle) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { block.style = { ...block.style, ...newStyle }; } return blocks; }, peekUpdateBlockSize: (blockId, size) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { block.size = size; } return blocks; }, peekUpdateBlockPosition: (blockId, position) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { block.position = position; } return blocks; }, peekUpdateBlockPositionType: (blockId, positionType) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { block.positionType = positionType; } return blocks; }, peekUpdateBlockZIndex: (blockId, zIndex) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { block.zIndex = zIndex; } return blocks; }, peekAddElement: (blockId, type, graphType, dataType?: DataType) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (!block) return blocks; let newElement: UIElement; const commonProps = { elementUuid: MathUtils.generateUUID(), positionType: "relative" as const, position: { x: 0, y: 0 }, zIndex: 1, data: { key: MathUtils.generateUUID(), dataSource: "static" as const, staticValue: "", label: undefined, }, }; switch (type) { case "label-value": newElement = { ...commonProps, type: "label-value", title: "Label Value", dataSource: "", dataValue: "metric-1", style: { color: "#ffffff", fontSize: 14, textAlign: "left" as const, display: "flex", flexDirection: "column", gap: "4px", alignItems: "flex-start", justifyContent: "center", labelColor: "#ffffff", valueColor: "#ffffff", }, data: { ...commonProps.data, staticValue: "Value", label: "Label", }, size: { width: 200, height: 60 }, }; break; case "graph": const baseGraphProps = { ...commonProps, type: "graph" as const, graphType: graphType, graphTitle: "Graph Title", style: { width: "100%", height: "100%", minHeight: "120px", color: "#ffffff", fontSize: 14, textAlign: "left" as const, backgroundColor: "rgba(0, 0, 0, 0.2)", borderRadius: "4px", padding: "8px", }, graphData: defaultGraphData, size: { width: 400, height: 200 }, }; if (dataType === "multiple-machine") { newElement = { ...baseGraphProps, dataType: "multiple-machine" as const, title: "Multi Machine Chart", dataSource: [], commonValue: "", }; } else { newElement = { ...baseGraphProps, dataType: "single-machine" as const, title: "Single Machine Chart", dataSource: "", dataValue: [], }; } break; case "text": newElement = { ...commonProps, type: "text", style: { color: "#ffffff", fontSize: 14, textAlign: "left" as const, }, data: { ...commonProps.data, staticValue: "Text", }, size: { width: 200, height: 40 }, }; break; case "icon": newElement = { ...commonProps, type: "icon", style: { color: "#ffffff", fontSize: 14, textAlign: "center" as const, }, size: { width: 40, height: 40 }, }; break; default: return blocks; } block.elements.push(newElement); return blocks; }, peekRemoveElement: (blockId, elementId) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { block.elements = block.elements.filter((el) => el.elementUuid !== elementId); } return blocks; }, peekUpdateElement: (blockId, elementId, updates) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { Object.assign(element, updates); } } return blocks; }, peekUpdateElementStyle: (blockId, elementId, newStyle) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.style = { ...element.style, ...newStyle }; } } return blocks; }, peekUpdateElementSize: (blockId, elementId, size) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.size = size; } } return blocks; }, peekUpdateElementPosition: (blockId, elementId, position) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.position = position; } } return blocks; }, peekUpdateElementPositionType: (blockId, elementId, positionType) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.positionType = positionType; } } return blocks; }, peekUpdateElementZIndex: (blockId, elementId, zIndex) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.zIndex = zIndex; } } return blocks; }, peekUpdateElementData: (blockId, elementId, updates) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element?.data) { element.data = { ...element.data, ...updates }; } } return blocks; }, peekUpdateGraphData: (blockId, elementId, newData) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.graphData = newData; } } return blocks; }, peekUpdateGraphTitle: (blockId, elementId, title) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element) { element.graphTitle = title; } } return blocks; }, peekUpdateGraphType: (blockId, elementId, graphType) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element && element.type === "graph") { element.graphType = graphType; element.graphTitle = `${graphType.charAt(0).toUpperCase() + graphType.slice(1)} Chart`; element.graphData = defaultGraphData; } } return blocks; }, peekUpdateDataType: (blockId, elementId, dataType) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (!block) return blocks; const index = block.elements.findIndex((el) => el.elementUuid === elementId); if (index === -1) return blocks; const element = block.elements[index]; if (element.type !== "graph" || element.dataType === dataType) return blocks; let newElement: UIElement; if (dataType === "single-machine") { const { commonValue, ...rest } = element as Extract; newElement = { ...rest, dataType: "single-machine", dataSource: "", dataValue: [], }; } else { const { dataValue, ...rest } = element as Extract; newElement = { ...rest, dataType: "multiple-machine", dataSource: [], commonValue: "", }; } block.elements[index] = newElement; return blocks; }, peekUpdateCommonValue: (blockId, elementId, commonValue) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element && element.type === "graph" && element.dataType === "multiple-machine") { element.commonValue = commonValue; } } return blocks; }, peekUpdateDataValue: (blockId, elementId, dataValue) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element && element.type === "graph" && element.dataType === "single-machine") { element.dataValue = dataValue as string[]; } else if (element && element.type === "label-value") { element.dataValue = dataValue as string; } } return blocks; }, peekUpdateDataSource: (blockId, elementId, dataSource) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { const element = block.elements.find((el) => el.elementUuid === elementId); if (element?.type === "label-value") { element.dataValue = ""; } if (element) { element.dataSource = dataSource; } } return blocks; }, peekSwapElements: (blockId, elementId1, elementId2) => { const blocks = cloneBlocks(get().blocks); const block = blocks.find((b) => b.blockUuid === blockId); if (block) { block.elements = block.elements.map((el) => { if (el.elementUuid === elementId1) { const targetElement = block.elements.find((e) => e.elementUuid === elementId2); return targetElement ? { ...targetElement, elementUuid: elementId1 } : el; } if (el.elementUuid === elementId2) { const sourceElement = block.elements.find((e) => e.elementUuid === elementId1); return sourceElement ? { ...sourceElement, elementUuid: elementId2 } : el; } return el; }); } return blocks; }, // Helper functions getBlockById: (blockId) => { return get().blocks.find((b) => b.blockUuid === blockId); }, getElementById: (blockId, elementId) => { const block = get().blocks.find((b) => b.blockUuid === blockId); return block?.elements.find((el) => el.elementUuid === elementId); }, getSelectedBlock: () => { const { selectedBlock, blocks } = get(); return selectedBlock ? blocks.find((b) => b.blockUuid === selectedBlock) : undefined; }, getSelectedElement: () => { const { selectedElement, selectedBlock, blocks } = get(); if (!selectedElement || !selectedBlock) return undefined; const block = blocks.find((b) => b.blockUuid === selectedBlock); return block?.elements.find((el) => el.elementUuid === selectedElement); }, hasBlock: (blockId) => { return get().blocks.some((b) => b.blockUuid === blockId); }, hasElement: (blockId, elementId) => { const block = get().blocks.find((b) => b.blockUuid === blockId); return block?.elements.some((el) => el.elementUuid === elementId) || false; }, })) ); }; export type SimulationDashboardStoreType = ReturnType;