Add basic undo/redo support

This commit is contained in:
Brian Beck 2022-12-04 16:31:38 -08:00
parent fa71b0c0c9
commit f7ee868969
18 changed files with 145 additions and 16 deletions

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
self.__BUILD_MANIFEST={__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":["static/chunks/78e521c3-312593b4f3190cc4.js","static/chunks/95b64a6e-6f3d919198a9be32.js","static/chunks/31664189-c9b38f80aa85a6f1.js","static/chunks/545f34e4-f96cf9e26b6b92a5.js","static/chunks/1bfc9850-6b316c8ef06e5170.js","static/chunks/d7eeaac4-06e64d251e2cbda7.js","static/chunks/f580fadb-a8e2c6896615a304.js","static/chunks/50-cece886aa17c1d5e.js","static/chunks/pages/index-97f296bdf7ac0e30.js"],"/_error":["static/chunks/pages/_error-479484f6c157e921.js"],sortedPages:["/","/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
self.__BUILD_MANIFEST={__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":["static/chunks/78e521c3-312593b4f3190cc4.js","static/chunks/95b64a6e-6f3d919198a9be32.js","static/chunks/31664189-b73d5a5994fa8d3b.js","static/chunks/545f34e4-f96cf9e26b6b92a5.js","static/chunks/1bfc9850-6b316c8ef06e5170.js","static/chunks/d7eeaac4-06e64d251e2cbda7.js","static/chunks/f580fadb-a8e2c6896615a304.js","static/chunks/50-cece886aa17c1d5e.js","static/chunks/pages/index-ae68f54c010dce90.js"],"/_error":["static/chunks/pages/_error-479484f6c157e921.js"],sortedPages:["/","/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

View file

@ -0,0 +1,2 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[609],{3990:function(t,n,c){c.d(n,{UIL:function(){return v},rks:function(){return a},yAv:function(){return e}});var r=c(8357);function v(t){return(0,r.w_)({tag:"svg",attr:{version:"1.1",viewBox:"0 0 16 16"},child:[{tag:"path",attr:{d:"M11.904 16c1.777-3.219 2.076-8.13-4.904-7.966v3.966l-6-6 6-6v3.881c8.359-0.218 9.29 7.378 4.904 12.119z"}}]})(t)}function a(t){return(0,r.w_)({tag:"svg",attr:{version:"1.1",viewBox:"0 0 16 16"},child:[{tag:"path",attr:{d:"M9 3.881v-3.881l6 6-6 6v-3.966c-6.98-0.164-6.681 4.747-4.904 7.966-4.386-4.741-3.455-12.337 4.904-12.119z"}}]})(t)}function e(t){return(0,r.w_)({tag:"svg",attr:{version:"1.1",viewBox:"0 0 16 16"},child:[{tag:"path",attr:{d:"M15.5 6h-5.5v-5.5c0-0.276-0.224-0.5-0.5-0.5h-3c-0.276 0-0.5 0.224-0.5 0.5v5.5h-5.5c-0.276 0-0.5 0.224-0.5 0.5v3c0 0.276 0.224 0.5 0.5 0.5h5.5v5.5c0 0.276 0.224 0.5 0.5 0.5h3c0.276 0 0.5-0.224 0.5-0.5v-5.5h5.5c0.276 0 0.5-0.224 0.5-0.5v-3c0-0.276-0.224-0.5-0.5-0.5z"}}]})(t)}}}]);
//# sourceMappingURL=31664189-b73d5a5994fa8d3b.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[609],{3990:function(c,t,h){h.d(t,{yAv:function(){return v}});var n=h(8357);function v(c){return(0,n.w_)({tag:"svg",attr:{version:"1.1",viewBox:"0 0 16 16"},child:[{tag:"path",attr:{d:"M15.5 6h-5.5v-5.5c0-0.276-0.224-0.5-0.5-0.5h-3c-0.276 0-0.5 0.224-0.5 0.5v5.5h-5.5c-0.276 0-0.5 0.224-0.5 0.5v3c0 0.276 0.224 0.5 0.5 0.5h5.5v5.5c0 0.276 0.224 0.5 0.5 0.5h3c0.276 0 0.5-0.224 0.5-0.5v-5.5h5.5c0.276 0 0.5-0.224 0.5-0.5v-3c0-0.276-0.224-0.5-0.5-0.5z"}}]})(c)}}}]);
//# sourceMappingURL=31664189-c9b38f80aa85a6f1.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,8 @@ import useTools from "./useTools";
import { fabric } from "fabric";
import { createFabricImage } from "./fabricUtils";
type JSONSnapshot = ReturnType<typeof Canvas.prototype["toJSON"]>;
function updateObjectControlOptions() {
fabric.Object.prototype.set({
transparentCorners: false,
@ -41,6 +43,12 @@ export default function Canvas({
const { registerCanvas, unregisterCanvas } = useCanvas();
const [isDrawingMode, setDrawingMode] = useState(defaultDrawingMode);
const handleChangeRef = useRef<CanvasProps["onChange"]>();
const trackChanges = useRef(true);
const [undoHistory, setUndoHistory] = useState<JSONSnapshot[]>(() => []);
const [redoHistory, setRedoHistory] = useState<JSONSnapshot[]>(() => []);
const canUndo = undoHistory.length > 1;
const canRedo = redoHistory.length > 0;
const handleChange: CanvasProps["onChange"] = useCallback((canvas) => {
const handleChange = handleChangeRef.current;
@ -49,6 +57,49 @@ export default function Canvas({
}
}, []);
const undo = useCallback(async () => {
if (!canvas) {
return;
}
if (undoHistory.length > 1) {
const [restoreState, currentState] = undoHistory.slice(-2);
trackChanges.current = false;
canvas.renderOnAddRemove = false;
canvas.clear();
canvas.loadFromJSON(restoreState, () => {
canvas.renderAll();
trackChanges.current = true;
canvas.renderOnAddRemove = true;
});
setUndoHistory((undoHistory) => undoHistory.slice(0, -1));
setRedoHistory((redoHistory) => [currentState, ...redoHistory]);
}
}, [canvas, undoHistory]);
useEffect(() => {
console.log("undo:", undoHistory);
console.log("redo:", redoHistory);
}, [undoHistory, redoHistory]);
const redo = useCallback(() => {
if (!canvas) {
return;
}
if (redoHistory.length > 0) {
const nextState = redoHistory[0];
trackChanges.current = false;
canvas.renderOnAddRemove = false;
canvas.clear();
canvas.loadFromJSON(nextState, () => {
canvas.renderAll();
trackChanges.current = true;
canvas.renderOnAddRemove = true;
});
setUndoHistory((undoHistory) => [...undoHistory, nextState]);
setRedoHistory((redoHistory) => redoHistory.slice(1));
}
}, [canvas, redoHistory]);
useEffect(() => {
handleChangeRef.current = onChange;
}, [onChange]);
@ -59,23 +110,49 @@ export default function Canvas({
const options = {
preserveObjectStacking: true,
targetFindTolerance: 2,
// imageSmoothingEnabled: false,
};
updateObjectControlOptions();
const canvas = new fabric.Canvas(canvasElementRef.current, options);
let isSnapshotting = false;
let changeTimer: ReturnType<typeof setTimeout>;
const handleChangeWithCanvasArg = () => {
handleChange(canvas);
};
const handleRender = () => {
if (isSnapshotting) {
return;
}
if (!trackChanges.current) {
return;
}
clearTimeout(changeTimer);
changeTimer = setTimeout(() => {
const snapshot = snapshotCanvas();
setUndoHistory((history) => [...history.slice(-2), snapshot]);
setRedoHistory([]);
}, 150);
};
const snapshotCanvas = () => {
isSnapshotting = true;
const snapshot = canvas.toJSON();
isSnapshotting = false;
return snapshot;
};
canvas.on("object:modified", handleChangeWithCanvasArg);
canvas.on("object:added", handleChangeWithCanvasArg);
canvas.on("object:removed", handleChangeWithCanvasArg);
canvas.on("after:render", handleRender);
setCanvas(canvas);
return () => {
clearTimeout(changeTimer);
setCanvas(null);
canvas.dispose();
};
@ -101,6 +178,10 @@ export default function Canvas({
canvas.renderAll();
handleChange(canvas);
},
undo,
redo,
canUndo,
canRedo,
isDrawingMode,
setDrawingMode,
});
@ -116,10 +197,15 @@ export default function Canvas({
handleChange,
isDrawingMode,
setDrawingMode,
undo,
redo,
canUndo,
canRedo,
]);
useEffect(() => {
if (canvas && textureSize) {
trackChanges.current = false;
canvas.clear();
if (baseImageUrl) {
let stale = false;
@ -151,6 +237,8 @@ export default function Canvas({
canvas.centerObject(image);
canvas.add(image);
}
trackChanges.current = true;
canvas.requestRenderAll();
};
addImage();

View file

@ -15,6 +15,8 @@ export default function CanvasInteractions({
duplicate,
deleteSelection,
addImages,
undo,
redo,
} = useTools();
const { canvas, notifyChange, setDrawingMode } = useCanvas(activeCanvas);
@ -78,11 +80,11 @@ export default function CanvasInteractions({
return;
} else if (event.shiftKey) {
event.preventDefault();
// redo();
redo();
return;
} else {
event.preventDefault();
// undo();
undo();
return;
}
case "y":
@ -90,7 +92,7 @@ export default function CanvasInteractions({
return;
} else {
event.preventDefault();
// redo();
redo();
return;
}
}

View file

@ -8,7 +8,7 @@ import { FaTrashAlt, FaLock, FaUnlock } from "react-icons/fa";
import { GoArrowUp, GoArrowDown } from "react-icons/go";
import { GiArrowCursor } from "react-icons/gi";
import { IoMdBrush } from "react-icons/io";
import { ImPlus } from "react-icons/im";
import { ImPlus, ImUndo2, ImRedo2 } from "react-icons/im";
export default function CanvasTools() {
const nameInputRef = useRef<HTMLInputElement | null>(null);
@ -26,6 +26,10 @@ export default function CanvasTools() {
sendBackward,
duplicate,
deleteSelection,
undo,
redo,
canUndo,
canRedo,
brushColor,
setBrushColor,
brushSize,
@ -194,6 +198,24 @@ export default function CanvasTools() {
>
<FaTrashAlt />
</button>
<button
type="button"
aria-label="Undo"
title="Undo (Ctrl Z)"
onClick={undo}
disabled={!canUndo}
>
<ImUndo2 />
</button>
<button
type="button"
aria-label="Redo"
title="Redo (Ctrl Y)"
onClick={redo}
disabled={!canRedo}
>
<ImRedo2 />
</button>
</>
) : null}

View file

@ -72,7 +72,8 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
: null;
const metallicCanvasId = materialDef ? `${materialDef.name}:metallic` : null;
const { canvases } = useCanvas();
const { canvas, notifyChange } = useCanvas(activeCanvas);
const { canvas, notifyChange, undo, redo, canUndo, canRedo } =
useCanvas(activeCanvas);
const { canvas: metallicCanvas } = useCanvas(metallicCanvasId);
const [isDrawingMode, setDrawingMode] = useState(false);
const { combineColorAndAlphaImageUrls } = useImageWorker();
@ -307,6 +308,10 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
addImages,
duplicate,
deleteSelection,
undo,
redo,
canUndo,
canRedo,
exportSkin,
isDrawingMode,
setDrawingMode,
@ -330,6 +335,10 @@ export default function ToolsProvider({ children }: { children: ReactNode }) {
addImages,
duplicate,
deleteSelection,
undo,
redo,
canUndo,
canRedo,
exportSkin,
isDrawingMode,
selectedMaterialIndex,

View file

@ -6,6 +6,10 @@ export interface CanvasInfo {
notifyChange: () => void;
isDrawingMode: boolean;
setDrawingMode: (isDrawingMode: boolean) => void;
undo: () => void;
redo: () => void;
canUndo: boolean;
canRedo: boolean;
}
interface CanvasContextValue {

View file

@ -11,6 +11,10 @@ interface ToolsContextValue {
brushColor: number;
setBrushColor: (brushColor: number) => void;
deleteSelection: () => void;
undo: () => void;
redo: () => void;
canUndo: boolean;
canRedo: boolean;
addImages: (imageUrls: string[]) => void;
duplicate: () => void;
sendBackward: () => void;