@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.
- package/README.md +138 -0
- package/canvas/core/canvasConfig.js +203 -0
- package/canvas/core/omdCanvas.js +475 -0
- package/canvas/drawing/segment.js +168 -0
- package/canvas/drawing/stroke.js +386 -0
- package/canvas/events/eventManager.js +435 -0
- package/canvas/events/pointerEventHandler.js +263 -0
- package/canvas/features/focusFrameManager.js +287 -0
- package/canvas/index.js +49 -0
- package/canvas/tools/eraserTool.js +322 -0
- package/canvas/tools/pencilTool.js +319 -0
- package/canvas/tools/selectTool.js +457 -0
- package/canvas/tools/tool.js +223 -0
- package/canvas/tools/toolManager.js +394 -0
- package/canvas/ui/cursor.js +438 -0
- package/canvas/ui/toolbar.js +304 -0
- package/canvas/utils/boundingBox.js +378 -0
- package/canvas/utils/mathUtils.js +259 -0
- package/docs/api/configuration-options.md +104 -0
- package/docs/api/eventManager.md +68 -0
- package/docs/api/focusFrameManager.md +150 -0
- package/docs/api/index.md +91 -0
- package/docs/api/main.md +58 -0
- package/docs/api/omdBinaryExpressionNode.md +227 -0
- package/docs/api/omdCanvas.md +142 -0
- package/docs/api/omdConfigManager.md +192 -0
- package/docs/api/omdConstantNode.md +117 -0
- package/docs/api/omdDisplay.md +121 -0
- package/docs/api/omdEquationNode.md +161 -0
- package/docs/api/omdEquationSequenceNode.md +301 -0
- package/docs/api/omdEquationStack.md +139 -0
- package/docs/api/omdFunctionNode.md +141 -0
- package/docs/api/omdGroupNode.md +182 -0
- package/docs/api/omdHelpers.md +96 -0
- package/docs/api/omdLeafNode.md +163 -0
- package/docs/api/omdNode.md +101 -0
- package/docs/api/omdOperationDisplayNode.md +139 -0
- package/docs/api/omdOperatorNode.md +127 -0
- package/docs/api/omdParenthesisNode.md +122 -0
- package/docs/api/omdPopup.md +117 -0
- package/docs/api/omdPowerNode.md +127 -0
- package/docs/api/omdRationalNode.md +128 -0
- package/docs/api/omdSequenceNode.md +128 -0
- package/docs/api/omdSimplification.md +110 -0
- package/docs/api/omdSqrtNode.md +79 -0
- package/docs/api/omdStepVisualizer.md +115 -0
- package/docs/api/omdStepVisualizerHighlighting.md +61 -0
- package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
- package/docs/api/omdStepVisualizerLayout.md +60 -0
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
- package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
- package/docs/api/omdToolbar.md +102 -0
- package/docs/api/omdTranscriptionService.md +76 -0
- package/docs/api/omdTreeDiff.md +134 -0
- package/docs/api/omdUnaryExpressionNode.md +174 -0
- package/docs/api/omdUtilities.md +70 -0
- package/docs/api/omdVariableNode.md +148 -0
- package/docs/api/selectTool.md +74 -0
- package/docs/api/simplificationEngine.md +98 -0
- package/docs/api/simplificationRules.md +77 -0
- package/docs/api/simplificationUtils.md +64 -0
- package/docs/api/transcribe.md +43 -0
- package/docs/api-reference.md +85 -0
- package/docs/index.html +454 -0
- package/docs/user-guide.md +9 -0
- package/index.js +67 -0
- package/omd/config/omdConfigManager.js +267 -0
- package/omd/core/index.js +150 -0
- package/omd/core/omdEquationStack.js +347 -0
- package/omd/core/omdUtilities.js +115 -0
- package/omd/display/omdDisplay.js +443 -0
- package/omd/display/omdToolbar.js +502 -0
- package/omd/nodes/omdBinaryExpressionNode.js +460 -0
- package/omd/nodes/omdConstantNode.js +142 -0
- package/omd/nodes/omdEquationNode.js +1223 -0
- package/omd/nodes/omdEquationSequenceNode.js +1273 -0
- package/omd/nodes/omdFunctionNode.js +352 -0
- package/omd/nodes/omdGroupNode.js +68 -0
- package/omd/nodes/omdLeafNode.js +77 -0
- package/omd/nodes/omdNode.js +557 -0
- package/omd/nodes/omdOperationDisplayNode.js +322 -0
- package/omd/nodes/omdOperatorNode.js +109 -0
- package/omd/nodes/omdParenthesisNode.js +293 -0
- package/omd/nodes/omdPowerNode.js +236 -0
- package/omd/nodes/omdRationalNode.js +295 -0
- package/omd/nodes/omdSqrtNode.js +308 -0
- package/omd/nodes/omdUnaryExpressionNode.js +178 -0
- package/omd/nodes/omdVariableNode.js +123 -0
- package/omd/simplification/omdSimplification.js +171 -0
- package/omd/simplification/omdSimplificationEngine.js +886 -0
- package/omd/simplification/package.json +6 -0
- package/omd/simplification/rules/binaryRules.js +1037 -0
- package/omd/simplification/rules/functionRules.js +111 -0
- package/omd/simplification/rules/index.js +48 -0
- package/omd/simplification/rules/parenthesisRules.js +19 -0
- package/omd/simplification/rules/powerRules.js +143 -0
- package/omd/simplification/rules/rationalRules.js +475 -0
- package/omd/simplification/rules/sqrtRules.js +48 -0
- package/omd/simplification/rules/unaryRules.js +37 -0
- package/omd/simplification/simplificationRules.js +32 -0
- package/omd/simplification/simplificationUtils.js +1056 -0
- package/omd/step-visualizer/omdStepVisualizer.js +597 -0
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
- package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
- package/omd/utils/omdNodeOverlay.js +638 -0
- package/omd/utils/omdPopup.js +1084 -0
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
- package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
- package/omd/utils/omdTranscriptionService.js +125 -0
- package/omd/utils/omdTreeDiff.js +734 -0
- package/package.json +46 -0
- package/src/index.js +62 -0
- package/src/json-schemas.md +109 -0
- package/src/omd-json-samples.js +115 -0
- package/src/omd.js +109 -0
- package/src/omdApp.js +391 -0
- package/src/omdAppCanvas.js +336 -0
- package/src/omdBalanceHanger.js +172 -0
- package/src/omdColor.js +13 -0
- package/src/omdCoordinatePlane.js +467 -0
- package/src/omdEquation.js +125 -0
- package/src/omdExpression.js +104 -0
- package/src/omdFunction.js +113 -0
- package/src/omdMetaExpression.js +287 -0
- package/src/omdNaturalExpression.js +564 -0
- package/src/omdNode.js +384 -0
- package/src/omdNumber.js +53 -0
- package/src/omdNumberLine.js +107 -0
- package/src/omdNumberTile.js +119 -0
- package/src/omdOperator.js +73 -0
- package/src/omdPowerExpression.js +92 -0
- package/src/omdProblem.js +55 -0
- package/src/omdRatioChart.js +232 -0
- package/src/omdRationalExpression.js +115 -0
- package/src/omdSampleData.js +215 -0
- package/src/omdShapes.js +476 -0
- package/src/omdSpinner.js +148 -0
- package/src/omdString.js +39 -0
- package/src/omdTable.js +369 -0
- package/src/omdTapeDiagram.js +245 -0
- package/src/omdTerm.js +92 -0
- package/src/omdTileEquation.js +349 -0
- 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
|
+
}
|