diff --git a/src/atoms.ts b/src/atoms.ts index 7aaf31d..46a3494 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -13,6 +13,53 @@ export const COLORS = [ type DrawMode = 'line' | 'trash' | 'grid'; +export interface Point { + x: number; + y: number; +} + +export interface Line { + x0: number; + y0: number; + x1: number; + y1: number; + stroke: string; +} + +export interface AddLineAction { + line: Line; +} + +export interface AddLinesAction { + addLines: Line[]; +} + +export interface DeleteLinesAction { + deleteLines: Line[]; +} + +export type Action = AddLineAction | AddLinesAction | DeleteLinesAction; + +export function isAddLineAction(action: Action): action is AddLineAction { + return 'line' in action; +} + +export function isAddLinesAction(action: Action): action is AddLinesAction { + return 'addLines' in action; +} + +export function isDeleteLinesAction( + action: Action +): action is DeleteLinesAction { + return 'deleteLines' in action; +} + export const lineColorState = atom(COLORS[0]!); export const drawModeState = atom('line'); + +export const userLinesState = atom([]); +export const userActionsState = atom([]); +export const userRedoActionsState = atom([]); + +export const userStartPointState = atom(null); diff --git a/src/components/grid/grid-config.tsx b/src/components/grid/grid-config.tsx index 3b3c6b9..7196fb6 100644 --- a/src/components/grid/grid-config.tsx +++ b/src/components/grid/grid-config.tsx @@ -1,6 +1,21 @@ -import { ChangeEvent, FC, useCallback } from 'react'; +import { ChangeEvent, FC, useCallback, useEffect } from 'react'; +import * as lodash from 'lodash'; import { PrimitiveAtom, useAtom } from 'jotai'; -import { COLORS, drawModeState, lineColorState } from '../../atoms'; +import { + Action, + COLORS, + drawModeState, + isAddLineAction, + isAddLinesAction, + isDeleteLinesAction, + Line, + lineColorState, + Point, + userActionsState, + userLinesState, + userRedoActionsState, + userStartPointState, +} from '../../atoms'; import './grid-config.scss'; @@ -51,6 +66,26 @@ const GridIcon = ( ); +function useKeyPress( + callback: (event: KeyboardEvent) => void, + key: string, + ctrlKey: boolean = false +) { + useEffect(() => { + const listener = (eventRaw: Event) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const event = eventRaw as any as KeyboardEvent; + if (event.key === key && event.ctrlKey === ctrlKey) { + callback(event); + } + }; + window.addEventListener('keydown', listener); + return () => { + window.removeEventListener('keydown', listener); + }; + }, [callback, key, ctrlKey]); +} + const ColorOption: FC = (props: ColorOptionProps) => { const { color } = props; const [lineColor, setLineColor] = useAtom(lineColorState); @@ -76,17 +111,41 @@ const ColorOption: FC = (props: ColorOptionProps) => { ); }; -const ColorConfig: FC = () => ( -
- {COLORS.map((color) => ( - - ))} -
-); +const ColorConfig: FC = () => { + const [userLineColor, setUserLineColor] = useAtom(lineColorState); + const [userDrawMode, setUserDrawMode] = useAtom(drawModeState); + + const setUserColorBind = useCallback( + (color: string) => { + setUserLineColor(color); + setUserDrawMode('line'); + }, + [setUserLineColor, setUserDrawMode] + ); + + useKeyPress(() => setUserColorBind(COLORS[0]!), '1'); + useKeyPress(() => setUserColorBind(COLORS[1]!), '2'); + useKeyPress(() => setUserColorBind(COLORS[2]!), '3'); + useKeyPress(() => setUserColorBind(COLORS[3]!), '4'); + useKeyPress(() => setUserColorBind(COLORS[4]!), '5'); + useKeyPress(() => setUserColorBind(COLORS[5]!), '6'); + useKeyPress(() => setUserColorBind(COLORS[6]!), '7'); + useKeyPress(() => setUserColorBind(COLORS[7]!), '8'); + + return ( +
+ {COLORS.map((color) => ( + + ))} +
+ ); +}; const ModeConfig: FC = () => { const [drawMode, setDrawMode] = useAtom(drawModeState); + useKeyPress(() => setDrawMode('trash'), 't'); + const lineClassName = drawMode === 'line' ? 'option active' : 'option'; const trashClassName = drawMode === 'trash' ? 'option active' : 'option'; const gridClassName = drawMode === 'grid' ? 'option active' : 'option'; @@ -116,11 +175,92 @@ const ModeConfig: FC = () => { ); }; -const GridConfig: FC = () => ( -
- - -
-); +const StateConfig: FC = () => { + const [userLines, setUserLines] = useAtom(userLinesState); + const [userActions, setUserActions] = useAtom(userActionsState); + const [userRedoActions, setUserRedoActions] = useAtom(userRedoActionsState); + + const [userStartPoint, setUserStartPoint] = useAtom(userStartPointState); + + // ctrl+z/ctrl+y for undo/redo + const onUndo = useCallback(() => { + if (!lodash.isNull(userStartPoint)) { + // if the user has a start point, just cancel the pending line + setUserStartPoint(null); + return; + } + + const action = userActions[userActions.length - 1]; + if (!action) return; + + if (isAddLineAction(action)) { + // remove action.line + setUserLines( + userLines.filter((userLine) => userLine !== action.line) + ); + } else if (isAddLinesAction(action)) { + // remove all of action.addLines + setUserLines( + userLines.filter( + (userLine) => + !action.addLines.find( + (actionLine) => actionLine === userLine + ) + ) + ); + } else if (isDeleteLinesAction(action)) { + // add back all of action.deleteLines + // NOTE: this does not preserve layering + setUserLines([...userLines, ...action.deleteLines]); + } else { + console.error('invalid action'); + } + setUserActions(userActions.slice(0, -1)); + setUserRedoActions([...userRedoActions, action]); + }, [userLines, userActions, userRedoActions, userStartPoint]); + + const onRedo = useCallback(() => { + const action = userRedoActions[userRedoActions.length - 1]; + if (!action) return; + + if (isAddLineAction(action)) { + // add back action.line + setUserLines([...userLines, action.line]); + } else if (isAddLinesAction(action)) { + // add back all of action.addLines + // NOTE: this does not preserve layering + setUserLines([...userLines, ...action.addLines]); + } else if (isDeleteLinesAction(action)) { + // remove all of action.delteLines + setUserLines( + userLines.filter( + (userLine) => + !action.deleteLines.find( + (actionLine) => userLine === actionLine + ) + ) + ); + } else { + throw new Error('invalid action'); + } + setUserRedoActions(userRedoActions.slice(0, -1)); + setUserActions([...userActions, action]); + }, [userLines, userActions, userRedoActions]); + + useKeyPress(onUndo, 'z', true); + useKeyPress(onRedo, 'y', true); + + return null; +}; + +const GridConfig: FC = () => { + return ( +
+ + + +
+ ); +}; export default GridConfig; diff --git a/src/components/grid/grid.tsx b/src/components/grid/grid.tsx index 0d5ab7d..f869d8d 100644 --- a/src/components/grid/grid.tsx +++ b/src/components/grid/grid.tsx @@ -12,14 +12,23 @@ import { import './grid.scss'; import * as lodash from 'lodash'; -import { COLORS, drawModeState, lineColorState } from '../../atoms'; +import { + COLORS, + drawModeState, + lineColorState, + Line, + Point, + Action, + AddLineAction, + AddLinesAction, + DeleteLinesAction, + userLinesState, + userActionsState, + userRedoActionsState, + userStartPointState, +} from '../../atoms'; import { useAtom } from 'jotai'; -interface Point { - x: number; - y: number; -} - interface Range { x0: number; y0: number; @@ -27,40 +36,6 @@ interface Range { y1: number; } -interface Line { - x0: number; - y0: number; - x1: number; - y1: number; - stroke: string; -} - -interface AddLineAction { - line: Line; -} - -interface AddLinesAction { - addLines: Line[]; -} - -interface DeleteLinesAction { - deleteLines: Line[]; -} - -type Action = AddLineAction | AddLinesAction | DeleteLinesAction; - -function isAddLineAction(action: Action): action is AddLineAction { - return 'line' in action; -} - -function isAddLinesAction(action: Action): action is AddLinesAction { - return 'addLines' in action; -} - -function isDeleteLinesAction(action: Action): action is DeleteLinesAction { - return 'deleteLines' in action; -} - // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection // https://stackoverflow.com/questions/9043805/test-if-two-lines-intersect-javascript-function function lineSegmentIntersection(l0: Range, l1: Range): boolean { @@ -141,26 +116,6 @@ function useMouseInRange(range: Range): Point { return { x: mouse.x + range.x0, y: mouse.y + range.y0 }; } -function useKeyPress( - callback: (event: KeyboardEvent) => void, - key: string, - ctrlKey: boolean = false -) { - useEffect(() => { - const listener = (eventRaw: Event) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const event = eventRaw as any as KeyboardEvent; - if (event.key === key && event.ctrlKey === ctrlKey) { - callback(event); - } - }; - window.addEventListener('keydown', listener); - return () => { - window.removeEventListener('keydown', listener); - }; - }, [callback, key, ctrlKey]); -} - const THICK_COLOR = '#313d49'; const THIN_COLOR = '#1e2226'; @@ -200,12 +155,11 @@ const Grid: FC = () => { return lines_thin.concat(lines_thick); }, [range, GAP]); - const [userLines, setUserLines] = useState([]); + const [userLines, setUserLines] = useAtom(userLinesState); + const [userActions, setUserActions] = useAtom(userActionsState); + const [userRedoActions, setUserRedoActions] = useAtom(userRedoActionsState); - const [userActions, setUserActions] = useState([]); - const [userRedoActions, setUserRedoActions] = useState([]); - - const [userStartPoint, setUserStartPoint] = useState(null); + const [userStartPoint, setUserStartPoint] = useAtom(userStartPointState); const [userLineColor, setUserLineColor] = useAtom(lineColorState); const [userDrawMode, setUserDrawMode] = useAtom(drawModeState); @@ -309,93 +263,6 @@ const Grid: FC = () => { [] ); - // ctrl+z/ctrl+y for undo/redo - const onUndo = useCallback(() => { - if (!lodash.isNull(userStartPoint)) { - // if the user has a start point, just cancel the pending line - setUserStartPoint(null); - return; - } - - const action = userActions[userActions.length - 1]; - if (!action) return; - - if (isAddLineAction(action)) { - // remove action.line - setUserLines( - userLines.filter((userLine) => userLine !== action.line) - ); - } else if (isAddLinesAction(action)) { - // remove all of action.addLines - setUserLines( - userLines.filter( - (userLine) => - !action.addLines.find( - (actionLine) => actionLine === userLine - ) - ) - ); - } else if (isDeleteLinesAction(action)) { - // add back all of action.deleteLines - // NOTE: this does not preserve layering - setUserLines([...userLines, ...action.deleteLines]); - } else { - console.error('invalid action'); - } - setUserActions(userActions.slice(0, -1)); - setUserRedoActions([...userRedoActions, action]); - }, [userLines, userActions, userRedoActions, userStartPoint]); - - const onRedo = useCallback(() => { - const action = userRedoActions[userRedoActions.length - 1]; - if (!action) return; - - if (isAddLineAction(action)) { - // add back action.line - setUserLines([...userLines, action.line]); - } else if (isAddLinesAction(action)) { - // add back all of action.addLines - // NOTE: this does not preserve layering - setUserLines([...userLines, ...action.addLines]); - } else if (isDeleteLinesAction(action)) { - // remove all of action.delteLines - setUserLines( - userLines.filter( - (userLine) => - !action.deleteLines.find( - (actionLine) => userLine === actionLine - ) - ) - ); - } else { - throw new Error('invalid action'); - } - setUserRedoActions(userRedoActions.slice(0, -1)); - setUserActions([...userActions, action]); - }, [userLines, userActions, userRedoActions]); - - useKeyPress(onUndo, 'z', true); - useKeyPress(onRedo, 'y', true); - - const setUserColorBind = useCallback( - (color: string) => { - setUserLineColor(color); - setUserDrawMode('line'); - }, - [setUserLineColor, setUserDrawMode] - ); - - useKeyPress(() => setUserColorBind(COLORS[0]!), '1'); - useKeyPress(() => setUserColorBind(COLORS[1]!), '2'); - useKeyPress(() => setUserColorBind(COLORS[2]!), '3'); - useKeyPress(() => setUserColorBind(COLORS[3]!), '4'); - useKeyPress(() => setUserColorBind(COLORS[4]!), '5'); - useKeyPress(() => setUserColorBind(COLORS[5]!), '6'); - useKeyPress(() => setUserColorBind(COLORS[6]!), '7'); - useKeyPress(() => setUserColorBind(COLORS[7]!), '8'); - - useKeyPress(() => setUserDrawMode('trash'), 't'); - const gridLineElements = useMemo( () => gridLines.map((line, idx) => (