mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
refactor: update collision from ga to vector geometry (#7636)
* new collision api * isPointOnShape * removed redundant code * new collision methods in app * curve shape takes starting point * clean up geometry * curve rotation * freedraw * inside curve * improve ellipse inside check * ellipse distance func * curve inside * include frame name bounds * replace previous private methods for getting elements at x,y * arrow bound text hit detection * keep iframes on top * remove dependence on old collision methods from app * remove old collision functions * move some hit functions outside of app * code refactor * type * text collision from inside * fix context menu test * highest z-index collision * fix 1px away binding test * strictly less * remove unused imports * lint * 'ignore' resize flipping test * more lint fix * skip 'flips while resizing' test * more test * fix merge errors * fix selection in resize test * added a bit more comment --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
3e334a67ed
commit
bbdcd30a73
20 changed files with 2721 additions and 1627 deletions
278
packages/utils/geometry/shape.ts
Normal file
278
packages/utils/geometry/shape.ts
Normal file
|
@ -0,0 +1,278 @@
|
|||
/**
|
||||
* this file defines pure geometric shapes
|
||||
*
|
||||
* for instance, a cubic bezier curve is specified by its four control points and
|
||||
* an ellipse is defined by its center, angle, semi major axis and semi minor axis
|
||||
* (but in semi-width and semi-height so it's more relevant to Excalidraw)
|
||||
*
|
||||
* the idea with pure shapes is so that we can provide collision and other geoemtric methods not depending on
|
||||
* the specifics of roughjs or elements in Excalidraw; instead, we can focus on the pure shapes themselves
|
||||
*
|
||||
* also included in this file are methods for converting an Excalidraw element or a Drawable from roughjs
|
||||
* to pure shapes
|
||||
*/
|
||||
|
||||
import {
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawIframeElement,
|
||||
ExcalidrawImageElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawSelectionElement,
|
||||
ExcalidrawTextElement,
|
||||
} from "../../excalidraw/element/types";
|
||||
import { angleToDegrees, close, pointAdd, pointRotate } from "./geometry";
|
||||
import { pointsOnBezierCurves } from "points-on-curve";
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
|
||||
// a point is specified by its coordinate (x, y)
|
||||
export type Point = [number, number];
|
||||
export type Vector = Point;
|
||||
|
||||
// a line (segment) is defined by two endpoints
|
||||
export type Line = [Point, Point];
|
||||
|
||||
// a polyline (made up term here) is a line consisting of other line segments
|
||||
// this corresponds to a straight line element in the editor but it could also
|
||||
// be used to model other elements
|
||||
export type Polyline = Line[];
|
||||
|
||||
// cubic bezier curve with four control points
|
||||
export type Curve = [Point, Point, Point, Point];
|
||||
|
||||
// a polycurve is a curve consisting of ther curves, this corresponds to a complex
|
||||
// curve on the canvas
|
||||
export type Polycurve = Curve[];
|
||||
|
||||
// a polygon is a closed shape by connecting the given points
|
||||
// rectangles and diamonds are modelled by polygons
|
||||
export type Polygon = Point[];
|
||||
|
||||
// an ellipse is specified by its center, angle, and its major and minor axes
|
||||
// but for the sake of simplicity, we've used halfWidth and halfHeight instead
|
||||
// in replace of semi major and semi minor axes
|
||||
export type Ellipse = {
|
||||
center: Point;
|
||||
angle: number;
|
||||
halfWidth: number;
|
||||
halfHeight: number;
|
||||
};
|
||||
|
||||
export type GeometricShape =
|
||||
| {
|
||||
type: "line";
|
||||
data: Line;
|
||||
}
|
||||
| {
|
||||
type: "polygon";
|
||||
data: Polygon;
|
||||
}
|
||||
| {
|
||||
type: "curve";
|
||||
data: Curve;
|
||||
}
|
||||
| {
|
||||
type: "ellipse";
|
||||
data: Ellipse;
|
||||
}
|
||||
| {
|
||||
type: "polyline";
|
||||
data: Polyline;
|
||||
}
|
||||
| {
|
||||
type: "polycurve";
|
||||
data: Polycurve;
|
||||
};
|
||||
|
||||
type RectangularElement =
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawFrameLikeElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawIframeElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawSelectionElement;
|
||||
|
||||
// polygon
|
||||
export const getPolygonShape = (
|
||||
element: RectangularElement,
|
||||
): GeometricShape => {
|
||||
const { angle, width, height, x, y } = element;
|
||||
const angleInDegrees = angleToDegrees(angle);
|
||||
const cx = x + width / 2;
|
||||
const cy = y + height / 2;
|
||||
|
||||
const center: Point = [cx, cy];
|
||||
|
||||
let data: Polygon = [];
|
||||
|
||||
if (element.type === "diamond") {
|
||||
data = [
|
||||
pointRotate([cx, y], angleInDegrees, center),
|
||||
pointRotate([x + width, cy], angleInDegrees, center),
|
||||
pointRotate([cx, y + height], angleInDegrees, center),
|
||||
pointRotate([x, cy], angleInDegrees, center),
|
||||
] as Polygon;
|
||||
} else {
|
||||
data = [
|
||||
pointRotate([x, y], angleInDegrees, center),
|
||||
pointRotate([x + width, y], angleInDegrees, center),
|
||||
pointRotate([x + width, y + height], angleInDegrees, center),
|
||||
pointRotate([x, y + height], angleInDegrees, center),
|
||||
] as Polygon;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "polygon",
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
// ellipse
|
||||
export const getEllipseShape = (
|
||||
element: ExcalidrawEllipseElement,
|
||||
): GeometricShape => {
|
||||
const { width, height, angle, x, y } = element;
|
||||
|
||||
return {
|
||||
type: "ellipse",
|
||||
data: {
|
||||
center: [x + width / 2, y + height / 2],
|
||||
angle,
|
||||
halfWidth: width / 2,
|
||||
halfHeight: height / 2,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getCurvePathOps = (shape: Drawable): Op[] => {
|
||||
for (const set of shape.sets) {
|
||||
if (set.type === "path") {
|
||||
return set.ops;
|
||||
}
|
||||
}
|
||||
return shape.sets[0].ops;
|
||||
};
|
||||
|
||||
// linear
|
||||
export const getCurveShape = (
|
||||
roughShape: Drawable,
|
||||
startingPoint: Point = [0, 0],
|
||||
angleInRadian: number,
|
||||
center: Point,
|
||||
): GeometricShape => {
|
||||
const transform = (p: Point) =>
|
||||
pointRotate(
|
||||
[p[0] + startingPoint[0], p[1] + startingPoint[1]],
|
||||
angleToDegrees(angleInRadian),
|
||||
center,
|
||||
);
|
||||
|
||||
const ops = getCurvePathOps(roughShape);
|
||||
const polycurve: Polycurve = [];
|
||||
let p0: Point = [0, 0];
|
||||
|
||||
for (const op of ops) {
|
||||
if (op.op === "move") {
|
||||
p0 = transform(op.data as Point);
|
||||
}
|
||||
if (op.op === "bcurveTo") {
|
||||
const p1: Point = transform([op.data[0], op.data[1]]);
|
||||
const p2: Point = transform([op.data[2], op.data[3]]);
|
||||
const p3: Point = transform([op.data[4], op.data[5]]);
|
||||
polycurve.push([p0, p1, p2, p3]);
|
||||
p0 = p3;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "polycurve",
|
||||
data: polycurve,
|
||||
};
|
||||
};
|
||||
|
||||
const polylineFromPoints = (points: Point[]) => {
|
||||
let previousPoint = points[0];
|
||||
const polyline: Polyline = [];
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const nextPoint = points[i];
|
||||
polyline.push([previousPoint, nextPoint]);
|
||||
previousPoint = nextPoint;
|
||||
}
|
||||
|
||||
return polyline;
|
||||
};
|
||||
|
||||
export const getFreedrawShape = (
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
center: Point,
|
||||
isClosed: boolean = false,
|
||||
): GeometricShape => {
|
||||
const angle = angleToDegrees(element.angle);
|
||||
const transform = (p: Point) =>
|
||||
pointRotate(pointAdd(p, [element.x, element.y] as Point), angle, center);
|
||||
|
||||
const polyline = polylineFromPoints(
|
||||
element.points.map((p) => transform(p as Point)),
|
||||
);
|
||||
|
||||
return isClosed
|
||||
? {
|
||||
type: "polygon",
|
||||
data: close(polyline.flat()) as Polygon,
|
||||
}
|
||||
: {
|
||||
type: "polyline",
|
||||
data: polyline,
|
||||
};
|
||||
};
|
||||
|
||||
export const getClosedCurveShape = (
|
||||
roughShape: Drawable,
|
||||
startingPoint: Point = [0, 0],
|
||||
angleInRadian: number,
|
||||
center: Point,
|
||||
): GeometricShape => {
|
||||
const ops = getCurvePathOps(roughShape);
|
||||
const transform = (p: Point) =>
|
||||
pointRotate(
|
||||
[p[0] + startingPoint[0], p[1] + startingPoint[1]],
|
||||
angleToDegrees(angleInRadian),
|
||||
center,
|
||||
);
|
||||
|
||||
const points: Point[] = [];
|
||||
let odd = false;
|
||||
for (const operation of ops) {
|
||||
if (operation.op === "move") {
|
||||
odd = !odd;
|
||||
if (odd) {
|
||||
points.push([operation.data[0], operation.data[1]]);
|
||||
}
|
||||
} else if (operation.op === "bcurveTo") {
|
||||
if (odd) {
|
||||
points.push([operation.data[0], operation.data[1]]);
|
||||
points.push([operation.data[2], operation.data[3]]);
|
||||
points.push([operation.data[4], operation.data[5]]);
|
||||
}
|
||||
} else if (operation.op === "lineTo") {
|
||||
if (odd) {
|
||||
points.push([operation.data[0], operation.data[1]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
|
||||
transform(p),
|
||||
);
|
||||
|
||||
return {
|
||||
type: "polygon",
|
||||
data: polygonPoints,
|
||||
};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue