@tldraw/editor 5.4.0 → 5.5.0-canary.04d1e6c4daff
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 -2
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/constants.js +2 -0
- package/dist-cjs/lib/constants.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +9 -6
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +10 -6
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js +7 -3
- package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +14 -6
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +3 -3
- 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/CubicBezier2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +8 -5
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Point2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Point2d.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 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/constants.mjs +2 -0
- package/dist-esm/lib/constants.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +10 -6
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +10 -6
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs +7 -3
- package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +14 -6
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +3 -3
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +12 -6
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Point2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Point2d.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/constants.ts +8 -0
- package/src/lib/editor/Editor.ts +19 -9
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +35 -4
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +13 -7
- package/src/lib/editor/managers/ThemeManager/ThemeManager.test.ts +53 -0
- package/src/lib/editor/managers/ThemeManager/ThemeManager.ts +9 -3
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +23 -8
- package/src/lib/primitives/geometry/Arc2d.test.ts +9 -0
- package/src/lib/primitives/geometry/Arc2d.ts +1 -1
- package/src/lib/primitives/geometry/CubicBezier2d.test.ts +13 -0
- package/src/lib/primitives/geometry/CubicBezier2d.ts +1 -1
- package/src/lib/primitives/geometry/Edge2d.test.ts +21 -3
- package/src/lib/primitives/geometry/Edge2d.ts +1 -1
- package/src/lib/primitives/geometry/Group2d.test.ts +26 -5
- package/src/lib/primitives/geometry/Group2d.ts +18 -7
- package/src/lib/primitives/geometry/Point2d.test.ts +14 -3
- package/src/lib/primitives/geometry/Point2d.ts +1 -1
- package/src/version.ts +3 -3
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -110,6 +110,7 @@ import {
|
|
|
110
110
|
import {
|
|
111
111
|
DEFAULT_ANIMATION_OPTIONS,
|
|
112
112
|
DEFAULT_CAMERA_OPTIONS,
|
|
113
|
+
FRAME_MS_60HZ,
|
|
113
114
|
INTERNAL_POINTER_IDS,
|
|
114
115
|
LEFT_MOUSE_BUTTON,
|
|
115
116
|
MIDDLE_MOUSE_BUTTON,
|
|
@@ -2303,8 +2304,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2303
2304
|
const shapeIds = readingOrderShapes.map((shape) => shape.id)
|
|
2304
2305
|
|
|
2305
2306
|
const currentIndex = currentShapeId ? shapeIds.indexOf(currentShapeId) : -1
|
|
2307
|
+
// With no current index, stepping from -1 makes 'prev' land one shape
|
|
2308
|
+
// before the last; seed it from 0 so it wraps to the last shape. See #10559.
|
|
2309
|
+
const startIndex = currentIndex === -1 && direction === 'prev' ? 0 : currentIndex
|
|
2306
2310
|
const adjacentIndex =
|
|
2307
|
-
(
|
|
2311
|
+
(startIndex + (direction === 'next' ? 1 : -1) + shapeIds.length) % shapeIds.length
|
|
2308
2312
|
adjacentShapeId = shapeIds[adjacentIndex]
|
|
2309
2313
|
} else {
|
|
2310
2314
|
if (!currentShapeId) return
|
|
@@ -3591,6 +3595,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3591
3595
|
const { isLocked } = this._cameraOptions.__unsafe__getWithoutCapture()
|
|
3592
3596
|
if (isLocked && !opts?.force) return this
|
|
3593
3597
|
|
|
3598
|
+
const _point = Vec.Cast(point)
|
|
3599
|
+
|
|
3600
|
+
// Reject non-finite values before anything else, so the call is a no-op rather than a
|
|
3601
|
+
// partial one. An animated move writes the camera from a 'tick' listener, and a listener
|
|
3602
|
+
// that throws stops TickManager scheduling the next frame, which kills every frame-driven
|
|
3603
|
+
// behavior for the rest of the session instead of surfacing the error to the caller.
|
|
3604
|
+
if (!Number.isFinite(_point.x) || !Number.isFinite(_point.y) || !Number.isFinite(_point.z)) {
|
|
3605
|
+
throw Error(
|
|
3606
|
+
`Editor.setCamera: expected finite values, got (${_point.x}, ${_point.y}, ${_point.z}).`
|
|
3607
|
+
)
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3594
3610
|
// Stop any camera animations
|
|
3595
3611
|
this.stopCameraAnimation()
|
|
3596
3612
|
|
|
@@ -3599,12 +3615,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
3599
3615
|
this.stopFollowingUser()
|
|
3600
3616
|
}
|
|
3601
3617
|
|
|
3602
|
-
const _point = Vec.Cast(point)
|
|
3603
|
-
|
|
3604
|
-
if (!Number.isFinite(_point.x)) _point.x = 0
|
|
3605
|
-
if (!Number.isFinite(_point.y)) _point.y = 0
|
|
3606
|
-
if (_point.z === undefined || !Number.isFinite(_point.z)) point.z = this.getZoomLevel()
|
|
3607
|
-
|
|
3608
3618
|
const camera = this.getConstrainedCamera(_point, opts)
|
|
3609
3619
|
|
|
3610
3620
|
if (opts?.animation) {
|
|
@@ -4086,8 +4096,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4086
4096
|
newCy += center.y / newCz - center.y / cz
|
|
4087
4097
|
}
|
|
4088
4098
|
|
|
4089
|
-
// Apply friction
|
|
4090
|
-
currentSpeed *= 1 - friction
|
|
4099
|
+
// Apply friction per unit of elapsed time, not per tick, or a 120 Hz display decays twice as fast
|
|
4100
|
+
currentSpeed *= (1 - friction) ** (elapsed / FRAME_MS_60HZ)
|
|
4091
4101
|
if (currentSpeed < speedThreshold) {
|
|
4092
4102
|
cancel()
|
|
4093
4103
|
} else {
|
|
@@ -262,15 +262,46 @@ describe('EdgeScrollManager', () => {
|
|
|
262
262
|
})
|
|
263
263
|
|
|
264
264
|
it('should adjust scroll speed based on zoom level', () => {
|
|
265
|
-
editor.getZoomLevel.mockReturnValue(2)
|
|
266
265
|
mockInputs.setCurrentScreenPoint(new Vec(5, 300))
|
|
267
266
|
|
|
268
267
|
edgeScrollManager.updateEdgeScrolling(300)
|
|
268
|
+
const atZoom1 = (editor.setCamera.mock.calls[0][0] as Vec).x
|
|
269
|
+
|
|
270
|
+
editor.setCamera.mockClear()
|
|
271
|
+
edgeScrollManager = new EdgeScrollManager(editor)
|
|
272
|
+
editor.getZoomLevel.mockReturnValue(2)
|
|
273
|
+
edgeScrollManager.updateEdgeScrolling(300)
|
|
274
|
+
const atZoom2 = (editor.setCamera.mock.calls[0][0] as Vec).x
|
|
269
275
|
|
|
270
|
-
expect(editor.setCamera).toHaveBeenCalled()
|
|
271
|
-
const callArgs = editor.setCamera.mock.calls[0][0] as Vec
|
|
272
276
|
// Higher zoom should result in smaller camera movement
|
|
273
|
-
expect(
|
|
277
|
+
expect(atZoom2).toBeCloseTo(atZoom1 / 2)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should move the same distance per second regardless of tick rate', () => {
|
|
281
|
+
mockInputs.setCurrentScreenPoint(new Vec(5, 300))
|
|
282
|
+
|
|
283
|
+
// Get past the delay and easing ramp so each tick moves at full speed
|
|
284
|
+
edgeScrollManager.updateEdgeScrolling(1000)
|
|
285
|
+
editor.setCamera.mockClear()
|
|
286
|
+
|
|
287
|
+
// One second of 60 Hz ticks
|
|
288
|
+
for (let i = 0; i < 60; i++) edgeScrollManager.updateEdgeScrolling(1000 / 60)
|
|
289
|
+
const at60Hz = editor.setCamera.mock.calls.reduce(
|
|
290
|
+
(sum, [camera]) => sum + (camera as Vec).x,
|
|
291
|
+
0
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
editor.setCamera.mockClear()
|
|
295
|
+
|
|
296
|
+
// One second of 120 Hz ticks
|
|
297
|
+
for (let i = 0; i < 120; i++) edgeScrollManager.updateEdgeScrolling(1000 / 120)
|
|
298
|
+
const at120Hz = editor.setCamera.mock.calls.reduce(
|
|
299
|
+
(sum, [camera]) => sum + (camera as Vec).x,
|
|
300
|
+
0
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
expect(at60Hz).toBeGreaterThan(0)
|
|
304
|
+
expect(at120Hz).toBeCloseTo(at60Hz)
|
|
274
305
|
})
|
|
275
306
|
|
|
276
307
|
it('should add scroll delta to current camera position', () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FRAME_MS_60HZ } from '../../../constants'
|
|
1
2
|
import { EASINGS } from '../../../primitives/easings'
|
|
2
3
|
import { Vec } from '../../../primitives/Vec'
|
|
3
4
|
import type { Editor } from '../../Editor'
|
|
@@ -51,10 +52,13 @@ export class EdgeScrollManager {
|
|
|
51
52
|
)
|
|
52
53
|
)
|
|
53
54
|
: 1
|
|
54
|
-
this.moveCameraWhenCloseToEdge(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
this.moveCameraWhenCloseToEdge(
|
|
56
|
+
{
|
|
57
|
+
x: edgeScrollProximityFactor.x * eased,
|
|
58
|
+
y: edgeScrollProximityFactor.y * eased,
|
|
59
|
+
},
|
|
60
|
+
elapsed
|
|
61
|
+
)
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
}
|
|
@@ -112,7 +116,7 @@ export class EdgeScrollManager {
|
|
|
112
116
|
* Moves the camera when the mouse is close to the edge of the screen.
|
|
113
117
|
* @public
|
|
114
118
|
*/
|
|
115
|
-
private moveCameraWhenCloseToEdge(proximityFactor: { x: number; y: number }) {
|
|
119
|
+
private moveCameraWhenCloseToEdge(proximityFactor: { x: number; y: number }, elapsed: number) {
|
|
116
120
|
if (proximityFactor.x === 0 && proximityFactor.y === 0) return
|
|
117
121
|
const { editor } = this
|
|
118
122
|
|
|
@@ -128,9 +132,11 @@ export class EdgeScrollManager {
|
|
|
128
132
|
? EDGE_SCROLL_SMALL_SCREEN_SPEED_FACTOR
|
|
129
133
|
: 1
|
|
130
134
|
|
|
131
|
-
// Determines the base speed of the scroll
|
|
135
|
+
// Determines the base speed of the scroll. edgeScrollSpeed is px per 60 Hz frame, so scale
|
|
136
|
+
// by elapsed time or a 120 Hz display scrolls twice as fast.
|
|
132
137
|
const zoomLevel = editor.getZoomLevel()
|
|
133
|
-
const pxSpeed =
|
|
138
|
+
const pxSpeed =
|
|
139
|
+
editor.user.getEdgeScrollSpeed() * editor.options.edgeScrollSpeed * (elapsed / FRAME_MS_60HZ)
|
|
134
140
|
const scrollDeltaX = (pxSpeed * proximityFactor.x * screenSizeFactorX) / zoomLevel
|
|
135
141
|
const scrollDeltaY = (pxSpeed * proximityFactor.y * screenSizeFactorY) / zoomLevel
|
|
136
142
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { TLTheme } from '@tldraw/tlschema'
|
|
2
|
+
import { MockInstance, vi } from 'vitest'
|
|
3
|
+
import { TestEditor } from '../../../test/TestEditor'
|
|
4
|
+
import { DEFAULT_THEME } from './defaultThemes'
|
|
5
|
+
|
|
6
|
+
// Optional so that other tests' theme objects still satisfy TLThemes.
|
|
7
|
+
declare module '@tldraw/tlschema' {
|
|
8
|
+
interface TLThemes {
|
|
9
|
+
ocean?: TLTheme
|
|
10
|
+
sunset?: TLTheme
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const oceanTheme: TLTheme = { ...DEFAULT_THEME, id: 'ocean' }
|
|
15
|
+
|
|
16
|
+
let editor: TestEditor
|
|
17
|
+
let warn: MockInstance
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
editor = new TestEditor({ themes: { ocean: oceanTheme } })
|
|
21
|
+
warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
warn.mockRestore()
|
|
26
|
+
editor.dispose()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('setCurrentTheme', () => {
|
|
30
|
+
it('switches to a registered theme', () => {
|
|
31
|
+
editor.setCurrentTheme('ocean')
|
|
32
|
+
|
|
33
|
+
expect(editor.getCurrentThemeId()).toBe('ocean')
|
|
34
|
+
expect(editor.getCurrentTheme()).toBe(oceanTheme)
|
|
35
|
+
expect(warn).not.toHaveBeenCalled()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('warns and keeps the current theme when the id is not registered', () => {
|
|
39
|
+
editor.setCurrentTheme('sunset')
|
|
40
|
+
|
|
41
|
+
expect(warn).toHaveBeenCalledWith("Theme 'sunset' not found. Available themes: default, ocean")
|
|
42
|
+
expect(editor.getCurrentThemeId()).toBe('default')
|
|
43
|
+
expect(editor.getCurrentTheme()).toBe(DEFAULT_THEME)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('keeps the previous theme rather than falling back to default', () => {
|
|
47
|
+
editor.setCurrentTheme('ocean')
|
|
48
|
+
editor.setCurrentTheme('sunset')
|
|
49
|
+
|
|
50
|
+
expect(editor.getCurrentThemeId()).toBe('ocean')
|
|
51
|
+
expect(editor.getCurrentTheme()).toBe(oceanTheme)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -69,14 +69,20 @@ export class ThemeManager {
|
|
|
69
69
|
return this._themes.get()[this.getCurrentThemeId()]!
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Set the current theme by id. The theme must have been previously registered; an
|
|
74
|
+
* unregistered id is ignored and the current theme is kept.
|
|
75
|
+
*/
|
|
73
76
|
setCurrentTheme(id: TLThemeId): void {
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
// Storing an unregistered id would make getCurrentTheme() return
|
|
78
|
+
// undefined despite its TLTheme return type.
|
|
79
|
+
if (!(id in this._themes.get())) {
|
|
80
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
76
81
|
console.warn(
|
|
77
82
|
`Theme '${id}' not found. Available themes: ${Object.keys(this._themes.get()).join(', ')}`
|
|
78
83
|
)
|
|
79
84
|
}
|
|
85
|
+
return
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
this._currentThemeId.set(id)
|
|
@@ -39,14 +39,29 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
|
|
|
39
39
|
return new Rectangle2d({ width: 1, height: 1, isFilled: false })
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
// A child id can outlive its record. The children index and the geometry cache are separate
|
|
43
|
+
// derivations, so inside a transaction that removes a child the index can still name it while
|
|
44
|
+
// `getShapeGeometry` already answers nothing — its `!` hides that it returns `undefined` for a
|
|
45
|
+
// record the store no longer holds. Asserting instead of checking threw a `TypeError` out of a
|
|
46
|
+
// reactive recompute, where no caller could catch it.
|
|
47
|
+
const geometries: Geometry2d[] = []
|
|
48
|
+
for (const childId of children) {
|
|
49
|
+
const child = this.editor.getShape(childId)
|
|
50
|
+
if (!child) continue
|
|
51
|
+
const geometry = this.editor.getShapeGeometry(childId)
|
|
52
|
+
if (!geometry) continue
|
|
53
|
+
const transform = this.editor.getShapeLocalTransform(child)
|
|
54
|
+
if (!transform) continue
|
|
55
|
+
geometries.push(geometry.transform(transform, { isLabel: false }))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Every child resolved to nothing, so there is no meaningful extent to report. Matches the
|
|
59
|
+
// no-children case above rather than building an empty `Group2d`, whose bounds are `NaN`.
|
|
60
|
+
if (geometries.length === 0) {
|
|
61
|
+
return new Rectangle2d({ width: 1, height: 1, isFilled: false })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return new Group2d({ children: geometries })
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
component(shape: TLGroupShape) {
|
|
@@ -225,4 +225,13 @@ describe('Arc2d.getSvgPathData', () => {
|
|
|
225
225
|
})
|
|
226
226
|
expect(arc.getSvgPathData()).toBe('M10, 0 A10 10 0 0 1 7.07, 7.07')
|
|
227
227
|
})
|
|
228
|
+
|
|
229
|
+
it('does not mutate the stored points', () => {
|
|
230
|
+
const start = new Vec(10, 0)
|
|
231
|
+
const end = new Vec(10 / Math.SQRT2, 10 / Math.SQRT2)
|
|
232
|
+
const arc = new Arc2d({ center: new Vec(0, 0), start, end, sweepFlag: 1, largeArcFlag: 0 })
|
|
233
|
+
arc.getSvgPathData()
|
|
234
|
+
expect(start).toMatchObject({ x: 10, y: 0 })
|
|
235
|
+
expect(end).toMatchObject({ x: 10 / Math.SQRT2, y: 10 / Math.SQRT2 })
|
|
236
|
+
})
|
|
228
237
|
})
|
|
@@ -105,7 +105,7 @@ export class Arc2d extends Geometry2d {
|
|
|
105
105
|
_largeArcFlag: largeArcFlag,
|
|
106
106
|
_sweepFlag: sweepFlag,
|
|
107
107
|
} = this
|
|
108
|
-
return `${first ? `M${start
|
|
108
|
+
return `${first ? `M${start}` : ``} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end}`
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
override getLength() {
|
|
@@ -172,4 +172,17 @@ describe('CubicBezier2d.getSvgPathData', () => {
|
|
|
172
172
|
})
|
|
173
173
|
expect(b.getSvgPathData()).toBe('M 0.12, 0 C1.01, 2 3, 5 10, 10')
|
|
174
174
|
})
|
|
175
|
+
|
|
176
|
+
it('does not mutate the stored points', () => {
|
|
177
|
+
const start = new Vec(0.123, 0)
|
|
178
|
+
const cp1 = new Vec(1.006, 2)
|
|
179
|
+
const cp2 = new Vec(3, 4.999)
|
|
180
|
+
const end = new Vec(10, 10.005)
|
|
181
|
+
const b = new CubicBezier2d({ start, cp1, cp2, end })
|
|
182
|
+
b.getSvgPathData()
|
|
183
|
+
expect(start).toMatchObject({ x: 0.123, y: 0 })
|
|
184
|
+
expect(cp1).toMatchObject({ x: 1.006, y: 2 })
|
|
185
|
+
expect(cp2).toMatchObject({ x: 3, y: 4.999 })
|
|
186
|
+
expect(end).toMatchObject({ x: 10, y: 10.005 })
|
|
187
|
+
})
|
|
175
188
|
})
|
|
@@ -81,7 +81,7 @@ export class CubicBezier2d extends Polyline2d {
|
|
|
81
81
|
|
|
82
82
|
getSvgPathData(first = true) {
|
|
83
83
|
const { _a: a, _b: b, _c: c, _d: d } = this
|
|
84
|
-
return `${first ? `M ${a
|
|
84
|
+
return `${first ? `M ${a} ` : ``} C${b} ${c} ${d}`
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
static GetAtT(segment: CubicBezier2d, t: number) {
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Vec } from '../Vec'
|
|
2
|
+
import { Edge2d } from './Edge2d'
|
|
3
|
+
|
|
4
|
+
describe('Edge2d.getSvgPathData', () => {
|
|
5
|
+
it('emits a move and a line command, rounded to two decimals', () => {
|
|
6
|
+
const edge = new Edge2d({ start: new Vec(0.123456, 0), end: new Vec(10, 10.987654) })
|
|
7
|
+
expect(edge.getSvgPathData()).toBe('M0.12, 0 L10, 10.99')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('omits the move command when not first', () => {
|
|
11
|
+
const edge = new Edge2d({ start: new Vec(0.123456, 0), end: new Vec(10, 10.987654) })
|
|
12
|
+
expect(edge.getSvgPathData(false)).toBe(' L10, 10.99')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('does not mutate the stored points', () => {
|
|
16
|
+
const start = new Vec(0.123456, 0)
|
|
17
|
+
const end = new Vec(10, 10.987654)
|
|
18
|
+
const edge = new Edge2d({ start, end })
|
|
19
|
+
edge.getSvgPathData()
|
|
20
|
+
expect(start).toMatchObject({ x: 0.123456, y: 0 })
|
|
21
|
+
expect(end).toMatchObject({ x: 10, y: 10.987654 })
|
|
4
22
|
})
|
|
5
23
|
})
|
|
@@ -340,20 +340,38 @@ describe('Group2d.transform', () => {
|
|
|
340
340
|
expect(t.vertices.length).toBe(8)
|
|
341
341
|
})
|
|
342
342
|
|
|
343
|
-
it('carries the
|
|
343
|
+
it('carries all the flags forward', () => {
|
|
344
344
|
const g = new Group2d({
|
|
345
345
|
children: [rectA()],
|
|
346
346
|
isLabel: true,
|
|
347
|
+
isEmptyLabel: true,
|
|
348
|
+
isInternal: true,
|
|
349
|
+
excludeFromShapeBounds: true,
|
|
347
350
|
debugColor: 'red',
|
|
348
351
|
ignore: true,
|
|
349
352
|
})
|
|
350
353
|
expect(g.transform(Mat.Identity())).toMatchObject({
|
|
351
354
|
isLabel: true,
|
|
355
|
+
isEmptyLabel: true,
|
|
356
|
+
isInternal: true,
|
|
357
|
+
excludeFromShapeBounds: true,
|
|
352
358
|
debugColor: 'red',
|
|
353
359
|
ignore: true,
|
|
354
360
|
})
|
|
355
361
|
})
|
|
356
362
|
|
|
363
|
+
it('applies the transform options over the carried flags', () => {
|
|
364
|
+
const g = new Group2d({ children: [rectA()], isLabel: true, isInternal: true })
|
|
365
|
+
expect(
|
|
366
|
+
g.transform(Mat.Identity(), {
|
|
367
|
+
isLabel: false,
|
|
368
|
+
isInternal: false,
|
|
369
|
+
debugColor: 'red',
|
|
370
|
+
ignore: true,
|
|
371
|
+
})
|
|
372
|
+
).toMatchObject({ isLabel: false, isInternal: false, debugColor: 'red', ignore: true })
|
|
373
|
+
})
|
|
374
|
+
|
|
357
375
|
it('scales hit testing with the transform', () => {
|
|
358
376
|
const t = group().transform(Mat.Scale(2, 2))
|
|
359
377
|
expect(t.hitTestPoint(new Vec(100, 50), 0, false)).toBe(true)
|
|
@@ -361,12 +379,15 @@ describe('Group2d.transform', () => {
|
|
|
361
379
|
expect(t.distanceToPoint(new Vec(300, 50))).toBe(100)
|
|
362
380
|
})
|
|
363
381
|
|
|
364
|
-
//
|
|
365
|
-
it('
|
|
382
|
+
// #10562: transform used to drop the ignored children.
|
|
383
|
+
it('keeps the transformed ignored children', () => {
|
|
366
384
|
const ignored = new Rectangle2d({ x: 500, width: 10, height: 10, isFilled: true, ignore: true })
|
|
367
385
|
const g = new Group2d({ children: [rectA(), ignored] })
|
|
368
|
-
|
|
369
|
-
expect(
|
|
386
|
+
const t = g.transform(Mat.Translate(10, 20)) as Group2d
|
|
387
|
+
expect(t.children.length).toBe(1)
|
|
388
|
+
expect(t.ignoredChildren.length).toBe(1)
|
|
389
|
+
expect(t.ignoredChildren[0].ignore).toBe(true)
|
|
390
|
+
expect(t.ignoredChildren[0].bounds).toMatchObject({ x: 510, y: 20, w: 10, h: 10 })
|
|
370
391
|
})
|
|
371
392
|
})
|
|
372
393
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { assert, invLerp, lerp } from '@tldraw/utils'
|
|
2
2
|
import { Box } from '../Box'
|
|
3
|
-
import {
|
|
3
|
+
import { MatModel } from '../Mat'
|
|
4
4
|
import { Vec, VecLike } from '../Vec'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Geometry2d,
|
|
7
|
+
Geometry2dFilters,
|
|
8
|
+
Geometry2dOptions,
|
|
9
|
+
TransformedGeometry2dOptions,
|
|
10
|
+
} from './Geometry2d'
|
|
6
11
|
|
|
7
12
|
/** @public */
|
|
8
13
|
export class Group2d extends Geometry2d {
|
|
@@ -226,12 +231,18 @@ export class Group2d extends Geometry2d {
|
|
|
226
231
|
return childTLength / totalLength
|
|
227
232
|
}
|
|
228
233
|
|
|
229
|
-
override transform(transform:
|
|
234
|
+
override transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d {
|
|
235
|
+
// Each child's own transform preserves its `ignore` flag, so the constructor
|
|
236
|
+
// re-partitions ignored children back into `ignoredChildren` instead of dropping
|
|
237
|
+
// them (#10562).
|
|
230
238
|
return new Group2d({
|
|
231
|
-
children: this.children.map((c) => c.transform(transform)),
|
|
232
|
-
isLabel: this.isLabel,
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
children: [...this.children, ...this.ignoredChildren].map((c) => c.transform(transform)),
|
|
240
|
+
isLabel: opts?.isLabel ?? this.isLabel,
|
|
241
|
+
isEmptyLabel: this.isEmptyLabel,
|
|
242
|
+
isInternal: opts?.isInternal ?? this.isInternal,
|
|
243
|
+
debugColor: opts?.debugColor ?? this.debugColor,
|
|
244
|
+
ignore: opts?.ignore ?? this.ignore,
|
|
245
|
+
excludeFromShapeBounds: this.excludeFromShapeBounds,
|
|
235
246
|
})
|
|
236
247
|
}
|
|
237
248
|
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Vec } from '../Vec'
|
|
2
|
+
import { Point2d } from './Point2d'
|
|
3
|
+
|
|
4
|
+
describe('Point2d.getSvgPathData', () => {
|
|
5
|
+
it('emits a move command rounded to two decimals', () => {
|
|
6
|
+
const point = new Point2d({ margin: 0, point: new Vec(0.123456, 7.891234) })
|
|
7
|
+
expect(point.getSvgPathData()).toBe('M0.12, 7.89')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('does not mutate the stored point', () => {
|
|
11
|
+
const vec = new Vec(0.123456, 7.891234)
|
|
12
|
+
const point = new Point2d({ margin: 0, point: vec })
|
|
13
|
+
point.getSvgPathData()
|
|
14
|
+
expect(vec).toMatchObject({ x: 0.123456, y: 7.891234 })
|
|
4
15
|
})
|
|
5
16
|
})
|
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 = '5.
|
|
4
|
+
export const version = '5.5.0-canary.04d1e6c4daff'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2026-05-06T16:28:18.473Z',
|
|
7
|
-
minor: '2026-09-
|
|
8
|
-
patch: '2026-09-
|
|
7
|
+
minor: '2026-09-04T14:57:28.023Z',
|
|
8
|
+
patch: '2026-09-04T14:57:28.023Z',
|
|
9
9
|
}
|