kopia lustrzana https://github.com/Tldraw/Tldraw
685 wiersze
16 KiB
TypeScript
685 wiersze
16 KiB
TypeScript
import { Box2d } from './Box2d'
|
|
import { Vec2d, VecLike } from './Vec2d'
|
|
|
|
/** @public */
|
|
export const PI = Math.PI
|
|
/** @public */
|
|
export const TAU = PI / 2
|
|
/** @public */
|
|
export const PI2 = PI * 2
|
|
/** @public */
|
|
export const EPSILON = Math.PI / 180
|
|
/** @public */
|
|
export const SIN = Math.sin
|
|
|
|
/**
|
|
* Clamp a value into a range.
|
|
*
|
|
* @example
|
|
*
|
|
* ```ts
|
|
* const A = clamp(0, 1) // 1
|
|
* ```
|
|
*
|
|
* @param n - The number to clamp.
|
|
* @param min - The minimum value.
|
|
* @public
|
|
*/
|
|
export function clamp(n: number, min: number): number
|
|
/**
|
|
* Clamp a value into a range.
|
|
*
|
|
* @example
|
|
*
|
|
* ```ts
|
|
* const A = clamp(0, 1, 10) // 1
|
|
* const B = clamp(11, 1, 10) // 10
|
|
* const C = clamp(5, 1, 10) // 5
|
|
* ```
|
|
*
|
|
* @param n - The number to clamp.
|
|
* @param min - The minimum value.
|
|
* @param max - The maximum value.
|
|
* @public
|
|
*/
|
|
export function clamp(n: number, min: number, max: number): number
|
|
export function clamp(n: number, min: number, max?: number): number {
|
|
return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
|
|
}
|
|
|
|
/**
|
|
* Get a number to a precision.
|
|
*
|
|
* @param n - The number.
|
|
* @param precision - The precision.
|
|
* @public
|
|
*/
|
|
export function toPrecision(n: number, precision = 10000000000) {
|
|
if (!n) return 0
|
|
return Math.round(n * precision) / precision
|
|
}
|
|
|
|
/**
|
|
* Whether two numbers numbers a and b are approximately equal.
|
|
*
|
|
* @param a - The first point.
|
|
* @param b - The second point.
|
|
* @public
|
|
*/
|
|
export function approximately(a: number, b: number, precision = 0.000001) {
|
|
return Math.abs(a - b) <= precision
|
|
}
|
|
|
|
/**
|
|
* Find the approximate perimeter of an ellipse.
|
|
*
|
|
* @param rx - The ellipse's x radius.
|
|
* @param ry - The ellipse's y radius.
|
|
* @public
|
|
*/
|
|
export function perimeterOfEllipse(rx: number, ry: number): number {
|
|
const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2)
|
|
const p = PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h)))
|
|
return p
|
|
}
|
|
|
|
/**
|
|
* @param a - Any angle in radians
|
|
* @returns A number between 0 and 2 * PI
|
|
* @public
|
|
*/
|
|
export function canonicalizeRotation(a: number) {
|
|
a = a % PI2
|
|
if (a < 0) {
|
|
a = a + PI2
|
|
} else if (a === 0) {
|
|
// prevent negative zero
|
|
a = 0
|
|
}
|
|
return a
|
|
}
|
|
|
|
/**
|
|
* Get the short angle distance between two angles.
|
|
*
|
|
* @param a0 - The first angle.
|
|
* @param a1 - The second angle.
|
|
* @public
|
|
*/
|
|
export function shortAngleDist(a0: number, a1: number): number {
|
|
const da = (a1 - a0) % PI2
|
|
return ((2 * da) % PI2) - da
|
|
}
|
|
|
|
/**
|
|
* Get the long angle distance between two angles.
|
|
*
|
|
* @param a0 - The first angle.
|
|
* @param a1 - The second angle.
|
|
* @public
|
|
*/
|
|
export function longAngleDist(a0: number, a1: number): number {
|
|
return PI2 - shortAngleDist(a0, a1)
|
|
}
|
|
|
|
/**
|
|
* Interpolate an angle between two angles.
|
|
*
|
|
* @param a0 - The first angle.
|
|
* @param a1 - The second angle.
|
|
* @param t - The interpolation value.
|
|
* @public
|
|
*/
|
|
export function lerpAngles(a0: number, a1: number, t: number): number {
|
|
return a0 + shortAngleDist(a0, a1) * t
|
|
}
|
|
|
|
/**
|
|
* Get the short distance between two angles.
|
|
*
|
|
* @param a0 - The first angle.
|
|
* @param a1 - The second angle.
|
|
* @public
|
|
*/
|
|
export function angleDelta(a0: number, a1: number): number {
|
|
return shortAngleDist(a0, a1)
|
|
}
|
|
|
|
/**
|
|
* Get the "sweep" or short distance between two points on a circle's perimeter.
|
|
*
|
|
* @param C - The center of the circle.
|
|
* @param A - The first point.
|
|
* @param B - The second point.
|
|
* @public
|
|
*/
|
|
export function getSweep(C: VecLike, A: VecLike, B: VecLike): number {
|
|
return angleDelta(Vec2d.Angle(C, A), Vec2d.Angle(C, B))
|
|
}
|
|
|
|
/**
|
|
* Clamp radians within 0 and 2PI
|
|
*
|
|
* @param r - The radian value.
|
|
* @public
|
|
*/
|
|
export function clampRadians(r: number): number {
|
|
return (PI2 + r) % PI2
|
|
}
|
|
|
|
/**
|
|
* Clamp rotation to even segments.
|
|
*
|
|
* @param r - The rotation in radians.
|
|
* @param segments - The number of segments.
|
|
* @public
|
|
*/
|
|
export function snapAngle(r: number, segments: number): number {
|
|
const seg = PI2 / segments
|
|
let ang = (Math.floor((clampRadians(r) + seg / 2) / seg) * seg) % PI2
|
|
if (ang < PI) ang += PI2
|
|
if (ang > PI) ang -= PI2
|
|
return ang
|
|
}
|
|
|
|
/**
|
|
* Checks whether two angles are approximately at right-angles or parallel to each other
|
|
*
|
|
* @param a - Angle a (radians)
|
|
* @param b - Angle b (radians)
|
|
* @returns True iff the angles are approximately at right-angles or parallel to each other
|
|
* @public
|
|
*/
|
|
export function areAnglesCompatible(a: number, b: number) {
|
|
return a === b || approximately((a % (Math.PI / 2)) - (b % (Math.PI / 2)), 0)
|
|
}
|
|
|
|
/**
|
|
* Is angle c between angles a and b?
|
|
*
|
|
* @param a - The first angle.
|
|
* @param b - The second angle.
|
|
* @param c - The third angle.
|
|
* @public
|
|
*/
|
|
export function isAngleBetween(a: number, b: number, c: number): boolean {
|
|
if (c === a || c === b) return true
|
|
|
|
const AB = (b - a + TAU) % TAU
|
|
const AC = (c - a + TAU) % TAU
|
|
return AB <= PI !== AC > AB
|
|
}
|
|
|
|
/**
|
|
* Convert degrees to radians.
|
|
*
|
|
* @param d - The degree in degrees.
|
|
* @public
|
|
*/
|
|
export function degreesToRadians(d: number): number {
|
|
return (d * PI) / 180
|
|
}
|
|
|
|
/**
|
|
* Convert radians to degrees.
|
|
*
|
|
* @param r - The degree in radians.
|
|
* @public
|
|
*/
|
|
export function radiansToDegrees(r: number): number {
|
|
return (r * 180) / PI
|
|
}
|
|
|
|
/**
|
|
* Get the length of an arc between two points on a circle's perimeter.
|
|
*
|
|
* @param C - The circle's center as [x, y].
|
|
* @param r - The circle's radius.
|
|
* @param A - The first point.
|
|
* @param B - The second point.
|
|
* @public
|
|
*/
|
|
export function getArcLength(C: VecLike, r: number, A: VecLike, B: VecLike): number {
|
|
const sweep = getSweep(C, A, B)
|
|
return r * PI2 * (sweep / PI2)
|
|
}
|
|
|
|
/**
|
|
* Get a point on the perimeter of a circle.
|
|
*
|
|
* @param cx - The center x of the circle.
|
|
* @param cy - The center y of the circle.
|
|
* @param r - The radius of the circle.
|
|
* @param a - The normalized point on the circle.
|
|
* @public
|
|
*/
|
|
export function getPointOnCircle(cx: number, cy: number, r: number, a: number) {
|
|
return new Vec2d(cx + r * Math.cos(a), cy + r * Math.sin(a))
|
|
}
|
|
/** @public */
|
|
export function getPolygonVertices(width: number, height: number, sides: number) {
|
|
const cx = width / 2
|
|
const cy = height / 2
|
|
const pointsOnPerimeter = []
|
|
for (let i = 0; i < sides; i++) {
|
|
const step = PI2 / sides
|
|
const t = -TAU + i * step
|
|
pointsOnPerimeter.push(new Vec2d(cx + cx * Math.cos(t), cy + cy * Math.sin(t)))
|
|
}
|
|
return pointsOnPerimeter
|
|
}
|
|
|
|
/**
|
|
* @param a0 - The start point in the A range
|
|
* @param a1 - The end point in the A range
|
|
* @param b0 - The start point in the B range
|
|
* @param b1 - The end point in the B range
|
|
* @returns True if the ranges overlap
|
|
* @public
|
|
*/
|
|
export function rangesOverlap(a0: number, a1: number, b0: number, b1: number): boolean {
|
|
return a0 < b1 && b0 < a1
|
|
}
|
|
|
|
/**
|
|
* Finds the intersection of two ranges.
|
|
*
|
|
* @param a0 - The start point in the A range
|
|
* @param a1 - The end point in the A range
|
|
* @param b0 - The start point in the B range
|
|
* @param b1 - The end point in the B range
|
|
* @returns The intersection of the ranges, or null if no intersection
|
|
* @public
|
|
*/
|
|
export function rangeIntersection(
|
|
a0: number,
|
|
a1: number,
|
|
b0: number,
|
|
b1: number
|
|
): [number, number] | null {
|
|
const min = Math.max(a0, b0)
|
|
const max = Math.min(a1, b1)
|
|
if (min <= max) {
|
|
return [min, max]
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Gets the width/height of a star given its input bounds.
|
|
*
|
|
* @param sides - Number of sides
|
|
* @param w - T target width
|
|
* @param h - Target height
|
|
* @returns Box2d
|
|
* @public
|
|
*/
|
|
export const getStarBounds = (sides: number, w: number, h: number): Box2d => {
|
|
const step = PI2 / sides / 2
|
|
const rightMostIndex = Math.floor(sides / 4) * 2
|
|
const leftMostIndex = sides * 2 - rightMostIndex
|
|
const topMostIndex = 0
|
|
const bottomMostIndex = Math.floor(sides / 2) * 2
|
|
const maxX = (Math.cos(-TAU + rightMostIndex * step) * w) / 2
|
|
const minX = (Math.cos(-TAU + leftMostIndex * step) * w) / 2
|
|
const minY = (Math.sin(-TAU + topMostIndex * step) * h) / 2
|
|
const maxY = (Math.sin(-TAU + bottomMostIndex * step) * h) / 2
|
|
return new Box2d(0, 0, maxX - minX, maxY - minY)
|
|
}
|
|
|
|
/** Helper for point in polygon */
|
|
function cross(x: VecLike, y: VecLike, z: VecLike): number {
|
|
return (y.x - x.x) * (z.y - x.y) - (z.x - x.x) * (y.y - x.y)
|
|
}
|
|
|
|
/**
|
|
* Utils for working with points.
|
|
*
|
|
* @public
|
|
*/
|
|
/**
|
|
* Get whether a point is inside of a circle.
|
|
*
|
|
* @param A - The point to check.
|
|
* @param C - The circle's center point as [x, y].
|
|
* @param r - The circle's radius.
|
|
* @returns Boolean
|
|
* @public
|
|
*/
|
|
export function pointInCircle(A: VecLike, C: VecLike, r: number): boolean {
|
|
return Vec2d.Dist(A, C) <= r
|
|
}
|
|
|
|
/**
|
|
* Get whether a point is inside of an ellipse.
|
|
*
|
|
* @param point - The point to check.
|
|
* @param center - The ellipse's center point as [x, y].
|
|
* @param rx - The ellipse's x radius.
|
|
* @param ry - The ellipse's y radius.
|
|
* @param rotation - The ellipse's rotation.
|
|
* @returns Boolean
|
|
* @public
|
|
*/
|
|
export function pointInEllipse(
|
|
A: VecLike,
|
|
C: VecLike,
|
|
rx: number,
|
|
ry: number,
|
|
rotation = 0
|
|
): boolean {
|
|
rotation = rotation || 0
|
|
const cos = Math.cos(rotation)
|
|
const sin = Math.sin(rotation)
|
|
const delta = Vec2d.Sub(A, C)
|
|
const tdx = cos * delta.x + sin * delta.y
|
|
const tdy = sin * delta.x - cos * delta.y
|
|
|
|
return (tdx * tdx) / (rx * rx) + (tdy * tdy) / (ry * ry) <= 1
|
|
}
|
|
|
|
/**
|
|
* Get whether a point is inside of a rectangle.
|
|
*
|
|
* @param A - The point to check.
|
|
* @param point - The rectangle's top left point as [x, y].
|
|
* @param size - The rectangle's size as [width, height].
|
|
* @public
|
|
*/
|
|
export function pointInRect(A: VecLike, point: VecLike, size: VecLike): boolean {
|
|
return !(A.x < point.x || A.x > point.x + size.x || A.y < point.y || A.y > point.y + size.y)
|
|
}
|
|
|
|
/**
|
|
* Get whether a point is inside of a polygon.
|
|
*
|
|
* ```ts
|
|
* const result = pointInPolygon(myPoint, myPoints)
|
|
* ```
|
|
*
|
|
* @public
|
|
*/
|
|
export function pointInPolygon(A: VecLike, points: VecLike[]): boolean {
|
|
let windingNumber = 0
|
|
let a: VecLike
|
|
let b: VecLike
|
|
|
|
for (let i = 0; i < points.length; i++) {
|
|
a = points[i]
|
|
b = points[(i + 1) % points.length]
|
|
|
|
if (a.y <= A.y) {
|
|
if (b.y > A.y && cross(a, b, A) > 0) {
|
|
windingNumber += 1
|
|
}
|
|
} else if (b.y <= A.y && cross(a, b, A) < 0) {
|
|
windingNumber -= 1
|
|
}
|
|
}
|
|
|
|
return windingNumber !== 0
|
|
}
|
|
|
|
/**
|
|
* Get whether a point is inside of a bounds.
|
|
*
|
|
* @param A - The point to check.
|
|
* @param b - The bounds to check.
|
|
* @returns Boolean
|
|
* @public
|
|
*/
|
|
export function pointInBounds(A: VecLike, b: Box2d): boolean {
|
|
return !(A.x < b.minX || A.x > b.maxX || A.y < b.minY || A.y > b.maxY)
|
|
}
|
|
|
|
/**
|
|
* Hit test a point and a polyline using a minimum distance.
|
|
*
|
|
* @param A - The point to check.
|
|
* @param points - The points that make up the polyline.
|
|
* @param distance - The mininum distance that qualifies a hit.
|
|
* @returns Boolean
|
|
* @public
|
|
*/
|
|
export function pointInPolyline(A: VecLike, points: VecLike[], distance = 3): boolean {
|
|
for (let i = 1; i < points.length; i++) {
|
|
if (Vec2d.DistanceToLineSegment(points[i - 1], points[i], A) < distance) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Get whether a point is within a certain distance from a polyline.
|
|
*
|
|
* @param A - The point to check.
|
|
* @param points - The points that make up the polyline.
|
|
* @param distance - The mininum distance that qualifies a hit.
|
|
* @public
|
|
*/
|
|
export function pointNearToPolyline(A: VecLike, points: VecLike[], distance = 8) {
|
|
const len = points.length
|
|
for (let i = 1; i < len; i++) {
|
|
const p1 = points[i - 1]
|
|
const p2 = points[i]
|
|
const d = Vec2d.DistanceToLineSegment(p1, p2, A)
|
|
if (d < distance) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Get whether a point is within a certain distance from a line segment.
|
|
*
|
|
* @param A - The point to check.
|
|
* @param p1 - The polyline's first point.
|
|
* @param p2 - The polyline's second point.
|
|
* @param distance - The mininum distance that qualifies a hit.
|
|
* @public
|
|
*/
|
|
export function pointNearToLineSegment(A: VecLike, p1: VecLike, p2: VecLike, distance = 8) {
|
|
const d = Vec2d.DistanceToLineSegment(p1, p2, A)
|
|
if (d < distance) return true
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Simplify a line (using Ramer-Douglas-Peucker algorithm).
|
|
*
|
|
* @param points - An array of points as [x, y, ...][]
|
|
* @param tolerance - The minimum line distance (also called epsilon).
|
|
* @returns Simplified array as [x, y, ...][]
|
|
* @public
|
|
*/
|
|
export function simplify(points: VecLike[], tolerance = 1): VecLike[] {
|
|
const len = points.length
|
|
const a = points[0]
|
|
const b = points[len - 1]
|
|
const { x: x1, y: y1 } = a
|
|
const { x: x2, y: y2 } = b
|
|
if (len > 2) {
|
|
let distance = 0
|
|
let index = 0
|
|
const max = new Vec2d(y2 - y1, x2 - x1).len2()
|
|
for (let i = 1; i < len - 1; i++) {
|
|
const { x: x0, y: y0 } = points[i]
|
|
const d = Math.pow(x0 * (y2 - y1) + x1 * (y0 - y2) + x2 * (y1 - y0), 2) / max
|
|
if (distance > d) continue
|
|
distance = d
|
|
index = i
|
|
}
|
|
if (distance > tolerance) {
|
|
const l0 = simplify(points.slice(0, index + 1), tolerance)
|
|
const l1 = simplify(points.slice(index + 1), tolerance)
|
|
return l0.concat(l1.slice(1))
|
|
}
|
|
}
|
|
return [a, b]
|
|
}
|
|
|
|
function _getSqSegDist(p: VecLike, p1: VecLike, p2: VecLike) {
|
|
let x = p1.x
|
|
let y = p1.y
|
|
let dx = p2.x - x
|
|
let dy = p2.y - y
|
|
if (dx !== 0 || dy !== 0) {
|
|
const t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy)
|
|
if (t > 1) {
|
|
x = p2.x
|
|
y = p2.y
|
|
} else if (t > 0) {
|
|
x += dx * t
|
|
y += dy * t
|
|
}
|
|
}
|
|
dx = p.x - x
|
|
dy = p.y - y
|
|
return dx * dx + dy * dy
|
|
}
|
|
|
|
function _simplifyStep(
|
|
points: VecLike[],
|
|
first: number,
|
|
last: number,
|
|
sqTolerance: number,
|
|
result: VecLike[]
|
|
) {
|
|
let maxSqDist = sqTolerance
|
|
let index = -1
|
|
for (let i = first + 1; i < last; i++) {
|
|
const sqDist = _getSqSegDist(points[i], points[first], points[last])
|
|
if (sqDist > maxSqDist) {
|
|
index = i
|
|
maxSqDist = sqDist
|
|
}
|
|
}
|
|
if (index > -1 && maxSqDist > sqTolerance) {
|
|
if (index - first > 1) _simplifyStep(points, first, index, sqTolerance, result)
|
|
result.push(points[index])
|
|
if (last - index > 1) _simplifyStep(points, index, last, sqTolerance, result)
|
|
}
|
|
}
|
|
|
|
/** @public */
|
|
export function simplify2(points: VecLike[], tolerance = 1) {
|
|
if (points.length <= 2) return points
|
|
const sqTolerance = tolerance * tolerance
|
|
// Radial distance
|
|
let A = points[0]
|
|
let B = points[1]
|
|
const newPoints = [A]
|
|
for (let i = 1, len = points.length; i < len; i++) {
|
|
B = points[i]
|
|
if ((B.x - A.x) * (B.x - A.x) + (B.y - A.y) * (B.y - A.y) > sqTolerance) {
|
|
newPoints.push(B)
|
|
A = B
|
|
}
|
|
}
|
|
if (A !== B) newPoints.push(B)
|
|
// Ramer-Douglas-Peucker
|
|
const last = newPoints.length - 1
|
|
const result = [newPoints[0]]
|
|
_simplifyStep(newPoints, 0, last, sqTolerance, result)
|
|
result.push(newPoints[last], points[points.length - 1])
|
|
return result
|
|
}
|
|
|
|
/** @public */
|
|
export function getMinX(pts: VecLike[]) {
|
|
let top = pts[0]
|
|
for (let i = 1; i < pts.length; i++) {
|
|
if (pts[i].x < top.x) {
|
|
top = pts[i]
|
|
}
|
|
}
|
|
return top.x
|
|
}
|
|
|
|
/** @public */
|
|
export function getMinY(pts: VecLike[]) {
|
|
let top = pts[0]
|
|
for (let i = 1; i < pts.length; i++) {
|
|
if (pts[i].y < top.y) {
|
|
top = pts[i]
|
|
}
|
|
}
|
|
return top.y
|
|
}
|
|
|
|
/** @public */
|
|
export function getMaxX(pts: VecLike[]) {
|
|
let top = pts[0]
|
|
for (let i = 1; i < pts.length; i++) {
|
|
if (pts[i].x > top.x) {
|
|
top = pts[i]
|
|
}
|
|
}
|
|
return top.x
|
|
}
|
|
|
|
/** @public */
|
|
export function getMaxY(pts: VecLike[]) {
|
|
let top = pts[0]
|
|
for (let i = 1; i < pts.length; i++) {
|
|
if (pts[i].y > top.y) {
|
|
top = pts[i]
|
|
}
|
|
}
|
|
return top.y
|
|
}
|
|
|
|
/** @public */
|
|
export function getMidX(pts: VecLike[]) {
|
|
const a = getMinX(pts)
|
|
const b = getMaxX(pts)
|
|
|
|
return a + (b - a) / 2
|
|
}
|
|
|
|
/** @public */
|
|
export function getMidY(pts: VecLike[]) {
|
|
const a = getMinY(pts)
|
|
const b = getMaxY(pts)
|
|
|
|
return a + (b - a) / 2
|
|
}
|
|
|
|
/** @public */
|
|
export function getWidth(pts: VecLike[]) {
|
|
const a = getMinX(pts)
|
|
const b = getMaxX(pts)
|
|
return b - a
|
|
}
|
|
|
|
/** @public */
|
|
export function getHeight(pts: VecLike[]) {
|
|
const a = getMinY(pts)
|
|
const b = getMaxY(pts)
|
|
return b - a
|
|
}
|
|
|
|
/**
|
|
* The DOM likes values to be fixed to 3 decimal places
|
|
*
|
|
* @public
|
|
*/
|
|
export function toDomPrecision(v: number) {
|
|
return +v.toFixed(4)
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function toFixed(v: number) {
|
|
return +v.toFixed(2)
|
|
}
|
|
|
|
/**
|
|
* Check if a float is safe to use. ie: Not too big or small.
|
|
* @public
|
|
*/
|
|
export const isSafeFloat = (n: number) => {
|
|
return Math.abs(n) < Number.MAX_SAFE_INTEGER
|
|
}
|