@tldraw/editor 5.5.0-next.f9308d324e1c → 5.5.0
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 +49 -23
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +16 -12
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +42 -3
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +5 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +2 -5
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +168 -181
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -0
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/EditorManager.js +11 -2
- package/dist-cjs/lib/editor/managers/EditorManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js +49 -63
- package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +1 -1
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +24 -24
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js +7 -1
- package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +4 -3
- package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +4 -4
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.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/editor/types/external-content.js.map +1 -1
- package/dist-cjs/lib/exports/ExportDelay.js +10 -2
- package/dist-cjs/lib/exports/ExportDelay.js.map +2 -2
- package/dist-cjs/lib/exports/FontEmbedder.js +25 -11
- package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/StyleEmbedder.js +9 -4
- package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/fetchCache.js +3 -0
- package/dist-cjs/lib/exports/fetchCache.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgAsImage.js +10 -4
- package/dist-cjs/lib/exports/getSvgAsImage.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgJsx.js +18 -9
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/exports/parseCss.js +1 -1
- package/dist-cjs/lib/exports/parseCss.js.map +1 -1
- package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +24 -0
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useIdentity.js +6 -2
- package/dist-cjs/lib/hooks/useIdentity.js.map +3 -3
- package/dist-cjs/lib/license/LicenseManager.js +9 -1
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +12 -4
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/utils/debug-flags.js +0 -6
- package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
- package/dist-cjs/lib/utils/deepLinks.js +2 -1
- package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
- package/dist-cjs/lib/utils/sync/LocalIndexedDb.js +32 -25
- package/dist-cjs/lib/utils/sync/LocalIndexedDb.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +81 -40
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.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 +49 -23
- package/dist-esm/index.mjs +4 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +17 -13
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +42 -3
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +5 -1
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +2 -5
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +168 -181
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -0
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/EditorManager.mjs +11 -2
- package/dist-esm/lib/editor/managers/EditorManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs +49 -63
- package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +1 -1
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +24 -24
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs +7 -1
- package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +4 -3
- package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +4 -4
- package/dist-esm/lib/editor/managers/TextManager/TextManager.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/exports/ExportDelay.mjs +11 -3
- package/dist-esm/lib/exports/ExportDelay.mjs.map +2 -2
- package/dist-esm/lib/exports/FontEmbedder.mjs +25 -11
- package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/StyleEmbedder.mjs +9 -4
- package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/fetchCache.mjs +3 -0
- package/dist-esm/lib/exports/fetchCache.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgAsImage.mjs +10 -4
- package/dist-esm/lib/exports/getSvgAsImage.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs +18 -9
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/exports/parseCss.mjs +1 -1
- package/dist-esm/lib/exports/parseCss.mjs.map +1 -1
- package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +24 -0
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useIdentity.mjs +7 -3
- package/dist-esm/lib/hooks/useIdentity.mjs.map +3 -3
- package/dist-esm/lib/license/LicenseManager.mjs +9 -1
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +13 -5
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/utils/debug-flags.mjs +0 -6
- package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
- package/dist-esm/lib/utils/deepLinks.mjs +2 -1
- package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs +32 -25
- package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +81 -40
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +9 -1
- package/package.json +8 -11
- package/src/index.ts +5 -1
- package/src/lib/TldrawEditor.tsx +35 -21
- package/src/lib/components/MenuClickCapture.tsx +55 -4
- package/src/lib/components/default-components/DefaultCanvas.tsx +7 -1
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +13 -5
- package/src/lib/components/default-components/DefaultSpinner.tsx +4 -1
- package/src/lib/editor/Editor.test.ts +65 -0
- package/src/lib/editor/Editor.ts +261 -294
- package/src/lib/editor/derivations/bindingsIndex.ts +3 -0
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +1 -0
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +3 -2
- package/src/lib/editor/managers/EditorManager.test.ts +32 -1
- package/src/lib/editor/managers/EditorManager.ts +15 -3
- package/src/lib/editor/managers/PerformanceManager/PerformanceManager.test.ts +80 -0
- package/src/lib/editor/managers/PerformanceManager/PerformanceManager.ts +54 -81
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +1 -1
- package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +32 -36
- package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +13 -2
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +13 -0
- package/src/lib/editor/managers/SnapManager/SnapManager.ts +6 -4
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +74 -0
- package/src/lib/editor/managers/TextManager/TextManager.ts +6 -5
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +23 -8
- package/src/lib/editor/types/external-content.ts +5 -0
- package/src/lib/exports/ExportDelay.tsx +16 -4
- package/src/lib/exports/FontEmbedder.test.ts +99 -0
- package/src/lib/exports/FontEmbedder.ts +32 -11
- package/src/lib/exports/StyleEmbedder.ts +13 -4
- package/src/lib/exports/fetchCache.test.ts +22 -0
- package/src/lib/exports/fetchCache.ts +4 -0
- package/src/lib/exports/getSvgAsImage.ts +13 -5
- package/src/lib/exports/getSvgJsx.test.ts +43 -1
- package/src/lib/exports/getSvgJsx.tsx +28 -16
- package/src/lib/exports/parseCss.test.ts +4 -0
- package/src/lib/exports/parseCss.ts +2 -2
- package/src/lib/hooks/EditorComponentsContext.tsx +2 -1
- package/src/lib/hooks/useCanvasEvents.ts +42 -0
- package/src/lib/hooks/useIdentity.tsx +12 -1
- package/src/lib/license/LicenseManager.ts +13 -1
- package/src/lib/license/LicenseProvider.test.tsx +76 -0
- package/src/lib/license/LicenseProvider.tsx +19 -5
- package/src/lib/test/preserveFocus.test.tsx +81 -0
- package/src/lib/utils/debug-flags.ts +0 -6
- package/src/lib/utils/deepLinks.test.ts +15 -0
- package/src/lib/utils/deepLinks.ts +8 -3
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +121 -1
- package/src/lib/utils/sync/LocalIndexedDb.ts +41 -28
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +257 -1
- package/src/lib/utils/sync/TLLocalSyncClient.ts +112 -54
- package/src/version.ts +3 -3
- package/dist-cjs/lib/primitives/geometry/geometry.bench.js +0 -133
- package/dist-cjs/lib/primitives/geometry/geometry.bench.js.map +0 -7
- package/dist-esm/lib/primitives/geometry/geometry.bench.mjs +0 -132
- package/dist-esm/lib/primitives/geometry/geometry.bench.mjs.map +0 -7
- package/src/lib/primitives/geometry/geometry.bench.ts +0 -179
|
@@ -18,6 +18,8 @@ function fromScratch(bindingsQuery: Computed<TLBinding[], unknown>) {
|
|
|
18
18
|
} else {
|
|
19
19
|
bindingsForFromShape.push(binding)
|
|
20
20
|
}
|
|
21
|
+
// A binding from a shape to itself belongs in that shape's array once, not twice
|
|
22
|
+
if (toId === fromId) continue
|
|
21
23
|
const bindingsForToShape = shapesToBindings.get(toId)
|
|
22
24
|
if (!bindingsForToShape) {
|
|
23
25
|
shapesToBindings.set(toId, [binding])
|
|
@@ -83,6 +85,7 @@ export function bindingsIndex(editor: Editor): Computed<TLBindingsIndex> {
|
|
|
83
85
|
|
|
84
86
|
function addBinding(binding: TLBinding) {
|
|
85
87
|
ensureNewArray(binding.fromId).push(binding)
|
|
88
|
+
if (binding.toId === binding.fromId) return
|
|
86
89
|
ensureNewArray(binding.toId).push(binding)
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -25,7 +25,7 @@ export class ClickManager {
|
|
|
25
25
|
@bind
|
|
26
26
|
_getClickTimeout(state: TLClickState, id = uniqueId()) {
|
|
27
27
|
this._clickId = id
|
|
28
|
-
clearTimeout(this._clickTimeout)
|
|
28
|
+
this.editor.timers.clearTimeout(this._clickTimeout)
|
|
29
29
|
this._clickTimeout = this.editor.timers.setTimeout(
|
|
30
30
|
() => {
|
|
31
31
|
if (this._clickState === state && this._clickId === id) {
|
|
@@ -166,7 +166,8 @@ export class ClickManager {
|
|
|
166
166
|
*/
|
|
167
167
|
@bind
|
|
168
168
|
cancelDoubleClickTimeout() {
|
|
169
|
-
this.
|
|
169
|
+
this.editor.timers.clearTimeout(this._clickTimeout)
|
|
170
|
+
this._clickTimeout = undefined
|
|
170
171
|
this._clickState = 'idle'
|
|
171
172
|
// when a double click is cancelled, we are no longer pending any further
|
|
172
173
|
// clicks, so we set this to false even if the user is still pressing
|
|
@@ -4,16 +4,24 @@ import { EditorManager } from './EditorManager'
|
|
|
4
4
|
|
|
5
5
|
class TestManager extends EditorManager {
|
|
6
6
|
onTestEvent = vi.fn()
|
|
7
|
+
removeTestEvent: () => void
|
|
7
8
|
|
|
8
9
|
constructor(editor: Editor) {
|
|
9
10
|
super(editor)
|
|
10
|
-
this.addEditorEvent('frame', this.onTestEvent)
|
|
11
|
+
this.removeTestEvent = this.addEditorEvent('frame', this.onTestEvent)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
detachTestEvent() {
|
|
15
|
+
this.unregister(this.removeTestEvent)
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
cancelRaf = vi.fn()
|
|
14
19
|
startRaf() {
|
|
15
20
|
this.register(this.cancelRaf)
|
|
16
21
|
}
|
|
22
|
+
stopRaf() {
|
|
23
|
+
this.unregister(this.cancelRaf)
|
|
24
|
+
}
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
describe('EditorManager', () => {
|
|
@@ -48,4 +56,27 @@ describe('EditorManager', () => {
|
|
|
48
56
|
manager.dispose()
|
|
49
57
|
expect(() => manager.dispose()).not.toThrow()
|
|
50
58
|
})
|
|
59
|
+
|
|
60
|
+
it('unregister runs the disposable now and drops it from dispose', () => {
|
|
61
|
+
manager.startRaf()
|
|
62
|
+
manager.stopRaf()
|
|
63
|
+
expect(manager.cancelRaf).toHaveBeenCalledTimes(1)
|
|
64
|
+
manager.dispose()
|
|
65
|
+
expect(manager.cancelRaf).toHaveBeenCalledTimes(1)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('unregister ignores a disposable that already ran', () => {
|
|
69
|
+
manager.startRaf()
|
|
70
|
+
manager.dispose()
|
|
71
|
+
manager.stopRaf()
|
|
72
|
+
expect(manager.cancelRaf).toHaveBeenCalledTimes(1)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('addEditorEvent returns the registered unsubscribe so it can be detached early', () => {
|
|
76
|
+
manager.detachTestEvent()
|
|
77
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
78
|
+
expect(off).toHaveBeenCalledWith('frame', manager.onTestEvent)
|
|
79
|
+
manager.dispose()
|
|
80
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
81
|
+
})
|
|
51
82
|
})
|
|
@@ -16,6 +16,8 @@ import type { TLEventMap } from '../types/emit-types'
|
|
|
16
16
|
* - Reaction (`react(...)` from `@tldraw/state`): `this.register(react(...))`
|
|
17
17
|
* - DOM listener: `this.register(() => el.removeEventListener(...))`
|
|
18
18
|
* - Other resource (cache, index, child manager): `this.register(() => x.dispose())`
|
|
19
|
+
* - Resource held only part of the time (a listener attached while subscribers exist):
|
|
20
|
+
* `this.register(...)` on attach and `this.unregister(...)` on detach, see `PerformanceManager`
|
|
19
21
|
*
|
|
20
22
|
* For timeouts, intervals, and animation frames prefer `editor.timers` (context-grouped
|
|
21
23
|
* and auto-disposed) over raw `setTimeout`. For cleanup on the editor itself rather than a
|
|
@@ -40,16 +42,26 @@ export abstract class EditorManager {
|
|
|
40
42
|
return dispose
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Run a registered teardown now instead of at `dispose()`. A teardown that is no longer
|
|
47
|
+
* registered (already run by `dispose()`, or never registered) is skipped, so nothing is
|
|
48
|
+
* torn down twice.
|
|
49
|
+
*/
|
|
50
|
+
protected unregister(dispose: () => void): void {
|
|
51
|
+
if (this.disposables.delete(dispose)) dispose()
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
/**
|
|
44
55
|
* Subscribe to an editor bus event and register the matching unsubscribe, so the listener
|
|
45
|
-
* is removed automatically on `dispose()`.
|
|
56
|
+
* is removed automatically on `dispose()`. Returns that unsubscribe, for passing to
|
|
57
|
+
* {@link EditorManager.unregister} to detach it before `dispose()`.
|
|
46
58
|
*/
|
|
47
59
|
protected addEditorEvent<E extends keyof TLEventMap>(
|
|
48
60
|
event: E,
|
|
49
61
|
fn: (...args: TLEventMap[E]) => void
|
|
50
|
-
): void {
|
|
62
|
+
): () => void {
|
|
51
63
|
this.editor.on(event, fn as any)
|
|
52
|
-
this.register(() => this.editor.off(event, fn as any))
|
|
64
|
+
return this.register(() => this.editor.off(event, fn as any))
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
/** @internal */
|
|
@@ -38,6 +38,7 @@ function createMockEditor() {
|
|
|
38
38
|
getViewportScreenBounds: () => ({ w: 1920, h: 1080 }),
|
|
39
39
|
timers: {
|
|
40
40
|
setTimeout: (fn: () => void, ms: number) => setTimeout(fn, ms),
|
|
41
|
+
clearTimeout: (id: number) => clearTimeout(id),
|
|
41
42
|
},
|
|
42
43
|
_listeners: listeners,
|
|
43
44
|
} as any
|
|
@@ -94,6 +95,85 @@ describe('PerformanceManager', () => {
|
|
|
94
95
|
})
|
|
95
96
|
})
|
|
96
97
|
|
|
98
|
+
describe('dispose()', () => {
|
|
99
|
+
it('removes every lazily attached editor listener', () => {
|
|
100
|
+
const editor = createMockEditor()
|
|
101
|
+
const pm = new PerformanceManager(editor)
|
|
102
|
+
pm.on('interaction-end', vi.fn())
|
|
103
|
+
pm.on('shapes-created', vi.fn())
|
|
104
|
+
pm.on('shapes-updated', vi.fn())
|
|
105
|
+
pm.on('shapes-deleted', vi.fn())
|
|
106
|
+
expect(editor._listeners['frame']).toHaveLength(1)
|
|
107
|
+
expect(editor._listeners['created-shapes']).toHaveLength(1)
|
|
108
|
+
expect(editor._listeners['edited-shapes']).toHaveLength(1)
|
|
109
|
+
expect(editor._listeners['deleted-shapes']).toHaveLength(1)
|
|
110
|
+
|
|
111
|
+
pm.dispose()
|
|
112
|
+
|
|
113
|
+
expect(editor._listeners).toEqual({
|
|
114
|
+
frame: [],
|
|
115
|
+
'created-shapes': [],
|
|
116
|
+
'edited-shapes': [],
|
|
117
|
+
'deleted-shapes': [],
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('disconnects the LoAF observer', () => {
|
|
122
|
+
const origPO = globalThis.PerformanceObserver
|
|
123
|
+
const mockDisconnect = vi.fn()
|
|
124
|
+
globalThis.PerformanceObserver = class MockPO {
|
|
125
|
+
constructor(_cb: any) {}
|
|
126
|
+
observe() {}
|
|
127
|
+
disconnect = mockDisconnect
|
|
128
|
+
static supportedEntryTypes = ['long-animation-frame']
|
|
129
|
+
} as any
|
|
130
|
+
|
|
131
|
+
const editor = createMockEditor()
|
|
132
|
+
const pm = new PerformanceManager(editor)
|
|
133
|
+
pm.on('interaction-end', vi.fn())
|
|
134
|
+
expect(mockDisconnect).not.toHaveBeenCalled()
|
|
135
|
+
|
|
136
|
+
pm.dispose()
|
|
137
|
+
expect(mockDisconnect).toHaveBeenCalledTimes(1)
|
|
138
|
+
|
|
139
|
+
globalThis.PerformanceObserver = origPO
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('does not tear down a listener that was already detached', () => {
|
|
143
|
+
const editor = createMockEditor()
|
|
144
|
+
const off = vi.spyOn(editor, 'off')
|
|
145
|
+
const pm = new PerformanceManager(editor)
|
|
146
|
+
pm.on('shapes-created', vi.fn())()
|
|
147
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
148
|
+
|
|
149
|
+
pm.dispose()
|
|
150
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('makes a stale unsubscribe a no-op after dispose', () => {
|
|
154
|
+
const editor = createMockEditor()
|
|
155
|
+
const off = vi.spyOn(editor, 'off')
|
|
156
|
+
const pm = new PerformanceManager(editor)
|
|
157
|
+
const unsub = pm.on('interaction-end', vi.fn())
|
|
158
|
+
|
|
159
|
+
pm.dispose()
|
|
160
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
161
|
+
unsub()
|
|
162
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('attaches again for a subscriber added after dispose', () => {
|
|
166
|
+
const editor = createMockEditor()
|
|
167
|
+
const pm = new PerformanceManager(editor)
|
|
168
|
+
pm.on('shapes-created', vi.fn())
|
|
169
|
+
pm.dispose()
|
|
170
|
+
expect(editor._listeners['created-shapes']).toHaveLength(0)
|
|
171
|
+
|
|
172
|
+
pm.on('shapes-created', vi.fn())
|
|
173
|
+
expect(editor._listeners['created-shapes']).toHaveLength(1)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
97
177
|
describe('interaction tracking', () => {
|
|
98
178
|
it('emits interaction-start when notified', () => {
|
|
99
179
|
const editor = createMockEditor()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TLRecord, TLShapeId } from '@tldraw/tlschema'
|
|
2
2
|
import { bind } from '@tldraw/utils'
|
|
3
3
|
import EventEmitter from 'eventemitter3'
|
|
4
|
-
import
|
|
4
|
+
import { EditorManager } from '../EditorManager'
|
|
5
5
|
import type {
|
|
6
6
|
TLCameraEndPerfEvent,
|
|
7
7
|
TLCameraStartPerfEvent,
|
|
@@ -14,6 +14,9 @@ import type {
|
|
|
14
14
|
TLUndoRedoPerfEvent,
|
|
15
15
|
} from './perf-types'
|
|
16
16
|
|
|
17
|
+
// The editor listeners and observer that are attached only while a subscriber needs them
|
|
18
|
+
type LazyListener = 'frame' | 'loaf' | 'shapes-created' | 'shapes-updated' | 'shapes-deleted'
|
|
19
|
+
|
|
17
20
|
function percentile(sorted: number[], p: number): number {
|
|
18
21
|
const idx = Math.ceil(p * sorted.length) - 1
|
|
19
22
|
return sorted[Math.max(0, idx)]
|
|
@@ -71,12 +74,10 @@ function toLoafEntry(entry: PerformanceEntry): TLPerfLongAnimationFrame | null {
|
|
|
71
74
|
*
|
|
72
75
|
* @public
|
|
73
76
|
*/
|
|
74
|
-
export class PerformanceManager {
|
|
77
|
+
export class PerformanceManager extends EditorManager {
|
|
75
78
|
/** @internal */
|
|
76
79
|
readonly emitter = new EventEmitter<TLPerfEventMap>()
|
|
77
80
|
|
|
78
|
-
private editor: Editor
|
|
79
|
-
|
|
80
81
|
// Active interaction tracking
|
|
81
82
|
private activeInteraction: {
|
|
82
83
|
name: string
|
|
@@ -96,18 +97,8 @@ export class PerformanceManager {
|
|
|
96
97
|
loafEntries: TLPerfLongAnimationFrame[]
|
|
97
98
|
} | null = null
|
|
98
99
|
|
|
99
|
-
//
|
|
100
|
-
private
|
|
101
|
-
private shapeCreatedCleanup: (() => void) | null = null
|
|
102
|
-
private shapeEditedCleanup: (() => void) | null = null
|
|
103
|
-
private shapeDeletedCleanup: (() => void) | null = null
|
|
104
|
-
|
|
105
|
-
// LoAF observer
|
|
106
|
-
private loafObserver: PerformanceObserver | null = null
|
|
107
|
-
|
|
108
|
-
constructor(editor: Editor) {
|
|
109
|
-
this.editor = editor
|
|
110
|
-
}
|
|
100
|
+
// Cleanups for whatever is currently attached, keyed so a detach unregisters only its own
|
|
101
|
+
private readonly lazyCleanups = new Map<LazyListener, () => void>()
|
|
111
102
|
|
|
112
103
|
/**
|
|
113
104
|
* Subscribe to a performance event. Returns an unsubscribe function.
|
|
@@ -158,19 +149,12 @@ export class PerformanceManager {
|
|
|
158
149
|
|
|
159
150
|
/** @internal */
|
|
160
151
|
dispose() {
|
|
161
|
-
if (this.activeCamera?.timeout) clearTimeout(this.activeCamera.timeout)
|
|
152
|
+
if (this.activeCamera?.timeout) this.editor.timers.clearTimeout(this.activeCamera.timeout)
|
|
162
153
|
this.activeInteraction = null
|
|
163
154
|
this.activeCamera = null
|
|
164
|
-
this.
|
|
165
|
-
this.frameCleanup = null
|
|
166
|
-
this.shapeCreatedCleanup?.()
|
|
167
|
-
this.shapeCreatedCleanup = null
|
|
168
|
-
this.shapeEditedCleanup?.()
|
|
169
|
-
this.shapeEditedCleanup = null
|
|
170
|
-
this.shapeDeletedCleanup?.()
|
|
171
|
-
this.shapeDeletedCleanup = null
|
|
172
|
-
this._stopLoafObserver()
|
|
155
|
+
this.lazyCleanups.clear()
|
|
173
156
|
this.emitter.removeAllListeners()
|
|
157
|
+
super.dispose()
|
|
174
158
|
}
|
|
175
159
|
|
|
176
160
|
// --- Internal notification methods ---
|
|
@@ -259,7 +243,7 @@ export class PerformanceManager {
|
|
|
259
243
|
if (this.activeCamera) {
|
|
260
244
|
// Extend existing camera session
|
|
261
245
|
if (this.activeCamera.timeout) {
|
|
262
|
-
clearTimeout(this.activeCamera.timeout)
|
|
246
|
+
this.editor.timers.clearTimeout(this.activeCamera.timeout)
|
|
263
247
|
}
|
|
264
248
|
// If type changed, end old and start new
|
|
265
249
|
if (this.activeCamera.type !== type) {
|
|
@@ -313,7 +297,7 @@ export class PerformanceManager {
|
|
|
313
297
|
const camera = this.activeCamera
|
|
314
298
|
if (!camera) return
|
|
315
299
|
this.activeCamera = null
|
|
316
|
-
if (camera.timeout) clearTimeout(camera.timeout)
|
|
300
|
+
if (camera.timeout) this.editor.timers.clearTimeout(camera.timeout)
|
|
317
301
|
|
|
318
302
|
if (this.emitter.listenerCount('camera-end') === 0) return
|
|
319
303
|
|
|
@@ -444,7 +428,7 @@ export class PerformanceManager {
|
|
|
444
428
|
return
|
|
445
429
|
}
|
|
446
430
|
|
|
447
|
-
|
|
431
|
+
const observer = new PerformanceObserver((list) => {
|
|
448
432
|
const isInteractionActive = this.activeInteraction !== null
|
|
449
433
|
const isCameraActive = this.activeCamera !== null
|
|
450
434
|
|
|
@@ -463,14 +447,11 @@ export class PerformanceManager {
|
|
|
463
447
|
}
|
|
464
448
|
})
|
|
465
449
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
this.loafObserver.disconnect()
|
|
472
|
-
this.loafObserver = null
|
|
473
|
-
}
|
|
450
|
+
observer.observe({ type: 'long-animation-frame', buffered: false })
|
|
451
|
+
this.lazyCleanups.set(
|
|
452
|
+
'loaf',
|
|
453
|
+
this.register(() => observer.disconnect())
|
|
454
|
+
)
|
|
474
455
|
}
|
|
475
456
|
|
|
476
457
|
// --- Lazy listener management ---
|
|
@@ -495,7 +476,7 @@ export class PerformanceManager {
|
|
|
495
476
|
private _maybeAttachLazyListeners(event: keyof TLPerfEventMap) {
|
|
496
477
|
// Frame listener needed for frame event + interaction/camera frame time tracking
|
|
497
478
|
if (
|
|
498
|
-
!this.
|
|
479
|
+
!this.lazyCleanups.has('frame') &&
|
|
499
480
|
(event === 'frame' ||
|
|
500
481
|
event === 'interaction-start' ||
|
|
501
482
|
event === 'interaction-end' ||
|
|
@@ -503,81 +484,73 @@ export class PerformanceManager {
|
|
|
503
484
|
event === 'camera-end')
|
|
504
485
|
) {
|
|
505
486
|
if (this._needsFrameListener()) {
|
|
506
|
-
this.
|
|
507
|
-
this.frameCleanup = () => this.editor.off('frame', this._onFrame)
|
|
487
|
+
this.lazyCleanups.set('frame', this.addEditorEvent('frame', this._onFrame))
|
|
508
488
|
}
|
|
509
489
|
}
|
|
510
490
|
|
|
511
491
|
// LoAF observer needed when interaction-end or camera-end listeners exist
|
|
512
|
-
if (!this.
|
|
492
|
+
if (!this.lazyCleanups.has('loaf') && (event === 'interaction-end' || event === 'camera-end')) {
|
|
513
493
|
if (this._needsLoafObserver()) {
|
|
514
494
|
this._startLoafObserver()
|
|
515
495
|
}
|
|
516
496
|
}
|
|
517
497
|
|
|
518
|
-
if (!this.
|
|
519
|
-
this.
|
|
520
|
-
|
|
498
|
+
if (!this.lazyCleanups.has('shapes-created') && event === 'shapes-created') {
|
|
499
|
+
this.lazyCleanups.set(
|
|
500
|
+
'shapes-created',
|
|
501
|
+
this.addEditorEvent('created-shapes', this._onShapesCreated)
|
|
502
|
+
)
|
|
521
503
|
}
|
|
522
504
|
|
|
523
|
-
if (!this.
|
|
524
|
-
this.
|
|
525
|
-
|
|
505
|
+
if (!this.lazyCleanups.has('shapes-updated') && event === 'shapes-updated') {
|
|
506
|
+
this.lazyCleanups.set(
|
|
507
|
+
'shapes-updated',
|
|
508
|
+
this.addEditorEvent('edited-shapes', this._onShapesEdited)
|
|
509
|
+
)
|
|
526
510
|
}
|
|
527
511
|
|
|
528
|
-
if (!this.
|
|
529
|
-
this.
|
|
530
|
-
|
|
512
|
+
if (!this.lazyCleanups.has('shapes-deleted') && event === 'shapes-deleted') {
|
|
513
|
+
this.lazyCleanups.set(
|
|
514
|
+
'shapes-deleted',
|
|
515
|
+
this.addEditorEvent('deleted-shapes', this._onShapesDeleted)
|
|
516
|
+
)
|
|
531
517
|
}
|
|
532
518
|
}
|
|
533
519
|
|
|
534
520
|
private _maybeDetachLazyListeners(event: keyof TLPerfEventMap) {
|
|
535
521
|
if (
|
|
536
|
-
this.frameCleanup &&
|
|
537
522
|
(event === 'frame' ||
|
|
538
523
|
event === 'interaction-start' ||
|
|
539
524
|
event === 'interaction-end' ||
|
|
540
525
|
event === 'camera-start' ||
|
|
541
|
-
event === 'camera-end')
|
|
526
|
+
event === 'camera-end') &&
|
|
527
|
+
!this._needsFrameListener()
|
|
542
528
|
) {
|
|
543
|
-
|
|
544
|
-
this.frameCleanup()
|
|
545
|
-
this.frameCleanup = null
|
|
546
|
-
}
|
|
529
|
+
this._detachLazy('frame')
|
|
547
530
|
}
|
|
548
531
|
|
|
549
532
|
// Stop LoAF observer when no longer needed
|
|
550
|
-
if (
|
|
551
|
-
|
|
552
|
-
this._stopLoafObserver()
|
|
553
|
-
}
|
|
533
|
+
if ((event === 'interaction-end' || event === 'camera-end') && !this._needsLoafObserver()) {
|
|
534
|
+
this._detachLazy('loaf')
|
|
554
535
|
}
|
|
555
536
|
|
|
556
|
-
if (
|
|
557
|
-
this.
|
|
558
|
-
event === 'shapes-created' &&
|
|
559
|
-
this.emitter.listenerCount('shapes-created') === 0
|
|
560
|
-
) {
|
|
561
|
-
this.shapeCreatedCleanup()
|
|
562
|
-
this.shapeCreatedCleanup = null
|
|
537
|
+
if (event === 'shapes-created' && this.emitter.listenerCount('shapes-created') === 0) {
|
|
538
|
+
this._detachLazy('shapes-created')
|
|
563
539
|
}
|
|
564
540
|
|
|
565
|
-
if (
|
|
566
|
-
this.
|
|
567
|
-
event === 'shapes-updated' &&
|
|
568
|
-
this.emitter.listenerCount('shapes-updated') === 0
|
|
569
|
-
) {
|
|
570
|
-
this.shapeEditedCleanup()
|
|
571
|
-
this.shapeEditedCleanup = null
|
|
541
|
+
if (event === 'shapes-updated' && this.emitter.listenerCount('shapes-updated') === 0) {
|
|
542
|
+
this._detachLazy('shapes-updated')
|
|
572
543
|
}
|
|
573
544
|
|
|
574
|
-
if (
|
|
575
|
-
this.
|
|
576
|
-
event === 'shapes-deleted' &&
|
|
577
|
-
this.emitter.listenerCount('shapes-deleted') === 0
|
|
578
|
-
) {
|
|
579
|
-
this.shapeDeletedCleanup()
|
|
580
|
-
this.shapeDeletedCleanup = null
|
|
545
|
+
if (event === 'shapes-deleted' && this.emitter.listenerCount('shapes-deleted') === 0) {
|
|
546
|
+
this._detachLazy('shapes-deleted')
|
|
581
547
|
}
|
|
582
548
|
}
|
|
549
|
+
|
|
550
|
+
private _detachLazy(key: LazyListener) {
|
|
551
|
+
const cleanup = this.lazyCleanups.get(key)
|
|
552
|
+
if (!cleanup) return
|
|
553
|
+
this.unregister(cleanup)
|
|
554
|
+
this.lazyCleanups.delete(key)
|
|
555
|
+
}
|
|
583
556
|
}
|
|
@@ -409,7 +409,7 @@ export class ScribbleManager {
|
|
|
409
409
|
|
|
410
410
|
private clearIdleTimeout(session: Session): void {
|
|
411
411
|
if (session.idleTimeoutHandle !== undefined) {
|
|
412
|
-
clearTimeout(session.idleTimeoutHandle)
|
|
412
|
+
this.editor.timers.clearTimeout(session.idleTimeoutHandle)
|
|
413
413
|
session.idleTimeoutHandle = undefined
|
|
414
414
|
}
|
|
415
415
|
}
|
|
@@ -159,6 +159,22 @@ function findAdjacentGaps(
|
|
|
159
159
|
return matches
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Adjacent evenly spaced items produce many center snaps whose gaps cover the
|
|
163
|
+
// same breadth; only the smallest such gap needs showing, so find the one a new
|
|
164
|
+
// gap would be redundant with. Touching breadths don't count as overlapping.
|
|
165
|
+
function findOverlappingCenterSnap(snaps: NearestSnap[], gap: Gap) {
|
|
166
|
+
return snaps.find(
|
|
167
|
+
(snap): snap is Extract<NearestSnap, { type: 'gap_center' }> =>
|
|
168
|
+
snap.type === 'gap_center' &&
|
|
169
|
+
rangesOverlap(
|
|
170
|
+
gap.breadthIntersection[0],
|
|
171
|
+
gap.breadthIntersection[1],
|
|
172
|
+
snap.gap.breadthIntersection[0],
|
|
173
|
+
snap.gap.breadthIntersection[1]
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
162
178
|
function dedupeGapSnaps(snaps: Array<Extract<SnapIndicator, { type: 'gaps' }>>) {
|
|
163
179
|
// sort by descending order of number of gaps
|
|
164
180
|
snaps.sort((a, b) => b.gaps.length - a.gaps.length)
|
|
@@ -597,9 +613,8 @@ export class BoundsSnaps {
|
|
|
597
613
|
// which are closest to it in each axis
|
|
598
614
|
for (const thisSnapPoint of selectionSnapPoints) {
|
|
599
615
|
for (const otherSnapPoint of otherNodeSnapPoints) {
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
const offsetY = Math.abs(offset.y)
|
|
616
|
+
const offsetX = Math.abs(thisSnapPoint.x - otherSnapPoint.x)
|
|
617
|
+
const offsetY = Math.abs(thisSnapPoint.y - otherSnapPoint.y)
|
|
603
618
|
|
|
604
619
|
if (round(offsetX) <= round(minOffset.x)) {
|
|
605
620
|
if (round(offsetX) < round(minOffset.x)) {
|
|
@@ -718,23 +733,14 @@ export class BoundsSnaps {
|
|
|
718
733
|
// │ │
|
|
719
734
|
// └─────────┘
|
|
720
735
|
|
|
721
|
-
const
|
|
722
|
-
| Extract<NearestSnap, { type: 'gap_center' }>
|
|
723
|
-
| undefined
|
|
724
|
-
|
|
725
|
-
const gapBreadthsOverlap =
|
|
726
|
-
otherCenterSnap &&
|
|
727
|
-
rangeIntersection(
|
|
728
|
-
gap.breadthIntersection[0],
|
|
729
|
-
gap.breadthIntersection[1],
|
|
730
|
-
otherCenterSnap.gap.breadthIntersection[0],
|
|
731
|
-
otherCenterSnap.gap.breadthIntersection[1]
|
|
732
|
-
)
|
|
736
|
+
const overlappingCenterSnap = findOverlappingCenterSnap(nearestSnapsX, gap)
|
|
733
737
|
|
|
734
|
-
// if there is another center snap
|
|
735
|
-
if (
|
|
736
|
-
|
|
737
|
-
|
|
738
|
+
// if there is another center snap that overlaps with this one, keep only the smaller gap
|
|
739
|
+
if (overlappingCenterSnap) {
|
|
740
|
+
if (overlappingCenterSnap.gap.length > gap.length) {
|
|
741
|
+
nearestSnapsX[nearestSnapsX.indexOf(overlappingCenterSnap)] = snap
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
738
744
|
nearestSnapsX.push(snap)
|
|
739
745
|
}
|
|
740
746
|
}
|
|
@@ -853,26 +859,16 @@ export class BoundsSnaps {
|
|
|
853
859
|
// │ │
|
|
854
860
|
// └─────────┘
|
|
855
861
|
|
|
856
|
-
const
|
|
857
|
-
| Extract<NearestSnap, { type: 'gap_center' }>
|
|
858
|
-
| undefined
|
|
859
|
-
|
|
860
|
-
const gapBreadthsOverlap =
|
|
861
|
-
otherCenterSnap &&
|
|
862
|
-
rangesOverlap(
|
|
863
|
-
otherCenterSnap.gap.breadthIntersection[0],
|
|
864
|
-
otherCenterSnap.gap.breadthIntersection[1],
|
|
865
|
-
gap.breadthIntersection[0],
|
|
866
|
-
gap.breadthIntersection[1]
|
|
867
|
-
)
|
|
862
|
+
const overlappingCenterSnap = findOverlappingCenterSnap(nearestSnapsY, gap)
|
|
868
863
|
|
|
869
|
-
// if there is another center snap
|
|
870
|
-
if (
|
|
871
|
-
|
|
872
|
-
|
|
864
|
+
// if there is another center snap that overlaps with this one, keep only the smaller gap
|
|
865
|
+
if (overlappingCenterSnap) {
|
|
866
|
+
if (overlappingCenterSnap.gap.length > gap.length) {
|
|
867
|
+
nearestSnapsY[nearestSnapsY.indexOf(overlappingCenterSnap)] = snap
|
|
868
|
+
}
|
|
869
|
+
} else {
|
|
873
870
|
nearestSnapsY.push(snap)
|
|
874
871
|
}
|
|
875
|
-
continue
|
|
876
872
|
}
|
|
877
873
|
|
|
878
874
|
// check for duplication top match
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { computed } from '@tldraw/state'
|
|
2
2
|
import { TLHandle, TLShape, TLShapeId, VecModel } from '@tldraw/tlschema'
|
|
3
3
|
import { assertExists, uniqueId } from '@tldraw/utils'
|
|
4
|
-
import { Geometry2d } from '../../../primitives/geometry/Geometry2d'
|
|
4
|
+
import { Geometry2d, Geometry2dFilters } from '../../../primitives/geometry/Geometry2d'
|
|
5
|
+
import { Group2d } from '../../../primitives/geometry/Group2d'
|
|
5
6
|
import { Vec } from '../../../primitives/Vec'
|
|
6
7
|
import type { Editor } from '../../Editor'
|
|
7
8
|
import type { PointsSnapIndicator, SnapData, SnapManager } from './SnapManager'
|
|
@@ -156,11 +157,21 @@ export class HandleSnaps {
|
|
|
156
157
|
let minDistanceForOutline = snapThreshold
|
|
157
158
|
let nearestPointOnOutline: Vec | null = null
|
|
158
159
|
|
|
160
|
+
// Labels and internal detail (a note's label, a geo check mark) are not outlines
|
|
161
|
+
// to snap to. Group2d.nearestPoint throws when the filter leaves no child, so
|
|
162
|
+
// skip those groups.
|
|
163
|
+
const filters = Geometry2dFilters.EXCLUDE_NON_STANDARD
|
|
159
164
|
for (const { shapeId, outline } of this.iterateSnapOutlines(currentShapeId, handle)) {
|
|
165
|
+
if (
|
|
166
|
+
outline instanceof Group2d &&
|
|
167
|
+
outline.children.every((child) => child.isExcludedByFilter(filters))
|
|
168
|
+
) {
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
160
171
|
const shapePageTransform = assertExists(this.editor.getShapePageTransform(shapeId))
|
|
161
172
|
const pointInShapeSpace = this.editor.getPointInShapeSpace(shapeId, handleInPageSpace)
|
|
162
173
|
|
|
163
|
-
const nearestShapePointInShapeSpace = outline.nearestPoint(pointInShapeSpace)
|
|
174
|
+
const nearestShapePointInShapeSpace = outline.nearestPoint(pointInShapeSpace, filters)
|
|
164
175
|
const nearestInPageSpace = shapePageTransform.applyToPoint(nearestShapePointInShapeSpace)
|
|
165
176
|
|
|
166
177
|
if (Vec.DistMin(handleInPageSpace, nearestInPageSpace, minDistanceForOutline)) {
|
|
@@ -78,6 +78,7 @@ describe('SnapManager', () => {
|
|
|
78
78
|
getShapePageBounds: vi.fn(),
|
|
79
79
|
isShapeOfType: vi.fn(),
|
|
80
80
|
isShapeFrameLike: vi.fn(() => false),
|
|
81
|
+
isShapeHidden: vi.fn(() => false),
|
|
81
82
|
} as any
|
|
82
83
|
|
|
83
84
|
editor.getShapeUtil.mockReturnValue({
|
|
@@ -337,6 +338,18 @@ describe('SnapManager', () => {
|
|
|
337
338
|
expect(result.has(shapeId)).toBe(false)
|
|
338
339
|
})
|
|
339
340
|
|
|
341
|
+
it('should exclude hidden shapes', () => {
|
|
342
|
+
const shapeId = createShapeId('shape1')
|
|
343
|
+
|
|
344
|
+
editor.getSortedChildIdsForParent.mockReturnValue([shapeId])
|
|
345
|
+
editor.getShape.mockReturnValue(createMockShape(shapeId) as any)
|
|
346
|
+
editor.getShapePageBounds.mockReturnValue(new Box(10, 10, 50, 50))
|
|
347
|
+
editor.isShapeHidden.mockReturnValue(true)
|
|
348
|
+
|
|
349
|
+
const result = snapManager.getSnappableShapes()
|
|
350
|
+
expect(result.has(shapeId)).toBe(false)
|
|
351
|
+
})
|
|
352
|
+
|
|
340
353
|
it('should exclude shapes outside viewport bounds', () => {
|
|
341
354
|
const shapeId = createShapeId('shape1')
|
|
342
355
|
const shape = createMockShape(shapeId)
|