@tldraw/editor 3.8.0-canary.888d6f3ea97f → 3.8.0-canary.8c96638cdde8

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 (98) hide show
  1. package/dist-cjs/index.d.ts +257 -60
  2. package/dist-cjs/index.js +14 -8
  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/TLSessionStateSnapshot.js.map +2 -2
  7. package/dist-cjs/lib/config/createTLStore.js +4 -2
  8. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  9. package/dist-cjs/lib/editor/Editor.js +98 -23
  10. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  11. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
  12. package/dist-cjs/lib/editor/managers/TextManager.js +1 -0
  13. package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
  14. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  15. package/dist-cjs/lib/editor/shapes/shared/resizeScaled.js +66 -0
  16. package/dist-cjs/lib/editor/shapes/shared/resizeScaled.js.map +7 -0
  17. package/dist-cjs/lib/editor/types/SvgExportContext.js.map +2 -2
  18. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  19. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  20. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  21. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  22. package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
  23. package/dist-cjs/lib/exports/getSvgAsImage.js +83 -0
  24. package/dist-cjs/lib/exports/getSvgAsImage.js.map +7 -0
  25. package/dist-cjs/lib/exports/getSvgJsx.js +16 -3
  26. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  27. package/dist-cjs/lib/hooks/useLocalStore.js +1 -1
  28. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  29. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -0
  30. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +3 -3
  31. package/dist-cjs/lib/options.js +3 -1
  32. package/dist-cjs/lib/options.js.map +2 -2
  33. package/dist-cjs/lib/utils/browserCanvasMaxSize.js +75 -0
  34. package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +7 -0
  35. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +3 -1
  36. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  37. package/dist-cjs/version.js +3 -3
  38. package/dist-cjs/version.js.map +1 -1
  39. package/dist-esm/index.d.mts +257 -60
  40. package/dist-esm/index.mjs +7 -1
  41. package/dist-esm/index.mjs.map +2 -2
  42. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +2 -5
  43. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  44. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
  45. package/dist-esm/lib/config/createTLStore.mjs +4 -2
  46. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  47. package/dist-esm/lib/editor/Editor.mjs +98 -23
  48. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  49. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
  50. package/dist-esm/lib/editor/managers/TextManager.mjs +1 -0
  51. package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
  52. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  53. package/dist-esm/lib/editor/shapes/shared/resizeScaled.mjs +46 -0
  54. package/dist-esm/lib/editor/shapes/shared/resizeScaled.mjs.map +7 -0
  55. package/dist-esm/lib/editor/types/SvgExportContext.mjs.map +2 -2
  56. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  57. package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
  58. package/dist-esm/lib/exports/getSvgAsImage.mjs +63 -0
  59. package/dist-esm/lib/exports/getSvgAsImage.mjs.map +7 -0
  60. package/dist-esm/lib/exports/getSvgJsx.mjs +16 -3
  61. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  62. package/dist-esm/lib/hooks/useLocalStore.mjs +1 -1
  63. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  64. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -0
  65. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +3 -3
  66. package/dist-esm/lib/options.mjs +3 -1
  67. package/dist-esm/lib/options.mjs.map +2 -2
  68. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs +45 -0
  69. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +7 -0
  70. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +3 -1
  71. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  72. package/dist-esm/version.mjs +3 -3
  73. package/dist-esm/version.mjs.map +1 -1
  74. package/editor.css +2 -1
  75. package/package.json +22 -20
  76. package/src/index.ts +19 -1
  77. package/src/lib/components/default-components/DefaultCanvas.tsx +2 -5
  78. package/src/lib/config/TLSessionStateSnapshot.ts +3 -1
  79. package/src/lib/config/createTLStore.ts +4 -2
  80. package/src/lib/editor/Editor.ts +147 -57
  81. package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +4 -4
  82. package/src/lib/editor/managers/TextManager.ts +1 -0
  83. package/src/lib/editor/shapes/ShapeUtil.ts +30 -1
  84. package/src/lib/editor/shapes/shared/resizeScaled.ts +61 -0
  85. package/src/lib/editor/types/SvgExportContext.tsx +21 -0
  86. package/src/lib/editor/types/emit-types.ts +1 -0
  87. package/src/lib/editor/types/external-content.ts +90 -50
  88. package/src/lib/editor/types/misc-types.ts +55 -2
  89. package/src/lib/exports/StyleEmbedder.ts +1 -1
  90. package/src/lib/exports/exportToSvg.tsx +2 -2
  91. package/src/lib/exports/getSvgAsImage.ts +92 -0
  92. package/src/lib/exports/getSvgJsx.tsx +17 -2
  93. package/src/lib/hooks/useLocalStore.ts +1 -1
  94. package/src/lib/hooks/usePassThroughWheelEvents.ts +7 -0
  95. package/src/lib/options.ts +11 -0
  96. package/src/lib/utils/browserCanvasMaxSize.ts +65 -0
  97. package/src/lib/utils/sync/TLLocalSyncClient.ts +3 -1
  98. package/src/version.ts +3 -3
@@ -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'
@@ -154,7 +155,7 @@ import {
154
155
  TLPointerEventInfo,
155
156
  TLWheelEventInfo,
156
157
  } from './types/event-types'
157
- import { TLExternalAssetContent, TLExternalContent } from './types/external-content'
158
+ import { TLExternalAsset, TLExternalContent } from './types/external-content'
158
159
  import { TLHistoryBatchOptions } from './types/history-types'
159
160
  import {
160
161
  OptionalKeys,
@@ -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.
@@ -1218,18 +1235,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1218
1235
  }
1219
1236
 
1220
1237
  /** @internal */
1221
- createErrorAnnotations(
1222
- origin: string,
1223
- willCrashApp: boolean | 'unknown'
1224
- ): {
1225
- tags: { origin: string; willCrashApp: boolean | 'unknown' }
1226
- extras: {
1227
- activeStateNode?: string
1228
- selectedShapes?: TLUnknownShape[]
1229
- editingShape?: TLUnknownShape
1230
- inputs?: Record<string, unknown>
1231
- }
1232
- } {
1238
+ createErrorAnnotations(origin: string, willCrashApp: boolean | 'unknown') {
1233
1239
  try {
1234
1240
  const editingShapeId = this.getEditingShapeId()
1235
1241
  return {
@@ -1239,9 +1245,20 @@ export class Editor extends EventEmitter<TLEventMap> {
1239
1245
  },
1240
1246
  extras: {
1241
1247
  activeStateNode: this.root.getPath(),
1242
- selectedShapes: this.getSelectedShapes(),
1248
+ selectedShapes: this.getSelectedShapes().map((s) => {
1249
+ const { props, ...rest } = s
1250
+ const { text: _text, richText: _richText, ...restProps } = props as any
1251
+ return {
1252
+ ...rest,
1253
+ props: restProps,
1254
+ }
1255
+ }),
1256
+ selectionCount: this.getSelectedShapes().length,
1243
1257
  editingShape: editingShapeId ? this.getShape(editingShapeId) : undefined,
1244
1258
  inputs: this.inputs,
1259
+ pageState: this.getCurrentPageState(),
1260
+ instanceState: this.getInstanceState(),
1261
+ collaboratorCount: this.getCollaboratorsOnCurrentPage().length,
1245
1262
  },
1246
1263
  }
1247
1264
  } catch {
@@ -1383,8 +1400,8 @@ export class Editor extends EventEmitter<TLEventMap> {
1383
1400
  *
1384
1401
  * @example
1385
1402
  * ```ts
1386
- * state.getStateDescendant('select')
1387
- * state.getStateDescendant('select.brushing')
1403
+ * editor.getStateDescendant('select')
1404
+ * editor.getStateDescendant('select.brushing')
1388
1405
  * ```
1389
1406
  *
1390
1407
  * @param path - The descendant's path of state ids, separated by periods.
@@ -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
 
@@ -1664,7 +1681,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1664
1681
  * @public
1665
1682
  */
1666
1683
  isAncestorSelected(shape: TLShape | TLShapeId): boolean {
1667
- const id = typeof shape === 'string' ? shape : shape?.id ?? null
1684
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
1668
1685
  const _shape = this.getShape(id)
1669
1686
  if (!_shape) return false
1670
1687
  const selectedShapeIds = this.getSelectedShapeIds()
@@ -1921,7 +1938,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1921
1938
  * @public
1922
1939
  */
1923
1940
  setFocusedGroup(shape: TLShapeId | TLGroupShape | null): this {
1924
- const id = typeof shape === 'string' ? shape : shape?.id ?? null
1941
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
1925
1942
 
1926
1943
  if (id !== null) {
1927
1944
  const shape = this.getShape(id)
@@ -2004,7 +2021,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2004
2021
  * @public
2005
2022
  */
2006
2023
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2007
- const id = typeof shape === 'string' ? shape : shape?.id ?? null
2024
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2008
2025
  if (id !== this.getEditingShapeId()) {
2009
2026
  if (id) {
2010
2027
  const shape = this.getShape(id)
@@ -2065,7 +2082,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2065
2082
  * @public
2066
2083
  */
2067
2084
  setHoveredShape(shape: TLShapeId | TLShape | null): this {
2068
- const id = typeof shape === 'string' ? shape : shape?.id ?? null
2085
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2069
2086
  if (id === this.getHoveredShapeId()) return this
2070
2087
  this.run(
2071
2088
  () => {
@@ -2214,7 +2231,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2214
2231
  * @public
2215
2232
  */
2216
2233
  setCroppingShape(shape: TLShapeId | TLShape | null): this {
2217
- const id = typeof shape === 'string' ? shape : shape?.id ?? null
2234
+ const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2218
2235
  if (id !== this.getCroppingShapeId()) {
2219
2236
  this.run(
2220
2237
  () => {
@@ -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(
@@ -7938,10 +7975,8 @@ export class Editor extends EventEmitter<TLEventMap> {
7938
7975
 
7939
7976
  /** @internal */
7940
7977
  externalAssetContentHandlers: {
7941
- [K in TLExternalAssetContent['type']]: {
7942
- [Key in K]:
7943
- | null
7944
- | ((info: TLExternalAssetContent & { type: Key }) => Promise<TLAsset | undefined>)
7978
+ [K in TLExternalAsset['type']]: {
7979
+ [Key in K]: null | ((info: TLExternalAsset & { type: Key }) => Promise<TLAsset | undefined>)
7945
7980
  }[K]
7946
7981
  } = {
7947
7982
  file: null,
@@ -7970,9 +8005,9 @@ export class Editor extends EventEmitter<TLEventMap> {
7970
8005
  *
7971
8006
  * @public
7972
8007
  */
7973
- registerExternalAssetHandler<T extends TLExternalAssetContent['type']>(
8008
+ registerExternalAssetHandler<T extends TLExternalAsset['type']>(
7974
8009
  type: T,
7975
- handler: null | ((info: TLExternalAssetContent & { type: T }) => Promise<TLAsset>)
8010
+ handler: null | ((info: TLExternalAsset & { type: T }) => Promise<TLAsset>)
7976
8011
  ): this {
7977
8012
  this.externalAssetContentHandlers[type] = handler as any
7978
8013
  return this
@@ -8040,11 +8075,11 @@ export class Editor extends EventEmitter<TLEventMap> {
8040
8075
  * @param info - Info about the external content.
8041
8076
  * @returns The asset.
8042
8077
  */
8043
- async getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined> {
8078
+ async getAssetForExternalContent(info: TLExternalAsset): Promise<TLAsset | undefined> {
8044
8079
  return await this.externalAssetContentHandlers[info.type]?.(info as any)
8045
8080
  }
8046
8081
 
8047
- hasExternalAssetHandler(type: TLExternalAssetContent['type']): boolean {
8082
+ hasExternalAssetHandler(type: TLExternalAsset['type']): boolean {
8048
8083
  return !!this.externalAssetContentHandlers[type]
8049
8084
  }
8050
8085
 
@@ -8564,11 +8599,13 @@ export class Editor extends EventEmitter<TLEventMap> {
8564
8599
  *
8565
8600
  * @public
8566
8601
  */
8567
- async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8602
+ async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
8568
8603
  const ids =
8569
- typeof shapes[0] === 'string'
8570
- ? (shapes as TLShapeId[])
8571
- : (shapes as TLShape[]).map((s) => s.id)
8604
+ shapes.length === 0
8605
+ ? this.getCurrentPageShapeIdsSorted()
8606
+ : typeof shapes[0] === 'string'
8607
+ ? (shapes as TLShapeId[])
8608
+ : (shapes as TLShape[]).map((s) => s.id)
8572
8609
 
8573
8610
  if (ids.length === 0) return undefined
8574
8611
 
@@ -8585,7 +8622,7 @@ export class Editor extends EventEmitter<TLEventMap> {
8585
8622
  *
8586
8623
  * @public
8587
8624
  */
8588
- async getSvgString(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8625
+ async getSvgString(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
8589
8626
  const result = await this.getSvgElement(shapes, opts)
8590
8627
  if (!result) return undefined
8591
8628
 
@@ -8598,12 +8635,63 @@ export class Editor extends EventEmitter<TLEventMap> {
8598
8635
  }
8599
8636
 
8600
8637
  /** @deprecated Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead. */
8601
- async getSvg(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8638
+ async getSvg(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) {
8602
8639
  const result = await this.getSvgElement(shapes, opts)
8603
8640
  if (!result) return undefined
8604
8641
  return result.svg
8605
8642
  }
8606
8643
 
8644
+ /**
8645
+ * Get an exported image of the given shapes.
8646
+ *
8647
+ * @param shapes - The shapes (or shape ids) to export.
8648
+ * @param opts - Options for the export.
8649
+ *
8650
+ * @returns A blob of the image.
8651
+ * @public
8652
+ */
8653
+ async toImage(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
8654
+ const withDefaults = {
8655
+ format: 'png',
8656
+ scale: 1,
8657
+ pixelRatio: opts.format === 'svg' ? undefined : 2,
8658
+ ...opts,
8659
+ } satisfies TLImageExportOptions
8660
+ const result = await this.getSvgString(shapes, withDefaults)
8661
+ if (!result) throw new Error('Could not create SVG')
8662
+
8663
+ switch (withDefaults.format) {
8664
+ case 'svg':
8665
+ return {
8666
+ blob: new Blob([result.svg], { type: 'text/plain' }),
8667
+ width: result.width,
8668
+ height: result.height,
8669
+ }
8670
+ case 'jpeg':
8671
+ case 'png':
8672
+ case 'webp': {
8673
+ const blob = await getSvgAsImage(result.svg, {
8674
+ type: withDefaults.format,
8675
+ quality: withDefaults.quality,
8676
+ pixelRatio: withDefaults.pixelRatio,
8677
+ width: result.width,
8678
+ height: result.height,
8679
+ })
8680
+ if (!blob) {
8681
+ throw new Error('Could not construct image.')
8682
+ }
8683
+ return {
8684
+ blob,
8685
+ width: result.width,
8686
+ height: result.height,
8687
+ }
8688
+ }
8689
+ default: {
8690
+ exhaustiveSwitchError(withDefaults.format)
8691
+ }
8692
+ }
8693
+ }
8694
+
8607
8695
  /* --------------------- Events --------------------- */
8608
8696
 
8609
8697
  /**
@@ -8713,8 +8801,8 @@ export class Editor extends EventEmitter<TLEventMap> {
8713
8801
  // If our pointer moved only because we're following some other user, then don't
8714
8802
  // update our last activity timestamp; otherwise, update it to the current timestamp.
8715
8803
  info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
8716
- ? this.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ??
8717
- this._tickManager.now
8804
+ ? (this.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ??
8805
+ this._tickManager.now)
8718
8806
  : this._tickManager.now,
8719
8807
  meta: {},
8720
8808
  },
@@ -9279,6 +9367,8 @@ export class Editor extends EventEmitter<TLEventMap> {
9279
9367
  // todo: replace with new readonly mode?
9280
9368
  if (this.getCrashingError()) return this
9281
9369
 
9370
+ this.emit('before-event', info)
9371
+
9282
9372
  const { inputs } = this
9283
9373
  const { type } = info
9284
9374
 
@@ -390,8 +390,8 @@ export class BoundsSnaps {
390
390
 
391
391
  // at the same time, calculate how far we need to nudge the shape to 'snap' to the target point(s)
392
392
  const nudge = new Vec(
393
- lockedAxis === 'x' ? 0 : nearestSnapsX[0]?.nudge ?? 0,
394
- lockedAxis === 'y' ? 0 : nearestSnapsY[0]?.nudge ?? 0
393
+ lockedAxis === 'x' ? 0 : (nearestSnapsX[0]?.nudge ?? 0),
394
+ lockedAxis === 'y' ? 0 : (nearestSnapsY[0]?.nudge ?? 0)
395
395
  )
396
396
 
397
397
  // ok we've figured out how much the box should be nudged, now let's find all the snap points
@@ -504,8 +504,8 @@ export class BoundsSnaps {
504
504
 
505
505
  // at the same time, calculate how far we need to nudge the shape to 'snap' to the target point(s)
506
506
  const nudge = new Vec(
507
- isXLocked ? 0 : nearestSnapsX[0]?.nudge ?? 0,
508
- isYLocked ? 0 : nearestSnapsY[0]?.nudge ?? 0
507
+ isXLocked ? 0 : (nearestSnapsX[0]?.nudge ?? 0),
508
+ isYLocked ? 0 : (nearestSnapsY[0]?.nudge ?? 0)
509
509
  )
510
510
 
511
511
  if (isAspectRatioLocked && isSelectionCorner(handle) && nudge.len() !== 0) {
@@ -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)
@@ -12,6 +12,7 @@ export interface TLEventMap {
12
12
  crash: [{ error: unknown }]
13
13
  'stop-camera-animation': []
14
14
  'stop-following': []
15
+ 'before-event': [TLEventInfo]
15
16
  event: [TLEventInfo]
16
17
  tick: [number]
17
18
  frame: [number]