@tldraw/editor 4.3.0 → 4.4.0-canary.09e80a09d230

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/dist-cjs/index.d.ts +180 -11
  3. package/dist-cjs/index.js +3 -1
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/LiveCollaborators.js +14 -24
  6. package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +201 -0
  8. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +7 -0
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +30 -16
  10. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  11. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +3 -1
  12. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  13. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js +13 -1
  14. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js.map +2 -2
  15. package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
  16. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  17. package/dist-cjs/lib/editor/Editor.js +58 -6
  18. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  19. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js +13 -21
  20. package/dist-cjs/lib/editor/derivations/notVisibleShapes.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +378 -89
  22. package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js +144 -0
  24. package/dist-cjs/lib/editor/managers/SpatialIndexManager/RBushIndex.js.map +7 -0
  25. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js +180 -0
  26. package/dist-cjs/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.js.map +7 -0
  27. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +8 -3
  28. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  29. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +29 -0
  30. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  31. package/dist-cjs/lib/hooks/usePeerIds.js +29 -0
  32. package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
  33. package/dist-cjs/lib/options.js +1 -0
  34. package/dist-cjs/lib/options.js.map +2 -2
  35. package/dist-cjs/lib/utils/collaboratorState.js +42 -0
  36. package/dist-cjs/lib/utils/collaboratorState.js.map +7 -0
  37. package/dist-cjs/version.js +3 -3
  38. package/dist-cjs/version.js.map +1 -1
  39. package/dist-esm/index.d.mts +180 -11
  40. package/dist-esm/index.mjs +3 -1
  41. package/dist-esm/index.mjs.map +2 -2
  42. package/dist-esm/lib/components/LiveCollaborators.mjs +17 -24
  43. package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
  44. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +181 -0
  45. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +7 -0
  46. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +30 -16
  47. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  48. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +3 -1
  49. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  50. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs +13 -1
  51. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs.map +2 -2
  52. package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
  53. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  54. package/dist-esm/lib/editor/Editor.mjs +58 -6
  55. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  56. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs +13 -21
  57. package/dist-esm/lib/editor/derivations/notVisibleShapes.mjs.map +2 -2
  58. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +378 -89
  59. package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
  60. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs +114 -0
  61. package/dist-esm/lib/editor/managers/SpatialIndexManager/RBushIndex.mjs.map +7 -0
  62. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs +160 -0
  63. package/dist-esm/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.mjs.map +7 -0
  64. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +8 -3
  65. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  66. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +29 -0
  67. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  68. package/dist-esm/lib/hooks/usePeerIds.mjs +33 -1
  69. package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
  70. package/dist-esm/lib/options.mjs +1 -0
  71. package/dist-esm/lib/options.mjs.map +2 -2
  72. package/dist-esm/lib/utils/collaboratorState.mjs +22 -0
  73. package/dist-esm/lib/utils/collaboratorState.mjs.map +7 -0
  74. package/dist-esm/version.mjs +3 -3
  75. package/dist-esm/version.mjs.map +1 -1
  76. package/editor.css +6 -0
  77. package/package.json +10 -8
  78. package/src/index.ts +3 -0
  79. package/src/lib/components/LiveCollaborators.tsx +26 -37
  80. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +244 -0
  81. package/src/lib/components/default-components/DefaultCanvas.tsx +16 -6
  82. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +6 -1
  83. package/src/lib/components/default-components/DefaultShapeIndicators.tsx +16 -1
  84. package/src/lib/config/TLUserPreferences.test.ts +1 -0
  85. package/src/lib/config/TLUserPreferences.ts +8 -0
  86. package/src/lib/editor/Editor.ts +84 -6
  87. package/src/lib/editor/derivations/notVisibleShapes.ts +15 -41
  88. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +491 -106
  89. package/src/lib/editor/managers/SpatialIndexManager/RBushIndex.ts +144 -0
  90. package/src/lib/editor/managers/SpatialIndexManager/SpatialIndexManager.ts +214 -0
  91. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +24 -0
  92. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
  93. package/src/lib/editor/shapes/ShapeUtil.ts +44 -0
  94. package/src/lib/hooks/usePeerIds.ts +46 -1
  95. package/src/lib/options.ts +7 -0
  96. package/src/lib/utils/collaboratorState.ts +54 -0
  97. package/src/version.ts +3 -3
  98. package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +0 -621
@@ -23,33 +23,25 @@ __export(notVisibleShapes_exports, {
23
23
  module.exports = __toCommonJS(notVisibleShapes_exports);
24
24
  var import_state = require("@tldraw/state");
25
25
  function notVisibleShapes(editor) {
26
- return (0, import_state.computed)("notVisibleShapes", function updateNotVisibleShapes(prevValue) {
27
- const shapeIds = editor.getCurrentPageShapeIds();
28
- const nextValue = /* @__PURE__ */ new Set();
26
+ return (0, import_state.computed)("notVisibleShapes", function(prevValue) {
27
+ const allShapeIds = editor.getCurrentPageShapeIds();
29
28
  const viewportPageBounds = editor.getViewportPageBounds();
30
- const viewMinX = viewportPageBounds.minX;
31
- const viewMinY = viewportPageBounds.minY;
32
- const viewMaxX = viewportPageBounds.maxX;
33
- const viewMaxY = viewportPageBounds.maxY;
34
- for (const id of shapeIds) {
35
- const pageBounds = editor.getShapePageBounds(id);
36
- if (pageBounds !== void 0 && pageBounds.maxX >= viewMinX && pageBounds.minX <= viewMaxX && pageBounds.maxY >= viewMinY && pageBounds.minY <= viewMaxY) {
37
- continue;
29
+ const visibleIds = editor.getShapeIdsInsideBounds(viewportPageBounds);
30
+ const nextValue = /* @__PURE__ */ new Set();
31
+ for (const id of allShapeIds) {
32
+ if (!visibleIds.has(id)) {
33
+ const shape = editor.getShape(id);
34
+ if (!shape) continue;
35
+ const canCull = editor.getShapeUtil(shape.type).canCull(shape);
36
+ if (!canCull) continue;
37
+ nextValue.add(id);
38
38
  }
39
- const shape = editor.getShape(id);
40
- if (!shape) continue;
41
- const canCull = editor.getShapeUtil(shape.type).canCull(shape);
42
- if (!canCull) continue;
43
- nextValue.add(id);
44
39
  }
45
- if ((0, import_state.isUninitialized)(prevValue)) {
40
+ if ((0, import_state.isUninitialized)(prevValue) || prevValue.size !== nextValue.size) {
46
41
  return nextValue;
47
42
  }
48
- if (prevValue.size !== nextValue.size) return nextValue;
49
43
  for (const prev of prevValue) {
50
- if (!nextValue.has(prev)) {
51
- return nextValue;
52
- }
44
+ if (!nextValue.has(prev)) return nextValue;
53
45
  }
54
46
  return prevValue;
55
47
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/editor/derivations/notVisibleShapes.ts"],
4
- "sourcesContent": ["import { computed, isUninitialized } from '@tldraw/state'\nimport { TLShapeId } from '@tldraw/tlschema'\nimport { Editor } from '../Editor'\n\n/**\n * Non visible shapes are shapes outside of the viewport page bounds.\n *\n * @param editor - Instance of the tldraw Editor.\n * @returns Incremental derivation of non visible shapes.\n */\nexport function notVisibleShapes(editor: Editor) {\n\treturn computed<Set<TLShapeId>>('notVisibleShapes', function updateNotVisibleShapes(prevValue) {\n\t\tconst shapeIds = editor.getCurrentPageShapeIds()\n\t\tconst nextValue = new Set<TLShapeId>()\n\n\t\t// Extract viewport bounds once to avoid repeated property access\n\t\tconst viewportPageBounds = editor.getViewportPageBounds()\n\t\tconst viewMinX = viewportPageBounds.minX\n\t\tconst viewMinY = viewportPageBounds.minY\n\t\tconst viewMaxX = viewportPageBounds.maxX\n\t\tconst viewMaxY = viewportPageBounds.maxY\n\n\t\tfor (const id of shapeIds) {\n\t\t\tconst pageBounds = editor.getShapePageBounds(id)\n\n\t\t\t// Hybrid check: if bounds exist and shape overlaps viewport, it's visible.\n\t\t\t// This inlines Box.Collides to avoid function call overhead and the\n\t\t\t// redundant Contains check that Box.Includes was doing.\n\t\t\tif (\n\t\t\t\tpageBounds !== undefined &&\n\t\t\t\tpageBounds.maxX >= viewMinX &&\n\t\t\t\tpageBounds.minX <= viewMaxX &&\n\t\t\t\tpageBounds.maxY >= viewMinY &&\n\t\t\t\tpageBounds.minY <= viewMaxY\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Shape is outside viewport or has no bounds - check if it can be culled.\n\t\t\t// We defer getShape and canCull checks until here since most shapes are\n\t\t\t// typically visible and we can skip these calls for them.\n\t\t\tconst shape = editor.getShape(id)\n\t\t\tif (!shape) continue\n\n\t\t\tconst canCull = editor.getShapeUtil(shape.type).canCull(shape)\n\t\t\tif (!canCull) continue\n\n\t\t\tnextValue.add(id)\n\t\t}\n\n\t\tif (isUninitialized(prevValue)) {\n\t\t\treturn nextValue\n\t\t}\n\n\t\t// If there are more or less shapes, we know there's a change\n\t\tif (prevValue.size !== nextValue.size) return nextValue\n\n\t\t// If any of the old shapes are not in the new set, we know there's a change\n\t\tfor (const prev of prevValue) {\n\t\t\tif (!nextValue.has(prev)) {\n\t\t\t\treturn nextValue\n\t\t\t}\n\t\t}\n\n\t\t// If we've made it here, we know that the set is the same\n\t\treturn prevValue\n\t})\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA0C;AAUnC,SAAS,iBAAiB,QAAgB;AAChD,aAAO,uBAAyB,oBAAoB,SAAS,uBAAuB,WAAW;AAC9F,UAAM,WAAW,OAAO,uBAAuB;AAC/C,UAAM,YAAY,oBAAI,IAAe;AAGrC,UAAM,qBAAqB,OAAO,sBAAsB;AACxD,UAAM,WAAW,mBAAmB;AACpC,UAAM,WAAW,mBAAmB;AACpC,UAAM,WAAW,mBAAmB;AACpC,UAAM,WAAW,mBAAmB;AAEpC,eAAW,MAAM,UAAU;AAC1B,YAAM,aAAa,OAAO,mBAAmB,EAAE;AAK/C,UACC,eAAe,UACf,WAAW,QAAQ,YACnB,WAAW,QAAQ,YACnB,WAAW,QAAQ,YACnB,WAAW,QAAQ,UAClB;AACD;AAAA,MACD;AAKA,YAAM,QAAQ,OAAO,SAAS,EAAE;AAChC,UAAI,CAAC,MAAO;AAEZ,YAAM,UAAU,OAAO,aAAa,MAAM,IAAI,EAAE,QAAQ,KAAK;AAC7D,UAAI,CAAC,QAAS;AAEd,gBAAU,IAAI,EAAE;AAAA,IACjB;AAEA,YAAI,8BAAgB,SAAS,GAAG;AAC/B,aAAO;AAAA,IACR;AAGA,QAAI,UAAU,SAAS,UAAU,KAAM,QAAO;AAG9C,eAAW,QAAQ,WAAW;AAC7B,UAAI,CAAC,UAAU,IAAI,IAAI,GAAG;AACzB,eAAO;AAAA,MACR;AAAA,IACD;AAGA,WAAO;AAAA,EACR,CAAC;AACF;",
4
+ "sourcesContent": ["import { computed, isUninitialized } from '@tldraw/state'\nimport { TLShapeId } from '@tldraw/tlschema'\nimport { Editor } from '../Editor'\n\n/**\n * Non visible shapes are shapes outside of the viewport page bounds.\n *\n * @param editor - Instance of the tldraw Editor.\n * @returns Incremental derivation of non visible shapes.\n */\nexport function notVisibleShapes(editor: Editor) {\n\treturn computed<Set<TLShapeId>>('notVisibleShapes', function (prevValue) {\n\t\tconst allShapeIds = editor.getCurrentPageShapeIds()\n\t\tconst viewportPageBounds = editor.getViewportPageBounds()\n\t\tconst visibleIds = editor.getShapeIdsInsideBounds(viewportPageBounds)\n\n\t\tconst nextValue = new Set<TLShapeId>()\n\n\t\t// Non-visible shapes are all shapes minus visible shapes\n\t\tfor (const id of allShapeIds) {\n\t\t\tif (!visibleIds.has(id)) {\n\t\t\t\tconst shape = editor.getShape(id)\n\t\t\t\tif (!shape) continue\n\n\t\t\t\tconst canCull = editor.getShapeUtil(shape.type).canCull(shape)\n\t\t\t\tif (!canCull) continue\n\n\t\t\t\tnextValue.add(id)\n\t\t\t}\n\t\t}\n\n\t\tif (isUninitialized(prevValue) || prevValue.size !== nextValue.size) {\n\t\t\treturn nextValue\n\t\t}\n\n\t\tfor (const prev of prevValue) {\n\t\t\tif (!nextValue.has(prev)) return nextValue\n\t\t}\n\n\t\treturn prevValue\n\t})\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA0C;AAUnC,SAAS,iBAAiB,QAAgB;AAChD,aAAO,uBAAyB,oBAAoB,SAAU,WAAW;AACxE,UAAM,cAAc,OAAO,uBAAuB;AAClD,UAAM,qBAAqB,OAAO,sBAAsB;AACxD,UAAM,aAAa,OAAO,wBAAwB,kBAAkB;AAEpE,UAAM,YAAY,oBAAI,IAAe;AAGrC,eAAW,MAAM,aAAa;AAC7B,UAAI,CAAC,WAAW,IAAI,EAAE,GAAG;AACxB,cAAM,QAAQ,OAAO,SAAS,EAAE;AAChC,YAAI,CAAC,MAAO;AAEZ,cAAM,UAAU,OAAO,aAAa,MAAM,IAAI,EAAE,QAAQ,KAAK;AAC7D,YAAI,CAAC,QAAS;AAEd,kBAAU,IAAI,EAAE;AAAA,MACjB;AAAA,IACD;AAEA,YAAI,8BAAgB,SAAS,KAAK,UAAU,SAAS,UAAU,MAAM;AACpE,aAAO;AAAA,IACR;AAEA,eAAW,QAAQ,WAAW;AAC7B,UAAI,CAAC,UAAU,IAAI,IAAI,EAAG,QAAO;AAAA,IAClC;AAEA,WAAO;AAAA,EACR,CAAC;AACF;",
6
6
  "names": []
7
7
  }
@@ -27,13 +27,53 @@ class ScribbleManager {
27
27
  constructor(editor) {
28
28
  this.editor = editor;
29
29
  }
30
- scribbleItems = /* @__PURE__ */ new Map();
31
- state = "paused";
32
- addScribble(scribble, id = (0, import_utils.uniqueId)()) {
33
- const item = {
30
+ sessions = /* @__PURE__ */ new Map();
31
+ // ==================== SESSION API ====================
32
+ /**
33
+ * Start a new session for grouping scribbles.
34
+ * Returns a session ID that can be used with other session methods.
35
+ *
36
+ * @param options - Session configuration
37
+ * @returns Session ID
38
+ * @public
39
+ */
40
+ startSession(options = {}) {
41
+ const id = options.id ?? (0, import_utils.uniqueId)();
42
+ const session = {
34
43
  id,
44
+ items: [],
45
+ state: "active",
46
+ options: {
47
+ selfConsume: options.selfConsume ?? true,
48
+ idleTimeoutMs: options.idleTimeoutMs ?? 0,
49
+ fadeMode: options.fadeMode ?? "individual",
50
+ fadeEasing: options.fadeEasing ?? (options.fadeMode === "grouped" ? "ease-in" : "linear"),
51
+ fadeDurationMs: options.fadeDurationMs ?? this.editor.options.laserFadeoutMs
52
+ },
53
+ fadeElapsed: 0,
54
+ totalPointsAtFadeStart: 0
55
+ };
56
+ this.sessions.set(id, session);
57
+ if (session.options.idleTimeoutMs > 0) {
58
+ this.resetIdleTimeout(session);
59
+ }
60
+ return id;
61
+ }
62
+ /**
63
+ * Add a scribble to a session.
64
+ *
65
+ * @param sessionId - The session ID
66
+ * @param scribble - Partial scribble properties
67
+ * @param scribbleId - Optional scribble ID
68
+ * @public
69
+ */
70
+ addScribbleToSession(sessionId, scribble, scribbleId = (0, import_utils.uniqueId)()) {
71
+ const session = this.sessions.get(sessionId);
72
+ if (!session) throw Error(`Session ${sessionId} not found`);
73
+ const item = {
74
+ id: scribbleId,
35
75
  scribble: {
36
- id,
76
+ id: scribbleId,
37
77
  size: 20,
38
78
  color: "accent",
39
79
  opacity: 0.8,
@@ -49,43 +89,187 @@ class ScribbleManager {
49
89
  prev: null,
50
90
  next: null
51
91
  };
52
- this.scribbleItems.set(id, item);
92
+ session.items.push(item);
93
+ if (session.options.idleTimeoutMs > 0) {
94
+ this.resetIdleTimeout(session);
95
+ }
53
96
  return item;
54
97
  }
55
- reset() {
56
- this.editor.updateInstanceState({ scribbles: [] });
57
- this.scribbleItems.clear();
58
- }
59
98
  /**
60
- * Start stopping the scribble. The scribble won't be removed until its last point is cleared.
99
+ * Add a point to a scribble in a session.
61
100
  *
101
+ * @param sessionId - The session ID
102
+ * @param scribbleId - The scribble ID
103
+ * @param x - X coordinate
104
+ * @param y - Y coordinate
105
+ * @param z - Z coordinate (pressure)
62
106
  * @public
63
107
  */
64
- stop(id) {
65
- const item = this.scribbleItems.get(id);
66
- if (!item) throw Error(`Scribble with id ${id} not found`);
67
- item.delayRemaining = Math.min(item.delayRemaining, 200);
68
- item.scribble.state = "stopping";
108
+ addPointToSession(sessionId, scribbleId, x, y, z = 0.5) {
109
+ const session = this.sessions.get(sessionId);
110
+ if (!session) throw Error(`Session ${sessionId} not found`);
111
+ const item = session.items.find((i) => i.id === scribbleId);
112
+ if (!item) throw Error(`Scribble ${scribbleId} not found in session ${sessionId}`);
113
+ const point = { x, y, z };
114
+ if (!item.prev || import_Vec.Vec.Dist(item.prev, point) >= 1) {
115
+ item.next = point;
116
+ }
117
+ if (session.options.idleTimeoutMs > 0) {
118
+ this.resetIdleTimeout(session);
119
+ }
69
120
  return item;
70
121
  }
71
122
  /**
72
- * Set the scribble's next point.
123
+ * Extend a session, resetting its idle timeout.
73
124
  *
74
- * @param id - The id of the scribble to add a point to.
75
- * @param x - The x coordinate of the point.
76
- * @param y - The y coordinate of the point.
77
- * @param z - The z coordinate of the point.
125
+ * @param sessionId - The session ID
126
+ * @public
127
+ */
128
+ extendSession(sessionId) {
129
+ const session = this.sessions.get(sessionId);
130
+ if (!session) return;
131
+ if (session.options.idleTimeoutMs > 0) {
132
+ this.resetIdleTimeout(session);
133
+ }
134
+ }
135
+ /**
136
+ * Stop a session, triggering fade-out.
137
+ *
138
+ * @param sessionId - The session ID
139
+ * @public
140
+ */
141
+ stopSession(sessionId) {
142
+ const session = this.sessions.get(sessionId);
143
+ if (!session || session.state !== "active") return;
144
+ this.clearIdleTimeout(session);
145
+ session.state = "stopping";
146
+ if (session.options.fadeMode === "grouped") {
147
+ session.totalPointsAtFadeStart = session.items.reduce(
148
+ (sum, item) => sum + item.scribble.points.length,
149
+ 0
150
+ );
151
+ session.fadeElapsed = 0;
152
+ for (const item of session.items) {
153
+ item.scribble.state = "stopping";
154
+ }
155
+ } else {
156
+ for (const item of session.items) {
157
+ item.delayRemaining = Math.min(item.delayRemaining, 200);
158
+ item.scribble.state = "stopping";
159
+ }
160
+ }
161
+ }
162
+ /**
163
+ * Clear all scribbles in a session immediately.
164
+ *
165
+ * @param sessionId - The session ID
166
+ * @public
167
+ */
168
+ clearSession(sessionId) {
169
+ const session = this.sessions.get(sessionId);
170
+ if (!session) return;
171
+ this.clearIdleTimeout(session);
172
+ for (const item of session.items) {
173
+ item.scribble.points.length = 0;
174
+ }
175
+ session.state = "complete";
176
+ }
177
+ /**
178
+ * Check if a session is active.
179
+ *
180
+ * @param sessionId - The session ID
181
+ * @public
182
+ */
183
+ isSessionActive(sessionId) {
184
+ const session = this.sessions.get(sessionId);
185
+ return session?.state === "active";
186
+ }
187
+ // ==================== SIMPLE API (for eraser, select, etc.) ====================
188
+ /**
189
+ * Add a scribble using the default self-consuming behavior.
190
+ * Creates an implicit session for the scribble.
191
+ *
192
+ * @param scribble - Partial scribble properties
193
+ * @param id - Optional scribble id
194
+ * @returns The created scribble item
195
+ * @public
196
+ */
197
+ addScribble(scribble, id = (0, import_utils.uniqueId)()) {
198
+ const sessionId = this.startSession();
199
+ return this.addScribbleToSession(sessionId, scribble, id);
200
+ }
201
+ /**
202
+ * Add a point to a scribble. Searches all sessions.
203
+ *
204
+ * @param id - The scribble id
205
+ * @param x - X coordinate
206
+ * @param y - Y coordinate
207
+ * @param z - Z coordinate (pressure)
78
208
  * @public
79
209
  */
80
210
  addPoint(id, x, y, z = 0.5) {
81
- const item = this.scribbleItems.get(id);
82
- if (!item) throw Error(`Scribble with id ${id} not found`);
83
- const { prev } = item;
84
- const point = { x, y, z };
85
- if (!prev || import_Vec.Vec.Dist(prev, point) >= 1) {
86
- item.next = point;
211
+ for (const session of this.sessions.values()) {
212
+ const item = session.items.find((i) => i.id === id);
213
+ if (item) {
214
+ const point = { x, y, z };
215
+ if (!item.prev || import_Vec.Vec.Dist(item.prev, point) >= 1) {
216
+ item.next = point;
217
+ }
218
+ if (session.options.idleTimeoutMs > 0) {
219
+ this.resetIdleTimeout(session);
220
+ }
221
+ return item;
222
+ }
87
223
  }
88
- return item;
224
+ throw Error(`Scribble with id ${id} not found`);
225
+ }
226
+ /**
227
+ * Mark a scribble as complete (done being drawn but not yet fading).
228
+ * Searches all sessions.
229
+ *
230
+ * @param id - The scribble id
231
+ * @public
232
+ */
233
+ complete(id) {
234
+ for (const session of this.sessions.values()) {
235
+ const item = session.items.find((i) => i.id === id);
236
+ if (item) {
237
+ if (item.scribble.state === "starting" || item.scribble.state === "active") {
238
+ item.scribble.state = "complete";
239
+ }
240
+ return item;
241
+ }
242
+ }
243
+ throw Error(`Scribble with id ${id} not found`);
244
+ }
245
+ /**
246
+ * Stop a scribble. Searches all sessions.
247
+ *
248
+ * @param id - The scribble id
249
+ * @public
250
+ */
251
+ stop(id) {
252
+ for (const session of this.sessions.values()) {
253
+ const item = session.items.find((i) => i.id === id);
254
+ if (item) {
255
+ item.delayRemaining = Math.min(item.delayRemaining, 200);
256
+ item.scribble.state = "stopping";
257
+ return item;
258
+ }
259
+ }
260
+ throw Error(`Scribble with id ${id} not found`);
261
+ }
262
+ /**
263
+ * Stop and remove all sessions.
264
+ *
265
+ * @public
266
+ */
267
+ reset() {
268
+ for (const session of this.sessions.values()) {
269
+ this.clearIdleTimeout(session);
270
+ }
271
+ this.sessions.clear();
272
+ this.editor.updateInstanceState({ scribbles: [] });
89
273
  }
90
274
  /**
91
275
  * Update on each animation frame.
@@ -94,77 +278,182 @@ class ScribbleManager {
94
278
  * @public
95
279
  */
96
280
  tick(elapsed) {
97
- if (this.scribbleItems.size === 0) return;
281
+ const currentScribbles = this.editor.getInstanceState().scribbles;
282
+ if (this.sessions.size === 0 && currentScribbles.length === 0) return;
98
283
  this.editor.run(() => {
99
- this.scribbleItems.forEach((item) => {
100
- if (item.scribble.state === "starting") {
101
- const { next: next2, prev: prev2 } = item;
102
- if (next2 && next2 !== prev2) {
103
- item.prev = next2;
104
- item.scribble.points.push(next2);
105
- }
106
- if (item.scribble.points.length > 8) {
107
- item.scribble.state = "active";
108
- }
109
- return;
284
+ for (const session of this.sessions.values()) {
285
+ this.tickSession(session, elapsed);
286
+ }
287
+ for (const [id, session] of this.sessions) {
288
+ if (session.state === "complete") {
289
+ this.clearIdleTimeout(session);
290
+ this.sessions.delete(id);
110
291
  }
111
- if (item.delayRemaining > 0) {
112
- item.delayRemaining = Math.max(0, item.delayRemaining - elapsed);
292
+ }
293
+ const scribbles = [];
294
+ for (const session of this.sessions.values()) {
295
+ for (const item of session.items) {
296
+ if (item.scribble.points.length > 0) {
297
+ scribbles.push({
298
+ ...item.scribble,
299
+ points: [...item.scribble.points]
300
+ });
301
+ }
113
302
  }
114
- item.timeoutMs += elapsed;
115
- if (item.timeoutMs >= 16) {
116
- item.timeoutMs = 0;
303
+ }
304
+ this.editor.updateInstanceState({ scribbles });
305
+ });
306
+ }
307
+ // ==================== PRIVATE HELPERS ====================
308
+ resetIdleTimeout(session) {
309
+ this.clearIdleTimeout(session);
310
+ session.idleTimeoutHandle = this.editor.timers.setTimeout(() => {
311
+ this.stopSession(session.id);
312
+ }, session.options.idleTimeoutMs);
313
+ }
314
+ clearIdleTimeout(session) {
315
+ if (session.idleTimeoutHandle !== void 0) {
316
+ clearTimeout(session.idleTimeoutHandle);
317
+ session.idleTimeoutHandle = void 0;
318
+ }
319
+ }
320
+ tickSession(session, elapsed) {
321
+ if (session.state === "complete") return;
322
+ if (session.state === "stopping" && session.options.fadeMode === "grouped") {
323
+ this.tickGroupedFade(session, elapsed);
324
+ } else {
325
+ this.tickSessionItems(session, elapsed);
326
+ }
327
+ const hasContent = session.items.some((item) => item.scribble.points.length > 0);
328
+ if (!hasContent && (session.state === "stopping" || session.items.length === 0)) {
329
+ session.state = "complete";
330
+ }
331
+ }
332
+ tickSessionItems(session, elapsed) {
333
+ for (const item of session.items) {
334
+ const shouldSelfConsume = session.options.selfConsume || session.state === "stopping" || item.scribble.state === "stopping";
335
+ if (shouldSelfConsume) {
336
+ this.tickSelfConsumingItem(item, elapsed);
337
+ } else {
338
+ this.tickPersistentItem(item);
339
+ }
340
+ }
341
+ if (session.options.fadeMode === "individual") {
342
+ for (let i = session.items.length - 1; i >= 0; i--) {
343
+ if (session.items[i].scribble.points.length === 0) {
344
+ session.items.splice(i, 1);
117
345
  }
118
- const { delayRemaining, timeoutMs, prev, next, scribble } = item;
119
- switch (scribble.state) {
120
- case "active": {
121
- if (next && next !== prev) {
122
- item.prev = next;
123
- scribble.points.push(next);
124
- if (delayRemaining === 0) {
125
- if (scribble.points.length > 8) {
126
- scribble.points.shift();
127
- }
128
- }
346
+ }
347
+ }
348
+ }
349
+ tickPersistentItem(item) {
350
+ const { scribble } = item;
351
+ if (scribble.state === "starting") {
352
+ const { next, prev } = item;
353
+ if (next && next !== prev) {
354
+ item.prev = next;
355
+ scribble.points.push(next);
356
+ }
357
+ if (scribble.points.length > 8) {
358
+ scribble.state = "active";
359
+ }
360
+ return;
361
+ }
362
+ if (scribble.state === "active") {
363
+ const { next, prev } = item;
364
+ if (next && next !== prev) {
365
+ item.prev = next;
366
+ scribble.points.push(next);
367
+ }
368
+ }
369
+ }
370
+ tickSelfConsumingItem(item, elapsed) {
371
+ const { scribble } = item;
372
+ if (scribble.state === "starting") {
373
+ const { next: next2, prev: prev2 } = item;
374
+ if (next2 && next2 !== prev2) {
375
+ item.prev = next2;
376
+ scribble.points.push(next2);
377
+ }
378
+ if (scribble.points.length > 8) {
379
+ scribble.state = "active";
380
+ }
381
+ return;
382
+ }
383
+ if (item.delayRemaining > 0) {
384
+ item.delayRemaining = Math.max(0, item.delayRemaining - elapsed);
385
+ }
386
+ item.timeoutMs += elapsed;
387
+ if (item.timeoutMs >= 16) {
388
+ item.timeoutMs = 0;
389
+ }
390
+ const { delayRemaining, timeoutMs, prev, next } = item;
391
+ switch (scribble.state) {
392
+ case "active": {
393
+ if (next && next !== prev) {
394
+ item.prev = next;
395
+ scribble.points.push(next);
396
+ if (delayRemaining === 0 && scribble.points.length > 8) {
397
+ scribble.points.shift();
398
+ }
399
+ } else {
400
+ if (timeoutMs === 0) {
401
+ if (scribble.points.length > 1) {
402
+ scribble.points.shift();
129
403
  } else {
130
- if (timeoutMs === 0) {
131
- if (scribble.points.length > 1) {
132
- scribble.points.shift();
133
- } else {
134
- item.delayRemaining = scribble.delay;
135
- }
136
- }
404
+ item.delayRemaining = scribble.delay;
137
405
  }
138
- break;
139
406
  }
140
- case "stopping": {
141
- if (item.delayRemaining === 0) {
142
- if (timeoutMs === 0) {
143
- if (scribble.points.length === 1) {
144
- this.scribbleItems.delete(item.id);
145
- return;
146
- }
147
- if (scribble.shrink) {
148
- scribble.size = Math.max(1, scribble.size * (1 - scribble.shrink));
149
- }
150
- scribble.points.shift();
151
- }
152
- }
153
- break;
407
+ }
408
+ break;
409
+ }
410
+ case "stopping": {
411
+ if (delayRemaining === 0 && timeoutMs === 0) {
412
+ if (scribble.points.length <= 1) {
413
+ scribble.points.length = 0;
414
+ return;
154
415
  }
155
- case "paused": {
156
- break;
416
+ if (scribble.shrink) {
417
+ scribble.size = Math.max(1, scribble.size * (1 - scribble.shrink));
157
418
  }
419
+ scribble.points.shift();
158
420
  }
159
- });
160
- this.editor.updateInstanceState({
161
- scribbles: Array.from(this.scribbleItems.values()).map(({ scribble }) => ({
162
- ...scribble,
163
- points: [...scribble.points]
164
- })).slice(-5)
165
- // limit to three as a minor sanity check
166
- });
167
- });
421
+ break;
422
+ }
423
+ case "paused": {
424
+ break;
425
+ }
426
+ }
427
+ }
428
+ tickGroupedFade(session, elapsed) {
429
+ session.fadeElapsed += elapsed;
430
+ let remainingPoints = 0;
431
+ for (const item of session.items) {
432
+ remainingPoints += item.scribble.points.length;
433
+ }
434
+ if (remainingPoints === 0) return;
435
+ if (session.fadeElapsed >= session.options.fadeDurationMs) {
436
+ for (const item of session.items) {
437
+ item.scribble.points.length = 0;
438
+ }
439
+ return;
440
+ }
441
+ const progress = session.fadeElapsed / session.options.fadeDurationMs;
442
+ const easedProgress = session.options.fadeEasing === "ease-in" ? progress * progress : progress;
443
+ const targetRemoved = Math.floor(easedProgress * session.totalPointsAtFadeStart);
444
+ const actuallyRemoved = session.totalPointsAtFadeStart - remainingPoints;
445
+ const pointsToRemove = Math.max(1, targetRemoved - actuallyRemoved);
446
+ let removed = 0;
447
+ let itemIndex = 0;
448
+ while (removed < pointsToRemove && itemIndex < session.items.length) {
449
+ const item = session.items[itemIndex];
450
+ if (item.scribble.points.length > 0) {
451
+ item.scribble.points.shift();
452
+ removed++;
453
+ } else {
454
+ itemIndex++;
455
+ }
456
+ }
168
457
  }
169
458
  }
170
459
  //# sourceMappingURL=ScribbleManager.js.map