@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
@@ -1,32 +1,24 @@
1
1
  import { computed, isUninitialized } from "@tldraw/state";
2
2
  function notVisibleShapes(editor) {
3
- return computed("notVisibleShapes", function updateNotVisibleShapes(prevValue) {
4
- const shapeIds = editor.getCurrentPageShapeIds();
5
- const nextValue = /* @__PURE__ */ new Set();
3
+ return computed("notVisibleShapes", function(prevValue) {
4
+ const allShapeIds = editor.getCurrentPageShapeIds();
6
5
  const viewportPageBounds = editor.getViewportPageBounds();
7
- const viewMinX = viewportPageBounds.minX;
8
- const viewMinY = viewportPageBounds.minY;
9
- const viewMaxX = viewportPageBounds.maxX;
10
- const viewMaxY = viewportPageBounds.maxY;
11
- for (const id of shapeIds) {
12
- const pageBounds = editor.getShapePageBounds(id);
13
- if (pageBounds !== void 0 && pageBounds.maxX >= viewMinX && pageBounds.minX <= viewMaxX && pageBounds.maxY >= viewMinY && pageBounds.minY <= viewMaxY) {
14
- continue;
6
+ const visibleIds = editor.getShapeIdsInsideBounds(viewportPageBounds);
7
+ const nextValue = /* @__PURE__ */ new Set();
8
+ for (const id of allShapeIds) {
9
+ if (!visibleIds.has(id)) {
10
+ const shape = editor.getShape(id);
11
+ if (!shape) continue;
12
+ const canCull = editor.getShapeUtil(shape.type).canCull(shape);
13
+ if (!canCull) continue;
14
+ nextValue.add(id);
15
15
  }
16
- const shape = editor.getShape(id);
17
- if (!shape) continue;
18
- const canCull = editor.getShapeUtil(shape.type).canCull(shape);
19
- if (!canCull) continue;
20
- nextValue.add(id);
21
16
  }
22
- if (isUninitialized(prevValue)) {
17
+ if (isUninitialized(prevValue) || prevValue.size !== nextValue.size) {
23
18
  return nextValue;
24
19
  }
25
- if (prevValue.size !== nextValue.size) return nextValue;
26
20
  for (const prev of prevValue) {
27
- if (!nextValue.has(prev)) {
28
- return nextValue;
29
- }
21
+ if (!nextValue.has(prev)) return nextValue;
30
22
  }
31
23
  return prevValue;
32
24
  });
@@ -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,SAAS,UAAU,uBAAuB;AAUnC,SAAS,iBAAiB,QAAgB;AAChD,SAAO,SAAyB,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,QAAI,gBAAgB,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,SAAS,UAAU,uBAAuB;AAUnC,SAAS,iBAAiB,QAAgB;AAChD,SAAO,SAAyB,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,QAAI,gBAAgB,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
  }
@@ -4,13 +4,53 @@ class ScribbleManager {
4
4
  constructor(editor) {
5
5
  this.editor = editor;
6
6
  }
7
- scribbleItems = /* @__PURE__ */ new Map();
8
- state = "paused";
9
- addScribble(scribble, id = uniqueId()) {
10
- const item = {
7
+ sessions = /* @__PURE__ */ new Map();
8
+ // ==================== SESSION API ====================
9
+ /**
10
+ * Start a new session for grouping scribbles.
11
+ * Returns a session ID that can be used with other session methods.
12
+ *
13
+ * @param options - Session configuration
14
+ * @returns Session ID
15
+ * @public
16
+ */
17
+ startSession(options = {}) {
18
+ const id = options.id ?? uniqueId();
19
+ const session = {
11
20
  id,
21
+ items: [],
22
+ state: "active",
23
+ options: {
24
+ selfConsume: options.selfConsume ?? true,
25
+ idleTimeoutMs: options.idleTimeoutMs ?? 0,
26
+ fadeMode: options.fadeMode ?? "individual",
27
+ fadeEasing: options.fadeEasing ?? (options.fadeMode === "grouped" ? "ease-in" : "linear"),
28
+ fadeDurationMs: options.fadeDurationMs ?? this.editor.options.laserFadeoutMs
29
+ },
30
+ fadeElapsed: 0,
31
+ totalPointsAtFadeStart: 0
32
+ };
33
+ this.sessions.set(id, session);
34
+ if (session.options.idleTimeoutMs > 0) {
35
+ this.resetIdleTimeout(session);
36
+ }
37
+ return id;
38
+ }
39
+ /**
40
+ * Add a scribble to a session.
41
+ *
42
+ * @param sessionId - The session ID
43
+ * @param scribble - Partial scribble properties
44
+ * @param scribbleId - Optional scribble ID
45
+ * @public
46
+ */
47
+ addScribbleToSession(sessionId, scribble, scribbleId = uniqueId()) {
48
+ const session = this.sessions.get(sessionId);
49
+ if (!session) throw Error(`Session ${sessionId} not found`);
50
+ const item = {
51
+ id: scribbleId,
12
52
  scribble: {
13
- id,
53
+ id: scribbleId,
14
54
  size: 20,
15
55
  color: "accent",
16
56
  opacity: 0.8,
@@ -26,43 +66,187 @@ class ScribbleManager {
26
66
  prev: null,
27
67
  next: null
28
68
  };
29
- this.scribbleItems.set(id, item);
69
+ session.items.push(item);
70
+ if (session.options.idleTimeoutMs > 0) {
71
+ this.resetIdleTimeout(session);
72
+ }
30
73
  return item;
31
74
  }
32
- reset() {
33
- this.editor.updateInstanceState({ scribbles: [] });
34
- this.scribbleItems.clear();
35
- }
36
75
  /**
37
- * Start stopping the scribble. The scribble won't be removed until its last point is cleared.
76
+ * Add a point to a scribble in a session.
38
77
  *
78
+ * @param sessionId - The session ID
79
+ * @param scribbleId - The scribble ID
80
+ * @param x - X coordinate
81
+ * @param y - Y coordinate
82
+ * @param z - Z coordinate (pressure)
39
83
  * @public
40
84
  */
41
- stop(id) {
42
- const item = this.scribbleItems.get(id);
43
- if (!item) throw Error(`Scribble with id ${id} not found`);
44
- item.delayRemaining = Math.min(item.delayRemaining, 200);
45
- item.scribble.state = "stopping";
85
+ addPointToSession(sessionId, scribbleId, x, y, z = 0.5) {
86
+ const session = this.sessions.get(sessionId);
87
+ if (!session) throw Error(`Session ${sessionId} not found`);
88
+ const item = session.items.find((i) => i.id === scribbleId);
89
+ if (!item) throw Error(`Scribble ${scribbleId} not found in session ${sessionId}`);
90
+ const point = { x, y, z };
91
+ if (!item.prev || Vec.Dist(item.prev, point) >= 1) {
92
+ item.next = point;
93
+ }
94
+ if (session.options.idleTimeoutMs > 0) {
95
+ this.resetIdleTimeout(session);
96
+ }
46
97
  return item;
47
98
  }
48
99
  /**
49
- * Set the scribble's next point.
100
+ * Extend a session, resetting its idle timeout.
50
101
  *
51
- * @param id - The id of the scribble to add a point to.
52
- * @param x - The x coordinate of the point.
53
- * @param y - The y coordinate of the point.
54
- * @param z - The z coordinate of the point.
102
+ * @param sessionId - The session ID
103
+ * @public
104
+ */
105
+ extendSession(sessionId) {
106
+ const session = this.sessions.get(sessionId);
107
+ if (!session) return;
108
+ if (session.options.idleTimeoutMs > 0) {
109
+ this.resetIdleTimeout(session);
110
+ }
111
+ }
112
+ /**
113
+ * Stop a session, triggering fade-out.
114
+ *
115
+ * @param sessionId - The session ID
116
+ * @public
117
+ */
118
+ stopSession(sessionId) {
119
+ const session = this.sessions.get(sessionId);
120
+ if (!session || session.state !== "active") return;
121
+ this.clearIdleTimeout(session);
122
+ session.state = "stopping";
123
+ if (session.options.fadeMode === "grouped") {
124
+ session.totalPointsAtFadeStart = session.items.reduce(
125
+ (sum, item) => sum + item.scribble.points.length,
126
+ 0
127
+ );
128
+ session.fadeElapsed = 0;
129
+ for (const item of session.items) {
130
+ item.scribble.state = "stopping";
131
+ }
132
+ } else {
133
+ for (const item of session.items) {
134
+ item.delayRemaining = Math.min(item.delayRemaining, 200);
135
+ item.scribble.state = "stopping";
136
+ }
137
+ }
138
+ }
139
+ /**
140
+ * Clear all scribbles in a session immediately.
141
+ *
142
+ * @param sessionId - The session ID
143
+ * @public
144
+ */
145
+ clearSession(sessionId) {
146
+ const session = this.sessions.get(sessionId);
147
+ if (!session) return;
148
+ this.clearIdleTimeout(session);
149
+ for (const item of session.items) {
150
+ item.scribble.points.length = 0;
151
+ }
152
+ session.state = "complete";
153
+ }
154
+ /**
155
+ * Check if a session is active.
156
+ *
157
+ * @param sessionId - The session ID
158
+ * @public
159
+ */
160
+ isSessionActive(sessionId) {
161
+ const session = this.sessions.get(sessionId);
162
+ return session?.state === "active";
163
+ }
164
+ // ==================== SIMPLE API (for eraser, select, etc.) ====================
165
+ /**
166
+ * Add a scribble using the default self-consuming behavior.
167
+ * Creates an implicit session for the scribble.
168
+ *
169
+ * @param scribble - Partial scribble properties
170
+ * @param id - Optional scribble id
171
+ * @returns The created scribble item
172
+ * @public
173
+ */
174
+ addScribble(scribble, id = uniqueId()) {
175
+ const sessionId = this.startSession();
176
+ return this.addScribbleToSession(sessionId, scribble, id);
177
+ }
178
+ /**
179
+ * Add a point to a scribble. Searches all sessions.
180
+ *
181
+ * @param id - The scribble id
182
+ * @param x - X coordinate
183
+ * @param y - Y coordinate
184
+ * @param z - Z coordinate (pressure)
55
185
  * @public
56
186
  */
57
187
  addPoint(id, x, y, z = 0.5) {
58
- const item = this.scribbleItems.get(id);
59
- if (!item) throw Error(`Scribble with id ${id} not found`);
60
- const { prev } = item;
61
- const point = { x, y, z };
62
- if (!prev || Vec.Dist(prev, point) >= 1) {
63
- item.next = point;
188
+ for (const session of this.sessions.values()) {
189
+ const item = session.items.find((i) => i.id === id);
190
+ if (item) {
191
+ const point = { x, y, z };
192
+ if (!item.prev || Vec.Dist(item.prev, point) >= 1) {
193
+ item.next = point;
194
+ }
195
+ if (session.options.idleTimeoutMs > 0) {
196
+ this.resetIdleTimeout(session);
197
+ }
198
+ return item;
199
+ }
64
200
  }
65
- return item;
201
+ throw Error(`Scribble with id ${id} not found`);
202
+ }
203
+ /**
204
+ * Mark a scribble as complete (done being drawn but not yet fading).
205
+ * Searches all sessions.
206
+ *
207
+ * @param id - The scribble id
208
+ * @public
209
+ */
210
+ complete(id) {
211
+ for (const session of this.sessions.values()) {
212
+ const item = session.items.find((i) => i.id === id);
213
+ if (item) {
214
+ if (item.scribble.state === "starting" || item.scribble.state === "active") {
215
+ item.scribble.state = "complete";
216
+ }
217
+ return item;
218
+ }
219
+ }
220
+ throw Error(`Scribble with id ${id} not found`);
221
+ }
222
+ /**
223
+ * Stop a scribble. Searches all sessions.
224
+ *
225
+ * @param id - The scribble id
226
+ * @public
227
+ */
228
+ stop(id) {
229
+ for (const session of this.sessions.values()) {
230
+ const item = session.items.find((i) => i.id === id);
231
+ if (item) {
232
+ item.delayRemaining = Math.min(item.delayRemaining, 200);
233
+ item.scribble.state = "stopping";
234
+ return item;
235
+ }
236
+ }
237
+ throw Error(`Scribble with id ${id} not found`);
238
+ }
239
+ /**
240
+ * Stop and remove all sessions.
241
+ *
242
+ * @public
243
+ */
244
+ reset() {
245
+ for (const session of this.sessions.values()) {
246
+ this.clearIdleTimeout(session);
247
+ }
248
+ this.sessions.clear();
249
+ this.editor.updateInstanceState({ scribbles: [] });
66
250
  }
67
251
  /**
68
252
  * Update on each animation frame.
@@ -71,77 +255,182 @@ class ScribbleManager {
71
255
  * @public
72
256
  */
73
257
  tick(elapsed) {
74
- if (this.scribbleItems.size === 0) return;
258
+ const currentScribbles = this.editor.getInstanceState().scribbles;
259
+ if (this.sessions.size === 0 && currentScribbles.length === 0) return;
75
260
  this.editor.run(() => {
76
- this.scribbleItems.forEach((item) => {
77
- if (item.scribble.state === "starting") {
78
- const { next: next2, prev: prev2 } = item;
79
- if (next2 && next2 !== prev2) {
80
- item.prev = next2;
81
- item.scribble.points.push(next2);
82
- }
83
- if (item.scribble.points.length > 8) {
84
- item.scribble.state = "active";
85
- }
86
- return;
261
+ for (const session of this.sessions.values()) {
262
+ this.tickSession(session, elapsed);
263
+ }
264
+ for (const [id, session] of this.sessions) {
265
+ if (session.state === "complete") {
266
+ this.clearIdleTimeout(session);
267
+ this.sessions.delete(id);
87
268
  }
88
- if (item.delayRemaining > 0) {
89
- item.delayRemaining = Math.max(0, item.delayRemaining - elapsed);
269
+ }
270
+ const scribbles = [];
271
+ for (const session of this.sessions.values()) {
272
+ for (const item of session.items) {
273
+ if (item.scribble.points.length > 0) {
274
+ scribbles.push({
275
+ ...item.scribble,
276
+ points: [...item.scribble.points]
277
+ });
278
+ }
90
279
  }
91
- item.timeoutMs += elapsed;
92
- if (item.timeoutMs >= 16) {
93
- item.timeoutMs = 0;
280
+ }
281
+ this.editor.updateInstanceState({ scribbles });
282
+ });
283
+ }
284
+ // ==================== PRIVATE HELPERS ====================
285
+ resetIdleTimeout(session) {
286
+ this.clearIdleTimeout(session);
287
+ session.idleTimeoutHandle = this.editor.timers.setTimeout(() => {
288
+ this.stopSession(session.id);
289
+ }, session.options.idleTimeoutMs);
290
+ }
291
+ clearIdleTimeout(session) {
292
+ if (session.idleTimeoutHandle !== void 0) {
293
+ clearTimeout(session.idleTimeoutHandle);
294
+ session.idleTimeoutHandle = void 0;
295
+ }
296
+ }
297
+ tickSession(session, elapsed) {
298
+ if (session.state === "complete") return;
299
+ if (session.state === "stopping" && session.options.fadeMode === "grouped") {
300
+ this.tickGroupedFade(session, elapsed);
301
+ } else {
302
+ this.tickSessionItems(session, elapsed);
303
+ }
304
+ const hasContent = session.items.some((item) => item.scribble.points.length > 0);
305
+ if (!hasContent && (session.state === "stopping" || session.items.length === 0)) {
306
+ session.state = "complete";
307
+ }
308
+ }
309
+ tickSessionItems(session, elapsed) {
310
+ for (const item of session.items) {
311
+ const shouldSelfConsume = session.options.selfConsume || session.state === "stopping" || item.scribble.state === "stopping";
312
+ if (shouldSelfConsume) {
313
+ this.tickSelfConsumingItem(item, elapsed);
314
+ } else {
315
+ this.tickPersistentItem(item);
316
+ }
317
+ }
318
+ if (session.options.fadeMode === "individual") {
319
+ for (let i = session.items.length - 1; i >= 0; i--) {
320
+ if (session.items[i].scribble.points.length === 0) {
321
+ session.items.splice(i, 1);
94
322
  }
95
- const { delayRemaining, timeoutMs, prev, next, scribble } = item;
96
- switch (scribble.state) {
97
- case "active": {
98
- if (next && next !== prev) {
99
- item.prev = next;
100
- scribble.points.push(next);
101
- if (delayRemaining === 0) {
102
- if (scribble.points.length > 8) {
103
- scribble.points.shift();
104
- }
105
- }
323
+ }
324
+ }
325
+ }
326
+ tickPersistentItem(item) {
327
+ const { scribble } = item;
328
+ if (scribble.state === "starting") {
329
+ const { next, prev } = item;
330
+ if (next && next !== prev) {
331
+ item.prev = next;
332
+ scribble.points.push(next);
333
+ }
334
+ if (scribble.points.length > 8) {
335
+ scribble.state = "active";
336
+ }
337
+ return;
338
+ }
339
+ if (scribble.state === "active") {
340
+ const { next, prev } = item;
341
+ if (next && next !== prev) {
342
+ item.prev = next;
343
+ scribble.points.push(next);
344
+ }
345
+ }
346
+ }
347
+ tickSelfConsumingItem(item, elapsed) {
348
+ const { scribble } = item;
349
+ if (scribble.state === "starting") {
350
+ const { next: next2, prev: prev2 } = item;
351
+ if (next2 && next2 !== prev2) {
352
+ item.prev = next2;
353
+ scribble.points.push(next2);
354
+ }
355
+ if (scribble.points.length > 8) {
356
+ scribble.state = "active";
357
+ }
358
+ return;
359
+ }
360
+ if (item.delayRemaining > 0) {
361
+ item.delayRemaining = Math.max(0, item.delayRemaining - elapsed);
362
+ }
363
+ item.timeoutMs += elapsed;
364
+ if (item.timeoutMs >= 16) {
365
+ item.timeoutMs = 0;
366
+ }
367
+ const { delayRemaining, timeoutMs, prev, next } = item;
368
+ switch (scribble.state) {
369
+ case "active": {
370
+ if (next && next !== prev) {
371
+ item.prev = next;
372
+ scribble.points.push(next);
373
+ if (delayRemaining === 0 && scribble.points.length > 8) {
374
+ scribble.points.shift();
375
+ }
376
+ } else {
377
+ if (timeoutMs === 0) {
378
+ if (scribble.points.length > 1) {
379
+ scribble.points.shift();
106
380
  } else {
107
- if (timeoutMs === 0) {
108
- if (scribble.points.length > 1) {
109
- scribble.points.shift();
110
- } else {
111
- item.delayRemaining = scribble.delay;
112
- }
113
- }
381
+ item.delayRemaining = scribble.delay;
114
382
  }
115
- break;
116
383
  }
117
- case "stopping": {
118
- if (item.delayRemaining === 0) {
119
- if (timeoutMs === 0) {
120
- if (scribble.points.length === 1) {
121
- this.scribbleItems.delete(item.id);
122
- return;
123
- }
124
- if (scribble.shrink) {
125
- scribble.size = Math.max(1, scribble.size * (1 - scribble.shrink));
126
- }
127
- scribble.points.shift();
128
- }
129
- }
130
- break;
384
+ }
385
+ break;
386
+ }
387
+ case "stopping": {
388
+ if (delayRemaining === 0 && timeoutMs === 0) {
389
+ if (scribble.points.length <= 1) {
390
+ scribble.points.length = 0;
391
+ return;
131
392
  }
132
- case "paused": {
133
- break;
393
+ if (scribble.shrink) {
394
+ scribble.size = Math.max(1, scribble.size * (1 - scribble.shrink));
134
395
  }
396
+ scribble.points.shift();
135
397
  }
136
- });
137
- this.editor.updateInstanceState({
138
- scribbles: Array.from(this.scribbleItems.values()).map(({ scribble }) => ({
139
- ...scribble,
140
- points: [...scribble.points]
141
- })).slice(-5)
142
- // limit to three as a minor sanity check
143
- });
144
- });
398
+ break;
399
+ }
400
+ case "paused": {
401
+ break;
402
+ }
403
+ }
404
+ }
405
+ tickGroupedFade(session, elapsed) {
406
+ session.fadeElapsed += elapsed;
407
+ let remainingPoints = 0;
408
+ for (const item of session.items) {
409
+ remainingPoints += item.scribble.points.length;
410
+ }
411
+ if (remainingPoints === 0) return;
412
+ if (session.fadeElapsed >= session.options.fadeDurationMs) {
413
+ for (const item of session.items) {
414
+ item.scribble.points.length = 0;
415
+ }
416
+ return;
417
+ }
418
+ const progress = session.fadeElapsed / session.options.fadeDurationMs;
419
+ const easedProgress = session.options.fadeEasing === "ease-in" ? progress * progress : progress;
420
+ const targetRemoved = Math.floor(easedProgress * session.totalPointsAtFadeStart);
421
+ const actuallyRemoved = session.totalPointsAtFadeStart - remainingPoints;
422
+ const pointsToRemove = Math.max(1, targetRemoved - actuallyRemoved);
423
+ let removed = 0;
424
+ let itemIndex = 0;
425
+ while (removed < pointsToRemove && itemIndex < session.items.length) {
426
+ const item = session.items[itemIndex];
427
+ if (item.scribble.points.length > 0) {
428
+ item.scribble.points.shift();
429
+ removed++;
430
+ } else {
431
+ itemIndex++;
432
+ }
433
+ }
145
434
  }
146
435
  }
147
436
  export {