Skip to content

Commit

Permalink
feat: finish snapping points with angle guide intersections
Browse files Browse the repository at this point in the history
  • Loading branch information
bertyhell committed Aug 10, 2024
1 parent e9092ca commit 7834d4b
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 49 deletions.
55 changes: 55 additions & 0 deletions src/App.consts.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,69 @@
/**
* Very small number that will be used to compare floating point numbers on equality
* since javascript isn't always very accurate with floating point numbers
*/
export const EPSILON = 1e-6;

/**
* Margin around the SVG elements when exporting to a .svg image
*/
export const SVG_MARGIN = 10;

/**
* The width and height of the cross that will be drawn instead of the cursor when hovering the drawing canvas
*/
export const CURSOR_SIZE = 30;

/**
* The background color of the canvas
*/
export const CANVAS_BACKGROUND_COLOR = '#111';

/**
* The foreground color of the canvas
* This will be the color of the lines you draw
*/
export const CANVAS_FOREGROUND_COLOR = '#fff';

/**
* The color of the angle guide lines that are drawn when you are close to an angle step from the last drawn point
*/
export const ANGLE_GUIDES_COLOR = '#666666';

/**
* The color of the snap points that are drawn when you are close to a snap point
* These are the shapes you see when you get near an line endpoint or a circle center point, ...
*/
export const SNAP_POINT_COLOR = '#FFFF00';

/**
* How far a snap point can be from the mouse to still be considered a close snap point
*/
export const SNAP_POINT_DISTANCE = 15;

/**
* How far the mouse can be from an angle guide line to show the "near angle step" snap point
*/
export const SNAP_ANGLE_DISTANCE = 15;

/**
* How far the mouse can be from an entity to highlight it and subsequently select it when you click
*/
export const HIGHLIGHT_ENTITY_DISTANCE = 15;

/**
* The size of the snap point indicator shapes that are shown on active snap points
*/
export const SNAP_POINT_SIZE = 10;

/**
* 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 = 500;

/**
* Maximum number of snap points that can be marked at the same time
* Marked snap points also get angle guides
*/
export const MAX_MARKED_SNAP_POINTS = 3;
201 changes: 179 additions & 22 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ 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 } from './App.types.ts';
import { HIGHLIGHT_ENTITY_DISTANCE } from './App.consts.ts';
import { DrawInfo, HoverPoint, SnapPoint } from './App.types.ts';
import {
HIGHLIGHT_ENTITY_DISTANCE,
HOVERED_SNAP_POINT_TIME,
SNAP_POINT_DISTANCE,
} from './App.consts.ts';
import {
clearCanvas,
drawActiveEntity,
Expand All @@ -23,6 +27,9 @@ import { findClosestEntity } from './helpers/find-closest-entity.ts';
import { convertEntitiesToSvgString } from './helpers/export-entities-to-svg.ts';
import { saveAs } from 'file-saver';
import { getDrawHelpers } from './helpers/get-draw-guides.ts';
import { pointDistance } from './helpers/distance-between-points.ts';
import { compact } from './helpers/compact.ts';
import { getClosestSnapPoint } from './helpers/get-closest-snap-point.ts';

function App() {
const [canvasSize, setCanvasSize] = useState<Point>(new Point(0, 0));
Expand All @@ -35,8 +42,23 @@ function App() {
const [helperEntities, setHelperEntities] = useState<Entity[]>([]);
const [debugEntities] = useState<Entity[]>([]);
const [angleStep, setAngleStep] = useState(45);

/**
* 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[]>([]);

const handleWindowResize = () => {
setCanvasSize(new Point(window.innerWidth, window.innerHeight));
};
Expand Down Expand Up @@ -221,30 +243,38 @@ function App() {
y: evt.clientY,
},
});

const [, closestSnapPoint] = getClosestSnapPoint(
compact([snapPoint, snapPointOnAngleGuide]),
mouseLocation,
);

handleMouseUpPoint(
snapPoint ? snapPoint.point : new Point(evt.clientX, evt.clientY),
closestSnapPoint
? closestSnapPoint.point
: new Point(evt.clientX, evt.clientY),
evt.ctrlKey,
evt.shiftKey,
);
}

const deHighlightEntities = useCallback(() => {
setEntities(
entities.map(entity => {
setEntities((oldEntities: Entity[]) => {
return oldEntities.map(entity => {
entity.isHighlighted = false;
return entity;
}),
);
}, [entities]);
});
});
}, []);

const deSelectEntities = useCallback(() => {
setEntities(
entities.map(entity => {
setEntities(oldEntities => {
return oldEntities.map(entity => {
entity.isSelected = false;
return entity;
}),
);
}, [entities]);
});
});
}, []);

const handleKeyUp = useCallback(
(evt: KeyboardEvent) => {
Expand Down Expand Up @@ -295,7 +325,15 @@ function App() {
drawEntities(drawInfo, entities);
drawDebugEntities(drawInfo, debugEntities);
drawActiveEntity(drawInfo, activeEntity);
drawSnapPoint(drawInfo, snapPoint);

const [, closestSnapPoint] = getClosestSnapPoint(
compact([snapPoint, snapPointOnAngleGuide]),
mouseLocation,
);
console.log('drawing snap point: ', snapPoint);
console.log('drawing snap point angle: ', snapPointOnAngleGuide);
drawSnapPoint(drawInfo, closestSnapPoint);

drawCursor(drawInfo, shouldDrawCursor);
}, [
activeEntity,
Expand All @@ -304,12 +342,108 @@ function App() {
debugEntities,
entities,
helperEntities,
mouseLocation.x,
mouseLocation.y,
mouseLocation,
shouldDrawCursor,
snapPoint,
snapPointOnAngleGuide,
]);

/**
* Checks the current snap point every 100ms to mark certain snap points when they are hovered for a certain amount of time (marked)
* So we can show extra angle guides for the ones that are marked
*/
const watchSnapPoint = useCallback(() => {
if (!snapPoint) {
return;
}

const lastHoveredPoint = hoveredSnapPoints.at(-1);
let newHoverSnapPoints: HoverPoint[];

// Angle guide points should never be marked
console.log('snap point: ', JSON.stringify(snapPoint));

if (lastHoveredPoint) {
if (
pointDistance(snapPoint.point, lastHoveredPoint.snapPoint.point) <
SNAP_POINT_DISTANCE
) {
console.log(
'INCREASE HOVER TIME: ',
JSON.stringify({
...lastHoveredPoint,
milliSecondsHovered: lastHoveredPoint.milliSecondsHovered + 100,
}),
);
// Last hovered snap point is still the current closest snap point
// Increase the hover time
newHoverSnapPoints = [
...hoveredSnapPoints.slice(0, hoveredSnapPoints.length - 1),
{
...lastHoveredPoint,
milliSecondsHovered: lastHoveredPoint.milliSecondsHovered + 100,
},
];
} else {
// The closest snap point has changed
// Check if the last snap point was hovered for long enough to be considered a marked snap point
if (lastHoveredPoint.milliSecondsHovered >= HOVERED_SNAP_POINT_TIME) {
console.log(
'NEW HOVERED SNAP POINT: ',
JSON.stringify({
snapPoint,
milliSecondsHovered: 100,
}),
);
// Append the new point to the list
newHoverSnapPoints = [
...hoveredSnapPoints,
{
snapPoint,
milliSecondsHovered: 100,
},
];
} else {
console.log(
'REPLACE HOVERED SNAP POINT: ',
JSON.stringify({
snapPoint,
milliSecondsHovered: 100,
}),
);
// Replace the last point with the new point
newHoverSnapPoints = [
...hoveredSnapPoints.slice(0, hoveredSnapPoints.length - 1),
{
snapPoint,
milliSecondsHovered: 100,
},
];
}
}
} else {
console.log(
'BRAND NEW HOVERED SNAP POINT: ',
JSON.stringify({
snapPoint,
milliSecondsHovered: 100,
}),
lastHoveredPoint,
);
// No snap points were hovered before
newHoverSnapPoints = [
{
snapPoint,
milliSecondsHovered: 100,
},
];
}

const newHoverSnapPointsTruncated = newHoverSnapPoints.slice(0, 3);
console.log('hovered snap points: ', newHoverSnapPointsTruncated);
setHoveredSnapPoints(newHoverSnapPointsTruncated);
}, [snapPoint, hoveredSnapPoints]);

useEffect(() => {
console.log('init app');
window.document.addEventListener('keyup', handleKeyUp);
Expand All @@ -323,7 +457,17 @@ function App() {
window.document.removeEventListener('keyup', handleKeyUp);
window.document.removeEventListener('resize', handleWindowResize);
};
}, [handleKeyUp]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
const watchSnapPointTimerId = setInterval(() => {
watchSnapPoint();
}, 1000);
return () => {
clearInterval(watchSnapPointTimerId);
};
}, [watchSnapPoint]);

useEffect(() => {
draw();
Expand All @@ -340,16 +484,29 @@ function App() {
) {
firstPoint = activeEntity.getFirstPoint();
}
const { angleGuide, snapPoint } = getDrawHelpers(
const { angleGuides, entitySnapPoint, angleSnapPoint } = getDrawHelpers(
entities,
firstPoint,
compact([
firstPoint,
...hoveredSnapPoints.map(
hoveredSnapPoint => hoveredSnapPoint.snapPoint.point,
),
]),
mouseLocation,
angleStep,
);
setHelperEntities(angleGuide ? [angleGuide] : []);
setSnapPoint(snapPoint);
setHelperEntities(angleGuides);
setSnapPoint(entitySnapPoint);
setSnapPointOnAngleGuide(angleSnapPoint);
}
}, [activeEntity, activeTool, angleStep, entities, mouseLocation]);
}, [
activeEntity,
activeTool,
angleStep,
entities,
hoveredSnapPoints,
mouseLocation,
]);

useEffect(() => {
if (activeTool === Tool.Select) {
Expand Down
5 changes: 5 additions & 0 deletions src/App.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ export interface SnapPoint {
}

export type SnapPointConfig = Record<SnapPointType, boolean>;

export interface HoverPoint {
snapPoint: SnapPoint;
milliSecondsHovered: number;
}
Loading

0 comments on commit 7834d4b

Please sign in to comment.