@teachinglab/omd 0.6.1 → 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 -57
- 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,564 +1,564 @@
|
|
|
1
|
-
import { omdMetaExpression } from "./omdMetaExpression.js";
|
|
2
|
-
import { jsvgTextLine } from "@teachinglab/jsvg";
|
|
3
|
-
|
|
4
|
-
/* Gerard - ADDED */
|
|
5
|
-
const precedence = {
|
|
6
|
-
"+": 1,
|
|
7
|
-
"-": 1,
|
|
8
|
-
"*": 2,
|
|
9
|
-
"/": 2,
|
|
10
|
-
"^": 3
|
|
11
|
-
};
|
|
12
|
-
function getPrecedence(opNode) {
|
|
13
|
-
return precedence[opNode.name] || 0;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* omdNode - A minimal class representing a node in the expression tree
|
|
19
|
-
* Focuses purely on tree structure and layout, delegates visuals to superclasses
|
|
20
|
-
* Uses math.js AST format
|
|
21
|
-
*/
|
|
22
|
-
export class omdNode extends omdMetaExpression {
|
|
23
|
-
/**
|
|
24
|
-
* Constructor - Creates a tree node from math.js AST data
|
|
25
|
-
* @param {Object} nodeData - The AST node from math.js parser
|
|
26
|
-
*/
|
|
27
|
-
constructor(nodeData) {
|
|
28
|
-
super();
|
|
29
|
-
|
|
30
|
-
// Gerard added
|
|
31
|
-
this.canvasContext = document.createElement("canvas").getContext("2d");
|
|
32
|
-
this.canvasContext.font = "24px 'Alberto Sans'";
|
|
33
|
-
let measure = this.canvasContext.measureText("M");
|
|
34
|
-
this.textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
|
|
35
|
-
|
|
36
|
-
this.type = "omdNode";
|
|
37
|
-
this.nodeData = nodeData; // The AST node from math.js
|
|
38
|
-
this.childNodes = []; // Array of child omdNodes
|
|
39
|
-
|
|
40
|
-
/* Gerard added */
|
|
41
|
-
this.name = this.getNodeLabel();
|
|
42
|
-
this.mathType = nodeData.type;
|
|
43
|
-
|
|
44
|
-
/* Gerard - Unnecessary */
|
|
45
|
-
this.edgeLines = []; // Array of edge lines
|
|
46
|
-
|
|
47
|
-
this.zoomLevel = 1.0; // Current zoom level
|
|
48
|
-
|
|
49
|
-
// Initialize the tree structure
|
|
50
|
-
this.initializeNode();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Sets zoom level for this node and all children recursively
|
|
55
|
-
* @param {number} zoom - The zoom level (1.0 = 100%, 2.0 = 200%, etc.)
|
|
56
|
-
*/
|
|
57
|
-
setZoomLevel(zoom) {
|
|
58
|
-
this.zoomLevel = zoom;
|
|
59
|
-
// Apply to all child nodes
|
|
60
|
-
this.childNodes.forEach(child => child.setZoomLevel(zoom));
|
|
61
|
-
// Invalidate cached dimensions
|
|
62
|
-
this.invalidateCache();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Gets the width of this node, scaled by zoom level
|
|
67
|
-
* @returns {number} The node width in pixels
|
|
68
|
-
*/
|
|
69
|
-
get nodeWidth() {
|
|
70
|
-
if (!this._cachedWidth) {
|
|
71
|
-
// Use superclass text measurement if available, otherwise estimate
|
|
72
|
-
const label = this.getNodeLabel();
|
|
73
|
-
const baseWidth = Math.max(label.length * 12 + 20, 30);
|
|
74
|
-
this._cachedWidth = baseWidth * this.zoomLevel;
|
|
75
|
-
}
|
|
76
|
-
return this._cachedWidth;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Gets the height of this node, scaled by zoom level
|
|
81
|
-
* @returns {number} The node height in pixels
|
|
82
|
-
*/
|
|
83
|
-
get nodeHeight() {
|
|
84
|
-
return 50 * this.zoomLevel;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Gets the total width needed for this entire subtree
|
|
89
|
-
* @returns {number} The subtree width in pixels
|
|
90
|
-
*/
|
|
91
|
-
/* Gerard - Unnecessary */
|
|
92
|
-
get subtreeWidth() {
|
|
93
|
-
if (this.childNodes.length === 0) {
|
|
94
|
-
return this.nodeWidth;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const childSubtreeWidths = this.childNodes.map(child => child.subtreeWidth);
|
|
98
|
-
const totalChildWidth = childSubtreeWidths.reduce((sum, width) => sum + width, 0);
|
|
99
|
-
const spacing = (this.childNodes.length - 1) * this.nodeSpacing;
|
|
100
|
-
|
|
101
|
-
return Math.max(this.nodeWidth, totalChildWidth + spacing);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Gets the horizontal spacing between child nodes, scaled by zoom level
|
|
106
|
-
* @returns {number} The spacing in pixels
|
|
107
|
-
*/
|
|
108
|
-
/* Gerard - Unnecessary */
|
|
109
|
-
get nodeSpacing() {
|
|
110
|
-
return 100 * this.zoomLevel;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Gets the vertical spacing between tree levels, scaled by zoom level
|
|
115
|
-
* @returns {number} The level height in pixels
|
|
116
|
-
*/
|
|
117
|
-
/* Gerard - Unnecessary */
|
|
118
|
-
get levelHeight() {
|
|
119
|
-
return 120 * this.zoomLevel;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Invalidates cached dimension calculations when content changes
|
|
124
|
-
*/
|
|
125
|
-
invalidateCache() {
|
|
126
|
-
this._cachedWidth = null;
|
|
127
|
-
if (this.parent && this.parent.invalidateCache) {
|
|
128
|
-
this.parent.invalidateCache();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Main initialization method - sets up visuals, creates children, and layouts tree
|
|
134
|
-
*/
|
|
135
|
-
initializeNode() {
|
|
136
|
-
this.setupVisuals(); // Gerard removed
|
|
137
|
-
this.createChildNodes();
|
|
138
|
-
//this.layoutTree(); // Gerard removed
|
|
139
|
-
this.layoutExpression();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Creates the visual elements for this node (text label and background)
|
|
144
|
-
*/
|
|
145
|
-
/* Gerard - Modifiable */
|
|
146
|
-
setupVisuals() {
|
|
147
|
-
// Create text label using jsvg
|
|
148
|
-
this.nodeLabel = new jsvgTextLine();
|
|
149
|
-
this.nodeLabel.setText(this.getNodeLabel());
|
|
150
|
-
this.nodeLabel.setFontSize(24 * this.zoomLevel);
|
|
151
|
-
this.nodeLabel.setAlignment('center');
|
|
152
|
-
|
|
153
|
-
// Update superclass background size
|
|
154
|
-
this.updateSize();
|
|
155
|
-
|
|
156
|
-
// Position text at center
|
|
157
|
-
/* Gerard modified */
|
|
158
|
-
this.nodeLabel.setPosition(0, this.textHeight/2 + this.nodeHeight/2);
|
|
159
|
-
|
|
160
|
-
this.addChild(this.nodeLabel);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Updates the size of visual elements to match calculated dimensions
|
|
165
|
-
*/
|
|
166
|
-
updateSize() {
|
|
167
|
-
// Update the superclass background rectangle
|
|
168
|
-
this.backRect.setWidthAndHeight(this.nodeWidth, this.nodeHeight);
|
|
169
|
-
this.setWidthAndHeight(this.nodeWidth, this.nodeHeight);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Creates child nodes from AST data and connecting edge lines
|
|
174
|
-
*/
|
|
175
|
-
/* Gerard - 50/50 */
|
|
176
|
-
createChildNodes() {
|
|
177
|
-
const childrenData = this.getNodeChildren();
|
|
178
|
-
|
|
179
|
-
childrenData.forEach((childData, index) => {
|
|
180
|
-
/* Gerard - Necessary */
|
|
181
|
-
// Create child node
|
|
182
|
-
const childNode = new omdNode(childData);
|
|
183
|
-
childNode.parent = this;
|
|
184
|
-
childNode.mathType = childData.type;
|
|
185
|
-
this.childNodes.push(childNode);
|
|
186
|
-
this.addChild(childNode);
|
|
187
|
-
|
|
188
|
-
/* Gerard - Unnecessary */
|
|
189
|
-
// Create edge line
|
|
190
|
-
// const edge = new jsvgLine();
|
|
191
|
-
// edge.setStrokeWidth(3);
|
|
192
|
-
// this.edgeLines.push(edge);
|
|
193
|
-
// this.addChild(edge);
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/* Gerard - Modified */
|
|
198
|
-
|
|
199
|
-
layoutExpression() {
|
|
200
|
-
// First, calculates and sets size of the expression
|
|
201
|
-
let bounds = this.getExpressionBounds(getPrecedence(this));
|
|
202
|
-
this.setWidthAndHeight(bounds.right - bounds.left, this.nodeHeight);
|
|
203
|
-
|
|
204
|
-
// Then, positions children based on size
|
|
205
|
-
this.positionChildrenExpression(bounds);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Gerard: ADDED + QUESTION - Is some of the recursion necessary if the nodes are
|
|
209
|
-
// being created and initializing themselves?
|
|
210
|
-
positionChildrenExpression(bounds) {
|
|
211
|
-
// Function for positioning changes based on node type. Store it here
|
|
212
|
-
let positionFn;
|
|
213
|
-
switch (this.mathType) {
|
|
214
|
-
case "OperatorNode":
|
|
215
|
-
positionFn = (child, index) => {
|
|
216
|
-
// TODO: Comment
|
|
217
|
-
let x, y;
|
|
218
|
-
if (index === 0) {
|
|
219
|
-
x = bounds.left + this.nodeWidth / 2;
|
|
220
|
-
//console.log(bounds);
|
|
221
|
-
}
|
|
222
|
-
else /* if (index === 1) */ {
|
|
223
|
-
x = bounds.right - this.nodeWidth / 2;
|
|
224
|
-
}
|
|
225
|
-
y = bounds.bottom * 2;
|
|
226
|
-
child.setPosition(x, y);
|
|
227
|
-
//child.backRect.setPosition(-this.width / 2, y - this.height);
|
|
228
|
-
}
|
|
229
|
-
break;
|
|
230
|
-
case "FunctionNode":
|
|
231
|
-
case "ParenthesisNode":
|
|
232
|
-
// TODO: Comment
|
|
233
|
-
positionFn = (child) => {
|
|
234
|
-
//console.log(bounds.right - bounds.left);
|
|
235
|
-
//this.setWidthAndHeight(bounds.right - bounds.left, this.nodeHeight);
|
|
236
|
-
child.setPosition(0, bounds.bottom * 2);
|
|
237
|
-
}
|
|
238
|
-
break;
|
|
239
|
-
case "ConstantNode":
|
|
240
|
-
case "SymbolNode":
|
|
241
|
-
positionFn = () => {};
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
this.childNodes.forEach(positionFn);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/* Gerard - ADDED */
|
|
249
|
-
// Calculates the bounds of an expression.
|
|
250
|
-
// topPrecedence determines if it should calculate just leaves or entire subtree
|
|
251
|
-
getExpressionBounds(topPrecedence) {
|
|
252
|
-
// If this has no children, return just node size
|
|
253
|
-
if (this.childNodes.length === 0) {
|
|
254
|
-
return {
|
|
255
|
-
left: -this.nodeWidth / 2,
|
|
256
|
-
right: this.nodeWidth / 2,
|
|
257
|
-
top: -this.nodeHeight / 2,
|
|
258
|
-
bottom: this.nodeHeight / 2
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (this.mathType === "OperatorNode") {
|
|
263
|
-
//console.log(getPrecedence(this), topPrecedence);
|
|
264
|
-
|
|
265
|
-
let precedence = getPrecedence(this);
|
|
266
|
-
if (precedence !== topPrecedence) {
|
|
267
|
-
precedence = topPrecedence;
|
|
268
|
-
}
|
|
269
|
-
// Find the direct left and right operands
|
|
270
|
-
// If child operations are of same precedence, SHOULD NOT include the entire operation
|
|
271
|
-
let leftChild = this.findLeftOperand(precedence);
|
|
272
|
-
let rightChild = this.findRightOperand(precedence);
|
|
273
|
-
|
|
274
|
-
// Get the bounds of left and right direct operands
|
|
275
|
-
// If topPrecedence is lower than current operation, include all bounds
|
|
276
|
-
let leftChildBounds = leftChild.getExpressionBounds(topPrecedence);
|
|
277
|
-
let rightChildBounds = rightChild.getExpressionBounds(topPrecedence);
|
|
278
|
-
|
|
279
|
-
let left = -this.nodeWidth / 2 - (leftChildBounds.right - leftChildBounds.left);
|
|
280
|
-
let right = this.nodeWidth / 2 + (rightChildBounds.right - rightChildBounds.left);
|
|
281
|
-
|
|
282
|
-
return {
|
|
283
|
-
left: left,
|
|
284
|
-
right: right,
|
|
285
|
-
top: Math.min(leftChildBounds.top, rightChildBounds.top),
|
|
286
|
-
bottom: Math.max(leftChildBounds.bottom, rightChildBounds.bottom),
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
if (this.mathType === "FunctionNode") {
|
|
290
|
-
return this.childNodes[0].getExpressionBounds(getPrecedence(this.name));
|
|
291
|
-
}
|
|
292
|
-
if (this.mathType === "ParenthesisNode") {
|
|
293
|
-
// Parentheses always include total bounds (lowest precedence)
|
|
294
|
-
//console.log(getPrecedence(this.name));
|
|
295
|
-
return this.childNodes[0].getExpressionBounds(getPrecedence(this.name));
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/* Gerard - ADDED */
|
|
300
|
-
// TODO: Combine into one function? Probably
|
|
301
|
-
findLeftOperand(topPrecedence) {
|
|
302
|
-
// Return if not another operand
|
|
303
|
-
// Parenthesized Nodes or Constants/Symbols
|
|
304
|
-
if (this.mathType !== "OperatorNode") return this;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// Keep searching if child precedence is the same as the top precedence
|
|
308
|
-
// TODO: Assure logic
|
|
309
|
-
let childPrecedence = getPrecedence(this.childNodes[0]);
|
|
310
|
-
if (childPrecedence === topPrecedence ) {
|
|
311
|
-
return this.childNodes[0].findRightOperand(topPrecedence);
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
return this.childNodes[0];
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
findRightOperand(topPrecedence) {
|
|
319
|
-
// Return if not another operand
|
|
320
|
-
// Parenthesized Nodes or Constants/Symbols
|
|
321
|
-
if (this.mathType !== "OperatorNode") return this;
|
|
322
|
-
|
|
323
|
-
// Keep searching if child precedence is the same as the top precedence
|
|
324
|
-
// TODO: Assure logic
|
|
325
|
-
let childPrecedence = getPrecedence(this.childNodes[1]);
|
|
326
|
-
if (childPrecedence === topPrecedence) {
|
|
327
|
-
return this.childNodes[1].findLeftOperand(topPrecedence);
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
return this.childNodes[1];
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Recursively layouts the entire tree structure
|
|
336
|
-
*/
|
|
337
|
-
/* Gerard - Unnecessary */
|
|
338
|
-
layoutTree() {
|
|
339
|
-
if (this.childNodes.length === 0) return;
|
|
340
|
-
|
|
341
|
-
// Layout children first to get their final sizes
|
|
342
|
-
this.childNodes.forEach(child => child.layoutTree());
|
|
343
|
-
|
|
344
|
-
// Calculate positions
|
|
345
|
-
this.positionChildren();
|
|
346
|
-
this.updateEdges();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Positions child nodes horizontally to center them under this node
|
|
351
|
-
*/
|
|
352
|
-
/* Gerard - Unnecessary */
|
|
353
|
-
positionChildren() {
|
|
354
|
-
if (this.childNodes.length === 0) return;
|
|
355
|
-
|
|
356
|
-
// Calculate starting position to center children
|
|
357
|
-
const totalWidth = this.childNodes.reduce((sum, child) => sum + child.subtreeWidth, 0);
|
|
358
|
-
const totalSpacing = (this.childNodes.length - 1) * this.nodeSpacing;
|
|
359
|
-
const totalRequiredWidth = totalWidth + totalSpacing;
|
|
360
|
-
|
|
361
|
-
const startX = -totalRequiredWidth / 2 + this.nodeWidth / 2;
|
|
362
|
-
const childY = this.nodeHeight + this.levelHeight;
|
|
363
|
-
|
|
364
|
-
let currentX = startX;
|
|
365
|
-
|
|
366
|
-
this.childNodes.forEach((child, index) => {
|
|
367
|
-
// Center child in its allocated subtree space
|
|
368
|
-
const childX = currentX + child.subtreeWidth / 2 - child.nodeWidth / 2;
|
|
369
|
-
|
|
370
|
-
// Position using jsvg method
|
|
371
|
-
child.setPosition(childX, childY);
|
|
372
|
-
|
|
373
|
-
// Move to next position
|
|
374
|
-
currentX += child.subtreeWidth + this.nodeSpacing;
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Updates the connecting edge lines between this node and its children
|
|
380
|
-
*/
|
|
381
|
-
/* Gerard - Unnecessary */
|
|
382
|
-
updateEdges() {
|
|
383
|
-
this.childNodes.forEach((child, index) => {
|
|
384
|
-
if (this.edgeLines[index]) {
|
|
385
|
-
// Connect center-bottom of parent to center-top of child
|
|
386
|
-
const parentCenterX = this.nodeWidth / 2;
|
|
387
|
-
const parentBottomY = this.nodeHeight;
|
|
388
|
-
|
|
389
|
-
const childCenterX = child.xpos + child.nodeWidth / 2;
|
|
390
|
-
const childTopY = child.ypos;
|
|
391
|
-
|
|
392
|
-
this.edgeLines[index].setEndpointA(parentCenterX, parentBottomY);
|
|
393
|
-
this.edgeLines[index].setEndpointB(childCenterX, childTopY);
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Extracts child node data from the math.js AST based on node type
|
|
400
|
-
* @returns {Array} Array of child AST nodes
|
|
401
|
-
*/
|
|
402
|
-
getNodeChildren() {
|
|
403
|
-
if (!this.nodeData) return [];
|
|
404
|
-
|
|
405
|
-
const nodeType = this.detectNodeType();
|
|
406
|
-
|
|
407
|
-
// Handle math.js node types
|
|
408
|
-
switch (nodeType) {
|
|
409
|
-
case 'FunctionNode':
|
|
410
|
-
return this.nodeData.args || [];
|
|
411
|
-
case 'MatrixNode':
|
|
412
|
-
return this.nodeData.args || [];
|
|
413
|
-
case 'OperatorNode':
|
|
414
|
-
return this.nodeData.args || [];
|
|
415
|
-
case 'ParenthesisNode':
|
|
416
|
-
return [this.nodeData.content];
|
|
417
|
-
case 'ArrayNode':
|
|
418
|
-
return this.nodeData.items || [];
|
|
419
|
-
case 'IndexNode':
|
|
420
|
-
return [this.nodeData.object, this.nodeData.index];
|
|
421
|
-
case 'AccessorNode':
|
|
422
|
-
return [this.nodeData.object, this.nodeData.index];
|
|
423
|
-
case 'AssignmentNode':
|
|
424
|
-
return [this.nodeData.object, this.nodeData.value];
|
|
425
|
-
case 'ConditionalNode':
|
|
426
|
-
return [this.nodeData.condition, this.nodeData.trueExpr, this.nodeData.falseExpr];
|
|
427
|
-
default:
|
|
428
|
-
return [];
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Determines the type of math.js AST node by examining its properties
|
|
434
|
-
* @returns {string} The node type (e.g., 'OperatorNode', 'ConstantNode', etc.)
|
|
435
|
-
*/
|
|
436
|
-
detectNodeType() {
|
|
437
|
-
if (!this.nodeData) return 'Unknown';
|
|
438
|
-
|
|
439
|
-
// Check for properties that uniquely identify each node type
|
|
440
|
-
if (this.nodeData.hasOwnProperty('op') && this.nodeData.hasOwnProperty('args')) {
|
|
441
|
-
return 'OperatorNode';
|
|
442
|
-
} else if (this.nodeData.hasOwnProperty('fn') && this.nodeData.hasOwnProperty('args')) {
|
|
443
|
-
if (this.nodeData.fn === 'matrix') {
|
|
444
|
-
return 'MatrixNode';
|
|
445
|
-
}
|
|
446
|
-
return 'FunctionNode';
|
|
447
|
-
} else if (this.nodeData.hasOwnProperty('value')) {
|
|
448
|
-
return 'ConstantNode';
|
|
449
|
-
} else if (this.nodeData.hasOwnProperty('name') && !this.nodeData.hasOwnProperty('args')) {
|
|
450
|
-
return 'SymbolNode';
|
|
451
|
-
} else if (this.nodeData.hasOwnProperty('content')) {
|
|
452
|
-
return 'ParenthesisNode';
|
|
453
|
-
} else if (this.nodeData.hasOwnProperty('items')) {
|
|
454
|
-
return 'ArrayNode';
|
|
455
|
-
} else if (this.nodeData.hasOwnProperty('object') && this.nodeData.hasOwnProperty('index')) {
|
|
456
|
-
return 'IndexNode';
|
|
457
|
-
} else if (this.nodeData.hasOwnProperty('condition')) {
|
|
458
|
-
return 'ConditionalNode';
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return 'Unknown';
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Generates the display label for this node based on its AST data
|
|
466
|
-
* @returns {string} The text label to display in the node
|
|
467
|
-
*/
|
|
468
|
-
getNodeLabel() {
|
|
469
|
-
const nodeType = this.detectNodeType();
|
|
470
|
-
|
|
471
|
-
switch (nodeType) {
|
|
472
|
-
case 'ConstantNode':
|
|
473
|
-
return this.nodeData.value.toString();
|
|
474
|
-
case 'SymbolNode':
|
|
475
|
-
return this.nodeData.name;
|
|
476
|
-
case 'FunctionNode':
|
|
477
|
-
return this.nodeData.fn.name || this.nodeData.fn;
|
|
478
|
-
case 'MatrixNode':
|
|
479
|
-
if (this.nodeData.args && this.nodeData.args.length > 0) {
|
|
480
|
-
const firstArg = this.nodeData.args[0];
|
|
481
|
-
if (firstArg && firstArg.items) {
|
|
482
|
-
const rows = firstArg.items.length;
|
|
483
|
-
const cols = firstArg.items[0] && firstArg.items[0].items ? firstArg.items[0].items.length : 1;
|
|
484
|
-
return `Matrix ${rows}×${cols}`;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return 'Matrix';
|
|
488
|
-
case 'OperatorNode':
|
|
489
|
-
const operatorMap = {
|
|
490
|
-
'add': '+',
|
|
491
|
-
'subtract': '-',
|
|
492
|
-
'multiply': '*',
|
|
493
|
-
'divide': '/',
|
|
494
|
-
'pow': '^',
|
|
495
|
-
'unaryMinus': '-',
|
|
496
|
-
'unaryPlus': '+'
|
|
497
|
-
};
|
|
498
|
-
return operatorMap[this.nodeData.fn] || this.nodeData.fn;
|
|
499
|
-
case 'ParenthesisNode':
|
|
500
|
-
return '( )';
|
|
501
|
-
case 'ArrayNode':
|
|
502
|
-
return '[ ]';
|
|
503
|
-
case 'IndexNode':
|
|
504
|
-
return '[]';
|
|
505
|
-
case 'AccessorNode':
|
|
506
|
-
return '.';
|
|
507
|
-
case 'AssignmentNode':
|
|
508
|
-
return '=';
|
|
509
|
-
case 'ConditionalNode':
|
|
510
|
-
return '?:';
|
|
511
|
-
default:
|
|
512
|
-
return nodeType || '?';
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Calculates the bounding box of this entire subtree
|
|
518
|
-
* @returns {Object} Object with left, right, top, bottom coordinates
|
|
519
|
-
*/
|
|
520
|
-
/* Gerard - Unnecessary */
|
|
521
|
-
getTreeBounds() {
|
|
522
|
-
const myX = this.xpos || 0;
|
|
523
|
-
const myY = this.ypos || 0;
|
|
524
|
-
|
|
525
|
-
if (this.childNodes.length === 0) {
|
|
526
|
-
return {
|
|
527
|
-
left: myX,
|
|
528
|
-
right: myX + this.nodeWidth,
|
|
529
|
-
top: myY,
|
|
530
|
-
bottom: myY + this.nodeHeight
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
let left = myX;
|
|
535
|
-
let right = myX + this.nodeWidth;
|
|
536
|
-
let top = myY;
|
|
537
|
-
let bottom = myY + this.nodeHeight;
|
|
538
|
-
|
|
539
|
-
this.childNodes.forEach(child => {
|
|
540
|
-
const childBounds = child.getTreeBounds();
|
|
541
|
-
left = Math.min(left, childBounds.left);
|
|
542
|
-
right = Math.max(right, childBounds.right);
|
|
543
|
-
top = Math.min(top, childBounds.top);
|
|
544
|
-
bottom = Math.max(bottom, childBounds.bottom);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
return { left, right, top, bottom };
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* Gets the total dimensions of this tree for layout purposes
|
|
552
|
-
* @returns {Object} Object with width, height, and bounds
|
|
553
|
-
*/
|
|
554
|
-
/* Gerard - Unnecessary */
|
|
555
|
-
/* Gerard: Question - Unused? */
|
|
556
|
-
getTreeDimensions() {
|
|
557
|
-
const bounds = this.getTreeBounds();
|
|
558
|
-
return {
|
|
559
|
-
width: bounds.right - bounds.left,
|
|
560
|
-
height: bounds.bottom - bounds.top,
|
|
561
|
-
bounds: bounds
|
|
562
|
-
};
|
|
563
|
-
}
|
|
1
|
+
import { omdMetaExpression } from "./omdMetaExpression.js";
|
|
2
|
+
import { jsvgTextLine } from "@teachinglab/jsvg";
|
|
3
|
+
|
|
4
|
+
/* Gerard - ADDED */
|
|
5
|
+
const precedence = {
|
|
6
|
+
"+": 1,
|
|
7
|
+
"-": 1,
|
|
8
|
+
"*": 2,
|
|
9
|
+
"/": 2,
|
|
10
|
+
"^": 3
|
|
11
|
+
};
|
|
12
|
+
function getPrecedence(opNode) {
|
|
13
|
+
return precedence[opNode.name] || 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* omdNode - A minimal class representing a node in the expression tree
|
|
19
|
+
* Focuses purely on tree structure and layout, delegates visuals to superclasses
|
|
20
|
+
* Uses math.js AST format
|
|
21
|
+
*/
|
|
22
|
+
export class omdNode extends omdMetaExpression {
|
|
23
|
+
/**
|
|
24
|
+
* Constructor - Creates a tree node from math.js AST data
|
|
25
|
+
* @param {Object} nodeData - The AST node from math.js parser
|
|
26
|
+
*/
|
|
27
|
+
constructor(nodeData) {
|
|
28
|
+
super();
|
|
29
|
+
|
|
30
|
+
// Gerard added
|
|
31
|
+
this.canvasContext = document.createElement("canvas").getContext("2d");
|
|
32
|
+
this.canvasContext.font = "24px 'Alberto Sans'";
|
|
33
|
+
let measure = this.canvasContext.measureText("M");
|
|
34
|
+
this.textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
|
|
35
|
+
|
|
36
|
+
this.type = "omdNode";
|
|
37
|
+
this.nodeData = nodeData; // The AST node from math.js
|
|
38
|
+
this.childNodes = []; // Array of child omdNodes
|
|
39
|
+
|
|
40
|
+
/* Gerard added */
|
|
41
|
+
this.name = this.getNodeLabel();
|
|
42
|
+
this.mathType = nodeData.type;
|
|
43
|
+
|
|
44
|
+
/* Gerard - Unnecessary */
|
|
45
|
+
this.edgeLines = []; // Array of edge lines
|
|
46
|
+
|
|
47
|
+
this.zoomLevel = 1.0; // Current zoom level
|
|
48
|
+
|
|
49
|
+
// Initialize the tree structure
|
|
50
|
+
this.initializeNode();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sets zoom level for this node and all children recursively
|
|
55
|
+
* @param {number} zoom - The zoom level (1.0 = 100%, 2.0 = 200%, etc.)
|
|
56
|
+
*/
|
|
57
|
+
setZoomLevel(zoom) {
|
|
58
|
+
this.zoomLevel = zoom;
|
|
59
|
+
// Apply to all child nodes
|
|
60
|
+
this.childNodes.forEach(child => child.setZoomLevel(zoom));
|
|
61
|
+
// Invalidate cached dimensions
|
|
62
|
+
this.invalidateCache();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Gets the width of this node, scaled by zoom level
|
|
67
|
+
* @returns {number} The node width in pixels
|
|
68
|
+
*/
|
|
69
|
+
get nodeWidth() {
|
|
70
|
+
if (!this._cachedWidth) {
|
|
71
|
+
// Use superclass text measurement if available, otherwise estimate
|
|
72
|
+
const label = this.getNodeLabel();
|
|
73
|
+
const baseWidth = Math.max(label.length * 12 + 20, 30);
|
|
74
|
+
this._cachedWidth = baseWidth * this.zoomLevel;
|
|
75
|
+
}
|
|
76
|
+
return this._cachedWidth;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Gets the height of this node, scaled by zoom level
|
|
81
|
+
* @returns {number} The node height in pixels
|
|
82
|
+
*/
|
|
83
|
+
get nodeHeight() {
|
|
84
|
+
return 50 * this.zoomLevel;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Gets the total width needed for this entire subtree
|
|
89
|
+
* @returns {number} The subtree width in pixels
|
|
90
|
+
*/
|
|
91
|
+
/* Gerard - Unnecessary */
|
|
92
|
+
get subtreeWidth() {
|
|
93
|
+
if (this.childNodes.length === 0) {
|
|
94
|
+
return this.nodeWidth;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const childSubtreeWidths = this.childNodes.map(child => child.subtreeWidth);
|
|
98
|
+
const totalChildWidth = childSubtreeWidths.reduce((sum, width) => sum + width, 0);
|
|
99
|
+
const spacing = (this.childNodes.length - 1) * this.nodeSpacing;
|
|
100
|
+
|
|
101
|
+
return Math.max(this.nodeWidth, totalChildWidth + spacing);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gets the horizontal spacing between child nodes, scaled by zoom level
|
|
106
|
+
* @returns {number} The spacing in pixels
|
|
107
|
+
*/
|
|
108
|
+
/* Gerard - Unnecessary */
|
|
109
|
+
get nodeSpacing() {
|
|
110
|
+
return 100 * this.zoomLevel;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Gets the vertical spacing between tree levels, scaled by zoom level
|
|
115
|
+
* @returns {number} The level height in pixels
|
|
116
|
+
*/
|
|
117
|
+
/* Gerard - Unnecessary */
|
|
118
|
+
get levelHeight() {
|
|
119
|
+
return 120 * this.zoomLevel;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Invalidates cached dimension calculations when content changes
|
|
124
|
+
*/
|
|
125
|
+
invalidateCache() {
|
|
126
|
+
this._cachedWidth = null;
|
|
127
|
+
if (this.parent && this.parent.invalidateCache) {
|
|
128
|
+
this.parent.invalidateCache();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Main initialization method - sets up visuals, creates children, and layouts tree
|
|
134
|
+
*/
|
|
135
|
+
initializeNode() {
|
|
136
|
+
this.setupVisuals(); // Gerard removed
|
|
137
|
+
this.createChildNodes();
|
|
138
|
+
//this.layoutTree(); // Gerard removed
|
|
139
|
+
this.layoutExpression();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Creates the visual elements for this node (text label and background)
|
|
144
|
+
*/
|
|
145
|
+
/* Gerard - Modifiable */
|
|
146
|
+
setupVisuals() {
|
|
147
|
+
// Create text label using jsvg
|
|
148
|
+
this.nodeLabel = new jsvgTextLine();
|
|
149
|
+
this.nodeLabel.setText(this.getNodeLabel());
|
|
150
|
+
this.nodeLabel.setFontSize(24 * this.zoomLevel);
|
|
151
|
+
this.nodeLabel.setAlignment('center');
|
|
152
|
+
|
|
153
|
+
// Update superclass background size
|
|
154
|
+
this.updateSize();
|
|
155
|
+
|
|
156
|
+
// Position text at center
|
|
157
|
+
/* Gerard modified */
|
|
158
|
+
this.nodeLabel.setPosition(0, this.textHeight/2 + this.nodeHeight/2);
|
|
159
|
+
|
|
160
|
+
this.addChild(this.nodeLabel);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Updates the size of visual elements to match calculated dimensions
|
|
165
|
+
*/
|
|
166
|
+
updateSize() {
|
|
167
|
+
// Update the superclass background rectangle
|
|
168
|
+
this.backRect.setWidthAndHeight(this.nodeWidth, this.nodeHeight);
|
|
169
|
+
this.setWidthAndHeight(this.nodeWidth, this.nodeHeight);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Creates child nodes from AST data and connecting edge lines
|
|
174
|
+
*/
|
|
175
|
+
/* Gerard - 50/50 */
|
|
176
|
+
createChildNodes() {
|
|
177
|
+
const childrenData = this.getNodeChildren();
|
|
178
|
+
|
|
179
|
+
childrenData.forEach((childData, index) => {
|
|
180
|
+
/* Gerard - Necessary */
|
|
181
|
+
// Create child node
|
|
182
|
+
const childNode = new omdNode(childData);
|
|
183
|
+
childNode.parent = this;
|
|
184
|
+
childNode.mathType = childData.type;
|
|
185
|
+
this.childNodes.push(childNode);
|
|
186
|
+
this.addChild(childNode);
|
|
187
|
+
|
|
188
|
+
/* Gerard - Unnecessary */
|
|
189
|
+
// Create edge line
|
|
190
|
+
// const edge = new jsvgLine();
|
|
191
|
+
// edge.setStrokeWidth(3);
|
|
192
|
+
// this.edgeLines.push(edge);
|
|
193
|
+
// this.addChild(edge);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Gerard - Modified */
|
|
198
|
+
|
|
199
|
+
layoutExpression() {
|
|
200
|
+
// First, calculates and sets size of the expression
|
|
201
|
+
let bounds = this.getExpressionBounds(getPrecedence(this));
|
|
202
|
+
this.setWidthAndHeight(bounds.right - bounds.left, this.nodeHeight);
|
|
203
|
+
|
|
204
|
+
// Then, positions children based on size
|
|
205
|
+
this.positionChildrenExpression(bounds);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Gerard: ADDED + QUESTION - Is some of the recursion necessary if the nodes are
|
|
209
|
+
// being created and initializing themselves?
|
|
210
|
+
positionChildrenExpression(bounds) {
|
|
211
|
+
// Function for positioning changes based on node type. Store it here
|
|
212
|
+
let positionFn;
|
|
213
|
+
switch (this.mathType) {
|
|
214
|
+
case "OperatorNode":
|
|
215
|
+
positionFn = (child, index) => {
|
|
216
|
+
// TODO: Comment
|
|
217
|
+
let x, y;
|
|
218
|
+
if (index === 0) {
|
|
219
|
+
x = bounds.left + this.nodeWidth / 2;
|
|
220
|
+
//console.log(bounds);
|
|
221
|
+
}
|
|
222
|
+
else /* if (index === 1) */ {
|
|
223
|
+
x = bounds.right - this.nodeWidth / 2;
|
|
224
|
+
}
|
|
225
|
+
y = bounds.bottom * 2;
|
|
226
|
+
child.setPosition(x, y);
|
|
227
|
+
//child.backRect.setPosition(-this.width / 2, y - this.height);
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
case "FunctionNode":
|
|
231
|
+
case "ParenthesisNode":
|
|
232
|
+
// TODO: Comment
|
|
233
|
+
positionFn = (child) => {
|
|
234
|
+
//console.log(bounds.right - bounds.left);
|
|
235
|
+
//this.setWidthAndHeight(bounds.right - bounds.left, this.nodeHeight);
|
|
236
|
+
child.setPosition(0, bounds.bottom * 2);
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
case "ConstantNode":
|
|
240
|
+
case "SymbolNode":
|
|
241
|
+
positionFn = () => {};
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.childNodes.forEach(positionFn);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Gerard - ADDED */
|
|
249
|
+
// Calculates the bounds of an expression.
|
|
250
|
+
// topPrecedence determines if it should calculate just leaves or entire subtree
|
|
251
|
+
getExpressionBounds(topPrecedence) {
|
|
252
|
+
// If this has no children, return just node size
|
|
253
|
+
if (this.childNodes.length === 0) {
|
|
254
|
+
return {
|
|
255
|
+
left: -this.nodeWidth / 2,
|
|
256
|
+
right: this.nodeWidth / 2,
|
|
257
|
+
top: -this.nodeHeight / 2,
|
|
258
|
+
bottom: this.nodeHeight / 2
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (this.mathType === "OperatorNode") {
|
|
263
|
+
//console.log(getPrecedence(this), topPrecedence);
|
|
264
|
+
|
|
265
|
+
let precedence = getPrecedence(this);
|
|
266
|
+
if (precedence !== topPrecedence) {
|
|
267
|
+
precedence = topPrecedence;
|
|
268
|
+
}
|
|
269
|
+
// Find the direct left and right operands
|
|
270
|
+
// If child operations are of same precedence, SHOULD NOT include the entire operation
|
|
271
|
+
let leftChild = this.findLeftOperand(precedence);
|
|
272
|
+
let rightChild = this.findRightOperand(precedence);
|
|
273
|
+
|
|
274
|
+
// Get the bounds of left and right direct operands
|
|
275
|
+
// If topPrecedence is lower than current operation, include all bounds
|
|
276
|
+
let leftChildBounds = leftChild.getExpressionBounds(topPrecedence);
|
|
277
|
+
let rightChildBounds = rightChild.getExpressionBounds(topPrecedence);
|
|
278
|
+
|
|
279
|
+
let left = -this.nodeWidth / 2 - (leftChildBounds.right - leftChildBounds.left);
|
|
280
|
+
let right = this.nodeWidth / 2 + (rightChildBounds.right - rightChildBounds.left);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
left: left,
|
|
284
|
+
right: right,
|
|
285
|
+
top: Math.min(leftChildBounds.top, rightChildBounds.top),
|
|
286
|
+
bottom: Math.max(leftChildBounds.bottom, rightChildBounds.bottom),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (this.mathType === "FunctionNode") {
|
|
290
|
+
return this.childNodes[0].getExpressionBounds(getPrecedence(this.name));
|
|
291
|
+
}
|
|
292
|
+
if (this.mathType === "ParenthesisNode") {
|
|
293
|
+
// Parentheses always include total bounds (lowest precedence)
|
|
294
|
+
//console.log(getPrecedence(this.name));
|
|
295
|
+
return this.childNodes[0].getExpressionBounds(getPrecedence(this.name));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Gerard - ADDED */
|
|
300
|
+
// TODO: Combine into one function? Probably
|
|
301
|
+
findLeftOperand(topPrecedence) {
|
|
302
|
+
// Return if not another operand
|
|
303
|
+
// Parenthesized Nodes or Constants/Symbols
|
|
304
|
+
if (this.mathType !== "OperatorNode") return this;
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
// Keep searching if child precedence is the same as the top precedence
|
|
308
|
+
// TODO: Assure logic
|
|
309
|
+
let childPrecedence = getPrecedence(this.childNodes[0]);
|
|
310
|
+
if (childPrecedence === topPrecedence ) {
|
|
311
|
+
return this.childNodes[0].findRightOperand(topPrecedence);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
return this.childNodes[0];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
findRightOperand(topPrecedence) {
|
|
319
|
+
// Return if not another operand
|
|
320
|
+
// Parenthesized Nodes or Constants/Symbols
|
|
321
|
+
if (this.mathType !== "OperatorNode") return this;
|
|
322
|
+
|
|
323
|
+
// Keep searching if child precedence is the same as the top precedence
|
|
324
|
+
// TODO: Assure logic
|
|
325
|
+
let childPrecedence = getPrecedence(this.childNodes[1]);
|
|
326
|
+
if (childPrecedence === topPrecedence) {
|
|
327
|
+
return this.childNodes[1].findLeftOperand(topPrecedence);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
return this.childNodes[1];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Recursively layouts the entire tree structure
|
|
336
|
+
*/
|
|
337
|
+
/* Gerard - Unnecessary */
|
|
338
|
+
layoutTree() {
|
|
339
|
+
if (this.childNodes.length === 0) return;
|
|
340
|
+
|
|
341
|
+
// Layout children first to get their final sizes
|
|
342
|
+
this.childNodes.forEach(child => child.layoutTree());
|
|
343
|
+
|
|
344
|
+
// Calculate positions
|
|
345
|
+
this.positionChildren();
|
|
346
|
+
this.updateEdges();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Positions child nodes horizontally to center them under this node
|
|
351
|
+
*/
|
|
352
|
+
/* Gerard - Unnecessary */
|
|
353
|
+
positionChildren() {
|
|
354
|
+
if (this.childNodes.length === 0) return;
|
|
355
|
+
|
|
356
|
+
// Calculate starting position to center children
|
|
357
|
+
const totalWidth = this.childNodes.reduce((sum, child) => sum + child.subtreeWidth, 0);
|
|
358
|
+
const totalSpacing = (this.childNodes.length - 1) * this.nodeSpacing;
|
|
359
|
+
const totalRequiredWidth = totalWidth + totalSpacing;
|
|
360
|
+
|
|
361
|
+
const startX = -totalRequiredWidth / 2 + this.nodeWidth / 2;
|
|
362
|
+
const childY = this.nodeHeight + this.levelHeight;
|
|
363
|
+
|
|
364
|
+
let currentX = startX;
|
|
365
|
+
|
|
366
|
+
this.childNodes.forEach((child, index) => {
|
|
367
|
+
// Center child in its allocated subtree space
|
|
368
|
+
const childX = currentX + child.subtreeWidth / 2 - child.nodeWidth / 2;
|
|
369
|
+
|
|
370
|
+
// Position using jsvg method
|
|
371
|
+
child.setPosition(childX, childY);
|
|
372
|
+
|
|
373
|
+
// Move to next position
|
|
374
|
+
currentX += child.subtreeWidth + this.nodeSpacing;
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Updates the connecting edge lines between this node and its children
|
|
380
|
+
*/
|
|
381
|
+
/* Gerard - Unnecessary */
|
|
382
|
+
updateEdges() {
|
|
383
|
+
this.childNodes.forEach((child, index) => {
|
|
384
|
+
if (this.edgeLines[index]) {
|
|
385
|
+
// Connect center-bottom of parent to center-top of child
|
|
386
|
+
const parentCenterX = this.nodeWidth / 2;
|
|
387
|
+
const parentBottomY = this.nodeHeight;
|
|
388
|
+
|
|
389
|
+
const childCenterX = child.xpos + child.nodeWidth / 2;
|
|
390
|
+
const childTopY = child.ypos;
|
|
391
|
+
|
|
392
|
+
this.edgeLines[index].setEndpointA(parentCenterX, parentBottomY);
|
|
393
|
+
this.edgeLines[index].setEndpointB(childCenterX, childTopY);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Extracts child node data from the math.js AST based on node type
|
|
400
|
+
* @returns {Array} Array of child AST nodes
|
|
401
|
+
*/
|
|
402
|
+
getNodeChildren() {
|
|
403
|
+
if (!this.nodeData) return [];
|
|
404
|
+
|
|
405
|
+
const nodeType = this.detectNodeType();
|
|
406
|
+
|
|
407
|
+
// Handle math.js node types
|
|
408
|
+
switch (nodeType) {
|
|
409
|
+
case 'FunctionNode':
|
|
410
|
+
return this.nodeData.args || [];
|
|
411
|
+
case 'MatrixNode':
|
|
412
|
+
return this.nodeData.args || [];
|
|
413
|
+
case 'OperatorNode':
|
|
414
|
+
return this.nodeData.args || [];
|
|
415
|
+
case 'ParenthesisNode':
|
|
416
|
+
return [this.nodeData.content];
|
|
417
|
+
case 'ArrayNode':
|
|
418
|
+
return this.nodeData.items || [];
|
|
419
|
+
case 'IndexNode':
|
|
420
|
+
return [this.nodeData.object, this.nodeData.index];
|
|
421
|
+
case 'AccessorNode':
|
|
422
|
+
return [this.nodeData.object, this.nodeData.index];
|
|
423
|
+
case 'AssignmentNode':
|
|
424
|
+
return [this.nodeData.object, this.nodeData.value];
|
|
425
|
+
case 'ConditionalNode':
|
|
426
|
+
return [this.nodeData.condition, this.nodeData.trueExpr, this.nodeData.falseExpr];
|
|
427
|
+
default:
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Determines the type of math.js AST node by examining its properties
|
|
434
|
+
* @returns {string} The node type (e.g., 'OperatorNode', 'ConstantNode', etc.)
|
|
435
|
+
*/
|
|
436
|
+
detectNodeType() {
|
|
437
|
+
if (!this.nodeData) return 'Unknown';
|
|
438
|
+
|
|
439
|
+
// Check for properties that uniquely identify each node type
|
|
440
|
+
if (this.nodeData.hasOwnProperty('op') && this.nodeData.hasOwnProperty('args')) {
|
|
441
|
+
return 'OperatorNode';
|
|
442
|
+
} else if (this.nodeData.hasOwnProperty('fn') && this.nodeData.hasOwnProperty('args')) {
|
|
443
|
+
if (this.nodeData.fn === 'matrix') {
|
|
444
|
+
return 'MatrixNode';
|
|
445
|
+
}
|
|
446
|
+
return 'FunctionNode';
|
|
447
|
+
} else if (this.nodeData.hasOwnProperty('value')) {
|
|
448
|
+
return 'ConstantNode';
|
|
449
|
+
} else if (this.nodeData.hasOwnProperty('name') && !this.nodeData.hasOwnProperty('args')) {
|
|
450
|
+
return 'SymbolNode';
|
|
451
|
+
} else if (this.nodeData.hasOwnProperty('content')) {
|
|
452
|
+
return 'ParenthesisNode';
|
|
453
|
+
} else if (this.nodeData.hasOwnProperty('items')) {
|
|
454
|
+
return 'ArrayNode';
|
|
455
|
+
} else if (this.nodeData.hasOwnProperty('object') && this.nodeData.hasOwnProperty('index')) {
|
|
456
|
+
return 'IndexNode';
|
|
457
|
+
} else if (this.nodeData.hasOwnProperty('condition')) {
|
|
458
|
+
return 'ConditionalNode';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return 'Unknown';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Generates the display label for this node based on its AST data
|
|
466
|
+
* @returns {string} The text label to display in the node
|
|
467
|
+
*/
|
|
468
|
+
getNodeLabel() {
|
|
469
|
+
const nodeType = this.detectNodeType();
|
|
470
|
+
|
|
471
|
+
switch (nodeType) {
|
|
472
|
+
case 'ConstantNode':
|
|
473
|
+
return this.nodeData.value.toString();
|
|
474
|
+
case 'SymbolNode':
|
|
475
|
+
return this.nodeData.name;
|
|
476
|
+
case 'FunctionNode':
|
|
477
|
+
return this.nodeData.fn.name || this.nodeData.fn;
|
|
478
|
+
case 'MatrixNode':
|
|
479
|
+
if (this.nodeData.args && this.nodeData.args.length > 0) {
|
|
480
|
+
const firstArg = this.nodeData.args[0];
|
|
481
|
+
if (firstArg && firstArg.items) {
|
|
482
|
+
const rows = firstArg.items.length;
|
|
483
|
+
const cols = firstArg.items[0] && firstArg.items[0].items ? firstArg.items[0].items.length : 1;
|
|
484
|
+
return `Matrix ${rows}×${cols}`;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return 'Matrix';
|
|
488
|
+
case 'OperatorNode':
|
|
489
|
+
const operatorMap = {
|
|
490
|
+
'add': '+',
|
|
491
|
+
'subtract': '-',
|
|
492
|
+
'multiply': '*',
|
|
493
|
+
'divide': '/',
|
|
494
|
+
'pow': '^',
|
|
495
|
+
'unaryMinus': '-',
|
|
496
|
+
'unaryPlus': '+'
|
|
497
|
+
};
|
|
498
|
+
return operatorMap[this.nodeData.fn] || this.nodeData.fn;
|
|
499
|
+
case 'ParenthesisNode':
|
|
500
|
+
return '( )';
|
|
501
|
+
case 'ArrayNode':
|
|
502
|
+
return '[ ]';
|
|
503
|
+
case 'IndexNode':
|
|
504
|
+
return '[]';
|
|
505
|
+
case 'AccessorNode':
|
|
506
|
+
return '.';
|
|
507
|
+
case 'AssignmentNode':
|
|
508
|
+
return '=';
|
|
509
|
+
case 'ConditionalNode':
|
|
510
|
+
return '?:';
|
|
511
|
+
default:
|
|
512
|
+
return nodeType || '?';
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Calculates the bounding box of this entire subtree
|
|
518
|
+
* @returns {Object} Object with left, right, top, bottom coordinates
|
|
519
|
+
*/
|
|
520
|
+
/* Gerard - Unnecessary */
|
|
521
|
+
getTreeBounds() {
|
|
522
|
+
const myX = this.xpos || 0;
|
|
523
|
+
const myY = this.ypos || 0;
|
|
524
|
+
|
|
525
|
+
if (this.childNodes.length === 0) {
|
|
526
|
+
return {
|
|
527
|
+
left: myX,
|
|
528
|
+
right: myX + this.nodeWidth,
|
|
529
|
+
top: myY,
|
|
530
|
+
bottom: myY + this.nodeHeight
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
let left = myX;
|
|
535
|
+
let right = myX + this.nodeWidth;
|
|
536
|
+
let top = myY;
|
|
537
|
+
let bottom = myY + this.nodeHeight;
|
|
538
|
+
|
|
539
|
+
this.childNodes.forEach(child => {
|
|
540
|
+
const childBounds = child.getTreeBounds();
|
|
541
|
+
left = Math.min(left, childBounds.left);
|
|
542
|
+
right = Math.max(right, childBounds.right);
|
|
543
|
+
top = Math.min(top, childBounds.top);
|
|
544
|
+
bottom = Math.max(bottom, childBounds.bottom);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
return { left, right, top, bottom };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Gets the total dimensions of this tree for layout purposes
|
|
552
|
+
* @returns {Object} Object with width, height, and bounds
|
|
553
|
+
*/
|
|
554
|
+
/* Gerard - Unnecessary */
|
|
555
|
+
/* Gerard: Question - Unused? */
|
|
556
|
+
getTreeDimensions() {
|
|
557
|
+
const bounds = this.getTreeBounds();
|
|
558
|
+
return {
|
|
559
|
+
width: bounds.right - bounds.left,
|
|
560
|
+
height: bounds.bottom - bounds.top,
|
|
561
|
+
bounds: bounds
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
564
|
}
|