/** * Layer Effects Store * Manages non-destructive visual effects for layers */ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { LayerEffect, LayerEffectsConfig, EffectType, } from '@/types/layer-effects'; import { DEFAULT_DROP_SHADOW, DEFAULT_OUTER_GLOW, DEFAULT_STROKE, DEFAULT_COLOR_OVERLAY, DEFAULT_INNER_SHADOW, DEFAULT_INNER_GLOW, DEFAULT_BEVEL, } from '@/types/layer-effects'; interface LayerEffectsStore { // Layer effects configurations (one per layer) layerEffects: Map; // Global light settings (shared across effects that use it) globalLightAngle: number; globalLightAltitude: number; // Actions getLayerEffects: (layerId: string) => LayerEffectsConfig | undefined; setLayerEffectsEnabled: (layerId: string, enabled: boolean) => void; addEffect: (layerId: string, effectType: EffectType) => void; removeEffect: (layerId: string, effectId: string) => void; updateEffect: (layerId: string, effectId: string, updates: Partial) => void; toggleEffect: (layerId: string, effectId: string) => void; reorderEffect: (layerId: string, effectId: string, newIndex: number) => void; duplicateEffect: (layerId: string, effectId: string) => void; clearLayerEffects: (layerId: string) => void; setGlobalLight: (angle: number, altitude: number) => void; // Preset management copyEffects: (fromLayerId: string) => LayerEffect[] | null; pasteEffects: (toLayerId: string, effects: LayerEffect[]) => void; } // Helper to create default effect by type function createDefaultEffect(type: EffectType): Omit { const defaults = { dropShadow: DEFAULT_DROP_SHADOW, outerGlow: DEFAULT_OUTER_GLOW, stroke: DEFAULT_STROKE, colorOverlay: DEFAULT_COLOR_OVERLAY, innerShadow: DEFAULT_INNER_SHADOW, innerGlow: DEFAULT_INNER_GLOW, bevel: DEFAULT_BEVEL, // TODO: Add defaults for remaining effect types gradientOverlay: DEFAULT_COLOR_OVERLAY, // Placeholder patternOverlay: DEFAULT_COLOR_OVERLAY, // Placeholder satin: DEFAULT_INNER_SHADOW, // Placeholder }; return defaults[type] as Omit; } export const useLayerEffectsStore = create()( persist( (set, get) => ({ layerEffects: new Map(), globalLightAngle: 120, globalLightAltitude: 30, getLayerEffects: (layerId: string) => { return get().layerEffects.get(layerId); }, setLayerEffectsEnabled: (layerId: string, enabled: boolean) => set((state) => { const newMap = new Map(state.layerEffects); const config = newMap.get(layerId); if (config) { newMap.set(layerId, { ...config, enabled }); } return { layerEffects: newMap }; }), addEffect: (layerId: string, effectType: EffectType) => set((state) => { const newMap = new Map(state.layerEffects); let config = newMap.get(layerId); const newEffect: LayerEffect = { ...createDefaultEffect(effectType), id: `effect-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type: effectType, } as LayerEffect; if (!config) { // Create new config for this layer config = { layerId, effects: [newEffect], enabled: true, globalLightAngle: state.globalLightAngle, globalLightAltitude: state.globalLightAltitude, }; } else { // Add effect to existing config config = { ...config, effects: [...config.effects, newEffect], }; } newMap.set(layerId, config); return { layerEffects: newMap }; }), removeEffect: (layerId: string, effectId: string) => set((state) => { const newMap = new Map(state.layerEffects); const config = newMap.get(layerId); if (config) { const newEffects = config.effects.filter((e) => e.id !== effectId); if (newEffects.length === 0) { // Remove config if no effects left newMap.delete(layerId); } else { newMap.set(layerId, { ...config, effects: newEffects }); } } return { layerEffects: newMap }; }), updateEffect: (layerId: string, effectId: string, updates: Partial) => set((state) => { const newMap = new Map(state.layerEffects); const config = newMap.get(layerId); if (config) { const newEffects = config.effects.map((effect) => effect.id === effectId ? ({ ...effect, ...updates } as LayerEffect) : effect ); newMap.set(layerId, { ...config, effects: newEffects }); } return { layerEffects: newMap }; }), toggleEffect: (layerId: string, effectId: string) => set((state) => { const newMap = new Map(state.layerEffects); const config = newMap.get(layerId); if (config) { const newEffects = config.effects.map((effect) => effect.id === effectId ? { ...effect, enabled: !effect.enabled } : effect ); newMap.set(layerId, { ...config, effects: newEffects }); } return { layerEffects: newMap }; }), reorderEffect: (layerId: string, effectId: string, newIndex: number) => set((state) => { const newMap = new Map(state.layerEffects); const config = newMap.get(layerId); if (config) { const effects = [...config.effects]; const currentIndex = effects.findIndex((e) => e.id === effectId); if (currentIndex !== -1) { const [effect] = effects.splice(currentIndex, 1); effects.splice(newIndex, 0, effect); newMap.set(layerId, { ...config, effects }); } } return { layerEffects: newMap }; }), duplicateEffect: (layerId: string, effectId: string) => set((state) => { const newMap = new Map(state.layerEffects); const config = newMap.get(layerId); if (config) { const effect = config.effects.find((e) => e.id === effectId); if (effect) { const newEffect: LayerEffect = { ...effect, id: `effect-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, }; const effectIndex = config.effects.findIndex((e) => e.id === effectId); const newEffects = [ ...config.effects.slice(0, effectIndex + 1), newEffect, ...config.effects.slice(effectIndex + 1), ]; newMap.set(layerId, { ...config, effects: newEffects }); } } return { layerEffects: newMap }; }), clearLayerEffects: (layerId: string) => set((state) => { const newMap = new Map(state.layerEffects); newMap.delete(layerId); return { layerEffects: newMap }; }), setGlobalLight: (angle: number, altitude: number) => set((state) => { const newMap = new Map(state.layerEffects); // Update global light for all layer configs newMap.forEach((config, layerId) => { newMap.set(layerId, { ...config, globalLightAngle: angle, globalLightAltitude: altitude, }); }); return { globalLightAngle: angle, globalLightAltitude: altitude, layerEffects: newMap, }; }), copyEffects: (fromLayerId: string) => { const config = get().layerEffects.get(fromLayerId); return config ? [...config.effects] : null; }, pasteEffects: (toLayerId: string, effects: LayerEffect[]) => set((state) => { const newMap = new Map(state.layerEffects); let config = newMap.get(toLayerId); // Clone effects with new IDs const newEffects = effects.map((effect) => ({ ...effect, id: `effect-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, })); if (!config) { config = { layerId: toLayerId, effects: newEffects, enabled: true, globalLightAngle: state.globalLightAngle, globalLightAltitude: state.globalLightAltitude, }; } else { config = { ...config, effects: [...config.effects, ...newEffects], }; } newMap.set(toLayerId, config); return { layerEffects: newMap }; }), }), { name: 'layer-effects-storage', partialize: (state) => ({ layerEffects: Array.from(state.layerEffects.entries()), globalLightAngle: state.globalLightAngle, globalLightAltitude: state.globalLightAltitude, }), merge: (persistedState: any, currentState) => { // Convert array back to Map const layerEffects = new Map( (persistedState?.layerEffects || []) as [string, LayerEffectsConfig][] ); return { ...currentState, layerEffects, globalLightAngle: persistedState?.globalLightAngle ?? currentState.globalLightAngle, globalLightAltitude: persistedState?.globalLightAltitude ?? currentState.globalLightAltitude, }; }, } ) );