@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
|
@@ -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
|
+
}
|