@tldraw/editor 3.15.0-canary.20472333970a → 3.15.0-canary.22a03ce9c171

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.
Files changed (60) hide show
  1. package/dist-cjs/index.d.ts +4 -9
  2. package/dist-cjs/index.js +1 -3
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/editor/Editor.js +2 -20
  5. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  6. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +101 -96
  7. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  8. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  9. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  10. package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
  11. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  12. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -3
  13. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  14. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
  15. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  16. package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
  17. package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
  18. package/dist-cjs/lib/primitives/intersect.js +4 -4
  19. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  20. package/dist-cjs/lib/primitives/utils.js +0 -4
  21. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  22. package/dist-cjs/version.js +3 -3
  23. package/dist-cjs/version.js.map +1 -1
  24. package/dist-esm/index.d.mts +4 -9
  25. package/dist-esm/index.mjs +1 -3
  26. package/dist-esm/index.mjs.map +2 -2
  27. package/dist-esm/lib/editor/Editor.mjs +2 -20
  28. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  29. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +101 -96
  30. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  31. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  32. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  33. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
  34. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  35. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -3
  36. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  37. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
  38. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  39. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
  40. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
  41. package/dist-esm/lib/primitives/intersect.mjs +5 -5
  42. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  43. package/dist-esm/lib/primitives/utils.mjs +0 -4
  44. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  45. package/dist-esm/version.mjs +3 -3
  46. package/dist-esm/version.mjs.map +1 -1
  47. package/package.json +7 -7
  48. package/src/index.ts +0 -1
  49. package/src/lib/editor/Editor.test.ts +0 -407
  50. package/src/lib/editor/Editor.ts +4 -29
  51. package/src/lib/editor/managers/TextManager/TextManager.ts +128 -108
  52. package/src/lib/primitives/geometry/Arc2d.ts +2 -2
  53. package/src/lib/primitives/geometry/Circle2d.ts +2 -2
  54. package/src/lib/primitives/geometry/CubicBezier2d.ts +1 -4
  55. package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
  56. package/src/lib/primitives/geometry/geometry-constants.ts +1 -2
  57. package/src/lib/primitives/intersect.test.ts +11 -57
  58. package/src/lib/primitives/intersect.ts +5 -12
  59. package/src/lib/primitives/utils.ts +0 -11
  60. 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
- this.elm = elm
86
-
87
- for (const key of objectMapKeys(initialDefaultStyles)) {
88
- elm.style.setProperty(key, initialDefaultStyles[key])
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
- private setElementStyles(styles: Record<string, string | undefined>) {
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
- const newStyles = {
123
- 'font-family': opts.fontFamily,
124
- 'font-style': opts.fontStyle,
125
- 'font-weight': opts.fontWeight,
126
- 'font-size': opts.fontSize + 'px',
127
- 'line-height': opts.lineHeight.toString(),
128
- padding: opts.padding,
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
- const restoreStyles = this.setElementStyles(newStyles)
119
+ elm.innerHTML = html
136
120
 
137
- try {
138
- elm.innerHTML = html
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
- const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
141
- const rect = elm.getBoundingClientRect()
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
- return {
144
- x: 0,
145
- y: 0,
146
- w: rect.width,
147
- h: rect.height,
148
- scrollWidth,
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
- } finally {
151
- restoreStyles()
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
- const elementWidth = Math.ceil(opts.width - opts.padding * 2)
274
- const newStyles = {
275
- 'font-family': opts.fontFamily,
276
- 'font-style': opts.fontStyle,
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
- try {
290
- const normalizedText = normalizeTextForDom(textToMeasure)
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
- // Render the text into the measurement element:
293
- elm.textContent = normalizedText
313
+ const normalizedText = normalizeTextForDom(textToMeasure)
294
314
 
295
- // actually measure the text:
296
- const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
297
- shouldTruncateToFirstLine,
298
- })
315
+ // Render the text into the measurement element:
316
+ elm.textContent = normalizedText
299
317
 
300
- if (opts.overflow === 'truncate-ellipsis' && didTruncate) {
301
- // we need to measure the ellipsis to know how much space it takes up
302
- elm.textContent = '…'
303
- const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w)
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
- return spans
331
- } finally {
332
- restoreStyles()
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 { getVerticesCountForArcLength } from './geometry-constants'
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 = getVerticesCountForArcLength(Math.abs(length)); i < n + 1; i++) {
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 { getVerticesCountForArcLength } from './geometry-constants'
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 = getVerticesCountForArcLength(perimeter); i < n; i++) {
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 = this._resolution; i <= n; i++) {
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 { getVerticesCountForArcLength } from './geometry-constants'
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 = getVerticesCountForArcLength(p)
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
- /** @internal */
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 { approximately, approximatelyLte, pointInPolygon } from './utils'
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 (approximately(ua_t, 0, precision) || approximately(ub_t, 0, precision)) return null // coincident
32
+ if (ua_t === 0 || ub_t === 0) return null // coincident
34
33
 
35
- if (approximately(u_b, 0, precision)) return null // parallel
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.20472333970a'
4
+ export const version = '3.15.0-canary.22a03ce9c171'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-07-10T11:43:07.890Z',
8
- patch: '2025-07-10T11:43:07.890Z',
7
+ minor: '2025-07-09T11:27:52.513Z',
8
+ patch: '2025-07-09T11:27:52.513Z',
9
9
  }