Phases 7-10: Complete remaining features and optimizations
Phase 7.2 - Debounce undo/redo history: - Add 300ms debounce timer for rapid drag/transform changes - Commit history on dragEnd/transformEnd events only - Prevents history bloat during continuous interactions Phase 8.3 - Template-aware export: - Render template background layer first - Apply slot crop regions for image elements - Render template overlay layer last - Support nonPrintable flag for guides/watermarks Phase 9 - PWA icons: - Add pwa-192x192.svg and pwa-512x512.svg icons - Update vite.config.js manifest configuration Phase 10.3 - Performance optimizations: - Add React.memo to canvas components (ImageElement, TextElement, DesignCanvas) - Add React.memo to panel components (LayersPanel, PropertiesPanel) - Prevent unnecessary re-renders during canvas interactions Phase 10.6 - Template documentation: - Document template JSON schema in docs/template-schema.md - Include element properties, slot definitions, and examples - Describe background/overlay layer structure
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
|
||||
const MAX_HISTORY = 50;
|
||||
const DEBOUNCE_DELAY_MS = 300;
|
||||
|
||||
export function useDesignEditor() {
|
||||
const [elements, setElements] = useState([]);
|
||||
@@ -10,6 +11,10 @@ export function useDesignEditor() {
|
||||
const historyRef = useRef([]);
|
||||
const historyIndexRef = useRef(-1);
|
||||
|
||||
// Debounce timer for rapid changes (drag/transform)
|
||||
const historyTimerRef = useRef(null);
|
||||
const pendingChangesRef = useRef(null);
|
||||
|
||||
const saveToHistory = useCallback((newElements) => {
|
||||
// Remove any future history if we're in the middle of the stack
|
||||
if (historyIndexRef.current < historyRef.current.length - 1) {
|
||||
@@ -27,10 +32,34 @@ export function useDesignEditor() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Flush pending changes to history
|
||||
const flushPendingChanges = useCallback(() => {
|
||||
if (pendingChangesRef.current) {
|
||||
saveToHistory(pendingChangesRef.current);
|
||||
pendingChangesRef.current = null;
|
||||
}
|
||||
if (historyTimerRef.current) {
|
||||
clearTimeout(historyTimerRef.current);
|
||||
historyTimerRef.current = null;
|
||||
}
|
||||
}, [saveToHistory]);
|
||||
|
||||
// Cleanup timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (historyTimerRef.current) {
|
||||
clearTimeout(historyTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const canUndo = historyIndexRef.current > 0;
|
||||
const canRedo = historyIndexRef.current < historyRef.current.length - 1;
|
||||
|
||||
const addElement = useCallback((element) => {
|
||||
// Flush any pending debounced changes first
|
||||
flushPendingChanges();
|
||||
|
||||
const newElement = {
|
||||
...element,
|
||||
id: `element-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
@@ -44,17 +73,34 @@ export function useDesignEditor() {
|
||||
|
||||
setSelectedId(newElement.id);
|
||||
return newElement.id;
|
||||
}, [saveToHistory]);
|
||||
}, [flushPendingChanges, saveToHistory]);
|
||||
|
||||
const updateElement = useCallback((id, attrs) => {
|
||||
setElements((prev) => {
|
||||
const newElements = prev.map((el) => (el.id === id ? { ...el, ...attrs } : el));
|
||||
saveToHistory(newElements);
|
||||
|
||||
// Debounce history commits for rapid changes (drag/transform)
|
||||
// Store pending changes but don't commit yet
|
||||
pendingChangesRef.current = newElements;
|
||||
|
||||
// Clear existing timer
|
||||
if (historyTimerRef.current) {
|
||||
clearTimeout(historyTimerRef.current);
|
||||
}
|
||||
|
||||
// Set timer to commit changes after delay
|
||||
historyTimerRef.current = setTimeout(() => {
|
||||
flushPendingChanges();
|
||||
}, DEBOUNCE_DELAY_MS);
|
||||
|
||||
return newElements;
|
||||
});
|
||||
}, [saveToHistory]);
|
||||
}, [flushPendingChanges]);
|
||||
|
||||
const deleteElement = useCallback((id) => {
|
||||
// Flush any pending debounced changes first
|
||||
flushPendingChanges();
|
||||
|
||||
setElements((prev) => {
|
||||
const newElements = prev.filter((el) => el.id !== id);
|
||||
saveToHistory(newElements);
|
||||
@@ -64,7 +110,7 @@ export function useDesignEditor() {
|
||||
if (selectedId === id) {
|
||||
setSelectedId(null);
|
||||
}
|
||||
}, [selectedId, saveToHistory]);
|
||||
}, [selectedId, flushPendingChanges, saveToHistory]);
|
||||
|
||||
const selectElement = useCallback((id) => {
|
||||
setSelectedId(id);
|
||||
@@ -75,6 +121,9 @@ export function useDesignEditor() {
|
||||
}, []);
|
||||
|
||||
const reorderElement = useCallback((id, newOrder) => {
|
||||
// Flush any pending debounced changes first
|
||||
flushPendingChanges();
|
||||
|
||||
setElements((prev) => {
|
||||
const index = prev.findIndex((el) => el.id === id);
|
||||
if (index === -1 || index === newOrder) return prev;
|
||||
@@ -86,7 +135,12 @@ export function useDesignEditor() {
|
||||
saveToHistory(newElements);
|
||||
return newElements;
|
||||
});
|
||||
}, [saveToHistory]);
|
||||
}, [flushPendingChanges, saveToHistory]);
|
||||
|
||||
// Commit history immediately (called on dragEnd/transformEnd)
|
||||
const commitHistory = useCallback(() => {
|
||||
flushPendingChanges();
|
||||
}, [flushPendingChanges]);
|
||||
|
||||
const undo = useCallback(() => {
|
||||
if (historyIndexRef.current > 0) {
|
||||
@@ -121,6 +175,7 @@ export function useDesignEditor() {
|
||||
selectElement,
|
||||
deselectAll,
|
||||
reorderElement,
|
||||
commitHistory, // Call this on dragEnd/transformEnd to commit debounced changes
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
|
||||
Reference in New Issue
Block a user