From a258592054140b9fa524208c0bdbcc2d141d00fa Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Sat, 27 Jan 2024 12:40:13 -0800 Subject: [PATCH] undo/redo --- src/components/grid/grid.tsx | 97 ++++++++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 25 deletions(-) diff --git a/src/components/grid/grid.tsx b/src/components/grid/grid.tsx index 3974165..35c2ffb 100644 --- a/src/components/grid/grid.tsx +++ b/src/components/grid/grid.tsx @@ -1,5 +1,12 @@ import { useMouse, useWindowSize } from '@uidotdev/usehooks'; -import { FC, MouseEvent, useCallback, useMemo, useState } from 'react'; +import { + FC, + KeyboardEvent, + MouseEvent, + useCallback, + useMemo, + useState, +} from 'react'; import './grid.scss'; @@ -51,7 +58,6 @@ function useMouseInRange(range: Range): Point { const Grid: FC = () => { const GAP = 20; - // const [offset, setOffset] = useState({ x: GAP / 2, y: GAP / 2 }); const [offset, setOffset] = useState({ x: 10, y: 10 }); const range = useRange(offset); const mousePoint = useMouseInRange(range); @@ -71,8 +77,68 @@ const Grid: FC = () => { }, [range, GAP]); const [userLines, setUserLines] = useState([]); + const [userRedoLines, setUserRedoLines] = useState([]); const [userStartPoint, setUserStartPoint] = useState(null); + // mouse clicks create lines + const onGridClick = useCallback( + (event: MouseEvent) => { + const point = snapToGrid(mousePoint, GAP); + + setUserRedoLines([]); + + if (lodash.isEqual(userStartPoint, point)) return; + if (lodash.isNull(userStartPoint)) { + setUserStartPoint(point); + return; + } + + const line = pointsToRange(userStartPoint, point); + setUserLines([...userLines, line]); + + if (event.shiftKey == false) { + setUserStartPoint(null); + } else { + setUserStartPoint(point); + } + }, + [userStartPoint, setUserStartPoint, setUserLines, GAP, mousePoint] + ); + + const onGridRightClick = useCallback( + (event: MouseEvent) => { + event.preventDefault(); // disable default context menu + + setUserStartPoint(null); + }, + [setUserStartPoint] + ); + + // ctrl+z/ctrl+y for undo/redo + const onUndo = useCallback(() => { + const line = userLines.at(-1); + if (!line) return; + + setUserLines(userLines.slice(0, -1)); + setUserRedoLines([...userRedoLines, line]); + }, [userLines, setUserLines, userRedoLines, setUserRedoLines]); + + const onRedo = useCallback(() => { + const line = userRedoLines.at(-1); + if (!line) return; + + setUserRedoLines(userRedoLines.slice(0, -1)); + setUserLines([...userLines, line]); + }, [userLines, setUserLines, userRedoLines, setUserRedoLines]); + + const onGridKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key == 'z' && event.ctrlKey) onUndo(); + if (event.key == 'y' && event.ctrlKey) onRedo(); + }, + [onUndo, onRedo] + ); + const gridLineElements = useMemo(() => { return gridLines.map((line, idx) => ( { )); }, [userLines]); - const onGridClick = useCallback( - (event: MouseEvent) => { - const point = snapToGrid(mousePoint, GAP); - - if (lodash.isEqual(userStartPoint, point)) return; - if (lodash.isNull(userStartPoint)) { - setUserStartPoint(point); - return; - } - - const line = pointsToRange(userStartPoint, point); - setUserLines([...userLines, line]); - - if (event.shiftKey == false) { - setUserStartPoint(null); - } else { - setUserStartPoint(point); - } - }, - [userStartPoint, setUserStartPoint, setUserLines, GAP, mousePoint] - ); - return ( { viewBox={`${range.x0} ${range.y0} ${range.x1 - range.x0} ${ range.y1 - range.y0 }`} - onClick={onGridClick} + tabIndex={0} // to support onKeyDown event + onMouseDown={onGridClick} + onContextMenu={onGridRightClick} + onKeyDown={onGridKeyDown} > {gridLineElements} {userLineElements}