@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
package/src/omdNode.js
CHANGED
|
@@ -1,384 +1,384 @@
|
|
|
1
|
-
import { omdMetaExpression } from "../src/omdMetaExpression.js";
|
|
2
|
-
import { jsvgTextLine, jsvgLine } from "@teachinglab/jsvg";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* omdNode - A minimal class representing a node in the expression tree
|
|
6
|
-
* Focuses purely on tree structure and layout, delegates visuals to superclasses
|
|
7
|
-
* Uses math.js AST format
|
|
8
|
-
*/
|
|
9
|
-
export class omdNode extends omdMetaExpression {
|
|
10
|
-
/**
|
|
11
|
-
* Constructor - Creates a tree node from math.js AST data
|
|
12
|
-
* @param {Object} nodeData - The AST node from math.js parser
|
|
13
|
-
*/
|
|
14
|
-
constructor(nodeData) {
|
|
15
|
-
super();
|
|
16
|
-
|
|
17
|
-
this.type = "omdNode";
|
|
18
|
-
this.nodeData = nodeData; // The AST node from math.js
|
|
19
|
-
this.childNodes = []; // Array of child omdNodes
|
|
20
|
-
this.edgeLines = []; // Array of edge lines
|
|
21
|
-
this.zoomLevel = 1.0; // Current zoom level
|
|
22
|
-
|
|
23
|
-
// Initialize the tree structure
|
|
24
|
-
this.initializeNode();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Sets zoom level for this node and all children recursively
|
|
29
|
-
* @param {number} zoom - The zoom level (1.0 = 100%, 2.0 = 200%, etc.)
|
|
30
|
-
*/
|
|
31
|
-
setZoomLevel(zoom) {
|
|
32
|
-
this.zoomLevel = zoom;
|
|
33
|
-
// Apply to all child nodes
|
|
34
|
-
this.childNodes.forEach(child => child.setZoomLevel(zoom));
|
|
35
|
-
// Invalidate cached dimensions
|
|
36
|
-
this.invalidateCache();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Gets the width of this node, scaled by zoom level
|
|
41
|
-
* @returns {number} The node width in pixels
|
|
42
|
-
*/
|
|
43
|
-
get nodeWidth() {
|
|
44
|
-
if (!this._cachedWidth) {
|
|
45
|
-
// Use superclass text measurement if available, otherwise estimate
|
|
46
|
-
const label = this.getNodeLabel();
|
|
47
|
-
const baseWidth = Math.max(label.length * 12 + 20, 60);
|
|
48
|
-
this._cachedWidth = baseWidth * this.zoomLevel;
|
|
49
|
-
}
|
|
50
|
-
return this._cachedWidth;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Gets the height of this node, scaled by zoom level
|
|
55
|
-
* @returns {number} The node height in pixels
|
|
56
|
-
*/
|
|
57
|
-
get nodeHeight() {
|
|
58
|
-
return 50 * this.zoomLevel;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Gets the total width needed for this entire subtree
|
|
63
|
-
* @returns {number} The subtree width in pixels
|
|
64
|
-
*/
|
|
65
|
-
get subtreeWidth() {
|
|
66
|
-
if (this.childNodes.length === 0) {
|
|
67
|
-
return this.nodeWidth;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const childSubtreeWidths = this.childNodes.map(child => child.subtreeWidth);
|
|
71
|
-
const totalChildWidth = childSubtreeWidths.reduce((sum, width) => sum + width, 0);
|
|
72
|
-
const spacing = (this.childNodes.length - 1) * this.nodeSpacing;
|
|
73
|
-
|
|
74
|
-
return Math.max(this.nodeWidth, totalChildWidth + spacing);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Gets the horizontal spacing between child nodes, scaled by zoom level
|
|
79
|
-
* @returns {number} The spacing in pixels
|
|
80
|
-
*/
|
|
81
|
-
get nodeSpacing() {
|
|
82
|
-
return 100 * this.zoomLevel;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Gets the vertical spacing between tree levels, scaled by zoom level
|
|
87
|
-
* @returns {number} The level height in pixels
|
|
88
|
-
*/
|
|
89
|
-
get levelHeight() {
|
|
90
|
-
return 120 * this.zoomLevel;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Invalidates cached dimension calculations when content changes
|
|
95
|
-
*/
|
|
96
|
-
invalidateCache() {
|
|
97
|
-
this._cachedWidth = null;
|
|
98
|
-
if (this.parent && this.parent.invalidateCache) {
|
|
99
|
-
this.parent.invalidateCache();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Main initialization method - sets up visuals, creates children, and layouts tree
|
|
105
|
-
*/
|
|
106
|
-
initializeNode() {
|
|
107
|
-
this.setupVisuals();
|
|
108
|
-
this.createChildNodes();
|
|
109
|
-
this.layoutTree();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Creates the visual elements for this node (text label and background)
|
|
114
|
-
*/
|
|
115
|
-
setupVisuals() {
|
|
116
|
-
// Create text label using jsvg
|
|
117
|
-
this.nodeLabel = new jsvgTextLine();
|
|
118
|
-
this.nodeLabel.setText(this.getNodeLabel());
|
|
119
|
-
this.nodeLabel.setFontSize(24 * this.zoomLevel);
|
|
120
|
-
this.nodeLabel.setAlignment('center');
|
|
121
|
-
|
|
122
|
-
// Update superclass background size
|
|
123
|
-
this.updateSize();
|
|
124
|
-
|
|
125
|
-
// Position text at center
|
|
126
|
-
this.nodeLabel.setPosition(this.nodeWidth/2, this.nodeHeight/2);
|
|
127
|
-
this.addChild(this.nodeLabel);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Updates the size of visual elements to match calculated dimensions
|
|
132
|
-
*/
|
|
133
|
-
updateSize() {
|
|
134
|
-
// Update the superclass background rectangle
|
|
135
|
-
this.backRect.setWidthAndHeight(this.nodeWidth, this.nodeHeight);
|
|
136
|
-
this.setWidthAndHeight(this.nodeWidth, this.nodeHeight);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Creates child nodes from AST data and connecting edge lines
|
|
141
|
-
*/
|
|
142
|
-
createChildNodes() {
|
|
143
|
-
const childrenData = this.getNodeChildren();
|
|
144
|
-
|
|
145
|
-
childrenData.forEach((childData, index) => {
|
|
146
|
-
// Create child node
|
|
147
|
-
const childNode = new omdNode(childData);
|
|
148
|
-
childNode.parent = this;
|
|
149
|
-
this.childNodes.push(childNode);
|
|
150
|
-
this.addChild(childNode);
|
|
151
|
-
|
|
152
|
-
// Create edge line
|
|
153
|
-
const edge = new jsvgLine();
|
|
154
|
-
edge.setStrokeWidth(3);
|
|
155
|
-
this.edgeLines.push(edge);
|
|
156
|
-
this.addChild(edge);
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Recursively layouts the entire tree structure
|
|
162
|
-
*/
|
|
163
|
-
layoutTree() {
|
|
164
|
-
if (this.childNodes.length === 0) return;
|
|
165
|
-
|
|
166
|
-
// Layout children first to get their final sizes
|
|
167
|
-
this.childNodes.forEach(child => child.layoutTree());
|
|
168
|
-
|
|
169
|
-
// Calculate positions
|
|
170
|
-
this.positionChildren();
|
|
171
|
-
this.updateEdges();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Positions child nodes horizontally to center them under this node
|
|
176
|
-
*/
|
|
177
|
-
positionChildren() {
|
|
178
|
-
if (this.childNodes.length === 0) return;
|
|
179
|
-
|
|
180
|
-
// Calculate starting position to center children
|
|
181
|
-
const totalWidth = this.childNodes.reduce((sum, child) => sum + child.subtreeWidth, 0);
|
|
182
|
-
const totalSpacing = (this.childNodes.length - 1) * this.nodeSpacing;
|
|
183
|
-
const totalRequiredWidth = totalWidth + totalSpacing;
|
|
184
|
-
|
|
185
|
-
const startX = -totalRequiredWidth / 2 + this.nodeWidth / 2;
|
|
186
|
-
const childY = this.nodeHeight + this.levelHeight;
|
|
187
|
-
|
|
188
|
-
let currentX = startX;
|
|
189
|
-
|
|
190
|
-
this.childNodes.forEach((child, index) => {
|
|
191
|
-
// Center child in its allocated subtree space
|
|
192
|
-
const childX = currentX + child.subtreeWidth / 2 - child.nodeWidth / 2;
|
|
193
|
-
|
|
194
|
-
// Position using jsvg method
|
|
195
|
-
child.setPosition(childX, childY);
|
|
196
|
-
|
|
197
|
-
// Move to next position
|
|
198
|
-
currentX += child.subtreeWidth + this.nodeSpacing;
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Updates the connecting edge lines between this node and its children
|
|
204
|
-
*/
|
|
205
|
-
updateEdges() {
|
|
206
|
-
this.childNodes.forEach((child, index) => {
|
|
207
|
-
if (this.edgeLines[index]) {
|
|
208
|
-
// Connect center-bottom of parent to center-top of child
|
|
209
|
-
const parentCenterX = this.nodeWidth / 2;
|
|
210
|
-
const parentBottomY = this.nodeHeight;
|
|
211
|
-
|
|
212
|
-
const childCenterX = child.xpos + child.nodeWidth / 2;
|
|
213
|
-
const childTopY = child.ypos;
|
|
214
|
-
|
|
215
|
-
this.edgeLines[index].setEndpointA(parentCenterX, parentBottomY);
|
|
216
|
-
this.edgeLines[index].setEndpointB(childCenterX, childTopY);
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Extracts child node data from the math.js AST based on node type
|
|
223
|
-
* @returns {Array} Array of child AST nodes
|
|
224
|
-
*/
|
|
225
|
-
getNodeChildren() {
|
|
226
|
-
if (!this.nodeData) return [];
|
|
227
|
-
|
|
228
|
-
const nodeType = this.detectNodeType();
|
|
229
|
-
|
|
230
|
-
// Handle math.js node types
|
|
231
|
-
switch (nodeType) {
|
|
232
|
-
case 'FunctionNode':
|
|
233
|
-
return this.nodeData.args || [];
|
|
234
|
-
case 'MatrixNode':
|
|
235
|
-
return this.nodeData.args || [];
|
|
236
|
-
case 'OperatorNode':
|
|
237
|
-
return this.nodeData.args || [];
|
|
238
|
-
case 'ParenthesisNode':
|
|
239
|
-
return [this.nodeData.content];
|
|
240
|
-
case 'ArrayNode':
|
|
241
|
-
return this.nodeData.items || [];
|
|
242
|
-
case 'IndexNode':
|
|
243
|
-
return [this.nodeData.object, this.nodeData.index];
|
|
244
|
-
case 'AccessorNode':
|
|
245
|
-
return [this.nodeData.object, this.nodeData.index];
|
|
246
|
-
case 'AssignmentNode':
|
|
247
|
-
return [this.nodeData.object, this.nodeData.value];
|
|
248
|
-
case 'ConditionalNode':
|
|
249
|
-
return [this.nodeData.condition, this.nodeData.trueExpr, this.nodeData.falseExpr];
|
|
250
|
-
default:
|
|
251
|
-
return [];
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Determines the type of math.js AST node by examining its properties
|
|
257
|
-
* @returns {string} The node type (e.g., 'OperatorNode', 'ConstantNode', etc.)
|
|
258
|
-
*/
|
|
259
|
-
detectNodeType() {
|
|
260
|
-
if (!this.nodeData) return 'Unknown';
|
|
261
|
-
|
|
262
|
-
// Check for properties that uniquely identify each node type
|
|
263
|
-
if (this.nodeData.hasOwnProperty('op') && this.nodeData.hasOwnProperty('args')) {
|
|
264
|
-
return 'OperatorNode';
|
|
265
|
-
} else if (this.nodeData.hasOwnProperty('fn') && this.nodeData.hasOwnProperty('args')) {
|
|
266
|
-
if (this.nodeData.fn === 'matrix') {
|
|
267
|
-
return 'MatrixNode';
|
|
268
|
-
}
|
|
269
|
-
return 'FunctionNode';
|
|
270
|
-
} else if (this.nodeData.hasOwnProperty('value')) {
|
|
271
|
-
return 'ConstantNode';
|
|
272
|
-
} else if (this.nodeData.hasOwnProperty('name') && !this.nodeData.hasOwnProperty('args')) {
|
|
273
|
-
return 'SymbolNode';
|
|
274
|
-
} else if (this.nodeData.hasOwnProperty('content')) {
|
|
275
|
-
return 'ParenthesisNode';
|
|
276
|
-
} else if (this.nodeData.hasOwnProperty('items')) {
|
|
277
|
-
return 'ArrayNode';
|
|
278
|
-
} else if (this.nodeData.hasOwnProperty('object') && this.nodeData.hasOwnProperty('index')) {
|
|
279
|
-
return 'IndexNode';
|
|
280
|
-
} else if (this.nodeData.hasOwnProperty('condition')) {
|
|
281
|
-
return 'ConditionalNode';
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return 'Unknown';
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Generates the display label for this node based on its AST data
|
|
289
|
-
* @returns {string} The text label to display in the node
|
|
290
|
-
*/
|
|
291
|
-
getNodeLabel() {
|
|
292
|
-
const nodeType = this.detectNodeType();
|
|
293
|
-
|
|
294
|
-
switch (nodeType) {
|
|
295
|
-
case 'ConstantNode':
|
|
296
|
-
return this.nodeData.value.toString();
|
|
297
|
-
case 'SymbolNode':
|
|
298
|
-
return this.nodeData.name;
|
|
299
|
-
case 'FunctionNode':
|
|
300
|
-
return this.nodeData.fn.name || this.nodeData.fn;
|
|
301
|
-
case 'MatrixNode':
|
|
302
|
-
if (this.nodeData.args && this.nodeData.args.length > 0) {
|
|
303
|
-
const firstArg = this.nodeData.args[0];
|
|
304
|
-
if (firstArg && firstArg.items) {
|
|
305
|
-
const rows = firstArg.items.length;
|
|
306
|
-
const cols = firstArg.items[0] && firstArg.items[0].items ? firstArg.items[0].items.length : 1;
|
|
307
|
-
return `Matrix ${rows}×${cols}`;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return 'Matrix';
|
|
311
|
-
case 'OperatorNode':
|
|
312
|
-
const operatorMap = {
|
|
313
|
-
'add': '+',
|
|
314
|
-
'subtract': '-',
|
|
315
|
-
'multiply': '*',
|
|
316
|
-
'divide': '/',
|
|
317
|
-
'pow': '^',
|
|
318
|
-
'unaryMinus': '-',
|
|
319
|
-
'unaryPlus': '+'
|
|
320
|
-
};
|
|
321
|
-
return operatorMap[this.nodeData.fn] || this.nodeData.fn;
|
|
322
|
-
case 'ParenthesisNode':
|
|
323
|
-
return '( )';
|
|
324
|
-
case 'ArrayNode':
|
|
325
|
-
return '[ ]';
|
|
326
|
-
case 'IndexNode':
|
|
327
|
-
return '[]';
|
|
328
|
-
case 'AccessorNode':
|
|
329
|
-
return '.';
|
|
330
|
-
case 'AssignmentNode':
|
|
331
|
-
return '=';
|
|
332
|
-
case 'ConditionalNode':
|
|
333
|
-
return '?:';
|
|
334
|
-
default:
|
|
335
|
-
return nodeType || '?';
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Calculates the bounding box of this entire subtree
|
|
341
|
-
* @returns {Object} Object with left, right, top, bottom coordinates
|
|
342
|
-
*/
|
|
343
|
-
getTreeBounds() {
|
|
344
|
-
const myX = this.xpos || 0;
|
|
345
|
-
const myY = this.ypos || 0;
|
|
346
|
-
|
|
347
|
-
if (this.childNodes.length === 0) {
|
|
348
|
-
return {
|
|
349
|
-
left: myX,
|
|
350
|
-
right: myX + this.nodeWidth,
|
|
351
|
-
top: myY,
|
|
352
|
-
bottom: myY + this.nodeHeight
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
let left = myX;
|
|
357
|
-
let right = myX + this.nodeWidth;
|
|
358
|
-
let top = myY;
|
|
359
|
-
let bottom = myY + this.nodeHeight;
|
|
360
|
-
|
|
361
|
-
this.childNodes.forEach(child => {
|
|
362
|
-
const childBounds = child.getTreeBounds();
|
|
363
|
-
left = Math.min(left, childBounds.left);
|
|
364
|
-
right = Math.max(right, childBounds.right);
|
|
365
|
-
top = Math.min(top, childBounds.top);
|
|
366
|
-
bottom = Math.max(bottom, childBounds.bottom);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
return { left, right, top, bottom };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Gets the total dimensions of this tree for layout purposes
|
|
374
|
-
* @returns {Object} Object with width, height, and bounds
|
|
375
|
-
*/
|
|
376
|
-
getTreeDimensions() {
|
|
377
|
-
const bounds = this.getTreeBounds();
|
|
378
|
-
return {
|
|
379
|
-
width: bounds.right - bounds.left,
|
|
380
|
-
height: bounds.bottom - bounds.top,
|
|
381
|
-
bounds: bounds
|
|
382
|
-
};
|
|
383
|
-
}
|
|
1
|
+
import { omdMetaExpression } from "../src/omdMetaExpression.js";
|
|
2
|
+
import { jsvgTextLine, jsvgLine } from "@teachinglab/jsvg";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* omdNode - A minimal class representing a node in the expression tree
|
|
6
|
+
* Focuses purely on tree structure and layout, delegates visuals to superclasses
|
|
7
|
+
* Uses math.js AST format
|
|
8
|
+
*/
|
|
9
|
+
export class omdNode extends omdMetaExpression {
|
|
10
|
+
/**
|
|
11
|
+
* Constructor - Creates a tree node from math.js AST data
|
|
12
|
+
* @param {Object} nodeData - The AST node from math.js parser
|
|
13
|
+
*/
|
|
14
|
+
constructor(nodeData) {
|
|
15
|
+
super();
|
|
16
|
+
|
|
17
|
+
this.type = "omdNode";
|
|
18
|
+
this.nodeData = nodeData; // The AST node from math.js
|
|
19
|
+
this.childNodes = []; // Array of child omdNodes
|
|
20
|
+
this.edgeLines = []; // Array of edge lines
|
|
21
|
+
this.zoomLevel = 1.0; // Current zoom level
|
|
22
|
+
|
|
23
|
+
// Initialize the tree structure
|
|
24
|
+
this.initializeNode();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sets zoom level for this node and all children recursively
|
|
29
|
+
* @param {number} zoom - The zoom level (1.0 = 100%, 2.0 = 200%, etc.)
|
|
30
|
+
*/
|
|
31
|
+
setZoomLevel(zoom) {
|
|
32
|
+
this.zoomLevel = zoom;
|
|
33
|
+
// Apply to all child nodes
|
|
34
|
+
this.childNodes.forEach(child => child.setZoomLevel(zoom));
|
|
35
|
+
// Invalidate cached dimensions
|
|
36
|
+
this.invalidateCache();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gets the width of this node, scaled by zoom level
|
|
41
|
+
* @returns {number} The node width in pixels
|
|
42
|
+
*/
|
|
43
|
+
get nodeWidth() {
|
|
44
|
+
if (!this._cachedWidth) {
|
|
45
|
+
// Use superclass text measurement if available, otherwise estimate
|
|
46
|
+
const label = this.getNodeLabel();
|
|
47
|
+
const baseWidth = Math.max(label.length * 12 + 20, 60);
|
|
48
|
+
this._cachedWidth = baseWidth * this.zoomLevel;
|
|
49
|
+
}
|
|
50
|
+
return this._cachedWidth;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Gets the height of this node, scaled by zoom level
|
|
55
|
+
* @returns {number} The node height in pixels
|
|
56
|
+
*/
|
|
57
|
+
get nodeHeight() {
|
|
58
|
+
return 50 * this.zoomLevel;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gets the total width needed for this entire subtree
|
|
63
|
+
* @returns {number} The subtree width in pixels
|
|
64
|
+
*/
|
|
65
|
+
get subtreeWidth() {
|
|
66
|
+
if (this.childNodes.length === 0) {
|
|
67
|
+
return this.nodeWidth;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const childSubtreeWidths = this.childNodes.map(child => child.subtreeWidth);
|
|
71
|
+
const totalChildWidth = childSubtreeWidths.reduce((sum, width) => sum + width, 0);
|
|
72
|
+
const spacing = (this.childNodes.length - 1) * this.nodeSpacing;
|
|
73
|
+
|
|
74
|
+
return Math.max(this.nodeWidth, totalChildWidth + spacing);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Gets the horizontal spacing between child nodes, scaled by zoom level
|
|
79
|
+
* @returns {number} The spacing in pixels
|
|
80
|
+
*/
|
|
81
|
+
get nodeSpacing() {
|
|
82
|
+
return 100 * this.zoomLevel;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Gets the vertical spacing between tree levels, scaled by zoom level
|
|
87
|
+
* @returns {number} The level height in pixels
|
|
88
|
+
*/
|
|
89
|
+
get levelHeight() {
|
|
90
|
+
return 120 * this.zoomLevel;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Invalidates cached dimension calculations when content changes
|
|
95
|
+
*/
|
|
96
|
+
invalidateCache() {
|
|
97
|
+
this._cachedWidth = null;
|
|
98
|
+
if (this.parent && this.parent.invalidateCache) {
|
|
99
|
+
this.parent.invalidateCache();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Main initialization method - sets up visuals, creates children, and layouts tree
|
|
105
|
+
*/
|
|
106
|
+
initializeNode() {
|
|
107
|
+
this.setupVisuals();
|
|
108
|
+
this.createChildNodes();
|
|
109
|
+
this.layoutTree();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates the visual elements for this node (text label and background)
|
|
114
|
+
*/
|
|
115
|
+
setupVisuals() {
|
|
116
|
+
// Create text label using jsvg
|
|
117
|
+
this.nodeLabel = new jsvgTextLine();
|
|
118
|
+
this.nodeLabel.setText(this.getNodeLabel());
|
|
119
|
+
this.nodeLabel.setFontSize(24 * this.zoomLevel);
|
|
120
|
+
this.nodeLabel.setAlignment('center');
|
|
121
|
+
|
|
122
|
+
// Update superclass background size
|
|
123
|
+
this.updateSize();
|
|
124
|
+
|
|
125
|
+
// Position text at center
|
|
126
|
+
this.nodeLabel.setPosition(this.nodeWidth/2, this.nodeHeight/2);
|
|
127
|
+
this.addChild(this.nodeLabel);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Updates the size of visual elements to match calculated dimensions
|
|
132
|
+
*/
|
|
133
|
+
updateSize() {
|
|
134
|
+
// Update the superclass background rectangle
|
|
135
|
+
this.backRect.setWidthAndHeight(this.nodeWidth, this.nodeHeight);
|
|
136
|
+
this.setWidthAndHeight(this.nodeWidth, this.nodeHeight);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Creates child nodes from AST data and connecting edge lines
|
|
141
|
+
*/
|
|
142
|
+
createChildNodes() {
|
|
143
|
+
const childrenData = this.getNodeChildren();
|
|
144
|
+
|
|
145
|
+
childrenData.forEach((childData, index) => {
|
|
146
|
+
// Create child node
|
|
147
|
+
const childNode = new omdNode(childData);
|
|
148
|
+
childNode.parent = this;
|
|
149
|
+
this.childNodes.push(childNode);
|
|
150
|
+
this.addChild(childNode);
|
|
151
|
+
|
|
152
|
+
// Create edge line
|
|
153
|
+
const edge = new jsvgLine();
|
|
154
|
+
edge.setStrokeWidth(3);
|
|
155
|
+
this.edgeLines.push(edge);
|
|
156
|
+
this.addChild(edge);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Recursively layouts the entire tree structure
|
|
162
|
+
*/
|
|
163
|
+
layoutTree() {
|
|
164
|
+
if (this.childNodes.length === 0) return;
|
|
165
|
+
|
|
166
|
+
// Layout children first to get their final sizes
|
|
167
|
+
this.childNodes.forEach(child => child.layoutTree());
|
|
168
|
+
|
|
169
|
+
// Calculate positions
|
|
170
|
+
this.positionChildren();
|
|
171
|
+
this.updateEdges();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Positions child nodes horizontally to center them under this node
|
|
176
|
+
*/
|
|
177
|
+
positionChildren() {
|
|
178
|
+
if (this.childNodes.length === 0) return;
|
|
179
|
+
|
|
180
|
+
// Calculate starting position to center children
|
|
181
|
+
const totalWidth = this.childNodes.reduce((sum, child) => sum + child.subtreeWidth, 0);
|
|
182
|
+
const totalSpacing = (this.childNodes.length - 1) * this.nodeSpacing;
|
|
183
|
+
const totalRequiredWidth = totalWidth + totalSpacing;
|
|
184
|
+
|
|
185
|
+
const startX = -totalRequiredWidth / 2 + this.nodeWidth / 2;
|
|
186
|
+
const childY = this.nodeHeight + this.levelHeight;
|
|
187
|
+
|
|
188
|
+
let currentX = startX;
|
|
189
|
+
|
|
190
|
+
this.childNodes.forEach((child, index) => {
|
|
191
|
+
// Center child in its allocated subtree space
|
|
192
|
+
const childX = currentX + child.subtreeWidth / 2 - child.nodeWidth / 2;
|
|
193
|
+
|
|
194
|
+
// Position using jsvg method
|
|
195
|
+
child.setPosition(childX, childY);
|
|
196
|
+
|
|
197
|
+
// Move to next position
|
|
198
|
+
currentX += child.subtreeWidth + this.nodeSpacing;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Updates the connecting edge lines between this node and its children
|
|
204
|
+
*/
|
|
205
|
+
updateEdges() {
|
|
206
|
+
this.childNodes.forEach((child, index) => {
|
|
207
|
+
if (this.edgeLines[index]) {
|
|
208
|
+
// Connect center-bottom of parent to center-top of child
|
|
209
|
+
const parentCenterX = this.nodeWidth / 2;
|
|
210
|
+
const parentBottomY = this.nodeHeight;
|
|
211
|
+
|
|
212
|
+
const childCenterX = child.xpos + child.nodeWidth / 2;
|
|
213
|
+
const childTopY = child.ypos;
|
|
214
|
+
|
|
215
|
+
this.edgeLines[index].setEndpointA(parentCenterX, parentBottomY);
|
|
216
|
+
this.edgeLines[index].setEndpointB(childCenterX, childTopY);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Extracts child node data from the math.js AST based on node type
|
|
223
|
+
* @returns {Array} Array of child AST nodes
|
|
224
|
+
*/
|
|
225
|
+
getNodeChildren() {
|
|
226
|
+
if (!this.nodeData) return [];
|
|
227
|
+
|
|
228
|
+
const nodeType = this.detectNodeType();
|
|
229
|
+
|
|
230
|
+
// Handle math.js node types
|
|
231
|
+
switch (nodeType) {
|
|
232
|
+
case 'FunctionNode':
|
|
233
|
+
return this.nodeData.args || [];
|
|
234
|
+
case 'MatrixNode':
|
|
235
|
+
return this.nodeData.args || [];
|
|
236
|
+
case 'OperatorNode':
|
|
237
|
+
return this.nodeData.args || [];
|
|
238
|
+
case 'ParenthesisNode':
|
|
239
|
+
return [this.nodeData.content];
|
|
240
|
+
case 'ArrayNode':
|
|
241
|
+
return this.nodeData.items || [];
|
|
242
|
+
case 'IndexNode':
|
|
243
|
+
return [this.nodeData.object, this.nodeData.index];
|
|
244
|
+
case 'AccessorNode':
|
|
245
|
+
return [this.nodeData.object, this.nodeData.index];
|
|
246
|
+
case 'AssignmentNode':
|
|
247
|
+
return [this.nodeData.object, this.nodeData.value];
|
|
248
|
+
case 'ConditionalNode':
|
|
249
|
+
return [this.nodeData.condition, this.nodeData.trueExpr, this.nodeData.falseExpr];
|
|
250
|
+
default:
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Determines the type of math.js AST node by examining its properties
|
|
257
|
+
* @returns {string} The node type (e.g., 'OperatorNode', 'ConstantNode', etc.)
|
|
258
|
+
*/
|
|
259
|
+
detectNodeType() {
|
|
260
|
+
if (!this.nodeData) return 'Unknown';
|
|
261
|
+
|
|
262
|
+
// Check for properties that uniquely identify each node type
|
|
263
|
+
if (this.nodeData.hasOwnProperty('op') && this.nodeData.hasOwnProperty('args')) {
|
|
264
|
+
return 'OperatorNode';
|
|
265
|
+
} else if (this.nodeData.hasOwnProperty('fn') && this.nodeData.hasOwnProperty('args')) {
|
|
266
|
+
if (this.nodeData.fn === 'matrix') {
|
|
267
|
+
return 'MatrixNode';
|
|
268
|
+
}
|
|
269
|
+
return 'FunctionNode';
|
|
270
|
+
} else if (this.nodeData.hasOwnProperty('value')) {
|
|
271
|
+
return 'ConstantNode';
|
|
272
|
+
} else if (this.nodeData.hasOwnProperty('name') && !this.nodeData.hasOwnProperty('args')) {
|
|
273
|
+
return 'SymbolNode';
|
|
274
|
+
} else if (this.nodeData.hasOwnProperty('content')) {
|
|
275
|
+
return 'ParenthesisNode';
|
|
276
|
+
} else if (this.nodeData.hasOwnProperty('items')) {
|
|
277
|
+
return 'ArrayNode';
|
|
278
|
+
} else if (this.nodeData.hasOwnProperty('object') && this.nodeData.hasOwnProperty('index')) {
|
|
279
|
+
return 'IndexNode';
|
|
280
|
+
} else if (this.nodeData.hasOwnProperty('condition')) {
|
|
281
|
+
return 'ConditionalNode';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return 'Unknown';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Generates the display label for this node based on its AST data
|
|
289
|
+
* @returns {string} The text label to display in the node
|
|
290
|
+
*/
|
|
291
|
+
getNodeLabel() {
|
|
292
|
+
const nodeType = this.detectNodeType();
|
|
293
|
+
|
|
294
|
+
switch (nodeType) {
|
|
295
|
+
case 'ConstantNode':
|
|
296
|
+
return this.nodeData.value.toString();
|
|
297
|
+
case 'SymbolNode':
|
|
298
|
+
return this.nodeData.name;
|
|
299
|
+
case 'FunctionNode':
|
|
300
|
+
return this.nodeData.fn.name || this.nodeData.fn;
|
|
301
|
+
case 'MatrixNode':
|
|
302
|
+
if (this.nodeData.args && this.nodeData.args.length > 0) {
|
|
303
|
+
const firstArg = this.nodeData.args[0];
|
|
304
|
+
if (firstArg && firstArg.items) {
|
|
305
|
+
const rows = firstArg.items.length;
|
|
306
|
+
const cols = firstArg.items[0] && firstArg.items[0].items ? firstArg.items[0].items.length : 1;
|
|
307
|
+
return `Matrix ${rows}×${cols}`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return 'Matrix';
|
|
311
|
+
case 'OperatorNode':
|
|
312
|
+
const operatorMap = {
|
|
313
|
+
'add': '+',
|
|
314
|
+
'subtract': '-',
|
|
315
|
+
'multiply': '*',
|
|
316
|
+
'divide': '/',
|
|
317
|
+
'pow': '^',
|
|
318
|
+
'unaryMinus': '-',
|
|
319
|
+
'unaryPlus': '+'
|
|
320
|
+
};
|
|
321
|
+
return operatorMap[this.nodeData.fn] || this.nodeData.fn;
|
|
322
|
+
case 'ParenthesisNode':
|
|
323
|
+
return '( )';
|
|
324
|
+
case 'ArrayNode':
|
|
325
|
+
return '[ ]';
|
|
326
|
+
case 'IndexNode':
|
|
327
|
+
return '[]';
|
|
328
|
+
case 'AccessorNode':
|
|
329
|
+
return '.';
|
|
330
|
+
case 'AssignmentNode':
|
|
331
|
+
return '=';
|
|
332
|
+
case 'ConditionalNode':
|
|
333
|
+
return '?:';
|
|
334
|
+
default:
|
|
335
|
+
return nodeType || '?';
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Calculates the bounding box of this entire subtree
|
|
341
|
+
* @returns {Object} Object with left, right, top, bottom coordinates
|
|
342
|
+
*/
|
|
343
|
+
getTreeBounds() {
|
|
344
|
+
const myX = this.xpos || 0;
|
|
345
|
+
const myY = this.ypos || 0;
|
|
346
|
+
|
|
347
|
+
if (this.childNodes.length === 0) {
|
|
348
|
+
return {
|
|
349
|
+
left: myX,
|
|
350
|
+
right: myX + this.nodeWidth,
|
|
351
|
+
top: myY,
|
|
352
|
+
bottom: myY + this.nodeHeight
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let left = myX;
|
|
357
|
+
let right = myX + this.nodeWidth;
|
|
358
|
+
let top = myY;
|
|
359
|
+
let bottom = myY + this.nodeHeight;
|
|
360
|
+
|
|
361
|
+
this.childNodes.forEach(child => {
|
|
362
|
+
const childBounds = child.getTreeBounds();
|
|
363
|
+
left = Math.min(left, childBounds.left);
|
|
364
|
+
right = Math.max(right, childBounds.right);
|
|
365
|
+
top = Math.min(top, childBounds.top);
|
|
366
|
+
bottom = Math.max(bottom, childBounds.bottom);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return { left, right, top, bottom };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Gets the total dimensions of this tree for layout purposes
|
|
374
|
+
* @returns {Object} Object with width, height, and bounds
|
|
375
|
+
*/
|
|
376
|
+
getTreeDimensions() {
|
|
377
|
+
const bounds = this.getTreeBounds();
|
|
378
|
+
return {
|
|
379
|
+
width: bounds.right - bounds.left,
|
|
380
|
+
height: bounds.bottom - bounds.top,
|
|
381
|
+
bounds: bounds
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
384
|
}
|