@tldraw/editor 3.15.0-canary.20472333970a → 3.15.0-canary.21bb6b44433a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +4 -9
- package/dist-cjs/index.js +1 -3
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +2 -20
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +101 -96
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -3
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
- package/dist-cjs/lib/primitives/intersect.js +4 -4
- package/dist-cjs/lib/primitives/intersect.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +0 -4
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +4 -9
- package/dist-esm/index.mjs +1 -3
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +2 -20
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +101 -96
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -3
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
- package/dist-esm/lib/primitives/intersect.mjs +5 -5
- package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +0 -4
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +0 -1
- package/src/lib/editor/Editor.test.ts +0 -407
- package/src/lib/editor/Editor.ts +4 -29
- package/src/lib/editor/managers/TextManager/TextManager.ts +128 -108
- package/src/lib/primitives/geometry/Arc2d.ts +2 -2
- package/src/lib/primitives/geometry/Circle2d.ts +2 -2
- package/src/lib/primitives/geometry/CubicBezier2d.ts +1 -4
- package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
- package/src/lib/primitives/geometry/geometry-constants.ts +1 -2
- package/src/lib/primitives/intersect.test.ts +11 -57
- package/src/lib/primitives/intersect.ts +5 -12
- package/src/lib/primitives/utils.ts +0 -11
- package/src/version.ts +3 -3
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
|
2
|
-
import { objectMapKeys } from '@tldraw/utils'
|
|
3
2
|
import { Editor } from '../../Editor'
|
|
4
3
|
|
|
5
4
|
const fixNewLines = /\r?\n|\r/g
|
|
@@ -61,18 +60,10 @@ export interface TLMeasureTextSpanOpts {
|
|
|
61
60
|
|
|
62
61
|
const spaceCharacterRegex = /\s/
|
|
63
62
|
|
|
64
|
-
const initialDefaultStyles = Object.freeze({
|
|
65
|
-
'overflow-wrap': 'break-word',
|
|
66
|
-
'word-break': 'auto',
|
|
67
|
-
width: null,
|
|
68
|
-
height: null,
|
|
69
|
-
'max-width': null,
|
|
70
|
-
'min-width': null,
|
|
71
|
-
})
|
|
72
|
-
|
|
73
63
|
/** @public */
|
|
74
64
|
export class TextManager {
|
|
75
65
|
private elm: HTMLDivElement
|
|
66
|
+
private defaultStyles: Record<string, string | null>
|
|
76
67
|
|
|
77
68
|
constructor(public editor: Editor) {
|
|
78
69
|
const elm = document.createElement('div')
|
|
@@ -82,34 +73,31 @@ export class TextManager {
|
|
|
82
73
|
elm.tabIndex = -1
|
|
83
74
|
this.editor.getContainer().appendChild(elm)
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
// we need to save the default styles so that we can restore them when we're done
|
|
77
|
+
// these must be the css names, not the js names for the styles
|
|
78
|
+
this.defaultStyles = {
|
|
79
|
+
'overflow-wrap': 'break-word',
|
|
80
|
+
'word-break': 'auto',
|
|
81
|
+
width: null,
|
|
82
|
+
height: null,
|
|
83
|
+
'max-width': null,
|
|
84
|
+
'min-width': null,
|
|
89
85
|
}
|
|
90
|
-
}
|
|
91
86
|
|
|
92
|
-
|
|
93
|
-
const stylesToReinstate = {} as any
|
|
94
|
-
for (const key of objectMapKeys(styles)) {
|
|
95
|
-
if (typeof styles[key] === 'string') {
|
|
96
|
-
const oldValue = this.elm.style.getPropertyValue(key)
|
|
97
|
-
if (oldValue === styles[key]) continue
|
|
98
|
-
stylesToReinstate[key] = oldValue
|
|
99
|
-
this.elm.style.setProperty(key, styles[key])
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return () => {
|
|
103
|
-
for (const key of objectMapKeys(stylesToReinstate)) {
|
|
104
|
-
this.elm.style.setProperty(key, stylesToReinstate[key])
|
|
105
|
-
}
|
|
106
|
-
}
|
|
87
|
+
this.elm = elm
|
|
107
88
|
}
|
|
108
89
|
|
|
109
90
|
dispose() {
|
|
110
91
|
return this.elm.remove()
|
|
111
92
|
}
|
|
112
93
|
|
|
94
|
+
private resetElmStyles() {
|
|
95
|
+
const { elm, defaultStyles } = this
|
|
96
|
+
for (const key in defaultStyles) {
|
|
97
|
+
elm.style.setProperty(key, defaultStyles[key])
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
113
101
|
measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
|
|
114
102
|
const div = document.createElement('div')
|
|
115
103
|
div.textContent = normalizeTextForDom(textToMeasure)
|
|
@@ -119,36 +107,54 @@ export class TextManager {
|
|
|
119
107
|
measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
|
|
120
108
|
const { elm } = this
|
|
121
109
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,
|
|
130
|
-
'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,
|
|
131
|
-
'overflow-wrap': opts.disableOverflowWrapBreaking ? 'normal' : undefined,
|
|
132
|
-
...opts.otherStyles,
|
|
110
|
+
if (opts.otherStyles) {
|
|
111
|
+
for (const key in opts.otherStyles) {
|
|
112
|
+
if (!this.defaultStyles[key]) {
|
|
113
|
+
// we need to save the original style so that we can restore it when we're done
|
|
114
|
+
this.defaultStyles[key] = elm.style.getPropertyValue(key)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
133
117
|
}
|
|
134
118
|
|
|
135
|
-
|
|
119
|
+
elm.innerHTML = html
|
|
136
120
|
|
|
137
|
-
|
|
138
|
-
|
|
121
|
+
// Apply the default styles to the element (for all styles here or that were ever seen in opts.otherStyles)
|
|
122
|
+
this.resetElmStyles()
|
|
139
123
|
|
|
140
|
-
|
|
141
|
-
|
|
124
|
+
elm.style.setProperty('font-family', opts.fontFamily)
|
|
125
|
+
elm.style.setProperty('font-style', opts.fontStyle)
|
|
126
|
+
elm.style.setProperty('font-weight', opts.fontWeight)
|
|
127
|
+
elm.style.setProperty('font-size', opts.fontSize + 'px')
|
|
128
|
+
elm.style.setProperty('line-height', opts.lineHeight.toString())
|
|
129
|
+
elm.style.setProperty('padding', opts.padding)
|
|
142
130
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
131
|
+
if (opts.maxWidth) {
|
|
132
|
+
elm.style.setProperty('max-width', opts.maxWidth + 'px')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (opts.minWidth) {
|
|
136
|
+
elm.style.setProperty('min-width', opts.minWidth + 'px')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (opts.disableOverflowWrapBreaking) {
|
|
140
|
+
elm.style.setProperty('overflow-wrap', 'normal')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (opts.otherStyles) {
|
|
144
|
+
for (const [key, value] of Object.entries(opts.otherStyles)) {
|
|
145
|
+
elm.style.setProperty(key, value)
|
|
149
146
|
}
|
|
150
|
-
}
|
|
151
|
-
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
|
|
150
|
+
const rect = elm.getBoundingClientRect()
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
x: 0,
|
|
154
|
+
y: 0,
|
|
155
|
+
w: rect.width,
|
|
156
|
+
h: rect.height,
|
|
157
|
+
scrollWidth,
|
|
152
158
|
}
|
|
153
159
|
}
|
|
154
160
|
|
|
@@ -268,68 +274,82 @@ export class TextManager {
|
|
|
268
274
|
|
|
269
275
|
const { elm } = this
|
|
270
276
|
|
|
277
|
+
if (opts.otherStyles) {
|
|
278
|
+
for (const key in opts.otherStyles) {
|
|
279
|
+
if (!this.defaultStyles[key]) {
|
|
280
|
+
// we need to save the original style so that we can restore it when we're done
|
|
281
|
+
this.defaultStyles[key] = elm.style.getPropertyValue(key)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this.resetElmStyles()
|
|
287
|
+
|
|
288
|
+
elm.style.setProperty('font-family', opts.fontFamily)
|
|
289
|
+
elm.style.setProperty('font-style', opts.fontStyle)
|
|
290
|
+
elm.style.setProperty('font-weight', opts.fontWeight)
|
|
291
|
+
elm.style.setProperty('font-size', opts.fontSize + 'px')
|
|
292
|
+
elm.style.setProperty('line-height', opts.lineHeight.toString())
|
|
293
|
+
|
|
294
|
+
const elementWidth = Math.ceil(opts.width - opts.padding * 2)
|
|
295
|
+
elm.style.setProperty('width', `${elementWidth}px`)
|
|
296
|
+
elm.style.setProperty('height', 'min-content')
|
|
297
|
+
elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
|
|
298
|
+
|
|
271
299
|
const shouldTruncateToFirstLine =
|
|
272
300
|
opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
'
|
|
276
|
-
'
|
|
277
|
-
'font-weight': opts.fontWeight,
|
|
278
|
-
'font-size': opts.fontSize + 'px',
|
|
279
|
-
'line-height': opts.lineHeight.toString(),
|
|
280
|
-
width: `${elementWidth}px`,
|
|
281
|
-
height: 'min-content',
|
|
282
|
-
'text-align': textAlignmentsForLtr[opts.textAlign],
|
|
283
|
-
'overflow-wrap': shouldTruncateToFirstLine ? 'anywhere' : undefined,
|
|
284
|
-
'word-break': shouldTruncateToFirstLine ? 'break-all' : undefined,
|
|
285
|
-
...opts.otherStyles,
|
|
301
|
+
|
|
302
|
+
if (shouldTruncateToFirstLine) {
|
|
303
|
+
elm.style.setProperty('overflow-wrap', 'anywhere')
|
|
304
|
+
elm.style.setProperty('word-break', 'break-all')
|
|
286
305
|
}
|
|
287
|
-
const restoreStyles = this.setElementStyles(newStyles)
|
|
288
306
|
|
|
289
|
-
|
|
290
|
-
const
|
|
307
|
+
if (opts.otherStyles) {
|
|
308
|
+
for (const [key, value] of Object.entries(opts.otherStyles)) {
|
|
309
|
+
elm.style.setProperty(key, value)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
291
312
|
|
|
292
|
-
|
|
293
|
-
elm.textContent = normalizedText
|
|
313
|
+
const normalizedText = normalizeTextForDom(textToMeasure)
|
|
294
314
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
shouldTruncateToFirstLine,
|
|
298
|
-
})
|
|
315
|
+
// Render the text into the measurement element:
|
|
316
|
+
elm.textContent = normalizedText
|
|
299
317
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// then, we need to subtract that space from the width we have and measure again:
|
|
306
|
-
elm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)
|
|
307
|
-
elm.textContent = normalizedText
|
|
308
|
-
const truncatedSpans = this.measureElementTextNodeSpans(elm, {
|
|
309
|
-
shouldTruncateToFirstLine: true,
|
|
310
|
-
}).spans
|
|
311
|
-
|
|
312
|
-
// Finally, we add in our ellipsis at the end of the last span. We
|
|
313
|
-
// have to do this after measuring, not before, because adding the
|
|
314
|
-
// ellipsis changes how whitespace might be getting collapsed by the
|
|
315
|
-
// browser.
|
|
316
|
-
const lastSpan = truncatedSpans[truncatedSpans.length - 1]!
|
|
317
|
-
truncatedSpans.push({
|
|
318
|
-
text: '…',
|
|
319
|
-
box: {
|
|
320
|
-
x: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),
|
|
321
|
-
y: lastSpan.box.y,
|
|
322
|
-
w: ellipsisWidth,
|
|
323
|
-
h: lastSpan.box.h,
|
|
324
|
-
},
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
return truncatedSpans
|
|
328
|
-
}
|
|
318
|
+
// actually measure the text:
|
|
319
|
+
const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
|
|
320
|
+
shouldTruncateToFirstLine,
|
|
321
|
+
})
|
|
329
322
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
323
|
+
if (opts.overflow === 'truncate-ellipsis' && didTruncate) {
|
|
324
|
+
// we need to measure the ellipsis to know how much space it takes up
|
|
325
|
+
elm.textContent = '…'
|
|
326
|
+
const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w)
|
|
327
|
+
|
|
328
|
+
// then, we need to subtract that space from the width we have and measure again:
|
|
329
|
+
elm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)
|
|
330
|
+
elm.textContent = normalizedText
|
|
331
|
+
const truncatedSpans = this.measureElementTextNodeSpans(elm, {
|
|
332
|
+
shouldTruncateToFirstLine: true,
|
|
333
|
+
}).spans
|
|
334
|
+
|
|
335
|
+
// Finally, we add in our ellipsis at the end of the last span. We
|
|
336
|
+
// have to do this after measuring, not before, because adding the
|
|
337
|
+
// ellipsis changes how whitespace might be getting collapsed by the
|
|
338
|
+
// browser.
|
|
339
|
+
const lastSpan = truncatedSpans[truncatedSpans.length - 1]!
|
|
340
|
+
truncatedSpans.push({
|
|
341
|
+
text: '…',
|
|
342
|
+
box: {
|
|
343
|
+
x: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),
|
|
344
|
+
y: lastSpan.box.y,
|
|
345
|
+
w: ellipsisWidth,
|
|
346
|
+
h: lastSpan.box.h,
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
return truncatedSpans
|
|
333
351
|
}
|
|
352
|
+
|
|
353
|
+
return spans
|
|
334
354
|
}
|
|
335
355
|
}
|
|
@@ -2,7 +2,7 @@ import { Vec, VecLike } from '../Vec'
|
|
|
2
2
|
import { intersectLineSegmentCircle } from '../intersect'
|
|
3
3
|
import { getArcMeasure, getPointInArcT, getPointOnCircle } from '../utils'
|
|
4
4
|
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
|
|
5
|
-
import {
|
|
5
|
+
import { getVerticesCountForLength } from './geometry-constants'
|
|
6
6
|
|
|
7
7
|
/** @public */
|
|
8
8
|
export class Arc2d extends Geometry2d {
|
|
@@ -94,7 +94,7 @@ export class Arc2d extends Geometry2d {
|
|
|
94
94
|
getVertices(): Vec[] {
|
|
95
95
|
const { _center, _measure: measure, length, _radius: radius, _angleStart: angleStart } = this
|
|
96
96
|
const vertices: Vec[] = []
|
|
97
|
-
for (let i = 0, n =
|
|
97
|
+
for (let i = 0, n = getVerticesCountForLength(Math.abs(length)); i < n + 1; i++) {
|
|
98
98
|
const t = (i / n) * measure
|
|
99
99
|
const angle = angleStart + t
|
|
100
100
|
vertices.push(getPointOnCircle(_center, radius, angle))
|
|
@@ -3,7 +3,7 @@ import { Vec, VecLike } from '../Vec'
|
|
|
3
3
|
import { intersectLineSegmentCircle } from '../intersect'
|
|
4
4
|
import { PI2, getPointOnCircle } from '../utils'
|
|
5
5
|
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
|
|
6
|
-
import {
|
|
6
|
+
import { getVerticesCountForLength } from './geometry-constants'
|
|
7
7
|
|
|
8
8
|
/** @public */
|
|
9
9
|
export class Circle2d extends Geometry2d {
|
|
@@ -36,7 +36,7 @@ export class Circle2d extends Geometry2d {
|
|
|
36
36
|
const { _center, _radius: radius } = this
|
|
37
37
|
const perimeter = PI2 * radius
|
|
38
38
|
const vertices: Vec[] = []
|
|
39
|
-
for (let i = 0, n =
|
|
39
|
+
for (let i = 0, n = getVerticesCountForLength(perimeter); i < n; i++) {
|
|
40
40
|
const angle = (i / n) * PI2
|
|
41
41
|
vertices.push(getPointOnCircle(_center, radius, angle))
|
|
42
42
|
}
|
|
@@ -8,7 +8,6 @@ export class CubicBezier2d extends Polyline2d {
|
|
|
8
8
|
private _b: Vec
|
|
9
9
|
private _c: Vec
|
|
10
10
|
private _d: Vec
|
|
11
|
-
private _resolution: number
|
|
12
11
|
|
|
13
12
|
constructor(
|
|
14
13
|
config: Omit<Geometry2dOptions, 'isFilled' | 'isClosed'> & {
|
|
@@ -16,7 +15,6 @@ export class CubicBezier2d extends Polyline2d {
|
|
|
16
15
|
cp1: Vec
|
|
17
16
|
cp2: Vec
|
|
18
17
|
end: Vec
|
|
19
|
-
resolution?: number
|
|
20
18
|
}
|
|
21
19
|
) {
|
|
22
20
|
const { start: a, cp1: b, cp2: c, end: d } = config
|
|
@@ -26,14 +24,13 @@ export class CubicBezier2d extends Polyline2d {
|
|
|
26
24
|
this._b = b
|
|
27
25
|
this._c = c
|
|
28
26
|
this._d = d
|
|
29
|
-
this._resolution = config.resolution ?? 10
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
override getVertices() {
|
|
33
30
|
const vertices = [] as Vec[]
|
|
34
31
|
const { _a: a, _b: b, _c: c, _d: d } = this
|
|
35
32
|
// we'll always use ten vertices for each bezier curve
|
|
36
|
-
for (let i = 0, n =
|
|
33
|
+
for (let i = 0, n = 10; i <= n; i++) {
|
|
37
34
|
const t = i / n
|
|
38
35
|
vertices.push(
|
|
39
36
|
new Vec(
|
|
@@ -3,7 +3,7 @@ import { Vec, VecLike } from '../Vec'
|
|
|
3
3
|
import { PI, PI2, clamp, perimeterOfEllipse } from '../utils'
|
|
4
4
|
import { Edge2d } from './Edge2d'
|
|
5
5
|
import { Geometry2d, Geometry2dOptions } from './Geometry2d'
|
|
6
|
-
import {
|
|
6
|
+
import { getVerticesCountForLength } from './geometry-constants'
|
|
7
7
|
|
|
8
8
|
/** @public */
|
|
9
9
|
export class Ellipse2d extends Geometry2d {
|
|
@@ -47,7 +47,7 @@ export class Ellipse2d extends Geometry2d {
|
|
|
47
47
|
const q = Math.pow(cx - cy, 2) / Math.pow(cx + cy, 2)
|
|
48
48
|
const p = PI * (cx + cy) * (1 + (3 * q) / (10 + Math.sqrt(4 - 3 * q)))
|
|
49
49
|
// Number of points
|
|
50
|
-
const len =
|
|
50
|
+
const len = getVerticesCountForLength(p)
|
|
51
51
|
// Size of step
|
|
52
52
|
const step = PI2 / len
|
|
53
53
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const SPACING = 20
|
|
2
2
|
const MIN_COUNT = 8
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
export function getVerticesCountForArcLength(length: number, spacing = SPACING) {
|
|
4
|
+
export function getVerticesCountForLength(length: number, spacing = SPACING) {
|
|
6
5
|
return Math.max(MIN_COUNT, Math.ceil(length / spacing))
|
|
7
6
|
}
|
|
@@ -181,17 +181,6 @@ describe('intersectLineSegmentLineSegment', () => {
|
|
|
181
181
|
expect(result!.x).toBeCloseTo(5, 1)
|
|
182
182
|
expect(result!.y).toBeCloseTo(5, 1)
|
|
183
183
|
})
|
|
184
|
-
|
|
185
|
-
it('should find intersection when segments cross at endpoints (floating point error case)', () => {
|
|
186
|
-
const result = intersectLineSegmentLineSegment(
|
|
187
|
-
{ x: 100, y: 100 },
|
|
188
|
-
{ x: 20, y: 20 },
|
|
189
|
-
{ x: 36.141160159025375, y: 31.811740238538057 },
|
|
190
|
-
{ x: 34.14213562373095, y: 34.14213562373095 }
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
expect(result).not.toBeNull()
|
|
194
|
-
})
|
|
195
184
|
})
|
|
196
185
|
|
|
197
186
|
describe('edge cases', () => {
|
|
@@ -206,6 +195,17 @@ describe('intersectLineSegmentLineSegment', () => {
|
|
|
206
195
|
expect(result).not.toBeNull()
|
|
207
196
|
})
|
|
208
197
|
|
|
198
|
+
it('should handle segments with very small coordinates', () => {
|
|
199
|
+
const a1 = new Vec(1e-10, 1e-10)
|
|
200
|
+
const a2 = new Vec(1e-9, 1e-9)
|
|
201
|
+
const b1 = new Vec(1e-10, 1e-9)
|
|
202
|
+
const b2 = new Vec(1e-9, 1e-10)
|
|
203
|
+
|
|
204
|
+
const result = intersectLineSegmentLineSegment(a1, a2, b1, b2)
|
|
205
|
+
|
|
206
|
+
expect(result).not.toBeNull()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
209
|
it('should handle segments with very large coordinates', () => {
|
|
210
210
|
const a1 = new Vec(1e10, 1e10)
|
|
211
211
|
const a2 = new Vec(1e11, 1e11)
|
|
@@ -531,52 +531,6 @@ describe('intersectLineSegmentPolyline', () => {
|
|
|
531
531
|
expect(sorted[3].x).toBeCloseTo(18, 5)
|
|
532
532
|
sorted.forEach((pt) => expect(pt.y).toBeCloseTo(5, 5))
|
|
533
533
|
})
|
|
534
|
-
|
|
535
|
-
// Test cases for vertex intersection issues
|
|
536
|
-
describe('vertex intersection edge cases', () => {
|
|
537
|
-
it('should detect intersection when line segment passes through polyline vertex', () => {
|
|
538
|
-
const a1 = new Vec(0, 5)
|
|
539
|
-
const a2 = new Vec(10, 5)
|
|
540
|
-
const points = [new Vec(5, 0), new Vec(5, 10)] // vertical line at x=5
|
|
541
|
-
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
542
|
-
expect(result).not.toBeNull()
|
|
543
|
-
expect(result!.length).toBe(1)
|
|
544
|
-
expect(result![0].x).toBeCloseTo(5, 5)
|
|
545
|
-
expect(result![0].y).toBeCloseTo(5, 5)
|
|
546
|
-
})
|
|
547
|
-
|
|
548
|
-
it('should detect intersection when line segment passes through polyline vertex at angle', () => {
|
|
549
|
-
const a1 = new Vec(0, 0)
|
|
550
|
-
const a2 = new Vec(10, 10)
|
|
551
|
-
const points = [new Vec(5, 0), new Vec(5, 10)] // vertical line at x=5
|
|
552
|
-
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
553
|
-
expect(result).not.toBeNull()
|
|
554
|
-
expect(result!.length).toBe(1)
|
|
555
|
-
expect(result![0].x).toBeCloseTo(5, 5)
|
|
556
|
-
expect(result![0].y).toBeCloseTo(5, 5)
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
it('should detect intersection when line segment passes through polyline vertex at middle', () => {
|
|
560
|
-
const a1 = new Vec(0, 5)
|
|
561
|
-
const a2 = new Vec(10, 5)
|
|
562
|
-
const points = [new Vec(0, 0), new Vec(5, 5), new Vec(10, 0)] // vertex at (5,5)
|
|
563
|
-
const result = intersectLineSegmentPolyline(a1, a2, points)
|
|
564
|
-
expect(result).not.toBeNull()
|
|
565
|
-
expect(result!.length).toBe(1)
|
|
566
|
-
expect(result![0].x).toBeCloseTo(5, 5)
|
|
567
|
-
expect(result![0].y).toBeCloseTo(5, 5)
|
|
568
|
-
})
|
|
569
|
-
|
|
570
|
-
it('should detect intersection when line segment passes through a polyline vertext (floating point error case)', () => {
|
|
571
|
-
const result = intersectLineSegmentPolyline({ x: 100, y: 100 }, { x: 20, y: 20 }, [
|
|
572
|
-
{ x: 36.141160159025375, y: 31.811740238538057 },
|
|
573
|
-
{ x: 34.14213562373095, y: 34.14213562373095 },
|
|
574
|
-
{ x: 31.811740238538057, y: 36.141160159025375 },
|
|
575
|
-
])
|
|
576
|
-
|
|
577
|
-
expect(result).not.toBeNull()
|
|
578
|
-
})
|
|
579
|
-
})
|
|
580
534
|
})
|
|
581
535
|
|
|
582
536
|
describe('intersectLineSegmentPolygon', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box } from './Box'
|
|
2
|
-
import {
|
|
2
|
+
import { pointInPolygon } from './utils'
|
|
3
3
|
import { Vec, VecLike } from './Vec'
|
|
4
4
|
|
|
5
5
|
// need even more intersections? See https://gist.github.com/steveruizok/35c02d526c707003a5c79761bfb89a52
|
|
@@ -17,8 +17,7 @@ export function intersectLineSegmentLineSegment(
|
|
|
17
17
|
a1: VecLike,
|
|
18
18
|
a2: VecLike,
|
|
19
19
|
b1: VecLike,
|
|
20
|
-
b2: VecLike
|
|
21
|
-
precision = 1e-10
|
|
20
|
+
b2: VecLike
|
|
22
21
|
) {
|
|
23
22
|
const ABx = a1.x - b1.x
|
|
24
23
|
const ABy = a1.y - b1.y
|
|
@@ -30,19 +29,14 @@ export function intersectLineSegmentLineSegment(
|
|
|
30
29
|
const ub_t = AVx * ABy - AVy * ABx
|
|
31
30
|
const u_b = BVy * AVx - BVx * AVy
|
|
32
31
|
|
|
33
|
-
if (
|
|
32
|
+
if (ua_t === 0 || ub_t === 0) return null // coincident
|
|
34
33
|
|
|
35
|
-
if (
|
|
34
|
+
if (u_b === 0) return null // parallel
|
|
36
35
|
|
|
37
36
|
if (u_b !== 0) {
|
|
38
37
|
const ua = ua_t / u_b
|
|
39
38
|
const ub = ub_t / u_b
|
|
40
|
-
if (
|
|
41
|
-
approximatelyLte(0, ua, precision) &&
|
|
42
|
-
approximatelyLte(ua, 1, precision) &&
|
|
43
|
-
approximatelyLte(0, ub, precision) &&
|
|
44
|
-
approximatelyLte(ub, 1, precision)
|
|
45
|
-
) {
|
|
39
|
+
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
|
|
46
40
|
return Vec.AddXY(a1, ua * AVx, ua * AVy)
|
|
47
41
|
}
|
|
48
42
|
}
|
|
@@ -131,7 +125,6 @@ export function intersectLineSegmentPolygon(a1: VecLike, a2: VecLike, points: Ve
|
|
|
131
125
|
points[i - 1],
|
|
132
126
|
points[i % points.length]
|
|
133
127
|
)
|
|
134
|
-
|
|
135
128
|
if (segmentIntersection) result.push(segmentIntersection)
|
|
136
129
|
}
|
|
137
130
|
|
|
@@ -77,17 +77,6 @@ export function approximately(a: number, b: number, precision = 0.000001) {
|
|
|
77
77
|
return Math.abs(a - b) <= precision
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
* Whether a number is approximately less than or equal to another number.
|
|
82
|
-
*
|
|
83
|
-
* @param a - The first number.
|
|
84
|
-
* @param b - The second number.
|
|
85
|
-
* @public
|
|
86
|
-
*/
|
|
87
|
-
export function approximatelyLte(a: number, b: number, precision = 0.000001) {
|
|
88
|
-
return a < b || approximately(a, b, precision)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
80
|
/**
|
|
92
81
|
* Find the approximate perimeter of an ellipse.
|
|
93
82
|
*
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '3.15.0-canary.
|
|
4
|
+
export const version = '3.15.0-canary.21bb6b44433a'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2024-09-13T14:36:29.063Z',
|
|
7
|
-
minor: '2025-07-
|
|
8
|
-
patch: '2025-07-
|
|
7
|
+
minor: '2025-07-08T16:23:39.770Z',
|
|
8
|
+
patch: '2025-07-08T16:23:39.770Z',
|
|
9
9
|
}
|