@tldraw/editor
Version:
tldraw infinite canvas SDK (editor).
362 lines (361 loc) • 11.9 kB
JavaScript
import { assert, invLerp } from "@tldraw/utils";
import { Box } from "../Box.mjs";
import { Mat } from "../Mat.mjs";
import { Vec } from "../Vec.mjs";
import {
intersectCirclePolygon,
intersectCirclePolyline,
intersectLineSegmentPolygon,
intersectLineSegmentPolyline,
intersectPolys,
linesIntersect
} from "../intersect.mjs";
import { approximately, pointInPolygon } from "../utils.mjs";
const Geometry2dFilters = {
EXCLUDE_NON_STANDARD: {
includeLabels: false,
includeInternal: false
},
INCLUDE_ALL: { includeLabels: true, includeInternal: true },
EXCLUDE_LABELS: { includeLabels: false, includeInternal: true },
EXCLUDE_INTERNAL: { includeLabels: true, includeInternal: false }
};
class Geometry2d {
// todo: consider making accessors for these too, so that they can be overridden in subclasses by geometries with more complex logic
isFilled = false;
isClosed = true;
isLabel = false;
isEmptyLabel = false;
isInternal = false;
debugColor;
ignore;
constructor(opts) {
const { isLabel = false, isEmptyLabel = false, isInternal = false } = opts;
this.isFilled = opts.isFilled;
this.isClosed = opts.isClosed;
this.debugColor = opts.debugColor;
this.ignore = opts.ignore;
this.isLabel = isLabel;
this.isEmptyLabel = isEmptyLabel;
this.isInternal = isInternal;
}
isExcludedByFilter(filters) {
if (!filters) return false;
if (this.isLabel && !filters.includeLabels) return true;
if (this.isInternal && !filters.includeInternal) return true;
return false;
}
hitTestPoint(point, margin = 0, hitInside = false, _filters) {
if (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)) {
return true;
}
return Vec.Dist2(point, this.nearestPoint(point)) <= margin * margin;
}
distanceToPoint(point, hitInside = false, filters) {
return Vec.Dist(point, this.nearestPoint(point, filters)) * (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices) ? -1 : 1);
}
distanceToLineSegment(A, B, filters) {
if (Vec.Equals(A, B)) return this.distanceToPoint(A, false, filters);
const { vertices } = this;
let nearest;
let dist = Infinity;
let d, p, q;
const nextLimit = this.isClosed ? vertices.length : vertices.length - 1;
for (let i = 0; i < vertices.length; i++) {
p = vertices[i];
if (i < nextLimit) {
const next = vertices[(i + 1) % vertices.length];
if (linesIntersect(A, B, p, next)) return 0;
}
q = Vec.NearestPointOnLineSegment(A, B, p, true);
d = Vec.Dist2(p, q);
if (d < dist) {
dist = d;
nearest = q;
}
}
if (!nearest) throw Error("nearest point not found");
return this.isClosed && this.isFilled && pointInPolygon(nearest, this.vertices) ? -dist : dist;
}
hitTestLineSegment(A, B, distance = 0, filters) {
return this.distanceToLineSegment(A, B, filters) <= distance;
}
intersectLineSegment(A, B, _filters) {
const intersections = this.isClosed ? intersectLineSegmentPolygon(A, B, this.vertices) : intersectLineSegmentPolyline(A, B, this.vertices);
return intersections ?? [];
}
intersectCircle(center, radius, _filters) {
const intersections = this.isClosed ? intersectCirclePolygon(center, radius, this.vertices) : intersectCirclePolyline(center, radius, this.vertices);
return intersections ?? [];
}
intersectPolygon(polygon, _filters) {
return intersectPolys(polygon, this.vertices, true, this.isClosed);
}
intersectPolyline(polyline, _filters) {
return intersectPolys(polyline, this.vertices, false, this.isClosed);
}
/**
* Find a point along the edge of the geometry that is a fraction `t` along the entire way round.
*/
interpolateAlongEdge(t, _filters) {
const { vertices } = this;
if (t <= 0) return vertices[0];
const distanceToTravel = t * this.length;
let distanceTraveled = 0;
for (let i = 0; i < (this.isClosed ? vertices.length : vertices.length - 1); i++) {
const curr = vertices[i];
const next = vertices[(i + 1) % vertices.length];
const dist = Vec.Dist(curr, next);
const newDistanceTraveled = distanceTraveled + dist;
if (newDistanceTraveled >= distanceToTravel) {
const p = Vec.Lrp(
curr,
next,
invLerp(distanceTraveled, newDistanceTraveled, distanceToTravel)
);
return p;
}
distanceTraveled = newDistanceTraveled;
}
return this.isClosed ? vertices[0] : vertices[vertices.length - 1];
}
/**
* Take `point`, find the closest point to it on the edge of the geometry, and return how far
* along the edge it is as a fraction of the total length.
*/
uninterpolateAlongEdge(point, _filters) {
const { vertices, length } = this;
let closestSegment = null;
let closestDistance = Infinity;
let distanceTraveled = 0;
for (let i = 0; i < (this.isClosed ? vertices.length : vertices.length - 1); i++) {
const curr = vertices[i];
const next = vertices[(i + 1) % vertices.length];
const nearestPoint = Vec.NearestPointOnLineSegment(curr, next, point, true);
const distance = Vec.Dist(nearestPoint, point);
if (distance < closestDistance) {
closestDistance = distance;
closestSegment = {
start: curr,
end: next,
nearestPoint,
distanceToStart: distanceTraveled
};
}
distanceTraveled += Vec.Dist(curr, next);
}
assert(closestSegment);
const distanceAlongRoute = closestSegment.distanceToStart + Vec.Dist(closestSegment.start, closestSegment.nearestPoint);
return distanceAlongRoute / length;
}
/** @deprecated Iterate the vertices instead. */
nearestPointOnLineSegment(A, B) {
const { vertices } = this;
let nearest;
let dist = Infinity;
let d, p, q;
for (let i = 0; i < vertices.length; i++) {
p = vertices[i];
q = Vec.NearestPointOnLineSegment(A, B, p, true);
d = Vec.Dist2(p, q);
if (d < dist) {
dist = d;
nearest = q;
}
}
if (!nearest) throw Error("nearest point not found");
return nearest;
}
isPointInBounds(point, margin = 0) {
const { bounds } = this;
return !(point.x < bounds.minX - margin || point.y < bounds.minY - margin || point.x > bounds.maxX + margin || point.y > bounds.maxY + margin);
}
transform(transform, opts) {
return new TransformedGeometry2d(this, transform, opts);
}
_vertices;
// eslint-disable-next-line no-restricted-syntax
get vertices() {
if (!this._vertices) {
this._vertices = this.getVertices(Geometry2dFilters.EXCLUDE_LABELS);
}
return this._vertices;
}
getBounds() {
return Box.FromPoints(this.vertices);
}
_bounds;
// eslint-disable-next-line no-restricted-syntax
get bounds() {
if (!this._bounds) {
this._bounds = this.getBounds();
}
return this._bounds;
}
// eslint-disable-next-line no-restricted-syntax
get center() {
return this.bounds.center;
}
_area;
// eslint-disable-next-line no-restricted-syntax
get area() {
if (!this._area) {
this._area = this.getArea();
}
return this._area;
}
getArea() {
if (!this.isClosed) {
return 0;
}
const { vertices } = this;
let area = 0;
for (let i = 0, n = vertices.length; i < n; i++) {
const curr = vertices[i];
const next = vertices[(i + 1) % n];
area += curr.x * next.y - next.x * curr.y;
}
return area / 2;
}
toSimpleSvgPath() {
let path = "";
const { vertices } = this;
const n = vertices.length;
if (n === 0) return path;
path += `M${vertices[0].x},${vertices[0].y}`;
for (let i = 1; i < n; i++) {
path += `L${vertices[i].x},${vertices[i].y}`;
}
if (this.isClosed) {
path += "Z";
}
return path;
}
_length;
// eslint-disable-next-line no-restricted-syntax
get length() {
if (this._length) return this._length;
this._length = this.getLength(Geometry2dFilters.EXCLUDE_LABELS);
return this._length;
}
getLength(_filters) {
const vertices = this.getVertices(_filters ?? Geometry2dFilters.EXCLUDE_LABELS);
if (vertices.length === 0) return 0;
let prev = vertices[0];
let length = 0;
for (let i = 1; i < vertices.length; i++) {
const next = vertices[i];
length += Vec.Dist(prev, next);
prev = next;
}
if (this.isClosed) {
length += Vec.Dist(vertices[vertices.length - 1], vertices[0]);
}
return length;
}
}
class TransformedGeometry2d extends Geometry2d {
constructor(geometry, matrix, opts) {
super(geometry);
this.geometry = geometry;
this.matrix = matrix;
this.inverse = Mat.Inverse(matrix);
this.decomposed = Mat.Decompose(matrix);
if (opts) {
if (opts.isLabel != null) this.isLabel = opts.isLabel;
if (opts.isInternal != null) this.isInternal = opts.isInternal;
if (opts.debugColor != null) this.debugColor = opts.debugColor;
if (opts.ignore != null) this.ignore = opts.ignore;
}
assert(
approximately(this.decomposed.scaleX, this.decomposed.scaleY),
"non-uniform scaling is not yet supported"
);
}
inverse;
decomposed;
getVertices(filters) {
return this.geometry.getVertices(filters).map((v) => Mat.applyToPoint(this.matrix, v));
}
nearestPoint(point, filters) {
return Mat.applyToPoint(
this.matrix,
this.geometry.nearestPoint(Mat.applyToPoint(this.inverse, point), filters)
);
}
hitTestPoint(point, margin = 0, hitInside, filters) {
return this.geometry.hitTestPoint(
Mat.applyToPoint(this.inverse, point),
margin / this.decomposed.scaleX,
hitInside,
filters
);
}
distanceToPoint(point, hitInside = false, filters) {
return this.geometry.distanceToPoint(Mat.applyToPoint(this.inverse, point), hitInside, filters) * this.decomposed.scaleX;
}
distanceToLineSegment(A, B, filters) {
return this.geometry.distanceToLineSegment(
Mat.applyToPoint(this.inverse, A),
Mat.applyToPoint(this.inverse, B),
filters
) * this.decomposed.scaleX;
}
hitTestLineSegment(A, B, distance = 0, filters) {
return this.geometry.hitTestLineSegment(
Mat.applyToPoint(this.inverse, A),
Mat.applyToPoint(this.inverse, B),
distance / this.decomposed.scaleX,
filters
);
}
intersectLineSegment(A, B, filters) {
return Mat.applyToPoints(
this.matrix,
this.geometry.intersectLineSegment(
Mat.applyToPoint(this.inverse, A),
Mat.applyToPoint(this.inverse, B),
filters
)
);
}
intersectCircle(center, radius, filters) {
return Mat.applyToPoints(
this.matrix,
this.geometry.intersectCircle(
Mat.applyToPoint(this.inverse, center),
radius / this.decomposed.scaleX,
filters
)
);
}
intersectPolygon(polygon, filters) {
return Mat.applyToPoints(
this.matrix,
this.geometry.intersectPolygon(Mat.applyToPoints(this.inverse, polygon), filters)
);
}
intersectPolyline(polyline, filters) {
return Mat.applyToPoints(
this.matrix,
this.geometry.intersectPolyline(Mat.applyToPoints(this.inverse, polyline), filters)
);
}
transform(transform, opts) {
return new TransformedGeometry2d(this.geometry, Mat.Multiply(transform, this.matrix), {
isLabel: opts?.isLabel ?? this.isLabel,
isInternal: opts?.isInternal ?? this.isInternal,
debugColor: opts?.debugColor ?? this.debugColor,
ignore: opts?.ignore ?? this.ignore
});
}
getSvgPathData() {
throw new Error("Cannot get SVG path data for transformed geometry.");
}
}
export {
Geometry2d,
Geometry2dFilters,
TransformedGeometry2d
};
//# sourceMappingURL=Geometry2d.mjs.map