mirror of
https://github.com/excalidraw/excalidraw.git
synced 2026-08-04 09:52:38 -04:00
* Initial commit * Segments detection * Remove aspect ratio * Refactor * Fix tests * Toggle shapeSnap * Fix linting * debug: live preview * Rebase fixes Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * Bind to elements (no hover background) * Shape closed * Fix rad calc + rename segments * Comments * Fix binding highlighting + lints * Rename to convert to shape * fix: Binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Refactor spline simplification Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove unused doBoundsIntersect Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Restore type signature Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Propose new button shapes Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove unused types Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Icon shapes Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Element cloning Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Element cloning Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Remove unused Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Shape settings in recognition mode Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove debug Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Actions Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Lint Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Arrowheads for freedraw Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Draw shape tool instead of freedraw Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Test Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Lint Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Keyboard shortcut visibility Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Unique cursor for draw shape Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Remove selection Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Shape selection Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Improve shape recognition Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Further refine shape recognition Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Preview Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Add hint Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Refined shape recognition Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Less ellipse more rectanguloid Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Refined arrow recognition Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Refinerecognition Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Lower linear threshold Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Rectangle head arrow Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: More arrowhead sizes Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Arrow remains an arrow Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Draw line between elements converts to arrow Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * remove pendingDrawShapeElement * fix: Arrow start point binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Different arrow endpoint heuristic Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Point type Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Various small fixes Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Avoid recompute Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Type issues Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * test(editor): cover the drawShape (snap-to-shape) tool The drawShape tool had no dedicated tests, which let master's toolbar / active-tool rewrite silently break it during the merge: actionFinalize reverted to the selection tool after every sketch, because the drawShape flow finalizes without a `newElement` and so failed master's new `&& element` guard. Covers the behavior the merge had to preserve: - sketch recognition (rectangle, ellipse) and the too-small no-op - the tool stays active after finalizing, so shapes can be sketched consecutively (regression guard for the above) - activation via the S shortcut and the extra-tools dropdown - the tool's own cursor Points are fed into the trail directly rather than through pointermove, which is rAF-throttled and would drop all but one point per frame — the same approach lasso.test.tsx uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: Add test Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Breakout from App.tsx Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Shortcut is now Shift+X Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: No selecting arrows for shape recog Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Double history Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: PCA + feature extraction recognition Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * reduce and normalize min-size gates * never select newElement when shape recog tool active * simplify lifecycle & remove double finalize * make arrows bindings orbit * funnel though actionFinalize & fix two unrelated cleanup cases * use crosshair cursor * show strokeWidth * switch icons for drawShape & extraTools * dedupe getBoundsFromPoints * remove unused helpers * jsdoc cleanup * dynamic trail size * improve rectangle recognition * disallow linears to deviate too much from a straight line * put under freedraw tool on tablet/mobile * tweak label * rename `drawShape` -> `autoshape` --------- Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> Co-authored-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
import { pointFrom } from "../src/point";
|
|
import {
|
|
convexHull,
|
|
polygonArea,
|
|
polygonSignedArea,
|
|
simplifyConvexPolygon,
|
|
} from "../src/polygon";
|
|
|
|
import type { GlobalPoint } from "../src/types";
|
|
|
|
const square: GlobalPoint[] = [
|
|
pointFrom(0, 0),
|
|
pointFrom(10, 0),
|
|
pointFrom(10, 10),
|
|
pointFrom(0, 10),
|
|
];
|
|
|
|
describe("polygonArea", () => {
|
|
it("measures a polygon, whichever way it winds", () => {
|
|
expect(polygonArea(square)).toBe(100);
|
|
expect(polygonArea([...square].reverse())).toBe(100);
|
|
});
|
|
|
|
it("ignores a repeated closing vertex", () => {
|
|
expect(polygonArea([...square, square[0]])).toBe(100);
|
|
});
|
|
|
|
it("reports the winding direction in the sign", () => {
|
|
expect(polygonSignedArea(square)).toBe(100);
|
|
expect(polygonSignedArea([...square].reverse())).toBe(-100);
|
|
});
|
|
});
|
|
|
|
describe("convexHull", () => {
|
|
it("drops the points inside the hull", () => {
|
|
const hull = convexHull([...square, pointFrom<GlobalPoint>(5, 5)]);
|
|
|
|
expect(hull).toHaveLength(4);
|
|
expect(hull).toEqual(expect.arrayContaining(square));
|
|
});
|
|
|
|
it("drops collinear points", () => {
|
|
expect(convexHull([...square, pointFrom<GlobalPoint>(5, 0)])).toHaveLength(
|
|
4,
|
|
);
|
|
});
|
|
|
|
it("wraps a point cloud whatever order it arrives in", () => {
|
|
const cloud: GlobalPoint[] = Array.from({ length: 30 }, (_, i) =>
|
|
pointFrom(Math.cos(i * 2.4) * 10, Math.sin(i * 2.4) * 10),
|
|
);
|
|
const hull = convexHull(cloud);
|
|
|
|
expect(polygonArea(hull)).toBeCloseTo(
|
|
polygonArea(convexHull([...cloud].reverse())),
|
|
);
|
|
// Every point is inside or on the hull it produced.
|
|
expect(polygonArea(hull)).toBeGreaterThan(250);
|
|
expect(polygonArea(hull)).toBeLessThanOrEqual(Math.PI * 100);
|
|
});
|
|
|
|
it("returns degenerate input as-is", () => {
|
|
const two = square.slice(0, 2);
|
|
|
|
expect(convexHull(two)).toEqual(two);
|
|
});
|
|
});
|
|
|
|
describe("simplifyConvexPolygon", () => {
|
|
const circle = (n: number): GlobalPoint[] =>
|
|
Array.from({ length: n }, (_, i) =>
|
|
pointFrom(
|
|
Math.cos((i * 2 * Math.PI) / n) * 100,
|
|
Math.sin((i * 2 * Math.PI) / n) * 100,
|
|
),
|
|
);
|
|
|
|
it("keeps the corners of a polygon and drops the wobble along its sides", () => {
|
|
// A square whose sides bulge slightly outward, as a hand-drawn one would.
|
|
const wobbly = convexHull([
|
|
...square,
|
|
pointFrom<GlobalPoint>(5, -0.2),
|
|
pointFrom<GlobalPoint>(10.2, 5),
|
|
pointFrom<GlobalPoint>(5, 10.2),
|
|
pointFrom<GlobalPoint>(-0.2, 5),
|
|
]);
|
|
|
|
expect(simplifyConvexPolygon(wobbly, (25 * Math.PI) / 180)).toHaveLength(4);
|
|
});
|
|
|
|
it("spreads an ellipse's turn over many corners", () => {
|
|
const corners = simplifyConvexPolygon(
|
|
convexHull(circle(64)),
|
|
(25 * Math.PI) / 180,
|
|
);
|
|
|
|
// A full turn is 360°, emitted in ~25° steps.
|
|
expect(corners.length).toBeGreaterThan(10);
|
|
expect(corners.length).toBeLessThanOrEqual(15);
|
|
});
|
|
|
|
it("does not depend on where the hull happens to start", () => {
|
|
const hull = convexHull([
|
|
...square,
|
|
pointFrom<GlobalPoint>(5, -0.2),
|
|
pointFrom<GlobalPoint>(10.2, 5),
|
|
]);
|
|
const rotated = [...hull.slice(2), ...hull.slice(0, 2)];
|
|
|
|
expect(simplifyConvexPolygon(rotated, (25 * Math.PI) / 180)).toHaveLength(
|
|
simplifyConvexPolygon(hull, (25 * Math.PI) / 180).length,
|
|
);
|
|
});
|
|
|
|
it("returns degenerate input as-is", () => {
|
|
const two = square.slice(0, 2);
|
|
|
|
expect(simplifyConvexPolygon(two, 0.4)).toEqual(two);
|
|
});
|
|
});
|