@tldraw/editor 3.15.0-canary.22a03ce9c171 → 3.15.0-canary.2fa0050cd4a6
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 +5 -4
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/editor/Editor.js +20 -2
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +96 -101
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.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 +4 -0
- 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 +5 -4
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/editor/Editor.mjs +20 -2
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +96 -101
- package/dist-esm/lib/editor/managers/TextManager/TextManager.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 +4 -0
- 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/lib/editor/Editor.test.ts +407 -0
- package/src/lib/editor/Editor.ts +29 -4
- package/src/lib/editor/managers/TextManager/TextManager.ts +108 -128
- package/src/lib/primitives/intersect.test.ts +57 -11
- package/src/lib/primitives/intersect.ts +12 -5
- package/src/lib/primitives/utils.ts +11 -0
- package/src/version.ts +3 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
|
2
|
+
import { objectMapKeys } from '@tldraw/utils'
|
|
2
3
|
import { Editor } from '../../Editor'
|
|
3
4
|
|
|
4
5
|
const fixNewLines = /\r?\n|\r/g
|
|
@@ -60,10 +61,18 @@ export interface TLMeasureTextSpanOpts {
|
|
|
60
61
|
|
|
61
62
|
const spaceCharacterRegex = /\s/
|
|
62
63
|
|
|
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
|
+
|
|
63
73
|
/** @public */
|
|
64
74
|
export class TextManager {
|
|
65
75
|
private elm: HTMLDivElement
|
|
66
|
-
private defaultStyles: Record<string, string | null>
|
|
67
76
|
|
|
68
77
|
constructor(public editor: Editor) {
|
|
69
78
|
const elm = document.createElement('div')
|
|
@@ -73,31 +82,34 @@ export class TextManager {
|
|
|
73
82
|
elm.tabIndex = -1
|
|
74
83
|
this.editor.getContainer().appendChild(elm)
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
'word-break': 'auto',
|
|
81
|
-
width: null,
|
|
82
|
-
height: null,
|
|
83
|
-
'max-width': null,
|
|
84
|
-
'min-width': null,
|
|
85
|
+
this.elm = elm
|
|
86
|
+
|
|
87
|
+
for (const key of objectMapKeys(initialDefaultStyles)) {
|
|
88
|
+
elm.style.setProperty(key, initialDefaultStyles[key])
|
|
85
89
|
}
|
|
90
|
+
}
|
|
86
91
|
|
|
87
|
-
|
|
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
|
+
}
|
|
88
107
|
}
|
|
89
108
|
|
|
90
109
|
dispose() {
|
|
91
110
|
return this.elm.remove()
|
|
92
111
|
}
|
|
93
112
|
|
|
94
|
-
private resetElmStyles() {
|
|
95
|
-
const { elm, defaultStyles } = this
|
|
96
|
-
for (const key in defaultStyles) {
|
|
97
|
-
elm.style.setProperty(key, defaultStyles[key])
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
113
|
measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
|
|
102
114
|
const div = document.createElement('div')
|
|
103
115
|
div.textContent = normalizeTextForDom(textToMeasure)
|
|
@@ -107,54 +119,36 @@ export class TextManager {
|
|
|
107
119
|
measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
|
|
108
120
|
const { elm } = this
|
|
109
121
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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,
|
|
117
133
|
}
|
|
118
134
|
|
|
119
|
-
|
|
135
|
+
const restoreStyles = this.setElementStyles(newStyles)
|
|
120
136
|
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
try {
|
|
138
|
+
elm.innerHTML = html
|
|
123
139
|
|
|
124
|
-
|
|
125
|
-
|
|
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)
|
|
140
|
+
const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
|
|
141
|
+
const rect = elm.getBoundingClientRect()
|
|
130
142
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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)
|
|
143
|
+
return {
|
|
144
|
+
x: 0,
|
|
145
|
+
y: 0,
|
|
146
|
+
w: rect.width,
|
|
147
|
+
h: rect.height,
|
|
148
|
+
scrollWidth,
|
|
146
149
|
}
|
|
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,
|
|
150
|
+
} finally {
|
|
151
|
+
restoreStyles()
|
|
158
152
|
}
|
|
159
153
|
}
|
|
160
154
|
|
|
@@ -274,82 +268,68 @@ export class TextManager {
|
|
|
274
268
|
|
|
275
269
|
const { elm } = this
|
|
276
270
|
|
|
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
|
-
|
|
299
271
|
const shouldTruncateToFirstLine =
|
|
300
272
|
opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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,
|
|
311
286
|
}
|
|
287
|
+
const restoreStyles = this.setElementStyles(newStyles)
|
|
312
288
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
// Render the text into the measurement element:
|
|
316
|
-
elm.textContent = normalizedText
|
|
317
|
-
|
|
318
|
-
// actually measure the text:
|
|
319
|
-
const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
|
|
320
|
-
shouldTruncateToFirstLine,
|
|
321
|
-
})
|
|
289
|
+
try {
|
|
290
|
+
const normalizedText = normalizeTextForDom(textToMeasure)
|
|
322
291
|
|
|
323
|
-
|
|
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`)
|
|
292
|
+
// Render the text into the measurement element:
|
|
330
293
|
elm.textContent = normalizedText
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
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
|
-
},
|
|
294
|
+
|
|
295
|
+
// actually measure the text:
|
|
296
|
+
const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
|
|
297
|
+
shouldTruncateToFirstLine,
|
|
348
298
|
})
|
|
349
299
|
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
}
|
|
352
329
|
|
|
353
|
-
|
|
330
|
+
return spans
|
|
331
|
+
} finally {
|
|
332
|
+
restoreStyles()
|
|
333
|
+
}
|
|
354
334
|
}
|
|
355
335
|
}
|
|
@@ -181,6 +181,17 @@ 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
|
+
})
|
|
184
195
|
})
|
|
185
196
|
|
|
186
197
|
describe('edge cases', () => {
|
|
@@ -195,17 +206,6 @@ describe('intersectLineSegmentLineSegment', () => {
|
|
|
195
206
|
expect(result).not.toBeNull()
|
|
196
207
|
})
|
|
197
208
|
|
|
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,6 +531,52 @@ 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
|
+
})
|
|
534
580
|
})
|
|
535
581
|
|
|
536
582
|
describe('intersectLineSegmentPolygon', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box } from './Box'
|
|
2
|
-
import { pointInPolygon } from './utils'
|
|
2
|
+
import { approximately, approximatelyLte, 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,7 +17,8 @@ export function intersectLineSegmentLineSegment(
|
|
|
17
17
|
a1: VecLike,
|
|
18
18
|
a2: VecLike,
|
|
19
19
|
b1: VecLike,
|
|
20
|
-
b2: VecLike
|
|
20
|
+
b2: VecLike,
|
|
21
|
+
precision = 1e-10
|
|
21
22
|
) {
|
|
22
23
|
const ABx = a1.x - b1.x
|
|
23
24
|
const ABy = a1.y - b1.y
|
|
@@ -29,14 +30,19 @@ export function intersectLineSegmentLineSegment(
|
|
|
29
30
|
const ub_t = AVx * ABy - AVy * ABx
|
|
30
31
|
const u_b = BVy * AVx - BVx * AVy
|
|
31
32
|
|
|
32
|
-
if (ua_t
|
|
33
|
+
if (approximately(ua_t, 0, precision) || approximately(ub_t, 0, precision)) return null // coincident
|
|
33
34
|
|
|
34
|
-
if (u_b
|
|
35
|
+
if (approximately(u_b, 0, precision)) return null // parallel
|
|
35
36
|
|
|
36
37
|
if (u_b !== 0) {
|
|
37
38
|
const ua = ua_t / u_b
|
|
38
39
|
const ub = ub_t / u_b
|
|
39
|
-
if (
|
|
40
|
+
if (
|
|
41
|
+
approximatelyLte(0, ua, precision) &&
|
|
42
|
+
approximatelyLte(ua, 1, precision) &&
|
|
43
|
+
approximatelyLte(0, ub, precision) &&
|
|
44
|
+
approximatelyLte(ub, 1, precision)
|
|
45
|
+
) {
|
|
40
46
|
return Vec.AddXY(a1, ua * AVx, ua * AVy)
|
|
41
47
|
}
|
|
42
48
|
}
|
|
@@ -125,6 +131,7 @@ export function intersectLineSegmentPolygon(a1: VecLike, a2: VecLike, points: Ve
|
|
|
125
131
|
points[i - 1],
|
|
126
132
|
points[i % points.length]
|
|
127
133
|
)
|
|
134
|
+
|
|
128
135
|
if (segmentIntersection) result.push(segmentIntersection)
|
|
129
136
|
}
|
|
130
137
|
|
|
@@ -77,6 +77,17 @@ 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
|
+
|
|
80
91
|
/**
|
|
81
92
|
* Find the approximate perimeter of an ellipse.
|
|
82
93
|
*
|
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.2fa0050cd4a6'
|
|
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-10T08:17:03.263Z',
|
|
8
|
+
patch: '2025-07-10T08:17:03.263Z',
|
|
9
9
|
}
|