@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,491 @@
1
+ import { omdColor } from '../../src/omdColor.js';
2
+ import { jsvgLayoutGroup, jsvgTextBox } from '@teachinglab/jsvg';
3
+ /**
4
+ * Creates interactive step elements using jsvgLayoutGroup for multiple simplification steps
5
+ * Each step is a separate jsvgTextBox that can have hover interactions with the omdSequence
6
+ */
7
+ export class omdStepVisualizerInteractiveSteps {
8
+ constructor(stepVisualizer, simplificationData) {
9
+ this.stepVisualizer = stepVisualizer;
10
+ this.simplificationData = simplificationData || {};
11
+ this.messages = this._extractMessages(simplificationData);
12
+ this.ruleNames = this._extractRuleNames(simplificationData);
13
+ this.stepElements = [];
14
+ this.layoutGroup = new jsvgLayoutGroup();
15
+ this.layoutGroup.setSpacer(20); // Much larger spacing to prevent clipping
16
+
17
+ // Styling configuration
18
+ this.stepWidth = 380; // Increased width to prevent text cutoff
19
+ this.baseStepHeight = 40; // Increased base height
20
+ this.headerHeight = 40;
21
+ this.fontSize = 14;
22
+ this.smallFontSize = 12;
23
+
24
+ this.setupLayoutGroup();
25
+ this.createStepElements();
26
+ }
27
+
28
+ /**
29
+ * Extracts messages from simplification data
30
+ * @param {Object} data - Simplification data
31
+ * @returns {Array} Array of clean messages
32
+ * @private
33
+ */
34
+ _extractMessages(data) {
35
+ if (!data) return [];
36
+
37
+ let messages = [];
38
+ if (data.rawMessages && Array.isArray(data.rawMessages)) {
39
+ messages = data.rawMessages;
40
+ } else if (data.message) {
41
+ messages = [data.message];
42
+ }
43
+
44
+ // Clean up messages - remove HTML tags and bullet points
45
+ return messages.map(msg => {
46
+ let clean = msg.replace(/<[^>]*>/g, ''); // Strip HTML tags
47
+ clean = clean.replace(/^[•·◦▪▫‣⁃]\s*/, ''); // Strip bullet points
48
+ return clean.trim();
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Extracts rule names from simplification data
54
+ * @param {Object} data - Simplification data
55
+ * @returns {Array} Array of rule names
56
+ * @private
57
+ */
58
+ _extractRuleNames(data) {
59
+ if (!data) return ['Operation'];
60
+
61
+ if (data.ruleNames && Array.isArray(data.ruleNames)) {
62
+ return data.ruleNames;
63
+ }
64
+
65
+ // Default based on data type
66
+ if (data.multipleSimplifications) {
67
+ return ['Multiple Rules'];
68
+ } else {
69
+ return ['Operation'];
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Sets up the main layout group properties
75
+ * @private
76
+ */
77
+ setupLayoutGroup() {
78
+ // Add background using explainColor for the entire step group
79
+ this.backgroundRect = new jsvgRect();
80
+ this.backgroundRect.setWidthAndHeight(this.stepWidth + 24, 100); // Height will be updated, wider for increased padding
81
+ this.backgroundRect.setFillColor(omdColor.lightGray);
82
+ this.backgroundRect.setStrokeColor('#e0e0e0');
83
+ this.backgroundRect.setStrokeWidth(1);
84
+ this.backgroundRect.setCornerRadius(6);
85
+ this.backgroundRect.setPosition(0, 0); // Start at origin, not negative offset
86
+ this.layoutGroup.addChild(this.backgroundRect);
87
+ }
88
+
89
+ /**
90
+ * Creates individual step elements from the messages array
91
+ * @private
92
+ */
93
+ createStepElements() {
94
+ if (!this.messages || this.messages.length === 0) return;
95
+
96
+ // Create content container to separate from background
97
+ this.contentGroup = new jsvgLayoutGroup();
98
+ this.contentGroup.setSpacer(8); // Larger spacing between elements
99
+ this.contentGroup.setPosition(12, 12); // More offset from background edge
100
+
101
+ if (this.messages.length === 1) {
102
+
103
+ this.createSingleStepElement(this.messages[0], 0);
104
+ } else {
105
+
106
+ this.createMultipleStepElements();
107
+ }
108
+
109
+ this.contentGroup.doVerticalLayout();
110
+ this.layoutGroup.addChild(this.contentGroup);
111
+ this.updateBackgroundSize();
112
+
113
+ // Debug logging
114
+
115
+ }
116
+
117
+ /**
118
+ * Creates a single step element with header
119
+ * @param {string} message - The step message
120
+ * @param {number} index - Step index
121
+ * @private
122
+ */
123
+ createSingleStepElement(message, index) {
124
+ // Create header for single step using rule name
125
+ const ruleName = this.ruleNames[0] || 'Operation';
126
+ const headerBox = this.createHeaderBox(ruleName + ':');
127
+ this.contentGroup.addChild(headerBox);
128
+
129
+ // Create the step box
130
+ const stepBox = this.createStepTextBox(message, index, false);
131
+ this.stepElements.push(stepBox);
132
+ this.contentGroup.addChild(stepBox);
133
+ }
134
+
135
+ /**
136
+ * Creates multiple step elements with header
137
+ * @private
138
+ */
139
+ createMultipleStepElements() {
140
+ // Only create header for truly multiple steps (more than 1)
141
+ if (this.messages.length > 1) {
142
+ // Create header showing rule names
143
+ let headerText;
144
+ if (this.ruleNames.length === 1) {
145
+ headerText = this.ruleNames[0] + ':';
146
+ } else if (this.ruleNames.length <= 3) {
147
+ headerText = this.ruleNames.join(' + ') + ':';
148
+ } else {
149
+ headerText = `${this.ruleNames.length} Rules Applied:`;
150
+ }
151
+
152
+ const headerBox = this.createHeaderBox(headerText);
153
+ this.contentGroup.addChild(headerBox);
154
+ }
155
+
156
+ // Create individual step elements
157
+ this.messages.forEach((message, index) => {
158
+ const stepBox = this.createStepTextBox(message, index, this.messages.length > 1);
159
+ this.stepElements.push(stepBox);
160
+ this.contentGroup.addChild(stepBox);
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Creates a header box with custom text
166
+ * @param {string} headerText - Text to display in header
167
+ * @returns {jsvgTextBox} Header text box
168
+ * @private
169
+ */
170
+ createHeaderBox(headerText = 'Operation:') {
171
+ const headerBox = new jsvgTextBox();
172
+ const headerHeight = Math.max(this.headerHeight, 40); // Ensure minimum height
173
+
174
+
175
+ headerBox.setWidthAndHeight(this.stepWidth, headerHeight);
176
+ headerBox.setText(headerText);
177
+ headerBox.setFontSize(this.fontSize);
178
+ headerBox.setFontWeight('600');
179
+ headerBox.setFontColor('#2c3e50');
180
+
181
+ // Style the header with border
182
+ if (headerBox.div) {
183
+ Object.assign(headerBox.div.style, {
184
+ borderBottom: '1px solid #e0e0e0',
185
+ padding: '8px 12px 6px 12px',
186
+ margin: '0',
187
+ boxSizing: 'border-box',
188
+ minHeight: `${headerHeight}px`,
189
+ overflow: 'visible',
190
+ whiteSpace: 'normal',
191
+ wordWrap: 'break-word',
192
+ overflowWrap: 'break-word',
193
+ width: '100%'
194
+ });
195
+ }
196
+
197
+
198
+ return headerBox;
199
+ }
200
+
201
+ /**
202
+ * Creates an individual step text box
203
+ * @param {string} message - Step message
204
+ * @param {number} index - Step index
205
+ * @param {boolean} isMultiple - Whether this is part of multiple steps
206
+ * @returns {jsvgTextBox} Step text box
207
+ * @private
208
+ */
209
+ createStepTextBox(message, index, isMultiple) {
210
+ const stepBox = new jsvgTextBox();
211
+ const height = this.calculateStepHeight(message);
212
+
213
+
214
+
215
+ stepBox.setWidthAndHeight(this.stepWidth, height);
216
+ stepBox.setFontSize(this.fontSize);
217
+ stepBox.setFontColor('#2c3e50');
218
+
219
+ // Store step data for interactions
220
+ stepBox.stepIndex = index;
221
+ stepBox.stepMessage = message;
222
+ stepBox.isMultiple = isMultiple;
223
+
224
+ // Format the step content
225
+ const formattedContent = this.formatStepContent(message, index, isMultiple);
226
+
227
+ // Apply styling and content
228
+ if (stepBox.div) {
229
+ this.applyStepStyling(stepBox, formattedContent, isMultiple);
230
+ this.setupStepInteractions(stepBox);
231
+
232
+
233
+ }
234
+
235
+ return stepBox;
236
+ }
237
+
238
+ /**
239
+ * Formats the content for a step
240
+ * @param {string} message - Raw message
241
+ * @param {number} index - Step index
242
+ * @param {boolean} isMultiple - Whether part of multiple steps
243
+ * @returns {string} Formatted content
244
+ * @private
245
+ */
246
+ formatStepContent(message, index, isMultiple) {
247
+ const cleanMessage = message.trim();
248
+ let content = '';
249
+
250
+ // Only show step numbers for multiple steps
251
+ if (isMultiple && this.messages.length > 1) {
252
+ content += `<div class="step-number" style="color: #666; font-size: ${this.smallFontSize}px; margin: 0 0 2px 0; font-weight: 500;">Step ${index + 1}</div>`;
253
+ }
254
+
255
+ content += '<div class="step-content" style="display: flex; align-items: flex-start; gap: 8px; margin: 0; width: 100%;">';
256
+ content += '<span class="bullet" style="color: #666; font-weight: bold; flex-shrink: 0; margin-top: 2px;">•</span>';
257
+ content += '<div class="step-text" style="margin: 0; flex: 1; min-width: 0; word-wrap: break-word; overflow-wrap: break-word;">';
258
+
259
+ // Parse operation details
260
+ if (this.isOperationMessage(cleanMessage)) {
261
+ const action = this.extractOperationAction(cleanMessage);
262
+ const value = this.extractOperationValue(cleanMessage);
263
+ const valueNode = this.extractOperationValueNode(cleanMessage);
264
+
265
+ if (action && (value || valueNode)) {
266
+ content += `<span style="font-weight: 600; color: #2c3e50;">${action}</span> `;
267
+ const displayValue = valueNode ? valueNode.toString() : value;
268
+ content += `<span style="background: #f5f5f5; padding: 2px 6px; border-radius: 3px; font-family: 'Courier New', monospace; color: #d63384;">${displayValue}</span>`;
269
+ content += `<span style="color: #666; font-size: ${this.smallFontSize}px;"> to both sides</span>`;
270
+ } else {
271
+ content += `<span>${cleanMessage}</span>`;
272
+ }
273
+ } else {
274
+ content += `<span style="font-weight: 500;">${cleanMessage}</span>`;
275
+ }
276
+
277
+ content += '</div></div>';
278
+ return content;
279
+ }
280
+
281
+ /**
282
+ * Applies styling to a step text box
283
+ * @param {jsvgTextBox} stepBox - The step box
284
+ * @param {string} content - Formatted content
285
+ * @param {boolean} isMultiple - Whether part of multiple steps
286
+ * @private
287
+ */
288
+ applyStepStyling(stepBox, content, isMultiple) {
289
+ const baseStyles = {
290
+ padding: '8px 12px',
291
+ borderRadius: '4px',
292
+ transition: 'all 0.2s ease',
293
+ cursor: 'pointer',
294
+ lineHeight: '1.5',
295
+ backgroundColor: 'transparent',
296
+ margin: '0',
297
+ boxSizing: 'border-box',
298
+ overflow: 'visible',
299
+ minHeight: `${this.baseStepHeight}px`,
300
+ width: '100%',
301
+ whiteSpace: 'normal', // Allow text wrapping
302
+ wordWrap: 'break-word', // Break long words if necessary
303
+ overflowWrap: 'break-word', // Modern standard for word breaking
304
+ maxWidth: '100%' // Ensure content doesn't exceed container
305
+ };
306
+
307
+ Object.assign(stepBox.div.style, baseStyles);
308
+ stepBox.div.innerHTML = content;
309
+
310
+ // Force a reflow to ensure proper sizing
311
+ stepBox.div.offsetHeight;
312
+ }
313
+
314
+ /**
315
+ * Sets up hover and click interactions for a step
316
+ * @param {jsvgTextBox} stepBox - The step box
317
+ * @private
318
+ */
319
+ setupStepInteractions(stepBox) {
320
+ // Hover effects
321
+ stepBox.div.addEventListener('mouseenter', () => {
322
+ stepBox.div.style.backgroundColor = omdColor.mediumGray; // Slightly darker version of explainColor
323
+ stepBox.div.style.transform = 'translateX(2px)';
324
+
325
+ // Console logging for hover enter
326
+
327
+
328
+ // Call hover callback if provided
329
+ this.onStepHover?.(stepBox.stepIndex, stepBox.stepMessage, true);
330
+ });
331
+
332
+ stepBox.div.addEventListener('mouseleave', () => {
333
+ stepBox.div.style.backgroundColor = 'transparent';
334
+ stepBox.div.style.transform = 'translateX(0)';
335
+
336
+ // Console logging for hover leave
337
+
338
+
339
+ // Call hover callback if provided
340
+ this.onStepHover?.(stepBox.stepIndex, stepBox.stepMessage, false);
341
+ });
342
+
343
+ // Click interactions
344
+ stepBox.div.addEventListener('click', () => {
345
+ // Console logging for clicks
346
+
347
+
348
+ this.onStepClick?.(stepBox.stepIndex, stepBox.stepMessage);
349
+ });
350
+ }
351
+
352
+ /**
353
+ * Calculates the height needed for a step box
354
+ * @param {string} message - Step message
355
+ * @returns {number} Height in pixels
356
+ * @private
357
+ */
358
+ calculateStepHeight(message) {
359
+ // Create a temporary element to measure actual text height
360
+ const tempDiv = document.createElement('div');
361
+ tempDiv.style.position = 'absolute';
362
+ tempDiv.style.visibility = 'hidden';
363
+ tempDiv.style.width = `${this.stepWidth - 24}px`; // Account for padding
364
+ tempDiv.style.fontSize = `${this.fontSize}px`;
365
+ tempDiv.style.lineHeight = '1.5';
366
+ tempDiv.style.fontFamily = 'inherit';
367
+ tempDiv.style.whiteSpace = 'normal';
368
+ tempDiv.style.wordWrap = 'break-word';
369
+ tempDiv.style.overflowWrap = 'break-word';
370
+ tempDiv.style.padding = '8px 12px';
371
+ tempDiv.style.boxSizing = 'border-box';
372
+
373
+ // Use actual formatted content for measurement
374
+ const isMultiple = this.messages && this.messages.length > 1;
375
+ const formattedContent = this.formatStepContent(message, 0, isMultiple);
376
+ tempDiv.innerHTML = formattedContent;
377
+
378
+ // Append to document to measure
379
+ document.body.appendChild(tempDiv);
380
+ const measuredHeight = tempDiv.offsetHeight;
381
+ document.body.removeChild(tempDiv);
382
+
383
+ // Ensure minimum height and add buffer for interactions
384
+ const finalHeight = Math.max(this.baseStepHeight, measuredHeight + 8);
385
+
386
+ return finalHeight;
387
+ }
388
+
389
+ /**
390
+ * Updates the background rectangle size after layout
391
+ * @private
392
+ */
393
+ updateBackgroundSize() {
394
+ if (this.backgroundRect && this.contentGroup) {
395
+ const totalHeight = this.contentGroup.height + 24; // More padding for larger offset
396
+ const totalWidth = this.stepWidth + 24; // More padding for larger offset
397
+ this.backgroundRect.setWidthAndHeight(totalWidth, totalHeight);
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Sets callback for step hover events
403
+ * @param {Function} callback - Function called with (stepIndex, message, isEntering)
404
+ */
405
+ setOnStepHover(callback) {
406
+ this.onStepHover = callback;
407
+ }
408
+
409
+ /**
410
+ * Sets callback for step click events
411
+ * @param {Function} callback - Function called with (stepIndex, message)
412
+ */
413
+ setOnStepClick(callback) {
414
+ this.onStepClick = callback;
415
+ }
416
+
417
+ /**
418
+ * Gets the main layout group for adding to parent containers
419
+ * @returns {jsvgLayoutGroup} The layout group
420
+ */
421
+ getLayoutGroup() {
422
+ return this.layoutGroup;
423
+ }
424
+
425
+ /**
426
+ * Sets the position of the entire step group
427
+ * @param {number} x - X position
428
+ * @param {number} y - Y position
429
+ */
430
+ setPosition(x, y) {
431
+ this.layoutGroup.setPosition(x, y);
432
+ }
433
+
434
+ /**
435
+ * Gets the dimensions of the step group
436
+ * @returns {Object} Width and height
437
+ */
438
+ getDimensions() {
439
+ return {
440
+ width: this.backgroundRect ? this.backgroundRect.width : this.stepWidth + 16,
441
+ height: this.backgroundRect ? this.backgroundRect.height : 100
442
+ };
443
+ }
444
+
445
+ // Helper methods for message parsing (same as in formatter)
446
+
447
+ isOperationMessage(message) {
448
+ const operationKeywords = ['Applied', 'added', 'subtracted', 'multiplied', 'divided', 'both sides'];
449
+ return operationKeywords.some(keyword =>
450
+ message.toLowerCase().includes(keyword.toLowerCase())
451
+ );
452
+ }
453
+
454
+ extractOperationAction(message) {
455
+ const match = message.match(/^(Added|Subtracted|Multiplied|Divided)/i);
456
+ return match ? match[0] : null;
457
+ }
458
+
459
+ extractOperationValue(message) {
460
+ // Updated regex to handle simple values and expressions
461
+ const match = message.match(/(?:Added|Subtracted|Multiplied|Divided)\s(.*?)\s(?:to|by)/i);
462
+ if (match && match[1]) {
463
+ // Avoid returning "[object Object]"
464
+ if (match[1].includes('[object Object]')) {
465
+ return null;
466
+ }
467
+ return match[1];
468
+ }
469
+ return null;
470
+ }
471
+
472
+ extractOperationValueNode(message) {
473
+ if (this.simplificationData && this.simplificationData.operationValueNode) {
474
+ return this.simplificationData.operationValueNode;
475
+ }
476
+ return null;
477
+ }
478
+
479
+ /**
480
+ * Destroys the step group and cleans up resources
481
+ */
482
+ destroy() {
483
+ this.stepElements = [];
484
+ if (this.contentGroup) {
485
+ this.contentGroup.removeAllChildren();
486
+ }
487
+ this.layoutGroup.removeAllChildren();
488
+ this.onStepHover = null;
489
+ this.onStepClick = null;
490
+ }
491
+ }