@tldraw/editor 3.13.0-canary.6cb770c78958 → 3.13.0-canary.70e156551c5c

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 (134) hide show
  1. package/dist-cjs/index.d.ts +84 -97
  2. package/dist-cjs/index.js +7 -22
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/Shape.js +12 -8
  5. package/dist-cjs/lib/components/Shape.js.map +2 -2
  6. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +21 -2
  7. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +17 -11
  9. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  10. package/dist-cjs/lib/editor/Editor.js +33 -14
  11. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  12. package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
  13. package/dist-cjs/lib/editor/managers/TextManager.js +10 -0
  14. package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
  15. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  17. package/dist-cjs/lib/exports/getSvgJsx.js +12 -3
  18. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  19. package/dist-cjs/lib/primitives/Box.js +16 -0
  20. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  21. package/dist-cjs/lib/primitives/Mat.js +1 -1
  22. package/dist-cjs/lib/primitives/Mat.js.map +2 -2
  23. package/dist-cjs/lib/primitives/Vec.js +20 -0
  24. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  25. package/dist-cjs/lib/primitives/geometry/Arc2d.js +2 -2
  26. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  27. package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
  28. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  29. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
  30. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  31. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
  32. package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
  33. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  34. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  35. package/dist-cjs/lib/primitives/geometry/Geometry2d.js +91 -20
  36. package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
  37. package/dist-cjs/lib/primitives/geometry/Group2d.js +55 -2
  38. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  39. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  40. package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
  41. package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
  42. package/dist-cjs/lib/utils/areShapesContentEqual.js +25 -0
  43. package/dist-cjs/lib/utils/areShapesContentEqual.js.map +7 -0
  44. package/dist-cjs/lib/utils/debug-flags.js +5 -2
  45. package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
  46. package/dist-cjs/lib/utils/nearestMultiple.js +34 -0
  47. package/dist-cjs/lib/utils/nearestMultiple.js.map +7 -0
  48. package/dist-cjs/version.js +3 -3
  49. package/dist-cjs/version.js.map +1 -1
  50. package/dist-esm/index.d.mts +84 -97
  51. package/dist-esm/index.mjs +9 -41
  52. package/dist-esm/index.mjs.map +2 -2
  53. package/dist-esm/lib/components/Shape.mjs +12 -8
  54. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  55. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +21 -2
  56. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  57. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +17 -11
  58. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  59. package/dist-esm/lib/editor/Editor.mjs +33 -14
  60. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  61. package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
  62. package/dist-esm/lib/editor/managers/TextManager.mjs +10 -0
  63. package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
  64. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  65. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  66. package/dist-esm/lib/exports/getSvgJsx.mjs +12 -3
  67. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  68. package/dist-esm/lib/primitives/Box.mjs +16 -0
  69. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  70. package/dist-esm/lib/primitives/Mat.mjs +1 -1
  71. package/dist-esm/lib/primitives/Mat.mjs.map +2 -2
  72. package/dist-esm/lib/primitives/Vec.mjs +20 -0
  73. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  74. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  75. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  76. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +1 -1
  77. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  78. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
  79. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  80. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
  81. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
  82. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  83. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  84. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +92 -21
  85. package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
  86. package/dist-esm/lib/primitives/geometry/Group2d.mjs +55 -2
  87. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  88. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  89. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
  90. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
  91. package/dist-esm/lib/utils/areShapesContentEqual.mjs +5 -0
  92. package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +7 -0
  93. package/dist-esm/lib/utils/debug-flags.mjs +5 -2
  94. package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
  95. package/dist-esm/lib/utils/nearestMultiple.mjs +14 -0
  96. package/dist-esm/lib/utils/nearestMultiple.mjs.map +7 -0
  97. package/dist-esm/version.mjs +3 -3
  98. package/dist-esm/version.mjs.map +1 -1
  99. package/editor.css +29 -0
  100. package/package.json +7 -7
  101. package/src/index.ts +16 -31
  102. package/src/lib/components/Shape.tsx +14 -10
  103. package/src/lib/components/default-components/DefaultCanvas.tsx +21 -2
  104. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +17 -8
  105. package/src/lib/editor/Editor.test.ts +1 -1
  106. package/src/lib/editor/Editor.ts +33 -14
  107. package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +0 -1
  108. package/src/lib/editor/managers/TextManager.ts +12 -0
  109. package/src/lib/editor/shapes/ShapeUtil.ts +9 -1
  110. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +9 -9
  111. package/src/lib/exports/getSvgJsx.tsx +16 -7
  112. package/src/lib/primitives/Box.ts +20 -0
  113. package/src/lib/primitives/Mat.ts +5 -4
  114. package/src/lib/primitives/Vec.ts +23 -0
  115. package/src/lib/primitives/geometry/Arc2d.ts +5 -5
  116. package/src/lib/primitives/geometry/Circle2d.ts +4 -4
  117. package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -4
  118. package/src/lib/primitives/geometry/CubicSpline2d.ts +3 -3
  119. package/src/lib/primitives/geometry/Edge2d.ts +3 -3
  120. package/src/lib/primitives/geometry/Ellipse2d.ts +3 -3
  121. package/src/lib/primitives/geometry/Geometry2d.test.ts +42 -0
  122. package/src/lib/primitives/geometry/Geometry2d.ts +123 -35
  123. package/src/lib/primitives/geometry/Group2d.ts +70 -7
  124. package/src/lib/primitives/geometry/Point2d.ts +2 -2
  125. package/src/lib/primitives/geometry/Polyline2d.ts +3 -3
  126. package/src/lib/primitives/geometry/Stadium2d.ts +3 -3
  127. package/src/lib/test/currentToolIdMask.test.ts +1 -1
  128. package/src/lib/test/user.test.ts +1 -1
  129. package/src/lib/utils/areShapesContentEqual.ts +4 -0
  130. package/src/lib/utils/debug-flags.ts +7 -2
  131. package/src/lib/utils/nearestMultiple.ts +13 -0
  132. package/src/lib/utils/sync/LocalIndexedDb.test.ts +1 -1
  133. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +1 -1
  134. package/src/version.ts +3 -3
@@ -0,0 +1,14 @@
1
+ function gcd(a, b) {
2
+ return b === 0 ? a : gcd(b, a % b);
3
+ }
4
+ function nearestMultiple(float) {
5
+ const decimal = float.toString().split(".")[1];
6
+ if (!decimal) return 1;
7
+ const denominator = Math.pow(10, decimal.length);
8
+ const numerator = parseInt(decimal, 10);
9
+ return denominator / gcd(numerator, denominator);
10
+ }
11
+ export {
12
+ nearestMultiple
13
+ };
14
+ //# sourceMappingURL=nearestMultiple.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/utils/nearestMultiple.ts"],
4
+ "sourcesContent": ["// Euclidean algorithm to find the GCD\nfunction gcd(a: number, b: number): number {\n\treturn b === 0 ? a : gcd(b, a % b)\n}\n\n// Returns the lowest value that the given number can be multiplied by to reach an integer\nexport function nearestMultiple(float: number) {\n\tconst decimal = float.toString().split('.')[1]\n\tif (!decimal) return 1\n\tconst denominator = Math.pow(10, decimal.length)\n\tconst numerator = parseInt(decimal, 10)\n\treturn denominator / gcd(numerator, denominator)\n}\n"],
5
+ "mappings": "AACA,SAAS,IAAI,GAAW,GAAmB;AAC1C,SAAO,MAAM,IAAI,IAAI,IAAI,GAAG,IAAI,CAAC;AAClC;AAGO,SAAS,gBAAgB,OAAe;AAC9C,QAAM,UAAU,MAAM,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC;AAC7C,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,cAAc,KAAK,IAAI,IAAI,QAAQ,MAAM;AAC/C,QAAM,YAAY,SAAS,SAAS,EAAE;AACtC,SAAO,cAAc,IAAI,WAAW,WAAW;AAChD;",
6
+ "names": []
7
+ }
@@ -1,8 +1,8 @@
1
- const version = "3.13.0-canary.6cb770c78958";
1
+ const version = "3.13.0-canary.70e156551c5c";
2
2
  const publishDates = {
3
3
  major: "2024-09-13T14:36:29.063Z",
4
- minor: "2025-04-30T12:29:15.138Z",
5
- patch: "2025-04-30T12:29:15.138Z"
4
+ minor: "2025-05-08T14:57:13.497Z",
5
+ patch: "2025-05-08T14:57:13.497Z"
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.13.0-canary.6cb770c78958'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-04-30T12:29:15.138Z',\n\tpatch: '2025-04-30T12:29:15.138Z',\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.13.0-canary.70e156551c5c'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-05-08T14:57:13.497Z',\n\tpatch: '2025-05-08T14:57:13.497Z',\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
@@ -600,6 +600,31 @@ input,
600
600
  }
601
601
  }
602
602
 
603
+ /* --------------------- Arrow Hints -------------------- */
604
+
605
+ .tl-arrow-hint-handle {
606
+ fill: var(--color-selected-contrast);
607
+ stroke: var(--color-selection-stroke);
608
+ stroke-width: calc(1.5px * var(--tl-scale));
609
+ r: calc(4px * var(--tl-scale));
610
+ }
611
+
612
+ .tl-arrow-hint-snap {
613
+ stroke: transparent;
614
+ fill: var(--color-selection-fill);
615
+ r: calc(12px * var(--tl-scale));
616
+ }
617
+
618
+ .tl-arrow-hint-snap__none,
619
+ .tl-arrow-hint-snap__center,
620
+ .tl-arrow-hint-snap__axis {
621
+ display: none;
622
+ }
623
+
624
+ .tl-arrow-hint-snap__edge {
625
+ r: calc(8px * var(--tl-scale));
626
+ }
627
+
603
628
  /* ------------------ Bounds Detail ----------------- */
604
629
 
605
630
  .tl-image,
@@ -969,6 +994,8 @@ input,
969
994
 
970
995
  .tl-rich-text p {
971
996
  margin: 0;
997
+ /* Depending on the extensions, <p> tags can be empty, without a <br />. */
998
+ min-height: 1lh;
972
999
  }
973
1000
 
974
1001
  .tl-rich-text ul,
@@ -976,6 +1003,8 @@ input,
976
1003
  text-align: left;
977
1004
  margin: 0;
978
1005
  padding-left: 3.25ch;
1006
+ /* Some resets, like Tailwind, nix the list styling. */
1007
+ list-style: revert;
979
1008
  }
980
1009
 
981
1010
  .tl-rich-text ol:has(> li:nth-child(10)) {
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.13.0-canary.6cb770c78958",
4
+ "version": "3.13.0-canary.70e156551c5c",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -48,12 +48,12 @@
48
48
  "@tiptap/core": "^2.9.1",
49
49
  "@tiptap/pm": "^2.9.1",
50
50
  "@tiptap/react": "^2.9.1",
51
- "@tldraw/state": "3.13.0-canary.6cb770c78958",
52
- "@tldraw/state-react": "3.13.0-canary.6cb770c78958",
53
- "@tldraw/store": "3.13.0-canary.6cb770c78958",
54
- "@tldraw/tlschema": "3.13.0-canary.6cb770c78958",
55
- "@tldraw/utils": "3.13.0-canary.6cb770c78958",
56
- "@tldraw/validate": "3.13.0-canary.6cb770c78958",
51
+ "@tldraw/state": "3.13.0-canary.70e156551c5c",
52
+ "@tldraw/state-react": "3.13.0-canary.70e156551c5c",
53
+ "@tldraw/store": "3.13.0-canary.70e156551c5c",
54
+ "@tldraw/tlschema": "3.13.0-canary.70e156551c5c",
55
+ "@tldraw/utils": "3.13.0-canary.70e156551c5c",
56
+ "@tldraw/validate": "3.13.0-canary.70e156551c5c",
57
57
  "@types/core-js": "^2.5.8",
58
58
  "@use-gesture/react": "^10.3.1",
59
59
  "classnames": "^2.5.1",
package/src/index.ts CHANGED
@@ -4,37 +4,11 @@ import 'core-js/stable/array/flat-map.js'
4
4
  import 'core-js/stable/array/flat.js'
5
5
  import 'core-js/stable/string/at.js'
6
6
  import 'core-js/stable/string/replace-all.js'
7
- export {
8
- EMPTY_ARRAY,
9
- EffectScheduler,
10
- atom,
11
- computed,
12
- react,
13
- transact,
14
- transaction,
15
- whyAmIRunning,
16
- type Atom,
17
- type Signal,
18
- } from '@tldraw/state'
19
- export {
20
- track,
21
- useAtom,
22
- useComputed,
23
- useQuickReactor,
24
- useReactor,
25
- useStateTracking,
26
- useValue,
27
- } from '@tldraw/state-react'
28
- export { resizeScaled } from './lib/editor/shapes/shared/resizeScaled'
29
- export {
30
- getFontsFromRichText,
31
- type RichTextFontVisitor,
32
- type RichTextFontVisitorState,
33
- type TLTextOptions,
34
- type TiptapEditor,
35
- type TiptapNode,
36
- } from './lib/utils/richText'
37
- export { LocalIndexedDb, Table, type StoreName } from './lib/utils/sync/LocalIndexedDb'
7
+
8
+ // eslint-disable-next-line local/no-export-star
9
+ export * from '@tldraw/state'
10
+ // eslint-disable-next-line local/no-export-star
11
+ export * from '@tldraw/state-react'
38
12
  // eslint-disable-next-line local/no-export-star
39
13
  export * from '@tldraw/store'
40
14
  // eslint-disable-next-line local/no-export-star
@@ -43,6 +17,7 @@ export * from '@tldraw/tlschema'
43
17
  export * from '@tldraw/utils'
44
18
  // eslint-disable-next-line local/no-export-star
45
19
  export * from '@tldraw/validate'
20
+
46
21
  export {
47
22
  ErrorScreen,
48
23
  LoadingScreen,
@@ -212,6 +187,7 @@ export {
212
187
  export { GroupShapeUtil } from './lib/editor/shapes/group/GroupShapeUtil'
213
188
  export { getPerfectDashProps } from './lib/editor/shapes/shared/getPerfectDashProps'
214
189
  export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox'
190
+ export { resizeScaled } from './lib/editor/shapes/shared/resizeScaled'
215
191
  export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
216
192
  export { maybeSnapToGrid } from './lib/editor/tools/BaseBoxShapeTool/children/Pointing'
217
193
  export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode'
@@ -459,12 +435,21 @@ export { hardResetEditor } from './lib/utils/hardResetEditor'
459
435
  export { isAccelKey } from './lib/utils/keyboard'
460
436
  export { normalizeWheel } from './lib/utils/normalizeWheel'
461
437
  export { refreshPage } from './lib/utils/refreshPage'
438
+ export {
439
+ getFontsFromRichText,
440
+ type RichTextFontVisitor,
441
+ type RichTextFontVisitorState,
442
+ type TLTextOptions,
443
+ type TiptapEditor,
444
+ type TiptapNode,
445
+ } from './lib/utils/richText'
462
446
  export {
463
447
  applyRotationToSnapshotShapes,
464
448
  getRotationSnapshot,
465
449
  type TLRotationSnapshot,
466
450
  } from './lib/utils/rotation'
467
451
  export { runtime, setRuntimeOverrides } from './lib/utils/runtime'
452
+ export { LocalIndexedDb, Table, type StoreName } from './lib/utils/sync/LocalIndexedDb'
468
453
  export { type TLStoreWithStatus } from './lib/utils/sync/StoreWithStatus'
469
454
  export { hardReset } from './lib/utils/sync/hardReset'
470
455
  export { uniq } from './lib/utils/uniq'
@@ -6,6 +6,7 @@ import { ShapeUtil } from '../editor/shapes/ShapeUtil'
6
6
  import { useEditor } from '../hooks/useEditor'
7
7
  import { useEditorComponents } from '../hooks/useEditorComponents'
8
8
  import { Mat } from '../primitives/Mat'
9
+ import { areShapesContentEqual } from '../utils/areShapesContentEqual'
9
10
  import { setStyleProperty } from '../utils/dom'
10
11
  import { OptionalErrorBoundary } from './ErrorBoundary'
11
12
 
@@ -27,6 +28,7 @@ export const Shape = memo(function Shape({
27
28
  index,
28
29
  backgroundIndex,
29
30
  opacity,
31
+ dprMultiple,
30
32
  }: {
31
33
  id: TLShapeId
32
34
  shape: TLShape
@@ -34,6 +36,7 @@ export const Shape = memo(function Shape({
34
36
  index: number
35
37
  backgroundIndex: number
36
38
  opacity: number
39
+ dprMultiple: number
37
40
  }) {
38
41
  const editor = useEditor()
39
42
 
@@ -88,14 +91,18 @@ export const Shape = memo(function Shape({
88
91
  }
89
92
 
90
93
  // Width / Height
91
- const width = Math.max(bounds.width, 1)
92
- const height = Math.max(bounds.height, 1)
94
+ // We round the shape width and height up to the nearest multiple of dprMultiple
95
+ // to avoid the browser making miscalculations when applying the transform.
96
+ const widthRemainder = bounds.w % dprMultiple
97
+ const heightRemainder = bounds.h % dprMultiple
98
+ const width = widthRemainder === 0 ? bounds.w : bounds.w + (dprMultiple - widthRemainder)
99
+ const height = heightRemainder === 0 ? bounds.h : bounds.h + (dprMultiple - heightRemainder)
93
100
 
94
101
  if (width !== prev.width || height !== prev.height) {
95
- setStyleProperty(containerRef.current, 'width', width + 'px')
96
- setStyleProperty(containerRef.current, 'height', height + 'px')
97
- setStyleProperty(bgContainerRef.current, 'width', width + 'px')
98
- setStyleProperty(bgContainerRef.current, 'height', height + 'px')
102
+ setStyleProperty(containerRef.current, 'width', Math.max(width, dprMultiple) + 'px')
103
+ setStyleProperty(containerRef.current, 'height', Math.max(height, dprMultiple) + 'px')
104
+ setStyleProperty(bgContainerRef.current, 'width', Math.max(width, dprMultiple) + 'px')
105
+ setStyleProperty(bgContainerRef.current, 'height', Math.max(height, dprMultiple) + 'px')
99
106
  prev.width = width
100
107
  prev.height = height
101
108
  }
@@ -184,10 +191,7 @@ export const InnerShape = memo(
184
191
  [util, shape.id]
185
192
  )
186
193
  },
187
- (prev, next) =>
188
- prev.shape.props === next.shape.props &&
189
- prev.shape.meta === next.shape.meta &&
190
- prev.util === next.util
194
+ (prev, next) => areShapesContentEqual(prev.shape, next.shape) && prev.util === next.util
191
195
  )
192
196
 
193
197
  export const InnerShapeBackground = memo(
@@ -22,6 +22,7 @@ import { Vec } from '../../primitives/Vec'
22
22
  import { toDomPrecision } from '../../primitives/utils'
23
23
  import { debugFlags } from '../../utils/debug-flags'
24
24
  import { setStyleProperty } from '../../utils/dom'
25
+ import { nearestMultiple } from '../../utils/nearestMultiple'
25
26
  import { GeometryDebuggingView } from '../GeometryDebuggingView'
26
27
  import { LiveCollaborators } from '../LiveCollaborators'
27
28
  import { MenuClickCapture } from '../MenuClickCapture'
@@ -388,9 +389,18 @@ function ShapesWithSVGs() {
388
389
 
389
390
  const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
390
391
 
392
+ const dprMultiple = useValue(
393
+ 'dpr multiple',
394
+ () =>
395
+ // dprMultiple is the smallest number we can multiply dpr by to get an integer
396
+ // it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively)
397
+ nearestMultiple(Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100),
398
+ [editor]
399
+ )
400
+
391
401
  return renderingShapes.map((result) => (
392
402
  <Fragment key={result.id + '_fragment'}>
393
- <Shape {...result} />
403
+ <Shape {...result} dprMultiple={dprMultiple} />
394
404
  <DebugSvgCopy id={result.id} mode="iframe" />
395
405
  </Fragment>
396
406
  ))
@@ -425,10 +435,19 @@ function ShapesToDisplay() {
425
435
 
426
436
  const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
427
437
 
438
+ const dprMultiple = useValue(
439
+ 'dpr multiple',
440
+ () =>
441
+ // dprMultiple is the smallest number we can multiply dpr by to get an integer
442
+ // it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively)
443
+ nearestMultiple(Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100),
444
+ [editor]
445
+ )
446
+
428
447
  return (
429
448
  <>
430
449
  {renderingShapes.map((result) => (
431
- <Shape key={result.id + '_shape'} {...result} />
450
+ <Shape key={result.id + '_shape'} {...result} dprMultiple={dprMultiple} />
432
451
  ))}
433
452
  {tlenv.isSafari && <ReflowIfNeeded />}
434
453
  </>
@@ -9,13 +9,21 @@ import { useEditorComponents } from '../../hooks/useEditorComponents'
9
9
  import { OptionalErrorBoundary } from '../ErrorBoundary'
10
10
 
11
11
  // need an extra layer of indirection here to allow hooks to be used inside the indicator render
12
- const EvenInnererIndicator = memo(({ shape, util }: { shape: TLShape; util: ShapeUtil<any> }) => {
13
- return useStateTracking('Indicator: ' + shape.type, () =>
14
- // always fetch the latest shape from the store even if the props/meta have not changed, to avoid
15
- // calling the render method with stale data.
16
- util.indicator(util.editor.store.unsafeGetWithoutCapture(shape.id) as TLShape)
17
- )
18
- })
12
+ const EvenInnererIndicator = memo(
13
+ ({ shape, util }: { shape: TLShape; util: ShapeUtil<any> }) => {
14
+ return useStateTracking('Indicator: ' + shape.type, () =>
15
+ // always fetch the latest shape from the store even if the props/meta have not changed, to avoid
16
+ // calling the render method with stale data.
17
+ util.indicator(util.editor.store.unsafeGetWithoutCapture(shape.id) as TLShape)
18
+ )
19
+ },
20
+ (prevProps, nextProps) => {
21
+ return (
22
+ prevProps.shape.props === nextProps.shape.props &&
23
+ prevProps.shape.meta === nextProps.shape.meta
24
+ )
25
+ }
26
+ )
19
27
 
20
28
  const InnerIndicator = memo(({ editor, id }: { editor: Editor; id: TLShapeId }) => {
21
29
  const shape = useValue('shape for indicator', () => editor.store.get(id), [editor, id])
@@ -61,13 +69,14 @@ export const DefaultShapeIndicator = memo(function DefaultShapeIndicator({
61
69
  useQuickReactor(
62
70
  'indicator transform',
63
71
  () => {
72
+ if (hidden) return
64
73
  const elm = rIndicator.current
65
74
  if (!elm) return
66
75
  const pageTransform = editor.getShapePageTransform(shapeId)
67
76
  if (!pageTransform) return
68
77
  elm.style.setProperty('transform', pageTransform.toCssString())
69
78
  },
70
- [editor, shapeId]
79
+ [editor, shapeId, hidden]
71
80
  )
72
81
 
73
82
  useLayoutEffect(() => {
@@ -52,7 +52,7 @@ beforeEach(() => {
52
52
  shapeUtils: [CustomShape],
53
53
  bindingUtils: [],
54
54
  tools: [],
55
- store: createTLStore({ shapeUtils: [CustomShape] }),
55
+ store: createTLStore({ shapeUtils: [CustomShape], bindingUtils: [] }),
56
56
  getContainer: () => document.body,
57
57
  })
58
58
  editor.setCameraOptions({ isLocked: true })
@@ -129,6 +129,7 @@ import { Group2d } from '../primitives/geometry/Group2d'
129
129
  import { intersectPolygonPolygon } from '../primitives/intersect'
130
130
  import { PI, approximately, areAnglesCompatible, clamp, pointInPolygon } from '../primitives/utils'
131
131
  import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap'
132
+ import { areShapesContentEqual } from '../utils/areShapesContentEqual'
132
133
  import { dataUrlToFile } from '../utils/assets'
133
134
  import { debugFlags } from '../utils/debug-flags'
134
135
  import {
@@ -2275,13 +2276,15 @@ export class Editor extends EventEmitter<TLEventMap> {
2275
2276
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2276
2277
  const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2277
2278
  this.setRichTextEditor(null)
2278
- if (id !== this.getEditingShapeId()) {
2279
+ const prevEditingShapeId = this.getEditingShapeId()
2280
+ if (id !== prevEditingShapeId) {
2279
2281
  if (id) {
2280
2282
  const shape = this.getShape(id)
2281
2283
  if (shape && this.getShapeUtil(shape).canEdit(shape)) {
2282
2284
  this.run(
2283
2285
  () => {
2284
2286
  this._updateCurrentPageState({ editingShapeId: id })
2287
+ this.getShapeUtil(shape).onEditStart?.(shape)
2285
2288
  },
2286
2289
  { history: 'ignore' }
2287
2290
  )
@@ -2294,6 +2297,12 @@ export class Editor extends EventEmitter<TLEventMap> {
2294
2297
  () => {
2295
2298
  this._updateCurrentPageState({ editingShapeId: null })
2296
2299
  this._currentRichTextEditor.set(null)
2300
+ if (prevEditingShapeId) {
2301
+ const prevEditingShape = this.getShape(prevEditingShapeId)
2302
+ if (prevEditingShape) {
2303
+ this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape)
2304
+ }
2305
+ }
2297
2306
  },
2298
2307
  { history: 'ignore' }
2299
2308
  )
@@ -4574,7 +4583,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4574
4583
  this.fonts.trackFontsForShape(shape)
4575
4584
  return this.getShapeUtil(shape).getGeometry(shape, opts)
4576
4585
  },
4577
- { areRecordsEqual: (a, b) => a.props === b.props }
4586
+ { areRecordsEqual: areShapesContentEqual }
4578
4587
  )
4579
4588
  }
4580
4589
  return this._shapeGeometryCaches[context].get(
@@ -4622,9 +4631,15 @@ export class Editor extends EventEmitter<TLEventMap> {
4622
4631
 
4623
4632
  /** @internal */
4624
4633
  @computed private _getShapeHandlesCache(): ComputedCache<TLHandle[] | undefined, TLShape> {
4625
- return this.store.createComputedCache('handles', (shape) => {
4626
- return this.getShapeUtil(shape).getHandles?.(shape)
4627
- })
4634
+ return this.store.createComputedCache(
4635
+ 'handles',
4636
+ (shape) => {
4637
+ return this.getShapeUtil(shape).getHandles?.(shape)
4638
+ },
4639
+ {
4640
+ areRecordsEqual: areShapesContentEqual,
4641
+ }
4642
+ )
4628
4643
  }
4629
4644
 
4630
4645
  /**
@@ -5845,9 +5860,15 @@ export class Editor extends EventEmitter<TLEventMap> {
5845
5860
  @computed
5846
5861
  private _getBindingsIndexCache() {
5847
5862
  const index = bindingsIndex(this)
5848
- return this.store.createComputedCache<TLBinding[], TLShape>('bindingsIndex', (shape) => {
5849
- return index.get().get(shape.id)
5850
- })
5863
+ return this.store.createComputedCache<TLBinding[], TLShape>(
5864
+ 'bindingsIndex',
5865
+ (shape) => {
5866
+ return index.get().get(shape.id)
5867
+ },
5868
+ // we can ignore the shape equality check here because the index is
5869
+ // computed incrementally based on what bindings are in the store
5870
+ { areRecordsEqual: () => true }
5871
+ )
5851
5872
  }
5852
5873
 
5853
5874
  /**
@@ -10214,7 +10235,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10214
10235
 
10215
10236
  // If the camera behavior is "zoom" and the ctrl key is pressed, then pan;
10216
10237
  // If the camera behavior is "pan" and the ctrl key is not pressed, then zoom
10217
- if (inputs.ctrlKey) behavior = wheelBehavior === 'pan' ? 'zoom' : 'pan'
10238
+ if (info.ctrlKey) behavior = wheelBehavior === 'pan' ? 'zoom' : 'pan'
10218
10239
 
10219
10240
  switch (behavior) {
10220
10241
  case 'zoom': {
@@ -10330,12 +10351,10 @@ export class Editor extends EventEmitter<TLEventMap> {
10330
10351
  if (this.inputs.isPanning && this.inputs.isPointing) {
10331
10352
  // Handle spacebar / middle mouse button panning
10332
10353
  const { currentScreenPoint, previousScreenPoint } = this.inputs
10333
- const { panSpeed } = cameraOptions
10334
10354
  const offset = Vec.Sub(currentScreenPoint, previousScreenPoint)
10335
- this.setCamera(
10336
- new Vec(cx + (offset.x * panSpeed) / cz, cy + (offset.y * panSpeed) / cz, cz),
10337
- { immediate: true }
10338
- )
10355
+ this.setCamera(new Vec(cx + offset.x / cz, cy + offset.y / cz, cz), {
10356
+ immediate: true,
10357
+ })
10339
10358
  this.maybeTrackPerformance('Panning')
10340
10359
  return
10341
10360
  }
@@ -45,7 +45,6 @@ export interface HandleSnapGeometry {
45
45
 
46
46
  const defaultGetSelfSnapOutline = () => null
47
47
  const defaultGetSelfSnapPoints = () => []
48
-
49
48
  /** @public */
50
49
  export class HandleSnaps {
51
50
  readonly editor: Editor
@@ -32,6 +32,7 @@ export interface TLMeasureTextSpanOpts {
32
32
  fontStyle: string
33
33
  lineHeight: number
34
34
  textAlign: TLDefaultHorizontalAlignStyle
35
+ otherStyles?: Record<string, string>
35
36
  }
36
37
 
37
38
  const spaceCharacterRegex = /\s/
@@ -86,6 +87,7 @@ export class TextManager {
86
87
  */
87
88
  maxWidth: null | number
88
89
  minWidth?: null | number
90
+ otherStyles?: Record<string, string>
89
91
  padding: string
90
92
  disableOverflowWrapBreaking?: boolean
91
93
  }
@@ -112,6 +114,11 @@ export class TextManager {
112
114
  'overflow-wrap',
113
115
  opts.disableOverflowWrapBreaking ? 'normal' : 'break-word'
114
116
  )
117
+ if (opts.otherStyles) {
118
+ for (const [key, value] of Object.entries(opts.otherStyles)) {
119
+ wrapperElm.style.setProperty(key, value)
120
+ }
121
+ }
115
122
 
116
123
  const scrollWidth = wrapperElm.scrollWidth
117
124
  const rect = wrapperElm.getBoundingClientRect()
@@ -256,6 +263,11 @@ export class TextManager {
256
263
  elm.style.setProperty('line-height', `${opts.lineHeight * opts.fontSize}px`)
257
264
  elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
258
265
  elm.style.setProperty('font-style', opts.fontStyle)
266
+ if (opts.otherStyles) {
267
+ for (const [key, value] of Object.entries(opts.otherStyles)) {
268
+ elm.style.setProperty(key, value)
269
+ }
270
+ }
259
271
 
260
272
  const shouldTruncateToFirstLine =
261
273
  opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
@@ -707,7 +707,15 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
707
707
  onClick?(shape: Shape): TLShapePartial<Shape> | void
708
708
 
709
709
  /**
710
- * A callback called when a shape finishes being editing.
710
+ * A callback called when a shape starts being edited.
711
+ *
712
+ * @param shape - The shape.
713
+ * @public
714
+ */
715
+ onEditStart?(shape: Shape): void
716
+
717
+ /**
718
+ * A callback called when a shape finishes being edited.
711
719
  *
712
720
  * @param shape - The shape.
713
721
  * @public
@@ -4,15 +4,15 @@ import { TLDefaultDashStyle } from '@tldraw/tlschema'
4
4
  export function getPerfectDashProps(
5
5
  totalLength: number,
6
6
  strokeWidth: number,
7
- opts = {} as Partial<{
8
- style: TLDefaultDashStyle
9
- snap: number
10
- end: 'skip' | 'outset' | 'none'
11
- start: 'skip' | 'outset' | 'none'
12
- lengthRatio: number
13
- closed: boolean
14
- forceSolid: boolean
15
- }>
7
+ opts: {
8
+ style?: TLDefaultDashStyle
9
+ snap?: number
10
+ end?: 'skip' | 'outset' | 'none'
11
+ start?: 'skip' | 'outset' | 'none'
12
+ lengthRatio?: number
13
+ closed?: boolean
14
+ forceSolid?: boolean
15
+ } = {}
16
16
  ): {
17
17
  strokeDasharray: string
18
18
  strokeDashoffset: string
@@ -365,6 +365,21 @@ function SvgExport({
365
365
  onMount()
366
366
  }, [onMount, shapeElements])
367
367
 
368
+ let backgroundColor = background ? theme.background : 'transparent'
369
+
370
+ if (singleFrameShapeId && background) {
371
+ const frameShapeUtil = editor.getShapeUtil('frame') as any as
372
+ | undefined
373
+ | { options: { showColors: boolean } }
374
+ if (frameShapeUtil?.options.showColors) {
375
+ const shape = editor.getShape(singleFrameShapeId)! as TLFrameShape
376
+ const color = theme[shape.props.color]
377
+ backgroundColor = color.frame.fill
378
+ } else {
379
+ backgroundColor = theme.solid
380
+ }
381
+ }
382
+
368
383
  return (
369
384
  <SvgExportContextProvider editor={editor} context={exportContext}>
370
385
  <svg
@@ -375,13 +390,7 @@ function SvgExport({
375
390
  viewBox={`${bbox.minX} ${bbox.minY} ${bbox.width} ${bbox.height}`}
376
391
  strokeLinecap="round"
377
392
  strokeLinejoin="round"
378
- style={{
379
- backgroundColor: background
380
- ? singleFrameShapeId
381
- ? theme.solid
382
- : theme.background
383
- : 'transparent',
384
- }}
393
+ style={{ backgroundColor }}
385
394
  data-color-mode={isDarkMode ? 'dark' : 'light'}
386
395
  className={`tl-container tl-theme__force-sRGB ${isDarkMode ? 'tl-theme__dark' : 'tl-theme__light'}`}
387
396
  >
@@ -57,6 +57,11 @@ export class Box {
57
57
  this.x = n
58
58
  }
59
59
 
60
+ // eslint-disable-next-line no-restricted-syntax
61
+ get left() {
62
+ return this.x
63
+ }
64
+
60
65
  // eslint-disable-next-line no-restricted-syntax
61
66
  get midX() {
62
67
  return this.x + this.w / 2
@@ -67,6 +72,11 @@ export class Box {
67
72
  return this.x + this.w
68
73
  }
69
74
 
75
+ // eslint-disable-next-line no-restricted-syntax
76
+ get right() {
77
+ return this.x + this.w
78
+ }
79
+
70
80
  // eslint-disable-next-line no-restricted-syntax
71
81
  get minY() {
72
82
  return this.y
@@ -77,6 +87,11 @@ export class Box {
77
87
  this.y = n
78
88
  }
79
89
 
90
+ // eslint-disable-next-line no-restricted-syntax
91
+ get top() {
92
+ return this.y
93
+ }
94
+
80
95
  // eslint-disable-next-line no-restricted-syntax
81
96
  get midY() {
82
97
  return this.y + this.h / 2
@@ -87,6 +102,11 @@ export class Box {
87
102
  return this.y + this.h
88
103
  }
89
104
 
105
+ // eslint-disable-next-line no-restricted-syntax
106
+ get bottom() {
107
+ return this.y + this.h
108
+ }
109
+
90
110
  // eslint-disable-next-line no-restricted-syntax
91
111
  get width() {
92
112
  return this.w