@tldraw/editor 5.4.0-next.ef99ab47e5ce → 5.5.0-canary.078a77321dc3

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 (288) hide show
  1. package/dist-cjs/index.d.ts +119 -49
  2. package/dist-cjs/index.js +4 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +7 -2
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
  7. package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
  9. package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
  12. package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
  13. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  14. package/dist-cjs/lib/config/createTLStore.js +1 -1
  15. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  16. package/dist-cjs/lib/constants.js +2 -0
  17. package/dist-cjs/lib/constants.js.map +2 -2
  18. package/dist-cjs/lib/editor/Editor.js +190 -300
  19. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  20. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
  21. package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -3
  23. package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
  25. package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +10 -6
  27. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
  29. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  30. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
  31. package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
  32. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +7 -0
  33. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  34. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
  35. package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
  36. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +1 -2
  37. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  38. package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js +7 -3
  39. package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js.map +2 -2
  40. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
  41. package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
  42. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +12 -1
  43. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  44. package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
  45. package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
  46. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
  47. package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
  48. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
  49. package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
  50. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
  51. package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
  52. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
  53. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  54. package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
  55. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  56. package/dist-cjs/lib/globals/editors.js +57 -0
  57. package/dist-cjs/lib/globals/editors.js.map +7 -0
  58. package/dist-cjs/lib/globals/menus.js +9 -4
  59. package/dist-cjs/lib/globals/menus.js.map +2 -2
  60. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
  61. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  62. package/dist-cjs/lib/hooks/useDocumentEvents.js +39 -9
  63. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  64. package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
  65. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  66. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
  67. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  68. package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
  69. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  70. package/dist-cjs/lib/license/LicenseManager.js +9 -2
  71. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  72. package/dist-cjs/lib/license/LicenseProvider.js +16 -3
  73. package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
  74. package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
  75. package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
  76. package/dist-cjs/lib/primitives/Box.js +2 -1
  77. package/dist-cjs/lib/primitives/Box.js.map +2 -2
  78. package/dist-cjs/lib/primitives/Vec.js +4 -3
  79. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  80. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  81. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  82. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
  83. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  84. package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
  85. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  86. package/dist-cjs/lib/primitives/geometry/Group2d.js +71 -26
  87. package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
  88. package/dist-cjs/lib/primitives/geometry/Point2d.js +1 -1
  89. package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
  90. package/dist-cjs/lib/primitives/utils.js +2 -1
  91. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  92. package/dist-cjs/lib/utils/deepLinks.js +5 -1
  93. package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
  94. package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
  95. package/dist-cjs/lib/utils/normalizeWheel.js.map +2 -2
  96. package/dist-cjs/lib/utils/rotation.js +1 -9
  97. package/dist-cjs/lib/utils/rotation.js.map +2 -2
  98. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +17 -0
  99. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  100. package/dist-cjs/lib/utils/sync/alerts.js +1 -1
  101. package/dist-cjs/lib/utils/sync/alerts.js.map +1 -1
  102. package/dist-cjs/version.js +3 -3
  103. package/dist-cjs/version.js.map +1 -1
  104. package/dist-esm/index.d.mts +119 -49
  105. package/dist-esm/index.mjs +9 -2
  106. package/dist-esm/index.mjs.map +2 -2
  107. package/dist-esm/lib/TldrawEditor.mjs +9 -4
  108. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  109. package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
  110. package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
  111. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
  112. package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
  113. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
  114. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
  115. package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
  116. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  117. package/dist-esm/lib/config/createTLStore.mjs +1 -1
  118. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  119. package/dist-esm/lib/constants.mjs +2 -0
  120. package/dist-esm/lib/constants.mjs.map +2 -2
  121. package/dist-esm/lib/editor/Editor.mjs +199 -301
  122. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  123. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
  124. package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
  125. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -3
  126. package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
  127. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
  128. package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
  129. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +10 -6
  130. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  131. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
  132. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  133. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
  134. package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
  135. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +7 -0
  136. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  137. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
  138. package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
  139. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -3
  140. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  141. package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs +7 -3
  142. package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs.map +2 -2
  143. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
  144. package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
  145. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +12 -1
  146. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  147. package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
  148. package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
  149. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
  150. package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
  151. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
  152. package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
  153. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
  154. package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
  155. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
  156. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  157. package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
  158. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  159. package/dist-esm/lib/globals/editors.mjs +37 -0
  160. package/dist-esm/lib/globals/editors.mjs.map +7 -0
  161. package/dist-esm/lib/globals/menus.mjs +9 -4
  162. package/dist-esm/lib/globals/menus.mjs.map +2 -2
  163. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
  164. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  165. package/dist-esm/lib/hooks/useDocumentEvents.mjs +40 -10
  166. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  167. package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
  168. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  169. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
  170. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  171. package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
  172. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  173. package/dist-esm/lib/license/LicenseManager.mjs +9 -2
  174. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  175. package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
  176. package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
  177. package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
  178. package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
  179. package/dist-esm/lib/primitives/Box.mjs +2 -1
  180. package/dist-esm/lib/primitives/Box.mjs.map +2 -2
  181. package/dist-esm/lib/primitives/Vec.mjs +4 -3
  182. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  183. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +1 -1
  184. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  185. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
  186. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  187. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
  188. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  189. package/dist-esm/lib/primitives/geometry/Group2d.mjs +75 -27
  190. package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
  191. package/dist-esm/lib/primitives/geometry/Point2d.mjs +1 -1
  192. package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
  193. package/dist-esm/lib/primitives/utils.mjs +2 -1
  194. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  195. package/dist-esm/lib/utils/deepLinks.mjs +5 -1
  196. package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
  197. package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
  198. package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
  199. package/dist-esm/lib/utils/rotation.mjs +1 -9
  200. package/dist-esm/lib/utils/rotation.mjs.map +2 -2
  201. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +17 -0
  202. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  203. package/dist-esm/lib/utils/sync/alerts.mjs +1 -1
  204. package/dist-esm/lib/utils/sync/alerts.mjs.map +1 -1
  205. package/dist-esm/version.mjs +3 -3
  206. package/dist-esm/version.mjs.map +1 -1
  207. package/package.json +13 -7
  208. package/src/index.ts +6 -1
  209. package/src/lib/TldrawEditor.tsx +16 -4
  210. package/src/lib/components/MenuClickCapture.tsx +19 -18
  211. package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
  212. package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
  213. package/src/lib/config/TLUserPreferences.test.ts +18 -1
  214. package/src/lib/config/TLUserPreferences.ts +8 -4
  215. package/src/lib/config/createTLStore.ts +1 -1
  216. package/src/lib/constants.ts +8 -0
  217. package/src/lib/editor/Editor.ts +285 -392
  218. package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
  219. package/src/lib/editor/managers/ClickManager/ClickManager.ts +5 -3
  220. package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
  221. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +35 -4
  222. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +13 -7
  223. package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
  224. package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
  225. package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
  226. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +12 -1
  227. package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
  228. package/src/lib/editor/managers/TextManager/TextManager.ts +2 -6
  229. package/src/lib/editor/managers/ThemeManager/ThemeManager.test.ts +53 -0
  230. package/src/lib/editor/managers/ThemeManager/ThemeManager.ts +9 -3
  231. package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
  232. package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
  233. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +64 -3
  234. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +18 -1
  235. package/src/lib/editor/overlays/OverlayManager.ts +3 -7
  236. package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
  237. package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
  238. package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
  239. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
  240. package/src/lib/editor/tools/StateNode.test.ts +64 -0
  241. package/src/lib/editor/tools/StateNode.ts +12 -2
  242. package/src/lib/globals/editors.test.ts +76 -0
  243. package/src/lib/globals/editors.ts +53 -0
  244. package/src/lib/globals/menus.test.ts +33 -0
  245. package/src/lib/globals/menus.ts +11 -4
  246. package/src/lib/hooks/useCanvasEvents.ts +2 -0
  247. package/src/lib/hooks/useDocumentEvents.ts +60 -16
  248. package/src/lib/hooks/useLocalStore.ts +16 -8
  249. package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
  250. package/src/lib/hooks/usePeerIds.ts +2 -1
  251. package/src/lib/license/LicenseManager.test.ts +160 -0
  252. package/src/lib/license/LicenseManager.ts +13 -4
  253. package/src/lib/license/LicenseProvider.tsx +24 -3
  254. package/src/lib/license/useLicenseManagerState.ts +8 -6
  255. package/src/lib/primitives/Box.test.ts +9 -1
  256. package/src/lib/primitives/Box.ts +3 -1
  257. package/src/lib/primitives/Vec.test.ts +10 -0
  258. package/src/lib/primitives/Vec.ts +4 -4
  259. package/src/lib/primitives/geometry/Arc2d.test.ts +235 -3
  260. package/src/lib/primitives/geometry/Arc2d.ts +1 -1
  261. package/src/lib/primitives/geometry/CubicBezier2d.test.ts +186 -3
  262. package/src/lib/primitives/geometry/CubicBezier2d.ts +1 -1
  263. package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
  264. package/src/lib/primitives/geometry/Edge2d.test.ts +21 -3
  265. package/src/lib/primitives/geometry/Edge2d.ts +1 -1
  266. package/src/lib/primitives/geometry/Ellipse2d.test.ts +243 -3
  267. package/src/lib/primitives/geometry/Geometry2d.test.ts +553 -1
  268. package/src/lib/primitives/geometry/Group2d.test.ts +419 -3
  269. package/src/lib/primitives/geometry/Group2d.ts +81 -34
  270. package/src/lib/primitives/geometry/Point2d.test.ts +14 -3
  271. package/src/lib/primitives/geometry/Point2d.ts +1 -1
  272. package/src/lib/primitives/geometry/Polyline2d.test.ts +207 -0
  273. package/src/lib/primitives/geometry/Stadium2d.test.ts +191 -3
  274. package/src/lib/primitives/utils.test.ts +10 -1
  275. package/src/lib/primitives/utils.ts +4 -1
  276. package/src/lib/test/useCanvasEvents.test.tsx +74 -0
  277. package/src/lib/test/useDocumentEvents.test.tsx +107 -0
  278. package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
  279. package/src/lib/utils/deepLinks.test.ts +15 -0
  280. package/src/lib/utils/deepLinks.ts +7 -1
  281. package/src/lib/utils/normalizeWheel.test.ts +39 -0
  282. package/src/lib/utils/normalizeWheel.ts +6 -0
  283. package/src/lib/utils/rotation.ts +4 -12
  284. package/src/lib/utils/sync/TLLocalSyncClient.test.ts +56 -0
  285. package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -0
  286. package/src/lib/utils/sync/alerts.ts +1 -1
  287. package/src/version.ts +3 -3
  288. package/src/lib/primitives/geometry/Polyline.test.ts +0 -5
@@ -1,5 +1,5 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
- import { useEffect } from 'react'
2
+ import { useEffect, useRef } from 'react'
3
3
  import { Editor } from '../editor/Editor'
4
4
  import { TLKeyboardEventInfo } from '../editor/types/event-types'
5
5
  import { activeElementShouldCaptureKeys, preventDefault } from '../utils/dom'
@@ -14,6 +14,10 @@ export function useDocumentEvents() {
14
14
  const isEditing = useValue('isEditing', () => editor.getEditingShapeId(), [editor])
15
15
  const isAppFocused = useValue('isFocused', () => editor.getIsFocused(), [editor])
16
16
 
17
+ // `inputs.keys` only stores codes, but tools match `onKeyUp` on `info.key`, so the window
18
+ // blur release below needs the key remembered per code.
19
+ const heldKeysRef = useRef(new Map<string, string>())
20
+
17
21
  // Prevent the browser's default drag and drop behavior on our container (UI, etc)
18
22
  useEffect(() => {
19
23
  if (!container) return
@@ -29,8 +33,12 @@ export function useDocumentEvents() {
29
33
  // re-dispatched, which would lead to an infinite loop.
30
34
  if ((e as any).isSpecialRedispatchedEvent) return
31
35
  preventDefault(e)
32
- e.stopPropagation()
33
36
  const cvs = container.querySelector('.tl-canvas')
37
+ // A drop on the canvas itself already reaches the canvas (and any shape's own React
38
+ // drop handlers) by bubbling; stopping it here would hide it from React, which
39
+ // delegates from the root above this container.
40
+ if (cvs?.contains(e.target as Node | null)) return
41
+ e.stopPropagation()
34
42
  if (!cvs) return
35
43
  const newEvent = new DragEvent(e.type, e)
36
44
  ;(newEvent as any).isSpecialRedispatchedEvent = true
@@ -208,6 +216,7 @@ export function useDocumentEvents() {
208
216
  accelKey: isAccelKey(e),
209
217
  }
210
218
 
219
+ heldKeysRef.current.set(e.code, e.key)
211
220
  editor.dispatch(info)
212
221
  }
213
222
 
@@ -235,24 +244,26 @@ export function useDocumentEvents() {
235
244
  accelKey: isAccelKey(e),
236
245
  }
237
246
 
247
+ heldKeysRef.current.delete(e.code)
238
248
  editor.dispatch(info)
239
249
  }
240
250
 
241
251
  function handleTouchStart(e: TouchEvent) {
242
252
  if (container.contains(e.target as Node)) {
243
- // Center point of the touch area
244
- const touchXPosition = e.touches[0].pageX
253
+ // Center point of the touch area, measured from the edge of the window the touch
254
+ // has to reach to trigger the navigation
255
+ const touchXPosition = e.touches[0].clientX
245
256
  // Size of the touch area
246
257
  const touchXRadius = e.touches[0].radiusX || 0
247
258
 
248
- // We set a threshold (10px) on both sizes of the screen,
249
- // if the touch area overlaps with the screen edges
250
- // it's likely to trigger the navigation. We prevent the
251
- // touchstart event in that case.
252
- // todo: make this relative to the actual window, not the editor's screen bounds
259
+ // If the touch area overlaps with the screen edges it's likely to trigger the
260
+ // navigation. We prevent the touchstart event in that case. The gesture belongs to
261
+ // the window, so an editor inset from the window's edges — beside a sidebar, say —
262
+ // is not near an edge the system reacts to.
263
+ const windowWidth = editor.getContainerWindow().innerWidth
253
264
  if (
254
265
  touchXPosition - touchXRadius < 10 ||
255
- touchXPosition + touchXRadius > editor.getViewportScreenBounds().width - 10
266
+ touchXPosition + touchXRadius > windowWidth - 10
256
267
  ) {
257
268
  if ((e.target as HTMLElement)?.tagName === 'BUTTON') {
258
269
  // Force a click before bailing
@@ -276,10 +287,13 @@ export function useDocumentEvents() {
276
287
 
277
288
  container.addEventListener('wheel', handleWheel, { passive: false })
278
289
 
290
+ // Not the shared `preventDefault` reference: the DOM dedupes identical listeners, so with two
291
+ // editors in one document the first cleanup would otherwise remove it for both.
292
+ const handleGesture = (e: Event) => preventDefault(e)
279
293
  const ownerDoc = container.ownerDocument
280
- ownerDoc.addEventListener('gesturestart', preventDefault)
281
- ownerDoc.addEventListener('gesturechange', preventDefault)
282
- ownerDoc.addEventListener('gestureend', preventDefault)
294
+ ownerDoc.addEventListener('gesturestart', handleGesture)
295
+ ownerDoc.addEventListener('gesturechange', handleGesture)
296
+ ownerDoc.addEventListener('gestureend', handleGesture)
283
297
 
284
298
  container.addEventListener('keydown', handleKeyDown)
285
299
  container.addEventListener('keyup', handleKeyUp)
@@ -289,14 +303,44 @@ export function useDocumentEvents() {
289
303
 
290
304
  container.removeEventListener('wheel', handleWheel)
291
305
 
292
- ownerDoc.removeEventListener('gesturestart', preventDefault)
293
- ownerDoc.removeEventListener('gesturechange', preventDefault)
294
- ownerDoc.removeEventListener('gestureend', preventDefault)
306
+ ownerDoc.removeEventListener('gesturestart', handleGesture)
307
+ ownerDoc.removeEventListener('gesturechange', handleGesture)
308
+ ownerDoc.removeEventListener('gestureend', handleGesture)
295
309
 
296
310
  container.removeEventListener('keydown', handleKeyDown)
297
311
  container.removeEventListener('keyup', handleKeyUp)
298
312
  }
299
313
  }, [editor, container, isAppFocused, isEditing])
314
+
315
+ // Alt+Tab / Cmd+Tab away while holding a key delivers the keyup to the other app, so without
316
+ // this Space-panning (grab cursor, drags pan) stuck until the key was pressed again (#10442).
317
+ // Replaying a key_up per held key runs the normal release path, including tool onKeyUp.
318
+ useEffect(() => {
319
+ const win = editor.getContainerWindow()
320
+
321
+ const handleWindowBlur = () => {
322
+ const heldKeys = heldKeysRef.current
323
+ for (const code of [...editor.inputs.keys]) {
324
+ editor.dispatch({
325
+ type: 'keyboard',
326
+ name: 'key_up',
327
+ key: heldKeys.get(code) ?? code,
328
+ code,
329
+ shiftKey: false,
330
+ altKey: false,
331
+ ctrlKey: false,
332
+ metaKey: false,
333
+ accelKey: false,
334
+ })
335
+ }
336
+ heldKeys.clear()
337
+ }
338
+
339
+ win.addEventListener('blur', handleWindowBlur)
340
+ return () => {
341
+ win.removeEventListener('blur', handleWindowBlur)
342
+ }
343
+ }, [editor])
300
344
  }
301
345
 
302
346
  function areShortcutsDisabled(editor: Editor) {
@@ -1,5 +1,5 @@
1
- import { TLAsset, TLAssetStore, TLStoreSnapshot } from '@tldraw/tlschema'
2
- import { WeakCache } from '@tldraw/utils'
1
+ import { TLAssetId, TLAssetStore, TLStoreSnapshot } from '@tldraw/tlschema'
2
+ import { noop } from '@tldraw/utils'
3
3
  import { useEffect } from 'react'
4
4
  import { TLStoreOptions, createTLStore } from '../config/createTLStore'
5
5
  import { TLEditorSnapshot } from '../config/TLEditorSnapshot'
@@ -33,7 +33,9 @@ export function useLocalStore(
33
33
 
34
34
  setState({ status: 'loading' })
35
35
 
36
- const objectURLCache = new WeakCache<TLAsset, Promise<string | null>>()
36
+ // Keyed by asset id rather than record identity so that a record update doesn't mint
37
+ // another object url for the same blob; every url minted here is revoked on cleanup.
38
+ const objectURLCache = new Map<TLAssetId, Promise<string | null>>()
37
39
  const assets: TLAssetStore = {
38
40
  upload: async (asset, file) => {
39
41
  await client.db.storeAsset(asset.id, file)
@@ -43,11 +45,14 @@ export function useLocalStore(
43
45
  if (!asset.props.src) return null
44
46
 
45
47
  if (asset.props.src.startsWith('asset:')) {
46
- return await objectURLCache.get(asset, async () => {
47
- const blob = await client.db.getAsset(asset.id)
48
- if (!blob) return null
49
- return URL.createObjectURL(blob)
50
- })
48
+ let objectURL = objectURLCache.get(asset.id)
49
+ if (!objectURL) {
50
+ objectURL = client.db
51
+ .getAsset(asset.id)
52
+ .then((blob) => (blob ? URL.createObjectURL(blob) : null))
53
+ objectURLCache.set(asset.id, objectURL)
54
+ }
55
+ return await objectURL
51
56
  }
52
57
 
53
58
  return asset.props.src
@@ -78,6 +83,9 @@ export function useLocalStore(
78
83
  return () => {
79
84
  isClosed = true
80
85
  client.close()
86
+ for (const objectURL of objectURLCache.values()) {
87
+ objectURL.then((url) => url && URL.revokeObjectURL(url), noop)
88
+ }
81
89
  }
82
90
  }, [options, setState])
83
91
 
@@ -70,6 +70,9 @@ export function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {
70
70
  const newEvent = new WheelEvent('wheel', e as any)
71
71
  ;(newEvent as any).isSpecialRedispatchedEvent = true
72
72
  cvs.dispatchEvent(newEvent)
73
+ // Flag the original too: it keeps bubbling, and an enclosing pass-through root (the
74
+ // navigation panel around the minimap) would otherwise redispatch it a second time.
75
+ ;(e as any).isSpecialRedispatchedEvent = true
73
76
  })
74
77
 
75
78
  // The element the listener is currently on, which isn't necessarily `ref.current`: a `RefObject`
@@ -1,4 +1,5 @@
1
1
  import { useComputed, useValue } from '@tldraw/state-react'
2
+ import { areArraysShallowEqual } from '@tldraw/utils'
2
3
  import { uniq } from '../utils/uniq'
3
4
  import { useEditor } from './useEditor'
4
5
 
@@ -17,7 +18,7 @@ export function usePeerIds() {
17
18
  const $userIds = useComputed(
18
19
  'userIds',
19
20
  () => uniq(editor.getVisibleCollaborators().map((p) => p.userId)).sort(),
20
- { isEqual: (a, b) => a.join(',') === b.join?.(',') },
21
+ { isEqual: areArraysShallowEqual },
21
22
  [editor]
22
23
  )
23
24
 
@@ -316,6 +316,42 @@ describe('LicenseManager', () => {
316
316
  expect(result.isDomainValid).toBe(true)
317
317
  })
318
318
 
319
+ it('Fails if the wildcard domain is a prefix of the current hostname', async () => {
320
+ // @ts-ignore
321
+ delete window.location
322
+ // @ts-ignore
323
+ window.location = new URL('https://sub.example.com.other.io')
324
+
325
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
326
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
327
+ const permissiveLicenseKey = await generateLicenseKey(
328
+ JSON.stringify(permissiveHostsInfo),
329
+ keyPair
330
+ )
331
+ const result = (await licenseManager.getLicenseFromKey(
332
+ permissiveLicenseKey
333
+ )) as ValidLicenseKeyResult
334
+ expect(result.isDomainValid).toBe(false)
335
+ })
336
+
337
+ it('Fails if the current hostname only ends with the wildcard domain', async () => {
338
+ // @ts-ignore
339
+ delete window.location
340
+ // @ts-ignore
341
+ window.location = new URL('https://notexample.com')
342
+
343
+ const permissiveHostsInfo = JSON.parse(STANDARD_LICENSE_INFO)
344
+ permissiveHostsInfo[PROPERTIES.HOSTS] = ['*.example.com']
345
+ const permissiveLicenseKey = await generateLicenseKey(
346
+ JSON.stringify(permissiveHostsInfo),
347
+ keyPair
348
+ )
349
+ const result = (await licenseManager.getLicenseFromKey(
350
+ permissiveLicenseKey
351
+ )) as ValidLicenseKeyResult
352
+ expect(result.isDomainValid).toBe(false)
353
+ })
354
+
319
355
  it('Fails if has a subdomain wildcard isnt for the same base domain', async () => {
320
356
  // @ts-ignore
321
357
  delete window.location
@@ -422,6 +458,107 @@ describe('LicenseManager', () => {
422
458
  expect(result.isDomainValid).toBe(true)
423
459
  })
424
460
 
461
+ it('Is not development mode on a custom protocol', async () => {
462
+ process.env.NODE_ENV = 'production'
463
+ try {
464
+ // @ts-ignore
465
+ delete window.location
466
+ // @ts-ignore
467
+ window.location = new URL('app-bundle://app/index.html')
468
+
469
+ const nativeLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
470
+ nativeLicenseInfo[PROPERTIES.FLAGS] = FLAGS.NATIVE_LICENSE
471
+ nativeLicenseInfo[PROPERTIES.HOSTS] = ['app-bundle:']
472
+ const nativeLicenseKey = await generateLicenseKey(
473
+ JSON.stringify(nativeLicenseInfo),
474
+ keyPair
475
+ )
476
+ const nativeLicenseManager = new LicenseManager('', keyPair.publicKey)
477
+ const result = (await nativeLicenseManager.getLicenseFromKey(
478
+ nativeLicenseKey
479
+ )) as ValidLicenseKeyResult
480
+ expect(result).toMatchObject({
481
+ isLicenseParseable: true,
482
+ isNativeLicense: true,
483
+ isDomainValid: true,
484
+ isDevelopment: false,
485
+ })
486
+ expect(getLicenseState(result, () => {}, result.isDevelopment)).toBe('licensed')
487
+ } finally {
488
+ process.env.NODE_ENV = 'test'
489
+ }
490
+ })
491
+
492
+ it('Validates the domain on a custom protocol', async () => {
493
+ process.env.NODE_ENV = 'production'
494
+ try {
495
+ // @ts-ignore
496
+ delete window.location
497
+ // @ts-ignore
498
+ window.location = new URL('app-bundle://app/index.html')
499
+
500
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
501
+ const customProtocolLicenseManager = new LicenseManager('', keyPair.publicKey)
502
+ const result = (await customProtocolLicenseManager.getLicenseFromKey(
503
+ licenseKey
504
+ )) as ValidLicenseKeyResult
505
+ expect(result).toMatchObject({ isDomainValid: false, isDevelopment: false })
506
+ expect(getLicenseState(result, () => {}, result.isDevelopment)).toBe(
507
+ 'unlicensed-production'
508
+ )
509
+ } finally {
510
+ process.env.NODE_ENV = 'test'
511
+ }
512
+ })
513
+
514
+ it('Is development mode over http on a non-loopback host', async () => {
515
+ process.env.NODE_ENV = 'production'
516
+ try {
517
+ // @ts-ignore
518
+ delete window.location
519
+ // @ts-ignore
520
+ window.location = new URL('http://www.example.com')
521
+
522
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
523
+ const httpLicenseManager = new LicenseManager('', keyPair.publicKey)
524
+ const result = (await httpLicenseManager.getLicenseFromKey(
525
+ licenseKey
526
+ )) as ValidLicenseKeyResult
527
+ expect(result.isDevelopment).toBe(true)
528
+ } finally {
529
+ process.env.NODE_ENV = 'test'
530
+ }
531
+ })
532
+
533
+ it.each([
534
+ ['http://localhost:5173/', true],
535
+ ['http://127.0.0.1:3000/', true],
536
+ ['https://localhost:8443/', true],
537
+ ['https://[::1]:8443/', true],
538
+ ['http://192.168.1.5:3000/', true],
539
+ ['http://staging-box/', true],
540
+ ['https://www.example.com/', false],
541
+ ['tauri://localhost/index.html', false],
542
+ ['http://tauri.localhost/index.html', false],
543
+ ['app://tauri.localhost/com.example.app', false],
544
+ ['app-bundle://localhost/index.html', false],
545
+ ])('Development mode at %s is %s', async (href, expected) => {
546
+ process.env.NODE_ENV = 'production'
547
+ try {
548
+ // @ts-ignore
549
+ delete window.location
550
+ // @ts-ignore
551
+ window.location = new URL(href)
552
+
553
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
554
+ const manager = new LicenseManager('', keyPair.publicKey)
555
+ const result = (await manager.getLicenseFromKey(licenseKey)) as ValidLicenseKeyResult
556
+ expect(result.isDevelopment).toBe(expected)
557
+ } finally {
558
+ process.env.NODE_ENV = 'test'
559
+ }
560
+ })
561
+
425
562
  it('Fails if it is a native app with the wrong protocol', async () => {
426
563
  // @ts-ignore
427
564
  delete window.location
@@ -437,6 +574,29 @@ describe('LicenseManager', () => {
437
574
  )) as ValidLicenseKeyResult
438
575
  expect(result.isDomainValid).toBe(false)
439
576
  })
577
+
578
+ it.each(['tauri://localhost/index.html', 'http://tauri.localhost/index.html'])(
579
+ 'Does not license an unrelated key at %s',
580
+ async (href) => {
581
+ process.env.NODE_ENV = 'production'
582
+ try {
583
+ // @ts-ignore
584
+ delete window.location
585
+ // @ts-ignore
586
+ window.location = new URL(href)
587
+
588
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
589
+ const manager = new LicenseManager('', keyPair.publicKey)
590
+ const result = (await manager.getLicenseFromKey(licenseKey)) as ValidLicenseKeyResult
591
+ expect(result).toMatchObject({ isDevelopment: false, isDomainValid: false })
592
+ expect(getLicenseState(result, () => {}, result.isDevelopment)).toBe(
593
+ 'unlicensed-production'
594
+ )
595
+ } finally {
596
+ process.env.NODE_ENV = 'test'
597
+ }
598
+ }
599
+ )
440
600
  })
441
601
 
442
602
  describe('License types and flags', () => {
@@ -174,10 +174,17 @@ export class LicenseManager {
174
174
  }
175
175
 
176
176
  private getIsDevelopment() {
177
- // If we are using https on a non-loopback domain we assume it's a production env and a development one otherwise
177
+ const protocol = window.location.protocol
178
+ const hostname = window.location.hostname
179
+
180
+ // Tauri uses `tauri://localhost` on macOS and Linux and `http://tauri.localhost` on Windows.
181
+ if (hostname.toLowerCase().endsWith('.localhost')) {
182
+ return process.env.NODE_ENV !== 'production'
183
+ }
184
+
178
185
  return (
179
- !['https:', 'vscode-webview:'].includes(window.location.protocol) ||
180
- this.isLoopbackHost(window.location.hostname) ||
186
+ protocol === 'http:' ||
187
+ (protocol === 'https:' && this.isLoopbackHost(hostname)) ||
181
188
  process.env.NODE_ENV !== 'production'
182
189
  )
183
190
  }
@@ -407,7 +414,9 @@ export class LicenseManager {
407
414
 
408
415
  // Glob testing, we only support '*.somedomain.com' right now.
409
416
  if (host.includes('*')) {
410
- const globToRegex = new RegExp(host.replace(/\*/g, '.*?'))
417
+ const globToRegex = new RegExp(
418
+ normalizedHostOrUrlRegex.replace(/\./g, '\\.').replace(/\*/g, '.*?') + '$'
419
+ )
411
420
  return globToRegex.test(currentHostname) || globToRegex.test(`www.${currentHostname}`)
412
421
  }
413
422
 
@@ -1,13 +1,34 @@
1
1
  import { useValue } from '@tldraw/state-react'
2
2
  import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
3
+ import { useMaybeEditor } from '../hooks/useEditor'
3
4
  import { LicenseManager } from './LicenseManager'
4
5
 
5
6
  /** @internal */
6
- export const LicenseContext = createContext({} as LicenseManager)
7
+ export const LicenseContext = createContext<LicenseManager | null>(null)
7
8
 
8
9
  /** @internal */
9
- export function useLicenseContext() {
10
- return useContext(LicenseContext)
10
+ export function useLicenseContext(): LicenseManager {
11
+ const licenseManager = useMaybeLicenseManager()
12
+ if (!licenseManager) {
13
+ throw new Error(
14
+ 'useLicenseContext must be used inside of the <Tldraw /> or <TldrawEditor /> components, or inside an <EditorProvider /> wrapping an editor created by them'
15
+ )
16
+ }
17
+ return licenseManager
18
+ }
19
+
20
+ /**
21
+ * Returns the license manager for the current editor, or `null` if there is none. Reads the
22
+ * license context when inside `<TldrawEditor />`, and otherwise falls back to the license manager
23
+ * of the nearest editor, so UI mounted outside the editor tree (via `EditorProvider`) resolves the
24
+ * same license as the editor it belongs to.
25
+ *
26
+ * @internal
27
+ */
28
+ export function useMaybeLicenseManager(): LicenseManager | null {
29
+ const licenseManager = useContext(LicenseContext)
30
+ const editor = useMaybeEditor()
31
+ return licenseManager ?? editor?.licenseManager ?? null
11
32
  }
12
33
 
13
34
  function shouldHideEditorAfterDelay(licenseState: string): boolean {
@@ -8,16 +8,18 @@ export function useLicenseManagerState(licenseManager: LicenseManager): LicenseS
8
8
 
9
9
  /**
10
10
  * Reactively reads whether a licensable feature is enabled for the current license. Re-renders when
11
- * license validation resolves.
11
+ * license validation resolves. Returns `false` when there is no license manager, so gated UI stays
12
+ * hidden when mounted without an editor or license context.
12
13
  *
13
14
  * @internal
14
15
  */
15
16
  export function useLicenseFeatureFlag(
16
- licenseManager: LicenseManager,
17
+ licenseManager: LicenseManager | null,
17
18
  feature: LicenseFeatureName
18
19
  ): boolean {
19
- return useValue('licenseFeature', () => licenseManager.isFeatureEnabled(feature), [
20
- licenseManager,
21
- feature,
22
- ])
20
+ return useValue(
21
+ 'licenseFeature',
22
+ () => (licenseManager ? licenseManager.isFeatureEnabled(feature) : false),
23
+ [licenseManager, feature]
24
+ )
23
25
  }
@@ -1,4 +1,4 @@
1
- import { Box } from './Box'
1
+ import { Box, rotateSelectionHandle } from './Box'
2
2
  import { Vec } from './Vec'
3
3
 
4
4
  describe('Box', () => {
@@ -759,3 +759,11 @@ describe('Box', () => {
759
759
  })
760
760
  })
761
761
  })
762
+
763
+ describe('rotateSelectionHandle', () => {
764
+ it('wraps around for negative rotations', () => {
765
+ expect(rotateSelectionHandle('top', Math.PI / 2)).toBe('right')
766
+ expect(rotateSelectionHandle('top', -Math.PI / 2)).toBe('left')
767
+ expect(rotateSelectionHandle('top_left', -Math.PI)).toBe('bottom_right')
768
+ })
769
+ })
@@ -678,7 +678,9 @@ export function rotateSelectionHandle(handle: SelectionHandle, rotation: number)
678
678
  const numSteps = Math.round(rotation / (PI / 4))
679
679
 
680
680
  const currentIndex = ORDERED_SELECTION_HANDLES.indexOf(handle)
681
- return ORDERED_SELECTION_HANDLES[(currentIndex + numSteps) % ORDERED_SELECTION_HANDLES.length]
681
+ // numSteps is negative for negative rotations, so wrap the index into range
682
+ const n = ORDERED_SELECTION_HANDLES.length
683
+ return ORDERED_SELECTION_HANDLES[(((currentIndex + numSteps) % n) + n) % n]
682
684
  }
683
685
 
684
686
  /** @public */
@@ -336,6 +336,16 @@ describe('Vec.Slope', () => {
336
336
  expect(Vec.Slope(new Vec(0, 0), new Vec(50, 100))).toEqual(2)
337
337
  expect(Vec.Slope(new Vec(0, 0), new Vec(-50, 100))).toEqual(-2)
338
338
  expect(Vec.Slope(new Vec(123, 456), new Vec(789, 24))).toEqual(-0.6486486486486487)
339
+ // A.x happened to equal B.y here, which used to trip the vertical guard
340
+ expect(Vec.Slope(new Vec(1, 0), new Vec(0, 1))).toEqual(-1)
341
+ expect(Vec.Slope(new Vec(5, 0), new Vec(5, 10))).toBeNaN()
342
+ })
343
+ })
344
+
345
+ describe('Vec.cross', () => {
346
+ it('Matches the static Vec.Cross.', () => {
347
+ expect(Vec.Cross(new Vec(1, 2, 3), new Vec(4, 5, 6))).toMatchObject({ x: -3, y: 6 })
348
+ expect(new Vec(1, 2, 3).cross(new Vec(4, 5, 6))).toMatchObject({ x: -3, y: 6 })
339
349
  })
340
350
  })
341
351
 
@@ -164,9 +164,9 @@ export class Vec {
164
164
  }
165
165
 
166
166
  cross(V: VecLike) {
167
- this.x = this.y * V.z! - this.z * V.y
168
- this.y = this.z * V.x - this.x * V.z!
169
- // this.z = this.x * V.y - this.y * V.x
167
+ const { x, y, z } = this
168
+ this.x = y * V.z! - z * V.y
169
+ this.y = z * V.x - x * V.z!
170
170
  return this
171
171
  }
172
172
 
@@ -497,7 +497,7 @@ export class Vec {
497
497
  }
498
498
 
499
499
  static Slope(A: VecLike, B: VecLike): number {
500
- if (A.x === B.y) return NaN
500
+ if (A.x === B.x) return NaN
501
501
  return (A.y - B.y) / (A.x - B.x)
502
502
  }
503
503