@tldraw/editor 3.8.0-canary.46ed9df28e1c → 3.8.0-canary.4703b6039d91

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.
Files changed (59) hide show
  1. package/dist-cjs/index.d.ts +144 -12
  2. package/dist-cjs/index.js +5 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -4
  5. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  6. package/dist-cjs/lib/config/createTLStore.js +3 -1
  7. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  8. package/dist-cjs/lib/editor/Editor.js +62 -5
  9. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  10. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  11. package/dist-cjs/lib/editor/types/SvgExportContext.js.map +2 -2
  12. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  13. package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
  14. package/dist-cjs/lib/exports/getSvgAsImage.js +83 -0
  15. package/dist-cjs/lib/exports/getSvgAsImage.js.map +7 -0
  16. package/dist-cjs/lib/exports/getSvgJsx.js +16 -3
  17. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  18. package/dist-cjs/lib/hooks/useLocalStore.js +1 -1
  19. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  20. package/dist-cjs/lib/utils/browserCanvasMaxSize.js +75 -0
  21. package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +7 -0
  22. package/dist-cjs/version.js +3 -3
  23. package/dist-cjs/version.js.map +1 -1
  24. package/dist-esm/index.d.mts +144 -12
  25. package/dist-esm/index.mjs +5 -1
  26. package/dist-esm/index.mjs.map +2 -2
  27. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -4
  28. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  29. package/dist-esm/lib/config/createTLStore.mjs +3 -1
  30. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  31. package/dist-esm/lib/editor/Editor.mjs +62 -5
  32. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  33. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  34. package/dist-esm/lib/editor/types/SvgExportContext.mjs.map +2 -2
  35. package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
  36. package/dist-esm/lib/exports/getSvgAsImage.mjs +63 -0
  37. package/dist-esm/lib/exports/getSvgAsImage.mjs.map +7 -0
  38. package/dist-esm/lib/exports/getSvgJsx.mjs +16 -3
  39. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  40. package/dist-esm/lib/hooks/useLocalStore.mjs +1 -1
  41. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  42. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs +45 -0
  43. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +7 -0
  44. package/dist-esm/version.mjs +3 -3
  45. package/dist-esm/version.mjs.map +1 -1
  46. package/package.json +9 -7
  47. package/src/index.ts +5 -0
  48. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -4
  49. package/src/lib/config/createTLStore.ts +3 -1
  50. package/src/lib/editor/Editor.ts +90 -12
  51. package/src/lib/editor/shapes/ShapeUtil.ts +30 -1
  52. package/src/lib/editor/types/SvgExportContext.tsx +21 -0
  53. package/src/lib/editor/types/misc-types.ts +55 -2
  54. package/src/lib/exports/exportToSvg.tsx +2 -2
  55. package/src/lib/exports/getSvgAsImage.ts +92 -0
  56. package/src/lib/exports/getSvgJsx.tsx +17 -2
  57. package/src/lib/hooks/useLocalStore.ts +1 -1
  58. package/src/lib/utils/browserCanvasMaxSize.ts +65 -0
  59. package/src/version.ts +3 -3
@@ -485,10 +485,7 @@ function DebugSvgCopy({ id, mode }: { id: TLShapeId; mode: 'img' | 'iframe' }) {
485
485
  if (!bounds) return
486
486
  bounds = bounds.clone().expandBy(padding)
487
487
 
488
- const result = await editor.getSvgString([id], {
489
- padding,
490
- background: editor.getInstanceState().exportBackground,
491
- })
488
+ const result = await editor.getSvgString([id], { padding })
492
489
 
493
490
  if (latest !== renderId || !result) return
494
491
 
@@ -61,7 +61,9 @@ const defaultAssetResolve: NonNullable<TLAssetStore['resolve']> = (asset) => ass
61
61
 
62
62
  /** @public */
63
63
  export const inlineBase64AssetStore: TLAssetStore = {
64
- upload: (_, file) => FileHelpers.blobToDataUrl(file),
64
+ upload: async (_, file) => {
65
+ return { src: await FileHelpers.blobToDataUrl(file) }
66
+ },
65
67
  }
66
68
 
67
69
  /**
@@ -104,6 +104,7 @@ import {
104
104
  ZOOM_TO_FIT_PADDING,
105
105
  } from '../constants'
106
106
  import { exportToSvg } from '../exports/exportToSvg'
107
+ import { getSvgAsImage } from '../exports/getSvgAsImage'
107
108
  import { tlenv } from '../globals/environment'
108
109
  import { tlmenus } from '../globals/menus'
109
110
  import { tltime } from '../globals/time'
@@ -162,6 +163,7 @@ import {
162
163
  TLCameraMoveOptions,
163
164
  TLCameraOptions,
164
165
  TLImageExportOptions,
166
+ TLSvgExportOptions,
165
167
  } from './types/misc-types'
166
168
  import { TLResizeHandle } from './types/selection-types'
167
169
 
@@ -927,6 +929,21 @@ export class Editor extends EventEmitter<TLEventMap> {
927
929
  return shapeUtil
928
930
  }
929
931
 
932
+ /**
933
+ * Returns true if the editor has a shape util for the given shape / shape type.
934
+ *
935
+ * @param shape - A shape, shape partial, or shape type.
936
+ */
937
+ hasShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): boolean
938
+ hasShapeUtil<S extends TLUnknownShape>(type: S['type']): boolean
939
+ hasShapeUtil<T extends ShapeUtil>(
940
+ type: T extends ShapeUtil<infer R> ? R['type'] : string
941
+ ): boolean
942
+ hasShapeUtil(arg: string | { type: string }): boolean {
943
+ const type = typeof arg === 'string' ? arg : arg.type
944
+ return hasOwnProperty(this.shapeUtils, type)
945
+ }
946
+
930
947
  /* ------------------- Binding Utils ------------------ */
931
948
  /**
932
949
  * A map of shape utility classes (TLShapeUtils) by shape type.
@@ -1458,10 +1475,10 @@ export class Editor extends EventEmitter<TLEventMap> {
1458
1475
  if (partial.isChangingStyle !== undefined) {
1459
1476
  clearTimeout(this._isChangingStyleTimeout)
1460
1477
  if (partial.isChangingStyle === true) {
1461
- // If we've set to true, set a new reset timeout to change the value back to false after 2 seconds
1478
+ // If we've set to true, set a new reset timeout to change the value back to false after 1 seconds
1462
1479
  this._isChangingStyleTimeout = this.timers.setTimeout(() => {
1463
1480
  this._updateInstanceState({ isChangingStyle: false }, { history: 'ignore' })
1464
- }, 2000)
1481
+ }, 1000)
1465
1482
  }
1466
1483
  }
1467
1484
 
@@ -4145,20 +4162,24 @@ export class Editor extends EventEmitter<TLEventMap> {
4145
4162
  context: {
4146
4163
  screenScale?: number
4147
4164
  shouldResolveToOriginal?: boolean
4165
+ dpr?: number
4148
4166
  }
4149
4167
  ): Promise<string | null> {
4150
4168
  if (!assetId) return null
4151
4169
  const asset = this.getAsset(assetId)
4152
4170
  if (!asset) return null
4153
4171
 
4154
- const { screenScale = 1, shouldResolveToOriginal = false } = context
4172
+ const {
4173
+ screenScale = 1,
4174
+ shouldResolveToOriginal = false,
4175
+ dpr = this.getInstanceState().devicePixelRatio,
4176
+ } = context
4155
4177
 
4156
4178
  // We only look at the zoom level at powers of 2.
4157
4179
  const zoomStepFunction = (zoom: number) => Math.pow(2, Math.ceil(Math.log2(zoom)))
4158
- const steppedScreenScale = Math.max(0.125, zoomStepFunction(screenScale))
4180
+ const steppedScreenScale = zoomStepFunction(screenScale)
4159
4181
  const networkEffectiveType: string | null =
4160
4182
  'connection' in navigator ? (navigator as any).connection.effectiveType : null
4161
- const dpr = this.getInstanceState().devicePixelRatio
4162
4183
 
4163
4184
  return await this.store.props.assets.resolve(asset, {
4164
4185
  screenScale: screenScale || 1,
@@ -4172,7 +4193,11 @@ export class Editor extends EventEmitter<TLEventMap> {
4172
4193
  * Upload an asset to the store's asset service, returning a URL that can be used to resolve the
4173
4194
  * asset.
4174
4195
  */
4175
- async uploadAsset(asset: TLAsset, file: File, abortSignal?: AbortSignal): Promise<string> {
4196
+ async uploadAsset(
4197
+ asset: TLAsset,
4198
+ file: File,
4199
+ abortSignal?: AbortSignal
4200
+ ): Promise<{ src: string; meta?: JsonObject }> {
4176
4201
  return await this.store.props.assets.upload(asset, file, abortSignal)
4177
4202
  }
4178
4203
 
@@ -8564,11 +8589,13 @@ export class Editor extends EventEmitter<TLEventMap> {
8564
8589
  *
8565
8590
  * @public
8566
8591
  */
8567
- async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8592
+ async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
8568
8593
  const ids =
8569
- typeof shapes[0] === 'string'
8570
- ? (shapes as TLShapeId[])
8571
- : (shapes as TLShape[]).map((s) => s.id)
8594
+ shapes.length === 0
8595
+ ? this.getCurrentPageShapeIdsSorted()
8596
+ : typeof shapes[0] === 'string'
8597
+ ? (shapes as TLShapeId[])
8598
+ : (shapes as TLShape[]).map((s) => s.id)
8572
8599
 
8573
8600
  if (ids.length === 0) return undefined
8574
8601
 
@@ -8585,7 +8612,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8585
8612
  *
8586
8613
  * @public
8587
8614
  */
8588
- async getSvgString(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8615
+ async getSvgString(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
8589
8616
  const result = await this.getSvgElement(shapes, opts)
8590
8617
  if (!result) return undefined
8591
8618
 
@@ -8598,12 +8625,63 @@ export class Editor extends EventEmitter<TLEventMap> {
8598
8625
  }
8599
8626
 
8600
8627
  /** @deprecated Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead. */
8601
- async getSvg(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8628
+ async getSvg(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
8602
8629
  const result = await this.getSvgElement(shapes, opts)
8603
8630
  if (!result) return undefined
8604
8631
  return result.svg
8605
8632
  }
8606
8633
 
8634
+ /**
8635
+ * Get an exported image of the given shapes.
8636
+ *
8637
+ * @param shapes - The shapes (or shape ids) to export.
8638
+ * @param opts - Options for the export.
8639
+ *
8640
+ * @returns A blob of the image.
8641
+ * @public
8642
+ */
8643
+ async toImage(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8644
+ const withDefaults = {
8645
+ format: 'png',
8646
+ scale: 1,
8647
+ pixelRatio: opts.format === 'svg' ? undefined : 2,
8648
+ ...opts,
8649
+ } satisfies TLImageExportOptions
8650
+ const result = await this.getSvgString(shapes, withDefaults)
8651
+ if (!result) throw new Error('Could not create SVG')
8652
+
8653
+ switch (withDefaults.format) {
8654
+ case 'svg':
8655
+ return {
8656
+ blob: new Blob([result.svg], { type: 'text/plain' }),
8657
+ width: result.width,
8658
+ height: result.height,
8659
+ }
8660
+ case 'jpeg':
8661
+ case 'png':
8662
+ case 'webp': {
8663
+ const blob = await getSvgAsImage(result.svg, {
8664
+ type: withDefaults.format,
8665
+ quality: withDefaults.quality,
8666
+ pixelRatio: withDefaults.pixelRatio,
8667
+ width: result.width,
8668
+ height: result.height,
8669
+ })
8670
+ if (!blob) {
8671
+ throw new Error('Could not construct image.')
8672
+ }
8673
+ return {
8674
+ blob,
8675
+ width: result.width,
8676
+ height: result.height,
8677
+ }
8678
+ }
8679
+ default: {
8680
+ exhaustiveSwitchError(withDefaults.format)
8681
+ }
8682
+ }
8683
+ }
8684
+
8607
8685
  /* --------------------- Events --------------------- */
8608
8686
 
8609
8687
  /**
@@ -5,11 +5,12 @@ import {
5
5
  TLHandle,
6
6
  TLPropsMigrations,
7
7
  TLShape,
8
+ TLShapeCrop,
8
9
  TLShapePartial,
9
10
  TLUnknownShape,
10
11
  } from '@tldraw/tlschema'
11
12
  import { ReactElement } from 'react'
12
- import { Box } from '../../primitives/Box'
13
+ import { Box, SelectionHandle } from '../../primitives/Box'
13
14
  import { Vec } from '../../primitives/Vec'
14
15
  import { Geometry2d } from '../../primitives/geometry/Geometry2d'
15
16
  import type { Editor } from '../Editor'
@@ -419,6 +420,19 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
419
420
  */
420
421
  onBeforeUpdate?(prev: Shape, next: Shape): Shape | void
421
422
 
423
+ /**
424
+ * A callback called when a shape changes from a crop.
425
+ *
426
+ * @param shape - The shape at the start of the crop.
427
+ * @param info - Info about the crop.
428
+ * @returns A change to apply to the shape, or void.
429
+ * @public
430
+ */
431
+ onCrop?(
432
+ shape: Shape,
433
+ info: TLCropInfo<Shape>
434
+ ): Omit<TLShapePartial<Shape>, 'id' | 'type'> | undefined | void
435
+
422
436
  /**
423
437
  * A callback called when some other shapes are dragged over this one.
424
438
  *
@@ -616,6 +630,21 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
616
630
  onEditEnd?(shape: Shape): void
617
631
  }
618
632
 
633
+ /**
634
+ * Info about a crop.
635
+ * @param handle - The handle being dragged.
636
+ * @param change - The distance the handle is moved.
637
+ * @param initialShape - The shape at the start of the resize.
638
+ * @public
639
+ */
640
+ export interface TLCropInfo<T extends TLShape> {
641
+ handle: SelectionHandle
642
+ change: Vec
643
+ crop: TLShapeCrop
644
+ uncroppedSize: { w: number; h: number }
645
+ initialShape: T
646
+ }
647
+
619
648
  /**
620
649
  * The type of resize.
621
650
  *
@@ -1,3 +1,4 @@
1
+ import { TLAssetId } from '@tldraw/tlschema'
1
2
  import { promiseWithResolve } from '@tldraw/utils'
2
3
  import { ReactElement, ReactNode, createContext, useContext, useEffect, useState } from 'react'
3
4
  import { ContainerProvider } from '../../hooks/useContainer'
@@ -29,10 +30,30 @@ export interface SvgExportContext {
29
30
  */
30
31
  waitUntil(promise: Promise<void>): void
31
32
 
33
+ /**
34
+ * Resolve an asset URL in the context of this export. Supply the asset ID and the width in
35
+ * shape-pixels it'll be displayed at, and this will resolve the asset according to the export
36
+ * options.
37
+ */
38
+ resolveAssetUrl(assetId: TLAssetId, width: number): Promise<string | null>
39
+
32
40
  /**
33
41
  * Whether the export should be in dark mode.
34
42
  */
35
43
  readonly isDarkMode: boolean
44
+
45
+ /**
46
+ * The scale of the export - how much CSS pixels will be scaled up/down by.
47
+ */
48
+ readonly scale: number
49
+
50
+ /**
51
+ * Use this value to optionally downscale images in the export. If we're exporting directly to
52
+ * an SVG, this will usually be null, and you shouldn't downscale images. If the export is to a
53
+ * raster format like PNG, this will be the number of raster pixels in the resulting bitmap per
54
+ * CSS pixel in the resulting SVG.
55
+ */
56
+ readonly pixelRatio: number | null
36
57
  }
37
58
 
38
59
  const Context = createContext<SvgExportContext | null>(null)
@@ -8,17 +8,70 @@ export type RequiredKeys<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T,
8
8
  export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
9
9
 
10
10
  /** @public */
11
- export interface TLImageExportOptions {
11
+ export type TLExportType = 'svg' | 'png' | 'jpeg' | 'webp'
12
+
13
+ /** @public */
14
+ export interface TLSvgExportOptions {
15
+ /**
16
+ * The bounding box, in page coordinates, of the area being exported.
17
+ */
12
18
  bounds?: Box
19
+ /**
20
+ * The logical scale of the export. This scales the resulting size of the SVG being generated.
21
+ */
13
22
  scale?: number
14
- quality?: number
23
+ /**
24
+ * When exporting an SVG, the expected pixel ratio of the export will be passed in to
25
+ * {@link @tldraw/tlschema#TLAssetStore.resolve} as the `dpr` property, so that assets can be
26
+ * downscaled to the appropriate resolution.
27
+ *
28
+ * When exporting to a bitmap image format, the size of the resulting image will be multiplied
29
+ * by this number.
30
+ *
31
+ * For SVG exports, this defaults to undefined - which means we'll request original-quality
32
+ * assets. For bitmap exports, this defaults to 2.
33
+ */
15
34
  pixelRatio?: number
35
+
36
+ /**
37
+ * Should the background color be included in the export? If false, the generated image will be
38
+ * transparent (if exporting to a format that supports transparency).
39
+ */
16
40
  background?: boolean
41
+
42
+ /**
43
+ * How much padding to include around the bounds of exports? Defaults to 32px.
44
+ */
17
45
  padding?: number
46
+
47
+ /**
48
+ * Should the export be rendered in dark mode (true) or light mode (false)? Defaults to the
49
+ * current instance's dark mode setting.
50
+ */
18
51
  darkMode?: boolean
52
+
53
+ /**
54
+ * The
55
+ * {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio | `preserveAspectRatio` }
56
+ * attribute of the SVG element.
57
+ */
19
58
  preserveAspectRatio?: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']
20
59
  }
21
60
 
61
+ /** @public */
62
+ export interface TLImageExportOptions extends TLSvgExportOptions {
63
+ /**
64
+ * If the export is being converted to a lossy bitmap format (e.g. jpeg), this is the quality of
65
+ * the export. This is a number between 0 and 1.
66
+ */
67
+ quality?: number
68
+
69
+ /**
70
+ * The format to export as. Defaults to 'png'.
71
+ */
72
+ format?: TLExportType
73
+ }
74
+
22
75
  /**
23
76
  * @public
24
77
  * @deprecated use {@link TLImageExportOptions} instead
@@ -3,7 +3,7 @@ import { assert } from '@tldraw/utils'
3
3
  import { flushSync } from 'react-dom'
4
4
  import { createRoot } from 'react-dom/client'
5
5
  import { Editor } from '../editor/Editor'
6
- import { TLImageExportOptions } from '../editor/types/misc-types'
6
+ import { TLSvgExportOptions } from '../editor/types/misc-types'
7
7
  import { StyleEmbedder } from './StyleEmbedder'
8
8
  import { embedMedia } from './embedMedia'
9
9
  import { getSvgJsx } from './getSvgJsx'
@@ -13,7 +13,7 @@ let idCounter = 1
13
13
  export async function exportToSvg(
14
14
  editor: Editor,
15
15
  shapeIds: TLShapeId[],
16
- opts: TLImageExportOptions = {}
16
+ opts: TLSvgExportOptions = {}
17
17
  ) {
18
18
  // when rendering to SVG, we start by creating a JSX representation of the SVG that we can
19
19
  // render with react. Hopefully elements will have a `toSvg` method that renders them to SVG,
@@ -0,0 +1,92 @@
1
+ import { FileHelpers, Image, PngHelpers, sleep } from '@tldraw/utils'
2
+ import { tlenv } from '../globals/environment'
3
+ import { clampToBrowserMaxCanvasSize } from '../utils/browserCanvasMaxSize'
4
+ import { debugFlags } from '../utils/debug-flags'
5
+
6
+ /** @public */
7
+ export async function getSvgAsImage(
8
+ svgString: string,
9
+ options: {
10
+ type: 'png' | 'jpeg' | 'webp'
11
+ width: number
12
+ height: number
13
+ quality?: number
14
+ pixelRatio?: number
15
+ }
16
+ ) {
17
+ const { type, width, height, quality = 1, pixelRatio = 2 } = options
18
+
19
+ let [clampedWidth, clampedHeight] = await clampToBrowserMaxCanvasSize(
20
+ width * pixelRatio,
21
+ height * pixelRatio
22
+ )
23
+ clampedWidth = Math.floor(clampedWidth)
24
+ clampedHeight = Math.floor(clampedHeight)
25
+ const effectiveScale = clampedWidth / width
26
+
27
+ // usually we would use `URL.createObjectURL` here, but chrome has a bug where `blob:` URLs of
28
+ // SVGs that use <foreignObject> mark the canvas as tainted, where data: ones do not.
29
+ // https://issues.chromium.org/issues/41054640
30
+ const svgUrl = await FileHelpers.blobToDataUrl(new Blob([svgString], { type: 'image/svg+xml' }))
31
+
32
+ const canvas = await new Promise<HTMLCanvasElement | null>((resolve) => {
33
+ const image = Image()
34
+ image.crossOrigin = 'anonymous'
35
+
36
+ image.onload = async () => {
37
+ // safari will fire `onLoad` before the fonts in the SVG are
38
+ // actually loaded. just waiting around a while is brittle, but
39
+ // there doesn't seem to be any better solution for now :( see
40
+ // https://bugs.webkit.org/show_bug.cgi?id=219770
41
+ if (tlenv.isSafari) {
42
+ await sleep(250)
43
+ }
44
+
45
+ const canvas = document.createElement('canvas') as HTMLCanvasElement
46
+ const ctx = canvas.getContext('2d')!
47
+
48
+ canvas.width = clampedWidth
49
+ canvas.height = clampedHeight
50
+
51
+ ctx.imageSmoothingEnabled = true
52
+ ctx.imageSmoothingQuality = 'high'
53
+ ctx.drawImage(image, 0, 0, clampedWidth, clampedHeight)
54
+
55
+ URL.revokeObjectURL(svgUrl)
56
+
57
+ resolve(canvas)
58
+ }
59
+
60
+ image.onerror = () => {
61
+ resolve(null)
62
+ }
63
+
64
+ image.src = svgUrl
65
+ })
66
+
67
+ if (!canvas) return null
68
+
69
+ const blob = await new Promise<Blob | null>((resolve) =>
70
+ canvas.toBlob(
71
+ (blob) => {
72
+ if (!blob || debugFlags.throwToBlob.get()) {
73
+ resolve(null)
74
+ }
75
+ resolve(blob)
76
+ },
77
+ 'image/' + type,
78
+ quality
79
+ )
80
+ )
81
+
82
+ if (!blob) return null
83
+
84
+ if (type === 'png') {
85
+ const view = new DataView(await blob.arrayBuffer())
86
+ return PngHelpers.setPhysChunk(view, effectiveScale, {
87
+ type: 'image/' + type,
88
+ })
89
+ } else {
90
+ return blob
91
+ }
92
+ }
@@ -41,7 +41,7 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
41
41
  const {
42
42
  scale = 1,
43
43
  // should we include the background in the export? or is it transparent?
44
- background = false,
44
+ background = editor.getInstanceState().exportBackground,
45
45
  padding = editor.options.defaultSvgPadding,
46
46
  preserveAspectRatio,
47
47
  } = opts
@@ -102,6 +102,7 @@ export function getSvgJsx(editor: Editor, ids: TLShapeId[], opts: TLImageExportO
102
102
  editor={editor}
103
103
  preserveAspectRatio={preserveAspectRatio}
104
104
  scale={scale}
105
+ pixelRatio={opts.pixelRatio ?? null}
105
106
  bbox={bbox}
106
107
  background={background}
107
108
  singleFrameShapeId={singleFrameShapeId}
@@ -121,6 +122,7 @@ function SvgExport({
121
122
  editor,
122
123
  preserveAspectRatio,
123
124
  scale,
125
+ pixelRatio,
124
126
  bbox,
125
127
  background,
126
128
  singleFrameShapeId,
@@ -132,6 +134,7 @@ function SvgExport({
132
134
  editor: Editor
133
135
  preserveAspectRatio?: string
134
136
  scale: number
137
+ pixelRatio: number | null
135
138
  bbox: Box
136
139
  background: boolean
137
140
  singleFrameShapeId: TLShapeId | null
@@ -177,8 +180,20 @@ function SvgExport({
177
180
  isDarkMode,
178
181
  waitUntil,
179
182
  addExportDef,
183
+ scale,
184
+ pixelRatio,
185
+ async resolveAssetUrl(assetId, width) {
186
+ const asset = editor.getAsset(assetId)
187
+ if (!asset || (asset.type !== 'image' && asset.type !== 'video')) return null
188
+
189
+ return await editor.resolveAssetUrl(assetId, {
190
+ screenScale: scale * (width / asset.props.w),
191
+ shouldResolveToOriginal: pixelRatio === null,
192
+ dpr: pixelRatio ?? undefined,
193
+ })
194
+ },
180
195
  }),
181
- [isDarkMode, waitUntil, addExportDef]
196
+ [isDarkMode, waitUntil, addExportDef, scale, pixelRatio, editor]
182
197
  )
183
198
 
184
199
  const didRenderRef = useRef(false)
@@ -37,7 +37,7 @@ export function useLocalStore(
37
37
  const assets: TLAssetStore = {
38
38
  upload: async (asset, file) => {
39
39
  await client.db.storeAsset(asset.id, file)
40
- return asset.id
40
+ return { src: asset.id }
41
41
  },
42
42
  resolve: async (asset) => {
43
43
  if (!asset.props.src) return null
@@ -0,0 +1,65 @@
1
+ import canvasSize from 'canvas-size'
2
+
3
+ /** @internal */
4
+ export interface CanvasMaxSize {
5
+ maxWidth: number
6
+ maxHeight: number
7
+ maxArea: number
8
+ }
9
+
10
+ let maxSizePromise: Promise<CanvasMaxSize> | null = null
11
+
12
+ function getBrowserCanvasMaxSize() {
13
+ if (!maxSizePromise) {
14
+ maxSizePromise = calculateBrowserCanvasMaxSize()
15
+ }
16
+
17
+ return maxSizePromise
18
+ }
19
+
20
+ async function calculateBrowserCanvasMaxSize(): Promise<CanvasMaxSize> {
21
+ const maxWidth = await canvasSize.maxWidth({ usePromise: true })
22
+ const maxHeight = await canvasSize.maxHeight({ usePromise: true })
23
+ const maxArea = await canvasSize.maxArea({ usePromise: true })
24
+ return {
25
+ maxWidth: maxWidth.width,
26
+ maxHeight: maxHeight.height,
27
+ maxArea: maxArea.width * maxArea.height,
28
+ }
29
+ }
30
+
31
+ // https://github.com/jhildenbiddle/canvas-size?tab=readme-ov-file#test-results
32
+ const MAX_SAFE_CANVAS_DIMENSION = 8192
33
+ const MAX_SAFE_CANVAS_AREA = 4096 * 4096
34
+
35
+ /** @internal */
36
+ export async function clampToBrowserMaxCanvasSize(width: number, height: number) {
37
+ if (
38
+ width <= MAX_SAFE_CANVAS_DIMENSION &&
39
+ height <= MAX_SAFE_CANVAS_DIMENSION &&
40
+ width * height <= MAX_SAFE_CANVAS_AREA
41
+ ) {
42
+ return [width, height]
43
+ }
44
+
45
+ const { maxWidth, maxHeight, maxArea } = await getBrowserCanvasMaxSize()
46
+ const aspectRatio = width / height
47
+
48
+ if (width > maxWidth) {
49
+ width = maxWidth
50
+ height = width / aspectRatio
51
+ }
52
+
53
+ if (height > maxHeight) {
54
+ height = maxHeight
55
+ width = height * aspectRatio
56
+ }
57
+
58
+ if (width * height > maxArea) {
59
+ const ratio = Math.sqrt(maxArea / (width * height))
60
+ width *= ratio
61
+ height *= ratio
62
+ }
63
+
64
+ return [width, height]
65
+ }
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '3.8.0-canary.46ed9df28e1c'
4
+ export const version = '3.8.0-canary.4703b6039d91'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-01-20T11:54:52.144Z',
8
- patch: '2025-01-20T11:54:52.144Z',
7
+ minor: '2025-01-28T10:50:55.780Z',
8
+ patch: '2025-01-28T10:50:55.780Z',
9
9
  }