@tldraw/editor 3.8.0-canary.962422478a23 → 3.8.0-canary.98976dbbb3dd

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 (75) hide show
  1. package/dist-cjs/index.d.ts +167 -12
  2. package/dist-cjs/index.js +7 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +2 -5
  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 +82 -19
  9. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  10. package/dist-cjs/lib/editor/managers/TextManager.js +1 -0
  11. package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
  12. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  13. package/dist-cjs/lib/editor/shapes/shared/resizeScaled.js +66 -0
  14. package/dist-cjs/lib/editor/shapes/shared/resizeScaled.js.map +7 -0
  15. package/dist-cjs/lib/editor/types/SvgExportContext.js.map +2 -2
  16. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  17. package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
  18. package/dist-cjs/lib/exports/getSvgAsImage.js +83 -0
  19. package/dist-cjs/lib/exports/getSvgAsImage.js.map +7 -0
  20. package/dist-cjs/lib/exports/getSvgJsx.js +16 -3
  21. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  22. package/dist-cjs/lib/hooks/useLocalStore.js +1 -1
  23. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  24. package/dist-cjs/lib/options.js +2 -1
  25. package/dist-cjs/lib/options.js.map +2 -2
  26. package/dist-cjs/lib/utils/browserCanvasMaxSize.js +75 -0
  27. package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +7 -0
  28. package/dist-cjs/version.js +3 -3
  29. package/dist-cjs/version.js.map +1 -1
  30. package/dist-esm/index.d.mts +167 -12
  31. package/dist-esm/index.mjs +7 -1
  32. package/dist-esm/index.mjs.map +2 -2
  33. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +2 -5
  34. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  35. package/dist-esm/lib/config/createTLStore.mjs +3 -1
  36. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  37. package/dist-esm/lib/editor/Editor.mjs +82 -19
  38. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  39. package/dist-esm/lib/editor/managers/TextManager.mjs +1 -0
  40. package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
  41. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  42. package/dist-esm/lib/editor/shapes/shared/resizeScaled.mjs +46 -0
  43. package/dist-esm/lib/editor/shapes/shared/resizeScaled.mjs.map +7 -0
  44. package/dist-esm/lib/editor/types/SvgExportContext.mjs.map +2 -2
  45. package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
  46. package/dist-esm/lib/exports/getSvgAsImage.mjs +63 -0
  47. package/dist-esm/lib/exports/getSvgAsImage.mjs.map +7 -0
  48. package/dist-esm/lib/exports/getSvgJsx.mjs +16 -3
  49. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  50. package/dist-esm/lib/hooks/useLocalStore.mjs +1 -1
  51. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  52. package/dist-esm/lib/options.mjs +2 -1
  53. package/dist-esm/lib/options.mjs.map +2 -2
  54. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs +45 -0
  55. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +7 -0
  56. package/dist-esm/version.mjs +3 -3
  57. package/dist-esm/version.mjs.map +1 -1
  58. package/editor.css +2 -1
  59. package/package.json +9 -7
  60. package/src/index.ts +6 -0
  61. package/src/lib/components/default-components/DefaultCanvas.tsx +2 -5
  62. package/src/lib/config/createTLStore.ts +3 -1
  63. package/src/lib/editor/Editor.ts +116 -26
  64. package/src/lib/editor/managers/TextManager.ts +1 -0
  65. package/src/lib/editor/shapes/ShapeUtil.ts +30 -1
  66. package/src/lib/editor/shapes/shared/resizeScaled.ts +61 -0
  67. package/src/lib/editor/types/SvgExportContext.tsx +21 -0
  68. package/src/lib/editor/types/misc-types.ts +55 -2
  69. package/src/lib/exports/exportToSvg.tsx +2 -2
  70. package/src/lib/exports/getSvgAsImage.ts +92 -0
  71. package/src/lib/exports/getSvgJsx.tsx +17 -2
  72. package/src/lib/hooks/useLocalStore.ts +1 -1
  73. package/src/lib/options.ts +6 -0
  74. package/src/lib/utils/browserCanvasMaxSize.ts +65 -0
  75. package/src/version.ts +3 -3
@@ -42,7 +42,8 @@ const defaultTldrawOptions = {
42
42
  temporaryAssetPreviewLifetimeMs: 18e4,
43
43
  actionShortcutsLocation: "swap",
44
44
  createTextOnCanvasDoubleClick: true,
45
- exportProvider: Fragment
45
+ exportProvider: Fragment,
46
+ noteShapeResizeMode: "none"
46
47
  };
47
48
  export {
48
49
  defaultTldrawOptions
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/options.ts"],
4
- "sourcesContent": ["import { ComponentType, Fragment } from 'react'\n\n/**\n * Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.\n *\n * @example\n * ```tsx\n * const options: Partial<TldrawOptions> = {\n * maxPages: 3,\n * maxShapesPerPage: 1000,\n * }\n *\n * function MyTldrawComponent() {\n * return <Tldraw options={options} />\n * }\n * ```\n *\n * @public\n */\nexport interface TldrawOptions {\n\treadonly maxShapesPerPage: number\n\treadonly maxFilesAtOnce: number\n\treadonly maxPages: number\n\treadonly animationMediumMs: number\n\treadonly followChaseViewportSnap: number\n\treadonly doubleClickDurationMs: number\n\treadonly multiClickDurationMs: number\n\treadonly coarseDragDistanceSquared: number\n\treadonly dragDistanceSquared: number\n\treadonly defaultSvgPadding: number\n\treadonly cameraSlideFriction: number\n\treadonly maxPointsPerDrawShape: number\n\treadonly gridSteps: readonly {\n\t\treadonly min: number\n\t\treadonly mid: number\n\t\treadonly step: number\n\t}[]\n\treadonly collaboratorInactiveTimeoutMs: number\n\treadonly collaboratorIdleTimeoutMs: number\n\treadonly collaboratorCheckIntervalMs: number\n\treadonly cameraMovingTimeoutMs: number\n\treadonly hitTestMargin: number\n\treadonly edgeScrollDelay: number\n\treadonly edgeScrollEaseDuration: number\n\treadonly edgeScrollSpeed: number\n\treadonly edgeScrollDistance: number\n\treadonly coarsePointerWidth: number\n\treadonly coarseHandleRadius: number\n\treadonly handleRadius: number\n\treadonly longPressDurationMs: number\n\treadonly textShadowLod: number\n\treadonly adjacentShapeMargin: number\n\treadonly flattenImageBoundsExpand: number\n\treadonly flattenImageBoundsPadding: number\n\treadonly laserDelayMs: number\n\treadonly maxExportDelayMs: number\n\t/**\n\t * How long should previews created by {@link Editor.createTemporaryAssetPreview} last before\n\t * they expire? Defaults to 3 minutes.\n\t */\n\treadonly temporaryAssetPreviewLifetimeMs: number\n\treadonly actionShortcutsLocation: 'menu' | 'toolbar' | 'swap'\n\treadonly createTextOnCanvasDoubleClick: boolean\n\t/**\n\t * The react provider to use when exporting an image. This is useful if your shapes depend on\n\t * external context providers. By default, this is `React.Fragment`.\n\t */\n\treadonly exportProvider: ComponentType<{ children: React.ReactNode }>\n}\n\n/** @public */\nexport const defaultTldrawOptions = {\n\tmaxShapesPerPage: 4000,\n\tmaxFilesAtOnce: 100,\n\tmaxPages: 40,\n\tanimationMediumMs: 320,\n\tfollowChaseViewportSnap: 2,\n\tdoubleClickDurationMs: 450,\n\tmultiClickDurationMs: 200,\n\tcoarseDragDistanceSquared: 36, // 6 squared\n\tdragDistanceSquared: 16, // 4 squared\n\tdefaultSvgPadding: 32,\n\tcameraSlideFriction: 0.09,\n\tmaxPointsPerDrawShape: 500,\n\tgridSteps: [\n\t\t{ min: -1, mid: 0.15, step: 64 },\n\t\t{ min: 0.05, mid: 0.375, step: 16 },\n\t\t{ min: 0.15, mid: 1, step: 4 },\n\t\t{ min: 0.7, mid: 2.5, step: 1 },\n\t],\n\tcollaboratorInactiveTimeoutMs: 60000,\n\tcollaboratorIdleTimeoutMs: 3000,\n\tcollaboratorCheckIntervalMs: 1200,\n\tcameraMovingTimeoutMs: 64,\n\thitTestMargin: 8,\n\tedgeScrollDelay: 200,\n\tedgeScrollEaseDuration: 200,\n\tedgeScrollSpeed: 25,\n\tedgeScrollDistance: 8,\n\tcoarsePointerWidth: 12,\n\tcoarseHandleRadius: 20,\n\thandleRadius: 12,\n\tlongPressDurationMs: 500,\n\ttextShadowLod: 0.35,\n\tadjacentShapeMargin: 10,\n\tflattenImageBoundsExpand: 64,\n\tflattenImageBoundsPadding: 16,\n\tlaserDelayMs: 1200,\n\tmaxExportDelayMs: 5000,\n\ttemporaryAssetPreviewLifetimeMs: 180000,\n\tactionShortcutsLocation: 'swap',\n\tcreateTextOnCanvasDoubleClick: true,\n\texportProvider: Fragment,\n} as const satisfies TldrawOptions\n"],
5
- "mappings": "AAAA,SAAwB,gBAAgB;AAuEjC,MAAM,uBAAuB;AAAA,EACnC,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,mBAAmB;AAAA,EACnB,yBAAyB;AAAA,EACzB,uBAAuB;AAAA,EACvB,sBAAsB;AAAA,EACtB,2BAA2B;AAAA;AAAA,EAC3B,qBAAqB;AAAA;AAAA,EACrB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,uBAAuB;AAAA,EACvB,WAAW;AAAA,IACV,EAAE,KAAK,IAAI,KAAK,MAAM,MAAM,GAAG;AAAA,IAC/B,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,GAAG;AAAA,IAClC,EAAE,KAAK,MAAM,KAAK,GAAG,MAAM,EAAE;AAAA,IAC7B,EAAE,KAAK,KAAK,KAAK,KAAK,MAAM,EAAE;AAAA,EAC/B;AAAA,EACA,+BAA+B;AAAA,EAC/B,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,0BAA0B;AAAA,EAC1B,2BAA2B;AAAA,EAC3B,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,iCAAiC;AAAA,EACjC,yBAAyB;AAAA,EACzB,+BAA+B;AAAA,EAC/B,gBAAgB;AACjB;",
4
+ "sourcesContent": ["import { ComponentType, Fragment } from 'react'\n\n/**\n * Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.\n *\n * @example\n * ```tsx\n * const options: Partial<TldrawOptions> = {\n * maxPages: 3,\n * maxShapesPerPage: 1000,\n * }\n *\n * function MyTldrawComponent() {\n * return <Tldraw options={options} />\n * }\n * ```\n *\n * @public\n */\nexport interface TldrawOptions {\n\treadonly maxShapesPerPage: number\n\treadonly maxFilesAtOnce: number\n\treadonly maxPages: number\n\treadonly animationMediumMs: number\n\treadonly followChaseViewportSnap: number\n\treadonly doubleClickDurationMs: number\n\treadonly multiClickDurationMs: number\n\treadonly coarseDragDistanceSquared: number\n\treadonly dragDistanceSquared: number\n\treadonly defaultSvgPadding: number\n\treadonly cameraSlideFriction: number\n\treadonly maxPointsPerDrawShape: number\n\treadonly gridSteps: readonly {\n\t\treadonly min: number\n\t\treadonly mid: number\n\t\treadonly step: number\n\t}[]\n\treadonly collaboratorInactiveTimeoutMs: number\n\treadonly collaboratorIdleTimeoutMs: number\n\treadonly collaboratorCheckIntervalMs: number\n\treadonly cameraMovingTimeoutMs: number\n\treadonly hitTestMargin: number\n\treadonly edgeScrollDelay: number\n\treadonly edgeScrollEaseDuration: number\n\treadonly edgeScrollSpeed: number\n\treadonly edgeScrollDistance: number\n\treadonly coarsePointerWidth: number\n\treadonly coarseHandleRadius: number\n\treadonly handleRadius: number\n\treadonly longPressDurationMs: number\n\treadonly textShadowLod: number\n\treadonly adjacentShapeMargin: number\n\treadonly flattenImageBoundsExpand: number\n\treadonly flattenImageBoundsPadding: number\n\treadonly laserDelayMs: number\n\treadonly maxExportDelayMs: number\n\t/**\n\t * How long should previews created by {@link Editor.createTemporaryAssetPreview} last before\n\t * they expire? Defaults to 3 minutes.\n\t */\n\treadonly temporaryAssetPreviewLifetimeMs: number\n\treadonly actionShortcutsLocation: 'menu' | 'toolbar' | 'swap'\n\treadonly createTextOnCanvasDoubleClick: boolean\n\t/**\n\t * The react provider to use when exporting an image. This is useful if your shapes depend on\n\t * external context providers. By default, this is `React.Fragment`.\n\t */\n\treadonly exportProvider: ComponentType<{ children: React.ReactNode }>\n\t/**\n\t * How should the note shape resize? By default it does not resize (except automatically based on its text content),\n\t * but you can set it to be user-resizable using scale.\n\t */\n\treadonly noteShapeResizeMode: 'none' | 'scale'\n}\n\n/** @public */\nexport const defaultTldrawOptions = {\n\tmaxShapesPerPage: 4000,\n\tmaxFilesAtOnce: 100,\n\tmaxPages: 40,\n\tanimationMediumMs: 320,\n\tfollowChaseViewportSnap: 2,\n\tdoubleClickDurationMs: 450,\n\tmultiClickDurationMs: 200,\n\tcoarseDragDistanceSquared: 36, // 6 squared\n\tdragDistanceSquared: 16, // 4 squared\n\tdefaultSvgPadding: 32,\n\tcameraSlideFriction: 0.09,\n\tmaxPointsPerDrawShape: 500,\n\tgridSteps: [\n\t\t{ min: -1, mid: 0.15, step: 64 },\n\t\t{ min: 0.05, mid: 0.375, step: 16 },\n\t\t{ min: 0.15, mid: 1, step: 4 },\n\t\t{ min: 0.7, mid: 2.5, step: 1 },\n\t],\n\tcollaboratorInactiveTimeoutMs: 60000,\n\tcollaboratorIdleTimeoutMs: 3000,\n\tcollaboratorCheckIntervalMs: 1200,\n\tcameraMovingTimeoutMs: 64,\n\thitTestMargin: 8,\n\tedgeScrollDelay: 200,\n\tedgeScrollEaseDuration: 200,\n\tedgeScrollSpeed: 25,\n\tedgeScrollDistance: 8,\n\tcoarsePointerWidth: 12,\n\tcoarseHandleRadius: 20,\n\thandleRadius: 12,\n\tlongPressDurationMs: 500,\n\ttextShadowLod: 0.35,\n\tadjacentShapeMargin: 10,\n\tflattenImageBoundsExpand: 64,\n\tflattenImageBoundsPadding: 16,\n\tlaserDelayMs: 1200,\n\tmaxExportDelayMs: 5000,\n\ttemporaryAssetPreviewLifetimeMs: 180000,\n\tactionShortcutsLocation: 'swap',\n\tcreateTextOnCanvasDoubleClick: true,\n\texportProvider: Fragment,\n\tnoteShapeResizeMode: 'none',\n} as const satisfies TldrawOptions\n"],
5
+ "mappings": "AAAA,SAAwB,gBAAgB;AA4EjC,MAAM,uBAAuB;AAAA,EACnC,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,mBAAmB;AAAA,EACnB,yBAAyB;AAAA,EACzB,uBAAuB;AAAA,EACvB,sBAAsB;AAAA,EACtB,2BAA2B;AAAA;AAAA,EAC3B,qBAAqB;AAAA;AAAA,EACrB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,uBAAuB;AAAA,EACvB,WAAW;AAAA,IACV,EAAE,KAAK,IAAI,KAAK,MAAM,MAAM,GAAG;AAAA,IAC/B,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,GAAG;AAAA,IAClC,EAAE,KAAK,MAAM,KAAK,GAAG,MAAM,EAAE;AAAA,IAC7B,EAAE,KAAK,KAAK,KAAK,KAAK,MAAM,EAAE;AAAA,EAC/B;AAAA,EACA,+BAA+B;AAAA,EAC/B,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,0BAA0B;AAAA,EAC1B,2BAA2B;AAAA,EAC3B,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,iCAAiC;AAAA,EACjC,yBAAyB;AAAA,EACzB,+BAA+B;AAAA,EAC/B,gBAAgB;AAAA,EAChB,qBAAqB;AACtB;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,45 @@
1
+ import canvasSize from "canvas-size";
2
+ let maxSizePromise = null;
3
+ function getBrowserCanvasMaxSize() {
4
+ if (!maxSizePromise) {
5
+ maxSizePromise = calculateBrowserCanvasMaxSize();
6
+ }
7
+ return maxSizePromise;
8
+ }
9
+ async function calculateBrowserCanvasMaxSize() {
10
+ const maxWidth = await canvasSize.maxWidth({ usePromise: true });
11
+ const maxHeight = await canvasSize.maxHeight({ usePromise: true });
12
+ const maxArea = await canvasSize.maxArea({ usePromise: true });
13
+ return {
14
+ maxWidth: maxWidth.width,
15
+ maxHeight: maxHeight.height,
16
+ maxArea: maxArea.width * maxArea.height
17
+ };
18
+ }
19
+ const MAX_SAFE_CANVAS_DIMENSION = 8192;
20
+ const MAX_SAFE_CANVAS_AREA = 4096 * 4096;
21
+ async function clampToBrowserMaxCanvasSize(width, height) {
22
+ if (width <= MAX_SAFE_CANVAS_DIMENSION && height <= MAX_SAFE_CANVAS_DIMENSION && width * height <= MAX_SAFE_CANVAS_AREA) {
23
+ return [width, height];
24
+ }
25
+ const { maxWidth, maxHeight, maxArea } = await getBrowserCanvasMaxSize();
26
+ const aspectRatio = width / height;
27
+ if (width > maxWidth) {
28
+ width = maxWidth;
29
+ height = width / aspectRatio;
30
+ }
31
+ if (height > maxHeight) {
32
+ height = maxHeight;
33
+ width = height * aspectRatio;
34
+ }
35
+ if (width * height > maxArea) {
36
+ const ratio = Math.sqrt(maxArea / (width * height));
37
+ width *= ratio;
38
+ height *= ratio;
39
+ }
40
+ return [width, height];
41
+ }
42
+ export {
43
+ clampToBrowserMaxCanvasSize
44
+ };
45
+ //# sourceMappingURL=browserCanvasMaxSize.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/utils/browserCanvasMaxSize.ts"],
4
+ "sourcesContent": ["import canvasSize from 'canvas-size'\n\n/** @internal */\nexport interface CanvasMaxSize {\n\tmaxWidth: number\n\tmaxHeight: number\n\tmaxArea: number\n}\n\nlet maxSizePromise: Promise<CanvasMaxSize> | null = null\n\nfunction getBrowserCanvasMaxSize() {\n\tif (!maxSizePromise) {\n\t\tmaxSizePromise = calculateBrowserCanvasMaxSize()\n\t}\n\n\treturn maxSizePromise\n}\n\nasync function calculateBrowserCanvasMaxSize(): Promise<CanvasMaxSize> {\n\tconst maxWidth = await canvasSize.maxWidth({ usePromise: true })\n\tconst maxHeight = await canvasSize.maxHeight({ usePromise: true })\n\tconst maxArea = await canvasSize.maxArea({ usePromise: true })\n\treturn {\n\t\tmaxWidth: maxWidth.width,\n\t\tmaxHeight: maxHeight.height,\n\t\tmaxArea: maxArea.width * maxArea.height,\n\t}\n}\n\n// https://github.com/jhildenbiddle/canvas-size?tab=readme-ov-file#test-results\nconst MAX_SAFE_CANVAS_DIMENSION = 8192\nconst MAX_SAFE_CANVAS_AREA = 4096 * 4096\n\n/** @internal */\nexport async function clampToBrowserMaxCanvasSize(width: number, height: number) {\n\tif (\n\t\twidth <= MAX_SAFE_CANVAS_DIMENSION &&\n\t\theight <= MAX_SAFE_CANVAS_DIMENSION &&\n\t\twidth * height <= MAX_SAFE_CANVAS_AREA\n\t) {\n\t\treturn [width, height]\n\t}\n\n\tconst { maxWidth, maxHeight, maxArea } = await getBrowserCanvasMaxSize()\n\tconst aspectRatio = width / height\n\n\tif (width > maxWidth) {\n\t\twidth = maxWidth\n\t\theight = width / aspectRatio\n\t}\n\n\tif (height > maxHeight) {\n\t\theight = maxHeight\n\t\twidth = height * aspectRatio\n\t}\n\n\tif (width * height > maxArea) {\n\t\tconst ratio = Math.sqrt(maxArea / (width * height))\n\t\twidth *= ratio\n\t\theight *= ratio\n\t}\n\n\treturn [width, height]\n}\n"],
5
+ "mappings": "AAAA,OAAO,gBAAgB;AASvB,IAAI,iBAAgD;AAEpD,SAAS,0BAA0B;AAClC,MAAI,CAAC,gBAAgB;AACpB,qBAAiB,8BAA8B;AAAA,EAChD;AAEA,SAAO;AACR;AAEA,eAAe,gCAAwD;AACtE,QAAM,WAAW,MAAM,WAAW,SAAS,EAAE,YAAY,KAAK,CAAC;AAC/D,QAAM,YAAY,MAAM,WAAW,UAAU,EAAE,YAAY,KAAK,CAAC;AACjE,QAAM,UAAU,MAAM,WAAW,QAAQ,EAAE,YAAY,KAAK,CAAC;AAC7D,SAAO;AAAA,IACN,UAAU,SAAS;AAAA,IACnB,WAAW,UAAU;AAAA,IACrB,SAAS,QAAQ,QAAQ,QAAQ;AAAA,EAClC;AACD;AAGA,MAAM,4BAA4B;AAClC,MAAM,uBAAuB,OAAO;AAGpC,eAAsB,4BAA4B,OAAe,QAAgB;AAChF,MACC,SAAS,6BACT,UAAU,6BACV,QAAQ,UAAU,sBACjB;AACD,WAAO,CAAC,OAAO,MAAM;AAAA,EACtB;AAEA,QAAM,EAAE,UAAU,WAAW,QAAQ,IAAI,MAAM,wBAAwB;AACvE,QAAM,cAAc,QAAQ;AAE5B,MAAI,QAAQ,UAAU;AACrB,YAAQ;AACR,aAAS,QAAQ;AAAA,EAClB;AAEA,MAAI,SAAS,WAAW;AACvB,aAAS;AACT,YAAQ,SAAS;AAAA,EAClB;AAEA,MAAI,QAAQ,SAAS,SAAS;AAC7B,UAAM,QAAQ,KAAK,KAAK,WAAW,QAAQ,OAAO;AAClD,aAAS;AACT,cAAU;AAAA,EACX;AAEA,SAAO,CAAC,OAAO,MAAM;AACtB;",
6
+ "names": []
7
+ }
@@ -1,8 +1,8 @@
1
- const version = "3.8.0-canary.962422478a23";
1
+ const version = "3.8.0-canary.98976dbbb3dd";
2
2
  const publishDates = {
3
3
  major: "2024-09-13T14:36:29.063Z",
4
- minor: "2025-01-20T14:03:06.643Z",
5
- patch: "2025-01-20T14:03:06.643Z"
4
+ minor: "2025-01-29T12:08:39.683Z",
5
+ patch: "2025-01-29T12:08:39.683Z"
6
6
  };
7
7
  export {
8
8
  publishDates,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '3.8.0-canary.962422478a23'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-01-20T14:03:06.643Z',\n\tpatch: '2025-01-20T14:03:06.643Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '3.8.0-canary.98976dbbb3dd'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-01-29T12:08:39.683Z',\n\tpatch: '2025-01-29T12:08:39.683Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/editor.css CHANGED
@@ -39,9 +39,10 @@
39
39
  --layer-overlays-user-brush: 50;
40
40
  --layer-overlays-user-indicator-selected: 60;
41
41
  --layer-overlays-user-indicator-hovered: 70;
42
- --layer-overlays-user-handles: 80;
43
42
  --layer-overlays-user-snapline: 90;
44
43
  --layer-overlays-selection-fg: 100;
44
+ /* User handles need to be above selection edges / corners, matters for sticky note clone handles */
45
+ --layer-overlays-user-handles: 105;
45
46
  --layer-overlays-user-indicator-hint: 110;
46
47
  --layer-overlays-collaborator-cursor-hint: 120;
47
48
  --layer-overlays-collaborator-cursor: 130;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "A tiny little drawing app (editor).",
4
- "version": "3.8.0-canary.962422478a23",
4
+ "version": "3.8.0-canary.98976dbbb3dd",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -45,14 +45,15 @@
45
45
  "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
46
46
  },
47
47
  "dependencies": {
48
- "@tldraw/state": "3.8.0-canary.962422478a23",
49
- "@tldraw/state-react": "3.8.0-canary.962422478a23",
50
- "@tldraw/store": "3.8.0-canary.962422478a23",
51
- "@tldraw/tlschema": "3.8.0-canary.962422478a23",
52
- "@tldraw/utils": "3.8.0-canary.962422478a23",
53
- "@tldraw/validate": "3.8.0-canary.962422478a23",
48
+ "@tldraw/state": "3.8.0-canary.98976dbbb3dd",
49
+ "@tldraw/state-react": "3.8.0-canary.98976dbbb3dd",
50
+ "@tldraw/store": "3.8.0-canary.98976dbbb3dd",
51
+ "@tldraw/tlschema": "3.8.0-canary.98976dbbb3dd",
52
+ "@tldraw/utils": "3.8.0-canary.98976dbbb3dd",
53
+ "@tldraw/validate": "3.8.0-canary.98976dbbb3dd",
54
54
  "@types/core-js": "^2.5.5",
55
55
  "@use-gesture/react": "^10.2.27",
56
+ "canvas-size": "~2.0.0",
56
57
  "classnames": "^2.3.2",
57
58
  "core-js": "^3.31.1",
58
59
  "eventemitter3": "^4.0.7",
@@ -69,6 +70,7 @@
69
70
  "@testing-library/jest-dom": "^5.16.5",
70
71
  "@testing-library/react": "^15.0.6",
71
72
  "@types/benchmark": "^2.1.2",
73
+ "@types/canvas-size": "^1.2.2",
72
74
  "@types/wicg-file-system-access": "^2020.9.5",
73
75
  "benchmark": "^2.1.4",
74
76
  "fake-indexeddb": "^4.0.0",
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export {
25
25
  useStateTracking,
26
26
  useValue,
27
27
  } from '@tldraw/state-react'
28
+ export { resizeScaled } from './lib/editor/shapes/shared/resizeScaled'
28
29
  export { LocalIndexedDb, Table, type StoreName } from './lib/utils/sync/LocalIndexedDb'
29
30
  // eslint-disable-next-line local/no-export-star
30
31
  export * from '@tldraw/store'
@@ -182,6 +183,7 @@ export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesMan
182
183
  export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseBoxShapeUtil'
183
184
  export {
184
185
  ShapeUtil,
186
+ type TLCropInfo,
185
187
  type TLHandleDragInfo,
186
188
  type TLResizeInfo,
187
189
  type TLResizeMode,
@@ -254,10 +256,13 @@ export {
254
256
  type TLCameraConstraints,
255
257
  type TLCameraMoveOptions,
256
258
  type TLCameraOptions,
259
+ type TLExportType,
257
260
  type TLImageExportOptions,
261
+ type TLSvgExportOptions,
258
262
  type TLSvgOptions,
259
263
  } from './lib/editor/types/misc-types'
260
264
  export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types'
265
+ export { getSvgAsImage } from './lib/exports/getSvgAsImage'
261
266
  export { tlenv } from './lib/globals/environment'
262
267
  export { tlmenus } from './lib/globals/menus'
263
268
  export { tltime } from './lib/globals/time'
@@ -381,6 +386,7 @@ export {
381
386
  type SharedStyle,
382
387
  } from './lib/utils/SharedStylesMap'
383
388
  export { dataUrlToFile, getDefaultCdnBaseUrl } from './lib/utils/assets'
389
+ export { clampToBrowserMaxCanvasSize, type CanvasMaxSize } from './lib/utils/browserCanvasMaxSize'
384
390
  export {
385
391
  debugFlags,
386
392
  featureFlags,
@@ -160,7 +160,6 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
160
160
  <div className="tl-overlays">
161
161
  <div ref={rHtmlLayer2} className="tl-html-layer">
162
162
  {debugGeometry ? <GeometryDebuggingView /> : null}
163
- <HandlesWrapper />
164
163
  <BrushWrapper />
165
164
  <ScribbleWrapper />
166
165
  <ZoomBrushWrapper />
@@ -168,6 +167,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
168
167
  <HintedShapeIndicator />
169
168
  <SnapIndicatorWrapper />
170
169
  <SelectionForegroundWrapper />
170
+ <HandlesWrapper />
171
171
  <LiveCollaborators />
172
172
  </div>
173
173
  </div>
@@ -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
 
@@ -6750,6 +6775,8 @@ export class Editor extends EventEmitter<TLEventMap> {
6750
6775
  }
6751
6776
  }
6752
6777
 
6778
+ let didResize = false
6779
+
6753
6780
  if (util.onResize && util.canResize(initialShape)) {
6754
6781
  // get the model changes from the shape util
6755
6782
  const newPagePoint = this._scalePagePoint(
@@ -6788,24 +6815,30 @@ export class Editor extends EventEmitter<TLEventMap> {
6788
6815
  )
6789
6816
  }
6790
6817
 
6818
+ const resizedShape = util.onResize(
6819
+ { ...initialShape, x, y },
6820
+ {
6821
+ newPoint: newLocalPoint,
6822
+ handle: opts.dragHandle ?? 'bottom_right',
6823
+ // don't set isSingle to true for children
6824
+ mode: opts.mode ?? 'scale_shape',
6825
+ scaleX: myScale.x,
6826
+ scaleY: myScale.y,
6827
+ initialBounds,
6828
+ initialShape,
6829
+ }
6830
+ )
6831
+
6832
+ if (resizedShape) {
6833
+ didResize = true
6834
+ }
6835
+
6791
6836
  workingShape = applyPartialToRecordWithProps(workingShape, {
6792
6837
  id,
6793
6838
  type: initialShape.type as any,
6794
6839
  x: newLocalPoint.x,
6795
6840
  y: newLocalPoint.y,
6796
- ...util.onResize(
6797
- { ...initialShape, x, y },
6798
- {
6799
- newPoint: newLocalPoint,
6800
- handle: opts.dragHandle ?? 'bottom_right',
6801
- // don't set isSingle to true for children
6802
- mode: opts.mode ?? 'scale_shape',
6803
- scaleX: myScale.x,
6804
- scaleY: myScale.y,
6805
- initialBounds,
6806
- initialShape,
6807
- }
6808
- ),
6841
+ ...resizedShape,
6809
6842
  })
6810
6843
 
6811
6844
  if (!opts.skipStartAndEndCallbacks) {
@@ -6816,7 +6849,11 @@ export class Editor extends EventEmitter<TLEventMap> {
6816
6849
  }
6817
6850
 
6818
6851
  this.updateShapes([workingShape])
6819
- } else {
6852
+ }
6853
+
6854
+ if (!didResize) {
6855
+ // reposition shape (rather than resizing it) based on where its resized center would be
6856
+
6820
6857
  const initialPageCenter = Mat.applyToPoint(pageTransform, initialBounds.center)
6821
6858
  // get the model changes from the shape util
6822
6859
  const newPageCenter = this._scalePagePoint(
@@ -8564,11 +8601,13 @@ export class Editor extends EventEmitter<TLEventMap> {
8564
8601
  *
8565
8602
  * @public
8566
8603
  */
8567
- async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8604
+ async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
8568
8605
  const ids =
8569
- typeof shapes[0] === 'string'
8570
- ? (shapes as TLShapeId[])
8571
- : (shapes as TLShape[]).map((s) => s.id)
8606
+ shapes.length === 0
8607
+ ? this.getCurrentPageShapeIdsSorted()
8608
+ : typeof shapes[0] === 'string'
8609
+ ? (shapes as TLShapeId[])
8610
+ : (shapes as TLShape[]).map((s) => s.id)
8572
8611
 
8573
8612
  if (ids.length === 0) return undefined
8574
8613
 
@@ -8585,7 +8624,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8585
8624
  *
8586
8625
  * @public
8587
8626
  */
8588
- async getSvgString(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8627
+ async getSvgString(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
8589
8628
  const result = await this.getSvgElement(shapes, opts)
8590
8629
  if (!result) return undefined
8591
8630
 
@@ -8598,12 +8637,63 @@ export class Editor extends EventEmitter<TLEventMap> {
8598
8637
  }
8599
8638
 
8600
8639
  /** @deprecated Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead. */
8601
- async getSvg(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8640
+ async getSvg(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
8602
8641
  const result = await this.getSvgElement(shapes, opts)
8603
8642
  if (!result) return undefined
8604
8643
  return result.svg
8605
8644
  }
8606
8645
 
8646
+ /**
8647
+ * Get an exported image of the given shapes.
8648
+ *
8649
+ * @param shapes - The shapes (or shape ids) to export.
8650
+ * @param opts - Options for the export.
8651
+ *
8652
+ * @returns A blob of the image.
8653
+ * @public
8654
+ */
8655
+ async toImage(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8656
+ const withDefaults = {
8657
+ format: 'png',
8658
+ scale: 1,
8659
+ pixelRatio: opts.format === 'svg' ? undefined : 2,
8660
+ ...opts,
8661
+ } satisfies TLImageExportOptions
8662
+ const result = await this.getSvgString(shapes, withDefaults)
8663
+ if (!result) throw new Error('Could not create SVG')
8664
+
8665
+ switch (withDefaults.format) {
8666
+ case 'svg':
8667
+ return {
8668
+ blob: new Blob([result.svg], { type: 'text/plain' }),
8669
+ width: result.width,
8670
+ height: result.height,
8671
+ }
8672
+ case 'jpeg':
8673
+ case 'png':
8674
+ case 'webp': {
8675
+ const blob = await getSvgAsImage(result.svg, {
8676
+ type: withDefaults.format,
8677
+ quality: withDefaults.quality,
8678
+ pixelRatio: withDefaults.pixelRatio,
8679
+ width: result.width,
8680
+ height: result.height,
8681
+ })
8682
+ if (!blob) {
8683
+ throw new Error('Could not construct image.')
8684
+ }
8685
+ return {
8686
+ blob,
8687
+ width: result.width,
8688
+ height: result.height,
8689
+ }
8690
+ }
8691
+ default: {
8692
+ exhaustiveSwitchError(withDefaults.format)
8693
+ }
8694
+ }
8695
+ }
8696
+
8607
8697
  /* --------------------- Events --------------------- */
8608
8698
 
8609
8699
  /**
@@ -230,6 +230,7 @@ export class TextManager {
230
230
  elm.style.setProperty('font-weight', opts.fontWeight)
231
231
  elm.style.setProperty('line-height', `${opts.lineHeight * opts.fontSize}px`)
232
232
  elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
233
+ elm.style.setProperty('font-style', opts.fontStyle)
233
234
 
234
235
  const shouldTruncateToFirstLine =
235
236
  opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
@@ -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
  *
@@ -0,0 +1,61 @@
1
+ import { TLBaseShape } from '@tldraw/tlschema'
2
+ import { exhaustiveSwitchError } from '@tldraw/utils'
3
+ import { Vec } from '../../../primitives/Vec'
4
+ import { TLResizeInfo } from '../ShapeUtil'
5
+
6
+ /**
7
+ * Resize a shape that has a scale prop.
8
+ *
9
+ * @param shape - The shape to resize
10
+ * @param info - The resize info
11
+ *
12
+ * @public */
13
+ export function resizeScaled(
14
+ shape: TLBaseShape<any, { scale: number }>,
15
+ { initialBounds, scaleX, scaleY, newPoint, handle }: TLResizeInfo<any>
16
+ ) {
17
+ let scaleDelta: number
18
+ switch (handle) {
19
+ case 'bottom_left':
20
+ case 'bottom_right':
21
+ case 'top_left':
22
+ case 'top_right': {
23
+ scaleDelta = Math.max(0.01, Math.max(Math.abs(scaleX), Math.abs(scaleY)))
24
+ break
25
+ }
26
+ case 'left':
27
+ case 'right': {
28
+ scaleDelta = Math.max(0.01, Math.abs(scaleX))
29
+ break
30
+ }
31
+ case 'bottom':
32
+ case 'top': {
33
+ scaleDelta = Math.max(0.01, Math.abs(scaleY))
34
+ break
35
+ }
36
+ default: {
37
+ throw exhaustiveSwitchError(handle)
38
+ }
39
+ }
40
+
41
+ // Compute the offset (if flipped X or flipped Y)
42
+ const offset = new Vec(0, 0)
43
+
44
+ if (scaleX < 0) {
45
+ offset.x = -(initialBounds.width * scaleDelta)
46
+ }
47
+ if (scaleY < 0) {
48
+ offset.y = -(initialBounds.height * scaleDelta)
49
+ }
50
+
51
+ // Apply the offset to the new point
52
+ const { x, y } = Vec.Add(newPoint, offset.rot(shape.rotation))
53
+
54
+ return {
55
+ x,
56
+ y,
57
+ props: {
58
+ scale: scaleDelta * shape.props.scale,
59
+ },
60
+ }
61
+ }
@@ -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)