[tech debt] Primitives renaming party / cleanup (#2396)

This PR:
- renames Vec2d to Vec
- renames Vec2dModel to VecModel
- renames Box2d to Box
- renames Box2dModel to BoxModel
- renames Matrix2d to Mat
- renames Matrix2dModel to MatModel
- removes unused primitive helpers
- removes unused exports
- removes a few redundant tests in dgreensp

### Change Type

- [x] `major` — Breaking change

### Release Notes

- renames Vec2d to Vec
- renames Vec2dModel to VecModel
- renames Box2d to Box
- renames Box2dModel to BoxModel
- renames Matrix2d to Mat
- renames Matrix2dModel to MatModel
- removes unused primitive helpers
This commit is contained in:
Steve Ruiz 2024-01-03 12:13:15 +00:00 committed by GitHub
parent afd5af1cb6
commit 6b1005ef71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
159 changed files with 4757 additions and 5319 deletions

View file

@ -1,5 +1,5 @@
import test, { Page, expect } from '@playwright/test'
import { Box2dModel, Editor } from '@tldraw/tldraw'
import { BoxModel, Editor } from '@tldraw/tldraw'
import { setupPage } from '../shared-e2e'
export function sleep(ms: number) {
@ -29,7 +29,7 @@ const measureTextSpansOptions = {
textAlign: 'start' as 'start' | 'middle' | 'end',
}
function formatLines(spans: { box: Box2dModel; text: string }[]) {
function formatLines(spans: { box: BoxModel; text: string }[]) {
const lines = []
let currentLine: string[] | null = null
@ -85,7 +85,7 @@ test.describe('text measurement', () => {
test('should get a single text span', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('testing', options),
@ -97,7 +97,7 @@ test.describe('text measurement', () => {
test('should wrap a word when it has to', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('testing', { ...options, width: 50 }),
@ -109,7 +109,7 @@ test.describe('text measurement', () => {
test('should preserve whitespace at line breaks', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('testing testing', options),
@ -121,7 +121,7 @@ test.describe('text measurement', () => {
test('should preserve whitespace at the end of wrapped lines', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('testing testing ', options),
@ -136,7 +136,7 @@ test.describe('text measurement', () => {
test('preserves whitespace at the end of unwrapped lines', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) =>
@ -149,7 +149,7 @@ test.describe('text measurement', () => {
test('preserves whitespace at the start of an unwrapped line', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) =>
@ -162,7 +162,7 @@ test.describe('text measurement', () => {
test('should place starting whitespace on its own line if it has to', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans(' testing testing', options),
@ -174,7 +174,7 @@ test.describe('text measurement', () => {
test('should handle multiline text', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) =>
@ -192,7 +192,7 @@ test.describe('text measurement', () => {
test('should break long strings of text', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) =>
@ -212,7 +212,7 @@ test.describe('text measurement', () => {
test('should return an empty array if the text is empty', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(async (options) => editor.textMeasure.measureTextSpans('', options), measureTextSpansOptions)
@ -221,7 +221,7 @@ test.describe('text measurement', () => {
test('should handle trailing newlines', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('hi\n\n\n', options),
@ -233,7 +233,7 @@ test.describe('text measurement', () => {
test('should handle only newlines', async () => {
const spans = await page.evaluate<
{ text: string; box: Box2dModel }[],
{ text: string; box: BoxModel }[],
typeof measureTextSpansOptions
>(
async (options) => editor.textMeasure.measureTextSpans('\n\n\n', options),

View file

@ -1,4 +1,4 @@
import { Tldraw, Vec2d, useContainer, useEditor } from '@tldraw/tldraw'
import { Tldraw, Vec, useContainer, useEditor } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { useEffect } from 'react'
@ -32,7 +32,7 @@ function SneakyFloatyHook() {
if (sx !== x || sy !== y) {
x = sx
y = sy
editor.setCamera(new Vec2d(-x, -y))
editor.setCamera(new Vec(-x, -y))
}
}

View file

@ -1,4 +1,4 @@
import { Box2d, StateNode, atom, copyAs, exportAs } from '@tldraw/tldraw'
import { Box, StateNode, atom, copyAs, exportAs } from '@tldraw/tldraw'
// There's a guide at the bottom of this file!
@ -6,7 +6,7 @@ export class ScreenshotDragging extends StateNode {
static override id = 'dragging'
// [1]
screenshotBox = atom('screenshot brush', new Box2d())
screenshotBox = atom('screenshot brush', new Box())
// [2]
override onEnter = () => {
@ -30,7 +30,7 @@ export class ScreenshotDragging extends StateNode {
inputs: { shiftKey, altKey, originPagePoint, currentPagePoint },
} = this.editor
const box = Box2d.FromPoints([originPagePoint, currentPagePoint])
const box = Box.FromPoints([originPagePoint, currentPagePoint])
if (shiftKey) {
if (box.w > box.h * (16 / 9)) {

View file

@ -1,10 +1,10 @@
import {
Box2d,
Box,
TLEditorComponents,
TLUiAssetUrlOverrides,
TLUiOverrides,
Tldraw,
Vec2d,
Vec,
toolbarItem,
useEditor,
useValue,
@ -69,11 +69,11 @@ function ScreenshotBox() {
// "page space", i.e. uneffected by scale, and relative to the tldraw
// page's top left corner.
const zoomLevel = editor.getZoomLevel()
const { x, y } = Vec2d.Sub(
const { x, y } = Vec.Sub(
editor.pageToScreen({ x: box.x, y: box.y }),
editor.getViewportScreenBounds()
)
return new Box2d(x, y, box.w * zoomLevel, box.h * zoomLevel)
return new Box(x, y, box.w * zoomLevel, box.h * zoomLevel)
},
[editor]
)

View file

@ -12,7 +12,7 @@ import {
TLOnBeforeUpdateHandler,
TLOnHandleChangeHandler,
TLOnResizeHandler,
Vec2d,
Vec,
deepCopy,
getDefaultColorTheme,
resizeBox,
@ -128,21 +128,21 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
const MIN_DISTANCE = slantedLength / 5
const MAX_DISTANCE = slantedLength / 1.5
const handleInShapeSpace = new Vec2d(handles.handle.x * w, handles.handle.y * h)
const handleInShapeSpace = new Vec(handles.handle.x * w, handles.handle.y * h)
const distanceToIntersection = handleInShapeSpace.dist(segmentsIntersection)
const center = new Vec2d(w / 2, h / 2)
const vHandle = Vec2d.Sub(handleInShapeSpace, center).uni()
const center = new Vec(w / 2, h / 2)
const vHandle = Vec.Sub(handleInShapeSpace, center).uni()
let newPoint = handleInShapeSpace
if (insideShape) {
newPoint = Vec2d.Add(segmentsIntersection, vHandle.mul(MIN_DISTANCE))
newPoint = Vec.Add(segmentsIntersection, vHandle.mul(MIN_DISTANCE))
} else {
if (distanceToIntersection <= MIN_DISTANCE) {
newPoint = Vec2d.Add(segmentsIntersection, vHandle.mul(MIN_DISTANCE))
newPoint = Vec.Add(segmentsIntersection, vHandle.mul(MIN_DISTANCE))
} else if (distanceToIntersection >= MAX_DISTANCE) {
newPoint = Vec2d.Add(segmentsIntersection, vHandle.mul(MAX_DISTANCE))
newPoint = Vec.Add(segmentsIntersection, vHandle.mul(MAX_DISTANCE))
}
}

View file

@ -1,19 +1,19 @@
import { Vec2d, VecLike, lerp, pointInPolygon } from '@tldraw/tldraw'
import { Vec, VecLike, lerp, pointInPolygon } from '@tldraw/tldraw'
import { SpeechBubbleShape } from './SpeechBubbleUtil'
export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec2d[] => {
export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec[] => {
const { w, h, handles } = shape.props
const handleInShapeSpace = new Vec2d(handles.handle.x * w, handles.handle.y * h)
const handleInShapeSpace = new Vec(handles.handle.x * w, handles.handle.y * h)
const [tl, tr, br, bl] = [new Vec2d(0, 0), new Vec2d(w, 0), new Vec2d(w, h), new Vec2d(0, h)]
const [tl, tr, br, bl] = [new Vec(0, 0), new Vec(w, 0), new Vec(w, h), new Vec(0, h)]
const offsetH = w / 10
const offsetV = h / 10
const { adjustedIntersection, intersectionSegmentIndex } = getHandleIntersectionPoint(shape)
let vertices: Vec2d[]
let vertices: Vec[]
// Inject the tail segments into the geometry of the shape
switch (intersectionSegmentIndex) {
@ -21,9 +21,9 @@ export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec2d[] => {
// top
vertices = [
tl,
new Vec2d(adjustedIntersection.x - offsetH, adjustedIntersection.y),
new Vec2d(handleInShapeSpace.x, handleInShapeSpace.y),
new Vec2d(adjustedIntersection.x + offsetH, adjustedIntersection.y),
new Vec(adjustedIntersection.x - offsetH, adjustedIntersection.y),
new Vec(handleInShapeSpace.x, handleInShapeSpace.y),
new Vec(adjustedIntersection.x + offsetH, adjustedIntersection.y),
tr,
br,
bl,
@ -34,9 +34,9 @@ export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec2d[] => {
vertices = [
tl,
tr,
new Vec2d(adjustedIntersection.x, adjustedIntersection.y - offsetV),
new Vec2d(handleInShapeSpace.x, handleInShapeSpace.y),
new Vec2d(adjustedIntersection.x, adjustedIntersection.y + offsetV),
new Vec(adjustedIntersection.x, adjustedIntersection.y - offsetV),
new Vec(handleInShapeSpace.x, handleInShapeSpace.y),
new Vec(adjustedIntersection.x, adjustedIntersection.y + offsetV),
br,
bl,
]
@ -47,9 +47,9 @@ export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec2d[] => {
tl,
tr,
br,
new Vec2d(adjustedIntersection.x + offsetH, adjustedIntersection.y),
new Vec2d(handleInShapeSpace.x, handleInShapeSpace.y),
new Vec2d(adjustedIntersection.x - offsetH, adjustedIntersection.y),
new Vec(adjustedIntersection.x + offsetH, adjustedIntersection.y),
new Vec(handleInShapeSpace.x, handleInShapeSpace.y),
new Vec(adjustedIntersection.x - offsetH, adjustedIntersection.y),
bl,
]
break
@ -60,9 +60,9 @@ export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec2d[] => {
tr,
br,
bl,
new Vec2d(adjustedIntersection.x, adjustedIntersection.y + offsetV),
new Vec2d(handleInShapeSpace.x, handleInShapeSpace.y),
new Vec2d(adjustedIntersection.x, adjustedIntersection.y - offsetV),
new Vec(adjustedIntersection.x, adjustedIntersection.y + offsetV),
new Vec(handleInShapeSpace.x, handleInShapeSpace.y),
new Vec(adjustedIntersection.x, adjustedIntersection.y - offsetV),
]
break
default:
@ -74,10 +74,10 @@ export const getSpeechBubbleVertices = (shape: SpeechBubbleShape): Vec2d[] => {
export function getHandleIntersectionPoint(shape: SpeechBubbleShape) {
const { w, h, handles } = shape.props
const handleInShapeSpace = new Vec2d(handles.handle.x * w, handles.handle.y * h)
const handleInShapeSpace = new Vec(handles.handle.x * w, handles.handle.y * h)
const center = new Vec2d(w / 2, h / 2)
const corners = [new Vec2d(0, 0), new Vec2d(w, 0), new Vec2d(w, h), new Vec2d(0, h)]
const center = new Vec(w / 2, h / 2)
const corners = [new Vec(0, 0), new Vec(w, 0), new Vec(w, h), new Vec(0, h)]
const segments = [
[corners[0], corners[1]],
[corners[1], corners[2]],
@ -85,8 +85,8 @@ export function getHandleIntersectionPoint(shape: SpeechBubbleShape) {
[corners[3], corners[0]],
]
let segmentsIntersection: Vec2d | null = null
let intersectionSegment: Vec2d[] | null = null
let segmentsIntersection: Vec | null = null
let intersectionSegment: Vec[] | null = null
// If the point inside of the box's corners?
const insideShape = pointInPolygon(handleInShapeSpace, corners)
@ -94,7 +94,7 @@ export function getHandleIntersectionPoint(shape: SpeechBubbleShape) {
// We want to be sure we get an intersection, so if the point is
// inside the shape, push it away from the center by a big distance
const pointToCheck = insideShape
? Vec2d.Add(handleInShapeSpace, Vec2d.Sub(handleInShapeSpace, center).uni().mul(1000000))
? Vec.Add(handleInShapeSpace, Vec.Sub(handleInShapeSpace, center).uni().mul(1000000))
: handleInShapeSpace
// Test each segment for an intersection
@ -120,11 +120,11 @@ export function getHandleIntersectionPoint(shape: SpeechBubbleShape) {
const intersectionSegmentIndex = segments.indexOf(intersectionSegment)
// a normalised vector from start to end, so this can work in any direction
const unit = Vec2d.Sub(end, start).uni()
const unit = Vec.Sub(end, start).uni()
// Where is the intersection relative to the start?
const totalDistance = Vec2d.Dist(start, end)
const distance = Vec2d.Dist(segmentsIntersection, start)
const totalDistance = Vec.Dist(start, end)
const distance = Vec.Dist(segmentsIntersection, start)
// make it stick to the middle
const middleRelative = mapRange(0, totalDistance, -1, 1, distance) // absolute -> -1 to 1
@ -170,7 +170,7 @@ function intersectLineSegmentLineSegment(a1: VecLike, a2: VecLike, b1: VecLike,
const ua = ua_t / u_b
const ub = ub_t / u_b
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
return Vec2d.AddXY(a1, ua * AVx, ua * AVy)
return Vec.AddXY(a1, ua * AVx, ua * AVy)
}
}

View file

@ -4,7 +4,7 @@ import {
TLEditorComponents,
track,
useEditor,
Vec2d,
Vec,
} from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { useState } from 'react'
@ -62,7 +62,7 @@ const MyComponentInFront = track(() => {
if (!selectionRotatedPageBounds) return null
const pageCoordinates = Vec2d.Sub(
const pageCoordinates = Vec.Sub(
editor.pageToScreen(selectionRotatedPageBounds.point),
editor.getViewportScreenBounds()
)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -145,6 +145,15 @@ export {
type TLEditorOptions,
type TLResizeShapeOptions,
} from './lib/editor/Editor'
export type {
TLAfterChangeHandler,
TLAfterCreateHandler,
TLAfterDeleteHandler,
TLBatchCompleteHandler,
TLBeforeChangeHandler,
TLBeforeCreateHandler,
TLBeforeDeleteHandler,
} from './lib/editor/managers/SideEffectManager'
export {
SnapManager,
type GapsSnapLine,
@ -181,16 +190,12 @@ export {
type TLShapeUtilFlag,
} from './lib/editor/shapes/ShapeUtil'
export { GroupShapeUtil } from './lib/editor/shapes/group/GroupShapeUtil'
export { getArrowheadPathForType } from './lib/editor/shapes/shared/arrow/arrowheads'
export {
getCurvedArrowHandlePath,
getSolidCurvedArrowPath,
} from './lib/editor/shapes/shared/arrow/curved-arrow'
type TLArcInfo,
type TLArrowInfo,
type TLArrowPoint,
} from './lib/editor/shapes/shared/arrow/arrow-types'
export { getArrowTerminalsInArrowSpace } from './lib/editor/shapes/shared/arrow/shared'
export {
getSolidStraightArrowPath,
getStraightArrowHandlePath,
} from './lib/editor/shapes/shared/arrow/straight-arrow'
export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox'
export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode'
@ -259,7 +264,7 @@ export { useSelectionEvents } from './lib/hooks/useSelectionEvents'
export { useTLStore } from './lib/hooks/useTLStore'
export { useTransform } from './lib/hooks/useTransform'
export {
Box2d,
Box,
ROTATE_CORNER_TO_SELECTION_CORNER,
rotateSelectionHandle,
type BoxLike,
@ -267,9 +272,9 @@ export {
type SelectionCorner,
type SelectionEdge,
type SelectionHandle,
} from './lib/primitives/Box2d'
export { Matrix2d, type Matrix2dModel } from './lib/primitives/Matrix2d'
export { Vec2d, type VecLike } from './lib/primitives/Vec2d'
} from './lib/primitives/Box'
export { Mat, type MatLike, type MatModel } from './lib/primitives/Mat'
export { Vec, type VecLike } from './lib/primitives/Vec'
export { EASINGS } from './lib/primitives/easings'
export { Arc2d } from './lib/primitives/geometry/Arc2d'
export { Circle2d } from './lib/primitives/geometry/Circle2d'
@ -279,24 +284,29 @@ export { Edge2d } from './lib/primitives/geometry/Edge2d'
export { Ellipse2d } from './lib/primitives/geometry/Ellipse2d'
export { Geometry2d } from './lib/primitives/geometry/Geometry2d'
export { Group2d } from './lib/primitives/geometry/Group2d'
export { Point2d } from './lib/primitives/geometry/Point2d'
export { Polygon2d } from './lib/primitives/geometry/Polygon2d'
export { Polyline2d } from './lib/primitives/geometry/Polyline2d'
export { Rectangle2d } from './lib/primitives/geometry/Rectangle2d'
export { Stadium2d } from './lib/primitives/geometry/Stadium2d'
export {
intersectCircleCircle,
intersectCirclePolygon,
intersectCirclePolyline,
intersectLineSegmentCircle,
intersectLineSegmentLineSegment,
intersectLineSegmentPolygon,
intersectLineSegmentPolyline,
intersectPolygonBounds,
intersectPolygonPolygon,
linesIntersect,
polygonsIntersect,
} from './lib/primitives/intersect'
export {
EPSILON,
HALF_PI,
PI,
PI2,
SIN,
TAU,
angleDelta,
approximately,
areAnglesCompatible,
average,
@ -305,24 +315,11 @@ export {
clampRadians,
clockwiseAngleDist,
degreesToRadians,
getArcLength,
getPointOnCircle,
getPolygonVertices,
getStarBounds,
getSweep,
isAngleBetween,
isSafeFloat,
lerpAngles,
longAngleDist,
perimeterOfEllipse,
pointInBounds,
pointInCircle,
pointInEllipse,
pointInPolygon,
pointInPolyline,
pointInRect,
pointNearToLineSegment,
pointNearToPolyline,
precise,
radiansToDegrees,
rangeIntersection,

View file

@ -12,7 +12,7 @@ import { useFixSafariDoubleTapZoomPencilEvents } from '../hooks/useFixSafariDoub
import { useGestureEvents } from '../hooks/useGestureEvents'
import { useHandleEvents } from '../hooks/useHandleEvents'
import { useScreenBounds } from '../hooks/useScreenBounds'
import { Matrix2d } from '../primitives/Matrix2d'
import { Mat } from '../primitives/Mat'
import { toDomPrecision } from '../primitives/utils'
import { debugFlags } from '../utils/debug-flags'
import { GeometryDebuggingView } from './GeometryDebuggingView'
@ -273,7 +273,7 @@ function HandlesWrapper() {
return (
<Handles>
<g transform={Matrix2d.toCssString(transform)}>
<g transform={Mat.toCssString(transform)}>
{handlesToDisplay.map((handle) => {
return (
<HandleWrapper
@ -520,7 +520,7 @@ function UiLogger() {
)
}
export function SelectionForegroundWrapper() {
function SelectionForegroundWrapper() {
const editor = useEditor()
const selectionRotation = useValue('selection rotation', () => editor.getSelectionRotation(), [
editor,
@ -535,7 +535,7 @@ export function SelectionForegroundWrapper() {
return <SelectionForeground bounds={selectionBounds} rotation={selectionRotation} />
}
export function SelectionBackgroundWrapper() {
function SelectionBackgroundWrapper() {
const editor = useEditor()
const selectionRotation = useValue('selection rotation', () => editor.getSelectionRotation(), [
editor,
@ -550,13 +550,13 @@ export function SelectionBackgroundWrapper() {
return <SelectionBackground bounds={selectionBounds} rotation={selectionRotation} />
}
export function OnTheCanvasWrapper() {
function OnTheCanvasWrapper() {
const { OnTheCanvas } = useEditorComponents()
if (!OnTheCanvas) return null
return <OnTheCanvas />
}
export function InFrontOfTheCanvasWrapper() {
function InFrontOfTheCanvasWrapper() {
const { InFrontOfTheCanvas } = useEditorComponents()
if (!InFrontOfTheCanvas) return null
return <InFrontOfTheCanvas />

View file

@ -5,7 +5,7 @@ import { ShapeUtil } from '../editor/shapes/ShapeUtil'
import { nearestMultiple } from '../hooks/useDPRMultiple'
import { useEditor } from '../hooks/useEditor'
import { useEditorComponents } from '../hooks/useEditorComponents'
import { Matrix2d } from '../primitives/Matrix2d'
import { Mat } from '../primitives/Mat'
import { toDomPrecision } from '../primitives/utils'
import { OptionalErrorBoundary } from './ErrorBoundary'
@ -56,7 +56,7 @@ export const Shape = track(function Shape({
if (!shape) return // probably the shape was just deleted
const pageTransform = editor.getShapePageTransform(id)
const transform = Matrix2d.toCssString(pageTransform)
const transform = Mat.toCssString(pageTransform)
setProperty('transform', transform)
},
[editor, setProperty]

View file

@ -24,7 +24,7 @@ const EvenInnererIndicator = ({ shape, util }: { shape: TLShape; util: ShapeUtil
return useStateTracking('Indicator:' + shape.type, () => util.indicator(shape))
}
export const InnerIndicator = ({ editor, id }: { editor: Editor; id: TLShapeId }) => {
const InnerIndicator = ({ editor, id }: { editor: Editor; id: TLShapeId }) => {
const shape = useValue('shape', () => new ShapeWithPropsEquality(editor.store.get(id)), [
editor,
id,

View file

@ -1,11 +1,11 @@
import { Box2dModel } from '@tldraw/tlschema'
import { BoxModel } from '@tldraw/tlschema'
import { ComponentType, useRef } from 'react'
import { useTransform } from '../../hooks/useTransform'
import { toDomPrecision } from '../../primitives/utils'
/** @public */
export type TLBrushComponent = ComponentType<{
brush: Box2dModel
brush: BoxModel
color?: string
opacity?: number
className?: string

View file

@ -1,16 +1,16 @@
import { Vec2dModel } from '@tldraw/tlschema'
import { VecModel } from '@tldraw/tlschema'
import classNames from 'classnames'
import { ComponentType, useRef } from 'react'
import { useTransform } from '../../hooks/useTransform'
import { Box2d } from '../../primitives/Box2d'
import { Vec2d } from '../../primitives/Vec2d'
import { Box } from '../../primitives/Box'
import { Vec } from '../../primitives/Vec'
import { clamp } from '../../primitives/utils'
/** @public */
export type TLCollaboratorHintComponent = ComponentType<{
className?: string
point: Vec2dModel
viewport: Box2d
point: VecModel
viewport: Box
zoom: number
opacity?: number
color: string
@ -32,7 +32,7 @@ export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({
clamp(point.x, viewport.minX + 5 / zoom, viewport.maxX - 5 / zoom),
clamp(point.y, viewport.minY + 5 / zoom, viewport.maxY - 5 / zoom),
1 / zoom,
Vec2d.Angle(viewport.center, point)
Vec.Angle(viewport.center, point)
)
return (

View file

@ -1,4 +1,4 @@
import { Vec2dModel } from '@tldraw/tlschema'
import { VecModel } from '@tldraw/tlschema'
import classNames from 'classnames'
import { ComponentType, memo, useRef } from 'react'
import { useTransform } from '../../hooks/useTransform'
@ -6,7 +6,7 @@ import { useTransform } from '../../hooks/useTransform'
/** @public */
export type TLCursorComponent = ComponentType<{
className?: string
point: Vec2dModel | null
point: VecModel | null
zoom: number
color?: string
name: string | null

View file

@ -1,11 +1,11 @@
import * as React from 'react'
import { useTransform } from '../../hooks/useTransform'
import { Box2d } from '../../primitives/Box2d'
import { Box } from '../../primitives/Box'
import { toDomPrecision } from '../../primitives/utils'
/** @public */
export type TLSelectionBackgroundComponent = React.ComponentType<{
bounds: Box2d
bounds: Box
rotation: number
}>

View file

@ -3,12 +3,12 @@ import classNames from 'classnames'
import { ComponentType, useRef } from 'react'
import { useEditor } from '../../hooks/useEditor'
import { useTransform } from '../../hooks/useTransform'
import { Box2d } from '../../primitives/Box2d'
import { Box } from '../../primitives/Box'
import { toDomPrecision } from '../../primitives/utils'
/** @public */
export type TLSelectionForegroundComponent = ComponentType<{
bounds: Box2d
bounds: Box
rotation: number
}>

View file

@ -75,7 +75,7 @@ const Versions = {
Initial: 0,
} as const
export const CURRENT_SESSION_STATE_SNAPSHOT_VERSION = Versions.Initial
const CURRENT_SESSION_STATE_SNAPSHOT_VERSION = Versions.Initial
/**
* The state of the editor instance, not including any document state.

View file

@ -76,9 +76,9 @@ import {
SVG_PADDING,
ZOOMS,
} from '../constants'
import { Box2d } from '../primitives/Box2d'
import { MatLike, Matrix2d, Matrix2dModel } from '../primitives/Matrix2d'
import { Vec2d, VecLike } from '../primitives/Vec2d'
import { Box } from '../primitives/Box'
import { Mat, MatLike, MatModel } from '../primitives/Mat'
import { Vec, VecLike } from '../primitives/Vec'
import { EASINGS } from '../primitives/easings'
import { Geometry2d } from '../primitives/geometry/Geometry2d'
import { Group2d } from '../primitives/geometry/Group2d'
@ -140,7 +140,7 @@ export type TLAnimationOptions = Partial<{
/** @public */
export type TLResizeShapeOptions = Partial<{
initialBounds: Box2d
initialBounds: Box
scaleOrigin: VecLike
scaleAxisRotation: number
initialShape: TLShape
@ -1630,11 +1630,11 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
@computed getSelectionPageBounds(): Box2d | null {
@computed getSelectionPageBounds(): Box | null {
const selectedShapeIds = this.getCurrentPageState().selectedShapeIds
if (selectedShapeIds.length === 0) return null
return Box2d.Common(compact(selectedShapeIds.map((id) => this.getShapePageBounds(id))))
return Box.Common(compact(selectedShapeIds.map((id) => this.getShapePageBounds(id))))
}
/**
@ -1665,7 +1665,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @readonly
* @public
*/
@computed getSelectionRotatedPageBounds(): Box2d | undefined {
@computed getSelectionRotatedPageBounds(): Box | undefined {
const selectedShapeIds = this.getSelectedShapeIds()
if (selectedShapeIds.length === 0) {
@ -1685,14 +1685,14 @@ export class Editor extends EventEmitter<TLEventMap> {
}
// need to 'un-rotate' all the outlines of the existing nodes so we can fit them inside a box
const boxFromRotatedVertices = Box2d.FromPoints(
const boxFromRotatedVertices = Box.FromPoints(
this.getSelectedShapeIds()
.flatMap((id) => {
const pageTransform = this.getShapePageTransform(id)
if (!pageTransform) return []
return pageTransform.applyToPoints(this.getShapeGeometry(id).vertices)
})
.map((p) => Vec2d.Rot(p, -selectionRotation))
.map((p) => Vec.Rot(p, -selectionRotation))
)
// now position box so that it's top-left corner is in the right place
boxFromRotatedVertices.point = boxFromRotatedVertices.point.rot(selectionRotation)
@ -2124,7 +2124,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (animation) {
const { width, height } = this.getViewportScreenBounds()
return this._animateToViewport(new Box2d(-x, -y, width / z, height / z), animation)
return this._animateToViewport(new Box(-x, -y, width / z, height / z), animation)
} else {
this._setCamera({ x, y, z })
}
@ -2200,7 +2200,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const ids = [...this.getCurrentPageShapeIds()]
if (ids.length <= 0) return this
const pageBounds = Box2d.Common(compact(ids.map((id) => this.getShapePageBounds(id))))
const pageBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id))))
this.zoomToBounds(pageBounds, undefined, animation)
return this
}
@ -2349,7 +2349,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (!this.getInstanceState().canMoveCamera) return this
if (ids.length <= 0) return this
const selectionBounds = Box2d.Common(compact(ids.map((id) => this.getShapePageBounds(id))))
const selectionBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id))))
const viewportPageBounds = this.getViewportPageBounds()
@ -2407,7 +2407,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
zoomToBounds(bounds: Box2d, targetZoom?: number, animation?: TLAnimationOptions): this {
zoomToBounds(bounds: Box, targetZoom?: number, animation?: TLAnimationOptions): this {
if (!this.getInstanceState().canMoveCamera) return this
const viewportScreenBounds = this.getViewportScreenBounds()
@ -2473,8 +2473,8 @@ export class Editor extends EventEmitter<TLEventMap> {
elapsed: number
duration: number
easing: (t: number) => number
start: Box2d
end: Box2d
start: Box
end: Box
}
/** @internal */
@ -2510,7 +2510,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/** @internal */
private _animateToViewport(targetViewportPage: Box2d, opts = {} as TLAnimationOptions) {
private _animateToViewport(targetViewportPage: Box, opts = {} as TLAnimationOptions) {
const { duration = 0, easing = EASINGS.easeInOutCubic } = opts
const animationSpeed = this.user.getAnimationSpeed()
const viewportPageBounds = this.getViewportPageBounds()
@ -2579,7 +2579,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const moveCamera = (elapsed: number) => {
const { x: cx, y: cy, z: cz } = this.getCamera()
const movementVec = Vec2d.Mul(direction, (currentSpeed * elapsed) / cz)
const movementVec = Vec.Mul(direction, (currentSpeed * elapsed) / cz)
// Apply friction
currentSpeed *= 1 - friction
@ -2709,7 +2709,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (!container) return this
const rect = container.getBoundingClientRect()
const screenBounds = new Box2d(
const screenBounds = new Box(
rect.left || rect.x,
rect.top || rect.y,
Math.max(rect.width, 1),
@ -2761,7 +2761,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
@computed getViewportScreenBounds() {
const { x, y, w, h } = this.getInstanceState().screenBounds
return new Box2d(x, y, w, h)
return new Box(x, y, w, h)
}
/**
@ -2780,7 +2780,7 @@ export class Editor extends EventEmitter<TLEventMap> {
@computed getViewportPageBounds() {
const { w, h } = this.getViewportScreenBounds()
const { x: cx, y: cy, z: cz } = this.getCamera()
return new Box2d(-cx, -cy, w / cz, h / cz)
return new Box(-cx, -cy, w / cz, h / cz)
}
/**
@ -2898,10 +2898,10 @@ export class Editor extends EventEmitter<TLEventMap> {
// Get the bounds of the follower (me) and the leader (them)
const { center, width, height } = this.getViewportPageBounds()
const leaderScreen = Box2d.From(leaderPresence.screenBounds)
const leaderScreen = Box.From(leaderPresence.screenBounds)
const leaderWidth = leaderScreen.width / leaderPresence.camera.z
const leaderHeight = leaderScreen.height / leaderPresence.camera.z
const leaderCenter = new Vec2d(
const leaderCenter = new Vec(
leaderWidth / 2 - leaderPresence.camera.x,
leaderHeight / 2 - leaderPresence.camera.y
)
@ -2924,10 +2924,10 @@ export class Editor extends EventEmitter<TLEventMap> {
// Figure out where to move the camera
const displacement = leaderCenter.sub(center)
const targetCenter = Vec2d.Add(center, Vec2d.Mul(displacement, chaseProportion))
const targetCenter = Vec.Add(center, Vec.Mul(displacement, chaseProportion))
// Now let's assess whether we've caught up to the leader or not
const distance = Vec2d.Sub(targetCenter, center).len()
const distance = Vec.Sub(targetCenter, center).len()
const zoomChange = Math.abs(targetZoom - this.getCamera().z)
// If we're chasing the leader...
@ -3049,7 +3049,7 @@ export class Editor extends EventEmitter<TLEventMap> {
backgroundIndex: number
opacity: number
isCulled: boolean
maskedPageBounds: Box2d | undefined
maskedPageBounds: Box | undefined
}[] = []
let nextIndex = MAX_SHAPES_PER_PAGE * 2
@ -3165,7 +3165,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/** @internal */
private readonly _renderingBounds = atom('rendering viewport', new Box2d())
private readonly _renderingBounds = atom('rendering viewport', new Box())
/**
* The current rendering bounds in the current page space, expanded slightly. Used for determining which shapes
@ -3178,7 +3178,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/** @internal */
private readonly _renderingBoundsExpanded = atom('rendering viewport expanded', new Box2d())
private readonly _renderingBoundsExpanded = atom('rendering viewport expanded', new Box())
/**
* Update the rendering bounds. This should be called when the viewport has stopped changing, such
@ -3791,7 +3791,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/** @internal */
@computed private _getShapeOutlineSegmentsCache(): ComputedCache<Vec2d[][], TLShape> {
@computed private _getShapeOutlineSegmentsCache(): ComputedCache<Vec[][], TLShape> {
return this.store.createComputedCache('outline-segments', (shape) => {
return this.getShapeUtil(shape).getOutlineSegments(shape)
})
@ -3810,7 +3810,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getShapeOutlineSegments<T extends TLShape>(shape: T | T['id']): Vec2d[][] {
getShapeOutlineSegments<T extends TLShape>(shape: T | T['id']): Vec[][] {
return (
this._getShapeOutlineSegmentsCache().get(typeof shape === 'string' ? shape : shape.id) ??
EMPTY_ARRAY
@ -3855,11 +3855,11 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getShapeLocalTransform(shape: TLShape | TLShapeId): Matrix2d {
getShapeLocalTransform(shape: TLShape | TLShapeId): Mat {
const id = typeof shape === 'string' ? shape : shape.id
const freshShape = this.getShape(id)
if (!freshShape) throw Error('Editor.getTransform: shape not found')
return Matrix2d.Identity().translate(freshShape.x, freshShape.y).rotate(freshShape.rotation)
return Mat.Identity().translate(freshShape.x, freshShape.y).rotate(freshShape.rotation)
}
/**
@ -3867,8 +3867,8 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @internal
*/
@computed private _getShapePageTransformCache(): ComputedCache<Matrix2d, TLShape> {
return this.store.createComputedCache<Matrix2d, TLShape>('pageTransformCache', (shape) => {
@computed private _getShapePageTransformCache(): ComputedCache<Mat, TLShape> {
return this.store.createComputedCache<Mat, TLShape>('pageTransformCache', (shape) => {
if (isPageId(shape.parentId)) {
return this.getShapeLocalTransform(shape)
}
@ -3878,8 +3878,8 @@ export class Editor extends EventEmitter<TLEventMap> {
// In the future we should look at creating a store update mechanism that understands and preserves
// ordering.
const parentTransform =
this._getShapePageTransformCache().get(shape.parentId) ?? Matrix2d.Identity()
return Matrix2d.Compose(parentTransform, this.getShapeLocalTransform(shape)!)
this._getShapePageTransformCache().get(shape.parentId) ?? Mat.Identity()
return Mat.Compose(parentTransform, this.getShapeLocalTransform(shape)!)
})
}
@ -3895,11 +3895,11 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getShapeParentTransform(shape: TLShape | TLShapeId): Matrix2d {
getShapeParentTransform(shape: TLShape | TLShapeId): Mat {
const id = typeof shape === 'string' ? shape : shape.id
const freshShape = this.getShape(id)
if (!freshShape || isPageId(freshShape.parentId)) return Matrix2d.Identity()
return this._getShapePageTransformCache().get(freshShape.parentId) ?? Matrix2d.Identity()
if (!freshShape || isPageId(freshShape.parentId)) return Mat.Identity()
return this._getShapePageTransformCache().get(freshShape.parentId) ?? Mat.Identity()
}
/**
@ -3915,20 +3915,20 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getShapePageTransform(shape: TLShape | TLShapeId): Matrix2d {
getShapePageTransform(shape: TLShape | TLShapeId): Mat {
const id = typeof shape === 'string' ? shape : this.getShape(shape)!.id
return this._getShapePageTransformCache().get(id) ?? Matrix2d.Identity()
return this._getShapePageTransformCache().get(id) ?? Mat.Identity()
}
/** @internal */
@computed private _getShapePageBoundsCache(): ComputedCache<Box2d, TLShape> {
return this.store.createComputedCache<Box2d, TLShape>('pageBoundsCache', (shape) => {
@computed private _getShapePageBoundsCache(): ComputedCache<Box, TLShape> {
return this.store.createComputedCache<Box, TLShape>('pageBoundsCache', (shape) => {
const pageTransform = this._getShapePageTransformCache().get(shape.id)
if (!pageTransform) return new Box2d()
if (!pageTransform) return new Box()
const result = Box2d.FromPoints(
Matrix2d.applyToPoints(pageTransform, this.getShapeGeometry(shape).vertices)
const result = Box.FromPoints(
Mat.applyToPoints(pageTransform, this.getShapeGeometry(shape).vertices)
)
return result
@ -3948,7 +3948,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getShapePageBounds(shape: TLShape | TLShapeId): Box2d | undefined {
getShapePageBounds(shape: TLShape | TLShapeId): Box | undefined {
return this._getShapePageBoundsCache().get(typeof shape === 'string' ? shape : shape.id)
}
@ -3968,7 +3968,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const pageTransform = this._getShapePageTransformCache().get(shape.id)
if (!pageTransform) return undefined
const localMask = Matrix2d.applyToPoints(Matrix2d.Inverse(pageTransform), pageMask)
const localMask = Mat.applyToPoints(Mat.Inverse(pageTransform), pageMask)
return `polygon(${localMask.map((p) => `${p.x}px ${p.y}px`).join(',')})`
})
@ -3994,7 +3994,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/** @internal */
@computed private _getShapeMaskCache(): ComputedCache<Vec2d[], TLShape> {
@computed private _getShapeMaskCache(): ComputedCache<Vec[], TLShape> {
return this.store.createComputedCache('pageMaskCache', (shape) => {
if (isPageId(shape.parentId)) return undefined
@ -4005,7 +4005,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (frameAncestors.length === 0) return undefined
const pageMask = frameAncestors
.map<Vec2d[] | undefined>((s) =>
.map<Vec[] | undefined>((s) =>
// Apply the frame transform to the frame outline to get the frame outline in the current page space
this._getShapePageTransformCache()
.get(s.id)!
@ -4015,7 +4015,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (!(b && acc)) return undefined
const intersection = intersectPolygonPolygon(acc, b)
if (intersection) {
return intersection.map(Vec2d.Cast)
return intersection.map(Vec.Cast)
}
return []
})
@ -4057,7 +4057,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getShapeMaskedPageBounds(shape: TLShapeId | TLShape): Box2d | undefined {
getShapeMaskedPageBounds(shape: TLShapeId | TLShape): Box | undefined {
if (typeof shape !== 'string') shape = shape.id
const pageBounds = this._getShapePageBoundsCache().get(shape)
if (!pageBounds) return
@ -4066,12 +4066,12 @@ export class Editor extends EventEmitter<TLEventMap> {
if (pageMask.length === 0) return undefined
const { corners } = pageBounds
if (corners.every((p, i) => Vec2d.Equals(p, pageMask[i]))) return pageBounds.clone()
if (corners.every((p, i) => Vec.Equals(p, pageMask[i]))) return pageBounds.clone()
// todo: find out why intersect polygon polygon for identical polygons produces zero w/h intersections
const intersection = intersectPolygonPolygon(pageMask, corners)
if (!intersection) return
return Box2d.FromPoints(intersection)
return Box.FromPoints(intersection)
}
return pageBounds
@ -4217,8 +4217,8 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
@computed getCurrentPageBounds(): Box2d | undefined {
let commonBounds: Box2d | undefined
@computed getCurrentPageBounds(): Box | undefined {
let commonBounds: Box | undefined
this.getCurrentPageShapeIds().forEach((shapeId) => {
const bounds = this.getShapeMaskedPageBounds(shapeId)
@ -4504,7 +4504,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec2d {
getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec {
const id = typeof shape === 'string' ? shape : shape.id
return this._getShapePageTransformCache().get(id)!.clone().invert().applyToPoint(point)
}
@ -4522,14 +4522,14 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getPointInParentSpace(shape: TLShapeId | TLShape, point: VecLike): Vec2d {
getPointInParentSpace(shape: TLShapeId | TLShape, point: VecLike): Vec {
const id = typeof shape === 'string' ? shape : shape.id
const freshShape = this.getShape(id)
if (!freshShape) return new Vec2d(0, 0)
if (isPageId(freshShape.parentId)) return Vec2d.From(point)
if (!freshShape) return new Vec(0, 0)
if (isPageId(freshShape.parentId)) return Vec.From(point)
const parentTransform = this.getShapePageTransform(freshShape.parentId)
if (!parentTransform) return Vec2d.From(point)
if (!parentTransform) return Vec.From(point)
return parentTransform.clone().invert().applyToPoint(point)
}
@ -4769,7 +4769,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const changes: TLShapePartial[] = []
const parentTransform = isPageId(parentId)
? Matrix2d.Identity()
? Mat.Identity()
: this.getShapePageTransform(parentId)!
const parentPageRotation = parentTransform.rotation()
@ -5095,7 +5095,7 @@ export class Editor extends EventEmitter<TLEventMap> {
throw Error(`Could not find a shape with the id ${id}.`)
}
const localDelta = Vec2d.Cast(offset)
const localDelta = Vec.Cast(offset)
const parentTransform = this.getShapeParentTransform(shape)
if (parentTransform) localDelta.rot(-parentTransform.rotation())
@ -5177,7 +5177,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (offset && initialIds.has(id)) {
const parentTransform = this.getShapeParentTransform(shape)
const vec = new Vec2d(offset.x, offset.y).rot(-parentTransform!.rotation())
const vec = new Vec(offset.x, offset.y).rot(-parentTransform!.rotation())
ox = vec.x
oy = vec.y
}
@ -5250,9 +5250,9 @@ export class Editor extends EventEmitter<TLEventMap> {
: getCurvedArrowInfo(this, newShape)
if (info?.isValid && infoAfter?.isValid && !getIsArrowStraight(shape)) {
const mpA = Vec2d.Med(info.start.handle, info.end.handle)
const distA = Vec2d.Dist(info.middle, mpA)
const distB = Vec2d.Dist(infoAfter.middle, mpA)
const mpA = Vec.Med(info.start.handle, info.end.handle)
const distA = Vec.Dist(info.middle, mpA)
const distB = Vec.Dist(infoAfter.middle, mpA)
if (newShape.props.bend < 0) {
newShape.props.bend += distB - distA
} else {
@ -5566,7 +5566,7 @@ export class Editor extends EventEmitter<TLEventMap> {
.flat()
)
const scaleOriginPage = Box2d.Common(
const scaleOriginPage = Box.Common(
compact(shapesToFlip.map((id) => this.getShapePageBounds(id)))
).center
@ -5718,7 +5718,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const parent = this.getShapeParent(shape)
const localDelta = parent
? Vec2d.Rot(delta, -this.getShapePageTransform(parent)!.decompose().rotation)
? Vec.Rot(delta, -this.getShapePageTransform(parent)!.decompose().rotation)
: delta
const translateStartChanges = this.getShapeUtil(shape).onTranslateStart?.(shape)
@ -5780,11 +5780,11 @@ export class Editor extends EventEmitter<TLEventMap> {
return true
})
)
const shapePageBounds: Record<string, Box2d> = {}
const nextShapePageBounds: Record<string, Box2d> = {}
const shapePageBounds: Record<string, Box> = {}
const nextShapePageBounds: Record<string, Box> = {}
let shape: TLShape,
bounds: Box2d,
bounds: Box,
area = 0
for (let i = 0; i < shapesToPack.length; i++) {
@ -5795,7 +5795,7 @@ export class Editor extends EventEmitter<TLEventMap> {
area += bounds.width * bounds.height
}
const commonBounds = Box2d.Common(compact(Object.values(shapePageBounds)))
const commonBounds = Box.Common(compact(Object.values(shapePageBounds)))
const maxWidth = commonBounds.width
@ -5806,12 +5806,12 @@ export class Editor extends EventEmitter<TLEventMap> {
const startWidth = Math.max(Math.ceil(Math.sqrt(area / 0.95)), maxWidth)
// first shape fills the width and is infinitely tall
const spaces: Box2d[] = [new Box2d(commonBounds.x, commonBounds.y, startWidth, Infinity)]
const spaces: Box[] = [new Box(commonBounds.x, commonBounds.y, startWidth, Infinity)]
let width = 0
let height = 0
let space: Box2d
let last: Box2d
let space: Box
let last: Box
for (let i = 0; i < shapesToPack.length; i++) {
shape = shapesToPack[i]
@ -5846,7 +5846,7 @@ export class Editor extends EventEmitter<TLEventMap> {
} else {
// split the space into two spaces
spaces.push(
new Box2d(
new Box(
space.x + (bounds.width + gap),
space.y,
space.width - (bounds.width + gap),
@ -5860,10 +5860,10 @@ export class Editor extends EventEmitter<TLEventMap> {
}
}
const commonAfter = Box2d.Common(Object.values(nextShapePageBounds))
const centerDelta = Vec2d.Sub(commonBounds.center, commonAfter.center)
const commonAfter = Box.Common(Object.values(nextShapePageBounds))
const centerDelta = Vec.Sub(commonBounds.center, commonAfter.center)
let nextBounds: Box2d
let nextBounds: Box
const changes: TLShapePartial<any>[] = []
@ -5872,7 +5872,7 @@ export class Editor extends EventEmitter<TLEventMap> {
bounds = shapePageBounds[shape.id]
nextBounds = nextShapePageBounds[shape.id]
const delta = Vec2d.Sub(nextBounds.point, bounds.point).add(centerDelta)
const delta = Vec.Sub(nextBounds.point, bounds.point).add(centerDelta)
const parentTransform = this.getShapeParentTransform(shape)
if (parentTransform) delta.rot(-parentTransform.rotation())
@ -5933,7 +5933,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const shapePageBounds = Object.fromEntries(
shapesToAlign.map((shape) => [shape.id, this.getShapePageBounds(shape)])
)
const commonBounds = Box2d.Common(compact(Object.values(shapePageBounds)))
const commonBounds = Box.Common(compact(Object.values(shapePageBounds)))
const changes: TLShapePartial[] = []
@ -5972,7 +5972,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const parent = this.getShapeParent(shape)
const localDelta = parent
? Vec2d.Rot(delta, -this.getShapePageTransform(parent)!.decompose().rotation)
? Vec.Rot(delta, -this.getShapePageTransform(parent)!.decompose().rotation)
: delta
const translateChanges = this.getShapeUtil(shape).onTranslateStart?.(shape)
@ -6066,7 +6066,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const parent = this.getShapeParent(shape)
const localDelta = parent
? Vec2d.Rot(delta, -this.getShapePageTransform(parent)!.rotation())
? Vec.Rot(delta, -this.getShapePageTransform(parent)!.rotation())
: delta
const translateStartChanges = this.getShapeUtil(shape).onTranslateStart?.(shape)
@ -6114,7 +6114,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const shapesToStretch = compact(ids.map((id) => this.getShape(id))) // always fresh shapes
const shapeBounds = Object.fromEntries(ids.map((id) => [id, this.getShapeGeometry(id).bounds]))
const shapePageBounds = Object.fromEntries(ids.map((id) => [id, this.getShapePageBounds(id)!]))
const commonBounds = Box2d.Common(compact(Object.values(shapePageBounds)))
const commonBounds = Box.Common(compact(Object.values(shapePageBounds)))
switch (operation) {
case 'vertical': {
@ -6124,16 +6124,16 @@ export class Editor extends EventEmitter<TLEventMap> {
if (pageRotation % PI2) continue
const bounds = shapeBounds[shape.id]
const pageBounds = shapePageBounds[shape.id]
const localOffset = new Vec2d(0, commonBounds.minY - pageBounds.minY)
const localOffset = new Vec(0, commonBounds.minY - pageBounds.minY)
const parentTransform = this.getShapeParentTransform(shape)
if (parentTransform) localOffset.rot(-parentTransform.rotation())
const { x, y } = Vec2d.Add(localOffset, shape)
const { x, y } = Vec.Add(localOffset, shape)
this.updateShapes([{ id: shape.id, type: shape.type, x, y }], { squashing: true })
const scale = new Vec2d(1, commonBounds.height / pageBounds.height)
const scale = new Vec(1, commonBounds.height / pageBounds.height)
this.resizeShape(shape.id, scale, {
initialBounds: bounds,
scaleOrigin: new Vec2d(pageBounds.center.x, commonBounds.minY),
scaleOrigin: new Vec(pageBounds.center.x, commonBounds.minY),
scaleAxisRotation: 0,
})
}
@ -6147,16 +6147,16 @@ export class Editor extends EventEmitter<TLEventMap> {
const pageBounds = shapePageBounds[shape.id]
const pageRotation = this.getShapePageTransform(shape)!.rotation()
if (pageRotation % PI2) continue
const localOffset = new Vec2d(commonBounds.minX - pageBounds.minX, 0)
const localOffset = new Vec(commonBounds.minX - pageBounds.minX, 0)
const parentTransform = this.getShapeParentTransform(shape)
if (parentTransform) localOffset.rot(-parentTransform.rotation())
const { x, y } = Vec2d.Add(localOffset, shape)
const { x, y } = Vec.Add(localOffset, shape)
this.updateShapes([{ id: shape.id, type: shape.type, x, y }], { squashing: true })
const scale = new Vec2d(commonBounds.width / pageBounds.width, 1)
const scale = new Vec(commonBounds.width / pageBounds.width, 1)
this.resizeShape(shape.id, scale, {
initialBounds: bounds,
scaleOrigin: new Vec2d(commonBounds.minX, pageBounds.center.y),
scaleOrigin: new Vec(commonBounds.minX, pageBounds.center.y),
scaleAxisRotation: 0,
})
}
@ -6186,8 +6186,8 @@ export class Editor extends EventEmitter<TLEventMap> {
const id = typeof shape === 'string' ? shape : shape.id
if (this.getInstanceState().isReadonly) return this
if (!Number.isFinite(scale.x)) scale = new Vec2d(1, scale.y)
if (!Number.isFinite(scale.y)) scale = new Vec2d(scale.x, 1)
if (!Number.isFinite(scale.x)) scale = new Vec(1, scale.y)
if (!Number.isFinite(scale.y)) scale = new Vec(scale.x, 1)
const initialShape = options.initialShape ?? this.getShape(id)
if (!initialShape) return this
@ -6196,7 +6196,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (!scaleOrigin) return this
const pageTransform = options.initialPageTransform
? Matrix2d.Cast(options.initialPageTransform)
? Mat.Cast(options.initialPageTransform)
: this.getShapePageTransform(id)
if (!pageTransform) return this
@ -6229,16 +6229,16 @@ export class Editor extends EventEmitter<TLEventMap> {
if (util.isAspectRatioLocked(initialShape)) {
if (Math.abs(scale.x) > Math.abs(scale.y)) {
scale = new Vec2d(scale.x, Math.sign(scale.y) * Math.abs(scale.x))
scale = new Vec(scale.x, Math.sign(scale.y) * Math.abs(scale.x))
} else {
scale = new Vec2d(Math.sign(scale.x) * Math.abs(scale.y), scale.y)
scale = new Vec(Math.sign(scale.x) * Math.abs(scale.y), scale.y)
}
}
if (util.onResize && util.canResize(initialShape)) {
// get the model changes from the shape util
const newPagePoint = this._scalePagePoint(
Matrix2d.applyToPoint(pageTransform, new Vec2d(0, 0)),
Mat.applyToPoint(pageTransform, new Vec(0, 0)),
scaleOrigin,
scale,
scaleAxisRotation
@ -6247,7 +6247,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const newLocalPoint = this.getPointInParentSpace(initialShape.id, newPagePoint)
// resize the shape's local bounding box
const myScale = new Vec2d(scale.x, scale.y)
const myScale = new Vec(scale.x, scale.y)
// the shape is aligned with the rest of the shapes in the selection, but may be
// 90deg offset from the main rotation of the selection, in which case
// we need to flip the width and height scale factors
@ -6260,7 +6260,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// adjust initial model for situations where the parent has moved during the resize
// e.g. groups
const initialPagePoint = Matrix2d.applyToPoint(pageTransform, new Vec2d())
const initialPagePoint = Mat.applyToPoint(pageTransform, new Vec())
// need to adjust the shape's x and y points in case the parent has moved since start of resizing
const { x, y } = this.getPointInParentSpace(initialShape.id, initialPagePoint)
@ -6290,7 +6290,7 @@ export class Editor extends EventEmitter<TLEventMap> {
{ squashing: true }
)
} else {
const initialPageCenter = Matrix2d.applyToPoint(pageTransform, initialBounds.center)
const initialPageCenter = Mat.applyToPoint(pageTransform, initialBounds.center)
// get the model changes from the shape util
const newPageCenter = this._scalePagePoint(
initialPageCenter,
@ -6305,7 +6305,7 @@ export class Editor extends EventEmitter<TLEventMap> {
)
const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter)
const delta = Vec2d.Sub(newPageCenterInParentSpace, initialPageCenterInParentSpace)
const delta = Vec.Sub(newPageCenterInParentSpace, initialPageCenterInParentSpace)
// apply the changes to the model
this.updateShapes(
[
@ -6330,13 +6330,13 @@ export class Editor extends EventEmitter<TLEventMap> {
scale: VecLike,
scaleAxisRotation: number
) {
const relativePoint = Vec2d.RotWith(point, scaleOrigin, -scaleAxisRotation).sub(scaleOrigin)
const relativePoint = Vec.RotWith(point, scaleOrigin, -scaleAxisRotation).sub(scaleOrigin)
// calculate the new point position relative to the scale origin
const newRelativePagePoint = Vec2d.MulV(relativePoint, scale)
const newRelativePagePoint = Vec.MulV(relativePoint, scale)
// and rotate it back to page coords to get the new page point of the resized shape
const destination = Vec2d.Add(newRelativePagePoint, scaleOrigin).rotWith(
const destination = Vec.Add(newRelativePagePoint, scaleOrigin).rotWith(
scaleOrigin,
scaleAxisRotation
)
@ -6349,7 +6349,7 @@ export class Editor extends EventEmitter<TLEventMap> {
id: TLShapeId,
scale: VecLike,
options: {
initialBounds: Box2d
initialBounds: Box
scaleOrigin: VecLike
scaleAxisRotation: number
initialShape: TLShape
@ -6362,7 +6362,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// and then after applying the scale to the shape we also rotate it if required and translate it so that it's center
// point ends up in the right place.
const shapeScale = new Vec2d(scale.x, scale.y)
const shapeScale = new Vec(scale.x, scale.y)
// // make sure we are constraining aspect ratio, and using the smallest scale axis to avoid shapes getting bigger
// // than the selection bounding box
@ -6381,14 +6381,14 @@ export class Editor extends EventEmitter<TLEventMap> {
// then if the shape is flipped in one axis only, we need to apply an extra rotation
// to make sure the shape is mirrored correctly
if (Math.sign(scale.x) * Math.sign(scale.y) < 0) {
let { rotation } = Matrix2d.Decompose(options.initialPageTransform)
let { rotation } = Mat.Decompose(options.initialPageTransform)
rotation -= 2 * rotation
this.updateShapes([{ id, type, rotation }], { squashing: true })
}
// Next we need to translate the shape so that it's center point ends up in the right place.
// To do that we first need to calculate the center point of the shape in the current page space before the scale was applied.
const preScaleShapePageCenter = Matrix2d.applyToPoint(
const preScaleShapePageCenter = Mat.applyToPoint(
options.initialPageTransform,
options.initialBounds.center
)
@ -6407,10 +6407,10 @@ export class Editor extends EventEmitter<TLEventMap> {
const currentPageCenter = pageBounds.center
const shapePageTransformOrigin = pageTransform.point()
if (!currentPageCenter || !shapePageTransformOrigin) return this
const pageDelta = Vec2d.Sub(postScaleShapePageCenter, currentPageCenter)
const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter)
// and finally figure out what the shape's new position should be
const postScaleShapePagePoint = Vec2d.Add(shapePageTransformOrigin, pageDelta)
const postScaleShapePagePoint = Vec.Add(shapePageTransformOrigin, pageDelta)
const { x, y } = this.getPointInParentSpace(id, postScaleShapePagePoint)
this.updateShapes([{ id, type, x, y }], { squashing: true })
@ -6819,7 +6819,7 @@ export class Editor extends EventEmitter<TLEventMap> {
const shapesToGroup = compact(this._getUnlockedShapeIds(ids).map((id) => this.getShape(id)))
const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id)
const pageBounds = Box2d.Common(compact(shapesToGroup.map((id) => this.getShapePageBounds(id))))
const pageBounds = Box.Common(compact(shapesToGroup.map((id) => this.getShapePageBounds(id))))
const { x, y } = pageBounds.point
@ -7610,7 +7610,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (!ids) return
if (ids.length === 0) return
const pageTransforms: Record<string, Matrix2dModel> = {}
const pageTransforms: Record<string, MatModel> = {}
let shapesForContent = dedupe(
ids
@ -7687,9 +7687,9 @@ export class Editor extends EventEmitter<TLEventMap> {
: getCurvedArrowInfo(this, shape)
if (info?.isValid && infoAfter?.isValid && !getIsArrowStraight(shape)) {
const mpA = Vec2d.Med(info.start.handle, info.end.handle)
const distA = Vec2d.Dist(info.middle, mpA)
const distB = Vec2d.Dist(infoAfter.middle, mpA)
const mpA = Vec.Med(info.start.handle, info.end.handle)
const distA = Vec.Dist(info.middle, mpA)
const distB = Vec.Dist(infoAfter.middle, mpA)
if (shape.props.bend < 0) {
shape.props.bend += distB - distA
} else {
@ -8007,19 +8007,19 @@ export class Editor extends EventEmitter<TLEventMap> {
}
const newCreatedShapes = newShapes.map((s) => this.getShape(s.id)!)
const bounds = Box2d.Common(newCreatedShapes.map((s) => this.getShapePageBounds(s)!))
const bounds = Box.Common(newCreatedShapes.map((s) => this.getShapePageBounds(s)!))
if (point === undefined) {
if (!isPageId(pasteParentId)) {
// Put the shapes in the middle of the (on screen) parent
const shape = this.getShape(pasteParentId)!
point = Matrix2d.applyToPoint(
point = Mat.applyToPoint(
this.getShapePageTransform(shape),
this.getShapeGeometry(shape).bounds.center
)
} else {
const viewportPageBounds = this.getViewportPageBounds()
if (preservePosition || viewportPageBounds.includes(Box2d.From(bounds))) {
if (preservePosition || viewportPageBounds.includes(Box.From(bounds))) {
// Otherwise, put shapes where they used to be
point = bounds.center
} else {
@ -8047,17 +8047,17 @@ export class Editor extends EventEmitter<TLEventMap> {
}
}
const pageCenter = Box2d.Common(
const pageCenter = Box.Common(
compact(rootShapes.map(({ id }) => this.getShapePageBounds(id)))
).center
const offset = Vec2d.Sub(point, pageCenter)
const offset = Vec.Sub(point, pageCenter)
this.updateShapes(
rootShapes.map(({ id }) => {
const s = this.getShape(id)!
const localRotation = this.getShapeParentTransform(id).decompose().rotation
const localDelta = Vec2d.Rot(offset, -localRotation)
const localDelta = Vec.Rot(offset, -localRotation)
return { id: s.id, type: s.type, x: s.x + localDelta.x, y: s.y + localDelta.y }
})
@ -8301,17 +8301,17 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
inputs = {
/** The most recent pointer down's position in the current page space. */
originPagePoint: new Vec2d(),
originPagePoint: new Vec(),
/** The most recent pointer down's position in screen space. */
originScreenPoint: new Vec2d(),
originScreenPoint: new Vec(),
/** The previous pointer position in the current page space. */
previousPagePoint: new Vec2d(),
previousPagePoint: new Vec(),
/** The previous pointer position in screen space. */
previousScreenPoint: new Vec2d(),
previousScreenPoint: new Vec(),
/** The most recent pointer position in the current page space. */
currentPagePoint: new Vec2d(),
currentPagePoint: new Vec(),
/** The most recent pointer position in screen space. */
currentScreenPoint: new Vec2d(),
currentScreenPoint: new Vec(),
/** A set containing the currently pressed keys. */
keys: new Set<string>(),
/** A set containing the currently pressed buttons. */
@ -8335,7 +8335,7 @@ export class Editor extends EventEmitter<TLEventMap> {
/** Whether the user is panning. */
isPanning: false,
/** Velocity of mouse pointer, in pixels per millisecond */
pointerVelocity: new Vec2d(),
pointerVelocity: new Vec(),
}
/**
@ -8777,7 +8777,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (this.inputs.isPanning && this.inputs.isPointing) {
// Handle panning
const { currentScreenPoint, previousScreenPoint } = this.inputs
this.pan(Vec2d.Sub(currentScreenPoint, previousScreenPoint))
this.pan(Vec.Sub(currentScreenPoint, previousScreenPoint))
return
}

View file

@ -2,7 +2,7 @@ import { Computed, RESET_VALUE, computed, isUninitialized } from '@tldraw/state'
import { TLArrowShape, TLShape, TLShapeId } from '@tldraw/tlschema'
import { Editor } from '../Editor'
export type TLArrowBindingsIndex = Record<
type TLArrowBindingsIndex = Record<
TLShapeId,
undefined | { arrowId: TLShapeId; handleId: 'start' | 'end' }[]
>

View file

@ -4,7 +4,7 @@ import {
DRAG_DISTANCE,
MULTI_CLICK_DURATION,
} from '../../constants'
import { Vec2d } from '../../primitives/Vec2d'
import { Vec } from '../../primitives/Vec'
import { uniqueId } from '../../utils/uniqueId'
import type { Editor } from '../Editor'
import { TLClickEventInfo, TLPointerEventInfo } from '../types/event-types'
@ -26,9 +26,9 @@ export class ClickManager {
private _clickTimeout?: any
private _clickScreenPoint?: Vec2d
private _clickScreenPoint?: Vec
private _previousScreenPoint?: Vec2d
private _previousScreenPoint?: Vec
private _getClickTimeout = (state: TLClickState, id = uniqueId()) => {
this._clickId = id
@ -103,7 +103,7 @@ export class ClickManager {
transformPointerDownEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
if (!this._clickState) return info
this._clickScreenPoint = Vec2d.From(info.point)
this._clickScreenPoint = Vec.From(info.point)
if (
this._previousScreenPoint &&
@ -173,7 +173,7 @@ export class ClickManager {
transformPointerUpEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => {
if (!this._clickState) return info
this._clickScreenPoint = Vec2d.From(info.point)
this._clickScreenPoint = Vec.From(info.point)
switch (this._clickState) {
case 'pendingTriple': {

View file

@ -1,5 +1,5 @@
import { TLScribble, Vec2dModel } from '@tldraw/tlschema'
import { Vec2d } from '../../primitives/Vec2d'
import { TLScribble, VecModel } from '@tldraw/tlschema'
import { Vec } from '../../primitives/Vec'
import { uniqueId } from '../../utils/uniqueId'
import { Editor } from '../Editor'
import { TLTickEvent } from '../types/event-types'
@ -9,8 +9,8 @@ type ScribbleItem = {
scribble: TLScribble
timeoutMs: number
delayRemaining: number
prev: null | Vec2dModel
next: null | Vec2dModel
prev: null | VecModel
next: null | VecModel
}
/** @public */
@ -87,7 +87,7 @@ export class ScribbleManager {
if (!item) throw Error(`Scribble with id ${id} not found`)
const { prev } = item
const point = { x, y, z: 0.5 }
if (!prev || Vec2d.Dist(prev, point) >= 1) {
if (!prev || Vec.Dist(prev, point) >= 1) {
item.next = point
}
return item

View file

@ -1,17 +1,17 @@
import { atom, computed, EMPTY_ARRAY } from '@tldraw/state'
import { TLGroupShape, TLParentId, TLShape, TLShapeId, Vec2dModel } from '@tldraw/tlschema'
import { TLGroupShape, TLParentId, TLShape, TLShapeId, VecModel } from '@tldraw/tlschema'
import { dedupe, deepCopy } from '@tldraw/utils'
import {
Box2d,
Box,
flipSelectionHandleX,
flipSelectionHandleY,
isSelectionCorner,
SelectionCorner,
SelectionEdge,
} from '../../primitives/Box2d'
import { Matrix2d } from '../../primitives/Matrix2d'
} from '../../primitives/Box'
import { Mat } from '../../primitives/Mat'
import { rangeIntersection, rangesOverlap } from '../../primitives/utils'
import { Vec2d, VecLike } from '../../primitives/Vec2d'
import { Vec, VecLike } from '../../primitives/Vec'
import { uniqueId } from '../../utils/uniqueId'
import type { Editor } from '../Editor'
@ -36,16 +36,6 @@ export type GapsSnapLine = {
/** @public */
export type SnapLine = PointsSnapLine | GapsSnapLine
export type SnapInteractionType =
| {
type: 'translate'
lockedAxis: 'x' | 'y' | null
initialSelectionSnapPoints: Vec2d[]
}
| {
type: 'resize'
}
/** @public */
export interface SnapPoint {
id: string
@ -82,7 +72,7 @@ type NearestSnap =
type GapNode = {
id: TLShapeId
pageBounds: Box2d
pageBounds: Box
isClosed: boolean
}
@ -108,14 +98,14 @@ type Gap = {
// ◄─────────────────────────►
startNode: GapNode
endNode: GapNode
startEdge: [Vec2d, Vec2d]
endEdge: [Vec2d, Vec2d]
startEdge: [Vec, Vec]
endEdge: [Vec, Vec]
length: number
breadthIntersection: [number, number]
}
interface SnapData {
nudge: Vec2d
nudge: Vec
}
const round = (x: number) => {
@ -235,7 +225,7 @@ export class SnapManager {
if (!pageTransfrorm) return undefined
const snapPoints = this.editor.getShapeGeometry(shape).snapPoints
return snapPoints.map((point, i) => {
const { x, y } = Matrix2d.applyToPoint(pageTransfrorm, point)
const { x, y } = Mat.applyToPoint(pageTransfrorm, point)
return { x, y, id: `${shape.id}:${i}` }
})
})
@ -336,12 +326,12 @@ export class SnapManager {
startNode,
endNode,
startEdge: [
new Vec2d(startNode.pageBounds.maxX, startNode.pageBounds.minY),
new Vec2d(startNode.pageBounds.maxX, startNode.pageBounds.maxY),
new Vec(startNode.pageBounds.maxX, startNode.pageBounds.minY),
new Vec(startNode.pageBounds.maxX, startNode.pageBounds.maxY),
],
endEdge: [
new Vec2d(endNode.pageBounds.minX, endNode.pageBounds.minY),
new Vec2d(endNode.pageBounds.minX, endNode.pageBounds.maxY),
new Vec(endNode.pageBounds.minX, endNode.pageBounds.minY),
new Vec(endNode.pageBounds.minX, endNode.pageBounds.maxY),
],
length: endNode.pageBounds.minX - startNode.pageBounds.maxX,
breadthIntersection: rangeIntersection(
@ -380,12 +370,12 @@ export class SnapManager {
startNode,
endNode,
startEdge: [
new Vec2d(startNode.pageBounds.minX, startNode.pageBounds.maxY),
new Vec2d(startNode.pageBounds.maxX, startNode.pageBounds.maxY),
new Vec(startNode.pageBounds.minX, startNode.pageBounds.maxY),
new Vec(startNode.pageBounds.maxX, startNode.pageBounds.maxY),
],
endEdge: [
new Vec2d(endNode.pageBounds.minX, endNode.pageBounds.minY),
new Vec2d(endNode.pageBounds.maxX, endNode.pageBounds.minY),
new Vec(endNode.pageBounds.minX, endNode.pageBounds.minY),
new Vec(endNode.pageBounds.maxX, endNode.pageBounds.minY),
],
length: endNode.pageBounds.minY - startNode.pageBounds.maxY,
breadthIntersection: rangeIntersection(
@ -410,8 +400,8 @@ export class SnapManager {
}: {
lockedAxis: 'x' | 'y' | null
initialSelectionSnapPoints: SnapPoint[]
initialSelectionPageBounds: Box2d
dragDelta: Vec2d
initialSelectionPageBounds: Box
dragDelta: Vec
}): SnapData {
const snapThreshold = this.getSnapThreshold()
const visibleSnapPointsNotInSelection = this.getSnappablePoints()
@ -428,7 +418,7 @@ export class SnapManager {
const nearestSnapsX: NearestSnap[] = []
const nearestSnapsY: NearestSnap[] = []
const minOffset = new Vec2d(snapThreshold, snapThreshold)
const minOffset = new Vec(snapThreshold, snapThreshold)
this.collectPointSnaps({
minOffset,
@ -446,7 +436,7 @@ export class SnapManager {
})
// at the same time, calculate how far we need to nudge the shape to 'snap' to the target point(s)
const nudge = new Vec2d(
const nudge = new Vec(
lockedAxis === 'x' ? 0 : nearestSnapsX[0]?.nudge ?? 0,
lockedAxis === 'y' ? 0 : nearestSnapsY[0]?.nudge ?? 0
)
@ -501,7 +491,7 @@ export class SnapManager {
if (isClosed) outline.push(outline[0])
const pageTransform = this.editor.getShapePageTransform(id)
if (!pageTransform) throw Error('No page transform')
return Matrix2d.applyToPoints(pageTransform, outline)
return Mat.applyToPoints(pageTransform, outline)
})
}
@ -509,16 +499,16 @@ export class SnapManager {
handlePoint,
additionalSegments,
}: {
handlePoint: Vec2d
additionalSegments: Vec2d[][]
}): Vec2d | null {
handlePoint: Vec
additionalSegments: Vec[][]
}): Vec | null {
const snapThreshold = this.getSnapThreshold()
const outlinesInPageSpace = this.getOutlinesInPageSpace()
// Find the nearest point that is within the snap threshold
let minDistance = snapThreshold
let nearestPoint: Vec2d | null = null
let C: Vec2dModel, D: Vec2dModel, nearest: Vec2d, distance: number
let nearestPoint: Vec | null = null
let C: VecModel, D: VecModel, nearest: Vec, distance: number
const allSegments = [...outlinesInPageSpace, ...additionalSegments]
for (const outline of allSegments) {
@ -526,8 +516,8 @@ export class SnapManager {
C = outline[i]
D = outline[i + 1]
nearest = Vec2d.NearestPointOnLineSegment(C, D, handlePoint)
distance = Vec2d.Dist(handlePoint, nearest)
nearest = Vec.NearestPointOnLineSegment(C, D, handlePoint)
distance = Vec.Dist(handlePoint, nearest)
if (isNaN(distance)) continue
if (distance < minDistance) {
@ -547,7 +537,7 @@ export class SnapManager {
},
])
return Vec2d.Sub(nearestPoint, handlePoint)
return Vec.Sub(nearestPoint, handlePoint)
}
return null
@ -561,9 +551,9 @@ export class SnapManager {
isResizingFromCenter,
}: {
// the page bounds when the pointer went down, before any dragging
initialSelectionPageBounds: Box2d
initialSelectionPageBounds: Box
// how far the pointer has been dragged
dragDelta: Vec2d
dragDelta: Vec
handle: SelectionCorner | SelectionEdge
isAspectRatioLocked: boolean
@ -576,7 +566,7 @@ export class SnapManager {
box: unsnappedResizedPageBounds,
scaleX,
scaleY,
} = Box2d.Resize(
} = Box.Resize(
initialSelectionPageBounds,
originalHandle,
isResizingFromCenter ? dragDelta.x * 2 : dragDelta.x,
@ -607,7 +597,7 @@ export class SnapManager {
const nearestSnapsX: NearestPointsSnap[] = []
const nearestSnapsY: NearestPointsSnap[] = []
const minOffset = new Vec2d(snapThreshold, snapThreshold)
const minOffset = new Vec(snapThreshold, snapThreshold)
this.collectPointSnaps({
minOffset,
@ -618,7 +608,7 @@ export class SnapManager {
})
// at the same time, calculate how far we need to nudge the shape to 'snap' to the target point(s)
const nudge = new Vec2d(
const nudge = new Vec(
isXLocked ? 0 : nearestSnapsX[0]?.nudge ?? 0,
isYLocked ? 0 : nearestSnapsY[0]?.nudge ?? 0
)
@ -657,10 +647,10 @@ export class SnapManager {
// now resize the box after nudging, calculate the snaps again, and return the snap lines to match
// the fully resized box
const snappedDelta = Vec2d.Add(dragDelta, nudge)
const snappedDelta = Vec.Add(dragDelta, nudge)
// first figure out the new bounds of the selection
const { box: snappedResizedPageBounds } = Box2d.Resize(
const { box: snappedResizedPageBounds } = Box.Resize(
initialSelectionPageBounds,
originalHandle,
isResizingFromCenter ? snappedDelta.x * 2 : snappedDelta.x,
@ -706,7 +696,7 @@ export class SnapManager {
}: {
selectionSnapPoints: SnapPoint[]
otherNodeSnapPoints: SnapPoint[]
minOffset: Vec2d
minOffset: Vec
nearestSnapsX: NearestSnap[]
nearestSnapsY: NearestSnap[]
}) {
@ -714,7 +704,7 @@ export class SnapManager {
// which are closest to it in each axis
for (const thisSnapPoint of selectionSnapPoints) {
for (const otherSnapPoint of otherNodeSnapPoints) {
const offset = Vec2d.Sub(thisSnapPoint, otherSnapPoint)
const offset = Vec.Sub(thisSnapPoint, otherSnapPoint)
const offsetX = Math.abs(offset.x)
const offsetY = Math.abs(offset.y)
@ -756,8 +746,8 @@ export class SnapManager {
nearestSnapsX,
nearestSnapsY,
}: {
selectionPageBounds: Box2d
minOffset: Vec2d
selectionPageBounds: Box
minOffset: Vec
nearestSnapsX: NearestSnap[]
nearestSnapsY: NearestSnap[]
}) {
@ -1078,10 +1068,10 @@ export class SnapManager {
type: 'points',
points: dedupe(
snapGroup
.map((snap) => Vec2d.From(snap.otherPoint))
.map((snap) => Vec.From(snap.otherPoint))
// be sure to nudge over the selection snap points
.concat(snapGroup.map((snap) => Vec2d.From(snap.thisPoint))),
(a: Vec2d, b: Vec2d) => a.equals(b)
.concat(snapGroup.map((snap) => Vec.From(snap.thisPoint))),
(a: Vec, b: Vec) => a.equals(b)
),
}))
}
@ -1091,13 +1081,13 @@ export class SnapManager {
nearestSnapsX,
nearestSnapsY,
}: {
selectionPageBounds: Box2d
selectionPageBounds: Box
nearestSnapsX: NearestSnap[]
nearestSnapsY: NearestSnap[]
}): GapsSnapLine[] {
const { vertical, horizontal } = this.getVisibleGaps()
const selectionSides: Record<SelectionEdge, [Vec2d, Vec2d]> = {
const selectionSides: Record<SelectionEdge, [Vec, Vec]> = {
top: selectionPageBounds.sides[0],
right: selectionPageBounds.sides[1],
// need bottom and left to be sorted asc, which .sides is not.
@ -1175,7 +1165,7 @@ export class SnapManager {
startEdge: selectionSides.right,
endEdge: startEdge.map((v) =>
v.clone().addXY(-startNode.pageBounds.width, 0)
) as [Vec2d, Vec2d],
) as [Vec, Vec],
},
{ startEdge, endEdge },
...findAdjacentGaps(
@ -1198,7 +1188,7 @@ export class SnapManager {
{
startEdge: endEdge.map((v) =>
v.clone().addXY(snap.gap.endNode.pageBounds.width, 0)
) as [Vec2d, Vec2d],
) as [Vec, Vec],
endEdge: selectionSides.left,
},
],
@ -1279,7 +1269,7 @@ export class SnapManager {
startEdge: selectionSides.bottom,
endEdge: startEdge.map((v) =>
v.clone().addXY(0, -startNode.pageBounds.height)
) as [Vec2d, Vec2d],
) as [Vec, Vec],
},
{ startEdge, endEdge },
...findAdjacentGaps(
@ -1302,7 +1292,7 @@ export class SnapManager {
{
startEdge: endEdge.map((v) =>
v.clone().addXY(0, endNode.pageBounds.height)
) as [Vec2d, Vec2d],
) as [Vec, Vec],
endEdge: selectionSides.top,
},
],
@ -1320,7 +1310,7 @@ export class SnapManager {
function getResizeSnapPointsForHandle(
handle: SelectionCorner | SelectionEdge | 'any',
selectionPageBounds: Box2d
selectionPageBounds: Box
): SnapPoint[] {
const { minX, maxX, minY, maxY } = selectionPageBounds
const result: SnapPoint[] = []

View file

@ -1,4 +1,4 @@
import { Box2dModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
import { uniqueId } from '../../utils/uniqueId'
import { Editor } from '../Editor'
@ -72,7 +72,7 @@ export class TextManager {
minWidth?: string
padding: string
}
): Box2dModel => {
): BoxModel => {
const elm = this.getTextElement()
elm.setAttribute('dir', 'ltr')
@ -103,7 +103,7 @@ export class TextManager {
measureElementTextNodeSpans(
element: HTMLElement,
{ shouldTruncateToFirstLine = false }: { shouldTruncateToFirstLine?: boolean } = {}
): { spans: { box: Box2dModel; text: string }[]; didTruncate: boolean } {
): { spans: { box: BoxModel; text: string }[]; didTruncate: boolean } {
const spans = []
// Measurements of individual spans are relative to the containing element
@ -195,7 +195,7 @@ export class TextManager {
measureTextSpans(
textToMeasure: string,
opts: TLMeasureTextSpanOpts
): { text: string; box: Box2dModel }[] {
): { text: string; box: BoxModel }[] {
if (textToMeasure === '') return []
const shouldTruncateToFirstLine =

View file

@ -1,4 +1,4 @@
import { Vec2d } from '../../primitives/Vec2d'
import { Vec } from '../../primitives/Vec'
import { Editor } from '../Editor'
export class TickManager {
@ -48,7 +48,7 @@ export class TickManager {
cancelAnimationFrame(this.raf)
}
private prevPoint = new Vec2d()
private prevPoint = new Vec()
private updatePointerVelocity = (elapsed: number) => {
const {
@ -60,11 +60,11 @@ export class TickManager {
if (elapsed === 0) return
const delta = Vec2d.Sub(currentScreenPoint, prevPoint)
const delta = Vec.Sub(currentScreenPoint, prevPoint)
this.prevPoint = currentScreenPoint.clone()
const length = delta.len()
const direction = length ? delta.div(length) : new Vec2d(0, 0)
const direction = length ? delta.div(length) : new Vec(0, 0)
// consider adjusting this with an easing rather than a linear interpolation
const next = pointerVelocity.clone().lrp(direction.mul(length / elapsed), 0.5)

View file

@ -1,25 +0,0 @@
import { atom } from '@tldraw/state'
import { getAtomManager } from './getRecordManager'
describe('atom manager', () => {
it('manages an atom object', () => {
const cb = jest.fn()
const A = atom('abc', { a: 1, b: 2, c: 3 })
const manager = getAtomManager(A, cb)
expect(A.lastChangedEpoch).toBe(0)
manager.a = 2
expect(manager.a).toBe(2)
expect(A.lastChangedEpoch).toBe(1)
manager.b = 4
expect(manager.b).toBe(4)
expect(A.lastChangedEpoch).toBe(2)
manager.b
expect(A.get()).toMatchObject({ a: 2, b: 4, c: 3 })
expect(cb).toHaveBeenCalledTimes(2)
})
})

View file

@ -1,28 +0,0 @@
import { Atom, computed } from '@tldraw/state'
export function getAtomManager<T extends { [key: string]: any }>(
atom: Atom<T>,
transform?: (prev: T, next: T) => T
): T {
const update = (value: Partial<T>) => {
const curr = atom.get()
const next = { ...curr, ...value }
const final = transform?.(atom.get(), atom.get()) ?? next
atom.set(final)
}
return Object.defineProperties(
{} as T,
Object.keys(atom.get()).reduce((acc, key) => {
acc[key as keyof T] = computed(atom, key, {
get() {
return atom.get()[key as keyof T]
},
set(value: T[keyof T]) {
update({ [key]: value } as any)
},
})
return acc
}, {} as { [key in keyof T]: PropertyDescriptor })
)
}

View file

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Migrations } from '@tldraw/store'
import { ShapeProps, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
import { Box2d } from '../../primitives/Box2d'
import { Vec2d } from '../../primitives/Vec2d'
import { Box } from '../../primitives/Box'
import { Vec } from '../../primitives/Vec'
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
import type { Editor } from '../Editor'
import { SvgExportContext } from '../types/SvgExportContext'
@ -214,7 +214,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
getOutlineSegments(shape: Shape): Vec2d[][] {
getOutlineSegments(shape: Shape): Vec[][] {
return [this.editor.getShapeGeometry(shape).vertices]
}
@ -554,12 +554,12 @@ export type TLResizeMode = 'scale_shape' | 'resize_bounds'
* @public
*/
export type TLResizeInfo<T extends TLShape> = {
newPoint: Vec2d
newPoint: Vec
handle: TLResizeHandle
mode: TLResizeMode
scaleX: number
scaleY: number
initialBounds: Box2d
initialBounds: Box
initialShape: T
}

View file

@ -1,9 +1,9 @@
import { useValue } from '@tldraw/state'
import { useEditor } from '../../../hooks/useEditor'
import { Box2d } from '../../../primitives/Box2d'
import { Box } from '../../../primitives/Box'
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
export function DashedOutlineBox({ bounds, className }: { bounds: Box2d; className: string }) {
export function DashedOutlineBox({ bounds, className }: { bounds: Box; className: string }) {
const editor = useEditor()
const zoomLevel = useValue('zoom level', () => editor.getZoomLevel(), [editor])

View file

@ -1,5 +1,5 @@
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema'
import { VecLike } from '../../../../primitives/Vec2d'
import { VecLike } from '../../../../primitives/Vec'
/** @public */
export type TLArrowPoint = {

View file

@ -1,7 +1,6 @@
import { TLArrowShape } from '@tldraw/tlschema'
import { Box2d } from '../../../../primitives/Box2d'
import { Matrix2d } from '../../../../primitives/Matrix2d'
import { Vec2d, VecLike } from '../../../../primitives/Vec2d'
import { Mat } from '../../../../primitives/Mat'
import { Vec, VecLike } from '../../../../primitives/Vec'
import { intersectCirclePolygon, intersectCirclePolyline } from '../../../../primitives/intersect'
import {
PI,
@ -37,9 +36,9 @@ export function getCurvedArrowInfo(
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(editor, shape)
const med = Vec2d.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end) // point between start and end
const u = Vec2d.Sub(terminalsInArrowSpace.end, terminalsInArrowSpace.start).uni() // unit vector between start and end
const middle = Vec2d.Add(med, u.per().mul(-bend)) // middle handle
const med = Vec.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end) // point between start and end
const u = Vec.Sub(terminalsInArrowSpace.end, terminalsInArrowSpace.start).uni() // unit vector between start and end
const middle = Vec.Add(med, u.per().mul(-bend)) // middle handle
const startShapeInfo = getBoundShapeInfoForTerminal(editor, shape.props.start)
const endShapeInfo = getBoundShapeInfoForTerminal(editor, shape.props.end)
@ -50,7 +49,7 @@ export function getCurvedArrowInfo(
const b = terminalsInArrowSpace.end.clone()
const c = middle.clone()
if (Vec2d.Equals(a, b)) {
if (Vec.Equals(a, b)) {
return {
isStraight: true,
start: {
@ -73,8 +72,8 @@ export function getCurvedArrowInfo(
const distFn = isClockwise ? clockwiseAngleDist : counterClockwiseAngleDist
const handleArc = getArcInfo(a, b, c)
const handle_aCA = Vec2d.Angle(handleArc.center, a)
const handle_aCB = Vec2d.Angle(handleArc.center, b)
const handle_aCA = Vec.Angle(handleArc.center, a)
const handle_aCB = Vec.Angle(handleArc.center, b)
const handle_dAB = distFn(handle_aCA, handle_aCB)
if (
@ -98,15 +97,15 @@ export function getCurvedArrowInfo(
let minLength = MIN_ARROW_LENGTH
if (startShapeInfo && !startShapeInfo.isExact) {
const startInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, tempA)
const centerInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, handleArc.center)
const endInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, tempB)
const startInPageSpace = Mat.applyToPoint(arrowPageTransform, tempA)
const centerInPageSpace = Mat.applyToPoint(arrowPageTransform, handleArc.center)
const endInPageSpace = Mat.applyToPoint(arrowPageTransform, tempB)
const inverseTransform = Matrix2d.Inverse(startShapeInfo.transform)
const inverseTransform = Mat.Inverse(startShapeInfo.transform)
const startInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, startInPageSpace)
const centerInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace)
const endInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace)
const startInStartShapeLocalSpace = Mat.applyToPoint(inverseTransform, startInPageSpace)
const centerInStartShapeLocalSpace = Mat.applyToPoint(inverseTransform, centerInPageSpace)
const endInStartShapeLocalSpace = Mat.applyToPoint(inverseTransform, endInPageSpace)
const { isClosed } = startShapeInfo
const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline
@ -148,7 +147,7 @@ export function getCurvedArrowInfo(
if (point) {
tempA.setTo(
editor.getPointInShapeSpace(shape, Matrix2d.applyToPoint(startShapeInfo.transform, point))
editor.getPointInShapeSpace(shape, Mat.applyToPoint(startShapeInfo.transform, point))
)
startShapeInfo.didIntersect = true
@ -167,15 +166,15 @@ export function getCurvedArrowInfo(
if (endShapeInfo && !endShapeInfo.isExact) {
// get points in shape's coordinates?
const startInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, tempA)
const endInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, tempB)
const centerInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, handleArc.center)
const startInPageSpace = Mat.applyToPoint(arrowPageTransform, tempA)
const endInPageSpace = Mat.applyToPoint(arrowPageTransform, tempB)
const centerInPageSpace = Mat.applyToPoint(arrowPageTransform, handleArc.center)
const inverseTransform = Matrix2d.Inverse(endShapeInfo.transform)
const inverseTransform = Mat.Inverse(endShapeInfo.transform)
const startInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, startInPageSpace)
const centerInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace)
const endInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace)
const startInEndShapeLocalSpace = Mat.applyToPoint(inverseTransform, startInPageSpace)
const centerInEndShapeLocalSpace = Mat.applyToPoint(inverseTransform, centerInPageSpace)
const endInEndShapeLocalSpace = Mat.applyToPoint(inverseTransform, endInPageSpace)
const isClosed = endShapeInfo.isClosed
const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline
@ -222,7 +221,7 @@ export function getCurvedArrowInfo(
if (point) {
// Set b to target local point -> page point -> shape local point
tempB.setTo(
editor.getPointInShapeSpace(shape, Matrix2d.applyToPoint(endShapeInfo.transform, point))
editor.getPointInShapeSpace(shape, Mat.applyToPoint(endShapeInfo.transform, point))
)
endShapeInfo.didIntersect = true
@ -239,8 +238,8 @@ export function getCurvedArrowInfo(
// Apply arrowhead offsets
let aCA = Vec2d.Angle(handleArc.center, tempA) // angle center -> a
let aCB = Vec2d.Angle(handleArc.center, tempB) // angle center -> b
let aCA = Vec.Angle(handleArc.center, tempA) // angle center -> a
let aCB = Vec.Angle(handleArc.center, tempB) // angle center -> b
let dAB = distFn(aCA, aCB) // angle distance between a and b
let lAB = dAB * handleArc.radius // length of arc between a and b
@ -252,17 +251,17 @@ export function getCurvedArrowInfo(
if (offsetA !== 0) {
const n = (offsetA / lAB) * (isClockwise ? 1 : -1)
const u = Vec2d.FromAngle(aCA + dAB * n)
const u = Vec.FromAngle(aCA + dAB * n)
tA.setTo(handleArc.center).add(u.mul(handleArc.radius))
}
if (offsetB !== 0) {
const n = (offsetB / lAB) * (isClockwise ? -1 : 1)
const u = Vec2d.FromAngle(aCB + dAB * n)
const u = Vec.FromAngle(aCB + dAB * n)
tB.setTo(handleArc.center).add(u.mul(handleArc.radius))
}
const distAB = Vec2d.Dist(tA, tB)
const distAB = Vec.Dist(tA, tB)
if (distAB < minLength) {
if (offsetA !== 0 && offsetB !== 0) {
offsetA *= -1.5
@ -278,20 +277,20 @@ export function getCurvedArrowInfo(
if (offsetA !== 0) {
const n = (offsetA / lAB) * (isClockwise ? 1 : -1)
const u = Vec2d.FromAngle(aCA + dAB * n)
const u = Vec.FromAngle(aCA + dAB * n)
tempA.setTo(handleArc.center).add(u.mul(handleArc.radius))
}
if (offsetB !== 0) {
const n = (offsetB / lAB) * (isClockwise ? -1 : 1)
const u = Vec2d.FromAngle(aCB + dAB * n)
const u = Vec.FromAngle(aCB + dAB * n)
tempB.setTo(handleArc.center).add(u.mul(handleArc.radius))
}
// Did we miss intersections? This happens when we have overlapping shapes.
if (startShapeInfo && endShapeInfo && !startShapeInfo.isExact && !endShapeInfo.isExact) {
aCA = Vec2d.Angle(handleArc.center, tempA) // angle center -> a
aCB = Vec2d.Angle(handleArc.center, tempB) // angle center -> b
aCA = Vec.Angle(handleArc.center, tempA) // angle center -> a
aCB = Vec.Angle(handleArc.center, tempB) // angle center -> b
dAB = distFn(aCA, aCB) // angle distance between a and b
lAB = dAB * handleArc.radius // length of arc between a and b
const relationship = getBoundShapeRelationships(
@ -314,7 +313,7 @@ export function getCurvedArrowInfo(
distFn(handle_aCA, aCA) > distFn(handle_aCA, aCB)
) {
const n = Math.min(0.9, MIN_ARROW_LENGTH / lAB) * (isClockwise ? 1 : -1)
const u = Vec2d.FromAngle(aCA + dAB * n)
const u = Vec.FromAngle(aCA + dAB * n)
tempB.setTo(handleArc.center).add(u.mul(handleArc.radius))
}
}
@ -359,87 +358,6 @@ export function getCurvedArrowInfo(
}
}
/**
* Get a solid path for a curved arrow's handles.
*
* @param info - The arrow info.
* @public
*/
export function getCurvedArrowHandlePath(info: TLArrowInfo & { isStraight: false }) {
const {
start,
end,
handleArc: { radius, largeArcFlag, sweepFlag },
} = info
return `M${start.handle.x},${start.handle.y} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end.handle.x},${end.handle.y}`
}
/**
* Get a solid path for a curved arrow's body.
*
* @param info - The arrow info.
* @public
*/
export function getSolidCurvedArrowPath(info: TLArrowInfo & { isStraight: false }) {
const {
start,
end,
bodyArc: { radius, largeArcFlag, sweepFlag },
} = info
return `M${start.point.x},${start.point.y} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end.point.x},${end.point.y}`
}
/**
* Get a point along an arc.
*
* @param center - The arc's center.
* @param radius - The arc's radius.
* @param startAngle - The start point of the arc.
* @param size - The size of the arc.
* @param t - The point along the arc to get.
*/
export function getPointOnArc(
center: VecLike,
radius: number,
startAngle: number,
size: number,
t: number
) {
const angle = startAngle + size * t
return new Vec2d(center.x + radius * Math.cos(angle), center.y + radius * Math.sin(angle))
}
/**
* Get a bounding box for an arc.
*
* @param center - The arc's center.
* @param radius - The arc's radius.
* @param start - The start point of the arc.
* @param size - The size of the arc.
*/
export function getArcBoundingBox(center: VecLike, radius: number, start: VecLike, size: number) {
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
const startAngle = Vec2d.Angle(center, start)
// Test 20 points along the arc
for (let i = 0; i < 20; i++) {
const angle = startAngle + size * (i / 19)
const x = center.x + radius * Math.cos(angle)
const y = center.y + radius * Math.sin(angle)
minX = Math.min(x, minX)
minY = Math.min(y, minY)
maxX = Math.max(x, maxX)
maxY = Math.max(y, maxY)
}
return new Box2d(minX, minY, maxX - minX, maxY - minY)
}
/**
* Get info about an arc formed by three points.
*
@ -447,7 +365,7 @@ export function getArcBoundingBox(center: VecLike, radius: number, start: VecLik
* @param b - The end of the arc
* @param c - A point on the arc
*/
export function getArcInfo(a: VecLike, b: VecLike, c: VecLike): TLArcInfo {
function getArcInfo(a: VecLike, b: VecLike, c: VecLike): TLArcInfo {
// find a circle from the three points
const u = -2 * (a.x * (b.y - c.y) - a.y * (b.x - c.x) + b.x * c.y - c.x * b.y)
@ -464,10 +382,10 @@ export function getArcInfo(a: VecLike, b: VecLike, c: VecLike): TLArcInfo {
u,
}
const radius = Vec2d.Dist(center, a)
const radius = Vec.Dist(center, a)
// Whether to draw the arc clockwise or counter-clockwise (are the points clockwise?)
const sweepFlag = +Vec2d.Clockwise(a, c, b)
const sweepFlag = +Vec.Clockwise(a, c, b)
// The base angle of the arc in radians
const ab = Math.hypot(a.y - b.y, a.x - b.x)
@ -498,19 +416,19 @@ export function getArcInfo(a: VecLike, b: VecLike, c: VecLike): TLArcInfo {
function placeCenterHandle(
center: VecLike,
radius: number,
tempA: Vec2d,
tempB: Vec2d,
tempC: Vec2d,
tempA: Vec,
tempB: Vec,
tempC: Vec,
originalArcLength: number,
isClockwise: boolean
) {
const aCA = Vec2d.Angle(center, tempA) // angle center -> a
const aCB = Vec2d.Angle(center, tempB) // angle center -> b
const aCA = Vec.Angle(center, tempA) // angle center -> a
const aCB = Vec.Angle(center, tempB) // angle center -> b
let dAB = clockwiseAngleDist(aCA, aCB) // angle distance between a and b
if (!isClockwise) dAB = PI2 - dAB
const n = 0.5 * (isClockwise ? 1 : -1)
const u = Vec2d.FromAngle(aCA + dAB * n)
const u = Vec.FromAngle(aCA + dAB * n)
tempC.setTo(center).add(u.mul(radius))
if (dAB > originalArcLength) {

View file

@ -1,6 +1,6 @@
import { TLArrowShape, TLArrowShapeTerminal, TLShape, TLShapeId } from '@tldraw/tlschema'
import { Matrix2d } from '../../../../primitives/Matrix2d'
import { Vec2d } from '../../../../primitives/Vec2d'
import { Mat } from '../../../../primitives/Mat'
import { Vec } from '../../../../primitives/Vec'
import { Group2d } from '../../../../primitives/geometry/Group2d'
import { Editor } from '../../../Editor'
@ -13,8 +13,8 @@ export type BoundShapeInfo<T extends TLShape = TLShape> = {
didIntersect: boolean
isExact: boolean
isClosed: boolean
transform: Matrix2d
outline: Vec2d[]
transform: Mat
outline: Vec[]
}
export function getBoundShapeInfoForTerminal(
@ -45,36 +45,36 @@ export function getBoundShapeInfoForTerminal(
}
}
export function getArrowTerminalInArrowSpace(
function getArrowTerminalInArrowSpace(
editor: Editor,
arrowPageTransform: Matrix2d,
arrowPageTransform: Mat,
terminal: TLArrowShapeTerminal,
forceImprecise: boolean
) {
if (terminal.type === 'point') {
return Vec2d.From(terminal)
return Vec.From(terminal)
}
const boundShape = editor.getShape(terminal.boundShapeId)
if (!boundShape) {
// this can happen in multiplayer contexts where the shape is being deleted
return new Vec2d(0, 0)
return new Vec(0, 0)
} else {
// Find the actual local point of the normalized terminal on
// the bound shape and transform it to page space, then transform
// it to arrow space
const { point, size } = editor.getShapeGeometry(boundShape).bounds
const shapePoint = Vec2d.Add(
const shapePoint = Vec.Add(
point,
Vec2d.MulV(
Vec.MulV(
// if the parent is the bound shape, then it's ALWAYS precise
terminal.isPrecise || forceImprecise ? terminal.normalizedAnchor : { x: 0.5, y: 0.5 },
size
)
)
const pagePoint = Matrix2d.applyToPoint(editor.getShapePageTransform(boundShape)!, shapePoint)
const arrowPoint = Matrix2d.applyToPoint(Matrix2d.Inverse(arrowPageTransform), pagePoint)
const pagePoint = Mat.applyToPoint(editor.getShapePageTransform(boundShape)!, shapePoint)
const arrowPoint = Mat.applyToPoint(Mat.Inverse(arrowPageTransform), pagePoint)
return arrowPoint
}
}

View file

@ -1,7 +1,6 @@
import { TLArrowShape } from '@tldraw/tlschema'
import { Box2d } from '../../../../primitives/Box2d'
import { Matrix2d, Matrix2dModel } from '../../../../primitives/Matrix2d'
import { Vec2d, VecLike } from '../../../../primitives/Vec2d'
import { Mat, MatModel } from '../../../../primitives/Mat'
import { Vec, VecLike } from '../../../../primitives/Vec'
import {
intersectLineSegmentPolygon,
intersectLineSegmentPolyline,
@ -25,9 +24,9 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
const a = terminalsInArrowSpace.start.clone()
const b = terminalsInArrowSpace.end.clone()
const c = Vec2d.Med(a, b)
const c = Vec.Med(a, b)
if (Vec2d.Equals(a, b)) {
if (Vec.Equals(a, b)) {
return {
isStraight: true,
start: {
@ -46,7 +45,7 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
}
}
const uAB = Vec2d.Sub(b, a).uni()
const uAB = Vec.Sub(b, a).uni()
// Update the arrowhead points using intersections with the bound shapes, if any.
@ -110,8 +109,8 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
}
}
const u = Vec2d.Sub(b, a).uni()
const didFlip = !Vec2d.Equals(u, uAB)
const u = Vec.Sub(b, a).uni()
const didFlip = !Vec.Equals(u, uAB)
// If the arrow is bound non-exact to a start shape and the
// start point has an arrowhead, then offset the start point
@ -151,7 +150,7 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
const tA = a.clone().add(u.clone().mul(offsetA * (didFlip ? -1 : 1)))
const tB = b.clone().sub(u.clone().mul(offsetB * (didFlip ? -1 : 1)))
const distAB = Vec2d.Dist(tA, tB)
const distAB = Vec.Dist(tA, tB)
if (distAB < minLength) {
if (offsetA !== 0 && offsetB !== 0) {
@ -179,14 +178,14 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
if (startShapeInfo && endShapeInfo) {
// If we have two bound shapes...then make the arrow a short arrow from
// the start point towards where the end point should be.
b.setTo(Vec2d.Add(a, u.clone().mul(-MIN_ARROW_LENGTH)))
b.setTo(Vec.Add(a, u.clone().mul(-MIN_ARROW_LENGTH)))
}
c.setTo(Vec2d.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end))
c.setTo(Vec.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end))
} else {
c.setTo(Vec2d.Med(a, b))
c.setTo(Vec.Med(a, b))
}
const length = Vec2d.Dist(a, b)
const length = Vec.Dist(a, b)
return {
isStraight: true,
@ -208,9 +207,9 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArr
/** Get an intersection point from A -> B with bound shape (target) from shape (arrow). */
function updateArrowheadPointWithBoundShape(
point: Vec2d,
opposite: Vec2d,
arrowPageTransform: Matrix2dModel,
point: Vec,
opposite: Vec,
arrowPageTransform: MatModel,
targetShapeInfo?: BoundShapeInfo
) {
if (targetShapeInfo === undefined) {
@ -224,12 +223,12 @@ function updateArrowheadPointWithBoundShape(
}
// From and To in page space
const pageFrom = Matrix2d.applyToPoint(arrowPageTransform, opposite)
const pageTo = Matrix2d.applyToPoint(arrowPageTransform, point)
const pageFrom = Mat.applyToPoint(arrowPageTransform, opposite)
const pageTo = Mat.applyToPoint(arrowPageTransform, point)
// From and To in local space of the target shape
const targetFrom = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageFrom)
const targetTo = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageTo)
const targetFrom = Mat.applyToPoint(Mat.Inverse(targetShapeInfo.transform), pageFrom)
const targetTo = Mat.applyToPoint(Mat.Inverse(targetShapeInfo.transform), pageTo)
const isClosed = targetShapeInfo.isClosed
const fn = isClosed ? intersectLineSegmentPolygon : intersectLineSegmentPolyline
@ -240,7 +239,7 @@ function updateArrowheadPointWithBoundShape(
if (intersection !== null) {
targetInt =
intersection.sort((p1, p2) => Vec2d.Dist(p1, targetFrom) - Vec2d.Dist(p2, targetFrom))[0] ??
intersection.sort((p1, p2) => Vec.Dist(p1, targetFrom) - Vec.Dist(p2, targetFrom))[0] ??
(isClosed ? undefined : targetTo)
}
@ -249,34 +248,10 @@ function updateArrowheadPointWithBoundShape(
return
}
const pageInt = Matrix2d.applyToPoint(targetShapeInfo.transform, targetInt)
const arrowInt = Matrix2d.applyToPoint(Matrix2d.Inverse(arrowPageTransform), pageInt)
const pageInt = Mat.applyToPoint(targetShapeInfo.transform, targetInt)
const arrowInt = Mat.applyToPoint(Mat.Inverse(arrowPageTransform), pageInt)
point.setTo(arrowInt)
targetShapeInfo.didIntersect = true
}
/** @public */
export function getStraightArrowHandlePath(info: TLArrowInfo & { isStraight: true }) {
return getArrowPath(info.start.handle, info.end.handle)
}
/** @public */
export function getSolidStraightArrowPath(info: TLArrowInfo & { isStraight: true }) {
return getArrowPath(info.start.point, info.end.point)
}
function getArrowPath(start: VecLike, end: VecLike) {
return `M${start.x},${start.y}L${end.x},${end.y}`
}
/** @public */
export function getStraightArrowBoundingBox(start: VecLike, end: VecLike) {
return new Box2d(
Math.min(start.x, end.x),
Math.min(start.y, end.y),
Math.abs(start.x - end.x),
Math.abs(start.y - end.y)
)
}

View file

@ -1,6 +1,6 @@
import { Vec2dModel } from '@tldraw/tlschema'
import { Box2d } from '../../../primitives/Box2d'
import { Vec2d } from '../../../primitives/Vec2d'
import { VecModel } from '@tldraw/tlschema'
import { Box } from '../../../primitives/Box'
import { Vec } from '../../../primitives/Vec'
import { TLResizeHandle } from '../../types/selection-types'
import { TLBaseBoxShape } from '../BaseBoxShapeUtil'
import { TLResizeMode } from '../ShapeUtil'
@ -17,12 +17,12 @@ export type ResizeBoxOptions = Partial<{
export function resizeBox(
shape: TLBaseBoxShape,
info: {
newPoint: Vec2dModel
newPoint: VecModel
handle: TLResizeHandle
mode: TLResizeMode
scaleX: number
scaleY: number
initialBounds: Box2d
initialBounds: Box
initialShape: TLBaseBoxShape
},
opts = {} as ResizeBoxOptions
@ -33,7 +33,7 @@ export function resizeBox(
let w = shape.props.w * scaleX
let h = shape.props.h * scaleY
const offset = new Vec2d(0, 0)
const offset = new Vec(0, 0)
if (w > 0) {
if (w < minWidth) {

View file

@ -1,42 +0,0 @@
import { TLShape, Vec2dModel } from '@tldraw/tlschema'
import { Box2d } from '../../../primitives/Box2d'
import { Vec2d } from '../../../primitives/Vec2d'
export function resizeScaled(
shape: Extract<TLShape, { props: { scale: number } }>,
{
initialBounds,
scaleX,
scaleY,
newPoint,
}: {
newPoint: Vec2dModel
initialBounds: Box2d
scaleX: number
scaleY: number
}
) {
// Compute the new scale (to apply to the scale prop)
const scaleDelta = Math.max(0.01, Math.min(Math.abs(scaleX), Math.abs(scaleY)))
// Compute the offset (if flipped X or flipped Y)
const offset = new Vec2d(0, 0)
if (scaleX < 0) {
offset.x = -(initialBounds.width * scaleDelta)
}
if (scaleY < 0) {
offset.y = -(initialBounds.height * scaleDelta)
}
// Apply the offset to the new point
const { x, y } = Vec2d.Add(newPoint, offset.rot(shape.rotation))
return {
x,
y,
props: {
scale: scaleDelta * shape.props.scale,
},
}
}

View file

@ -1,5 +1,5 @@
import { createShapeId } from '@tldraw/tlschema'
import { Vec2d } from '../../../../primitives/Vec2d'
import { Vec } from '../../../../primitives/Vec'
import { TLBaseBoxShape } from '../../../shapes/BaseBoxShapeUtil'
import { TLEventHandlers } from '../../../types/event-types'
import { StateNode } from '../../StateNode'
@ -96,7 +96,7 @@ export class Pointing extends StateNode {
const shape = this.editor.getShape<TLBaseBoxShape>(id)!
const { w, h } = this.editor.getShapeUtil(shape).getDefaultProps() as TLBaseBoxShape['props']
const delta = new Vec2d(w / 2, h / 2)
const delta = new Vec(w / 2, h / 2)
const parentTransform = this.editor.getShapeParentTransform(shape)
if (parentTransform) delta.rot(-parentTransform.rotation())

View file

@ -1,5 +1,5 @@
import { TLHandle, TLShape, Vec2dModel } from '@tldraw/tlschema'
import { VecLike } from '../../primitives/Vec2d'
import { TLHandle, TLShape, VecModel } from '@tldraw/tlschema'
import { VecLike } from '../../primitives/Vec'
import { TLSelectionHandle } from './selection-types'
/** @public */
@ -80,16 +80,16 @@ export type TLKeyboardEventInfo = TLBaseEventInfo & {
export type TLPinchEventInfo = TLBaseEventInfo & {
type: 'pinch'
name: TLPinchEventName
point: Vec2dModel
delta: Vec2dModel
point: VecModel
delta: VecModel
}
/** @public */
export type TLWheelEventInfo = TLBaseEventInfo & {
type: 'wheel'
name: 'wheel'
delta: Vec2dModel
point: Vec2dModel
delta: VecModel
point: VecModel
}
/** @public */

View file

@ -1,5 +1,5 @@
import { EmbedDefinition } from '@tldraw/tlschema'
import { VecLike } from '../../primitives/Vec2d'
import { VecLike } from '../../primitives/Vec'
import { TLContent } from './clipboard-types'
/** @public */

View file

@ -1,4 +1,4 @@
import { Box2d } from '../../primitives/Box2d'
import { Box } from '../../primitives/Box'
/** @public */
export type RequiredKeys<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>
@ -7,7 +7,7 @@ export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>
/** @public */
export type TLSvgOptions = {
bounds: Box2d
bounds: Box
scale: number
background: boolean
padding: number

View file

@ -1,4 +1,4 @@
import { RotateCorner, SelectionCorner, SelectionEdge } from '../../primitives/Box2d'
import { RotateCorner, SelectionCorner, SelectionEdge } from '../../primitives/Box'
/** @public */
export type TLSelectionHandle = SelectionCorner | SelectionEdge | RotateCorner

View file

@ -2,7 +2,7 @@ import type { AnyHandlerEventTypes, EventTypes, GestureKey, Handler } from '@use
import { createUseGesture, pinchAction, wheelAction } from '@use-gesture/react'
import * as React from 'react'
import { TLWheelEventInfo } from '../editor/types/event-types'
import { Vec2d } from '../primitives/Vec2d'
import { Vec } from '../primitives/Vec'
import { preventDefault } from '../utils/dom'
import { normalizeWheel } from '../utils/normalizeWheel'
import { useEditor } from './useEditor'
@ -122,7 +122,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
type: 'wheel',
name: 'wheel',
delta,
point: new Vec2d(event.clientX, event.clientY).sub({
point: new Vec(event.clientX, event.clientY).sub({
x: container.left,
y: container.top,
}),
@ -138,8 +138,8 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
let initZoom = 1 // the browser's zoom level when the pinch starts
let currZoom = 1 // the current zoom level according to the pinch gesture recognizer
let currDistanceBetweenFingers = 0
const initPointBetweenFingers = new Vec2d()
const prevPointBetweenFingers = new Vec2d()
const initPointBetweenFingers = new Vec()
const prevPointBetweenFingers = new Vec()
const onPinchStart: PinchHandler = (gesture) => {
const elm = ref.current
@ -186,7 +186,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
// How far have the two touch points moved towards or away from eachother?
const touchDistance = Math.abs(currDistanceBetweenFingers - initDistanceBetweenFingers)
// How far has the point between the touches moved?
const originDistance = Vec2d.Dist(initPointBetweenFingers, prevPointBetweenFingers)
const originDistance = Vec.Dist(initPointBetweenFingers, prevPointBetweenFingers)
switch (pinchState) {
case 'not sure': {

View file

@ -1,5 +1,5 @@
import { useLayoutEffect } from 'react'
import { VecLike } from '../primitives/Vec2d'
import { VecLike } from '../primitives/Vec'
/** @public */
export function useTransform(

View file

@ -0,0 +1,24 @@
import { Box } from './Box'
import { Vec } from './Vec'
describe('Box', () => {
it('Creates a box', () => {
const mat3 = new Box(0, 0, 100, 100)
expect(mat3).toMatchObject({
x: 0,
y: 0,
w: 100,
h: 100,
})
})
it('can have the point set with a Vec', () => {
const box = new Box(0, 0, 100, 100)
expect(box).toMatchObject({ x: 0, y: 0 })
box.point = new Vec(19, 23)
expect(box).toMatchObject({ x: 19, y: 23 })
})
})

View file

@ -1,9 +1,9 @@
import { Box2dModel } from '@tldraw/tlschema'
import { Vec2d, VecLike } from './Vec2d'
import { BoxModel } from '@tldraw/tlschema'
import { Vec, VecLike } from './Vec'
import { PI, PI2, toPrecision } from './utils'
/** @public */
export type BoxLike = Box2dModel | Box2d
export type BoxLike = BoxModel | Box
/** @public */
export type SelectionEdge = 'top' | 'right' | 'bottom' | 'left'
@ -23,7 +23,7 @@ export type RotateCorner =
| 'mobile_rotate'
/** @public */
export class Box2d {
export class Box {
constructor(x = 0, y = 0, w = 0, h = 0) {
this.x = x
this.y = y
@ -38,11 +38,11 @@ export class Box2d {
// eslint-disable-next-line no-restricted-syntax
get point() {
return new Vec2d(this.x, this.y)
return new Vec(this.x, this.y)
}
// eslint-disable-next-line no-restricted-syntax
set point(val: Vec2d) {
set point(val: Vec) {
this.x = val.x
this.y = val.y
}
@ -114,11 +114,11 @@ export class Box2d {
// eslint-disable-next-line no-restricted-syntax
get center() {
return new Vec2d(this.midX, this.midY)
return new Vec(this.midX, this.midY)
}
// eslint-disable-next-line no-restricted-syntax
set center(v: Vec2d) {
set center(v: Vec) {
this.minX = v.x - this.width / 2
this.minY = v.y - this.height / 2
}
@ -126,26 +126,26 @@ export class Box2d {
// eslint-disable-next-line no-restricted-syntax
get corners() {
return [
new Vec2d(this.minX, this.minY),
new Vec2d(this.maxX, this.minY),
new Vec2d(this.maxX, this.maxY),
new Vec2d(this.minX, this.maxY),
new Vec(this.minX, this.minY),
new Vec(this.maxX, this.minY),
new Vec(this.maxX, this.maxY),
new Vec(this.minX, this.maxY),
]
}
// eslint-disable-next-line no-restricted-syntax
get snapPoints() {
return [
new Vec2d(this.minX, this.minY),
new Vec2d(this.maxX, this.minY),
new Vec2d(this.maxX, this.maxY),
new Vec2d(this.minX, this.maxY),
new Vec(this.minX, this.minY),
new Vec(this.maxX, this.minY),
new Vec(this.maxX, this.maxY),
new Vec(this.minX, this.maxY),
this.center,
]
}
// eslint-disable-next-line no-restricted-syntax
get sides(): Array<[Vec2d, Vec2d]> {
get sides(): Array<[Vec, Vec]> {
const { corners } = this
return [
[corners[0], corners[1]],
@ -156,8 +156,8 @@ export class Box2d {
}
// eslint-disable-next-line no-restricted-syntax
get size(): Vec2d {
return new Vec2d(this.w, this.h)
get size(): Vec {
return new Vec(this.w, this.h)
}
toFixed() {
@ -168,7 +168,7 @@ export class Box2d {
return this
}
setTo(B: Box2d) {
setTo(B: Box) {
this.x = B.x
this.y = B.y
this.w = B.w
@ -184,7 +184,7 @@ export class Box2d {
return this
}
expand(A: Box2d) {
expand(A: Box) {
const minX = Math.min(this.minX, A.minX)
const minY = Math.min(this.minY, A.minY)
const maxX = Math.max(this.maxX, A.maxX)
@ -215,7 +215,7 @@ export class Box2d {
clone() {
const { x, y, w, h } = this
return new Box2d(x, y, w, h)
return new Box(x, y, w, h)
}
translate(delta: VecLike) {
@ -235,44 +235,44 @@ export class Box2d {
this.height = Math.max(1, maxY - minY)
}
collides(B: Box2d) {
return Box2d.Collides(this, B)
collides(B: Box) {
return Box.Collides(this, B)
}
contains(B: Box2d) {
return Box2d.Contains(this, B)
contains(B: Box) {
return Box.Contains(this, B)
}
includes(B: Box2d) {
return Box2d.Includes(this, B)
includes(B: Box) {
return Box.Includes(this, B)
}
containsPoint(V: VecLike, margin = 0) {
return Box2d.ContainsPoint(this, V, margin)
return Box.ContainsPoint(this, V, margin)
}
getHandlePoint(handle: SelectionCorner | SelectionEdge) {
switch (handle) {
case 'top_left':
return new Vec2d(this.minX, this.minY)
return new Vec(this.minX, this.minY)
case 'top_right':
return new Vec2d(this.maxX, this.minY)
return new Vec(this.maxX, this.minY)
case 'bottom_left':
return new Vec2d(this.minX, this.maxY)
return new Vec(this.minX, this.maxY)
case 'bottom_right':
return new Vec2d(this.maxX, this.maxY)
return new Vec(this.maxX, this.maxY)
case 'top':
return new Vec2d(this.midX, this.minY)
return new Vec(this.midX, this.minY)
case 'right':
return new Vec2d(this.maxX, this.midY)
return new Vec(this.maxX, this.midY)
case 'bottom':
return new Vec2d(this.midX, this.maxY)
return new Vec(this.midX, this.maxY)
case 'left':
return new Vec2d(this.minX, this.midY)
return new Vec(this.minX, this.midY)
}
}
toJson(): Box2dModel {
toJson(): BoxModel {
return { x: this.minX, y: this.minY, w: this.w, h: this.h }
}
@ -336,7 +336,7 @@ export class Box2d {
this.height = Math.abs(b1y - b0y)
}
union(box: Box2dModel) {
union(box: BoxModel) {
const minX = Math.min(this.minX, box.x)
const minY = Math.min(this.minY, box.y)
const maxX = Math.max(this.maxX, box.w + box.x)
@ -350,12 +350,12 @@ export class Box2d {
return this
}
static From(box: Box2dModel) {
return new Box2d(box.x, box.y, box.w, box.h)
static From(box: BoxModel) {
return new Box(box.x, box.y, box.w, box.h)
}
static FromPoints(points: VecLike[]) {
if (points.length === 0) return new Box2d()
if (points.length === 0) return new Box()
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
@ -369,35 +369,35 @@ export class Box2d {
maxY = Math.max(point.y, maxY)
}
return new Box2d(minX, minY, maxX - minX, maxY - minY)
return new Box(minX, minY, maxX - minX, maxY - minY)
}
static Expand(A: Box2d, B: Box2d) {
static Expand(A: Box, B: Box) {
const minX = Math.min(B.minX, A.minX)
const minY = Math.min(B.minY, A.minY)
const maxX = Math.max(B.maxX, A.maxX)
const maxY = Math.max(B.maxY, A.maxY)
return new Box2d(minX, minY, maxX - minX, maxY - minY)
return new Box(minX, minY, maxX - minX, maxY - minY)
}
static ExpandBy(A: Box2d, n: number) {
return new Box2d(A.minX - n, A.minY - n, A.width + n * 2, A.height + n * 2)
static ExpandBy(A: Box, n: number) {
return new Box(A.minX - n, A.minY - n, A.width + n * 2, A.height + n * 2)
}
static Collides = (A: Box2d, B: Box2d) => {
static Collides = (A: Box, B: Box) => {
return !(A.maxX < B.minX || A.minX > B.maxX || A.maxY < B.minY || A.minY > B.maxY)
}
static Contains = (A: Box2d, B: Box2d) => {
static Contains = (A: Box, B: Box) => {
return A.minX < B.minX && A.minY < B.minY && A.maxY > B.maxY && A.maxX > B.maxX
}
static Includes = (A: Box2d, B: Box2d) => {
return Box2d.Collides(A, B) || Box2d.Contains(A, B)
static Includes = (A: Box, B: Box) => {
return Box.Collides(A, B) || Box.Contains(A, B)
}
static ContainsPoint = (A: Box2d, B: VecLike, margin = 0) => {
static ContainsPoint = (A: Box, B: VecLike, margin = 0) => {
return !(
B.x < A.minX - margin ||
B.y < A.minY - margin ||
@ -406,7 +406,7 @@ export class Box2d {
)
}
static Common = (boxes: Box2d[]): Box2d => {
static Common = (boxes: Box[]): Box => {
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
@ -420,10 +420,10 @@ export class Box2d {
maxY = Math.max(maxY, B.maxY)
}
return new Box2d(minX, minY, maxX - minX, maxY - minY)
return new Box(minX, minY, maxX - minX, maxY - minY)
}
static Sides = (A: Box2d, inset = 0) => {
static Sides = (A: Box, inset = 0) => {
const { corners } = A
if (inset) {
// TODO: Inset the corners by the inset amount.
@ -438,7 +438,7 @@ export class Box2d {
}
static Resize(
box: Box2d,
box: Box,
handle: SelectionCorner | SelectionEdge | string,
dx: number,
dy: number,
@ -550,7 +550,7 @@ export class Box2d {
b0y = t
}
const final = new Box2d(b0x, b0y, Math.abs(b1x - b0x), Math.abs(b1y - b0y))
const final = new Box(b0x, b0y, Math.abs(b1x - b0x), Math.abs(b1y - b0y))
return {
box: final,
@ -559,11 +559,11 @@ export class Box2d {
}
}
equals(other: Box2d | Box2dModel) {
return Box2d.Equals(this, other)
equals(other: Box | BoxModel) {
return Box.Equals(this, other)
}
static Equals(a: Box2d | Box2dModel, b: Box2d | Box2dModel) {
static Equals(a: Box | BoxModel, b: Box | BoxModel) {
return b.x === a.x && b.y === a.y && b.w === a.w && b.h === a.h
}
@ -573,8 +573,8 @@ export class Box2d {
return this
}
static ZeroFix(other: Box2d | Box2dModel) {
return new Box2d(other.x, other.y, Math.max(1, other.w), Math.max(1, other.h))
static ZeroFix(other: Box | BoxModel) {
return new Box(other.x, other.y, Math.max(1, other.w), Math.max(1, other.h))
}
}

View file

@ -1,24 +0,0 @@
import { Box2d } from './Box2d'
import { Vec2d } from './Vec2d'
describe('Box2d', () => {
it('Creates a box', () => {
const mat3 = new Box2d(0, 0, 100, 100)
expect(mat3).toMatchObject({
x: 0,
y: 0,
w: 100,
h: 100,
})
})
it('can have the point set with a vec2d', () => {
const box = new Box2d(0, 0, 100, 100)
expect(box).toMatchObject({ x: 0, y: 0 })
box.point = new Vec2d(19, 23)
expect(box).toMatchObject({ x: 19, y: 23 })
})
})

View file

@ -0,0 +1,46 @@
import { Mat } from './Mat'
describe('Mat', () => {
it('Creates a matrix', () => {
const mat3 = new Mat(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
expect(mat3).toMatchObject(Mat.Identity())
})
it('Multiplies a matrix', () => {
const m1 = new Mat(1, 2, 3, 4, 5, 6)
const m2 = new Mat(1, 2, 3, 4, 5, 6)
expect(m1.multiply(m2)).toMatchObject({
a: 7,
b: 10,
c: 15,
d: 22,
e: 28,
f: 40,
})
})
it('Composes matrices', () => {
const m1 = new Mat(1, 2, 3, 4, 5, 6)
const m2 = new Mat(1, 2, 3, 4, 5, 6)
expect(Mat.Compose(m1, m2)).toMatchObject({
a: 7,
b: 10,
c: 15,
d: 22,
e: 28,
f: 40,
})
})
it('Inverts a matrix', () => {
const m1 = new Mat(1, 2, 3, 4, 5, 6)
expect(m1.invert()).toMatchObject({
a: -2,
b: 1,
c: 1.5,
d: -0.5,
e: 1,
f: -2,
})
})
})

View file

@ -1,21 +1,12 @@
import { Box2d } from './Box2d'
import { clampRadians, TAU, toDomPrecision } from './utils'
import { Vec2d, VecLike } from './Vec2d'
import { Box } from './Box'
import { clampRadians, HALF_PI, toDomPrecision } from './utils'
import { Vec, VecLike } from './Vec'
/** @public */
export type MatLike = Matrix2dModel | Matrix2d
export type MatLike = MatModel | Mat
/** @public */
export interface MatrixInfo {
x: number
y: number
scaleX: number
scaleY: number
rotation: number
}
/** @public */
export interface Matrix2dModel {
export interface MatModel {
a: number
b: number
c: number
@ -25,11 +16,11 @@ export interface Matrix2dModel {
}
// function getIdentity() {
// return new Matrix2d(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
// return new Mat(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
// }
/** @public */
export class Matrix2d {
export class Mat {
constructor(a: number, b: number, c: number, d: number, e: number, f: number) {
this.a = a
this.b = b
@ -46,7 +37,7 @@ export class Matrix2d {
e = 0.0
f = 0.0
equals(m: Matrix2d | Matrix2dModel) {
equals(m: Mat | MatModel) {
return (
this.a === m.a &&
this.b === m.b &&
@ -67,8 +58,8 @@ export class Matrix2d {
return this
}
multiply(m: Matrix2d | Matrix2dModel) {
const m2: Matrix2dModel = m
multiply(m: Mat | MatModel) {
const m2: MatModel = m
const { a, b, c, d, e, f } = this
this.a = a * m2.a + c * m2.b
this.c = a * m2.c + c * m2.d
@ -81,16 +72,16 @@ export class Matrix2d {
rotate(r: number, cx?: number, cy?: number) {
if (r === 0) return this
if (cx === undefined) return this.multiply(Matrix2d.Rotate(r))
return this.translate(cx, cy!).multiply(Matrix2d.Rotate(r)).translate(-cx, -cy!)
if (cx === undefined) return this.multiply(Mat.Rotate(r))
return this.translate(cx, cy!).multiply(Mat.Rotate(r)).translate(-cx, -cy!)
}
translate(x: number, y: number): Matrix2d {
return this.multiply(Matrix2d.Translate(x, y!))
translate(x: number, y: number): Mat {
return this.multiply(Mat.Translate(x, y!))
}
scale(x: number, y: number) {
return this.multiply(Matrix2d.Scale(x, y))
return this.multiply(Mat.Scale(x, y))
}
invert() {
@ -106,85 +97,77 @@ export class Matrix2d {
}
applyToPoint(point: VecLike) {
return Matrix2d.applyToPoint(this, point)
return Mat.applyToPoint(this, point)
}
applyToPoints(points: VecLike[]) {
return Matrix2d.applyToPoints(this, points)
return Mat.applyToPoints(this, points)
}
rotation() {
return Matrix2d.Rotation(this)
return Mat.Rotation(this)
}
point() {
return Matrix2d.Point(this)
return Mat.Point(this)
}
decomposed() {
return Matrix2d.Decompose(this)
return Mat.Decompose(this)
}
toCssString() {
return Matrix2d.toCssString(this)
return Mat.toCssString(this)
}
setTo(model: Matrix2dModel) {
setTo(model: MatModel) {
Object.assign(this, model)
return this
}
decompose() {
return Matrix2d.Decompose(this)
return Mat.Decompose(this)
}
clone() {
return new Matrix2d(this.a, this.b, this.c, this.d, this.e, this.f)
return new Mat(this.a, this.b, this.c, this.d, this.e, this.f)
}
/* --------------------- Static --------------------- */
static Identity() {
return new Matrix2d(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
return new Mat(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
}
static Translate(x: number, y: number) {
return new Matrix2d(1.0, 0.0, 0.0, 1.0, x, y)
return new Mat(1.0, 0.0, 0.0, 1.0, x, y)
}
static Rotate(r: number, cx?: number, cy?: number) {
if (r === 0) return Matrix2d.Identity()
if (r === 0) return Mat.Identity()
const cosAngle = Math.cos(r)
const sinAngle = Math.sin(r)
const rotationMatrix2d = new Matrix2d(cosAngle, sinAngle, -sinAngle, cosAngle, 0.0, 0.0)
const rotationMatrix = new Mat(cosAngle, sinAngle, -sinAngle, cosAngle, 0.0, 0.0)
if (cx === undefined) return rotationMatrix2d
if (cx === undefined) return rotationMatrix
return Matrix2d.Compose(
Matrix2d.Translate(cx, cy!),
rotationMatrix2d,
Matrix2d.Translate(-cx, -cy!)
)
return Mat.Compose(Mat.Translate(cx, cy!), rotationMatrix, Mat.Translate(-cx, -cy!))
}
static Scale: {
(x: number, y: number): Matrix2dModel
(x: number, y: number, cx: number, cy: number): Matrix2dModel
(x: number, y: number): MatModel
(x: number, y: number, cx: number, cy: number): MatModel
} = (x: number, y: number, cx?: number, cy?: number) => {
const scaleMatrix2d = new Matrix2d(x, 0, 0, y, 0, 0)
const scaleMatrix = new Mat(x, 0, 0, y, 0, 0)
if (cx === undefined) return scaleMatrix2d
if (cx === undefined) return scaleMatrix
return Matrix2d.Compose(
Matrix2d.Translate(cx, cy!),
scaleMatrix2d,
Matrix2d.Translate(-cx, -cy!)
)
return Mat.Compose(Mat.Translate(cx, cy!), scaleMatrix, Mat.Translate(-cx, -cy!))
}
static Multiply(m1: Matrix2dModel, m2: Matrix2dModel): Matrix2dModel {
static Multiply(m1: MatModel, m2: MatModel): MatModel {
return {
a: m1.a * m2.a + m1.c * m2.b,
c: m1.a * m2.c + m1.c * m2.d,
@ -195,7 +178,7 @@ export class Matrix2d {
}
}
static Inverse(m: Matrix2dModel): Matrix2dModel {
static Inverse(m: MatModel): MatModel {
const denom = m.a * m.d - m.b * m.c
return {
a: m.d / denom,
@ -207,7 +190,7 @@ export class Matrix2d {
}
}
static Absolute(m: MatLike): Matrix2dModel {
static Absolute(m: MatLike): MatModel {
const denom = m.a * m.d - m.b * m.c
return {
a: m.d / denom,
@ -220,7 +203,7 @@ export class Matrix2d {
}
static Compose(...matrices: MatLike[]) {
const matrix = Matrix2d.Identity()
const matrix = Mat.Identity()
for (let i = 0, n = matrices.length; i < n; i++) {
matrix.multiply(matrices[i])
}
@ -228,7 +211,7 @@ export class Matrix2d {
}
static Point(m: MatLike) {
return new Vec2d(m.e, m.f)
return new Vec(m.e, m.f)
}
static Rotation(m: MatLike): number {
@ -239,7 +222,7 @@ export class Matrix2d {
rotation = Math.acos(m.a / hypotAc) * (m.c > 0 ? -1 : 1)
} else if (m.b !== 0 || m.d !== 0) {
const hypotBd = Math.hypot(m.b, m.d)
rotation = TAU + Math.acos(m.b / hypotBd) * (m.d > 0 ? -1 : 1)
rotation = HALF_PI + Math.acos(m.b / hypotBd) * (m.d > 0 ? -1 : 1)
} else {
rotation = 0
}
@ -247,7 +230,7 @@ export class Matrix2d {
return clampRadians(rotation)
}
static Decompose(m: MatLike): MatrixInfo {
static Decompose(m: MatLike) {
let scaleX, scaleY, rotation
if (m.a !== 0 || m.c !== 0) {
@ -259,7 +242,7 @@ export class Matrix2d {
const hypotBd = Math.hypot(m.b, m.d)
scaleX = (m.a * m.d - m.b * m.c) / hypotBd
scaleY = hypotBd
rotation = TAU + Math.acos(m.b / hypotBd) * (m.d > 0 ? -1 : 1)
rotation = HALF_PI + Math.acos(m.b / hypotBd) * (m.d > 0 ? -1 : 1)
} else {
scaleX = 0
scaleY = 0
@ -292,7 +275,7 @@ export class Matrix2d {
}
static applyToPoint(m: MatLike, point: VecLike) {
return new Vec2d(
return new Vec(
m.a * point.x + m.c * point.y + m.e,
m.b * point.x + m.d * point.y + m.f,
point.z
@ -303,28 +286,28 @@ export class Matrix2d {
return [m.a * x + m.c * y + m.e, m.b * x + m.d * y + m.f]
}
static applyToPoints(m: MatLike, points: VecLike[]): Vec2d[] {
static applyToPoints(m: MatLike, points: VecLike[]): Vec[] {
return points.map(
(point) =>
new Vec2d(m.a * point.x + m.c * point.y + m.e, m.b * point.x + m.d * point.y + m.f, point.z)
new Vec(m.a * point.x + m.c * point.y + m.e, m.b * point.x + m.d * point.y + m.f, point.z)
)
}
static applyToBounds(m: MatLike, box: Box2d) {
return new Box2d(m.e + box.minX, m.f + box.minY, box.width, box.height)
static applyToBounds(m: MatLike, box: Box) {
return new Box(m.e + box.minX, m.f + box.minY, box.width, box.height)
}
static From(m: MatLike) {
return new Matrix2d(m.a, m.b, m.c, m.d, m.e, m.f)
return new Mat(m.a, m.b, m.c, m.d, m.e, m.f)
}
static Cast(m: MatLike) {
return m instanceof Matrix2d ? m : Matrix2d.From(m)
return m instanceof Mat ? m : Mat.From(m)
}
}
/** @public */
export function decomposeMatrix2d(m: MatLike) {
export function decomposeMatrix(m: MatLike) {
return {
x: m.e,
y: m.f,

View file

@ -1,46 +0,0 @@
import { Matrix2d } from './Matrix2d'
describe('Matrix2d', () => {
it('Creates a matrix', () => {
const mat3 = new Matrix2d(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
expect(mat3).toMatchObject(Matrix2d.Identity())
})
it('Multiplies a matrix', () => {
const m1 = new Matrix2d(1, 2, 3, 4, 5, 6)
const m2 = new Matrix2d(1, 2, 3, 4, 5, 6)
expect(m1.multiply(m2)).toMatchObject({
a: 7,
b: 10,
c: 15,
d: 22,
e: 28,
f: 40,
})
})
it('Composes matrices', () => {
const m1 = new Matrix2d(1, 2, 3, 4, 5, 6)
const m2 = new Matrix2d(1, 2, 3, 4, 5, 6)
expect(Matrix2d.Compose(m1, m2)).toMatchObject({
a: 7,
b: 10,
c: 15,
d: 22,
e: 28,
f: 40,
})
})
it('Inverts a matrix', () => {
const m1 = new Matrix2d(1, 2, 3, 4, 5, 6)
expect(m1.invert()).toMatchObject({
a: -2,
b: 1,
c: 1.5,
d: -0.5,
e: 1,
f: -2,
})
})
})

View file

@ -0,0 +1,373 @@
import { Vec } from './Vec'
describe('iteratable', () => {
it('Constructs', () => {
const v = new Vec(1, 2)
const { x, y } = v
expect(x).toBeCloseTo(1)
expect(y).toBeCloseTo(2)
})
})
describe('Vec.Clamp', () => {
it('Clamps a vector between a range.', () => {
expect(Vec.Clamp(new Vec(9, 5), 7, 10)).toMatchObject(new Vec(9, 7))
expect(Vec.Clamp(new Vec(-9, 5), 0, 10)).toMatchObject(new Vec(0, 5))
})
})
describe('Vec.Clamp', () => {
it('Clamps a vector between a range.', () => {
expect(Vec.Clamp(new Vec(9, 5), 7, 10)).toMatchObject(new Vec(9, 7))
expect(Vec.Clamp(new Vec(-9, 5), 0, 10)).toMatchObject(new Vec(0, 5))
})
it('Clamps a vector between a range.', () => {
expect(Vec.Clamp(new Vec(9, 5), 10)).toMatchObject(new Vec(10, 10))
expect(Vec.Clamp(new Vec(-9, 5), 10)).toMatchObject(new Vec(10, 10))
})
})
describe('Vec.Neg', () => {
it('Negates a vector.', () => {
expect(Vec.Neg(new Vec(9, 5))).toMatchObject(new Vec(-9, -5))
expect(Vec.Neg(new Vec(-9, 0))).toMatchObject(new Vec(9, -0))
})
})
describe('Vec.Add', () => {
it('Adds two vectors.', () => {
expect(Vec.Add(new Vec(9, 5), new Vec(2, 1))).toMatchObject(new Vec(11, 6))
expect(Vec.Add(new Vec(-9, 5), new Vec(2, -1))).toMatchObject(new Vec(-7, 4))
})
})
describe('Vec.AddScalar', () => {
it('Adds a scalar to a vector.', () => {
expect(Vec.AddScalar(new Vec(9, 5), 2)).toMatchObject(new Vec(11, 7))
expect(Vec.AddScalar(new Vec(-9, 5), 2)).toMatchObject(new Vec(-7, 7))
})
})
describe('Vec.Sub', () => {
it('Subtracts two vectors.', () => {
expect(Vec.Sub(new Vec(9, 5), new Vec(2, 1))).toMatchObject(new Vec(7, 4))
expect(Vec.Sub(new Vec(-9, 5), new Vec(2, -1))).toMatchObject(new Vec(-11, 6))
})
})
describe('Vec.SubScalar', () => {
it('Subtracts a scalar from a vector.', () => {
expect(Vec.SubScalar(new Vec(9, 5), 2)).toMatchObject(new Vec(7, 3))
expect(Vec.SubScalar(new Vec(-9, 5), 2)).toMatchObject(new Vec(-11, 3))
})
})
describe('Vec.Mul', () => {
it('Get a vector multiplied by a scalar.', () => {
expect(Vec.Mul(new Vec(9, 9), 3)).toMatchObject(new Vec(27, 27))
expect(Vec.Mul(new Vec(10, 10), 2)).toMatchObject(new Vec(20, 20))
})
})
describe('Vec.DivV', () => {
it('Get a vector multiplied by a vector.', () => {
expect(Vec.MulV(new Vec(16, 12), new Vec(2, 4))).toMatchObject(new Vec(32, 48))
expect(Vec.MulV(new Vec(5, 15), new Vec(5, 3))).toMatchObject(new Vec(25, 45))
})
})
describe('Vec.Div', () => {
it('Get a vector divided by a scalar.', () => {
expect(Vec.Div(new Vec(9, 9), 3)).toMatchObject(new Vec(3, 3))
expect(Vec.Div(new Vec(10, 10), 2)).toMatchObject(new Vec(5, 5))
})
})
describe('Vec.DivV', () => {
it('Get a vector divided by a vector.', () => {
expect(Vec.DivV(new Vec(16, 12), new Vec(2, 4))).toMatchObject(new Vec(8, 3))
expect(Vec.DivV(new Vec(5, 15), new Vec(5, 3))).toMatchObject(new Vec(1, 5))
})
})
describe('Vec.Per', () => {
it('Gets the perpendicular rotation of a vector.', () => {
expect(Vec.Per(new Vec(1, -1))).toMatchObject(new Vec(-1, -1))
expect(Vec.Per(new Vec(-1, 1))).toMatchObject(new Vec(1, 1))
})
})
describe('Vec.Dpr', () => {
it('Gets the dot product of two vectors.', () => {
expect(Vec.Dpr(new Vec(1, 0), new Vec(1, 0))).toEqual(1)
expect(Vec.Dpr(new Vec(1, 0), new Vec(0, 0))).toEqual(0)
expect(Vec.Dpr(new Vec(1, 0), new Vec(-1, 0))).toEqual(-1)
})
})
describe('Vec.Cpr', () => {
it('Gets the cross product (outer product) of two vectors.', () => {
expect(Vec.Cpr(new Vec(0, 1), new Vec(1, 1))).toEqual(-1)
expect(Vec.Cpr(new Vec(1, 1), new Vec(1, 1))).toEqual(0)
expect(Vec.Cpr(new Vec(1, 1), new Vec(0, 1))).toEqual(1)
})
})
describe('Vec.Len2', () => {
it('Gets the length of a vector squared.', () => {
expect(Vec.Len2(new Vec(0, 0))).toEqual(0)
expect(Vec.Len2(new Vec(0, 1))).toEqual(1)
expect(Vec.Len2(new Vec(1, 1))).toEqual(2)
})
})
describe('Vec.Len', () => {
it('Gets the length of a vector.', () => {
expect(Vec.Len(new Vec(0, 0))).toEqual(0)
expect(Vec.Len(new Vec(0, 1))).toEqual(1)
expect(Vec.Len(new Vec(1, 1))).toEqual(1.4142135623730951)
})
})
describe('Vec.Pry', () => {
it('Projects a vector A over vector B.', () => {
expect(Vec.Pry(new Vec(0, 0), new Vec(0, 10))).toEqual(0)
expect(Vec.Pry(new Vec(0, 0), new Vec(10, 10))).toEqual(0)
expect(Vec.Pry(new Vec(10, 10), new Vec(0, 10))).toEqual(10)
expect(Vec.Pry(new Vec(10, 10), new Vec(10, 10))).toEqual(14.14213562373095)
})
})
describe('Vec.Uni', () => {
it('Gets the normalized vector.', () => {
expect(Vec.Uni(new Vec(0, 10))).toMatchObject(new Vec(0, 1))
expect(Vec.Uni(new Vec(10, 10))).toMatchObject(new Vec(0.7071067811865475, 0.7071067811865475))
})
})
describe('Vec.Tan', () => {
it('Gets the tangent between two vectors.', () => {
expect(Vec.Tan(new Vec(0, 0), new Vec(0, 10))).toMatchObject(new Vec(0, -1))
expect(Vec.Tan(new Vec(0, 0), new Vec(10, 10))).toMatchObject(
new Vec(-0.7071067811865475, -0.7071067811865475)
)
})
})
describe('Vec.Dist2', () => {
it('Finds the squared distance between two points.', () => {
expect(Vec.Dist2(new Vec(0, 0), new Vec(0, 10))).toEqual(100)
expect(Vec.Dist2(new Vec(0, 0), new Vec(10, 10))).toEqual(200)
})
})
describe('Vec.Dist', () => {
it('Finds the distance between two points.', () => {
expect(Vec.Dist(new Vec(0, 0), new Vec(0, 10))).toEqual(10)
expect(Vec.Dist(new Vec(0, 0), new Vec(10, 10))).toEqual(14.142135623730951)
})
})
// describe('Vec.Ang2', () => {
// it('Finds the angle in radians between two vectors.', () => {
// expect(Vec.Ang2(new Vec(1, 0), new Vec(0, 1))).toEqual(Math.PI / 2)
// })
// })
// describe('Vec.Ang3', () => {
// it('Gets the angle of ∠ABC', () => {
// expect(Vec.Ang3([5, 0], new Vec(0, 0), new Vec(0, 5))).toEqual(Math.PI / 2)
// expect(Vec.Ang3(new Vec(1, 0), new Vec(0, 0), new Vec(0, 1))).toEqual(Math.PI / 2)
// })
// })
describe('Vec.Angle', () => {
it('Finds the angle in radians between two points.', () => {
expect(Vec.Angle(new Vec(0, 0), new Vec(10, 10))).toEqual(Math.PI / 4)
expect(Vec.Angle(new Vec(0, 0), new Vec(10, 0))).toEqual(0)
expect(Vec.Angle(new Vec(0, 0), new Vec(0, 10))).toEqual(Math.PI / 2)
})
})
describe('Vec.Med', () => {
it('Finds the midpoint between two vectors.', () => {
expect(Vec.Med(new Vec(0, 0), new Vec(10, 10))).toMatchObject(new Vec(5, 5))
expect(Vec.Med(new Vec(0, 0), new Vec(10, 0))).toMatchObject(new Vec(5, 0))
expect(Vec.Med(new Vec(0, 0), new Vec(0, 10))).toMatchObject(new Vec(0, 5))
expect(Vec.Med(new Vec(-100, 0), new Vec(0, 100))).toMatchObject(new Vec(-50, 50))
})
})
describe('Vec.Rot', () => {
it('Rotates a vector by a rotation in radians.', () => {
const { x, y } = Vec.Rot(new Vec(1, 0), Math.PI / 4)
expect(x).toBeCloseTo(0.7, 1)
expect(y).toBeCloseTo(0.7, 1)
})
})
describe('Vec.RotWith', () => {
it('Rotates a vector around a second vector by a rotation in radians.', () => {
expect(Vec.RotWith(new Vec(1, 0), new Vec(0, 0), Math.PI / 4)).toMatchObject(
new Vec(0.7071067811865476, 0.7071067811865475)
)
})
})
describe('Vec.Equals', () => {
it('Gets whether two vectors are identical.', () => {
expect(Vec.Equals(new Vec(1, 2), new Vec(1, 2))).toEqual(true)
expect(Vec.Equals(new Vec(1, 2), new Vec(1, 3))).toEqual(false)
expect(Vec.Equals(new Vec(-0, 2), new Vec(0, 2))).toEqual(true)
})
})
describe('Vec.Int', () => {
it('Interpolate from A to B', () => {
expect(Vec.Lrp(new Vec(0, 0), new Vec(10, 10), 0.5)).toMatchObject(new Vec(5, 5))
expect(Vec.Lrp(new Vec(0, 0), new Vec(10, 10), 2)).toMatchObject(new Vec(20, 20))
})
})
describe('Vec.Rescale', () => {
it('Rescales a vector by a scalar', () => {
expect(Vec.Rescale(new Vec(5, 0), 1)).toMatchObject(new Vec(1, 0))
expect(Vec.Rescale(new Vec(5, 0), 2)).toMatchObject(new Vec(2, 0))
expect(Vec.Rescale(new Vec(0.5, 0.25), 2)).toEqual(
new Vec(1.7888543819998317, 0.8944271909999159)
)
})
})
describe('Vec.IsClockwise', () => {
it('Gets whether point A and point B wind clockwise around point C.', () => {
expect(Vec.Clockwise(new Vec(0, 0), new Vec(5, 5), new Vec(0, 5))).toEqual(true)
expect(Vec.Clockwise(new Vec(5, 5), new Vec(0, 0), new Vec(0, 5))).toEqual(false)
expect(Vec.Clockwise(new Vec(0, 10), new Vec(0, 0), new Vec(0, 5))).toEqual(false)
})
})
describe('Vec.ToFixed', () => {
it('Rounds a vector to the a given precision.', () => {
expect(Vec.ToFixed(new Vec(1.2345, 5.678), 1)).toMatchObject(new Vec(1.2, 5.7))
expect(Vec.ToFixed(new Vec(1.2345, 5.678), 2)).toMatchObject(new Vec(1.23, 5.68))
})
})
describe('Vec.Snap', () => {
it('Snaps a vector to the nearest increment provided.', () => {
expect(Vec.Snap(new Vec(10.5, 28), 10)).toMatchObject(new Vec(10, 30))
})
})
describe('Vec.NearestPointOnLineThroughPoint', () => {
it('Gets the distance from a point to a line that passes through a given point.', () => {
expect(
Vec.NearestPointOnLineThroughPoint(new Vec(0, 0), new Vec(0, 1), new Vec(5, 5))
).toMatchObject(new Vec(0, 5))
})
})
describe('Vec.DistanceToLineThroughPoint', () => {
it('Gets the distance from a point to a line that passes through a given point.', () => {
expect(Vec.DistanceToLineThroughPoint(new Vec(0, 0), new Vec(0, 1), new Vec(5, 5))).toEqual(5)
})
})
describe('Vec.NearestPointOnLineSegment', () => {
it('Gets the distance from a point to a line segment.', () => {
expect(
Vec.NearestPointOnLineSegment(new Vec(0, 0), new Vec(0, 10), new Vec(5, 5))
).toMatchObject(new Vec(0, 5))
})
})
describe('Vec.DistanceToLineSegment', () => {
it('Gets the distance from a point to a line segment.', () => {
expect(Vec.DistanceToLineSegment(new Vec(0, 0), new Vec(0, 10), new Vec(5, 5))).toEqual(5)
})
})
describe('Vec.Nudge', () => {
it('Pushes a point towards another point by a given distance.', () => {
expect(Vec.Nudge(new Vec(0, 0), new Vec(0, 10), 5)).toMatchObject(new Vec(0, 5))
})
})
// describe('Vec.NudgeAtVector', () => {
// it('Pushes a point in a given direction vector by a given distance.', () => {
// expect(Vec.NudgeAtVector(new Vec(0, 0), new Vec(0.5, 0.75), 10)).toEqual(
// new Vec(5, 7.5)
// )
// })
// })
// describe('Vec.NudgeAtAngle', () => {
// it('Pushes a point in a given angle by a given distance.', () => {
// expect(Vec.NudgeAtAngle(new Vec(0, 0), Math.PI / 8, 10)).toEqual(
// new Vec(9.238795325112868, 3.826834323650898)
// )
// })
// })
// describe('Vec.PointsBetween', () => {
// it('Interpolates points between two points.', () => {
// expect(Vec.PointsBetween(new Vec(0, 0), [100, 100], 10)).toMatchObject(new Vec2)(
// new Vec(0, 0, 1),
// new Vec(11.11111111111111, 11.11111111111111, 0.8888888888888888),
// new Vec(22.22222222222222, 22.22222222222222, 0.7777777777777778),
// new Vec(33.33333333333333, 33.33333333333333, 0.6666666666666667),
// new Vec(44.44444444444444, 44.44444444444444, 0.5555555555555556),
// new Vec(55.55555555555556, 55.55555555555556, 0.5555555555555556),
// new Vec(66.66666666666666, 66.66666666666666, 0.6666666666666666),
// new Vec(77.77777777777779, 77.77777777777779, 0.7777777777777778),
// new Vec(88.88888888888889, 88.88888888888889, 0.8888888888888888),
// new Vec(100, 100, 1),
// ])
// })
// })
describe('Vec.Slope', () => {
it('Gets a slope from a vector.', () => {
expect(Vec.Slope(new Vec(0, 0), new Vec(100, 100))).toEqual(1)
expect(Vec.Slope(new Vec(0, 0), new Vec(50, 100))).toEqual(2)
expect(Vec.Slope(new Vec(0, 0), new Vec(-50, 100))).toEqual(-2)
expect(Vec.Slope(new Vec(123, 456), new Vec(789, 24))).toEqual(-0.6486486486486487)
})
})
describe('Vec.ToAngle', () => {
it('Gets an angle from a vector.', () => {
expect(Vec.ToAngle(new Vec(1, 0.5))).toEqual(0.4636476090008061)
})
})
describe('Vec.Max', () => {
it('Gets the minimum of the given vectors', () => {
expect(Vec.Max(new Vec(4, 1), new Vec(3, 2))).toMatchObject(new Vec(4, 2))
expect(Vec.Max(new Vec(3, 2), new Vec(4, 1))).toMatchObject(new Vec(4, 2))
})
})
describe('Vec.Min', () => {
it('Gets the minimum of the given vectors', () => {
expect(Vec.Min(new Vec(4, 1), new Vec(3, 2))).toMatchObject(new Vec(3, 1))
expect(Vec.Min(new Vec(3, 2), new Vec(4, 1))).toMatchObject(new Vec(3, 1))
})
})
describe('Vec.snapToGrid', () => {
it('snaps to the nearest given increment, mutating the original vector and returning it', () => {
expect(new Vec(25, 29).snapToGrid(8)).toMatchObject(new Vec(24, 32))
expect(new Vec(25, 29).snapToGrid(8)).toMatchObject(new Vec(24, 32))
expect(new Vec(25, 29).snapToGrid(3)).toMatchObject(new Vec(24, 30))
expect(new Vec(25, 29).snapToGrid(10)).toMatchObject(new Vec(30, 30))
expect(new Vec(12, 49).snapToGrid(10)).toMatchObject(new Vec(10, 50))
expect(Vec.SnapToGrid(new Vec(25, 29))).toMatchObject(new Vec(24, 32))
expect(Vec.SnapToGrid(new Vec(25, 29), 8)).toMatchObject(new Vec(24, 32))
expect(Vec.SnapToGrid(new Vec(25, 29), 3)).toMatchObject(new Vec(24, 30))
expect(Vec.SnapToGrid(new Vec(25, 29), 10)).toMatchObject(new Vec(30, 30))
expect(Vec.SnapToGrid(new Vec(12, 49), 10)).toMatchObject(new Vec(10, 50))
})
})

View file

@ -1,11 +1,11 @@
import { Vec2dModel } from '@tldraw/tlschema'
import { VecModel } from '@tldraw/tlschema'
import { EASINGS } from './easings'
/** @public */
export type VecLike = Vec2d | Vec2dModel
export type VecLike = Vec | VecModel
/** @public */
export class Vec2d {
export class Vec {
constructor(public x = 0, public y = 0, public z = 1) {}
// eslint-disable-next-line no-restricted-syntax
@ -48,9 +48,9 @@ export class Vec2d {
return this
}
clone(): Vec2d {
clone(): Vec {
const { x, y, z } = this
return new Vec2d(x, y, z)
return new Vec(x, y, z)
}
sub(V: VecLike) {
@ -138,7 +138,7 @@ export class Vec2d {
}
nudge(B: VecLike, distance: number) {
const tan = Vec2d.Tan(B, this)
const tan = Vec.Tan(B, this)
return this.add(tan.mul(distance))
}
@ -157,23 +157,23 @@ export class Vec2d {
}
dpr(V: VecLike): number {
return Vec2d.Dpr(this, V)
return Vec.Dpr(this, V)
}
cpr(V: VecLike) {
return Vec2d.Cpr(this, V)
return Vec.Cpr(this, V)
}
len2(): number {
return Vec2d.Len2(this)
return Vec.Len2(this)
}
len(): number {
return Vec2d.Len(this)
return Vec.Len(this)
}
pry(V: VecLike): number {
return Vec2d.Pry(this, V)
return Vec.Pry(this, V)
}
per() {
@ -184,23 +184,23 @@ export class Vec2d {
}
uni() {
return Vec2d.Uni(this)
return Vec.Uni(this)
}
tan(V: VecLike): Vec2d {
return Vec2d.Tan(this, V)
tan(V: VecLike): Vec {
return Vec.Tan(this, V)
}
dist(V: VecLike): number {
return Vec2d.Dist(this, V)
return Vec.Dist(this, V)
}
distanceToLineSegment(A: VecLike, B: VecLike): number {
return Vec2d.DistanceToLineSegment(A, B, this)
return Vec.DistanceToLineSegment(A, B, this)
}
slope(B: VecLike): number {
return Vec2d.Slope(this, B)
return Vec.Slope(this, B)
}
snapToGrid(gridSize: number) {
@ -210,25 +210,25 @@ export class Vec2d {
}
angle(B: VecLike): number {
return Vec2d.Angle(this, B)
return Vec.Angle(this, B)
}
toAngle() {
return Vec2d.ToAngle(this)
return Vec.ToAngle(this)
}
lrp(B: VecLike, t: number): Vec2d {
lrp(B: VecLike, t: number): Vec {
this.x = this.x + (B.x - this.x) * t
this.y = this.y + (B.y - this.y) * t
return this
}
equals(B: VecLike) {
return Vec2d.Equals(this, B)
return Vec.Equals(this, B)
}
equalsXY(x: number, y: number) {
return Vec2d.EqualsXY(this, x, y)
return Vec.EqualsXY(this, x, y)
}
norm() {
@ -239,75 +239,75 @@ export class Vec2d {
}
toFixed() {
return Vec2d.ToFixed(this)
return Vec.ToFixed(this)
}
toString() {
return Vec2d.ToString(Vec2d.ToFixed(this))
return Vec.ToString(Vec.ToFixed(this))
}
toJson(): Vec2dModel {
return Vec2d.ToJson(this)
toJson(): VecModel {
return Vec.ToJson(this)
}
toArray(): number[] {
return Vec2d.ToArray(this)
return Vec.ToArray(this)
}
static Add(A: VecLike, B: VecLike): Vec2d {
return new Vec2d(A.x + B.x, A.y + B.y)
static Add(A: VecLike, B: VecLike): Vec {
return new Vec(A.x + B.x, A.y + B.y)
}
static AddXY(A: VecLike, x: number, y: number): Vec2d {
return new Vec2d(A.x + x, A.y + y)
static AddXY(A: VecLike, x: number, y: number): Vec {
return new Vec(A.x + x, A.y + y)
}
static Sub(A: VecLike, B: VecLike): Vec2d {
return new Vec2d(A.x - B.x, A.y - B.y)
static Sub(A: VecLike, B: VecLike): Vec {
return new Vec(A.x - B.x, A.y - B.y)
}
static SubXY(A: VecLike, x: number, y: number): Vec2d {
return new Vec2d(A.x - x, A.y - y)
static SubXY(A: VecLike, x: number, y: number): Vec {
return new Vec(A.x - x, A.y - y)
}
static AddScalar(A: VecLike, n: number): Vec2d {
return new Vec2d(A.x + n, A.y + n)
static AddScalar(A: VecLike, n: number): Vec {
return new Vec(A.x + n, A.y + n)
}
static SubScalar(A: VecLike, n: number): Vec2d {
return new Vec2d(A.x - n, A.y - n)
static SubScalar(A: VecLike, n: number): Vec {
return new Vec(A.x - n, A.y - n)
}
static Div(A: VecLike, t: number): Vec2d {
return new Vec2d(A.x / t, A.y / t)
static Div(A: VecLike, t: number): Vec {
return new Vec(A.x / t, A.y / t)
}
static Mul(A: VecLike, t: number): Vec2d {
return new Vec2d(A.x * t, A.y * t)
static Mul(A: VecLike, t: number): Vec {
return new Vec(A.x * t, A.y * t)
}
static DivV(A: VecLike, B: VecLike): Vec2d {
return new Vec2d(A.x / B.x, A.y / B.y)
static DivV(A: VecLike, B: VecLike): Vec {
return new Vec(A.x / B.x, A.y / B.y)
}
static MulV(A: VecLike, B: VecLike): Vec2d {
return new Vec2d(A.x * B.x, A.y * B.y)
static MulV(A: VecLike, B: VecLike): Vec {
return new Vec(A.x * B.x, A.y * B.y)
}
static Neg(A: VecLike): Vec2d {
return new Vec2d(-A.x, -A.y)
static Neg(A: VecLike): Vec {
return new Vec(-A.x, -A.y)
}
static Per(A: VecLike): Vec2d {
return new Vec2d(A.y, -A.x)
static Per(A: VecLike): Vec {
return new Vec(A.y, -A.x)
}
static Dist2(A: VecLike, B: VecLike): number {
return Vec2d.Sub(A, B).len2()
return Vec.Sub(A, B).len2()
}
static Abs(A: VecLike): Vec2d {
return new Vec2d(Math.abs(A.x), Math.abs(A.y))
static Abs(A: VecLike): Vec {
return new Vec(Math.abs(A.x), Math.abs(A.y))
}
static Dist(A: VecLike, B: VecLike): number {
@ -319,7 +319,7 @@ export class Vec2d {
}
static Cross(A: VecLike, V: VecLike) {
return new Vec2d(
return new Vec(
A.y * V.z! - A.z! * V.y,
A.z! * V.x - A.x * V.z!
// A.z = A.x * V.y - A.y * V.x
@ -339,45 +339,45 @@ export class Vec2d {
}
static Pry(A: VecLike, B: VecLike): number {
return Vec2d.Dpr(A, B) / Vec2d.Len(B)
return Vec.Dpr(A, B) / Vec.Len(B)
}
static Uni(A: VecLike) {
return Vec2d.Div(A, Vec2d.Len(A))
return Vec.Div(A, Vec.Len(A))
}
static Tan(A: VecLike, B: VecLike): Vec2d {
return Vec2d.Uni(Vec2d.Sub(A, B))
static Tan(A: VecLike, B: VecLike): Vec {
return Vec.Uni(Vec.Sub(A, B))
}
static Min(A: VecLike, B: VecLike): Vec2d {
return new Vec2d(Math.min(A.x, B.x), Math.min(A.y, B.y))
static Min(A: VecLike, B: VecLike): Vec {
return new Vec(Math.min(A.x, B.x), Math.min(A.y, B.y))
}
static Max(A: VecLike, B: VecLike): Vec2d {
return new Vec2d(Math.max(A.x, B.x), Math.max(A.y, B.y))
static Max(A: VecLike, B: VecLike): Vec {
return new Vec(Math.max(A.x, B.x), Math.max(A.y, B.y))
}
static From({ x, y, z = 1 }: Vec2dModel) {
return new Vec2d(x, y, z)
static From({ x, y, z = 1 }: VecModel) {
return new Vec(x, y, z)
}
static FromArray(v: number[]): Vec2d {
return new Vec2d(v[0], v[1])
static FromArray(v: number[]): Vec {
return new Vec(v[0], v[1])
}
static Rot(A: VecLike, r = 0): Vec2d {
static Rot(A: VecLike, r = 0): Vec {
const s = Math.sin(r)
const c = Math.cos(r)
return new Vec2d(A.x * c - A.y * s, A.x * s + A.y * c)
return new Vec(A.x * c - A.y * s, A.x * s + A.y * c)
}
static RotWith(A: VecLike, C: VecLike, r: number): Vec2d {
static RotWith(A: VecLike, C: VecLike, r: number): Vec {
const x = A.x - C.x
const y = A.y - C.y
const s = Math.sin(r)
const c = Math.cos(r)
return new Vec2d(C.x + (x * c - y * s), C.y + (x * s + y * c))
return new Vec(C.x + (x * c - y * s), C.y + (x * s + y * c))
}
/**
@ -391,41 +391,41 @@ export class Vec2d {
* @param u - The unit vector for the line.
* @param P - A point not on the line to test.
*/
static NearestPointOnLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): Vec2d {
return Vec2d.Mul(u, Vec2d.Sub(P, A).pry(u)).add(A)
static NearestPointOnLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): Vec {
return Vec.Mul(u, Vec.Sub(P, A).pry(u)).add(A)
}
static NearestPointOnLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp = true): Vec2d {
const u = Vec2d.Tan(B, A)
const C = Vec2d.Add(A, Vec2d.Mul(u, Vec2d.Sub(P, A).pry(u)))
static NearestPointOnLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp = true): Vec {
const u = Vec.Tan(B, A)
const C = Vec.Add(A, Vec.Mul(u, Vec.Sub(P, A).pry(u)))
// todo: fix error P is B or A, which leads to a NaN value
if (clamp) {
if (C.x < Math.min(A.x, B.x)) return Vec2d.Cast(A.x < B.x ? A : B)
if (C.x > Math.max(A.x, B.x)) return Vec2d.Cast(A.x > B.x ? A : B)
if (C.y < Math.min(A.y, B.y)) return Vec2d.Cast(A.y < B.y ? A : B)
if (C.y > Math.max(A.y, B.y)) return Vec2d.Cast(A.y > B.y ? A : B)
if (C.x < Math.min(A.x, B.x)) return Vec.Cast(A.x < B.x ? A : B)
if (C.x > Math.max(A.x, B.x)) return Vec.Cast(A.x > B.x ? A : B)
if (C.y < Math.min(A.y, B.y)) return Vec.Cast(A.y < B.y ? A : B)
if (C.y > Math.max(A.y, B.y)) return Vec.Cast(A.y > B.y ? A : B)
}
return C
}
static DistanceToLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): number {
return Vec2d.Dist(P, Vec2d.NearestPointOnLineThroughPoint(A, u, P))
return Vec.Dist(P, Vec.NearestPointOnLineThroughPoint(A, u, P))
}
static DistanceToLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp = true): number {
return Vec2d.Dist(P, Vec2d.NearestPointOnLineSegment(A, B, P, clamp))
return Vec.Dist(P, Vec.NearestPointOnLineSegment(A, B, P, clamp))
}
static Snap(A: VecLike, step = 1) {
return new Vec2d(Math.round(A.x / step) * step, Math.round(A.y / step) * step)
return new Vec(Math.round(A.x / step) * step, Math.round(A.y / step) * step)
}
static Cast(A: VecLike): Vec2d {
if (A instanceof Vec2d) return A
return Vec2d.From(A)
static Cast(A: VecLike): Vec {
if (A instanceof Vec) return A
return Vec.From(A)
}
static Slope(A: VecLike, B: VecLike): number {
@ -437,12 +437,12 @@ export class Vec2d {
return Math.atan2(B.y - A.y, B.x - A.x)
}
static Lrp(A: VecLike, B: VecLike, t: number): Vec2d {
return Vec2d.Sub(B, A).mul(t).add(A)
static Lrp(A: VecLike, B: VecLike, t: number): Vec {
return Vec.Sub(B, A).mul(t).add(A)
}
static Med(A: VecLike, B: VecLike): Vec2d {
return new Vec2d((A.x + B.x) / 2, (A.y + B.y) / 2)
static Med(A: VecLike, B: VecLike): Vec {
return new Vec((A.x + B.x) / 2, (A.y + B.y) / 2)
}
static Equals(A: VecLike, B: VecLike): boolean {
@ -458,20 +458,20 @@ export class Vec2d {
}
static Rescale(A: VecLike, n: number) {
const l = Vec2d.Len(A)
return new Vec2d((n * A.x) / l, (n * A.y) / l)
const l = Vec.Len(A)
return new Vec((n * A.x) / l, (n * A.y) / l)
}
static ScaleWithOrigin(A: VecLike, scale: number, origin: VecLike) {
return Vec2d.Sub(A, origin).mul(scale).add(origin)
return Vec.Sub(A, origin).mul(scale).add(origin)
}
static ToFixed(A: VecLike, n = 2) {
return new Vec2d(+A.x.toFixed(n), +A.y.toFixed(n), +A.z!.toFixed(n))
return new Vec(+A.x.toFixed(n), +A.y.toFixed(n), +A.z!.toFixed(n))
}
static Nudge(A: VecLike, B: VecLike, distance: number) {
return Vec2d.Add(A, Vec2d.Tan(B, A).mul(distance))
return Vec.Add(A, Vec.Tan(B, A).mul(distance))
}
static ToString(A: VecLike) {
@ -486,7 +486,7 @@ export class Vec2d {
}
static FromAngle(r: number, length = 1) {
return new Vec2d(Math.cos(r) * length, Math.sin(r) * length)
return new Vec(Math.cos(r) * length, Math.sin(r) * length)
}
static ToArray(A: VecLike) {
@ -500,19 +500,19 @@ export class Vec2d {
static Average(arr: VecLike[]) {
const len = arr.length
const avg = new Vec2d(0, 0)
const avg = new Vec(0, 0)
for (let i = 0; i < len; i++) {
avg.add(arr[i])
}
return avg.div(len)
}
static Clamp(A: Vec2d, min: number, max?: number) {
static Clamp(A: Vec, min: number, max?: number) {
if (max === undefined) {
return new Vec2d(Math.min(Math.max(A.x, min)), Math.min(Math.max(A.y, min)))
return new Vec(Math.min(Math.max(A.x, min)), Math.min(Math.max(A.y, min)))
}
return new Vec2d(Math.min(Math.max(A.x, min), max), Math.min(Math.max(A.y, min), max))
return new Vec(Math.min(Math.max(A.x, min), max), Math.min(Math.max(A.y, min), max))
}
/**
@ -522,12 +522,12 @@ export class Vec2d {
* @param B - The second point.
* @param steps - The number of points to return.
*/
static PointsBetween(A: Vec2dModel, B: Vec2dModel, steps = 6): Vec2d[] {
const results: Vec2d[] = []
static PointsBetween(A: VecModel, B: VecModel, steps = 6): Vec[] {
const results: Vec[] = []
for (let i = 0; i < steps; i++) {
const t = EASINGS.easeInQuad(i / (steps - 1))
const point = Vec2d.Lrp(A, B, t)
const point = Vec.Lrp(A, B, t)
point.z = Math.min(1, 0.5 + Math.abs(0.5 - ease(t)) * 0.65)
results.push(point)
}
@ -536,7 +536,7 @@ export class Vec2d {
}
static SnapToGrid(A: VecLike, gridSize = 8) {
return new Vec2d(Math.round(A.x / gridSize) * gridSize, Math.round(A.y / gridSize) * gridSize)
return new Vec(Math.round(A.x / gridSize) * gridSize, Math.round(A.y / gridSize) * gridSize)
}
}

View file

@ -1,379 +0,0 @@
import { Vec2d } from './Vec2d'
describe('iteratable', () => {
it('Constructs', () => {
const v = new Vec2d(1, 2)
const { x, y } = v
expect(x).toBeCloseTo(1)
expect(y).toBeCloseTo(2)
})
})
describe('Vec2d.Clamp', () => {
it('Clamps a vector between a range.', () => {
expect(Vec2d.Clamp(new Vec2d(9, 5), 7, 10)).toMatchObject(new Vec2d(9, 7))
expect(Vec2d.Clamp(new Vec2d(-9, 5), 0, 10)).toMatchObject(new Vec2d(0, 5))
})
})
describe('Vec2d.Clamp', () => {
it('Clamps a vector between a range.', () => {
expect(Vec2d.Clamp(new Vec2d(9, 5), 7, 10)).toMatchObject(new Vec2d(9, 7))
expect(Vec2d.Clamp(new Vec2d(-9, 5), 0, 10)).toMatchObject(new Vec2d(0, 5))
})
it('Clamps a vector between a range.', () => {
expect(Vec2d.Clamp(new Vec2d(9, 5), 10)).toMatchObject(new Vec2d(10, 10))
expect(Vec2d.Clamp(new Vec2d(-9, 5), 10)).toMatchObject(new Vec2d(10, 10))
})
})
describe('Vec2d.Neg', () => {
it('Negates a vector.', () => {
expect(Vec2d.Neg(new Vec2d(9, 5))).toMatchObject(new Vec2d(-9, -5))
expect(Vec2d.Neg(new Vec2d(-9, 0))).toMatchObject(new Vec2d(9, -0))
})
})
describe('Vec2d.Add', () => {
it('Adds two vectors.', () => {
expect(Vec2d.Add(new Vec2d(9, 5), new Vec2d(2, 1))).toMatchObject(new Vec2d(11, 6))
expect(Vec2d.Add(new Vec2d(-9, 5), new Vec2d(2, -1))).toMatchObject(new Vec2d(-7, 4))
})
})
describe('Vec2d.AddScalar', () => {
it('Adds a scalar to a vector.', () => {
expect(Vec2d.AddScalar(new Vec2d(9, 5), 2)).toMatchObject(new Vec2d(11, 7))
expect(Vec2d.AddScalar(new Vec2d(-9, 5), 2)).toMatchObject(new Vec2d(-7, 7))
})
})
describe('Vec2d.Sub', () => {
it('Subtracts two vectors.', () => {
expect(Vec2d.Sub(new Vec2d(9, 5), new Vec2d(2, 1))).toMatchObject(new Vec2d(7, 4))
expect(Vec2d.Sub(new Vec2d(-9, 5), new Vec2d(2, -1))).toMatchObject(new Vec2d(-11, 6))
})
})
describe('Vec2d.SubScalar', () => {
it('Subtracts a scalar from a vector.', () => {
expect(Vec2d.SubScalar(new Vec2d(9, 5), 2)).toMatchObject(new Vec2d(7, 3))
expect(Vec2d.SubScalar(new Vec2d(-9, 5), 2)).toMatchObject(new Vec2d(-11, 3))
})
})
describe('Vec2d.Mul', () => {
it('Get a vector multiplied by a scalar.', () => {
expect(Vec2d.Mul(new Vec2d(9, 9), 3)).toMatchObject(new Vec2d(27, 27))
expect(Vec2d.Mul(new Vec2d(10, 10), 2)).toMatchObject(new Vec2d(20, 20))
})
})
describe('Vec2d.DivV', () => {
it('Get a vector multiplied by a vector.', () => {
expect(Vec2d.MulV(new Vec2d(16, 12), new Vec2d(2, 4))).toMatchObject(new Vec2d(32, 48))
expect(Vec2d.MulV(new Vec2d(5, 15), new Vec2d(5, 3))).toMatchObject(new Vec2d(25, 45))
})
})
describe('Vec2d.Div', () => {
it('Get a vector divided by a scalar.', () => {
expect(Vec2d.Div(new Vec2d(9, 9), 3)).toMatchObject(new Vec2d(3, 3))
expect(Vec2d.Div(new Vec2d(10, 10), 2)).toMatchObject(new Vec2d(5, 5))
})
})
describe('Vec2d.DivV', () => {
it('Get a vector divided by a vector.', () => {
expect(Vec2d.DivV(new Vec2d(16, 12), new Vec2d(2, 4))).toMatchObject(new Vec2d(8, 3))
expect(Vec2d.DivV(new Vec2d(5, 15), new Vec2d(5, 3))).toMatchObject(new Vec2d(1, 5))
})
})
describe('Vec2d.Per', () => {
it('Gets the perpendicular rotation of a vector.', () => {
expect(Vec2d.Per(new Vec2d(1, -1))).toMatchObject(new Vec2d(-1, -1))
expect(Vec2d.Per(new Vec2d(-1, 1))).toMatchObject(new Vec2d(1, 1))
})
})
describe('Vec2d.Dpr', () => {
it('Gets the dot product of two vectors.', () => {
expect(Vec2d.Dpr(new Vec2d(1, 0), new Vec2d(1, 0))).toEqual(1)
expect(Vec2d.Dpr(new Vec2d(1, 0), new Vec2d(0, 0))).toEqual(0)
expect(Vec2d.Dpr(new Vec2d(1, 0), new Vec2d(-1, 0))).toEqual(-1)
})
})
describe('Vec2d.Cpr', () => {
it('Gets the cross product (outer product) of two vectors.', () => {
expect(Vec2d.Cpr(new Vec2d(0, 1), new Vec2d(1, 1))).toEqual(-1)
expect(Vec2d.Cpr(new Vec2d(1, 1), new Vec2d(1, 1))).toEqual(0)
expect(Vec2d.Cpr(new Vec2d(1, 1), new Vec2d(0, 1))).toEqual(1)
})
})
describe('Vec2d.Len2', () => {
it('Gets the length of a vector squared.', () => {
expect(Vec2d.Len2(new Vec2d(0, 0))).toEqual(0)
expect(Vec2d.Len2(new Vec2d(0, 1))).toEqual(1)
expect(Vec2d.Len2(new Vec2d(1, 1))).toEqual(2)
})
})
describe('Vec2d.Len', () => {
it('Gets the length of a vector.', () => {
expect(Vec2d.Len(new Vec2d(0, 0))).toEqual(0)
expect(Vec2d.Len(new Vec2d(0, 1))).toEqual(1)
expect(Vec2d.Len(new Vec2d(1, 1))).toEqual(1.4142135623730951)
})
})
describe('Vec2d.Pry', () => {
it('Projects a vector A over vector B.', () => {
expect(Vec2d.Pry(new Vec2d(0, 0), new Vec2d(0, 10))).toEqual(0)
expect(Vec2d.Pry(new Vec2d(0, 0), new Vec2d(10, 10))).toEqual(0)
expect(Vec2d.Pry(new Vec2d(10, 10), new Vec2d(0, 10))).toEqual(10)
expect(Vec2d.Pry(new Vec2d(10, 10), new Vec2d(10, 10))).toEqual(14.14213562373095)
})
})
describe('Vec2d.Uni', () => {
it('Gets the normalized vector.', () => {
expect(Vec2d.Uni(new Vec2d(0, 10))).toMatchObject(new Vec2d(0, 1))
expect(Vec2d.Uni(new Vec2d(10, 10))).toMatchObject(
new Vec2d(0.7071067811865475, 0.7071067811865475)
)
})
})
describe('Vec2d.Tan', () => {
it('Gets the tangent between two vectors.', () => {
expect(Vec2d.Tan(new Vec2d(0, 0), new Vec2d(0, 10))).toMatchObject(new Vec2d(0, -1))
expect(Vec2d.Tan(new Vec2d(0, 0), new Vec2d(10, 10))).toMatchObject(
new Vec2d(-0.7071067811865475, -0.7071067811865475)
)
})
})
describe('Vec2d.Dist2', () => {
it('Finds the squared distance between two points.', () => {
expect(Vec2d.Dist2(new Vec2d(0, 0), new Vec2d(0, 10))).toEqual(100)
expect(Vec2d.Dist2(new Vec2d(0, 0), new Vec2d(10, 10))).toEqual(200)
})
})
describe('Vec2d.Dist', () => {
it('Finds the distance between two points.', () => {
expect(Vec2d.Dist(new Vec2d(0, 0), new Vec2d(0, 10))).toEqual(10)
expect(Vec2d.Dist(new Vec2d(0, 0), new Vec2d(10, 10))).toEqual(14.142135623730951)
})
})
// describe('Vec2d.Ang2', () => {
// it('Finds the angle in radians between two vectors.', () => {
// expect(Vec2d.Ang2(new Vec2d(1, 0), new Vec2d(0, 1))).toEqual(Math.PI / 2)
// })
// })
// describe('Vec2d.Ang3', () => {
// it('Gets the angle of ∠ABC', () => {
// expect(Vec2d.Ang3([5, 0], new Vec2d(0, 0), new Vec2d(0, 5))).toEqual(Math.PI / 2)
// expect(Vec2d.Ang3(new Vec2d(1, 0), new Vec2d(0, 0), new Vec2d(0, 1))).toEqual(Math.PI / 2)
// })
// })
describe('Vec2d.Angle', () => {
it('Finds the angle in radians between two points.', () => {
expect(Vec2d.Angle(new Vec2d(0, 0), new Vec2d(10, 10))).toEqual(Math.PI / 4)
expect(Vec2d.Angle(new Vec2d(0, 0), new Vec2d(10, 0))).toEqual(0)
expect(Vec2d.Angle(new Vec2d(0, 0), new Vec2d(0, 10))).toEqual(Math.PI / 2)
})
})
describe('Vec2d.Med', () => {
it('Finds the midpoint between two vectors.', () => {
expect(Vec2d.Med(new Vec2d(0, 0), new Vec2d(10, 10))).toMatchObject(new Vec2d(5, 5))
expect(Vec2d.Med(new Vec2d(0, 0), new Vec2d(10, 0))).toMatchObject(new Vec2d(5, 0))
expect(Vec2d.Med(new Vec2d(0, 0), new Vec2d(0, 10))).toMatchObject(new Vec2d(0, 5))
expect(Vec2d.Med(new Vec2d(-100, 0), new Vec2d(0, 100))).toMatchObject(new Vec2d(-50, 50))
})
})
describe('Vec2d.Rot', () => {
it('Rotates a vector by a rotation in radians.', () => {
const { x, y } = Vec2d.Rot(new Vec2d(1, 0), Math.PI / 4)
expect(x).toBeCloseTo(0.7, 1)
expect(y).toBeCloseTo(0.7, 1)
})
})
describe('Vec2d.RotWith', () => {
it('Rotates a vector around a second vector by a rotation in radians.', () => {
expect(Vec2d.RotWith(new Vec2d(1, 0), new Vec2d(0, 0), Math.PI / 4)).toMatchObject(
new Vec2d(0.7071067811865476, 0.7071067811865475)
)
})
})
describe('Vec2d.Equals', () => {
it('Gets whether two vectors are identical.', () => {
expect(Vec2d.Equals(new Vec2d(1, 2), new Vec2d(1, 2))).toEqual(true)
expect(Vec2d.Equals(new Vec2d(1, 2), new Vec2d(1, 3))).toEqual(false)
expect(Vec2d.Equals(new Vec2d(-0, 2), new Vec2d(0, 2))).toEqual(true)
})
})
describe('Vec2d.Int', () => {
it('Interpolate from A to B', () => {
expect(Vec2d.Lrp(new Vec2d(0, 0), new Vec2d(10, 10), 0.5)).toMatchObject(new Vec2d(5, 5))
expect(Vec2d.Lrp(new Vec2d(0, 0), new Vec2d(10, 10), 2)).toMatchObject(new Vec2d(20, 20))
})
})
describe('Vec2d.Rescale', () => {
it('Rescales a vector by a scalar', () => {
expect(Vec2d.Rescale(new Vec2d(5, 0), 1)).toMatchObject(new Vec2d(1, 0))
expect(Vec2d.Rescale(new Vec2d(5, 0), 2)).toMatchObject(new Vec2d(2, 0))
expect(Vec2d.Rescale(new Vec2d(0.5, 0.25), 2)).toEqual(
new Vec2d(1.7888543819998317, 0.8944271909999159)
)
})
})
describe('Vec2d.IsClockwise', () => {
it('Gets whether point A and point B wind clockwise around point C.', () => {
expect(Vec2d.Clockwise(new Vec2d(0, 0), new Vec2d(5, 5), new Vec2d(0, 5))).toEqual(true)
expect(Vec2d.Clockwise(new Vec2d(5, 5), new Vec2d(0, 0), new Vec2d(0, 5))).toEqual(false)
expect(Vec2d.Clockwise(new Vec2d(0, 10), new Vec2d(0, 0), new Vec2d(0, 5))).toEqual(false)
})
})
describe('Vec2d.ToFixed', () => {
it('Rounds a vector to the a given precision.', () => {
expect(Vec2d.ToFixed(new Vec2d(1.2345, 5.678), 1)).toMatchObject(new Vec2d(1.2, 5.7))
expect(Vec2d.ToFixed(new Vec2d(1.2345, 5.678), 2)).toMatchObject(new Vec2d(1.23, 5.68))
})
})
describe('Vec2d.Snap', () => {
it('Snaps a vector to the nearest increment provided.', () => {
expect(Vec2d.Snap(new Vec2d(10.5, 28), 10)).toMatchObject(new Vec2d(10, 30))
})
})
describe('Vec2d.NearestPointOnLineThroughPoint', () => {
it('Gets the distance from a point to a line that passes through a given point.', () => {
expect(
Vec2d.NearestPointOnLineThroughPoint(new Vec2d(0, 0), new Vec2d(0, 1), new Vec2d(5, 5))
).toMatchObject(new Vec2d(0, 5))
})
})
describe('Vec2d.DistanceToLineThroughPoint', () => {
it('Gets the distance from a point to a line that passes through a given point.', () => {
expect(
Vec2d.DistanceToLineThroughPoint(new Vec2d(0, 0), new Vec2d(0, 1), new Vec2d(5, 5))
).toEqual(5)
})
})
describe('Vec2d.NearestPointOnLineSegment', () => {
it('Gets the distance from a point to a line segment.', () => {
expect(
Vec2d.NearestPointOnLineSegment(new Vec2d(0, 0), new Vec2d(0, 10), new Vec2d(5, 5))
).toMatchObject(new Vec2d(0, 5))
})
})
describe('Vec2d.DistanceToLineSegment', () => {
it('Gets the distance from a point to a line segment.', () => {
expect(Vec2d.DistanceToLineSegment(new Vec2d(0, 0), new Vec2d(0, 10), new Vec2d(5, 5))).toEqual(
5
)
})
})
describe('Vec2d.Nudge', () => {
it('Pushes a point towards another point by a given distance.', () => {
expect(Vec2d.Nudge(new Vec2d(0, 0), new Vec2d(0, 10), 5)).toMatchObject(new Vec2d(0, 5))
})
})
// describe('Vec2d.NudgeAtVector', () => {
// it('Pushes a point in a given direction vector by a given distance.', () => {
// expect(Vec2d.NudgeAtVector(new Vec2d(0, 0), new Vec2d(0.5, 0.75), 10)).toEqual(
// new Vec2d(5, 7.5)
// )
// })
// })
// describe('Vec2d.NudgeAtAngle', () => {
// it('Pushes a point in a given angle by a given distance.', () => {
// expect(Vec2d.NudgeAtAngle(new Vec2d(0, 0), Math.PI / 8, 10)).toEqual(
// new Vec2d(9.238795325112868, 3.826834323650898)
// )
// })
// })
// describe('Vec2d.PointsBetween', () => {
// it('Interpolates points between two points.', () => {
// expect(Vec2d.PointsBetween(new Vec2d(0, 0), [100, 100], 10)).toMatchObject(new Vec2)(
// new Vec2d(0, 0, 1),
// new Vec2d(11.11111111111111, 11.11111111111111, 0.8888888888888888),
// new Vec2d(22.22222222222222, 22.22222222222222, 0.7777777777777778),
// new Vec2d(33.33333333333333, 33.33333333333333, 0.6666666666666667),
// new Vec2d(44.44444444444444, 44.44444444444444, 0.5555555555555556),
// new Vec2d(55.55555555555556, 55.55555555555556, 0.5555555555555556),
// new Vec2d(66.66666666666666, 66.66666666666666, 0.6666666666666666),
// new Vec2d(77.77777777777779, 77.77777777777779, 0.7777777777777778),
// new Vec2d(88.88888888888889, 88.88888888888889, 0.8888888888888888),
// new Vec2d(100, 100, 1),
// ])
// })
// })
describe('Vec2d.Slope', () => {
it('Gets a slope from a vector.', () => {
expect(Vec2d.Slope(new Vec2d(0, 0), new Vec2d(100, 100))).toEqual(1)
expect(Vec2d.Slope(new Vec2d(0, 0), new Vec2d(50, 100))).toEqual(2)
expect(Vec2d.Slope(new Vec2d(0, 0), new Vec2d(-50, 100))).toEqual(-2)
expect(Vec2d.Slope(new Vec2d(123, 456), new Vec2d(789, 24))).toEqual(-0.6486486486486487)
})
})
describe('Vec2d.ToAngle', () => {
it('Gets an angle from a vector.', () => {
expect(Vec2d.ToAngle(new Vec2d(1, 0.5))).toEqual(0.4636476090008061)
})
})
describe('Vec2d.Max', () => {
it('Gets the minimum of the given vectors', () => {
expect(Vec2d.Max(new Vec2d(4, 1), new Vec2d(3, 2))).toMatchObject(new Vec2d(4, 2))
expect(Vec2d.Max(new Vec2d(3, 2), new Vec2d(4, 1))).toMatchObject(new Vec2d(4, 2))
})
})
describe('Vec2d.Min', () => {
it('Gets the minimum of the given vectors', () => {
expect(Vec2d.Min(new Vec2d(4, 1), new Vec2d(3, 2))).toMatchObject(new Vec2d(3, 1))
expect(Vec2d.Min(new Vec2d(3, 2), new Vec2d(4, 1))).toMatchObject(new Vec2d(3, 1))
})
})
describe('Vec2d.snapToGrid', () => {
it('snaps to the nearest given increment, mutating the original vector and returning it', () => {
expect(new Vec2d(25, 29).snapToGrid(8)).toMatchObject(new Vec2d(24, 32))
expect(new Vec2d(25, 29).snapToGrid(8)).toMatchObject(new Vec2d(24, 32))
expect(new Vec2d(25, 29).snapToGrid(3)).toMatchObject(new Vec2d(24, 30))
expect(new Vec2d(25, 29).snapToGrid(10)).toMatchObject(new Vec2d(30, 30))
expect(new Vec2d(12, 49).snapToGrid(10)).toMatchObject(new Vec2d(10, 50))
expect(Vec2d.SnapToGrid(new Vec2d(25, 29))).toMatchObject(new Vec2d(24, 32))
expect(Vec2d.SnapToGrid(new Vec2d(25, 29), 8)).toMatchObject(new Vec2d(24, 32))
expect(Vec2d.SnapToGrid(new Vec2d(25, 29), 3)).toMatchObject(new Vec2d(24, 30))
expect(Vec2d.SnapToGrid(new Vec2d(25, 29), 10)).toMatchObject(new Vec2d(30, 30))
expect(Vec2d.SnapToGrid(new Vec2d(12, 49), 10)).toMatchObject(new Vec2d(10, 50))
})
})

View file

@ -1,4 +1,4 @@
import { Vec2d } from '../Vec2d'
import { Vec } from '../Vec'
import { intersectLineSegmentCircle } from '../intersect'
import { PI, PI2, shortAngleDist } from '../utils'
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
@ -6,10 +6,10 @@ import { getVerticesCountForLength } from './geometry-constants'
/** @public */
export class Arc2d extends Geometry2d {
_center: Vec2d
_center: Vec
radius: number
start: Vec2d
end: Vec2d
start: Vec
end: Vec
measure: number
length: number
@ -18,10 +18,10 @@ export class Arc2d extends Geometry2d {
constructor(
config: Omit<Geometry2dOptions, 'isFilled' | 'isClosed'> & {
center: Vec2d
center: Vec
radius: number
start: Vec2d
end: Vec2d
start: Vec
end: Vec
sweepFlag: number
largeArcFlag: number
}
@ -31,8 +31,8 @@ export class Arc2d extends Geometry2d {
if (start.equals(end)) throw Error(`Arc must have different start and end points.`)
// ensure that the start and end are clockwise
this.angleStart = Vec2d.Angle(center, start)
this.angleEnd = Vec2d.Angle(center, end)
this.angleStart = Vec.Angle(center, start)
this.angleEnd = Vec.Angle(center, end)
this.measure = getArcMeasure(this.angleStart, this.angleEnd, sweepFlag, largeArcFlag)
this.length = this.measure * radius
@ -43,7 +43,7 @@ export class Arc2d extends Geometry2d {
this.radius = radius
}
nearestPoint(point: Vec2d): Vec2d {
nearestPoint(point: Vec): Vec {
const { _center, measure, radius, angleEnd, angleStart, start: A, end: B } = this
const t = getPointInArcT(measure, angleStart, angleEnd, _center.angle(point))
if (t <= 0) return A
@ -53,7 +53,7 @@ export class Arc2d extends Geometry2d {
const P = _center.clone().add(point.clone().sub(_center).uni().mul(radius))
let distance = Infinity
let nearest: Vec2d | undefined
let nearest: Vec | undefined
for (const pt of [A, B, P]) {
if (point.dist(pt) < distance) {
nearest = pt
@ -65,7 +65,7 @@ export class Arc2d extends Geometry2d {
return nearest
}
hitTestLineSegment(A: Vec2d, B: Vec2d, _zoom: number): boolean {
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean {
const { _center, radius, measure, angleStart, angleEnd } = this
const intersection = intersectLineSegmentCircle(A, B, _center, radius)
if (intersection === null) return false
@ -76,14 +76,14 @@ export class Arc2d extends Geometry2d {
})
}
getVertices(): Vec2d[] {
getVertices(): Vec[] {
const { _center, measure, length, radius, angleStart } = this
const vertices: Vec2d[] = []
const vertices: Vec[] = []
for (let i = 0, n = getVerticesCountForLength(Math.abs(length)); i < n + 1; i++) {
const t = (i / n) * measure
const angle = angleStart + t
vertices.push(_center.clone().add(new Vec2d(Math.cos(angle), Math.sin(angle)).mul(radius)))
vertices.push(_center.clone().add(new Vec(Math.cos(angle), Math.sin(angle)).mul(radius)))
}
return vertices

View file

@ -1,4 +1,4 @@
import { Vec2d } from '../Vec2d'
import { Vec } from '../Vec'
import { Circle2d } from './Circle2d'
describe('Circle2d.bounds', () => {
@ -53,7 +53,7 @@ describe('Circle2d.isPointInBounds', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).isPointInBounds(new Vec2d(-2, -2))
}).isPointInBounds(new Vec(-2, -2))
).toBe(false)
})
@ -62,7 +62,7 @@ describe('Circle2d.isPointInBounds', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).isPointInBounds(new Vec2d(5, 5))
}).isPointInBounds(new Vec(5, 5))
).toBe(true)
})
@ -71,7 +71,7 @@ describe('Circle2d.isPointInBounds', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).isPointInBounds(new Vec2d(-2, -2), 2)
}).isPointInBounds(new Vec(-2, -2), 2)
).toBe(true)
})
})
@ -82,7 +82,7 @@ describe('Circle2d.getDistanceToPoint', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).distanceToPoint(new Vec2d(10, -2))
}).distanceToPoint(new Vec(10, -2))
).toBe(2)
})
@ -91,7 +91,7 @@ describe('Circle2d.getDistanceToPoint', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).distanceToPoint(new Vec2d(10, 2))
}).distanceToPoint(new Vec(10, 2))
).toBe(2)
})
@ -100,7 +100,7 @@ describe('Circle2d.getDistanceToPoint', () => {
new Circle2d({
radius: 10,
isFilled: true,
}).distanceToPoint(new Vec2d(10, 2))
}).distanceToPoint(new Vec(10, 2))
).toBe(-2)
})
})
@ -111,7 +111,7 @@ describe('Circle2d.hitTestPoint', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).hitTestPoint(new Vec2d(10, -2), 0)
}).hitTestPoint(new Vec(10, -2), 0)
).toBe(false)
})
@ -120,7 +120,7 @@ describe('Circle2d.hitTestPoint', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).hitTestPoint(new Vec2d(10, 2), 0)
}).hitTestPoint(new Vec(10, 2), 0)
).toBe(false)
})
@ -129,7 +129,7 @@ describe('Circle2d.hitTestPoint', () => {
new Circle2d({
radius: 10,
isFilled: true,
}).hitTestPoint(new Vec2d(10, 2), 0)
}).hitTestPoint(new Vec(10, 2), 0)
).toBe(true)
})
@ -140,7 +140,7 @@ describe('Circle2d.hitTestPoint', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).hitTestPoint(new Vec2d(10, -2), 2)
}).hitTestPoint(new Vec(10, -2), 2)
).toBe(true)
})
@ -149,7 +149,7 @@ describe('Circle2d.hitTestPoint', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).hitTestPoint(new Vec2d(10, 2), 2)
}).hitTestPoint(new Vec(10, 2), 2)
).toBe(true)
})
@ -158,7 +158,7 @@ describe('Circle2d.hitTestPoint', () => {
new Circle2d({
radius: 10,
isFilled: true,
}).hitTestPoint(new Vec2d(10, 2), 2)
}).hitTestPoint(new Vec(10, 2), 2)
).toBe(true)
})
})
@ -169,22 +169,22 @@ describe('Circle2d.nearestPoint', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).nearestPoint(new Vec2d(10, -2))
).toMatchObject(new Vec2d(10, 0))
}).nearestPoint(new Vec(10, -2))
).toMatchObject(new Vec(10, 0))
expect(
new Circle2d({
radius: 10,
isFilled: false,
}).nearestPoint(new Vec2d(10, 2))
).toMatchObject(new Vec2d(10, 0))
}).nearestPoint(new Vec(10, 2))
).toMatchObject(new Vec(10, 0))
expect(
new Circle2d({
radius: 10,
isFilled: false,
}).nearestPoint(new Vec2d(10, 30))
).toMatchObject(new Vec2d(10, 20))
}).nearestPoint(new Vec(10, 30))
).toMatchObject(new Vec(10, 20))
})
})
@ -194,14 +194,14 @@ describe('Circle2d.hitTestLineSegment', () => {
new Circle2d({
radius: 10,
isFilled: false,
}).hitTestLineSegment(new Vec2d(0, -2), new Vec2d(20, -2), 1)
}).hitTestLineSegment(new Vec(0, -2), new Vec(20, -2), 1)
).toBe(false)
expect(
new Circle2d({
radius: 10,
isFilled: false,
}).hitTestLineSegment(new Vec2d(0, 2), new Vec2d(20, 2), 1)
}).hitTestLineSegment(new Vec(0, 2), new Vec(20, 2), 1)
).toBe(true)
})
})

View file

@ -1,5 +1,5 @@
import { Box2d } from '../Box2d'
import { Vec2d } from '../Vec2d'
import { Box } from '../Box'
import { Vec } from '../Vec'
import { intersectLineSegmentCircle } from '../intersect'
import { PI2 } from '../utils'
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
@ -7,7 +7,7 @@ import { getVerticesCountForLength } from './geometry-constants'
/** @public */
export class Circle2d extends Geometry2d {
_center: Vec2d
_center: Vec
radius: number
x: number
y: number
@ -24,32 +24,32 @@ export class Circle2d extends Geometry2d {
const { x = 0, y = 0, radius } = config
this.x = x
this.y = y
this._center = new Vec2d(radius + x, radius + y)
this._center = new Vec(radius + x, radius + y)
this.radius = radius
}
getBounds() {
return new Box2d(this.x, this.y, this.radius * 2, this.radius * 2)
return new Box(this.x, this.y, this.radius * 2, this.radius * 2)
}
getVertices(): Vec2d[] {
getVertices(): Vec[] {
const { _center, radius } = this
const perimeter = PI2 * radius
const vertices: Vec2d[] = []
const vertices: Vec[] = []
for (let i = 0, n = getVerticesCountForLength(perimeter); i < n; i++) {
const angle = (i / n) * PI2
vertices.push(_center.clone().add(Vec2d.FromAngle(angle).mul(radius)))
vertices.push(_center.clone().add(Vec.FromAngle(angle).mul(radius)))
}
return vertices
}
nearestPoint(point: Vec2d): Vec2d {
nearestPoint(point: Vec): Vec {
const { _center, radius } = this
if (_center.equals(point)) return Vec2d.AddXY(_center, radius, 0)
if (_center.equals(point)) return Vec.AddXY(_center, radius, 0)
return _center.clone().add(point.clone().sub(_center).uni().mul(radius))
}
hitTestLineSegment(A: Vec2d, B: Vec2d, _zoom: number): boolean {
hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean {
const { _center, radius } = this
return intersectLineSegmentCircle(A, B, _center, radius) !== null
}

View file

@ -1,20 +1,20 @@
import { Vec2d } from '../Vec2d'
import { Vec } from '../Vec'
import { Geometry2dOptions } from './Geometry2d'
import { Polyline2d } from './Polyline2d'
/** @public */
export class CubicBezier2d extends Polyline2d {
a: Vec2d
b: Vec2d
c: Vec2d
d: Vec2d
a: Vec
b: Vec
c: Vec
d: Vec
constructor(
config: Omit<Geometry2dOptions, 'isFilled' | 'isClosed'> & {
start: Vec2d
cp1: Vec2d
cp2: Vec2d
end: Vec2d
start: Vec
cp1: Vec
cp2: Vec
end: Vec
}
) {
const { start: a, cp1: b, cp2: c, end: d } = config
@ -26,13 +26,13 @@ export class CubicBezier2d extends Polyline2d {
}
override getVertices() {
const vertices = [] as Vec2d[]
const vertices = [] as Vec[]
const { a, b, c, d } = this
// we'll always use ten vertices for each bezier curve
for (let i = 0, n = 10; i <= n; i++) {
const t = i / n
vertices.push(
new Vec2d(
new Vec(
(1 - t) * (1 - t) * (1 - t) * a.x +
3 * ((1 - t) * (1 - t)) * t * b.x +
3 * (1 - t) * (t * t) * c.x +
@ -51,8 +51,8 @@ export class CubicBezier2d extends Polyline2d {
return getAtT(this, 0.5)
}
nearestPoint(A: Vec2d): Vec2d {
let nearest: Vec2d | undefined
nearestPoint(A: Vec): Vec {
let nearest: Vec | undefined
let dist = Infinity
for (const edge of this.segments) {
const p = edge.nearestPoint(A)
@ -70,7 +70,7 @@ export class CubicBezier2d extends Polyline2d {
function getAtT(segment: CubicBezier2d, t: number) {
const { a, b, c, d } = segment
return new Vec2d(
return new Vec(
(1 - t) * (1 - t) * (1 - t) * a.x +
3 * ((1 - t) * (1 - t)) * t * b.x +
3 * (1 - t) * (t * t) * c.x +

View file

@ -1,12 +1,12 @@
import { Vec2d } from '../Vec2d'
import { Vec } from '../Vec'
import { CubicBezier2d } from './CubicBezier2d'
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
/** @public */
export class CubicSpline2d extends Geometry2d {
points: Vec2d[]
points: Vec[]
constructor(config: Omit<Geometry2dOptions, 'isClosed' | 'isFilled'> & { points: Vec2d[] }) {
constructor(config: Omit<Geometry2dOptions, 'isClosed' | 'isFilled'> & { points: Vec[] }) {
super({ ...config, isClosed: false, isFilled: false })
const { points } = config
@ -32,13 +32,11 @@ export class CubicSpline2d extends Geometry2d {
const p3 = i === last ? p2 : points[i + 2]
const start = p1,
cp1 =
i === 0
? p0
: new Vec2d(p1.x + ((p2.x - p0.x) / 6) * k, p1.y + ((p2.y - p0.y) / 6) * k),
i === 0 ? p0 : new Vec(p1.x + ((p2.x - p0.x) / 6) * k, p1.y + ((p2.y - p0.y) / 6) * k),
cp2 =
i === last
? p2
: new Vec2d(p2.x - ((p3.x - p1.x) / 6) * k, p2.y - ((p3.y - p1.y) / 6) * k),
: new Vec(p2.x - ((p3.x - p1.x) / 6) * k, p2.y - ((p3.y - p1.y) / 6) * k),
end = p2
this._segments.push(new CubicBezier2d({ start, cp1, cp2, end }))
@ -61,13 +59,13 @@ export class CubicSpline2d extends Geometry2d {
getVertices() {
const vertices = this.segments.reduce((acc, segment) => {
return acc.concat(segment.vertices)
}, [] as Vec2d[])
}, [] as Vec[])
vertices.push(this.points[this.points.length - 1])
return vertices
}
nearestPoint(A: Vec2d): Vec2d {
let nearest: Vec2d | undefined
nearestPoint(A: Vec): Vec {
let nearest: Vec | undefined
let dist = Infinity
for (const segment of this.segments) {
const p = segment.nearestPoint(A)
@ -82,7 +80,7 @@ export class CubicSpline2d extends Geometry2d {
return nearest
}
hitTestLineSegment(A: Vec2d, B: Vec2d, zoom: number): boolean {
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
return this.segments.some((segment) => segment.hitTestLineSegment(A, B, zoom))
}
}

View file

@ -1,16 +1,16 @@
import { Vec2d } from '../Vec2d'
import { Vec } from '../Vec'
import { linesIntersect } from '../intersect'
import { Geometry2d } from './Geometry2d'
/** @public */
export class Edge2d extends Geometry2d {
start: Vec2d
end: Vec2d
d: Vec2d
u: Vec2d
start: Vec
end: Vec
d: Vec
u: Vec
ul: number
constructor(config: { start: Vec2d; end: Vec2d; isSnappable?: boolean }) {
constructor(config: { start: Vec; end: Vec; isSnappable?: boolean }) {
super({ ...config, isClosed: false, isFilled: false })
const { start, end } = config
@ -32,28 +32,28 @@ export class Edge2d extends Geometry2d {
return this._length
}
midPoint(): Vec2d {
midPoint(): Vec {
return this.start.lrp(this.end, 0.5)
}
override getVertices(): Vec2d[] {
override getVertices(): Vec[] {
return [this.start, this.end]
}
override nearestPoint(point: Vec2d): Vec2d {
override nearestPoint(point: Vec): Vec {
const { start, end, u, ul: l } = this
if (l === 0) return start // no length in the unit vector
const k = Vec2d.Sub(point, start).dpr(u) / l
const k = Vec.Sub(point, start).dpr(u) / l
const cx = start.x + u.x * k
if (cx < Math.min(start.x, end.x)) return start.x < end.x ? start : end
if (cx > Math.max(start.x, end.x)) return start.x > end.x ? start : end
const cy = start.y + u.y * k
if (cy < Math.min(start.y, end.y)) return start.y < end.y ? start : end
if (cy > Math.max(start.y, end.y)) return start.y > end.y ? start : end
return new Vec2d(cx, cy)
return new Vec(cx, cy)
}
override hitTestLineSegment(A: Vec2d, B: Vec2d, _zoom: number): boolean {
override hitTestLineSegment(A: Vec, B: Vec, _zoom: number): boolean {
return linesIntersect(A, B, this.start, this.end)
}
}

View file

@ -1,5 +1,5 @@
import { Box2d } from '../Box2d'
import { Vec2d } from '../Vec2d'
import { Box } from '../Box'
import { Vec } from '../Vec'
import { PI, PI2 } from '../utils'
import { Edge2d } from './Edge2d'
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
@ -63,7 +63,7 @@ export class Ellipse2d extends Geometry2d {
const vertices = Array(len)
for (let i = 0; i < len; i++) {
vertices[i] = new Vec2d(cx + cx * cos, cy + cy * sin)
vertices[i] = new Vec(cx + cx * cos, cy + cy * sin)
ts = b * cos + a * sin
tc = a * cos - b * sin
sin = ts
@ -73,8 +73,8 @@ export class Ellipse2d extends Geometry2d {
return vertices
}
nearestPoint(A: Vec2d): Vec2d {
let nearest: Vec2d | undefined
nearestPoint(A: Vec): Vec {
let nearest: Vec | undefined
let dist = Infinity
for (const edge of this.edges) {
const p = edge.nearestPoint(A)
@ -89,11 +89,11 @@ export class Ellipse2d extends Geometry2d {
return nearest
}
hitTestLineSegment(A: Vec2d, B: Vec2d, zoom: number): boolean {
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
return this.edges.some((edge) => edge.hitTestLineSegment(A, B, zoom))
}
getBounds() {
return new Box2d(0, 0, this.w, this.h)
return new Box(0, 0, this.w, this.h)
}
}

View file

@ -1,5 +1,5 @@
import { Box2d } from '../Box2d'
import { Vec2d } from '../Vec2d'
import { Box } from '../Box'
import { Vec } from '../Vec'
import { pointInPolygon } from '../utils'
export interface Geometry2dOptions {
@ -23,16 +23,16 @@ export abstract class Geometry2d {
this.isLabel = opts.isLabel ?? false
}
abstract getVertices(): Vec2d[]
abstract getVertices(): Vec[]
abstract nearestPoint(point: Vec2d): Vec2d
abstract nearestPoint(point: Vec): Vec
hitTestPoint(point: Vec2d, margin = 0, hitInside = false) {
hitTestPoint(point: Vec, margin = 0, hitInside = false) {
// We've removed the broad phase here; that should be done outside of the call
return this.distanceToPoint(point, hitInside) <= margin
}
distanceToPoint(point: Vec2d, hitInside = false) {
distanceToPoint(point: Vec, hitInside = false) {
const dist = point.dist(this.nearestPoint(point))
if (this.isClosed && (this.isFilled || hitInside) && pointInPolygon(point, this.vertices)) {
@ -41,22 +41,22 @@ export abstract class Geometry2d {
return dist
}
distanceToLineSegment(A: Vec2d, B: Vec2d) {
distanceToLineSegment(A: Vec, B: Vec) {
const point = this.nearestPointOnLineSegment(A, B)
const dist = Vec2d.DistanceToLineSegment(A, B, point) // repeated, bleh
const dist = Vec.DistanceToLineSegment(A, B, point) // repeated, bleh
return this.isClosed && this.isFilled && pointInPolygon(point, this.vertices) ? -dist : dist
}
hitTestLineSegment(A: Vec2d, B: Vec2d, distance = 0): boolean {
hitTestLineSegment(A: Vec, B: Vec, distance = 0): boolean {
return this.distanceToLineSegment(A, B) <= distance
}
nearestPointOnLineSegment(A: Vec2d, B: Vec2d): Vec2d {
nearestPointOnLineSegment(A: Vec, B: Vec): Vec {
let distance = Infinity
let nearest: Vec2d | undefined
let nearest: Vec | undefined
for (let i = 0; i < this.vertices.length; i++) {
const point = this.vertices[i]
const d = Vec2d.DistanceToLineSegment(A, B, point)
const d = Vec.DistanceToLineSegment(A, B, point)
if (d < distance) {
distance = d
nearest = point
@ -66,7 +66,7 @@ export abstract class Geometry2d {
return nearest
}
isPointInBounds(point: Vec2d, margin = 0) {
isPointInBounds(point: Vec, margin = 0) {
const { bounds } = this
return !(
point.x < bounds.minX - margin ||
@ -76,10 +76,10 @@ export abstract class Geometry2d {
)
}
_vertices: Vec2d[] | undefined
_vertices: Vec[] | undefined
// eslint-disable-next-line no-restricted-syntax
get vertices(): Vec2d[] {
get vertices(): Vec[] {
if (!this._vertices) {
this._vertices = this.getVertices()
}
@ -88,20 +88,20 @@ export abstract class Geometry2d {
}
getBounds() {
return Box2d.FromPoints(this.vertices)
return Box.FromPoints(this.vertices)
}
_bounds: Box2d | undefined
_bounds: Box | undefined
// eslint-disable-next-line no-restricted-syntax
get bounds(): Box2d {
get bounds(): Box {
if (!this._bounds) {
this._bounds = this.getBounds()
}
return this._bounds
}
_snapPoints: Vec2d[] | undefined
_snapPoints: Vec[] | undefined
// eslint-disable-next-line no-restricted-syntax
get snapPoints() {

View file

@ -1,5 +1,5 @@
import { Box2d } from '../Box2d'
import { Vec2d } from '../Vec2d'
import { Box } from '../Box'
import { Vec } from '../Vec'
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
/** @public */
@ -19,13 +19,13 @@ export class Group2d extends Geometry2d {
this.children = children
}
override getVertices(): Vec2d[] {
override getVertices(): Vec[] {
return this.children.filter((c) => !c.isLabel).flatMap((c) => c.vertices)
}
override nearestPoint(point: Vec2d): Vec2d {
override nearestPoint(point: Vec): Vec {
let d = Infinity
let p: Vec2d | undefined
let p: Vec | undefined
const { children } = this
@ -45,17 +45,17 @@ export class Group2d extends Geometry2d {
return p
}
override distanceToPoint(point: Vec2d, hitInside = false) {
override distanceToPoint(point: Vec, hitInside = false) {
return Math.min(...this.children.map((c, i) => c.distanceToPoint(point, hitInside || i > 0)))
}
override hitTestPoint(point: Vec2d, margin: number, hitInside: boolean): boolean {
override hitTestPoint(point: Vec, margin: number, hitInside: boolean): boolean {
return !!this.children
.filter((c) => !c.isLabel)
.find((c) => c.hitTestPoint(point, margin, hitInside))
}
override hitTestLineSegment(A: Vec2d, B: Vec2d, zoom: number): boolean {
override hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
return !!this.children.filter((c) => !c.isLabel).find((c) => c.hitTestLineSegment(A, B, zoom))
}
@ -70,7 +70,7 @@ export class Group2d extends Geometry2d {
path += child.toSimpleSvgPath()
}
const corners = Box2d.FromPoints(this.vertices).corners
const corners = Box.FromPoints(this.vertices).corners
// draw just a few pixels around each corner, e.g. an L shape for the bottom left
for (let i = 0, n = corners.length; i < n; i++) {

View file

@ -1,12 +1,12 @@
import { Vec2d } from '../Vec2d'
import { Vec } from '../Vec'
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
/** @public */
export class Point2d extends Geometry2d {
point: Vec2d
point: Vec
constructor(
config: Omit<Geometry2dOptions, 'isClosed' | 'isFilled'> & { margin: number; point: Vec2d }
config: Omit<Geometry2dOptions, 'isClosed' | 'isFilled'> & { margin: number; point: Vec }
) {
super({ ...config, isClosed: true, isFilled: true })
const { point } = config
@ -18,11 +18,11 @@ export class Point2d extends Geometry2d {
return [this.point]
}
nearestPoint(): Vec2d {
nearestPoint(): Vec {
return this.point
}
hitTestLineSegment(A: Vec2d, B: Vec2d, margin: number): boolean {
return Vec2d.DistanceToLineSegment(A, B, this.point) < margin
hitTestLineSegment(A: Vec, B: Vec, margin: number): boolean {
return Vec.DistanceToLineSegment(A, B, this.point) < margin
}
}

View file

@ -1,10 +1,10 @@
import { Vec2d } from '../Vec2d'
import { Vec } from '../Vec'
import { Geometry2dOptions } from './Geometry2d'
import { Polyline2d } from './Polyline2d'
/** @public */
export class Polygon2d extends Polyline2d {
constructor(config: Omit<Geometry2dOptions, 'isClosed'> & { points: Vec2d[] }) {
constructor(config: Omit<Geometry2dOptions, 'isClosed'> & { points: Vec[] }) {
super({ ...config })
this.isClosed = true
}

View file

@ -1,12 +1,12 @@
import { Vec2d } from '../Vec2d'
import { Vec } from '../Vec'
import { Edge2d } from './Edge2d'
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
/** @public */
export class Polyline2d extends Geometry2d {
points: Vec2d[]
points: Vec[]
constructor(config: Omit<Geometry2dOptions, 'isFilled' | 'isClosed'> & { points: Vec2d[] }) {
constructor(config: Omit<Geometry2dOptions, 'isFilled' | 'isClosed'> & { points: Vec[] }) {
super({ isClosed: false, isFilled: false, ...config })
const { points } = config
this.points = points
@ -47,12 +47,12 @@ export class Polyline2d extends Geometry2d {
return this.points
}
nearestPoint(A: Vec2d): Vec2d {
nearestPoint(A: Vec): Vec {
const { segments } = this
let nearest = this.points[0]
let dist = Infinity
let p: Vec2d // current point on segment
let p: Vec // current point on segment
let d: number // distance from A to p
for (let i = 0; i < segments.length; i++) {
p = segments[i].nearestPoint(A)
@ -66,7 +66,7 @@ export class Polyline2d extends Geometry2d {
return nearest
}
hitTestLineSegment(A: Vec2d, B: Vec2d, zoom: number): boolean {
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean {
return this.segments.some((edge) => edge.hitTestLineSegment(A, B, zoom))
}
}

View file

@ -1,5 +1,5 @@
import { Box2d } from '../Box2d'
import { Vec2d } from '../Vec2d'
import { Box } from '../Box'
import { Vec } from '../Vec'
import { Geometry2dOptions } from './Geometry2d'
import { Polygon2d } from './Polygon2d'
@ -22,10 +22,10 @@ export class Rectangle2d extends Polygon2d {
super({
...config,
points: [
new Vec2d(x, y),
new Vec2d(x + width, y),
new Vec2d(x + width, y + height),
new Vec2d(x, y + height),
new Vec(x, y),
new Vec(x + width, y),
new Vec(x + width, y + height),
new Vec(x, y + height),
],
})
this.x = x
@ -35,6 +35,6 @@ export class Rectangle2d extends Polygon2d {
}
getBounds() {
return new Box2d(this.x, this.y, this.w, this.h)
return new Box(this.x, this.y, this.w, this.h)
}
}

View file

@ -1,5 +1,5 @@
import { Vec2d } from '../Vec2d'
import { PI, TAU } from '../utils'
import { Vec } from '../Vec'
import { HALF_PI, PI } from '../utils'
import { Ellipse2d } from './Ellipse2d'
import { Geometry2dOptions } from './Geometry2d'
@ -19,21 +19,21 @@ export class Stadium2d extends Ellipse2d {
const cy = h / 2
const len = 10
const points: Vec2d[] = Array(len * 2 - 2)
const points: Vec[] = Array(len * 2 - 2)
if (h > w) {
for (let i = 0; i < len - 1; i++) {
const t1 = -PI + (PI * i) / (len - 2)
const t2 = (PI * i) / (len - 2)
points[i] = new Vec2d(cx + cx * Math.cos(t1), cx + cx * Math.sin(t1))
points[i + (len - 1)] = new Vec2d(cx + cx * Math.cos(t2), h - cx + cx * Math.sin(t2))
points[i] = new Vec(cx + cx * Math.cos(t1), cx + cx * Math.sin(t1))
points[i + (len - 1)] = new Vec(cx + cx * Math.cos(t2), h - cx + cx * Math.sin(t2))
}
} else {
for (let i = 0; i < len - 1; i++) {
const t1 = -TAU + (PI * i) / (len - 2)
const t2 = TAU + (PI * -i) / (len - 2)
points[i] = new Vec2d(w - cy + cy * Math.cos(t1), h - cy + cy * Math.sin(t1))
points[i + (len - 1)] = new Vec2d(cy - cy * Math.cos(t2), h - cy + cy * Math.sin(t2))
const t1 = -HALF_PI + (PI * i) / (len - 2)
const t2 = HALF_PI + (PI * -i) / (len - 2)
points[i] = new Vec(w - cy + cy * Math.cos(t1), h - cy + cy * Math.sin(t1))
points[i + (len - 1)] = new Vec(cy - cy * Math.cos(t2), h - cy + cy * Math.sin(t2))
}
}

View file

@ -1,5 +1,5 @@
export const SPACING = 20
export const MIN_COUNT = 8
const SPACING = 20
const MIN_COUNT = 8
export function getVerticesCountForLength(length: number, spacing = SPACING) {
return Math.max(MIN_COUNT, Math.ceil(length / spacing))

View file

@ -1,6 +1,6 @@
import { Box2d } from './Box2d'
import { Box } from './Box'
import { pointInPolygon } from './utils'
import { Vec2d, VecLike } from './Vec2d'
import { Vec, VecLike } from './Vec'
// need even more intersections? See https://gist.github.com/steveruizok/35c02d526c707003a5c79761bfb89a52
@ -37,7 +37,7 @@ export function intersectLineSegmentLineSegment(
const ua = ua_t / u_b
const ub = ub_t / u_b
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
return Vec2d.AddXY(a1, ua * AVx, ua * AVy)
return Vec.AddXY(a1, ua * AVx, ua * AVy)
}
}
@ -76,8 +76,8 @@ export function intersectLineSegmentCircle(a1: VecLike, a2: VecLike, c: VecLike,
const result: VecLike[] = []
if (0 <= u1 && u1 <= 1) result.push(Vec2d.Lrp(a1, a2, u1))
if (0 <= u2 && u2 <= 1) result.push(Vec2d.Lrp(a1, a2, u2))
if (0 <= u1 && u1 <= 1) result.push(Vec.Lrp(a1, a2, u1))
if (0 <= u2 && u2 <= 1) result.push(Vec.Lrp(a1, a2, u2))
if (result.length === 0) return null // no intersection
@ -151,8 +151,8 @@ export function intersectCircleCircle(c1: VecLike, r1: number, c2: VecLike, r2:
dx /= d
dy /= d
return [
new Vec2d(c1.x + dx * x - dy * y, c1.y + dy * x + dx * y),
new Vec2d(c1.x + dx * x + dy * y, c1.y + dy * x - dx * y),
new Vec(c1.x + dx * x - dy * y, c1.y + dy * x + dx * y),
new Vec(c1.x + dx * x + dy * y, c1.y + dy * x - dx * y),
]
}
@ -209,7 +209,7 @@ export function intersectCirclePolyline(c: VecLike, r: number, points: VecLike[]
*
* @public
*/
export function intersectPolygonBounds(points: VecLike[], bounds: Box2d) {
export function intersectPolygonBounds(points: VecLike[], bounds: Box) {
const result: VecLike[] = []
let segmentIntersection: VecLike[] | null
@ -285,8 +285,8 @@ export function intersectPolygonPolygon(
}
function orderClockwise(points: VecLike[]): VecLike[] {
const C = Vec2d.Average(points)
return points.sort((A, B) => Vec2d.Angle(C, A) - Vec2d.Angle(C, B))
const C = Vec.Average(points)
return points.sort((A, B) => Vec.Angle(C, A) - Vec.Angle(C, B))
}
/** @public */

View file

@ -1,5 +1,4 @@
import { Box2d } from './Box2d'
import { Vec2d, VecLike } from './Vec2d'
import { Vec, VecLike } from './Vec'
/** @public */
export function precise(A: VecLike) {
@ -14,12 +13,10 @@ export function average(A: VecLike, B: VecLike) {
/** @public */
export const PI = Math.PI
/** @public */
export const TAU = PI / 2
export const HALF_PI = PI / 2
/** @public */
export const PI2 = PI * 2
/** @public */
export const EPSILON = Math.PI / 180
/** @public */
export const SIN = Math.sin
/**
@ -148,52 +145,6 @@ export function shortAngleDist(a0: number, a1: number): number {
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
*
@ -231,38 +182,6 @@ 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 {
// Normalize the angles to ensure they're in the same domain
a = canonicalizeRotation(a)
b = canonicalizeRotation(b)
c = canonicalizeRotation(c)
// Compute vectors corresponding to angles a and b
const ax = Math.cos(a)
const ay = Math.sin(a)
const bx = Math.cos(b)
const by = Math.sin(b)
// Compute the vector corresponding to angle c
const cx = Math.cos(c)
const cy = Math.sin(c)
// Calculate dot products
const dotAc = ax * cx + ay * cy
const dotBc = bx * cx + by * cy
// If angle c is between a and b, both dot products should be >= 0
return dotAc >= 0 && dotBc >= 0
}
/**
* Convert degrees to radians.
*
@ -283,20 +202,6 @@ 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.
*
@ -307,13 +212,13 @@ export function getArcLength(C: VecLike, r: number, A: VecLike, B: VecLike): num
* @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))
return new Vec(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: Vec2d[] = []
const pointsOnPerimeter: Vec[] = []
let minX = Infinity
let maxX = -Infinity
@ -321,14 +226,14 @@ export function getPolygonVertices(width: number, height: number, sides: number)
let maxY = -Infinity
for (let i = 0; i < sides; i++) {
const step = PI2 / sides
const t = -TAU + i * step
const t = -HALF_PI + i * step
const x = cx + cx * Math.cos(t)
const y = cy + cy * Math.sin(t)
if (x < minX) minX = x
if (y < minY) minY = y
if (x > maxX) maxX = x
if (y > maxY) maxY = y
pointsOnPerimeter.push(new Vec2d(x, y))
pointsOnPerimeter.push(new Vec(x, y))
}
// Bounds of calculated points
@ -387,91 +292,11 @@ export function rangeIntersection(
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.
*
@ -502,245 +327,6 @@ export function pointInPolygon(A: VecLike, points: VecLike[]): boolean {
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
*

View file

@ -202,8 +202,7 @@ interface Defaults<T> {
all: T
}
/** @internal */
export interface DebugFlagDef<T> {
interface DebugFlagDef<T> {
name: string
defaults: Defaults<T>
shouldStoreForSession: boolean

View file

@ -7,7 +7,7 @@ import { Editor } from '../editor/Editor'
* @param dimension - The component dimension on the axis.
* @internal
*/
export function getEdgeProximityFactor(position: number, dimension: number) {
function getEdgeProximityFactor(position: number, dimension: number) {
if (position < 0) {
return 1
} else if (position > dimension) {

View file

@ -1,4 +1,4 @@
import { VecLike } from '../primitives/Vec2d'
import { VecLike } from '../primitives/Vec'
import { average, precise } from '../primitives/utils'
/**

View file

@ -1,86 +1,6 @@
import {
decrementInteger,
generateKeyBetween,
generateNKeysBetween,
incrementInteger,
midpoint,
} from './dgreensp'
import { generateNKeysBetween } from './dgreensp'
describe('midpoint', () => {
it('passes tests', () => {
expect(midpoint('', undefined)).toBe('V')
expect(midpoint('V', undefined)).toBe('l')
expect(midpoint('l', undefined)).toBe('t')
expect(midpoint('t', undefined)).toBe('x')
expect(midpoint('x', undefined)).toBe('z')
expect(midpoint('z', undefined)).toBe('zV')
expect(midpoint('zV', undefined)).toBe('zl')
expect(midpoint('zl', undefined)).toBe('zt')
expect(midpoint('zt', undefined)).toBe('zx')
expect(midpoint('zx', undefined)).toBe('zz')
expect(midpoint('zz', undefined)).toBe('zzV')
expect(midpoint('1', '2')).toBe('1V')
expect(() => midpoint('2', '1')).toThrowError()
expect(() => midpoint('', '')).toThrowError()
expect(() => midpoint('0', '1')).toThrowError()
expect(() => midpoint('1', '10')).toThrowError()
expect(() => midpoint('11', '1')).toThrowError()
expect(midpoint('001', '001002')).toBe('001001')
expect(midpoint('001', '001001')).toBe('001000V')
expect(midpoint('', 'V')).toBe('G')
expect(midpoint('', 'G')).toBe('8')
expect(midpoint('', '8')).toBe('4')
expect(midpoint('', '4')).toBe('2')
expect(midpoint('', '2')).toBe('1')
expect(midpoint('', '1')).toBe('0V')
expect(midpoint('0V', '1')).toBe('0l')
expect(midpoint('', '0G')).toBe('08')
expect(midpoint('', '08')).toBe('04')
expect(midpoint('', '02')).toBe('01')
expect(midpoint('', '01')).toBe('00V')
expect(midpoint('4zz', '5')).toBe('4zzV')
})
})
describe('decrement integer', () => {
it('passes tests', () => {
expect(decrementInteger('a1')).toBe('a0')
expect(decrementInteger('a2')).toBe('a1')
expect(decrementInteger('b00')).toBe('az')
expect(decrementInteger('b10')).toBe('b0z')
expect(decrementInteger('b20')).toBe('b1z')
expect(decrementInteger('c000')).toBe('bzz')
expect(decrementInteger('Zz')).toBe('Zy')
expect(decrementInteger('a0')).toBe('Zz')
expect(decrementInteger('Yzz')).toBe('Yzy')
expect(decrementInteger('Z0')).toBe('Yzz')
expect(decrementInteger('Xz00')).toBe('Xyzz')
expect(decrementInteger('Xz01')).toBe('Xz00')
expect(decrementInteger('Y00')).toBe('Xzzz')
expect(decrementInteger('dAC00')).toBe('dABzz')
expect(decrementInteger('A00000000000000000000000000')).toBe(undefined)
})
})
describe('increment integer', () => {
it('produces the right integer string', () => {
expect(incrementInteger('a0')).toBe('a1')
expect(incrementInteger('a1')).toBe('a2')
expect(incrementInteger('az')).toBe('b00')
expect(incrementInteger('b0z')).toBe('b10')
expect(incrementInteger('b1z')).toBe('b20')
expect(incrementInteger('bzz')).toBe('c000')
expect(incrementInteger('Zy')).toBe('Zz')
expect(incrementInteger('Zz')).toBe('a0')
expect(incrementInteger('Yzy')).toBe('Yzz')
expect(incrementInteger('Yzz')).toBe('Z0')
expect(incrementInteger('Xyzz')).toBe('Xz00')
expect(incrementInteger('Xz00')).toBe('Xz01')
expect(incrementInteger('Xzzz')).toBe('Y00')
expect(incrementInteger('dABzz')).toBe('dAC00')
expect(incrementInteger('zzzzzzzzzzzzzzzzzzzzzzzzzzz')).toBe(undefined)
})
})
const generateKeyBetween = (a?: string, b?: string) => generateNKeysBetween(a, b, 1)[0]
describe('get order between', () => {
it('passes tests', () => {

View file

@ -10,7 +10,7 @@ const SMALLEST_INTEGER = 'A00000000000000000000000000'
*
* @param head - The integer to use.
*/
export function getIntegerLength(head: string): number {
function getIntegerLength(head: string): number {
if (head >= 'a' && head <= 'z') {
return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2
} else if (head >= 'A' && head <= 'Z') {
@ -25,13 +25,13 @@ export function getIntegerLength(head: string): number {
*
* @param int - The integer to use.
*/
export function validateInteger(int: string): asserts int is string {
function validateInteger(int: string): asserts int is string {
if (int.length !== getIntegerLength(int.charAt(0))) {
throw new Error('invalid integer part of index key: ' + int)
}
}
export function isNotUndefined(n: string | undefined): asserts n is string {
function isNotUndefined(n: string | undefined): asserts n is string {
if (n === undefined) throw Error('n is undefined')
}
@ -39,8 +39,10 @@ export function isNotUndefined(n: string | undefined): asserts n is string {
* Increment an integer.
*
* @param x - The integer to increment
*
* @internal
*/
export function incrementInteger(x: string): string | undefined {
function incrementInteger(x: string): string | undefined {
validateInteger(x)
const [head, ...digs] = x.split('')
let carry = true
@ -72,8 +74,10 @@ export function incrementInteger(x: string): string | undefined {
* Decrement an integer.
*
* @param x - The integer to decrement
*
* @internal
*/
export function decrementInteger(x: string): string | undefined {
function decrementInteger(x: string): string | undefined {
validateInteger(x)
const [head, ...digs] = x.split('')
let borrow = true
@ -106,8 +110,10 @@ export function decrementInteger(x: string): string | undefined {
*
* @param a - The start index.
* @param b - The end index.
*
* @internal
*/
export function midpoint(a: string, b: string | undefined): string {
function midpoint(a: string, b: string | undefined): string {
if (b !== undefined && a >= b) {
throw new Error(a + ' >= ' + b)
}
@ -142,7 +148,7 @@ export function midpoint(a: string, b: string | undefined): string {
*
* @param index - The index to use.
*/
export function getIntegerPart(index: string): string {
function getIntegerPart(index: string): string {
const integerPartLength = getIntegerLength(index.charAt(0))
if (integerPartLength > index.length) {
throw new Error('invalid index: ' + index)
@ -155,7 +161,7 @@ export function getIntegerPart(index: string): string {
*
* @param x - The index to validate.
*/
export function validateOrder(index: string): asserts index is string {
function validateOrder(index: string): asserts index is string {
if (index === SMALLEST_INTEGER) {
throw new Error('invalid index: ' + index)
}
@ -173,7 +179,7 @@ export function validateOrder(index: string): asserts index is string {
* A string made up of an integer part followed by a fraction part. The fraction point consists of
* zero or more digits with no trailing zeros.
*/
export type OrderKey = string
type OrderKey = string
/**
* Generate an index key at the midpoint between a start and end.
@ -181,7 +187,7 @@ export type OrderKey = string
* @param a - The start index key string.
* @param b - The end index key string, greater than A.
*/
export function generateKeyBetween(a: OrderKey | undefined, b: OrderKey | undefined): OrderKey {
function generateKeyBetween(a: OrderKey | undefined, b: OrderKey | undefined): OrderKey {
if (a !== undefined) validateOrder(a)
if (b !== undefined) validateOrder(b)
if (a !== undefined && b !== undefined && a >= b) {
@ -259,21 +265,3 @@ export function generateNKeysBetween(
const c = generateKeyBetween(a, b)
return [...generateNKeysBetween(a, c, mid), c, ...generateNKeysBetween(c, b, n - mid - 1)]
}
export function getCounter() {
let index = 'a0'
return () => {
index = generateKeyBetween(index, undefined)
return index
}
}
export function* iterableRange(n = 1) {
let index = 'a0'
let i = 0
while (i < n) {
i++
index = generateKeyBetween(index, undefined)
yield index
}
}

View file

@ -1,2 +0,0 @@
import { generateKeyBetween, generateNKeysBetween, getCounter } from './dgreensp'
export { getCounter, generateKeyBetween, generateNKeysBetween }

View file

@ -1,4 +1,4 @@
import { generateNKeysBetween } from './dgreensp'
import { generateNKeysBetween } from './dgreensp/dgreensp'
/**
* Get a number of indices between two indices.

View file

@ -1,9 +1,9 @@
import { isShapeId, TLShape, TLShapePartial } from '@tldraw/tlschema'
import { structuredClone } from '@tldraw/utils'
import { Editor } from '../editor/Editor'
import { Matrix2d } from '../primitives/Matrix2d'
import { Mat } from '../primitives/Mat'
import { canonicalizeRotation } from '../primitives/utils'
import { Vec2d } from '../primitives/Vec2d'
import { Vec } from '../primitives/Vec'
/** @internal */
export function getRotationSnapshot({ editor }: { editor: Editor }): TLRotationSnapshot | null {
@ -49,12 +49,12 @@ export function getRotationSnapshot({ editor }: { editor: Editor }): TLRotationS
* @public
**/
export type TLRotationSnapshot = {
selectionPageCenter: Vec2d
selectionPageCenter: Vec
initialCursorAngle: number
initialSelectionRotation: number
shapeSnapshots: {
shape: TLShape
initialPagePoint: Vec2d
initialPagePoint: Vec
}[]
}
@ -79,14 +79,14 @@ export function applyRotationToSnapshotShapes({
const parentTransform = isShapeId(shape.parentId)
? editor.getShapePageTransform(shape.parentId)!
: Matrix2d.Identity()
: Mat.Identity()
const newPagePoint = Vec2d.RotWith(initialPagePoint, selectionPageCenter, delta)
const newPagePoint = Vec.RotWith(initialPagePoint, selectionPageCenter, delta)
const newLocalPoint = Matrix2d.applyToPoint(
const newLocalPoint = Mat.applyToPoint(
// use the current parent transform in case it has moved/resized since the start
// (e.g. if rotating a shape at the edge of a group)
Matrix2d.Inverse(parentTransform),
Mat.Inverse(parentTransform),
newPagePoint
)
const newRotation = canonicalizeRotation(shape.rotation + delta)

View file

@ -9,7 +9,7 @@
import { ArrayOfValidator } from '@tldraw/editor';
import { BaseBoxShapeTool } from '@tldraw/editor';
import { BaseBoxShapeUtil } from '@tldraw/editor';
import { Box2d } from '@tldraw/editor';
import { Box } from '@tldraw/editor';
import { Circle2d } from '@tldraw/editor';
import { CubicSpline2d } from '@tldraw/editor';
import { DictValidator } from '@tldraw/editor';
@ -21,8 +21,8 @@ import { Geometry2d } from '@tldraw/editor';
import { Group2d } from '@tldraw/editor';
import { JsonObject } from '@tldraw/editor';
import { LANGUAGES } from '@tldraw/editor';
import { Matrix2d } from '@tldraw/editor';
import { Matrix2dModel } from '@tldraw/editor';
import { Mat } from '@tldraw/editor';
import { MatModel } from '@tldraw/editor';
import { MigrationFailureReason } from '@tldraw/editor';
import { Migrations } from '@tldraw/editor';
import { NamedExoticComponent } from 'react';
@ -107,9 +107,9 @@ import { TLVideoShape } from '@tldraw/editor';
import { UnionValidator } from '@tldraw/editor';
import { UnknownRecord } from '@tldraw/editor';
import { Validator } from '@tldraw/editor';
import { Vec2d } from '@tldraw/editor';
import { Vec2dModel } from '@tldraw/editor';
import { Vec } from '@tldraw/editor';
import { VecLike } from '@tldraw/editor';
import { VecModel } from '@tldraw/editor';
// @public (undocumented)
export class ArrowShapeTool extends StateNode {
@ -177,7 +177,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
binding: ObjectValidator< {
type: "binding";
boundShapeId: TLShapeId;
normalizedAnchor: Vec2dModel;
normalizedAnchor: VecModel;
isExact: boolean;
isPrecise: boolean;
}>;
@ -191,7 +191,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
binding: ObjectValidator< {
type: "binding";
boundShapeId: TLShapeId;
normalizedAnchor: Vec2dModel;
normalizedAnchor: VecModel;
isExact: boolean;
isPrecise: boolean;
}>;
@ -365,7 +365,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
segments: ArrayOfValidator< {
type: "free" | "straight";
points: Vec2dModel[];
points: VecModel[];
}>;
isComplete: Validator<boolean>;
isClosed: Validator<boolean>;
@ -774,7 +774,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
segments: ArrayOfValidator< {
type: "free" | "straight";
points: Vec2dModel[];
points: VecModel[];
}>;
isComplete: Validator<boolean>;
isPen: Validator<boolean>;
@ -816,8 +816,8 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
url: Validator<string>;
assetId: Validator<TLAssetId | null>;
crop: Validator< {
topLeft: Vec2dModel;
bottomRight: Vec2dModel;
topLeft: VecModel;
bottomRight: VecModel;
} | null>;
};
// (undocumented)
@ -889,7 +889,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
// (undocumented)
getHandles(shape: TLLineShape): TLHandle[];
// (undocumented)
getOutlineSegments(shape: TLLineShape): Vec2d[][];
getOutlineSegments(shape: TLLineShape): Vec[][];
// (undocumented)
hideResizeHandles: () => boolean;
// (undocumented)

View file

@ -1321,8 +1321,8 @@
},
{
"kind": "Reference",
"text": "Vec2dModel",
"canonicalReference": "@tldraw/tlschema!Vec2dModel:interface"
"text": "VecModel",
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
},
{
"kind": "Content",
@ -1366,8 +1366,8 @@
},
{
"kind": "Reference",
"text": "Vec2dModel",
"canonicalReference": "@tldraw/tlschema!Vec2dModel:interface"
"text": "VecModel",
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
},
{
"kind": "Content",
@ -3601,8 +3601,8 @@
},
{
"kind": "Reference",
"text": "Vec2dModel",
"canonicalReference": "@tldraw/tlschema!Vec2dModel:interface"
"text": "VecModel",
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
},
{
"kind": "Content",
@ -8826,8 +8826,8 @@
},
{
"kind": "Reference",
"text": "Vec2dModel",
"canonicalReference": "@tldraw/tlschema!Vec2dModel:interface"
"text": "VecModel",
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
},
{
"kind": "Content",
@ -9480,8 +9480,8 @@
},
{
"kind": "Reference",
"text": "Vec2dModel",
"canonicalReference": "@tldraw/tlschema!Vec2dModel:interface"
"text": "VecModel",
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
},
{
"kind": "Content",
@ -9489,8 +9489,8 @@
},
{
"kind": "Reference",
"text": "Vec2dModel",
"canonicalReference": "@tldraw/tlschema!Vec2dModel:interface"
"text": "VecModel",
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
},
{
"kind": "Content",
@ -10320,8 +10320,8 @@
},
{
"kind": "Reference",
"text": "Vec2d",
"canonicalReference": "@tldraw/editor!Vec2d:class"
"text": "Vec",
"canonicalReference": "@tldraw/editor!Vec:class"
},
{
"kind": "Content",
@ -15722,7 +15722,7 @@
"text": "export interface TLUiContextMenuProps "
}
],
"fileUrlPath": "packages/tldraw/.tsbuild-api/lib/ui/components/ContextMenu.d.ts",
"fileUrlPath": "packages/tldraw/src/lib/ui/components/ContextMenu.tsx",
"releaseTag": "Public",
"name": "TLUiContextMenuProps",
"preserveMemberOrder": false,
@ -15745,7 +15745,6 @@
"text": ";"
}
],
"fileUrlPath": "packages/tldraw/src/lib/ui/components/ContextMenu.tsx",
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",

View file

@ -1,5 +1,5 @@
import {
Box2d,
Box,
RotateCorner,
TLEmbedShape,
TLSelectionForegroundComponent,
@ -19,7 +19,7 @@ import { TldrawCropHandles } from './TldrawCropHandles'
/** @public */
export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
function TldrawSelectionForeground({ bounds, rotation }: { bounds: Box2d; rotation: number }) {
function TldrawSelectionForeground({ bounds, rotation }: { bounds: Box; rotation: number }) {
const editor = useEditor()
const rSvg = useRef<SVGSVGElement>(null)

View file

@ -10,7 +10,7 @@ import {
TLShapePartial,
TLTextShape,
TLTextShapeProps,
Vec2d,
Vec,
VecLike,
compact,
createShapeId,
@ -226,7 +226,7 @@ export function registerDefaultExternalContentHandlers(
point ??
(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())
const pagePoint = new Vec2d(position.x, position.y)
const pagePoint = new Vec(position.x, position.y)
const assets: TLAsset[] = []
@ -404,7 +404,7 @@ export async function createShapesForAssets(
): Promise<TLShapeId[]> {
if (!assets.length) return []
const currentPoint = Vec2d.From(position)
const currentPoint = Vec.From(position)
const partials: TLShapePartial[] = []
for (const asset of assets) {
@ -489,7 +489,7 @@ function centerSelectionAroundPoint(editor: Editor, position: VecLike) {
editor.updateShapes(
editor.getSelectedShapes().map((shape) => {
const localRotation = editor.getShapeParentTransform(shape).decompose().rotation
const localDelta = Vec2d.Rot(offset, -localRotation)
const localDelta = Vec.Rot(offset, -localRotation)
return {
id: shape.id,
type: shape.type,

View file

@ -1,4 +1,4 @@
import { TLArrowShape, Vec2d, createShapeId } from '@tldraw/editor'
import { TLArrowShape, Vec, createShapeId } from '@tldraw/editor'
import { TestEditor } from '../../../test/TestEditor'
let editor: TestEditor
@ -132,7 +132,7 @@ describe('When pointing a start shape', () => {
expect(editor.getHintingShapeIds().length).toBe(1)
// Fake some velocity
editor.inputs.pointerVelocity = new Vec2d(1, 1)
editor.inputs.pointerVelocity = new Vec(1, 1)
editor.pointerMove(375, 500)
@ -172,7 +172,7 @@ describe('When pointing an end shape', () => {
expect(editor.getHintingShapeIds().length).toBe(0)
// Fake some velocity
editor.inputs.pointerVelocity = new Vec2d(1, 1)
editor.inputs.pointerVelocity = new Vec(1, 1)
// Move onto shape
editor.pointerMove(375, 375)
@ -206,7 +206,7 @@ describe('When pointing an end shape', () => {
it('unbinds and rebinds', () => {
editor.setCurrentTool('arrow').pointerDown(0, 0)
editor.inputs.pointerVelocity = new Vec2d(1, 1)
editor.inputs.pointerVelocity = new Vec(1, 1)
editor.pointerMove(375, 375)
@ -268,7 +268,7 @@ describe('When pointing an end shape', () => {
})
// Build up some velocity
editor.inputs.pointerVelocity = new Vec2d(1, 1)
editor.inputs.pointerVelocity = new Vec(1, 1)
editor.pointerMove(325, 325)
expect(editor.getHintingShapeIds().length).toBe(1)
@ -318,7 +318,7 @@ describe('When pointing an end shape', () => {
it('begins imprecise when moving quickly', () => {
editor.setCurrentTool('arrow').pointerDown(0, 0)
editor.inputs.pointerVelocity = new Vec2d(1, 1)
editor.inputs.pointerVelocity = new Vec(1, 1)
editor.pointerMove(370, 370)
const arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]
@ -361,7 +361,7 @@ describe('When pointing an end shape', () => {
expect(editor.getHintingShapeIds().length).toBe(0)
editor.inputs.pointerVelocity = new Vec2d(0.001, 0.001)
editor.inputs.pointerVelocity = new Vec(0.001, 0.001)
editor.pointerMove(375, 375)
arrow = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1]

View file

@ -1,7 +1,7 @@
import {
assert,
createShapeId,
TAU,
HALF_PI,
TLArrowShape,
TLArrowShapeTerminal,
TLShapeId,
@ -343,7 +343,7 @@ describe('When a shape it rotated', () => {
},
})
editor.updateShapes([{ id: ids.box2, type: 'geo', rotation: TAU }])
editor.updateShapes([{ id: ids.box2, type: 'geo', rotation: HALF_PI }])
editor.pointerMove(225, 350)

View file

@ -1,6 +1,6 @@
import {
Arc2d,
Box2d,
Box,
DefaultFontFamilies,
Edge2d,
Group2d,
@ -21,17 +21,12 @@ import {
TLShapePartial,
TLShapeUtilCanvasSvgDef,
TLShapeUtilFlag,
Vec2d,
Vec,
arrowShapeMigrations,
arrowShapeProps,
deepCopy,
getArrowTerminalsInArrowSpace,
getArrowheadPathForType,
getCurvedArrowHandlePath,
getDefaultColorTheme,
getSolidCurvedArrowPath,
getSolidStraightArrowPath,
getStraightArrowHandlePath,
toDomPrecision,
useIsEditing,
} from '@tldraw/editor'
@ -50,6 +45,13 @@ import {
getFontDefForExport,
} from '../shared/defaultStyleDefs'
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
import { getArrowheadPathForType } from './arrowheads'
import {
getCurvedArrowHandlePath,
getSolidCurvedArrowPath,
getSolidStraightArrowPath,
getStraightArrowHandlePath,
} from './arrowpaths'
import { ArrowTextLabel } from './components/ArrowTextLabel'
let globalRenderIndex = 0
@ -92,14 +94,14 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
const bodyGeom = info.isStraight
? new Edge2d({
start: Vec2d.From(info.start.point),
end: Vec2d.From(info.end.point),
start: Vec.From(info.start.point),
end: Vec.From(info.end.point),
})
: new Arc2d({
center: Vec2d.Cast(info.handleArc.center),
center: Vec.Cast(info.handleArc.center),
radius: info.handleArc.radius,
start: Vec2d.Cast(info.start.point),
end: Vec2d.Cast(info.end.point),
start: Vec.Cast(info.start.point),
end: Vec.Cast(info.end.point),
sweepFlag: info.bodyArc.sweepFlag,
largeArcFlag: info.bodyArc.largeArcFlag,
})
@ -209,16 +211,16 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// Bending the arrow...
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape)
const delta = Vec2d.Sub(end, start)
const v = Vec2d.Per(delta)
const delta = Vec.Sub(end, start)
const v = Vec.Per(delta)
const med = Vec2d.Med(end, start)
const A = Vec2d.Sub(med, v)
const B = Vec2d.Add(med, v)
const med = Vec.Med(end, start)
const A = Vec.Sub(med, v)
const B = Vec.Add(med, v)
const point = Vec2d.NearestPointOnLineSegment(A, B, handle, false)
let bend = Vec2d.Dist(point, med)
if (Vec2d.Clockwise(point, end, med)) bend *= -1
const point = Vec.NearestPointOnLineSegment(A, B, handle, false)
let bend = Vec.Dist(point, med)
if (Vec.Clockwise(point, end, med)) bend *= -1
return { id: shape.id, type: shape.type, props: { bend } }
}
@ -264,7 +266,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// we've got a target! the handle is being dragged over a shape, bind to it
const targetGeometry = this.editor.getShapeGeometry(target)
const targetBounds = Box2d.ZeroFix(targetGeometry.bounds)
const targetBounds = Box.ZeroFix(targetGeometry.bounds)
const pointInTargetSpace = this.editor.getPointInShapeSpace(target, pointInPageSpace)
let precise = isPrecise
@ -307,7 +309,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// Funky math but we want the snap distance to be 4 at the minimum and either
// 16 or 15% of the smaller dimension of the target shape, whichever is smaller
if (
Vec2d.Dist(pointInTargetSpace, targetBounds.center) <
Vec.Dist(pointInTargetSpace, targetBounds.center) <
Math.max(4, Math.min(Math.min(targetBounds.width, targetBounds.height) * 0.15, 16)) /
this.editor.getZoomLevel()
) {
@ -326,7 +328,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
if (next.props.start.type === 'binding' && next.props.end.type === 'binding') {
if (next.props.start.boundShapeId === next.props.end.boundShapeId) {
if (Vec2d.Equals(next.props.start.normalizedAnchor, next.props.end.normalizedAnchor)) {
if (Vec.Equals(next.props.start.normalizedAnchor, next.props.end.normalizedAnchor)) {
next.props.end.normalizedAnchor.x += 0.05
}
}
@ -501,7 +503,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
) && !this.editor.getInstanceState().isReadonly
const info = this.editor.getArrowInfo(shape)
const bounds = Box2d.ZeroFix(this.editor.getShapeGeometry(shape).bounds)
const bounds = Box.ZeroFix(this.editor.getShapeGeometry(shape).bounds)
// eslint-disable-next-line react-hooks/rules-of-hooks
const changeIndex = React.useMemo<number>(() => {
@ -524,7 +526,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
const sw = 2
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
info.isStraight
? Vec2d.Dist(info.start.handle, info.end.handle)
? Vec.Dist(info.start.handle, info.end.handle)
: Math.abs(info.handleArc.length),
sw,
{
@ -686,7 +688,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
const isEditing = useIsEditing(shape.id)
if (!info) return null
if (Vec2d.Equals(start, end)) return null
if (Vec.Equals(start, end)) return null
const strokeWidth = STROKE_SIZES[shape.props.size]

View file

@ -1,7 +1,4 @@
import { Vec2d, VecLike } from '../../../../primitives/Vec2d'
import { intersectCircleCircle } from '../../../../primitives/intersect'
import { PI, TAU } from '../../../../primitives/utils'
import { TLArrowInfo } from './arrow-types'
import { HALF_PI, PI, TLArrowInfo, Vec, VecLike, intersectCircleCircle } from '@tldraw/editor'
type TLArrowPointsInfo = {
point: VecLike
@ -16,14 +13,14 @@ function getArrowPoints(
const PT = side === 'end' ? info.end.point : info.start.point
const PB = side === 'end' ? info.start.point : info.end.point
const compareLength = info.isStraight ? Vec2d.Dist(PB, PT) : Math.abs(info.bodyArc.length) // todo: arc length for curved arrows
const compareLength = info.isStraight ? Vec.Dist(PB, PT) : Math.abs(info.bodyArc.length) // todo: arc length for curved arrows
const length = Math.max(Math.min(compareLength / 5, strokeWidth * 3), strokeWidth)
let P0: VecLike
if (info.isStraight) {
P0 = Vec2d.Nudge(PT, PB, length)
P0 = Vec.Nudge(PT, PB, length)
} else {
const ints = intersectCircleCircle(PT, length, info.handleArc.center, info.handleArc.radius)
P0 =
@ -42,72 +39,68 @@ function getArrowPoints(
}
}
export function getArrowhead({ point, int }: TLArrowPointsInfo) {
const PL = Vec2d.RotWith(int, point, PI / 6)
const PR = Vec2d.RotWith(int, point, -PI / 6)
function getArrowhead({ point, int }: TLArrowPointsInfo) {
const PL = Vec.RotWith(int, point, PI / 6)
const PR = Vec.RotWith(int, point, -PI / 6)
return `M ${PL.x} ${PL.y} L ${point.x} ${point.y} L ${PR.x} ${PR.y}`
}
export function getTriangleHead({ point, int }: TLArrowPointsInfo) {
const PL = Vec2d.RotWith(int, point, PI / 6)
const PR = Vec2d.RotWith(int, point, -PI / 6)
function getTriangleHead({ point, int }: TLArrowPointsInfo) {
const PL = Vec.RotWith(int, point, PI / 6)
const PR = Vec.RotWith(int, point, -PI / 6)
return `M ${PL.x} ${PL.y} L ${point.x} ${point.y} L ${PR.x} ${PR.y} Z`
}
export function getInvertedTriangleHead({ point, int }: TLArrowPointsInfo) {
const d = Vec2d.Sub(int, point).div(2)
const PL = Vec2d.Add(point, Vec2d.Rot(d, TAU))
const PR = Vec2d.Sub(point, Vec2d.Rot(d, TAU))
function getInvertedTriangleHead({ point, int }: TLArrowPointsInfo) {
const d = Vec.Sub(int, point).div(2)
const PL = Vec.Add(point, Vec.Rot(d, HALF_PI))
const PR = Vec.Sub(point, Vec.Rot(d, HALF_PI))
return `M ${PL.x} ${PL.y} L ${int.x} ${int.y} L ${PR.x} ${PR.y} Z`
}
export function getDotHead({ point, int }: TLArrowPointsInfo) {
const A = Vec2d.Lrp(point, int, 0.45)
const r = Vec2d.Dist(A, point)
function getDotHead({ point, int }: TLArrowPointsInfo) {
const A = Vec.Lrp(point, int, 0.45)
const r = Vec.Dist(A, point)
return `M ${A.x - r},${A.y}
a ${r},${r} 0 1,0 ${r * 2},0
a ${r},${r} 0 1,0 -${r * 2},0 `
}
export function getDiamondHead({ point, int }: TLArrowPointsInfo) {
const PB = Vec2d.Lrp(point, int, 0.75)
const PL = Vec2d.RotWith(PB, point, PI / 4)
const PR = Vec2d.RotWith(PB, point, -PI / 4)
function getDiamondHead({ point, int }: TLArrowPointsInfo) {
const PB = Vec.Lrp(point, int, 0.75)
const PL = Vec.RotWith(PB, point, PI / 4)
const PR = Vec.RotWith(PB, point, -PI / 4)
const PQ = Vec2d.Lrp(PL, PR, 0.5)
PQ.add(Vec2d.Sub(PQ, point))
const PQ = Vec.Lrp(PL, PR, 0.5)
PQ.add(Vec.Sub(PQ, point))
return `M ${PQ.x} ${PQ.y} L ${PL.x} ${PL.y} ${point.x} ${point.y} L ${PR.x} ${PR.y} Z`
}
export function getSquareHead({ int, point }: TLArrowPointsInfo) {
const PB = Vec2d.Lrp(point, int, 0.85)
const d = Vec2d.Sub(PB, point).div(2)
const PL1 = Vec2d.Add(point, Vec2d.Rot(d, TAU))
const PR1 = Vec2d.Sub(point, Vec2d.Rot(d, TAU))
const PL2 = Vec2d.Add(PB, Vec2d.Rot(d, TAU))
const PR2 = Vec2d.Sub(PB, Vec2d.Rot(d, TAU))
function getSquareHead({ int, point }: TLArrowPointsInfo) {
const PB = Vec.Lrp(point, int, 0.85)
const d = Vec.Sub(PB, point).div(2)
const PL1 = Vec.Add(point, Vec.Rot(d, HALF_PI))
const PR1 = Vec.Sub(point, Vec.Rot(d, HALF_PI))
const PL2 = Vec.Add(PB, Vec.Rot(d, HALF_PI))
const PR2 = Vec.Sub(PB, Vec.Rot(d, HALF_PI))
return `M ${PL1.x} ${PL1.y} L ${PL2.x} ${PL2.y} L ${PR2.x} ${PR2.y} L ${PR1.x} ${PR1.y} Z`
}
export function getBarHead({ int, point }: TLArrowPointsInfo) {
const d = Vec2d.Sub(int, point).div(2)
function getBarHead({ int, point }: TLArrowPointsInfo) {
const d = Vec.Sub(int, point).div(2)
const PL = Vec2d.Add(point, Vec2d.Rot(d, TAU))
const PR = Vec2d.Sub(point, Vec2d.Rot(d, TAU))
const PL = Vec.Add(point, Vec.Rot(d, HALF_PI))
const PR = Vec.Sub(point, Vec.Rot(d, HALF_PI))
return `M ${PL.x} ${PL.y} L ${PR.x} ${PR.y}`
}
export function getPipeHead() {
return ''
}
/** @public */
export function getArrowheadPathForType(
info: TLArrowInfo,

View file

@ -0,0 +1,49 @@
import { TLArrowInfo, VecLike } from '@tldraw/editor'
/* --------------------- Curved --------------------- */
/**
* Get a solid path for a curved arrow's handles.
*
* @param info - The arrow info.
* @public
*/
export function getCurvedArrowHandlePath(info: TLArrowInfo & { isStraight: false }) {
const {
start,
end,
handleArc: { radius, largeArcFlag, sweepFlag },
} = info
return `M${start.handle.x},${start.handle.y} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end.handle.x},${end.handle.y}`
}
/**
* Get a solid path for a curved arrow's body.
*
* @param info - The arrow info.
* @public
*/
export function getSolidCurvedArrowPath(info: TLArrowInfo & { isStraight: false }) {
const {
start,
end,
bodyArc: { radius, largeArcFlag, sweepFlag },
} = info
return `M${start.point.x},${start.point.y} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end.point.x},${end.point.y}`
}
/* -------------------- Straight -------------------- */
function getArrowPath(start: VecLike, end: VecLike) {
return `M${start.x},${start.y}L${end.x},${end.y}`
}
/** @public */
export function getStraightArrowHandlePath(info: TLArrowInfo & { isStraight: true }) {
return getArrowPath(info.start.handle, info.end.handle)
}
/** @public */
export function getSolidStraightArrowPath(info: TLArrowInfo & { isStraight: true }) {
return getArrowPath(info.start.point, info.end.point)
}

View file

@ -1,6 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */
import {
Box2d,
Box,
Circle2d,
Polygon2d,
Polyline2d,
@ -60,7 +60,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
// A dot
if (shape.props.segments.length === 1) {
const box = Box2d.FromPoints(points)
const box = Box.FromPoints(points)
if (box.width < strokeWidth * 2 && box.height < strokeWidth * 2) {
return new Circle2d({
x: -strokeWidth,

View file

@ -5,7 +5,7 @@ import {
TLDefaultDashStyle,
TLDrawShape,
TLDrawShapeSegment,
Vec2d,
Vec,
} from '@tldraw/editor'
import { StrokeOptions } from '../shared/freehand/types'
@ -81,17 +81,17 @@ export function getFreehandOptions(
}
export function getPointsFromSegments(segments: TLDrawShapeSegment[]) {
const points: Vec2d[] = []
const points: Vec[] = []
for (const segment of segments) {
if (segment.type === 'free' || segment.points.length < 2) {
points.push(...segment.points.map(Vec2d.Cast))
points.push(...segment.points.map(Vec.Cast))
} else {
const pointsToInterpolate = Math.max(
4,
Math.floor(Vec2d.Dist(segment.points[0], segment.points[1]) / 16)
Math.floor(Vec.Dist(segment.points[0], segment.points[1]) / 16)
)
points.push(...Vec2d.PointsBetween(segment.points[0], segment.points[1], pointsToInterpolate))
points.push(...Vec.PointsBetween(segment.points[0], segment.points[1], pointsToInterpolate))
}
}

View file

@ -1,6 +1,6 @@
import {
DRAG_DISTANCE,
Matrix2d,
Mat,
StateNode,
TLDefaultSizeStyle,
TLDrawShape,
@ -9,8 +9,8 @@ import {
TLHighlightShape,
TLPointerEventInfo,
TLShapePartial,
Vec2d,
Vec2dModel,
Vec,
VecModel,
createShapeId,
last,
snapAngle,
@ -38,11 +38,11 @@ export class Drawing extends StateNode {
didJustShiftClickToExtendPreviousShapeLine = false
pagePointWhereCurrentSegmentChanged = {} as Vec2d
pagePointWhereCurrentSegmentChanged = {} as Vec
pagePointWhereNextSegmentChanged = null as Vec2d | null
pagePointWhereNextSegmentChanged = null as Vec | null
lastRecordedPoint = {} as Vec2d
lastRecordedPoint = {} as Vec
mergeNextPoint = false
currentLineLength = 0
@ -86,7 +86,7 @@ export class Drawing extends StateNode {
// Don't update the shape if we haven't moved far enough from the last time we recorded a point
if (inputs.isPen) {
if (
Vec2d.Dist(inputs.currentPagePoint, this.lastRecordedPoint) >=
Vec.Dist(inputs.currentPagePoint, this.lastRecordedPoint) >=
1 / this.editor.getZoomLevel()
) {
this.lastRecordedPoint = inputs.currentPagePoint.clone()
@ -161,7 +161,7 @@ export class Drawing extends StateNode {
return (
firstPoint !== lastPoint &&
this.currentLineLength > strokeWidth * 4 &&
Vec2d.Dist(firstPoint, lastPoint) < strokeWidth * 2
Vec.Dist(firstPoint, lastPoint) < strokeWidth * 2
)
}
@ -217,7 +217,7 @@ export class Drawing extends StateNode {
}
// Convert prevPoint to page space
const prevPointPageSpace = Matrix2d.applyToPoint(
const prevPointPageSpace = Mat.applyToPoint(
this.editor.getShapePageTransform(shape.id)!,
prevPoint
)
@ -310,7 +310,7 @@ export class Drawing extends StateNode {
}
const hasMovedFarEnough =
Vec2d.Dist(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
Vec.Dist(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
// Find the distance from where the pointer was when shift was released and
// where it is now; if it's far enough away, then update the page point where
@ -337,7 +337,7 @@ export class Drawing extends StateNode {
.toJson()
if (prevSegment.type === 'straight') {
this.currentLineLength += Vec2d.Dist(prevLastPoint, newLastPoint)
this.currentLineLength += Vec.Dist(prevLastPoint, newLastPoint)
newSegment = {
type: 'straight',
@ -346,10 +346,7 @@ export class Drawing extends StateNode {
const transform = this.editor.getShapePageTransform(shape)!
this.pagePointWhereCurrentSegmentChanged = Matrix2d.applyToPoint(
transform,
prevLastPoint
)
this.pagePointWhereCurrentSegmentChanged = Mat.applyToPoint(transform, prevLastPoint)
} else {
newSegment = {
type: 'straight',
@ -386,7 +383,7 @@ export class Drawing extends StateNode {
}
const hasMovedFarEnough =
Vec2d.Dist(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
Vec.Dist(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > DRAG_DISTANCE
// Find the distance from where the pointer was when shift was released and
// where it is now; if it's far enough away, then update the page point where
@ -411,9 +408,7 @@ export class Drawing extends StateNode {
// ended and where the pointer is now
const newFreeSegment: TLDrawShapeSegment = {
type: 'free',
points: [
...Vec2d.PointsBetween(prevPoint, newPoint, 6).map((p) => p.toFixed().toJson()),
],
points: [...Vec.PointsBetween(prevPoint, newPoint, 6).map((p) => p.toFixed().toJson())],
}
const finalSegments = [...newSegments, newFreeSegment]
@ -449,7 +444,7 @@ export class Drawing extends StateNode {
if (!pagePointWhereCurrentSegmentChanged)
throw Error('We should have a point where the segment changed')
let pagePoint: Vec2dModel
let pagePoint: VecModel
let shouldSnapToAngle = false
if (this.didJustShiftClickToExtendPreviousShapeLine) {
@ -473,7 +468,7 @@ export class Drawing extends StateNode {
if (shouldSnap) {
if (newSegments.length > 2) {
let nearestPoint: Vec2dModel | undefined = undefined
let nearestPoint: VecModel | undefined = undefined
let minDistance = 8 / this.editor.getZoomLevel()
// Don't try to snap to the last two segments
@ -487,12 +482,12 @@ export class Drawing extends StateNode {
if (!(first && lastPoint)) continue
// Snap to the nearest point on the segment, if it's closer than the previous snapped point
const nearestPointOnSegment = Vec2d.NearestPointOnLineSegment(
const nearestPointOnSegment = Vec.NearestPointOnLineSegment(
first,
lastPoint,
newPoint
)
const distance = Vec2d.Dist(nearestPointOnSegment, newPoint)
const distance = Vec.Dist(nearestPointOnSegment, newPoint)
if (distance < minDistance) {
nearestPoint = nearestPointOnSegment.toFixed().toJson()
@ -515,11 +510,11 @@ export class Drawing extends StateNode {
const lastPoint = last(snapSegment.points)
if (!lastPoint) throw Error('Expected a last point!')
const A = Matrix2d.applyToPoint(transform, first)
const A = Mat.applyToPoint(transform, first)
const B = Matrix2d.applyToPoint(transform, lastPoint)
const B = Mat.applyToPoint(transform, lastPoint)
const snappedPoint = Matrix2d.applyToPoint(transform, newPoint)
const snappedPoint = Mat.applyToPoint(transform, newPoint)
this.editor.snaps.setLines([
{
@ -533,11 +528,11 @@ export class Drawing extends StateNode {
if (shouldSnapToAngle) {
// Snap line angle to nearest 15 degrees
const currentAngle = Vec2d.Angle(pagePointWhereCurrentSegmentChanged, currentPagePoint)
const currentAngle = Vec.Angle(pagePointWhereCurrentSegmentChanged, currentPagePoint)
const snappedAngle = snapAngle(currentAngle, 24)
const angleDiff = snappedAngle - currentAngle
pagePoint = Vec2d.RotWith(
pagePoint = Vec.RotWith(
currentPagePoint,
pagePointWhereCurrentSegmentChanged,
angleDiff
@ -553,7 +548,7 @@ export class Drawing extends StateNode {
// then the user just did a click-and-immediately-press-shift to create a new straight line
// without continuing the previous line. In this case, we want to remove the previous segment.
this.currentLineLength += Vec2d.Dist(newSegment.points[0], newPoint)
this.currentLineLength += Vec.Dist(newSegment.points[0], newPoint)
newSegments[newSegments.length - 1] = {
...newSegment,
@ -595,7 +590,7 @@ export class Drawing extends StateNode {
// Note: we could recompute the line length here, but it's not really necessary
// this.currentLineLength = this.getLineLength(newSegments)
} else {
this.currentLineLength += Vec2d.Dist(newPoints[newPoints.length - 1], newPoint)
this.currentLineLength += Vec.Dist(newPoints[newPoints.length - 1], newPoint)
newPoints.push(newPoint)
}
@ -667,7 +662,7 @@ export class Drawing extends StateNode {
for (let i = 0; i < segment.points.length - 1; i++) {
const A = segment.points[i]
const B = segment.points[i + 1]
length += Vec2d.Sub(B, A).len2()
length += Vec.Sub(B, A).len2()
}
}

View file

@ -5,6 +5,7 @@ import {
Ellipse2d,
Geometry2d,
Group2d,
HALF_PI,
HTMLContainer,
PI2,
Polygon2d,
@ -13,13 +14,12 @@ import {
SVGContainer,
Stadium2d,
SvgExportContext,
TAU,
TLDefaultDashStyle,
TLGeoShape,
TLOnEditEndHandler,
TLOnResizeHandler,
TLShapeUtilCanvasSvgDef,
Vec2d,
Vec,
VecLike,
geoShapeMigrations,
geoShapeProps,
@ -110,14 +110,14 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
}
case 'triangle': {
body = new Polygon2d({
points: [new Vec2d(cx, 0), new Vec2d(w, h), new Vec2d(0, h)],
points: [new Vec(cx, 0), new Vec(w, h), new Vec(0, h)],
isFilled,
})
break
}
case 'diamond': {
body = new Polygon2d({
points: [new Vec2d(cx, 0), new Vec2d(w, cy), new Vec2d(cx, h), new Vec2d(0, cy)],
points: [new Vec(cx, 0), new Vec(w, cy), new Vec(cx, h), new Vec(0, cy)],
isFilled,
})
break
@ -170,11 +170,11 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
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 maxX = (Math.cos(-HALF_PI + rightMostIndex * step) * w) / 2
const minX = (Math.cos(-HALF_PI + leftMostIndex * step) * w) / 2
const minY = (Math.sin(-TAU + topMostIndex * step) * h) / 2
const maxY = (Math.sin(-TAU + bottomMostIndex * step) * h) / 2
const minY = (Math.sin(-HALF_PI + topMostIndex * step) * h) / 2
const maxY = (Math.sin(-HALF_PI + bottomMostIndex * step) * h) / 2
const diffX = w - Math.abs(maxX - minX)
const diffY = h - Math.abs(maxY - minY)
const offsetX = w / 2 + minX - (w / 2 - maxX)
@ -190,8 +190,8 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
body = new Polygon2d({
points: Array.from(Array(sides * 2)).map((_, i) => {
const theta = -TAU + i * step
return new Vec2d(
const theta = -HALF_PI + i * step
return new Vec(
cx + (i % 2 ? ix : ox) * Math.cos(theta),
cy + (i % 2 ? iy : oy) * Math.sin(theta)
)
@ -203,12 +203,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
case 'rhombus': {
const offset = Math.min(w * 0.38, h * 0.38)
body = new Polygon2d({
points: [
new Vec2d(offset, 0),
new Vec2d(w, 0),
new Vec2d(w - offset, h),
new Vec2d(0, h),
],
points: [new Vec(offset, 0), new Vec(w, 0), new Vec(w - offset, h), new Vec(0, h)],
isFilled,
})
break
@ -216,12 +211,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
case 'rhombus-2': {
const offset = Math.min(w * 0.38, h * 0.38)
body = new Polygon2d({
points: [
new Vec2d(0, 0),
new Vec2d(w - offset, 0),
new Vec2d(w, h),
new Vec2d(offset, h),
],
points: [new Vec(0, 0), new Vec(w - offset, 0), new Vec(w, h), new Vec(offset, h)],
isFilled,
})
break
@ -229,12 +219,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
case 'trapezoid': {
const offset = Math.min(w * 0.38, h * 0.38)
body = new Polygon2d({
points: [
new Vec2d(offset, 0),
new Vec2d(w - offset, 0),
new Vec2d(w, h),
new Vec2d(0, h),
],
points: [new Vec(offset, 0), new Vec(w - offset, 0), new Vec(w, h), new Vec(0, h)],
isFilled,
})
break
@ -244,13 +229,13 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
const oy = h * 0.16
body = new Polygon2d({
points: [
new Vec2d(0, oy),
new Vec2d(w - ox, oy),
new Vec2d(w - ox, 0),
new Vec2d(w, h / 2),
new Vec2d(w - ox, h),
new Vec2d(w - ox, h - oy),
new Vec2d(0, h - oy),
new Vec(0, oy),
new Vec(w - ox, oy),
new Vec(w - ox, 0),
new Vec(w, h / 2),
new Vec(w - ox, h),
new Vec(w - ox, h - oy),
new Vec(0, h - oy),
],
isFilled,
})
@ -261,13 +246,13 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
const oy = h * 0.16
body = new Polygon2d({
points: [
new Vec2d(ox, 0),
new Vec2d(ox, oy),
new Vec2d(w, oy),
new Vec2d(w, h - oy),
new Vec2d(ox, h - oy),
new Vec2d(ox, h),
new Vec2d(0, h / 2),
new Vec(ox, 0),
new Vec(ox, oy),
new Vec(w, oy),
new Vec(w, h - oy),
new Vec(ox, h - oy),
new Vec(ox, h),
new Vec(0, h / 2),
],
isFilled,
})
@ -278,13 +263,13 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
const oy = Math.min(w, h) * 0.38
body = new Polygon2d({
points: [
new Vec2d(w / 2, 0),
new Vec2d(w, oy),
new Vec2d(w - ox, oy),
new Vec2d(w - ox, h),
new Vec2d(ox, h),
new Vec2d(ox, oy),
new Vec2d(0, oy),
new Vec(w / 2, 0),
new Vec(w, oy),
new Vec(w - ox, oy),
new Vec(w - ox, h),
new Vec(ox, h),
new Vec(ox, oy),
new Vec(0, oy),
],
isFilled,
})
@ -295,13 +280,13 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
const oy = Math.min(w, h) * 0.38
body = new Polygon2d({
points: [
new Vec2d(ox, 0),
new Vec2d(w - ox, 0),
new Vec2d(w - ox, h - oy),
new Vec2d(w, h - oy),
new Vec2d(w / 2, h),
new Vec2d(0, h - oy),
new Vec2d(ox, h - oy),
new Vec(ox, 0),
new Vec(w - ox, 0),
new Vec(w - ox, h - oy),
new Vec(w, h - oy),
new Vec(w / 2, h),
new Vec(0, h - oy),
new Vec(ox, h - oy),
],
isFilled,
})
@ -862,7 +847,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
h = nextH
}
const offset = new Vec2d(0, 0)
const offset = new Vec(0, 0)
// x offsets
@ -1113,10 +1098,10 @@ function getXBoxLines(w: number, h: number, sw: number, dash: TLDefaultDashStyle
if (dash === 'dashed') {
return [
[new Vec2d(0, 0), new Vec2d(w / 2, h / 2)],
[new Vec2d(w, h), new Vec2d(w / 2, h / 2)],
[new Vec2d(0, h), new Vec2d(w / 2, h / 2)],
[new Vec2d(w, 0), new Vec2d(w / 2, h / 2)],
[new Vec(0, 0), new Vec(w / 2, h / 2)],
[new Vec(w, h), new Vec(w / 2, h / 2)],
[new Vec(0, h), new Vec(w / 2, h / 2)],
[new Vec(w, 0), new Vec(w / 2, h / 2)],
]
}
@ -1125,12 +1110,12 @@ function getXBoxLines(w: number, h: number, sw: number, dash: TLDefaultDashStyle
return [
[
new Vec2d(clampX(sw * inset), clampY(sw * inset)),
new Vec2d(clampX(w - sw * inset), clampY(h - sw * inset)),
new Vec(clampX(sw * inset), clampY(sw * inset)),
new Vec(clampX(w - sw * inset), clampY(h - sw * inset)),
],
[
new Vec2d(clampX(sw * inset), clampY(h - sw * inset)),
new Vec2d(clampX(w - sw * inset), clampY(sw * inset)),
new Vec(clampX(sw * inset), clampY(h - sw * inset)),
new Vec(clampX(w - sw * inset), clampY(sw * inset)),
],
]
}
@ -1145,12 +1130,12 @@ function getCheckBoxLines(w: number, h: number) {
return [
[
new Vec2d(clampX(ox + size * 0.25), clampY(oy + size * 0.52)),
new Vec2d(clampX(ox + size * 0.45), clampY(oy + size * 0.82)),
new Vec(clampX(ox + size * 0.25), clampY(oy + size * 0.52)),
new Vec(clampX(ox + size * 0.45), clampY(oy + size * 0.82)),
],
[
new Vec2d(clampX(ox + size * 0.45), clampY(oy + size * 0.82)),
new Vec2d(clampX(ox + size * 0.82), clampY(oy + size * 0.22)),
new Vec(clampX(ox + size * 0.45), clampY(oy + size * 0.82)),
new Vec(clampX(ox + size * 0.82), clampY(oy + size * 0.22)),
],
]
}
@ -1168,5 +1153,5 @@ export function getCentroidOfRegularPolygon(points: VecLike[]) {
x += points[i].x
y += points[i].y
}
return new Vec2d(x / len, y / len)
return new Vec(x / len, y / len)
}

View file

@ -1,8 +1,8 @@
import {
PI,
TLDefaultSizeStyle,
Vec2d,
Vec2dModel,
Vec,
VecModel,
clockwiseAngleDist,
getPointOnCircle,
rng,
@ -19,12 +19,12 @@ function getPillCircumference(width: number, height: number) {
type PillSection =
| {
type: 'straight'
start: Vec2dModel
delta: Vec2dModel
start: VecModel
delta: VecModel
}
| {
type: 'arc'
center: Vec2dModel
center: VecModel
startAngle: number
}
@ -41,55 +41,55 @@ export function getPillPoints(width: number, height: number, numPoints: number)
? [
{
type: 'straight',
start: new Vec2d(radius, 0),
delta: new Vec2d(1, 0),
start: new Vec(radius, 0),
delta: new Vec(1, 0),
},
{
type: 'arc',
center: new Vec2d(width - radius, radius),
center: new Vec(width - radius, radius),
startAngle: -PI / 2,
},
{
type: 'straight',
start: new Vec2d(width - radius, height),
delta: new Vec2d(-1, 0),
start: new Vec(width - radius, height),
delta: new Vec(-1, 0),
},
{
type: 'arc',
center: new Vec2d(radius, radius),
center: new Vec(radius, radius),
startAngle: PI / 2,
},
]
: [
{
type: 'straight',
start: new Vec2d(width, radius),
delta: new Vec2d(0, 1),
start: new Vec(width, radius),
delta: new Vec(0, 1),
},
{
type: 'arc',
center: new Vec2d(radius, height - radius),
center: new Vec(radius, height - radius),
startAngle: 0,
},
{
type: 'straight',
start: new Vec2d(0, height - radius),
delta: new Vec2d(0, -1),
start: new Vec(0, height - radius),
delta: new Vec(0, -1),
},
{
type: 'arc',
center: new Vec2d(radius, radius),
center: new Vec(radius, radius),
startAngle: PI,
},
]
let sectionOffset = 0
const points: Vec2d[] = []
const points: Vec[] = []
for (let i = 0; i < numPoints; i++) {
const section = sections[0]
if (section.type === 'straight') {
points.push(Vec2d.Add(section.start, Vec2d.Mul(section.delta, sectionOffset)))
points.push(Vec.Add(section.start, Vec.Mul(section.delta, sectionOffset)))
} else {
points.push(
getPointOnCircle(
@ -159,12 +159,12 @@ export function getCloudArcs(
// in at the bottom-right and the top-left looks relatively stable
const wiggledPoints = bumpPoints.slice(0)
for (let i = 0; i < Math.floor(numBumps / 2); i++) {
wiggledPoints[i] = Vec2d.AddXY(
wiggledPoints[i] = Vec.AddXY(
wiggledPoints[i],
getRandom() * maxWiggleX,
getRandom() * maxWiggleY
)
wiggledPoints[numBumps - i - 1] = Vec2d.AddXY(
wiggledPoints[numBumps - i - 1] = Vec.AddXY(
wiggledPoints[numBumps - i - 1],
getRandom() * maxWiggleX,
getRandom() * maxWiggleY
@ -180,17 +180,17 @@ export function getCloudArcs(
const leftPoint = bumpPoints[i]
const rightPoint = bumpPoints[j]
const midPoint = Vec2d.Average([leftPoint, rightPoint])
const offsetAngle = Vec2d.Angle(leftPoint, rightPoint) - Math.PI / 2
const midPoint = Vec.Average([leftPoint, rightPoint])
const offsetAngle = Vec.Angle(leftPoint, rightPoint) - Math.PI / 2
// when the points are on the curvy part of a pill, there is a natural arc that we need to extends past
// otherwise it looks like the bumps get less bumpy on the curvy parts
const distanceBetweenOriginalPoints = Vec2d.Dist(leftPoint, rightPoint)
const distanceBetweenOriginalPoints = Vec.Dist(leftPoint, rightPoint)
const curvatureOffset = distanceBetweenPointsOnPerimeter - distanceBetweenOriginalPoints
const distanceBetweenWigglePoints = Vec2d.Dist(leftWigglePoint, rightWigglePoint)
const distanceBetweenWigglePoints = Vec.Dist(leftWigglePoint, rightWigglePoint)
const relativeSize = distanceBetweenWigglePoints / distanceBetweenOriginalPoints
const finalDistance = (Math.max(paddingX, paddingY) + curvatureOffset) * relativeSize
const arcPoint = Vec2d.Add(midPoint, Vec2d.FromAngle(offsetAngle, finalDistance))
const arcPoint = Vec.Add(midPoint, Vec.FromAngle(offsetAngle, finalDistance))
if (arcPoint.x < 0) {
arcPoint.x = 0
} else if (arcPoint.x > width) {
@ -203,8 +203,8 @@ export function getCloudArcs(
}
const center = getCenterOfCircleGivenThreePoints(leftWigglePoint, rightWigglePoint, arcPoint)
const radius = Vec2d.Dist(
center ? center : Vec2d.Average([leftWigglePoint, rightWigglePoint]),
const radius = Vec.Dist(
center ? center : Vec.Average([leftWigglePoint, rightWigglePoint]),
leftWigglePoint
)
@ -221,14 +221,14 @@ export function getCloudArcs(
}
type Arc = {
leftPoint: Vec2d
rightPoint: Vec2d
arcPoint: Vec2d
center: Vec2d | null
leftPoint: Vec
rightPoint: Vec
arcPoint: Vec
center: Vec | null
radius: number
}
function getCenterOfCircleGivenThreePoints(a: Vec2d, b: Vec2d, c: Vec2d) {
function getCenterOfCircleGivenThreePoints(a: Vec, b: Vec, c: Vec) {
const A = a.x * (b.y - c.y) - a.y * (b.x - c.x) + b.x * c.y - c.x * b.y
const B =
(a.x * a.x + a.y * a.y) * (c.y - b.y) +
@ -247,7 +247,7 @@ function getCenterOfCircleGivenThreePoints(a: Vec2d, b: Vec2d, c: Vec2d) {
return null
}
return new Vec2d(x, y)
return new Vec(x, y)
}
export function cloudOutline(
@ -256,7 +256,7 @@ export function cloudOutline(
seed: string,
size: TLDefaultSizeStyle
) {
const path: Vec2d[] = []
const path: Vec[] = []
const arcs = getCloudArcs(width, height, seed, size)
@ -316,12 +316,10 @@ export function inkyCloudSvgPath(
}
const arcs = getCloudArcs(width, height, seed, size)
const avgArcLength =
arcs.reduce((sum, arc) => sum + Vec2d.Dist(arc.leftPoint, arc.rightPoint), 0) / arcs.length
arcs.reduce((sum, arc) => sum + Vec.Dist(arc.leftPoint, arc.rightPoint), 0) / arcs.length
const shouldMutatePoints = avgArcLength > mutMultiplier * 15
const mutPoint = shouldMutatePoints
? (p: Vec2d) => new Vec2d(mut(p.x), mut(p.y))
: (p: Vec2d) => p
const mutPoint = shouldMutatePoints ? (p: Vec) => new Vec(mut(p.x), mut(p.y)) : (p: Vec) => p
let pathA = `M${toDomPrecision(arcs[0].leftPoint.x)},${toDomPrecision(arcs[0].leftPoint.y)}`
let leftMutPoint = mutPoint(arcs[0].leftPoint)
let pathB = `M${toDomPrecision(leftMutPoint.x)},${toDomPrecision(leftMutPoint.y)}`
@ -348,7 +346,7 @@ export function inkyCloudSvgPath(
leftMutPoint = rightMutPoint
continue
}
const mutRadius = Math.abs(Vec2d.Dist(mutCenter, leftMutPoint))
const mutRadius = Math.abs(Vec.Dist(mutCenter, leftMutPoint))
pathB += ` A${toDomPrecision(mutRadius)},${toDomPrecision(
mutRadius
@ -360,19 +358,19 @@ export function inkyCloudSvgPath(
}
export function pointsOnArc(
startPoint: Vec2dModel,
endPoint: Vec2dModel,
center: Vec2dModel | null,
startPoint: VecModel,
endPoint: VecModel,
center: VecModel | null,
radius: number,
numPoints: number
): Vec2d[] {
): Vec[] {
if (center === null) {
return [Vec2d.From(startPoint), Vec2d.From(endPoint)]
return [Vec.From(startPoint), Vec.From(endPoint)]
}
const results: Vec2d[] = []
const results: Vec[] = []
const startAngle = Vec2d.Angle(center, startPoint)
const endAngle = Vec2d.Angle(center, endPoint)
const startAngle = Vec.Angle(center, startPoint)
const endAngle = Vec.Angle(center, endPoint)
const l = clockwiseAngleDist(startAngle, endAngle)
@ -386,6 +384,6 @@ export function pointsOnArc(
return results
}
function isLeft(a: Vec2d, b: Vec2d, c: Vec2d) {
function isLeft(a: Vec, b: Vec, c: Vec) {
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) > 0
}

View file

@ -2,7 +2,7 @@ import {
TLDefaultColorTheme,
TLGeoShape,
TLShapeId,
Vec2d,
Vec,
canonicalizeRotation,
} from '@tldraw/editor'
import * as React from 'react'
@ -40,10 +40,10 @@ export const DashStyleCloud = React.memo(function DashStylePolygon({
const arcLength = center
? radius *
canonicalizeRotation(
canonicalizeRotation(Vec2d.Angle(center, rightPoint)) -
canonicalizeRotation(Vec2d.Angle(center, leftPoint))
canonicalizeRotation(Vec.Angle(center, rightPoint)) -
canonicalizeRotation(Vec.Angle(center, leftPoint))
)
: Vec2d.Dist(leftPoint, rightPoint)
: Vec.Dist(leftPoint, rightPoint)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
arcLength,
@ -100,10 +100,10 @@ export function DashStyleCloudSvg({
const arcLength = center
? radius *
canonicalizeRotation(
canonicalizeRotation(Vec2d.Angle(center, rightPoint)) -
canonicalizeRotation(Vec2d.Angle(center, leftPoint))
canonicalizeRotation(Vec.Angle(center, rightPoint)) -
canonicalizeRotation(Vec.Angle(center, leftPoint))
)
: Vec2d.Dist(leftPoint, rightPoint)
: Vec.Dist(leftPoint, rightPoint)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(arcLength, strokeWidth, {
style: dash,

View file

@ -1,4 +1,4 @@
import { TLDefaultColorTheme, TLGeoShape, Vec2d, VecLike } from '@tldraw/editor'
import { TLDefaultColorTheme, TLGeoShape, Vec, VecLike } from '@tldraw/editor'
import * as React from 'react'
import {
ShapeFill,
@ -31,7 +31,7 @@ export const DashStylePolygon = React.memo(function DashStylePolygon({
const A = outline[i]
const B = outline[(i + 1) % outline.length]
const dist = Vec2d.Dist(A, B)
const dist = Vec.Dist(A, B)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(dist, strokeWidth, {
style: dash,
@ -53,7 +53,7 @@ export const DashStylePolygon = React.memo(function DashStylePolygon({
})}
{lines &&
lines.map(([A, B], i) => {
const dist = Vec2d.Dist(A, B)
const dist = Vec.Dist(A, B)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(dist, strokeWidth, {
style: dash,
@ -102,7 +102,7 @@ export function DashStylePolygonSvg({
const A = outline[i]
const B = outline[(i + 1) % outline.length]
const dist = Vec2d.Dist(A, B)
const dist = Vec.Dist(A, B)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(dist, strokeWidth, {
style: dash,
})
@ -120,7 +120,7 @@ export function DashStylePolygonSvg({
if (lines) {
for (const [A, B] of lines) {
const dist = Vec2d.Dist(A, B)
const dist = Vec.Dist(A, B)
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(dist, strokeWidth, {
style: dash,
start: 'skip',

View file

@ -1,11 +1,11 @@
import {
EASINGS,
HALF_PI,
PI2,
TAU,
TLDefaultColorTheme,
TLGeoShape,
TLShapeId,
Vec2d,
Vec,
getSvgPathFromPoints,
perimeterOfEllipse,
rng,
@ -98,10 +98,10 @@ export function getEllipseStrokePoints(
const ry = height / 2
const perimeter = perimeterOfEllipse(rx, ry)
const points: Vec2d[] = []
const points: Vec[] = []
const start = PI2 * getRandom()
const length = PI2 + TAU / 2 + Math.abs(getRandom()) * TAU
const length = PI2 + HALF_PI / 2 + Math.abs(getRandom()) * HALF_PI
const count = Math.max(16, perimeter / 10)
for (let i = 0; i < count; i++) {
@ -110,7 +110,7 @@ export function getEllipseStrokePoints(
const c = Math.cos(r)
const s = Math.sin(r)
points.push(
new Vec2d(
new Vec(
rx * c + width * 0.5 + 0.05 * getRandom(),
ry * s + height / 2 + 0.05 * getRandom(),
Math.min(

View file

@ -88,14 +88,14 @@ export function DrawStylePolygonSvg({
}
// function getPolygonDrawPoints(id: string, outline: VecLike[], strokeWidth: number) {
// const points: Vec2d[] = []
// const points: Vec[] = []
// const getRandom = rng(id)
// const start = Math.round(Math.abs(getRandom()) * outline.length)
// const corners = outline.map((p) =>
// Vec2d.AddXY(p, (getRandom() * strokeWidth) / 4, (getRandom() * strokeWidth) / 4)
// Vec.AddXY(p, (getRandom() * strokeWidth) / 4, (getRandom() * strokeWidth) / 4)
// )
// const len = corners.length
@ -104,20 +104,20 @@ export function DrawStylePolygonSvg({
// const At = corners[(start + i) % len]
// const Bt = corners[(start + i + 1) % len]
// const dist = Math.min(Vec2d.Dist(At, Bt) / 2, strokeWidth / 2)
// const A = Vec2d.Nudge(At, Bt, dist)
// const dist = Math.min(Vec.Dist(At, Bt) / 2, strokeWidth / 2)
// const A = Vec.Nudge(At, Bt, dist)
// const D = Vec2d.Med(At, Bt)
// const D = Vec.Med(At, Bt)
// if (i === 0) {
// Bt.z = 0.7
// points.push(new Vec2d(D.x, D.y, 0.7), Bt)
// points.push(new Vec(D.x, D.y, 0.7), Bt)
// } else if (i === outline.length) {
// const lastSegPoints = Vec2d.PointsBetween(A, D, 4)
// const lastSegPoints = Vec.PointsBetween(A, D, 4)
// lastSegPoints.forEach((p) => (p.z = 0.7))
// points.push(...lastSegPoints)
// } else {
// points.push(...Vec2d.PointsBetween(A, Bt, 6))
// points.push(...Vec.PointsBetween(A, Bt, 6))
// }
// }
@ -199,8 +199,8 @@ export function DrawStylePolygonSvg({
// ox = random() * offset
// oy = random() * offset
// const c1 = Vec2d.Lrp(p0, p1, 0.25)
// const c2 = Vec2d.Lrp(p0, p1, 0.75)
// const c1 = Vec.Lrp(p0, p1, 0.25)
// const c2 = Vec.Lrp(p0, p1, 0.75)
// polylineB += `C${c1.x + ox},${c1.y + oy} ${c2.x - ox},${c2.y - oy} ${p1.x},${p1.y}`
// }
@ -224,9 +224,9 @@ export function DrawStylePolygonSvg({
// for (let i = 0, n = len * 2; i < n; i++) {
// p0 = outline[i % len]
// p1 = outline[(i + 1) % len]
// const dist = Vec2d.Dist(p0, p1)
// const dist = Vec.Dist(p0, p1)
// const c1 = Vec2d.Lrp(p0, p1, 0.5 + random() / 2)
// const c1 = Vec.Lrp(p0, p1, 0.5 + random() / 2)
// polylineA += `${c1.x + random() * Math.min(dist / 10, offset)},${
// c1.y + random() * Math.min(dist / 10, offset)
// } ${p1.x + (random() * offset) / 2},${p1.y + (random() * offset) / 2} `
@ -249,9 +249,9 @@ export function DrawStylePolygonSvg({
// for (let i = 0, n = len * 2; i < n; i++) {
// p0 = outline[i % len]
// p1 = outline[(i + 1) % len]
// const dist = Vec2d.Dist(p0, p1)
// const dist = Vec.Dist(p0, p1)
// const c1 = Vec2d.Lrp(p0, p1, 0.5 + random() / 2)
// const c1 = Vec.Lrp(p0, p1, 0.5 + random() / 2)
// polylineA += `${c1.x + random() * Math.min(dist / 10, offset)},${
// c1.y + random() * Math.min(dist / 10, offset)
// } ${p1.x + (random() * offset) / 2},${p1.y + (random() * offset) / 2} `

View file

@ -1,11 +1,10 @@
import {
Box2d,
Box,
GeoShapeGeoStyle,
StateNode,
TLEventHandlers,
TLGeoShape,
createShapeId,
getStarBounds,
} from '@tldraw/editor'
export class Pointing extends StateNode {
@ -93,10 +92,10 @@ export class Pointing extends StateNode {
const bounds =
shape.props.geo === 'star'
? getStarBounds(5, 200, 200)
? new Box(0, 0, 200, 190)
: shape.props.geo === 'cloud'
? new Box2d(0, 0, 300, 180)
: new Box2d(0, 0, 200, 200)
? new Box(0, 0, 300, 180)
: new Box(0, 0, 200, 200)
const delta = bounds.center
const parentTransform = this.editor.getShapeParentTransform(shape)

View file

@ -5,7 +5,7 @@ import {
TLImageShape,
TLOnDoubleClickHandler,
TLShapePartial,
Vec2d,
Vec,
deepCopy,
imageShapeMigrations,
imageShapeProps,
@ -190,10 +190,10 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
const croppedHeight = (crop.bottomRight.y - crop.topLeft.y) * height
const points = [
new Vec2d(0, 0),
new Vec2d(croppedWidth, 0),
new Vec2d(croppedWidth, croppedHeight),
new Vec2d(0, croppedHeight),
new Vec(0, 0),
new Vec(croppedWidth, 0),
new Vec(croppedWidth, croppedHeight),
new Vec(0, croppedHeight),
]
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon')
@ -261,7 +261,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
const w = (1 / (crop.bottomRight.x - crop.topLeft.x)) * shape.props.w
const h = (1 / (crop.bottomRight.y - crop.topLeft.y)) * shape.props.h
const pointDelta = new Vec2d(crop.topLeft.x * w, crop.topLeft.y * h).rot(shape.rotation)
const pointDelta = new Vec(crop.topLeft.x * w, crop.topLeft.y * h).rot(shape.rotation)
const partial: TLShapePartial<TLImageShape> = {
id: shape.id,

View file

@ -8,7 +8,7 @@ import {
TLLineShape,
TLOnHandleChangeHandler,
TLOnResizeHandler,
Vec2d,
Vec,
WeakMapCache,
deepCopy,
getDefaultColorTheme,
@ -408,7 +408,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
/** @public */
export function getGeometryForLineShape(shape: TLLineShape): CubicSpline2d | Polyline2d {
const { spline, handles } = shape.props
const handlePoints = Object.values(handles).sort(sortByIndex).map(Vec2d.From)
const handlePoints = Object.values(handles).sort(sortByIndex).map(Vec.From)
switch (spline) {
case 'cubic': {

View file

@ -3,7 +3,7 @@ import {
CubicSpline2d,
Edge2d,
Polyline2d,
Vec2d,
Vec,
toDomPrecision,
} from '@tldraw/editor'
@ -20,7 +20,7 @@ export function getSvgPathForEdge(edge: Edge2d, first: boolean) {
export function getSvgPathForBezierCurve(curve: CubicBezier2d, first: boolean) {
const { a, b, c, d } = curve
if (Vec2d.Equals(a, d)) return ''
if (Vec.Equals(a, d)) return ''
return `${first ? `M${toDomPrecision(a.x)},${toDomPrecision(a.y)}` : ``}C${toDomPrecision(
b.x

View file

@ -1,12 +1,12 @@
import {
Matrix2d,
Mat,
StateNode,
TLEventHandlers,
TLHandle,
TLInterruptEvent,
TLLineShape,
TLShapeId,
Vec2d,
Vec,
createShapeId,
getIndexAbove,
last,
@ -45,18 +45,18 @@ export class Pointing extends StateNode {
const endHandle = vertexHandles[vertexHandles.length - 1]
const prevEndHandle = vertexHandles[vertexHandles.length - 2]
const shapePagePoint = Matrix2d.applyToPoint(
const shapePagePoint = Mat.applyToPoint(
this.editor.getShapeParentTransform(this.shape)!,
new Vec2d(this.shape.x, this.shape.y)
new Vec(this.shape.x, this.shape.y)
)
let nextEndHandleIndex: string, nextEndHandleId: string, nextEndHandle: TLHandle
const nextPoint = Vec2d.Sub(currentPagePoint, shapePagePoint)
const nextPoint = Vec.Sub(currentPagePoint, shapePagePoint)
if (
Vec2d.Dist(endHandle, prevEndHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES ||
Vec2d.Dist(nextPoint, endHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES
Vec.Dist(endHandle, prevEndHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES ||
Vec.Dist(nextPoint, endHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES
) {
// If the end handle is too close to the previous end handle, we'll just extend the previous end handle
nextEndHandleIndex = endHandle.index

Some files were not shown because too many files have changed in this diff Show more