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