Files
yvcodesnap/src/store/canvasStore.ts
T

294 lines
7.6 KiB
TypeScript

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { persist } from 'zustand/middleware';
import { v4 as uuidv4 } from 'uuid';
import type {
Snap,
CanvasElement,
CodeElement,
TextElement,
ArrowElement,
Background,
CanvasMeta,
} from '../types';
interface HistoryState {
past: Snap[];
future: Snap[];
}
interface CanvasState {
snap: Snap;
selectedElementId: string | null;
zoom: number;
showGrid: boolean;
tool: 'select' | 'code' | 'text' | 'arrow';
history: HistoryState;
// Actions
setSnap: (snap: Snap) => void;
updateMeta: (meta: Partial<CanvasMeta>) => void;
setBackground: (background: Partial<Background>) => void;
addElement: (element: CanvasElement) => void;
updateElement: (id: string, updates: Partial<CanvasElement>) => void;
deleteElement: (id: string) => void;
duplicateElement: (id: string) => void;
selectElement: (id: string | null) => void;
setZoom: (zoom: number) => void;
setShowGrid: (show: boolean) => void;
setTool: (tool: 'select' | 'code' | 'text' | 'arrow') => void;
moveElementUp: (id: string) => void;
moveElementDown: (id: string) => void;
undo: () => void;
redo: () => void;
saveToHistory: () => void;
newSnap: (meta: CanvasMeta) => void;
exportSnap: () => string;
importSnap: (json: string) => void;
}
const defaultSnap: Snap = {
version: '1.0.0',
meta: {
title: 'Untitled',
aspect: '16:9',
width: 1920,
height: 1080,
},
background: {
type: 'gradient',
solid: { color: '#101022' },
gradient: { from: '#101022', to: '#1f1f3a', angle: 135 },
},
elements: [],
};
export const useCanvasStore = create<CanvasState>()(
persist(
immer((set, get) => ({
snap: defaultSnap,
selectedElementId: null,
zoom: 0.5,
showGrid: false,
tool: 'select',
history: { past: [], future: [] },
setSnap: (snap) => set((state) => {
state.snap = snap;
}),
updateMeta: (meta) => set((state) => {
state.snap.meta = { ...state.snap.meta, ...meta };
}),
setBackground: (background) => set((state) => {
state.snap.background = { ...state.snap.background, ...background };
}),
addElement: (element) => set((state) => {
get().saveToHistory();
state.snap.elements.push(element);
state.selectedElementId = element.id;
state.tool = 'select';
}),
updateElement: (id, updates) => set((state) => {
const index = state.snap.elements.findIndex((el) => el.id === id);
if (index !== -1) {
state.snap.elements[index] = { ...state.snap.elements[index], ...updates } as CanvasElement;
}
}),
deleteElement: (id) => set((state) => {
get().saveToHistory();
state.snap.elements = state.snap.elements.filter((el) => el.id !== id);
if (state.selectedElementId === id) {
state.selectedElementId = null;
}
}),
duplicateElement: (id) => set((state) => {
const element = state.snap.elements.find((el) => el.id === id);
if (element) {
get().saveToHistory();
const newElement = {
...JSON.parse(JSON.stringify(element)),
id: uuidv4(),
x: element.x + 20,
y: element.y + 20,
};
state.snap.elements.push(newElement);
state.selectedElementId = newElement.id;
}
}),
selectElement: (id) => set((state) => {
state.selectedElementId = id;
}),
setZoom: (zoom) => set((state) => {
state.zoom = Math.max(0.25, Math.min(4, zoom));
}),
setShowGrid: (show) => set((state) => {
state.showGrid = show;
}),
setTool: (tool) => set((state) => {
state.tool = tool;
if (tool !== 'select') {
state.selectedElementId = null;
}
}),
moveElementUp: (id) => set((state) => {
const index = state.snap.elements.findIndex((el) => el.id === id);
if (index < state.snap.elements.length - 1) {
const temp = state.snap.elements[index];
state.snap.elements[index] = state.snap.elements[index + 1];
state.snap.elements[index + 1] = temp;
}
}),
moveElementDown: (id) => set((state) => {
const index = state.snap.elements.findIndex((el) => el.id === id);
if (index > 0) {
const temp = state.snap.elements[index];
state.snap.elements[index] = state.snap.elements[index - 1];
state.snap.elements[index - 1] = temp;
}
}),
saveToHistory: () => set((state) => {
state.history.past.push(JSON.parse(JSON.stringify(state.snap)));
state.history.future = [];
if (state.history.past.length > 50) {
state.history.past.shift();
}
}),
undo: () => set((state) => {
if (state.history.past.length > 0) {
const previous = state.history.past.pop()!;
state.history.future.push(JSON.parse(JSON.stringify(state.snap)));
state.snap = previous;
state.selectedElementId = null;
}
}),
redo: () => set((state) => {
if (state.history.future.length > 0) {
const next = state.history.future.pop()!;
state.history.past.push(JSON.parse(JSON.stringify(state.snap)));
state.snap = next;
state.selectedElementId = null;
}
}),
newSnap: (meta) => set((state) => {
state.snap = {
...defaultSnap,
meta: { ...defaultSnap.meta, ...meta },
};
state.selectedElementId = null;
state.history = { past: [], future: [] };
}),
exportSnap: () => {
return JSON.stringify(get().snap, null, 2);
},
importSnap: (json) => {
try {
const snap = JSON.parse(json) as Snap;
set((state) => {
state.snap = snap;
state.selectedElementId = null;
state.history = { past: [], future: [] };
});
} catch (e) {
console.error('Failed to import snap:', e);
}
},
})),
{
name: 'code-canvas-storage',
partialize: (state) => ({ snap: state.snap }),
}
)
);
// Helper functions to create elements
export const createCodeElement = (x: number, y: number): CodeElement => ({
id: uuidv4(),
type: 'code',
x,
y,
width: 600,
height: 300,
rotation: 0,
locked: false,
visible: true,
props: {
code: '// Your code here\nconsole.log("Hello, World!");',
language: 'javascript',
theme: 'github-dark',
fontFamily: 'JetBrains Mono',
fontSize: 14,
lineHeight: 1.5,
lineNumbers: true,
highlights: [],
padding: 24,
cornerRadius: 12,
shadow: { blur: 24, spread: 0, color: 'rgba(0,0,0,0.4)' },
},
});
export const createTextElement = (x: number, y: number): TextElement => ({
id: uuidv4(),
type: 'text',
x,
y,
rotation: 0,
locked: false,
visible: true,
props: {
text: 'Your text here',
fontFamily: 'Inter',
fontSize: 24,
color: '#ffffff',
bold: false,
italic: false,
underline: false,
align: 'left',
background: null,
padding: 8,
cornerRadius: 4,
},
});
export const createArrowElement = (x: number, y: number): ArrowElement => ({
id: uuidv4(),
type: 'arrow',
x: 0,
y: 0,
rotation: 0,
locked: false,
visible: true,
points: [
{ x, y },
{ x: x + 150, y: y + 80 },
],
props: {
style: 'straight',
color: '#60a5fa',
thickness: 3,
head: 'filled',
},
});