@tldraw/editor
Version:
tldraw infinite canvas SDK (editor).
375 lines (374 loc) • 13.4 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var Geometry2d_exports = {};
__export(Geometry2d_exports, {
Geometry2d: () => Geometry2d,
Geometry2dFilters: () => Geometry2dFilters,
TransformedGeometry2d: () => TransformedGeometry2d
});
module.exports = __toCommonJS(Geometry2d_exports);
var import_utils = require("@tldraw/utils");
var import_Box = require("../Box");
var import_Mat = require("../Mat");
var import_Vec = require("../Vec");
var import_intersect = require("../intersect");
var import_utils2 = require("../utils");
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) && (0, import_utils2.pointInPolygon)(point, this.vertices)) {
return true;
}
return import_Vec.Vec.Dist2(point, this.nearestPoint(point)) <= margin * margin;
}
distanceToPoint(point, hitInside = false, filters) {
return import_Vec.Vec.Dist(point, this.nearestPoint(point, filters)) * (this.isClosed && (this.isFilled || hitInside) && (0, import_utils2.pointInPolygon)(point, this.vertices) ? -1 : 1);
}
distanceToLineSegment(A, B, filters) {
if (import_Vec.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 ((0, import_intersect.linesIntersect)(A, B, p, next)) return 0;
}
q = import_Vec.Vec.NearestPointOnLineSegment(A, B, p, true);
d = import_Vec.Vec.Dist2(p, q);
if (d < dist) {
dist = d;
nearest = q;
}
}
if (!nearest) throw Error("nearest point not found");
return this.isClosed && this.isFilled && (0, import_utils2.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 ? (0, import_intersect.intersectLineSegmentPolygon)(A, B, this.vertices) : (0, import_intersect.intersectLineSegmentPolyline)(A, B, this.vertices);
return intersections ?? [];
}
intersectCircle(center, radius, _filters) {
const intersections = this.isClosed ? (0, import_intersect.intersectCirclePolygon)(center, radius, this.vertices) : (0, import_intersect.intersectCirclePolyline)(center, radius, this.vertices);
return intersections ?? [];
}
intersectPolygon(polygon, _filters) {
return (0, import_intersect.intersectPolys)(polygon, this.vertices, true, this.isClosed);
}
intersectPolyline(polyline, _filters) {
return (0, import_intersect.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 = import_Vec.Vec.Dist(curr, next);
const newDistanceTraveled = distanceTraveled + dist;
if (newDistanceTraveled >= distanceToTravel) {
const p = import_Vec.Vec.Lrp(
curr,
next,
(0, import_utils.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 = import_Vec.Vec.NearestPointOnLineSegment(curr, next, point, true);
const distance = import_Vec.Vec.Dist(nearestPoint, point);
if (distance < closestDistance) {
closestDistance = distance;
closestSegment = {
start: curr,
end: next,
nearestPoint,
distanceToStart: distanceTraveled
};
}
distanceTraveled += import_Vec.Vec.Dist(curr, next);
}
(0, import_utils.assert)(closestSegment);
const distanceAlongRoute = closestSegment.distanceToStart + import_Vec.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 = import_Vec.Vec.NearestPointOnLineSegment(A, B, p, true);
d = import_Vec.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 import_Box.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 += import_Vec.Vec.Dist(prev, next);
prev = next;
}
if (this.isClosed) {
length += import_Vec.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 = import_Mat.Mat.Inverse(matrix);
this.decomposed = import_Mat.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;
}
(0, import_utils.assert)(
(0, import_utils2.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) => import_Mat.Mat.applyToPoint(this.matrix, v));
}
nearestPoint(point, filters) {
return import_Mat.Mat.applyToPoint(
this.matrix,
this.geometry.nearestPoint(import_Mat.Mat.applyToPoint(this.inverse, point), filters)
);
}
hitTestPoint(point, margin = 0, hitInside, filters) {
return this.geometry.hitTestPoint(
import_Mat.Mat.applyToPoint(this.inverse, point),
margin / this.decomposed.scaleX,
hitInside,
filters
);
}
distanceToPoint(point, hitInside = false, filters) {
return this.geometry.distanceToPoint(import_Mat.Mat.applyToPoint(this.inverse, point), hitInside, filters) * this.decomposed.scaleX;
}
distanceToLineSegment(A, B, filters) {
return this.geometry.distanceToLineSegment(
import_Mat.Mat.applyToPoint(this.inverse, A),
import_Mat.Mat.applyToPoint(this.inverse, B),
filters
) * this.decomposed.scaleX;
}
hitTestLineSegment(A, B, distance = 0, filters) {
return this.geometry.hitTestLineSegment(
import_Mat.Mat.applyToPoint(this.inverse, A),
import_Mat.Mat.applyToPoint(this.inverse, B),
distance / this.decomposed.scaleX,
filters
);
}
intersectLineSegment(A, B, filters) {
return import_Mat.Mat.applyToPoints(
this.matrix,
this.geometry.intersectLineSegment(
import_Mat.Mat.applyToPoint(this.inverse, A),
import_Mat.Mat.applyToPoint(this.inverse, B),
filters
)
);
}
intersectCircle(center, radius, filters) {
return import_Mat.Mat.applyToPoints(
this.matrix,
this.geometry.intersectCircle(
import_Mat.Mat.applyToPoint(this.inverse, center),
radius / this.decomposed.scaleX,
filters
)
);
}
intersectPolygon(polygon, filters) {
return import_Mat.Mat.applyToPoints(
this.matrix,
this.geometry.intersectPolygon(import_Mat.Mat.applyToPoints(this.inverse, polygon), filters)
);
}
intersectPolyline(polyline, filters) {
return import_Mat.Mat.applyToPoints(
this.matrix,
this.geometry.intersectPolyline(import_Mat.Mat.applyToPoints(this.inverse, polyline), filters)
);
}
transform(transform, opts) {
return new TransformedGeometry2d(this.geometry, import_Mat.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.");
}
}
//# sourceMappingURL=Geometry2d.js.map