@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.
Files changed (144) hide show
  1. package/README.md +138 -0
  2. package/canvas/core/canvasConfig.js +203 -0
  3. package/canvas/core/omdCanvas.js +475 -0
  4. package/canvas/drawing/segment.js +168 -0
  5. package/canvas/drawing/stroke.js +386 -0
  6. package/canvas/events/eventManager.js +435 -0
  7. package/canvas/events/pointerEventHandler.js +263 -0
  8. package/canvas/features/focusFrameManager.js +287 -0
  9. package/canvas/index.js +49 -0
  10. package/canvas/tools/eraserTool.js +322 -0
  11. package/canvas/tools/pencilTool.js +319 -0
  12. package/canvas/tools/selectTool.js +457 -0
  13. package/canvas/tools/tool.js +223 -0
  14. package/canvas/tools/toolManager.js +394 -0
  15. package/canvas/ui/cursor.js +438 -0
  16. package/canvas/ui/toolbar.js +304 -0
  17. package/canvas/utils/boundingBox.js +378 -0
  18. package/canvas/utils/mathUtils.js +259 -0
  19. package/docs/api/configuration-options.md +104 -0
  20. package/docs/api/eventManager.md +68 -0
  21. package/docs/api/focusFrameManager.md +150 -0
  22. package/docs/api/index.md +91 -0
  23. package/docs/api/main.md +58 -0
  24. package/docs/api/omdBinaryExpressionNode.md +227 -0
  25. package/docs/api/omdCanvas.md +142 -0
  26. package/docs/api/omdConfigManager.md +192 -0
  27. package/docs/api/omdConstantNode.md +117 -0
  28. package/docs/api/omdDisplay.md +121 -0
  29. package/docs/api/omdEquationNode.md +161 -0
  30. package/docs/api/omdEquationSequenceNode.md +301 -0
  31. package/docs/api/omdEquationStack.md +139 -0
  32. package/docs/api/omdFunctionNode.md +141 -0
  33. package/docs/api/omdGroupNode.md +182 -0
  34. package/docs/api/omdHelpers.md +96 -0
  35. package/docs/api/omdLeafNode.md +163 -0
  36. package/docs/api/omdNode.md +101 -0
  37. package/docs/api/omdOperationDisplayNode.md +139 -0
  38. package/docs/api/omdOperatorNode.md +127 -0
  39. package/docs/api/omdParenthesisNode.md +122 -0
  40. package/docs/api/omdPopup.md +117 -0
  41. package/docs/api/omdPowerNode.md +127 -0
  42. package/docs/api/omdRationalNode.md +128 -0
  43. package/docs/api/omdSequenceNode.md +128 -0
  44. package/docs/api/omdSimplification.md +110 -0
  45. package/docs/api/omdSqrtNode.md +79 -0
  46. package/docs/api/omdStepVisualizer.md +115 -0
  47. package/docs/api/omdStepVisualizerHighlighting.md +61 -0
  48. package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
  49. package/docs/api/omdStepVisualizerLayout.md +60 -0
  50. package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
  51. package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
  52. package/docs/api/omdToolbar.md +102 -0
  53. package/docs/api/omdTranscriptionService.md +76 -0
  54. package/docs/api/omdTreeDiff.md +134 -0
  55. package/docs/api/omdUnaryExpressionNode.md +174 -0
  56. package/docs/api/omdUtilities.md +70 -0
  57. package/docs/api/omdVariableNode.md +148 -0
  58. package/docs/api/selectTool.md +74 -0
  59. package/docs/api/simplificationEngine.md +98 -0
  60. package/docs/api/simplificationRules.md +77 -0
  61. package/docs/api/simplificationUtils.md +64 -0
  62. package/docs/api/transcribe.md +43 -0
  63. package/docs/api-reference.md +85 -0
  64. package/docs/index.html +454 -0
  65. package/docs/user-guide.md +9 -0
  66. package/index.js +67 -0
  67. package/omd/config/omdConfigManager.js +267 -0
  68. package/omd/core/index.js +150 -0
  69. package/omd/core/omdEquationStack.js +347 -0
  70. package/omd/core/omdUtilities.js +115 -0
  71. package/omd/display/omdDisplay.js +443 -0
  72. package/omd/display/omdToolbar.js +502 -0
  73. package/omd/nodes/omdBinaryExpressionNode.js +460 -0
  74. package/omd/nodes/omdConstantNode.js +142 -0
  75. package/omd/nodes/omdEquationNode.js +1223 -0
  76. package/omd/nodes/omdEquationSequenceNode.js +1273 -0
  77. package/omd/nodes/omdFunctionNode.js +352 -0
  78. package/omd/nodes/omdGroupNode.js +68 -0
  79. package/omd/nodes/omdLeafNode.js +77 -0
  80. package/omd/nodes/omdNode.js +557 -0
  81. package/omd/nodes/omdOperationDisplayNode.js +322 -0
  82. package/omd/nodes/omdOperatorNode.js +109 -0
  83. package/omd/nodes/omdParenthesisNode.js +293 -0
  84. package/omd/nodes/omdPowerNode.js +236 -0
  85. package/omd/nodes/omdRationalNode.js +295 -0
  86. package/omd/nodes/omdSqrtNode.js +308 -0
  87. package/omd/nodes/omdUnaryExpressionNode.js +178 -0
  88. package/omd/nodes/omdVariableNode.js +123 -0
  89. package/omd/simplification/omdSimplification.js +171 -0
  90. package/omd/simplification/omdSimplificationEngine.js +886 -0
  91. package/omd/simplification/package.json +6 -0
  92. package/omd/simplification/rules/binaryRules.js +1037 -0
  93. package/omd/simplification/rules/functionRules.js +111 -0
  94. package/omd/simplification/rules/index.js +48 -0
  95. package/omd/simplification/rules/parenthesisRules.js +19 -0
  96. package/omd/simplification/rules/powerRules.js +143 -0
  97. package/omd/simplification/rules/rationalRules.js +475 -0
  98. package/omd/simplification/rules/sqrtRules.js +48 -0
  99. package/omd/simplification/rules/unaryRules.js +37 -0
  100. package/omd/simplification/simplificationRules.js +32 -0
  101. package/omd/simplification/simplificationUtils.js +1056 -0
  102. package/omd/step-visualizer/omdStepVisualizer.js +597 -0
  103. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
  104. package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
  105. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
  106. package/omd/utils/omdNodeOverlay.js +638 -0
  107. package/omd/utils/omdPopup.js +1084 -0
  108. package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
  109. package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
  110. package/omd/utils/omdTranscriptionService.js +125 -0
  111. package/omd/utils/omdTreeDiff.js +734 -0
  112. package/package.json +46 -0
  113. package/src/index.js +62 -0
  114. package/src/json-schemas.md +109 -0
  115. package/src/omd-json-samples.js +115 -0
  116. package/src/omd.js +109 -0
  117. package/src/omdApp.js +391 -0
  118. package/src/omdAppCanvas.js +336 -0
  119. package/src/omdBalanceHanger.js +172 -0
  120. package/src/omdColor.js +13 -0
  121. package/src/omdCoordinatePlane.js +467 -0
  122. package/src/omdEquation.js +125 -0
  123. package/src/omdExpression.js +104 -0
  124. package/src/omdFunction.js +113 -0
  125. package/src/omdMetaExpression.js +287 -0
  126. package/src/omdNaturalExpression.js +564 -0
  127. package/src/omdNode.js +384 -0
  128. package/src/omdNumber.js +53 -0
  129. package/src/omdNumberLine.js +107 -0
  130. package/src/omdNumberTile.js +119 -0
  131. package/src/omdOperator.js +73 -0
  132. package/src/omdPowerExpression.js +92 -0
  133. package/src/omdProblem.js +55 -0
  134. package/src/omdRatioChart.js +232 -0
  135. package/src/omdRationalExpression.js +115 -0
  136. package/src/omdSampleData.js +215 -0
  137. package/src/omdShapes.js +476 -0
  138. package/src/omdSpinner.js +148 -0
  139. package/src/omdString.js +39 -0
  140. package/src/omdTable.js +369 -0
  141. package/src/omdTapeDiagram.js +245 -0
  142. package/src/omdTerm.js +92 -0
  143. package/src/omdTileEquation.js +349 -0
  144. 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
+ }
@@ -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
+ }