@tldraw/editor 5.4.1 → 5.5.0-canary.003bf5951274
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 +37 -14
- 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/constants.js +2 -0
- package/dist-cjs/lib/constants.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +147 -157
- 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/EdgeScrollManager/EdgeScrollManager.js +10 -6
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.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/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/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/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/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 +22 -24
- 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 +37 -14
- 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/constants.mjs +2 -0
- package/dist-esm/lib/constants.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +148 -157
- 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/EdgeScrollManager/EdgeScrollManager.mjs +10 -6
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.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/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/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/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/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 +22 -24
- 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/constants.ts +8 -0
- package/src/lib/editor/Editor.test.ts +20 -0
- package/src/lib/editor/Editor.ts +228 -262
- package/src/lib/editor/derivations/bindingsIndex.ts +3 -0
- 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/TextManager/TextManager.test.ts +74 -0
- package/src/lib/editor/managers/TextManager/TextManager.ts +6 -5
- 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/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/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/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 +95 -0
- package/src/lib/utils/sync/LocalIndexedDb.ts +31 -27
- 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
|
@@ -145,7 +145,13 @@ function measureContentBounds(
|
|
|
145
145
|
// (extraPx * 2 >= w means declaredRight <= declaredLeft, producing zero/negative crop)
|
|
146
146
|
if (extraPx <= 0 || extraPx * 2 >= w || extraPx * 2 >= h) return null
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
let imageData: ImageData
|
|
149
|
+
try {
|
|
150
|
+
imageData = ctx.getImageData(0, 0, w, h)
|
|
151
|
+
} catch {
|
|
152
|
+
// a tainted or unallocatable canvas can't be measured; export untrimmed rather than fail
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
149
155
|
const data = imageData.data
|
|
150
156
|
|
|
151
157
|
// Determine how to detect "empty" pixels.
|
|
@@ -302,9 +308,11 @@ export async function trimSvgToContent(
|
|
|
302
308
|
|
|
303
309
|
if (trimPadding <= 0) return null
|
|
304
310
|
|
|
305
|
-
// Render SVG to a temporary canvas at 1:1 pixel ratio
|
|
306
|
-
|
|
307
|
-
|
|
311
|
+
// Render SVG to a temporary canvas at 1:1 pixel ratio, clamped like the raster path so a
|
|
312
|
+
// large export doesn't exceed the browser's canvas limits
|
|
313
|
+
let [canvasWidth, canvasHeight] = clampToBrowserMaxCanvasSize(width, height)
|
|
314
|
+
canvasWidth = Math.floor(canvasWidth)
|
|
315
|
+
canvasHeight = Math.floor(canvasHeight)
|
|
308
316
|
|
|
309
317
|
if (canvasWidth <= 0 || canvasHeight <= 0) return null
|
|
310
318
|
|
|
@@ -313,7 +321,7 @@ export async function trimSvgToContent(
|
|
|
313
321
|
if (!canvas) return null
|
|
314
322
|
|
|
315
323
|
// Measure content bounds on the canvas
|
|
316
|
-
const trimPaddingPx = trimPadding * scale
|
|
324
|
+
const trimPaddingPx = trimPadding * scale * (canvasWidth / width)
|
|
317
325
|
const bounds = measureContentBounds(canvas, trimPaddingPx)
|
|
318
326
|
if (!bounds) return null
|
|
319
327
|
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BaseFrameLikeShapeUtil,
|
|
3
|
+
Geometry2d,
|
|
4
|
+
RecordProps,
|
|
5
|
+
Rectangle2d,
|
|
6
|
+
ShapeUtil,
|
|
7
|
+
T,
|
|
8
|
+
TLShape,
|
|
9
|
+
createShapeId,
|
|
10
|
+
} from '../..'
|
|
2
11
|
import { createTLStore } from '../config/createTLStore'
|
|
3
12
|
import { Editor } from '../editor/Editor'
|
|
4
13
|
import { Box } from '../primitives/Box'
|
|
@@ -861,3 +870,36 @@ describe('getExportDefaultBounds', () => {
|
|
|
861
870
|
})
|
|
862
871
|
})
|
|
863
872
|
})
|
|
873
|
+
|
|
874
|
+
// Deliberately not registered in TLGlobalShapePropsMap (test-only types leak into the api report)
|
|
875
|
+
const FRAME_LIKE_TYPE = 'test-frame-like'
|
|
876
|
+
|
|
877
|
+
class FrameLikeShape extends BaseFrameLikeShapeUtil<any> {
|
|
878
|
+
static override type = FRAME_LIKE_TYPE
|
|
879
|
+
static override props = { w: T.number, h: T.number }
|
|
880
|
+
getDefaultProps() {
|
|
881
|
+
return { w: 100, h: 100 }
|
|
882
|
+
}
|
|
883
|
+
getIndicatorPath() {
|
|
884
|
+
return undefined
|
|
885
|
+
}
|
|
886
|
+
component() {}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
describe('exporting a single frame-like shape', () => {
|
|
890
|
+
it('does not need the default frame shape util to be registered', async () => {
|
|
891
|
+
const editor = new Editor({
|
|
892
|
+
shapeUtils: [FrameLikeShape],
|
|
893
|
+
bindingUtils: [],
|
|
894
|
+
tools: [],
|
|
895
|
+
store: createTLStore({ shapeUtils: [FrameLikeShape], bindingUtils: [] }),
|
|
896
|
+
getContainer: () => document.body,
|
|
897
|
+
})
|
|
898
|
+
const id = createShapeId('frame-like')
|
|
899
|
+
editor.createShape({ id, type: FRAME_LIKE_TYPE, x: 0, y: 0, props: { w: 100, h: 100 } } as any)
|
|
900
|
+
|
|
901
|
+
const result = await editor.getSvgElement([id], { background: true })
|
|
902
|
+
expect(result?.svg.style.backgroundColor).not.toBe('')
|
|
903
|
+
expect(result?.svg.style.backgroundColor).not.toBe('transparent')
|
|
904
|
+
})
|
|
905
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useAtom, useValue } from '@tldraw/state-react'
|
|
2
|
-
import {
|
|
2
|
+
import { DefaultColorStyle, TLShape, TLShapeId } from '@tldraw/tlschema'
|
|
3
3
|
import { TLFontFace } from '@tldraw/tlschema'
|
|
4
4
|
import { hasOwnProperty, promiseWithResolve, uniqueId } from '@tldraw/utils'
|
|
5
5
|
import {
|
|
@@ -117,6 +117,7 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
|
|
|
117
117
|
colorMode={colorMode}
|
|
118
118
|
renderingShapes={renderingShapes}
|
|
119
119
|
onMount={initialEffectPromise.resolve}
|
|
120
|
+
onError={exportDelay.fail}
|
|
120
121
|
waitUntil={exportDelay.waitUntil}
|
|
121
122
|
>
|
|
122
123
|
{}
|
|
@@ -208,6 +209,7 @@ function SvgExport({
|
|
|
208
209
|
colorMode,
|
|
209
210
|
renderingShapes,
|
|
210
211
|
onMount,
|
|
212
|
+
onError,
|
|
211
213
|
waitUntil,
|
|
212
214
|
}: {
|
|
213
215
|
editor: Editor
|
|
@@ -221,6 +223,7 @@ function SvgExport({
|
|
|
221
223
|
colorMode: 'light' | 'dark'
|
|
222
224
|
renderingShapes: TLRenderingShape[]
|
|
223
225
|
onMount(): void
|
|
226
|
+
onError(error: unknown): void
|
|
224
227
|
waitUntil(promise: Promise<void>): void
|
|
225
228
|
}) {
|
|
226
229
|
const masksId = useUniqueSafeId()
|
|
@@ -283,6 +286,8 @@ function SvgExport({
|
|
|
283
286
|
throw new Error('SvgExport should only render once - do not use with react strict mode')
|
|
284
287
|
}
|
|
285
288
|
didRenderRef.current = true
|
|
289
|
+
// the promise is caught at the end: a throwing `toSvg` would otherwise leave `onMount`
|
|
290
|
+
// pending forever, so the export would wait out its max delay and snapshot an empty svg
|
|
286
291
|
;(async () => {
|
|
287
292
|
const shapeDefs: Record<string, { pending: false; element: ReactElement }> = {}
|
|
288
293
|
|
|
@@ -418,8 +423,17 @@ function SvgExport({
|
|
|
418
423
|
defsById: { ...state.defsById, ...shapeDefs },
|
|
419
424
|
}))
|
|
420
425
|
})
|
|
421
|
-
})()
|
|
422
|
-
}, [
|
|
426
|
+
})().catch(onError)
|
|
427
|
+
}, [
|
|
428
|
+
bbox,
|
|
429
|
+
editor,
|
|
430
|
+
exportContext,
|
|
431
|
+
masksId,
|
|
432
|
+
onError,
|
|
433
|
+
renderingShapes,
|
|
434
|
+
singleFrameShapeId,
|
|
435
|
+
stateAtom,
|
|
436
|
+
])
|
|
423
437
|
|
|
424
438
|
useEffect(() => {
|
|
425
439
|
const fontsInUse = new Set<TLFontFace>()
|
|
@@ -449,19 +463,17 @@ function SvgExport({
|
|
|
449
463
|
let backgroundColor = background ? colors.background : 'transparent'
|
|
450
464
|
|
|
451
465
|
if (singleFrameShapeId && background) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
backgroundColor = colors.solid
|
|
464
|
-
}
|
|
466
|
+
// the frame-like shape's own util decides this - it may not be the default 'frame' util
|
|
467
|
+
const shape = editor.getShape(singleFrameShapeId)!
|
|
468
|
+
const { showColors } = editor.getShapeUtil(shape).options as { showColors?: boolean }
|
|
469
|
+
// the color style may live under any prop key, so look it up through the style map and
|
|
470
|
+
// fall back to a literal `color` prop for frame-like shapes that don't use the style
|
|
471
|
+
const color = showColors
|
|
472
|
+
? (editor.getShapeStyleIfExists(shape, DefaultColorStyle) ??
|
|
473
|
+
('color' in shape.props ? shape.props.color : null))
|
|
474
|
+
: null
|
|
475
|
+
backgroundColor =
|
|
476
|
+
typeof color === 'string' ? getColorValue(colors, color, 'frameFill') : colors.solid
|
|
465
477
|
}
|
|
466
478
|
|
|
467
479
|
return (
|
|
@@ -2,7 +2,7 @@ import { safeParseUrl } from '@tldraw/utils'
|
|
|
2
2
|
|
|
3
3
|
export interface ParsedFontFace {
|
|
4
4
|
fontFace: string
|
|
5
|
-
urls: { original: string; resolved: string | null
|
|
5
|
+
urls: { original: string; resolved: string | null }[]
|
|
6
6
|
fontFamilies: Set<string>
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -18,7 +18,7 @@ export interface ParsedFontFace {
|
|
|
18
18
|
// Parse @import declarations:
|
|
19
19
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/@import#formal_syntax
|
|
20
20
|
const importsRegex =
|
|
21
|
-
/@import\s+(?:"([^"]+)"|'([^']+)'|url\s*\(\s*(?:"([^"]+)"|'([^']+)'|([^'")]+))\s*\))([^;]
|
|
21
|
+
/@import\s+(?:"([^"]+)"|'([^']+)'|url\s*\(\s*(?:"([^"]+)"|'([^']+)'|([^'")]+))\s*\))([^;]*);/gi
|
|
22
22
|
|
|
23
23
|
// Locate @font-face declarations:
|
|
24
24
|
const fontFaceRegex = /@font-face\s*{([^}]+)}/gi
|
|
@@ -5,6 +5,7 @@ import type { TLErrorFallbackComponent } from '../components/default-components/
|
|
|
5
5
|
import type { TLGridProps } from '../components/default-components/DefaultGrid'
|
|
6
6
|
import type { TLShapeErrorFallbackComponent } from '../components/default-components/DefaultShapeErrorFallback'
|
|
7
7
|
import type { TLShapeWrapperProps } from '../components/default-components/DefaultShapeWrapper'
|
|
8
|
+
import type { TLSpinnerProps } from '../components/default-components/DefaultSpinner'
|
|
8
9
|
|
|
9
10
|
/** @public */
|
|
10
11
|
export interface TLEditorComponents {
|
|
@@ -16,7 +17,7 @@ export interface TLEditorComponents {
|
|
|
16
17
|
LoadingScreen?: ComponentType | null
|
|
17
18
|
OnTheCanvas?: ComponentType | null
|
|
18
19
|
ShapeWrapper?: ComponentType<TLShapeWrapperProps & RefAttributes<HTMLDivElement>> | null
|
|
19
|
-
Spinner?: ComponentType<
|
|
20
|
+
Spinner?: ComponentType<TLSpinnerProps> | null
|
|
20
21
|
SvgDefs?: ComponentType | null
|
|
21
22
|
|
|
22
23
|
// These will always have defaults
|
|
@@ -19,6 +19,9 @@ export function useCanvasEvents() {
|
|
|
19
19
|
const events = useMemo(
|
|
20
20
|
function canvasEvents() {
|
|
21
21
|
let isSecondaryClickPointerDown = false
|
|
22
|
+
// The button each accepted press started with, by pointer id. pointercancel
|
|
23
|
+
// reports button -1, so the synthetic pointer_up needs the original.
|
|
24
|
+
const pointerDownButtons = new Map<number, number>()
|
|
22
25
|
|
|
23
26
|
function onPointerDown(e: React.PointerEvent) {
|
|
24
27
|
if (editor.wasEventAlreadyHandled(e)) return
|
|
@@ -45,6 +48,15 @@ export function useCanvasEvents() {
|
|
|
45
48
|
|
|
46
49
|
setPointerCapture(e.currentTarget, e)
|
|
47
50
|
|
|
51
|
+
// Only remember presses the editor will act on. A rejected palm in pen mode
|
|
52
|
+
// must not overwrite the button of the pen press it lands on top of, or the
|
|
53
|
+
// pen's pointercancel would release the wrong button and skip the
|
|
54
|
+
// stylus-eraser tool restore.
|
|
55
|
+
const isPenMode = editor.getInstanceState().isPenMode
|
|
56
|
+
if (!editor.inputs.getIsPinching() && !(isPenMode && e.pointerType !== 'pen')) {
|
|
57
|
+
pointerDownButtons.set(e.pointerId, button)
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
editor.dispatch({
|
|
49
61
|
type: 'pointer',
|
|
50
62
|
target: 'canvas',
|
|
@@ -56,6 +68,7 @@ export function useCanvasEvents() {
|
|
|
56
68
|
|
|
57
69
|
function onPointerUp(e: React.PointerEvent) {
|
|
58
70
|
if (editor.wasEventAlreadyHandled(e)) return
|
|
71
|
+
pointerDownButtons.delete(e.pointerId)
|
|
59
72
|
const button = isSecondaryClickPointerDown ? 2 : getPointerEventButton(e)
|
|
60
73
|
if (button !== 0 && button !== 1 && button !== 2 && button !== 5) return
|
|
61
74
|
|
|
@@ -91,6 +104,34 @@ export function useCanvasEvents() {
|
|
|
91
104
|
isSecondaryClickPointerDown = false
|
|
92
105
|
}
|
|
93
106
|
|
|
107
|
+
function onPointerCancel(e: React.PointerEvent) {
|
|
108
|
+
if (editor.wasEventAlreadyHandled(e)) return
|
|
109
|
+
const pointerDownButton = pointerDownButtons.get(e.pointerId)
|
|
110
|
+
pointerDownButtons.delete(e.pointerId)
|
|
111
|
+
// In pen mode a cancelled touch is usually a rejected palm, and the pen's own
|
|
112
|
+
// interaction must survive it.
|
|
113
|
+
if (editor.getInstanceState().isPenMode && e.pointerType !== 'pen') return
|
|
114
|
+
|
|
115
|
+
releasePointerCapture(e.currentTarget, e)
|
|
116
|
+
|
|
117
|
+
// Nothing to end if the press itself was filtered out (pen mode, unsupported
|
|
118
|
+
// button), or if this pointer never started an accepted press.
|
|
119
|
+
if (!editor.inputs.getIsPointing() || pointerDownButton === undefined) return
|
|
120
|
+
|
|
121
|
+
// The browser has taken the pointer (edge swipe, incoming call) and will never send
|
|
122
|
+
// pointerup, so end the gesture here or the tool stays stuck in its pointing or
|
|
123
|
+
// dragging state and the next touch continues it.
|
|
124
|
+
editor.interrupt()
|
|
125
|
+
editor.dispatch({
|
|
126
|
+
type: 'pointer',
|
|
127
|
+
target: 'canvas',
|
|
128
|
+
name: 'pointer_up',
|
|
129
|
+
...getPointerInfo(editor, e),
|
|
130
|
+
button: pointerDownButton,
|
|
131
|
+
})
|
|
132
|
+
isSecondaryClickPointerDown = false
|
|
133
|
+
}
|
|
134
|
+
|
|
94
135
|
function onPointerEnter(e: React.PointerEvent) {
|
|
95
136
|
if (editor.wasEventAlreadyHandled(e)) return
|
|
96
137
|
if (editor.getInstanceState().isPenMode && e.pointerType !== 'pen') return
|
|
@@ -201,6 +242,7 @@ export function useCanvasEvents() {
|
|
|
201
242
|
return {
|
|
202
243
|
onPointerDown,
|
|
203
244
|
onPointerUp,
|
|
245
|
+
onPointerCancel,
|
|
204
246
|
onPointerEnter,
|
|
205
247
|
onPointerLeave,
|
|
206
248
|
onDragOver,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { areArraysShallowEqual, areObjectsShallowEqual } from '@tldraw/utils'
|
|
1
|
+
import { areArraysShallowEqual, areObjectsShallowEqual, isEqual } from '@tldraw/utils'
|
|
2
2
|
import { useRef } from 'react'
|
|
3
3
|
|
|
4
4
|
function useIdentity<T>(value: T, isEqual: (a: T, b: T) => boolean): T {
|
|
@@ -49,3 +49,14 @@ const areNullableObjectsShallowEqual = (
|
|
|
49
49
|
export function useShallowObjectIdentity<T extends object | null | undefined>(obj: T): T {
|
|
50
50
|
return useIdentity(obj, areNullableObjectsShallowEqual)
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* For small option objects that may nest (e.g. camera constraints), where a shallow comparison
|
|
55
|
+
* would still see a fresh identity on every render. Functions and class instances inside still
|
|
56
|
+
* compare by reference, so an inline callback or a fresh `Extension.configure()` counts as a change.
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
export function useDeepObjectIdentity<T extends object | null | undefined>(obj: T): T {
|
|
61
|
+
return useIdentity(obj, isEqual)
|
|
62
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { atom, transact } from '@tldraw/state'
|
|
2
|
+
import { noop } from '@tldraw/utils'
|
|
2
3
|
import { publishDates, version } from '../../version'
|
|
3
4
|
import { getDefaultCdnBaseUrl } from '../utils/assets'
|
|
4
5
|
import { importPublicKey, str2ab } from '../utils/licensing'
|
|
@@ -127,6 +128,7 @@ export class LicenseManager {
|
|
|
127
128
|
...NO_FEATURES,
|
|
128
129
|
})
|
|
129
130
|
public verbose = true
|
|
131
|
+
private isDisposed = false
|
|
130
132
|
|
|
131
133
|
constructor(licenseKey: string | undefined, testPublicKey?: string) {
|
|
132
134
|
this.isTest = process.env.NODE_ENV === 'test'
|
|
@@ -144,6 +146,9 @@ export class LicenseManager {
|
|
|
144
146
|
|
|
145
147
|
this.getLicenseFromKey(licenseKey)
|
|
146
148
|
.then((result) => {
|
|
149
|
+
// replaced by a manager for a newer key: this one's messages, tracking ping and state
|
|
150
|
+
// would describe a key the app no longer uses
|
|
151
|
+
if (this.isDisposed) return
|
|
147
152
|
const licenseState = getLicenseState(
|
|
148
153
|
result,
|
|
149
154
|
(messages: string[]) => this.outputMessages(messages),
|
|
@@ -160,11 +165,17 @@ export class LicenseManager {
|
|
|
160
165
|
})
|
|
161
166
|
})
|
|
162
167
|
.catch((error) => {
|
|
168
|
+
if (this.isDisposed) return
|
|
163
169
|
console.error('License validation failed:', error)
|
|
164
170
|
this.state.set('unlicensed')
|
|
165
171
|
})
|
|
166
172
|
}
|
|
167
173
|
|
|
174
|
+
/** Ignore the pending validation: nothing is logged, tracked or set once the manager is replaced. */
|
|
175
|
+
dispose() {
|
|
176
|
+
this.isDisposed = true
|
|
177
|
+
}
|
|
178
|
+
|
|
168
179
|
/**
|
|
169
180
|
* Returns whether a given licensable feature is enabled. Reactive: reading this inside a signal
|
|
170
181
|
* recomputes when license validation resolves.
|
|
@@ -247,8 +258,9 @@ export class LicenseManager {
|
|
|
247
258
|
url.searchParams.set('environment', process.env.NODE_ENV)
|
|
248
259
|
}
|
|
249
260
|
|
|
261
|
+
// best-effort: a blocked or offline request must not surface as an unhandled rejection
|
|
250
262
|
// eslint-disable-next-line no-restricted-globals
|
|
251
|
-
fetch(url.toString())
|
|
263
|
+
fetch(url.toString()).catch(noop)
|
|
252
264
|
}
|
|
253
265
|
|
|
254
266
|
private async extractLicenseKey(licenseKey: string): Promise<LicenseInfo> {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { act, render } from '@testing-library/react'
|
|
2
|
+
import { vi } from 'vitest'
|
|
3
|
+
import { LicenseFromKeyResult, LicenseManager } from './LicenseManager'
|
|
4
|
+
import { LICENSE_TIMEOUT, LicenseProvider, useLicenseContext } from './LicenseProvider'
|
|
5
|
+
|
|
6
|
+
const NO_KEY: LicenseFromKeyResult = { isLicenseParseable: false, reason: 'no-key-provided' }
|
|
7
|
+
|
|
8
|
+
describe('LicenseProvider', () => {
|
|
9
|
+
const managers: LicenseManager[] = []
|
|
10
|
+
function Probe() {
|
|
11
|
+
managers.push(useLicenseContext())
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
const renderWithKey = (licenseKey: string | undefined) => (
|
|
15
|
+
<LicenseProvider licenseKey={licenseKey}>
|
|
16
|
+
<Probe />
|
|
17
|
+
</LicenseProvider>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
managers.length = 0
|
|
22
|
+
})
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks()
|
|
25
|
+
vi.useRealTimers()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('creates a new license manager when the license key changes', async () => {
|
|
29
|
+
const rendered = await act(async () => render(renderWithKey(undefined)))
|
|
30
|
+
const initial = managers.at(-1)!
|
|
31
|
+
|
|
32
|
+
await act(async () => rendered.rerender(renderWithKey('not-a-real-key')))
|
|
33
|
+
const changed = managers.at(-1)!
|
|
34
|
+
expect(changed).not.toBe(initial)
|
|
35
|
+
|
|
36
|
+
// same key again: the manager is stable
|
|
37
|
+
await act(async () => rendered.rerender(renderWithKey('not-a-real-key')))
|
|
38
|
+
expect(managers.at(-1)).toBe(changed)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('shows the editor again when a new key arrives after the gate closed', async () => {
|
|
42
|
+
// validation never settles so the test drives the state by hand
|
|
43
|
+
vi.spyOn(LicenseManager.prototype, 'getLicenseFromKey').mockReturnValue(new Promise(() => {}))
|
|
44
|
+
vi.useFakeTimers()
|
|
45
|
+
|
|
46
|
+
const rendered = await act(async () => render(renderWithKey('expired-key')))
|
|
47
|
+
const expired = managers.at(-1)!
|
|
48
|
+
act(() => expired.state.set('expired'))
|
|
49
|
+
act(() => vi.advanceTimersByTime(LICENSE_TIMEOUT))
|
|
50
|
+
expect(rendered.queryByTestId('tl-license-expired')).not.toBeNull()
|
|
51
|
+
|
|
52
|
+
await act(async () => rendered.rerender(renderWithKey('fresh-key')))
|
|
53
|
+
expect(rendered.queryByTestId('tl-license-expired')).toBeNull()
|
|
54
|
+
expect(managers.at(-1)).not.toBe(expired)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('ignores the validation result of a manager replaced by a new key', async () => {
|
|
58
|
+
const resolvers: ((result: LicenseFromKeyResult) => void)[] = []
|
|
59
|
+
vi.spyOn(LicenseManager.prototype, 'getLicenseFromKey').mockImplementation(
|
|
60
|
+
() => new Promise((resolve) => resolvers.push(resolve))
|
|
61
|
+
)
|
|
62
|
+
const track = vi.spyOn(LicenseManager.prototype as any, 'maybeTrack')
|
|
63
|
+
|
|
64
|
+
const rendered = await act(async () => render(renderWithKey(undefined)))
|
|
65
|
+
const orphan = managers.at(-1)!
|
|
66
|
+
await act(async () => rendered.rerender(renderWithKey('real-key')))
|
|
67
|
+
expect(managers.at(-1)).not.toBe(orphan)
|
|
68
|
+
|
|
69
|
+
await act(async () => resolvers[0](NO_KEY))
|
|
70
|
+
expect(orphan.state.get()).toBe('pending')
|
|
71
|
+
expect(track).not.toHaveBeenCalled()
|
|
72
|
+
|
|
73
|
+
await act(async () => resolvers[1](NO_KEY))
|
|
74
|
+
expect(managers.at(-1)!.state.get()).toBe('unlicensed')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useValue } from '@tldraw/state-react'
|
|
2
|
-
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
|
|
2
|
+
import { createContext, ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
3
3
|
import { useMaybeEditor } from '../hooks/useEditor'
|
|
4
4
|
import { LicenseManager } from './LicenseManager'
|
|
5
5
|
|
|
@@ -46,21 +46,35 @@ export function LicenseProvider({
|
|
|
46
46
|
licenseKey?: string
|
|
47
47
|
children: ReactNode
|
|
48
48
|
}) {
|
|
49
|
-
|
|
49
|
+
// Keyed on the license key: the editor is recreated when the key changes, and must not be
|
|
50
|
+
// handed a manager that validated the old key.
|
|
51
|
+
const licenseManager = useMemo(() => new LicenseManager(licenseKey), [licenseKey])
|
|
50
52
|
const licenseState = useValue(licenseManager.state)
|
|
51
|
-
|
|
53
|
+
// The manager whose LICENSE_TIMEOUT elapsed; compared by identity so a new key un-gates the editor.
|
|
54
|
+
const [gatedManager, setGatedManager] = useState<LicenseManager | null>(null)
|
|
55
|
+
const showEditor = gatedManager !== licenseManager
|
|
56
|
+
|
|
57
|
+
// Dispose only the replaced manager, never on cleanup: strict mode re-runs effects with the
|
|
58
|
+
// same manager, and disposing it there would silence the live one.
|
|
59
|
+
const previousManager = useRef<LicenseManager | null>(null)
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (previousManager.current && previousManager.current !== licenseManager) {
|
|
62
|
+
previousManager.current.dispose()
|
|
63
|
+
}
|
|
64
|
+
previousManager.current = licenseManager
|
|
65
|
+
}, [licenseManager])
|
|
52
66
|
|
|
53
67
|
// When license expires or no license in production, show for 5 seconds then hide
|
|
54
68
|
useEffect(() => {
|
|
55
69
|
if (shouldHideEditorAfterDelay(licenseState) && showEditor) {
|
|
56
70
|
// eslint-disable-next-line no-restricted-globals
|
|
57
71
|
const timer = setTimeout(() => {
|
|
58
|
-
|
|
72
|
+
setGatedManager(licenseManager)
|
|
59
73
|
}, LICENSE_TIMEOUT)
|
|
60
74
|
|
|
61
75
|
return () => clearTimeout(timer)
|
|
62
76
|
}
|
|
63
|
-
}, [licenseState, showEditor])
|
|
77
|
+
}, [licenseManager, licenseState, showEditor])
|
|
64
78
|
|
|
65
79
|
// If license is expired or no license in production and 5 seconds have passed, don't render anything (blank screen)
|
|
66
80
|
if (shouldHideEditorAfterDelay(licenseState) && !showEditor) {
|
|
@@ -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
|
|