@teachinglab/omd 0.1.0

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