@teachinglab/omd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -0
- package/canvas/core/canvasConfig.js +203 -0
- package/canvas/core/omdCanvas.js +475 -0
- package/canvas/drawing/segment.js +168 -0
- package/canvas/drawing/stroke.js +386 -0
- package/canvas/events/eventManager.js +435 -0
- package/canvas/events/pointerEventHandler.js +263 -0
- package/canvas/features/focusFrameManager.js +287 -0
- package/canvas/index.js +49 -0
- package/canvas/tools/eraserTool.js +322 -0
- package/canvas/tools/pencilTool.js +319 -0
- package/canvas/tools/selectTool.js +457 -0
- package/canvas/tools/tool.js +223 -0
- package/canvas/tools/toolManager.js +394 -0
- package/canvas/ui/cursor.js +438 -0
- package/canvas/ui/toolbar.js +304 -0
- package/canvas/utils/boundingBox.js +378 -0
- package/canvas/utils/mathUtils.js +259 -0
- package/docs/api/configuration-options.md +104 -0
- package/docs/api/eventManager.md +68 -0
- package/docs/api/focusFrameManager.md +150 -0
- package/docs/api/index.md +91 -0
- package/docs/api/main.md +58 -0
- package/docs/api/omdBinaryExpressionNode.md +227 -0
- package/docs/api/omdCanvas.md +142 -0
- package/docs/api/omdConfigManager.md +192 -0
- package/docs/api/omdConstantNode.md +117 -0
- package/docs/api/omdDisplay.md +121 -0
- package/docs/api/omdEquationNode.md +161 -0
- package/docs/api/omdEquationSequenceNode.md +301 -0
- package/docs/api/omdEquationStack.md +139 -0
- package/docs/api/omdFunctionNode.md +141 -0
- package/docs/api/omdGroupNode.md +182 -0
- package/docs/api/omdHelpers.md +96 -0
- package/docs/api/omdLeafNode.md +163 -0
- package/docs/api/omdNode.md +101 -0
- package/docs/api/omdOperationDisplayNode.md +139 -0
- package/docs/api/omdOperatorNode.md +127 -0
- package/docs/api/omdParenthesisNode.md +122 -0
- package/docs/api/omdPopup.md +117 -0
- package/docs/api/omdPowerNode.md +127 -0
- package/docs/api/omdRationalNode.md +128 -0
- package/docs/api/omdSequenceNode.md +128 -0
- package/docs/api/omdSimplification.md +110 -0
- package/docs/api/omdSqrtNode.md +79 -0
- package/docs/api/omdStepVisualizer.md +115 -0
- package/docs/api/omdStepVisualizerHighlighting.md +61 -0
- package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
- package/docs/api/omdStepVisualizerLayout.md +60 -0
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
- package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
- package/docs/api/omdToolbar.md +102 -0
- package/docs/api/omdTranscriptionService.md +76 -0
- package/docs/api/omdTreeDiff.md +134 -0
- package/docs/api/omdUnaryExpressionNode.md +174 -0
- package/docs/api/omdUtilities.md +70 -0
- package/docs/api/omdVariableNode.md +148 -0
- package/docs/api/selectTool.md +74 -0
- package/docs/api/simplificationEngine.md +98 -0
- package/docs/api/simplificationRules.md +77 -0
- package/docs/api/simplificationUtils.md +64 -0
- package/docs/api/transcribe.md +43 -0
- package/docs/api-reference.md +85 -0
- package/docs/index.html +454 -0
- package/docs/user-guide.md +9 -0
- package/index.js +67 -0
- package/omd/config/omdConfigManager.js +267 -0
- package/omd/core/index.js +150 -0
- package/omd/core/omdEquationStack.js +347 -0
- package/omd/core/omdUtilities.js +115 -0
- package/omd/display/omdDisplay.js +443 -0
- package/omd/display/omdToolbar.js +502 -0
- package/omd/nodes/omdBinaryExpressionNode.js +460 -0
- package/omd/nodes/omdConstantNode.js +142 -0
- package/omd/nodes/omdEquationNode.js +1223 -0
- package/omd/nodes/omdEquationSequenceNode.js +1273 -0
- package/omd/nodes/omdFunctionNode.js +352 -0
- package/omd/nodes/omdGroupNode.js +68 -0
- package/omd/nodes/omdLeafNode.js +77 -0
- package/omd/nodes/omdNode.js +557 -0
- package/omd/nodes/omdOperationDisplayNode.js +322 -0
- package/omd/nodes/omdOperatorNode.js +109 -0
- package/omd/nodes/omdParenthesisNode.js +293 -0
- package/omd/nodes/omdPowerNode.js +236 -0
- package/omd/nodes/omdRationalNode.js +295 -0
- package/omd/nodes/omdSqrtNode.js +308 -0
- package/omd/nodes/omdUnaryExpressionNode.js +178 -0
- package/omd/nodes/omdVariableNode.js +123 -0
- package/omd/simplification/omdSimplification.js +171 -0
- package/omd/simplification/omdSimplificationEngine.js +886 -0
- package/omd/simplification/package.json +6 -0
- package/omd/simplification/rules/binaryRules.js +1037 -0
- package/omd/simplification/rules/functionRules.js +111 -0
- package/omd/simplification/rules/index.js +48 -0
- package/omd/simplification/rules/parenthesisRules.js +19 -0
- package/omd/simplification/rules/powerRules.js +143 -0
- package/omd/simplification/rules/rationalRules.js +475 -0
- package/omd/simplification/rules/sqrtRules.js +48 -0
- package/omd/simplification/rules/unaryRules.js +37 -0
- package/omd/simplification/simplificationRules.js +32 -0
- package/omd/simplification/simplificationUtils.js +1056 -0
- package/omd/step-visualizer/omdStepVisualizer.js +597 -0
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
- package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
- package/omd/utils/omdNodeOverlay.js +638 -0
- package/omd/utils/omdPopup.js +1084 -0
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
- package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
- package/omd/utils/omdTranscriptionService.js +125 -0
- package/omd/utils/omdTreeDiff.js +734 -0
- package/package.json +46 -0
- package/src/index.js +62 -0
- package/src/json-schemas.md +109 -0
- package/src/omd-json-samples.js +115 -0
- package/src/omd.js +109 -0
- package/src/omdApp.js +391 -0
- package/src/omdAppCanvas.js +336 -0
- package/src/omdBalanceHanger.js +172 -0
- package/src/omdColor.js +13 -0
- package/src/omdCoordinatePlane.js +467 -0
- package/src/omdEquation.js +125 -0
- package/src/omdExpression.js +104 -0
- package/src/omdFunction.js +113 -0
- package/src/omdMetaExpression.js +287 -0
- package/src/omdNaturalExpression.js +564 -0
- package/src/omdNode.js +384 -0
- package/src/omdNumber.js +53 -0
- package/src/omdNumberLine.js +107 -0
- package/src/omdNumberTile.js +119 -0
- package/src/omdOperator.js +73 -0
- package/src/omdPowerExpression.js +92 -0
- package/src/omdProblem.js +55 -0
- package/src/omdRatioChart.js +232 -0
- package/src/omdRationalExpression.js +115 -0
- package/src/omdSampleData.js +215 -0
- package/src/omdShapes.js +476 -0
- package/src/omdSpinner.js +148 -0
- package/src/omdString.js +39 -0
- package/src/omdTable.js +369 -0
- package/src/omdTapeDiagram.js +245 -0
- package/src/omdTerm.js +92 -0
- package/src/omdTileEquation.js +349 -0
- package/src/omdVariable.js +51 -0
package/src/omdNode.js
ADDED
|
@@ -0,0 +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
|
+
}
|
|
384
|
+
}
|
package/src/omdNumber.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
import { omdColor } from "./omdColor.js";
|
|
3
|
+
import { omdMetaExpression } from "./omdMetaExpression.js"
|
|
4
|
+
|
|
5
|
+
export class omdNumber extends omdMetaExpression
|
|
6
|
+
{
|
|
7
|
+
constructor( V = 1 )
|
|
8
|
+
{
|
|
9
|
+
// initialization
|
|
10
|
+
super();
|
|
11
|
+
|
|
12
|
+
this.type = "omdNumber";
|
|
13
|
+
this.value = V;
|
|
14
|
+
|
|
15
|
+
this.numText = new jsvgTextBox();
|
|
16
|
+
this.numText.setWidthAndHeight( 30,30 );
|
|
17
|
+
this.numText.setText ( this.value.toString() );
|
|
18
|
+
this.numText.setFontFamily( "Albert Sans" );
|
|
19
|
+
this.numText.setFontColor( "black" );
|
|
20
|
+
this.numText.setFontSize( 18 );
|
|
21
|
+
this.numText.setVerticalCentering();
|
|
22
|
+
this.numText.setAlignment("center");
|
|
23
|
+
// this.numText.div.style.border = "1px solid black";
|
|
24
|
+
this.addChild( this.numText );
|
|
25
|
+
|
|
26
|
+
this.setValue( V );
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
loadFromJSON( data )
|
|
30
|
+
{
|
|
31
|
+
if ( typeof data.value != "undefined" )
|
|
32
|
+
this.value = data.value;
|
|
33
|
+
|
|
34
|
+
this.updateLayout();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setValue( V )
|
|
38
|
+
{
|
|
39
|
+
this.value = V;
|
|
40
|
+
this.updateLayout();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
updateLayout()
|
|
44
|
+
{
|
|
45
|
+
var T = this.value.toString();
|
|
46
|
+
var W = 10 + T.length*10;
|
|
47
|
+
this.backRect.setWidthAndHeight( W, 30 );
|
|
48
|
+
this.numText.setWidthAndHeight( W, 30 );
|
|
49
|
+
this.numText.setText ( this.value.toString() );
|
|
50
|
+
|
|
51
|
+
this.setWidthAndHeight( this.backRect.width, this.backRect.height );
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
|
|
2
|
+
import { omdColor } from "./omdColor.js";
|
|
3
|
+
import { jsvgGroup, jsvgRect, jsvgLine, jsvgTextBox, jsvgEllipse } from "@teachinglab/jsvg";
|
|
4
|
+
|
|
5
|
+
export class omdNumberLine extends jsvgGroup
|
|
6
|
+
{
|
|
7
|
+
constructor()
|
|
8
|
+
{
|
|
9
|
+
// initialization
|
|
10
|
+
super();
|
|
11
|
+
|
|
12
|
+
this.type = "omdNumberLine";
|
|
13
|
+
|
|
14
|
+
this.min = 0;
|
|
15
|
+
this.max = 10;
|
|
16
|
+
this.dotValues = [];
|
|
17
|
+
this.label = "";
|
|
18
|
+
this.updateLayout();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
loadFromJSON( data )
|
|
22
|
+
{
|
|
23
|
+
if ( typeof data.min != "undefined" )
|
|
24
|
+
this.min = data.min;
|
|
25
|
+
|
|
26
|
+
if ( typeof data.max != "undefined" )
|
|
27
|
+
this.max = data.max;
|
|
28
|
+
|
|
29
|
+
if ( typeof data.dotValues != "undefined" )
|
|
30
|
+
this.dotValues = data.dotValues;
|
|
31
|
+
|
|
32
|
+
if ( typeof data.label != "undefined" )
|
|
33
|
+
this.label = data.label;
|
|
34
|
+
|
|
35
|
+
this.updateLayout();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setMinAndMax( min, max )
|
|
39
|
+
{
|
|
40
|
+
this.min = min;
|
|
41
|
+
this.max = max;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
addNumberDot( V )
|
|
45
|
+
{
|
|
46
|
+
this.dotValues.push( V );
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
updateLayout()
|
|
50
|
+
{
|
|
51
|
+
this.removeAllChildren();
|
|
52
|
+
|
|
53
|
+
// make line
|
|
54
|
+
this.line = new jsvgRect();
|
|
55
|
+
// this.line.setStrokeColor( "black" );
|
|
56
|
+
// this.line.setStrokeWidth( 1 );
|
|
57
|
+
// this.line.setEndpoints( 0,0, 300, 0 );
|
|
58
|
+
this.line.setWidthAndHeight(320,5);
|
|
59
|
+
this.line.setPosition( -10, -2.5 );
|
|
60
|
+
this.line.setFillColor( omdColor.mediumGray );
|
|
61
|
+
this.line.setCornerRadius( 2.5 );
|
|
62
|
+
this.addChild( this.line );
|
|
63
|
+
|
|
64
|
+
// make ticks with text
|
|
65
|
+
for( var i=this.min; i<=this.max; i++ )
|
|
66
|
+
{
|
|
67
|
+
var N = i - this.min;
|
|
68
|
+
var dX = 300 / (this.max - this.min);
|
|
69
|
+
|
|
70
|
+
var pX = N*dX;
|
|
71
|
+
|
|
72
|
+
var tick = new jsvgLine();
|
|
73
|
+
tick.setStrokeColor( "black" );
|
|
74
|
+
tick.setStrokeWidth( 1 );
|
|
75
|
+
tick.setEndpoints( pX, -5, pX, 5 );
|
|
76
|
+
this.addChild( tick );
|
|
77
|
+
|
|
78
|
+
var tickText = new jsvgTextBox();
|
|
79
|
+
tickText.setWidthAndHeight( 30,30 );
|
|
80
|
+
tickText.setText ( this.name );
|
|
81
|
+
tickText.setFontFamily( "Albert Sans" );
|
|
82
|
+
tickText.setFontColor( "black" );
|
|
83
|
+
tickText.setFontSize( 10 );
|
|
84
|
+
tickText.setAlignment("center");
|
|
85
|
+
tickText.setText( i.toString() );
|
|
86
|
+
tickText.setPosition( pX-15, 10 );
|
|
87
|
+
this.addChild( tickText );
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// make dots
|
|
91
|
+
for( var i=0; i<this.dotValues.length; i++ )
|
|
92
|
+
{
|
|
93
|
+
var V = this.dotValues[i];
|
|
94
|
+
|
|
95
|
+
var N = V - this.min;
|
|
96
|
+
var dX = 300 / (this.max - this.min);
|
|
97
|
+
var pX = N*dX;
|
|
98
|
+
|
|
99
|
+
var dot = new jsvgEllipse();
|
|
100
|
+
dot.setFillColor( "black" );
|
|
101
|
+
dot.setWidthAndHeight( 10,10 );
|
|
102
|
+
dot.setPosition( pX, 0 );
|
|
103
|
+
this.addChild( dot );
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
omdColor
|
|
3
|
+
} from "./omdColor.js";
|
|
4
|
+
import { jsvgGroup, jsvgRect, jsvgEllipse } from "@teachinglab/jsvg";
|
|
5
|
+
|
|
6
|
+
export class omdNumberTile extends jsvgGroup {
|
|
7
|
+
constructor() {
|
|
8
|
+
// initialization
|
|
9
|
+
super();
|
|
10
|
+
|
|
11
|
+
this.type = "omdNumberLine";
|
|
12
|
+
|
|
13
|
+
this.value = 1;
|
|
14
|
+
this.size = 'large';
|
|
15
|
+
this.dotsPerColumn = 10; // arrange dots in columns of 10 by default
|
|
16
|
+
this.backgroundColor = omdColor.lightGray;
|
|
17
|
+
this.dotColor = "black";
|
|
18
|
+
this.updateLayout();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
loadFromJSON(data) {
|
|
22
|
+
if (typeof data.value != "undefined")
|
|
23
|
+
this.value = data.value;
|
|
24
|
+
|
|
25
|
+
if (typeof data.size != "undefined")
|
|
26
|
+
this.size = data.size;
|
|
27
|
+
|
|
28
|
+
if (typeof data.dotsPerColumn != "undefined")
|
|
29
|
+
this.dotsPerColumn = Math.max(1, Number(data.dotsPerColumn));
|
|
30
|
+
|
|
31
|
+
if (typeof data.backgroundColor != "undefined")
|
|
32
|
+
this.backgroundColor = data.backgroundColor;
|
|
33
|
+
|
|
34
|
+
if (typeof data.dotColor != "undefined")
|
|
35
|
+
this.dotColor = data.dotColor;
|
|
36
|
+
|
|
37
|
+
this.updateLayout();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
setValue(V) {
|
|
41
|
+
this.value = V;
|
|
42
|
+
this.updateLayout();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setSize(size) {
|
|
46
|
+
this.size = size;
|
|
47
|
+
this.updateLayout();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
updateLayout() {
|
|
51
|
+
this.removeAllChildren();
|
|
52
|
+
|
|
53
|
+
// Normalize inputs
|
|
54
|
+
const totalValue = Math.max(0, Number(this.value) || 0);
|
|
55
|
+
const perColumn = Math.max(1, Number(this.dotsPerColumn) || 1);
|
|
56
|
+
|
|
57
|
+
// 2) Sizing based on selected tile size
|
|
58
|
+
let dotSize;
|
|
59
|
+
if (this.size === 'large') {
|
|
60
|
+
dotSize = 15;
|
|
61
|
+
} else if (this.size === 'medium') {
|
|
62
|
+
dotSize = 12;
|
|
63
|
+
} else {
|
|
64
|
+
dotSize = 5;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const gap = Math.max(2, Math.round(dotSize * 0.6));
|
|
68
|
+
const pad = Math.max(3, Math.round(dotSize * 0.8));
|
|
69
|
+
|
|
70
|
+
// Grid geometry
|
|
71
|
+
const columns = Math.ceil(totalValue / perColumn);
|
|
72
|
+
const rowsTallest = Math.min(perColumn, totalValue);
|
|
73
|
+
|
|
74
|
+
// Expose for external alignment consumers
|
|
75
|
+
this._dotSize = dotSize;
|
|
76
|
+
this._pad = pad;
|
|
77
|
+
|
|
78
|
+
// Compute background rect
|
|
79
|
+
const innerW = columns > 0 ? (columns * dotSize + (columns - 1) * gap) : 0;
|
|
80
|
+
const innerH = rowsTallest > 0 ? (rowsTallest * dotSize + (rowsTallest - 1) * gap) : 0;
|
|
81
|
+
const W = innerW + 2 * pad;
|
|
82
|
+
const H = innerH + 2 * pad;
|
|
83
|
+
|
|
84
|
+
const backRect = new jsvgRect();
|
|
85
|
+
backRect.setWidthAndHeight(W, H);
|
|
86
|
+
|
|
87
|
+
// Pill for single column; rounded rectangle for multiple
|
|
88
|
+
const cornerR = (columns > 1) ? Math.round(dotSize) : Math.min(W, H) / 2;
|
|
89
|
+
backRect.setCornerRadius(cornerR);
|
|
90
|
+
backRect.setFillColor(this.backgroundColor || omdColor.lightGray);
|
|
91
|
+
this.addChild(backRect);
|
|
92
|
+
|
|
93
|
+
this.width = backRect.width;
|
|
94
|
+
this.height = backRect.height;
|
|
95
|
+
// Ensure group viewBox matches content so hosts size correctly
|
|
96
|
+
this.svgObject.setAttribute("viewBox", `0 0 ${this.width} ${this.height}`);
|
|
97
|
+
|
|
98
|
+
// Render dots top-down, perColumn dots per column
|
|
99
|
+
for (let i = 0; i < totalValue; i++) {
|
|
100
|
+
const col = Math.floor(i / perColumn);
|
|
101
|
+
const row = i % perColumn;
|
|
102
|
+
const cx = pad + col * (dotSize + gap) + dotSize / 2;
|
|
103
|
+
const cy = pad + row * (dotSize + gap) + dotSize / 2;
|
|
104
|
+
const dot = new jsvgEllipse();
|
|
105
|
+
dot.setFillColor(this.dotColor || "black");
|
|
106
|
+
dot.setWidthAndHeight(dotSize, dotSize);
|
|
107
|
+
dot.setPosition(cx, cy);
|
|
108
|
+
this.addChild(dot);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Returns y of the top-most dot center in local coordinates
|
|
113
|
+
getTopDotCenterY() {
|
|
114
|
+
const pad = this._pad ?? 0;
|
|
115
|
+
const dotSize = this._dotSize ?? 0;
|
|
116
|
+
return pad + dotSize / 2;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
}
|