Skip to content

Commit

Permalink
feat: add undo and redo
Browse files Browse the repository at this point in the history
  • Loading branch information
bertyhell committed Aug 17, 2024
1 parent 253081c commit 7f71d38
Show file tree
Hide file tree
Showing 21 changed files with 341 additions and 288 deletions.
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
"dependencies": {
"@flatten-js/core": "^1.6.1",
"clsx": "^2.1.1",
"es-toolkit": "^1.16.0",
"file-saver": "^2.0.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"teenyicons": "^0.4.1",
"undo-stacker": "^0.2.1",
"vite-plugin-svgr": "^4.2.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/App.consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const SNAP_POINT_SIZE = 15;
* How long you need to hover over a snap point to make it a marked snap point that will show angle guides
* in milliseconds
*/
export const HOVERED_SNAP_POINT_TIME = 2000;
export const HOVERED_SNAP_POINT_TIME = 1000;

/**
* Maximum number of snap points that can be marked at the same time
Expand Down
140 changes: 0 additions & 140 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,146 +2,6 @@ import './App.scss';
import { Toolbar } from './components/Toolbar.tsx';

function App() {
// const [activeTool, setActiveTool] = useState(Tool.Line);
// const [entities, setEntities] = useState<Entity[]>([]);
// const [getActiveEntity(), setActiveEntity] = useState<Entity | null>(null);
// const [shouldDrawCursor, setShouldDrawCursor] = useState(true);
// const [debugEntities] = useState<Entity[]>([]);
// const [angleStep, setAngleStep] = useState(45);
// const [screenOffset, setScreenOffset] = useState<Point>(new Point(0, 0));
// const [screenScale, setScreenScale] = useState<number>(1);
// const [panStartLocation, setPanStartLocation] = useState<Point | null>(null);

// computed
// const worldMouseLocation = screenToWorld(
// screenMouseLocation,
// screenOffset,
// screenScale,
// );

// /**
// * Entity snap point or intersection
// */
// const [snapPoint, setSnapPoint] = useState<SnapPoint | null>(null);
//
// /**
// * Snap point on angle guide
// */
// const [snapPointOnAngleGuide, setSnapPointOnAngleGuide] =
// useState<SnapPoint | null>(null);
//
// /**
// * Snap points that are hovered for a certain amount of time
// */
// const [hoveredSnapPoints, setHoveredSnapPoints] = useState<HoverPoint[]>([]);

/**
* Keep track of the hovered snap points
*/
// useEffect(() => {
// const watchSnapPointTimerId = setInterval(() => {
// trackHoveredSnapPoint(
// snapPoint,
// hoveredSnapPoints,
// setHoveredSnapPoints,
// SNAP_POINT_DISTANCE / screenScale,
// );
// }, 100);
// return () => {
// clearInterval(watchSnapPointTimerId);
// };
// }, [hoveredSnapPoints, snapPoint]);

// /**
// * Redraw the canvas when the mouse moves or the window resizes
// */
// useEffect(() => {
// const activeTool = getActiveTool();
// const angleStep = getAngleStep();
// const activeEntity = getActiveEntity();
// const entities = getEntities();
// const hoveredSnapPoints = getHoveredSnapPoints();
// const worldMouseLocation = getWorldMouseLocation();
// const screenScale = getScreenScale();
// const screenOffset = getScreenOffset();
// const shouldDrawCursor = getShouldDrawCursor();
// const debugEntities = getDebugEntities();
//
// const context: CanvasRenderingContext2D | null | undefined =
// canvas?.getContext('2d');
// if (!context) return;
//
// const drawInfo: DrawInfo = {
// context,
// canvasSize,
// worldMouseLocation: worldMouseLocation,
// screenMouseLocation: screenMouseLocation,
// screenOffset,
// screenZoom: screenScale,
// };
// let helperEntitiesTemp: Entity[] = [];
// let snapPointTemp: SnapPoint | null = null;
// let snapPointOnAngleGuideTemp: SnapPoint | null = null;
// if ([Tool.Line, Tool.Rectangle, Tool.Circle].includes(activeTool)) {
// // If you're in the progress of drawing a shape, show the angle guides and closest snap point
// let firstPoint: Point | null = null;
// if (
// activeEntity &&
// !activeEntity.getShape() &&
// activeEntity.getFirstPoint()
// ) {
// firstPoint = activeEntity.getFirstPoint();
// }
// const { angleGuides, entitySnapPoint, angleSnapPoint } = getDrawHelpers(
// entities,
// compact([
// firstPoint,
// ...hoveredSnapPoints.map(
// hoveredSnapPoint => hoveredSnapPoint.snapPoint.point,
// ),
// ]),
// worldMouseLocation,
// angleStep,
// SNAP_POINT_DISTANCE / screenScale,
// );
// helperEntitiesTemp = angleGuides;
// snapPointTemp = entitySnapPoint;
// snapPointOnAngleGuideTemp = angleSnapPoint;
// // setHelperEntities(angleGuides);
// // setSnapPoint(entitySnapPoint);
// // setSnapPointOnAngleGuide(angleSnapPoint);
// }
// draw(
// drawInfo,
// entities,
// debugEntities,
// helperEntitiesTemp,
// getActiveEntity(),
// snapPointTemp,
// snapPointOnAngleGuideTemp,
// hoveredSnapPoints,
// worldMouseLocation,
// shouldDrawCursor,
// );
// }, [canvasRef, canvasSize, screenMouseLocation]);

/**
* Show the angle guides and closest snap point when drawing a shape
*/
// useEffect(() => {
//
// }, [
// // getActiveEntity(),
// activeTool,
// angleStep,
// // entities,
// // hoveredSnapPoints,
// // screenMouseLocation,
// // screenOffset,
// // screenScale,
// worldMouseLocation,
// ]);

return (
<div>
<Toolbar />
Expand Down
61 changes: 36 additions & 25 deletions src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
import { FC, useCallback } from 'react';
import { FC, useCallback, useState } from 'react';
import { IconName } from './icon.tsx';
import { Tool } from '../tools.ts';
import { DropdownButton } from './DropdownButton.tsx';
import { Button } from './Button.tsx';
import {
getActiveTool,
getAngleStep,
getCanvasSize,
getEntities,
redo,
setActiveEntity,
setActiveTool,
setAngleStep,
setEntities,
setSelectedEntityIds,
undo,
} from '../state.ts';
import { deSelectEntities } from '../helpers/select-entities.ts';
import { convertEntitiesToSvgString } from '../helpers/export-entities-to-svg.ts';
import { saveAs } from 'file-saver';

interface ToolbarProps {}

export const Toolbar: FC<ToolbarProps> = () => {
const activeTool = getActiveTool();
const angleStep = getAngleStep();
const [activeToolLocal, setActiveToolLocal] = useState<Tool>(Tool.Line);
const [angleStepLocal, setAngleStepLocal] = useState<number>(45);

const handleToolClick = useCallback((tool: Tool) => {
console.log('set active tool: ', tool);
const entities = getEntities();
setActiveToolLocal(tool);
setActiveTool(tool);
setActiveEntity(null);
setEntities(deSelectEntities(entities));
setSelectedEntityIds([]);
}, []);

const handleExportClick = useCallback(() => {
Expand All @@ -41,74 +40,86 @@ export const Toolbar: FC<ToolbarProps> = () => {
saveAs(blob, 'open-web-cad--drawing.svg');
}, []);

const handleAngleChanged = useCallback((angle: number) => {
setAngleStepLocal(angle);
setAngleStep(angle);
}, []);

return (
<div className="controls absolute top-0 left-0 flex flex-col gap-1 m-1">
<Button
title="Select"
icon={IconName.Direction}
onClick={() => handleToolClick(Tool.Select)}
active={activeTool === Tool.Select}
active={activeToolLocal === Tool.Select}
/>
<Button
title="Line"
icon={IconName.Line}
onClick={() => handleToolClick(Tool.Line)}
active={activeTool === Tool.Line}
active={activeToolLocal === Tool.Line}
/>
<Button
title="Rectangle"
icon={IconName.Square}
onClick={() => handleToolClick(Tool.Rectangle)}
active={activeTool === Tool.Rectangle}
active={activeToolLocal === Tool.Rectangle}
/>
<Button
title="Circle"
icon={IconName.Circle}
onClick={() => handleToolClick(Tool.Circle)}
active={activeTool === Tool.Circle}
active={activeToolLocal === Tool.Circle}
/>
<Button
className="mt-2"
title="Undo"
icon={IconName.AntiClockwise}
onClick={() => undo()}
/>
<Button title="Redo" icon={IconName.Clockwise} onClick={() => redo()} />
{/* TODO add delete segments logic */}
{/*<Button*/}
{/* className="mt-2"*/}
{/* title="Delete segments"*/}
{/* icon={IconName.LayersDifference}*/}
{/* onClick={() => handleToolClick(Tool.Eraser)}*/}
{/* active={activeTool === Tool.Eraser}*/}
{/* active={activeToolLocal === Tool.Eraser}*/}
{/*/>*/}
<DropdownButton
className="mt-2"
title="Snap angles"
label={angleStep + '°'}
label={angleStepLocal + '°'}
>
<Button
title="Add guide every 5 degrees"
label="5°"
onClick={() => setAngleStep(5)}
active={angleStep === 5}
onClick={() => handleAngleChanged(5)}
active={angleStepLocal === 5}
/>
<Button
title="Add guide every 15 degrees"
label="15°"
onClick={() => setAngleStep(15)}
active={angleStep === 15}
onClick={() => handleAngleChanged(15)}
active={angleStepLocal === 15}
/>
<Button
title="Add guide every 30 degrees"
label="30°"
onClick={() => setAngleStep(30)}
active={angleStep === 30}
onClick={() => handleAngleChanged(30)}
active={angleStepLocal === 30}
/>
<Button
title="Add guide every 45 degrees"
label="45°"
onClick={() => setAngleStep(45)}
active={angleStep === 45}
onClick={() => handleAngleChanged(45)}
active={angleStepLocal === 45}
/>
<Button
title="Add guide every 90 degrees"
label="90°"
onClick={() => setAngleStep(90)}
active={angleStep === 90}
onClick={() => handleAngleChanged(90)}
active={angleStepLocal === 90}
/>
</DropdownButton>
<Button
Expand Down
6 changes: 6 additions & 0 deletions src/components/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import SolidDownIcon from 'teenyicons/solid/down.svg?react';
import SolidUpIcon from 'teenyicons/solid/up.svg?react';
import SolidUpSmallIcon from 'teenyicons/solid/up-small.svg?react';
import SolidDownSmallIcon from 'teenyicons/solid/down-small.svg?react';
import AntiClockwiseIcon from 'teenyicons/outline/anti-clockwise.svg?react';
import ClockwiseIcon from 'teenyicons/outline/clockwise.svg?react';
import AngleIcon from './angle.svg?react';
import { FC } from 'react';

Expand All @@ -20,6 +22,8 @@ enum IconName {
Direction = 'Direction',
VectorDocument = 'VectorDocument',
LayersDifference = 'LayersDifference',
AntiClockwise = 'AntiClockwise',
Clockwise = 'Clockwise',
Svg = 'Svg',
Angle = 'Angle',
SolidDown = 'SolidDown',
Expand All @@ -35,6 +39,8 @@ const icons: Record<IconName, FC> = {
[IconName.Direction]: DirectionIcon,
[IconName.VectorDocument]: VectorDocumentIcon,
[IconName.LayersDifference]: LayersDifferenceIcon,
[IconName.AntiClockwise]: AntiClockwiseIcon,
[IconName.Clockwise]: ClockwiseIcon,
[IconName.Svg]: SvgIcon,
[IconName.Angle]: AngleIcon,
[IconName.SolidDown]: SolidDownIcon,
Expand Down
3 changes: 1 addition & 2 deletions src/entities/CircleEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import { Box, circle, Circle, point, Point, Segment } from '@flatten-js/core';
import { worldToScreen } from '../helpers/world-screen-conversion.ts';

export class CircleEntity implements Entity {
public readonly id: string = crypto.randomUUID();
private circle: Circle | null = null;
private centerPoint: Point | null = null;
public isSelected: boolean = false;
public isHighlighted: boolean = false;

public send(newPoint: Point): boolean {
if (!this.centerPoint) {
Expand Down
Loading

0 comments on commit 7f71d38

Please sign in to comment.