Merge pull request #86 from tldraw/utils-refactor

Split vectors and intersections into new packages
This commit is contained in:
Steve Ruiz 2021-09-12 13:26:08 +01:00 committed by GitHub
commit 9f309bb485
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1801 additions and 880 deletions

17
.vscode/tasks.json vendored
View file

@ -2,17 +2,12 @@
"version": "2.0.0",
"tasks": [
{
"label": "tsc",
"type": "shell",
"command": "./node_modules/.bin/tsc",
"args": ["--noEmit"],
"presentation": {
"reveal": "never",
"echo": false,
"focus": false,
"panel": "dedicated"
},
"problemMatcher": "$tsc-watch"
"label": "Check for type errors",
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": ["$tsc-watch"],
"group": "build"
}
]
}

View file

@ -13,7 +13,9 @@
"packages/core",
"packages/tldraw",
"packages/dev",
"packages/www"
"packages/www",
"packages/vec",
"packages/intersect"
],
"scripts": {
"test": "jest",
@ -22,7 +24,7 @@
"start": "lerna run start:pre && lerna run start --stream --parallel",
"start:www": "yarn build:packages && lerna run start --parallel & cd packages/www && yarn dev",
"build": "yarn build:packages && cd packages/www && yarn build",
"build:packages": "cd packages/core && yarn build && cd ../tldraw && yarn build",
"build:packages": "cd packages/vec && yarn build && cd ../intersect && yarn build && cd ../core && yarn build && cd ../tldraw && yarn build",
"publish:patch": "yarn build:packages && lerna publish patch",
"docs": "lerna run docs --stream",
"docs:watch": "lerna run docs:watch --stream"
@ -105,11 +107,15 @@
},
"testEnvironment": "jsdom",
"modulePathIgnorePatterns": [
"<rootDir>/packages/vec/dist/",
"<rootDir>/packages/intersect/dist/",
"<rootDir>/packages/core/dist/",
"<rootDir>/packages/tldraw/dist/",
"<rootDir>/packages/tldraw/test-utils/"
],
"moduleNameMapper": {
"@tldraw/vec": "<rootDir>/packages/vec/src",
"@tldraw/intersect": "<rootDir>/packages/intersect/src",
"@tldraw/core": "<rootDir>/packages/core/src",
"@tldraw/tldraw": "<rootDir>/packages/tldraw/src",
"\\~(.*)": "<rootDir>/packages/tldraw/src/$1",

View file

@ -55,7 +55,9 @@
"react-dom": "^17.0.2"
},
"dependencies": {
"@tldraw/vec": "^0.0.86",
"@tldraw/intersect": "^0.0.86",
"@use-gesture/react": "^10.0.0-beta.24"
},
"gitHead": "55da8880eb3d8ab5fb62b5eb7853065922c95dcf"
}
}

View file

@ -1,6 +1,7 @@
/* eslint-disable */
const fs = require('fs')
const esbuild = require('esbuild')
const { gzip } = require('zlib')
const name = process.env.npm_package_name || ''
@ -25,9 +26,10 @@ async function main() {
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.build.json',
external: ['react', 'react-dom'],
metafile: true,
})
esbuild.buildSync({
const esmResult = esbuild.buildSync({
entryPoints: ['./src/index.ts'],
outdir: 'dist/esm',
minify: true,
@ -38,9 +40,23 @@ async function main() {
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
external: ['react', 'react-dom'],
metafile: true,
})
console.log(`${name}: Built package.`)
let esmSize = 0
Object.values(esmResult.metafile.outputs).forEach((output) => {
esmSize += output.bytes
})
fs.readFile('./dist/esm/index.js', (_err, data) => {
gzip(data, (_err, result) => {
console.log(
`${name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${(
result.length / 1000
).toFixed(2)}kb minified)`
)
})
})
} catch (e) {
console.log(`× ${name}: Build failed due to an error.`)
console.log(e)

View file

@ -1,5 +1,5 @@
import * as React from 'react'
import { Vec } from '+utils'
import { Vec } from '@tldraw/vec'
import type { TLShape } from '+types'
import { Handle } from './handle'

View file

@ -11,7 +11,8 @@ import type {
TLBinding,
TLBounds,
} from '+types'
import { Utils, Vec } from '+utils'
import { Utils } from '+utils'
import { Vec } from '@tldraw/vec'
function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
shape: T,

View file

@ -2,8 +2,8 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import * as React from 'react'
import { useTLContext } from './useTLContext'
import Utils, { Vec } from '+utils'
import { useGesture } from '@use-gesture/react'
import { Vec } from '@tldraw/vec'
// Capture zoom gestures (pinches, wheels and pans)
export function useZoomEvents<T extends Element>(ref: React.RefObject<T>) {

View file

@ -1,6 +1,7 @@
import type React from 'react'
import type { TLKeyboardInfo, TLPointerInfo } from './types'
import { Vec, Utils } from './utils'
import { Utils } from './utils'
import { Vec } from '@tldraw/vec'
const DOUBLE_CLICK_DURATION = 250
@ -354,14 +355,11 @@ export class Inputs {
e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent,
offset = [0, 0]
): number[] {
return [
Number(e.clientX.toPrecision(5)) - offset[0],
Number(e.clientY.toPrecision(5)) - offset[1],
]
return [+e.clientX.toFixed(2) - offset[0], +e.clientY.toFixed(2) - offset[1]]
}
static getPressure(e: PointerEvent | React.PointerEvent | Touch | React.Touch | WheelEvent) {
return 'pressure' in e ? Number(e.pressure.toPrecision(5)) || 0.5 : 0.5
return 'pressure' in e ? +e.pressure.toFixed(2) || 0.5 : 0.5
}
static commandKey(): string {

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as React from 'react'
import { TLShapeUtil, TLShape, TLShapeProps, TLBounds, TLRenderInfo, TLTransformInfo } from '+types'
import Utils, { Intersect } from '+utils'
import Utils from '+utils'
export interface BoxShape extends TLShape {
size: number[]
@ -62,15 +62,6 @@ export class Box extends TLShapeUtil<BoxShape, SVGGElement> {
return Utils.pointInBounds(point, this.getBounds(shape))
}
hitTestBounds(shape: BoxShape, bounds: TLBounds) {
const rotatedCorners = Utils.getRotatedCorners(this.getBounds(shape), shape.rotation)
return (
rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) ||
Intersect.polyline.bounds(rotatedCorners, bounds).length > 0
)
}
transform(shape: BoxShape, bounds: TLBounds, _info: TLTransformInfo<BoxShape>): BoxShape {
return { ...shape, point: [bounds.minX, bounds.minY] }
}

View file

@ -2,8 +2,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* --------------------- Primary -------------------- */
import { Intersect, Vec } from '+utils'
import { Vec } from '@tldraw/vec'
import React, { ForwardedRef } from 'react'
import { intersectPolylineBounds } from '@tldraw/intersect'
export type Patch<T> = Partial<{ [P in keyof T]: T | Partial<T> | Patch<T[P]> }>
@ -426,7 +427,7 @@ export abstract class TLShapeUtil<T extends TLShape, E extends Element> {
point[1] < bounds.minY ||
point[1] > bounds.maxY
)
) || Intersect.polyline.bounds(corners, bounds).length > 0
) || intersectPolylineBounds(corners, bounds).length > 0
)
}
}

View file

@ -1,7 +1,5 @@
import { Utils } from './utils'
export { Intersect } from './intersect'
export { Utils } from './utils'
export { Svg } from './svg'
export { Vec } from './vec'
export default Utils

View file

@ -1,680 +0,0 @@
import type { TLBounds, TLIntersection } from '../types'
import { Vec } from './vec'
import { Utils } from './utils'
/* ----------------- Start Copy Here ---------------- */
function getIntersection(message: string, ...points: number[][]): TLIntersection {
const didIntersect = points.length > 0
return { didIntersect, message, points }
}
export class Intersect {
static ray = {
// Intersect a ray with a ray.
ray(p0: number[], n0: number[], p1: number[], n1: number[]): TLIntersection {
const dx = p1[0] - p0[0]
const dy = p1[1] - p0[1]
const det = n1[0] * n0[1] - n1[1] * n0[0]
const u = (dy * n1[0] - dx * n1[1]) / det
const v = (dy * n0[0] - dx * n0[1]) / det
if (u < 0 || v < 0) return getIntersection('miss')
const m0 = n0[1] / n0[0]
const m1 = n1[1] / n1[0]
const b0 = p0[1] - m0 * p0[0]
const b1 = p1[1] - m1 * p1[0]
const x = (b1 - b0) / (m0 - m1)
const y = m0 * x + b0
return Number.isFinite(x)
? getIntersection('intersection', [x, y])
: getIntersection('parallel')
},
// Interseg a ray with a line segment.
lineSegment(origin: number[], direction: number[], a1: number[], a2: number[]): TLIntersection {
const [x, y] = origin
const [dx, dy] = direction
const [x1, y1] = a1
const [x2, y2] = a2
if (dy / dx !== (y2 - y1) / (x2 - x1)) {
const d = dx * (y2 - y1) - dy * (x2 - x1)
if (d !== 0) {
const r = ((y - y1) * (x2 - x1) - (x - x1) * (y2 - y1)) / d
const s = ((y - y1) * dx - (x - x1) * dy) / d
if (r >= 0 && s >= 0 && s <= 1) {
return getIntersection('intersection', [x + r * dx, y + r * dy])
}
}
}
return getIntersection('no intersection')
},
// Intersect a ray with a rectangle.
rectangle(
origin: number[],
direction: number[],
point: number[],
size: number[],
rotation = 0
): TLIntersection[] {
return Intersect.rectangle.ray(point, size, rotation, origin, direction)
},
// Intersect a ray with an ellipse.
ellipse(
origin: number[],
direction: number[],
center: number[],
rx: number,
ry: number,
rotation: number
): TLIntersection {
const a1 = origin
const a2 = Vec.mul(direction, 999999999)
return Intersect.lineSegment.ellipse(a1, a2, center, rx, ry, rotation)
},
// Intersect a ray with a bounding box.
bounds(
origin: number[],
direction: number[],
bounds: TLBounds,
rotation = 0
): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.ray.rectangle(origin, direction, [minX, minY], [width, height], rotation)
},
}
static lineSegment = {
// Intersect a line segment with a ray.
ray(a1: number[], a2: number[], origin: number[], direction: number[]): TLIntersection {
return Intersect.ray.lineSegment(origin, direction, a1, a2)
},
// Intersect a line segment with a line segment.
lineSegment(a1: number[], a2: number[], b1: number[], b2: number[]): TLIntersection {
const AB = Vec.sub(a1, b1)
const BV = Vec.sub(b2, b1)
const AV = Vec.sub(a2, a1)
const ua_t = BV[0] * AB[1] - BV[1] * AB[0]
const ub_t = AV[0] * AB[1] - AV[1] * AB[0]
const u_b = BV[1] * AV[0] - BV[0] * AV[1]
if (ua_t === 0 || ub_t === 0) {
return getIntersection('coincident')
}
if (u_b === 0) {
return getIntersection('parallel')
}
if (u_b !== 0) {
const ua = ua_t / u_b
const ub = ub_t / u_b
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
return getIntersection('intersection', Vec.add(a1, Vec.mul(AV, ua)))
}
}
return getIntersection('no intersection')
},
// Intersect a line segment with a rectangle
rectangle(a1: number[], a2: number[], point: number[], size: number[]): TLIntersection[] {
return Intersect.rectangle.lineSegment(point, size, a1, a2)
},
// Intersect a line segment with an arc.
arc(
a1: number[],
a2: number[],
center: number[],
radius: number,
start: number[],
end: number[]
): TLIntersection {
const sa = Vec.angle(center, start)
const ea = Vec.angle(center, end)
const ellipseTest = Intersect.ellipse.lineSegment(center, radius, radius, 0, a1, a2)
if (!ellipseTest.didIntersect) return getIntersection('No intersection')
const points = ellipseTest.points.filter((point) =>
Utils.isAngleBetween(sa, ea, Vec.angle(center, point))
)
if (points.length === 0) {
return getIntersection('No intersection')
}
return getIntersection('intersection', ...points)
},
// Intersect a line segment with a circle.
circle(a1: number[], a2: number[], c: number[], r: number): TLIntersection {
const a = (a2[0] - a1[0]) * (a2[0] - a1[0]) + (a2[1] - a1[1]) * (a2[1] - a1[1])
const b = 2 * ((a2[0] - a1[0]) * (a1[0] - c[0]) + (a2[1] - a1[1]) * (a1[1] - c[1]))
const cc =
c[0] * c[0] +
c[1] * c[1] +
a1[0] * a1[0] +
a1[1] * a1[1] -
2 * (c[0] * a1[0] + c[1] * a1[1]) -
r * r
const deter = b * b - 4 * a * cc
if (deter < 0) {
return getIntersection('outside')
}
if (deter === 0) {
return getIntersection('tangent')
}
const e = Math.sqrt(deter)
const u1 = (-b + e) / (2 * a)
const u2 = (-b - e) / (2 * a)
if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
return getIntersection('outside')
} else {
return getIntersection('inside')
}
}
const results: number[][] = []
if (0 <= u1 && u1 <= 1) results.push(Vec.lrp(a1, a2, u1))
if (0 <= u2 && u2 <= 1) results.push(Vec.lrp(a1, a2, u2))
return getIntersection('intersection', ...results)
},
// Intersect a line segment with an ellipse.
ellipse(
a1: number[],
a2: number[],
center: number[],
rx: number,
ry: number,
rotation = 0
): TLIntersection {
// If the ellipse or line segment are empty, return no tValues.
if (rx === 0 || ry === 0 || Vec.isEqual(a1, a2)) {
return getIntersection('No intersection')
}
// Get the semimajor and semiminor axes.
rx = rx < 0 ? rx : -rx
ry = ry < 0 ? ry : -ry
// Rotate points and translate so the ellipse is centered at the origin.
a1 = Vec.sub(Vec.rotWith(a1, center, -rotation), center)
a2 = Vec.sub(Vec.rotWith(a2, center, -rotation), center)
// Calculate the quadratic parameters.
const diff = Vec.sub(a2, a1)
const A = (diff[0] * diff[0]) / rx / rx + (diff[1] * diff[1]) / ry / ry
const B = (2 * a1[0] * diff[0]) / rx / rx + (2 * a1[1] * diff[1]) / ry / ry
const C = (a1[0] * a1[0]) / rx / rx + (a1[1] * a1[1]) / ry / ry - 1
// Make a list of t values (normalized points on the line where intersections occur).
const tValues: number[] = []
// Calculate the discriminant.
const discriminant = B * B - 4 * A * C
if (discriminant === 0) {
// One real solution.
tValues.push(-B / 2 / A)
} else if (discriminant > 0) {
const root = Math.sqrt(discriminant)
// Two real solutions.
tValues.push((-B + root) / 2 / A)
tValues.push((-B - root) / 2 / A)
}
// Filter to only points that are on the segment.
// Solve for points, then counter-rotate points.
const points = tValues
.filter((t) => t >= 0 && t <= 1)
.map((t) => Vec.add(center, Vec.add(a1, Vec.mul(Vec.sub(a2, a1), t))))
.map((p) => Vec.rotWith(p, center, rotation))
return getIntersection('intersection', ...points)
},
// Intersect a line segment with a bounding box.
bounds(a1: number[], a2: number[], bounds: TLBounds): TLIntersection[] {
return Intersect.bounds.lineSegment(bounds, a1, a2)
},
// Intersect a line segment with a polyline
polyline(a1: number[], a2: number[], points: number[][]): TLIntersection[] {
const intersections: TLIntersection[] = []
for (let i = 1; i < points.length + 1; i++) {
const int = Intersect.lineSegment.lineSegment(
a1,
a2,
points[i - 1],
points[i % points.length]
)
if (int) {
intersections.push(int)
}
}
return intersections
},
}
static rectangle = {
// Intersect a rectangle with a ray.
ray(
point: number[],
size: number[],
rotation: number,
origin: number[],
direction: number[]
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size, rotation).reduce<
TLIntersection[]
>((acc, [message, [a1, a2]]) => {
const intersection = Intersect.ray.lineSegment(origin, direction, a1, a2)
if (intersection) {
acc.push(getIntersection(message, ...intersection.points))
}
return acc
}, [])
return sideIntersections.filter((int) => int.didIntersect)
},
// Intersect a rectangle with a line segment.
lineSegment(point: number[], size: number[], a1: number[], a2: number[]): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<TLIntersection[]>(
(acc, [message, [b1, b2]]) => {
const intersection = Intersect.lineSegment.lineSegment(a1, a2, b1, b2)
if (intersection) {
acc.push(getIntersection(message, ...intersection.points))
}
return acc
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
// Intersect a rectangle with a rectangle.
rectangle(
point1: number[],
size1: number[],
point2: number[],
size2: number[]
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point1, size1).reduce<TLIntersection[]>(
(acc, [message, [a1, a2]]) => {
const intersections = Intersect.rectangle.lineSegment(point2, size2, a1, a2)
acc.push(
...intersections.map((int) =>
getIntersection(`${message} ${int.message}`, ...int.points)
)
)
return acc
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
// Intersect a rectangle with an arc.
arc(
point: number[],
size: number[],
center: number[],
radius: number,
start: number[],
end: number[]
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<TLIntersection[]>(
(acc, [message, [a1, a2]]) => {
const intersection = Intersect.arc.lineSegment(center, radius, start, end, a1, a2)
if (intersection) {
acc.push({ ...intersection, message })
}
return acc
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
// Intersect a rectangle with a circle.
circle(point: number[], size: number[], c: number[], r: number): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<TLIntersection[]>(
(acc, [message, [a1, a2]]) => {
const intersection = Intersect.lineSegment.circle(a1, a2, c, r)
if (intersection) {
acc.push({ ...intersection, message })
}
return acc
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
// Intersect a rectangle with an ellipse.
ellipse(
point: number[],
size: number[],
c: number[],
rx: number,
ry: number,
rotation = 0
): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<TLIntersection[]>(
(acc, [message, [a1, a2]]) => {
const intersection = Intersect.lineSegment.ellipse(a1, a2, c, rx, ry, rotation)
if (intersection) {
acc.push({ ...intersection, message })
}
return acc
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
// Intersect a rectangle with a bounding box.
bounds(point: number[], size: number[], bounds: TLBounds): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.rectangle.rectangle(point, size, [minX, minY], [width, height])
},
// Intersect a rectangle with a polyline
polyline(point: number[], size: number[], points: number[][]): TLIntersection[] {
const sideIntersections = Utils.getRectangleSides(point, size).reduce<TLIntersection[]>(
(acc, [message, [a1, a2]]) => {
const intersections = Intersect.lineSegment.polyline(a1, a2, points)
if (intersections.length > 0) {
acc.push(getIntersection(message, ...intersections.flatMap((i) => i.points)))
}
return acc
},
[]
)
return sideIntersections.filter((int) => int.didIntersect)
},
}
static arc = {
// Intersect an arc with a line segment.
lineSegment(
center: number[],
radius: number,
start: number[],
end: number[],
a1: number[],
a2: number[]
): TLIntersection {
return Intersect.lineSegment.arc(a1, a2, center, radius, start, end)
},
// Intersect an arc with a rectangle.
rectangle(
center: number[],
radius: number,
start: number[],
end: number[],
point: number[],
size: number[]
): TLIntersection[] {
return Intersect.rectangle.arc(point, size, center, radius, start, end)
},
// Intersect an arc with a bounding box.
bounds(
center: number[],
radius: number,
start: number[],
end: number[],
bounds: TLBounds
): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.arc.rectangle(center, radius, start, end, [minX, minY], [width, height])
},
}
static circle = {
// Intersect a circle with a line segment.
lineSegment(c: number[], r: number, a1: number[], a2: number[]): TLIntersection {
return Intersect.lineSegment.circle(a1, a2, c, r)
},
// Intersect a circle with a circle.
circle(c1: number[], r1: number, c2: number[], r2: number): TLIntersection {
let dx = c2[0] - c1[0],
dy = c2[1] - c1[1]
const d = Math.sqrt(dx * dx + dy * dy),
x = (d * d - r2 * r2 + r1 * r1) / (2 * d),
y = Math.sqrt(r1 * r1 - x * x)
dx /= d
dy /= d
return getIntersection(
'intersection',
[c1[0] + dx * x - dy * y, c1[1] + dy * x + dx * y],
[c1[0] + dx * x + dy * y, c1[1] + dy * x - dx * y]
)
},
// Intersect a circle with a rectangle.
rectangle(c: number[], r: number, point: number[], size: number[]): TLIntersection[] {
return Intersect.rectangle.circle(point, size, c, r)
},
// Intersect a circle with a bounding box.
bounds(c: number[], r: number, bounds: TLBounds): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.circle.rectangle(c, r, [minX, minY], [width, height])
},
}
static ellipse = {
// Intersect an ellipse with a ray.
ray(
center: number[],
rx: number,
ry: number,
rotation: number,
point: number[],
direction: number[]
): TLIntersection {
return Intersect.ray.ellipse(point, direction, center, rx, ry, rotation)
},
// Intersect an ellipse with a line segment.
lineSegment(
center: number[],
rx: number,
ry: number,
rotation = 0,
a1: number[],
a2: number[]
): TLIntersection {
if (rx === ry) {
return Intersect.lineSegment.circle(a1, a2, center, rx)
}
return Intersect.lineSegment.ellipse(a1, a2, center, rx, ry, rotation)
},
// Intersect an ellipse with a rectangle.
rectangle(
center: number[],
rx: number,
ry: number,
rotation = 0,
point: number[],
size: number[]
): TLIntersection[] {
if (rx === ry) {
return Intersect.rectangle.circle(point, size, center, rx)
}
return Intersect.rectangle.ellipse(point, size, center, rx, ry, rotation)
},
// Get an intersection between an ellipse and a second ellipse.
// Adapted from https://gist.github.com/drawable/92792f59b6ff8869d8b1
ellipse(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_c1: number[],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_rx1: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_ry1: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_r1: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_c2: number[],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_rx2: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_ry2: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_r2: number
): TLIntersection {
// TODO
return getIntersection('no intersection')
},
circle(
c: number[],
rx: number,
ry: number,
rotation: number,
c2: number[],
r2: number
): TLIntersection {
return Intersect.ellipse.ellipse(c, rx, ry, rotation, c2, r2, r2, 0)
},
// Intersect an ellipse with a bounding box.
bounds(
c: number[],
rx: number,
ry: number,
rotation: number,
bounds: TLBounds
): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.ellipse.rectangle(c, rx, ry, rotation, [minX, minY], [width, height])
},
}
static bounds = {
ray(bounds: TLBounds, origin: number[], direction: number[]): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.ray.rectangle(origin, direction, [minX, minY], [width, height])
},
lineSegment(bounds: TLBounds, a1: number[], a2: number[]): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.lineSegment.rectangle(a1, a2, [minX, minY], [width, height])
},
rectangle(bounds: TLBounds, point: number[], size: number[]): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.rectangle.rectangle(point, size, [minX, minY], [width, height])
},
bounds(bounds1: TLBounds, bounds2: TLBounds): TLIntersection[] {
return Intersect.rectangle.rectangle(
[bounds1.minX, bounds1.minY],
[bounds1.width, bounds1.height],
[bounds2.minX, bounds2.minY],
[bounds2.width, bounds2.height]
)
},
arc(
bounds: TLBounds,
center: number[],
radius: number,
start: number[],
end: number[]
): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.arc.rectangle(center, radius, start, end, [minX, minY], [width, height])
},
circle(bounds: TLBounds, c: number[], r: number): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.circle.rectangle(c, r, [minX, minY], [width, height])
},
ellipse(bounds: TLBounds, c: number[], rx: number, ry: number, rotation = 0): TLIntersection[] {
const { minX, minY, width, height } = bounds
return Intersect.ellipse.rectangle(c, rx, ry, rotation, [minX, minY], [width, height])
},
polyline(bounds: TLBounds, points: number[][]): TLIntersection[] {
return Intersect.polyline.bounds(points, bounds)
},
}
static polyline = {
// Intersect a polyline with a line segment.
lineSegment(points: number[][], a1: number[], a2: number[]): TLIntersection[] {
return Intersect.lineSegment.polyline(a1, a2, points)
},
// Interesct a polyline with a rectangle.
rectangle(points: number[][], point: number[], size: number[]): TLIntersection[] {
return Intersect.rectangle.polyline(point, size, points)
},
// Intersect a polyline with a bounding box.
bounds(points: number[][], bounds: TLBounds): TLIntersection[] {
return Intersect.rectangle.polyline(
[bounds.minX, bounds.minY],
[bounds.width, bounds.height],
points
)
},
}
}
export default Intersect

View file

@ -4,7 +4,7 @@
/* eslint-disable no-redeclare */
import type React from 'react'
import { TLBezierCurveSegment, TLBounds, TLBoundsCorner, TLBoundsEdge } from '../types'
import vec from './vec'
import { Vec } from '@tldraw/vec'
import './polyfills'
import type { Patch } from '+index'
@ -204,10 +204,10 @@ export class Utils {
static getRectangleSides(point: number[], size: number[], rotation = 0): [string, number[][]][] {
const center = [point[0] + size[0] / 2, point[1] + size[1] / 2]
const tl = vec.rotWith(point, center, rotation)
const tr = vec.rotWith(vec.add(point, [size[0], 0]), center, rotation)
const br = vec.rotWith(vec.add(point, size), center, rotation)
const bl = vec.rotWith(vec.add(point, [0, size[1]]), center, rotation)
const tl = Vec.rotWith(point, center, rotation)
const tr = Vec.rotWith(Vec.add(point, [size[0], 0]), center, rotation)
const br = Vec.rotWith(Vec.add(point, size), center, rotation)
const bl = Vec.rotWith(Vec.add(point, [0, size[1]]), center, rotation)
return [
['top', [tl, tr]],
@ -258,10 +258,10 @@ export class Utils {
P: number[],
side: number
): number[] | null {
const B = vec.lrp(C, P, 0.5)
const r1 = vec.dist(C, B)
const delta = vec.sub(B, C)
const d = vec.len(delta)
const B = Vec.lrp(C, P, 0.5)
const r1 = Vec.dist(C, B)
const delta = Vec.sub(B, C)
const d = Vec.len(delta)
if (!(d <= r + r1 && d >= Math.abs(r - r1))) {
return null
@ -269,11 +269,11 @@ export class Utils {
const a = (r * r - r1 * r1 + d * d) / (2.0 * d)
const n = 1 / d
const p = vec.add(C, vec.mul(delta, a * n))
const p = Vec.add(C, Vec.mul(delta, a * n))
const h = Math.sqrt(r * r - a * a)
const k = vec.mul(vec.per(delta), h * n)
const k = Vec.mul(Vec.per(delta), h * n)
return side === 0 ? vec.add(p, k) : vec.sub(p, k)
return side === 0 ? Vec.add(p, k) : Vec.sub(p, k)
}
/**
@ -292,8 +292,8 @@ export class Utils {
C1: number[],
r1: number
): number[][] | null {
const a0 = vec.angle(C0, C1)
const d = vec.dist(C0, C1)
const a0 = Vec.angle(C0, C1)
const d = Vec.dist(C0, C1)
// Circles are overlapping, no tangents
if (d < Math.abs(r1 - r0)) {
@ -319,8 +319,8 @@ export class Utils {
* @param P The point.
*/
static getClosestPointOnCircle(C: number[], r: number, P: number[]): number[] {
const v = vec.sub(C, P)
return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r))
const v = Vec.sub(C, P)
return Vec.sub(C, Vec.mul(Vec.div(v, Vec.len(v)), r))
}
/**
@ -411,7 +411,7 @@ export class Utils {
* @param B
*/
static getSweep(C: number[], A: number[], B: number[]): number {
return Utils.angleDelta(vec.angle(C, A), vec.angle(C, B))
return Utils.angleDelta(Vec.angle(C, A), Vec.angle(C, B))
}
/**
@ -766,7 +766,7 @@ export class Utils {
const len = pts.length
const res: number[][] = [] // results
let t1x: number, // tension vectors
let t1x: number, // tension Vectors
t2x: number,
t1y: number,
t2y: number,
@ -871,7 +871,7 @@ export class Utils {
* @returns
*/
static pointInCircle(A: number[], C: number[], r: number): boolean {
return vec.dist(A, C) <= r
return Vec.dist(A, C) <= r
}
/**
@ -887,7 +887,7 @@ export class Utils {
rotation = rotation || 0
const cos = Math.cos(rotation)
const sin = Math.sin(rotation)
const delta = vec.sub(A, C)
const delta = Vec.sub(A, C)
const tdx = cos * delta[0] + sin * delta[1]
const tdy = sin * delta[0] - cos * delta[1]
@ -914,10 +914,10 @@ export class Utils {
points.forEach((a, i) => {
const b = points[(i + 1) % points.length]
if (a[1] <= p[1]) {
if (b[1] > p[1] && vec.cross(a, b, p) > 0) {
if (b[1] > p[1] && Vec.cross(a, b, p) > 0) {
wn += 1
}
} else if (b[1] <= p[1] && vec.cross(a, b, p) < 0) {
} else if (b[1] <= p[1] && Vec.cross(a, b, p) < 0) {
wn -= 1
}
})
@ -1024,7 +1024,7 @@ export class Utils {
if (rotation !== 0) {
return Utils.getBoundsFromPoints(
points.map((pt) => vec.rotWith(pt, [(minX + maxX) / 2, (minY + maxY) / 2], rotation))
points.map((pt) => Vec.rotWith(pt, [(minX + maxX) / 2, (minY + maxY) / 2], rotation))
)
}
@ -1074,8 +1074,8 @@ export class Utils {
* @param rotation
*/
static rotateBounds(bounds: TLBounds, center: number[], rotation: number): TLBounds {
const [minX, minY] = vec.rotWith([bounds.minX, bounds.minY], center, rotation)
const [maxX, maxY] = vec.rotWith([bounds.maxX, bounds.maxY], center, rotation)
const [minX, minY] = Vec.rotWith([bounds.minX, bounds.minY], center, rotation)
const [maxX, maxY] = Vec.rotWith([bounds.maxX, bounds.maxY], center, rotation)
return {
minX,
@ -1158,7 +1158,7 @@ export class Utils {
[b.maxX, b.minY],
[b.maxX, b.maxY],
[b.minX, b.maxY],
].map((point) => vec.rotWith(point, center, rotation))
].map((point) => Vec.rotWith(point, center, rotation))
}
static getTransformedBoundingBox(
@ -1192,7 +1192,7 @@ export class Utils {
// Counter rotate the delta. This lets us make changes as if
// the (possibly rotated) boxes were axis aligned.
const [dx, dy] = vec.rot(delta, -rotation)
const [dx, dy] = Vec.rot(delta, -rotation)
/*
1. Delta
@ -1299,67 +1299,67 @@ new box's aspect ratio matches the original aspect ratio.
/*
3. Rotation
If the bounds are rotated, get a vector from the rotated anchor
If the bounds are rotated, get a Vector from the rotated anchor
corner in the inital bounds to the rotated anchor corner in the
result's bounds. Subtract this vector from the result's corners,
result's bounds. Subtract this Vector from the result's corners,
so that the two anchor points (initial and result) will be equal.
*/
if (rotation % (Math.PI * 2) !== 0) {
let cv = [0, 0]
const c0 = vec.med([ax0, ay0], [ax1, ay1])
const c1 = vec.med([bx0, by0], [bx1, by1])
const c0 = Vec.med([ax0, ay0], [ax1, ay1])
const c1 = Vec.med([bx0, by0], [bx1, by1])
switch (handle) {
case TLBoundsCorner.TopLeft: {
cv = vec.sub(vec.rotWith([bx1, by1], c1, rotation), vec.rotWith([ax1, ay1], c0, rotation))
cv = Vec.sub(Vec.rotWith([bx1, by1], c1, rotation), Vec.rotWith([ax1, ay1], c0, rotation))
break
}
case TLBoundsCorner.TopRight: {
cv = vec.sub(vec.rotWith([bx0, by1], c1, rotation), vec.rotWith([ax0, ay1], c0, rotation))
cv = Vec.sub(Vec.rotWith([bx0, by1], c1, rotation), Vec.rotWith([ax0, ay1], c0, rotation))
break
}
case TLBoundsCorner.BottomRight: {
cv = vec.sub(vec.rotWith([bx0, by0], c1, rotation), vec.rotWith([ax0, ay0], c0, rotation))
cv = Vec.sub(Vec.rotWith([bx0, by0], c1, rotation), Vec.rotWith([ax0, ay0], c0, rotation))
break
}
case TLBoundsCorner.BottomLeft: {
cv = vec.sub(vec.rotWith([bx1, by0], c1, rotation), vec.rotWith([ax1, ay0], c0, rotation))
cv = Vec.sub(Vec.rotWith([bx1, by0], c1, rotation), Vec.rotWith([ax1, ay0], c0, rotation))
break
}
case TLBoundsEdge.Top: {
cv = vec.sub(
vec.rotWith(vec.med([bx0, by1], [bx1, by1]), c1, rotation),
vec.rotWith(vec.med([ax0, ay1], [ax1, ay1]), c0, rotation)
cv = Vec.sub(
Vec.rotWith(Vec.med([bx0, by1], [bx1, by1]), c1, rotation),
Vec.rotWith(Vec.med([ax0, ay1], [ax1, ay1]), c0, rotation)
)
break
}
case TLBoundsEdge.Left: {
cv = vec.sub(
vec.rotWith(vec.med([bx1, by0], [bx1, by1]), c1, rotation),
vec.rotWith(vec.med([ax1, ay0], [ax1, ay1]), c0, rotation)
cv = Vec.sub(
Vec.rotWith(Vec.med([bx1, by0], [bx1, by1]), c1, rotation),
Vec.rotWith(Vec.med([ax1, ay0], [ax1, ay1]), c0, rotation)
)
break
}
case TLBoundsEdge.Bottom: {
cv = vec.sub(
vec.rotWith(vec.med([bx0, by0], [bx1, by0]), c1, rotation),
vec.rotWith(vec.med([ax0, ay0], [ax1, ay0]), c0, rotation)
cv = Vec.sub(
Vec.rotWith(Vec.med([bx0, by0], [bx1, by0]), c1, rotation),
Vec.rotWith(Vec.med([ax0, ay0], [ax1, ay0]), c0, rotation)
)
break
}
case TLBoundsEdge.Right: {
cv = vec.sub(
vec.rotWith(vec.med([bx0, by0], [bx0, by1]), c1, rotation),
vec.rotWith(vec.med([ax0, ay0], [ax0, ay1]), c0, rotation)
cv = Vec.sub(
Vec.rotWith(Vec.med([bx0, by0], [bx0, by1]), c1, rotation),
Vec.rotWith(Vec.med([ax0, ay0], [ax0, ay1]), c0, rotation)
)
break
}
}
;[bx0, by0] = vec.sub([bx0, by0], cv)
;[bx1, by1] = vec.sub([bx1, by1], cv)
;[bx0, by0] = Vec.sub([bx0, by0], cv)
;[bx1, by1] = Vec.sub([bx1, by1], cv)
}
/*
@ -1500,10 +1500,10 @@ left past the initial left edge) then swap points on that axis.
* @param rotation
*/
static getRotatedSize(size: number[], rotation: number): number[] {
const center = vec.div(size, 2)
const center = Vec.div(size, 2)
const points = [[0, 0], [size[0], 0], size, [0, size[1]]].map((point) =>
vec.rotWith(point, center, rotation)
Vec.rotWith(point, center, rotation)
)
const bounds = Utils.getBoundsFromPoints(points)
@ -1534,7 +1534,7 @@ left past the initial left edge) then swap points on that axis.
*/
static removeDuplicatePoints(points: number[][]) {
return points.reduce<number[][]>((acc, pt, i) => {
if (i === 0 || !vec.isEqual(pt, acc[i - 1])) {
if (i === 0 || !Vec.isEqual(pt, acc[i - 1])) {
acc.push(pt)
}
return acc

View file

@ -3,13 +3,17 @@
"include": ["src"],
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"composite": true,
"emitDeclarationOnly": true,
"rootDir": "src",
"outDir": "./dist/types",
"rootDir": "src",
"baseUrl": "src",
"paths": {
"+*": ["./*"]
}
}
},
"references": [
{
"path": "../../packages/intersect"
},
{ "path": "../../packages/vec" }
]
}

View file

@ -17,6 +17,7 @@
"README.md",
"src"
],
"sideEffects": false,
"dependencies": {
"@tldraw/tldraw": "^0.0.86",
"idb": "^6.1.2",
@ -37,4 +38,4 @@
"typescript": "4.2.3"
},
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
}
}

View file

@ -3,7 +3,8 @@
"include": ["src"],
"exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts", "dist"],
"compilerOptions": {
"composite": true
"composite": true,
"rootDir": "."
},
"references": [
{

View file

@ -0,0 +1,3 @@
# Dev Server
Dev server with fast refresh.

View file

@ -0,0 +1,57 @@
{
"name": "@tldraw/intersect",
"version": "0.0.86",
"private": false,
"description": "A tiny little drawing app (intersect)",
"author": "@steveruizok",
"repository": {
"type": "git",
"url": "git+https://github.com/tldraw/tldraw.git",
"directory": "packages/intersect"
},
"license": "MIT",
"keywords": [],
"files": [
"dist/**/*"
],
"sideEffects": false,
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"typings": "./dist/types/index.d.ts",
"scripts": {
"start:pre": "node scripts/pre-dev && yarn types:pre",
"start": "node scripts/dev & yarn types:dev",
"build": "node scripts/build && yarn types:build",
"types:pre": "tsc",
"types:dev": "tsc --watch",
"types:build": "tsc --project tsconfig.build.json",
"lint": "eslint src/ --ext .ts,.tsx",
"clean": "rm -rf dist",
"ts-node": "ts-node",
"docs": "typedoc",
"docs:watch": "typedoc --watch"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/preset-env": "^7.15.4",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.10",
"@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^4.30.0",
"esbuild": "^0.12.24",
"eslint": "^7.32.0",
"lerna": "^4.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"ts-node": "^10.2.1",
"tslib": "^2.3.1",
"typedoc": "^0.21.9",
"typescript": "^4.4.2"
},
"peerDependencies": {},
"dependencies": {
"@tldraw/vec": "^0.0.86"
},
"gitHead": "55da8880eb3d8ab5fb62b5eb7853065922c95dcf"
}

View file

@ -0,0 +1,50 @@
/* eslint-disable */
const fs = require('fs')
const esbuild = require('esbuild')
const name = process.env.npm_package_name || ''
async function main() {
if (fs.existsSync('./dist')) {
fs.rmSync('./dist', { recursive: true }, (e) => {
if (e) {
throw e
}
})
}
try {
esbuild.buildSync({
entryPoints: ['./src/index.ts'],
outdir: 'dist/cjs',
minify: true,
bundle: true,
format: 'cjs',
target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.build.json',
external: ['react', 'react-dom'],
})
esbuild.buildSync({
entryPoints: ['./src/index.ts'],
outdir: 'dist/esm',
minify: true,
bundle: true,
format: 'esm',
target: 'es6',
tsconfig: './tsconfig.build.json',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
external: ['react', 'react-dom'],
})
console.log(`${name}: Built package.`)
} catch (e) {
console.log(`× ${name}: Build failed due to an error.`)
console.log(e)
}
}
main()

View file

@ -0,0 +1,31 @@
/* eslint-disable */
const esbuild = require('esbuild')
const name = process.env.npm_package_name || ''
async function main() {
esbuild.build({
entryPoints: ['./src/index.ts'],
outdir: 'dist/cjs',
minify: false,
bundle: true,
format: 'cjs',
target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.json',
external: ['react', 'react-dom'],
incremental: true,
watch: {
onRebuild(error) {
if (error) {
console.log(`× ${name}: An error in prevented the rebuild.`)
return
}
console.log(`${name}: Rebuilt.`)
},
},
})
}
main()

View file

@ -0,0 +1,28 @@
/* eslint-disable */
const fs = require('fs')
const esbuild = require('esbuild')
async function main() {
if (fs.existsSync('./dist')) {
fs.rmSync('./dist', { recursive: true }, (e) => {
if (e) {
throw e
}
})
}
esbuild.build({
entryPoints: ['./src/index.ts'],
outdir: 'dist/cjs',
minify: false,
bundle: true,
format: 'cjs',
target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.json',
external: ['react', 'react-dom'],
})
}
main()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"exclude": [
"node_modules",
"**/*.test.tsx",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.spec.ts",
"src/test",
"dist",
"docs"
],
"compilerOptions": {
"composite": false,
"incremental": false,
"declarationMap": false,
"sourceMap": false
}
}

View file

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"outDir": "./dist/types",
"rootDir": "src",
"baseUrl": "src"
},
"references": [{ "path": "../../packages/vec" }]
}

View file

@ -65,9 +65,11 @@
"@radix-ui/react-tooltip": "^0.0.20",
"@stitches/react": "^1.0.0",
"@tldraw/core": "^0.0.86",
"@tldraw/vec": "^0.0.86",
"@tldraw/intersect": "^0.0.86",
"perfect-freehand": "^0.5.3",
"react-hotkeys-hook": "^3.4.0",
"rko": "^0.5.25"
},
"gitHead": "55da8880eb3d8ab5fb62b5eb7853065922c95dcf"
}
}

View file

@ -1,6 +1,7 @@
/* eslint-disable */
const fs = require('fs')
const esbuild = require('esbuild')
const { gzip } = require('zlib')
const name = process.env.npm_package_name || ''
@ -25,9 +26,10 @@ async function main() {
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.json',
external: ['react', 'react-dom'],
metafile: true,
})
esbuild.buildSync({
const esmResult = esbuild.buildSync({
entryPoints: ['./src/index.ts'],
outdir: 'dist/esm',
minify: true,
@ -38,6 +40,22 @@ async function main() {
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
external: ['react', 'react-dom'],
metafile: true,
})
let esmSize = 0
Object.values(esmResult.metafile.outputs).forEach((output) => {
esmSize += output.bytes
})
fs.readFile('./dist/esm/index.js', (_err, data) => {
gzip(data, (_err, result) => {
console.log(
`${name}: Built package. ${(esmSize / 1000).toFixed(2)}kb (${(
result.length / 1000
).toFixed(2)}kb minified)`
)
})
})
console.log(`${name}: Built package.`)

View file

@ -3,13 +3,13 @@ import {
SVGContainer,
TLBounds,
Utils,
Vec,
TLTransformInfo,
Intersect,
TLHandle,
TLPointerInfo,
TLShapeProps,
} from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import getStroke from 'perfect-freehand'
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
import {
@ -22,6 +22,14 @@ import {
TLDrawShape,
ArrowBinding,
} from '~types'
import {
intersectArcBounds,
intersectCircleCircle,
intersectCircleLineSegment,
intersectLineSegmentBounds,
intersectRayBounds,
intersectRayEllipse,
} from '@tldraw/intersect'
export class Arrow extends TLDrawShapeUtil<ArrowShape, SVGSVGElement> {
type = TLDrawShapeType.Arrow as const
@ -299,12 +307,12 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape, SVGSVGElement> {
}
if (Vec.isEqual(Vec.med(start.point, end.point), bend.point)) {
return Intersect.lineSegment.bounds(sp, ep, brushBounds).length > 0
return intersectLineSegmentBounds(sp, ep, brushBounds).length > 0
} else {
const [cx, cy, r] = getCtp(shape)
const cp = Vec.add(shape.point, [cx, cy])
return Intersect.arc.bounds(cp, r, sp, ep, brushBounds).length > 0
return intersectArcBounds(cp, r, sp, ep, brushBounds).length > 0
}
}
@ -430,15 +438,13 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape, SVGSVGElement> {
const direction = Vec.uni(Vec.sub(Vec.add(anchor, shape.point), origin))
if ([TLDrawShapeType.Rectangle, TLDrawShapeType.Text].includes(target.type)) {
let hits = Intersect.ray
.bounds(origin, direction, intersectBounds, target.rotation)
let hits = intersectRayBounds(origin, direction, intersectBounds, target.rotation)
.filter((int) => int.didIntersect)
.map((int) => int.points[0])
.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))
if (hits.length < 2) {
hits = Intersect.ray
.bounds(origin, Vec.neg(direction), intersectBounds)
hits = intersectRayBounds(origin, Vec.neg(direction), intersectBounds)
.filter((int) => int.didIntersect)
.map((int) => int.points[0])
.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))
@ -451,16 +457,14 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape, SVGSVGElement> {
handlePoint = Vec.sub(hits[0], shape.point)
} else if (target.type === TLDrawShapeType.Ellipse) {
const hits = Intersect.ray
.ellipse(
origin,
direction,
center,
target.radius[0] + binding.distance,
target.radius[1] + binding.distance,
target.rotation || 0
)
.points.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))
const hits = intersectRayEllipse(
origin,
direction,
center,
target.radius[0] + binding.distance,
target.radius[1] + binding.distance,
target.rotation || 0
).points.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))
if (!hits[0]) {
console.warn('No intersections')
@ -701,7 +705,7 @@ function getCurvedArrowHeadPoints(
r2: number,
sweep: boolean
) {
const ints = Intersect.circle.circle(A, r1 * 0.618, C, r2).points
const ints = intersectCircleCircle(A, r1 * 0.618, C, r2).points
if (!ints) {
console.warn('Could not find an intersection for the arrow head.')
return { left: A, right: A }
@ -714,7 +718,7 @@ function getCurvedArrowHeadPoints(
}
function getStraightArrowHeadPoints(A: number[], B: number[], r: number) {
const ints = Intersect.circle.lineSegment(A, r, A, B).points
const ints = intersectCircleLineSegment(A, r, A, B).points
if (!ints) {
console.warn('Could not find an intersection for the arrow head.')
return { left: A, right: A }

View file

@ -1,5 +1,7 @@
import * as React from 'react'
import { SVGContainer, TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
import { SVGContainer, TLBounds, Utils, TLTransformInfo } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { intersectBoundsBounds, intersectBoundsPolyline } from '@tldraw/intersect'
import getStroke, { getStrokePoints } from 'perfect-freehand'
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
import {
@ -202,10 +204,10 @@ export class Draw extends TLDrawShapeUtil<DrawShape, SVGSVGElement> {
return (
Utils.boundsContain(brushBounds, bounds) ||
((Utils.boundsContain(bounds, brushBounds) ||
Intersect.bounds.bounds(bounds, brushBounds).length > 0) &&
Intersect.polyline.bounds(
shape.points,
Utils.translateBounds(brushBounds, Vec.neg(shape.point))
intersectBoundsBounds(bounds, brushBounds).length > 0) &&
intersectBoundsPolyline(
Utils.translateBounds(brushBounds, Vec.neg(shape.point)),
shape.points
).length > 0)
)
}
@ -220,7 +222,7 @@ export class Draw extends TLDrawShapeUtil<DrawShape, SVGSVGElement> {
return (
Utils.boundsContain(brushBounds, rBounds) ||
Intersect.bounds.polyline(
intersectBoundsPolyline(
Utils.translateBounds(brushBounds, Vec.neg(shape.point)),
rotatedBounds
).length > 0

View file

@ -1,13 +1,6 @@
import * as React from 'react'
import {
SVGContainer,
Utils,
TLTransformInfo,
TLBounds,
Intersect,
TLShapeProps,
Vec,
} from '@tldraw/core'
import { SVGContainer, Utils, TLTransformInfo, TLBounds, TLShapeProps } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import {
ArrowShape,
DashStyle,
@ -18,6 +11,11 @@ import {
} from '~types'
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
import getStroke from 'perfect-freehand'
import {
intersectLineSegmentEllipse,
intersectPolylineBounds,
intersectRayEllipse,
} from '@tldraw/intersect'
// TODO
// [ ] Improve indicator shape for drawn shapes
@ -181,7 +179,7 @@ export class Ellipse extends TLDrawShapeUtil<EllipseShape, SVGSVGElement> {
return (
rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) ||
Intersect.polyline.bounds(rotatedCorners, bounds).length > 0
intersectPolylineBounds(rotatedCorners, bounds).length > 0
)
}
@ -226,15 +224,24 @@ export class Ellipse extends TLDrawShapeUtil<EllipseShape, SVGSVGElement> {
// .map((int) => int.points[0])
// .sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0]
let intersection = Intersect.ray
.ellipse(origin, direction, center, shape.radius[0], shape.radius[1], shape.rotation || 0)
.points.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))[0]
let intersection = intersectRayEllipse(
origin,
direction,
center,
shape.radius[0],
shape.radius[1],
shape.rotation || 0
).points.sort((a, b) => Vec.dist(a, origin) - Vec.dist(b, origin))[0]
if (!intersection) {
intersection = Intersect.lineSegment
.ellipse(point, center, center, shape.radius[0], shape.radius[1], shape.rotation || 0)
.points.sort((a, b) => Vec.dist(a, point) - Vec.dist(b, point))[0]
intersection = intersectLineSegmentEllipse(
point,
center,
center,
shape.radius[0],
shape.radius[1],
shape.rotation || 0
).points.sort((a, b) => Vec.dist(a, point) - Vec.dist(b, point))[0]
}
// The anchor is a point between the handle and the intersection
@ -258,7 +265,7 @@ export class Ellipse extends TLDrawShapeUtil<EllipseShape, SVGSVGElement> {
distance = 16
} else {
// Find the distance between the point and the ellipse
const innerIntersection = Intersect.lineSegment.ellipse(
const innerIntersection = intersectLineSegmentEllipse(
point,
center,
center,

View file

@ -1,5 +1,7 @@
import * as React from 'react'
import { SVGContainer, TLBounds, Utils, Vec, Intersect, TLShapeProps } from '@tldraw/core'
import { SVGContainer, TLBounds, Utils, TLShapeProps } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { intersectRayBounds, intersectPolylineBounds } from '@tldraw/intersect'
import { defaultStyle, getPerfectDashProps } from '~shape/shape-styles'
import {
GroupShape,
@ -179,8 +181,7 @@ export class Group extends TLDrawShapeUtil<GroupShape, SVGSVGElement> {
// origin through point and expanded bounds.
// TODO: Make this a ray vs rounded rect intersection
const intersection = Intersect.ray
.bounds(origin, direction, expandedBounds)
const intersection = intersectRayBounds(origin, direction, expandedBounds)
.filter((int) => int.didIntersect)
.map((int) => int.points[0])
.sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0]
@ -228,7 +229,7 @@ export class Group extends TLDrawShapeUtil<GroupShape, SVGSVGElement> {
return (
rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) ||
Intersect.polyline.bounds(rotatedCorners, bounds).length > 0
intersectPolylineBounds(rotatedCorners, bounds).length > 0
)
}

View file

@ -1,15 +1,9 @@
import * as React from 'react'
import {
TLBounds,
Utils,
Vec,
TLTransformInfo,
Intersect,
TLShapeProps,
HTMLContainer,
} from '@tldraw/core'
import { TLBounds, Utils, TLTransformInfo, TLShapeProps, HTMLContainer } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
import { PostItShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, ArrowShape } from '~types'
import { intersectPolylineBounds, intersectRayBounds } from '@tldraw/intersect'
// TODO
// [ ] - Make sure that fill does not extend drawn shape at corners
@ -154,8 +148,7 @@ export class PostIt extends TLDrawShapeUtil<PostItShape, HTMLDivElement> {
// origin through point and expanded bounds.
// TODO: Make this a ray vs rounded rect intersection
const intersection = Intersect.ray
.bounds(origin, direction, expandedBounds)
const intersection = intersectRayBounds(origin, direction, expandedBounds)
.filter((int) => int.didIntersect)
.map((int) => int.points[0])
.sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0]
@ -198,7 +191,7 @@ export class PostIt extends TLDrawShapeUtil<PostItShape, HTMLDivElement> {
return (
rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) ||
Intersect.polyline.bounds(rotatedCorners, bounds).length > 0
intersectPolylineBounds(rotatedCorners, bounds).length > 0
)
}

View file

@ -1,14 +1,7 @@
import * as React from 'react'
import {
TLBounds,
Utils,
Vec,
TLTransformInfo,
Intersect,
TLShapeProps,
SVGContainer,
HTMLContainer,
} from '@tldraw/core'
import { TLBounds, Utils, TLTransformInfo, TLShapeProps, SVGContainer } from '@tldraw/core'
import { intersectRayBounds } from '@tldraw/intersect'
import { Vec } from '@tldraw/vec'
import getStroke from 'perfect-freehand'
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
import {
@ -234,8 +227,7 @@ export class Rectangle extends TLDrawShapeUtil<RectangleShape, SVGSVGElement> {
// origin through point and expanded bounds.
// TODO: Make this a ray vs rounded rect intersection
const intersection = Intersect.ray
.bounds(origin, direction, expandedBounds)
const intersection = intersectRayBounds(origin, direction, expandedBounds)
.filter((int) => int.didIntersect)
.map((int) => int.points[0])
.sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0]
@ -278,12 +270,7 @@ export class Rectangle extends TLDrawShapeUtil<RectangleShape, SVGSVGElement> {
bounds: TLBounds,
{ initialShape, transformOrigin, scaleX, scaleY }: TLTransformInfo<RectangleShape>
) {
if (!shape.rotation && !shape.isAspectRatioLocked) {
return {
point: Vec.round([bounds.minX, bounds.minY]),
size: Vec.round([bounds.width, bounds.height]),
}
} else {
if (shape.rotation || shape.isAspectRatioLocked) {
const size = Vec.round(
Vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY)))
)
@ -309,6 +296,11 @@ export class Rectangle extends TLDrawShapeUtil<RectangleShape, SVGSVGElement> {
point,
rotation,
}
} else {
return {
point: Vec.round([bounds.minX, bounds.minY]),
size: Vec.round([bounds.width, bounds.height]),
}
}
}

View file

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import { HTMLContainer, TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
import { HTMLContainer, TLBounds, Utils, TLTransformInfo } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { getShapeStyle, getFontStyle, defaultStyle } from '~shape/shape-styles'
import {
TextShape,
@ -12,6 +13,7 @@ import {
} from '~types'
import styled from '~styles'
import TextAreaUtils from './text-utils'
import { intersectPolylineBounds, intersectRayBounds } from '@tldraw/intersect'
const LETTER_SPACING = -1.5
@ -255,7 +257,7 @@ export class Text extends TLDrawShapeUtil<TextShape, HTMLDivElement> {
return (
rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) ||
Intersect.polyline.bounds(rotatedCorners, bounds).length > 0
intersectPolylineBounds(rotatedCorners, bounds).length > 0
)
}
@ -373,8 +375,7 @@ export class Text extends TLDrawShapeUtil<TextShape, HTMLDivElement> {
// origin through point and expanded bounds.
// TODO: Make this a ray vs rounded rect intersection
const intersection = Intersect.ray
.bounds(origin, direction, expandedBounds)
const intersection = intersectRayBounds(origin, direction, expandedBounds)
.filter((int) => int.didIntersect)
.map((int) => int.points[0])
.sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0]

View file

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Utils, Vec } from '@tldraw/core'
import { Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { TLDR } from '~state/tldr'
import type { Data, PagePartial, TLDrawCommand } from '~types'

View file

@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { ArrowShape, Data, PagePartial, TLDrawCommand, TLDrawShape } from '~types'
import { TLDR } from '~state/tldr'
import { Utils, Vec } from '@tldraw/core'
import { Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
export function moveToPage(
data: Data,

View file

@ -1,4 +1,5 @@
import { Utils, Vec } from '@tldraw/core'
import { Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import type { TLDrawCommand, Data } from '~types'
import { TLDR } from '~state/tldr'

View file

@ -1,4 +1,4 @@
import { Vec } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import type { Data, TLDrawCommand, PagePartial } from '~types'
import { TLDR } from '~state/tldr'

View file

@ -7,7 +7,8 @@ import {
Session,
TLDrawStatus,
} from '~types'
import { Vec, Utils, TLHandle } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { Utils } from '@tldraw/core'
import { TLDR } from '~state/tldr'
export class ArrowSession implements Session {

View file

@ -1,4 +1,5 @@
import { brushUpdater, Utils, Vec } from '@tldraw/core'
import { brushUpdater, Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { Data, Session, TLDrawPatch, TLDrawStatus } from '~types'
import { getShapeUtils } from '~shape'
import { TLDR } from '~state/tldr'

View file

@ -1,4 +1,5 @@
import { Utils, Vec } from '@tldraw/core'
import { Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { Data, DrawShape, Session, TLDrawStatus } from '~types'
import { TLDR } from '~state/tldr'

View file

@ -1,4 +1,4 @@
import { Vec } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { ShapesWithProp, TLDrawStatus } from '~types'
import type { Session } from '~types'
import type { Data } from '~types'

View file

@ -1,4 +1,5 @@
import { Utils, Vec } from '@tldraw/core'
import { Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { Session, TLDrawShape, TLDrawStatus } from '~types'
import type { Data } from '~types'
import { TLDR } from '~state/tldr'

View file

@ -1,4 +1,5 @@
import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core'
import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { TLDrawShape, TLDrawStatus } from '~types'
import type { Session } from '~types'
import type { Data } from '~types'

View file

@ -1,4 +1,5 @@
import { TLBoundsCorner, TLBoundsEdge, Utils, Vec } from '@tldraw/core'
import { TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import { Session, TLDrawShape, TLDrawStatus } from '~types'
import type { Data } from '~types'
import { TLDR } from '~state/tldr'

View file

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { TLPageState, Utils, Vec } from '@tldraw/core'
import { TLPageState, Utils } from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import {
TLDrawShape,
TLDrawBinding,

View file

@ -1,4 +1,4 @@
import { TLBounds, TLTransformInfo, Vec, Utils, TLPageState } from '@tldraw/core'
import { TLBounds, TLTransformInfo, Utils, TLPageState } from '@tldraw/core'
import { getShapeUtils } from '~shape'
import type {
Data,
@ -11,6 +11,7 @@ import type {
TLDrawCommand,
TLDrawPatch,
} from '~types'
import { Vec } from '@tldraw/vec'
export class TLDR {
static getShapeUtils<T extends TLDrawShape>(

View file

@ -11,11 +11,11 @@ import {
TLPointerEventHandler,
TLWheelEventHandler,
Utils,
Vec,
brushUpdater,
TLPointerInfo,
TLBounds,
} from '@tldraw/core'
import { Vec } from '@tldraw/vec'
import {
FlipType,
TextShape,

View file

@ -6,12 +6,16 @@
"strict": true,
"composite": true,
"emitDeclarationOnly": true,
"rootDir": "src",
"rootDir": ".",
"outDir": "./dist/types",
"baseUrl": "src",
"paths": {
"~*": ["./*"]
}
},
"references": [{ "path": "../../packages/core" }]
"references": [
{ "path": "../../packages/intersect" },
{ "path": "../../packages/vec" },
{ "path": "../../packages/core" }
]
}

3
packages/vec/README.md Normal file
View file

@ -0,0 +1,3 @@
# Dev Server
Dev server with fast refresh.

55
packages/vec/package.json Normal file
View file

@ -0,0 +1,55 @@
{
"name": "@tldraw/vec",
"version": "0.0.86",
"private": false,
"description": "A tiny little drawing app (vec)",
"author": "@steveruizok",
"repository": {
"type": "git",
"url": "git+https://github.com/tldraw/tldraw.git",
"directory": "packages/vec"
},
"license": "MIT",
"keywords": [],
"files": [
"dist/**/*"
],
"sideEffects": false,
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"typings": "./dist/types/index.d.ts",
"scripts": {
"start:pre": "node scripts/pre-dev && yarn types:pre",
"start": "node scripts/dev & yarn types:dev",
"build": "node scripts/build && yarn types:build",
"types:pre": "tsc",
"types:dev": "tsc --watch",
"types:build": "tsc --project tsconfig.build.json",
"lint": "eslint src/ --ext .ts,.tsx",
"clean": "rm -rf dist",
"ts-node": "ts-node",
"docs": "typedoc",
"docs:watch": "typedoc --watch"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/preset-env": "^7.15.4",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.10",
"@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^4.30.0",
"esbuild": "^0.12.24",
"eslint": "^7.32.0",
"lerna": "^4.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"ts-node": "^10.2.1",
"tslib": "^2.3.1",
"typedoc": "^0.21.9",
"typescript": "^4.4.2"
},
"peerDependencies": {},
"dependencies": {},
"gitHead": "55da8880eb3d8ab5fb62b5eb7853065922c95dcf"
}

View file

@ -0,0 +1,52 @@
/* eslint-disable */
const fs = require('fs')
const esbuild = require('esbuild')
const name = process.env.npm_package_name || ''
async function main() {
if (fs.existsSync('./dist')) {
fs.rmSync('./dist', { recursive: true }, (e) => {
if (e) {
throw e
}
})
}
try {
esbuild.buildSync({
entryPoints: ['./src/index.ts'],
outdir: 'dist/cjs',
minify: true,
bundle: true,
sourcemap: true,
format: 'cjs',
target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.build.json',
external: ['react', 'react-dom'],
})
esbuild.buildSync({
entryPoints: ['./src/index.ts'],
outdir: 'dist/esm',
minify: true,
bundle: true,
sourcemap: true,
format: 'esm',
target: 'es6',
tsconfig: './tsconfig.build.json',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
external: ['react', 'react-dom'],
})
console.log(`${name}: Built package.`)
} catch (e) {
console.log(`× ${name}: Build failed due to an error.`)
console.log(e)
}
}
main()

View file

@ -0,0 +1,31 @@
/* eslint-disable */
const esbuild = require('esbuild')
const name = process.env.npm_package_name || ''
async function main() {
esbuild.build({
entryPoints: ['./src/index.ts'],
outdir: 'dist/cjs',
minify: false,
bundle: true,
format: 'cjs',
target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
tsconfig: './tsconfig.json',
external: ['react', 'react-dom'],
incremental: true,
watch: {
onRebuild(error) {
if (error) {
console.log(`× ${name}: An error in prevented the rebuild.`)
return
}
console.log(`${name}: Rebuilt.`)
},
},
})
}
main()

View file

@ -0,0 +1,27 @@
/* eslint-disable */
const fs = require('fs')
const esbuild = require('esbuild')
async function main() {
if (fs.existsSync('./dist')) {
fs.rmSync('./dist', { recursive: true }, (e) => {
if (e) {
throw e
}
})
}
esbuild.build({
entryPoints: ['./src/index.ts'],
outdir: 'dist/cjs',
minify: true,
bundle: true,
sourcemap: true,
format: 'cjs',
target: 'es6',
tsconfig: './tsconfig.json',
external: ['react', 'react-dom'],
})
}
main()

View file

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"exclude": [
"node_modules",
"**/*.test.tsx",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.spec.ts",
"src/test",
"dist",
"docs"
],
"compilerOptions": {
"composite": false,
"incremental": false,
"declarationMap": false,
"sourceMap": false
}
}

View file

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"outDir": "./dist/types",
"rootDir": "src",
"baseUrl": "src"
}
}

View file

@ -3,7 +3,9 @@
// For references
"declaration": true,
"declarationMap": true,
"composite": true,
"sourceMap": true,
"emitDeclarationOnly": true,
// Other
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,

View file

@ -3,12 +3,19 @@
"extends": "./tsconfig.base.json",
"exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts"],
"files": [],
"references": [{ "path": "./packages/tldraw" }, { "path": "./packages/core" }],
"references": [
{ "path": "./packages/vec" },
{ "path": "./packages/intersect" },
{ "path": "./packages/tldraw" },
{ "path": "./packages/core" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@tldraw/tldraw": ["./packages/tldraw/dist"],
"@tldraw/core": ["./packages/core/dist"],
"@tldraw/tldraw": ["./packages/tldraw"],
"@tldraw/core": ["./packages/core"],
"@tldraw/vec": ["./packages/vec"],
"@tldraw/intersect": ["./packages/intersect"],
"+*": ["./packages/core/src/*"],
"~*": ["./packages/tldraw/src/*"]
}