@tldraw/editor 4.4.0-canary.afdcafe834b3 → 4.4.0-canary.b5c642789999

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 (65) hide show
  1. package/dist-cjs/index.d.ts +52 -9
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/components/Shape.js +12 -17
  4. package/dist-cjs/lib/components/Shape.js.map +2 -2
  5. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +26 -1
  6. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +16 -1
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  9. package/dist-cjs/lib/editor/Editor.js +19 -11
  10. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  11. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +32 -13
  12. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  13. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +2 -3
  14. package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
  15. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/hooks/usePeerIds.js +8 -2
  17. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  18. package/dist-cjs/lib/hooks/useShapeCulling.js +75 -0
  19. package/dist-cjs/lib/hooks/useShapeCulling.js.map +7 -0
  20. package/dist-cjs/lib/license/LicenseManager.js +6 -6
  21. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  22. package/dist-cjs/lib/options.js +2 -1
  23. package/dist-cjs/lib/options.js.map +2 -2
  24. package/dist-cjs/version.js +3 -3
  25. package/dist-cjs/version.js.map +1 -1
  26. package/dist-esm/index.d.mts +52 -9
  27. package/dist-esm/index.mjs +1 -1
  28. package/dist-esm/lib/components/Shape.mjs +12 -17
  29. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  30. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +27 -2
  31. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +2 -2
  32. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +16 -1
  33. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  34. package/dist-esm/lib/editor/Editor.mjs +19 -11
  35. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  36. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +32 -13
  37. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  38. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +2 -3
  39. package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
  40. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  41. package/dist-esm/lib/hooks/usePeerIds.mjs +8 -2
  42. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  43. package/dist-esm/lib/hooks/useShapeCulling.mjs +55 -0
  44. package/dist-esm/lib/hooks/useShapeCulling.mjs.map +7 -0
  45. package/dist-esm/lib/license/LicenseManager.mjs +6 -6
  46. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  47. package/dist-esm/lib/options.mjs +2 -1
  48. package/dist-esm/lib/options.mjs.map +2 -2
  49. package/dist-esm/version.mjs +3 -3
  50. package/dist-esm/version.mjs.map +1 -1
  51. package/editor.css +22 -2
  52. package/package.json +8 -9
  53. package/src/lib/components/Shape.tsx +15 -16
  54. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +46 -2
  55. package/src/lib/components/default-components/DefaultCanvas.tsx +24 -2
  56. package/src/lib/editor/Editor.ts +33 -11
  57. package/src/lib/editor/derivations/notVisibleShapes.ts +39 -17
  58. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +0 -35
  59. package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +4 -8
  60. package/src/lib/editor/shapes/ShapeUtil.ts +19 -5
  61. package/src/lib/hooks/usePeerIds.ts +9 -2
  62. package/src/lib/hooks/useShapeCulling.tsx +98 -0
  63. package/src/lib/license/LicenseManager.ts +6 -6
  64. package/src/lib/options.ts +10 -2
  65. package/src/version.ts +3 -3
@@ -57,7 +57,8 @@ const defaultTldrawOptions = {
57
57
  debouncedZoomThreshold: 500,
58
58
  spacebarPanning: true,
59
59
  zoomToFitPadding: 128,
60
- snapThreshold: 8
60
+ snapThreshold: 8,
61
+ quickZoomPreservesScreenBounds: true
61
62
  };
62
63
  export {
63
64
  defaultTldrawOptions
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/options.ts"],
4
- "sourcesContent": ["import { ComponentType, Fragment } from 'react'\n\n/**\n * Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.\n *\n * @example\n * ```tsx\n * const options: Partial<TldrawOptions> = {\n * maxPages: 3,\n * maxShapesPerPage: 1000,\n * }\n *\n * function MyTldrawComponent() {\n * return <Tldraw options={options} />\n * }\n * ```\n *\n * @public\n */\nexport interface TldrawOptions {\n\treadonly maxShapesPerPage: number\n\treadonly maxFilesAtOnce: number\n\treadonly maxPages: number\n\treadonly animationMediumMs: number\n\treadonly followChaseViewportSnap: number\n\treadonly doubleClickDurationMs: number\n\treadonly multiClickDurationMs: number\n\treadonly coarseDragDistanceSquared: number\n\treadonly dragDistanceSquared: number\n\treadonly uiDragDistanceSquared: number\n\treadonly uiCoarseDragDistanceSquared: number\n\treadonly defaultSvgPadding: number\n\treadonly cameraSlideFriction: number\n\treadonly gridSteps: readonly {\n\t\treadonly min: number\n\t\treadonly mid: number\n\t\treadonly step: number\n\t}[]\n\treadonly collaboratorInactiveTimeoutMs: number\n\treadonly collaboratorIdleTimeoutMs: number\n\treadonly collaboratorCheckIntervalMs: number\n\treadonly cameraMovingTimeoutMs: number\n\treadonly hitTestMargin: number\n\treadonly edgeScrollDelay: number\n\treadonly edgeScrollEaseDuration: number\n\treadonly edgeScrollSpeed: number\n\treadonly edgeScrollDistance: number\n\treadonly coarsePointerWidth: number\n\treadonly coarseHandleRadius: number\n\treadonly handleRadius: number\n\treadonly longPressDurationMs: number\n\treadonly textShadowLod: number\n\treadonly adjacentShapeMargin: number\n\treadonly flattenImageBoundsExpand: number\n\treadonly flattenImageBoundsPadding: number\n\treadonly laserDelayMs: number\n\t/**\n\t * How long (in milliseconds) to fade all laser scribbles after the session ends.\n\t * The total points across all scribbles will be removed proportionally over this duration.\n\t * Defaults to 500ms (0.5 seconds).\n\t */\n\treadonly laserFadeoutMs: number\n\treadonly maxExportDelayMs: number\n\treadonly tooltipDelayMs: number\n\t/**\n\t * How long should previews created by {@link Editor.createTemporaryAssetPreview} last before\n\t * they expire? Defaults to 3 minutes.\n\t */\n\treadonly temporaryAssetPreviewLifetimeMs: number\n\treadonly actionShortcutsLocation: 'menu' | 'toolbar' | 'swap'\n\treadonly createTextOnCanvasDoubleClick: boolean\n\t/**\n\t * The react provider to use when exporting an image. This is useful if your shapes depend on\n\t * external context providers. By default, this is `React.Fragment`.\n\t */\n\treadonly exportProvider: ComponentType<{ children: React.ReactNode }>\n\t/**\n\t * By default, the toolbar items are accessible via number shortcuts according to their order. To disable this, set this option to false.\n\t */\n\treadonly enableToolbarKeyboardShortcuts: boolean\n\t/**\n\t * The maximum number of fonts that will be loaded while blocking the main rendering of the\n\t * canvas. If there are more than this number of fonts needed, we'll just show the canvas right\n\t * away and let the fonts load in in the background.\n\t */\n\treadonly maxFontsToLoadBeforeRender: number\n\t/**\n\t * If you have a CSP policy that blocks inline styles, you can use this prop to provide a\n\t * nonce to use in the editor's styles.\n\t */\n\treadonly nonce: string | undefined\n\t/**\n\t * Branding name of the app, currently only used for adding aria-label for the application.\n\t */\n\treadonly branding?: string\n\t/**\n\t * Whether to use debounced zoom level for certain rendering optimizations. When true,\n\t * `editor.getDebouncedZoomLevel()` returns a cached zoom value while the camera is moving,\n\t * reducing re-renders. When false, it always returns the current zoom level.\n\t */\n\treadonly debouncedZoom: boolean\n\t/**\n\t * The number of shapes that must be on the page for the debounced zoom level to be used.\n\t * Defaults to 300 shapes.\n\t */\n\treadonly debouncedZoomThreshold: number\n\t/**\n\t * Whether to allow spacebar panning. When true, the spacebar will pan the camera when held down.\n\t * When false, the spacebar will not pan the camera.\n\t */\n\treadonly spacebarPanning: boolean\n\t/**\n\t * The default padding (in pixels) used when zooming to fit content in the viewport.\n\t * This affects methods like `zoomToFit()`, `zoomToSelection()`, and `zoomToBounds()`.\n\t * The actual padding used is the minimum of this value and 28% of the viewport width.\n\t * Defaults to 128 pixels.\n\t */\n\treadonly zoomToFitPadding: number\n\t/**\n\t * The distance (in screen pixels) at which shapes snap to guides and other shapes.\n\t */\n\treadonly snapThreshold: number\n}\n\n/** @public */\nexport const defaultTldrawOptions = {\n\tmaxShapesPerPage: 4000,\n\tmaxFilesAtOnce: 100,\n\tmaxPages: 40,\n\tanimationMediumMs: 320,\n\tfollowChaseViewportSnap: 2,\n\tdoubleClickDurationMs: 450,\n\tmultiClickDurationMs: 200,\n\tcoarseDragDistanceSquared: 36, // 6 squared\n\tdragDistanceSquared: 16, // 4 squared\n\tuiDragDistanceSquared: 16, // 4 squared\n\t// it's really easy to accidentally drag from the toolbar on mobile, so we use a much larger\n\t// threshold than usual here to try and prevent accidental drags.\n\tuiCoarseDragDistanceSquared: 625, // 25 squared\n\tdefaultSvgPadding: 32,\n\tcameraSlideFriction: 0.09,\n\tgridSteps: [\n\t\t{ min: -1, mid: 0.15, step: 64 },\n\t\t{ min: 0.05, mid: 0.375, step: 16 },\n\t\t{ min: 0.15, mid: 1, step: 4 },\n\t\t{ min: 0.7, mid: 2.5, step: 1 },\n\t],\n\tcollaboratorInactiveTimeoutMs: 60000,\n\tcollaboratorIdleTimeoutMs: 3000,\n\tcollaboratorCheckIntervalMs: 1200,\n\tcameraMovingTimeoutMs: 64,\n\thitTestMargin: 8,\n\tedgeScrollDelay: 200,\n\tedgeScrollEaseDuration: 200,\n\tedgeScrollSpeed: 25,\n\tedgeScrollDistance: 8,\n\tcoarsePointerWidth: 12,\n\tcoarseHandleRadius: 20,\n\thandleRadius: 12,\n\tlongPressDurationMs: 500,\n\ttextShadowLod: 0.35,\n\tadjacentShapeMargin: 10,\n\tflattenImageBoundsExpand: 64,\n\tflattenImageBoundsPadding: 16,\n\tlaserDelayMs: 1200,\n\tlaserFadeoutMs: 500,\n\tmaxExportDelayMs: 5000,\n\ttooltipDelayMs: 700,\n\ttemporaryAssetPreviewLifetimeMs: 180000,\n\tactionShortcutsLocation: 'swap',\n\tcreateTextOnCanvasDoubleClick: true,\n\texportProvider: Fragment,\n\tenableToolbarKeyboardShortcuts: true,\n\tmaxFontsToLoadBeforeRender: Infinity,\n\tnonce: undefined,\n\tdebouncedZoom: true,\n\tdebouncedZoomThreshold: 500,\n\tspacebarPanning: true,\n\tzoomToFitPadding: 128,\n\tsnapThreshold: 8,\n} as const satisfies TldrawOptions\n"],
5
- "mappings": "AAAA,SAAwB,gBAAgB;AA6HjC,MAAM,uBAAuB;AAAA,EACnC,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,mBAAmB;AAAA,EACnB,yBAAyB;AAAA,EACzB,uBAAuB;AAAA,EACvB,sBAAsB;AAAA,EACtB,2BAA2B;AAAA;AAAA,EAC3B,qBAAqB;AAAA;AAAA,EACrB,uBAAuB;AAAA;AAAA;AAAA;AAAA,EAGvB,6BAA6B;AAAA;AAAA,EAC7B,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,WAAW;AAAA,IACV,EAAE,KAAK,IAAI,KAAK,MAAM,MAAM,GAAG;AAAA,IAC/B,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,GAAG;AAAA,IAClC,EAAE,KAAK,MAAM,KAAK,GAAG,MAAM,EAAE;AAAA,IAC7B,EAAE,KAAK,KAAK,KAAK,KAAK,MAAM,EAAE;AAAA,EAC/B;AAAA,EACA,+BAA+B;AAAA,EAC/B,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,0BAA0B;AAAA,EAC1B,2BAA2B;AAAA,EAC3B,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,iCAAiC;AAAA,EACjC,yBAAyB;AAAA,EACzB,+BAA+B;AAAA,EAC/B,gBAAgB;AAAA,EAChB,gCAAgC;AAAA,EAChC,4BAA4B;AAAA,EAC5B,OAAO;AAAA,EACP,eAAe;AAAA,EACf,wBAAwB;AAAA,EACxB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,eAAe;AAChB;",
4
+ "sourcesContent": ["import { ComponentType, Fragment } from 'react'\n\n/**\n * Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.\n *\n * @example\n * ```tsx\n * const options: Partial<TldrawOptions> = {\n * maxPages: 3,\n * maxShapesPerPage: 1000,\n * }\n *\n * function MyTldrawComponent() {\n * return <Tldraw options={options} />\n * }\n * ```\n *\n * @public\n */\nexport interface TldrawOptions {\n\treadonly maxShapesPerPage: number\n\treadonly maxFilesAtOnce: number\n\treadonly maxPages: number\n\treadonly animationMediumMs: number\n\treadonly followChaseViewportSnap: number\n\treadonly doubleClickDurationMs: number\n\treadonly multiClickDurationMs: number\n\treadonly coarseDragDistanceSquared: number\n\treadonly dragDistanceSquared: number\n\treadonly uiDragDistanceSquared: number\n\treadonly uiCoarseDragDistanceSquared: number\n\treadonly defaultSvgPadding: number\n\treadonly cameraSlideFriction: number\n\treadonly gridSteps: readonly {\n\t\treadonly min: number\n\t\treadonly mid: number\n\t\treadonly step: number\n\t}[]\n\treadonly collaboratorInactiveTimeoutMs: number\n\treadonly collaboratorIdleTimeoutMs: number\n\treadonly collaboratorCheckIntervalMs: number\n\treadonly cameraMovingTimeoutMs: number\n\treadonly hitTestMargin: number\n\treadonly edgeScrollDelay: number\n\treadonly edgeScrollEaseDuration: number\n\treadonly edgeScrollSpeed: number\n\treadonly edgeScrollDistance: number\n\treadonly coarsePointerWidth: number\n\treadonly coarseHandleRadius: number\n\treadonly handleRadius: number\n\treadonly longPressDurationMs: number\n\treadonly textShadowLod: number\n\treadonly adjacentShapeMargin: number\n\treadonly flattenImageBoundsExpand: number\n\treadonly flattenImageBoundsPadding: number\n\treadonly laserDelayMs: number\n\t/**\n\t * How long (in milliseconds) to fade all laser scribbles after the session ends.\n\t * The total points across all scribbles will be removed proportionally over this duration.\n\t * Defaults to 500ms (0.5 seconds).\n\t */\n\treadonly laserFadeoutMs: number\n\treadonly maxExportDelayMs: number\n\treadonly tooltipDelayMs: number\n\t/**\n\t * How long should previews created by {@link Editor.createTemporaryAssetPreview} last before\n\t * they expire? Defaults to 3 minutes.\n\t */\n\treadonly temporaryAssetPreviewLifetimeMs: number\n\treadonly actionShortcutsLocation: 'menu' | 'toolbar' | 'swap'\n\treadonly createTextOnCanvasDoubleClick: boolean\n\t/**\n\t * The react provider to use when exporting an image. This is useful if your shapes depend on\n\t * external context providers. By default, this is `React.Fragment`.\n\t */\n\treadonly exportProvider: ComponentType<{ children: React.ReactNode }>\n\t/**\n\t * By default, the toolbar items are accessible via number shortcuts according to their order. To disable this, set this option to false.\n\t */\n\treadonly enableToolbarKeyboardShortcuts: boolean\n\t/**\n\t * The maximum number of fonts that will be loaded while blocking the main rendering of the\n\t * canvas. If there are more than this number of fonts needed, we'll just show the canvas right\n\t * away and let the fonts load in in the background.\n\t */\n\treadonly maxFontsToLoadBeforeRender: number\n\t/**\n\t * If you have a CSP policy that blocks inline styles, you can use this prop to provide a\n\t * nonce to use in the editor's styles.\n\t */\n\treadonly nonce: string | undefined\n\t/**\n\t * Branding name of the app, currently only used for adding aria-label for the application.\n\t */\n\treadonly branding?: string\n\t/**\n\t * Whether to use debounced zoom level for certain rendering optimizations. When true,\n\t * `editor.getEfficientZoomLevel()` returns a cached zoom value while the camera is moving,\n\t * reducing re-renders. When false, it always returns the current zoom level.\n\t */\n\treadonly debouncedZoom: boolean\n\t/**\n\t * The number of shapes that must be on the page for the debounced zoom level to be used.\n\t * Defaults to 500 shapes.\n\t */\n\treadonly debouncedZoomThreshold: number\n\t/**\n\t * Whether to allow spacebar panning. When true, the spacebar will pan the camera when held down.\n\t * When false, the spacebar will not pan the camera.\n\t */\n\treadonly spacebarPanning: boolean\n\t/**\n\t * The default padding (in pixels) used when zooming to fit content in the viewport.\n\t * This affects methods like `zoomToFit()`, `zoomToSelection()`, and `zoomToBounds()`.\n\t * The actual padding used is the minimum of this value and 28% of the viewport width.\n\t * Defaults to 128 pixels.\n\t */\n\treadonly zoomToFitPadding: number\n\t/**\n\t * The distance (in screen pixels) at which shapes snap to guides and other shapes.\n\t */\n\treadonly snapThreshold: number\n\t/**\n\t * Whether the quick-zoom brush preserves its screen-pixel size when the user\n\t * zooms the overview. When true, zooming in shrinks the target viewport (higher\n\t * return zoom); zooming out expands it. When false, the brush keeps the original\n\t * viewport's page dimensions regardless of overview zoom changes.\n\t */\n\treadonly quickZoomPreservesScreenBounds: boolean\n}\n\n/** @public */\nexport const defaultTldrawOptions = {\n\tmaxShapesPerPage: 4000,\n\tmaxFilesAtOnce: 100,\n\tmaxPages: 40,\n\tanimationMediumMs: 320,\n\tfollowChaseViewportSnap: 2,\n\tdoubleClickDurationMs: 450,\n\tmultiClickDurationMs: 200,\n\tcoarseDragDistanceSquared: 36, // 6 squared\n\tdragDistanceSquared: 16, // 4 squared\n\tuiDragDistanceSquared: 16, // 4 squared\n\t// it's really easy to accidentally drag from the toolbar on mobile, so we use a much larger\n\t// threshold than usual here to try and prevent accidental drags.\n\tuiCoarseDragDistanceSquared: 625, // 25 squared\n\tdefaultSvgPadding: 32,\n\tcameraSlideFriction: 0.09,\n\tgridSteps: [\n\t\t{ min: -1, mid: 0.15, step: 64 },\n\t\t{ min: 0.05, mid: 0.375, step: 16 },\n\t\t{ min: 0.15, mid: 1, step: 4 },\n\t\t{ min: 0.7, mid: 2.5, step: 1 },\n\t],\n\tcollaboratorInactiveTimeoutMs: 60000,\n\tcollaboratorIdleTimeoutMs: 3000,\n\tcollaboratorCheckIntervalMs: 1200,\n\tcameraMovingTimeoutMs: 64,\n\thitTestMargin: 8,\n\tedgeScrollDelay: 200,\n\tedgeScrollEaseDuration: 200,\n\tedgeScrollSpeed: 25,\n\tedgeScrollDistance: 8,\n\tcoarsePointerWidth: 12,\n\tcoarseHandleRadius: 20,\n\thandleRadius: 12,\n\tlongPressDurationMs: 500,\n\ttextShadowLod: 0.35,\n\tadjacentShapeMargin: 10,\n\tflattenImageBoundsExpand: 64,\n\tflattenImageBoundsPadding: 16,\n\tlaserDelayMs: 1200,\n\tlaserFadeoutMs: 500,\n\tmaxExportDelayMs: 5000,\n\ttooltipDelayMs: 700,\n\ttemporaryAssetPreviewLifetimeMs: 180000,\n\tactionShortcutsLocation: 'swap',\n\tcreateTextOnCanvasDoubleClick: true,\n\texportProvider: Fragment,\n\tenableToolbarKeyboardShortcuts: true,\n\tmaxFontsToLoadBeforeRender: Infinity,\n\tnonce: undefined,\n\tdebouncedZoom: true,\n\tdebouncedZoomThreshold: 500,\n\tspacebarPanning: true,\n\tzoomToFitPadding: 128,\n\tsnapThreshold: 8,\n\tquickZoomPreservesScreenBounds: true,\n} as const satisfies TldrawOptions\n"],
5
+ "mappings": "AAAA,SAAwB,gBAAgB;AAoIjC,MAAM,uBAAuB;AAAA,EACnC,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,mBAAmB;AAAA,EACnB,yBAAyB;AAAA,EACzB,uBAAuB;AAAA,EACvB,sBAAsB;AAAA,EACtB,2BAA2B;AAAA;AAAA,EAC3B,qBAAqB;AAAA;AAAA,EACrB,uBAAuB;AAAA;AAAA;AAAA;AAAA,EAGvB,6BAA6B;AAAA;AAAA,EAC7B,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB,WAAW;AAAA,IACV,EAAE,KAAK,IAAI,KAAK,MAAM,MAAM,GAAG;AAAA,IAC/B,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,GAAG;AAAA,IAClC,EAAE,KAAK,MAAM,KAAK,GAAG,MAAM,EAAE;AAAA,IAC7B,EAAE,KAAK,KAAK,KAAK,KAAK,MAAM,EAAE;AAAA,EAC/B;AAAA,EACA,+BAA+B;AAAA,EAC/B,2BAA2B;AAAA,EAC3B,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,0BAA0B;AAAA,EAC1B,2BAA2B;AAAA,EAC3B,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,iCAAiC;AAAA,EACjC,yBAAyB;AAAA,EACzB,+BAA+B;AAAA,EAC/B,gBAAgB;AAAA,EAChB,gCAAgC;AAAA,EAChC,4BAA4B;AAAA,EAC5B,OAAO;AAAA,EACP,eAAe;AAAA,EACf,wBAAwB;AAAA,EACxB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,gCAAgC;AACjC;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "4.4.0-canary.afdcafe834b3";
1
+ const version = "4.4.0-canary.b5c642789999";
2
2
  const publishDates = {
3
3
  major: "2025-09-18T14:39:22.803Z",
4
- minor: "2026-02-04T08:49:09.694Z",
5
- patch: "2026-02-04T08:49:09.694Z"
4
+ minor: "2026-02-11T11:56:14.353Z",
5
+ patch: "2026-02-11T11:56:14.353Z"
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 = '4.4.0-canary.afdcafe834b3'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-02-04T08:49:09.694Z',\n\tpatch: '2026-02-04T08:49:09.694Z',\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 = '4.4.0-canary.b5c642789999'\nexport const publishDates = {\n\tmajor: '2025-09-18T14:39:22.803Z',\n\tminor: '2026-02-11T11:56:14.353Z',\n\tpatch: '2026-02-11T11:56:14.353Z',\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
@@ -22,14 +22,25 @@
22
22
  --tl-radius-3: 9px;
23
23
  --tl-radius-4: 11px;
24
24
 
25
- /* Canvas z-index */
25
+ /*
26
+ * Canvas z-index
27
+ *
28
+ * .tl-canvas has `contain: strict` which creates its own stacking context.
29
+ * background, grid, shapes, overlays, and blocker are all children of .tl-canvas,
30
+ * so their z-indices are relative to the canvas, not the container.
31
+ *
32
+ * watermark and canvas-in-front are siblings of .tl-canvas (outside it),
33
+ * so they compete with the UI layer (.tlui-layout at z-index 300).
34
+ * canvas-in-front must be below the UI layer (300) to sit between
35
+ * the canvas and the UI.
36
+ */
26
37
  --tl-layer-canvas-hidden: -999999;
27
38
  --tl-layer-canvas-background: 100;
28
39
  --tl-layer-canvas-grid: 150;
29
40
  --tl-layer-watermark: 200;
41
+ --tl-layer-canvas-in-front: 250;
30
42
  --tl-layer-canvas-shapes: 300;
31
43
  --tl-layer-canvas-overlays: 500;
32
- --tl-layer-canvas-in-front: 600;
33
44
  --tl-layer-canvas-blocker: 10000;
34
45
 
35
46
  /* Canvas overlays z-index */
@@ -60,6 +71,7 @@
60
71
 
61
72
  /* Misc */
62
73
  --tl-zoom: 1;
74
+ --tl-tab-size: 2;
63
75
 
64
76
  /* Cursor SVGs */
65
77
  --tl-cursor-none: none;
@@ -146,6 +158,8 @@
146
158
  --tl-color-selection-fill: hsl(210, 100%, 56%, 24%);
147
159
  --tl-color-selection-stroke: hsl(214, 84%, 56%);
148
160
  --tl-color-background: hsl(210, 20%, 98%);
161
+ /* if you ever update --tl-color-background, update the hardcoded values in theme-init.js and globals.css
162
+ they're there to make sure the background color matches the user's preference before the actual app loads*/
149
163
  --tl-color-brush-fill: hsl(0, 0%, 56%, 10.2%);
150
164
  --tl-color-brush-stroke: hsl(0, 0%, 56%, 25.1%);
151
165
  --tl-color-grid: hsl(0, 0%, 43%);
@@ -202,6 +216,8 @@
202
216
  --tl-color-selection-fill: hsl(209, 100%, 57%, 20%);
203
217
  --tl-color-selection-stroke: hsl(214, 84%, 56%);
204
218
  --tl-color-background: hsl(240, 5%, 6.5%);
219
+ /* if you ever update --tl-color-background, update the hardcoded values in theme-init.js and globals.css
220
+ they're there to make sure the background color matches the user's preference before the actual app loads*/
205
221
  --tl-color-brush-fill: hsl(0, 0%, 71%, 5.1%);
206
222
  --tl-color-brush-stroke: hsl(0, 0%, 71%, 25.1%);
207
223
  --tl-color-grid: hsl(0, 0%, 40%);
@@ -895,6 +911,10 @@ input,
895
911
  /* white-space: break-spaces; */
896
912
  }
897
913
 
914
+ .tl-rich-text {
915
+ tab-size: var(--tl-tab-size, 2);
916
+ }
917
+
898
918
  .tl-rich-text p {
899
919
  margin: 0;
900
920
  /* Depending on the extensions, <p> tags can be empty, without a <br />. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "tldraw infinite canvas SDK (editor).",
4
- "version": "4.4.0-canary.afdcafe834b3",
4
+ "version": "4.4.0-canary.b5c642789999",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -43,19 +43,18 @@
43
43
  "prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
44
44
  "postpack": "../../internal/scripts/postpack.sh",
45
45
  "pack-tarball": "yarn pack",
46
- "lint": "yarn run -T tsx ../../internal/scripts/lint.ts",
47
- "context": "yarn run -T tsx ../../internal/scripts/context.ts"
46
+ "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
48
47
  },
49
48
  "dependencies": {
50
49
  "@tiptap/core": "^3.12.1",
51
50
  "@tiptap/pm": "^3.12.1",
52
51
  "@tiptap/react": "^3.12.1",
53
- "@tldraw/state": "4.4.0-canary.afdcafe834b3",
54
- "@tldraw/state-react": "4.4.0-canary.afdcafe834b3",
55
- "@tldraw/store": "4.4.0-canary.afdcafe834b3",
56
- "@tldraw/tlschema": "4.4.0-canary.afdcafe834b3",
57
- "@tldraw/utils": "4.4.0-canary.afdcafe834b3",
58
- "@tldraw/validate": "4.4.0-canary.afdcafe834b3",
52
+ "@tldraw/state": "4.4.0-canary.b5c642789999",
53
+ "@tldraw/state-react": "4.4.0-canary.b5c642789999",
54
+ "@tldraw/store": "4.4.0-canary.b5c642789999",
55
+ "@tldraw/tlschema": "4.4.0-canary.b5c642789999",
56
+ "@tldraw/utils": "4.4.0-canary.b5c642789999",
57
+ "@tldraw/validate": "4.4.0-canary.b5c642789999",
59
58
  "@use-gesture/react": "^10.3.1",
60
59
  "classnames": "^2.5.1",
61
60
  "eventemitter3": "^4.0.7",
@@ -5,6 +5,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useRef } from 'react'
5
5
  import { ShapeUtil } from '../editor/shapes/ShapeUtil'
6
6
  import { useEditor } from '../hooks/useEditor'
7
7
  import { useEditorComponents } from '../hooks/useEditorComponents'
8
+ import { useShapeCulling } from '../hooks/useShapeCulling'
8
9
  import { Mat } from '../primitives/Mat'
9
10
  import { areShapesContentEqual } from '../utils/areShapesContentEqual'
10
11
  import { setStyleProperty } from '../utils/dom'
@@ -57,7 +58,6 @@ export const Shape = memo(function Shape({
57
58
  height: 0,
58
59
  x: 0,
59
60
  y: 0,
60
- isCulled: false,
61
61
  })
62
62
 
63
63
  useQuickReactor(
@@ -118,22 +118,21 @@ export const Shape = memo(function Shape({
118
118
  setStyleProperty(bgContainer, 'z-index', backgroundIndex)
119
119
  }, [opacity, index, backgroundIndex])
120
120
 
121
- useQuickReactor(
122
- 'set display',
123
- () => {
124
- const shape = editor.getShape(id)
125
- if (!shape) return // probably the shape was just deleted
121
+ // Register container refs with the centralized culling context.
122
+ // This runs on mount and handles initial display state.
123
+ const { register, unregister } = useShapeCulling()
124
+ useLayoutEffect(() => {
125
+ const container = containerRef.current
126
+ if (!container) return
126
127
 
127
- const culledShapes = editor.getCulledShapes()
128
- const isCulled = culledShapes.has(id)
129
- if (isCulled !== memoizedStuffRef.current.isCulled) {
130
- setStyleProperty(containerRef.current, 'display', isCulled ? 'none' : 'block')
131
- setStyleProperty(bgContainerRef.current, 'display', isCulled ? 'none' : 'block')
132
- memoizedStuffRef.current.isCulled = isCulled
133
- }
134
- },
135
- [editor]
136
- )
128
+ // Check initial culling state and register with the context
129
+ const isCulled = editor.getCulledShapes().has(id)
130
+ register(id, container, bgContainerRef.current, isCulled)
131
+
132
+ return () => {
133
+ unregister(id)
134
+ }
135
+ }, [editor, id, register, unregister])
137
136
  const annotateError = useCallback(
138
137
  (error: any) => editor.annotateError(error, { origin: 'shape', willCrashApp: false }),
139
138
  [editor]
@@ -1,7 +1,7 @@
1
1
  import { useComputed, useQuickReactor } from '@tldraw/state-react'
2
2
  import { createComputedCache } from '@tldraw/store'
3
3
  import { TLShape, TLShapeId } from '@tldraw/tlschema'
4
- import { dedupe, isEqual } from '@tldraw/utils'
4
+ import { dedupe } from '@tldraw/utils'
5
5
  import { memo, useEffect, useRef } from 'react'
6
6
  import { Editor } from '../../editor/Editor'
7
7
  import { TLIndicatorPath } from '../../editor/shapes/ShapeUtil'
@@ -14,6 +14,50 @@ interface CollaboratorIndicatorData {
14
14
  shapeIds: TLShapeId[]
15
15
  }
16
16
 
17
+ interface RenderData {
18
+ idsToDisplay: Set<TLShapeId>
19
+ renderingShapeIds: Set<TLShapeId>
20
+ hintingShapeIds: TLShapeId[]
21
+ collaboratorIndicators: CollaboratorIndicatorData[]
22
+ }
23
+
24
+ function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
25
+ if (a.size !== b.size) return false
26
+ for (const item of a) {
27
+ if (!b.has(item)) return false
28
+ }
29
+ return true
30
+ }
31
+
32
+ function arraysEqual<T>(a: readonly T[], b: readonly T[]): boolean {
33
+ if (a.length !== b.length) return false
34
+ for (let i = 0; i < a.length; i++) {
35
+ if (a[i] !== b[i]) return false
36
+ }
37
+ return true
38
+ }
39
+
40
+ function collaboratorIndicatorsEqual(
41
+ a: CollaboratorIndicatorData[],
42
+ b: CollaboratorIndicatorData[]
43
+ ): boolean {
44
+ if (a.length !== b.length) return false
45
+ for (let i = 0; i < a.length; i++) {
46
+ if (a[i].color !== b[i].color) return false
47
+ if (!arraysEqual(a[i].shapeIds, b[i].shapeIds)) return false
48
+ }
49
+ return true
50
+ }
51
+
52
+ function renderDataEqual(a: RenderData, b: RenderData): boolean {
53
+ return (
54
+ setsEqual(a.idsToDisplay, b.idsToDisplay) &&
55
+ setsEqual(a.renderingShapeIds, b.renderingShapeIds) &&
56
+ arraysEqual(a.hintingShapeIds, b.hintingShapeIds) &&
57
+ collaboratorIndicatorsEqual(a.collaboratorIndicators, b.collaboratorIndicators)
58
+ )
59
+ }
60
+
17
61
  const indicatorPathCache = createComputedCache(
18
62
  'indicatorPath',
19
63
  (editor: Editor, shape: TLShape) => {
@@ -162,7 +206,7 @@ export const CanvasShapeIndicators = memo(function CanvasShapeIndicators() {
162
206
  collaboratorIndicators,
163
207
  }
164
208
  },
165
- { isEqual: isEqual },
209
+ { isEqual: renderDataEqual },
166
210
  [editor, activePeerIds$]
167
211
  )
168
212
 
@@ -16,6 +16,7 @@ import { useGestureEvents } from '../../hooks/useGestureEvents'
16
16
  import { useHandleEvents } from '../../hooks/useHandleEvents'
17
17
  import { useSharedSafeId } from '../../hooks/useSafeId'
18
18
  import { useScreenBounds } from '../../hooks/useScreenBounds'
19
+ import { ShapeCullingProvider, useShapeCulling } from '../../hooks/useShapeCulling'
19
20
  import { Box } from '../../primitives/Box'
20
21
  import { Mat } from '../../primitives/Mat'
21
22
  import { Vec } from '../../primitives/Vec'
@@ -428,18 +429,39 @@ function ReflowIfNeeded() {
428
429
  return null
429
430
  }
430
431
 
432
+ /**
433
+ * Centralized culling controller that updates shape container visibility.
434
+ * This single reactor replaces per-shape subscriptions for O(1) instead of O(N) subscriptions.
435
+ */
436
+ function CullingController() {
437
+ const editor = useEditor()
438
+ const { updateCulling } = useShapeCulling()
439
+
440
+ useQuickReactor(
441
+ 'update shape culling',
442
+ () => {
443
+ const culledShapes = editor.getCulledShapes()
444
+ updateCulling(culledShapes)
445
+ },
446
+ [editor, updateCulling]
447
+ )
448
+
449
+ return null
450
+ }
451
+
431
452
  function ShapesToDisplay() {
432
453
  const editor = useEditor()
433
454
 
434
455
  const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
435
456
 
436
457
  return (
437
- <>
458
+ <ShapeCullingProvider>
438
459
  {renderingShapes.map((result) => (
439
460
  <Shape key={result.id + '_shape'} {...result} />
440
461
  ))}
462
+ <CullingController />
441
463
  {tlenv.isSafari && <ReflowIfNeeded />}
442
- </>
464
+ </ShapeCullingProvider>
443
465
  )
444
466
  }
445
467
 
@@ -153,7 +153,13 @@ import { SpatialIndexManager } from './managers/SpatialIndexManager/SpatialIndex
153
153
  import { TextManager } from './managers/TextManager/TextManager'
154
154
  import { TickManager } from './managers/TickManager/TickManager'
155
155
  import { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager'
156
- import { ShapeUtil, TLEditStartInfo, TLGeometryOpts, TLResizeMode } from './shapes/ShapeUtil'
156
+ import {
157
+ ShapeUtil,
158
+ TLEditStartInfo,
159
+ TLGeometryOpts,
160
+ TLResizeMode,
161
+ TLShapeUtilCanBindOpts,
162
+ } from './shapes/ShapeUtil'
157
163
  import { RootState } from './tools/RootState'
158
164
  import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
159
165
  import { TLContent } from './types/clipboard-types'
@@ -443,6 +449,7 @@ export class Editor extends EventEmitter<TLEventMap> {
443
449
  const deletedShapeIds = new Set<TLShapeId>()
444
450
  const invalidParents = new Set<TLShapeId>()
445
451
  let invalidBindingTypes = new Set<TLBinding['type']>()
452
+
446
453
  this.disposables.add(
447
454
  this.sideEffects.registerOperationCompleteHandler(() => {
448
455
  // this needs to be cleared here because further effects may delete more shapes
@@ -4180,23 +4187,25 @@ export class Editor extends EventEmitter<TLEventMap> {
4180
4187
  // unmount / remount in the DOM, which is expensive; and computing visibility is
4181
4188
  // also expensive in large projects. For this reason, we use a second bounding
4182
4189
  // box just for rendering, and we only update after the camera stops moving.
4183
- private _cameraState = atom('camera state', 'idle' as 'idle' | 'moving')
4184
4190
  private _cameraStateTimeoutRemaining = 0
4185
4191
  private _decayCameraStateTimeout(elapsed: number) {
4186
4192
  this._cameraStateTimeoutRemaining -= elapsed
4187
4193
  if (this._cameraStateTimeoutRemaining > 0) return
4188
4194
  this.off('tick', this._decayCameraStateTimeout)
4189
- this._cameraState.set('idle')
4195
+ this._setCameraState('idle')
4190
4196
  }
4191
4197
  private _tickCameraState() {
4192
4198
  // always reset the timeout
4193
4199
  this._cameraStateTimeoutRemaining = this.options.cameraMovingTimeoutMs
4194
4200
  // If the state is idle, then start the tick
4195
- if (this._cameraState.__unsafe__getWithoutCapture() !== 'idle') return
4196
- this._cameraState.set('moving')
4201
+ if (this.getInstanceState().cameraState !== 'idle') return
4202
+ this._setCameraState('moving')
4197
4203
  this._debouncedZoomLevel.set(unsafe__withoutCapture(() => this.getCamera().z))
4198
4204
  this.on('tick', this._decayCameraStateTimeout)
4199
4205
  }
4206
+ private _setCameraState(cameraState: 'idle' | 'moving') {
4207
+ this.updateInstanceState({ cameraState }, { history: 'ignore' })
4208
+ }
4200
4209
 
4201
4210
  /**
4202
4211
  * Whether the camera is moving or idle.
@@ -4209,7 +4218,7 @@ export class Editor extends EventEmitter<TLEventMap> {
4209
4218
  * @public
4210
4219
  */
4211
4220
  getCameraState() {
4212
- return this._cameraState.get()
4221
+ return this.getInstanceState().cameraState
4213
4222
  }
4214
4223
 
4215
4224
  /**
@@ -5481,7 +5490,7 @@ export class Editor extends EventEmitter<TLEventMap> {
5481
5490
  * @param bounds - The bounds to search within.
5482
5491
  * @returns Unordered set of shape IDs within the given bounds.
5483
5492
  *
5484
- * @internal
5493
+ * @public
5485
5494
  */
5486
5495
  getShapeIdsInsideBounds(bounds: Box): Set<TLShapeId> {
5487
5496
  return this._spatialIndex.getShapeIdsInsideBounds(bounds)
@@ -6246,7 +6255,13 @@ export class Editor extends EventEmitter<TLEventMap> {
6246
6255
  const toShapeType = typeof toShape === 'string' ? toShape : toShape.type
6247
6256
  const bindingType = typeof binding === 'string' ? binding : binding.type
6248
6257
 
6249
- const canBindOpts = { fromShapeType, toShapeType, bindingType } as const
6258
+ const canBindOpts: TLShapeUtilCanBindOpts = {
6259
+ fromShape: typeof fromShape === 'string' ? { type: fromShape } : fromShape,
6260
+ toShape: typeof toShape === 'string' ? { type: toShape } : toShape,
6261
+ bindingType,
6262
+ fromShapeType,
6263
+ toShapeType,
6264
+ }
6250
6265
 
6251
6266
  if (fromShapeType === toShapeType) {
6252
6267
  return this.getShapeUtil(fromShapeType).canBind(canBindOpts)
@@ -7839,6 +7854,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7839
7854
  initialShape: options.initialShape,
7840
7855
  initialBounds: options.initialBounds,
7841
7856
  isAspectRatioLocked: options.isAspectRatioLocked,
7857
+ initialPageTransform: options.initialPageTransform,
7842
7858
  })
7843
7859
 
7844
7860
  // then if the shape is flipped in one axis only, we need to apply an extra rotation
@@ -10421,9 +10437,10 @@ export class Editor extends EventEmitter<TLEventMap> {
10421
10437
  if (inputs.getIsPinching()) return
10422
10438
 
10423
10439
  if (!inputs.getIsEditing()) {
10424
- if (!this._selectedShapeIdsAtPointerDown.length) {
10425
- this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10426
- }
10440
+ // Always capture the current selection when pinch starts.
10441
+ // This ensures Safari (which uses gesture events instead of wheel)
10442
+ // doesn't restore a stale selection from an earlier pointer_down.
10443
+ this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds]
10427
10444
 
10428
10445
  this._didPinch = true
10429
10446
 
@@ -10734,6 +10751,11 @@ export class Editor extends EventEmitter<TLEventMap> {
10734
10751
  this.setCurrentTool(this._restoreToolId)
10735
10752
  }
10736
10753
  }
10754
+
10755
+ // Clear the stashed selection so the next pinch captures fresh state.
10756
+ // This fixes Safari pinch zoom restoring outdated selections.
10757
+ this._selectedShapeIdsAtPointerDown = []
10758
+
10737
10759
  break
10738
10760
  }
10739
10761
  }
@@ -1,5 +1,5 @@
1
1
  import { computed, isUninitialized } from '@tldraw/state'
2
- import { TLShapeId } from '@tldraw/tlschema'
2
+ import { TLShape, TLShapeId } from '@tldraw/tlschema'
3
3
  import { Editor } from '../Editor'
4
4
 
5
5
  /**
@@ -9,34 +9,56 @@ import { Editor } from '../Editor'
9
9
  * @returns Incremental derivation of non visible shapes.
10
10
  */
11
11
  export function notVisibleShapes(editor: Editor) {
12
+ const emptySet = new Set<TLShapeId>()
13
+
12
14
  return computed<Set<TLShapeId>>('notVisibleShapes', function (prevValue) {
13
- const allShapeIds = editor.getCurrentPageShapeIds()
15
+ const allShapes = editor.getCurrentPageShapes()
14
16
  const viewportPageBounds = editor.getViewportPageBounds()
15
17
  const visibleIds = editor.getShapeIdsInsideBounds(viewportPageBounds)
16
18
 
17
- const nextValue = new Set<TLShapeId>()
18
-
19
- // Non-visible shapes are all shapes minus visible shapes
20
- for (const id of allShapeIds) {
21
- if (!visibleIds.has(id)) {
22
- const shape = editor.getShape(id)
23
- if (!shape) continue
19
+ let shape: TLShape | undefined
24
20
 
25
- const canCull = editor.getShapeUtil(shape.type).canCull(shape)
26
- if (!canCull) continue
27
-
28
- nextValue.add(id)
21
+ // Fast path: if all shapes are visible, return empty set
22
+ if (visibleIds.size === allShapes.length) {
23
+ if (isUninitialized(prevValue) || prevValue.size > 0) {
24
+ return emptySet
29
25
  }
26
+ return prevValue
30
27
  }
31
28
 
32
- if (isUninitialized(prevValue) || prevValue.size !== nextValue.size) {
29
+ // First run: compute from scratch
30
+ if (isUninitialized(prevValue)) {
31
+ const nextValue = new Set<TLShapeId>()
32
+ for (let i = 0; i < allShapes.length; i++) {
33
+ shape = allShapes[i]
34
+ if (visibleIds.has(shape.id)) continue
35
+ if (!editor.getShapeUtil(shape.type).canCull(shape)) continue
36
+ nextValue.add(shape.id)
37
+ }
33
38
  return nextValue
34
39
  }
35
40
 
36
- for (const prev of prevValue) {
37
- if (!nextValue.has(prev)) return nextValue
41
+ // Subsequent runs: single pass to collect IDs and detect changes
42
+ const notVisibleIds: TLShapeId[] = []
43
+ for (let i = 0; i < allShapes.length; i++) {
44
+ shape = allShapes[i]
45
+ if (visibleIds.has(shape.id)) continue
46
+ if (!editor.getShapeUtil(shape.type).canCull(shape)) continue
47
+ notVisibleIds.push(shape.id)
48
+ }
49
+
50
+ // Check if the result changed
51
+ if (notVisibleIds.length === prevValue.size) {
52
+ let same = true
53
+ for (let i = 0; i < notVisibleIds.length; i++) {
54
+ if (!prevValue.has(notVisibleIds[i])) {
55
+ same = false
56
+ break
57
+ }
58
+ }
59
+ if (same) return prevValue
38
60
  }
39
61
 
40
- return prevValue
62
+ return new Set(notVisibleIds)
41
63
  })
42
64
  }
@@ -240,41 +240,6 @@ describe('EdgeScrollManager', () => {
240
240
  })
241
241
  })
242
242
 
243
- describe('camera movement conditions', () => {
244
- it('should not move camera when not dragging', () => {
245
- editor.inputs.setIsDragging(false)
246
- mockInputs.setCurrentScreenPoint(new Vec(5, 300))
247
-
248
- edgeScrollManager.updateEdgeScrolling(300)
249
-
250
- expect(editor.setCamera).not.toHaveBeenCalled()
251
- })
252
-
253
- it('should not move camera when panning', () => {
254
- editor.inputs.setIsPanning(true)
255
- mockInputs.setCurrentScreenPoint(new Vec(5, 300))
256
-
257
- edgeScrollManager.updateEdgeScrolling(300)
258
-
259
- expect(editor.setCamera).not.toHaveBeenCalled()
260
- })
261
-
262
- it('should not move camera when camera is locked', () => {
263
- editor.getCameraOptions.mockReturnValue({
264
- isLocked: true,
265
- panSpeed: 1,
266
- zoomSpeed: 1,
267
- zoomSteps: [1],
268
- wheelBehavior: 'pan' as const,
269
- })
270
- mockInputs.setCurrentScreenPoint(new Vec(5, 300))
271
-
272
- edgeScrollManager.updateEdgeScrolling(300)
273
-
274
- expect(editor.setCamera).not.toHaveBeenCalled()
275
- })
276
- })
277
-
278
243
  describe('camera movement calculation', () => {
279
244
  it('should calculate scroll speed based on user preference', () => {
280
245
  editor.user.getEdgeScrollSpeed.mockReturnValue(2)
@@ -21,6 +21,9 @@ export class EdgeScrollManager {
21
21
  */
22
22
  updateEdgeScrolling(elapsed: number) {
23
23
  const { editor } = this
24
+
25
+ if (editor.getCameraOptions().isLocked) return
26
+
24
27
  const edgeScrollProximityFactor = this.getEdgeScroll()
25
28
  if (edgeScrollProximityFactor.x === 0 && edgeScrollProximityFactor.y === 0) {
26
29
  if (this._isEdgeScrolling) {
@@ -106,15 +109,8 @@ export class EdgeScrollManager {
106
109
  * @public
107
110
  */
108
111
  private moveCameraWhenCloseToEdge(proximityFactor: { x: number; y: number }) {
109
- const { editor } = this
110
- if (
111
- !editor.inputs.getIsDragging() ||
112
- editor.inputs.getIsPanning() ||
113
- editor.getCameraOptions().isLocked
114
- )
115
- return
116
-
117
112
  if (proximityFactor.x === 0 && proximityFactor.y === 0) return
113
+ const { editor } = this
118
114
 
119
115
  const screenBounds = editor.getViewportScreenBounds()
120
116
 
@@ -35,17 +35,31 @@ export interface TLShapeUtilConstructor<T extends TLShape, U extends ShapeUtil<T
35
35
 
36
36
  /**
37
37
  * Options passed to {@link ShapeUtil.canBind}. A binding that could be made. At least one of
38
- * `fromShapeType` or `toShapeType` will belong to this shape util.
38
+ * `fromShape` or `toShape` will belong to this shape util.
39
+ *
40
+ * The shapes may be full {@link @tldraw/tlschema#TLShape} objects when available, or just
41
+ * `{ type }` stubs when the shape hasn't been created yet (e.g. during arrow creation). Use
42
+ * `'id' in shape` to check whether the full shape is available.
39
43
  *
40
44
  * @public
41
45
  */
42
46
  export interface TLShapeUtilCanBindOpts<Shape extends TLShape = TLShape> {
43
- /** The type of shape referenced by the `fromId` of the binding. */
44
- fromShapeType: TLShape['type']
45
- /** The type of shape referenced by the `toId` of the binding. */
46
- toShapeType: TLShape['type']
47
+ /** The shape referenced by the `fromId` of the binding, or a `{ type }` stub if unavailable. */
48
+ fromShape: TLShape | { type: TLShape['type'] }
49
+ /** The shape referenced by the `toId` of the binding, or a `{ type }` stub if unavailable. */
50
+ toShape: TLShape | { type: TLShape['type'] }
47
51
  /** The type of binding. */
48
52
  bindingType: string
53
+ /**
54
+ * The type of shape referenced by the `fromId` of the binding.
55
+ * @deprecated Use `fromShape.type` instead.
56
+ */
57
+ fromShapeType: TLShape['type']
58
+ /**
59
+ * The type of shape referenced by the `toId` of the binding.
60
+ * @deprecated Use `toShape.type` instead.
61
+ */
62
+ toShapeType: TLShape['type']
49
63
  }
50
64
 
51
65
  /**