@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,475 @@
1
+ import { CanvasConfig } from './canvasConfig.js';
2
+ import { EventManager } from '../events/eventManager.js';
3
+ import { ToolManager } from '../tools/toolManager.js';
4
+ import { PencilTool } from '../tools/PencilTool.js';
5
+ import { EraserTool } from '../tools/EraserTool.js';
6
+ import { SelectTool } from '../tools/SelectTool.js';
7
+ import { Cursor } from '../ui/cursor.js';
8
+ import { Toolbar } from '../ui/toolbar.js';
9
+ import { FocusFrameManager } from '../features/focusFrameManager.js';
10
+
11
+ /**
12
+ * Main OMD Canvas class
13
+ * Provides the primary interface for creating and managing a drawing canvas
14
+ */
15
+ export class omdCanvas {
16
+ /**
17
+ * @param {HTMLElement|string} container - Container element or selector
18
+ * @param {Object} options - Configuration options
19
+ */
20
+ constructor(container, options = {}) {
21
+ // Resolve container element
22
+ this.container = typeof container === 'string'
23
+ ? document.querySelector(container)
24
+ : container;
25
+
26
+ if (!this.container) {
27
+ throw new Error('Container element not found');
28
+ }
29
+
30
+ // Set container to relative positioning for absolute positioned children
31
+ this.container.style.position = 'relative';
32
+
33
+ // Initialize configuration
34
+ this.config = new CanvasConfig(options);
35
+
36
+ // Initialize state
37
+ this.strokes = new Map();
38
+ this.selectedStrokes = new Set();
39
+ this.isDestroyed = false;
40
+ this.strokeCounter = 0;
41
+
42
+ // Event system
43
+ this.listeners = new Map();
44
+
45
+ // Initialize canvas
46
+ this._createSVGContainer();
47
+ this._createLayers();
48
+ this._initializeManagers();
49
+ this._setupEventListeners();
50
+
51
+ console.log('OMDCanvas initialized successfully');
52
+ }
53
+
54
+ /**
55
+ * Create the main SVG container
56
+ * @private
57
+ */
58
+ _createSVGContainer() {
59
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
60
+ this.svg.setAttribute('width', this.config.width);
61
+ this.svg.setAttribute('height', this.config.height);
62
+ this.svg.setAttribute('viewBox', `0 0 ${this.config.width} ${this.config.height}`);
63
+ this.svg.style.cssText = `
64
+ display: block;
65
+ background: ${this.config.backgroundColor};
66
+ cursor: none;
67
+ touch-action: none;
68
+ user-select: none;
69
+ `;
70
+
71
+ this.container.appendChild(this.svg);
72
+ }
73
+
74
+ /**
75
+ * Create SVG layers for different canvas elements
76
+ * @private
77
+ */
78
+ _createLayers() {
79
+ // Background layer (grid, etc.)
80
+ this.backgroundLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
81
+ this.backgroundLayer.setAttribute('class', 'background-layer');
82
+ this.svg.appendChild(this.backgroundLayer);
83
+
84
+ // Drawing layer (strokes)
85
+ this.drawingLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
86
+ this.drawingLayer.setAttribute('class', 'drawing-layer');
87
+ this.svg.appendChild(this.drawingLayer);
88
+
89
+ // UI layer (selection boxes, etc.)
90
+ this.uiLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
91
+ this.uiLayer.setAttribute('class', 'ui-layer');
92
+ this.svg.appendChild(this.uiLayer);
93
+
94
+ // Focus frame layer
95
+ this.focusFrameLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
96
+ this.focusFrameLayer.setAttribute('class', 'focus-frame-layer');
97
+ this.svg.appendChild(this.focusFrameLayer);
98
+
99
+ // Add grid if enabled
100
+ if (this.config.showGrid) {
101
+ this._createGrid();
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Initialize managers
107
+ * @private
108
+ */
109
+ _initializeManagers() {
110
+ // Tool manager
111
+ this.toolManager = new ToolManager(this);
112
+
113
+ // Register default tools
114
+ if (this.config.enabledTools.includes('pencil')) {
115
+ this.toolManager.registerTool('pencil', new PencilTool(this));
116
+ }
117
+ if (this.config.enabledTools.includes('eraser')) {
118
+ this.toolManager.registerTool('eraser', new EraserTool(this));
119
+ }
120
+ if (this.config.enabledTools.includes('select')) {
121
+ this.toolManager.registerTool('select', new SelectTool(this));
122
+ }
123
+
124
+ // Set default tool
125
+ if (this.config.defaultTool && this.config.enabledTools.includes(this.config.defaultTool)) {
126
+ this.toolManager.setActiveTool(this.config.defaultTool);
127
+ }
128
+
129
+ // Event manager
130
+ this.eventManager = new EventManager(this);
131
+ this.eventManager.initialize();
132
+
133
+ // Cursor
134
+ this.cursor = new Cursor(this);
135
+
136
+ // Toolbar
137
+ if (this.config.showToolbar) {
138
+ this.toolbar = new Toolbar(this);
139
+ // Toolbar is now added directly to container in its constructor
140
+ }
141
+
142
+ // Cursor will be shown/hidden based on mouse enter/leave events
143
+
144
+ // Focus frame manager
145
+ if (this.config.enableFocusFrames) {
146
+ this.focusFrameManager = new FocusFrameManager(this);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Setup event listeners
152
+ * @private
153
+ */
154
+ _setupEventListeners() {
155
+ // Listen for window resize
156
+ this._resizeHandler = () => this._handleResize();
157
+ window.addEventListener('resize', this._resizeHandler);
158
+
159
+ // Hide cursor initially - EventManager will handle show/hide
160
+ if (this.cursor) {
161
+ this.cursor.hide();
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Create grid background
167
+ * @private
168
+ */
169
+ _createGrid() {
170
+ const gridSize = 20;
171
+ const pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
172
+ pattern.setAttribute('id', 'grid');
173
+ pattern.setAttribute('width', gridSize);
174
+ pattern.setAttribute('height', gridSize);
175
+ pattern.setAttribute('patternUnits', 'userSpaceOnUse');
176
+
177
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
178
+ path.setAttribute('d', `M ${gridSize} 0 L 0 0 0 ${gridSize}`);
179
+ path.setAttribute('fill', 'none');
180
+ path.setAttribute('stroke', '#ddd');
181
+ path.setAttribute('stroke-width', '1');
182
+
183
+ pattern.appendChild(path);
184
+
185
+ const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
186
+ defs.appendChild(pattern);
187
+ this.svg.insertBefore(defs, this.backgroundLayer);
188
+
189
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
190
+ rect.setAttribute('width', '100%');
191
+ rect.setAttribute('height', '100%');
192
+ rect.setAttribute('fill', 'url(#grid)');
193
+ this.backgroundLayer.appendChild(rect);
194
+ }
195
+
196
+ /**
197
+ * Add a stroke to the canvas
198
+ * @param {Stroke} stroke - Stroke to add
199
+ * @returns {string} Stroke ID
200
+ */
201
+ addStroke(stroke) {
202
+ const id = `stroke_${++this.strokeCounter}`;
203
+ stroke.id = id;
204
+ this.strokes.set(id, stroke);
205
+ this.drawingLayer.appendChild(stroke.element);
206
+
207
+ this.emit('strokeAdded', { id, stroke });
208
+ return id;
209
+ }
210
+
211
+ /**
212
+ * Remove a stroke from the canvas
213
+ * @param {string} strokeId - ID of stroke to remove
214
+ * @returns {boolean} True if stroke was removed
215
+ */
216
+ removeStroke(strokeId) {
217
+ const stroke = this.strokes.get(strokeId);
218
+ if (!stroke) return false;
219
+
220
+ if (stroke.element.parentNode) {
221
+ stroke.element.parentNode.removeChild(stroke.element);
222
+ }
223
+
224
+ this.strokes.delete(strokeId);
225
+ this.selectedStrokes.delete(strokeId);
226
+
227
+ this.emit('strokeRemoved', { strokeId });
228
+ return true;
229
+ }
230
+
231
+ /**
232
+ * Clear all strokes
233
+ */
234
+ clear() {
235
+ this.strokes.forEach((stroke, id) => {
236
+ if (stroke.element.parentNode) {
237
+ stroke.element.parentNode.removeChild(stroke.element);
238
+ }
239
+ });
240
+
241
+ this.strokes.clear();
242
+ this.selectedStrokes.clear();
243
+
244
+ this.emit('cleared');
245
+ }
246
+
247
+ /**
248
+ * Select strokes by IDs
249
+ * @param {Array<string>} strokeIds - Array of stroke IDs
250
+ */
251
+ selectStrokes(strokeIds) {
252
+ this.selectedStrokes.clear();
253
+ strokeIds.forEach(id => {
254
+ if (this.strokes.has(id)) {
255
+ this.selectedStrokes.add(id);
256
+ }
257
+ });
258
+
259
+ this._updateStrokeSelection();
260
+ this.emit('selectionChanged', { selected: Array.from(this.selectedStrokes) });
261
+ }
262
+
263
+ /**
264
+ * Update visual selection state of strokes
265
+ * @private
266
+ */
267
+ _updateStrokeSelection() {
268
+ this.strokes.forEach((stroke, id) => {
269
+ const isSelected = this.selectedStrokes.has(id);
270
+ if (stroke.setSelected) {
271
+ stroke.setSelected(isSelected);
272
+ }
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Convert client coordinates to SVG coordinates
278
+ * @param {number} clientX - Client X coordinate
279
+ * @param {number} clientY - Client Y coordinate
280
+ * @returns {Object} {x, y} SVG coordinates
281
+ */
282
+ clientToSVG(clientX, clientY) {
283
+ const rect = this.svg.getBoundingClientRect();
284
+ const scaleX = this.config.width / rect.width;
285
+ const scaleY = this.config.height / rect.height;
286
+
287
+ return {
288
+ x: (clientX - rect.left) * scaleX,
289
+ y: (clientY - rect.top) * scaleY
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Export canvas as SVG string
295
+ * @returns {string} SVG content
296
+ */
297
+ exportSVG() {
298
+ const svgClone = this.svg.cloneNode(true);
299
+
300
+ // Remove UI elements from export
301
+ const uiLayer = svgClone.querySelector('.ui-layer');
302
+ if (uiLayer) uiLayer.remove();
303
+
304
+ const focusFrameLayer = svgClone.querySelector('.focus-frame-layer');
305
+ if (focusFrameLayer) focusFrameLayer.remove();
306
+
307
+ return new XMLSerializer().serializeToString(svgClone);
308
+ }
309
+
310
+ /**
311
+ * Export canvas as image
312
+ * @param {string} format - Image format (png, jpeg, webp)
313
+ * @param {number} quality - Image quality (0-1)
314
+ * @returns {Promise<Blob>} Image blob
315
+ */
316
+ async exportImage(format = 'png', quality = 1) {
317
+ const svgData = this.exportSVG();
318
+ const canvas = document.createElement('canvas');
319
+ canvas.width = this.config.width;
320
+ canvas.height = this.config.height;
321
+
322
+ const ctx = canvas.getContext('2d');
323
+ const img = new Image();
324
+
325
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml' });
326
+ const url = URL.createObjectURL(svgBlob);
327
+
328
+ try {
329
+ await new Promise((resolve, reject) => {
330
+ img.onload = resolve;
331
+ img.onerror = reject;
332
+ img.src = url;
333
+ });
334
+
335
+ ctx.drawImage(img, 0, 0);
336
+
337
+ return new Promise(resolve => {
338
+ canvas.toBlob(resolve, `image/${format}`, quality);
339
+ });
340
+ } finally {
341
+ URL.revokeObjectURL(url);
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Resize canvas
347
+ * @param {number} width - New width
348
+ * @param {number} height - New height
349
+ */
350
+ resize(width, height) {
351
+ this.config.width = width;
352
+ this.config.height = height;
353
+
354
+ this.svg.setAttribute('width', width);
355
+ this.svg.setAttribute('height', height);
356
+ this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
357
+
358
+ this.emit('resized', { width, height });
359
+ }
360
+
361
+ /**
362
+ * Handle window resize
363
+ * @private
364
+ */
365
+ _handleResize() {
366
+ // Optional: implement responsive behavior
367
+ }
368
+
369
+ /**
370
+ * Toggle grid visibility
371
+ */
372
+ toggleGrid() {
373
+ if (this.backgroundLayer.querySelector('rect[fill="url(#grid)"]')) {
374
+ // Remove grid
375
+ const gridRect = this.backgroundLayer.querySelector('rect[fill="url(#grid)"]');
376
+ if (gridRect) gridRect.remove();
377
+ const defs = this.svg.querySelector('defs');
378
+ if (defs) defs.remove();
379
+ } else {
380
+ // Add grid
381
+ this._createGrid();
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Add event listener
387
+ * @param {string} event - Event name
388
+ * @param {Function} callback - Event callback
389
+ */
390
+ on(event, callback) {
391
+ if (!this.listeners.has(event)) {
392
+ this.listeners.set(event, []);
393
+ }
394
+ this.listeners.get(event).push(callback);
395
+ }
396
+
397
+ /**
398
+ * Remove event listener
399
+ * @param {string} event - Event name
400
+ * @param {Function} callback - Event callback
401
+ */
402
+ off(event, callback) {
403
+ const callbacks = this.listeners.get(event);
404
+ if (callbacks) {
405
+ const index = callbacks.indexOf(callback);
406
+ if (index !== -1) {
407
+ callbacks.splice(index, 1);
408
+ }
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Emit event
414
+ * @param {string} event - Event name
415
+ * @param {Object} data - Event data
416
+ */
417
+ emit(event, data = {}) {
418
+ const callbacks = this.listeners.get(event);
419
+ if (callbacks) {
420
+ callbacks.forEach(callback => {
421
+ try {
422
+ callback({ type: event, detail: data });
423
+ } catch (error) {
424
+ console.error(`Error in event listener for ${event}:`, error);
425
+ }
426
+ });
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Get canvas information
432
+ * @returns {Object} Canvas information
433
+ */
434
+ getInfo() {
435
+ return {
436
+ width: this.config.width,
437
+ height: this.config.height,
438
+ strokeCount: this.strokes.size,
439
+ selectedStrokeCount: this.selectedStrokes.size,
440
+ activeTool: this.toolManager.getActiveTool()?.name,
441
+ availableTools: this.toolManager.getToolNames(),
442
+ isDestroyed: this.isDestroyed
443
+ };
444
+ }
445
+
446
+ /**
447
+ * Destroy the canvas and clean up resources
448
+ */
449
+ destroy() {
450
+ if (this.isDestroyed) return;
451
+
452
+ // Clean up event listeners
453
+ window.removeEventListener('resize', this._resizeHandler);
454
+
455
+ // Destroy managers
456
+ if (this.eventManager) this.eventManager.destroy();
457
+ if (this.toolManager) this.toolManager.destroy();
458
+ if (this.focusFrameManager) this.focusFrameManager.destroy();
459
+ if (this.toolbar) this.toolbar.destroy();
460
+ if (this.cursor) this.cursor.destroy();
461
+
462
+ // Remove DOM elements
463
+ if (this.svg.parentNode) {
464
+ this.svg.parentNode.removeChild(this.svg);
465
+ }
466
+
467
+ // Clear state
468
+ this.strokes.clear();
469
+ this.selectedStrokes.clear();
470
+ this.listeners.clear();
471
+
472
+ this.isDestroyed = true;
473
+ this.emit('destroyed');
474
+ }
475
+ }
@@ -0,0 +1,168 @@
1
+ import { BoundingBox } from '../utils/boundingBox.js';
2
+
3
+ /**
4
+ * Represents a segment of a stroke
5
+ */
6
+ export class segment {
7
+ /**
8
+ * @param {Object} options - Segment configuration
9
+ */
10
+ constructor(options = {}) {
11
+ this.id = options.id || this._generateId();
12
+ this.points = [];
13
+ this.strokeWidth = options.strokeWidth || 5;
14
+ this.strokeColor = options.strokeColor || '#000000';
15
+ this.strokeOpacity = options.strokeOpacity || 1;
16
+
17
+ this.boundingBox = new BoundingBox();
18
+ this.isFinished = false;
19
+
20
+ // Create SVG element
21
+ this._createElement();
22
+
23
+ // Add starting point if provided
24
+ if (options.x !== undefined && options.y !== undefined) {
25
+ this.addPoint(options.x, options.y, options.pressure);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Create SVG path element
31
+ * @private
32
+ */
33
+ _createElement() {
34
+ this.element = document.createElementNS('http://www.w3.org/2000/svg', 'path');
35
+ this.element.setAttribute('fill', 'none');
36
+ this.element.setAttribute('stroke', this.strokeColor);
37
+ this.element.setAttribute('stroke-width', this.strokeWidth);
38
+ this.element.setAttribute('stroke-opacity', this.strokeOpacity);
39
+ this.element.setAttribute('stroke-linecap', 'round');
40
+ this.element.setAttribute('stroke-linejoin', 'round');
41
+ this.element.setAttribute('data-segment-id', this.id);
42
+ }
43
+
44
+ /**
45
+ * Add point to segment
46
+ */
47
+ addPoint(x, y, pressure = 0.5) {
48
+ const point = { x, y, pressure, timestamp: Date.now() };
49
+ this.points.push(point);
50
+ this._updatePath();
51
+ this._updateBoundingBox();
52
+ }
53
+
54
+ /**
55
+ * Update SVG path
56
+ * @private
57
+ */
58
+ _updatePath() {
59
+ if (this.points.length === 0) return;
60
+
61
+ let pathData = '';
62
+
63
+ if (this.points.length === 1) {
64
+ const point = this.points[0];
65
+ pathData = `M ${point.x},${point.y} L ${point.x + 0.1},${point.y}`;
66
+ } else {
67
+ pathData = `M ${this.points[0].x},${this.points[0].y}`;
68
+ for (let i = 1; i < this.points.length; i++) {
69
+ pathData += ` L ${this.points[i].x},${this.points[i].y}`;
70
+ }
71
+ }
72
+
73
+ this.element.setAttribute('d', pathData);
74
+ }
75
+
76
+ /**
77
+ * Update bounding box
78
+ * @private
79
+ */
80
+ _updateBoundingBox() {
81
+ if (this.points.length === 0) return;
82
+
83
+ let minX = Infinity, minY = Infinity;
84
+ let maxX = -Infinity, maxY = -Infinity;
85
+
86
+ this.points.forEach(point => {
87
+ const radius = this.strokeWidth / 2;
88
+ minX = Math.min(minX, point.x - radius);
89
+ minY = Math.min(minY, point.y - radius);
90
+ maxX = Math.max(maxX, point.x + radius);
91
+ maxY = Math.max(maxY, point.y + radius);
92
+ });
93
+
94
+ this.boundingBox.set(minX, minY, maxX - minX, maxY - minY);
95
+ }
96
+
97
+ /**
98
+ * Mark segment as finished
99
+ */
100
+ finish() {
101
+ this.isFinished = true;
102
+ this.element.setAttribute('data-finished', 'true');
103
+ }
104
+
105
+ /**
106
+ * Generate unique ID
107
+ * @private
108
+ */
109
+ _generateId() {
110
+ return `segment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
111
+ }
112
+
113
+ /**
114
+ * Get segment length
115
+ */
116
+ getLength() {
117
+ let length = 0;
118
+ for (let i = 1; i < this.points.length; i++) {
119
+ const dx = this.points[i].x - this.points[i-1].x;
120
+ const dy = this.points[i].y - this.points[i-1].y;
121
+ length += Math.sqrt(dx * dx + dy * dy);
122
+ }
123
+ return length;
124
+ }
125
+
126
+ /**
127
+ * Check if point is near segment
128
+ */
129
+ isNearPoint(x, y, tolerance = 10) {
130
+ return this.boundingBox.containsPoint(x, y, tolerance);
131
+ }
132
+
133
+ /**
134
+ * Convert to JSON
135
+ */
136
+ toJSON() {
137
+ return {
138
+ id: this.id,
139
+ points: this.points,
140
+ strokeWidth: this.strokeWidth,
141
+ strokeColor: this.strokeColor,
142
+ strokeOpacity: this.strokeOpacity,
143
+ isFinished: this.isFinished
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Create from JSON
149
+ */
150
+ static fromJSON(data) {
151
+ const segment = new Segment({
152
+ id: data.id,
153
+ strokeWidth: data.strokeWidth,
154
+ strokeColor: data.strokeColor,
155
+ strokeOpacity: data.strokeOpacity
156
+ });
157
+
158
+ data.points.forEach(point => {
159
+ segment.addPoint(point.x, point.y, point.pressure);
160
+ });
161
+
162
+ if (data.isFinished) {
163
+ segment.finish();
164
+ }
165
+
166
+ return segment;
167
+ }
168
+ }