Skip to content

Commit

Permalink
feat: add save and load buttons to save/load to json file
Browse files Browse the repository at this point in the history
  • Loading branch information
bertyhell committed Aug 28, 2024
1 parent 5ea61f8 commit 7070b72
Show file tree
Hide file tree
Showing 19 changed files with 406 additions and 41 deletions.
5 changes: 4 additions & 1 deletion src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC } from 'react';
import { FC, ReactNode } from 'react';
import { Icon, IconName } from './icon.tsx';

interface ButtonProps {
Expand All @@ -7,6 +7,7 @@ interface ButtonProps {
icon?: IconName;
active?: boolean;
onClick?: () => void;
children?: ReactNode;
className?: string;
}

Expand All @@ -16,6 +17,7 @@ export const Button: FC<ButtonProps> = ({
icon,
onClick,
active = false,
children,
className,
}) => {
return (
Expand All @@ -31,6 +33,7 @@ export const Button: FC<ButtonProps> = ({
>
{icon && <Icon name={icon} />}
{label && <span>{label}</span>}
{children}
</button>
</>
);
Expand Down
28 changes: 26 additions & 2 deletions src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
import { StateVariable } from '../helpers/undo-stack.ts';
import { exportEntitiesToSvgFile } from '../helpers/export-entities-to-svg.ts';
import { exportEntitiesToPngFile } from '../helpers/export-entities-to-png.ts';
import { exportEntitiesToJsonFile } from '../helpers/export-entities-to-json.ts';
import { importEntitiesFromJsonFile } from '../helpers/import-entities-from-json.ts';
import { noop } from 'es-toolkit';

interface ToolbarProps {}

Expand Down Expand Up @@ -149,12 +152,33 @@ export const Toolbar: FC<ToolbarProps> = () => {
</DropdownButton>
<Button
className="mt-2"
title="Export SVG"
title="Save to JSON file"
icon={IconName.Save}
onClick={() => exportEntitiesToJsonFile()}
/>
<Button
className="relative"
title="Load from JSON file"
icon={IconName.Folder}
onClick={noop}
>
<input
className="absolute inset-0 opacity-0"
type="file"
accept="*.json"
onChange={async evt => {
await importEntitiesFromJsonFile(evt.target.files?.[0]);
evt.target.files = null;
}}
></input>
</Button>
<Button
title="Export to SVG file"
label="SVG"
onClick={() => exportEntitiesToSvgFile()}
/>
<Button
title="Export PNG"
title="Export to PNG file"
label="PNG"
onClick={() => exportEntitiesToPngFile()}
/>
Expand Down
6 changes: 6 additions & 0 deletions src/components/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import AntiClockwiseIcon from 'teenyicons/outline/anti-clockwise.svg?react';
import ClockwiseIcon from 'teenyicons/outline/clockwise.svg?react';
import GithubIcon from 'teenyicons/outline/github.svg?react';
import CropIcon from 'teenyicons/outline/crop.svg?react';
import FolderIcon from 'teenyicons/outline/folder.svg?react';
import SaveIcon from 'teenyicons/outline/save.svg?react';
import { FC } from 'react';

// https://icon-sets.iconify.design/teenyicons
Expand All @@ -27,6 +29,8 @@ enum IconName {
Clockwise = 'Clockwise',
Github = 'Github',
Crop = 'Crop',
Folder = 'Folder',
Save = 'Save',
Svg = 'Svg',
SolidDown = 'SolidDown',
SolidUp = 'SolidUp',
Expand All @@ -45,6 +49,8 @@ const icons: Record<IconName, FC> = {
[IconName.Clockwise]: ClockwiseIcon,
[IconName.Github]: GithubIcon,
[IconName.Crop]: CropIcon,
[IconName.Folder]: FolderIcon,
[IconName.Save]: SaveIcon,
[IconName.Svg]: SvgIcon,
[IconName.SolidDown]: SolidDownIcon,
[IconName.SolidUp]: SolidUpIcon,
Expand Down
52 changes: 49 additions & 3 deletions src/entities/ArcEntity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Entity, EntityName } from './Entitity.ts';
import { Entity, EntityName, JsonEntity } from './Entity.ts';
import { DrawInfo, Shape, SnapPoint, SnapPointType } from '../App.types.ts';
import { Arc, Box, Line, point, Point, Segment } from '@flatten-js/core';
import { worldToScreen } from '../helpers/world-screen-conversion.ts';
Expand All @@ -8,7 +8,7 @@ import { isPointEqual } from '../helpers/is-point-equal.ts';
import { sortPointsOnArc } from '../helpers/sort-points-on-arc.ts';

export class ArcEntity implements Entity {
public readonly id: string = crypto.randomUUID();
public id: string = crypto.randomUUID();
private arc: Arc | null = null;
private centerPoint: Point | null = null;
private firstPoint: Point | null = null;
Expand Down Expand Up @@ -166,7 +166,7 @@ export class ArcEntity implements Entity {
return EntityName.Arc;
}

public containsPointOnLine(point: Point): boolean {
public containsPointOnShape(point: Point): boolean {
if (!this.arc) {
return false;
}
Expand Down Expand Up @@ -205,4 +205,50 @@ export class ArcEntity implements Entity {
}
return segmentArcs;
}

public toJson(): JsonEntity<ArcJsonData> | null {
if (!this.arc) {
return null;
}
return {
id: this.id,
type: EntityName.Arc,
shapeData: {
center: { x: this.arc.center.x, y: this.arc.center.y },
start: { x: this.arc.start.x, y: this.arc.start.y },
end: { x: this.arc.end.x, y: this.arc.end.y },
counterClockwise: this.arc.counterClockwise,
},
};
}

public fromJson(jsonEntity: JsonEntity<ArcJsonData>): ArcEntity {
if (jsonEntity.type !== EntityName.Arc) {
throw new Error('Invalid Entity type in JSON');
}

const center = new Point(
jsonEntity.shapeData.center.x,
jsonEntity.shapeData.center.y,
);
const start = new Point(
jsonEntity.shapeData.start.x,
jsonEntity.shapeData.start.y,
);
const end = new Point(
jsonEntity.shapeData.end.x,
jsonEntity.shapeData.end.y,
);
const counterClockwise = jsonEntity.shapeData.counterClockwise;
const arcEntity = new ArcEntity(center, start, end, counterClockwise);
arcEntity.id = jsonEntity.id;
return arcEntity;
}
}

export interface ArcJsonData {
center: { x: number; y: number };
start: { x: number; y: number };
end: { x: number; y: number };
counterClockwise: boolean;
}
51 changes: 48 additions & 3 deletions src/entities/CircleEntity.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import { Entity, EntityName } from './Entitity.ts';
import { Entity, EntityName, JsonEntity } from './Entity.ts';
import { DrawInfo, Shape, SnapPoint, SnapPointType } from '../App.types.ts';
import { Box, circle, Circle, point, Point, Segment } from '@flatten-js/core';
import { worldToScreen } from '../helpers/world-screen-conversion.ts';
import { ArcEntity } from './ArcEntity.ts';
import { wrapModule } from '../helpers/wrap-module.ts';
import { pointDistance } from '../helpers/distance-between-points.ts';

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

constructor(centerPoint?: Point, radiusOrSecondPoint?: number | Point) {
if (centerPoint) {
this.centerPoint = centerPoint;
}
if (centerPoint && radiusOrSecondPoint) {
this.circle = new Circle(
centerPoint,
typeof radiusOrSecondPoint === 'number'
? radiusOrSecondPoint
: pointDistance(centerPoint, radiusOrSecondPoint),
);
}
}

public send(newPoint: Point): boolean {
if (!this.centerPoint) {
this.centerPoint = point(newPoint.x, newPoint.y);
Expand Down Expand Up @@ -144,7 +159,7 @@ export class CircleEntity implements Entity {
return EntityName.Circle;
}

public containsPointOnLine(point: Point): boolean {
public containsPointOnShape(point: Point): boolean {
if (!this.circle) {
return false;
}
Expand Down Expand Up @@ -174,4 +189,34 @@ export class CircleEntity implements Entity {
}
return segmentArcs;
}

public toJson(): JsonEntity<CircleJsonData> | null {
if (!this.circle) {
return null;
}
return {
id: this.id,
type: EntityName.Circle,
shapeData: {
center: { x: this.circle.center.x, y: this.circle.center.y },
radius: this.circle?.r,
},
};
}

public fromJson(jsonEntity: JsonEntity<CircleJsonData>): CircleEntity {
const center = new Point(
jsonEntity.shapeData.center.x,
jsonEntity.shapeData.center.y,
);
const radius = jsonEntity.shapeData.radius;
const circleEntity = new CircleEntity(center, radius);
circleEntity.id = jsonEntity.id;
return circleEntity;
}
}

export interface CircleJsonData {
center: { x: number; y: number };
radius: number;
}
22 changes: 21 additions & 1 deletion src/entities/Entitity.ts → src/entities/Entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { DrawInfo, Shape, SnapPoint } from '../App.types.ts';
import { Box, Point, Segment } from '@flatten-js/core';
import { ArcJsonData } from './ArcEntity.ts';
import { CircleJsonData } from './CircleEntity.ts';
import { LineJsonData } from './LineEntity.ts';
import { RectangleJsonData } from './RectangleEntity.ts';
import { PointJsonData } from './PointEntity.ts';

export interface Entity {
// Random uuid generated when the Entity is created
Expand All @@ -16,7 +21,9 @@ export interface Entity {
distanceTo(shape: Shape): [number, Segment] | null;
getSvgString(): string | null;
getType(): EntityName;
containsPointOnLine(point: Point): boolean;
containsPointOnShape(point: Point): boolean;
toJson(): JsonEntity | null;
fromJson(jsonEntity: JsonEntity): Entity | null;
}

export enum EntityName {
Expand All @@ -27,3 +34,16 @@ export enum EntityName {
SelectionRectangle = 'SelectionRectangle',
Point = 'Point',
}

export type ShapeJsonData =
| RectangleJsonData
| CircleJsonData
| ArcJsonData
| LineJsonData
| PointJsonData;

export interface JsonEntity<TShapeJsonData = ShapeJsonData> {
id: string;
type: EntityName;
shapeData: TShapeJsonData;
}
39 changes: 36 additions & 3 deletions src/entities/LineEntity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Entity, EntityName } from './Entitity.ts';
import { Entity, EntityName, JsonEntity } from './Entity.ts';
import { DrawInfo, Shape, SnapPoint, SnapPointType } from '../App.types.ts';
import { Box, Point, Segment } from '@flatten-js/core';
import { worldToScreen } from '../helpers/world-screen-conversion.ts';
Expand All @@ -7,7 +7,7 @@ import { isPointEqual } from '../helpers/is-point-equal.ts';
import { pointDistance } from '../helpers/distance-between-points.ts';

export class LineEntity implements Entity {
public readonly id: string = crypto.randomUUID();
public id: string = crypto.randomUUID();
private segment: Segment | null = null;
private startPoint: Point | null = null;

Expand Down Expand Up @@ -133,7 +133,7 @@ export class LineEntity implements Entity {
return EntityName.Line;
}

public containsPointOnLine(point: Point): boolean {
public containsPointOnShape(point: Point): boolean {
if (!this.segment) {
return false;
}
Expand Down Expand Up @@ -170,4 +170,37 @@ export class LineEntity implements Entity {
}
return lineSegments;
}

public toJson(): JsonEntity<LineJsonData> | null {
if (!this.segment) {
return null;
}
return {
id: this.id,
type: EntityName.Line,
shapeData: {
startPoint: { x: this.segment.start.x, y: this.segment.start.y },
endPoint: { x: this.segment.end.x, y: this.segment.end.y },
},
};
}

public fromJson(jsonEntity: JsonEntity<LineJsonData>): LineEntity {
const startPoint = new Point(
jsonEntity.shapeData.startPoint.x,
jsonEntity.shapeData.startPoint.y,
);
const endPoint = new Point(
jsonEntity.shapeData.endPoint.x,
jsonEntity.shapeData.endPoint.y,
);
const lineEntity = new LineEntity(startPoint, endPoint);
lineEntity.id = jsonEntity.id;
return lineEntity;
}
}

export interface LineJsonData {
startPoint: { x: number; y: number };
endPoint: { x: number; y: number };
}
Loading

0 comments on commit 7070b72

Please sign in to comment.