@teachinglab/omd 0.1.0

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 (144) hide show
  1. package/README.md +138 -0
  2. package/canvas/core/canvasConfig.js +203 -0
  3. package/canvas/core/omdCanvas.js +475 -0
  4. package/canvas/drawing/segment.js +168 -0
  5. package/canvas/drawing/stroke.js +386 -0
  6. package/canvas/events/eventManager.js +435 -0
  7. package/canvas/events/pointerEventHandler.js +263 -0
  8. package/canvas/features/focusFrameManager.js +287 -0
  9. package/canvas/index.js +49 -0
  10. package/canvas/tools/eraserTool.js +322 -0
  11. package/canvas/tools/pencilTool.js +319 -0
  12. package/canvas/tools/selectTool.js +457 -0
  13. package/canvas/tools/tool.js +223 -0
  14. package/canvas/tools/toolManager.js +394 -0
  15. package/canvas/ui/cursor.js +438 -0
  16. package/canvas/ui/toolbar.js +304 -0
  17. package/canvas/utils/boundingBox.js +378 -0
  18. package/canvas/utils/mathUtils.js +259 -0
  19. package/docs/api/configuration-options.md +104 -0
  20. package/docs/api/eventManager.md +68 -0
  21. package/docs/api/focusFrameManager.md +150 -0
  22. package/docs/api/index.md +91 -0
  23. package/docs/api/main.md +58 -0
  24. package/docs/api/omdBinaryExpressionNode.md +227 -0
  25. package/docs/api/omdCanvas.md +142 -0
  26. package/docs/api/omdConfigManager.md +192 -0
  27. package/docs/api/omdConstantNode.md +117 -0
  28. package/docs/api/omdDisplay.md +121 -0
  29. package/docs/api/omdEquationNode.md +161 -0
  30. package/docs/api/omdEquationSequenceNode.md +301 -0
  31. package/docs/api/omdEquationStack.md +139 -0
  32. package/docs/api/omdFunctionNode.md +141 -0
  33. package/docs/api/omdGroupNode.md +182 -0
  34. package/docs/api/omdHelpers.md +96 -0
  35. package/docs/api/omdLeafNode.md +163 -0
  36. package/docs/api/omdNode.md +101 -0
  37. package/docs/api/omdOperationDisplayNode.md +139 -0
  38. package/docs/api/omdOperatorNode.md +127 -0
  39. package/docs/api/omdParenthesisNode.md +122 -0
  40. package/docs/api/omdPopup.md +117 -0
  41. package/docs/api/omdPowerNode.md +127 -0
  42. package/docs/api/omdRationalNode.md +128 -0
  43. package/docs/api/omdSequenceNode.md +128 -0
  44. package/docs/api/omdSimplification.md +110 -0
  45. package/docs/api/omdSqrtNode.md +79 -0
  46. package/docs/api/omdStepVisualizer.md +115 -0
  47. package/docs/api/omdStepVisualizerHighlighting.md +61 -0
  48. package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
  49. package/docs/api/omdStepVisualizerLayout.md +60 -0
  50. package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
  51. package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
  52. package/docs/api/omdToolbar.md +102 -0
  53. package/docs/api/omdTranscriptionService.md +76 -0
  54. package/docs/api/omdTreeDiff.md +134 -0
  55. package/docs/api/omdUnaryExpressionNode.md +174 -0
  56. package/docs/api/omdUtilities.md +70 -0
  57. package/docs/api/omdVariableNode.md +148 -0
  58. package/docs/api/selectTool.md +74 -0
  59. package/docs/api/simplificationEngine.md +98 -0
  60. package/docs/api/simplificationRules.md +77 -0
  61. package/docs/api/simplificationUtils.md +64 -0
  62. package/docs/api/transcribe.md +43 -0
  63. package/docs/api-reference.md +85 -0
  64. package/docs/index.html +454 -0
  65. package/docs/user-guide.md +9 -0
  66. package/index.js +67 -0
  67. package/omd/config/omdConfigManager.js +267 -0
  68. package/omd/core/index.js +150 -0
  69. package/omd/core/omdEquationStack.js +347 -0
  70. package/omd/core/omdUtilities.js +115 -0
  71. package/omd/display/omdDisplay.js +443 -0
  72. package/omd/display/omdToolbar.js +502 -0
  73. package/omd/nodes/omdBinaryExpressionNode.js +460 -0
  74. package/omd/nodes/omdConstantNode.js +142 -0
  75. package/omd/nodes/omdEquationNode.js +1223 -0
  76. package/omd/nodes/omdEquationSequenceNode.js +1273 -0
  77. package/omd/nodes/omdFunctionNode.js +352 -0
  78. package/omd/nodes/omdGroupNode.js +68 -0
  79. package/omd/nodes/omdLeafNode.js +77 -0
  80. package/omd/nodes/omdNode.js +557 -0
  81. package/omd/nodes/omdOperationDisplayNode.js +322 -0
  82. package/omd/nodes/omdOperatorNode.js +109 -0
  83. package/omd/nodes/omdParenthesisNode.js +293 -0
  84. package/omd/nodes/omdPowerNode.js +236 -0
  85. package/omd/nodes/omdRationalNode.js +295 -0
  86. package/omd/nodes/omdSqrtNode.js +308 -0
  87. package/omd/nodes/omdUnaryExpressionNode.js +178 -0
  88. package/omd/nodes/omdVariableNode.js +123 -0
  89. package/omd/simplification/omdSimplification.js +171 -0
  90. package/omd/simplification/omdSimplificationEngine.js +886 -0
  91. package/omd/simplification/package.json +6 -0
  92. package/omd/simplification/rules/binaryRules.js +1037 -0
  93. package/omd/simplification/rules/functionRules.js +111 -0
  94. package/omd/simplification/rules/index.js +48 -0
  95. package/omd/simplification/rules/parenthesisRules.js +19 -0
  96. package/omd/simplification/rules/powerRules.js +143 -0
  97. package/omd/simplification/rules/rationalRules.js +475 -0
  98. package/omd/simplification/rules/sqrtRules.js +48 -0
  99. package/omd/simplification/rules/unaryRules.js +37 -0
  100. package/omd/simplification/simplificationRules.js +32 -0
  101. package/omd/simplification/simplificationUtils.js +1056 -0
  102. package/omd/step-visualizer/omdStepVisualizer.js +597 -0
  103. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
  104. package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
  105. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
  106. package/omd/utils/omdNodeOverlay.js +638 -0
  107. package/omd/utils/omdPopup.js +1084 -0
  108. package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
  109. package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
  110. package/omd/utils/omdTranscriptionService.js +125 -0
  111. package/omd/utils/omdTreeDiff.js +734 -0
  112. package/package.json +46 -0
  113. package/src/index.js +62 -0
  114. package/src/json-schemas.md +109 -0
  115. package/src/omd-json-samples.js +115 -0
  116. package/src/omd.js +109 -0
  117. package/src/omdApp.js +391 -0
  118. package/src/omdAppCanvas.js +336 -0
  119. package/src/omdBalanceHanger.js +172 -0
  120. package/src/omdColor.js +13 -0
  121. package/src/omdCoordinatePlane.js +467 -0
  122. package/src/omdEquation.js +125 -0
  123. package/src/omdExpression.js +104 -0
  124. package/src/omdFunction.js +113 -0
  125. package/src/omdMetaExpression.js +287 -0
  126. package/src/omdNaturalExpression.js +564 -0
  127. package/src/omdNode.js +384 -0
  128. package/src/omdNumber.js +53 -0
  129. package/src/omdNumberLine.js +107 -0
  130. package/src/omdNumberTile.js +119 -0
  131. package/src/omdOperator.js +73 -0
  132. package/src/omdPowerExpression.js +92 -0
  133. package/src/omdProblem.js +55 -0
  134. package/src/omdRatioChart.js +232 -0
  135. package/src/omdRationalExpression.js +115 -0
  136. package/src/omdSampleData.js +215 -0
  137. package/src/omdShapes.js +476 -0
  138. package/src/omdSpinner.js +148 -0
  139. package/src/omdString.js +39 -0
  140. package/src/omdTable.js +369 -0
  141. package/src/omdTapeDiagram.js +245 -0
  142. package/src/omdTerm.js +92 -0
  143. package/src/omdTileEquation.js +349 -0
  144. package/src/omdVariable.js +51 -0
@@ -0,0 +1,435 @@
1
+ import { pointerEventHandler } from './pointerEventHandler.js';
2
+
3
+ /**
4
+ * Event manager for canvas interactions
5
+ * Handles pointer, keyboard, and wheel events
6
+ */
7
+ export class EventManager {
8
+ /**
9
+ * @param {OMDCanvas} canvas - Canvas instance
10
+ */
11
+ constructor(canvas) {
12
+ this.canvas = canvas;
13
+ this.isInitialized = false;
14
+
15
+ // Event handlers
16
+ this.pointerEventHandler = new pointerEventHandler(canvas);
17
+
18
+ // Bound event listeners
19
+ this._onPointerDown = this._onPointerDown.bind(this);
20
+ this._onPointerMove = this._onPointerMove.bind(this);
21
+ this._onPointerUp = this._onPointerUp.bind(this);
22
+ this._onPointerCancel = this._onPointerCancel.bind(this);
23
+ this._onPointerEnter = this._onPointerEnter.bind(this);
24
+ this._onPointerLeave = this._onPointerLeave.bind(this);
25
+ this._onKeyDown = this._onKeyDown.bind(this);
26
+ this._onKeyUp = this._onKeyUp.bind(this);
27
+ this._onWheel = this._onWheel.bind(this);
28
+ this._onContextMenu = this._onContextMenu.bind(this);
29
+
30
+ // State tracking
31
+ this.activePointers = new Map();
32
+ this.isDrawing = false;
33
+ this.lastEventTime = 0;
34
+ }
35
+
36
+ /**
37
+ * Initialize event listeners
38
+ */
39
+ initialize() {
40
+ if (this.isInitialized) return;
41
+
42
+ const svg = this.canvas.svg;
43
+
44
+ // Pointer events (unified mouse/touch/pen handling)
45
+ svg.addEventListener('pointerdown', this._onPointerDown);
46
+ svg.addEventListener('pointermove', this._onPointerMove);
47
+ svg.addEventListener('pointerup', this._onPointerUp);
48
+ svg.addEventListener('pointercancel', this._onPointerCancel);
49
+ svg.addEventListener('pointerenter', this._onPointerEnter);
50
+ svg.addEventListener('pointerleave', this._onPointerLeave);
51
+
52
+ // Keyboard events (on document for global shortcuts)
53
+ if (this.canvas.config.enableKeyboardShortcuts) {
54
+ document.addEventListener('keydown', this._onKeyDown);
55
+ document.addEventListener('keyup', this._onKeyUp);
56
+ }
57
+
58
+ // Mouse wheel events
59
+ svg.addEventListener('wheel', this._onWheel);
60
+
61
+ // Prevent context menu
62
+ svg.addEventListener('contextmenu', this._onContextMenu);
63
+
64
+ // Set touch-action for better touch handling
65
+ svg.style.touchAction = 'none';
66
+
67
+ this.isInitialized = true;
68
+ }
69
+
70
+ /**
71
+ * Handle pointer down events
72
+ * @private
73
+ */
74
+ _onPointerDown(event) {
75
+ event.preventDefault();
76
+ // Track active pointer
77
+ this.activePointers.set(event.pointerId, event);
78
+ // Set pointer capture so canvas continues to receive events
79
+ event.target.setPointerCapture(event.pointerId);
80
+ // Convert to canvas coordinates
81
+ const canvasCoords = this.canvas.clientToSVG(event.clientX, event.clientY);
82
+ // Create normalized event
83
+ const normalizedEvent = this._normalizePointerEvent(event, canvasCoords);
84
+ // Handle multi-touch
85
+ if (this.canvas.config.enableMultiTouch && this.activePointers.size > 1) {
86
+ this.pointerEventHandler.handleMultiTouchStart(this.activePointers);
87
+ return;
88
+ }
89
+ // Set drawing state
90
+ this.isDrawing = true;
91
+ // Delegate to pointer event handler
92
+ this.pointerEventHandler.handlePointerDown(event, normalizedEvent);
93
+ // Delegate to active tool
94
+ const activeTool = this.canvas.toolManager.getActiveTool();
95
+ if (activeTool) {
96
+ activeTool.onPointerDown(normalizedEvent);
97
+ }
98
+ // Emit canvas event
99
+ this.canvas.emit('pointerDown', normalizedEvent);
100
+ }
101
+
102
+ /**
103
+ * Handle pointer move events
104
+ * @private
105
+ */
106
+ _onPointerMove(event) {
107
+ // Throttle events for performance
108
+ const now = Date.now();
109
+ if (now - this.lastEventTime < 16) return; // ~60fps
110
+ this.lastEventTime = now;
111
+
112
+ // Update active pointer
113
+ if (this.activePointers.has(event.pointerId)) {
114
+ this.activePointers.set(event.pointerId, event);
115
+ }
116
+
117
+ // Convert to canvas coordinates
118
+ const canvasCoords = this.canvas.clientToSVG(event.clientX, event.clientY);
119
+
120
+ // Create normalized event
121
+ const normalizedEvent = this._normalizePointerEvent(event, canvasCoords);
122
+
123
+ // Handle multi-touch
124
+ if (this.canvas.config.enableMultiTouch && this.activePointers.size > 1) {
125
+ this.pointerEventHandler.handleMultiTouchMove(this.activePointers);
126
+ return;
127
+ }
128
+
129
+ // Update cursor position
130
+ if (this.canvas.cursor) {
131
+ this.canvas.cursor.setPosition(canvasCoords.x, canvasCoords.y);
132
+
133
+ // Update pressure feedback if available
134
+ if (event.pressure !== undefined) {
135
+ this.canvas.cursor.setPressureFeedback(event.pressure);
136
+ }
137
+ }
138
+
139
+ // Delegate to pointer event handler
140
+ this.pointerEventHandler.handlePointerMove(event, normalizedEvent);
141
+
142
+ // Delegate to active tool if drawing
143
+ if (this.isDrawing) {
144
+ const activeTool = this.canvas.toolManager.getActiveTool();
145
+ if (activeTool) {
146
+ activeTool.onPointerMove(normalizedEvent);
147
+ }
148
+ }
149
+
150
+ // Emit canvas event
151
+ this.canvas.emit('pointerMove', normalizedEvent);
152
+ }
153
+
154
+ /**
155
+ * Handle pointer up events
156
+ * @private
157
+ */
158
+ _onPointerUp(event) {
159
+ // Remove from active pointers
160
+ this.activePointers.delete(event.pointerId);
161
+ // Release pointer capture
162
+ event.target.releasePointerCapture(event.pointerId);
163
+ // Convert to canvas coordinates
164
+ const canvasCoords = this.canvas.clientToSVG(event.clientX, event.clientY);
165
+ // Create normalized event
166
+ const normalizedEvent = this._normalizePointerEvent(event, canvasCoords);
167
+ // Handle multi-touch
168
+ if (this.canvas.config.enableMultiTouch && this.activePointers.size >= 1) {
169
+ this.pointerEventHandler.handleMultiTouchEnd(this.activePointers);
170
+ return;
171
+ }
172
+ // Clear drawing state
173
+ this.isDrawing = false;
174
+ // Delegate to pointer event handler
175
+ this.pointerEventHandler.handlePointerUp(event, normalizedEvent);
176
+ // Delegate to active tool
177
+ const activeTool = this.canvas.toolManager.getActiveTool();
178
+ if (activeTool) {
179
+ activeTool.onPointerUp(normalizedEvent);
180
+ }
181
+ // Emit canvas event
182
+ this.canvas.emit('pointerUp', normalizedEvent);
183
+ }
184
+
185
+ /**
186
+ * Handle pointer cancel events
187
+ * @private
188
+ */
189
+ _onPointerCancel(event) {
190
+ // Remove from active pointers
191
+ this.activePointers.delete(event.pointerId);
192
+ // Release pointer capture
193
+ event.target.releasePointerCapture(event.pointerId);
194
+ // Clear drawing state
195
+ this.isDrawing = false;
196
+ // Cancel active tool
197
+ const activeTool = this.canvas.toolManager.getActiveTool();
198
+ if (activeTool) {
199
+ activeTool.onCancel();
200
+ }
201
+ // Emit canvas event
202
+ this.canvas.emit('pointerCancel', { pointerId: event.pointerId });
203
+ }
204
+
205
+ /**
206
+ * Handle pointer enter events
207
+ * @private
208
+ */
209
+ _onPointerEnter(event) {
210
+ // Show custom cursor when entering canvas
211
+ if (this.canvas.cursor) {
212
+ this.canvas.cursor.show();
213
+ // Update cursor from current tool config
214
+ const activeTool = this.canvas.toolManager.getActiveTool();
215
+ if (activeTool && activeTool.config) {
216
+ this.canvas.cursor.updateFromToolConfig(activeTool.config);
217
+ }
218
+ }
219
+ // If mouse is down (event.buttons !== 0), start drawing
220
+ if (event.buttons !== 0) {
221
+ // Convert to canvas coordinates
222
+ const canvasCoords = this.canvas.clientToSVG(event.clientX, event.clientY);
223
+ const normalizedEvent = this._normalizePointerEvent(event, canvasCoords);
224
+ this.isDrawing = true;
225
+ const activeTool = this.canvas.toolManager.getActiveTool();
226
+ if (activeTool) {
227
+ activeTool.onPointerDown(normalizedEvent);
228
+ }
229
+ }
230
+ // Emit canvas event
231
+ this.canvas.emit('pointerEnter', { event });
232
+ }
233
+
234
+ /**
235
+ * Handle pointer leave events
236
+ * @private
237
+ */
238
+ _onPointerLeave(event) {
239
+ // Hide custom cursor when leaving canvas
240
+ if (this.canvas.cursor) {
241
+ this.canvas.cursor.hide();
242
+ }
243
+ // Do NOT cancel drawing on pointerleave; drawing continues outside canvas
244
+ // Clear active pointers
245
+ this.activePointers.clear();
246
+ // Emit canvas event
247
+ this.canvas.emit('pointerLeave', { event });
248
+ }
249
+
250
+ /**
251
+ * Handle keyboard down events
252
+ * @private
253
+ */
254
+ _onKeyDown(event) {
255
+ // Ignore if typing in input fields
256
+ if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
257
+ return;
258
+ }
259
+
260
+ const key = event.key.toLowerCase();
261
+
262
+ // Global shortcuts
263
+ switch (key) {
264
+ case 'p':
265
+ if (this.canvas.config.enabledTools.includes('pencil')) {
266
+ this.canvas.toolManager.setActiveTool('pencil');
267
+ event.preventDefault();
268
+ }
269
+ break;
270
+ case 'e':
271
+ if (this.canvas.config.enabledTools.includes('eraser')) {
272
+ this.canvas.toolManager.setActiveTool('eraser');
273
+ event.preventDefault();
274
+ }
275
+ break;
276
+ case 's':
277
+ if (this.canvas.config.enabledTools.includes('select')) {
278
+ this.canvas.toolManager.setActiveTool('select');
279
+ event.preventDefault();
280
+ }
281
+ break;
282
+ case 'escape':
283
+ // Cancel current operation
284
+ const activeTool = this.canvas.toolManager.getActiveTool();
285
+ if (activeTool) {
286
+ activeTool.onCancel();
287
+ }
288
+ event.preventDefault();
289
+ break;
290
+ }
291
+
292
+ // Delegate to active tool
293
+ const activeTool = this.canvas.toolManager.getActiveTool();
294
+ if (activeTool && activeTool.onKeyboardShortcut) {
295
+ const handled = activeTool.onKeyboardShortcut(key, event);
296
+ if (handled) {
297
+ event.preventDefault();
298
+ }
299
+ }
300
+
301
+ // Emit canvas event
302
+ this.canvas.emit('keyDown', { key, event });
303
+ }
304
+
305
+ /**
306
+ * Handle keyboard up events
307
+ * @private
308
+ */
309
+ _onKeyUp(event) {
310
+ // Ignore if typing in input fields
311
+ if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
312
+ return;
313
+ }
314
+
315
+ const key = event.key.toLowerCase();
316
+
317
+ // Emit canvas event
318
+ this.canvas.emit('keyUp', { key, event });
319
+ }
320
+
321
+ /**
322
+ * Handle wheel events (zoom/pan)
323
+ * @private
324
+ */
325
+ _onWheel(event) {
326
+ // Prevent default scrolling
327
+ event.preventDefault();
328
+
329
+ // Convert to canvas coordinates
330
+ const canvasCoords = this.canvas.clientToSVG(event.clientX, event.clientY);
331
+
332
+ // Emit canvas event
333
+ this.canvas.emit('wheel', {
334
+ deltaX: event.deltaX,
335
+ deltaY: event.deltaY,
336
+ deltaZ: event.deltaZ,
337
+ x: canvasCoords.x,
338
+ y: canvasCoords.y,
339
+ ctrlKey: event.ctrlKey,
340
+ metaKey: event.metaKey
341
+ });
342
+ }
343
+
344
+ /**
345
+ * Prevent context menu
346
+ * @private
347
+ */
348
+ _onContextMenu(event) {
349
+ event.preventDefault();
350
+ }
351
+
352
+ /**
353
+ * Normalize pointer event data
354
+ * @private
355
+ */
356
+ _normalizePointerEvent(event, canvasCoords) {
357
+ return {
358
+ pointerId: event.pointerId,
359
+ pointerType: event.pointerType,
360
+ isPrimary: event.isPrimary,
361
+ x: canvasCoords.x,
362
+ y: canvasCoords.y,
363
+ clientX: event.clientX,
364
+ clientY: event.clientY,
365
+ pressure: this._normalizePressure(event.pressure),
366
+ tiltX: event.tiltX || 0,
367
+ tiltY: event.tiltY || 0,
368
+ twist: event.twist || 0,
369
+ width: event.width || 1,
370
+ height: event.height || 1,
371
+ tangentialPressure: event.tangentialPressure || 0,
372
+ buttons: event.buttons,
373
+ shiftKey: event.shiftKey,
374
+ ctrlKey: event.ctrlKey,
375
+ altKey: event.altKey,
376
+ metaKey: event.metaKey,
377
+ timestamp: event.timeStamp || Date.now()
378
+ };
379
+ }
380
+
381
+ /**
382
+ * Normalize pressure values
383
+ * @private
384
+ */
385
+ _normalizePressure(pressure) {
386
+ if (pressure === undefined || pressure === null) {
387
+ return 0.5; // Default pressure for devices without pressure sensitivity
388
+ }
389
+
390
+ // Clamp pressure between 0 and 1
391
+ return Math.max(0, Math.min(1, pressure));
392
+ }
393
+
394
+ /**
395
+ * Get current pointer information
396
+ * @returns {Object} Pointer information
397
+ */
398
+ getPointerInfo() {
399
+ return {
400
+ activePointers: this.activePointers.size,
401
+ isDrawing: this.isDrawing,
402
+ multiTouch: this.activePointers.size > 1
403
+ };
404
+ }
405
+
406
+ /**
407
+ * Destroy event manager
408
+ */
409
+ destroy() {
410
+ if (!this.isInitialized) return;
411
+
412
+ const svg = this.canvas.svg;
413
+
414
+ // Remove pointer events
415
+ svg.removeEventListener('pointerdown', this._onPointerDown);
416
+ svg.removeEventListener('pointermove', this._onPointerMove);
417
+ svg.removeEventListener('pointerup', this._onPointerUp);
418
+ svg.removeEventListener('pointercancel', this._onPointerCancel);
419
+ svg.removeEventListener('pointerenter', this._onPointerEnter);
420
+ svg.removeEventListener('pointerleave', this._onPointerLeave);
421
+
422
+ // Remove keyboard events
423
+ document.removeEventListener('keydown', this._onKeyDown);
424
+ document.removeEventListener('keyup', this._onKeyUp);
425
+
426
+ // Remove other events
427
+ svg.removeEventListener('wheel', this._onWheel);
428
+ svg.removeEventListener('contextmenu', this._onContextMenu);
429
+
430
+ // Clear state
431
+ this.activePointers.clear();
432
+ this.isDrawing = false;
433
+ this.isInitialized = false;
434
+ }
435
+ }
@@ -0,0 +1,263 @@
1
+ export class pointerEventHandler {
2
+ /**
3
+ * @param {OMDCanvas} canvas - Canvas instance
4
+ */
5
+ constructor(canvas) {
6
+ this.canvas = canvas;
7
+
8
+ // State tracking
9
+ this.lastPointerPosition = { x: 0, y: 0 };
10
+ this.lastPointerTime = 0;
11
+ this.velocity = { x: 0, y: 0 };
12
+
13
+ // Multi-touch state
14
+ this.multiTouchState = {
15
+ isActive: false,
16
+ initialDistance: 0,
17
+ initialAngle: 0,
18
+ lastScale: 1,
19
+ lastRotation: 0
20
+ };
21
+
22
+ // Gesture thresholds
23
+ this.gestureThresholds = {
24
+ minPinchDistance: 20,
25
+ minRotationAngle: 0.1
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Handle pointer down event
31
+ * @param {PointerEvent} event - Original pointer event
32
+ * @param {Object} normalizedEvent - Normalized event data
33
+ */
34
+ handlePointerDown(event, normalizedEvent) {
35
+ this.lastPointerPosition = { x: normalizedEvent.x, y: normalizedEvent.y };
36
+ this.lastPointerTime = normalizedEvent.timestamp;
37
+ this.velocity = { x: 0, y: 0 };
38
+
39
+ // Update velocity and pressure for active tool
40
+ normalizedEvent.velocity = this.velocity;
41
+ normalizedEvent.normalizedPressure = this._normalizePressure(normalizedEvent.pressure);
42
+ }
43
+
44
+ /**
45
+ * Handle pointer move event
46
+ * @param {PointerEvent} event - Original pointer event
47
+ * @param {Object} normalizedEvent - Normalized event data
48
+ */
49
+ handlePointerMove(event, normalizedEvent) {
50
+ // Calculate velocity
51
+ this._calculateVelocity(normalizedEvent);
52
+
53
+ // Handle coalesced events for smoother drawing
54
+ if (event.getCoalescedEvents) {
55
+ const coalescedEvents = event.getCoalescedEvents();
56
+ normalizedEvent.coalescedEvents = coalescedEvents.map(coalescedEvent => {
57
+ const coalescedCoords = this.canvas.clientToSVG(coalescedEvent.clientX, coalescedEvent.clientY);
58
+ return {
59
+ x: coalescedCoords.x,
60
+ y: coalescedCoords.y,
61
+ pressure: this._normalizePressure(coalescedEvent.pressure),
62
+ timestamp: coalescedEvent.timeStamp || Date.now()
63
+ };
64
+ });
65
+ }
66
+
67
+ // Add velocity and pressure data
68
+ normalizedEvent.velocity = this.velocity;
69
+ normalizedEvent.normalizedPressure = this._normalizePressure(normalizedEvent.pressure);
70
+
71
+ // Update last position and time
72
+ this.lastPointerPosition = { x: normalizedEvent.x, y: normalizedEvent.y };
73
+ this.lastPointerTime = normalizedEvent.timestamp;
74
+ }
75
+
76
+ /**
77
+ * Handle pointer up event
78
+ * @param {PointerEvent} event - Original pointer event
79
+ * @param {Object} normalizedEvent - Normalized event data
80
+ */
81
+ handlePointerUp(event, normalizedEvent) {
82
+ // Final velocity calculation
83
+ this._calculateVelocity(normalizedEvent);
84
+ normalizedEvent.velocity = this.velocity;
85
+ normalizedEvent.normalizedPressure = this._normalizePressure(normalizedEvent.pressure);
86
+
87
+ // Reset velocity
88
+ this.velocity = { x: 0, y: 0 };
89
+ }
90
+
91
+ /**
92
+ * Handle multi-touch start
93
+ * @param {Map} activePointers - Map of active pointers
94
+ */
95
+ handleMultiTouchStart(activePointers) {
96
+ if (activePointers.size === 2) {
97
+ const pointers = Array.from(activePointers.values());
98
+ const pointer1 = pointers[0];
99
+ const pointer2 = pointers[1];
100
+
101
+ this.multiTouchState.isActive = true;
102
+ this.multiTouchState.initialDistance = this._calculateDistance(pointer1, pointer2);
103
+ this.multiTouchState.initialAngle = this._calculateAngle(pointer1, pointer2);
104
+ this.multiTouchState.lastScale = 1;
105
+ this.multiTouchState.lastRotation = 0;
106
+
107
+ this.canvas.emit('multiTouchStart', {
108
+ pointers: pointers,
109
+ distance: this.multiTouchState.initialDistance,
110
+ angle: this.multiTouchState.initialAngle
111
+ });
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Handle multi-touch move
117
+ * @param {Map} activePointers - Map of active pointers
118
+ */
119
+ handleMultiTouchMove(activePointers) {
120
+ if (activePointers.size === 2 && this.multiTouchState.isActive) {
121
+ const pointers = Array.from(activePointers.values());
122
+ const pointer1 = pointers[0];
123
+ const pointer2 = pointers[1];
124
+
125
+ const currentDistance = this._calculateDistance(pointer1, pointer2);
126
+ const currentAngle = this._calculateAngle(pointer1, pointer2);
127
+
128
+ const scale = currentDistance / this.multiTouchState.initialDistance;
129
+ const rotation = currentAngle - this.multiTouchState.initialAngle;
130
+
131
+ // Detect pinch gesture
132
+ if (Math.abs(scale - 1) > 0.1) {
133
+ this.canvas.emit('pinch', {
134
+ scale: scale,
135
+ deltaScale: scale - this.multiTouchState.lastScale,
136
+ center: this._calculateCenter(pointer1, pointer2)
137
+ });
138
+ this.multiTouchState.lastScale = scale;
139
+ }
140
+
141
+ // Detect rotation gesture
142
+ if (Math.abs(rotation) > this.gestureThresholds.minRotationAngle) {
143
+ this.canvas.emit('rotate', {
144
+ rotation: rotation,
145
+ deltaRotation: rotation - this.multiTouchState.lastRotation,
146
+ center: this._calculateCenter(pointer1, pointer2)
147
+ });
148
+ this.multiTouchState.lastRotation = rotation;
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Handle multi-touch end
155
+ * @param {Map} activePointers - Map of active pointers
156
+ */
157
+ handleMultiTouchEnd(activePointers) {
158
+ if (activePointers.size < 2) {
159
+ this.multiTouchState.isActive = false;
160
+ this.canvas.emit('multiTouchEnd');
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Calculate velocity between pointer events
166
+ * @private
167
+ */
168
+ _calculateVelocity(normalizedEvent) {
169
+ const deltaTime = normalizedEvent.timestamp - this.lastPointerTime;
170
+
171
+ if (deltaTime > 0) {
172
+ const deltaX = normalizedEvent.x - this.lastPointerPosition.x;
173
+ const deltaY = normalizedEvent.y - this.lastPointerPosition.y;
174
+
175
+ this.velocity.x = deltaX / deltaTime * 1000; // pixels per second
176
+ this.velocity.y = deltaY / deltaTime * 1000;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Normalize pressure value with device-specific adjustments
182
+ * @param {number} pressure - Raw pressure value
183
+ * @returns {number} Normalized pressure (0-1)
184
+ * @private
185
+ */
186
+ _normalizePressure(pressure = 0.5) {
187
+ if (pressure === undefined || pressure === null) {
188
+ return 0.5;
189
+ }
190
+
191
+ // Device-specific pressure normalization
192
+ // Some devices report pressure differently
193
+ let normalizedPressure = pressure;
194
+
195
+ // Apple Pencil typically reports good pressure values
196
+ // Surface Pen and other styluses might need adjustment
197
+ if (normalizedPressure < 0.1) {
198
+ normalizedPressure = 0.1; // Minimum pressure to ensure visibility
199
+ }
200
+
201
+ // Apply slight curve to make pressure feel more natural
202
+ normalizedPressure = Math.pow(normalizedPressure, 0.8);
203
+
204
+ return Math.max(0, Math.min(1, normalizedPressure));
205
+ }
206
+
207
+ /**
208
+ * Calculate distance between two pointers
209
+ * @private
210
+ */
211
+ _calculateDistance(pointer1, pointer2) {
212
+ const dx = pointer2.clientX - pointer1.clientX;
213
+ const dy = pointer2.clientY - pointer1.clientY;
214
+ return Math.sqrt(dx * dx + dy * dy);
215
+ }
216
+
217
+ /**
218
+ * Calculate angle between two pointers
219
+ * @private
220
+ */
221
+ _calculateAngle(pointer1, pointer2) {
222
+ return Math.atan2(
223
+ pointer2.clientY - pointer1.clientY,
224
+ pointer2.clientX - pointer1.clientX
225
+ );
226
+ }
227
+
228
+ /**
229
+ * Calculate center point between two pointers
230
+ * @private
231
+ */
232
+ _calculateCenter(pointer1, pointer2) {
233
+ const centerX = (pointer1.clientX + pointer2.clientX) / 2;
234
+ const centerY = (pointer1.clientY + pointer2.clientY) / 2;
235
+
236
+ return this.canvas.clientToSVG(centerX, centerY);
237
+ }
238
+
239
+ /**
240
+ * Get velocity information
241
+ * @returns {Object} Current velocity data
242
+ */
243
+ getVelocity() {
244
+ return {
245
+ x: this.velocity.x,
246
+ y: this.velocity.y,
247
+ magnitude: Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.y * this.velocity.y),
248
+ angle: Math.atan2(this.velocity.y, this.velocity.x)
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Get multi-touch state
254
+ * @returns {Object} Current multi-touch state
255
+ */
256
+ getMultiTouchState() {
257
+ return {
258
+ isActive: this.multiTouchState.isActive,
259
+ scale: this.multiTouchState.lastScale,
260
+ rotation: this.multiTouchState.lastRotation
261
+ };
262
+ }
263
+ }