make lots of stuff atoms and add stateconfig element

This commit is contained in:
Michael Peters 2024-06-27 22:07:46 -07:00
parent 1cd12a6ac2
commit 7229c48a9a
3 changed files with 221 additions and 167 deletions

View File

@ -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<string>(COLORS[0]!);
export const drawModeState = atom<DrawMode>('line');
export const userLinesState = atom<Line[]>([]);
export const userActionsState = atom<Action[]>([]);
export const userRedoActionsState = atom<Action[]>([]);
export const userStartPointState = atom<Point | null>(null);

View File

@ -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 = (
</svg>
);
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<ColorOptionProps> = (props: ColorOptionProps) => {
const { color } = props;
const [lineColor, setLineColor] = useAtom(lineColorState);
@ -76,17 +111,41 @@ const ColorOption: FC<ColorOptionProps> = (props: ColorOptionProps) => {
);
};
const ColorConfig: FC = () => (
<div className="color-options">
{COLORS.map((color) => (
<ColorOption key={color} color={color} />
))}
</div>
);
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 (
<div className="color-options">
{COLORS.map((color) => (
<ColorOption key={color} color={color} />
))}
</div>
);
};
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 = () => (
<div className="grid-config">
<ColorConfig />
<ModeConfig />
</div>
);
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 (
<div className="grid-config">
<ColorConfig />
<ModeConfig />
<StateConfig />
</div>
);
};
export default GridConfig;

View File

@ -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<Line[]>([]);
const [userLines, setUserLines] = useAtom(userLinesState);
const [userActions, setUserActions] = useAtom(userActionsState);
const [userRedoActions, setUserRedoActions] = useAtom(userRedoActionsState);
const [userActions, setUserActions] = useState<Action[]>([]);
const [userRedoActions, setUserRedoActions] = useState<Action[]>([]);
const [userStartPoint, setUserStartPoint] = useState<Point | null>(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) => (