Skip to content

Commit

Permalink
feat: add initial snap points
Browse files Browse the repository at this point in the history
  • Loading branch information
bertyhell committed Aug 10, 2024
1 parent 4831eab commit e9092ca
Show file tree
Hide file tree
Showing 18 changed files with 449 additions and 124 deletions.
4 changes: 3 additions & 1 deletion src/App.consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const CANVAS_BACKGROUND_COLOR = '#111';
export const CANVAS_FOREGROUND_COLOR = '#fff';
export const ANGLE_GUIDES_COLOR = '#666666';
export const SNAP_POINT_COLOR = '#FFFF00';
export const SNAP_DISTANCE = 15;
export const SNAP_POINT_DISTANCE = 15;
export const SNAP_ANGLE_DISTANCE = 15;
export const HIGHLIGHT_ENTITY_DISTANCE = 15;
export const SNAP_POINT_SIZE = 10;
107 changes: 46 additions & 61 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { RectangleEntity } from './entities/RectangleEntity.ts';
import { CircleEntity } from './entities/CircleEntity.ts';
import { SelectionRectangleEntity } from './entities/SelectionRectangleEntity.ts';
import { Box, Point } from '@flatten-js/core';
import { DrawInfo, SnapPoint, SnapPointType } from './App.types.ts';
import { SNAP_DISTANCE } from './App.consts.ts';
import { DrawInfo, SnapPoint } from './App.types.ts';
import { HIGHLIGHT_ENTITY_DISTANCE } from './App.consts.ts';
import {
clearCanvas,
drawActiveEntity,
Expand All @@ -22,8 +22,7 @@ import { Toolbar } from './components/Toolbar.tsx';
import { findClosestEntity } from './helpers/find-closest-entity.ts';
import { convertEntitiesToSvgString } from './helpers/export-entities-to-svg.ts';
import { saveAs } from 'file-saver';
import { getAngleGuideLines } from './helpers/get-angle-guide-lines.ts';
import { getClosestSnapPoint } from './helpers/snap-points.ts';
import { getDrawHelpers } from './helpers/get-draw-guides.ts';

function App() {
const [canvasSize, setCanvasSize] = useState<Point>(new Point(0, 0));
Expand All @@ -36,7 +35,7 @@ function App() {
const [helperEntities, setHelperEntities] = useState<Entity[]>([]);
const [debugEntities] = useState<Entity[]>([]);
const [angleStep, setAngleStep] = useState(45);
const [snapPointInfos, setSnapPointInfos] = useState<SnapPoint[]>([]);
const [snapPoint, setSnapPoint] = useState<SnapPoint | null>(null);

const handleWindowResize = () => {
setCanvasSize(new Point(window.innerWidth, window.innerHeight));
Expand Down Expand Up @@ -115,7 +114,7 @@ function App() {
// Mouse is close to entity and is not dragging a rectangle
if (
closestEntityInfo &&
closestEntityInfo[0] < SNAP_DISTANCE &&
closestEntityInfo[0] < HIGHLIGHT_ENTITY_DISTANCE &&
!activeSelectionRectangle
) {
// Select the entity close to the mouse
Expand All @@ -141,7 +140,7 @@ function App() {
);
// Start a new selection rectangle drag
activeSelectionRectangle = new SelectionRectangleEntity();
setActiveEntity(activeSelectionRectangle);
setActiveEntity(activeSelectionRectangle); // TODO make selection a separate concept from entities
}

const completed = activeSelectionRectangle.send(
Expand Down Expand Up @@ -222,26 +221,13 @@ function App() {
y: evt.clientY,
},
});
const closestSnapPoint = getClosestSnapPoint(snapPointInfos);
handleMouseUpPoint(
closestSnapPoint ? closestSnapPoint : new Point(evt.clientX, evt.clientY),
snapPoint ? snapPoint.point : new Point(evt.clientX, evt.clientY),
evt.ctrlKey,
evt.shiftKey,
);
}

function handleKeyUp(evt: KeyboardEvent) {
if (evt.key === 'Escape') {
setActiveEntity(null);
deSelectEntities();
deHighlightEntities();
} else if (evt.key === 'Delete') {
setEntities(oldEntities =>
oldEntities.filter(entity => !entity.isSelected),
);
}
}

const deHighlightEntities = useCallback(() => {
setEntities(
entities.map(entity => {
Expand All @@ -260,6 +246,21 @@ function App() {
);
}, [entities]);

const handleKeyUp = useCallback(
(evt: KeyboardEvent) => {
if (evt.key === 'Escape') {
setActiveEntity(null);
deSelectEntities();
deHighlightEntities();
} else if (evt.key === 'Delete') {
setEntities(oldEntities =>
oldEntities.filter(entity => !entity.isSelected),
);
}
},
[deHighlightEntities, deSelectEntities],
);

const handleToolClick = useCallback(
(tool: Tool) => {
console.log('set active tool: ', tool);
Expand All @@ -277,40 +278,6 @@ function App() {
saveAs(blob, 'open-web-cad--drawing.svg');
}, [canvasSize, entities]);

const calculateHelpers = useCallback(() => {
if ([Tool.Line, Tool.Rectangle, Tool.Circle].includes(activeTool)) {
// If you're in the progress of drawing a shape, show the guides
if (
activeEntity &&
!activeEntity.getShape() &&
activeEntity.getFirstPoint()
) {
const firstPoint: Point = activeEntity.getFirstPoint() as Point;

const angleGuideLines = getAngleGuideLines(firstPoint, angleStep);

const closestLineInfo = findClosestEntity(
mouseLocation,
angleGuideLines,
);

if (closestLineInfo[0] < SNAP_DISTANCE) {
setHelperEntities([closestLineInfo[2]!]);
setSnapPointInfos([
{
point: closestLineInfo[1]!.start,
type: SnapPointType.AngleGuide,
},
]); // TODO
return;
}
}

setHelperEntities([]);
setSnapPointInfos([]);
}
}, [angleStep, activeEntity, activeTool, mouseLocation]);

const draw = useCallback(() => {
const context: CanvasRenderingContext2D | null | undefined =
canvasRef.current?.getContext('2d');
Expand All @@ -328,7 +295,7 @@ function App() {
drawEntities(drawInfo, entities);
drawDebugEntities(drawInfo, debugEntities);
drawActiveEntity(drawInfo, activeEntity);
drawSnapPoint(drawInfo, getClosestSnapPoint(snapPointInfos));
drawSnapPoint(drawInfo, snapPoint);
drawCursor(drawInfo, shouldDrawCursor);
}, [
activeEntity,
Expand All @@ -340,7 +307,7 @@ function App() {
mouseLocation.x,
mouseLocation.y,
shouldDrawCursor,
snapPointInfos,
snapPoint,
]);

useEffect(() => {
Expand All @@ -356,15 +323,33 @@ function App() {
window.document.removeEventListener('keyup', handleKeyUp);
window.document.removeEventListener('resize', handleWindowResize);
};
}, []);
}, [handleKeyUp]);

useEffect(() => {
draw();
}, [canvasSize.x, canvasSize.y, mouseLocation.x, mouseLocation.y, draw]);

useEffect(() => {
calculateHelpers();
}, [calculateHelpers]);
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 { angleGuide, snapPoint } = getDrawHelpers(
entities,
firstPoint,
mouseLocation,
angleStep,
);
setHelperEntities(angleGuide ? [angleGuide] : []);
setSnapPoint(snapPoint);
}
}, [activeEntity, activeTool, angleStep, entities, mouseLocation]);

useEffect(() => {
if (activeTool === Tool.Select) {
Expand All @@ -373,7 +358,7 @@ function App() {
const closestEntityInfo = findClosestEntity(mouseLocation, entities);

const [distance, , closestEntity] = closestEntityInfo;
if (distance < SNAP_DISTANCE) {
if (distance < HIGHLIGHT_ENTITY_DISTANCE) {
closestEntity.isHighlighted = true;
}
}
Expand Down
19 changes: 10 additions & 9 deletions src/App.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ export interface DrawInfo {
export type Shape = Box | Segment | Point | Circle;

export enum SnapPointType {
AngleGuide = 'AngleGuide',
LineEndPoint = 'LineEndPoint',
LineIntersection = 'LineIntersection',
CircleCenter = 'CircleCenter',
CircleCardinal = 'CircleCardinal',
CircleTangent = 'CircleTangent',
MiddlePoint = 'MiddlePoint'
AngleGuide = 'AngleGuide',
LineEndPoint = 'LineEndPoint',
Intersection = 'Intersection',
CircleCenter = 'CircleCenter',
CircleCardinal = 'CircleCardinal',
CircleTangent = 'CircleTangent',
LineMidPoint = 'LineMidPoint',
Point = 'Point',
}

export interface SnapPoint {
point: Point;
type: SnapPointType;
point: Point;
type: SnapPointType;
}

export type SnapPointConfig = Record<SnapPointType, boolean>;
49 changes: 47 additions & 2 deletions src/entities/CircleEntity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Entity } from './Entitity.ts';
import { DrawInfo, Shape, SnapPoint } from '../App.types.ts';
import { DrawInfo, Shape, SnapPoint, SnapPointType } from '../App.types.ts';
import { Box, circle, Circle, point, Point, Segment } from '@flatten-js/core';

export class CircleEntity implements Entity {
Expand Down Expand Up @@ -73,7 +73,52 @@ export class CircleEntity implements Entity {
}

public getSnapPoints(): SnapPoint[] {
return []; // TODO
if (this.centerPoint === null || this.circle === null) {
return [];
}
return [
{
point: this.centerPoint,
type: SnapPointType.CircleCenter,
},
{
point: new Point(
this.centerPoint.x + this.circle.r,
this.centerPoint.y,
),
type: SnapPointType.CircleCardinal,
},
{
point: new Point(
this.centerPoint.x - this.circle.r,
this.centerPoint.y,
),
type: SnapPointType.CircleCardinal,
},
{
point: new Point(
this.centerPoint.x,
this.centerPoint.y + this.circle.r,
),
type: SnapPointType.CircleCardinal,
},
{
point: new Point(
this.centerPoint.x,
this.centerPoint.y - this.circle.r,
),
type: SnapPointType.CircleCardinal,
},
// TODO add tangent points from mouse location to circle
];
}

public getIntersections(entity: Entity): Point[] {
const otherShape = entity.getShape();
if (!this.circle || !otherShape) {
return [];
}
return this.circle.intersect(otherShape);
}

public getFirstPoint(): Point | null {
Expand Down
1 change: 1 addition & 0 deletions src/entities/Entitity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Entity {
getFirstPoint(): Point | null;
getShape(): Shape | null;
getSnapPoints(): SnapPoint[];
getIntersections(entity: Entity): Point[];
distanceTo(shape: Shape): [number, Segment] | null;
getSvgString(): string | null;
}
35 changes: 31 additions & 4 deletions src/entities/LineEntity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Entity } from './Entitity.ts';
import { DrawInfo, Shape, SnapPoint } from '../App.types.ts';
import { DrawInfo, Shape, SnapPoint, SnapPointType } from '../App.types.ts';
import { Box, Point, Segment } from '@flatten-js/core';

export class LineEntity implements Entity {
Expand All @@ -8,8 +8,11 @@ export class LineEntity implements Entity {
public isSelected: boolean = false;
public isHighlighted: boolean = false;

constructor(p1?: Point, p2?: Point) {
if (p1 && p2) {
constructor(p1?: Point | Segment, p2?: Point) {
if (p1 instanceof Segment) {
this.startPoint = p1.start;
this.segment = p1;
} else if (p1 && p2) {
this.startPoint = p1;
this.segment = new Segment(p1, p2);
} else if (p1) {
Expand Down Expand Up @@ -77,7 +80,31 @@ export class LineEntity implements Entity {
}

public getSnapPoints(): SnapPoint[] {
return []; // TODO
if (!this.segment?.start || !this.segment?.end) {
return [];
}
return [
{
point: this.segment.start,
type: SnapPointType.LineEndPoint,
},
{
point: this.segment.end,
type: SnapPointType.LineEndPoint,
},
{
point: this.segment.middle(),
type: SnapPointType.LineMidPoint,
},
];
}

public getIntersections(entity: Entity): Point[] {
const otherShape = entity.getShape();
if (!this.segment || !otherShape) {
return [];
}
return this.segment.intersect(otherShape);
}

public getFirstPoint(): Point | null {
Expand Down
16 changes: 14 additions & 2 deletions src/entities/PointEntity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Entity } from './Entitity.ts';
import { DrawInfo, Shape, SnapPoint } from '../App.types.ts';
import { DrawInfo, Shape, SnapPoint, SnapPointType } from '../App.types.ts';
import { Box, Point, Segment } from '@flatten-js/core';

export class PointEntity implements Entity {
Expand Down Expand Up @@ -37,7 +37,19 @@ export class PointEntity implements Entity {
}

public getSnapPoints(): SnapPoint[] {
return []; // TODO
if (!this.point) {
return [];
}
return [
{
point: this.point,
type: SnapPointType.Point,
},
];
}

public getIntersections(): Point[] {
return [];
}

public getFirstPoint(): Point | null {
Expand Down
Loading

0 comments on commit e9092ca

Please sign in to comment.