300 lines
9.6 KiB
TypeScript
300 lines
9.6 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<string, LayerEffectsConfig>;
|
||
|
|
|
||
|
|
// 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<LayerEffect>) => 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<LayerEffect, 'id'> {
|
||
|
|
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<LayerEffect, 'id'>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const useLayerEffectsStore = create<LayerEffectsStore>()(
|
||
|
|
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<LayerEffect>) =>
|
||
|
|
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,
|
||
|
|
};
|
||
|
|
},
|
||
|
|
}
|
||
|
|
)
|
||
|
|
);
|