@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,734 @@
1
+ import { omdStepVisualizerNodeUtils } from '../utils/omdStepVisualizerNodeUtils.js';
2
+
3
+ /**
4
+ * Robust tree diff algorithm using optimal substructure matching
5
+ * This replaces the special-case-heavy approach with a systematic algorithm
6
+ */
7
+ export class omdTreeDiff {
8
+
9
+ /**
10
+ * Main entry point - finds changed nodes between two equations
11
+ * @param {omdEquationNode} oldEquation - Previous equation
12
+ * @param {omdEquationNode} newEquation - Current equation
13
+ * @param {Object} options - Configuration options
14
+ * @param {boolean} options.educationalMode - If true, highlights mathematically neutral changes for learning
15
+ * @returns {Array} Array of changed nodes to highlight
16
+ */
17
+ static findChangedNodes(oldEquation, newEquation, options = {}) {
18
+ const { educationalMode = false } = options;
19
+
20
+
21
+
22
+ // === SPECIAL CASE: Same operation added to both sides ===
23
+ const specialCaseNodes = this.findEquationSpecialCases(oldEquation, newEquation);
24
+ if (specialCaseNodes.length > 0) {
25
+ return specialCaseNodes;
26
+ }
27
+
28
+ const changedNodes = [];
29
+
30
+ // Compare left sides if they differ
31
+ if (oldEquation.left.toString() !== newEquation.left.toString()) {
32
+ const leftChanges = this.diffSubtrees(oldEquation.left, newEquation.left, educationalMode);
33
+ changedNodes.push(...leftChanges);
34
+ }
35
+
36
+ // Compare right sides if they differ
37
+ if (oldEquation.right.toString() !== newEquation.right.toString()) {
38
+ const rightChanges = this.diffSubtrees(oldEquation.right, newEquation.right, educationalMode);
39
+ changedNodes.push(...rightChanges);
40
+ }
41
+
42
+ return changedNodes;
43
+ }
44
+
45
+ /**
46
+ * Find equation-level special cases (like adding same operation to both sides)
47
+ * @param {omdEquationNode} oldEquation - Previous equation
48
+ * @param {omdEquationNode} newEquation - Current equation
49
+ * @returns {Array} Nodes to highlight for equation special cases
50
+ */
51
+ static findEquationSpecialCases(oldEquation, newEquation) {
52
+ const oldLeftStr = oldEquation.left.toString();
53
+ const newLeftStr = newEquation.left.toString();
54
+ const oldRightStr = oldEquation.right.toString();
55
+ const newRightStr = newEquation.right.toString();
56
+
57
+ // Check if we're adding the same operation to both sides
58
+ if (newLeftStr.startsWith(oldLeftStr) && newRightStr.startsWith(oldRightStr)) {
59
+ const leftSuffix = newLeftStr.substring(oldLeftStr.length).trim();
60
+ const rightSuffix = newRightStr.substring(oldRightStr.length).trim();
61
+
62
+ // Case 1: Adding subtraction to both sides (e.g., "x + 2 = 5" → "x + 2 - 2 = 5 - 2")
63
+ if (leftSuffix.startsWith("-") && rightSuffix.startsWith("-") &&
64
+ leftSuffix.substring(1).trim() === rightSuffix.substring(1).trim()) {
65
+
66
+ const subtractedValue = leftSuffix.substring(1).trim();
67
+
68
+ const nodesToHighlight = [];
69
+
70
+ // Find rightmost occurrence of the subtracted value on left side
71
+ const leftSubtractedNode = omdStepVisualizerNodeUtils.findRightmostNodeWithValue(newEquation.left, subtractedValue);
72
+ if (leftSubtractedNode) {
73
+ // If it's a leaf node, add it directly; otherwise find its leaf nodes
74
+ if (omdStepVisualizerNodeUtils.isLeafNode(leftSubtractedNode)) {
75
+ nodesToHighlight.push(leftSubtractedNode);
76
+ } else {
77
+ const leftLeaves = omdStepVisualizerNodeUtils.findLeafNodes(leftSubtractedNode);
78
+ nodesToHighlight.push(...leftLeaves);
79
+ }
80
+ }
81
+
82
+ // Find rightmost occurrence of the subtracted value on right side
83
+ const rightSubtractedNode = omdStepVisualizerNodeUtils.findRightmostNodeWithValue(newEquation.right, subtractedValue);
84
+ if (rightSubtractedNode) {
85
+ // If it's a leaf node, add it directly; otherwise find its leaf nodes
86
+ if (omdStepVisualizerNodeUtils.isLeafNode(rightSubtractedNode)) {
87
+ nodesToHighlight.push(rightSubtractedNode);
88
+ } else {
89
+ const rightLeaves = omdStepVisualizerNodeUtils.findLeafNodes(rightSubtractedNode);
90
+ nodesToHighlight.push(...rightLeaves);
91
+ }
92
+ }
93
+
94
+ return nodesToHighlight;
95
+ }
96
+
97
+ // Case 2: Adding addition to both sides (e.g., "x - 2 = 3" → "x - 2 + 2 = 3 + 2")
98
+ if (leftSuffix.startsWith("+") && rightSuffix.startsWith("+") &&
99
+ leftSuffix.substring(1).trim() === rightSuffix.substring(1).trim()) {
100
+
101
+ const addedValue = leftSuffix.substring(1).trim();
102
+
103
+ const nodesToHighlight = [];
104
+
105
+ // Find rightmost occurrence of the added value on left side
106
+ const leftAddedNode = omdStepVisualizerNodeUtils.findRightmostNodeWithValue(newEquation.left, addedValue);
107
+ if (leftAddedNode) {
108
+ // If it's a leaf node, add it directly; otherwise find its leaf nodes
109
+ if (omdStepVisualizerNodeUtils.isLeafNode(leftAddedNode)) {
110
+ nodesToHighlight.push(leftAddedNode);
111
+ } else {
112
+ const leftLeaves = omdStepVisualizerNodeUtils.findLeafNodes(leftAddedNode);
113
+ nodesToHighlight.push(...leftLeaves);
114
+ }
115
+ }
116
+
117
+ // Find rightmost occurrence of the added value on right side
118
+ const rightAddedNode = omdStepVisualizerNodeUtils.findRightmostNodeWithValue(newEquation.right, addedValue);
119
+ if (rightAddedNode) {
120
+ // If it's a leaf node, add it directly; otherwise find its leaf nodes
121
+ if (omdStepVisualizerNodeUtils.isLeafNode(rightAddedNode)) {
122
+ nodesToHighlight.push(rightAddedNode);
123
+ } else {
124
+ const rightLeaves = omdStepVisualizerNodeUtils.findLeafNodes(rightAddedNode);
125
+ nodesToHighlight.push(...rightLeaves);
126
+ }
127
+ }
128
+
129
+ return nodesToHighlight;
130
+ }
131
+ }
132
+
133
+ return [];
134
+ }
135
+
136
+ /**
137
+ * Core algorithm: find optimal subtree matching and return unmatched nodes
138
+ * @param {omdNode} oldTree - Old tree root
139
+ * @param {omdNode} newTree - New tree root
140
+ * @param {boolean} educationalMode - Whether to highlight pedagogical changes
141
+ * @returns {Array} Array of unmatched leaf nodes in new tree
142
+ */
143
+ static diffSubtrees(oldTree, newTree, educationalMode = false) {
144
+ // === STEP 1: CHECK FOR EDUCATIONAL PATTERNS FIRST ===
145
+ // These patterns from the old system worked really well for highlighting
146
+
147
+ // Check for common prefix patterns (like "2x + 4" → "2x + 4 - 4")
148
+ const prefixHighlights = this.findCommonPrefixHighlights(oldTree, newTree);
149
+ if (prefixHighlights.length > 0) {
150
+ return prefixHighlights;
151
+ }
152
+
153
+ // Check for variable preservation patterns (when variables stay same but constants change)
154
+ const variableHighlights = this.findVariablePreservationHighlights(oldTree, newTree);
155
+ if (variableHighlights.length > 0) {
156
+ return variableHighlights;
157
+ }
158
+
159
+ // Check for type difference patterns (constant becoming binary expression, etc.)
160
+ const typeHighlights = this.findTypeDifferenceHighlights(oldTree, newTree);
161
+ if (typeHighlights.length > 0) {
162
+ return typeHighlights;
163
+ }
164
+
165
+ // Check for subtraction patterns (when one part matches and other is subtracted)
166
+ const subtractionHighlights = this.findSubtractionPatternHighlights(oldTree, newTree);
167
+ if (subtractionHighlights.length > 0) {
168
+ return subtractionHighlights;
169
+ }
170
+
171
+ // === STEP 2: FALLBACK TO OPTIMAL MATCHING ALGORITHM ===
172
+
173
+ // Find all possible subtree matches
174
+ const allMatches = this.findAllSubtreeMatches(oldTree, newTree);
175
+
176
+ // Select optimal non-overlapping set of matches
177
+ const optimalMatches = this.selectOptimalMatching(allMatches);
178
+
179
+ // Find unmatched nodes (these are the changes)
180
+ let unmatchedNodes = this.findUnmatchedLeafNodes(newTree, optimalMatches);
181
+
182
+ // Educational mode - highlight simplifications
183
+ if (educationalMode && unmatchedNodes.length === 0) {
184
+ const educationalHighlights = this.findEducationalHighlights(oldTree, newTree, optimalMatches);
185
+ unmatchedNodes.push(...educationalHighlights);
186
+ }
187
+
188
+ return unmatchedNodes;
189
+ }
190
+
191
+ /**
192
+ * Find educational highlights for cases where mathematical content didn't change
193
+ * but pedagogical highlighting is desired (e.g., removing + 0)
194
+ * @param {omdNode} oldTree - Old tree root
195
+ * @param {omdNode} newTree - New tree root
196
+ * @param {Array} optimalMatches - The matches already found
197
+ * @returns {Array} Additional nodes to highlight for educational purposes
198
+ */
199
+ static findEducationalHighlights(oldTree, newTree, optimalMatches) {
200
+ const educationalNodes = [];
201
+
202
+ // Case 1: Additive identity removal (+ 0 or - 0)
203
+ const identityHighlights = this.findAdditiveIdentityChanges(oldTree, newTree);
204
+ educationalNodes.push(...identityHighlights);
205
+
206
+ // Case 2: Multiplicative identity removal (* 1 or / 1)
207
+ const multiplicativeHighlights = this.findMultiplicativeIdentityChanges(oldTree, newTree);
208
+ educationalNodes.push(...multiplicativeHighlights);
209
+
210
+ // Case 3: Double negative simplification (--x → x)
211
+ const doubleNegativeHighlights = this.findDoubleNegativeChanges(oldTree, newTree);
212
+ educationalNodes.push(...doubleNegativeHighlights);
213
+
214
+ return educationalNodes;
215
+ }
216
+
217
+ /**
218
+ * Find additive identity changes (removal of + 0 or - 0)
219
+ * @param {omdNode} oldTree - Old tree
220
+ * @param {omdNode} newTree - New tree
221
+ * @returns {Array} Nodes to highlight for additive identity
222
+ */
223
+ static findAdditiveIdentityChanges(oldTree, newTree) {
224
+ // Check if old tree has + 0 or - 0 that's not in new tree
225
+ const oldStr = oldTree.toString();
226
+ const newStr = newTree.toString();
227
+
228
+ // Pattern: "expression + 0" → "expression" or "expression - 0" → "expression"
229
+ if ((oldStr.includes(" + 0") || oldStr.includes(" - 0")) &&
230
+ !newStr.includes(" + 0") && !newStr.includes(" - 0")) {
231
+
232
+ // Highlight ALL leaf nodes of the remaining expression to show the complete term
233
+ const allLeafNodes = omdStepVisualizerNodeUtils.findLeafNodes(newTree);
234
+
235
+ if (allLeafNodes.length > 0) {
236
+ return allLeafNodes; // Highlight all leaf nodes in the remaining expression
237
+ }
238
+ }
239
+
240
+ return [];
241
+ }
242
+
243
+ /**
244
+ * Find multiplicative identity changes (removal of * 1 or / 1)
245
+ * @param {omdNode} oldTree - Old tree
246
+ * @param {omdNode} newTree - New tree
247
+ * @returns {Array} Nodes to highlight for multiplicative identity
248
+ */
249
+ static findMultiplicativeIdentityChanges(oldTree, newTree) {
250
+ const oldStr = oldTree.toString();
251
+ const newStr = newTree.toString();
252
+
253
+ if ((oldStr.includes(" * 1") || oldStr.includes(" / 1")) &&
254
+ !newStr.includes(" * 1") && !newStr.includes(" / 1")) {
255
+
256
+ const allLeafNodes = omdStepVisualizerNodeUtils.findLeafNodes(newTree);
257
+ if (allLeafNodes.length > 0) {
258
+ return allLeafNodes; // Highlight entire remaining expression
259
+ }
260
+ }
261
+
262
+ return [];
263
+ }
264
+
265
+ /**
266
+ * Find double negative changes (--x → x)
267
+ * @param {omdNode} oldTree - Old tree
268
+ * @param {omdNode} newTree - New tree
269
+ * @returns {Array} Nodes to highlight for double negative removal
270
+ */
271
+ static findDoubleNegativeChanges(oldTree, newTree) {
272
+ const oldStr = oldTree.toString();
273
+ const newStr = newTree.toString();
274
+
275
+ if (oldStr.includes("--") && !newStr.includes("--")) {
276
+ const allLeafNodes = omdStepVisualizerNodeUtils.findLeafNodes(newTree);
277
+ if (allLeafNodes.length > 0) {
278
+ return allLeafNodes; // Highlight entire remaining expression
279
+ }
280
+ }
281
+
282
+ return [];
283
+ }
284
+
285
+ /**
286
+ * Find common prefix highlighting patterns
287
+ * Example: "2x + 4" → "2x + 4 - 4" should highlight only the "- 4" part
288
+ * @param {omdNode} oldTree - Old tree
289
+ * @param {omdNode} newTree - New tree
290
+ * @returns {Array} Nodes to highlight for common prefix patterns
291
+ */
292
+ static findCommonPrefixHighlights(oldTree, newTree) {
293
+ // Only apply to binary expressions
294
+ if (!omdStepVisualizerNodeUtils.isBinaryNode(newTree)) {
295
+ return [];
296
+ }
297
+
298
+ const oldStr = oldTree.toString();
299
+ const newStr = newTree.toString();
300
+
301
+ // Find common prefix
302
+ const commonPrefix = this._findCommonPrefix(oldStr, newStr);
303
+ if (!commonPrefix || commonPrefix.length <= 1) {
304
+ return [];
305
+ }
306
+
307
+ const oldSuffix = oldStr.substring(commonPrefix.length).trim();
308
+ const newSuffix = newStr.substring(commonPrefix.length).trim();
309
+
310
+ // Case 1: New suffix is "0" (simplification to zero)
311
+ if (newSuffix === "0") {
312
+ const zeroNodes = omdStepVisualizerNodeUtils.findLeafNodesWithValue(newTree, "0");
313
+ if (zeroNodes.length > 0) {
314
+ return zeroNodes;
315
+ }
316
+ }
317
+
318
+ // Case 2: New suffix is a subtraction (adding negative term)
319
+ if (oldSuffix === "" && newSuffix.startsWith("- ")) {
320
+ const subtractedValue = newSuffix.substring(2).trim();
321
+
322
+ const subtractedNodes = omdStepVisualizerNodeUtils.findLeafNodesWithValue(newTree, subtractedValue);
323
+ if (subtractedNodes.length > 0) {
324
+ return subtractedNodes;
325
+ }
326
+ }
327
+
328
+ return [];
329
+ }
330
+
331
+ /**
332
+ * Find variable preservation highlighting patterns
333
+ * Example: "2x + 4" → "2x + 2" should highlight only the changed constant
334
+ * @param {omdNode} oldTree - Old tree
335
+ * @param {omdNode} newTree - New tree
336
+ * @returns {Array} Nodes to highlight for variable preservation patterns
337
+ */
338
+ static findVariablePreservationHighlights(oldTree, newTree) {
339
+ // Only apply to binary expressions
340
+ if (!omdStepVisualizerNodeUtils.isBinaryNode(oldTree) ||
341
+ !omdStepVisualizerNodeUtils.isBinaryNode(newTree)) {
342
+ return [];
343
+ }
344
+
345
+ const oldStr = oldTree.toString();
346
+ const newStr = newTree.toString();
347
+
348
+ // Check if both expressions contain the same variable term
349
+ const variablePattern = /(\d*[a-zA-Z])/;
350
+ const oldMatch = oldStr.match(variablePattern);
351
+ const newMatch = newStr.match(variablePattern);
352
+
353
+ if (oldMatch && newMatch && oldMatch[0] === newMatch[0]) {
354
+ // Find constants that changed
355
+ const oldConstNodes = omdStepVisualizerNodeUtils.findConstantNodes(oldTree);
356
+ const newConstNodes = omdStepVisualizerNodeUtils.findConstantNodes(newTree);
357
+
358
+ const changedConstNodes = newConstNodes.filter(newNode => {
359
+ return !oldConstNodes.some(oldNode =>
360
+ oldNode.toString() === newNode.toString()
361
+ );
362
+ });
363
+
364
+ return changedConstNodes;
365
+ }
366
+
367
+ return [];
368
+ }
369
+
370
+ /**
371
+ * Find type difference highlighting patterns
372
+ * Example: constant "3" → binary expression "x + 2" should highlight the new expression
373
+ * @param {omdNode} oldTree - Old tree
374
+ * @param {omdNode} newTree - New tree
375
+ * @returns {Array} Nodes to highlight for type difference patterns
376
+ */
377
+ static findTypeDifferenceHighlights(oldTree, newTree) {
378
+ const oldType = oldTree.constructor ? oldTree.type : 'unknown';
379
+ const newType = newTree.constructor ? newTree.type : 'unknown';
380
+
381
+ if (oldType === newType) {
382
+ return []; // Same type, not a type difference pattern
383
+ }
384
+
385
+ // Case 1: New node is binary, check if old node is part of it
386
+ if (omdStepVisualizerNodeUtils.isBinaryNode(newTree)) {
387
+ const oldStr = oldTree.toString();
388
+ const newLeftStr = newTree.left ? newTree.left.toString() : '';
389
+ const newRightStr = newTree.right ? newTree.right.toString() : '';
390
+
391
+ if (oldStr === newLeftStr) {
392
+ if (newTree.right) {
393
+ const leafNodes = omdStepVisualizerNodeUtils.findLeafNodes(newTree.right);
394
+ return leafNodes;
395
+ }
396
+ } else if (oldStr === newRightStr) {
397
+ if (newTree.left) {
398
+ const leafNodes = omdStepVisualizerNodeUtils.findLeafNodes(newTree.left);
399
+ return leafNodes;
400
+ }
401
+ }
402
+ }
403
+
404
+ // Case 2: Complete change - highlight all leaf nodes in new tree
405
+ const leaves = omdStepVisualizerNodeUtils.findLeafNodes(newTree);
406
+ return leaves;
407
+ }
408
+
409
+ /**
410
+ * Find subtraction pattern highlighting
411
+ * Example: "x + 2" → "x + 2 - 2" should highlight only the "- 2" part
412
+ * @param {omdNode} oldTree - Old tree
413
+ * @param {omdNode} newTree - New tree
414
+ * @returns {Array} Nodes to highlight for subtraction patterns
415
+ */
416
+ static findSubtractionPatternHighlights(oldTree, newTree) {
417
+ // Check if new tree is a subtraction and old tree matches the left side
418
+ if (omdStepVisualizerNodeUtils.isBinaryNode(newTree) &&
419
+ newTree.operation === 'subtract') {
420
+
421
+ const oldStr = oldTree.toString();
422
+ const newLeftStr = newTree.left?.toString();
423
+
424
+ if (oldStr === newLeftStr) {
425
+ if (newTree.right) {
426
+ const rightLeaves = omdStepVisualizerNodeUtils.findLeafNodes(newTree.right);
427
+ return rightLeaves;
428
+ }
429
+ }
430
+ }
431
+
432
+ return [];
433
+ }
434
+
435
+ /**
436
+ * Helper: Find the longest common prefix between two strings
437
+ * @param {string} str1 - First string
438
+ * @param {string} str2 - Second string
439
+ * @returns {string} The common prefix
440
+ * @private
441
+ */
442
+ static _findCommonPrefix(str1, str2) {
443
+ let i = 0;
444
+ while (i < str1.length && i < str2.length && str1[i] === str2[i]) {
445
+ i++;
446
+ }
447
+ return str1.substring(0, i);
448
+ }
449
+
450
+ /**
451
+ * Find all possible matches between subtrees of old and new trees
452
+ * @param {omdNode} oldTree - Old tree root
453
+ * @param {omdNode} newTree - New tree root
454
+ * @returns {Array} Array of match objects {oldNode, newNode, size, score}
455
+ */
456
+ static findAllSubtreeMatches(oldTree, newTree) {
457
+ const matches = [];
458
+ const oldSubtrees = this.getAllSubtrees(oldTree);
459
+ const newSubtrees = this.getAllSubtrees(newTree);
460
+
461
+ for (const oldSub of oldSubtrees) {
462
+ for (const newSub of newSubtrees) {
463
+ const similarity = this.calculateSimilarity(oldSub, newSub);
464
+ if (similarity.isMatch) {
465
+ matches.push({
466
+ oldNode: oldSub,
467
+ newNode: newSub,
468
+ size: similarity.size,
469
+ score: similarity.score,
470
+ type: similarity.type
471
+ });
472
+ }
473
+ }
474
+ }
475
+
476
+ return matches;
477
+ }
478
+
479
+ /**
480
+ * Get all subtrees (including single nodes) from a tree
481
+ * @param {omdNode} root - Root node
482
+ * @returns {Array} Array of all subtrees
483
+ */
484
+ static getAllSubtrees(root) {
485
+ if (!root) return [];
486
+
487
+ const subtrees = [root];
488
+
489
+ // Add all child subtrees recursively
490
+ if (omdStepVisualizerNodeUtils.isBinaryNode(root)) {
491
+ subtrees.push(...this.getAllSubtrees(root.left));
492
+ subtrees.push(...this.getAllSubtrees(root.right));
493
+ } else if (omdStepVisualizerNodeUtils.isUnaryNode(root)) {
494
+ subtrees.push(...this.getAllSubtrees(root.argument));
495
+ } else if (omdStepVisualizerNodeUtils.hasExpression(root)) {
496
+ subtrees.push(...this.getAllSubtrees(root.expression));
497
+ }
498
+
499
+ return subtrees;
500
+ }
501
+
502
+ /**
503
+ * Calculate similarity between two subtrees
504
+ * @param {omdNode} tree1 - First tree
505
+ * @param {omdNode} tree2 - Second tree
506
+ * @returns {Object} Similarity info {isMatch, size, score, type}
507
+ */
508
+ static calculateSimilarity(tree1, tree2) {
509
+ // Exact structural match
510
+ if (this.treesStructurallyEqual(tree1, tree2)) {
511
+ const size = this.getSubtreeSize(tree1);
512
+ return {
513
+ isMatch: true,
514
+ size: size,
515
+ score: size * 10, // High score for exact matches
516
+ type: 'exact'
517
+ };
518
+ }
519
+
520
+ // Exact string match (different structure, same result)
521
+ if (tree1.toString() === tree2.toString()) {
522
+ const size = this.getSubtreeSize(tree1);
523
+ return {
524
+ isMatch: true,
525
+ size: size,
526
+ score: size * 8, // Slightly lower than structural match
527
+ type: 'equivalent'
528
+ };
529
+ }
530
+
531
+ // Leaf node value match
532
+ if (omdStepVisualizerNodeUtils.isLeafNode(tree1) &&
533
+ omdStepVisualizerNodeUtils.isLeafNode(tree2)) {
534
+ const val1 = omdStepVisualizerNodeUtils.getNodeValue(tree1);
535
+ const val2 = omdStepVisualizerNodeUtils.getNodeValue(tree2);
536
+
537
+ if (val1 === val2) {
538
+ return {
539
+ isMatch: true,
540
+ size: 1,
541
+ score: 5, // Lower score for single nodes
542
+ type: 'leaf'
543
+ };
544
+ }
545
+ }
546
+
547
+ return { isMatch: false, size: 0, score: 0, type: 'none' };
548
+ }
549
+
550
+ /**
551
+ * Check if two trees are structurally identical
552
+ * @param {omdNode} tree1 - First tree
553
+ * @param {omdNode} tree2 - Second tree
554
+ * @returns {boolean} True if structurally identical
555
+ */
556
+ static treesStructurallyEqual(tree1, tree2) {
557
+ if (!tree1 && !tree2) return true;
558
+ if (!tree1 || !tree2) return false;
559
+
560
+ // Check node types
561
+ const type1 = tree1.constructor ? tree1.type : 'unknown';
562
+ const type2 = tree2.constructor ? tree2.type : 'unknown';
563
+ if (type1 !== type2) return false;
564
+
565
+ // Check leaf nodes
566
+ if (omdStepVisualizerNodeUtils.isLeafNode(tree1)) {
567
+ const val1 = omdStepVisualizerNodeUtils.getNodeValue(tree1);
568
+ const val2 = omdStepVisualizerNodeUtils.getNodeValue(tree2);
569
+ return val1 === val2;
570
+ }
571
+
572
+ // Check binary nodes
573
+ if (omdStepVisualizerNodeUtils.isBinaryNode(tree1)) {
574
+ if (tree1.operation !== tree2.operation) return false;
575
+ return this.treesStructurallyEqual(tree1.left, tree2.left) &&
576
+ this.treesStructurallyEqual(tree1.right, tree2.right);
577
+ }
578
+
579
+ // Check unary nodes
580
+ if (omdStepVisualizerNodeUtils.isUnaryNode(tree1)) {
581
+ if (tree1.operation !== tree2.operation) return false;
582
+ return this.treesStructurallyEqual(tree1.argument, tree2.argument);
583
+ }
584
+
585
+ // Check expression nodes
586
+ if (omdStepVisualizerNodeUtils.hasExpression(tree1)) {
587
+ return this.treesStructurallyEqual(tree1.expression, tree2.expression);
588
+ }
589
+
590
+ return false;
591
+ }
592
+
593
+ /**
594
+ * Calculate the size (number of nodes) in a subtree
595
+ * @param {omdNode} root - Root of subtree
596
+ * @returns {number} Number of nodes in subtree
597
+ */
598
+ static getSubtreeSize(root) {
599
+ if (!root) return 0;
600
+
601
+ let size = 1; // Count this node
602
+
603
+ if (omdStepVisualizerNodeUtils.isBinaryNode(root)) {
604
+ size += this.getSubtreeSize(root.left);
605
+ size += this.getSubtreeSize(root.right);
606
+ } else if (omdStepVisualizerNodeUtils.isUnaryNode(root)) {
607
+ size += this.getSubtreeSize(root.argument);
608
+ } else if (omdStepVisualizerNodeUtils.hasExpression(root)) {
609
+ size += this.getSubtreeSize(root.expression);
610
+ }
611
+
612
+ return size;
613
+ }
614
+
615
+ /**
616
+ * Select optimal non-overlapping set of matches using greedy algorithm
617
+ * @param {Array} matches - Array of potential matches
618
+ * @returns {Array} Array of selected optimal matches
619
+ */
620
+ static selectOptimalMatching(matches) {
621
+ // Sort by score (descending) to prefer better matches
622
+ const sortedMatches = matches.slice().sort((a, b) => b.score - a.score);
623
+
624
+ const selectedMatches = [];
625
+ const usedOldNodes = new Set();
626
+ const usedNewNodes = new Set();
627
+
628
+ for (const match of sortedMatches) {
629
+ // Check if this match overlaps with already selected matches
630
+ if (!this.hasNodeOverlap(match.oldNode, usedOldNodes) &&
631
+ !this.hasNodeOverlap(match.newNode, usedNewNodes)) {
632
+
633
+ selectedMatches.push(match);
634
+ this.markSubtreeAsUsed(match.oldNode, usedOldNodes);
635
+ this.markSubtreeAsUsed(match.newNode, usedNewNodes);
636
+ }
637
+ }
638
+
639
+ return selectedMatches;
640
+ }
641
+
642
+ /**
643
+ * Check if a node overlaps with any node in the used set
644
+ * @param {omdNode} node - Node to check
645
+ * @param {Set} usedNodes - Set of already used nodes
646
+ * @returns {boolean} True if there's overlap
647
+ */
648
+ static hasNodeOverlap(node, usedNodes) {
649
+ // Check if this node or any of its ancestors/descendants are used
650
+ const nodeSubtrees = this.getAllSubtrees(node);
651
+ return nodeSubtrees.some(subtree => usedNodes.has(subtree));
652
+ }
653
+
654
+ /**
655
+ * Mark all nodes in a subtree as used
656
+ * @param {omdNode} root - Root of subtree to mark
657
+ * @param {Set} usedNodes - Set to add nodes to
658
+ */
659
+ static markSubtreeAsUsed(root, usedNodes) {
660
+ const allNodes = this.getAllSubtrees(root);
661
+ allNodes.forEach(node => usedNodes.add(node));
662
+ }
663
+
664
+ /**
665
+ * Find leaf nodes in new tree that aren't covered by any match
666
+ * @param {omdNode} newTree - New tree root
667
+ * @param {Array} matches - Array of selected matches
668
+ * @returns {Array} Array of unmatched leaf nodes
669
+ */
670
+ static findUnmatchedLeafNodes(newTree, matches) {
671
+ const allLeafNodes = omdStepVisualizerNodeUtils.findLeafNodes(newTree);
672
+ const matchedNodes = new Set();
673
+
674
+ // Mark all nodes covered by matches
675
+ for (const match of matches) {
676
+ const matchedSubtreeNodes = this.getAllSubtrees(match.newNode);
677
+ matchedSubtreeNodes.forEach(node => matchedNodes.add(node));
678
+ }
679
+
680
+ // Return leaf nodes not covered by any match
681
+ const unmatchedLeaves = allLeafNodes.filter(leaf => !matchedNodes.has(leaf));
682
+
683
+ return unmatchedLeaves;
684
+ }
685
+
686
+ /**
687
+ * Find leaf nodes in old tree that aren't covered by any match (i.e., removed nodes)
688
+ * @param {omdNode} oldTree - Old tree root
689
+ * @param {Array} matches - Array of selected matches
690
+ * @returns {Array} Array of unmatched leaf nodes from old tree
691
+ */
692
+ static findUnmatchedOldNodes(oldTree, matches) {
693
+ const allOldLeafNodes = omdStepVisualizerNodeUtils.findLeafNodes(oldTree);
694
+ const matchedOldNodes = new Set();
695
+
696
+ // Mark all old nodes covered by matches
697
+ for (const match of matches) {
698
+ const matchedSubtreeNodes = this.getAllSubtrees(match.oldNode);
699
+ matchedSubtreeNodes.forEach(node => matchedOldNodes.add(node));
700
+ }
701
+
702
+ // Return old leaf nodes not covered by any match (these were removed)
703
+ const unmatchedOldLeaves = allOldLeafNodes.filter(leaf => !matchedOldNodes.has(leaf));
704
+
705
+ return unmatchedOldLeaves;
706
+ }
707
+
708
+ /**
709
+ * Debug helper: print tree structure
710
+ * @param {omdNode} node - Node to print
711
+ * @param {number} depth - Current depth for indentation
712
+ * @returns {string} String representation of tree structure
713
+ */
714
+ static debugPrintTree(node, depth = 0) {
715
+ if (!node) return '';
716
+
717
+ const indent = ' '.repeat(depth);
718
+ const nodeType = node.constructor ? node.type : 'unknown';
719
+ const nodeValue = node.toString ? node.toString() : 'unknown';
720
+
721
+ let result = `${indent}${nodeType}: "${nodeValue}"\n`;
722
+
723
+ if (omdStepVisualizerNodeUtils.isBinaryNode(node)) {
724
+ result += `${indent}├─ left:\n${this.debugPrintTree(node.left, depth + 1)}`;
725
+ result += `${indent}└─ right:\n${this.debugPrintTree(node.right, depth + 1)}`;
726
+ } else if (omdStepVisualizerNodeUtils.isUnaryNode(node)) {
727
+ result += `${indent}└─ argument:\n${this.debugPrintTree(node.argument, depth + 1)}`;
728
+ } else if (omdStepVisualizerNodeUtils.hasExpression(node)) {
729
+ result += `${indent}└─ expression:\n${this.debugPrintTree(node.expression, depth + 1)}`;
730
+ }
731
+
732
+ return result;
733
+ }
734
+ }