@teachinglab/omd 0.6.1 → 0.6.3

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 (198) hide show
  1. package/README.md +257 -251
  2. package/README.old.md +137 -137
  3. package/canvas/core/canvasConfig.js +202 -202
  4. package/canvas/drawing/segment.js +167 -167
  5. package/canvas/drawing/stroke.js +385 -385
  6. package/canvas/events/eventManager.js +444 -444
  7. package/canvas/events/pointerEventHandler.js +262 -262
  8. package/canvas/index.js +48 -48
  9. package/canvas/tools/PointerTool.js +71 -71
  10. package/canvas/tools/tool.js +222 -222
  11. package/canvas/utils/boundingBox.js +377 -377
  12. package/canvas/utils/mathUtils.js +258 -258
  13. package/docs/api/configuration-options.md +198 -198
  14. package/docs/api/eventManager.md +82 -82
  15. package/docs/api/focusFrameManager.md +144 -144
  16. package/docs/api/index.md +105 -105
  17. package/docs/api/main.md +62 -62
  18. package/docs/api/omdBinaryExpressionNode.md +86 -86
  19. package/docs/api/omdCanvas.md +83 -83
  20. package/docs/api/omdConfigManager.md +112 -112
  21. package/docs/api/omdConstantNode.md +52 -52
  22. package/docs/api/omdDisplay.md +87 -87
  23. package/docs/api/omdEquationNode.md +174 -174
  24. package/docs/api/omdEquationSequenceNode.md +258 -258
  25. package/docs/api/omdEquationStack.md +192 -192
  26. package/docs/api/omdFunctionNode.md +82 -82
  27. package/docs/api/omdGroupNode.md +78 -78
  28. package/docs/api/omdHelpers.md +87 -87
  29. package/docs/api/omdLeafNode.md +85 -85
  30. package/docs/api/omdNode.md +201 -201
  31. package/docs/api/omdOperationDisplayNode.md +117 -117
  32. package/docs/api/omdOperatorNode.md +91 -91
  33. package/docs/api/omdParenthesisNode.md +133 -133
  34. package/docs/api/omdPopup.md +191 -191
  35. package/docs/api/omdPowerNode.md +131 -131
  36. package/docs/api/omdRationalNode.md +144 -144
  37. package/docs/api/omdSequenceNode.md +128 -128
  38. package/docs/api/omdSimplification.md +78 -78
  39. package/docs/api/omdSqrtNode.md +144 -144
  40. package/docs/api/omdStepVisualizer.md +146 -146
  41. package/docs/api/omdStepVisualizerHighlighting.md +65 -65
  42. package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
  43. package/docs/api/omdStepVisualizerLayout.md +70 -70
  44. package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
  45. package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
  46. package/docs/api/omdToolbar.md +130 -130
  47. package/docs/api/omdTranscriptionService.md +95 -95
  48. package/docs/api/omdTreeDiff.md +169 -169
  49. package/docs/api/omdUnaryExpressionNode.md +137 -137
  50. package/docs/api/omdUtilities.md +82 -82
  51. package/docs/api/omdVariableNode.md +123 -123
  52. package/docs/api/selectTool.md +74 -74
  53. package/docs/api/simplificationEngine.md +97 -97
  54. package/docs/api/simplificationRules.md +76 -76
  55. package/docs/api/simplificationUtils.md +64 -64
  56. package/docs/api/transcribe.md +43 -43
  57. package/docs/api-reference.md +85 -85
  58. package/docs/index.html +453 -453
  59. package/docs/index.md +38 -38
  60. package/docs/omd-objects.md +258 -258
  61. package/index.js +79 -79
  62. package/jsvg/index.js +3 -0
  63. package/jsvg/jsvg.js +898 -898
  64. package/jsvg/jsvgComponents.js +357 -358
  65. package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
  66. package/npm-docs/README.md +251 -251
  67. package/npm-docs/api/api-reference.md +85 -85
  68. package/npm-docs/api/configuration-options.md +198 -198
  69. package/npm-docs/api/eventManager.md +82 -82
  70. package/npm-docs/api/expression-nodes.md +561 -561
  71. package/npm-docs/api/focusFrameManager.md +144 -144
  72. package/npm-docs/api/index.md +105 -105
  73. package/npm-docs/api/main.md +62 -62
  74. package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
  75. package/npm-docs/api/omdCanvas.md +83 -83
  76. package/npm-docs/api/omdConfigManager.md +112 -112
  77. package/npm-docs/api/omdConstantNode.md +52 -52
  78. package/npm-docs/api/omdDisplay.md +87 -87
  79. package/npm-docs/api/omdEquationNode.md +174 -174
  80. package/npm-docs/api/omdEquationSequenceNode.md +258 -258
  81. package/npm-docs/api/omdEquationStack.md +192 -192
  82. package/npm-docs/api/omdFunctionNode.md +82 -82
  83. package/npm-docs/api/omdGroupNode.md +78 -78
  84. package/npm-docs/api/omdHelpers.md +87 -87
  85. package/npm-docs/api/omdLeafNode.md +85 -85
  86. package/npm-docs/api/omdNode.md +201 -201
  87. package/npm-docs/api/omdOperationDisplayNode.md +117 -117
  88. package/npm-docs/api/omdOperatorNode.md +91 -91
  89. package/npm-docs/api/omdParenthesisNode.md +133 -133
  90. package/npm-docs/api/omdPopup.md +191 -191
  91. package/npm-docs/api/omdPowerNode.md +131 -131
  92. package/npm-docs/api/omdRationalNode.md +144 -144
  93. package/npm-docs/api/omdSequenceNode.md +128 -128
  94. package/npm-docs/api/omdSimplification.md +78 -78
  95. package/npm-docs/api/omdSqrtNode.md +144 -144
  96. package/npm-docs/api/omdStepVisualizer.md +146 -146
  97. package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
  98. package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
  99. package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
  100. package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
  101. package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
  102. package/npm-docs/api/omdToolbar.md +130 -130
  103. package/npm-docs/api/omdTranscriptionService.md +95 -95
  104. package/npm-docs/api/omdTreeDiff.md +169 -169
  105. package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
  106. package/npm-docs/api/omdUtilities.md +82 -82
  107. package/npm-docs/api/omdVariableNode.md +123 -123
  108. package/npm-docs/api/selectTool.md +74 -74
  109. package/npm-docs/api/simplificationEngine.md +97 -97
  110. package/npm-docs/api/simplificationRules.md +76 -76
  111. package/npm-docs/api/simplificationUtils.md +64 -64
  112. package/npm-docs/api/transcribe.md +43 -43
  113. package/npm-docs/guides/equations.md +854 -854
  114. package/npm-docs/guides/factory-functions.md +354 -354
  115. package/npm-docs/guides/getting-started.md +318 -318
  116. package/npm-docs/guides/quick-examples.md +525 -525
  117. package/npm-docs/guides/visualizations.md +682 -682
  118. package/npm-docs/index.html +12 -0
  119. package/npm-docs/json-schemas.md +826 -826
  120. package/omd/config/omdConfigManager.js +279 -267
  121. package/omd/core/index.js +158 -158
  122. package/omd/core/omdEquationStack.js +606 -547
  123. package/omd/core/omdUtilities.js +113 -113
  124. package/omd/display/omdDisplay.js +1045 -963
  125. package/omd/display/omdToolbar.js +501 -501
  126. package/omd/nodes/omdBinaryExpressionNode.js +459 -459
  127. package/omd/nodes/omdConstantNode.js +141 -141
  128. package/omd/nodes/omdEquationNode.js +1327 -1327
  129. package/omd/nodes/omdFunctionNode.js +351 -351
  130. package/omd/nodes/omdGroupNode.js +67 -67
  131. package/omd/nodes/omdLeafNode.js +76 -76
  132. package/omd/nodes/omdNode.js +556 -556
  133. package/omd/nodes/omdOperationDisplayNode.js +321 -321
  134. package/omd/nodes/omdOperatorNode.js +108 -108
  135. package/omd/nodes/omdParenthesisNode.js +292 -292
  136. package/omd/nodes/omdPowerNode.js +235 -235
  137. package/omd/nodes/omdRationalNode.js +295 -295
  138. package/omd/nodes/omdSqrtNode.js +307 -307
  139. package/omd/nodes/omdUnaryExpressionNode.js +227 -227
  140. package/omd/nodes/omdVariableNode.js +122 -122
  141. package/omd/simplification/omdSimplification.js +140 -140
  142. package/omd/simplification/omdSimplificationEngine.js +887 -887
  143. package/omd/simplification/package.json +5 -5
  144. package/omd/simplification/rules/binaryRules.js +1037 -1037
  145. package/omd/simplification/rules/functionRules.js +111 -111
  146. package/omd/simplification/rules/index.js +48 -48
  147. package/omd/simplification/rules/parenthesisRules.js +19 -19
  148. package/omd/simplification/rules/powerRules.js +143 -143
  149. package/omd/simplification/rules/rationalRules.js +725 -725
  150. package/omd/simplification/rules/sqrtRules.js +48 -48
  151. package/omd/simplification/rules/unaryRules.js +37 -37
  152. package/omd/simplification/simplificationRules.js +31 -31
  153. package/omd/simplification/simplificationUtils.js +1055 -1055
  154. package/omd/step-visualizer/omdStepVisualizer.js +947 -947
  155. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
  156. package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
  157. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
  158. package/omd/utils/aiNextEquationStep.js +106 -106
  159. package/omd/utils/omdNodeOverlay.js +638 -638
  160. package/omd/utils/omdPopup.js +1203 -1203
  161. package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
  162. package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
  163. package/omd/utils/omdTranscriptionService.js +123 -123
  164. package/omd/utils/omdTreeDiff.js +733 -733
  165. package/package.json +59 -57
  166. package/readme.html +184 -120
  167. package/src/index.js +74 -74
  168. package/src/json-schemas.md +576 -576
  169. package/src/omd-json-samples.js +147 -147
  170. package/src/omdApp.js +391 -391
  171. package/src/omdAppCanvas.js +335 -335
  172. package/src/omdBalanceHanger.js +199 -199
  173. package/src/omdColor.js +13 -13
  174. package/src/omdCoordinatePlane.js +541 -541
  175. package/src/omdExpression.js +115 -115
  176. package/src/omdFactory.js +150 -150
  177. package/src/omdFunction.js +114 -114
  178. package/src/omdMetaExpression.js +290 -290
  179. package/src/omdNaturalExpression.js +563 -563
  180. package/src/omdNode.js +383 -383
  181. package/src/omdNumber.js +52 -52
  182. package/src/omdNumberLine.js +114 -112
  183. package/src/omdNumberTile.js +118 -118
  184. package/src/omdOperator.js +72 -72
  185. package/src/omdPowerExpression.js +91 -91
  186. package/src/omdProblem.js +259 -259
  187. package/src/omdRatioChart.js +251 -251
  188. package/src/omdRationalExpression.js +114 -114
  189. package/src/omdSampleData.js +215 -215
  190. package/src/omdShapes.js +512 -512
  191. package/src/omdSpinner.js +151 -151
  192. package/src/omdString.js +49 -49
  193. package/src/omdTable.js +498 -498
  194. package/src/omdTapeDiagram.js +244 -244
  195. package/src/omdTerm.js +91 -91
  196. package/src/omdTileEquation.js +349 -349
  197. package/src/omdUtils.js +84 -84
  198. package/src/omdVariable.js +51 -51
@@ -1,734 +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
- }
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
734
  }