@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
@@ -0,0 +1,347 @@
1
+ import { omdEquationSequenceNode } from '../nodes/omdEquationSequenceNode.js';
2
+ import { omdStepVisualizer } from '../step-visualizer/omdStepVisualizer.js';
3
+ import { omdToolbar } from '../display/omdToolbar.js';
4
+ import { jsvgGroup, jsvgLayoutGroup } from '@teachinglab/jsvg';
5
+ import { omdEquationNode } from '../nodes/omdEquationNode.js';
6
+ import { omdOperationDisplayNode } from '../nodes/omdOperationDisplayNode.js';
7
+
8
+ /**
9
+ * A renderable component that bundles a sequence and optional UI controls.
10
+ * It acts as a node that can be rendered by an omdDisplay.
11
+ * @extends jsvgGroup
12
+ */
13
+ export class omdEquationStack extends jsvgGroup {
14
+ /**
15
+ * @param {Array<omdNode>} [steps=[]] - An initial array of equation steps.
16
+ * @param {Object} [options={}] - Configuration options.
17
+ * @param {boolean} [options.toolbar=false] - If true, creates a toolbar-driven sequence.
18
+ * @param {boolean} [options.stepVisualizer=false] - If true, creates a sequence with a step visualizer.
19
+ */
20
+ constructor(steps = [], options = {}) {
21
+ super();
22
+ this.options = { ...options };
23
+
24
+ // Normalize new structured options
25
+ this.toolbarOptions = null;
26
+ if (typeof options.toolbar === 'object') {
27
+ this.toolbarOptions = { enabled: true, ...options.toolbar };
28
+ } else if (options.toolbar === true) {
29
+ this.toolbarOptions = { enabled: true };
30
+ } else if (options.toolbar === false) {
31
+ this.toolbarOptions = { enabled: false };
32
+ }
33
+ this.stylingOptions = options.styling || null;
34
+
35
+ // The sequence is the core. If a visualizer is needed, that's our sequence.
36
+ if (options.stepVisualizer) {
37
+ this.sequence = new omdStepVisualizer(steps);
38
+ } else {
39
+ this.sequence = new omdEquationSequenceNode(steps);
40
+ }
41
+
42
+ // Apply equation background styling if provided
43
+ if (this.stylingOptions?.equationBackground) {
44
+ this.sequence.setDefaultEquationBackground(this.stylingOptions.equationBackground);
45
+ }
46
+
47
+ // If a toolbar is needed, create it.
48
+ if (this.toolbarOptions?.enabled) {
49
+ // Default undo: call global hook if provided
50
+ const toolbarOpts = { ...this.toolbarOptions };
51
+ if (toolbarOpts.showUndoButton && !toolbarOpts.onUndo) {
52
+ toolbarOpts.onUndo = () => {
53
+ if (typeof window !== 'undefined' && typeof window.onOMDToolbarUndo === 'function') {
54
+ try { window.onOMDToolbarUndo(this.sequence); } catch (_) {}
55
+ }
56
+ };
57
+ }
58
+ this.toolbar = new omdToolbar(this, this.sequence, toolbarOpts);
59
+ }
60
+
61
+ // Overlay padding (distance from bottom when overlayed)
62
+ this.overlayPadding = typeof this.toolbarOptions?.overlayPadding === 'number'
63
+ ? this.toolbarOptions.overlayPadding
64
+ : 34; // Default a bit above the very bottom to match buttons
65
+
66
+ // Create a vertical layout group to hold the sequence and toolbar
67
+ this.layoutGroup = new jsvgLayoutGroup();
68
+ this.layoutGroup.setSpacer(16); // Adjust as needed for spacing
69
+ this.layoutGroup.addChild(this.sequence);
70
+
71
+ // Handle toolbar positioning
72
+ const position = this.toolbarOptions?.position || this.options.toolbarPosition; // backward compat
73
+ const overlayBottom = position === 'bottom' || position === 'overlay-bottom';
74
+
75
+ if (this.toolbar) {
76
+ if (overlayBottom) {
77
+ // For overlay positioning, add toolbar directly to this (not layoutGroup)
78
+ console.log('[constructor] Adding toolbar as overlay to stack');
79
+ this.addChild(this.toolbar.elements.toolbarGroup);
80
+ } else {
81
+ // For in-flow positioning, add to layout group
82
+ console.log('[constructor] Adding toolbar to layout group');
83
+ this.layoutGroup.addChild(this.toolbar.elements.toolbarGroup);
84
+ }
85
+ }
86
+
87
+ this.addChild(this.layoutGroup);
88
+ this.updateLayout();
89
+ }
90
+
91
+ /**
92
+ * Updates the layout and positioning of internal components.
93
+ */
94
+ updateLayout() {
95
+ this.sequence.updateLayout();
96
+ this.layoutGroup.doVerticalLayout();
97
+
98
+ // Handle toolbar positioning based on overlay flag
99
+ const position = this.toolbarOptions?.position || this.options.toolbarPosition; // backward compat
100
+ const overlayBottom = position === 'bottom' || position === 'overlay-bottom';
101
+
102
+ if (this.toolbar && !overlayBottom) {
103
+ // Center the toolbar under the stack if in-flow and their widths differ
104
+ const stackWidth = this.sequence.width;
105
+ const toolbarWidth = this.toolbar.elements.background.width;
106
+ const toolbarGroup = this.toolbar.elements.toolbarGroup;
107
+ // Center toolbar horizontally under the stack
108
+ toolbarGroup.setPosition(
109
+ (stackWidth - toolbarWidth) / 2,
110
+ toolbarGroup.ypos // y is handled by layout group
111
+ );
112
+ }
113
+
114
+ this.width = this.layoutGroup.width;
115
+ this.height = this.layoutGroup.height;
116
+ }
117
+
118
+ /**
119
+ * Returns the underlying sequence instance.
120
+ * @returns {omdEquationSequenceNode|omdStepVisualizer} The managed sequence instance.
121
+ */
122
+ getSequence() {
123
+ return this.sequence;
124
+ }
125
+
126
+ /**
127
+ * Expose overlay padding to the display so it can pass it during reposition
128
+ */
129
+ getOverlayPadding() {
130
+ return this.overlayPadding;
131
+ }
132
+
133
+ /**
134
+ * Returns the visual height in pixels of the toolbar background (unscaled), if present.
135
+ * Useful for reserving space when overlaying the toolbar.
136
+ */
137
+ getToolbarVisualHeight() {
138
+ if (this.toolbar && this.toolbar.elements && this.toolbar.elements.background) {
139
+ return this.toolbar.elements.background.height || 0;
140
+ }
141
+ return 0;
142
+ }
143
+
144
+ /**
145
+ * Whether the toolbar is configured to be overlayed at the bottom of the container
146
+ * @returns {boolean}
147
+ */
148
+ isToolbarOverlay() {
149
+ const position = this.toolbarOptions?.position || this.options.toolbarPosition; // backward compat
150
+ return !!(this.toolbar && (position === 'bottom' || position === 'overlay-bottom'));
151
+ }
152
+
153
+ /**
154
+ * Positions the toolbar overlay at the bottom center of the container
155
+ * @param {number} containerWidth - Width of the container
156
+ * @param {number} containerHeight - Height of the container
157
+ * @param {number} [padding=16] - Padding from the bottom edge
158
+ */
159
+ positionToolbarOverlay(containerWidth, containerHeight, padding = 16) {
160
+ if (!this.toolbar || !this.isToolbarOverlay()) return;
161
+
162
+ const toolbarGroup = this.toolbar.elements.toolbarGroup;
163
+ const toolbarWidth = this.toolbar.elements.background.width;
164
+ const toolbarHeight = this.toolbar.elements.background.height;
165
+
166
+ // Position at bottom center of the DISPLAY (container) while this toolbar
167
+ // lives inside the stack's local coordinate system, which may be scaled.
168
+ // Convert container (global) coordinates to stack-local by subtracting
169
+ // the stack's position and dividing by its scale.
170
+ const stackX = this.xpos || 0;
171
+ const stackY = this.ypos || 0;
172
+ const s = (typeof this.scale === 'number' && this.scale > 0) ? this.scale : 1;
173
+ const effectivePadding = (typeof padding === 'number') ? padding : this.overlayPadding;
174
+
175
+ // Compute top-left of toolbar in container coordinates using UN-SCALED toolbar size
176
+ // because we counter-scale the toolbar by 1/s to keep constant on-screen size.
177
+ let containerX = (containerWidth - toolbarWidth) / 2;
178
+ let containerY = containerHeight - toolbarHeight - effectivePadding;
179
+ // Snap to integer pixels to avoid subpixel jitter when scaling
180
+ containerX = Math.round(containerX);
181
+ containerY = Math.round(containerY);
182
+
183
+ // Convert to stack-local coordinates
184
+ const x = (containerX - stackX) / s;
185
+ const y = (containerY - stackY) / s;
186
+
187
+ // Find the root SVG to check its viewBox
188
+ let rootSVG = toolbarGroup.svgObject;
189
+ while (rootSVG && rootSVG.tagName !== 'svg' && rootSVG.parentElement) {
190
+ rootSVG = rootSVG.parentElement;
191
+ }
192
+ const svgViewBox = rootSVG?.getAttribute?.('viewBox') || 'unknown';
193
+
194
+
195
+ // Counter-scale the toolbar so it remains a constant on-screen size
196
+ if (typeof toolbarGroup.setScale === 'function') {
197
+ toolbarGroup.setScale(1 / s);
198
+ }
199
+ toolbarGroup.setPosition(x, y);
200
+
201
+ // Ensure toolbar is visible and on top
202
+ if (toolbarGroup.svgObject) {
203
+ toolbarGroup.svgObject.style.display = 'block';
204
+ toolbarGroup.svgObject.style.zIndex = '1000';
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Returns the toolbar instance, if one was created.
210
+ * @returns {omdToolbar|undefined}
211
+ */
212
+ getToolbar() {
213
+ return this.toolbar;
214
+ }
215
+
216
+ /**
217
+ * Undo the last operation (remove bottom-most equation and its preceding operation display)
218
+ * Also updates a step visualizer if present.
219
+ * @returns {boolean} Whether an operation was undone
220
+ */
221
+ undoLastOperation() {
222
+ const seq = this.sequence;
223
+ if (!seq || !Array.isArray(seq.steps) || seq.steps.length === 0) return false;
224
+
225
+ // Find bottom-most equation
226
+ let eqIndex = -1;
227
+ for (let i = seq.steps.length - 1; i >= 0; i--) {
228
+ const st = seq.steps[i];
229
+ const name = st?.constructor?.name;
230
+ if (st instanceof omdEquationNode || name === 'omdEquationNode') { eqIndex = i; break; }
231
+ }
232
+ if (eqIndex === -1) return false;
233
+
234
+ // Find nearest preceding operation display (if any)
235
+ let startIndex = eqIndex;
236
+ for (let i = eqIndex; i >= 0; i--) {
237
+ const st = seq.steps[i];
238
+ const name = st?.constructor?.name;
239
+ if (st instanceof omdOperationDisplayNode || name === 'omdOperationDisplayNode') { startIndex = i; break; }
240
+ }
241
+
242
+ try {
243
+ console.log('[undoLastOperation] before', {
244
+ totalSteps: seq.steps.length,
245
+ eqIndex,
246
+ startIndex,
247
+ stepTypes: seq.steps.map(s => s?.constructor?.name)
248
+ });
249
+ } catch (_) {}
250
+
251
+ // Remove DOM children and steps from startIndex to end
252
+ for (let i = seq.steps.length - 1; i >= startIndex; i--) {
253
+ const step = seq.steps[i];
254
+ try { seq.removeChild(step); } catch (_) {}
255
+ }
256
+ seq.steps.splice(startIndex);
257
+ seq.argumentNodeList.steps = seq.steps;
258
+ if (Array.isArray(seq.stepDescriptions)) seq.stepDescriptions.length = seq.steps.length;
259
+ if (Array.isArray(seq.importanceLevels)) seq.importanceLevels.length = seq.steps.length;
260
+
261
+ // Adjust current index
262
+ if (typeof seq.currentStepIndex === 'number' && seq.currentStepIndex >= seq.steps.length) {
263
+ seq.currentStepIndex = Math.max(0, seq.steps.length - 1);
264
+ }
265
+
266
+ // Rebuild maps and layout on sequence
267
+ if (typeof seq.rebuildNodeMap === 'function') seq.rebuildNodeMap();
268
+ if (typeof seq.computeDimensions === 'function') seq.computeDimensions();
269
+ if (typeof seq.updateLayout === 'function') seq.updateLayout();
270
+
271
+ // If this is a step visualizer, rebuild its dots/lines
272
+ if (typeof seq.rebuildVisualizer === 'function') {
273
+ try {
274
+ console.log('[undoLastOperation] rebuilding visualizer');
275
+ seq.rebuildVisualizer();
276
+ } catch (_) {}
277
+ } else if (typeof seq._initializeVisualElements === 'function') {
278
+ try {
279
+ seq._initializeVisualElements();
280
+ if (typeof seq.computeDimensions === 'function') seq.computeDimensions();
281
+ if (typeof seq.updateLayout === 'function') seq.updateLayout();
282
+ } catch (_) {}
283
+ }
284
+
285
+ // Safety: ensure dot/line counts match equations and prune orphan dots
286
+ try {
287
+ const isEquation = (s) => (s instanceof omdEquationNode) || (s?.constructor?.name === 'omdEquationNode');
288
+ const equationsCount = Array.isArray(seq.steps) ? seq.steps.filter(isEquation).length : 0;
289
+ console.log('[undoLastOperation] after removal', {
290
+ remainingSteps: seq.steps.length,
291
+ equationsCount,
292
+ dotCount: Array.isArray(seq.stepDots) ? seq.stepDots.length : 'n/a',
293
+ lineCount: Array.isArray(seq.stepLines) ? seq.stepLines.length : 'n/a'
294
+ });
295
+ // Remove dots whose equationRef is no longer present in steps
296
+ if (Array.isArray(seq.stepDots) && seq.visualContainer) {
297
+ const eqSet = new Set(seq.steps.filter(isEquation));
298
+ const keptDots = [];
299
+ for (const dot of seq.stepDots) {
300
+ if (!dot || !dot.equationRef || !eqSet.has(dot.equationRef)) {
301
+ try { seq.visualContainer.removeChild(dot); } catch (_) {}
302
+ } else {
303
+ keptDots.push(dot);
304
+ }
305
+ }
306
+ seq.stepDots = keptDots;
307
+ }
308
+ // Also purge any children in visualContainer that are not current dots or lines
309
+ if (seq.visualContainer && Array.isArray(seq.visualContainer.childList)) {
310
+ const valid = new Set([...(seq.stepDots||[]), ...(seq.stepLines||[])]);
311
+ const toRemove = [];
312
+ seq.visualContainer.childList.forEach(child => { if (!valid.has(child)) toRemove.push(child); });
313
+ toRemove.forEach(child => { try { seq.visualContainer.removeChild(child); } catch (_) {} });
314
+ }
315
+ if (Array.isArray(seq.stepDots) && seq.visualContainer) {
316
+ while (seq.stepDots.length > equationsCount) {
317
+ const dot = seq.stepDots.pop();
318
+ try { seq.visualContainer.removeChild(dot); } catch (_) {}
319
+ }
320
+ }
321
+ if (Array.isArray(seq.stepLines) && seq.visualContainer) {
322
+ const targetLines = Math.max(0, equationsCount - 1);
323
+ while (seq.stepLines.length > targetLines) {
324
+ const line = seq.stepLines.pop();
325
+ try { seq.visualContainer.removeChild(line); } catch (_) {}
326
+ }
327
+ }
328
+ if (seq.layoutManager) {
329
+ try {
330
+ seq.layoutManager.updateVisualLayout();
331
+ seq.layoutManager.updateVisualVisibility();
332
+ seq.layoutManager.updateAllLinePositions();
333
+ } catch (_) {}
334
+ }
335
+ } catch (_) {}
336
+
337
+ // Refresh stack layout
338
+ this.updateLayout();
339
+ try {
340
+ console.log('[undoLastOperation] done', {
341
+ finalDotCount: Array.isArray(seq.stepDots) ? seq.stepDots.length : 'n/a',
342
+ finalLineCount: Array.isArray(seq.stepLines) ? seq.stepLines.length : 'n/a'
343
+ });
344
+ } catch (_) {}
345
+ return true;
346
+ }
347
+ }
@@ -0,0 +1,115 @@
1
+ import { omdBinaryExpressionNode } from "../nodes/omdBinaryExpressionNode.js";
2
+ import { omdParenthesisNode } from "../nodes/omdParenthesisNode.js";
3
+ import { omdPowerNode } from "../nodes/omdPowerNode.js";
4
+ import { omdRationalNode } from "../nodes/omdRationalNode.js";
5
+ import { omdNode } from "../nodes/omdNode.js";
6
+ import { omdFunctionNode } from "../nodes/omdFunctionNode.js";
7
+ import { omdSqrtNode } from "../nodes/omdSqrtNode.js";
8
+ import { omdLeafNode } from "../nodes/omdLeafNode.js";
9
+ import { omdConstantNode } from "../nodes/omdConstantNode.js";
10
+ import { omdVariableNode } from "../nodes/omdVariableNode.js";
11
+ import { omdEquationNode } from "../nodes/omdEquationNode.js";
12
+ import { omdUnaryExpressionNode } from "../nodes/omdUnaryExpressionNode.js";
13
+
14
+ /**
15
+ * Maps an AST node type to its corresponding OMD node class
16
+ * @param {string} type - The type of the AST node (e.g., "OperatorNode", "ParenthesisNode")
17
+ * @param {Object} ast - The AST node data containing additional context
18
+ * @returns {class} The appropriate OMD node class for the given AST node type
19
+ */
20
+ export function astToOmdType(type, ast) {
21
+ switch (type) {
22
+ case "AssignmentNode":
23
+ return omdEquationNode;
24
+ case "OperatorNode":
25
+ // Check for unary minus: op is '-' and there's only one argument.
26
+ if (ast?.op === '-' && ast.args.length === 1 && !ast.implicit) {
27
+ return omdUnaryExpressionNode;
28
+ }
29
+ if (ast?.op === '=') return omdEquationNode;
30
+ if (ast?.op === '^') return omdPowerNode;
31
+ if (ast?.op === '/') {
32
+ return omdRationalNode;
33
+ }
34
+ return omdBinaryExpressionNode;
35
+ case "ParenthesisNode":
36
+ return omdParenthesisNode;
37
+ case "ConstantNode":
38
+ return omdConstantNode;
39
+ case "SymbolNode":
40
+ return omdVariableNode;
41
+ case "FunctionNode":
42
+ // Handle implicit multiplication from math.js AST
43
+ if ((ast?.fn?.name === 'multiply' || ast?.name === 'multiply') && ast.implicit) {
44
+ return omdBinaryExpressionNode;
45
+ }
46
+ // Check if this is a sqrt function
47
+ if (ast?.fn?.name === 'sqrt' || ast?.name === 'sqrt') {
48
+ return omdSqrtNode;
49
+ }
50
+ return omdFunctionNode;
51
+ default:
52
+ console.log(`Unknown AST node type: ${type}, using default omdNode`);
53
+ return omdNode;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Determines whether a division operation should be rendered as a fraction
59
+ * @param {Object} ast - The AST node representing a division operation
60
+ * @returns {boolean} True if the division should be rendered as a fraction, false otherwise
61
+ */
62
+ function shouldUseFractionNotation(ast) {
63
+ const numeratorComplex = isComplexExpression(ast.args[0]);
64
+ const denominatorComplex = isComplexExpression(ast.args[1]);
65
+ return !(numeratorComplex || denominatorComplex);
66
+ }
67
+
68
+ /**
69
+ * Checks if an AST node represents a complex expression
70
+ * @param {Object} ast - The AST node to check
71
+ * @returns {boolean} True if the expression is complex (contains multiple operations), false otherwise
72
+ */
73
+ function isComplexExpression(ast) {
74
+ if (!ast) return false;
75
+ // A binary plus/minus is complex. A unary minus is not.
76
+ if (ast.type === "OperatorNode" && (ast.op === "+" || ast.op === "-") && ast.args.length === 2) return true;
77
+ return ast.args?.some(arg => isComplexExpression(arg)) || false;
78
+ }
79
+
80
+ /**
81
+ * Gets the appropriate OMD node class for an AST node
82
+ * @param {Object} ast - The AST node to get the class for
83
+ * @returns {class} The appropriate OMD node class for the given AST node
84
+ */
85
+ export function getNodeForAST(ast) {
86
+ let nodeType = ast.type;
87
+ if (ast.mathjs) {
88
+ nodeType = ast.mathjs;
89
+ }
90
+ return astToOmdType(nodeType, ast);
91
+ }
92
+
93
+ /* Gerard - Added utility function */
94
+ export function getTextBounds(text, fontSize) {
95
+ // Create a temporary span element
96
+ const span = document.createElement('span');
97
+ span.style.visibility = 'hidden';
98
+ span.style.position = 'absolute';
99
+ span.style.whiteSpace = 'nowrap';
100
+ span.style.fontFamily = "Albert Sans";
101
+ span.style.fontSize = `${fontSize || 16}px`;
102
+ span.textContent = text;
103
+
104
+ // Append to the body to measure
105
+ document.body.appendChild(span);
106
+
107
+ // Measure dimensions using DOM
108
+ const width = span.offsetWidth;
109
+ const height = span.offsetHeight;
110
+
111
+ // Clean up DOM element
112
+ document.body.removeChild(span);
113
+
114
+ return {width: width, height: height};
115
+ }