@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.
- package/README.md +138 -0
- package/canvas/core/canvasConfig.js +203 -0
- package/canvas/core/omdCanvas.js +475 -0
- package/canvas/drawing/segment.js +168 -0
- package/canvas/drawing/stroke.js +386 -0
- package/canvas/events/eventManager.js +435 -0
- package/canvas/events/pointerEventHandler.js +263 -0
- package/canvas/features/focusFrameManager.js +287 -0
- package/canvas/index.js +49 -0
- package/canvas/tools/eraserTool.js +322 -0
- package/canvas/tools/pencilTool.js +319 -0
- package/canvas/tools/selectTool.js +457 -0
- package/canvas/tools/tool.js +223 -0
- package/canvas/tools/toolManager.js +394 -0
- package/canvas/ui/cursor.js +438 -0
- package/canvas/ui/toolbar.js +304 -0
- package/canvas/utils/boundingBox.js +378 -0
- package/canvas/utils/mathUtils.js +259 -0
- package/docs/api/configuration-options.md +104 -0
- package/docs/api/eventManager.md +68 -0
- package/docs/api/focusFrameManager.md +150 -0
- package/docs/api/index.md +91 -0
- package/docs/api/main.md +58 -0
- package/docs/api/omdBinaryExpressionNode.md +227 -0
- package/docs/api/omdCanvas.md +142 -0
- package/docs/api/omdConfigManager.md +192 -0
- package/docs/api/omdConstantNode.md +117 -0
- package/docs/api/omdDisplay.md +121 -0
- package/docs/api/omdEquationNode.md +161 -0
- package/docs/api/omdEquationSequenceNode.md +301 -0
- package/docs/api/omdEquationStack.md +139 -0
- package/docs/api/omdFunctionNode.md +141 -0
- package/docs/api/omdGroupNode.md +182 -0
- package/docs/api/omdHelpers.md +96 -0
- package/docs/api/omdLeafNode.md +163 -0
- package/docs/api/omdNode.md +101 -0
- package/docs/api/omdOperationDisplayNode.md +139 -0
- package/docs/api/omdOperatorNode.md +127 -0
- package/docs/api/omdParenthesisNode.md +122 -0
- package/docs/api/omdPopup.md +117 -0
- package/docs/api/omdPowerNode.md +127 -0
- package/docs/api/omdRationalNode.md +128 -0
- package/docs/api/omdSequenceNode.md +128 -0
- package/docs/api/omdSimplification.md +110 -0
- package/docs/api/omdSqrtNode.md +79 -0
- package/docs/api/omdStepVisualizer.md +115 -0
- package/docs/api/omdStepVisualizerHighlighting.md +61 -0
- package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
- package/docs/api/omdStepVisualizerLayout.md +60 -0
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
- package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
- package/docs/api/omdToolbar.md +102 -0
- package/docs/api/omdTranscriptionService.md +76 -0
- package/docs/api/omdTreeDiff.md +134 -0
- package/docs/api/omdUnaryExpressionNode.md +174 -0
- package/docs/api/omdUtilities.md +70 -0
- package/docs/api/omdVariableNode.md +148 -0
- package/docs/api/selectTool.md +74 -0
- package/docs/api/simplificationEngine.md +98 -0
- package/docs/api/simplificationRules.md +77 -0
- package/docs/api/simplificationUtils.md +64 -0
- package/docs/api/transcribe.md +43 -0
- package/docs/api-reference.md +85 -0
- package/docs/index.html +454 -0
- package/docs/user-guide.md +9 -0
- package/index.js +67 -0
- package/omd/config/omdConfigManager.js +267 -0
- package/omd/core/index.js +150 -0
- package/omd/core/omdEquationStack.js +347 -0
- package/omd/core/omdUtilities.js +115 -0
- package/omd/display/omdDisplay.js +443 -0
- package/omd/display/omdToolbar.js +502 -0
- package/omd/nodes/omdBinaryExpressionNode.js +460 -0
- package/omd/nodes/omdConstantNode.js +142 -0
- package/omd/nodes/omdEquationNode.js +1223 -0
- package/omd/nodes/omdEquationSequenceNode.js +1273 -0
- package/omd/nodes/omdFunctionNode.js +352 -0
- package/omd/nodes/omdGroupNode.js +68 -0
- package/omd/nodes/omdLeafNode.js +77 -0
- package/omd/nodes/omdNode.js +557 -0
- package/omd/nodes/omdOperationDisplayNode.js +322 -0
- package/omd/nodes/omdOperatorNode.js +109 -0
- package/omd/nodes/omdParenthesisNode.js +293 -0
- package/omd/nodes/omdPowerNode.js +236 -0
- package/omd/nodes/omdRationalNode.js +295 -0
- package/omd/nodes/omdSqrtNode.js +308 -0
- package/omd/nodes/omdUnaryExpressionNode.js +178 -0
- package/omd/nodes/omdVariableNode.js +123 -0
- package/omd/simplification/omdSimplification.js +171 -0
- package/omd/simplification/omdSimplificationEngine.js +886 -0
- package/omd/simplification/package.json +6 -0
- package/omd/simplification/rules/binaryRules.js +1037 -0
- package/omd/simplification/rules/functionRules.js +111 -0
- package/omd/simplification/rules/index.js +48 -0
- package/omd/simplification/rules/parenthesisRules.js +19 -0
- package/omd/simplification/rules/powerRules.js +143 -0
- package/omd/simplification/rules/rationalRules.js +475 -0
- package/omd/simplification/rules/sqrtRules.js +48 -0
- package/omd/simplification/rules/unaryRules.js +37 -0
- package/omd/simplification/simplificationRules.js +32 -0
- package/omd/simplification/simplificationUtils.js +1056 -0
- package/omd/step-visualizer/omdStepVisualizer.js +597 -0
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
- package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
- package/omd/utils/omdNodeOverlay.js +638 -0
- package/omd/utils/omdPopup.js +1084 -0
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
- package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
- package/omd/utils/omdTranscriptionService.js +125 -0
- package/omd/utils/omdTreeDiff.js +734 -0
- package/package.json +46 -0
- package/src/index.js +62 -0
- package/src/json-schemas.md +109 -0
- package/src/omd-json-samples.js +115 -0
- package/src/omd.js +109 -0
- package/src/omdApp.js +391 -0
- package/src/omdAppCanvas.js +336 -0
- package/src/omdBalanceHanger.js +172 -0
- package/src/omdColor.js +13 -0
- package/src/omdCoordinatePlane.js +467 -0
- package/src/omdEquation.js +125 -0
- package/src/omdExpression.js +104 -0
- package/src/omdFunction.js +113 -0
- package/src/omdMetaExpression.js +287 -0
- package/src/omdNaturalExpression.js +564 -0
- package/src/omdNode.js +384 -0
- package/src/omdNumber.js +53 -0
- package/src/omdNumberLine.js +107 -0
- package/src/omdNumberTile.js +119 -0
- package/src/omdOperator.js +73 -0
- package/src/omdPowerExpression.js +92 -0
- package/src/omdProblem.js +55 -0
- package/src/omdRatioChart.js +232 -0
- package/src/omdRationalExpression.js +115 -0
- package/src/omdSampleData.js +215 -0
- package/src/omdShapes.js +476 -0
- package/src/omdSpinner.js +148 -0
- package/src/omdString.js +39 -0
- package/src/omdTable.js +369 -0
- package/src/omdTapeDiagram.js +245 -0
- package/src/omdTerm.js +92 -0
- package/src/omdTileEquation.js +349 -0
- package/src/omdVariable.js +51 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { SimplificationEngine } from '../omdSimplificationEngine.js';
|
|
2
|
+
import * as utils from '../simplificationUtils.js';
|
|
3
|
+
|
|
4
|
+
// ===== RATIONAL NODE RULES =====
|
|
5
|
+
export const rationalRules = [
|
|
6
|
+
// Simplify x/x = 1 (variable divided by itself)
|
|
7
|
+
SimplificationEngine.createRule("Variable Self Division",
|
|
8
|
+
(node) => {
|
|
9
|
+
if (node.type !== 'omdRationalNode') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const numerator = SimplificationEngine.unwrapParentheses(node.numerator);
|
|
14
|
+
const denominator = SimplificationEngine.unwrapParentheses(node.denominator);
|
|
15
|
+
|
|
16
|
+
// Check if both numerator and denominator are the same variable
|
|
17
|
+
if (SimplificationEngine.isType(numerator, 'omdVariableNode') &&
|
|
18
|
+
SimplificationEngine.isType(denominator, 'omdVariableNode') &&
|
|
19
|
+
numerator.name === denominator.name) {
|
|
20
|
+
return {
|
|
21
|
+
variable: numerator.name,
|
|
22
|
+
numeratorNode: numerator,
|
|
23
|
+
denominatorNode: denominator
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return false;
|
|
28
|
+
},
|
|
29
|
+
(node, data) => {
|
|
30
|
+
const newNode = SimplificationEngine.createConstant(1, node.getFontSize());
|
|
31
|
+
newNode.provenance.push(data.numeratorNode.id);
|
|
32
|
+
newNode.provenance.push(data.denominatorNode.id);
|
|
33
|
+
newNode.provenance.push(node.id);
|
|
34
|
+
return newNode;
|
|
35
|
+
},
|
|
36
|
+
(originalNode, ruleData, newNode) => {
|
|
37
|
+
const { variable } = ruleData;
|
|
38
|
+
return `Simplified variable self-division: ${variable}/${variable} = 1`;
|
|
39
|
+
}
|
|
40
|
+
),
|
|
41
|
+
|
|
42
|
+
// Simplify x^n/x = x^(n-1) (power divided by base)
|
|
43
|
+
SimplificationEngine.createRule("Power Base Division",
|
|
44
|
+
(node) => {
|
|
45
|
+
if (node.type !== 'omdRationalNode') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const numerator = SimplificationEngine.unwrapParentheses(node.numerator);
|
|
50
|
+
const denominator = SimplificationEngine.unwrapParentheses(node.denominator);
|
|
51
|
+
|
|
52
|
+
// Check if numerator is a power and denominator is the same variable
|
|
53
|
+
if (SimplificationEngine.isType(numerator, 'omdPowerNode') &&
|
|
54
|
+
SimplificationEngine.isType(denominator, 'omdVariableNode') &&
|
|
55
|
+
SimplificationEngine.isType(numerator.base, 'omdVariableNode') &&
|
|
56
|
+
numerator.base.name === denominator.name &&
|
|
57
|
+
numerator.exponent.isConstant()) {
|
|
58
|
+
|
|
59
|
+
const exponent = numerator.exponent.getValue();
|
|
60
|
+
const newExponent = exponent - 1;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
variable: numerator.base.name,
|
|
64
|
+
originalExponent: exponent,
|
|
65
|
+
newExponent: newExponent,
|
|
66
|
+
baseNode: numerator.base,
|
|
67
|
+
powerNode: numerator,
|
|
68
|
+
denominatorNode: denominator
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return false;
|
|
73
|
+
},
|
|
74
|
+
(node, data) => {
|
|
75
|
+
const { variable, newExponent, baseNode, powerNode, denominatorNode } = data;
|
|
76
|
+
const fontSize = node.getFontSize();
|
|
77
|
+
|
|
78
|
+
let newNode;
|
|
79
|
+
if (newExponent === 0) {
|
|
80
|
+
// x^1/x = x^0 = 1
|
|
81
|
+
newNode = SimplificationEngine.createConstant(1, fontSize);
|
|
82
|
+
} else if (newExponent === 1) {
|
|
83
|
+
// x^2/x = x^1 = x
|
|
84
|
+
newNode = baseNode.clone();
|
|
85
|
+
} else {
|
|
86
|
+
// x^n/x = x^(n-1)
|
|
87
|
+
newNode = utils.createPowerTerm(baseNode.clone(), newExponent, fontSize);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Preserve provenance
|
|
91
|
+
newNode.provenance.push(powerNode.id);
|
|
92
|
+
newNode.provenance.push(denominatorNode.id);
|
|
93
|
+
newNode.provenance.push(node.id);
|
|
94
|
+
|
|
95
|
+
return newNode;
|
|
96
|
+
},
|
|
97
|
+
(originalNode, ruleData, newNode) => {
|
|
98
|
+
const { variable, originalExponent, newExponent } = ruleData;
|
|
99
|
+
|
|
100
|
+
if (newExponent === 0) {
|
|
101
|
+
return `Simplified power division: ${variable}^${originalExponent}/${variable} = 1`;
|
|
102
|
+
} else if (newExponent === 1) {
|
|
103
|
+
return `Simplified power division: ${variable}^${originalExponent}/${variable} = ${variable}`;
|
|
104
|
+
} else {
|
|
105
|
+
return `Simplified power division: ${variable}^${originalExponent}/${variable} = ${variable}^${newExponent}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
),
|
|
109
|
+
|
|
110
|
+
// Simplify cx/x = c (monomial divided by its variable)
|
|
111
|
+
SimplificationEngine.createRule("Monomial Variable Division",
|
|
112
|
+
(node) => {
|
|
113
|
+
if (node.type !== 'omdRationalNode') {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const numerator = SimplificationEngine.unwrapParentheses(node.numerator);
|
|
118
|
+
const denominator = SimplificationEngine.unwrapParentheses(node.denominator);
|
|
119
|
+
|
|
120
|
+
// Check if numerator is a monomial and denominator is the same variable
|
|
121
|
+
const monomialInfo = SimplificationEngine.isMonomial(numerator);
|
|
122
|
+
if (monomialInfo &&
|
|
123
|
+
SimplificationEngine.isType(denominator, 'omdVariableNode') &&
|
|
124
|
+
monomialInfo.variable === denominator.name &&
|
|
125
|
+
monomialInfo.power === 1) {
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
coefficient: monomialInfo.coefficient,
|
|
129
|
+
variable: monomialInfo.variable,
|
|
130
|
+
numeratorNode: numerator,
|
|
131
|
+
denominatorNode: denominator
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return false;
|
|
136
|
+
},
|
|
137
|
+
(node, data) => {
|
|
138
|
+
const { coefficient } = data;
|
|
139
|
+
const fontSize = node.getFontSize();
|
|
140
|
+
|
|
141
|
+
const newNode = SimplificationEngine.createConstant(coefficient, fontSize);
|
|
142
|
+
newNode.provenance.push(data.numeratorNode.id);
|
|
143
|
+
newNode.provenance.push(data.denominatorNode.id);
|
|
144
|
+
newNode.provenance.push(node.id);
|
|
145
|
+
|
|
146
|
+
return newNode;
|
|
147
|
+
},
|
|
148
|
+
(originalNode, ruleData, newNode) => {
|
|
149
|
+
const { coefficient, variable } = ruleData;
|
|
150
|
+
return `Simplified monomial division: ${coefficient}${variable}/${variable} = ${coefficient}`;
|
|
151
|
+
}
|
|
152
|
+
),
|
|
153
|
+
|
|
154
|
+
// Simplify fractions (e.g., 6/8 → 3/4, 4/2 → 2)
|
|
155
|
+
SimplificationEngine.createRule("Simplify Fraction",
|
|
156
|
+
(node) => {
|
|
157
|
+
if (node.type !== 'omdRationalNode') {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const numerator = node.numerator;
|
|
162
|
+
const denominator = node.denominator;
|
|
163
|
+
|
|
164
|
+
if (!numerator.isConstant() || !denominator.isConstant()) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const num = numerator.getValue();
|
|
169
|
+
const den = denominator.getValue();
|
|
170
|
+
|
|
171
|
+
if (den === 0) return false;
|
|
172
|
+
|
|
173
|
+
const gcd = utils.gcd(Math.abs(num), Math.abs(den));
|
|
174
|
+
|
|
175
|
+
// Check if we can simplify
|
|
176
|
+
if (gcd > 1 || den < 0) {
|
|
177
|
+
return {
|
|
178
|
+
originalNum: num,
|
|
179
|
+
originalDen: den,
|
|
180
|
+
gcd: gcd,
|
|
181
|
+
simplifiedNum: den < 0 ? -num / gcd : num / gcd,
|
|
182
|
+
simplifiedDen: Math.abs(den) / gcd
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return false;
|
|
187
|
+
},
|
|
188
|
+
(node, data) => {
|
|
189
|
+
const { simplifiedNum, simplifiedDen } = data;
|
|
190
|
+
|
|
191
|
+
if (simplifiedDen === 1) {
|
|
192
|
+
// Fraction reduces to whole number
|
|
193
|
+
const newNode = SimplificationEngine.createConstant(simplifiedNum, node.getFontSize(), node.numerator, node.denominator);
|
|
194
|
+
newNode.provenance.push(node.id);
|
|
195
|
+
return newNode;
|
|
196
|
+
} else {
|
|
197
|
+
// Create simplified fraction
|
|
198
|
+
const newNode = SimplificationEngine.rational(simplifiedNum, simplifiedDen, node.getFontSize());
|
|
199
|
+
_preserveComponentProvenance(newNode, node.numerator, node.denominator);
|
|
200
|
+
newNode.provenance.push(node.id);
|
|
201
|
+
return newNode;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
(originalNode, ruleData, newNode) => {
|
|
205
|
+
const { originalNum, originalDen, simplifiedNum, simplifiedDen, gcd } = ruleData;
|
|
206
|
+
|
|
207
|
+
if (originalDen < 0 && gcd > 1) {
|
|
208
|
+
return `Simplified fraction: ${originalNum}/${originalDen} = ${simplifiedNum}/${simplifiedDen} (corrected sign and reduced by GCD ${gcd})`;
|
|
209
|
+
} else if (simplifiedDen === 1) {
|
|
210
|
+
return `Simplified fraction to whole number: ${originalNum}/${originalDen} = ${simplifiedNum}`;
|
|
211
|
+
} else if (gcd > 1) {
|
|
212
|
+
return `Simplified fraction: ${originalNum}/${originalDen} = ${simplifiedNum}/${simplifiedDen} (reduced by GCD ${gcd})`;
|
|
213
|
+
} else {
|
|
214
|
+
return `Corrected fraction sign: ${originalNum}/${originalDen} = ${simplifiedNum}/${simplifiedDen}`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
),
|
|
218
|
+
|
|
219
|
+
// Multiplication in numerator over constant denominator ((4x)/3 → 4/3*x)
|
|
220
|
+
SimplificationEngine.createRule("Simplify Multiplication in Rational",
|
|
221
|
+
(node) => {
|
|
222
|
+
let numerator = node.numerator;
|
|
223
|
+
if (!node.denominator.isConstant()) return false;
|
|
224
|
+
const denominator = node.denominator.getValue();
|
|
225
|
+
|
|
226
|
+
if (denominator === 0) return false;
|
|
227
|
+
|
|
228
|
+
// Unwrap parentheses and check for multiplication
|
|
229
|
+
numerator = SimplificationEngine.unwrapParentheses(numerator);
|
|
230
|
+
if (!SimplificationEngine.isBinaryOp(numerator, 'multiply')) return false;
|
|
231
|
+
|
|
232
|
+
const constOp = SimplificationEngine.hasConstantOperand(numerator);
|
|
233
|
+
if (!constOp) return false;
|
|
234
|
+
|
|
235
|
+
const numeratorCoeff = constOp.constant.getValue();
|
|
236
|
+
return {
|
|
237
|
+
numeratorCoeff: numeratorCoeff,
|
|
238
|
+
denominator: denominator,
|
|
239
|
+
expression: constOp.other
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
(node, data) => {
|
|
243
|
+
const { numeratorCoeff, denominator, expression } = data;
|
|
244
|
+
|
|
245
|
+
// Create rational coefficient: numeratorCoeff/denominator
|
|
246
|
+
const rationalCoeff = SimplificationEngine.rational(numeratorCoeff, denominator, node.getFontSize());
|
|
247
|
+
|
|
248
|
+
// Preserve lineage from the multiplication components
|
|
249
|
+
_preserveMultiplicationProvenance(rationalCoeff, node);
|
|
250
|
+
|
|
251
|
+
// Create multiplication: (numeratorCoeff/denominator) * expression
|
|
252
|
+
const newNode = SimplificationEngine.createBinaryOp(rationalCoeff, 'multiply', expression.clone(), node.getFontSize());
|
|
253
|
+
|
|
254
|
+
// The new expression node inherits from its direct components.
|
|
255
|
+
if (newNode.right) {
|
|
256
|
+
newNode.right.provenance.push(expression.id);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Preserve lineage from the overall rational node
|
|
260
|
+
newNode.provenance.push(node.id);
|
|
261
|
+
|
|
262
|
+
return newNode;
|
|
263
|
+
},
|
|
264
|
+
(originalNode, ruleData, newNode) => {
|
|
265
|
+
const { numeratorCoeff, denominator } = ruleData;
|
|
266
|
+
|
|
267
|
+
const commonDivisor = utils.gcd(Math.abs(numeratorCoeff), Math.abs(denominator));
|
|
268
|
+
|
|
269
|
+
if (commonDivisor === 1) {
|
|
270
|
+
// This case is more about restructuring (e.g., (2x)/3 -> 2/3 * x), not cancellation.
|
|
271
|
+
return `Separated the coefficient from the variable, changing the fraction into a multiplication.`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const simplifiedNum = numeratorCoeff / commonDivisor;
|
|
275
|
+
const simplifiedDen = denominator / commonDivisor;
|
|
276
|
+
|
|
277
|
+
let message = `The numerator and denominator share a common factor of ${commonDivisor}. `;
|
|
278
|
+
message += `Dividing both by ${commonDivisor} (${numeratorCoeff} ÷ ${commonDivisor} = ${simplifiedNum}, and ${denominator} ÷ ${commonDivisor} = ${simplifiedDen}) simplifies the fraction.`;
|
|
279
|
+
|
|
280
|
+
return message;
|
|
281
|
+
},
|
|
282
|
+
'rational'
|
|
283
|
+
),
|
|
284
|
+
|
|
285
|
+
// Distribute division over addition/subtraction ((4*x+6)/3 → 4/3*x + 2)
|
|
286
|
+
SimplificationEngine.createRule("Simplify Rational Division",
|
|
287
|
+
(node) => {
|
|
288
|
+
if (node.type !== 'omdRationalNode') {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let numerator = node.numerator;
|
|
293
|
+
const denominator = node.denominator;
|
|
294
|
+
|
|
295
|
+
if (!denominator.isConstant()) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Unwrap parentheses to check the underlying structure
|
|
300
|
+
const originalNumerator = numerator;
|
|
301
|
+
numerator = SimplificationEngine.unwrapParentheses(numerator);
|
|
302
|
+
|
|
303
|
+
if (!SimplificationEngine.isBinaryOp(numerator, 'add') &&
|
|
304
|
+
!SimplificationEngine.isBinaryOp(numerator, 'subtract')) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Flatten the sum in the numerator
|
|
309
|
+
const terms = [];
|
|
310
|
+
utils.flattenSum(numerator, terms);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
terms: terms,
|
|
314
|
+
denominator: denominator.getValue()
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
(node, data) => {
|
|
318
|
+
const { terms, denominator } = data;
|
|
319
|
+
const fontSize = node.getFontSize();
|
|
320
|
+
|
|
321
|
+
const distributedTerms = terms.map(term => {
|
|
322
|
+
const termValue = term.node.isConstant() ? term.node.getValue() : 1;
|
|
323
|
+
const numeratorValue = termValue * term.sign;
|
|
324
|
+
|
|
325
|
+
if (term.node.isConstant()) {
|
|
326
|
+
// For constants, create a simplified fraction or integer
|
|
327
|
+
const gcd = utils.gcd(Math.abs(numeratorValue), Math.abs(denominator));
|
|
328
|
+
const simplifiedNum = numeratorValue / gcd;
|
|
329
|
+
const simplifiedDen = denominator / gcd;
|
|
330
|
+
|
|
331
|
+
if (simplifiedDen === 1) {
|
|
332
|
+
const constantNode = SimplificationEngine.createConstant(simplifiedNum, fontSize);
|
|
333
|
+
_preserveDistributionProvenance(constantNode, term.node, node.denominator);
|
|
334
|
+
return { node: constantNode, sign: 1 };
|
|
335
|
+
} else {
|
|
336
|
+
const rationalNode = SimplificationEngine.rational(Math.abs(simplifiedNum), simplifiedDen, fontSize);
|
|
337
|
+
_preserveDistributionProvenance(rationalNode, term.node, node.denominator);
|
|
338
|
+
return { node: rationalNode, sign: simplifiedNum >= 0 ? 1 : -1 };
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
// For non-constants, create coefficient * term / denominator
|
|
342
|
+
if (numeratorValue === denominator) {
|
|
343
|
+
// Coefficient cancels with denominator
|
|
344
|
+
const newNode = term.node.clone();
|
|
345
|
+
newNode.provenance.push(term.node.id);
|
|
346
|
+
return { node: newNode, sign: 1 };
|
|
347
|
+
} else {
|
|
348
|
+
const rationalCoeff = SimplificationEngine.rational(Math.abs(numeratorValue), denominator, fontSize);
|
|
349
|
+
const multiplicationNode = SimplificationEngine.createBinaryOp(rationalCoeff, 'multiply', term.node.clone(), fontSize);
|
|
350
|
+
|
|
351
|
+
_preserveDistributionProvenance(rationalCoeff, term.node, node.denominator);
|
|
352
|
+
multiplicationNode.provenance.push(node.id);
|
|
353
|
+
|
|
354
|
+
return { node: multiplicationNode, sign: numeratorValue >= 0 ? 1 : -1 };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
return utils.buildSumTree(distributedTerms, fontSize);
|
|
360
|
+
},
|
|
361
|
+
(originalNode, ruleData, newNode) => {
|
|
362
|
+
const { terms, denominator } = ruleData;
|
|
363
|
+
const numeratorStr = utils.nodeToString(originalNode.numerator);
|
|
364
|
+
const simplifiedTerms = terms.map(term => {
|
|
365
|
+
const coeff = (term.node.isConstant() ? term.node.getValue() : 1) * term.sign;
|
|
366
|
+
const gcd = utils.gcd(Math.abs(coeff), Math.abs(denominator));
|
|
367
|
+
const newNum = coeff / gcd;
|
|
368
|
+
const newDen = denominator / gcd;
|
|
369
|
+
const variablePart = term.node.isConstant() ? '' : utils.nodeToString(term.node);
|
|
370
|
+
if (newDen === 1) return `${newNum}${variablePart}`;
|
|
371
|
+
return `(${newNum}/${newDen})${variablePart}`;
|
|
372
|
+
}).join(' + ');
|
|
373
|
+
|
|
374
|
+
// Don't add extra parentheses if numerator already has them
|
|
375
|
+
const displayNumerator = numeratorStr.startsWith('(') && numeratorStr.endsWith(')') ?
|
|
376
|
+
numeratorStr : `(${numeratorStr})`;
|
|
377
|
+
return `Distributed division: ${displayNumerator}/${denominator} = ${simplifiedTerms}`;
|
|
378
|
+
},
|
|
379
|
+
'rational'
|
|
380
|
+
)
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
// ===== HELPER FUNCTIONS FOR PROVENANCE =====
|
|
384
|
+
// These helper functions consolidate the repetitive provenance logic
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Preserves provenance from numerator and denominator components
|
|
388
|
+
*/
|
|
389
|
+
function _preserveComponentProvenance(newNode, numerator, denominator) {
|
|
390
|
+
if (numerator?.provenance) {
|
|
391
|
+
numerator.provenance.forEach(id => {
|
|
392
|
+
if (!newNode.provenance.includes(id)) newNode.provenance.push(id);
|
|
393
|
+
});
|
|
394
|
+
} else if (numerator) {
|
|
395
|
+
newNode.provenance.push(numerator.id);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (denominator?.provenance) {
|
|
399
|
+
denominator.provenance.forEach(id => {
|
|
400
|
+
if (!newNode.provenance.includes(id)) newNode.provenance.push(id);
|
|
401
|
+
});
|
|
402
|
+
} else if (denominator) {
|
|
403
|
+
newNode.provenance.push(denominator.id);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Preserves provenance from multiplication components in rational nodes
|
|
409
|
+
*/
|
|
410
|
+
function _preserveMultiplicationProvenance(rationalCoeff, originalNode) {
|
|
411
|
+
// Handle numerator constant provenance
|
|
412
|
+
const numeratorConstant = originalNode.numerator?.left?.isConstant() ?
|
|
413
|
+
originalNode.numerator.left : originalNode.numerator?.right;
|
|
414
|
+
|
|
415
|
+
if (numeratorConstant?.provenance) {
|
|
416
|
+
numeratorConstant.provenance.forEach(id => {
|
|
417
|
+
if (!rationalCoeff.provenance.includes(id)) {
|
|
418
|
+
rationalCoeff.provenance.push(id);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
} else if (numeratorConstant) {
|
|
422
|
+
rationalCoeff.provenance.push(numeratorConstant.id);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Handle denominator provenance
|
|
426
|
+
if (originalNode.denominator?.provenance) {
|
|
427
|
+
originalNode.denominator.provenance.forEach(id => {
|
|
428
|
+
if (!rationalCoeff.provenance.includes(id)) {
|
|
429
|
+
rationalCoeff.provenance.push(id);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
} else if (originalNode.denominator) {
|
|
433
|
+
rationalCoeff.provenance.push(originalNode.denominator.id);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Preserves provenance for distributed rational terms
|
|
439
|
+
*/
|
|
440
|
+
function _preserveDistributionProvenance(rationalNode, termNode, denominatorNode) {
|
|
441
|
+
// Numerator should preserve lineage from the original term
|
|
442
|
+
if (rationalNode.numerator && termNode) {
|
|
443
|
+
rationalNode.numerator.provenance = [];
|
|
444
|
+
|
|
445
|
+
if (termNode.provenance?.length > 0) {
|
|
446
|
+
termNode.provenance.forEach(id => {
|
|
447
|
+
rationalNode.numerator.provenance.push(id);
|
|
448
|
+
});
|
|
449
|
+
} else {
|
|
450
|
+
rationalNode.numerator.provenance.push(termNode.id);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Denominator should preserve lineage from the original denominator
|
|
455
|
+
if (rationalNode.denominator && denominatorNode) {
|
|
456
|
+
rationalNode.denominator.provenance = [];
|
|
457
|
+
|
|
458
|
+
if (denominatorNode.provenance?.length > 0) {
|
|
459
|
+
denominatorNode.provenance.forEach(id => {
|
|
460
|
+
rationalNode.denominator.provenance.push(id);
|
|
461
|
+
});
|
|
462
|
+
} else {
|
|
463
|
+
rationalNode.denominator.provenance.push(denominatorNode.id);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Preserve term's provenance on the rational node itself
|
|
468
|
+
if (termNode.provenance?.length > 0) {
|
|
469
|
+
termNode.provenance.forEach(id => {
|
|
470
|
+
if (!rationalNode.provenance.includes(id)) {
|
|
471
|
+
rationalNode.provenance.push(id);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SimplificationEngine } from '../omdSimplificationEngine.js';
|
|
2
|
+
import * as utils from '../simplificationUtils.js';
|
|
3
|
+
|
|
4
|
+
// ===== SQRT NODE RULES =====
|
|
5
|
+
export const sqrtRules = [
|
|
6
|
+
// Simplify square root of perfect squares (sqrt(9) → 3)
|
|
7
|
+
SimplificationEngine.createRule("Simplify Square Root of Constant",
|
|
8
|
+
(node) => {
|
|
9
|
+
if (!node.argument.isConstant()) return false;
|
|
10
|
+
const value = node.argument.getValue();
|
|
11
|
+
if (value < 0) return false;
|
|
12
|
+
|
|
13
|
+
const sqrt = Math.sqrt(value);
|
|
14
|
+
return Number.isInteger(sqrt) ? { value: sqrt } : false;
|
|
15
|
+
},
|
|
16
|
+
(node, data) => {
|
|
17
|
+
const newNode = SimplificationEngine.createConstant(data.value, node.getFontSize());
|
|
18
|
+
newNode.provenance.push(node.id, node.argument.id);
|
|
19
|
+
return newNode;
|
|
20
|
+
},
|
|
21
|
+
(originalNode, ruleData, newNode) => {
|
|
22
|
+
const value = originalNode.argument.getValue();
|
|
23
|
+
const result = ruleData.value;
|
|
24
|
+
return `Calculated square root: √${value} = ${result}`;
|
|
25
|
+
}
|
|
26
|
+
),
|
|
27
|
+
|
|
28
|
+
// Simplify sqrt(x^2) → x (assuming x ≥ 0 for simplicity)
|
|
29
|
+
SimplificationEngine.createRule("Square Root of Square",
|
|
30
|
+
(node) => {
|
|
31
|
+
if (!node.argument || node.argument.type !== 'omdPowerNode') return false;
|
|
32
|
+
|
|
33
|
+
const powerNode = node.argument;
|
|
34
|
+
if (!powerNode.exponent.isConstant() || powerNode.exponent.getValue() !== 2) return false;
|
|
35
|
+
|
|
36
|
+
return { base: powerNode.base };
|
|
37
|
+
},
|
|
38
|
+
(node, data) => {
|
|
39
|
+
const newNode = data.base.clone();
|
|
40
|
+
newNode.provenance.push(node.id, node.argument.id);
|
|
41
|
+
return newNode;
|
|
42
|
+
},
|
|
43
|
+
(originalNode, ruleData, newNode) => {
|
|
44
|
+
const baseStr = utils.nodeToString(ruleData.base);
|
|
45
|
+
return `Simplified square root of square: √(${baseStr}²) = ${baseStr}`;
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SimplificationEngine } from '../omdSimplificationEngine.js';
|
|
2
|
+
import * as utils from '../simplificationUtils.js';
|
|
3
|
+
|
|
4
|
+
// ===== UNARY EXPRESSION RULES =====
|
|
5
|
+
export const unaryRules = [
|
|
6
|
+
// Simplify double negation (-(-x) → x)
|
|
7
|
+
SimplificationEngine.createRule("Simplify Double Negation",
|
|
8
|
+
(node) => {
|
|
9
|
+
return node.operation === 'unaryMinus' &&
|
|
10
|
+
SimplificationEngine.isType(node.argument, 'omdUnaryExpressionNode') &&
|
|
11
|
+
node.argument.operation === 'unaryMinus';
|
|
12
|
+
},
|
|
13
|
+
(node) => {
|
|
14
|
+
const newNode = node.argument.argument.clone();
|
|
15
|
+
newNode.provenance.push(node.id, node.argument.id);
|
|
16
|
+
return newNode;
|
|
17
|
+
},
|
|
18
|
+
(originalNode, ruleData, newNode) => {
|
|
19
|
+
const childStr = utils.nodeToString(originalNode.argument.argument);
|
|
20
|
+
return `Simplified double negation: -(-${childStr}) = ${childStr}`;
|
|
21
|
+
}
|
|
22
|
+
),
|
|
23
|
+
|
|
24
|
+
// Remove unary plus (+x → x)
|
|
25
|
+
SimplificationEngine.createRule("Remove Unary Plus",
|
|
26
|
+
(node) => node.operation === 'unaryPlus',
|
|
27
|
+
(node) => {
|
|
28
|
+
const newNode = node.argument.clone();
|
|
29
|
+
newNode.provenance.push(node.id);
|
|
30
|
+
return newNode;
|
|
31
|
+
},
|
|
32
|
+
(originalNode, ruleData, newNode) => {
|
|
33
|
+
const childStr = utils.nodeToString(originalNode.argument);
|
|
34
|
+
return `Removed unary plus from "${childStr}"`;
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Import rules progressively to identify the problematic one
|
|
2
|
+
import { unaryRules } from './rules/unaryRules.js';
|
|
3
|
+
import { parenthesisRules } from './rules/parenthesisRules.js';
|
|
4
|
+
import { sqrtRules } from './rules/sqrtRules.js';
|
|
5
|
+
import { powerRules } from './rules/powerRules.js';
|
|
6
|
+
import { functionRules } from './rules/functionRules.js';
|
|
7
|
+
import { rationalRules } from './rules/rationalRules.js';
|
|
8
|
+
import { binaryRules } from './rules/binaryRules.js';
|
|
9
|
+
|
|
10
|
+
export const rules = {
|
|
11
|
+
binary: binaryRules,
|
|
12
|
+
rational: rationalRules,
|
|
13
|
+
parenthesis: parenthesisRules,
|
|
14
|
+
unary: unaryRules,
|
|
15
|
+
power: powerRules,
|
|
16
|
+
sqrt: sqrtRules,
|
|
17
|
+
function: functionRules
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const ruleMap = {
|
|
21
|
+
'omdBinaryExpressionNode': rules.binary,
|
|
22
|
+
'omdRationalNode': rules.rational,
|
|
23
|
+
'omdParenthesisNode': rules.parenthesis,
|
|
24
|
+
'omdUnaryExpressionNode': rules.unary,
|
|
25
|
+
'omdPowerNode': rules.power,
|
|
26
|
+
'omdSqrtNode': rules.sqrt,
|
|
27
|
+
'omdFunctionNode': rules.function
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function getRulesForNode(node) {
|
|
31
|
+
return ruleMap[node.type] || [];
|
|
32
|
+
}
|