@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,888 +1,888 @@
1
- import { omdRationalNode } from "../nodes/omdRationalNode.js";
2
- import { omdBinaryExpressionNode } from "../nodes/omdBinaryExpressionNode.js";
3
- import { omdUnaryExpressionNode } from "../nodes/omdUnaryExpressionNode.js";
4
- import { omdVariableNode } from "../nodes/omdVariableNode.js";
5
- import { omdPowerNode } from "../nodes/omdPowerNode.js";
6
- import { omdConstantNode } from "../nodes/omdConstantNode.js";
7
- import { createConstantNode, applyProvenance } from './simplificationUtils.js';
8
- import * as utils from './simplificationUtils.js';
9
- import { useImplicitMultiplication, getMultiplicationSymbol } from "../config/omdConfigManager.js";
10
-
11
- /**
12
- * @class SimplificationEngine
13
- * @classdesc Provides a collection of static methods for creating, matching, and transforming
14
- * mathematical expression nodes for simplification purposes.
15
- * This class serves as the core logic for applying simplification rules within the OMD system.
16
- */
17
- export class SimplificationEngine {
18
-
19
- /**
20
- * ===== SIMPLIFIED NODE CREATION HELPERS =====
21
- * These functions provide an easy way to create various types of AST nodes
22
- * without directly dealing with the complexities of the Math.js AST structure.
23
- */
24
-
25
- /**
26
- * Creates a new constant node.
27
- * @param {number} value - The numeric value of the constant.
28
- * @param {number} [fontSize=16] - The font size for the node.
29
- * @param {...omdNode} sourceNodes - Source nodes for automatic provenance tracking.
30
- * @returns {Object} A new constant node.
31
- */
32
- static createConstant(value, fontSize = 16, ...sourceNodes) {
33
- const node = createConstantNode(value, fontSize);
34
-
35
- // Apply manual provenance tracking if source nodes provided
36
- if (sourceNodes.length > 0) {
37
- applyProvenance(node, ...sourceNodes);
38
- }
39
-
40
- return node;
41
- }
42
-
43
- /**
44
- * Creates a new binary expression node.
45
- * @param {Object} left - The left-hand side operand node.
46
- * @param {string} operator - The operator (e.g., '+', '-', '*', '/').
47
- * @param {Object} right - The right-hand side operand node.
48
- * @param {number} [fontSize=16] - The font size for the node.
49
- * @param {Array|null} [provenance=null] - The provenance array for the node (deprecated - use ...sourceNodes instead).
50
- * @param {omdNode|null} [operatorNode=null] - The original operator node for provenance (deprecated - use ...sourceNodes instead).
51
- * @param {...omdNode} sourceNodes - Additional source nodes for automatic provenance tracking.
52
- * @returns {Object} A new binary expression node.
53
- * @throws {Error} If an unknown operator is provided.
54
- */
55
- static createBinaryOp(left, operator, right, fontSize = 16, provenance = null, operatorNode = null, ...sourceNodes) {
56
- const opMap = {
57
- 'add': { op: '+', fn: 'add' },
58
- 'subtract': { op: '−', fn: 'subtract' },
59
- 'multiply': { op: getMultiplicationSymbol(), fn: 'multiply' },
60
- 'divide': { op: '÷', fn: 'divide' }
61
- };
62
-
63
- const opInfo = opMap[operator];
64
- if (!opInfo) throw new Error(`Unknown operator: ${operator}`);
65
-
66
- // For multiplication, reorder operands if needed (e.g., x*2 -> 2*x)
67
- let leftOperand = left;
68
- let rightOperand = right;
69
-
70
- if (operator === 'multiply') {
71
- const leftIsConstant = (left instanceof omdConstantNode && left.isConstant());
72
- const rightIsConstant = (right instanceof omdConstantNode && right.isConstant());
73
-
74
- // If left is non-constant and right is constant, swap them
75
- if (!leftIsConstant && rightIsConstant) {
76
- leftOperand = right;
77
- rightOperand = left;
78
- }
79
- }
80
-
81
- const ast = {
82
- type: 'OperatorNode',
83
- op: opInfo.op,
84
- fn: opInfo.fn,
85
- args: [leftOperand.toMathJSNode(), rightOperand.toMathJSNode()],
86
- clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
87
- };
88
-
89
- // Pass operator provenance into the constructor via the AST
90
- if (operatorNode) {
91
- ast.operatorProvenance = [operatorNode.id];
92
- }
93
-
94
- const node = new omdBinaryExpressionNode(ast);
95
- node.setFontSize(fontSize);
96
-
97
- // Use manual provenance tracking
98
- if (provenance) {
99
- // Legacy support: apply manual provenance
100
- node.provenance = provenance;
101
- } else {
102
- // Manual provenance tracking for binary operations
103
- const allSources = [leftOperand, rightOperand];
104
- if (operatorNode) allSources.push(operatorNode);
105
- allSources.push(...sourceNodes);
106
- applyProvenance(node, ...allSources);
107
- }
108
-
109
- node.initialize();
110
- return node;
111
- }
112
-
113
- /**
114
- * Creates a multiplication expression where the left operand is a constant value.
115
- * @param {Object} leftConstantNode - The constant node for the left operand.
116
- * @param {Object} rightNode - The node for the right operand.
117
- * @param {number} [fontSize=16] - The font size for the node.
118
- * @param {Array|null} [provenance=null] - The provenance array for the node.
119
- * @returns {Object} A new binary expression node representing multiplication.
120
- */
121
- static createMultiplication(leftConstantNode, rightNode, fontSize = 16, provenance = null) {
122
- // Clone the constant to create a new instance for the new expression,
123
- // but ensure its provenance points back to the original constant.
124
- const newLeftConstant = leftConstantNode.clone();
125
- newLeftConstant.provenance = [leftConstantNode.id];
126
-
127
- const rightClone = rightNode.clone(); // Clone the other node to ensure we don't modify the original
128
-
129
- // Safely get the operator node for provenance, handling cases where parent might be null
130
- const operatorNode = leftConstantNode.parent?.op || null;
131
-
132
- return SimplificationEngine.createBinaryOp(newLeftConstant, 'multiply', rightClone, fontSize, provenance, operatorNode);
133
- }
134
-
135
- /**
136
- * Creates a rational node (fraction).
137
- * @param {number} numerator - The numerator value.
138
- * @param {number} denominator - The denominator value.
139
- * @param {number} [fontSize=16] - The font size for the node.
140
- * @param {...omdNode} sourceNodes - Source nodes for automatic provenance tracking.
141
- * @returns {Object} A new rational node, or a constant node if the denominator is 1.
142
- */
143
- static createRational(numerator, denominator, fontSize = 16, ...sourceNodes) {
144
- if (denominator === 1) {
145
- return SimplificationEngine.createConstant(numerator, fontSize, ...sourceNodes);
146
- }
147
-
148
- const ast = {
149
- type: 'OperatorNode', op: '/', fn: 'divide',
150
- args: [
151
- { type: 'ConstantNode', value: Math.abs(numerator) },
152
- { type: 'ConstantNode', value: denominator }
153
- ],
154
- clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
155
- };
156
-
157
- let node = new omdRationalNode(ast);
158
-
159
- // Handle negative fractions by wrapping in a unary minus node
160
- if (numerator < 0) {
161
- const unaryAST = {
162
- type: 'OperatorNode', op: '-', fn: 'unaryMinus',
163
- args: [node.toMathJSNode()],
164
- clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
165
- };
166
- node = new omdUnaryExpressionNode(unaryAST);
167
- }
168
-
169
- node.setFontSize(fontSize);
170
-
171
- // Apply manual provenance tracking if source nodes provided
172
- if (sourceNodes.length > 0) {
173
- applyProvenance(node, ...sourceNodes);
174
- }
175
-
176
- node.initialize();
177
- return node;
178
- }
179
-
180
- /**
181
- * ===== SIMPLIFIED PATTERN MATCHING HELPERS =====
182
- * These functions provide convenient ways to check common patterns in AST nodes.
183
- */
184
-
185
- /**
186
- * Checks if a node is of a specific type.
187
- * @param {Object} node - The node to check.
188
- * @param {string} typeName - The name of the constructor type (e.g., 'omdConstantNode').
189
- * @returns {boolean} True if the node matches the type, false otherwise.
190
- */
191
- static isType(node, typeName) {
192
- if (!node) return false;
193
- return node.type === typeName;
194
- }
195
-
196
- /**
197
- * Checks if a node is a binary operation, optionally with a specific operator.
198
- * @param {Object} node - The node to check.
199
- * @param {string} [operator=null] - The specific operator to check for (e.g., 'add', 'multiply').
200
- * @returns {boolean} True if the node is a binary operation (and matches operator if provided), false otherwise.
201
- */
202
- static isBinaryOp(node, operator = null) {
203
- if (!SimplificationEngine.isType(node, 'omdBinaryExpressionNode')) return false;
204
-
205
- if (!operator) return true;
206
-
207
- // Handle both string operations and object operations (from Math.js AST)
208
- let nodeOp = node.operation;
209
- if (typeof nodeOp === 'object' && nodeOp.name) {
210
- nodeOp = nodeOp.name;
211
- }
212
-
213
- return nodeOp === operator;
214
- }
215
-
216
- /**
217
- * Checks if a node is a constant, optionally with a specific value.
218
- * @param {Object} node - The node to check.
219
- * @param {number} [value=null] - The specific constant value to check for.
220
- * @returns {boolean} True if the node is a constant (and matches value if provided), false otherwise.
221
- */
222
- static isConstantValue(node, value = null) {
223
- if (!node.isConstant()) return false;
224
- return value !== null ? node.getValue() === value : true;
225
- }
226
-
227
- /**
228
- * Checks if a binary operation node has a constant operand and returns it along with the other operand.
229
- * @param {Object} node - The binary expression node to check.
230
- * @returns {Object|null} An object containing the constant and other operand, or null if no constant operand is found.
231
- */
232
- static hasConstantOperand(node) {
233
- if (!SimplificationEngine.isBinaryOp(node)) return null;
234
-
235
- if (node.left.isConstant()) return { constant: node.left, other: node.right };
236
- if (node.right.isConstant()) return { constant: node.right, other: node.left };
237
- return null;
238
- }
239
-
240
- /**
241
- * Unwraps a parenthesis node, returning its inner expression. If the node is not a parenthesis node, it returns the node itself.
242
- * @param {Object} node - The node to unwrap.
243
- * @returns {Object} The inner expression if the node is a parenthesis node, otherwise the original node.
244
- */
245
- static unwrapParentheses(node) {
246
- return SimplificationEngine.isType(node, 'omdParenthesisNode') ? node.expression : node;
247
- }
248
-
249
- /**
250
- * ===== MONOMIAL AND LIKE TERMS HELPERS =====
251
- * These functions help identify and work with monomials and like terms.
252
- */
253
-
254
- /**
255
- * Checks if a node represents a monomial (a term of the form coefficient * variable^power).
256
- * Examples: x, 2x, 3y, -5z, x^2, 2x^3
257
- * @param {Object} node - The node to check.
258
- * @returns {Object|null} An object with coefficient, variable, and power if it's a monomial, null otherwise.
259
- */
260
- static isMonomial(node) {
261
- let coefficientMultiplier = 1;
262
-
263
- // Unwrap parentheses first, as they can contain a unary minus
264
- node = SimplificationEngine.unwrapParentheses(node);
265
-
266
- // Handle any number of nested unary minuses by repeatedly unwrapping them
267
- // and flipping the coefficient multiplier.
268
- while (SimplificationEngine.isType(node, 'omdUnaryExpressionNode') && node.operation === 'unaryMinus') {
269
- node = node.argument;
270
- coefficientMultiplier *= -1;
271
- // The argument of the unary minus could also be in parentheses
272
- node = SimplificationEngine.unwrapParentheses(node);
273
- }
274
-
275
- // Case 1: Just a variable (e.g., x)
276
- if (SimplificationEngine.isType(node, 'omdVariableNode')) {
277
- return {
278
- coefficient: 1 * coefficientMultiplier,
279
- variable: node.name,
280
- power: 1,
281
- variableNode: node
282
- };
283
- }
284
-
285
- // Case 2: Constant * Variable (e.g., 2x, 3y)
286
- if (SimplificationEngine.isBinaryOp(node, 'multiply')) {
287
- const constOp = SimplificationEngine.hasConstantOperand(node);
288
- if (constOp && SimplificationEngine.isType(constOp.other, 'omdVariableNode')) {
289
- return {
290
- coefficient: constOp.constant.getValue() * coefficientMultiplier,
291
- variable: constOp.other.name,
292
- power: 1,
293
- variableNode: constOp.other,
294
- coefficientNode: constOp.constant
295
- };
296
- }
297
-
298
- // Case 3: Variable * Constant (e.g., x*2, y*3) - less common but possible
299
- if (constOp && SimplificationEngine.isType(constOp.constant, 'omdVariableNode') && constOp.other.isConstant()) {
300
- return {
301
- coefficient: constOp.other.getValue() * coefficientMultiplier,
302
- variable: constOp.constant.name,
303
- power: 1,
304
- variableNode: constOp.constant,
305
- coefficientNode: constOp.other
306
- };
307
- }
308
-
309
- // Case 4: Constant * Power (e.g., 2*x^3)
310
- const constPowerOp = SimplificationEngine.hasConstantOperand(node);
311
- if (constPowerOp && SimplificationEngine.isType(constPowerOp.other, 'omdPowerNode')) {
312
- const powerNode = constPowerOp.other;
313
- if (SimplificationEngine.isType(powerNode.base, 'omdVariableNode') && powerNode.exponent.isConstant()) {
314
- return {
315
- coefficient: constPowerOp.constant.getValue() * coefficientMultiplier,
316
- variable: powerNode.base.name,
317
- power: powerNode.exponent.getValue(),
318
- variableNode: powerNode.base,
319
- coefficientNode: constPowerOp.constant,
320
- powerNode: powerNode
321
- };
322
- }
323
- }
324
- }
325
-
326
- // Case 5: Just a power (e.g., x^2)
327
- if (SimplificationEngine.isType(node, 'omdPowerNode')) {
328
- if (SimplificationEngine.isType(node.base, 'omdVariableNode') && node.exponent.isConstant()) {
329
- return {
330
- coefficient: 1 * coefficientMultiplier,
331
- variable: node.base.name,
332
- power: node.exponent.getValue(),
333
- variableNode: node.base,
334
- powerNode: node
335
- };
336
- }
337
- }
338
-
339
- return null;
340
- }
341
-
342
- /**
343
- * Checks if two monomials are like terms (same variable and power).
344
- * @param {Object} monomial1 - First monomial info from isMonomial().
345
- * @param {Object} monomial2 - Second monomial info from isMonomial().
346
- * @returns {boolean} True if they are like terms, false otherwise.
347
- */
348
- static areLikeTerms(monomial1, monomial2) {
349
- return monomial1.variable === monomial2.variable && monomial1.power === monomial2.power;
350
- }
351
-
352
- /**
353
- * Creates a monomial node from coefficient, variable, and power.
354
- * @param {number} coefficient - The coefficient of the monomial.
355
- * @param {string} variable - The variable name.
356
- * @param {number} power - The power of the variable.
357
- * @param {number} fontSize - The font size for the node.
358
- * @param {Array} [provenance=[]] - The provenance array to preserve lineage.
359
- * @returns {Object} A new monomial node.
360
- */
361
- static createMonomial(coefficient, variable, power, fontSize, provenance = []) {
362
- // Create variable node
363
- const variableAST = { type: 'SymbolNode', name: variable, clone: function() { return {...this}; } };
364
- const variableNode = new (SimplificationEngine.getNodeClass('omdVariableNode'))(variableAST);
365
- variableNode.setFontSize(fontSize);
366
- variableNode.initialize();
367
-
368
- // CRITICAL: Set provenance on the variable node
369
- if (provenance && provenance.length > 0) {
370
- variableNode.provenance = [...provenance];
371
- }
372
-
373
- let termNode = variableNode;
374
-
375
- // Add power if not 1
376
- if (power !== 1) {
377
- const powerAST = {
378
- type: 'OperatorNode',
379
- op: '^',
380
- fn: 'pow',
381
- args: [variableAST, { type: 'ConstantNode', value: power, clone: function() { return {...this}; } }],
382
- clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
383
- };
384
- termNode = new (SimplificationEngine.getNodeClass('omdPowerNode'))(powerAST);
385
- termNode.setFontSize(fontSize);
386
- termNode.initialize();
387
-
388
- // CRITICAL: Set provenance on the power node and its components
389
- if (provenance && provenance.length > 0) {
390
- termNode.provenance = [...provenance];
391
- // Set provenance on the base (variable) and exponent
392
- if (termNode.base) {
393
- termNode.base.provenance = [...provenance];
394
- }
395
- if (termNode.exponent) {
396
- termNode.exponent.provenance = [...provenance];
397
- }
398
- }
399
- }
400
-
401
- let result;
402
- if (coefficient === 1) {
403
- result = termNode;
404
- } else if (coefficient === -1) {
405
- // Create unary minus
406
- const unaryAST = {
407
- type: 'OperatorNode',
408
- op: '-',
409
- fn: 'unaryMinus',
410
- args: [termNode.toMathJSNode()],
411
- clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
412
- };
413
- result = new omdUnaryExpressionNode(unaryAST);
414
- result.setFontSize(fontSize);
415
- result.initialize();
416
-
417
- // CRITICAL: Set provenance on unary minus and its argument
418
- if (provenance && provenance.length > 0) {
419
- result.provenance = [...provenance];
420
- if (result.argument) {
421
- result.argument.provenance = [...provenance];
422
- }
423
- }
424
- } else {
425
- // Create coefficient * term
426
- const coeffNode = SimplificationEngine.createConstant(Math.abs(coefficient), fontSize);
427
-
428
- // CRITICAL: Set provenance on the coefficient node
429
- if (provenance && provenance.length > 0) {
430
- coeffNode.provenance = [...provenance];
431
- }
432
-
433
- const multiplicationNode = SimplificationEngine.createBinaryOp(coeffNode, 'multiply', termNode, fontSize);
434
-
435
- // CRITICAL: Set provenance on the multiplication node
436
- if (provenance && provenance.length > 0) {
437
- multiplicationNode.provenance = [...provenance];
438
- // Ensure left (coefficient) and right (variable/power) have provenance
439
- if (multiplicationNode.left) {
440
- multiplicationNode.left.provenance = [...provenance];
441
- }
442
- if (multiplicationNode.right) {
443
- multiplicationNode.right.provenance = [...provenance];
444
- }
445
- }
446
-
447
- // Wrap in unary minus if coefficient is negative
448
- if (coefficient < 0) {
449
- const unaryAST = {
450
- type: 'OperatorNode',
451
- op: '-',
452
- fn: 'unaryMinus',
453
- args: [multiplicationNode.toMathJSNode()],
454
- clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
455
- };
456
- result = new omdUnaryExpressionNode(unaryAST);
457
- result.setFontSize(fontSize);
458
- result.initialize();
459
-
460
- // CRITICAL: Set provenance on negative wrapper and all its components
461
- if (provenance && provenance.length > 0) {
462
- result.provenance = [...provenance];
463
- if (result.argument) {
464
- result.argument.provenance = [...provenance];
465
- // Also set on the multiplication components within the unary minus
466
- if (result.argument.left) {
467
- result.argument.left.provenance = [...provenance];
468
- }
469
- if (result.argument.right) {
470
- result.argument.right.provenance = [...provenance];
471
- }
472
- }
473
- }
474
- } else {
475
- result = multiplicationNode;
476
- }
477
- }
478
-
479
- // Preserve provenance from the original terms that were combined
480
- if (provenance && provenance.length > 0) {
481
- provenance.forEach(id => {
482
- if (!result.provenance.includes(id)) {
483
- result.provenance.push(id);
484
- }
485
- });
486
- }
487
-
488
- return result;
489
- }
490
-
491
- /**
492
- * Helper to get node class by name (needed for dynamic instantiation).
493
- * @param {string} className - The class name.
494
- * @returns {Function} The class constructor.
495
- */
496
- static getNodeClass(className) {
497
- const classMap = {
498
- 'omdVariableNode': omdVariableNode,
499
- 'omdPowerNode': omdPowerNode,
500
- 'omdUnaryExpressionNode': omdUnaryExpressionNode
501
- };
502
- return classMap[className];
503
- }
504
-
505
- /**
506
- * ===== SIMPLIFIED RULE ENGINE CLASS =====
507
- * Defines the structure and behavior of a simplification rule.
508
- */
509
- static SimplificationRule = class SimplificationRule {
510
- /**
511
- * Creates an instance of SimplificationRule.
512
- * @param {string} name - The name of the rule.
513
- * @param {function(Object): boolean|Object|null} matchFn - A function that attempts to match the rule against a node. Returns `false`, `null`, `undefined` for no match, `true` for a match with no additional data, or an object with data for a match.
514
- * @param {function(Object, Object): Object} transformFn - A function that transforms the matched node. Receives the node and data from `matchFn`.
515
- * @param {function(Object, Object, Object): string} [messageFn=null] - A function that generates a human-readable message for the transformation. Receives the original node, rule data, and the new node.
516
- * @param {string} [type=null] - The type/category of the rule (e.g., 'rational', 'arithmetic', 'algebraic').
517
- */
518
- constructor(name, matchFn, transformFn, messageFn = null, type = null) {
519
- this.name = name;
520
- this.type = type;
521
- this.match = matchFn;
522
- this.transform = transformFn;
523
- this.message = messageFn;
524
- }
525
-
526
- /**
527
- * Determines if the rule can be applied to a given node.
528
- * @param {Object} node - The node to check.
529
- * @returns {Object|null} An object containing rule data if the rule can be applied, otherwise null.
530
- */
531
- canApply(node) {
532
- const result = this.match(node);
533
- return result === false || result === null || result === undefined ? null :
534
- result === true ? {} : result; // Normalize match results
535
- }
536
-
537
- /**
538
- * Applies the transformation defined by the rule to a node.
539
- * @param {Object} node - The node to transform.
540
- * @param {Object} ruleData - Data returned by the `matchFn`.
541
- * @param {Object} currentRoot - The current root of the expression tree.
542
- * @returns {Object} An object indicating success, the new root, and history information.
543
- */
544
- apply(node, ruleData, currentRoot) {
545
- try {
546
- // Ensure the transform function is valid
547
- if (typeof this.transform !== 'function') {
548
- throw new Error(`Invalid transform function for rule: ${this.name}`);
549
- }
550
-
551
- const newNode = this.transform(node, ruleData, currentRoot);
552
- if (!newNode) {
553
- throw new Error(`Transform function for rule '${this.name}' did not return a new node.`);
554
- }
555
-
556
- // Collect the IDs of all nodes that are about to be replaced.
557
- const affectedNodeIds = this._collectAffectedNodeIds(node);
558
-
559
-
560
- const { success, newRoot } = utils._replaceNodeInTree(node, newNode, currentRoot);
561
-
562
- if (success) {
563
- // Use custom affected nodes if provided
564
- const finalAffectedIds = newNode.__affectedNodeIds || affectedNodeIds;
565
- const finalResultNodeId = newNode.__resultNodeId || newNode.id;
566
-
567
-
568
- const historyEntry = {
569
- name: this.name,
570
- affectedNodes: finalAffectedIds,
571
- resultNodeId: finalResultNodeId,
572
- resultProvSources: newNode.provenance,
573
- message: this.message(node, ruleData, newNode)
574
- };
575
-
576
-
577
-
578
- return { success: true, newRoot, historyEntry };
579
- } else {
580
- return { success: false, newRoot: currentRoot };
581
- }
582
- } catch (error) {
583
- console.error(`Error applying rule '${this.name}':`, error);
584
- return { success: false, newRoot: currentRoot };
585
- }
586
- }
587
-
588
- /**
589
- * Collects IDs of nodes directly affected by this rule application.
590
- * For rules that create new nodes with provenance, use that to determine affected nodes
591
- * Otherwise, use the direct node ID.
592
- * @param {Object} node - The original node that was transformed.
593
- * @returns {Array<string>} Array of node IDs that were affected.
594
- */
595
- _collectAffectedNodeIds(node) {
596
- // For simplification rules that set provenance, use that to determine affected nodes
597
- if (node && node.provenance && node.provenance.length > 0) {
598
- // Return the provenance IDs (excluding the original node ID which represents the transformation result)
599
- return node.provenance.filter(id => id !== node.id);
600
- }
601
-
602
- // Fallback: return the direct node ID
603
- return node && node.id ? [node.id] : [];
604
- }
605
-
606
- /**
607
- * Generates a human-readable message for this rule application.
608
- * @param {Object} originalNode - The original node before transformation.
609
- * @param {Object} ruleData - Data from the match function.
610
- * @param {Object} newNode - The new node after transformation.
611
- * @returns {string} A human-readable message describing what happened.
612
- */
613
- _generateMessage(originalNode, ruleData, newNode) {
614
- if (this.message) {
615
- try {
616
- return this.message(originalNode, ruleData, newNode);
617
- } catch (error) {
618
- console.warn(`Error generating message for rule ${this.name}:`, error);
619
- }
620
- }
621
-
622
- // Default message if no custom message function is provided
623
- const originalValue = originalNode.toString ? originalNode.toString() : 'expression';
624
- const newValue = newNode.toString ? newNode.toString() : 'expression';
625
- return `Applied ${this.name}: "${originalValue}" → "${newValue}"`;
626
- }
627
- }
628
-
629
- /**
630
- * ===== HELPER FUNCTIONS FOR COMMON RULE PATTERNS =====
631
- * These functions create common types of simplification rules.
632
- */
633
-
634
- /**
635
- * Creates a rational number node, simplifying it by dividing by the greatest common divisor.
636
- * Handles negative numerators and denominators to ensure a positive denominator.
637
- * @param {number} numerator - The numerator of the rational number.
638
- * @param {number} denominator - The denominator of the rational number.
639
- * @param {number} fontSize - The font size for the resulting node.
640
- * @returns {Object} A new rational node or a constant node if the denominator simplifies to 1.
641
- */
642
- static rational(numerator, denominator, fontSize) {
643
- const gcd = utils.gcd(Math.abs(numerator), Math.abs(denominator));
644
- const simpleNum = numerator / gcd;
645
- const simpleDen = denominator / gcd;
646
-
647
- // Ensure denominator is positive
648
- if (simpleDen < 0) {
649
- return SimplificationEngine.#createRationalSafe(-simpleNum, -simpleDen, fontSize);
650
- }
651
- return SimplificationEngine.#createRationalSafe(simpleNum, simpleDen, fontSize);
652
- }
653
-
654
- /**
655
- * A safe internal function to create a rational node, handling simplification and negative signs.
656
- * @param {number} numerator - The numerator.
657
- * @param {number} denominator - The denominator.
658
- * @param {number} fontSize - The font size.
659
- * @returns {Object} The created omdRationalNode or omdConstantNode.
660
- */
661
- static #createRationalSafe(numerator, denominator, fontSize) {
662
- if (denominator === 1) {
663
- const result = SimplificationEngine.createConstant(numerator, fontSize);
664
- return result;
665
- }
666
-
667
- const ast = {
668
- type: 'OperatorNode',
669
- op: '/',
670
- fn: 'divide',
671
- args: [
672
- { type: 'ConstantNode', value: Math.abs(numerator), clone: function() { return {...this}; } },
673
- { type: 'ConstantNode', value: denominator, clone: function() { return {...this}; } }
674
- ],
675
- clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
676
- };
677
-
678
- let node = new omdRationalNode(ast);
679
-
680
- // Wrap in unary minus if the numerator is negative
681
- if (numerator < 0) {
682
- const unaryAST = {
683
- type: 'OperatorNode',
684
- op: '-',
685
- fn: 'unaryMinus',
686
- args: [node.toMathJSNode()],
687
- clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
688
- };
689
- node = new omdUnaryExpressionNode(unaryAST);
690
- }
691
-
692
- node.setFontSize(fontSize);
693
- node.initialize();
694
-
695
- return node;
696
- }
697
-
698
- /**
699
- * Combines two constant nodes based on a given operator.
700
- * @param {Object} left - The left constant node.
701
- * @param {Object} right - The right constant node.
702
- * @param {string} operator - The operation to perform (add, subtract, multiply, divide).
703
- * @param {number} fontSize - The font size for the resulting node.
704
- * @returns {Object|null} A new constant or rational node representing the combined value, or null if division by zero occurs.
705
- */
706
- static combineConstants(left, right, operator, fontSize) {
707
- const leftVal = left.getValue();
708
- const rightVal = right.getValue();
709
-
710
- let resultNode = null;
711
-
712
- switch(operator) {
713
- case 'add':
714
- resultNode = SimplificationEngine.createConstant(leftVal + rightVal, fontSize, left, right);
715
- break;
716
- case 'subtract':
717
- resultNode = SimplificationEngine.createConstant(leftVal - rightVal, fontSize, left, right);
718
- break;
719
- case 'multiply':
720
- resultNode = SimplificationEngine.createConstant(leftVal * rightVal, fontSize, left, right);
721
- break;
722
- case 'divide':
723
- if (rightVal === 0) return null; // Avoid division by zero
724
- resultNode = SimplificationEngine.rational(leftVal, rightVal, fontSize, left, right);
725
- break;
726
- default:
727
- return null;
728
- }
729
-
730
- // The automatic provenance tracking is now handled by the create methods above
731
- return resultNode;
732
- }
733
-
734
- /**
735
- * ===== RULE FACTORY FUNCTIONS =====
736
- * Functions to create instances of SimplificationRule for common simplification patterns.
737
- */
738
-
739
- /**
740
- * Creates a new SimplificationRule instance.
741
- * @param {string} name - The name of the rule.
742
- * @param {function(Object): boolean|Object|null} matchFn - The match function for the rule.
743
- * @param {function(Object, Object): Object} transformFn - The transform function for the rule.
744
- * @param {function(Object, Object, Object): string} [messageFn=null] - Optional function to generate human-readable messages.
745
- * @param {string} [type=null] - The type/category of the rule (e.g., 'rational', 'arithmetic', 'algebraic').
746
- * @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule instance.
747
- */
748
- static createRule(name, matchFn, transformFn, messageFn = null, type = null) {
749
- return new SimplificationEngine.SimplificationRule(name, matchFn, transformFn, messageFn, type);
750
- }
751
-
752
- /**
753
- * Creates a rule for folding (simplifying) binary operations with two constant operands.
754
- * @param {string} name - The name of the constant fold rule.
755
- * @param {string} operator - The operator of the binary expression (e.g., 'add', 'subtract').
756
- * @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule for constant folding.
757
- */
758
- static createConstantFoldRule(name, operator) {
759
- return SimplificationEngine.createRule(name,
760
- // Match: binary op with two constant or rational constant operands
761
- (node) => {
762
- if (!SimplificationEngine.isBinaryOp(node, operator)) return false;
763
- return node.left.isConstant() && node.right.isConstant();
764
- },
765
- // Transform: combine the constants
766
- (node) => {
767
- const newNode = SimplificationEngine.combineConstants(node.left, node.right, operator, node.getFontSize());
768
- // The combineConstants function now correctly handles its own provenance.
769
- // No need to manually add the parent node's id.
770
- return newNode;
771
- },
772
- // Message: describe the constant folding operation
773
- (originalNode, ruleData, newNode) => {
774
- const leftVal = originalNode.left.getValue();
775
- const rightVal = originalNode.right.getValue();
776
- const result = newNode.getValue ? newNode.getValue() : newNode.toString();
777
-
778
- const operatorSymbols = {
779
- 'add': '+',
780
- 'subtract': '-',
781
- 'multiply': getMultiplicationSymbol(),
782
- 'divide': '÷'
783
- };
784
-
785
- const symbol = operatorSymbols[operator] || operator;
786
- return `Combined constants: ${leftVal} ${symbol} ${rightVal} = ${result}`;
787
- }
788
- );
789
- }
790
-
791
- /**
792
- * Creates an identity rule (e.g., x + 0 → x, x * 1 → x).
793
- * @param {string} name - The name of the identity rule.
794
- * @param {string} operator - The operator of the binary expression.
795
- * @param {number} identityValue - The identity value for the operation (e.g., 0 for addition, 1 for multiplication).
796
- * @param {'left'|'right'|'both'} [side='both'] - Specifies which side the identity value should be on ('left', 'right', or 'both').
797
- * @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule for the identity operation.
798
- */
799
- static createIdentityRule(name, operator, identityValue, side = 'both') {
800
- return SimplificationEngine.createRule(name,
801
- // Match: binary op with one constant operand being the identity value
802
- (node) => {
803
- if (!SimplificationEngine.isBinaryOp(node, operator)) return false;
804
-
805
- const constOperandInfo = SimplificationEngine.hasConstantOperand(node);
806
- if (!constOperandInfo || !constOperandInfo.constant.isConstant() || constOperandInfo.constant.getValue() !== identityValue) {
807
- return false;
808
- }
809
-
810
- if (side === 'left' && node.right.isConstant()) return false;
811
- if (side === 'right' && node.left.isConstant()) return false;
812
-
813
- return { other: constOperandInfo.other };
814
- },
815
- // Transform: return the other operand
816
- (node, data) => {
817
- const newNode = data.other.clone();
818
-
819
- // Use manual provenance tracking for substitution
820
- applyProvenance(newNode, node);
821
-
822
- // Add a flag to tell the highlighting engine that this was a simple identity transform.
823
- newNode.__isSimpleIdentity = true;
824
-
825
- return newNode;
826
- },
827
- // Message: describe the identity operation
828
- (originalNode, ruleData, newNode) => {
829
- const { other } = ruleData;
830
- const otherStr = other.toString ? other.toString() : 'expression';
831
-
832
- const operatorNames = {
833
- 'add': 'addition',
834
- 'subtract': 'subtraction',
835
- 'multiply': 'multiplication',
836
- 'divide': 'division'
837
- };
838
-
839
- const operatorSymbols = {
840
- 'add': '+',
841
- 'subtract': '-',
842
- 'multiply': getMultiplicationSymbol(),
843
- 'divide': '÷'
844
- };
845
-
846
- const opName = operatorNames[operator] || operator;
847
- const symbol = operatorSymbols[operator] || operator;
848
-
849
- const isLeftIdentity = originalNode.left.isConstant() && originalNode.left.getValue() === identityValue;
850
- const position = isLeftIdentity ? 'left' : 'right';
851
-
852
- if (operator === 'add' && identityValue === 0) {
853
- return `Applied additive identity: "${otherStr} + 0" simplified to "${otherStr}" (adding 0 doesn't change the value)`;
854
- } else if (operator === 'subtract' && identityValue === 0) {
855
- return `Applied subtraction identity: "${otherStr} - 0" simplified to "${otherStr}" (subtracting 0 doesn't change the value)`;
856
- } else if (operator === 'multiply' && identityValue === 1) {
857
- return `Applied multiplicative identity: "${otherStr} ${getMultiplicationSymbol()} 1" simplified to "${otherStr}" (multiplying by 1 doesn't change the value)`;
858
- } else if (operator === 'divide' && identityValue === 1) {
859
- return `Applied division identity: "${otherStr} ÷ 1" simplified to "${otherStr}" (dividing by 1 doesn't change the value)`;
860
- } else {
861
- return `Applied ${opName} identity: removed ${identityValue} from ${position} side, leaving "${otherStr}"`;
862
- }
863
- }
864
- );
865
- }
866
-
867
- /**
868
- * Creates a rule for simplifying zero multiplication (e.g., x * 0 = 0).
869
- */
870
- static createZeroMultiplicationRule() {
871
- return SimplificationEngine.createRule("Zero Multiplication",
872
- (node) => {
873
- if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
874
- const constOp = SimplificationEngine.hasConstantOperand(node);
875
- return constOp && constOp.constant.getValue() === 0;
876
- },
877
- (node) => {
878
- const newNode = SimplificationEngine.createConstant(0, node.getFontSize(), node);
879
- return newNode;
880
- },
881
- (originalNode, ruleData, newNode) => {
882
- const constOp = SimplificationEngine.hasConstantOperand(originalNode);
883
- const otherOperand = constOp.other;
884
- return `Applied zero multiplication: ${utils.nodeToString(otherOperand)} ${getMultiplicationSymbol()} 0 = 0`;
885
- }
886
- );
887
- }
1
+ import { omdRationalNode } from "../nodes/omdRationalNode.js";
2
+ import { omdBinaryExpressionNode } from "../nodes/omdBinaryExpressionNode.js";
3
+ import { omdUnaryExpressionNode } from "../nodes/omdUnaryExpressionNode.js";
4
+ import { omdVariableNode } from "../nodes/omdVariableNode.js";
5
+ import { omdPowerNode } from "../nodes/omdPowerNode.js";
6
+ import { omdConstantNode } from "../nodes/omdConstantNode.js";
7
+ import { createConstantNode, applyProvenance } from './simplificationUtils.js';
8
+ import * as utils from './simplificationUtils.js';
9
+ import { useImplicitMultiplication, getMultiplicationSymbol } from "../config/omdConfigManager.js";
10
+
11
+ /**
12
+ * @class SimplificationEngine
13
+ * @classdesc Provides a collection of static methods for creating, matching, and transforming
14
+ * mathematical expression nodes for simplification purposes.
15
+ * This class serves as the core logic for applying simplification rules within the OMD system.
16
+ */
17
+ export class SimplificationEngine {
18
+
19
+ /**
20
+ * ===== SIMPLIFIED NODE CREATION HELPERS =====
21
+ * These functions provide an easy way to create various types of AST nodes
22
+ * without directly dealing with the complexities of the Math.js AST structure.
23
+ */
24
+
25
+ /**
26
+ * Creates a new constant node.
27
+ * @param {number} value - The numeric value of the constant.
28
+ * @param {number} [fontSize=16] - The font size for the node.
29
+ * @param {...omdNode} sourceNodes - Source nodes for automatic provenance tracking.
30
+ * @returns {Object} A new constant node.
31
+ */
32
+ static createConstant(value, fontSize = 16, ...sourceNodes) {
33
+ const node = createConstantNode(value, fontSize);
34
+
35
+ // Apply manual provenance tracking if source nodes provided
36
+ if (sourceNodes.length > 0) {
37
+ applyProvenance(node, ...sourceNodes);
38
+ }
39
+
40
+ return node;
41
+ }
42
+
43
+ /**
44
+ * Creates a new binary expression node.
45
+ * @param {Object} left - The left-hand side operand node.
46
+ * @param {string} operator - The operator (e.g., '+', '-', '*', '/').
47
+ * @param {Object} right - The right-hand side operand node.
48
+ * @param {number} [fontSize=16] - The font size for the node.
49
+ * @param {Array|null} [provenance=null] - The provenance array for the node (deprecated - use ...sourceNodes instead).
50
+ * @param {omdNode|null} [operatorNode=null] - The original operator node for provenance (deprecated - use ...sourceNodes instead).
51
+ * @param {...omdNode} sourceNodes - Additional source nodes for automatic provenance tracking.
52
+ * @returns {Object} A new binary expression node.
53
+ * @throws {Error} If an unknown operator is provided.
54
+ */
55
+ static createBinaryOp(left, operator, right, fontSize = 16, provenance = null, operatorNode = null, ...sourceNodes) {
56
+ const opMap = {
57
+ 'add': { op: '+', fn: 'add' },
58
+ 'subtract': { op: '−', fn: 'subtract' },
59
+ 'multiply': { op: getMultiplicationSymbol(), fn: 'multiply' },
60
+ 'divide': { op: '÷', fn: 'divide' }
61
+ };
62
+
63
+ const opInfo = opMap[operator];
64
+ if (!opInfo) throw new Error(`Unknown operator: ${operator}`);
65
+
66
+ // For multiplication, reorder operands if needed (e.g., x*2 -> 2*x)
67
+ let leftOperand = left;
68
+ let rightOperand = right;
69
+
70
+ if (operator === 'multiply') {
71
+ const leftIsConstant = (left instanceof omdConstantNode && left.isConstant());
72
+ const rightIsConstant = (right instanceof omdConstantNode && right.isConstant());
73
+
74
+ // If left is non-constant and right is constant, swap them
75
+ if (!leftIsConstant && rightIsConstant) {
76
+ leftOperand = right;
77
+ rightOperand = left;
78
+ }
79
+ }
80
+
81
+ const ast = {
82
+ type: 'OperatorNode',
83
+ op: opInfo.op,
84
+ fn: opInfo.fn,
85
+ args: [leftOperand.toMathJSNode(), rightOperand.toMathJSNode()],
86
+ clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
87
+ };
88
+
89
+ // Pass operator provenance into the constructor via the AST
90
+ if (operatorNode) {
91
+ ast.operatorProvenance = [operatorNode.id];
92
+ }
93
+
94
+ const node = new omdBinaryExpressionNode(ast);
95
+ node.setFontSize(fontSize);
96
+
97
+ // Use manual provenance tracking
98
+ if (provenance) {
99
+ // Legacy support: apply manual provenance
100
+ node.provenance = provenance;
101
+ } else {
102
+ // Manual provenance tracking for binary operations
103
+ const allSources = [leftOperand, rightOperand];
104
+ if (operatorNode) allSources.push(operatorNode);
105
+ allSources.push(...sourceNodes);
106
+ applyProvenance(node, ...allSources);
107
+ }
108
+
109
+ node.initialize();
110
+ return node;
111
+ }
112
+
113
+ /**
114
+ * Creates a multiplication expression where the left operand is a constant value.
115
+ * @param {Object} leftConstantNode - The constant node for the left operand.
116
+ * @param {Object} rightNode - The node for the right operand.
117
+ * @param {number} [fontSize=16] - The font size for the node.
118
+ * @param {Array|null} [provenance=null] - The provenance array for the node.
119
+ * @returns {Object} A new binary expression node representing multiplication.
120
+ */
121
+ static createMultiplication(leftConstantNode, rightNode, fontSize = 16, provenance = null) {
122
+ // Clone the constant to create a new instance for the new expression,
123
+ // but ensure its provenance points back to the original constant.
124
+ const newLeftConstant = leftConstantNode.clone();
125
+ newLeftConstant.provenance = [leftConstantNode.id];
126
+
127
+ const rightClone = rightNode.clone(); // Clone the other node to ensure we don't modify the original
128
+
129
+ // Safely get the operator node for provenance, handling cases where parent might be null
130
+ const operatorNode = leftConstantNode.parent?.op || null;
131
+
132
+ return SimplificationEngine.createBinaryOp(newLeftConstant, 'multiply', rightClone, fontSize, provenance, operatorNode);
133
+ }
134
+
135
+ /**
136
+ * Creates a rational node (fraction).
137
+ * @param {number} numerator - The numerator value.
138
+ * @param {number} denominator - The denominator value.
139
+ * @param {number} [fontSize=16] - The font size for the node.
140
+ * @param {...omdNode} sourceNodes - Source nodes for automatic provenance tracking.
141
+ * @returns {Object} A new rational node, or a constant node if the denominator is 1.
142
+ */
143
+ static createRational(numerator, denominator, fontSize = 16, ...sourceNodes) {
144
+ if (denominator === 1) {
145
+ return SimplificationEngine.createConstant(numerator, fontSize, ...sourceNodes);
146
+ }
147
+
148
+ const ast = {
149
+ type: 'OperatorNode', op: '/', fn: 'divide',
150
+ args: [
151
+ { type: 'ConstantNode', value: Math.abs(numerator) },
152
+ { type: 'ConstantNode', value: denominator }
153
+ ],
154
+ clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
155
+ };
156
+
157
+ let node = new omdRationalNode(ast);
158
+
159
+ // Handle negative fractions by wrapping in a unary minus node
160
+ if (numerator < 0) {
161
+ const unaryAST = {
162
+ type: 'OperatorNode', op: '-', fn: 'unaryMinus',
163
+ args: [node.toMathJSNode()],
164
+ clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
165
+ };
166
+ node = new omdUnaryExpressionNode(unaryAST);
167
+ }
168
+
169
+ node.setFontSize(fontSize);
170
+
171
+ // Apply manual provenance tracking if source nodes provided
172
+ if (sourceNodes.length > 0) {
173
+ applyProvenance(node, ...sourceNodes);
174
+ }
175
+
176
+ node.initialize();
177
+ return node;
178
+ }
179
+
180
+ /**
181
+ * ===== SIMPLIFIED PATTERN MATCHING HELPERS =====
182
+ * These functions provide convenient ways to check common patterns in AST nodes.
183
+ */
184
+
185
+ /**
186
+ * Checks if a node is of a specific type.
187
+ * @param {Object} node - The node to check.
188
+ * @param {string} typeName - The name of the constructor type (e.g., 'omdConstantNode').
189
+ * @returns {boolean} True if the node matches the type, false otherwise.
190
+ */
191
+ static isType(node, typeName) {
192
+ if (!node) return false;
193
+ return node.type === typeName;
194
+ }
195
+
196
+ /**
197
+ * Checks if a node is a binary operation, optionally with a specific operator.
198
+ * @param {Object} node - The node to check.
199
+ * @param {string} [operator=null] - The specific operator to check for (e.g., 'add', 'multiply').
200
+ * @returns {boolean} True if the node is a binary operation (and matches operator if provided), false otherwise.
201
+ */
202
+ static isBinaryOp(node, operator = null) {
203
+ if (!SimplificationEngine.isType(node, 'omdBinaryExpressionNode')) return false;
204
+
205
+ if (!operator) return true;
206
+
207
+ // Handle both string operations and object operations (from Math.js AST)
208
+ let nodeOp = node.operation;
209
+ if (typeof nodeOp === 'object' && nodeOp.name) {
210
+ nodeOp = nodeOp.name;
211
+ }
212
+
213
+ return nodeOp === operator;
214
+ }
215
+
216
+ /**
217
+ * Checks if a node is a constant, optionally with a specific value.
218
+ * @param {Object} node - The node to check.
219
+ * @param {number} [value=null] - The specific constant value to check for.
220
+ * @returns {boolean} True if the node is a constant (and matches value if provided), false otherwise.
221
+ */
222
+ static isConstantValue(node, value = null) {
223
+ if (!node.isConstant()) return false;
224
+ return value !== null ? node.getValue() === value : true;
225
+ }
226
+
227
+ /**
228
+ * Checks if a binary operation node has a constant operand and returns it along with the other operand.
229
+ * @param {Object} node - The binary expression node to check.
230
+ * @returns {Object|null} An object containing the constant and other operand, or null if no constant operand is found.
231
+ */
232
+ static hasConstantOperand(node) {
233
+ if (!SimplificationEngine.isBinaryOp(node)) return null;
234
+
235
+ if (node.left.isConstant()) return { constant: node.left, other: node.right };
236
+ if (node.right.isConstant()) return { constant: node.right, other: node.left };
237
+ return null;
238
+ }
239
+
240
+ /**
241
+ * Unwraps a parenthesis node, returning its inner expression. If the node is not a parenthesis node, it returns the node itself.
242
+ * @param {Object} node - The node to unwrap.
243
+ * @returns {Object} The inner expression if the node is a parenthesis node, otherwise the original node.
244
+ */
245
+ static unwrapParentheses(node) {
246
+ return SimplificationEngine.isType(node, 'omdParenthesisNode') ? node.expression : node;
247
+ }
248
+
249
+ /**
250
+ * ===== MONOMIAL AND LIKE TERMS HELPERS =====
251
+ * These functions help identify and work with monomials and like terms.
252
+ */
253
+
254
+ /**
255
+ * Checks if a node represents a monomial (a term of the form coefficient * variable^power).
256
+ * Examples: x, 2x, 3y, -5z, x^2, 2x^3
257
+ * @param {Object} node - The node to check.
258
+ * @returns {Object|null} An object with coefficient, variable, and power if it's a monomial, null otherwise.
259
+ */
260
+ static isMonomial(node) {
261
+ let coefficientMultiplier = 1;
262
+
263
+ // Unwrap parentheses first, as they can contain a unary minus
264
+ node = SimplificationEngine.unwrapParentheses(node);
265
+
266
+ // Handle any number of nested unary minuses by repeatedly unwrapping them
267
+ // and flipping the coefficient multiplier.
268
+ while (SimplificationEngine.isType(node, 'omdUnaryExpressionNode') && node.operation === 'unaryMinus') {
269
+ node = node.argument;
270
+ coefficientMultiplier *= -1;
271
+ // The argument of the unary minus could also be in parentheses
272
+ node = SimplificationEngine.unwrapParentheses(node);
273
+ }
274
+
275
+ // Case 1: Just a variable (e.g., x)
276
+ if (SimplificationEngine.isType(node, 'omdVariableNode')) {
277
+ return {
278
+ coefficient: 1 * coefficientMultiplier,
279
+ variable: node.name,
280
+ power: 1,
281
+ variableNode: node
282
+ };
283
+ }
284
+
285
+ // Case 2: Constant * Variable (e.g., 2x, 3y)
286
+ if (SimplificationEngine.isBinaryOp(node, 'multiply')) {
287
+ const constOp = SimplificationEngine.hasConstantOperand(node);
288
+ if (constOp && SimplificationEngine.isType(constOp.other, 'omdVariableNode')) {
289
+ return {
290
+ coefficient: constOp.constant.getValue() * coefficientMultiplier,
291
+ variable: constOp.other.name,
292
+ power: 1,
293
+ variableNode: constOp.other,
294
+ coefficientNode: constOp.constant
295
+ };
296
+ }
297
+
298
+ // Case 3: Variable * Constant (e.g., x*2, y*3) - less common but possible
299
+ if (constOp && SimplificationEngine.isType(constOp.constant, 'omdVariableNode') && constOp.other.isConstant()) {
300
+ return {
301
+ coefficient: constOp.other.getValue() * coefficientMultiplier,
302
+ variable: constOp.constant.name,
303
+ power: 1,
304
+ variableNode: constOp.constant,
305
+ coefficientNode: constOp.other
306
+ };
307
+ }
308
+
309
+ // Case 4: Constant * Power (e.g., 2*x^3)
310
+ const constPowerOp = SimplificationEngine.hasConstantOperand(node);
311
+ if (constPowerOp && SimplificationEngine.isType(constPowerOp.other, 'omdPowerNode')) {
312
+ const powerNode = constPowerOp.other;
313
+ if (SimplificationEngine.isType(powerNode.base, 'omdVariableNode') && powerNode.exponent.isConstant()) {
314
+ return {
315
+ coefficient: constPowerOp.constant.getValue() * coefficientMultiplier,
316
+ variable: powerNode.base.name,
317
+ power: powerNode.exponent.getValue(),
318
+ variableNode: powerNode.base,
319
+ coefficientNode: constPowerOp.constant,
320
+ powerNode: powerNode
321
+ };
322
+ }
323
+ }
324
+ }
325
+
326
+ // Case 5: Just a power (e.g., x^2)
327
+ if (SimplificationEngine.isType(node, 'omdPowerNode')) {
328
+ if (SimplificationEngine.isType(node.base, 'omdVariableNode') && node.exponent.isConstant()) {
329
+ return {
330
+ coefficient: 1 * coefficientMultiplier,
331
+ variable: node.base.name,
332
+ power: node.exponent.getValue(),
333
+ variableNode: node.base,
334
+ powerNode: node
335
+ };
336
+ }
337
+ }
338
+
339
+ return null;
340
+ }
341
+
342
+ /**
343
+ * Checks if two monomials are like terms (same variable and power).
344
+ * @param {Object} monomial1 - First monomial info from isMonomial().
345
+ * @param {Object} monomial2 - Second monomial info from isMonomial().
346
+ * @returns {boolean} True if they are like terms, false otherwise.
347
+ */
348
+ static areLikeTerms(monomial1, monomial2) {
349
+ return monomial1.variable === monomial2.variable && monomial1.power === monomial2.power;
350
+ }
351
+
352
+ /**
353
+ * Creates a monomial node from coefficient, variable, and power.
354
+ * @param {number} coefficient - The coefficient of the monomial.
355
+ * @param {string} variable - The variable name.
356
+ * @param {number} power - The power of the variable.
357
+ * @param {number} fontSize - The font size for the node.
358
+ * @param {Array} [provenance=[]] - The provenance array to preserve lineage.
359
+ * @returns {Object} A new monomial node.
360
+ */
361
+ static createMonomial(coefficient, variable, power, fontSize, provenance = []) {
362
+ // Create variable node
363
+ const variableAST = { type: 'SymbolNode', name: variable, clone: function() { return {...this}; } };
364
+ const variableNode = new (SimplificationEngine.getNodeClass('omdVariableNode'))(variableAST);
365
+ variableNode.setFontSize(fontSize);
366
+ variableNode.initialize();
367
+
368
+ // CRITICAL: Set provenance on the variable node
369
+ if (provenance && provenance.length > 0) {
370
+ variableNode.provenance = [...provenance];
371
+ }
372
+
373
+ let termNode = variableNode;
374
+
375
+ // Add power if not 1
376
+ if (power !== 1) {
377
+ const powerAST = {
378
+ type: 'OperatorNode',
379
+ op: '^',
380
+ fn: 'pow',
381
+ args: [variableAST, { type: 'ConstantNode', value: power, clone: function() { return {...this}; } }],
382
+ clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
383
+ };
384
+ termNode = new (SimplificationEngine.getNodeClass('omdPowerNode'))(powerAST);
385
+ termNode.setFontSize(fontSize);
386
+ termNode.initialize();
387
+
388
+ // CRITICAL: Set provenance on the power node and its components
389
+ if (provenance && provenance.length > 0) {
390
+ termNode.provenance = [...provenance];
391
+ // Set provenance on the base (variable) and exponent
392
+ if (termNode.base) {
393
+ termNode.base.provenance = [...provenance];
394
+ }
395
+ if (termNode.exponent) {
396
+ termNode.exponent.provenance = [...provenance];
397
+ }
398
+ }
399
+ }
400
+
401
+ let result;
402
+ if (coefficient === 1) {
403
+ result = termNode;
404
+ } else if (coefficient === -1) {
405
+ // Create unary minus
406
+ const unaryAST = {
407
+ type: 'OperatorNode',
408
+ op: '-',
409
+ fn: 'unaryMinus',
410
+ args: [termNode.toMathJSNode()],
411
+ clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
412
+ };
413
+ result = new omdUnaryExpressionNode(unaryAST);
414
+ result.setFontSize(fontSize);
415
+ result.initialize();
416
+
417
+ // CRITICAL: Set provenance on unary minus and its argument
418
+ if (provenance && provenance.length > 0) {
419
+ result.provenance = [...provenance];
420
+ if (result.argument) {
421
+ result.argument.provenance = [...provenance];
422
+ }
423
+ }
424
+ } else {
425
+ // Create coefficient * term
426
+ const coeffNode = SimplificationEngine.createConstant(Math.abs(coefficient), fontSize);
427
+
428
+ // CRITICAL: Set provenance on the coefficient node
429
+ if (provenance && provenance.length > 0) {
430
+ coeffNode.provenance = [...provenance];
431
+ }
432
+
433
+ const multiplicationNode = SimplificationEngine.createBinaryOp(coeffNode, 'multiply', termNode, fontSize);
434
+
435
+ // CRITICAL: Set provenance on the multiplication node
436
+ if (provenance && provenance.length > 0) {
437
+ multiplicationNode.provenance = [...provenance];
438
+ // Ensure left (coefficient) and right (variable/power) have provenance
439
+ if (multiplicationNode.left) {
440
+ multiplicationNode.left.provenance = [...provenance];
441
+ }
442
+ if (multiplicationNode.right) {
443
+ multiplicationNode.right.provenance = [...provenance];
444
+ }
445
+ }
446
+
447
+ // Wrap in unary minus if coefficient is negative
448
+ if (coefficient < 0) {
449
+ const unaryAST = {
450
+ type: 'OperatorNode',
451
+ op: '-',
452
+ fn: 'unaryMinus',
453
+ args: [multiplicationNode.toMathJSNode()],
454
+ clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
455
+ };
456
+ result = new omdUnaryExpressionNode(unaryAST);
457
+ result.setFontSize(fontSize);
458
+ result.initialize();
459
+
460
+ // CRITICAL: Set provenance on negative wrapper and all its components
461
+ if (provenance && provenance.length > 0) {
462
+ result.provenance = [...provenance];
463
+ if (result.argument) {
464
+ result.argument.provenance = [...provenance];
465
+ // Also set on the multiplication components within the unary minus
466
+ if (result.argument.left) {
467
+ result.argument.left.provenance = [...provenance];
468
+ }
469
+ if (result.argument.right) {
470
+ result.argument.right.provenance = [...provenance];
471
+ }
472
+ }
473
+ }
474
+ } else {
475
+ result = multiplicationNode;
476
+ }
477
+ }
478
+
479
+ // Preserve provenance from the original terms that were combined
480
+ if (provenance && provenance.length > 0) {
481
+ provenance.forEach(id => {
482
+ if (!result.provenance.includes(id)) {
483
+ result.provenance.push(id);
484
+ }
485
+ });
486
+ }
487
+
488
+ return result;
489
+ }
490
+
491
+ /**
492
+ * Helper to get node class by name (needed for dynamic instantiation).
493
+ * @param {string} className - The class name.
494
+ * @returns {Function} The class constructor.
495
+ */
496
+ static getNodeClass(className) {
497
+ const classMap = {
498
+ 'omdVariableNode': omdVariableNode,
499
+ 'omdPowerNode': omdPowerNode,
500
+ 'omdUnaryExpressionNode': omdUnaryExpressionNode
501
+ };
502
+ return classMap[className];
503
+ }
504
+
505
+ /**
506
+ * ===== SIMPLIFIED RULE ENGINE CLASS =====
507
+ * Defines the structure and behavior of a simplification rule.
508
+ */
509
+ static SimplificationRule = class SimplificationRule {
510
+ /**
511
+ * Creates an instance of SimplificationRule.
512
+ * @param {string} name - The name of the rule.
513
+ * @param {function(Object): boolean|Object|null} matchFn - A function that attempts to match the rule against a node. Returns `false`, `null`, `undefined` for no match, `true` for a match with no additional data, or an object with data for a match.
514
+ * @param {function(Object, Object): Object} transformFn - A function that transforms the matched node. Receives the node and data from `matchFn`.
515
+ * @param {function(Object, Object, Object): string} [messageFn=null] - A function that generates a human-readable message for the transformation. Receives the original node, rule data, and the new node.
516
+ * @param {string} [type=null] - The type/category of the rule (e.g., 'rational', 'arithmetic', 'algebraic').
517
+ */
518
+ constructor(name, matchFn, transformFn, messageFn = null, type = null) {
519
+ this.name = name;
520
+ this.type = type;
521
+ this.match = matchFn;
522
+ this.transform = transformFn;
523
+ this.message = messageFn;
524
+ }
525
+
526
+ /**
527
+ * Determines if the rule can be applied to a given node.
528
+ * @param {Object} node - The node to check.
529
+ * @returns {Object|null} An object containing rule data if the rule can be applied, otherwise null.
530
+ */
531
+ canApply(node) {
532
+ const result = this.match(node);
533
+ return result === false || result === null || result === undefined ? null :
534
+ result === true ? {} : result; // Normalize match results
535
+ }
536
+
537
+ /**
538
+ * Applies the transformation defined by the rule to a node.
539
+ * @param {Object} node - The node to transform.
540
+ * @param {Object} ruleData - Data returned by the `matchFn`.
541
+ * @param {Object} currentRoot - The current root of the expression tree.
542
+ * @returns {Object} An object indicating success, the new root, and history information.
543
+ */
544
+ apply(node, ruleData, currentRoot) {
545
+ try {
546
+ // Ensure the transform function is valid
547
+ if (typeof this.transform !== 'function') {
548
+ throw new Error(`Invalid transform function for rule: ${this.name}`);
549
+ }
550
+
551
+ const newNode = this.transform(node, ruleData, currentRoot);
552
+ if (!newNode) {
553
+ throw new Error(`Transform function for rule '${this.name}' did not return a new node.`);
554
+ }
555
+
556
+ // Collect the IDs of all nodes that are about to be replaced.
557
+ const affectedNodeIds = this._collectAffectedNodeIds(node);
558
+
559
+
560
+ const { success, newRoot } = utils._replaceNodeInTree(node, newNode, currentRoot);
561
+
562
+ if (success) {
563
+ // Use custom affected nodes if provided
564
+ const finalAffectedIds = newNode.__affectedNodeIds || affectedNodeIds;
565
+ const finalResultNodeId = newNode.__resultNodeId || newNode.id;
566
+
567
+
568
+ const historyEntry = {
569
+ name: this.name,
570
+ affectedNodes: finalAffectedIds,
571
+ resultNodeId: finalResultNodeId,
572
+ resultProvSources: newNode.provenance,
573
+ message: this.message(node, ruleData, newNode)
574
+ };
575
+
576
+
577
+
578
+ return { success: true, newRoot, historyEntry };
579
+ } else {
580
+ return { success: false, newRoot: currentRoot };
581
+ }
582
+ } catch (error) {
583
+ console.error(`Error applying rule '${this.name}':`, error);
584
+ return { success: false, newRoot: currentRoot };
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Collects IDs of nodes directly affected by this rule application.
590
+ * For rules that create new nodes with provenance, use that to determine affected nodes
591
+ * Otherwise, use the direct node ID.
592
+ * @param {Object} node - The original node that was transformed.
593
+ * @returns {Array<string>} Array of node IDs that were affected.
594
+ */
595
+ _collectAffectedNodeIds(node) {
596
+ // For simplification rules that set provenance, use that to determine affected nodes
597
+ if (node && node.provenance && node.provenance.length > 0) {
598
+ // Return the provenance IDs (excluding the original node ID which represents the transformation result)
599
+ return node.provenance.filter(id => id !== node.id);
600
+ }
601
+
602
+ // Fallback: return the direct node ID
603
+ return node && node.id ? [node.id] : [];
604
+ }
605
+
606
+ /**
607
+ * Generates a human-readable message for this rule application.
608
+ * @param {Object} originalNode - The original node before transformation.
609
+ * @param {Object} ruleData - Data from the match function.
610
+ * @param {Object} newNode - The new node after transformation.
611
+ * @returns {string} A human-readable message describing what happened.
612
+ */
613
+ _generateMessage(originalNode, ruleData, newNode) {
614
+ if (this.message) {
615
+ try {
616
+ return this.message(originalNode, ruleData, newNode);
617
+ } catch (error) {
618
+ console.warn(`Error generating message for rule ${this.name}:`, error);
619
+ }
620
+ }
621
+
622
+ // Default message if no custom message function is provided
623
+ const originalValue = originalNode.toString ? originalNode.toString() : 'expression';
624
+ const newValue = newNode.toString ? newNode.toString() : 'expression';
625
+ return `Applied ${this.name}: "${originalValue}" → "${newValue}"`;
626
+ }
627
+ }
628
+
629
+ /**
630
+ * ===== HELPER FUNCTIONS FOR COMMON RULE PATTERNS =====
631
+ * These functions create common types of simplification rules.
632
+ */
633
+
634
+ /**
635
+ * Creates a rational number node, simplifying it by dividing by the greatest common divisor.
636
+ * Handles negative numerators and denominators to ensure a positive denominator.
637
+ * @param {number} numerator - The numerator of the rational number.
638
+ * @param {number} denominator - The denominator of the rational number.
639
+ * @param {number} fontSize - The font size for the resulting node.
640
+ * @returns {Object} A new rational node or a constant node if the denominator simplifies to 1.
641
+ */
642
+ static rational(numerator, denominator, fontSize) {
643
+ const gcd = utils.gcd(Math.abs(numerator), Math.abs(denominator));
644
+ const simpleNum = numerator / gcd;
645
+ const simpleDen = denominator / gcd;
646
+
647
+ // Ensure denominator is positive
648
+ if (simpleDen < 0) {
649
+ return SimplificationEngine.#createRationalSafe(-simpleNum, -simpleDen, fontSize);
650
+ }
651
+ return SimplificationEngine.#createRationalSafe(simpleNum, simpleDen, fontSize);
652
+ }
653
+
654
+ /**
655
+ * A safe internal function to create a rational node, handling simplification and negative signs.
656
+ * @param {number} numerator - The numerator.
657
+ * @param {number} denominator - The denominator.
658
+ * @param {number} fontSize - The font size.
659
+ * @returns {Object} The created omdRationalNode or omdConstantNode.
660
+ */
661
+ static #createRationalSafe(numerator, denominator, fontSize) {
662
+ if (denominator === 1) {
663
+ const result = SimplificationEngine.createConstant(numerator, fontSize);
664
+ return result;
665
+ }
666
+
667
+ const ast = {
668
+ type: 'OperatorNode',
669
+ op: '/',
670
+ fn: 'divide',
671
+ args: [
672
+ { type: 'ConstantNode', value: Math.abs(numerator), clone: function() { return {...this}; } },
673
+ { type: 'ConstantNode', value: denominator, clone: function() { return {...this}; } }
674
+ ],
675
+ clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
676
+ };
677
+
678
+ let node = new omdRationalNode(ast);
679
+
680
+ // Wrap in unary minus if the numerator is negative
681
+ if (numerator < 0) {
682
+ const unaryAST = {
683
+ type: 'OperatorNode',
684
+ op: '-',
685
+ fn: 'unaryMinus',
686
+ args: [node.toMathJSNode()],
687
+ clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
688
+ };
689
+ node = new omdUnaryExpressionNode(unaryAST);
690
+ }
691
+
692
+ node.setFontSize(fontSize);
693
+ node.initialize();
694
+
695
+ return node;
696
+ }
697
+
698
+ /**
699
+ * Combines two constant nodes based on a given operator.
700
+ * @param {Object} left - The left constant node.
701
+ * @param {Object} right - The right constant node.
702
+ * @param {string} operator - The operation to perform (add, subtract, multiply, divide).
703
+ * @param {number} fontSize - The font size for the resulting node.
704
+ * @returns {Object|null} A new constant or rational node representing the combined value, or null if division by zero occurs.
705
+ */
706
+ static combineConstants(left, right, operator, fontSize) {
707
+ const leftVal = left.getValue();
708
+ const rightVal = right.getValue();
709
+
710
+ let resultNode = null;
711
+
712
+ switch(operator) {
713
+ case 'add':
714
+ resultNode = SimplificationEngine.createConstant(leftVal + rightVal, fontSize, left, right);
715
+ break;
716
+ case 'subtract':
717
+ resultNode = SimplificationEngine.createConstant(leftVal - rightVal, fontSize, left, right);
718
+ break;
719
+ case 'multiply':
720
+ resultNode = SimplificationEngine.createConstant(leftVal * rightVal, fontSize, left, right);
721
+ break;
722
+ case 'divide':
723
+ if (rightVal === 0) return null; // Avoid division by zero
724
+ resultNode = SimplificationEngine.rational(leftVal, rightVal, fontSize, left, right);
725
+ break;
726
+ default:
727
+ return null;
728
+ }
729
+
730
+ // The automatic provenance tracking is now handled by the create methods above
731
+ return resultNode;
732
+ }
733
+
734
+ /**
735
+ * ===== RULE FACTORY FUNCTIONS =====
736
+ * Functions to create instances of SimplificationRule for common simplification patterns.
737
+ */
738
+
739
+ /**
740
+ * Creates a new SimplificationRule instance.
741
+ * @param {string} name - The name of the rule.
742
+ * @param {function(Object): boolean|Object|null} matchFn - The match function for the rule.
743
+ * @param {function(Object, Object): Object} transformFn - The transform function for the rule.
744
+ * @param {function(Object, Object, Object): string} [messageFn=null] - Optional function to generate human-readable messages.
745
+ * @param {string} [type=null] - The type/category of the rule (e.g., 'rational', 'arithmetic', 'algebraic').
746
+ * @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule instance.
747
+ */
748
+ static createRule(name, matchFn, transformFn, messageFn = null, type = null) {
749
+ return new SimplificationEngine.SimplificationRule(name, matchFn, transformFn, messageFn, type);
750
+ }
751
+
752
+ /**
753
+ * Creates a rule for folding (simplifying) binary operations with two constant operands.
754
+ * @param {string} name - The name of the constant fold rule.
755
+ * @param {string} operator - The operator of the binary expression (e.g., 'add', 'subtract').
756
+ * @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule for constant folding.
757
+ */
758
+ static createConstantFoldRule(name, operator) {
759
+ return SimplificationEngine.createRule(name,
760
+ // Match: binary op with two constant or rational constant operands
761
+ (node) => {
762
+ if (!SimplificationEngine.isBinaryOp(node, operator)) return false;
763
+ return node.left.isConstant() && node.right.isConstant();
764
+ },
765
+ // Transform: combine the constants
766
+ (node) => {
767
+ const newNode = SimplificationEngine.combineConstants(node.left, node.right, operator, node.getFontSize());
768
+ // The combineConstants function now correctly handles its own provenance.
769
+ // No need to manually add the parent node's id.
770
+ return newNode;
771
+ },
772
+ // Message: describe the constant folding operation
773
+ (originalNode, ruleData, newNode) => {
774
+ const leftVal = originalNode.left.getValue();
775
+ const rightVal = originalNode.right.getValue();
776
+ const result = newNode.getValue ? newNode.getValue() : newNode.toString();
777
+
778
+ const operatorSymbols = {
779
+ 'add': '+',
780
+ 'subtract': '-',
781
+ 'multiply': getMultiplicationSymbol(),
782
+ 'divide': '÷'
783
+ };
784
+
785
+ const symbol = operatorSymbols[operator] || operator;
786
+ return `Combined constants: ${leftVal} ${symbol} ${rightVal} = ${result}`;
787
+ }
788
+ );
789
+ }
790
+
791
+ /**
792
+ * Creates an identity rule (e.g., x + 0 → x, x * 1 → x).
793
+ * @param {string} name - The name of the identity rule.
794
+ * @param {string} operator - The operator of the binary expression.
795
+ * @param {number} identityValue - The identity value for the operation (e.g., 0 for addition, 1 for multiplication).
796
+ * @param {'left'|'right'|'both'} [side='both'] - Specifies which side the identity value should be on ('left', 'right', or 'both').
797
+ * @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule for the identity operation.
798
+ */
799
+ static createIdentityRule(name, operator, identityValue, side = 'both') {
800
+ return SimplificationEngine.createRule(name,
801
+ // Match: binary op with one constant operand being the identity value
802
+ (node) => {
803
+ if (!SimplificationEngine.isBinaryOp(node, operator)) return false;
804
+
805
+ const constOperandInfo = SimplificationEngine.hasConstantOperand(node);
806
+ if (!constOperandInfo || !constOperandInfo.constant.isConstant() || constOperandInfo.constant.getValue() !== identityValue) {
807
+ return false;
808
+ }
809
+
810
+ if (side === 'left' && node.right.isConstant()) return false;
811
+ if (side === 'right' && node.left.isConstant()) return false;
812
+
813
+ return { other: constOperandInfo.other };
814
+ },
815
+ // Transform: return the other operand
816
+ (node, data) => {
817
+ const newNode = data.other.clone();
818
+
819
+ // Use manual provenance tracking for substitution
820
+ applyProvenance(newNode, node);
821
+
822
+ // Add a flag to tell the highlighting engine that this was a simple identity transform.
823
+ newNode.__isSimpleIdentity = true;
824
+
825
+ return newNode;
826
+ },
827
+ // Message: describe the identity operation
828
+ (originalNode, ruleData, newNode) => {
829
+ const { other } = ruleData;
830
+ const otherStr = other.toString ? other.toString() : 'expression';
831
+
832
+ const operatorNames = {
833
+ 'add': 'addition',
834
+ 'subtract': 'subtraction',
835
+ 'multiply': 'multiplication',
836
+ 'divide': 'division'
837
+ };
838
+
839
+ const operatorSymbols = {
840
+ 'add': '+',
841
+ 'subtract': '-',
842
+ 'multiply': getMultiplicationSymbol(),
843
+ 'divide': '÷'
844
+ };
845
+
846
+ const opName = operatorNames[operator] || operator;
847
+ const symbol = operatorSymbols[operator] || operator;
848
+
849
+ const isLeftIdentity = originalNode.left.isConstant() && originalNode.left.getValue() === identityValue;
850
+ const position = isLeftIdentity ? 'left' : 'right';
851
+
852
+ if (operator === 'add' && identityValue === 0) {
853
+ return `Applied additive identity: "${otherStr} + 0" simplified to "${otherStr}" (adding 0 doesn't change the value)`;
854
+ } else if (operator === 'subtract' && identityValue === 0) {
855
+ return `Applied subtraction identity: "${otherStr} - 0" simplified to "${otherStr}" (subtracting 0 doesn't change the value)`;
856
+ } else if (operator === 'multiply' && identityValue === 1) {
857
+ return `Applied multiplicative identity: "${otherStr} ${getMultiplicationSymbol()} 1" simplified to "${otherStr}" (multiplying by 1 doesn't change the value)`;
858
+ } else if (operator === 'divide' && identityValue === 1) {
859
+ return `Applied division identity: "${otherStr} ÷ 1" simplified to "${otherStr}" (dividing by 1 doesn't change the value)`;
860
+ } else {
861
+ return `Applied ${opName} identity: removed ${identityValue} from ${position} side, leaving "${otherStr}"`;
862
+ }
863
+ }
864
+ );
865
+ }
866
+
867
+ /**
868
+ * Creates a rule for simplifying zero multiplication (e.g., x * 0 = 0).
869
+ */
870
+ static createZeroMultiplicationRule() {
871
+ return SimplificationEngine.createRule("Zero Multiplication",
872
+ (node) => {
873
+ if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
874
+ const constOp = SimplificationEngine.hasConstantOperand(node);
875
+ return constOp && constOp.constant.getValue() === 0;
876
+ },
877
+ (node) => {
878
+ const newNode = SimplificationEngine.createConstant(0, node.getFontSize(), node);
879
+ return newNode;
880
+ },
881
+ (originalNode, ruleData, newNode) => {
882
+ const constOp = SimplificationEngine.hasConstantOperand(originalNode);
883
+ const otherOperand = constOp.other;
884
+ return `Applied zero multiplication: ${utils.nodeToString(otherOperand)} ${getMultiplicationSymbol()} 0 = 0`;
885
+ }
886
+ );
887
+ }
888
888
  }