@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,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
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { omdNode } from "./omdNode.js";
|
|
2
|
+
import { getNodeForAST } from "../core/omdUtilities.js";
|
|
3
|
+
import { omdConstantNode } from "./omdConstantNode.js";
|
|
4
|
+
import { omdPowerNode } from "./omdPowerNode.js";
|
|
5
|
+
import { omdBinaryExpressionNode } from "./omdBinaryExpressionNode.js";
|
|
6
|
+
import { simplifyStep } from "../simplification/omdSimplification.js";
|
|
7
|
+
import { jsvgPath, jsvgLine } from '@teachinglab/jsvg';
|
|
8
|
+
/**
|
|
9
|
+
* Represents a square root node in the mathematical expression tree
|
|
10
|
+
* Handles rendering of radical symbol and expression under the root
|
|
11
|
+
* @extends omdNode
|
|
12
|
+
*/
|
|
13
|
+
export class omdSqrtNode extends omdNode {
|
|
14
|
+
/**
|
|
15
|
+
* Creates a square root node from AST data
|
|
16
|
+
* @param {Object} astNodeData - The AST node containing sqrt function information
|
|
17
|
+
*/
|
|
18
|
+
constructor(astNodeData) {
|
|
19
|
+
super(astNodeData);
|
|
20
|
+
this.type = "omdSqrtNode";
|
|
21
|
+
this.args = astNodeData.args || [];
|
|
22
|
+
|
|
23
|
+
this.value = this.parseValue();
|
|
24
|
+
this.argument = this.createArgumentNode();
|
|
25
|
+
|
|
26
|
+
// Populate the argumentNodeList for the mathematical child node
|
|
27
|
+
if (this.argument) {
|
|
28
|
+
this.argumentNodeList.argument = this.argument;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
[ this.radicalPath, this.radicalLine ] = this.createRadicalElements();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
parseValue() {
|
|
35
|
+
return "sqrt";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates node for the expression under the radical
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
createArgumentNode() {
|
|
43
|
+
if (this.args.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
const argAst = this.args[0];
|
|
46
|
+
const ArgNodeType = getNodeForAST(argAst);
|
|
47
|
+
let child = new ArgNodeType(argAst);
|
|
48
|
+
this.addChild(child);
|
|
49
|
+
|
|
50
|
+
return child;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates radical symbol and line for sqrt
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
createRadicalElements() {
|
|
58
|
+
// Create custom radical symbol using SVG path
|
|
59
|
+
let radicalPath = new jsvgPath();
|
|
60
|
+
radicalPath.setStrokeColor('black');
|
|
61
|
+
radicalPath.setStrokeWidth(2);
|
|
62
|
+
radicalPath.setFillColor('none');
|
|
63
|
+
this.addChild(radicalPath);
|
|
64
|
+
|
|
65
|
+
// Create the horizontal line over the expression
|
|
66
|
+
let radicalLine = new jsvgLine();
|
|
67
|
+
radicalLine.setStrokeColor('black');
|
|
68
|
+
radicalLine.setStrokeWidth(2);
|
|
69
|
+
this.addChild(radicalLine);
|
|
70
|
+
|
|
71
|
+
return [radicalPath, radicalLine];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Calculates the dimensions of the sqrt node and its children
|
|
76
|
+
* @override
|
|
77
|
+
*/
|
|
78
|
+
computeDimensions() {
|
|
79
|
+
if (!this.argument) return;
|
|
80
|
+
|
|
81
|
+
const fontSize = this.getFontSize();
|
|
82
|
+
const argFontSize = fontSize * 5/6; // Match rational node scaling
|
|
83
|
+
|
|
84
|
+
// Set font size for argument and compute its dimensions
|
|
85
|
+
this.argument.setFontSize(argFontSize);
|
|
86
|
+
this.argument.computeDimensions();
|
|
87
|
+
|
|
88
|
+
// Calculate dimensions using the expression height to size the radical
|
|
89
|
+
const ratio = fontSize / this.getRootFontSize();
|
|
90
|
+
const spacing = 4 * ratio;
|
|
91
|
+
|
|
92
|
+
const argWidth = this.argument.width;
|
|
93
|
+
const argHeight = this.argument.height;
|
|
94
|
+
|
|
95
|
+
// Radical width is proportional to expression height
|
|
96
|
+
const radicalWidth = Math.max(12 * ratio, argHeight * 0.3);
|
|
97
|
+
|
|
98
|
+
const totalWidth = radicalWidth + spacing + argWidth + spacing;
|
|
99
|
+
const totalHeight = argHeight + 8 * ratio; // Extra height for the radical top
|
|
100
|
+
|
|
101
|
+
this.setWidthAndHeight(totalWidth, totalHeight);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Updates the layout of the sqrt node and its children
|
|
106
|
+
* @override
|
|
107
|
+
*/
|
|
108
|
+
updateLayout() {
|
|
109
|
+
if (!this.argument) return;
|
|
110
|
+
|
|
111
|
+
const fontSize = this.getFontSize();
|
|
112
|
+
const ratio = fontSize / this.getRootFontSize();
|
|
113
|
+
const spacing = 4 * ratio;
|
|
114
|
+
|
|
115
|
+
let currentX = 0;
|
|
116
|
+
|
|
117
|
+
// Calculate radical dimensions based on expression
|
|
118
|
+
const expressionHeight = this.argument.height;
|
|
119
|
+
const radicalWidth = Math.max(12 * ratio, expressionHeight * 0.3);
|
|
120
|
+
|
|
121
|
+
// Position the expression first to get its exact position
|
|
122
|
+
const expressionX = currentX + radicalWidth + spacing;
|
|
123
|
+
const expressionY = (this.height - expressionHeight) / 2;
|
|
124
|
+
|
|
125
|
+
this.argument.setPosition(expressionX, expressionY);
|
|
126
|
+
this.argument.updateLayout();
|
|
127
|
+
|
|
128
|
+
// Draw the radical path using addPoint method
|
|
129
|
+
const radicalBottom = expressionY + expressionHeight - 2 * ratio;
|
|
130
|
+
const radicalTop = expressionY - 4 * ratio;
|
|
131
|
+
const radicalMid = expressionY + expressionHeight * 0.7;
|
|
132
|
+
|
|
133
|
+
// Clear previous points and create radical path: short diagonal down, long diagonal up
|
|
134
|
+
this.radicalPath.clearPoints();
|
|
135
|
+
this.radicalPath.addPoint(currentX + radicalWidth * 0.3, radicalMid);
|
|
136
|
+
this.radicalPath.addPoint(currentX + radicalWidth * 0.6, radicalBottom);
|
|
137
|
+
this.radicalPath.addPoint(currentX + radicalWidth, radicalTop);
|
|
138
|
+
this.radicalPath.updatePath();
|
|
139
|
+
|
|
140
|
+
// Position horizontal line above the expression
|
|
141
|
+
const lineY = expressionY - 2 * ratio;
|
|
142
|
+
const lineStartX = currentX + radicalWidth;
|
|
143
|
+
const lineEndX = expressionX + this.argument.width + spacing / 2;
|
|
144
|
+
this.radicalLine.setEndpoints(lineStartX, lineY, lineEndX, lineY);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
clone() {
|
|
148
|
+
let newAstData;
|
|
149
|
+
if (typeof this.astNodeData.clone === 'function') {
|
|
150
|
+
newAstData = this.astNodeData.clone();
|
|
151
|
+
} else {
|
|
152
|
+
newAstData = JSON.parse(JSON.stringify(this.astNodeData));
|
|
153
|
+
}
|
|
154
|
+
const clone = new omdSqrtNode(newAstData);
|
|
155
|
+
|
|
156
|
+
// Keep the backRect from the clone, not from 'this'
|
|
157
|
+
const backRect = clone.backRect;
|
|
158
|
+
clone.removeAllChildren();
|
|
159
|
+
clone.addChild(backRect);
|
|
160
|
+
|
|
161
|
+
// Create new jsvg elements for the clone
|
|
162
|
+
clone.radicalPath = new jsvgPath();
|
|
163
|
+
clone.radicalPath.setStrokeColor('black');
|
|
164
|
+
clone.radicalPath.setStrokeWidth(2);
|
|
165
|
+
clone.radicalPath.setFillColor('none');
|
|
166
|
+
clone.addChild(clone.radicalPath);
|
|
167
|
+
|
|
168
|
+
clone.radicalLine = new jsvgLine();
|
|
169
|
+
clone.radicalLine.setStrokeColor('black');
|
|
170
|
+
clone.radicalLine.setStrokeWidth(2);
|
|
171
|
+
clone.addChild(clone.radicalLine);
|
|
172
|
+
|
|
173
|
+
if (this.argument) {
|
|
174
|
+
clone.argument = this.argument.clone();
|
|
175
|
+
clone.addChild(clone.argument);
|
|
176
|
+
|
|
177
|
+
// Explicitly update the argumentNodeList in the cloned node
|
|
178
|
+
clone.argumentNodeList.argument = clone.argument;
|
|
179
|
+
|
|
180
|
+
// The crucial step: link the clone to its origin
|
|
181
|
+
clone.provenance.push(this.id);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return clone;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Highlights the sqrt node and its argument
|
|
189
|
+
*/
|
|
190
|
+
highlightAll() {
|
|
191
|
+
this.select();
|
|
192
|
+
|
|
193
|
+
if (this.argument && this.argument.highlightAll) {
|
|
194
|
+
this.argument.highlightAll();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Unhighlights the sqrt node and its argument
|
|
200
|
+
*/
|
|
201
|
+
unhighlightAll() {
|
|
202
|
+
this.deselect();
|
|
203
|
+
|
|
204
|
+
if (this.argument && this.argument.unhighlightAll) {
|
|
205
|
+
this.argument.unhighlightAll();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Converts the omdSqrtNode to a math.js AST node.
|
|
211
|
+
* @returns {Object} A math.js-compatible AST node.
|
|
212
|
+
*/
|
|
213
|
+
toMathJSNode() {
|
|
214
|
+
const astNode = {
|
|
215
|
+
type: 'FunctionNode',
|
|
216
|
+
fn: { type: 'SymbolNode', name: 'sqrt', clone: function() { return {...this}; } },
|
|
217
|
+
args: this.argument ? [this.argument.toMathJSNode()] : []
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Add a clone method to maintain compatibility with math.js's expectations.
|
|
221
|
+
astNode.clone = function() {
|
|
222
|
+
const clonedNode = { ...this };
|
|
223
|
+
if (this.args) {
|
|
224
|
+
clonedNode.args = this.args.map(arg => arg.clone());
|
|
225
|
+
}
|
|
226
|
+
if (this.fn && typeof this.fn.clone === 'function') {
|
|
227
|
+
clonedNode.fn = this.fn.clone();
|
|
228
|
+
}
|
|
229
|
+
return clonedNode;
|
|
230
|
+
};
|
|
231
|
+
return astNode;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Converts the square root node to a string representation.
|
|
235
|
+
* @returns {string} The string representation.
|
|
236
|
+
*/
|
|
237
|
+
toString() {
|
|
238
|
+
return `sqrt(${this.argument ? this.argument.toString() : ''})`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Evaluate the root expression.
|
|
243
|
+
* @param {Object} variables - Variable name to value mapping
|
|
244
|
+
* @returns {number} The evaluated root
|
|
245
|
+
*/
|
|
246
|
+
evaluate(variables = {}) {
|
|
247
|
+
if (!this.argument || !this.argument.evaluate) {
|
|
248
|
+
return NaN;
|
|
249
|
+
}
|
|
250
|
+
const radicandValue = this.argument.evaluate(variables);
|
|
251
|
+
if (radicandValue < 0) {
|
|
252
|
+
return NaN; // Or handle complex numbers if desired
|
|
253
|
+
}
|
|
254
|
+
return Math.sqrt(radicandValue);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Check if this is a square root (index = 2).
|
|
259
|
+
* @returns {boolean}
|
|
260
|
+
*/
|
|
261
|
+
isSquareRoot() {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check if this is a cube root (index = 3).
|
|
267
|
+
* @returns {boolean}
|
|
268
|
+
*/
|
|
269
|
+
isCubeRoot() {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Convert to equivalent power notation.
|
|
275
|
+
* @returns {omdPowerNode} Equivalent power expression
|
|
276
|
+
*/
|
|
277
|
+
toPowerForm() {
|
|
278
|
+
if (!this.argument) return null;
|
|
279
|
+
|
|
280
|
+
const powerAst = {
|
|
281
|
+
type: 'OperatorNode', op: '^', fn: 'pow',
|
|
282
|
+
args: [
|
|
283
|
+
this.argument.toMathJSNode(),
|
|
284
|
+
omdConstantNode.fromNumber(0.5).toMathJSNode()
|
|
285
|
+
]
|
|
286
|
+
};
|
|
287
|
+
return new omdPowerNode(powerAst);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create a root node from a string.
|
|
292
|
+
* @static
|
|
293
|
+
* @param {string} expressionString - Expression with root
|
|
294
|
+
* @returns {omdSqrtNode}
|
|
295
|
+
*/
|
|
296
|
+
static fromString(expressionString) {
|
|
297
|
+
try {
|
|
298
|
+
const ast = window.math.parse(expressionString);
|
|
299
|
+
if (ast.type === 'FunctionNode' && ast.fn.name === 'sqrt') {
|
|
300
|
+
return new omdSqrtNode(ast);
|
|
301
|
+
}
|
|
302
|
+
throw new Error("Expression is not a 'sqrt' function.");
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error("Failed to create sqrt node from string:", error);
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|