@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,1273 @@
1
+ import { omdNode } from "./omdNode.js";
2
+ import { simplifyStep } from "../simplification/omdSimplification.js";
3
+ import { omdEquationNode } from "./omdEquationNode.js";
4
+ import { getNodeForAST } from "../core/omdUtilities.js";
5
+ import { omdMetaExpression } from "../../src/omdMetaExpression.js";
6
+ import { omdOperationDisplayNode } from "./omdOperationDisplayNode.js";
7
+ import { getFontWeight } from "../config/omdConfigManager.js";
8
+ import { jsvgLayoutGroup } from '@teachinglab/jsvg';
9
+ /**
10
+ * Represents a sequence of equations for a step-by-step calculation.
11
+ * This node manages the layout of multiple equations, ensuring their
12
+ * equals signs are vertically aligned for readability.
13
+ * @extends omdNode
14
+ */
15
+ export class omdEquationSequenceNode extends omdNode {
16
+ static OPERATION_MAP = {
17
+ 'add': 'addToBothSides',
18
+ 'subtract': 'subtractFromBothSides',
19
+ 'multiply': 'multiplyBothSides',
20
+ 'divide': 'divideBothSides',
21
+ };
22
+
23
+ /**
24
+ * Sets the filter level for visible steps in the sequence.
25
+ * @param {number} level - The stepMark level to show (e.g., 0 for major steps)
26
+ */
27
+ setFilterLevel(level = 0) {
28
+ this.currentFilterLevels = [level];
29
+ this.updateStepsVisibility(step => (step.stepMark ?? 0) === level);
30
+ }
31
+
32
+ /**
33
+ * Sets multiple filter levels for visible steps in the sequence.
34
+ * @param {number[]} levels - Array of stepMark levels to show (e.g., [0, 1] for major and intermediate steps)
35
+ */
36
+ setFilterLevels(levels = [0]) {
37
+ this.currentFilterLevels = [...levels]; // Store a copy of the levels
38
+ this.updateStepsVisibility(step => {
39
+ const stepLevel = step.stepMark ?? 0;
40
+ return levels.includes(stepLevel);
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Reapplies the current filter levels
46
+ * @private
47
+ */
48
+ _reapplyCurrentFilter() {
49
+ if (this.currentFilterLevels && this.currentFilterLevels.length > 0) {
50
+ this.updateStepsVisibility(step => {
51
+ const stepLevel = step.stepMark ?? 0;
52
+ return this.currentFilterLevels.includes(stepLevel);
53
+ });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Gets the current filter level (always returns 0 since we default to level 0)
59
+ * @returns {number} The current filter level
60
+ */
61
+ getFilterLevel() {
62
+ return 0; // Always return 0 since that's our default
63
+ }
64
+
65
+ /**
66
+ * Creates a calculation node from an array of equation nodes.
67
+ * @param {Array<omdEquationNode>} steps - An array of omdEquationNode objects.
68
+ */
69
+ constructor(steps) {
70
+ super({}); // No specific AST for the container itself
71
+ this.type = "omdEquationSequenceNode";
72
+ this.steps = steps;
73
+ this.argumentNodeList.steps = this.steps;
74
+ this.steps.forEach(step => this.addChild(step));
75
+
76
+ this._initializeState();
77
+ this._initializeLayout();
78
+ this._initializeNodeMap();
79
+ this._disableContainerInteractions();
80
+
81
+ this._markInitialSteps();
82
+
83
+ // Apply default filter to show only level 0 steps by default
84
+ this._applyDefaultFilter();
85
+
86
+ // Default background style for new equation steps (optional)
87
+ this.defaultEquationBackground = null;
88
+ }
89
+
90
+ /**
91
+ * @private
92
+ */
93
+ _initializeState() {
94
+ this.currentStepIndex = 0;
95
+ this.stepDescriptions = [];
96
+ this.importanceLevels = [];
97
+ this.simplificationHistory = [];
98
+ this.currentFilterLevels = [0]; // Track current filter state, default to level 0 only
99
+ }
100
+
101
+ /**
102
+ * @private
103
+ */
104
+ _initializeLayout() {
105
+ this.hideBackgroundByDefault();
106
+ this.layoutHelper = new jsvgLayoutGroup();
107
+ this.layoutHelper.setSpacer(15);
108
+ }
109
+
110
+ /**
111
+ * @private
112
+ */
113
+ _initializeNodeMap() {
114
+ this.nodeMap = new Map();
115
+ this.rebuildNodeMap();
116
+ }
117
+
118
+ /**
119
+ * @private
120
+ */
121
+ _disableContainerInteractions() {
122
+ this.svgObject.onmouseenter = null;
123
+ this.svgObject.onmouseleave = null;
124
+ this.svgObject.style.cursor = "default";
125
+ this.svgObject.onclick = null;
126
+ }
127
+
128
+ /**
129
+ * Marks initial steps in the sequence
130
+ * @private
131
+ */
132
+ _markInitialSteps() {
133
+ if (!this.steps || !Array.isArray(this.steps)) return;
134
+
135
+ this.steps.forEach((step, index) => {
136
+ if (step instanceof omdEquationNode) {
137
+ // Mark property for filtering
138
+ step.stepMark = 0;
139
+ }
140
+ });
141
+ // Don't apply filtering here - let it happen naturally when needed
142
+ }
143
+
144
+ /**
145
+ * Applies the default filter (level 0) automatically
146
+ * @private
147
+ */
148
+ _applyDefaultFilter() {
149
+ // Only apply filter if we have steps and the steps array is properly initialized
150
+ if (this.steps && Array.isArray(this.steps) && this.steps.length > 0) {
151
+ this.updateStepsVisibility(step => (step.stepMark ?? 0) === 0);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Gets the last equation in the sequence (the current working equation)
157
+ * @returns {omdEquationNode|null} The last equation, or null if no equations exist
158
+ */
159
+ getCurrentEquation() {
160
+ if (!this.steps || this.steps.length === 0) return null;
161
+
162
+ // Find the last equation in the sequence
163
+ for (let i = this.steps.length - 1; i >= 0; i--) {
164
+ if (this.steps[i] instanceof omdEquationNode) {
165
+ return this.steps[i];
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+
171
+ /**
172
+ * Adds a new step to the sequence.
173
+ * Can be called with multiple signatures:
174
+ * - addStep(omdNode, optionsObject)
175
+ * - addStep(omdNode, description, importance)
176
+ * - addStep(string, ...)
177
+ * @param {omdNode|string} step - The node object or expression string for the step.
178
+ * @param {Object|string} [descriptionOrOptions] - An options object or a description string.
179
+ * @param {number} [importance] - The importance level (0, 1, 2) if using string description.
180
+ * @returns {number} The index of the added step.
181
+ */
182
+ addStep(step, descriptionOrOptions, importance) {
183
+ let options = {};
184
+ if (typeof descriptionOrOptions === 'string') {
185
+ options = { description: descriptionOrOptions, stepMark: importance ?? 2 };
186
+ } else if (descriptionOrOptions) {
187
+ options = descriptionOrOptions;
188
+ }
189
+
190
+ const stepNode = (typeof step === 'string') ? this._stringToNode(step) : step;
191
+ const stepIndex = this.steps.length;
192
+
193
+ // Store metadata
194
+ if (options.description !== undefined) {
195
+ this.stepDescriptions[stepIndex] = options.description;
196
+ }
197
+ if (options.stepMark !== undefined) {
198
+ this.importanceLevels[stepIndex] = options.stepMark;
199
+ } else {
200
+ this.importanceLevels[stepIndex] = 0;
201
+ }
202
+
203
+ // Add node to sequence
204
+ // Apply default equation background styling before initialization so layout includes padding
205
+ if (this.defaultEquationBackground && typeof stepNode?.setBackgroundStyle === 'function') {
206
+ stepNode.setBackgroundStyle(this.defaultEquationBackground);
207
+ }
208
+ stepNode.setFontSize(this.getFontSize());
209
+ stepNode.initialize();
210
+ this.steps.push(stepNode);
211
+ this.addChild(stepNode);
212
+ this.argumentNodeList.steps = this.steps;
213
+ this.rebuildNodeMap();
214
+
215
+ // Persist stepMark on the node for filtering
216
+ if (stepNode instanceof omdEquationNode) {
217
+ stepNode.stepMark = options.stepMark ?? this._determineStepMark(stepNode, options);
218
+ } else if (options.stepMark !== undefined) {
219
+ stepNode.stepMark = options.stepMark;
220
+ } else {
221
+ stepNode.stepMark = 0;
222
+ }
223
+
224
+ // Refresh layout and display
225
+ this.computeDimensions();
226
+ this.updateLayout();
227
+ if (window.refreshDisplayAndFilters) {
228
+ window.refreshDisplayAndFilters();
229
+ }
230
+
231
+ // Reapply current filter to maintain filter state
232
+ this._reapplyCurrentFilter();
233
+
234
+ return stepIndex;
235
+ }
236
+
237
+ /**
238
+ * Sets a default background style to be applied to all equation steps added thereafter.
239
+ * Also applies it to existing steps immediately.
240
+ * @param {{ backgroundColor?: string, cornerRadius?: number, pill?: boolean, padding?: number|{x:number,y:number} }} style
241
+ */
242
+ setDefaultEquationBackground(style = null) {
243
+ this.defaultEquationBackground = style;
244
+ if (style) {
245
+ (this.steps || []).forEach(step => {
246
+ if (typeof step?.setBackgroundStyle === 'function') {
247
+ step.setBackgroundStyle(style);
248
+ }
249
+ });
250
+ this.computeDimensions();
251
+ this.updateLayout();
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Determines the appropriate step mark for a step
257
+ * @param {omdNode} step - The step being added
258
+ * @param {Object} options - Options passed to addStep
259
+ * @returns {number} The step mark (0, 1, or 2)
260
+ * @private
261
+ */
262
+ _determineStepMark(step, options) {
263
+ // If this is called from applyEquationOperation, it's already handled there
264
+ // For other cases, we need to determine if it's a simplification step
265
+ if (options.isSimplification) {
266
+ return 2; // Verbose simplification step
267
+ }
268
+
269
+ // Check if this appears to be a final simplified result
270
+ if (this._isFullySimplified(step)) {
271
+ return 0; // Final result
272
+ }
273
+
274
+ // Default to verbose step
275
+ return 2;
276
+ }
277
+
278
+
279
+
280
+ /**
281
+ * Checks if an equation appears to be fully simplified
282
+ * @param {omdNode} step - The step to check
283
+ * @returns {boolean} Whether the step appears fully simplified
284
+ * @private
285
+ */
286
+ _isFullySimplified(step) {
287
+ // This is a heuristic - in practice you might want more sophisticated logic
288
+ // For now, we'll consider it simplified if it doesn't contain complex nested operations
289
+ if (!(step instanceof omdEquationNode)) return false;
290
+
291
+ // Simple heuristic: check if both sides are relatively simple
292
+ const leftIsSimple = this._isSimpleExpression(step.left);
293
+ const rightIsSimple = this._isSimpleExpression(step.right);
294
+
295
+ return leftIsSimple && rightIsSimple;
296
+ }
297
+
298
+ /**
299
+ * Checks if an expression is simple (constant, variable, or simple operations)
300
+ * @param {omdNode} node - The node to check
301
+ * @returns {boolean} Whether the expression is simple
302
+ * @private
303
+ */
304
+ _isSimpleExpression(node) {
305
+ // This is a simplified heuristic
306
+ if (node.isConstant() || node.type === 'omdVariableNode') {
307
+ return true;
308
+ }
309
+
310
+ // Allow simple binary operations with constants/variables
311
+ if (node.type === 'omdBinaryExpressionNode') {
312
+ return this._isSimpleExpression(node.left) && this._isSimpleExpression(node.right);
313
+ }
314
+
315
+ return false;
316
+ }
317
+
318
+ /**
319
+ * Rebuilds the nodeMap to include ALL nodes from ALL steps in the sequence
320
+ * This is crucial for provenance tracking across multiple steps
321
+ */
322
+ rebuildNodeMap() {
323
+ if (!this.nodeMap) {
324
+ this.nodeMap = new Map();
325
+ }
326
+
327
+ // Don't clear the map yet - first collect all current nodes
328
+ const newNodeMap = new Map();
329
+
330
+ // Add all nodes from all steps to the new nodeMap
331
+ this.steps.forEach((step, stepIndex) => {
332
+ const stepNodes = step.findAllNodes();
333
+ stepNodes.forEach(node => {
334
+ newNodeMap.set(node.id, node);
335
+ });
336
+ });
337
+
338
+ // Also add the sequence itself
339
+ newNodeMap.set(this.id, this);
340
+
341
+ // Now preserve historical nodes that are referenced in provenance chains
342
+ this.preserveProvenanceHistory(newNodeMap);
343
+
344
+ // Replace the old nodeMap with the new one
345
+ this.nodeMap = newNodeMap;
346
+ }
347
+
348
+ /**
349
+ * Preserves historical nodes that are referenced in provenance chains
350
+ * This ensures the highlighting system can find all nodes it needs
351
+ */
352
+ preserveProvenanceHistory(newNodeMap) {
353
+ const referencedIds = this._collectAllProvenanceIds(newNodeMap);
354
+ this._preserveReferencedNodes(referencedIds, newNodeMap);
355
+ }
356
+
357
+ /** @private */
358
+ _collectAllProvenanceIds(newNodeMap) {
359
+ const referencedIds = new Set();
360
+ const processedNodes = new Set();
361
+ newNodeMap.forEach(node => this._collectNodeProvenanceIds(node, referencedIds, processedNodes));
362
+ return referencedIds;
363
+ }
364
+
365
+ /** @private */
366
+ _collectNodeProvenanceIds(node, referencedIds, processedNodes) {
367
+ if (!node || !node.id || processedNodes.has(node.id)) return;
368
+ processedNodes.add(node.id);
369
+
370
+ if (node.provenance?.length > 0) {
371
+ node.provenance.forEach(id => referencedIds.add(id));
372
+ }
373
+
374
+ if (node.argumentNodeList) {
375
+ Object.values(node.argumentNodeList).flat().forEach(child => {
376
+ this._collectNodeProvenanceIds(child, referencedIds, processedNodes);
377
+ });
378
+ }
379
+ }
380
+
381
+ /** @private */
382
+ _preserveReferencedNodes(referencedIds, newNodeMap) {
383
+ const processedIds = new Set();
384
+ referencedIds.forEach(id => this._preserveNodeAndContext(id, newNodeMap, processedIds));
385
+ }
386
+
387
+ /** @private */
388
+ _preserveNodeAndContext(id, newNodeMap, processedIds) {
389
+ if (processedIds.has(id) || newNodeMap.has(id) || !this.nodeMap?.has(id)) {
390
+ return;
391
+ }
392
+ processedIds.add(id);
393
+
394
+ const historicalNode = this.nodeMap.get(id);
395
+ newNodeMap.set(id, historicalNode);
396
+
397
+ // Preserve this node's own provenance chain
398
+ if (historicalNode.provenance?.length > 0) {
399
+ historicalNode.provenance.forEach(nestedId => {
400
+ this._preserveNodeAndContext(nestedId, newNodeMap, processedIds);
401
+ });
402
+ }
403
+
404
+ // Preserve parent and sibling context
405
+ this._preserveParentContext(historicalNode, newNodeMap);
406
+ this._preserveSiblingContext(historicalNode, newNodeMap);
407
+ }
408
+
409
+ /** @private */
410
+ _preserveParentContext(node, newNodeMap) {
411
+ let parent = node.parent;
412
+ while (parent && parent.id) {
413
+ if (!newNodeMap.has(parent.id) && this.nodeMap.has(parent.id)) {
414
+ newNodeMap.set(parent.id, this.nodeMap.get(parent.id));
415
+ }
416
+ parent = parent.parent;
417
+ }
418
+ }
419
+
420
+ /** @private */
421
+ _preserveSiblingContext(node, newNodeMap) {
422
+ if (!node.parent?.argumentNodeList) return;
423
+
424
+ Object.values(node.parent.argumentNodeList).flat().forEach(sibling => {
425
+ if (sibling && sibling.id && !newNodeMap.has(sibling.id) && this.nodeMap.has(sibling.id)) {
426
+ newNodeMap.set(sibling.id, this.nodeMap.get(sibling.id));
427
+ }
428
+ });
429
+ }
430
+
431
+ /**
432
+ * Records a simplification step in the history
433
+ * @param {string} name - The name of the simplification rule that was applied
434
+ * @param {Array<string>} affectedNodes - Array of node IDs that were affected by the simplification
435
+ * @param {string} message - Human-readable description of what was simplified
436
+ * @param {Object} [metadata={}] - Additional metadata about the simplification
437
+ */
438
+ recordSimplificationHistory(name, affectedNodes, message, metadata = {}) {
439
+ const historyEntry = {
440
+ name,
441
+ affectedNodes: [...affectedNodes], // Create a copy of the array
442
+ message,
443
+ stepNumber: this.steps.length,
444
+ ...metadata
445
+ };
446
+
447
+ this.simplificationHistory.push(historyEntry);
448
+ }
449
+
450
+ /**
451
+ * Gets the complete simplification history for this sequence
452
+ * @returns {Array<Object>} Array of simplification history entries
453
+ */
454
+ getSimplificationHistory() {
455
+ return [...this.simplificationHistory]; // Return a copy
456
+ }
457
+
458
+ /**
459
+ * Clears the simplification history
460
+ */
461
+ clearSimplificationHistory() {
462
+ this.simplificationHistory = [];
463
+ }
464
+
465
+ /**
466
+ * Override setFontSize to propagate to all steps
467
+ * @param {number} fontSize - The new font size
468
+ */
469
+ setFontSize(fontSize) {
470
+ super.setFontSize(fontSize);
471
+
472
+ // Propagate the font size to all existing steps
473
+ this.steps.forEach(step => {
474
+ step.setFontSize(fontSize);
475
+ });
476
+
477
+ // Recompute dimensions and layout with the new font size
478
+ this.computeDimensions();
479
+ this.updateLayout();
480
+ }
481
+
482
+ /**
483
+ * Applies a specified operation to the current equation in the sequence and adds the result as a new step.
484
+ * @param {number|string} value - The constant value or expression string to apply.
485
+ * @param {string} operation - The operation name ('add', 'subtract', 'multiply', 'divide').
486
+ * @returns {omdEquationSequenceNode} Returns this sequence for chaining.
487
+ */
488
+ applyEquationOperation(value, operation) {
489
+ if (!omdEquationSequenceNode.OPERATION_MAP[operation]) {
490
+ console.error(`Invalid operation: ${operation}`);
491
+ return this;
492
+ }
493
+
494
+ const currentEquation = this.getCurrentEquation();
495
+ if (!currentEquation) {
496
+ console.error("No equation to apply operation to.");
497
+ return this;
498
+ }
499
+
500
+ let operationValue = value;
501
+ if (typeof value === 'string') {
502
+ if (!window.math) throw new Error("Math.js is required for parsing expressions");
503
+ operationValue = isNaN(value) ? window.math.parse(value) : parseFloat(value);
504
+ }
505
+
506
+ // Step 1: Add visual operation display
507
+ const operationDisplay = new omdOperationDisplayNode(operation, value);
508
+ this.addStep(operationDisplay, { stepMark: 0 });
509
+
510
+ // Step 2: Apply operation to a clone of the equation
511
+ const clonedEquation = currentEquation.clone();
512
+ const equationMethod = omdEquationSequenceNode.OPERATION_MAP[operation];
513
+ const unsimplifiedEquation = clonedEquation[equationMethod](operationValue, operationDisplay.id);
514
+
515
+ // Step 3: Check simplification potential and add the new equation step
516
+ const testClone = unsimplifiedEquation.clone();
517
+ const { foldedCount } = simplifyStep(testClone);
518
+ const isSimplified = foldedCount === 0;
519
+
520
+ this.addStep(unsimplifiedEquation, {
521
+ stepMark: isSimplified ? 0 : 1,
522
+ description: this._getOperationDescription(operation, value, !isSimplified)
523
+ });
524
+
525
+ return this;
526
+ }
527
+
528
+ /**
529
+ * Generates a description for an equation operation.
530
+ * @param {string} operation - The operation name.
531
+ * @param {number|string} value - The value used in the operation.
532
+ * @param {boolean} isUnsimplified - Whether the result is unsimplified.
533
+ * @returns {string} The formatted description.
534
+ * @private
535
+ */
536
+ _getOperationDescription(operation, value, isUnsimplified) {
537
+ const templates = {
538
+ 'add': `Added ${value} to both sides`,
539
+ 'subtract': `Subtracted ${value} from both sides`,
540
+ 'multiply': `Multiplied both sides by ${value}`,
541
+ 'divide': `Divided both sides by ${value}`
542
+ };
543
+ const baseDescription = templates[operation] || `Applied ${operation} with ${value}`;
544
+ return isUnsimplified ? `${baseDescription} (unsimplified)` : baseDescription;
545
+ }
546
+
547
+ /**
548
+ * Applies a function to both sides of the current equation in the sequence and adds the result as a new step.
549
+ * @param {string} functionName - The name of the function to apply.
550
+ * @returns {omdEquationSequenceNode} Returns this sequence for chaining.
551
+ */
552
+ applyEquationFunction(functionName) {
553
+ const currentEquation = this.getCurrentEquation();
554
+ if (!currentEquation) {
555
+ throw new Error("No equation found in sequence to operate on");
556
+ }
557
+
558
+ // Clone the current equation
559
+ const clonedEquation = currentEquation.clone();
560
+
561
+ // Apply the function to the clone
562
+ const newEquation = clonedEquation.applyFunction(functionName);
563
+
564
+ // Check if any simplifications are possible on this new step
565
+ const testClone = newEquation.clone();
566
+ const { foldedCount } = simplifyStep(testClone);
567
+
568
+ // Determine the appropriate step mark based on simplification potential
569
+ const stepMark = foldedCount === 0 ? 0 : 1;
570
+ const description = `Applied ${functionName} to both sides`;
571
+
572
+ this.addStep(newEquation, {
573
+ stepMark: stepMark,
574
+ description: description
575
+ });
576
+
577
+ return this;
578
+ }
579
+
580
+
581
+ /**
582
+ * Simplifies the current step in the sequence by applying one round of simplification rules
583
+ * @returns {Object} Result object containing:
584
+ * @returns {boolean} result.success - Whether any simplification was applied
585
+ * @returns {number} result.foldedCount - Number of simplification operations applied (0 if none)
586
+ * @returns {boolean} result.isFinalSimplification - Whether this represents the final simplified form
587
+ * @returns {string} result.message - Human-readable description of the result
588
+ */
589
+ simplify() {
590
+ console.log(`🔍 simplify called`);
591
+
592
+ const currentStep = this.steps[this.steps.length - 1];
593
+ if (!currentStep) {
594
+ console.log(`❌ No current step found`);
595
+ return { success: false, message: 'No expression found to simplify' };
596
+ }
597
+
598
+ console.log(`📋 Current step:`, {
599
+ type: currentStep.type,
600
+ constructorName: currentStep.type,
601
+ toString: currentStep.toString()
602
+ });
603
+
604
+ try {
605
+ const stepToSimplify = currentStep.clone();
606
+ console.log(`🔄 Cloned step for simplification:`, {
607
+ type: stepToSimplify.type,
608
+ constructorName: stepToSimplify.type,
609
+ toString: stepToSimplify.toString()
610
+ });
611
+
612
+ const simplificationResult = simplifyStep(stepToSimplify);
613
+ console.log(`📊 simplifyStep result:`, simplificationResult);
614
+
615
+ if (simplificationResult.foldedCount > 0) {
616
+ console.log(`✅ Simplification successful, handling result...`);
617
+ return this._handleSuccessfulSimplification(currentStep, simplificationResult);
618
+ } else {
619
+ console.log(`❌ No simplifications applied`);
620
+ return { success: false, foldedCount: 0, message: 'No simplifications available' };
621
+ }
622
+ } catch (error) {
623
+ console.error(`❌ Error during simplification:`, error);
624
+ return { success: false, message: `Simplification error: ${error.message}` };
625
+ }
626
+ }
627
+
628
+ /** @private */
629
+ _handleSuccessfulSimplification(originalStep, { newRoot, foldedCount, historyEntry }) {
630
+ if (historyEntry) {
631
+ historyEntry.stepNumber = this.steps.length - 1;
632
+ historyEntry.originalStep = originalStep.toString();
633
+ this.simplificationHistory.push(historyEntry);
634
+ }
635
+
636
+ const testClone = newRoot.clone();
637
+ const { foldedCount: moreFolds } = simplifyStep(testClone);
638
+ const isFinal = moreFolds === 0;
639
+
640
+ const description = isFinal
641
+ ? `Fully simplified result (${foldedCount} operation${foldedCount > 1 ? 's' : ''} applied)`
642
+ : `Simplification step (${foldedCount} operation${foldedCount > 1 ? 's' : ''} applied)`;
643
+
644
+ this.addStep(newRoot, {
645
+ stepMark: isFinal ? 0 : 2,
646
+ description: description,
647
+ isSimplification: true,
648
+ });
649
+
650
+ const message = isFinal
651
+ ? `Fully simplified! Applied ${foldedCount} simplification step(s).`
652
+ : `Simplified! Applied ${foldedCount} simplification step(s), more are available.`;
653
+
654
+ return { success: true, foldedCount, isFinalSimplification: isFinal, message };
655
+ }
656
+
657
+ /**
658
+ * Simplifies all possible expressions until no more simplifications can be applied
659
+ * Repeatedly calls simplify() until no further simplifications are possible
660
+ * @param {number} [maxIterations=50] - Maximum number of iterations to prevent infinite loops
661
+ * @returns {Object} Result object containing:
662
+ * @returns {boolean} result.success - Whether the operation completed successfully (false if stopped due to max iterations)
663
+ * @returns {number} result.totalSteps - Number of simplification steps that were added to the sequence
664
+ * @returns {number} result.iterations - Number of simplify() calls made during the process
665
+ * @returns {string} result.message - Human-readable description of the final result
666
+ */
667
+ simplifyAll(maxIterations = 50) {
668
+ console.log(`🚀 simplifyAll called with maxIterations: ${maxIterations}`);
669
+ console.log(`📋 Current steps:`, {
670
+ stepsLength: this.steps.length,
671
+ steps: this.steps.map((step, i) => ({
672
+ index: i,
673
+ type: step.type,
674
+ constructorName: step.type,
675
+ toString: step.toString()
676
+ }))
677
+ });
678
+
679
+ let iteration = 0;
680
+ let stepsBefore;
681
+ let totalSteps = 0;
682
+
683
+ do {
684
+ stepsBefore = this.steps.length;
685
+ console.log(`🔄 Iteration ${iteration + 1}: starting with ${stepsBefore} steps`);
686
+
687
+ const result = this.simplify();
688
+ console.log(`📊 Iteration ${iteration + 1} result:`, result);
689
+
690
+ if (result.success) {
691
+ totalSteps++;
692
+ console.log(`✅ Iteration ${iteration + 1} successful, totalSteps: ${totalSteps}`);
693
+ } else {
694
+ console.log(`❌ Iteration ${iteration + 1} failed: ${result.message}`);
695
+ }
696
+
697
+ iteration++;
698
+ } while (this.steps.length > stepsBefore && iteration < maxIterations);
699
+
700
+ if (iteration >= maxIterations) {
701
+ console.log(`⚠️ Stopped after ${maxIterations} iterations to avoid infinite loop`);
702
+ return {
703
+ success: false,
704
+ totalSteps,
705
+ iterations: iteration,
706
+ message: `Stopped after ${maxIterations} iterations to avoid an infinite loop.`
707
+ };
708
+ } else {
709
+ console.log(`✅ simplifyAll completed successfully:`, {
710
+ totalSteps,
711
+ iterations: iteration,
712
+ finalStepsLength: this.steps.length
713
+ });
714
+ return {
715
+ success: true,
716
+ totalSteps,
717
+ iterations: iteration,
718
+ message: `All possible simplifications completed. Added ${totalSteps} simplification steps.`
719
+ };
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Evaluates the current step in the sequence with the given variables.
725
+ * Logs the result to the console.
726
+ * @param {Object} variables - A map of variable names to their numeric values.
727
+ */
728
+ evaluate(variables = {}) {
729
+ const targetNode = this.getCurrentStep();
730
+ if (!targetNode || typeof targetNode.evaluate !== 'function') {
731
+ console.warn("Evaluation not supported for the current step.");
732
+ return;
733
+ }
734
+
735
+ try {
736
+ const result = targetNode.evaluate(variables);
737
+
738
+ if (typeof result === 'object' && result.left !== undefined && result.right !== undefined) {
739
+ const { left, right } = result;
740
+ const isEqual = Math.abs(left - right) < 1e-9;
741
+ console.log(`Evaluation: ${Number(left.toFixed(4))} = ${Number(right.toFixed(4))}. Result: ${isEqual}`);
742
+ } else {
743
+ console.log(`Evaluation result: ${result}`);
744
+ }
745
+ } catch (error) {
746
+ console.error("Evaluation failed:", error.message);
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Validates the provenance integrity across all steps in the sequence
752
+ * @returns {Array} Array of validation issues found
753
+ */
754
+ validateSequenceProvenance() {
755
+ const issues = [];
756
+ this._validateStepsProvenance(issues);
757
+ this._findOrphanedNodes(issues);
758
+ return issues;
759
+ }
760
+
761
+ /** @private */
762
+ _validateStepsProvenance(issues) {
763
+ this.steps.forEach((step, index) => {
764
+ const stepIssues = step.validateProvenance(this.nodeMap);
765
+ stepIssues.forEach(issue => issues.push({ ...issue, stepIndex: index }));
766
+ });
767
+ }
768
+
769
+ /** @private */
770
+ _findOrphanedNodes(issues) {
771
+ const currentNodeIds = new Set(this.steps.flatMap(step => step.findAllNodes().map(n => n.id)));
772
+ const allProvenanceIds = this._collectAllProvenanceIds(this.nodeMap);
773
+
774
+ this.nodeMap.forEach((node, id) => {
775
+ if (!currentNodeIds.has(id) && !allProvenanceIds.has(id)) {
776
+ issues.push({
777
+ type: 'orphaned_node',
778
+ nodeId: id,
779
+ nodeType: node.type,
780
+ });
781
+ }
782
+ });
783
+ }
784
+
785
+ /**
786
+ * Overrides the default select behavior to prevent the container from highlighting.
787
+ * This container should be inert and not react to selection events.
788
+ */
789
+ select() {
790
+ }
791
+
792
+ /**
793
+ * Overrides the default deselect behavior to prevent the container from highlighting.
794
+ */
795
+ deselect() {
796
+ }
797
+
798
+ /**
799
+ * Override highlight to prevent the sequence container itself from highlighting
800
+ * but still allow children to be highlighted
801
+ */
802
+ highlight(color) {
803
+ // Don't highlight the sequence container itself
804
+ // Just propagate to children (but not the backRect)
805
+ this.childList.forEach((child) => {
806
+ if (child instanceof omdMetaExpression && child !== this.backRect) {
807
+ child.highlight(color);
808
+ }
809
+ });
810
+ }
811
+
812
+ /**
813
+ * Override clearProvenanceHighlights to work with the sequence
814
+ */
815
+ clearProvenanceHighlights() {
816
+ // Don't change the sequence container's background
817
+ // Just clear highlights from children
818
+ this.childList.forEach((child) => {
819
+ if (child instanceof omdMetaExpression && typeof child.clearProvenanceHighlights === 'function' && child !== this.backRect) {
820
+ child.clearProvenanceHighlights();
821
+ }
822
+ });
823
+ }
824
+
825
+ /**
826
+ * Calculates the dimensions of the entire calculation block.
827
+ * It determines the correct alignment for all equals signs and calculates
828
+ * the total width and height required.
829
+ * @override
830
+ */
831
+ computeDimensions() {
832
+ const visibleSteps = this.steps.filter(s => s.visible !== false);
833
+ if (visibleSteps.length === 0) {
834
+ this.setWidthAndHeight(0, 0);
835
+ return;
836
+ }
837
+
838
+ visibleSteps.forEach(step => step.computeDimensions());
839
+
840
+ this.alignPointX = this._calculateAlignmentPoint(visibleSteps);
841
+
842
+ const { maxWidth, totalHeight } = this._calculateTotalDimensions(visibleSteps);
843
+ this.setWidthAndHeight(maxWidth, totalHeight);
844
+ }
845
+
846
+ /** @private */
847
+ _calculateAlignmentPoint(visibleSteps) {
848
+ const equalsCenters = [];
849
+ visibleSteps.forEach(step => {
850
+ if (step instanceof omdEquationNode) {
851
+ if (typeof step.getEqualsAnchorX === 'function') {
852
+ equalsCenters.push(step.getEqualsAnchorX());
853
+ } else if (step.equalsSign && step.left) {
854
+ const spacing = 8 * step.getFontSize() / step.getRootFontSize();
855
+ equalsCenters.push(step.left.width + spacing + (step.equalsSign.width / 2));
856
+ }
857
+ }
858
+ });
859
+ return equalsCenters.length > 0 ? Math.max(...equalsCenters) : 0;
860
+ }
861
+
862
+ /** @private */
863
+ _calculateTotalDimensions(visibleSteps) {
864
+ let maxWidth = 0;
865
+ let totalHeight = 0;
866
+ const verticalPadding = 15 * this.getFontSize() / this.getRootFontSize();
867
+
868
+ visibleSteps.forEach((step, index) => {
869
+ let stepWidth = 0;
870
+ if (step instanceof omdEquationNode) {
871
+ stepWidth = this.alignPointX + step.equalsSign.width + step.right.width;
872
+ } else {
873
+ stepWidth = step.width;
874
+ }
875
+
876
+ maxWidth = Math.max(maxWidth, stepWidth);
877
+ totalHeight += step.height;
878
+ if (index < visibleSteps.length - 1) {
879
+ totalHeight += verticalPadding;
880
+ }
881
+ });
882
+
883
+ return { maxWidth, totalHeight };
884
+ }
885
+
886
+ /**
887
+ * Computes the horizontal offset needed to align a step with the master equals anchor.
888
+ * Equations align their equals sign center to alignPointX; operation displays align their
889
+ * virtual equals (middle of the gap); other steps are centered within the sequence width.
890
+ * @param {omdNode} step
891
+ * @returns {number} x offset in local coordinates
892
+ * @private
893
+ */
894
+ _computeStepXOffset(step) {
895
+ if (step instanceof omdEquationNode) {
896
+ const equalsAnchorX = (typeof step.getEqualsAnchorX === 'function') ? step.getEqualsAnchorX() : step.left.width;
897
+ return this.alignPointX - equalsAnchorX;
898
+ }
899
+ if (step instanceof omdOperationDisplayNode) {
900
+ const leftWidth = (typeof step.getLeftWidthForAlignment === 'function')
901
+ ? step.getLeftWidthForAlignment()
902
+ : step.width / 2;
903
+ const halfGap = (typeof step.gap === 'number' ? step.gap : 0) / 2;
904
+ return this.alignPointX - (leftWidth + halfGap);
905
+ }
906
+ return (this.width - step.width) / 2;
907
+ }
908
+
909
+ /**
910
+ * Updates the layout of the calculation block.
911
+ * This method positions each equation vertically and aligns their
912
+ * equals signs to the calculated alignment point.
913
+ * @override
914
+ */
915
+ updateLayout() {
916
+ const verticalPadding = 15 * this.getFontSize() / this.getRootFontSize();
917
+ const visibleSteps = this.steps.filter(s => s.visible !== false);
918
+
919
+ visibleSteps.forEach(step => step.updateLayout());
920
+
921
+ this.alignPointX = this._calculateAlignmentPoint(visibleSteps);
922
+
923
+ let yCurrent = 0;
924
+ visibleSteps.forEach((step, index) => {
925
+ const xOffset = this._computeStepXOffset(step);
926
+ step.setPosition(xOffset, yCurrent);
927
+
928
+ yCurrent += step.height;
929
+ if (index < visibleSteps.length - 1) yCurrent += verticalPadding;
930
+ });
931
+ }
932
+
933
+ /**
934
+ * Creates an omdEquationSequenceNode instance from an array of strings.
935
+ * @param {Array<string>} stepStrings - An array of strings, each representing a calculation step.
936
+ * @returns {omdEquationSequenceNode} A new instance of omdEquationSequenceNode.
937
+ */
938
+ static fromStringArray(stepStrings) {
939
+ const stepNodes = stepStrings.map(str => {
940
+ const trimmedStr = str.trim();
941
+ // If the string contains an equals sign, parse it as a full equation.
942
+ if (trimmedStr.includes('=')) {
943
+ return omdEquationNode.fromString(trimmedStr);
944
+ }
945
+
946
+ // If it doesn't contain an equals sign, it's not a valid equation step for a sequence.
947
+ throw new Error(`Step string "${trimmedStr}" is not a valid equation for omdEquationSequenceNode.`);
948
+ });
949
+ return new omdEquationSequenceNode(stepNodes);
950
+ }
951
+
952
+ clone() {
953
+ const clonedSteps = this.steps.map(step => step.clone());
954
+ const clone = new omdEquationSequenceNode(clonedSteps);
955
+
956
+ // The crucial step: link the clone to its origin (following the pattern from omdNode)
957
+ clone.provenance.push(this.id);
958
+
959
+ // The clone gets a fresh nodeMap, as its history is self-contained
960
+ clone.nodeMap = new Map();
961
+ clone.findAllNodes().forEach(node => clone.nodeMap.set(node.id, node));
962
+ return clone;
963
+ }
964
+
965
+ /**
966
+ * Converts the omdEquationSequenceNode to a math.js AST node.
967
+ * Since sequences are containers, we return a custom representation.
968
+ * @returns {Object} A custom AST node representing the sequence.
969
+ */
970
+ toMathJSNode() {
971
+ const astNode = this.steps[this.steps.length-1].toMathJSNode();
972
+
973
+ return astNode;
974
+ }
975
+
976
+ /**
977
+ * Get the current step node
978
+ * @returns {omdNode} The current step
979
+ */
980
+ getCurrentStep() {
981
+ // No steps → no current step
982
+ if (!this.steps || this.steps.length === 0) return null;
983
+
984
+ // Prefer the bottom-most VISIBLE equation step, falling back gracefully
985
+ let chosenIndex = -1;
986
+ for (let i = this.steps.length - 1; i >= 0; i--) {
987
+ const step = this.steps[i];
988
+ if (!step) continue;
989
+ // If visibility is explicitly false, skip
990
+ if (step.visible === false) continue;
991
+ // Prefer equation nodes when present
992
+ if (step.constructor?.name === 'omdEquationNode') {
993
+ chosenIndex = i;
994
+ break;
995
+ }
996
+ // Remember last visible non-equation as a fallback if no equation exists
997
+ if (chosenIndex === -1) chosenIndex = i;
998
+ }
999
+
1000
+ if (chosenIndex === -1) {
1001
+ // If everything is hidden or invalid, fall back to the last step
1002
+ chosenIndex = this.steps.length - 1;
1003
+ }
1004
+
1005
+ // Clamp and store
1006
+ if (chosenIndex < 0) chosenIndex = 0;
1007
+ if (chosenIndex >= this.steps.length) chosenIndex = this.steps.length - 1;
1008
+ this.currentStepIndex = chosenIndex;
1009
+ return this.steps[chosenIndex];
1010
+ }
1011
+
1012
+ /**
1013
+ * Navigate to a specific step
1014
+ * @param {number} index - The step index to navigate to
1015
+ * @returns {boolean} Whether navigation was successful
1016
+ */
1017
+ navigateToStep(index) {
1018
+ if (index < 0 || index >= this.steps.length) {
1019
+ return false;
1020
+ }
1021
+ this.currentStepIndex = index;
1022
+
1023
+ // Trigger any UI updates if needed
1024
+ if (window.refreshDisplayAndFilters) {
1025
+ window.refreshDisplayAndFilters();
1026
+ }
1027
+
1028
+ return true;
1029
+ }
1030
+
1031
+ /**
1032
+ * Navigate to the next step
1033
+ * @returns {boolean} Whether there was a next step
1034
+ */
1035
+ nextStep() {
1036
+ if (this.currentStepIndex < this.steps.length - 1) {
1037
+ this.currentStepIndex++;
1038
+
1039
+ // Trigger any UI updates if needed
1040
+ if (window.refreshDisplayAndFilters) {
1041
+ window.refreshDisplayAndFilters();
1042
+ }
1043
+
1044
+ return true;
1045
+ }
1046
+ return false;
1047
+ }
1048
+
1049
+ /**
1050
+ * Navigate to the previous step
1051
+ * @returns {boolean} Whether there was a previous step
1052
+ */
1053
+ previousStep() {
1054
+ if (this.currentStepIndex > 0) {
1055
+ this.currentStepIndex--;
1056
+
1057
+ // Trigger any UI updates if needed
1058
+ if (window.refreshDisplayAndFilters) {
1059
+ window.refreshDisplayAndFilters();
1060
+ }
1061
+
1062
+ return true;
1063
+ }
1064
+ return false;
1065
+ }
1066
+
1067
+ /**
1068
+ * Get steps filtered by importance level
1069
+ * @param {number} maxImportance - Maximum importance level to include (0, 1, or 2)
1070
+ * @returns {Object[]} Array of objects containing step, description, importance, and index
1071
+ */
1072
+ getFilteredSteps(maxImportance) {
1073
+ const filteredSteps = [];
1074
+
1075
+ this.steps.forEach((step, index) => {
1076
+ const importance = this.importanceLevels[index] !== undefined ? this.importanceLevels[index] :
1077
+ (step.stepMark !== undefined ? step.stepMark : 0);
1078
+
1079
+ if (importance <= maxImportance) {
1080
+ filteredSteps.push({
1081
+ step: step,
1082
+ description: this.stepDescriptions[index] || '',
1083
+ importance: importance,
1084
+ index: index
1085
+ });
1086
+ }
1087
+ });
1088
+
1089
+ return filteredSteps;
1090
+ }
1091
+
1092
+ /**
1093
+ * Renders only the current step
1094
+ * @returns {SVGElement} The current step's rendering
1095
+ */
1096
+ renderCurrentStep() {
1097
+ const currentStep = this.getCurrentStep();
1098
+ if (!currentStep) {
1099
+ // Return empty SVG group if no current step
1100
+ const emptyGroup = new jsvgGroup();
1101
+ return emptyGroup.svgObject;
1102
+ }
1103
+
1104
+ // Create a temporary container to render just the current step
1105
+ const tempContainer = new jsvgGroup();
1106
+
1107
+ // Compute dimensions and render the current step
1108
+ currentStep.computeDimensions();
1109
+ currentStep.updateLayout();
1110
+ const stepRendering = currentStep.render();
1111
+
1112
+ tempContainer.addChild(stepRendering);
1113
+ return tempContainer.svgObject;
1114
+ }
1115
+
1116
+ /**
1117
+ * Convert the entire sequence to a string
1118
+ * @returns {string} Multi-line string of all steps
1119
+ */
1120
+ toString() {
1121
+ if (this.steps.length === 0) {
1122
+ return '';
1123
+ }
1124
+ return this.steps.map((step, index) => {
1125
+ const description = this.stepDescriptions[index] ? ` (${this.stepDescriptions[index]})` : '';
1126
+ return `Step ${index + 1}: ${step.toString()}${description}`;
1127
+ }).join('\\n');
1128
+ }
1129
+
1130
+ /**
1131
+ * Clear all steps from the sequence
1132
+ */
1133
+ clear() {
1134
+ // Remove all children
1135
+ this.steps.forEach(step => {
1136
+ this.removeChild(step);
1137
+ });
1138
+
1139
+ // Clear arrays
1140
+ this.steps = [];
1141
+ this.stepDescriptions = [];
1142
+ this.importanceLevels = [];
1143
+ this.argumentNodeList.steps = [];
1144
+ this.currentStepIndex = 0;
1145
+
1146
+ // Clear history
1147
+ this.clearSimplificationHistory();
1148
+
1149
+ // Rebuild node map
1150
+ this.rebuildNodeMap();
1151
+
1152
+ // Update dimensions
1153
+ this.computeDimensions();
1154
+ this.updateLayout();
1155
+
1156
+ // Trigger any UI updates if needed
1157
+ if (window.refreshDisplayAndFilters) {
1158
+ window.refreshDisplayAndFilters();
1159
+ }
1160
+ }
1161
+
1162
+ /**
1163
+ * Create a sequence from an array of expressions
1164
+ * @param {string[]} stepsArray - Array of expression strings
1165
+ * @returns {omdEquationSequenceNode} A new sequence node
1166
+ * @static
1167
+ */
1168
+ static fromSteps(stepsArray) {
1169
+ if (!Array.isArray(stepsArray)) {
1170
+ throw new Error('fromSteps requires an array of expression strings');
1171
+ }
1172
+
1173
+ const sequence = new omdEquationSequenceNode([]);
1174
+
1175
+ stepsArray.forEach((stepStr, index) => {
1176
+ const trimmedStr = stepStr.trim();
1177
+ let stepNode;
1178
+
1179
+ // If the string contains an equals sign, parse it as an equation
1180
+ if (trimmedStr.includes('=')) {
1181
+ stepNode = omdEquationNode.fromString(trimmedStr);
1182
+ } else {
1183
+ // Otherwise, parse it as a general expression
1184
+ if (!window.math) {
1185
+ throw new Error("Math.js is required for parsing expressions");
1186
+ }
1187
+
1188
+ const ast = window.math.parse(trimmedStr);
1189
+ const NodeType = getNodeForAST(ast);
1190
+ stepNode = new NodeType(ast);
1191
+ }
1192
+
1193
+ // Add the step with default importance
1194
+ sequence.addStep(stepNode, {
1195
+ stepMark: 0, // Default to major step
1196
+ description: ''
1197
+ });
1198
+ });
1199
+
1200
+ return sequence;
1201
+ }
1202
+
1203
+ /**
1204
+ * Converts an expression string into a proper omdNode.
1205
+ * @param {string} str - The expression string.
1206
+ * @returns {omdNode} The corresponding node.
1207
+ * @private
1208
+ */
1209
+ _stringToNode(str) {
1210
+ const trimmedStr = str.trim();
1211
+ if (trimmedStr.includes('=')) {
1212
+ return omdEquationNode.fromString(trimmedStr);
1213
+ }
1214
+
1215
+ if (!window.math) {
1216
+ throw new Error("Math.js is required for parsing expressions");
1217
+ }
1218
+
1219
+ const ast = window.math.parse(trimmedStr);
1220
+ const NodeType = getNodeForAST(ast);
1221
+ return new NodeType(ast);
1222
+ }
1223
+
1224
+ show() {
1225
+ super.show();
1226
+ if (this.layoutManager) {
1227
+ this.layoutManager.updateVisualVisibility();
1228
+ }
1229
+ }
1230
+
1231
+ hide() {
1232
+ super.hide();
1233
+ if (this.layoutManager) {
1234
+ this.layoutManager.updateVisualVisibility();
1235
+ }
1236
+ }
1237
+
1238
+ /**
1239
+ * Updates visibility of multiple steps at once
1240
+ * @param {Function} visibilityPredicate Function that takes a step and returns true if it should be visible
1241
+ */
1242
+ updateStepsVisibility(visibilityPredicate) {
1243
+ // Safety check - ensure steps array exists and is properly initialized
1244
+ if (!this.steps || !Array.isArray(this.steps)) {
1245
+ return;
1246
+ }
1247
+
1248
+ this.steps.forEach(step => {
1249
+ if (!step) return; // Skip null/undefined steps
1250
+
1251
+ if (visibilityPredicate(step)) {
1252
+ step.visible = true;
1253
+ if (step.svgObject) step.svgObject.style.display = '';
1254
+ } else {
1255
+ step.visible = false;
1256
+ if (step.svgObject) step.svgObject.style.display = 'none';
1257
+ }
1258
+
1259
+ // Apply font weight based on stepMark
1260
+ const weight = getFontWeight(step.stepMark ?? 0);
1261
+ if (step.svgObject) {
1262
+ step.svgObject.style.fontWeight = weight.toString();
1263
+ }
1264
+ });
1265
+
1266
+ if (this.layoutManager) {
1267
+ this.layoutManager.updateVisualVisibility();
1268
+ }
1269
+
1270
+ this.computeDimensions();
1271
+ this.updateLayout();
1272
+ }
1273
+ }