undo/redo
This commit is contained in:
parent
8e6abb95db
commit
a258592054
@ -1,5 +1,12 @@
|
|||||||
import { useMouse, useWindowSize } from '@uidotdev/usehooks';
|
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';
|
import './grid.scss';
|
||||||
|
|
||||||
@ -51,7 +58,6 @@ function useMouseInRange(range: Range): Point {
|
|||||||
const Grid: FC = () => {
|
const Grid: FC = () => {
|
||||||
const GAP = 20;
|
const GAP = 20;
|
||||||
|
|
||||||
// const [offset, setOffset] = useState<Point>({ x: GAP / 2, y: GAP / 2 });
|
|
||||||
const [offset, setOffset] = useState<Point>({ x: 10, y: 10 });
|
const [offset, setOffset] = useState<Point>({ x: 10, y: 10 });
|
||||||
const range = useRange(offset);
|
const range = useRange(offset);
|
||||||
const mousePoint = useMouseInRange(range);
|
const mousePoint = useMouseInRange(range);
|
||||||
@ -71,8 +77,68 @@ const Grid: FC = () => {
|
|||||||
}, [range, GAP]);
|
}, [range, GAP]);
|
||||||
|
|
||||||
const [userLines, setUserLines] = useState<Range[]>([]);
|
const [userLines, setUserLines] = useState<Range[]>([]);
|
||||||
|
const [userRedoLines, setUserRedoLines] = useState<Range[]>([]);
|
||||||
const [userStartPoint, setUserStartPoint] = useState<Point | null>(null);
|
const [userStartPoint, setUserStartPoint] = useState<Point | null>(null);
|
||||||
|
|
||||||
|
// mouse clicks create lines
|
||||||
|
const onGridClick = useCallback(
|
||||||
|
(event: MouseEvent<SVGSVGElement>) => {
|
||||||
|
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<SVGSVGElement>) => {
|
||||||
|
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<SVGSVGElement>) => {
|
||||||
|
if (event.key == 'z' && event.ctrlKey) onUndo();
|
||||||
|
if (event.key == 'y' && event.ctrlKey) onRedo();
|
||||||
|
},
|
||||||
|
[onUndo, onRedo]
|
||||||
|
);
|
||||||
|
|
||||||
const gridLineElements = useMemo(() => {
|
const gridLineElements = useMemo(() => {
|
||||||
return gridLines.map((line, idx) => (
|
return gridLines.map((line, idx) => (
|
||||||
<line
|
<line
|
||||||
@ -130,28 +196,6 @@ const Grid: FC = () => {
|
|||||||
));
|
));
|
||||||
}, [userLines]);
|
}, [userLines]);
|
||||||
|
|
||||||
const onGridClick = useCallback(
|
|
||||||
(event: MouseEvent<SVGSVGElement>) => {
|
|
||||||
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 (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className="grid"
|
className="grid"
|
||||||
@ -159,7 +203,10 @@ const Grid: FC = () => {
|
|||||||
viewBox={`${range.x0} ${range.y0} ${range.x1 - range.x0} ${
|
viewBox={`${range.x0} ${range.y0} ${range.x1 - range.x0} ${
|
||||||
range.y1 - range.y0
|
range.y1 - range.y0
|
||||||
}`}
|
}`}
|
||||||
onClick={onGridClick}
|
tabIndex={0} // to support onKeyDown event
|
||||||
|
onMouseDown={onGridClick}
|
||||||
|
onContextMenu={onGridRightClick}
|
||||||
|
onKeyDown={onGridKeyDown}
|
||||||
>
|
>
|
||||||
{gridLineElements}
|
{gridLineElements}
|
||||||
{userLineElements}
|
{userLineElements}
|
||||||
|
Loading…
Reference in New Issue
Block a user