@teachinglab/omd 0.2.5 → 0.2.7
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/docs/api/omdToolbar.md +130 -130
- package/omd/config/omdConfigManager.js +2 -2
- package/omd/core/omdEquationStack.js +521 -521
- package/omd/display/omdDisplay.js +421 -53
- package/omd/nodes/omdBinaryExpressionNode.js +459 -459
- package/omd/nodes/omdEquationNode.js +1273 -1222
- package/omd/nodes/omdEquationSequenceNode.js +1246 -1246
- package/omd/nodes/omdFunctionNode.js +351 -351
- package/omd/nodes/omdNode.js +556 -556
- package/omd/nodes/omdSqrtNode.js +307 -307
- package/omd/step-visualizer/omdStepVisualizer.js +95 -33
- package/omd/step-visualizer/omdStepVisualizerLayout.js +640 -9
- package/package.json +1 -1
- package/src/omdMetaExpression.js +1 -1
|
@@ -1,352 +1,352 @@
|
|
|
1
|
-
import { omdNode } from "./omdNode.js";
|
|
2
|
-
import { getNodeForAST, getTextBounds } from "../core/omdUtilities.js";
|
|
3
|
-
import { omdConstantNode } from "./omdConstantNode.js";
|
|
4
|
-
import { jsvgTextLine } from '@teachinglab/jsvg';
|
|
5
|
-
/**
|
|
6
|
-
* Represents a function call node in the mathematical expression tree
|
|
7
|
-
* Handles rendering of function names, arguments, and parentheses
|
|
8
|
-
* @extends omdNode
|
|
9
|
-
*/
|
|
10
|
-
export class omdFunctionNode extends omdNode {
|
|
11
|
-
/**
|
|
12
|
-
* Creates a function node from AST data
|
|
13
|
-
* @param {Object} astNodeData - The AST node containing function information
|
|
14
|
-
*/
|
|
15
|
-
constructor(astNodeData) {
|
|
16
|
-
super(astNodeData);
|
|
17
|
-
this.type = "omdFunctionNode";
|
|
18
|
-
this.functionName = astNodeData.fn?.name || astNodeData.name || 'f';
|
|
19
|
-
this.args = astNodeData.args || [];
|
|
20
|
-
|
|
21
|
-
this.createTextElements();
|
|
22
|
-
this.createArgumentNodes();
|
|
23
|
-
this.computeDimensions();
|
|
24
|
-
this.updateLayout();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Creates text elements for function name and parentheses
|
|
29
|
-
* @private
|
|
30
|
-
*/
|
|
31
|
-
createTextElements() {
|
|
32
|
-
const createText = (text) => {
|
|
33
|
-
const element = new jsvgTextLine();
|
|
34
|
-
element.setText(text);
|
|
35
|
-
element.setTextAnchor('start');
|
|
36
|
-
// Note: dominant-baseline doesn't have a jsvg method, keeping setAttribute for now
|
|
37
|
-
element.svgObject.setAttribute('dominant-baseline', 'middle');
|
|
38
|
-
this.addChild(element);
|
|
39
|
-
return element;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
this.functionNameElement = createText(this.functionName);
|
|
43
|
-
this.openParenElement = createText('(');
|
|
44
|
-
this.closeParenElement = createText(')');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Creates nodes for function arguments and comma separators
|
|
49
|
-
* @private
|
|
50
|
-
*/
|
|
51
|
-
createArgumentNodes() {
|
|
52
|
-
this.argNodes = [];
|
|
53
|
-
this.commaElements = [];
|
|
54
|
-
|
|
55
|
-
this.argumentNodeList.args = [];
|
|
56
|
-
|
|
57
|
-
this.args.forEach((argAst, index) => {
|
|
58
|
-
const ArgNodeType = getNodeForAST(argAst);
|
|
59
|
-
const argNode = new ArgNodeType(argAst);
|
|
60
|
-
this.argNodes.push(argNode);
|
|
61
|
-
this.addChild(argNode);
|
|
62
|
-
|
|
63
|
-
this.argumentNodeList.args.push(argNode);
|
|
64
|
-
|
|
65
|
-
if (index < this.args.length - 1) {
|
|
66
|
-
const commaElement = new jsvgTextLine();
|
|
67
|
-
commaElement.setText(', ');
|
|
68
|
-
commaElement.setTextAnchor('start');
|
|
69
|
-
commaElement.svgObject.setAttribute('dominant-baseline', 'middle');
|
|
70
|
-
this.commaElements.push(commaElement);
|
|
71
|
-
this.addChild(commaElement);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Calculates the dimensions of the function node and its children
|
|
78
|
-
* @override
|
|
79
|
-
*/
|
|
80
|
-
computeDimensions() {
|
|
81
|
-
const fontSize = this.getFontSize();
|
|
82
|
-
const argFontSize = fontSize * 5/6; // Match rational node scaling
|
|
83
|
-
|
|
84
|
-
// Set font sizes for text elements
|
|
85
|
-
this.functionNameElement.setFontSize(fontSize);
|
|
86
|
-
this.openParenElement.setFontSize(fontSize);
|
|
87
|
-
this.closeParenElement.setFontSize(fontSize);
|
|
88
|
-
|
|
89
|
-
// Set font sizes for arguments and compute their dimensions
|
|
90
|
-
this.argNodes.forEach(argNode => {
|
|
91
|
-
argNode.setFontSize(argFontSize);
|
|
92
|
-
argNode.computeDimensions();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Set font sizes for commas
|
|
96
|
-
this.commaElements.forEach(comma => {
|
|
97
|
-
comma.setFontSize(argFontSize);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Calculate dimensions using getTextBounds for consistency
|
|
101
|
-
const ratio = fontSize / this.getRootFontSize();
|
|
102
|
-
const spacing = 2 * ratio;
|
|
103
|
-
|
|
104
|
-
const functionNameBounds = getTextBounds(this.functionName, fontSize);
|
|
105
|
-
const openParenBounds = getTextBounds('(', fontSize);
|
|
106
|
-
const closeParenBounds = getTextBounds(')', fontSize);
|
|
107
|
-
const commaBounds = getTextBounds(', ', argFontSize);
|
|
108
|
-
|
|
109
|
-
let totalArgWidth = 0;
|
|
110
|
-
let maxArgHeight = 0;
|
|
111
|
-
|
|
112
|
-
this.argNodes.forEach((argNode, index) => {
|
|
113
|
-
totalArgWidth += argNode.width;
|
|
114
|
-
maxArgHeight = Math.max(maxArgHeight, argNode.height);
|
|
115
|
-
if (index < this.commaElements.length) {
|
|
116
|
-
totalArgWidth += commaBounds.width + spacing;
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const totalWidth = functionNameBounds.width + openParenBounds.width + totalArgWidth + closeParenBounds.width + (spacing * 2);
|
|
121
|
-
const totalHeight = Math.max(maxArgHeight, functionNameBounds.height, openParenBounds.height, closeParenBounds.height) + 4 * ratio;
|
|
122
|
-
|
|
123
|
-
this.setWidthAndHeight(totalWidth, totalHeight);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Updates the layout of the function node and its children
|
|
128
|
-
* @override
|
|
129
|
-
*/
|
|
130
|
-
updateLayout() {
|
|
131
|
-
const fontSize = this.getFontSize();
|
|
132
|
-
const argFontSize = fontSize * 5/6;
|
|
133
|
-
const ratio = fontSize / this.getRootFontSize();
|
|
134
|
-
const spacing = 2 * ratio;
|
|
135
|
-
|
|
136
|
-
let currentX = 0;
|
|
137
|
-
const textY = this.height / 2;
|
|
138
|
-
|
|
139
|
-
// Position function name using setPosition where possible
|
|
140
|
-
this.functionNameElement.setPosition(currentX, textY);
|
|
141
|
-
currentX += getTextBounds(this.functionName, fontSize).width;
|
|
142
|
-
|
|
143
|
-
// Position opening parenthesis
|
|
144
|
-
this.openParenElement.setPosition(currentX, textY);
|
|
145
|
-
currentX += getTextBounds('(', fontSize).width;
|
|
146
|
-
|
|
147
|
-
// Position arguments and commas
|
|
148
|
-
this.argNodes.forEach((argNode, index) => {
|
|
149
|
-
argNode.setPosition(currentX, (this.height - argNode.height) / 2);
|
|
150
|
-
argNode.updateLayout();
|
|
151
|
-
currentX += argNode.width;
|
|
152
|
-
|
|
153
|
-
if (index < this.commaElements.length) {
|
|
154
|
-
currentX += spacing;
|
|
155
|
-
this.commaElements[index].setPosition(currentX, textY);
|
|
156
|
-
currentX += getTextBounds(', ', argFontSize).width + spacing;
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Position closing parenthesis
|
|
161
|
-
this.closeParenElement.setPosition(currentX, textY);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Highlights the function node and all its arguments
|
|
166
|
-
*/
|
|
167
|
-
highlightAll() {
|
|
168
|
-
this.select();
|
|
169
|
-
|
|
170
|
-
this.argNodes.forEach(argNode => {
|
|
171
|
-
if (argNode.highlightAll) {
|
|
172
|
-
argNode.highlightAll();
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Unhighlights the function node and all its arguments
|
|
179
|
-
*/
|
|
180
|
-
unhighlightAll() {
|
|
181
|
-
this.deselect();
|
|
182
|
-
|
|
183
|
-
this.argNodes.forEach(argNode => {
|
|
184
|
-
if (argNode.unhighlightAll) {
|
|
185
|
-
argNode.unhighlightAll();
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
clone() {
|
|
191
|
-
let newAstData;
|
|
192
|
-
if (typeof this.astNodeData.clone === 'function') {
|
|
193
|
-
newAstData = this.astNodeData.clone();
|
|
194
|
-
} else {
|
|
195
|
-
newAstData = JSON.parse(JSON.stringify(this.astNodeData));
|
|
196
|
-
}
|
|
197
|
-
const clone = new omdFunctionNode(newAstData);
|
|
198
|
-
|
|
199
|
-
// Preserve the background rectangle.
|
|
200
|
-
const backRect = clone.backRect;
|
|
201
|
-
clone.removeAllChildren();
|
|
202
|
-
clone.addChild(backRect);
|
|
203
|
-
|
|
204
|
-
// Manually clone properties
|
|
205
|
-
clone.functionName = this.functionName;
|
|
206
|
-
clone.args = JSON.parse(JSON.stringify(this.args));
|
|
207
|
-
|
|
208
|
-
// Re-create text elements using the existing method
|
|
209
|
-
clone.createTextElements();
|
|
210
|
-
|
|
211
|
-
// Re-create argument nodes using the existing method, ensuring they are cloned properly
|
|
212
|
-
// The createArgumentNodes method already handles adding children and populating argumentNodeList.
|
|
213
|
-
clone.argNodes = this.argNodes.map(argNode => argNode.clone());
|
|
214
|
-
// Manually set parents and add to childList, as createArgumentNodes might re-parse AST if not careful
|
|
215
|
-
clone.argNodes.forEach(arg => {
|
|
216
|
-
arg.parent = clone;
|
|
217
|
-
clone.addChild(arg);
|
|
218
|
-
});
|
|
219
|
-
clone.argumentNodeList.args = clone.argNodes;
|
|
220
|
-
|
|
221
|
-
// Ensure commas are also cloned and added
|
|
222
|
-
clone.commaElements = this.commaElements.map(c => {
|
|
223
|
-
const newComma = new jsvgTextLine();
|
|
224
|
-
newComma.setText(c.text);
|
|
225
|
-
newComma.setTextAnchor(c.textAnchor);
|
|
226
|
-
newComma.svgObject.setAttribute('dominant-baseline', c.svgObject.getAttribute('dominant-baseline'));
|
|
227
|
-
newComma.parent = clone;
|
|
228
|
-
clone.addChild(newComma);
|
|
229
|
-
return newComma;
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Now ensure correct order of children, if not already handled by createTextElements and createArgumentNodes
|
|
233
|
-
// The constructor already calls computeDimensions and updateLayout, which will handle final layout.
|
|
234
|
-
clone.addChild(clone.closedParen);
|
|
235
|
-
|
|
236
|
-
// Explicitly update the argumentNodeList in the cloned node
|
|
237
|
-
clone.argumentNodeList.args = clone.args;
|
|
238
|
-
|
|
239
|
-
// The crucial step: link the clone to its origin
|
|
240
|
-
clone.provenance.push(this.id);
|
|
241
|
-
|
|
242
|
-
return clone;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Converts the omdFunctionNode to a math.js AST node.
|
|
247
|
-
* @returns {Object} A math.js-compatible AST node.
|
|
248
|
-
*/
|
|
249
|
-
toMathJSNode() {
|
|
250
|
-
const astNode = {
|
|
251
|
-
type: 'FunctionNode',
|
|
252
|
-
fn: { type: 'SymbolNode', name: this.functionName, clone: function() { return {...this}; } },
|
|
253
|
-
args: this.argNodes.map(arg => arg.toMathJSNode())
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// Add a clone method to maintain compatibility with math.js's expectations.
|
|
257
|
-
astNode.clone = function() {
|
|
258
|
-
const clonedNode = { ...this };
|
|
259
|
-
clonedNode.args = this.args.map(arg => arg.clone());
|
|
260
|
-
if (this.fn && typeof this.fn.clone === 'function') {
|
|
261
|
-
clonedNode.fn = this.fn.clone();
|
|
262
|
-
}
|
|
263
|
-
return clonedNode;
|
|
264
|
-
};
|
|
265
|
-
return astNode;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Convert to string representation
|
|
270
|
-
* @returns {string} The function as a string
|
|
271
|
-
*/
|
|
272
|
-
toString() {
|
|
273
|
-
const argStrings = this.argNodes.map(arg => arg.toString());
|
|
274
|
-
return `${this.functionName}(${argStrings.join(', ')})`;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Evaluate the function with given variable values
|
|
279
|
-
* @param {Object} variables - Variable name to value mapping
|
|
280
|
-
* @returns {number} The evaluated result
|
|
281
|
-
*/
|
|
282
|
-
evaluate(variables = {}) {
|
|
283
|
-
// First evaluate all arguments
|
|
284
|
-
const evaluatedArgs = this.argNodes.map(arg => {
|
|
285
|
-
if (arg.evaluate) {
|
|
286
|
-
return arg.evaluate(variables);
|
|
287
|
-
} else if (arg.type === 'omdConstantNode') {
|
|
288
|
-
return parseFloat(arg.value);
|
|
289
|
-
} else if (arg.type === 'omdVariableNode' && variables[arg.name] !== undefined) {
|
|
290
|
-
return variables[arg.name];
|
|
291
|
-
}
|
|
292
|
-
throw new Error(`Cannot evaluate argument: ${arg.toString()}`);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
// Use math.js to evaluate the function
|
|
296
|
-
if (window.math && window.math[this.functionName]) {
|
|
297
|
-
try {
|
|
298
|
-
return window.math[this.functionName](...evaluatedArgs);
|
|
299
|
-
} catch (error) {
|
|
300
|
-
throw new Error(`Error evaluating ${this.functionName}: ${error.message}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Fallback for common functions if math.js is not available
|
|
305
|
-
switch (this.functionName) {
|
|
306
|
-
case 'sin': return Math.sin(evaluatedArgs[0]);
|
|
307
|
-
case 'cos': return Math.cos(evaluatedArgs[0]);
|
|
308
|
-
case 'tan': return Math.tan(evaluatedArgs[0]);
|
|
309
|
-
case 'asin': return Math.asin(evaluatedArgs[0]);
|
|
310
|
-
case 'acos': return Math.acos(evaluatedArgs[0]);
|
|
311
|
-
case 'atan': return Math.atan(evaluatedArgs[0]);
|
|
312
|
-
case 'sqrt': return Math.sqrt(evaluatedArgs[0]);
|
|
313
|
-
case 'abs': return Math.abs(evaluatedArgs[0]);
|
|
314
|
-
case 'log': return Math.log(evaluatedArgs[0]);
|
|
315
|
-
case 'log10': return Math.log10(evaluatedArgs[0]);
|
|
316
|
-
case 'exp': return Math.exp(evaluatedArgs[0]);
|
|
317
|
-
case 'pow': return Math.pow(evaluatedArgs[0], evaluatedArgs[1]);
|
|
318
|
-
case 'min': return Math.min(...evaluatedArgs);
|
|
319
|
-
case 'max': return Math.max(...evaluatedArgs);
|
|
320
|
-
case 'floor': return Math.floor(evaluatedArgs[0]);
|
|
321
|
-
case 'ceil': return Math.ceil(evaluatedArgs[0]);
|
|
322
|
-
case 'round': return Math.round(evaluatedArgs[0]);
|
|
323
|
-
default:
|
|
324
|
-
throw new Error(`Unknown function: ${this.functionName}`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Create a function from a string
|
|
330
|
-
* @param {string} functionString - The function as a string
|
|
331
|
-
* @returns {omdFunctionNode} The created function node
|
|
332
|
-
* @static
|
|
333
|
-
*/
|
|
334
|
-
static fromString(functionString) {
|
|
335
|
-
if (!window.math) {
|
|
336
|
-
throw new Error("Math.js is required for parsing function strings");
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
try {
|
|
340
|
-
const ast = window.math.parse(functionString);
|
|
341
|
-
|
|
342
|
-
// Verify it's a function node
|
|
343
|
-
if (ast.type !== 'FunctionNode') {
|
|
344
|
-
throw new Error("String does not represent a function call");
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return new omdFunctionNode(ast);
|
|
348
|
-
} catch (error) {
|
|
349
|
-
throw new Error(`Failed to parse function string: ${error.message}`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
1
|
+
import { omdNode } from "./omdNode.js";
|
|
2
|
+
import { getNodeForAST, getTextBounds } from "../core/omdUtilities.js";
|
|
3
|
+
import { omdConstantNode } from "./omdConstantNode.js";
|
|
4
|
+
import { jsvgTextLine } from '@teachinglab/jsvg';
|
|
5
|
+
/**
|
|
6
|
+
* Represents a function call node in the mathematical expression tree
|
|
7
|
+
* Handles rendering of function names, arguments, and parentheses
|
|
8
|
+
* @extends omdNode
|
|
9
|
+
*/
|
|
10
|
+
export class omdFunctionNode extends omdNode {
|
|
11
|
+
/**
|
|
12
|
+
* Creates a function node from AST data
|
|
13
|
+
* @param {Object} astNodeData - The AST node containing function information
|
|
14
|
+
*/
|
|
15
|
+
constructor(astNodeData) {
|
|
16
|
+
super(astNodeData);
|
|
17
|
+
this.type = "omdFunctionNode";
|
|
18
|
+
this.functionName = astNodeData.fn?.name || astNodeData.name || 'f';
|
|
19
|
+
this.args = astNodeData.args || [];
|
|
20
|
+
|
|
21
|
+
this.createTextElements();
|
|
22
|
+
this.createArgumentNodes();
|
|
23
|
+
this.computeDimensions();
|
|
24
|
+
this.updateLayout();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates text elements for function name and parentheses
|
|
29
|
+
* @private
|
|
30
|
+
*/
|
|
31
|
+
createTextElements() {
|
|
32
|
+
const createText = (text) => {
|
|
33
|
+
const element = new jsvgTextLine();
|
|
34
|
+
element.setText(text);
|
|
35
|
+
element.setTextAnchor('start');
|
|
36
|
+
// Note: dominant-baseline doesn't have a jsvg method, keeping setAttribute for now
|
|
37
|
+
element.svgObject.setAttribute('dominant-baseline', 'middle');
|
|
38
|
+
this.addChild(element);
|
|
39
|
+
return element;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.functionNameElement = createText(this.functionName);
|
|
43
|
+
this.openParenElement = createText('(');
|
|
44
|
+
this.closeParenElement = createText(')');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates nodes for function arguments and comma separators
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
createArgumentNodes() {
|
|
52
|
+
this.argNodes = [];
|
|
53
|
+
this.commaElements = [];
|
|
54
|
+
|
|
55
|
+
this.argumentNodeList.args = [];
|
|
56
|
+
|
|
57
|
+
this.args.forEach((argAst, index) => {
|
|
58
|
+
const ArgNodeType = getNodeForAST(argAst);
|
|
59
|
+
const argNode = new ArgNodeType(argAst);
|
|
60
|
+
this.argNodes.push(argNode);
|
|
61
|
+
this.addChild(argNode);
|
|
62
|
+
|
|
63
|
+
this.argumentNodeList.args.push(argNode);
|
|
64
|
+
|
|
65
|
+
if (index < this.args.length - 1) {
|
|
66
|
+
const commaElement = new jsvgTextLine();
|
|
67
|
+
commaElement.setText(', ');
|
|
68
|
+
commaElement.setTextAnchor('start');
|
|
69
|
+
commaElement.svgObject.setAttribute('dominant-baseline', 'middle');
|
|
70
|
+
this.commaElements.push(commaElement);
|
|
71
|
+
this.addChild(commaElement);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Calculates the dimensions of the function node and its children
|
|
78
|
+
* @override
|
|
79
|
+
*/
|
|
80
|
+
computeDimensions() {
|
|
81
|
+
const fontSize = this.getFontSize();
|
|
82
|
+
const argFontSize = fontSize * 5/6; // Match rational node scaling
|
|
83
|
+
|
|
84
|
+
// Set font sizes for text elements
|
|
85
|
+
this.functionNameElement.setFontSize(fontSize);
|
|
86
|
+
this.openParenElement.setFontSize(fontSize);
|
|
87
|
+
this.closeParenElement.setFontSize(fontSize);
|
|
88
|
+
|
|
89
|
+
// Set font sizes for arguments and compute their dimensions
|
|
90
|
+
this.argNodes.forEach(argNode => {
|
|
91
|
+
argNode.setFontSize(argFontSize);
|
|
92
|
+
argNode.computeDimensions();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Set font sizes for commas
|
|
96
|
+
this.commaElements.forEach(comma => {
|
|
97
|
+
comma.setFontSize(argFontSize);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Calculate dimensions using getTextBounds for consistency
|
|
101
|
+
const ratio = fontSize / this.getRootFontSize();
|
|
102
|
+
const spacing = 2 * ratio;
|
|
103
|
+
|
|
104
|
+
const functionNameBounds = getTextBounds(this.functionName, fontSize);
|
|
105
|
+
const openParenBounds = getTextBounds('(', fontSize);
|
|
106
|
+
const closeParenBounds = getTextBounds(')', fontSize);
|
|
107
|
+
const commaBounds = getTextBounds(', ', argFontSize);
|
|
108
|
+
|
|
109
|
+
let totalArgWidth = 0;
|
|
110
|
+
let maxArgHeight = 0;
|
|
111
|
+
|
|
112
|
+
this.argNodes.forEach((argNode, index) => {
|
|
113
|
+
totalArgWidth += argNode.width;
|
|
114
|
+
maxArgHeight = Math.max(maxArgHeight, argNode.height);
|
|
115
|
+
if (index < this.commaElements.length) {
|
|
116
|
+
totalArgWidth += commaBounds.width + spacing;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const totalWidth = functionNameBounds.width + openParenBounds.width + totalArgWidth + closeParenBounds.width + (spacing * 2);
|
|
121
|
+
const totalHeight = Math.max(maxArgHeight, functionNameBounds.height, openParenBounds.height, closeParenBounds.height) + 4 * ratio;
|
|
122
|
+
|
|
123
|
+
this.setWidthAndHeight(totalWidth, totalHeight);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Updates the layout of the function node and its children
|
|
128
|
+
* @override
|
|
129
|
+
*/
|
|
130
|
+
updateLayout() {
|
|
131
|
+
const fontSize = this.getFontSize();
|
|
132
|
+
const argFontSize = fontSize * 5/6;
|
|
133
|
+
const ratio = fontSize / this.getRootFontSize();
|
|
134
|
+
const spacing = 2 * ratio;
|
|
135
|
+
|
|
136
|
+
let currentX = 0;
|
|
137
|
+
const textY = this.height / 2;
|
|
138
|
+
|
|
139
|
+
// Position function name using setPosition where possible
|
|
140
|
+
this.functionNameElement.setPosition(currentX, textY);
|
|
141
|
+
currentX += getTextBounds(this.functionName, fontSize).width;
|
|
142
|
+
|
|
143
|
+
// Position opening parenthesis
|
|
144
|
+
this.openParenElement.setPosition(currentX, textY);
|
|
145
|
+
currentX += getTextBounds('(', fontSize).width;
|
|
146
|
+
|
|
147
|
+
// Position arguments and commas
|
|
148
|
+
this.argNodes.forEach((argNode, index) => {
|
|
149
|
+
argNode.setPosition(currentX, (this.height - argNode.height) / 2);
|
|
150
|
+
argNode.updateLayout();
|
|
151
|
+
currentX += argNode.width;
|
|
152
|
+
|
|
153
|
+
if (index < this.commaElements.length) {
|
|
154
|
+
currentX += spacing;
|
|
155
|
+
this.commaElements[index].setPosition(currentX, textY);
|
|
156
|
+
currentX += getTextBounds(', ', argFontSize).width + spacing;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Position closing parenthesis
|
|
161
|
+
this.closeParenElement.setPosition(currentX, textY);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Highlights the function node and all its arguments
|
|
166
|
+
*/
|
|
167
|
+
highlightAll() {
|
|
168
|
+
this.select();
|
|
169
|
+
|
|
170
|
+
this.argNodes.forEach(argNode => {
|
|
171
|
+
if (argNode.highlightAll) {
|
|
172
|
+
argNode.highlightAll();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Unhighlights the function node and all its arguments
|
|
179
|
+
*/
|
|
180
|
+
unhighlightAll() {
|
|
181
|
+
this.deselect();
|
|
182
|
+
|
|
183
|
+
this.argNodes.forEach(argNode => {
|
|
184
|
+
if (argNode.unhighlightAll) {
|
|
185
|
+
argNode.unhighlightAll();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
clone() {
|
|
191
|
+
let newAstData;
|
|
192
|
+
if (typeof this.astNodeData.clone === 'function') {
|
|
193
|
+
newAstData = this.astNodeData.clone();
|
|
194
|
+
} else {
|
|
195
|
+
newAstData = JSON.parse(JSON.stringify(this.astNodeData));
|
|
196
|
+
}
|
|
197
|
+
const clone = new omdFunctionNode(newAstData);
|
|
198
|
+
|
|
199
|
+
// Preserve the background rectangle.
|
|
200
|
+
const backRect = clone.backRect;
|
|
201
|
+
clone.removeAllChildren();
|
|
202
|
+
clone.addChild(backRect);
|
|
203
|
+
|
|
204
|
+
// Manually clone properties
|
|
205
|
+
clone.functionName = this.functionName;
|
|
206
|
+
clone.args = JSON.parse(JSON.stringify(this.args));
|
|
207
|
+
|
|
208
|
+
// Re-create text elements using the existing method
|
|
209
|
+
clone.createTextElements();
|
|
210
|
+
|
|
211
|
+
// Re-create argument nodes using the existing method, ensuring they are cloned properly
|
|
212
|
+
// The createArgumentNodes method already handles adding children and populating argumentNodeList.
|
|
213
|
+
clone.argNodes = this.argNodes.map(argNode => argNode.clone());
|
|
214
|
+
// Manually set parents and add to childList, as createArgumentNodes might re-parse AST if not careful
|
|
215
|
+
clone.argNodes.forEach(arg => {
|
|
216
|
+
arg.parent = clone;
|
|
217
|
+
clone.addChild(arg);
|
|
218
|
+
});
|
|
219
|
+
clone.argumentNodeList.args = clone.argNodes;
|
|
220
|
+
|
|
221
|
+
// Ensure commas are also cloned and added
|
|
222
|
+
clone.commaElements = this.commaElements.map(c => {
|
|
223
|
+
const newComma = new jsvgTextLine();
|
|
224
|
+
newComma.setText(c.text);
|
|
225
|
+
newComma.setTextAnchor(c.textAnchor);
|
|
226
|
+
newComma.svgObject.setAttribute('dominant-baseline', c.svgObject.getAttribute('dominant-baseline'));
|
|
227
|
+
newComma.parent = clone;
|
|
228
|
+
clone.addChild(newComma);
|
|
229
|
+
return newComma;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Now ensure correct order of children, if not already handled by createTextElements and createArgumentNodes
|
|
233
|
+
// The constructor already calls computeDimensions and updateLayout, which will handle final layout.
|
|
234
|
+
clone.addChild(clone.closedParen);
|
|
235
|
+
|
|
236
|
+
// Explicitly update the argumentNodeList in the cloned node
|
|
237
|
+
clone.argumentNodeList.args = clone.args;
|
|
238
|
+
|
|
239
|
+
// The crucial step: link the clone to its origin
|
|
240
|
+
clone.provenance.push(this.id);
|
|
241
|
+
|
|
242
|
+
return clone;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Converts the omdFunctionNode to a math.js AST node.
|
|
247
|
+
* @returns {Object} A math.js-compatible AST node.
|
|
248
|
+
*/
|
|
249
|
+
toMathJSNode() {
|
|
250
|
+
const astNode = {
|
|
251
|
+
type: 'FunctionNode',
|
|
252
|
+
fn: { type: 'SymbolNode', name: this.functionName, clone: function() { return {...this}; } },
|
|
253
|
+
args: this.argNodes.map(arg => arg.toMathJSNode())
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Add a clone method to maintain compatibility with math.js's expectations.
|
|
257
|
+
astNode.clone = function() {
|
|
258
|
+
const clonedNode = { ...this };
|
|
259
|
+
clonedNode.args = this.args.map(arg => arg.clone());
|
|
260
|
+
if (this.fn && typeof this.fn.clone === 'function') {
|
|
261
|
+
clonedNode.fn = this.fn.clone();
|
|
262
|
+
}
|
|
263
|
+
return clonedNode;
|
|
264
|
+
};
|
|
265
|
+
return astNode;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Convert to string representation
|
|
270
|
+
* @returns {string} The function as a string
|
|
271
|
+
*/
|
|
272
|
+
toString() {
|
|
273
|
+
const argStrings = this.argNodes.map(arg => arg.toString());
|
|
274
|
+
return `${this.functionName}(${argStrings.join(', ')})`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Evaluate the function with given variable values
|
|
279
|
+
* @param {Object} variables - Variable name to value mapping
|
|
280
|
+
* @returns {number} The evaluated result
|
|
281
|
+
*/
|
|
282
|
+
evaluate(variables = {}) {
|
|
283
|
+
// First evaluate all arguments
|
|
284
|
+
const evaluatedArgs = this.argNodes.map(arg => {
|
|
285
|
+
if (arg.evaluate) {
|
|
286
|
+
return arg.evaluate(variables);
|
|
287
|
+
} else if (arg.type === 'omdConstantNode') {
|
|
288
|
+
return parseFloat(arg.value);
|
|
289
|
+
} else if (arg.type === 'omdVariableNode' && variables[arg.name] !== undefined) {
|
|
290
|
+
return variables[arg.name];
|
|
291
|
+
}
|
|
292
|
+
throw new Error(`Cannot evaluate argument: ${arg.toString()}`);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Use math.js to evaluate the function
|
|
296
|
+
if (window.math && window.math[this.functionName]) {
|
|
297
|
+
try {
|
|
298
|
+
return window.math[this.functionName](...evaluatedArgs);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw new Error(`Error evaluating ${this.functionName}: ${error.message}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Fallback for common functions if math.js is not available
|
|
305
|
+
switch (this.functionName) {
|
|
306
|
+
case 'sin': return Math.sin(evaluatedArgs[0]);
|
|
307
|
+
case 'cos': return Math.cos(evaluatedArgs[0]);
|
|
308
|
+
case 'tan': return Math.tan(evaluatedArgs[0]);
|
|
309
|
+
case 'asin': return Math.asin(evaluatedArgs[0]);
|
|
310
|
+
case 'acos': return Math.acos(evaluatedArgs[0]);
|
|
311
|
+
case 'atan': return Math.atan(evaluatedArgs[0]);
|
|
312
|
+
case 'sqrt': return Math.sqrt(evaluatedArgs[0]);
|
|
313
|
+
case 'abs': return Math.abs(evaluatedArgs[0]);
|
|
314
|
+
case 'log': return Math.log(evaluatedArgs[0]);
|
|
315
|
+
case 'log10': return Math.log10(evaluatedArgs[0]);
|
|
316
|
+
case 'exp': return Math.exp(evaluatedArgs[0]);
|
|
317
|
+
case 'pow': return Math.pow(evaluatedArgs[0], evaluatedArgs[1]);
|
|
318
|
+
case 'min': return Math.min(...evaluatedArgs);
|
|
319
|
+
case 'max': return Math.max(...evaluatedArgs);
|
|
320
|
+
case 'floor': return Math.floor(evaluatedArgs[0]);
|
|
321
|
+
case 'ceil': return Math.ceil(evaluatedArgs[0]);
|
|
322
|
+
case 'round': return Math.round(evaluatedArgs[0]);
|
|
323
|
+
default:
|
|
324
|
+
throw new Error(`Unknown function: ${this.functionName}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Create a function from a string
|
|
330
|
+
* @param {string} functionString - The function as a string
|
|
331
|
+
* @returns {omdFunctionNode} The created function node
|
|
332
|
+
* @static
|
|
333
|
+
*/
|
|
334
|
+
static fromString(functionString) {
|
|
335
|
+
if (!window.math) {
|
|
336
|
+
throw new Error("Math.js is required for parsing function strings");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const ast = window.math.parse(functionString);
|
|
341
|
+
|
|
342
|
+
// Verify it's a function node
|
|
343
|
+
if (ast.type !== 'FunctionNode') {
|
|
344
|
+
throw new Error("String does not represent a function call");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return new omdFunctionNode(ast);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
throw new Error(`Failed to parse function string: ${error.message}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
352
|
}
|