@tldraw/editor 3.14.0-canary.ff61ab6deaa2 → 3.14.0-internal.601adf6e424b
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 +74 -46
- package/dist-cjs/index.js +1 -3
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js +1 -12
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +3 -3
- package/dist-cjs/lib/editor/Editor.js +62 -21
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +1 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +73 -42
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
- package/dist-cjs/lib/editor/tools/StateNode.js +3 -3
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
- package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
- package/dist-cjs/lib/hooks/useCanvasEvents.js +1 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +0 -6
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/utils/areShapesContentEqual.js +1 -1
- package/dist-cjs/lib/utils/areShapesContentEqual.js.map +2 -2
- package/dist-cjs/lib/utils/richText.js +7 -2
- package/dist-cjs/lib/utils/richText.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 +74 -46
- package/dist-esm/index.mjs +1 -3
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +1 -1
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +62 -21
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +1 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +73 -42
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
- package/dist-esm/lib/editor/tools/StateNode.mjs +3 -3
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +0 -6
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/utils/areShapesContentEqual.mjs +1 -1
- package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +2 -2
- package/dist-esm/lib/utils/richText.mjs +8 -3
- package/dist-esm/lib/utils/richText.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +433 -482
- package/package.json +8 -9
- package/src/index.ts +2 -1
- package/src/lib/config/TLSessionStateSnapshot.ts +1 -1
- package/src/lib/editor/Editor.test.ts +252 -3
- package/src/lib/editor/Editor.ts +61 -18
- package/src/lib/editor/bindings/BindingUtil.ts +6 -0
- package/src/lib/editor/managers/FontManager/FontManager.ts +1 -2
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +1 -5
- package/src/lib/editor/managers/TextManager/TextManager.ts +118 -86
- package/src/lib/editor/shapes/ShapeUtil.ts +1 -0
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
- package/src/lib/editor/tools/StateNode.ts +3 -3
- package/src/lib/editor/types/emit-types.ts +4 -0
- package/src/lib/editor/types/external-content.ts +11 -2
- package/src/lib/hooks/useCanvasEvents.ts +0 -1
- package/src/lib/primitives/Box.ts +0 -8
- package/src/lib/utils/areShapesContentEqual.ts +1 -2
- package/src/lib/utils/richText.ts +9 -3
- package/src/version.ts +3 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/editor",
|
|
3
3
|
"description": "A tiny little drawing app (editor).",
|
|
4
|
-
"version": "3.14.0-
|
|
4
|
+
"version": "3.14.0-internal.601adf6e424b",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -48,20 +48,19 @@
|
|
|
48
48
|
"@tiptap/core": "^2.9.1",
|
|
49
49
|
"@tiptap/pm": "^2.9.1",
|
|
50
50
|
"@tiptap/react": "^2.9.1",
|
|
51
|
-
"@tldraw/state": "3.14.0-
|
|
52
|
-
"@tldraw/state-react": "3.14.0-
|
|
53
|
-
"@tldraw/store": "3.14.0-
|
|
54
|
-
"@tldraw/tlschema": "3.14.0-
|
|
55
|
-
"@tldraw/utils": "3.14.0-
|
|
56
|
-
"@tldraw/validate": "3.14.0-
|
|
51
|
+
"@tldraw/state": "3.14.0-internal.601adf6e424b",
|
|
52
|
+
"@tldraw/state-react": "3.14.0-internal.601adf6e424b",
|
|
53
|
+
"@tldraw/store": "3.14.0-internal.601adf6e424b",
|
|
54
|
+
"@tldraw/tlschema": "3.14.0-internal.601adf6e424b",
|
|
55
|
+
"@tldraw/utils": "3.14.0-internal.601adf6e424b",
|
|
56
|
+
"@tldraw/validate": "3.14.0-internal.601adf6e424b",
|
|
57
57
|
"@types/core-js": "^2.5.8",
|
|
58
58
|
"@use-gesture/react": "^10.3.1",
|
|
59
59
|
"classnames": "^2.5.1",
|
|
60
60
|
"core-js": "^3.40.0",
|
|
61
61
|
"eventemitter3": "^4.0.7",
|
|
62
62
|
"idb": "^7.1.1",
|
|
63
|
-
"is-plain-object": "^5.0.0"
|
|
64
|
-
"lodash.isequal": "^4.5.0"
|
|
63
|
+
"is-plain-object": "^5.0.0"
|
|
65
64
|
},
|
|
66
65
|
"peerDependencies": {
|
|
67
66
|
"react": "^18.2.0 || ^19.0.0",
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ import 'core-js/stable/array/flat-map.js'
|
|
|
4
4
|
import 'core-js/stable/array/flat.js'
|
|
5
5
|
import 'core-js/stable/string/at.js'
|
|
6
6
|
import 'core-js/stable/string/replace-all.js'
|
|
7
|
-
export { areShapesContentEqual } from './lib/utils/areShapesContentEqual'
|
|
8
7
|
|
|
9
8
|
// eslint-disable-next-line local/no-export-star
|
|
10
9
|
export * from '@tldraw/state'
|
|
@@ -175,6 +174,7 @@ export {
|
|
|
175
174
|
} from './lib/editor/managers/SnapManager/SnapManager'
|
|
176
175
|
export {
|
|
177
176
|
TextManager,
|
|
177
|
+
type TLMeasureTextOpts,
|
|
178
178
|
type TLMeasureTextSpanOpts,
|
|
179
179
|
} from './lib/editor/managers/TextManager/TextManager'
|
|
180
180
|
export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesManager/UserPreferencesManager'
|
|
@@ -253,6 +253,7 @@ export {
|
|
|
253
253
|
type TLExternalContent,
|
|
254
254
|
type TLExternalContentSource,
|
|
255
255
|
type TLFileExternalAsset,
|
|
256
|
+
type TLFileReplaceExternalContent,
|
|
256
257
|
type TLFilesExternalContent,
|
|
257
258
|
type TLSvgTextExternalContent,
|
|
258
259
|
type TLTextExternalContent,
|
|
@@ -14,12 +14,12 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
deleteFromSessionStorage,
|
|
16
16
|
getFromSessionStorage,
|
|
17
|
+
isEqual,
|
|
17
18
|
setInSessionStorage,
|
|
18
19
|
structuredClone,
|
|
19
20
|
uniqueId,
|
|
20
21
|
} from '@tldraw/utils'
|
|
21
22
|
import { T } from '@tldraw/validate'
|
|
22
|
-
import isEqual from 'lodash.isequal'
|
|
23
23
|
import { tlenv } from '../globals/environment'
|
|
24
24
|
|
|
25
25
|
const tabIdKey = 'TLDRAW_TAB_ID_v2' as const
|
|
@@ -17,6 +17,7 @@ type ICustomShape = TLBaseShape<
|
|
|
17
17
|
w: number
|
|
18
18
|
h: number
|
|
19
19
|
text: string | undefined
|
|
20
|
+
isFilled: boolean
|
|
20
21
|
}
|
|
21
22
|
>
|
|
22
23
|
|
|
@@ -26,19 +27,21 @@ class CustomShape extends ShapeUtil<ICustomShape> {
|
|
|
26
27
|
w: T.number,
|
|
27
28
|
h: T.number,
|
|
28
29
|
text: T.string.optional(),
|
|
30
|
+
isFilled: T.boolean,
|
|
29
31
|
}
|
|
30
32
|
getDefaultProps(): ICustomShape['props'] {
|
|
31
33
|
return {
|
|
32
34
|
w: 200,
|
|
33
35
|
h: 200,
|
|
34
36
|
text: '',
|
|
37
|
+
isFilled: false,
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
getGeometry(shape: ICustomShape): Geometry2d {
|
|
38
41
|
return new Rectangle2d({
|
|
39
42
|
width: shape.props.w,
|
|
40
43
|
height: shape.props.h,
|
|
41
|
-
isFilled:
|
|
44
|
+
isFilled: shape.props.isFilled,
|
|
42
45
|
})
|
|
43
46
|
}
|
|
44
47
|
indicator() {}
|
|
@@ -81,11 +84,11 @@ describe('updateShape', () => {
|
|
|
81
84
|
props: { w: 100, h: 100, text: 'Hello' },
|
|
82
85
|
})
|
|
83
86
|
const shape = editor.getShape(id) as ICustomShape
|
|
84
|
-
expect(shape.props).toEqual({ w: 100, h: 100, text: 'Hello' })
|
|
87
|
+
expect(shape.props).toEqual({ w: 100, h: 100, text: 'Hello', isFilled: false })
|
|
85
88
|
|
|
86
89
|
editor.updateShape({ ...shape, props: { ...shape.props, text: undefined } })
|
|
87
90
|
const updatedShape = editor.getShape(id) as ICustomShape
|
|
88
|
-
expect(updatedShape.props).toEqual({ w: 100, h: 100, text: undefined })
|
|
91
|
+
expect(updatedShape.props).toEqual({ w: 100, h: 100, text: undefined, isFilled: false })
|
|
89
92
|
})
|
|
90
93
|
})
|
|
91
94
|
|
|
@@ -176,3 +179,249 @@ describe('zoomToBounds', () => {
|
|
|
176
179
|
expect(editor.setCamera).toHaveBeenCalled()
|
|
177
180
|
})
|
|
178
181
|
})
|
|
182
|
+
|
|
183
|
+
describe('getShapesAtPoint', () => {
|
|
184
|
+
const ids = {
|
|
185
|
+
shape1: createShapeId('shape1'),
|
|
186
|
+
shape2: createShapeId('shape2'),
|
|
187
|
+
shape3: createShapeId('shape3'),
|
|
188
|
+
shape4: createShapeId('shape4'),
|
|
189
|
+
shape5: createShapeId('shape5'),
|
|
190
|
+
overlap1: createShapeId('overlap1'),
|
|
191
|
+
overlap2: createShapeId('overlap2'),
|
|
192
|
+
filledShape: createShapeId('filledShape'),
|
|
193
|
+
hollowShape: createShapeId('hollowShape'),
|
|
194
|
+
hiddenShape: createShapeId('hiddenShape'),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
beforeEach(() => {
|
|
198
|
+
// Create test shapes with different z-index positions
|
|
199
|
+
// Shape 1: Bottom layer, large square
|
|
200
|
+
editor.createShape({
|
|
201
|
+
id: ids.shape1,
|
|
202
|
+
type: 'my-custom-shape',
|
|
203
|
+
x: 0,
|
|
204
|
+
y: 0,
|
|
205
|
+
props: { w: 200, h: 200, text: 'Bottom' },
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Shape 2: Middle layer, overlapping square
|
|
209
|
+
editor.createShape({
|
|
210
|
+
id: ids.shape2,
|
|
211
|
+
type: 'my-custom-shape',
|
|
212
|
+
x: 100,
|
|
213
|
+
y: 0,
|
|
214
|
+
props: { w: 200, h: 200, text: 'Middle' },
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Shape 3: Top layer, small square
|
|
218
|
+
editor.createShape({
|
|
219
|
+
id: ids.shape3,
|
|
220
|
+
type: 'my-custom-shape',
|
|
221
|
+
x: 50,
|
|
222
|
+
y: 50,
|
|
223
|
+
props: { w: 100, h: 100, text: 'Top' },
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// Shape 4: Separate area, no overlap
|
|
227
|
+
editor.createShape({
|
|
228
|
+
id: ids.shape4,
|
|
229
|
+
type: 'my-custom-shape',
|
|
230
|
+
x: 50,
|
|
231
|
+
y: 100,
|
|
232
|
+
props: { w: 100, h: 100, text: 'Separate' },
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('returns shapes at a point in reverse z-index order', () => {
|
|
237
|
+
// Point at (50, 50) should hit shape3's edge (since it's at 50,50 with size 100x100)
|
|
238
|
+
// This point is exactly at the top-left corner of shape3
|
|
239
|
+
const shapes = editor.getShapesAtPoint({ x: 50, y: 50 })
|
|
240
|
+
const shapeIds = shapes.map((s) => s.id)
|
|
241
|
+
|
|
242
|
+
expect(shapeIds).toEqual([ids.shape3])
|
|
243
|
+
expect(shapes).toHaveLength(1)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('returns empty array when no shapes at point', () => {
|
|
247
|
+
const shapes = editor.getShapesAtPoint({ x: 1000, y: 1000 })
|
|
248
|
+
expect(shapes).toEqual([])
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('returns single shape when point hits only one shape', () => {
|
|
252
|
+
// Point at right edge of shape2 where it doesn't overlap with other shapes
|
|
253
|
+
// Shape2 is at (100,0) with size 200x200, so right edge is at x=300
|
|
254
|
+
const shapes = editor.getShapesAtPoint({ x: 300, y: 100 })
|
|
255
|
+
expect(shapes).toHaveLength(1)
|
|
256
|
+
expect(shapes[0].id).toBe(ids.shape2)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('returns shapes on edge when point is exactly on boundary', () => {
|
|
260
|
+
// Point at exact edge of shape1
|
|
261
|
+
const shapes = editor.getShapesAtPoint({ x: 0, y: 0 })
|
|
262
|
+
expect(shapes).toHaveLength(1)
|
|
263
|
+
expect(shapes[0].id).toBe(ids.shape1)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('respects hitInside option when false (default)', () => {
|
|
267
|
+
// Point inside shape1 (at 0,0 with size 200x200) but with hitInside false should not hit
|
|
268
|
+
const shapes = editor.getShapesAtPoint({ x: 25, y: 25 }, { hitInside: false })
|
|
269
|
+
expect(shapes).toEqual([])
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('respects hitInside option when true', () => {
|
|
273
|
+
// Point inside shape1 (at 0,0 with size 200x200) with hitInside true should hit
|
|
274
|
+
const shapes = editor.getShapesAtPoint({ x: 25, y: 25 }, { hitInside: true })
|
|
275
|
+
expect(shapes).toHaveLength(1)
|
|
276
|
+
expect(shapes[0].id).toBe(ids.shape1)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('respects margin option', () => {
|
|
280
|
+
// Point slightly outside shape1 at bottom edge but within margin should hit only shape1
|
|
281
|
+
// Shape1 is at (0,0) with size 200x200, shape2 goes to (300,200) so avoid overlap at (200,200)
|
|
282
|
+
const shapes = editor.getShapesAtPoint({ x: 205, y: 100 }, { margin: 10 })
|
|
283
|
+
expect(shapes).toHaveLength(1)
|
|
284
|
+
expect(shapes[0].id).toBe(ids.shape1)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('filters out hidden shapes', () => {
|
|
288
|
+
// Create a spy to mock isShapeHidden
|
|
289
|
+
const isShapeHiddenSpy = jest.spyOn(editor, 'isShapeHidden')
|
|
290
|
+
isShapeHiddenSpy.mockImplementation((shape) => {
|
|
291
|
+
return typeof shape === 'string' ? shape === ids.shape3 : shape.id === ids.shape3
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
const shapes = editor.getShapesAtPoint({ x: 50, y: 50 })
|
|
295
|
+
const shapeIds = shapes.map((s) => s.id)
|
|
296
|
+
|
|
297
|
+
// Should not include shape3 since it's hidden, and no other shapes are at this point
|
|
298
|
+
expect(shapeIds).toEqual([])
|
|
299
|
+
expect(shapes).toHaveLength(0)
|
|
300
|
+
|
|
301
|
+
isShapeHiddenSpy.mockRestore()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('handles point exactly at shape corner', () => {
|
|
305
|
+
// Point at bottom-left corner of shape1 where it doesn't overlap with other shapes
|
|
306
|
+
const shapes = editor.getShapesAtPoint({ x: 0, y: 200 })
|
|
307
|
+
expect(shapes).toHaveLength(1)
|
|
308
|
+
expect(shapes[0].id).toBe(ids.shape1)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('handles overlapping shapes with different hit areas', () => {
|
|
312
|
+
// Point that hits both shape1 and shape2 edges (they overlap at x=100,y=0)
|
|
313
|
+
const shapes = editor.getShapesAtPoint({ x: 100, y: 0 })
|
|
314
|
+
const shapeIds = shapes.map((s) => s.id)
|
|
315
|
+
|
|
316
|
+
// Both shapes should be detected at this overlapping point (reversed order - top-most first)
|
|
317
|
+
expect(shapeIds).toEqual([ids.shape2, ids.shape1])
|
|
318
|
+
expect(shapes).toHaveLength(2)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('maintains reverse shape order and responds to z-index changes', () => {
|
|
322
|
+
// Create filled shape that overlaps with shape2
|
|
323
|
+
editor.createShape({
|
|
324
|
+
id: ids.shape5,
|
|
325
|
+
type: 'my-custom-shape',
|
|
326
|
+
x: 110,
|
|
327
|
+
y: 110,
|
|
328
|
+
props: { w: 200, h: 200, isFilled: true, text: 'Shape5' },
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// Test with hitInside to detect multiple shapes
|
|
332
|
+
// Point (120,120) will hit shape1, shape2, shape3, shape4, and shape5 with hitInside: true
|
|
333
|
+
const shapes = editor.getShapesAtPoint({ x: 120, y: 120 }, { hitInside: true })
|
|
334
|
+
const shapeIds = shapes.map((s) => s.id)
|
|
335
|
+
|
|
336
|
+
// All shapes that contain this point should be returned in reverse z-index order (top-most first)
|
|
337
|
+
expect(shapeIds).toEqual([ids.shape5, ids.shape4, ids.shape3, ids.shape2, ids.shape1])
|
|
338
|
+
|
|
339
|
+
// After bringing shape2 to front, order should change (shape2 becomes top-most)
|
|
340
|
+
editor.bringToFront([ids.shape2])
|
|
341
|
+
const shapes2 = editor.getShapesAtPoint({ x: 120, y: 120 }, { hitInside: true })
|
|
342
|
+
const shapeIds2 = shapes2.map((s) => s.id)
|
|
343
|
+
expect(shapeIds2).toEqual([ids.shape2, ids.shape5, ids.shape4, ids.shape3, ids.shape1])
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('combines hitInside and margin options', () => {
|
|
347
|
+
// Point inside shape1 (at 0,0 with size 200x200) with hitInside and margin
|
|
348
|
+
const shapes = editor.getShapesAtPoint({ x: 25, y: 25 }, { hitInside: true, margin: 5 })
|
|
349
|
+
expect(shapes).toHaveLength(1)
|
|
350
|
+
expect(shapes[0].id).toBe(ids.shape1)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('returns empty array when all shapes are hidden', () => {
|
|
354
|
+
// Mock all shapes as hidden
|
|
355
|
+
const isShapeHiddenSpy = jest.spyOn(editor, 'isShapeHidden')
|
|
356
|
+
isShapeHiddenSpy.mockReturnValue(true)
|
|
357
|
+
|
|
358
|
+
const shapes = editor.getShapesAtPoint({ x: 50, y: 50 })
|
|
359
|
+
expect(shapes).toEqual([])
|
|
360
|
+
|
|
361
|
+
isShapeHiddenSpy.mockRestore()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('returns multiple shapes at same point in reverse z-index order', () => {
|
|
365
|
+
// Create two shapes at exactly the same position (away from existing shapes)
|
|
366
|
+
editor.createShape({
|
|
367
|
+
id: ids.overlap1,
|
|
368
|
+
type: 'my-custom-shape',
|
|
369
|
+
x: 600,
|
|
370
|
+
y: 600,
|
|
371
|
+
props: { w: 100, h: 100, text: 'First' },
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
editor.createShape({
|
|
375
|
+
id: ids.overlap2,
|
|
376
|
+
type: 'my-custom-shape',
|
|
377
|
+
x: 600,
|
|
378
|
+
y: 600,
|
|
379
|
+
props: { w: 100, h: 100, text: 'Second' },
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
// Test at corner where both shapes' edges meet
|
|
383
|
+
const shapes = editor.getShapesAtPoint({ x: 600, y: 600 })
|
|
384
|
+
const shapeIds = shapes.map((s) => s.id)
|
|
385
|
+
|
|
386
|
+
// Should return both shapes in reverse z-index order (top-most first)
|
|
387
|
+
expect(shapeIds).toEqual([ids.overlap2, ids.overlap1])
|
|
388
|
+
expect(shapes).toHaveLength(2)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('respects isFilled property for hit detection', () => {
|
|
392
|
+
// Create a filled shape
|
|
393
|
+
editor.createShape({
|
|
394
|
+
id: ids.filledShape,
|
|
395
|
+
type: 'my-custom-shape',
|
|
396
|
+
x: 300,
|
|
397
|
+
y: 300,
|
|
398
|
+
props: { w: 100, h: 100, isFilled: true, text: 'Filled' },
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
// Create a hollow shape at the same position
|
|
402
|
+
editor.createShape({
|
|
403
|
+
id: ids.hollowShape,
|
|
404
|
+
type: 'my-custom-shape',
|
|
405
|
+
x: 400,
|
|
406
|
+
y: 300,
|
|
407
|
+
props: { w: 100, h: 100, isFilled: false, text: 'Hollow' },
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
// Test point inside filled shape - should hit without hitInside option
|
|
411
|
+
const filledShapes = editor.getShapesAtPoint({ x: 350, y: 350 })
|
|
412
|
+
expect(filledShapes).toHaveLength(1)
|
|
413
|
+
expect(filledShapes[0].id).toBe(ids.filledShape)
|
|
414
|
+
|
|
415
|
+
// Test point inside hollow shape - should not hit without hitInside option
|
|
416
|
+
const hollowShapes = editor.getShapesAtPoint({ x: 450, y: 350 })
|
|
417
|
+
expect(hollowShapes).toHaveLength(0)
|
|
418
|
+
|
|
419
|
+
// Test point inside hollow shape with hitInside - should hit
|
|
420
|
+
const hollowShapesWithHitInside = editor.getShapesAtPoint(
|
|
421
|
+
{ x: 450, y: 350 },
|
|
422
|
+
{ hitInside: true }
|
|
423
|
+
)
|
|
424
|
+
expect(hollowShapesWithHitInside).toHaveLength(1)
|
|
425
|
+
expect(hollowShapesWithHitInside[0].id).toBe(ids.hollowShape)
|
|
426
|
+
})
|
|
427
|
+
})
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -348,6 +348,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
348
348
|
this.getContainer = getContainer
|
|
349
349
|
|
|
350
350
|
this.textMeasure = new TextManager(this)
|
|
351
|
+
this.disposables.add(() => this.textMeasure.dispose())
|
|
352
|
+
|
|
351
353
|
this.fonts = new FontManager(this, fontAssetUrls)
|
|
352
354
|
|
|
353
355
|
this._tickManager = new TickManager(this)
|
|
@@ -506,14 +508,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
506
508
|
shape: {
|
|
507
509
|
afterChange: (shapeBefore, shapeAfter) => {
|
|
508
510
|
for (const binding of this.getBindingsInvolvingShape(shapeAfter)) {
|
|
509
|
-
if (areShapesContentEqual(shapeBefore, shapeAfter)) continue
|
|
510
|
-
|
|
511
511
|
invalidBindingTypes.add(binding.type)
|
|
512
512
|
if (binding.fromId === shapeAfter.id) {
|
|
513
513
|
this.getBindingUtil(binding).onAfterChangeFromShape?.({
|
|
514
514
|
binding,
|
|
515
515
|
shapeBefore,
|
|
516
516
|
shapeAfter,
|
|
517
|
+
reason: 'self',
|
|
517
518
|
})
|
|
518
519
|
}
|
|
519
520
|
if (binding.toId === shapeAfter.id) {
|
|
@@ -521,6 +522,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
521
522
|
binding,
|
|
522
523
|
shapeBefore,
|
|
523
524
|
shapeAfter,
|
|
525
|
+
reason: 'self',
|
|
524
526
|
})
|
|
525
527
|
}
|
|
526
528
|
}
|
|
@@ -539,6 +541,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
539
541
|
binding,
|
|
540
542
|
shapeBefore: descendantShape,
|
|
541
543
|
shapeAfter: descendantShape,
|
|
544
|
+
reason: 'ancestry',
|
|
542
545
|
})
|
|
543
546
|
}
|
|
544
547
|
if (binding.toId === descendantShape.id) {
|
|
@@ -546,6 +549,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
546
549
|
binding,
|
|
547
550
|
shapeBefore: descendantShape,
|
|
548
551
|
shapeAfter: descendantShape,
|
|
552
|
+
reason: 'ancestry',
|
|
549
553
|
})
|
|
550
554
|
}
|
|
551
555
|
}
|
|
@@ -2118,6 +2122,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2118
2122
|
return this.getShapesPageBounds(this.getSelectedShapeIds())
|
|
2119
2123
|
}
|
|
2120
2124
|
|
|
2125
|
+
/**
|
|
2126
|
+
* The bounds of the selection bounding box in the current page space.
|
|
2127
|
+
*
|
|
2128
|
+
* @readonly
|
|
2129
|
+
* @public
|
|
2130
|
+
*/
|
|
2131
|
+
getSelectionScreenBounds(): Box | undefined {
|
|
2132
|
+
const bounds = this.getSelectionPageBounds()
|
|
2133
|
+
if (!bounds) return undefined
|
|
2134
|
+
const { x, y } = this.pageToScreen(bounds.point)
|
|
2135
|
+
const zoom = this.getZoomLevel()
|
|
2136
|
+
return new Box(x, y, bounds.width * zoom, bounds.height * zoom)
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2121
2139
|
/**
|
|
2122
2140
|
* @internal
|
|
2123
2141
|
*/
|
|
@@ -5035,28 +5053,33 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5035
5053
|
*
|
|
5036
5054
|
* @public
|
|
5037
5055
|
*/
|
|
5038
|
-
isShapeOrAncestorLocked(shape?: TLShape): boolean
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
if (shape.isLocked) return true
|
|
5044
|
-
return this.isShapeOrAncestorLocked(this.getShapeParent(shape))
|
|
5056
|
+
isShapeOrAncestorLocked(shape?: TLShape | TLShapeId): boolean {
|
|
5057
|
+
const _shape = shape && this.getShape(shape)
|
|
5058
|
+
if (_shape === undefined) return false
|
|
5059
|
+
if (_shape.isLocked) return true
|
|
5060
|
+
return this.isShapeOrAncestorLocked(this.getShapeParent(_shape))
|
|
5045
5061
|
}
|
|
5046
5062
|
|
|
5063
|
+
/**
|
|
5064
|
+
* Get shapes that are outside of the viewport.
|
|
5065
|
+
*
|
|
5066
|
+
* @public
|
|
5067
|
+
*/
|
|
5047
5068
|
@computed
|
|
5048
|
-
|
|
5049
|
-
return
|
|
5069
|
+
getNotVisibleShapes() {
|
|
5070
|
+
return this._notVisibleShapes.get()
|
|
5050
5071
|
}
|
|
5051
5072
|
|
|
5073
|
+
private _notVisibleShapes = notVisibleShapes(this)
|
|
5074
|
+
|
|
5052
5075
|
/**
|
|
5053
|
-
* Get culled shapes.
|
|
5076
|
+
* Get culled shapes (those that should not render), taking into account which shapes are selected or editing.
|
|
5054
5077
|
*
|
|
5055
5078
|
* @public
|
|
5056
5079
|
*/
|
|
5057
5080
|
@computed
|
|
5058
5081
|
getCulledShapes() {
|
|
5059
|
-
const notVisibleShapes = this.
|
|
5082
|
+
const notVisibleShapes = this.getNotVisibleShapes()
|
|
5060
5083
|
const selectedShapeIds = this.getSelectedShapeIds()
|
|
5061
5084
|
const editingId = this.getEditingShapeId()
|
|
5062
5085
|
const culledShapes = new Set<TLShapeId>(notVisibleShapes)
|
|
@@ -5305,21 +5328,23 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5305
5328
|
* @example
|
|
5306
5329
|
* ```ts
|
|
5307
5330
|
* editor.getShapesAtPoint({ x: 100, y: 100 })
|
|
5308
|
-
* editor.getShapesAtPoint({ x: 100, y: 100 }, { hitInside: true,
|
|
5331
|
+
* editor.getShapesAtPoint({ x: 100, y: 100 }, { hitInside: true, margin: 8 })
|
|
5309
5332
|
* ```
|
|
5310
5333
|
*
|
|
5311
5334
|
* @param point - The page point to test.
|
|
5312
5335
|
* @param opts - The options for the hit point testing.
|
|
5313
5336
|
*
|
|
5337
|
+
* @returns An array of shapes at the given point, sorted in reverse order of their absolute z-index (top-most shape first).
|
|
5338
|
+
*
|
|
5314
5339
|
* @public
|
|
5315
5340
|
*/
|
|
5316
5341
|
getShapesAtPoint(
|
|
5317
5342
|
point: VecLike,
|
|
5318
5343
|
opts = {} as { margin?: number; hitInside?: boolean }
|
|
5319
5344
|
): TLShape[] {
|
|
5320
|
-
return this.
|
|
5321
|
-
(shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts)
|
|
5322
|
-
|
|
5345
|
+
return this.getCurrentPageShapesSorted()
|
|
5346
|
+
.filter((shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts))
|
|
5347
|
+
.reverse()
|
|
5323
5348
|
}
|
|
5324
5349
|
|
|
5325
5350
|
/**
|
|
@@ -6186,11 +6211,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6186
6211
|
*/
|
|
6187
6212
|
duplicateShapes(shapes: TLShapeId[] | TLShape[], offset?: VecLike): this {
|
|
6188
6213
|
this.run(() => {
|
|
6189
|
-
const
|
|
6214
|
+
const _ids =
|
|
6190
6215
|
typeof shapes[0] === 'string'
|
|
6191
6216
|
? (shapes as TLShapeId[])
|
|
6192
6217
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6193
6218
|
|
|
6219
|
+
const ids = this._shouldIgnoreShapeLock ? _ids : this._getUnlockedShapeIds(_ids)
|
|
6194
6220
|
if (ids.length <= 0) return this
|
|
6195
6221
|
|
|
6196
6222
|
const initialIds = new Set(ids)
|
|
@@ -7911,6 +7937,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7911
7937
|
}
|
|
7912
7938
|
})
|
|
7913
7939
|
|
|
7940
|
+
this.emit('created-shapes', shapeRecordsToCreate)
|
|
7941
|
+
this.emit('edit')
|
|
7914
7942
|
this.store.put(shapeRecordsToCreate)
|
|
7915
7943
|
})
|
|
7916
7944
|
|
|
@@ -8305,6 +8333,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8305
8333
|
updates.push(updated)
|
|
8306
8334
|
}
|
|
8307
8335
|
|
|
8336
|
+
this.emit('edited-shapes', updates)
|
|
8337
|
+
this.emit('edit')
|
|
8308
8338
|
this.store.put(updates)
|
|
8309
8339
|
})
|
|
8310
8340
|
}
|
|
@@ -8354,6 +8384,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8354
8384
|
})
|
|
8355
8385
|
}
|
|
8356
8386
|
|
|
8387
|
+
this.emit('deleted-shapes', [...allShapeIdsToDelete])
|
|
8388
|
+
this.emit('edit')
|
|
8357
8389
|
return this.run(() => this.store.remove([...allShapeIdsToDelete]))
|
|
8358
8390
|
}
|
|
8359
8391
|
|
|
@@ -8802,6 +8834,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8802
8834
|
} = {
|
|
8803
8835
|
text: null,
|
|
8804
8836
|
files: null,
|
|
8837
|
+
'file-replace': null,
|
|
8805
8838
|
embed: null,
|
|
8806
8839
|
'svg-text': null,
|
|
8807
8840
|
url: null,
|
|
@@ -8851,6 +8884,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8851
8884
|
return this.externalContentHandlers[info.type]?.(info as any)
|
|
8852
8885
|
}
|
|
8853
8886
|
|
|
8887
|
+
/**
|
|
8888
|
+
* Handle replacing external content.
|
|
8889
|
+
*
|
|
8890
|
+
* @param info - Info about the external content.
|
|
8891
|
+
*/
|
|
8892
|
+
async replaceExternalContent<E>(info: TLExternalContent<E>): Promise<void> {
|
|
8893
|
+
return this.externalContentHandlers[info.type]?.(info as any)
|
|
8894
|
+
}
|
|
8895
|
+
|
|
8854
8896
|
/**
|
|
8855
8897
|
* Get content that can be exported for the given shape ids.
|
|
8856
8898
|
*
|
|
@@ -9268,6 +9310,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9268
9310
|
if (rootShapes.length === 1) {
|
|
9269
9311
|
const onlyRoot = rootShapes[0] as TLFrameShape
|
|
9270
9312
|
// If the old bounds are in the viewport...
|
|
9313
|
+
// todo: replace frame references with shapes that can accept children
|
|
9271
9314
|
if (this.isShapeOfType<TLFrameShape>(onlyRoot, 'frame')) {
|
|
9272
9315
|
while (
|
|
9273
9316
|
this.getShapesAtPoint(point).some(
|
|
@@ -62,6 +62,12 @@ export interface BindingOnShapeChangeOptions<Binding extends TLUnknownBinding> {
|
|
|
62
62
|
shapeBefore: TLShape
|
|
63
63
|
/** The shape record after the change is made. */
|
|
64
64
|
shapeAfter: TLShape
|
|
65
|
+
/**
|
|
66
|
+
* Why did this shape change?
|
|
67
|
+
* - 'self': the shape itself changed
|
|
68
|
+
* - 'ancestry': the ancestry of the shape changed, but the shape itself may not have done
|
|
69
|
+
*/
|
|
70
|
+
reason: 'self' | 'ancestry'
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/**
|
|
@@ -96,8 +96,7 @@ export class FontManager {
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
areResultsEqual: areArraysShallowEqual,
|
|
99
|
-
|
|
100
|
-
areRecordsEqual: (a, b) => a.props.richText === b.props.richText,
|
|
99
|
+
areRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,
|
|
101
100
|
}
|
|
102
101
|
)
|
|
103
102
|
|
|
@@ -99,7 +99,7 @@ describe('TextManager', () => {
|
|
|
99
99
|
})
|
|
100
100
|
|
|
101
101
|
it('should handle empty text', () => {
|
|
102
|
-
const result = textManager.measureText('', defaultOpts)
|
|
102
|
+
const result = textManager.measureText('', { ...defaultOpts, measureScrollWidth: true })
|
|
103
103
|
expect(result).toHaveProperty('x', 0)
|
|
104
104
|
expect(result).toHaveProperty('y', 0)
|
|
105
105
|
expect(result).toHaveProperty('w')
|
|
@@ -128,7 +128,6 @@ describe('TextManager', () => {
|
|
|
128
128
|
y: 0,
|
|
129
129
|
w: expect.any(Number),
|
|
130
130
|
h: expect.any(Number),
|
|
131
|
-
scrollWidth: expect.any(Number),
|
|
132
131
|
})
|
|
133
132
|
})
|
|
134
133
|
|
|
@@ -141,7 +140,6 @@ describe('TextManager', () => {
|
|
|
141
140
|
y: 0,
|
|
142
141
|
w: expect.any(Number),
|
|
143
142
|
h: expect.any(Number),
|
|
144
|
-
scrollWidth: expect.any(Number),
|
|
145
143
|
})
|
|
146
144
|
})
|
|
147
145
|
|
|
@@ -154,7 +152,6 @@ describe('TextManager', () => {
|
|
|
154
152
|
y: 0,
|
|
155
153
|
w: expect.any(Number),
|
|
156
154
|
h: expect.any(Number),
|
|
157
|
-
scrollWidth: expect.any(Number),
|
|
158
155
|
})
|
|
159
156
|
})
|
|
160
157
|
|
|
@@ -173,7 +170,6 @@ describe('TextManager', () => {
|
|
|
173
170
|
y: 0,
|
|
174
171
|
w: expect.any(Number),
|
|
175
172
|
h: expect.any(Number),
|
|
176
|
-
scrollWidth: expect.any(Number),
|
|
177
173
|
})
|
|
178
174
|
})
|
|
179
175
|
})
|