@teachinglab/omd 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -251
- package/README.old.md +137 -137
- package/canvas/core/canvasConfig.js +202 -202
- package/canvas/drawing/segment.js +167 -167
- package/canvas/drawing/stroke.js +385 -385
- package/canvas/events/eventManager.js +444 -444
- package/canvas/events/pointerEventHandler.js +262 -262
- package/canvas/index.js +48 -48
- package/canvas/tools/PointerTool.js +71 -71
- package/canvas/tools/tool.js +222 -222
- package/canvas/utils/boundingBox.js +377 -377
- package/canvas/utils/mathUtils.js +258 -258
- package/docs/api/configuration-options.md +198 -198
- package/docs/api/eventManager.md +82 -82
- package/docs/api/focusFrameManager.md +144 -144
- package/docs/api/index.md +105 -105
- package/docs/api/main.md +62 -62
- package/docs/api/omdBinaryExpressionNode.md +86 -86
- package/docs/api/omdCanvas.md +83 -83
- package/docs/api/omdConfigManager.md +112 -112
- package/docs/api/omdConstantNode.md +52 -52
- package/docs/api/omdDisplay.md +87 -87
- package/docs/api/omdEquationNode.md +174 -174
- package/docs/api/omdEquationSequenceNode.md +258 -258
- package/docs/api/omdEquationStack.md +192 -192
- package/docs/api/omdFunctionNode.md +82 -82
- package/docs/api/omdGroupNode.md +78 -78
- package/docs/api/omdHelpers.md +87 -87
- package/docs/api/omdLeafNode.md +85 -85
- package/docs/api/omdNode.md +201 -201
- package/docs/api/omdOperationDisplayNode.md +117 -117
- package/docs/api/omdOperatorNode.md +91 -91
- package/docs/api/omdParenthesisNode.md +133 -133
- package/docs/api/omdPopup.md +191 -191
- package/docs/api/omdPowerNode.md +131 -131
- package/docs/api/omdRationalNode.md +144 -144
- package/docs/api/omdSequenceNode.md +128 -128
- package/docs/api/omdSimplification.md +78 -78
- package/docs/api/omdSqrtNode.md +144 -144
- package/docs/api/omdStepVisualizer.md +146 -146
- package/docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/docs/api/omdStepVisualizerLayout.md +70 -70
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/docs/api/omdToolbar.md +130 -130
- package/docs/api/omdTranscriptionService.md +95 -95
- package/docs/api/omdTreeDiff.md +169 -169
- package/docs/api/omdUnaryExpressionNode.md +137 -137
- package/docs/api/omdUtilities.md +82 -82
- package/docs/api/omdVariableNode.md +123 -123
- package/docs/api/selectTool.md +74 -74
- package/docs/api/simplificationEngine.md +97 -97
- package/docs/api/simplificationRules.md +76 -76
- package/docs/api/simplificationUtils.md +64 -64
- package/docs/api/transcribe.md +43 -43
- package/docs/api-reference.md +85 -85
- package/docs/index.html +453 -453
- package/docs/index.md +38 -38
- package/docs/omd-objects.md +258 -258
- package/index.js +79 -79
- package/jsvg/index.js +3 -0
- package/jsvg/jsvg.js +898 -898
- package/jsvg/jsvgComponents.js +357 -358
- package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
- package/npm-docs/README.md +251 -251
- package/npm-docs/api/api-reference.md +85 -85
- package/npm-docs/api/configuration-options.md +198 -198
- package/npm-docs/api/eventManager.md +82 -82
- package/npm-docs/api/expression-nodes.md +561 -561
- package/npm-docs/api/focusFrameManager.md +144 -144
- package/npm-docs/api/index.md +105 -105
- package/npm-docs/api/main.md +62 -62
- package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
- package/npm-docs/api/omdCanvas.md +83 -83
- package/npm-docs/api/omdConfigManager.md +112 -112
- package/npm-docs/api/omdConstantNode.md +52 -52
- package/npm-docs/api/omdDisplay.md +87 -87
- package/npm-docs/api/omdEquationNode.md +174 -174
- package/npm-docs/api/omdEquationSequenceNode.md +258 -258
- package/npm-docs/api/omdEquationStack.md +192 -192
- package/npm-docs/api/omdFunctionNode.md +82 -82
- package/npm-docs/api/omdGroupNode.md +78 -78
- package/npm-docs/api/omdHelpers.md +87 -87
- package/npm-docs/api/omdLeafNode.md +85 -85
- package/npm-docs/api/omdNode.md +201 -201
- package/npm-docs/api/omdOperationDisplayNode.md +117 -117
- package/npm-docs/api/omdOperatorNode.md +91 -91
- package/npm-docs/api/omdParenthesisNode.md +133 -133
- package/npm-docs/api/omdPopup.md +191 -191
- package/npm-docs/api/omdPowerNode.md +131 -131
- package/npm-docs/api/omdRationalNode.md +144 -144
- package/npm-docs/api/omdSequenceNode.md +128 -128
- package/npm-docs/api/omdSimplification.md +78 -78
- package/npm-docs/api/omdSqrtNode.md +144 -144
- package/npm-docs/api/omdStepVisualizer.md +146 -146
- package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
- package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/npm-docs/api/omdToolbar.md +130 -130
- package/npm-docs/api/omdTranscriptionService.md +95 -95
- package/npm-docs/api/omdTreeDiff.md +169 -169
- package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
- package/npm-docs/api/omdUtilities.md +82 -82
- package/npm-docs/api/omdVariableNode.md +123 -123
- package/npm-docs/api/selectTool.md +74 -74
- package/npm-docs/api/simplificationEngine.md +97 -97
- package/npm-docs/api/simplificationRules.md +76 -76
- package/npm-docs/api/simplificationUtils.md +64 -64
- package/npm-docs/api/transcribe.md +43 -43
- package/npm-docs/guides/equations.md +854 -854
- package/npm-docs/guides/factory-functions.md +354 -354
- package/npm-docs/guides/getting-started.md +318 -318
- package/npm-docs/guides/quick-examples.md +525 -525
- package/npm-docs/guides/visualizations.md +682 -682
- package/npm-docs/index.html +12 -0
- package/npm-docs/json-schemas.md +826 -826
- package/omd/config/omdConfigManager.js +279 -267
- package/omd/core/index.js +158 -158
- package/omd/core/omdEquationStack.js +546 -546
- package/omd/core/omdUtilities.js +113 -113
- package/omd/display/omdDisplay.js +969 -962
- package/omd/display/omdToolbar.js +501 -501
- package/omd/nodes/omdBinaryExpressionNode.js +459 -459
- package/omd/nodes/omdConstantNode.js +141 -141
- package/omd/nodes/omdEquationNode.js +1327 -1327
- package/omd/nodes/omdFunctionNode.js +351 -351
- package/omd/nodes/omdGroupNode.js +67 -67
- package/omd/nodes/omdLeafNode.js +76 -76
- package/omd/nodes/omdNode.js +556 -556
- package/omd/nodes/omdOperationDisplayNode.js +321 -321
- package/omd/nodes/omdOperatorNode.js +108 -108
- package/omd/nodes/omdParenthesisNode.js +292 -292
- package/omd/nodes/omdPowerNode.js +235 -235
- package/omd/nodes/omdRationalNode.js +295 -295
- package/omd/nodes/omdSqrtNode.js +307 -307
- package/omd/nodes/omdUnaryExpressionNode.js +227 -227
- package/omd/nodes/omdVariableNode.js +122 -122
- package/omd/simplification/omdSimplification.js +140 -140
- package/omd/simplification/omdSimplificationEngine.js +887 -887
- package/omd/simplification/package.json +5 -5
- package/omd/simplification/rules/binaryRules.js +1037 -1037
- package/omd/simplification/rules/functionRules.js +111 -111
- package/omd/simplification/rules/index.js +48 -48
- package/omd/simplification/rules/parenthesisRules.js +19 -19
- package/omd/simplification/rules/powerRules.js +143 -143
- package/omd/simplification/rules/rationalRules.js +725 -725
- package/omd/simplification/rules/sqrtRules.js +48 -48
- package/omd/simplification/rules/unaryRules.js +37 -37
- package/omd/simplification/simplificationRules.js +31 -31
- package/omd/simplification/simplificationUtils.js +1055 -1055
- package/omd/step-visualizer/omdStepVisualizer.js +947 -947
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
- package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
- package/omd/utils/aiNextEquationStep.js +106 -106
- package/omd/utils/omdNodeOverlay.js +638 -638
- package/omd/utils/omdPopup.js +1203 -1203
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
- package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
- package/omd/utils/omdTranscriptionService.js +123 -123
- package/omd/utils/omdTreeDiff.js +733 -733
- package/package.json +59 -56
- package/readme.html +184 -120
- package/src/index.js +74 -74
- package/src/json-schemas.md +576 -576
- package/src/omd-json-samples.js +147 -147
- package/src/omdApp.js +391 -391
- package/src/omdAppCanvas.js +335 -335
- package/src/omdBalanceHanger.js +199 -199
- package/src/omdColor.js +13 -13
- package/src/omdCoordinatePlane.js +541 -541
- package/src/omdExpression.js +115 -115
- package/src/omdFactory.js +150 -150
- package/src/omdFunction.js +114 -114
- package/src/omdMetaExpression.js +290 -290
- package/src/omdNaturalExpression.js +563 -563
- package/src/omdNode.js +383 -383
- package/src/omdNumber.js +52 -52
- package/src/omdNumberLine.js +114 -112
- package/src/omdNumberTile.js +118 -118
- package/src/omdOperator.js +72 -72
- package/src/omdPowerExpression.js +91 -91
- package/src/omdProblem.js +259 -259
- package/src/omdRatioChart.js +251 -251
- package/src/omdRationalExpression.js +114 -114
- package/src/omdSampleData.js +215 -215
- package/src/omdShapes.js +512 -512
- package/src/omdSpinner.js +151 -151
- package/src/omdString.js +49 -49
- package/src/omdTable.js +498 -498
- package/src/omdTapeDiagram.js +244 -244
- package/src/omdTerm.js +91 -91
- package/src/omdTileEquation.js +349 -349
- package/src/omdUtils.js +84 -84
- package/src/omdVariable.js +51 -51
|
@@ -1,295 +1,295 @@
|
|
|
1
|
-
import { omdNode } from "./omdNode.js";
|
|
2
|
-
import { astToOmdType, getNodeForAST } from "../core/omdUtilities.js";
|
|
3
|
-
import { omdConstantNode } from "./omdConstantNode.js";
|
|
4
|
-
import { simplifyStep } from "../simplification/omdSimplification.js";
|
|
5
|
-
import { jsvgLine } from '@teachinglab/jsvg';
|
|
6
|
-
// Helper function for GCD
|
|
7
|
-
function gcd(a, b) {
|
|
8
|
-
return b === 0 ? a : gcd(b, a % b);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class omdRationalNode extends omdNode {
|
|
12
|
-
constructor(astNodeData) {
|
|
13
|
-
super(astNodeData);
|
|
14
|
-
this.type = "omdRationalNode";
|
|
15
|
-
|
|
16
|
-
if (!astNodeData.args || astNodeData.args.length < 2) {
|
|
17
|
-
console.error("omdRationalNode requires numerator and denominator");
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
this.value = this.parseValue();
|
|
22
|
-
this.numerator = this.createOperand(astNodeData.args[0]);
|
|
23
|
-
this.denominator = this.createOperand(astNodeData.args[1]);
|
|
24
|
-
|
|
25
|
-
// Populate the argumentNodeList for mathematical child nodes
|
|
26
|
-
this.argumentNodeList.numerator = this.numerator;
|
|
27
|
-
this.argumentNodeList.denominator = this.denominator;
|
|
28
|
-
|
|
29
|
-
// Gerard: Create purely aesthetic fraction line
|
|
30
|
-
this.fractionLine = new jsvgLine();
|
|
31
|
-
this.fractionLine.setStrokeColor('black');
|
|
32
|
-
this.fractionLine.setStrokeWidth(2);
|
|
33
|
-
this.addChild(this.fractionLine);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
parseValue() {
|
|
37
|
-
return "/";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
createOperand(ast) {
|
|
41
|
-
let OperandType = getNodeForAST(ast);
|
|
42
|
-
let child = new OperandType(ast);
|
|
43
|
-
this.addChild(child);
|
|
44
|
-
|
|
45
|
-
// Preserve lineage from the original AST node if it exists
|
|
46
|
-
// Note: The child constructor should already handle provenance from the AST
|
|
47
|
-
// This is just a safety check for cases where AST has explicit provenance
|
|
48
|
-
try {
|
|
49
|
-
if (ast.id && ast.provenance && ast.provenance.length > 0) {
|
|
50
|
-
// Only override if the child doesn't already have provenance
|
|
51
|
-
if (!child.provenance || child.provenance.length === 0) {
|
|
52
|
-
child.provenance = [...ast.provenance];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
} catch (error) {
|
|
56
|
-
// If lineage preservation fails, continue without it
|
|
57
|
-
console.debug('Failed to preserve lineage in createOperand:', error.message);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return child;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
computeDimensions() {
|
|
64
|
-
// Gerard: Shrink font size of numerator and denominator
|
|
65
|
-
this.numerator.setFontSize(this.getFontSize() * 5 / 6);
|
|
66
|
-
this.denominator.setFontSize(this.getFontSize() * 5 / 6);
|
|
67
|
-
|
|
68
|
-
this.numerator.computeDimensions();
|
|
69
|
-
this.denominator.computeDimensions();
|
|
70
|
-
|
|
71
|
-
// Extra horizontal spacing and vertical padding
|
|
72
|
-
const ratio = this.getFontSize() / this.getRootFontSize();
|
|
73
|
-
const spacing = 8 * ratio;
|
|
74
|
-
const padding = 8 * ratio;
|
|
75
|
-
|
|
76
|
-
let spacedWidth = Math.max(this.numerator.width, this.denominator.width) + spacing;
|
|
77
|
-
let spacedHeight = this.numerator.height + this.denominator.height + padding;
|
|
78
|
-
|
|
79
|
-
this.setWidthAndHeight(spacedWidth, spacedHeight);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
updateLayout() {
|
|
83
|
-
const fractionSpacing = 2;
|
|
84
|
-
|
|
85
|
-
// Gerard: Check math
|
|
86
|
-
// Position numerator (top, centered)
|
|
87
|
-
const numX = (this.width - this.numerator.width) / 2;
|
|
88
|
-
this.numerator.updateLayout();
|
|
89
|
-
this.numerator.setPosition(numX, fractionSpacing);
|
|
90
|
-
|
|
91
|
-
// Position fraction line (middle)
|
|
92
|
-
const lineY = this.numerator.height + fractionSpacing * 2;
|
|
93
|
-
this.fractionLine.setEndpoints(0, lineY, this.width, lineY);
|
|
94
|
-
|
|
95
|
-
// Position denominator (bottom, centered)
|
|
96
|
-
const denX = (this.width - this.denominator.width) / 2;
|
|
97
|
-
const denY = lineY + fractionSpacing;
|
|
98
|
-
this.denominator.updateLayout();
|
|
99
|
-
this.denominator.setPosition(denX, denY);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* For rational nodes, the alignment baseline is the fraction bar itself.
|
|
104
|
-
* @override
|
|
105
|
-
* @returns {number} The y-coordinate for alignment.
|
|
106
|
-
*/
|
|
107
|
-
getAlignmentBaseline() {
|
|
108
|
-
// Corresponds to the 'lineY' calculation in updateLayout.
|
|
109
|
-
const fractionSpacing = 2;
|
|
110
|
-
return this.numerator.height + fractionSpacing * 2;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
isConstant() {
|
|
114
|
-
return this.numerator.isConstant() && this.denominator.isConstant();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Retrieves the rational value of the node as a numerator/denominator pair.
|
|
119
|
-
* @returns {{num: number, den: number}}
|
|
120
|
-
*/
|
|
121
|
-
getRationalValue() {
|
|
122
|
-
if (this.isConstant()) {
|
|
123
|
-
return { num: this.numerator.getValue(), den: this.denominator.getValue() };
|
|
124
|
-
}
|
|
125
|
-
throw new Error('Rational node is not constant');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
getValue() {
|
|
129
|
-
if (this.isConstant()) {
|
|
130
|
-
const { num, den } = this.getRationalValue();
|
|
131
|
-
if (den === 0) return NaN;
|
|
132
|
-
return num / den;
|
|
133
|
-
}
|
|
134
|
-
throw new Error("Node is not a constant expression");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
clone() {
|
|
138
|
-
let newAstData;
|
|
139
|
-
if (typeof this.astNodeData.clone === 'function') {
|
|
140
|
-
newAstData = this.astNodeData.clone();
|
|
141
|
-
} else {
|
|
142
|
-
newAstData = JSON.parse(JSON.stringify(this.astNodeData));
|
|
143
|
-
}
|
|
144
|
-
const clone = new omdRationalNode(newAstData);
|
|
145
|
-
|
|
146
|
-
// Keep the backRect from the clone, not from 'this'
|
|
147
|
-
const backRect = clone.backRect;
|
|
148
|
-
clone.removeAllChildren();
|
|
149
|
-
clone.addChild(backRect);
|
|
150
|
-
|
|
151
|
-
clone.numerator = this.numerator.clone();
|
|
152
|
-
clone.denominator = this.denominator.clone();
|
|
153
|
-
|
|
154
|
-
// jsvgLine does not have a clone method, so we create a new one.
|
|
155
|
-
clone.fractionLine = new jsvgLine();
|
|
156
|
-
clone.fractionLine.setStrokeColor('black');
|
|
157
|
-
clone.fractionLine.setStrokeWidth(2);
|
|
158
|
-
|
|
159
|
-
clone.addChild(clone.numerator);
|
|
160
|
-
clone.addChild(clone.fractionLine);
|
|
161
|
-
clone.addChild(clone.denominator);
|
|
162
|
-
|
|
163
|
-
// Explicitly update the argumentNodeList in the cloned node
|
|
164
|
-
clone.argumentNodeList.numerator = clone.numerator;
|
|
165
|
-
clone.argumentNodeList.denominator = clone.denominator;
|
|
166
|
-
|
|
167
|
-
// The crucial step: link the clone to its origin
|
|
168
|
-
clone.provenance.push(this.id);
|
|
169
|
-
|
|
170
|
-
return clone;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Converts the omdRationalNode to a math.js AST node.
|
|
175
|
-
* @returns {Object} A math.js-compatible AST node.
|
|
176
|
-
*/
|
|
177
|
-
toMathJSNode() {
|
|
178
|
-
const astNode = {
|
|
179
|
-
type: 'OperatorNode', op: '/', fn: 'divide',
|
|
180
|
-
args: [this.numerator.toMathJSNode(), this.denominator.toMathJSNode()],
|
|
181
|
-
id: this.id,
|
|
182
|
-
provenance: this.provenance
|
|
183
|
-
};
|
|
184
|
-
// Add a clone method to maintain compatibility with math.js's expectations.
|
|
185
|
-
astNode.clone = function() {
|
|
186
|
-
const clonedNode = { ...this };
|
|
187
|
-
clonedNode.args = this.args.map(arg => arg.clone());
|
|
188
|
-
return clonedNode;
|
|
189
|
-
};
|
|
190
|
-
return astNode;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Converts the rational node to a string representation.
|
|
195
|
-
* Ensures that complex numerators/denominators are wrapped in parentheses
|
|
196
|
-
* to maintain correct order of operations.
|
|
197
|
-
* @returns {string} The string representation of the fraction.
|
|
198
|
-
*/
|
|
199
|
-
toString() {
|
|
200
|
-
const numStr = this.numerator.toString();
|
|
201
|
-
const denStr = this.denominator.toString();
|
|
202
|
-
|
|
203
|
-
// Check if the children are complex expressions that require parentheses
|
|
204
|
-
const numNeedsParens = this.numerator.type === 'omdBinaryExpressionNode';
|
|
205
|
-
const denNeedsParens = this.denominator.type === 'omdBinaryExpressionNode';
|
|
206
|
-
|
|
207
|
-
const finalNumStr = numNeedsParens ? `(${numStr})` : numStr;
|
|
208
|
-
const finalDenStr = denNeedsParens ? `(${denStr})` : denStr;
|
|
209
|
-
|
|
210
|
-
return `${finalNumStr} / ${finalDenStr}`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Evaluates the rational expression.
|
|
215
|
-
* @param {Object} variables - A map of variable names to their values.
|
|
216
|
-
* @returns {number} The result of the division.
|
|
217
|
-
*/
|
|
218
|
-
evaluate(variables = {}) {
|
|
219
|
-
const numValue = this.numerator.evaluate(variables);
|
|
220
|
-
const denValue = this.denominator.evaluate(variables);
|
|
221
|
-
|
|
222
|
-
if (denValue === 0) {
|
|
223
|
-
throw new Error("Division by zero in rational expression.");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return numValue / denValue;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Reduce the fraction to lowest terms if it's constant.
|
|
231
|
-
* @returns {omdRationalNode|omdNode} Reduced fraction or the original node.
|
|
232
|
-
*/
|
|
233
|
-
reduce() {
|
|
234
|
-
if (this.isConstant()) {
|
|
235
|
-
let { num, den } = this.getRationalValue();
|
|
236
|
-
if (den < 0) { // Keep denominator positive
|
|
237
|
-
num = -num;
|
|
238
|
-
den = -den;
|
|
239
|
-
}
|
|
240
|
-
const commonDivisor = gcd(Math.abs(num), Math.abs(den));
|
|
241
|
-
const newNum = num / commonDivisor;
|
|
242
|
-
const newDen = den / commonDivisor;
|
|
243
|
-
|
|
244
|
-
if (newNum === this.numerator.getValue() && newDen === this.denominator.getValue()) {
|
|
245
|
-
return this; // Already reduced
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (newDen === 1) {
|
|
249
|
-
return omdConstantNode.fromNumber(newNum);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const newAst = {
|
|
253
|
-
type: 'OperatorNode', op: '/', fn: 'divide',
|
|
254
|
-
args: [
|
|
255
|
-
omdConstantNode.fromNumber(newNum).toMathJSNode(),
|
|
256
|
-
omdConstantNode.fromNumber(newDen).toMathJSNode()
|
|
257
|
-
]
|
|
258
|
-
};
|
|
259
|
-
return new omdRationalNode(newAst);
|
|
260
|
-
}
|
|
261
|
-
return this.clone(); // Return a clone if not constant
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Check if the fraction is proper (absolute value of numerator < absolute value of denominator).
|
|
266
|
-
* Only works for constant fractions.
|
|
267
|
-
* @returns {boolean|null} True if proper, false if improper, null if not constant.
|
|
268
|
-
*/
|
|
269
|
-
isProper() {
|
|
270
|
-
if (this.isConstant()) {
|
|
271
|
-
const { num, den } = this.getRationalValue();
|
|
272
|
-
return Math.abs(num) < Math.abs(den);
|
|
273
|
-
}
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Create a fraction node from a string.
|
|
279
|
-
* @static
|
|
280
|
-
* @param {string} expressionString - Expression with division
|
|
281
|
-
* @returns {omdRationalNode}
|
|
282
|
-
*/
|
|
283
|
-
static fromString(expressionString) {
|
|
284
|
-
try {
|
|
285
|
-
const ast = window.math.parse(expressionString);
|
|
286
|
-
if (ast.type !== 'OperatorNode' || ast.op !== '/') {
|
|
287
|
-
throw new Error("Expression is not a division operation");
|
|
288
|
-
}
|
|
289
|
-
return new omdRationalNode(ast);
|
|
290
|
-
} catch (error) {
|
|
291
|
-
console.error("Failed to create rational node from string:", error);
|
|
292
|
-
throw error;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
1
|
+
import { omdNode } from "./omdNode.js";
|
|
2
|
+
import { astToOmdType, getNodeForAST } from "../core/omdUtilities.js";
|
|
3
|
+
import { omdConstantNode } from "./omdConstantNode.js";
|
|
4
|
+
import { simplifyStep } from "../simplification/omdSimplification.js";
|
|
5
|
+
import { jsvgLine } from '@teachinglab/jsvg';
|
|
6
|
+
// Helper function for GCD
|
|
7
|
+
function gcd(a, b) {
|
|
8
|
+
return b === 0 ? a : gcd(b, a % b);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class omdRationalNode extends omdNode {
|
|
12
|
+
constructor(astNodeData) {
|
|
13
|
+
super(astNodeData);
|
|
14
|
+
this.type = "omdRationalNode";
|
|
15
|
+
|
|
16
|
+
if (!astNodeData.args || astNodeData.args.length < 2) {
|
|
17
|
+
console.error("omdRationalNode requires numerator and denominator");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.value = this.parseValue();
|
|
22
|
+
this.numerator = this.createOperand(astNodeData.args[0]);
|
|
23
|
+
this.denominator = this.createOperand(astNodeData.args[1]);
|
|
24
|
+
|
|
25
|
+
// Populate the argumentNodeList for mathematical child nodes
|
|
26
|
+
this.argumentNodeList.numerator = this.numerator;
|
|
27
|
+
this.argumentNodeList.denominator = this.denominator;
|
|
28
|
+
|
|
29
|
+
// Gerard: Create purely aesthetic fraction line
|
|
30
|
+
this.fractionLine = new jsvgLine();
|
|
31
|
+
this.fractionLine.setStrokeColor('black');
|
|
32
|
+
this.fractionLine.setStrokeWidth(2);
|
|
33
|
+
this.addChild(this.fractionLine);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
parseValue() {
|
|
37
|
+
return "/";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
createOperand(ast) {
|
|
41
|
+
let OperandType = getNodeForAST(ast);
|
|
42
|
+
let child = new OperandType(ast);
|
|
43
|
+
this.addChild(child);
|
|
44
|
+
|
|
45
|
+
// Preserve lineage from the original AST node if it exists
|
|
46
|
+
// Note: The child constructor should already handle provenance from the AST
|
|
47
|
+
// This is just a safety check for cases where AST has explicit provenance
|
|
48
|
+
try {
|
|
49
|
+
if (ast.id && ast.provenance && ast.provenance.length > 0) {
|
|
50
|
+
// Only override if the child doesn't already have provenance
|
|
51
|
+
if (!child.provenance || child.provenance.length === 0) {
|
|
52
|
+
child.provenance = [...ast.provenance];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// If lineage preservation fails, continue without it
|
|
57
|
+
console.debug('Failed to preserve lineage in createOperand:', error.message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return child;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
computeDimensions() {
|
|
64
|
+
// Gerard: Shrink font size of numerator and denominator
|
|
65
|
+
this.numerator.setFontSize(this.getFontSize() * 5 / 6);
|
|
66
|
+
this.denominator.setFontSize(this.getFontSize() * 5 / 6);
|
|
67
|
+
|
|
68
|
+
this.numerator.computeDimensions();
|
|
69
|
+
this.denominator.computeDimensions();
|
|
70
|
+
|
|
71
|
+
// Extra horizontal spacing and vertical padding
|
|
72
|
+
const ratio = this.getFontSize() / this.getRootFontSize();
|
|
73
|
+
const spacing = 8 * ratio;
|
|
74
|
+
const padding = 8 * ratio;
|
|
75
|
+
|
|
76
|
+
let spacedWidth = Math.max(this.numerator.width, this.denominator.width) + spacing;
|
|
77
|
+
let spacedHeight = this.numerator.height + this.denominator.height + padding;
|
|
78
|
+
|
|
79
|
+
this.setWidthAndHeight(spacedWidth, spacedHeight);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
updateLayout() {
|
|
83
|
+
const fractionSpacing = 2;
|
|
84
|
+
|
|
85
|
+
// Gerard: Check math
|
|
86
|
+
// Position numerator (top, centered)
|
|
87
|
+
const numX = (this.width - this.numerator.width) / 2;
|
|
88
|
+
this.numerator.updateLayout();
|
|
89
|
+
this.numerator.setPosition(numX, fractionSpacing);
|
|
90
|
+
|
|
91
|
+
// Position fraction line (middle)
|
|
92
|
+
const lineY = this.numerator.height + fractionSpacing * 2;
|
|
93
|
+
this.fractionLine.setEndpoints(0, lineY, this.width, lineY);
|
|
94
|
+
|
|
95
|
+
// Position denominator (bottom, centered)
|
|
96
|
+
const denX = (this.width - this.denominator.width) / 2;
|
|
97
|
+
const denY = lineY + fractionSpacing;
|
|
98
|
+
this.denominator.updateLayout();
|
|
99
|
+
this.denominator.setPosition(denX, denY);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* For rational nodes, the alignment baseline is the fraction bar itself.
|
|
104
|
+
* @override
|
|
105
|
+
* @returns {number} The y-coordinate for alignment.
|
|
106
|
+
*/
|
|
107
|
+
getAlignmentBaseline() {
|
|
108
|
+
// Corresponds to the 'lineY' calculation in updateLayout.
|
|
109
|
+
const fractionSpacing = 2;
|
|
110
|
+
return this.numerator.height + fractionSpacing * 2;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
isConstant() {
|
|
114
|
+
return this.numerator.isConstant() && this.denominator.isConstant();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Retrieves the rational value of the node as a numerator/denominator pair.
|
|
119
|
+
* @returns {{num: number, den: number}}
|
|
120
|
+
*/
|
|
121
|
+
getRationalValue() {
|
|
122
|
+
if (this.isConstant()) {
|
|
123
|
+
return { num: this.numerator.getValue(), den: this.denominator.getValue() };
|
|
124
|
+
}
|
|
125
|
+
throw new Error('Rational node is not constant');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getValue() {
|
|
129
|
+
if (this.isConstant()) {
|
|
130
|
+
const { num, den } = this.getRationalValue();
|
|
131
|
+
if (den === 0) return NaN;
|
|
132
|
+
return num / den;
|
|
133
|
+
}
|
|
134
|
+
throw new Error("Node is not a constant expression");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
clone() {
|
|
138
|
+
let newAstData;
|
|
139
|
+
if (typeof this.astNodeData.clone === 'function') {
|
|
140
|
+
newAstData = this.astNodeData.clone();
|
|
141
|
+
} else {
|
|
142
|
+
newAstData = JSON.parse(JSON.stringify(this.astNodeData));
|
|
143
|
+
}
|
|
144
|
+
const clone = new omdRationalNode(newAstData);
|
|
145
|
+
|
|
146
|
+
// Keep the backRect from the clone, not from 'this'
|
|
147
|
+
const backRect = clone.backRect;
|
|
148
|
+
clone.removeAllChildren();
|
|
149
|
+
clone.addChild(backRect);
|
|
150
|
+
|
|
151
|
+
clone.numerator = this.numerator.clone();
|
|
152
|
+
clone.denominator = this.denominator.clone();
|
|
153
|
+
|
|
154
|
+
// jsvgLine does not have a clone method, so we create a new one.
|
|
155
|
+
clone.fractionLine = new jsvgLine();
|
|
156
|
+
clone.fractionLine.setStrokeColor('black');
|
|
157
|
+
clone.fractionLine.setStrokeWidth(2);
|
|
158
|
+
|
|
159
|
+
clone.addChild(clone.numerator);
|
|
160
|
+
clone.addChild(clone.fractionLine);
|
|
161
|
+
clone.addChild(clone.denominator);
|
|
162
|
+
|
|
163
|
+
// Explicitly update the argumentNodeList in the cloned node
|
|
164
|
+
clone.argumentNodeList.numerator = clone.numerator;
|
|
165
|
+
clone.argumentNodeList.denominator = clone.denominator;
|
|
166
|
+
|
|
167
|
+
// The crucial step: link the clone to its origin
|
|
168
|
+
clone.provenance.push(this.id);
|
|
169
|
+
|
|
170
|
+
return clone;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Converts the omdRationalNode to a math.js AST node.
|
|
175
|
+
* @returns {Object} A math.js-compatible AST node.
|
|
176
|
+
*/
|
|
177
|
+
toMathJSNode() {
|
|
178
|
+
const astNode = {
|
|
179
|
+
type: 'OperatorNode', op: '/', fn: 'divide',
|
|
180
|
+
args: [this.numerator.toMathJSNode(), this.denominator.toMathJSNode()],
|
|
181
|
+
id: this.id,
|
|
182
|
+
provenance: this.provenance
|
|
183
|
+
};
|
|
184
|
+
// Add a clone method to maintain compatibility with math.js's expectations.
|
|
185
|
+
astNode.clone = function() {
|
|
186
|
+
const clonedNode = { ...this };
|
|
187
|
+
clonedNode.args = this.args.map(arg => arg.clone());
|
|
188
|
+
return clonedNode;
|
|
189
|
+
};
|
|
190
|
+
return astNode;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Converts the rational node to a string representation.
|
|
195
|
+
* Ensures that complex numerators/denominators are wrapped in parentheses
|
|
196
|
+
* to maintain correct order of operations.
|
|
197
|
+
* @returns {string} The string representation of the fraction.
|
|
198
|
+
*/
|
|
199
|
+
toString() {
|
|
200
|
+
const numStr = this.numerator.toString();
|
|
201
|
+
const denStr = this.denominator.toString();
|
|
202
|
+
|
|
203
|
+
// Check if the children are complex expressions that require parentheses
|
|
204
|
+
const numNeedsParens = this.numerator.type === 'omdBinaryExpressionNode';
|
|
205
|
+
const denNeedsParens = this.denominator.type === 'omdBinaryExpressionNode';
|
|
206
|
+
|
|
207
|
+
const finalNumStr = numNeedsParens ? `(${numStr})` : numStr;
|
|
208
|
+
const finalDenStr = denNeedsParens ? `(${denStr})` : denStr;
|
|
209
|
+
|
|
210
|
+
return `${finalNumStr} / ${finalDenStr}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Evaluates the rational expression.
|
|
215
|
+
* @param {Object} variables - A map of variable names to their values.
|
|
216
|
+
* @returns {number} The result of the division.
|
|
217
|
+
*/
|
|
218
|
+
evaluate(variables = {}) {
|
|
219
|
+
const numValue = this.numerator.evaluate(variables);
|
|
220
|
+
const denValue = this.denominator.evaluate(variables);
|
|
221
|
+
|
|
222
|
+
if (denValue === 0) {
|
|
223
|
+
throw new Error("Division by zero in rational expression.");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return numValue / denValue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Reduce the fraction to lowest terms if it's constant.
|
|
231
|
+
* @returns {omdRationalNode|omdNode} Reduced fraction or the original node.
|
|
232
|
+
*/
|
|
233
|
+
reduce() {
|
|
234
|
+
if (this.isConstant()) {
|
|
235
|
+
let { num, den } = this.getRationalValue();
|
|
236
|
+
if (den < 0) { // Keep denominator positive
|
|
237
|
+
num = -num;
|
|
238
|
+
den = -den;
|
|
239
|
+
}
|
|
240
|
+
const commonDivisor = gcd(Math.abs(num), Math.abs(den));
|
|
241
|
+
const newNum = num / commonDivisor;
|
|
242
|
+
const newDen = den / commonDivisor;
|
|
243
|
+
|
|
244
|
+
if (newNum === this.numerator.getValue() && newDen === this.denominator.getValue()) {
|
|
245
|
+
return this; // Already reduced
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (newDen === 1) {
|
|
249
|
+
return omdConstantNode.fromNumber(newNum);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const newAst = {
|
|
253
|
+
type: 'OperatorNode', op: '/', fn: 'divide',
|
|
254
|
+
args: [
|
|
255
|
+
omdConstantNode.fromNumber(newNum).toMathJSNode(),
|
|
256
|
+
omdConstantNode.fromNumber(newDen).toMathJSNode()
|
|
257
|
+
]
|
|
258
|
+
};
|
|
259
|
+
return new omdRationalNode(newAst);
|
|
260
|
+
}
|
|
261
|
+
return this.clone(); // Return a clone if not constant
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check if the fraction is proper (absolute value of numerator < absolute value of denominator).
|
|
266
|
+
* Only works for constant fractions.
|
|
267
|
+
* @returns {boolean|null} True if proper, false if improper, null if not constant.
|
|
268
|
+
*/
|
|
269
|
+
isProper() {
|
|
270
|
+
if (this.isConstant()) {
|
|
271
|
+
const { num, den } = this.getRationalValue();
|
|
272
|
+
return Math.abs(num) < Math.abs(den);
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Create a fraction node from a string.
|
|
279
|
+
* @static
|
|
280
|
+
* @param {string} expressionString - Expression with division
|
|
281
|
+
* @returns {omdRationalNode}
|
|
282
|
+
*/
|
|
283
|
+
static fromString(expressionString) {
|
|
284
|
+
try {
|
|
285
|
+
const ast = window.math.parse(expressionString);
|
|
286
|
+
if (ast.type !== 'OperatorNode' || ast.op !== '/') {
|
|
287
|
+
throw new Error("Expression is not a division operation");
|
|
288
|
+
}
|
|
289
|
+
return new omdRationalNode(ast);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error("Failed to create rational node from string:", error);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|