@teachinglab/omd 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -251
- package/README.old.md +137 -137
- package/canvas/core/canvasConfig.js +202 -202
- package/canvas/drawing/segment.js +167 -167
- package/canvas/drawing/stroke.js +385 -385
- package/canvas/events/eventManager.js +444 -444
- package/canvas/events/pointerEventHandler.js +262 -262
- package/canvas/index.js +48 -48
- package/canvas/tools/PointerTool.js +71 -71
- package/canvas/tools/tool.js +222 -222
- package/canvas/utils/boundingBox.js +377 -377
- package/canvas/utils/mathUtils.js +258 -258
- package/docs/api/configuration-options.md +198 -198
- package/docs/api/eventManager.md +82 -82
- package/docs/api/focusFrameManager.md +144 -144
- package/docs/api/index.md +105 -105
- package/docs/api/main.md +62 -62
- package/docs/api/omdBinaryExpressionNode.md +86 -86
- package/docs/api/omdCanvas.md +83 -83
- package/docs/api/omdConfigManager.md +112 -112
- package/docs/api/omdConstantNode.md +52 -52
- package/docs/api/omdDisplay.md +87 -87
- package/docs/api/omdEquationNode.md +174 -174
- package/docs/api/omdEquationSequenceNode.md +258 -258
- package/docs/api/omdEquationStack.md +192 -192
- package/docs/api/omdFunctionNode.md +82 -82
- package/docs/api/omdGroupNode.md +78 -78
- package/docs/api/omdHelpers.md +87 -87
- package/docs/api/omdLeafNode.md +85 -85
- package/docs/api/omdNode.md +201 -201
- package/docs/api/omdOperationDisplayNode.md +117 -117
- package/docs/api/omdOperatorNode.md +91 -91
- package/docs/api/omdParenthesisNode.md +133 -133
- package/docs/api/omdPopup.md +191 -191
- package/docs/api/omdPowerNode.md +131 -131
- package/docs/api/omdRationalNode.md +144 -144
- package/docs/api/omdSequenceNode.md +128 -128
- package/docs/api/omdSimplification.md +78 -78
- package/docs/api/omdSqrtNode.md +144 -144
- package/docs/api/omdStepVisualizer.md +146 -146
- package/docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/docs/api/omdStepVisualizerLayout.md +70 -70
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/docs/api/omdToolbar.md +130 -130
- package/docs/api/omdTranscriptionService.md +95 -95
- package/docs/api/omdTreeDiff.md +169 -169
- package/docs/api/omdUnaryExpressionNode.md +137 -137
- package/docs/api/omdUtilities.md +82 -82
- package/docs/api/omdVariableNode.md +123 -123
- package/docs/api/selectTool.md +74 -74
- package/docs/api/simplificationEngine.md +97 -97
- package/docs/api/simplificationRules.md +76 -76
- package/docs/api/simplificationUtils.md +64 -64
- package/docs/api/transcribe.md +43 -43
- package/docs/api-reference.md +85 -85
- package/docs/index.html +453 -453
- package/docs/index.md +38 -38
- package/docs/omd-objects.md +258 -258
- package/index.js +79 -79
- package/jsvg/index.js +3 -0
- package/jsvg/jsvg.js +898 -898
- package/jsvg/jsvgComponents.js +357 -358
- package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
- package/npm-docs/README.md +251 -251
- package/npm-docs/api/api-reference.md +85 -85
- package/npm-docs/api/configuration-options.md +198 -198
- package/npm-docs/api/eventManager.md +82 -82
- package/npm-docs/api/expression-nodes.md +561 -561
- package/npm-docs/api/focusFrameManager.md +144 -144
- package/npm-docs/api/index.md +105 -105
- package/npm-docs/api/main.md +62 -62
- package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
- package/npm-docs/api/omdCanvas.md +83 -83
- package/npm-docs/api/omdConfigManager.md +112 -112
- package/npm-docs/api/omdConstantNode.md +52 -52
- package/npm-docs/api/omdDisplay.md +87 -87
- package/npm-docs/api/omdEquationNode.md +174 -174
- package/npm-docs/api/omdEquationSequenceNode.md +258 -258
- package/npm-docs/api/omdEquationStack.md +192 -192
- package/npm-docs/api/omdFunctionNode.md +82 -82
- package/npm-docs/api/omdGroupNode.md +78 -78
- package/npm-docs/api/omdHelpers.md +87 -87
- package/npm-docs/api/omdLeafNode.md +85 -85
- package/npm-docs/api/omdNode.md +201 -201
- package/npm-docs/api/omdOperationDisplayNode.md +117 -117
- package/npm-docs/api/omdOperatorNode.md +91 -91
- package/npm-docs/api/omdParenthesisNode.md +133 -133
- package/npm-docs/api/omdPopup.md +191 -191
- package/npm-docs/api/omdPowerNode.md +131 -131
- package/npm-docs/api/omdRationalNode.md +144 -144
- package/npm-docs/api/omdSequenceNode.md +128 -128
- package/npm-docs/api/omdSimplification.md +78 -78
- package/npm-docs/api/omdSqrtNode.md +144 -144
- package/npm-docs/api/omdStepVisualizer.md +146 -146
- package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
- package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/npm-docs/api/omdToolbar.md +130 -130
- package/npm-docs/api/omdTranscriptionService.md +95 -95
- package/npm-docs/api/omdTreeDiff.md +169 -169
- package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
- package/npm-docs/api/omdUtilities.md +82 -82
- package/npm-docs/api/omdVariableNode.md +123 -123
- package/npm-docs/api/selectTool.md +74 -74
- package/npm-docs/api/simplificationEngine.md +97 -97
- package/npm-docs/api/simplificationRules.md +76 -76
- package/npm-docs/api/simplificationUtils.md +64 -64
- package/npm-docs/api/transcribe.md +43 -43
- package/npm-docs/guides/equations.md +854 -854
- package/npm-docs/guides/factory-functions.md +354 -354
- package/npm-docs/guides/getting-started.md +318 -318
- package/npm-docs/guides/quick-examples.md +525 -525
- package/npm-docs/guides/visualizations.md +682 -682
- package/npm-docs/index.html +12 -0
- package/npm-docs/json-schemas.md +826 -826
- package/omd/config/omdConfigManager.js +279 -267
- package/omd/core/index.js +158 -158
- package/omd/core/omdEquationStack.js +546 -546
- package/omd/core/omdUtilities.js +113 -113
- package/omd/display/omdDisplay.js +969 -962
- package/omd/display/omdToolbar.js +501 -501
- package/omd/nodes/omdBinaryExpressionNode.js +459 -459
- package/omd/nodes/omdConstantNode.js +141 -141
- package/omd/nodes/omdEquationNode.js +1327 -1327
- package/omd/nodes/omdFunctionNode.js +351 -351
- package/omd/nodes/omdGroupNode.js +67 -67
- package/omd/nodes/omdLeafNode.js +76 -76
- package/omd/nodes/omdNode.js +556 -556
- package/omd/nodes/omdOperationDisplayNode.js +321 -321
- package/omd/nodes/omdOperatorNode.js +108 -108
- package/omd/nodes/omdParenthesisNode.js +292 -292
- package/omd/nodes/omdPowerNode.js +235 -235
- package/omd/nodes/omdRationalNode.js +295 -295
- package/omd/nodes/omdSqrtNode.js +307 -307
- package/omd/nodes/omdUnaryExpressionNode.js +227 -227
- package/omd/nodes/omdVariableNode.js +122 -122
- package/omd/simplification/omdSimplification.js +140 -140
- package/omd/simplification/omdSimplificationEngine.js +887 -887
- package/omd/simplification/package.json +5 -5
- package/omd/simplification/rules/binaryRules.js +1037 -1037
- package/omd/simplification/rules/functionRules.js +111 -111
- package/omd/simplification/rules/index.js +48 -48
- package/omd/simplification/rules/parenthesisRules.js +19 -19
- package/omd/simplification/rules/powerRules.js +143 -143
- package/omd/simplification/rules/rationalRules.js +725 -725
- package/omd/simplification/rules/sqrtRules.js +48 -48
- package/omd/simplification/rules/unaryRules.js +37 -37
- package/omd/simplification/simplificationRules.js +31 -31
- package/omd/simplification/simplificationUtils.js +1055 -1055
- package/omd/step-visualizer/omdStepVisualizer.js +947 -947
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
- package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
- package/omd/utils/aiNextEquationStep.js +106 -106
- package/omd/utils/omdNodeOverlay.js +638 -638
- package/omd/utils/omdPopup.js +1203 -1203
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
- package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
- package/omd/utils/omdTranscriptionService.js +123 -123
- package/omd/utils/omdTreeDiff.js +733 -733
- package/package.json +59 -57
- package/readme.html +184 -120
- package/src/index.js +74 -74
- package/src/json-schemas.md +576 -576
- package/src/omd-json-samples.js +147 -147
- package/src/omdApp.js +391 -391
- package/src/omdAppCanvas.js +335 -335
- package/src/omdBalanceHanger.js +199 -199
- package/src/omdColor.js +13 -13
- package/src/omdCoordinatePlane.js +541 -541
- package/src/omdExpression.js +115 -115
- package/src/omdFactory.js +150 -150
- package/src/omdFunction.js +114 -114
- package/src/omdMetaExpression.js +290 -290
- package/src/omdNaturalExpression.js +563 -563
- package/src/omdNode.js +383 -383
- package/src/omdNumber.js +52 -52
- package/src/omdNumberLine.js +114 -112
- package/src/omdNumberTile.js +118 -118
- package/src/omdOperator.js +72 -72
- package/src/omdPowerExpression.js +91 -91
- package/src/omdProblem.js +259 -259
- package/src/omdRatioChart.js +251 -251
- package/src/omdRationalExpression.js +114 -114
- package/src/omdSampleData.js +215 -215
- package/src/omdShapes.js +512 -512
- package/src/omdSpinner.js +151 -151
- package/src/omdString.js +49 -49
- package/src/omdTable.js +498 -498
- package/src/omdTapeDiagram.js +244 -244
- package/src/omdTerm.js +91 -91
- package/src/omdTileEquation.js +349 -349
- package/src/omdUtils.js +84 -84
- package/src/omdVariable.js +51 -51
package/omd/utils/omdTreeDiff.js
CHANGED
|
@@ -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
|
}
|