mirror of
https://github.com/exogen/t2-model-skinner.git
synced 2026-04-20 20:05:26 +00:00
Add basic undo/redo support
This commit is contained in:
parent
fa71b0c0c9
commit
f7ee868969
18 changed files with 145 additions and 16 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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();
|
||||
2
docs/_next/static/chunks/31664189-b73d5a5994fa8d3b.js
Normal file
2
docs/_next/static/chunks/31664189-b73d5a5994fa8d3b.js
Normal 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
|
|
@ -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
2
docs/_next/static/chunks/pages/index-ae68f54c010dce90.js
Normal file
2
docs/_next/static/chunks/pages/index-ae68f54c010dce90.js
Normal file
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
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue