@teachinglab/omd 0.6.0 → 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 -56
- 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
|
@@ -1,1056 +1,1056 @@
|
|
|
1
|
-
import { omdConstantNode } from "../nodes/omdConstantNode.js";
|
|
2
|
-
import { omdBinaryExpressionNode } from "../nodes/omdBinaryExpressionNode.js";
|
|
3
|
-
import { omdNode } from "../nodes/omdNode.js";
|
|
4
|
-
import { omdRationalNode } from "../nodes/omdRationalNode.js";
|
|
5
|
-
import { omdUnaryExpressionNode } from "../nodes/omdUnaryExpressionNode.js";
|
|
6
|
-
import { omdPowerNode } from "../nodes/omdPowerNode.js";
|
|
7
|
-
import { omdVariableNode } from "../nodes/omdVariableNode.js";
|
|
8
|
-
import { SimplificationEngine } from "./omdSimplificationEngine.js";
|
|
9
|
-
import { getMultiplicationSymbol } from '../config/omdConfigManager.js';
|
|
10
|
-
|
|
11
|
-
// ===== MANUAL PROVENANCE HELPER FUNCTIONS =====
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Manually applies provenance from source nodes to a target node
|
|
15
|
-
* @param {omdNode} targetNode - The node to apply provenance to
|
|
16
|
-
* @param {...omdNode} sourceNodes - The source nodes to collect provenance from
|
|
17
|
-
* @returns {omdNode} The target node with applied provenance
|
|
18
|
-
*/
|
|
19
|
-
export function applyProvenance(targetNode, ...sourceNodes) {
|
|
20
|
-
if (!targetNode) return targetNode;
|
|
21
|
-
|
|
22
|
-
targetNode.provenance = targetNode.provenance || [];
|
|
23
|
-
|
|
24
|
-
for (const sourceNode of sourceNodes) {
|
|
25
|
-
if (!sourceNode) continue;
|
|
26
|
-
|
|
27
|
-
// Add the source node's ID
|
|
28
|
-
if (sourceNode.id && !targetNode.provenance.includes(sourceNode.id)) {
|
|
29
|
-
targetNode.provenance.push(sourceNode.id);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Add all of the source node's provenance
|
|
33
|
-
if (sourceNode.provenance && Array.isArray(sourceNode.provenance)) {
|
|
34
|
-
sourceNode.provenance.forEach(id => {
|
|
35
|
-
if (id && !targetNode.provenance.includes(id)) {
|
|
36
|
-
targetNode.provenance.push(id);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Recursively collect from children
|
|
42
|
-
collectChildIds(sourceNode, targetNode.provenance);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return targetNode;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Recursively collects IDs from child nodes
|
|
50
|
-
* @param {omdNode} node - The node to collect from
|
|
51
|
-
* @param {Array} idSet - The array to add IDs to
|
|
52
|
-
*/
|
|
53
|
-
function collectChildIds(node, idSet) {
|
|
54
|
-
if (!node || !idSet) return;
|
|
55
|
-
|
|
56
|
-
// Visit common child properties
|
|
57
|
-
const childProps = ['left', 'right', 'base', 'exponent', 'argument', 'expression',
|
|
58
|
-
'numerator', 'denominator', 'content'];
|
|
59
|
-
|
|
60
|
-
for (const prop of childProps) {
|
|
61
|
-
const child = node[prop];
|
|
62
|
-
if (child && child.id && !idSet.includes(child.id)) {
|
|
63
|
-
idSet.push(child.id);
|
|
64
|
-
// Recursively collect from grandchildren
|
|
65
|
-
collectChildIds(child, idSet);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Handle array properties like args
|
|
70
|
-
if (node.args && Array.isArray(node.args)) {
|
|
71
|
-
node.args.forEach(arg => {
|
|
72
|
-
if (arg && arg.id && !idSet.includes(arg.id)) {
|
|
73
|
-
idSet.push(arg.id);
|
|
74
|
-
collectChildIds(arg, idSet);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Calculates the greatest common divisor of two numbers.
|
|
82
|
-
* @param {number} a
|
|
83
|
-
* @param {number} b
|
|
84
|
-
* @returns {number} The GCD of a and b.
|
|
85
|
-
*/
|
|
86
|
-
export function gcd(a, b) {
|
|
87
|
-
a = Math.abs(a);
|
|
88
|
-
b = Math.abs(b);
|
|
89
|
-
while(b) {
|
|
90
|
-
[a, b] = [b, a % b];
|
|
91
|
-
}
|
|
92
|
-
return a;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Applies a mathematical operation to two numbers.
|
|
97
|
-
* @param {string} op - The operator ('add', 'subtract', 'multiply', 'divide', or symbolic representation).
|
|
98
|
-
* @param {number} left - The left operand.
|
|
99
|
-
* @param {number} right - The right operand.
|
|
100
|
-
* @returns {number|null} The result of the operation, or null if the operation is invalid (e.g., division by zero).
|
|
101
|
-
*/
|
|
102
|
-
export function _applyOperator(op, left, right) {
|
|
103
|
-
switch (op) {
|
|
104
|
-
case 'add':
|
|
105
|
-
return left + right;
|
|
106
|
-
case 'subtract':
|
|
107
|
-
return left - right;
|
|
108
|
-
case 'multiply':
|
|
109
|
-
return left * right;
|
|
110
|
-
case 'divide':
|
|
111
|
-
return right !== 0 ? left / right : null;
|
|
112
|
-
default:
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Creates a new, fully initialized `omdConstantNode`.
|
|
119
|
-
* @param {number} value - The numerical value for the constant.
|
|
120
|
-
* @param {number} fontSize - The font size to render the node with.
|
|
121
|
-
* @returns {omdConstantNode} The newly created constant node.
|
|
122
|
-
*/
|
|
123
|
-
export function createConstantNode(value, fontSize) {
|
|
124
|
-
const newConstantAST = { type: 'ConstantNode', value: value, clone: function () { return { ...this }; } };
|
|
125
|
-
const newConstantNode = new omdConstantNode(newConstantAST);
|
|
126
|
-
newConstantNode.setFontSize(fontSize);
|
|
127
|
-
newConstantNode.initialize();
|
|
128
|
-
return newConstantNode;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Helper to safely replace an old node with a new node in the tree.
|
|
133
|
-
* Handles both root node replacement and child node replacement.
|
|
134
|
-
* @param {omdNode} oldNode - The node to be replaced.
|
|
135
|
-
* @param {omdNode} newNode - The new node to insert.
|
|
136
|
-
* @param {omdNode} currentRoot - The current root of the entire expression tree.
|
|
137
|
-
* @returns {{success: boolean, newRoot: omdNode}} The result of the operation.
|
|
138
|
-
*/
|
|
139
|
-
export function _replaceNodeInTree(oldNode, newNode, currentRoot) {
|
|
140
|
-
if (oldNode === currentRoot) {
|
|
141
|
-
return { success: true, newRoot: newNode };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// This ensures layout updates are triggered during the replacement itself to maintain visual consistency.
|
|
145
|
-
const success = oldNode.replaceWith(newNode, { updateLayout: true });
|
|
146
|
-
|
|
147
|
-
// Update the astNodeData for the new node and propagate changes upwards
|
|
148
|
-
if (success) {
|
|
149
|
-
newNode.astNodeData = newNode.toMathJSNode();
|
|
150
|
-
let current = newNode.parent;
|
|
151
|
-
while (current) {
|
|
152
|
-
// Only update astNodeData for actual omdNodes
|
|
153
|
-
if (current instanceof omdNode) {
|
|
154
|
-
current.astNodeData = current.toMathJSNode();
|
|
155
|
-
} else {
|
|
156
|
-
// Stop traversing if we encounter a non-omdNode parent (like jsvgContainer)
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
current = current.parent;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return { success: success, newRoot: currentRoot };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Extracts all leaf nodes (constants and variables) from an expression tree
|
|
168
|
-
* for granular provenance tracking
|
|
169
|
-
*/
|
|
170
|
-
export function extractLeafNodes(node) {
|
|
171
|
-
const leafNodes = [];
|
|
172
|
-
|
|
173
|
-
function traverse(n) {
|
|
174
|
-
if (!n) return;
|
|
175
|
-
|
|
176
|
-
// If it's a leaf node (constant or variable), add it
|
|
177
|
-
if (n.type === 'omdConstantNode' ||
|
|
178
|
-
n.type === 'omdVariableNode') {
|
|
179
|
-
leafNodes.push(n);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Recursively traverse child nodes
|
|
184
|
-
if (n.left) traverse(n.left);
|
|
185
|
-
if (n.right) traverse(n.right);
|
|
186
|
-
if (n.base) traverse(n.base);
|
|
187
|
-
if (n.exponent) traverse(n.exponent);
|
|
188
|
-
if (n.argument) traverse(n.argument);
|
|
189
|
-
if (n.expression) traverse(n.expression);
|
|
190
|
-
if (n.numerator) traverse(n.numerator);
|
|
191
|
-
if (n.denominator) traverse(n.denominator);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
traverse(node);
|
|
195
|
-
return leafNodes;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Recursively flattens a tree of additions and subtractions into a single list of terms.
|
|
200
|
-
* Each term is an object containing the node and its sign (1 for addition, -1 for subtraction).
|
|
201
|
-
* For example, `a - (b + c)` becomes `[{node: a, sign: 1}, {node: b, sign: -1}, {node: c, sign: -1}]`.
|
|
202
|
-
* @param {omdNode} node - The current node in the expression tree to process.
|
|
203
|
-
* @param {Array<Object>} terms - An array to accumulate the flattened terms. This array is modified by the function.
|
|
204
|
-
*/
|
|
205
|
-
export function flattenSum(node, terms) {
|
|
206
|
-
const op = node.operation;
|
|
207
|
-
if (node.type === 'omdBinaryExpressionNode' && (op === 'add' || op === '+')) {
|
|
208
|
-
flattenSum(node.left, terms);
|
|
209
|
-
flattenSum(node.right, terms);
|
|
210
|
-
} else if (node.type === 'omdBinaryExpressionNode' && (op === 'subtract' || op === '-')) {
|
|
211
|
-
flattenSum(node.left, terms);
|
|
212
|
-
const rightTerms = [];
|
|
213
|
-
flattenSum(node.right, rightTerms);
|
|
214
|
-
rightTerms.forEach(t => { t.sign *= -1; }); // Invert the sign for all terms on the right of a minus.
|
|
215
|
-
terms.push(...rightTerms);
|
|
216
|
-
} else {
|
|
217
|
-
// This is a leaf node in the sum (could be a variable, a multiplication, etc.)
|
|
218
|
-
// Extract leaf nodes for granular provenance tracking
|
|
219
|
-
const leafNodes = extractLeafNodes(node);
|
|
220
|
-
terms.push({
|
|
221
|
-
node: node,
|
|
222
|
-
sign: 1,
|
|
223
|
-
leafNodes: leafNodes // Add leaf nodes for provenance
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function buildSumTree(terms, fontSize) {
|
|
229
|
-
if (terms.length === 0) return createConstantNode(0, fontSize);
|
|
230
|
-
|
|
231
|
-
// Sort terms to handle subtractions gracefully
|
|
232
|
-
terms.sort((a, b) => b.sign - a.sign);
|
|
233
|
-
|
|
234
|
-
// If the expression starts with a negative term (e.g., -a + b), we prepend a '0'
|
|
235
|
-
if (terms.length > 1 && terms[0].sign === -1) {
|
|
236
|
-
if (terms[0].node.type === 'omdConstantNode') {
|
|
237
|
-
const first = terms.shift();
|
|
238
|
-
first.sign = 1;
|
|
239
|
-
terms.push(first);
|
|
240
|
-
} else {
|
|
241
|
-
terms.unshift({node: createConstantNode(0, fontSize), sign: 1});
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Build the tree from left to right
|
|
246
|
-
let firstTerm = terms.shift();
|
|
247
|
-
let currentTree = firstTerm.node;
|
|
248
|
-
|
|
249
|
-
// Ensure the first node is properly formed - clone it if needed
|
|
250
|
-
if (!currentTree || typeof currentTree.updateLayoutUpwards !== 'function') {
|
|
251
|
-
const originalId = currentTree.id;
|
|
252
|
-
currentTree = currentTree.clone();
|
|
253
|
-
currentTree.provenance = currentTree.provenance || [];
|
|
254
|
-
if (originalId && !currentTree.provenance.includes(originalId)) {
|
|
255
|
-
currentTree.provenance.push(originalId);
|
|
256
|
-
}
|
|
257
|
-
currentTree.setFontSize(fontSize);
|
|
258
|
-
currentTree.initialize();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// If there's only one term, return it directly (it should already have correct provenance)
|
|
262
|
-
if (terms.length === 0) {
|
|
263
|
-
return currentTree;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
while (terms.length > 0) {
|
|
267
|
-
const term = terms.shift();
|
|
268
|
-
const opSymbol = term.sign === 1 ? '+' : '-';
|
|
269
|
-
const opFn = term.sign === 1 ? 'add' : 'subtract';
|
|
270
|
-
|
|
271
|
-
let termNode = term.node;
|
|
272
|
-
|
|
273
|
-
if (!termNode || typeof termNode.updateLayoutUpwards !== 'function') {
|
|
274
|
-
const originalId = termNode.id;
|
|
275
|
-
termNode = termNode.clone();
|
|
276
|
-
termNode.provenance = termNode.provenance || [];
|
|
277
|
-
if (originalId && !termNode.provenance.includes(originalId)) {
|
|
278
|
-
termNode.provenance.push(originalId);
|
|
279
|
-
}
|
|
280
|
-
termNode.setFontSize(fontSize);
|
|
281
|
-
termNode.initialize();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Preserve expansion provenance metadata if it exists
|
|
285
|
-
if (term.expansionProvenance) {
|
|
286
|
-
// Add expansion metadata to the term node for later reference
|
|
287
|
-
termNode.expansionProvenance = termNode.expansionProvenance || [];
|
|
288
|
-
termNode.expansionProvenance.push(term.expansionProvenance);
|
|
289
|
-
|
|
290
|
-
// Ensure all expansion source IDs are in the term's provenance
|
|
291
|
-
[term.expansionProvenance.leftSource, term.expansionProvenance.rightSource, term.expansionProvenance.originalMultiplication].forEach(id => {
|
|
292
|
-
if (id && !termNode.provenance.includes(id)) {
|
|
293
|
-
termNode.provenance.push(id);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Create new binary expression
|
|
299
|
-
const newAST = {
|
|
300
|
-
type: 'OperatorNode',
|
|
301
|
-
op: opSymbol,
|
|
302
|
-
fn: opFn,
|
|
303
|
-
args: [currentTree.toMathJSNode(), termNode.toMathJSNode()],
|
|
304
|
-
clone: function () { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const newNode = new omdBinaryExpressionNode(newAST);
|
|
308
|
-
newNode.setFontSize(fontSize);
|
|
309
|
-
newNode.initialize();
|
|
310
|
-
|
|
311
|
-
// Binary operation nodes should inherit provenance from their operands
|
|
312
|
-
// This allows tracking which terms contributed to the final expression
|
|
313
|
-
const leftProvenance = currentTree.provenance || [];
|
|
314
|
-
const rightProvenance = termNode.provenance || [];
|
|
315
|
-
|
|
316
|
-
// Add provenance from both left and right operands
|
|
317
|
-
[...leftProvenance, ...rightProvenance, currentTree.id, termNode.id].forEach(id => {
|
|
318
|
-
if (id && !newNode.provenance.includes(id)) {
|
|
319
|
-
newNode.provenance.push(id);
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// Preserve expansion provenance metadata in the binary expression
|
|
324
|
-
if (termNode.expansionProvenance || currentTree.expansionProvenance) {
|
|
325
|
-
newNode.expansionProvenance = newNode.expansionProvenance || [];
|
|
326
|
-
if (termNode.expansionProvenance) {
|
|
327
|
-
newNode.expansionProvenance.push(...termNode.expansionProvenance);
|
|
328
|
-
}
|
|
329
|
-
if (currentTree.expansionProvenance) {
|
|
330
|
-
newNode.expansionProvenance.push(...currentTree.expansionProvenance);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
currentTree = newNode;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return currentTree;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Creates a rational node (fraction) safely with proper initialization
|
|
342
|
-
* @param {number} numerator - The numerator value
|
|
343
|
-
* @param {number} denominator - The denominator value
|
|
344
|
-
* @param {number} fontSize - The font size for the node
|
|
345
|
-
* @returns {omdNode} A properly initialized rational or constant node
|
|
346
|
-
*/
|
|
347
|
-
export function createRationalNode(numerator, denominator, fontSize) {
|
|
348
|
-
// If denominator is 1, just return a constant
|
|
349
|
-
if (denominator === 1) {
|
|
350
|
-
return createConstantNode(numerator, fontSize);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Create AST for the rational node
|
|
354
|
-
const ast = {
|
|
355
|
-
type: 'OperatorNode',
|
|
356
|
-
op: '/',
|
|
357
|
-
fn: 'divide',
|
|
358
|
-
args: [
|
|
359
|
-
{ type: 'ConstantNode', value: Math.abs(numerator), clone: function() { return {...this}; } },
|
|
360
|
-
{ type: 'ConstantNode', value: denominator, clone: function() { return {...this}; } }
|
|
361
|
-
],
|
|
362
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
let node = new omdRationalNode(ast);
|
|
366
|
-
|
|
367
|
-
// Handle negative fractions by wrapping in unary minus
|
|
368
|
-
if (numerator < 0) {
|
|
369
|
-
const unaryAST = {
|
|
370
|
-
type: 'OperatorNode',
|
|
371
|
-
op: '-',
|
|
372
|
-
fn: 'unaryMinus',
|
|
373
|
-
args: [node.toMathJSNode()],
|
|
374
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
375
|
-
};
|
|
376
|
-
node = new omdUnaryExpressionNode(unaryAST);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
node.setFontSize(fontSize);
|
|
380
|
-
node.initialize();
|
|
381
|
-
|
|
382
|
-
return node;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Converts any omdNode into a human-readable string representation.
|
|
387
|
-
* @param {omdNode} node - The node to convert.
|
|
388
|
-
* @returns {string} A string representation of the node.
|
|
389
|
-
*/
|
|
390
|
-
export function nodeToString(node) {
|
|
391
|
-
if (!node) return '';
|
|
392
|
-
|
|
393
|
-
// Operation name to symbol mapping
|
|
394
|
-
const operationSymbols = {
|
|
395
|
-
'add': '+',
|
|
396
|
-
'subtract': '-',
|
|
397
|
-
'multiply': getMultiplicationSymbol(),
|
|
398
|
-
'divide': '÷',
|
|
399
|
-
'unaryMinus': '-',
|
|
400
|
-
'unaryPlus': '+',
|
|
401
|
-
'pow': '^'
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
// Check for a getValue method (constants) - but only if actually constant
|
|
405
|
-
if (typeof node.getValue === 'function' && node.isConstant && node.isConstant()) {
|
|
406
|
-
return node.getValue().toString();
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Check for a name property (variables)
|
|
410
|
-
if (node.name) {
|
|
411
|
-
return node.name;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Handle binary expressions recursively
|
|
415
|
-
if (node.type === 'omdBinaryExpressionNode') {
|
|
416
|
-
const left = nodeToString(node.left);
|
|
417
|
-
const right = nodeToString(node.right);
|
|
418
|
-
// Handle cases where node.op might be null (implicit multiplication)
|
|
419
|
-
let op;
|
|
420
|
-
if (node.op && node.op.opName) {
|
|
421
|
-
op = operationSymbols[node.op.opName] || node.op.opName;
|
|
422
|
-
} else if (node.operation) {
|
|
423
|
-
op = operationSymbols[node.operation] || node.operation;
|
|
424
|
-
} else {
|
|
425
|
-
// For implicit multiplication, use empty string
|
|
426
|
-
op = ''; // This handles cases like "2x" where it's implicit multiplication
|
|
427
|
-
}
|
|
428
|
-
// Only add parentheses for explicit operations, not implicit multiplication
|
|
429
|
-
return op ? `${left} ${op} ${right}` : `${left}${right}`;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Handle unary expressions
|
|
433
|
-
if (node.type === 'omdUnaryExpressionNode') {
|
|
434
|
-
const arg = nodeToString(node.argument);
|
|
435
|
-
let op;
|
|
436
|
-
if (node.op && node.op.opName) {
|
|
437
|
-
op = operationSymbols[node.op.opName] || node.op.opName;
|
|
438
|
-
} else if (node.operation) {
|
|
439
|
-
op = operationSymbols[node.operation] || node.operation;
|
|
440
|
-
} else {
|
|
441
|
-
op = '-'; // Default for unary
|
|
442
|
-
}
|
|
443
|
-
// Only add parentheses around the argument if it's a complex expression
|
|
444
|
-
if (arg.includes(' ') || arg.includes('+') || arg.includes('-') || arg.includes(getMultiplicationSymbol()) || arg.includes('÷')) {
|
|
445
|
-
return `${op}(${arg})`;
|
|
446
|
-
}
|
|
447
|
-
return `${op}${arg}`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Handle rational nodes
|
|
451
|
-
if (node.type === 'omdRationalNode') {
|
|
452
|
-
const num = nodeToString(node.numerator);
|
|
453
|
-
const den = nodeToString(node.denominator);
|
|
454
|
-
return `(${num}/${den})`;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Handle parenthesis nodes
|
|
458
|
-
if (node.type === 'omdParenthesisNode') {
|
|
459
|
-
const content = nodeToString(node.content || node.expression);
|
|
460
|
-
// Only add parentheses if the content doesn't already start and end with them
|
|
461
|
-
if (content.startsWith('(') && content.endsWith(')')) {
|
|
462
|
-
return content;
|
|
463
|
-
}
|
|
464
|
-
return `(${content})`;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Handle power nodes
|
|
468
|
-
if (node.type === 'omdPowerNode') {
|
|
469
|
-
const base = nodeToString(node.base);
|
|
470
|
-
const exp = nodeToString(node.exponent);
|
|
471
|
-
return `${base}^${exp}`;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Handle sqrt nodes
|
|
475
|
-
if (node.type === 'omdSqrtNode') {
|
|
476
|
-
const arg = nodeToString(node.argument);
|
|
477
|
-
return `√(${arg})`;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Handle function nodes
|
|
481
|
-
if (node.type === 'omdFunctionNode') {
|
|
482
|
-
const functionName = node.functionName || node.name || 'f';
|
|
483
|
-
if (node.argNodes && node.argNodes.length > 0) {
|
|
484
|
-
const args = node.argNodes.map(arg => nodeToString(arg)).join(', ');
|
|
485
|
-
return `${functionName}(${args})`;
|
|
486
|
-
} else if (node.args && node.args.length > 0) {
|
|
487
|
-
const args = node.args.map(arg => nodeToString(arg)).join(', ');
|
|
488
|
-
return `${functionName}(${args})`;
|
|
489
|
-
}
|
|
490
|
-
return `${functionName}()`;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Handle equation nodes
|
|
494
|
-
if (node.type === 'omdEquationNode') {
|
|
495
|
-
const left = nodeToString(node.left);
|
|
496
|
-
const right = nodeToString(node.right);
|
|
497
|
-
return `${left} = ${right}`;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Fallback for other node types - try toString method first
|
|
501
|
-
if (typeof node.toString === 'function') {
|
|
502
|
-
try {
|
|
503
|
-
return node.toString();
|
|
504
|
-
} catch (e) {
|
|
505
|
-
// Continue to next fallback
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Use math.js as final fallback
|
|
510
|
-
if (node.toMathJSNode) {
|
|
511
|
-
try {
|
|
512
|
-
return math.parse(node.toMathJSNode()).toString();
|
|
513
|
-
} catch (e) {
|
|
514
|
-
// Continue to final fallback
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Use node name if available, otherwise use constructor name as last resort
|
|
519
|
-
return node.name || node.type || '[unknown]';
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// ===== POLYNOMIAL EXPANSION HELPER FUNCTIONS =====
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Expands a polynomial power using multinomial expansion
|
|
526
|
-
* For example: (a + b)^2 = a^2 + 2ab + b^2
|
|
527
|
-
* @param {Array} terms - Array of {node, sign} objects representing the polynomial terms
|
|
528
|
-
* @param {number} exponent - The power to expand to
|
|
529
|
-
* @param {number} fontSize - Font size for new nodes
|
|
530
|
-
* @returns {Array} Array of {node, sign} objects representing the expanded terms
|
|
531
|
-
*/
|
|
532
|
-
export function expandPolynomialPower(terms, exponent, fontSize) {
|
|
533
|
-
if (exponent === 1) {
|
|
534
|
-
return terms;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (exponent === 2) {
|
|
538
|
-
return expandBinomialSquare(terms, fontSize);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (exponent === 3) {
|
|
542
|
-
return expandBinomialCube(terms, fontSize);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
if (exponent === 4) {
|
|
546
|
-
return expandBinomialFourth(terms, fontSize);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// For higher powers, use recursive multiplication
|
|
550
|
-
let result = terms;
|
|
551
|
-
for (let i = 1; i < exponent; i++) {
|
|
552
|
-
result = multiplyTermArrays(result, terms, fontSize);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
return result;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Expands (a + b + ...)^2 using the multinomial theorem
|
|
560
|
-
*/
|
|
561
|
-
export function expandBinomialSquare(terms, fontSize) {
|
|
562
|
-
const expandedTerms = [];
|
|
563
|
-
|
|
564
|
-
// Square terms: a^2, b^2, ...
|
|
565
|
-
for (const term of terms) {
|
|
566
|
-
const squaredNode = SimplificationEngine.createBinaryOp(
|
|
567
|
-
term.node.clone(),
|
|
568
|
-
'multiply',
|
|
569
|
-
term.node.clone(),
|
|
570
|
-
fontSize
|
|
571
|
-
);
|
|
572
|
-
|
|
573
|
-
// Use granular provenance from leaf nodes
|
|
574
|
-
const leafNodes = term.leafNodes || [];
|
|
575
|
-
leafNodes.forEach(leafNode => {
|
|
576
|
-
[squaredNode.left, squaredNode.right, squaredNode].forEach(part => {
|
|
577
|
-
if (part && !part.provenance.includes(leafNode.id)) {
|
|
578
|
-
part.provenance.push(leafNode.id);
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
expandedTerms.push({
|
|
584
|
-
node: squaredNode,
|
|
585
|
-
sign: term.sign * term.sign // Always positive since we're squaring
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Cross terms: 2ab, 2ac, 2bc, ...
|
|
590
|
-
for (let i = 0; i < terms.length; i++) {
|
|
591
|
-
for (let j = i + 1; j < terms.length; j++) {
|
|
592
|
-
const term1 = terms[i];
|
|
593
|
-
const term2 = terms[j];
|
|
594
|
-
|
|
595
|
-
// Create 2 * term1 * term2
|
|
596
|
-
const coefficient2 = SimplificationEngine.createConstant(2, fontSize);
|
|
597
|
-
const product1 = SimplificationEngine.createBinaryOp(
|
|
598
|
-
coefficient2,
|
|
599
|
-
'multiply',
|
|
600
|
-
term1.node.clone(),
|
|
601
|
-
fontSize
|
|
602
|
-
);
|
|
603
|
-
const finalProduct = SimplificationEngine.createBinaryOp(
|
|
604
|
-
product1,
|
|
605
|
-
'multiply',
|
|
606
|
-
term2.node.clone(),
|
|
607
|
-
fontSize
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
// Use granular provenance from both terms
|
|
611
|
-
const allLeafNodes = [...(term1.leafNodes || []), ...(term2.leafNodes || [])];
|
|
612
|
-
allLeafNodes.forEach(leafNode => {
|
|
613
|
-
[product1.right, finalProduct.right, finalProduct].forEach(part => {
|
|
614
|
-
if (part && !part.provenance.includes(leafNode.id)) {
|
|
615
|
-
part.provenance.push(leafNode.id);
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
expandedTerms.push({
|
|
621
|
-
node: finalProduct,
|
|
622
|
-
sign: term1.sign * term2.sign
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return expandedTerms;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Expands (a + b + ...)^3 for binomial/trinomial cases
|
|
632
|
-
*/
|
|
633
|
-
export function expandBinomialCube(terms, fontSize) {
|
|
634
|
-
if (terms.length === 2) {
|
|
635
|
-
const [term1, term2] = terms;
|
|
636
|
-
const expandedTerms = [];
|
|
637
|
-
|
|
638
|
-
// Use the efficient helper functions with provenance
|
|
639
|
-
expandedTerms.push({
|
|
640
|
-
node: createPowerTermWithProvenance(term1.node, 3, fontSize, term1.leafNodes),
|
|
641
|
-
sign: Math.pow(term1.sign, 3)
|
|
642
|
-
});
|
|
643
|
-
expandedTerms.push({
|
|
644
|
-
node: createCoefficientProductTermWithProvenance(3, term1.node, 2, term2.node, 1, fontSize, term1.leafNodes, term2.leafNodes),
|
|
645
|
-
sign: Math.pow(term1.sign, 2) * term2.sign
|
|
646
|
-
});
|
|
647
|
-
expandedTerms.push({
|
|
648
|
-
node: createCoefficientProductTermWithProvenance(3, term1.node, 1, term2.node, 2, fontSize, term1.leafNodes, term2.leafNodes),
|
|
649
|
-
sign: term1.sign * Math.pow(term2.sign, 2)
|
|
650
|
-
});
|
|
651
|
-
expandedTerms.push({
|
|
652
|
-
node: createPowerTermWithProvenance(term2.node, 3, fontSize, term2.leafNodes),
|
|
653
|
-
sign: Math.pow(term2.sign, 3)
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
return expandedTerms;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// For more than 2 terms, use general multiplication
|
|
660
|
-
return multiplyTermArrays(expandBinomialSquare(terms, fontSize), terms, fontSize);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Expands (a + b)^4 = a^4 + 4a^3b + 6a^2b^2 + 4ab^3 + b^4
|
|
665
|
-
*/
|
|
666
|
-
export function expandBinomialFourth(terms, fontSize) {
|
|
667
|
-
if (terms.length === 2) {
|
|
668
|
-
const [term1, term2] = terms;
|
|
669
|
-
const expandedTerms = [];
|
|
670
|
-
|
|
671
|
-
// Use the efficient helper functions with provenance
|
|
672
|
-
expandedTerms.push({
|
|
673
|
-
node: createPowerTermWithProvenance(term1.node, 4, fontSize, term1.leafNodes),
|
|
674
|
-
sign: Math.pow(term1.sign, 4)
|
|
675
|
-
});
|
|
676
|
-
expandedTerms.push({
|
|
677
|
-
node: createCoefficientProductTermWithProvenance(4, term1.node, 3, term2.node, 1, fontSize, term1.leafNodes, term2.leafNodes),
|
|
678
|
-
sign: Math.pow(term1.sign, 3) * term2.sign
|
|
679
|
-
});
|
|
680
|
-
expandedTerms.push({
|
|
681
|
-
node: createCoefficientProductTermWithProvenance(6, term1.node, 2, term2.node, 2, fontSize, term1.leafNodes, term2.leafNodes),
|
|
682
|
-
sign: Math.pow(term1.sign, 2) * Math.pow(term2.sign, 2)
|
|
683
|
-
});
|
|
684
|
-
expandedTerms.push({
|
|
685
|
-
node: createCoefficientProductTermWithProvenance(4, term1.node, 1, term2.node, 3, fontSize, term1.leafNodes, term2.leafNodes),
|
|
686
|
-
sign: term1.sign * Math.pow(term2.sign, 3)
|
|
687
|
-
});
|
|
688
|
-
expandedTerms.push({
|
|
689
|
-
node: createPowerTermWithProvenance(term2.node, 4, fontSize, term2.leafNodes),
|
|
690
|
-
sign: Math.pow(term2.sign, 4)
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
return expandedTerms;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// For more than 2 terms, use general multiplication
|
|
697
|
-
const cubed = expandBinomialCube(terms, fontSize);
|
|
698
|
-
return multiplyTermArrays(cubed, terms, fontSize);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Creates a term like x^n with granular provenance tracking
|
|
703
|
-
*/
|
|
704
|
-
export function createPowerTermWithProvenance(baseNode, power, fontSize, leafNodes) {
|
|
705
|
-
if (power === 1) {
|
|
706
|
-
const cloned = baseNode.clone();
|
|
707
|
-
// Add leaf node provenance
|
|
708
|
-
if (leafNodes && leafNodes.length > 0) {
|
|
709
|
-
leafNodes.forEach(leafNode => {
|
|
710
|
-
if (!cloned.provenance.includes(leafNode.id)) {
|
|
711
|
-
cloned.provenance.push(leafNode.id);
|
|
712
|
-
}
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
return cloned;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
const powerConstant = SimplificationEngine.createConstant(power, fontSize);
|
|
719
|
-
|
|
720
|
-
// Create power node AST structure
|
|
721
|
-
const powerAST = {
|
|
722
|
-
type: 'OperatorNode',
|
|
723
|
-
op: '^',
|
|
724
|
-
fn: 'pow',
|
|
725
|
-
args: [baseNode.toMathJSNode(), powerConstant.toMathJSNode()],
|
|
726
|
-
clone: function() {
|
|
727
|
-
return {
|
|
728
|
-
...this,
|
|
729
|
-
args: this.args.map(arg => arg.clone())
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
const powerNode = new omdPowerNode(powerAST);
|
|
735
|
-
powerNode.setFontSize(fontSize);
|
|
736
|
-
powerNode.initialize();
|
|
737
|
-
|
|
738
|
-
// Add leaf node provenance
|
|
739
|
-
if (leafNodes && leafNodes.length > 0) {
|
|
740
|
-
leafNodes.forEach(leafNode => {
|
|
741
|
-
if (!powerNode.base.provenance.includes(leafNode.id)) {
|
|
742
|
-
powerNode.base.provenance.push(leafNode.id);
|
|
743
|
-
}
|
|
744
|
-
if (!powerNode.provenance.includes(leafNode.id)) {
|
|
745
|
-
powerNode.provenance.push(leafNode.id);
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
} else {
|
|
749
|
-
powerNode.base.provenance.push(baseNode.id);
|
|
750
|
-
powerNode.provenance.push(baseNode.id);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
return powerNode;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/**
|
|
757
|
-
* Creates a term like c * a^p1 * b^p2 with granular provenance tracking
|
|
758
|
-
*/
|
|
759
|
-
export function createCoefficientProductTermWithProvenance(coefficient, node1, power1, node2, power2, fontSize, leafNodes1, leafNodes2) {
|
|
760
|
-
let result = SimplificationEngine.createConstant(coefficient, fontSize);
|
|
761
|
-
|
|
762
|
-
// Multiply by node1^power1
|
|
763
|
-
if (power1 > 0) {
|
|
764
|
-
const term1 = createPowerTermWithProvenance(node1, power1, fontSize, leafNodes1);
|
|
765
|
-
result = SimplificationEngine.createBinaryOp(result, 'multiply', term1, fontSize);
|
|
766
|
-
|
|
767
|
-
// Add leaf node provenance to the result
|
|
768
|
-
if (leafNodes1 && leafNodes1.length > 0) {
|
|
769
|
-
leafNodes1.forEach(leafNode => {
|
|
770
|
-
if (!result.provenance.includes(leafNode.id)) {
|
|
771
|
-
result.provenance.push(leafNode.id);
|
|
772
|
-
}
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Multiply by node2^power2
|
|
778
|
-
if (power2 > 0) {
|
|
779
|
-
const term2 = createPowerTermWithProvenance(node2, power2, fontSize, leafNodes2);
|
|
780
|
-
result = SimplificationEngine.createBinaryOp(result, 'multiply', term2, fontSize);
|
|
781
|
-
|
|
782
|
-
// Add leaf node provenance to the result
|
|
783
|
-
if (leafNodes2 && leafNodes2.length > 0) {
|
|
784
|
-
leafNodes2.forEach(leafNode => {
|
|
785
|
-
if (!result.provenance.includes(leafNode.id)) {
|
|
786
|
-
result.provenance.push(leafNode.id);
|
|
787
|
-
}
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
return result;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Creates a term like x^n
|
|
797
|
-
*/
|
|
798
|
-
export function createPowerTerm(baseNode, power, fontSize) {
|
|
799
|
-
if (power === 1) {
|
|
800
|
-
return baseNode.clone();
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
const powerConstant = SimplificationEngine.createConstant(power, fontSize);
|
|
804
|
-
|
|
805
|
-
// Create power node AST structure
|
|
806
|
-
const powerAST = {
|
|
807
|
-
type: 'OperatorNode',
|
|
808
|
-
op: '^',
|
|
809
|
-
fn: 'pow',
|
|
810
|
-
args: [baseNode.toMathJSNode(), powerConstant.toMathJSNode()],
|
|
811
|
-
clone: function() {
|
|
812
|
-
return {
|
|
813
|
-
...this,
|
|
814
|
-
args: this.args.map(arg => arg.clone())
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
};
|
|
818
|
-
|
|
819
|
-
const powerNode = new omdPowerNode(powerAST);
|
|
820
|
-
powerNode.setFontSize(fontSize);
|
|
821
|
-
powerNode.initialize();
|
|
822
|
-
|
|
823
|
-
return powerNode;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* Creates a term like c * a^p1 * b^p2
|
|
828
|
-
*/
|
|
829
|
-
export function createCoefficientProductTerm(coefficient, node1, power1, node2, power2, fontSize) {
|
|
830
|
-
let result = SimplificationEngine.createConstant(coefficient, fontSize);
|
|
831
|
-
|
|
832
|
-
// Multiply by node1^power1
|
|
833
|
-
if (power1 > 0) {
|
|
834
|
-
const term1 = createPowerTerm(node1, power1, fontSize);
|
|
835
|
-
result = SimplificationEngine.createBinaryOp(result, 'multiply', term1, fontSize);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Multiply by node2^power2
|
|
839
|
-
if (power2 > 0) {
|
|
840
|
-
const term2 = createPowerTerm(node2, power2, fontSize);
|
|
841
|
-
result = SimplificationEngine.createBinaryOp(result, 'multiply', term2, fontSize);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
return result;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Multiplies two arrays of terms (for general polynomial multiplication)
|
|
849
|
-
*/
|
|
850
|
-
export function multiplyTermArrays(terms1, terms2, fontSize) {
|
|
851
|
-
const result = [];
|
|
852
|
-
|
|
853
|
-
for (const term1 of terms1) {
|
|
854
|
-
for (const term2 of terms2) {
|
|
855
|
-
const productNode = SimplificationEngine.createBinaryOp(
|
|
856
|
-
term1.node.clone(),
|
|
857
|
-
'multiply',
|
|
858
|
-
term2.node.clone(),
|
|
859
|
-
fontSize
|
|
860
|
-
);
|
|
861
|
-
|
|
862
|
-
// Add granular provenance from leaf nodes of both terms
|
|
863
|
-
const allLeafNodes = [...(term1.leafNodes || []), ...(term2.leafNodes || [])];
|
|
864
|
-
allLeafNodes.forEach(leafNode => {
|
|
865
|
-
[productNode.left, productNode.right, productNode].forEach(part => {
|
|
866
|
-
if (part && !part.provenance.includes(leafNode.id)) {
|
|
867
|
-
part.provenance.push(leafNode.id);
|
|
868
|
-
}
|
|
869
|
-
});
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
result.push({
|
|
873
|
-
node: productNode,
|
|
874
|
-
sign: term1.sign * term2.sign,
|
|
875
|
-
leafNodes: allLeafNodes
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
return result;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
/**
|
|
884
|
-
* Creates a monomial with granular provenance tracking for coefficients and variables separately
|
|
885
|
-
*/
|
|
886
|
-
export function createMonomialWithGranularProvenance(coefficient, variable, power, fontSize, coefficientProvenance = [], variableProvenance = []) {
|
|
887
|
-
// Create variable node
|
|
888
|
-
const variableAST = { type: 'SymbolNode', name: variable, clone: function() { return {...this}; } };
|
|
889
|
-
const variableNode = new (SimplificationEngine.getNodeClass('omdVariableNode'))(variableAST);
|
|
890
|
-
variableNode.setFontSize(fontSize);
|
|
891
|
-
variableNode.initialize();
|
|
892
|
-
|
|
893
|
-
// Set variable-specific provenance
|
|
894
|
-
if (variableProvenance && variableProvenance.length > 0) {
|
|
895
|
-
variableNode.provenance = [...variableProvenance];
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
let termNode = variableNode;
|
|
899
|
-
|
|
900
|
-
// Add power if not 1
|
|
901
|
-
if (power !== 1) {
|
|
902
|
-
const powerAST = {
|
|
903
|
-
type: 'OperatorNode',
|
|
904
|
-
op: '^',
|
|
905
|
-
fn: 'pow',
|
|
906
|
-
args: [variableAST, { type: 'ConstantNode', value: power, clone: function() { return {...this}; } }],
|
|
907
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
908
|
-
};
|
|
909
|
-
termNode = new (SimplificationEngine.getNodeClass('omdPowerNode'))(powerAST);
|
|
910
|
-
termNode.setFontSize(fontSize);
|
|
911
|
-
termNode.initialize();
|
|
912
|
-
|
|
913
|
-
// Set variable provenance on the base, power gets no special provenance
|
|
914
|
-
if (termNode.base && variableProvenance && variableProvenance.length > 0) {
|
|
915
|
-
termNode.base.provenance = [...variableProvenance];
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
let result;
|
|
920
|
-
if (coefficient === 1) {
|
|
921
|
-
result = termNode;
|
|
922
|
-
} else if (coefficient === -1) {
|
|
923
|
-
// Create unary minus
|
|
924
|
-
const unaryAST = {
|
|
925
|
-
type: 'OperatorNode',
|
|
926
|
-
op: '-',
|
|
927
|
-
fn: 'unaryMinus',
|
|
928
|
-
args: [termNode.toMathJSNode()],
|
|
929
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
930
|
-
};
|
|
931
|
-
result = new (SimplificationEngine.getNodeClass('omdUnaryExpressionNode'))(unaryAST);
|
|
932
|
-
result.setFontSize(fontSize);
|
|
933
|
-
result.initialize();
|
|
934
|
-
|
|
935
|
-
// The argument preserves variable provenance
|
|
936
|
-
if (result.argument && variableProvenance && variableProvenance.length > 0) {
|
|
937
|
-
result.argument.provenance = [...variableProvenance];
|
|
938
|
-
}
|
|
939
|
-
} else {
|
|
940
|
-
// Create coefficient * term
|
|
941
|
-
const coeffNode = SimplificationEngine.createConstant(Math.abs(coefficient), fontSize);
|
|
942
|
-
|
|
943
|
-
// Set coefficient-specific provenance
|
|
944
|
-
if (coefficientProvenance && coefficientProvenance.length > 0) {
|
|
945
|
-
coeffNode.provenance = [...coefficientProvenance];
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
const multiplicationNode = SimplificationEngine.createBinaryOp(coeffNode, 'multiply', termNode, fontSize);
|
|
949
|
-
|
|
950
|
-
// Apply granular provenance: coefficient to left, variable to right
|
|
951
|
-
if (multiplicationNode.left && coefficientProvenance && coefficientProvenance.length > 0) {
|
|
952
|
-
multiplicationNode.left.provenance = [...coefficientProvenance];
|
|
953
|
-
}
|
|
954
|
-
if (multiplicationNode.right && variableProvenance && variableProvenance.length > 0) {
|
|
955
|
-
multiplicationNode.right.provenance = [...variableProvenance];
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// Wrap in unary minus if coefficient is negative
|
|
959
|
-
if (coefficient < 0) {
|
|
960
|
-
const unaryAST = {
|
|
961
|
-
type: 'OperatorNode',
|
|
962
|
-
op: '-',
|
|
963
|
-
fn: 'unaryMinus',
|
|
964
|
-
args: [multiplicationNode.toMathJSNode()],
|
|
965
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
966
|
-
};
|
|
967
|
-
result = new (SimplificationEngine.getNodeClass('omdUnaryExpressionNode'))(unaryAST);
|
|
968
|
-
result.setFontSize(fontSize);
|
|
969
|
-
result.initialize();
|
|
970
|
-
|
|
971
|
-
// Preserve granular provenance within the unary minus
|
|
972
|
-
if (result.argument) {
|
|
973
|
-
if (result.argument.left && coefficientProvenance && coefficientProvenance.length > 0) {
|
|
974
|
-
result.argument.left.provenance = [...coefficientProvenance];
|
|
975
|
-
}
|
|
976
|
-
if (result.argument.right && variableProvenance && variableProvenance.length > 0) {
|
|
977
|
-
result.argument.right.provenance = [...variableProvenance];
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
} else {
|
|
981
|
-
result = multiplicationNode;
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
return result;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
/**
|
|
989
|
-
* Extracts coefficient and variable leaf nodes separately from a monomial term
|
|
990
|
-
* for granular provenance tracking in like term combination
|
|
991
|
-
*/
|
|
992
|
-
export function extractMonomialProvenance(termNode) {
|
|
993
|
-
const coefficientNodes = [];
|
|
994
|
-
const variableNodes = [];
|
|
995
|
-
|
|
996
|
-
function traverse(node, isInCoefficient = false) {
|
|
997
|
-
if (!node) return;
|
|
998
|
-
|
|
999
|
-
// If we find a variable node, it goes to variables
|
|
1000
|
-
if (node.type === 'omdVariableNode') {
|
|
1001
|
-
variableNodes.push(node);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// If we find a constant node
|
|
1006
|
-
if (node.type === 'omdConstantNode') {
|
|
1007
|
-
// If we're in a power expression, this is likely an exponent, not a coefficient
|
|
1008
|
-
if (node.parent && node.parent.type === 'omdPowerNode' && node.parent.exponent === node) {
|
|
1009
|
-
// Skip exponents for provenance purposes
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
// Otherwise, it's a coefficient
|
|
1013
|
-
coefficientNodes.push(node);
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// For power nodes, traverse base for variables
|
|
1018
|
-
if (node.type === 'omdPowerNode') {
|
|
1019
|
-
if (node.base) traverse(node.base, false);
|
|
1020
|
-
// Don't traverse exponent for provenance
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// For binary operations, determine what we're looking at
|
|
1025
|
-
if (node.type === 'omdBinaryExpressionNode') {
|
|
1026
|
-
if (node.operation === 'multiply') {
|
|
1027
|
-
// In multiplication, left is often coefficient, right is often variable
|
|
1028
|
-
if (node.left) traverse(node.left, true);
|
|
1029
|
-
if (node.right) traverse(node.right, false);
|
|
1030
|
-
} else {
|
|
1031
|
-
// For other operations, traverse both sides
|
|
1032
|
-
if (node.left) traverse(node.left, isInCoefficient);
|
|
1033
|
-
if (node.right) traverse(node.right, isInCoefficient);
|
|
1034
|
-
}
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// For unary operations, traverse the argument
|
|
1039
|
-
if (node.type === 'omdUnaryExpressionNode') {
|
|
1040
|
-
if (node.argument) traverse(node.argument, isInCoefficient);
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// For other node types, try to traverse child properties
|
|
1045
|
-
['left', 'right', 'base', 'exponent', 'argument', 'expression', 'numerator', 'denominator'].forEach(prop => {
|
|
1046
|
-
if (node[prop]) traverse(node[prop], isInCoefficient);
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
traverse(termNode);
|
|
1051
|
-
|
|
1052
|
-
return {
|
|
1053
|
-
coefficientNodes,
|
|
1054
|
-
variableNodes
|
|
1055
|
-
};
|
|
1
|
+
import { omdConstantNode } from "../nodes/omdConstantNode.js";
|
|
2
|
+
import { omdBinaryExpressionNode } from "../nodes/omdBinaryExpressionNode.js";
|
|
3
|
+
import { omdNode } from "../nodes/omdNode.js";
|
|
4
|
+
import { omdRationalNode } from "../nodes/omdRationalNode.js";
|
|
5
|
+
import { omdUnaryExpressionNode } from "../nodes/omdUnaryExpressionNode.js";
|
|
6
|
+
import { omdPowerNode } from "../nodes/omdPowerNode.js";
|
|
7
|
+
import { omdVariableNode } from "../nodes/omdVariableNode.js";
|
|
8
|
+
import { SimplificationEngine } from "./omdSimplificationEngine.js";
|
|
9
|
+
import { getMultiplicationSymbol } from '../config/omdConfigManager.js';
|
|
10
|
+
|
|
11
|
+
// ===== MANUAL PROVENANCE HELPER FUNCTIONS =====
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Manually applies provenance from source nodes to a target node
|
|
15
|
+
* @param {omdNode} targetNode - The node to apply provenance to
|
|
16
|
+
* @param {...omdNode} sourceNodes - The source nodes to collect provenance from
|
|
17
|
+
* @returns {omdNode} The target node with applied provenance
|
|
18
|
+
*/
|
|
19
|
+
export function applyProvenance(targetNode, ...sourceNodes) {
|
|
20
|
+
if (!targetNode) return targetNode;
|
|
21
|
+
|
|
22
|
+
targetNode.provenance = targetNode.provenance || [];
|
|
23
|
+
|
|
24
|
+
for (const sourceNode of sourceNodes) {
|
|
25
|
+
if (!sourceNode) continue;
|
|
26
|
+
|
|
27
|
+
// Add the source node's ID
|
|
28
|
+
if (sourceNode.id && !targetNode.provenance.includes(sourceNode.id)) {
|
|
29
|
+
targetNode.provenance.push(sourceNode.id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Add all of the source node's provenance
|
|
33
|
+
if (sourceNode.provenance && Array.isArray(sourceNode.provenance)) {
|
|
34
|
+
sourceNode.provenance.forEach(id => {
|
|
35
|
+
if (id && !targetNode.provenance.includes(id)) {
|
|
36
|
+
targetNode.provenance.push(id);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Recursively collect from children
|
|
42
|
+
collectChildIds(sourceNode, targetNode.provenance);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return targetNode;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Recursively collects IDs from child nodes
|
|
50
|
+
* @param {omdNode} node - The node to collect from
|
|
51
|
+
* @param {Array} idSet - The array to add IDs to
|
|
52
|
+
*/
|
|
53
|
+
function collectChildIds(node, idSet) {
|
|
54
|
+
if (!node || !idSet) return;
|
|
55
|
+
|
|
56
|
+
// Visit common child properties
|
|
57
|
+
const childProps = ['left', 'right', 'base', 'exponent', 'argument', 'expression',
|
|
58
|
+
'numerator', 'denominator', 'content'];
|
|
59
|
+
|
|
60
|
+
for (const prop of childProps) {
|
|
61
|
+
const child = node[prop];
|
|
62
|
+
if (child && child.id && !idSet.includes(child.id)) {
|
|
63
|
+
idSet.push(child.id);
|
|
64
|
+
// Recursively collect from grandchildren
|
|
65
|
+
collectChildIds(child, idSet);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle array properties like args
|
|
70
|
+
if (node.args && Array.isArray(node.args)) {
|
|
71
|
+
node.args.forEach(arg => {
|
|
72
|
+
if (arg && arg.id && !idSet.includes(arg.id)) {
|
|
73
|
+
idSet.push(arg.id);
|
|
74
|
+
collectChildIds(arg, idSet);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Calculates the greatest common divisor of two numbers.
|
|
82
|
+
* @param {number} a
|
|
83
|
+
* @param {number} b
|
|
84
|
+
* @returns {number} The GCD of a and b.
|
|
85
|
+
*/
|
|
86
|
+
export function gcd(a, b) {
|
|
87
|
+
a = Math.abs(a);
|
|
88
|
+
b = Math.abs(b);
|
|
89
|
+
while(b) {
|
|
90
|
+
[a, b] = [b, a % b];
|
|
91
|
+
}
|
|
92
|
+
return a;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Applies a mathematical operation to two numbers.
|
|
97
|
+
* @param {string} op - The operator ('add', 'subtract', 'multiply', 'divide', or symbolic representation).
|
|
98
|
+
* @param {number} left - The left operand.
|
|
99
|
+
* @param {number} right - The right operand.
|
|
100
|
+
* @returns {number|null} The result of the operation, or null if the operation is invalid (e.g., division by zero).
|
|
101
|
+
*/
|
|
102
|
+
export function _applyOperator(op, left, right) {
|
|
103
|
+
switch (op) {
|
|
104
|
+
case 'add':
|
|
105
|
+
return left + right;
|
|
106
|
+
case 'subtract':
|
|
107
|
+
return left - right;
|
|
108
|
+
case 'multiply':
|
|
109
|
+
return left * right;
|
|
110
|
+
case 'divide':
|
|
111
|
+
return right !== 0 ? left / right : null;
|
|
112
|
+
default:
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a new, fully initialized `omdConstantNode`.
|
|
119
|
+
* @param {number} value - The numerical value for the constant.
|
|
120
|
+
* @param {number} fontSize - The font size to render the node with.
|
|
121
|
+
* @returns {omdConstantNode} The newly created constant node.
|
|
122
|
+
*/
|
|
123
|
+
export function createConstantNode(value, fontSize) {
|
|
124
|
+
const newConstantAST = { type: 'ConstantNode', value: value, clone: function () { return { ...this }; } };
|
|
125
|
+
const newConstantNode = new omdConstantNode(newConstantAST);
|
|
126
|
+
newConstantNode.setFontSize(fontSize);
|
|
127
|
+
newConstantNode.initialize();
|
|
128
|
+
return newConstantNode;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Helper to safely replace an old node with a new node in the tree.
|
|
133
|
+
* Handles both root node replacement and child node replacement.
|
|
134
|
+
* @param {omdNode} oldNode - The node to be replaced.
|
|
135
|
+
* @param {omdNode} newNode - The new node to insert.
|
|
136
|
+
* @param {omdNode} currentRoot - The current root of the entire expression tree.
|
|
137
|
+
* @returns {{success: boolean, newRoot: omdNode}} The result of the operation.
|
|
138
|
+
*/
|
|
139
|
+
export function _replaceNodeInTree(oldNode, newNode, currentRoot) {
|
|
140
|
+
if (oldNode === currentRoot) {
|
|
141
|
+
return { success: true, newRoot: newNode };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// This ensures layout updates are triggered during the replacement itself to maintain visual consistency.
|
|
145
|
+
const success = oldNode.replaceWith(newNode, { updateLayout: true });
|
|
146
|
+
|
|
147
|
+
// Update the astNodeData for the new node and propagate changes upwards
|
|
148
|
+
if (success) {
|
|
149
|
+
newNode.astNodeData = newNode.toMathJSNode();
|
|
150
|
+
let current = newNode.parent;
|
|
151
|
+
while (current) {
|
|
152
|
+
// Only update astNodeData for actual omdNodes
|
|
153
|
+
if (current instanceof omdNode) {
|
|
154
|
+
current.astNodeData = current.toMathJSNode();
|
|
155
|
+
} else {
|
|
156
|
+
// Stop traversing if we encounter a non-omdNode parent (like jsvgContainer)
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
current = current.parent;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { success: success, newRoot: currentRoot };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extracts all leaf nodes (constants and variables) from an expression tree
|
|
168
|
+
* for granular provenance tracking
|
|
169
|
+
*/
|
|
170
|
+
export function extractLeafNodes(node) {
|
|
171
|
+
const leafNodes = [];
|
|
172
|
+
|
|
173
|
+
function traverse(n) {
|
|
174
|
+
if (!n) return;
|
|
175
|
+
|
|
176
|
+
// If it's a leaf node (constant or variable), add it
|
|
177
|
+
if (n.type === 'omdConstantNode' ||
|
|
178
|
+
n.type === 'omdVariableNode') {
|
|
179
|
+
leafNodes.push(n);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Recursively traverse child nodes
|
|
184
|
+
if (n.left) traverse(n.left);
|
|
185
|
+
if (n.right) traverse(n.right);
|
|
186
|
+
if (n.base) traverse(n.base);
|
|
187
|
+
if (n.exponent) traverse(n.exponent);
|
|
188
|
+
if (n.argument) traverse(n.argument);
|
|
189
|
+
if (n.expression) traverse(n.expression);
|
|
190
|
+
if (n.numerator) traverse(n.numerator);
|
|
191
|
+
if (n.denominator) traverse(n.denominator);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
traverse(node);
|
|
195
|
+
return leafNodes;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Recursively flattens a tree of additions and subtractions into a single list of terms.
|
|
200
|
+
* Each term is an object containing the node and its sign (1 for addition, -1 for subtraction).
|
|
201
|
+
* For example, `a - (b + c)` becomes `[{node: a, sign: 1}, {node: b, sign: -1}, {node: c, sign: -1}]`.
|
|
202
|
+
* @param {omdNode} node - The current node in the expression tree to process.
|
|
203
|
+
* @param {Array<Object>} terms - An array to accumulate the flattened terms. This array is modified by the function.
|
|
204
|
+
*/
|
|
205
|
+
export function flattenSum(node, terms) {
|
|
206
|
+
const op = node.operation;
|
|
207
|
+
if (node.type === 'omdBinaryExpressionNode' && (op === 'add' || op === '+')) {
|
|
208
|
+
flattenSum(node.left, terms);
|
|
209
|
+
flattenSum(node.right, terms);
|
|
210
|
+
} else if (node.type === 'omdBinaryExpressionNode' && (op === 'subtract' || op === '-')) {
|
|
211
|
+
flattenSum(node.left, terms);
|
|
212
|
+
const rightTerms = [];
|
|
213
|
+
flattenSum(node.right, rightTerms);
|
|
214
|
+
rightTerms.forEach(t => { t.sign *= -1; }); // Invert the sign for all terms on the right of a minus.
|
|
215
|
+
terms.push(...rightTerms);
|
|
216
|
+
} else {
|
|
217
|
+
// This is a leaf node in the sum (could be a variable, a multiplication, etc.)
|
|
218
|
+
// Extract leaf nodes for granular provenance tracking
|
|
219
|
+
const leafNodes = extractLeafNodes(node);
|
|
220
|
+
terms.push({
|
|
221
|
+
node: node,
|
|
222
|
+
sign: 1,
|
|
223
|
+
leafNodes: leafNodes // Add leaf nodes for provenance
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function buildSumTree(terms, fontSize) {
|
|
229
|
+
if (terms.length === 0) return createConstantNode(0, fontSize);
|
|
230
|
+
|
|
231
|
+
// Sort terms to handle subtractions gracefully
|
|
232
|
+
terms.sort((a, b) => b.sign - a.sign);
|
|
233
|
+
|
|
234
|
+
// If the expression starts with a negative term (e.g., -a + b), we prepend a '0'
|
|
235
|
+
if (terms.length > 1 && terms[0].sign === -1) {
|
|
236
|
+
if (terms[0].node.type === 'omdConstantNode') {
|
|
237
|
+
const first = terms.shift();
|
|
238
|
+
first.sign = 1;
|
|
239
|
+
terms.push(first);
|
|
240
|
+
} else {
|
|
241
|
+
terms.unshift({node: createConstantNode(0, fontSize), sign: 1});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Build the tree from left to right
|
|
246
|
+
let firstTerm = terms.shift();
|
|
247
|
+
let currentTree = firstTerm.node;
|
|
248
|
+
|
|
249
|
+
// Ensure the first node is properly formed - clone it if needed
|
|
250
|
+
if (!currentTree || typeof currentTree.updateLayoutUpwards !== 'function') {
|
|
251
|
+
const originalId = currentTree.id;
|
|
252
|
+
currentTree = currentTree.clone();
|
|
253
|
+
currentTree.provenance = currentTree.provenance || [];
|
|
254
|
+
if (originalId && !currentTree.provenance.includes(originalId)) {
|
|
255
|
+
currentTree.provenance.push(originalId);
|
|
256
|
+
}
|
|
257
|
+
currentTree.setFontSize(fontSize);
|
|
258
|
+
currentTree.initialize();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// If there's only one term, return it directly (it should already have correct provenance)
|
|
262
|
+
if (terms.length === 0) {
|
|
263
|
+
return currentTree;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
while (terms.length > 0) {
|
|
267
|
+
const term = terms.shift();
|
|
268
|
+
const opSymbol = term.sign === 1 ? '+' : '-';
|
|
269
|
+
const opFn = term.sign === 1 ? 'add' : 'subtract';
|
|
270
|
+
|
|
271
|
+
let termNode = term.node;
|
|
272
|
+
|
|
273
|
+
if (!termNode || typeof termNode.updateLayoutUpwards !== 'function') {
|
|
274
|
+
const originalId = termNode.id;
|
|
275
|
+
termNode = termNode.clone();
|
|
276
|
+
termNode.provenance = termNode.provenance || [];
|
|
277
|
+
if (originalId && !termNode.provenance.includes(originalId)) {
|
|
278
|
+
termNode.provenance.push(originalId);
|
|
279
|
+
}
|
|
280
|
+
termNode.setFontSize(fontSize);
|
|
281
|
+
termNode.initialize();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Preserve expansion provenance metadata if it exists
|
|
285
|
+
if (term.expansionProvenance) {
|
|
286
|
+
// Add expansion metadata to the term node for later reference
|
|
287
|
+
termNode.expansionProvenance = termNode.expansionProvenance || [];
|
|
288
|
+
termNode.expansionProvenance.push(term.expansionProvenance);
|
|
289
|
+
|
|
290
|
+
// Ensure all expansion source IDs are in the term's provenance
|
|
291
|
+
[term.expansionProvenance.leftSource, term.expansionProvenance.rightSource, term.expansionProvenance.originalMultiplication].forEach(id => {
|
|
292
|
+
if (id && !termNode.provenance.includes(id)) {
|
|
293
|
+
termNode.provenance.push(id);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Create new binary expression
|
|
299
|
+
const newAST = {
|
|
300
|
+
type: 'OperatorNode',
|
|
301
|
+
op: opSymbol,
|
|
302
|
+
fn: opFn,
|
|
303
|
+
args: [currentTree.toMathJSNode(), termNode.toMathJSNode()],
|
|
304
|
+
clone: function () { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const newNode = new omdBinaryExpressionNode(newAST);
|
|
308
|
+
newNode.setFontSize(fontSize);
|
|
309
|
+
newNode.initialize();
|
|
310
|
+
|
|
311
|
+
// Binary operation nodes should inherit provenance from their operands
|
|
312
|
+
// This allows tracking which terms contributed to the final expression
|
|
313
|
+
const leftProvenance = currentTree.provenance || [];
|
|
314
|
+
const rightProvenance = termNode.provenance || [];
|
|
315
|
+
|
|
316
|
+
// Add provenance from both left and right operands
|
|
317
|
+
[...leftProvenance, ...rightProvenance, currentTree.id, termNode.id].forEach(id => {
|
|
318
|
+
if (id && !newNode.provenance.includes(id)) {
|
|
319
|
+
newNode.provenance.push(id);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Preserve expansion provenance metadata in the binary expression
|
|
324
|
+
if (termNode.expansionProvenance || currentTree.expansionProvenance) {
|
|
325
|
+
newNode.expansionProvenance = newNode.expansionProvenance || [];
|
|
326
|
+
if (termNode.expansionProvenance) {
|
|
327
|
+
newNode.expansionProvenance.push(...termNode.expansionProvenance);
|
|
328
|
+
}
|
|
329
|
+
if (currentTree.expansionProvenance) {
|
|
330
|
+
newNode.expansionProvenance.push(...currentTree.expansionProvenance);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
currentTree = newNode;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return currentTree;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Creates a rational node (fraction) safely with proper initialization
|
|
342
|
+
* @param {number} numerator - The numerator value
|
|
343
|
+
* @param {number} denominator - The denominator value
|
|
344
|
+
* @param {number} fontSize - The font size for the node
|
|
345
|
+
* @returns {omdNode} A properly initialized rational or constant node
|
|
346
|
+
*/
|
|
347
|
+
export function createRationalNode(numerator, denominator, fontSize) {
|
|
348
|
+
// If denominator is 1, just return a constant
|
|
349
|
+
if (denominator === 1) {
|
|
350
|
+
return createConstantNode(numerator, fontSize);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Create AST for the rational node
|
|
354
|
+
const ast = {
|
|
355
|
+
type: 'OperatorNode',
|
|
356
|
+
op: '/',
|
|
357
|
+
fn: 'divide',
|
|
358
|
+
args: [
|
|
359
|
+
{ type: 'ConstantNode', value: Math.abs(numerator), clone: function() { return {...this}; } },
|
|
360
|
+
{ type: 'ConstantNode', value: denominator, clone: function() { return {...this}; } }
|
|
361
|
+
],
|
|
362
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
let node = new omdRationalNode(ast);
|
|
366
|
+
|
|
367
|
+
// Handle negative fractions by wrapping in unary minus
|
|
368
|
+
if (numerator < 0) {
|
|
369
|
+
const unaryAST = {
|
|
370
|
+
type: 'OperatorNode',
|
|
371
|
+
op: '-',
|
|
372
|
+
fn: 'unaryMinus',
|
|
373
|
+
args: [node.toMathJSNode()],
|
|
374
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
375
|
+
};
|
|
376
|
+
node = new omdUnaryExpressionNode(unaryAST);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
node.setFontSize(fontSize);
|
|
380
|
+
node.initialize();
|
|
381
|
+
|
|
382
|
+
return node;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Converts any omdNode into a human-readable string representation.
|
|
387
|
+
* @param {omdNode} node - The node to convert.
|
|
388
|
+
* @returns {string} A string representation of the node.
|
|
389
|
+
*/
|
|
390
|
+
export function nodeToString(node) {
|
|
391
|
+
if (!node) return '';
|
|
392
|
+
|
|
393
|
+
// Operation name to symbol mapping
|
|
394
|
+
const operationSymbols = {
|
|
395
|
+
'add': '+',
|
|
396
|
+
'subtract': '-',
|
|
397
|
+
'multiply': getMultiplicationSymbol(),
|
|
398
|
+
'divide': '÷',
|
|
399
|
+
'unaryMinus': '-',
|
|
400
|
+
'unaryPlus': '+',
|
|
401
|
+
'pow': '^'
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Check for a getValue method (constants) - but only if actually constant
|
|
405
|
+
if (typeof node.getValue === 'function' && node.isConstant && node.isConstant()) {
|
|
406
|
+
return node.getValue().toString();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Check for a name property (variables)
|
|
410
|
+
if (node.name) {
|
|
411
|
+
return node.name;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Handle binary expressions recursively
|
|
415
|
+
if (node.type === 'omdBinaryExpressionNode') {
|
|
416
|
+
const left = nodeToString(node.left);
|
|
417
|
+
const right = nodeToString(node.right);
|
|
418
|
+
// Handle cases where node.op might be null (implicit multiplication)
|
|
419
|
+
let op;
|
|
420
|
+
if (node.op && node.op.opName) {
|
|
421
|
+
op = operationSymbols[node.op.opName] || node.op.opName;
|
|
422
|
+
} else if (node.operation) {
|
|
423
|
+
op = operationSymbols[node.operation] || node.operation;
|
|
424
|
+
} else {
|
|
425
|
+
// For implicit multiplication, use empty string
|
|
426
|
+
op = ''; // This handles cases like "2x" where it's implicit multiplication
|
|
427
|
+
}
|
|
428
|
+
// Only add parentheses for explicit operations, not implicit multiplication
|
|
429
|
+
return op ? `${left} ${op} ${right}` : `${left}${right}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Handle unary expressions
|
|
433
|
+
if (node.type === 'omdUnaryExpressionNode') {
|
|
434
|
+
const arg = nodeToString(node.argument);
|
|
435
|
+
let op;
|
|
436
|
+
if (node.op && node.op.opName) {
|
|
437
|
+
op = operationSymbols[node.op.opName] || node.op.opName;
|
|
438
|
+
} else if (node.operation) {
|
|
439
|
+
op = operationSymbols[node.operation] || node.operation;
|
|
440
|
+
} else {
|
|
441
|
+
op = '-'; // Default for unary
|
|
442
|
+
}
|
|
443
|
+
// Only add parentheses around the argument if it's a complex expression
|
|
444
|
+
if (arg.includes(' ') || arg.includes('+') || arg.includes('-') || arg.includes(getMultiplicationSymbol()) || arg.includes('÷')) {
|
|
445
|
+
return `${op}(${arg})`;
|
|
446
|
+
}
|
|
447
|
+
return `${op}${arg}`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Handle rational nodes
|
|
451
|
+
if (node.type === 'omdRationalNode') {
|
|
452
|
+
const num = nodeToString(node.numerator);
|
|
453
|
+
const den = nodeToString(node.denominator);
|
|
454
|
+
return `(${num}/${den})`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Handle parenthesis nodes
|
|
458
|
+
if (node.type === 'omdParenthesisNode') {
|
|
459
|
+
const content = nodeToString(node.content || node.expression);
|
|
460
|
+
// Only add parentheses if the content doesn't already start and end with them
|
|
461
|
+
if (content.startsWith('(') && content.endsWith(')')) {
|
|
462
|
+
return content;
|
|
463
|
+
}
|
|
464
|
+
return `(${content})`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Handle power nodes
|
|
468
|
+
if (node.type === 'omdPowerNode') {
|
|
469
|
+
const base = nodeToString(node.base);
|
|
470
|
+
const exp = nodeToString(node.exponent);
|
|
471
|
+
return `${base}^${exp}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Handle sqrt nodes
|
|
475
|
+
if (node.type === 'omdSqrtNode') {
|
|
476
|
+
const arg = nodeToString(node.argument);
|
|
477
|
+
return `√(${arg})`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Handle function nodes
|
|
481
|
+
if (node.type === 'omdFunctionNode') {
|
|
482
|
+
const functionName = node.functionName || node.name || 'f';
|
|
483
|
+
if (node.argNodes && node.argNodes.length > 0) {
|
|
484
|
+
const args = node.argNodes.map(arg => nodeToString(arg)).join(', ');
|
|
485
|
+
return `${functionName}(${args})`;
|
|
486
|
+
} else if (node.args && node.args.length > 0) {
|
|
487
|
+
const args = node.args.map(arg => nodeToString(arg)).join(', ');
|
|
488
|
+
return `${functionName}(${args})`;
|
|
489
|
+
}
|
|
490
|
+
return `${functionName}()`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Handle equation nodes
|
|
494
|
+
if (node.type === 'omdEquationNode') {
|
|
495
|
+
const left = nodeToString(node.left);
|
|
496
|
+
const right = nodeToString(node.right);
|
|
497
|
+
return `${left} = ${right}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Fallback for other node types - try toString method first
|
|
501
|
+
if (typeof node.toString === 'function') {
|
|
502
|
+
try {
|
|
503
|
+
return node.toString();
|
|
504
|
+
} catch (e) {
|
|
505
|
+
// Continue to next fallback
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Use math.js as final fallback
|
|
510
|
+
if (node.toMathJSNode) {
|
|
511
|
+
try {
|
|
512
|
+
return math.parse(node.toMathJSNode()).toString();
|
|
513
|
+
} catch (e) {
|
|
514
|
+
// Continue to final fallback
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Use node name if available, otherwise use constructor name as last resort
|
|
519
|
+
return node.name || node.type || '[unknown]';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ===== POLYNOMIAL EXPANSION HELPER FUNCTIONS =====
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Expands a polynomial power using multinomial expansion
|
|
526
|
+
* For example: (a + b)^2 = a^2 + 2ab + b^2
|
|
527
|
+
* @param {Array} terms - Array of {node, sign} objects representing the polynomial terms
|
|
528
|
+
* @param {number} exponent - The power to expand to
|
|
529
|
+
* @param {number} fontSize - Font size for new nodes
|
|
530
|
+
* @returns {Array} Array of {node, sign} objects representing the expanded terms
|
|
531
|
+
*/
|
|
532
|
+
export function expandPolynomialPower(terms, exponent, fontSize) {
|
|
533
|
+
if (exponent === 1) {
|
|
534
|
+
return terms;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (exponent === 2) {
|
|
538
|
+
return expandBinomialSquare(terms, fontSize);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (exponent === 3) {
|
|
542
|
+
return expandBinomialCube(terms, fontSize);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (exponent === 4) {
|
|
546
|
+
return expandBinomialFourth(terms, fontSize);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// For higher powers, use recursive multiplication
|
|
550
|
+
let result = terms;
|
|
551
|
+
for (let i = 1; i < exponent; i++) {
|
|
552
|
+
result = multiplyTermArrays(result, terms, fontSize);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Expands (a + b + ...)^2 using the multinomial theorem
|
|
560
|
+
*/
|
|
561
|
+
export function expandBinomialSquare(terms, fontSize) {
|
|
562
|
+
const expandedTerms = [];
|
|
563
|
+
|
|
564
|
+
// Square terms: a^2, b^2, ...
|
|
565
|
+
for (const term of terms) {
|
|
566
|
+
const squaredNode = SimplificationEngine.createBinaryOp(
|
|
567
|
+
term.node.clone(),
|
|
568
|
+
'multiply',
|
|
569
|
+
term.node.clone(),
|
|
570
|
+
fontSize
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
// Use granular provenance from leaf nodes
|
|
574
|
+
const leafNodes = term.leafNodes || [];
|
|
575
|
+
leafNodes.forEach(leafNode => {
|
|
576
|
+
[squaredNode.left, squaredNode.right, squaredNode].forEach(part => {
|
|
577
|
+
if (part && !part.provenance.includes(leafNode.id)) {
|
|
578
|
+
part.provenance.push(leafNode.id);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
expandedTerms.push({
|
|
584
|
+
node: squaredNode,
|
|
585
|
+
sign: term.sign * term.sign // Always positive since we're squaring
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Cross terms: 2ab, 2ac, 2bc, ...
|
|
590
|
+
for (let i = 0; i < terms.length; i++) {
|
|
591
|
+
for (let j = i + 1; j < terms.length; j++) {
|
|
592
|
+
const term1 = terms[i];
|
|
593
|
+
const term2 = terms[j];
|
|
594
|
+
|
|
595
|
+
// Create 2 * term1 * term2
|
|
596
|
+
const coefficient2 = SimplificationEngine.createConstant(2, fontSize);
|
|
597
|
+
const product1 = SimplificationEngine.createBinaryOp(
|
|
598
|
+
coefficient2,
|
|
599
|
+
'multiply',
|
|
600
|
+
term1.node.clone(),
|
|
601
|
+
fontSize
|
|
602
|
+
);
|
|
603
|
+
const finalProduct = SimplificationEngine.createBinaryOp(
|
|
604
|
+
product1,
|
|
605
|
+
'multiply',
|
|
606
|
+
term2.node.clone(),
|
|
607
|
+
fontSize
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
// Use granular provenance from both terms
|
|
611
|
+
const allLeafNodes = [...(term1.leafNodes || []), ...(term2.leafNodes || [])];
|
|
612
|
+
allLeafNodes.forEach(leafNode => {
|
|
613
|
+
[product1.right, finalProduct.right, finalProduct].forEach(part => {
|
|
614
|
+
if (part && !part.provenance.includes(leafNode.id)) {
|
|
615
|
+
part.provenance.push(leafNode.id);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
expandedTerms.push({
|
|
621
|
+
node: finalProduct,
|
|
622
|
+
sign: term1.sign * term2.sign
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return expandedTerms;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Expands (a + b + ...)^3 for binomial/trinomial cases
|
|
632
|
+
*/
|
|
633
|
+
export function expandBinomialCube(terms, fontSize) {
|
|
634
|
+
if (terms.length === 2) {
|
|
635
|
+
const [term1, term2] = terms;
|
|
636
|
+
const expandedTerms = [];
|
|
637
|
+
|
|
638
|
+
// Use the efficient helper functions with provenance
|
|
639
|
+
expandedTerms.push({
|
|
640
|
+
node: createPowerTermWithProvenance(term1.node, 3, fontSize, term1.leafNodes),
|
|
641
|
+
sign: Math.pow(term1.sign, 3)
|
|
642
|
+
});
|
|
643
|
+
expandedTerms.push({
|
|
644
|
+
node: createCoefficientProductTermWithProvenance(3, term1.node, 2, term2.node, 1, fontSize, term1.leafNodes, term2.leafNodes),
|
|
645
|
+
sign: Math.pow(term1.sign, 2) * term2.sign
|
|
646
|
+
});
|
|
647
|
+
expandedTerms.push({
|
|
648
|
+
node: createCoefficientProductTermWithProvenance(3, term1.node, 1, term2.node, 2, fontSize, term1.leafNodes, term2.leafNodes),
|
|
649
|
+
sign: term1.sign * Math.pow(term2.sign, 2)
|
|
650
|
+
});
|
|
651
|
+
expandedTerms.push({
|
|
652
|
+
node: createPowerTermWithProvenance(term2.node, 3, fontSize, term2.leafNodes),
|
|
653
|
+
sign: Math.pow(term2.sign, 3)
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
return expandedTerms;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// For more than 2 terms, use general multiplication
|
|
660
|
+
return multiplyTermArrays(expandBinomialSquare(terms, fontSize), terms, fontSize);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Expands (a + b)^4 = a^4 + 4a^3b + 6a^2b^2 + 4ab^3 + b^4
|
|
665
|
+
*/
|
|
666
|
+
export function expandBinomialFourth(terms, fontSize) {
|
|
667
|
+
if (terms.length === 2) {
|
|
668
|
+
const [term1, term2] = terms;
|
|
669
|
+
const expandedTerms = [];
|
|
670
|
+
|
|
671
|
+
// Use the efficient helper functions with provenance
|
|
672
|
+
expandedTerms.push({
|
|
673
|
+
node: createPowerTermWithProvenance(term1.node, 4, fontSize, term1.leafNodes),
|
|
674
|
+
sign: Math.pow(term1.sign, 4)
|
|
675
|
+
});
|
|
676
|
+
expandedTerms.push({
|
|
677
|
+
node: createCoefficientProductTermWithProvenance(4, term1.node, 3, term2.node, 1, fontSize, term1.leafNodes, term2.leafNodes),
|
|
678
|
+
sign: Math.pow(term1.sign, 3) * term2.sign
|
|
679
|
+
});
|
|
680
|
+
expandedTerms.push({
|
|
681
|
+
node: createCoefficientProductTermWithProvenance(6, term1.node, 2, term2.node, 2, fontSize, term1.leafNodes, term2.leafNodes),
|
|
682
|
+
sign: Math.pow(term1.sign, 2) * Math.pow(term2.sign, 2)
|
|
683
|
+
});
|
|
684
|
+
expandedTerms.push({
|
|
685
|
+
node: createCoefficientProductTermWithProvenance(4, term1.node, 1, term2.node, 3, fontSize, term1.leafNodes, term2.leafNodes),
|
|
686
|
+
sign: term1.sign * Math.pow(term2.sign, 3)
|
|
687
|
+
});
|
|
688
|
+
expandedTerms.push({
|
|
689
|
+
node: createPowerTermWithProvenance(term2.node, 4, fontSize, term2.leafNodes),
|
|
690
|
+
sign: Math.pow(term2.sign, 4)
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
return expandedTerms;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// For more than 2 terms, use general multiplication
|
|
697
|
+
const cubed = expandBinomialCube(terms, fontSize);
|
|
698
|
+
return multiplyTermArrays(cubed, terms, fontSize);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Creates a term like x^n with granular provenance tracking
|
|
703
|
+
*/
|
|
704
|
+
export function createPowerTermWithProvenance(baseNode, power, fontSize, leafNodes) {
|
|
705
|
+
if (power === 1) {
|
|
706
|
+
const cloned = baseNode.clone();
|
|
707
|
+
// Add leaf node provenance
|
|
708
|
+
if (leafNodes && leafNodes.length > 0) {
|
|
709
|
+
leafNodes.forEach(leafNode => {
|
|
710
|
+
if (!cloned.provenance.includes(leafNode.id)) {
|
|
711
|
+
cloned.provenance.push(leafNode.id);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return cloned;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const powerConstant = SimplificationEngine.createConstant(power, fontSize);
|
|
719
|
+
|
|
720
|
+
// Create power node AST structure
|
|
721
|
+
const powerAST = {
|
|
722
|
+
type: 'OperatorNode',
|
|
723
|
+
op: '^',
|
|
724
|
+
fn: 'pow',
|
|
725
|
+
args: [baseNode.toMathJSNode(), powerConstant.toMathJSNode()],
|
|
726
|
+
clone: function() {
|
|
727
|
+
return {
|
|
728
|
+
...this,
|
|
729
|
+
args: this.args.map(arg => arg.clone())
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const powerNode = new omdPowerNode(powerAST);
|
|
735
|
+
powerNode.setFontSize(fontSize);
|
|
736
|
+
powerNode.initialize();
|
|
737
|
+
|
|
738
|
+
// Add leaf node provenance
|
|
739
|
+
if (leafNodes && leafNodes.length > 0) {
|
|
740
|
+
leafNodes.forEach(leafNode => {
|
|
741
|
+
if (!powerNode.base.provenance.includes(leafNode.id)) {
|
|
742
|
+
powerNode.base.provenance.push(leafNode.id);
|
|
743
|
+
}
|
|
744
|
+
if (!powerNode.provenance.includes(leafNode.id)) {
|
|
745
|
+
powerNode.provenance.push(leafNode.id);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
} else {
|
|
749
|
+
powerNode.base.provenance.push(baseNode.id);
|
|
750
|
+
powerNode.provenance.push(baseNode.id);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return powerNode;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Creates a term like c * a^p1 * b^p2 with granular provenance tracking
|
|
758
|
+
*/
|
|
759
|
+
export function createCoefficientProductTermWithProvenance(coefficient, node1, power1, node2, power2, fontSize, leafNodes1, leafNodes2) {
|
|
760
|
+
let result = SimplificationEngine.createConstant(coefficient, fontSize);
|
|
761
|
+
|
|
762
|
+
// Multiply by node1^power1
|
|
763
|
+
if (power1 > 0) {
|
|
764
|
+
const term1 = createPowerTermWithProvenance(node1, power1, fontSize, leafNodes1);
|
|
765
|
+
result = SimplificationEngine.createBinaryOp(result, 'multiply', term1, fontSize);
|
|
766
|
+
|
|
767
|
+
// Add leaf node provenance to the result
|
|
768
|
+
if (leafNodes1 && leafNodes1.length > 0) {
|
|
769
|
+
leafNodes1.forEach(leafNode => {
|
|
770
|
+
if (!result.provenance.includes(leafNode.id)) {
|
|
771
|
+
result.provenance.push(leafNode.id);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Multiply by node2^power2
|
|
778
|
+
if (power2 > 0) {
|
|
779
|
+
const term2 = createPowerTermWithProvenance(node2, power2, fontSize, leafNodes2);
|
|
780
|
+
result = SimplificationEngine.createBinaryOp(result, 'multiply', term2, fontSize);
|
|
781
|
+
|
|
782
|
+
// Add leaf node provenance to the result
|
|
783
|
+
if (leafNodes2 && leafNodes2.length > 0) {
|
|
784
|
+
leafNodes2.forEach(leafNode => {
|
|
785
|
+
if (!result.provenance.includes(leafNode.id)) {
|
|
786
|
+
result.provenance.push(leafNode.id);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return result;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Creates a term like x^n
|
|
797
|
+
*/
|
|
798
|
+
export function createPowerTerm(baseNode, power, fontSize) {
|
|
799
|
+
if (power === 1) {
|
|
800
|
+
return baseNode.clone();
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const powerConstant = SimplificationEngine.createConstant(power, fontSize);
|
|
804
|
+
|
|
805
|
+
// Create power node AST structure
|
|
806
|
+
const powerAST = {
|
|
807
|
+
type: 'OperatorNode',
|
|
808
|
+
op: '^',
|
|
809
|
+
fn: 'pow',
|
|
810
|
+
args: [baseNode.toMathJSNode(), powerConstant.toMathJSNode()],
|
|
811
|
+
clone: function() {
|
|
812
|
+
return {
|
|
813
|
+
...this,
|
|
814
|
+
args: this.args.map(arg => arg.clone())
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
const powerNode = new omdPowerNode(powerAST);
|
|
820
|
+
powerNode.setFontSize(fontSize);
|
|
821
|
+
powerNode.initialize();
|
|
822
|
+
|
|
823
|
+
return powerNode;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Creates a term like c * a^p1 * b^p2
|
|
828
|
+
*/
|
|
829
|
+
export function createCoefficientProductTerm(coefficient, node1, power1, node2, power2, fontSize) {
|
|
830
|
+
let result = SimplificationEngine.createConstant(coefficient, fontSize);
|
|
831
|
+
|
|
832
|
+
// Multiply by node1^power1
|
|
833
|
+
if (power1 > 0) {
|
|
834
|
+
const term1 = createPowerTerm(node1, power1, fontSize);
|
|
835
|
+
result = SimplificationEngine.createBinaryOp(result, 'multiply', term1, fontSize);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Multiply by node2^power2
|
|
839
|
+
if (power2 > 0) {
|
|
840
|
+
const term2 = createPowerTerm(node2, power2, fontSize);
|
|
841
|
+
result = SimplificationEngine.createBinaryOp(result, 'multiply', term2, fontSize);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Multiplies two arrays of terms (for general polynomial multiplication)
|
|
849
|
+
*/
|
|
850
|
+
export function multiplyTermArrays(terms1, terms2, fontSize) {
|
|
851
|
+
const result = [];
|
|
852
|
+
|
|
853
|
+
for (const term1 of terms1) {
|
|
854
|
+
for (const term2 of terms2) {
|
|
855
|
+
const productNode = SimplificationEngine.createBinaryOp(
|
|
856
|
+
term1.node.clone(),
|
|
857
|
+
'multiply',
|
|
858
|
+
term2.node.clone(),
|
|
859
|
+
fontSize
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
// Add granular provenance from leaf nodes of both terms
|
|
863
|
+
const allLeafNodes = [...(term1.leafNodes || []), ...(term2.leafNodes || [])];
|
|
864
|
+
allLeafNodes.forEach(leafNode => {
|
|
865
|
+
[productNode.left, productNode.right, productNode].forEach(part => {
|
|
866
|
+
if (part && !part.provenance.includes(leafNode.id)) {
|
|
867
|
+
part.provenance.push(leafNode.id);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
result.push({
|
|
873
|
+
node: productNode,
|
|
874
|
+
sign: term1.sign * term2.sign,
|
|
875
|
+
leafNodes: allLeafNodes
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Creates a monomial with granular provenance tracking for coefficients and variables separately
|
|
885
|
+
*/
|
|
886
|
+
export function createMonomialWithGranularProvenance(coefficient, variable, power, fontSize, coefficientProvenance = [], variableProvenance = []) {
|
|
887
|
+
// Create variable node
|
|
888
|
+
const variableAST = { type: 'SymbolNode', name: variable, clone: function() { return {...this}; } };
|
|
889
|
+
const variableNode = new (SimplificationEngine.getNodeClass('omdVariableNode'))(variableAST);
|
|
890
|
+
variableNode.setFontSize(fontSize);
|
|
891
|
+
variableNode.initialize();
|
|
892
|
+
|
|
893
|
+
// Set variable-specific provenance
|
|
894
|
+
if (variableProvenance && variableProvenance.length > 0) {
|
|
895
|
+
variableNode.provenance = [...variableProvenance];
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
let termNode = variableNode;
|
|
899
|
+
|
|
900
|
+
// Add power if not 1
|
|
901
|
+
if (power !== 1) {
|
|
902
|
+
const powerAST = {
|
|
903
|
+
type: 'OperatorNode',
|
|
904
|
+
op: '^',
|
|
905
|
+
fn: 'pow',
|
|
906
|
+
args: [variableAST, { type: 'ConstantNode', value: power, clone: function() { return {...this}; } }],
|
|
907
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
908
|
+
};
|
|
909
|
+
termNode = new (SimplificationEngine.getNodeClass('omdPowerNode'))(powerAST);
|
|
910
|
+
termNode.setFontSize(fontSize);
|
|
911
|
+
termNode.initialize();
|
|
912
|
+
|
|
913
|
+
// Set variable provenance on the base, power gets no special provenance
|
|
914
|
+
if (termNode.base && variableProvenance && variableProvenance.length > 0) {
|
|
915
|
+
termNode.base.provenance = [...variableProvenance];
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
let result;
|
|
920
|
+
if (coefficient === 1) {
|
|
921
|
+
result = termNode;
|
|
922
|
+
} else if (coefficient === -1) {
|
|
923
|
+
// Create unary minus
|
|
924
|
+
const unaryAST = {
|
|
925
|
+
type: 'OperatorNode',
|
|
926
|
+
op: '-',
|
|
927
|
+
fn: 'unaryMinus',
|
|
928
|
+
args: [termNode.toMathJSNode()],
|
|
929
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
930
|
+
};
|
|
931
|
+
result = new (SimplificationEngine.getNodeClass('omdUnaryExpressionNode'))(unaryAST);
|
|
932
|
+
result.setFontSize(fontSize);
|
|
933
|
+
result.initialize();
|
|
934
|
+
|
|
935
|
+
// The argument preserves variable provenance
|
|
936
|
+
if (result.argument && variableProvenance && variableProvenance.length > 0) {
|
|
937
|
+
result.argument.provenance = [...variableProvenance];
|
|
938
|
+
}
|
|
939
|
+
} else {
|
|
940
|
+
// Create coefficient * term
|
|
941
|
+
const coeffNode = SimplificationEngine.createConstant(Math.abs(coefficient), fontSize);
|
|
942
|
+
|
|
943
|
+
// Set coefficient-specific provenance
|
|
944
|
+
if (coefficientProvenance && coefficientProvenance.length > 0) {
|
|
945
|
+
coeffNode.provenance = [...coefficientProvenance];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const multiplicationNode = SimplificationEngine.createBinaryOp(coeffNode, 'multiply', termNode, fontSize);
|
|
949
|
+
|
|
950
|
+
// Apply granular provenance: coefficient to left, variable to right
|
|
951
|
+
if (multiplicationNode.left && coefficientProvenance && coefficientProvenance.length > 0) {
|
|
952
|
+
multiplicationNode.left.provenance = [...coefficientProvenance];
|
|
953
|
+
}
|
|
954
|
+
if (multiplicationNode.right && variableProvenance && variableProvenance.length > 0) {
|
|
955
|
+
multiplicationNode.right.provenance = [...variableProvenance];
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Wrap in unary minus if coefficient is negative
|
|
959
|
+
if (coefficient < 0) {
|
|
960
|
+
const unaryAST = {
|
|
961
|
+
type: 'OperatorNode',
|
|
962
|
+
op: '-',
|
|
963
|
+
fn: 'unaryMinus',
|
|
964
|
+
args: [multiplicationNode.toMathJSNode()],
|
|
965
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
966
|
+
};
|
|
967
|
+
result = new (SimplificationEngine.getNodeClass('omdUnaryExpressionNode'))(unaryAST);
|
|
968
|
+
result.setFontSize(fontSize);
|
|
969
|
+
result.initialize();
|
|
970
|
+
|
|
971
|
+
// Preserve granular provenance within the unary minus
|
|
972
|
+
if (result.argument) {
|
|
973
|
+
if (result.argument.left && coefficientProvenance && coefficientProvenance.length > 0) {
|
|
974
|
+
result.argument.left.provenance = [...coefficientProvenance];
|
|
975
|
+
}
|
|
976
|
+
if (result.argument.right && variableProvenance && variableProvenance.length > 0) {
|
|
977
|
+
result.argument.right.provenance = [...variableProvenance];
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
} else {
|
|
981
|
+
result = multiplicationNode;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Extracts coefficient and variable leaf nodes separately from a monomial term
|
|
990
|
+
* for granular provenance tracking in like term combination
|
|
991
|
+
*/
|
|
992
|
+
export function extractMonomialProvenance(termNode) {
|
|
993
|
+
const coefficientNodes = [];
|
|
994
|
+
const variableNodes = [];
|
|
995
|
+
|
|
996
|
+
function traverse(node, isInCoefficient = false) {
|
|
997
|
+
if (!node) return;
|
|
998
|
+
|
|
999
|
+
// If we find a variable node, it goes to variables
|
|
1000
|
+
if (node.type === 'omdVariableNode') {
|
|
1001
|
+
variableNodes.push(node);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// If we find a constant node
|
|
1006
|
+
if (node.type === 'omdConstantNode') {
|
|
1007
|
+
// If we're in a power expression, this is likely an exponent, not a coefficient
|
|
1008
|
+
if (node.parent && node.parent.type === 'omdPowerNode' && node.parent.exponent === node) {
|
|
1009
|
+
// Skip exponents for provenance purposes
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
// Otherwise, it's a coefficient
|
|
1013
|
+
coefficientNodes.push(node);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// For power nodes, traverse base for variables
|
|
1018
|
+
if (node.type === 'omdPowerNode') {
|
|
1019
|
+
if (node.base) traverse(node.base, false);
|
|
1020
|
+
// Don't traverse exponent for provenance
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// For binary operations, determine what we're looking at
|
|
1025
|
+
if (node.type === 'omdBinaryExpressionNode') {
|
|
1026
|
+
if (node.operation === 'multiply') {
|
|
1027
|
+
// In multiplication, left is often coefficient, right is often variable
|
|
1028
|
+
if (node.left) traverse(node.left, true);
|
|
1029
|
+
if (node.right) traverse(node.right, false);
|
|
1030
|
+
} else {
|
|
1031
|
+
// For other operations, traverse both sides
|
|
1032
|
+
if (node.left) traverse(node.left, isInCoefficient);
|
|
1033
|
+
if (node.right) traverse(node.right, isInCoefficient);
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// For unary operations, traverse the argument
|
|
1039
|
+
if (node.type === 'omdUnaryExpressionNode') {
|
|
1040
|
+
if (node.argument) traverse(node.argument, isInCoefficient);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// For other node types, try to traverse child properties
|
|
1045
|
+
['left', 'right', 'base', 'exponent', 'argument', 'expression', 'numerator', 'denominator'].forEach(prop => {
|
|
1046
|
+
if (node[prop]) traverse(node[prop], isInCoefficient);
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
traverse(termNode);
|
|
1051
|
+
|
|
1052
|
+
return {
|
|
1053
|
+
coefficientNodes,
|
|
1054
|
+
variableNodes
|
|
1055
|
+
};
|
|
1056
1056
|
}
|