@teachinglab/omd 0.6.0 → 0.6.2

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 (198) hide show
  1. package/README.md +257 -251
  2. package/README.old.md +137 -137
  3. package/canvas/core/canvasConfig.js +202 -202
  4. package/canvas/drawing/segment.js +167 -167
  5. package/canvas/drawing/stroke.js +385 -385
  6. package/canvas/events/eventManager.js +444 -444
  7. package/canvas/events/pointerEventHandler.js +262 -262
  8. package/canvas/index.js +48 -48
  9. package/canvas/tools/PointerTool.js +71 -71
  10. package/canvas/tools/tool.js +222 -222
  11. package/canvas/utils/boundingBox.js +377 -377
  12. package/canvas/utils/mathUtils.js +258 -258
  13. package/docs/api/configuration-options.md +198 -198
  14. package/docs/api/eventManager.md +82 -82
  15. package/docs/api/focusFrameManager.md +144 -144
  16. package/docs/api/index.md +105 -105
  17. package/docs/api/main.md +62 -62
  18. package/docs/api/omdBinaryExpressionNode.md +86 -86
  19. package/docs/api/omdCanvas.md +83 -83
  20. package/docs/api/omdConfigManager.md +112 -112
  21. package/docs/api/omdConstantNode.md +52 -52
  22. package/docs/api/omdDisplay.md +87 -87
  23. package/docs/api/omdEquationNode.md +174 -174
  24. package/docs/api/omdEquationSequenceNode.md +258 -258
  25. package/docs/api/omdEquationStack.md +192 -192
  26. package/docs/api/omdFunctionNode.md +82 -82
  27. package/docs/api/omdGroupNode.md +78 -78
  28. package/docs/api/omdHelpers.md +87 -87
  29. package/docs/api/omdLeafNode.md +85 -85
  30. package/docs/api/omdNode.md +201 -201
  31. package/docs/api/omdOperationDisplayNode.md +117 -117
  32. package/docs/api/omdOperatorNode.md +91 -91
  33. package/docs/api/omdParenthesisNode.md +133 -133
  34. package/docs/api/omdPopup.md +191 -191
  35. package/docs/api/omdPowerNode.md +131 -131
  36. package/docs/api/omdRationalNode.md +144 -144
  37. package/docs/api/omdSequenceNode.md +128 -128
  38. package/docs/api/omdSimplification.md +78 -78
  39. package/docs/api/omdSqrtNode.md +144 -144
  40. package/docs/api/omdStepVisualizer.md +146 -146
  41. package/docs/api/omdStepVisualizerHighlighting.md +65 -65
  42. package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
  43. package/docs/api/omdStepVisualizerLayout.md +70 -70
  44. package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
  45. package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
  46. package/docs/api/omdToolbar.md +130 -130
  47. package/docs/api/omdTranscriptionService.md +95 -95
  48. package/docs/api/omdTreeDiff.md +169 -169
  49. package/docs/api/omdUnaryExpressionNode.md +137 -137
  50. package/docs/api/omdUtilities.md +82 -82
  51. package/docs/api/omdVariableNode.md +123 -123
  52. package/docs/api/selectTool.md +74 -74
  53. package/docs/api/simplificationEngine.md +97 -97
  54. package/docs/api/simplificationRules.md +76 -76
  55. package/docs/api/simplificationUtils.md +64 -64
  56. package/docs/api/transcribe.md +43 -43
  57. package/docs/api-reference.md +85 -85
  58. package/docs/index.html +453 -453
  59. package/docs/index.md +38 -38
  60. package/docs/omd-objects.md +258 -258
  61. package/index.js +79 -79
  62. package/jsvg/index.js +3 -0
  63. package/jsvg/jsvg.js +898 -898
  64. package/jsvg/jsvgComponents.js +357 -358
  65. package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
  66. package/npm-docs/README.md +251 -251
  67. package/npm-docs/api/api-reference.md +85 -85
  68. package/npm-docs/api/configuration-options.md +198 -198
  69. package/npm-docs/api/eventManager.md +82 -82
  70. package/npm-docs/api/expression-nodes.md +561 -561
  71. package/npm-docs/api/focusFrameManager.md +144 -144
  72. package/npm-docs/api/index.md +105 -105
  73. package/npm-docs/api/main.md +62 -62
  74. package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
  75. package/npm-docs/api/omdCanvas.md +83 -83
  76. package/npm-docs/api/omdConfigManager.md +112 -112
  77. package/npm-docs/api/omdConstantNode.md +52 -52
  78. package/npm-docs/api/omdDisplay.md +87 -87
  79. package/npm-docs/api/omdEquationNode.md +174 -174
  80. package/npm-docs/api/omdEquationSequenceNode.md +258 -258
  81. package/npm-docs/api/omdEquationStack.md +192 -192
  82. package/npm-docs/api/omdFunctionNode.md +82 -82
  83. package/npm-docs/api/omdGroupNode.md +78 -78
  84. package/npm-docs/api/omdHelpers.md +87 -87
  85. package/npm-docs/api/omdLeafNode.md +85 -85
  86. package/npm-docs/api/omdNode.md +201 -201
  87. package/npm-docs/api/omdOperationDisplayNode.md +117 -117
  88. package/npm-docs/api/omdOperatorNode.md +91 -91
  89. package/npm-docs/api/omdParenthesisNode.md +133 -133
  90. package/npm-docs/api/omdPopup.md +191 -191
  91. package/npm-docs/api/omdPowerNode.md +131 -131
  92. package/npm-docs/api/omdRationalNode.md +144 -144
  93. package/npm-docs/api/omdSequenceNode.md +128 -128
  94. package/npm-docs/api/omdSimplification.md +78 -78
  95. package/npm-docs/api/omdSqrtNode.md +144 -144
  96. package/npm-docs/api/omdStepVisualizer.md +146 -146
  97. package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
  98. package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
  99. package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
  100. package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
  101. package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
  102. package/npm-docs/api/omdToolbar.md +130 -130
  103. package/npm-docs/api/omdTranscriptionService.md +95 -95
  104. package/npm-docs/api/omdTreeDiff.md +169 -169
  105. package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
  106. package/npm-docs/api/omdUtilities.md +82 -82
  107. package/npm-docs/api/omdVariableNode.md +123 -123
  108. package/npm-docs/api/selectTool.md +74 -74
  109. package/npm-docs/api/simplificationEngine.md +97 -97
  110. package/npm-docs/api/simplificationRules.md +76 -76
  111. package/npm-docs/api/simplificationUtils.md +64 -64
  112. package/npm-docs/api/transcribe.md +43 -43
  113. package/npm-docs/guides/equations.md +854 -854
  114. package/npm-docs/guides/factory-functions.md +354 -354
  115. package/npm-docs/guides/getting-started.md +318 -318
  116. package/npm-docs/guides/quick-examples.md +525 -525
  117. package/npm-docs/guides/visualizations.md +682 -682
  118. package/npm-docs/index.html +12 -0
  119. package/npm-docs/json-schemas.md +826 -826
  120. package/omd/config/omdConfigManager.js +279 -267
  121. package/omd/core/index.js +158 -158
  122. package/omd/core/omdEquationStack.js +546 -546
  123. package/omd/core/omdUtilities.js +113 -113
  124. package/omd/display/omdDisplay.js +969 -962
  125. package/omd/display/omdToolbar.js +501 -501
  126. package/omd/nodes/omdBinaryExpressionNode.js +459 -459
  127. package/omd/nodes/omdConstantNode.js +141 -141
  128. package/omd/nodes/omdEquationNode.js +1327 -1327
  129. package/omd/nodes/omdFunctionNode.js +351 -351
  130. package/omd/nodes/omdGroupNode.js +67 -67
  131. package/omd/nodes/omdLeafNode.js +76 -76
  132. package/omd/nodes/omdNode.js +556 -556
  133. package/omd/nodes/omdOperationDisplayNode.js +321 -321
  134. package/omd/nodes/omdOperatorNode.js +108 -108
  135. package/omd/nodes/omdParenthesisNode.js +292 -292
  136. package/omd/nodes/omdPowerNode.js +235 -235
  137. package/omd/nodes/omdRationalNode.js +295 -295
  138. package/omd/nodes/omdSqrtNode.js +307 -307
  139. package/omd/nodes/omdUnaryExpressionNode.js +227 -227
  140. package/omd/nodes/omdVariableNode.js +122 -122
  141. package/omd/simplification/omdSimplification.js +140 -140
  142. package/omd/simplification/omdSimplificationEngine.js +887 -887
  143. package/omd/simplification/package.json +5 -5
  144. package/omd/simplification/rules/binaryRules.js +1037 -1037
  145. package/omd/simplification/rules/functionRules.js +111 -111
  146. package/omd/simplification/rules/index.js +48 -48
  147. package/omd/simplification/rules/parenthesisRules.js +19 -19
  148. package/omd/simplification/rules/powerRules.js +143 -143
  149. package/omd/simplification/rules/rationalRules.js +725 -725
  150. package/omd/simplification/rules/sqrtRules.js +48 -48
  151. package/omd/simplification/rules/unaryRules.js +37 -37
  152. package/omd/simplification/simplificationRules.js +31 -31
  153. package/omd/simplification/simplificationUtils.js +1055 -1055
  154. package/omd/step-visualizer/omdStepVisualizer.js +947 -947
  155. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
  156. package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
  157. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
  158. package/omd/utils/aiNextEquationStep.js +106 -106
  159. package/omd/utils/omdNodeOverlay.js +638 -638
  160. package/omd/utils/omdPopup.js +1203 -1203
  161. package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
  162. package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
  163. package/omd/utils/omdTranscriptionService.js +123 -123
  164. package/omd/utils/omdTreeDiff.js +733 -733
  165. package/package.json +59 -56
  166. package/readme.html +184 -120
  167. package/src/index.js +74 -74
  168. package/src/json-schemas.md +576 -576
  169. package/src/omd-json-samples.js +147 -147
  170. package/src/omdApp.js +391 -391
  171. package/src/omdAppCanvas.js +335 -335
  172. package/src/omdBalanceHanger.js +199 -199
  173. package/src/omdColor.js +13 -13
  174. package/src/omdCoordinatePlane.js +541 -541
  175. package/src/omdExpression.js +115 -115
  176. package/src/omdFactory.js +150 -150
  177. package/src/omdFunction.js +114 -114
  178. package/src/omdMetaExpression.js +290 -290
  179. package/src/omdNaturalExpression.js +563 -563
  180. package/src/omdNode.js +383 -383
  181. package/src/omdNumber.js +52 -52
  182. package/src/omdNumberLine.js +114 -112
  183. package/src/omdNumberTile.js +118 -118
  184. package/src/omdOperator.js +72 -72
  185. package/src/omdPowerExpression.js +91 -91
  186. package/src/omdProblem.js +259 -259
  187. package/src/omdRatioChart.js +251 -251
  188. package/src/omdRationalExpression.js +114 -114
  189. package/src/omdSampleData.js +215 -215
  190. package/src/omdShapes.js +512 -512
  191. package/src/omdSpinner.js +151 -151
  192. package/src/omdString.js +49 -49
  193. package/src/omdTable.js +498 -498
  194. package/src/omdTapeDiagram.js +244 -244
  195. package/src/omdTerm.js +91 -91
  196. package/src/omdTileEquation.js +349 -349
  197. package/src/omdUtils.js +84 -84
  198. package/src/omdVariable.js +51 -51
@@ -1,557 +1,557 @@
1
- /**
2
- * omdNode - Base class for mathematical expression nodes
3
- *
4
- * This class serves as the foundation for all mathematical expression nodes.
5
- * It handles basic tree structure, layout calculations, and visual properties.
6
- * Built on top of omdMetaExpression which provides SVG rendering capabilities.
7
- */
8
-
9
- import { omdMetaExpression } from "../../src/omdMetaExpression.js";
10
- /**
11
- * Base class for mathematical expression nodes
12
- * Handles basic tree structure, layout calculations, and visual properties
13
- * @extends omdMetaExpression
14
- */
15
- let _simplifyStep = null;
16
- export function setSimplifyStep(fn) { _simplifyStep = fn; }
17
-
18
- export class omdNode extends omdMetaExpression {
19
- static nextId = 1;
20
- /**
21
- * Creates a tree node from math.js AST data
22
- * @param {Object} astNodeData - The AST node from math.js parser
23
- */
24
- constructor(astNodeData) {
25
- super();
26
-
27
- this.astNodeData = astNodeData; // The AST node from math.js
28
-
29
- this.type = "omdNode";
30
- this.id = omdNode.nextId++;
31
- this.argumentNodeList = {};
32
-
33
- // Preserve provenance from AST if it exists, otherwise initialize empty array
34
- this.provenance = astNodeData && astNodeData.provenance ? [...astNodeData.provenance] : [];
35
- this.isExplainHighlighted = false; // Initialize the lock
36
-
37
- this.parent = null;
38
- this.svgElement = null;
39
- this.x = 0;
40
- this.y = 0;
41
- this.width = 0;
42
- this.height = 0;
43
- this.fontSize = 32;
44
- }
45
-
46
- /**
47
- * Creates a deep clone of this node.
48
- * @returns {omdNode} A new node that is a deep clone of this one.
49
- */
50
- clone() {
51
- // A more robust deep clone for astNodeData might be needed if it contains complex objects.
52
- const newAstNodeData = JSON.parse(JSON.stringify(this.astNodeData));
53
- const clone = new this.constructor(newAstNodeData);
54
- // A clone gets a new ID, but its provenance points back to the node it was cloned from.
55
- // This is the crucial link for tracking history across simplification steps.
56
- clone.provenance.push(this.id);
57
- return clone;
58
- }
59
-
60
- /**
61
- * Recursively walks a cloned node tree and sets the provenance of each node
62
- * to point back to the corresponding node in the original tree.
63
- * @param {omdNode} originalNode - The corresponding node from the original tree.
64
- * @private
65
- */
66
- _syncProvenanceFrom(originalNode) {
67
- // This function is called on a node in a CLONED tree.
68
- // `originalNode` is the corresponding node from the ORIGINAL tree.
69
- if (!originalNode) return;
70
-
71
- // Base case: Add the original's ID to this cloned node's provenance.
72
- // The clone gets its own unique ID from the constructor, so this creates the link.
73
- // Don't replace existing provenance, just add to it.
74
- if (!this.provenance.includes(originalNode.id)) {
75
- this.provenance.push(originalNode.id);
76
- }
77
-
78
- // Recursive step: Recurse into all meaningful children.
79
- // We use `argumentNodeList` as the source of truth for children that
80
- // are part of the expression's structure (e.g., left/right, args).
81
- if (originalNode.argumentNodeList && this.argumentNodeList) {
82
- // Iterate over the original's keys. Using Object.keys() is safer than a
83
- // for...in loop as it only includes own properties.
84
- for (const key of Object.keys(originalNode.argumentNodeList)) {
85
- const originalChild = originalNode.argumentNodeList[key];
86
- const cloneChild = this.argumentNodeList[key];
87
-
88
- // Ensure the corresponding child exists on the clone before recursing.
89
- if (originalChild && cloneChild) {
90
- if (Array.isArray(originalChild) && Array.isArray(cloneChild)) {
91
- for (let i = 0; i < originalChild.length; i++) {
92
- // The optional chain `?` handles cases where an item in the array might be null/undefined.
93
- cloneChild[i]?._syncProvenanceFrom(originalChild[i]);
94
- }
95
- } else {
96
- // Handle children that are single nodes.
97
- cloneChild._syncProvenanceFrom(originalChild);
98
- }
99
- }
100
- }
101
- }
102
- }
103
-
104
- /**
105
- * Overridable method used to determine value of omdNode
106
- */
107
- parseValue() {
108
- }
109
-
110
- parseType() {
111
- }
112
-
113
- /**
114
- * Gerard: Uses this method to initiate the layout of all elements in tree
115
- */
116
- initialize() {
117
- this.computeDimensions();
118
- this.updateLayout();
119
- }
120
-
121
- /**
122
- * Calculates dimensions for this node and its children
123
- * Override in subclasses for specific dimension calculations
124
- */
125
- computeDimensions() {
126
- }
127
-
128
- /**
129
- * Updates the layout/positioning of child nodes
130
- * Override in subclasses for specific layout behavior
131
- */
132
- updateLayout() {
133
- }
134
-
135
- /**
136
- * Gets the vertical position that should be used for alignment with other nodes.
137
- * By default, this is the vertical center. Subclasses can override this.
138
- * @returns {number} The y-coordinate for alignment.
139
- */
140
- getAlignmentBaseline() {
141
- return this.height / 2;
142
- }
143
-
144
- /**
145
- * @param {omdNode} newNode - The new node that will take this node's place.
146
- * @param {object} options - Configuration for the replacement.
147
- * @param {boolean} [options.updateLayout=true] - If true, the layout of the entire
148
- * tree will be recalculated upwards from the point of replacement. This can be
149
- * set to false for batch operations to improve performance.
150
- * @returns {boolean} - True if the replacement was successful, false otherwise.
151
- */
152
- replaceWith(newNode, options = { updateLayout: true }) {
153
- if (!this.parent) {
154
- console.error("Cannot replace a node with no parent.");
155
- return false;
156
- }
157
-
158
- const parent = this.parent;
159
- const childIndex = parent.childList.indexOf(this);
160
-
161
- const revertChanges = () => {
162
- parent.childList[childIndex] = this;
163
- newNode.parent = null;
164
- this.parent = parent;
165
- };
166
-
167
- if (childIndex === -1) {
168
- console.error("Node not found in parent's childList.", this);
169
- return false;
170
- }
171
-
172
- parent.childList[childIndex] = newNode;
173
- newNode.parent = parent;
174
- this.parent = null;
175
-
176
- if (!this.replaceNodeInParent(newNode)) {
177
- revertChanges();
178
- console.error("Failed to replace specific references. Reverting changes.");
179
- return false;
180
- }
181
-
182
- if (options.updateLayout) {
183
- this.updateSvg(newNode);
184
- newNode.updateLayoutUpwards();
185
- }
186
-
187
- return true;
188
- }
189
-
190
- /**
191
- * Helper method to replace this node with a new node in the parent's specific properties.
192
- * @param {omdNode} newNode - The new node.
193
- * @returns {boolean} - True if successful.
194
- * @private
195
- */
196
- replaceNodeInParent(newNode) {
197
- const parent = newNode.parent;
198
- if (!parent || !parent.argumentNodeList) return false;
199
-
200
- for (const key in parent.argumentNodeList) {
201
- const property = parent.argumentNodeList[key];
202
- if (property === this) {
203
- parent.argumentNodeList[key] = newNode;
204
- if (Object.prototype.hasOwnProperty.call(parent, key)) {
205
- parent[key] = newNode;
206
- }
207
- return true;
208
- }
209
- if (Array.isArray(property) && property.includes(this)) {
210
- const index = property.indexOf(this);
211
- property[index] = newNode;
212
- if (parent[key] === property) {
213
- parent[key][index] = newNode;
214
- }
215
- return true;
216
- }
217
- }
218
- return false;
219
- }
220
-
221
- /**
222
- * Helper method to update the SVG representation in the DOM.
223
- * @param {omdNode} newNode - The new node.
224
- * @private
225
- */
226
- updateSvg(newNode) {
227
- const parent = newNode.parent;
228
- if (parent && parent.svgObject && this.svgObject && newNode.svgObject) {
229
- try {
230
- parent.svgObject.replaceChild(newNode.svgObject, this.svgObject);
231
- } catch (e) {
232
- console.error("SVG replacement failed, attempting fallback.", e);
233
- try {
234
- parent.svgObject.removeChild(this.svgObject);
235
- parent.svgObject.appendChild(newNode.svgObject);
236
- } catch (fallbackError) {
237
- console.error("SVG fallback replacement also failed:", fallbackError);
238
- }
239
- }
240
- }
241
- }
242
-
243
- /**
244
- * Traverses up the tree from this node's parent to re-calculate dimensions and layouts.
245
- */
246
- updateLayoutUpwards() {
247
- const ancestors = [];
248
- let current = this.parent;
249
- while (current) {
250
- ancestors.push(current);
251
- current = current.parent;
252
- }
253
- for (const ancestor of ancestors) {
254
- if (typeof ancestor.computeDimensions === 'function') ancestor.computeDimensions();
255
- }
256
- for (let i = ancestors.length - 1; i >= 0; i--) {
257
- if (typeof ancestors[i].updateLayout === 'function') ancestors[i].updateLayout();
258
- }
259
- }
260
-
261
- /**
262
- * Determines if the node represents a constant numerical value.
263
- * @returns {boolean}
264
- */
265
- isConstant() {
266
- return false;
267
- }
268
-
269
- /**
270
- * Retrieves the numerical value of a constant node.
271
- * Throws an error if the node is not constant.
272
- * @returns {number}
273
- */
274
- getValue() {
275
- throw new Error("Node is not a constant expression");
276
- }
277
-
278
- /**
279
- * Retrieves the rational value of a constant node as a numerator/denominator pair.
280
- * Throws an error if the node is not constant.
281
- * @returns {{num: number, den: number}}
282
- */
283
- getRationalValue() {
284
- throw new Error("Node is not a constant rational expression");
285
- }
286
-
287
- /**
288
- * Simplifies this standalone node if it's not part of a sequence
289
- * @returns {Promise<Object>} Result with {success: boolean, foldedCount: number, newRoot: omdNode|null, message: string}
290
- */
291
- simplify() {
292
- if (!_simplifyStep) throw new Error("simplifyStep not set");
293
- try {
294
- const { foldedCount, newRoot } = _simplifyStep(this);
295
- if (foldedCount > 0) {
296
- return {
297
- success: true,
298
- foldedCount,
299
- newRoot,
300
- message: `Simplified! Applied ${foldedCount} simplification step(s)`
301
- };
302
- } else {
303
- return {
304
- success: false,
305
- foldedCount: 0,
306
- newRoot: null,
307
- message: 'No simplifications available'
308
- };
309
- }
310
- } catch (error) {
311
- return {
312
- success: false,
313
- foldedCount: 0,
314
- newRoot: null,
315
- message: `Simplification error: ${error.message}`
316
- };
317
- }
318
- }
319
-
320
- /**
321
- * Converts the omdNode and its children back into a math.js AST node.
322
- * This method must be implemented by all subclasses.
323
- * @returns {Object} A math.js-compatible AST node.
324
- */
325
- toMathJSNode() {
326
- throw new Error(`toMathJSNode() must be implemented by ${this.type}`);
327
- }
328
-
329
- /**
330
- * @returns {string} A string representation of the node.
331
- */
332
- toString() {
333
- try {
334
- // Use toMathJSNode to get math.js compatible AST, then convert to string
335
- const mathJSNode = this.toMathJSNode();
336
- return mathJSNode.toString();
337
- } catch (error) {
338
- // Fallback to simple class name if conversion fails
339
- return `[${this.type}]`;
340
- }
341
- }
342
-
343
- /**
344
- * Render the node to SVG.
345
- * @returns {SVGElement} The rendered SVG element
346
- */
347
- render() {
348
- if (!this.svgElement) {
349
- this.svgElement = this.renderSelf();
350
- }
351
- return this.svgElement;
352
- }
353
-
354
- /**
355
- * Abstract method - Must be implemented by subclasses.
356
- * Creates the specific SVG representation for this node type.
357
- * @returns {SVGElement}
358
- */
359
- renderSelf() {
360
- throw new Error(`renderSelf() must be implemented by ${this.type}`);
361
- }
362
-
363
- /**
364
- * Set the font size for rendering.
365
- * @param {number} size - The font size in pixels
366
- */
367
- setFontSize(size) {
368
- this.fontSize = size;
369
- // Update all children
370
- if (this.childList) {
371
- this.childList.forEach(child => {
372
- if (child && typeof child.setFontSize === 'function') {
373
- child.setFontSize(size);
374
- }
375
- });
376
- }
377
- }
378
-
379
- /**
380
- * Move the node to a specific position.
381
- * @param {number} x - The x coordinate
382
- * @param {number} y - The y coordinate
383
- */
384
- moveTo(x, y) {
385
- const dx = x - this.x;
386
- const dy = y - this.y;
387
-
388
- this.x = x;
389
- this.y = y;
390
-
391
- // Update SVG position if rendered
392
- if (this.svgElement) {
393
- this.svgElement.setAttribute('transform', `translate(${this.x}, ${this.y})`);
394
- }
395
-
396
- // Move all children relatively
397
- if (this.childList) {
398
- this.childList.forEach(child => {
399
- if (child && typeof child.moveTo === 'function') {
400
- child.moveTo(child.x + dx, child.y + dy);
401
- }
402
- });
403
- }
404
- }
405
-
406
- /**
407
- * Make the node visible.
408
- */
409
- show() {
410
- this.visible = true;
411
- if (this.svgElement) {
412
- this.svgElement.style.display = 'block';
413
- }
414
- if (this.svgObject) {
415
- this.svgObject.style.display = 'block';
416
- }
417
- }
418
-
419
- /**
420
- * Hide the node.
421
- */
422
- hide() {
423
- this.visible = false;
424
- if (this.svgElement) {
425
- this.svgElement.style.display = 'none';
426
- }
427
- if (this.svgObject) {
428
- this.svgObject.style.display = 'none';
429
- }
430
- }
431
-
432
- /**
433
- * Get the depth of the node in the tree.
434
- * @returns {number} The depth (0 for root)
435
- */
436
- getDepth() {
437
- let depth = 0;
438
- let current = this.parent;
439
- while (current) {
440
- depth++;
441
- current = current.parent;
442
- }
443
- return depth;
444
- }
445
-
446
- /**
447
- * Find the nearest parent node of a specific type.
448
- * @param {string} type - The node type to search for
449
- * @returns {omdNode|null} The parent node or null if not found
450
- */
451
- findParentOfType(type) {
452
- let current = this.parent;
453
- while (current) {
454
- if (current.type === type || current.type === type) {
455
- return current;
456
- }
457
- current = current.parent;
458
- }
459
- return null;
460
- }
461
-
462
- /**
463
- * Create a node from a math.js AST.
464
- * Factory method that creates the appropriate node subclass based on the AST type.
465
- * @param {Object} ast - The math.js AST object
466
- * @returns {omdNode} The appropriate node subclass instance
467
- * @static
468
- */
469
- static fromAST(ast) {
470
- // This should ideally be implemented to use the node factory
471
- // For now, throw an error indicating it should use the helper
472
- throw new Error('Use omdHelpers.createNodeFromAST() instead');
473
- }
474
-
475
- /**
476
- * Validates the provenance integrity of this node and its descendants
477
- * @param {Map} [nodeMap] - Optional map of all known nodes for validation
478
- * @returns {Array} Array of validation issues found
479
- */
480
- validateProvenance(nodeMap = null) {
481
- const issues = [];
482
- const allNodes = this.findAllNodes();
483
-
484
- // Create a set of all valid node IDs if nodeMap not provided
485
- const validIds = nodeMap ?
486
- new Set(nodeMap.keys()) :
487
- new Set(allNodes.map(n => n.id));
488
-
489
- allNodes.forEach(node => {
490
- // Check for duplicate IDs in provenance
491
- if (node.provenance && node.provenance.length > 0) {
492
- const uniqueProvenance = new Set(node.provenance);
493
- if (uniqueProvenance.size !== node.provenance.length) {
494
- issues.push({
495
- type: 'duplicate_provenance',
496
- nodeId: node.id,
497
- nodeType: node.type,
498
- provenance: node.provenance
499
- });
500
- }
501
-
502
- // Check for invalid provenance references
503
- node.provenance.forEach(id => {
504
- if (!validIds.has(id)) {
505
- issues.push({
506
- type: 'invalid_provenance_reference',
507
- nodeId: node.id,
508
- nodeType: node.type,
509
- invalidId: id
510
- });
511
- }
512
- });
513
-
514
- // Check for self-reference in provenance
515
- if (node.provenance.includes(node.id)) {
516
- issues.push({
517
- type: 'self_reference_provenance',
518
- nodeId: node.id,
519
- nodeType: node.type
520
- });
521
- }
522
- }
523
- });
524
-
525
- return issues;
526
- }
527
-
528
- setHighlight(highlightOn = true, color = omdColor.highlightColor) {
529
- // If this node is already highlighted for explanation, keep that color
530
- if (this.backRect && this.backRect.fillColor === omdColor.explainColor) {
531
- return;
532
- }
533
-
534
- // Otherwise proceed with normal highlighting
535
- if (this.isExplainHighlighted) return; // Respect the lock
536
-
537
- if (this.backRect) {
538
- this.backRect.setFillColor(highlightOn ? color : omdColor.lightGray);
539
- this.backRect.setOpacity(1.0);
540
- }
541
- }
542
-
543
- lowlight() {
544
- // If this node is highlighted for explanation, keep that color
545
- if (this.backRect && this.backRect.fillColor === omdColor.explainColor) {
546
- return;
547
- }
548
-
549
- if (this.isExplainHighlighted) return; // Respect the lock
550
- super.lowlight();
551
- }
552
-
553
- setFillColor(color) {
554
- if (this.isExplainHighlighted) return; // Respect the lock
555
- // ... (rest of the method) ...
556
- }
1
+ /**
2
+ * omdNode - Base class for mathematical expression nodes
3
+ *
4
+ * This class serves as the foundation for all mathematical expression nodes.
5
+ * It handles basic tree structure, layout calculations, and visual properties.
6
+ * Built on top of omdMetaExpression which provides SVG rendering capabilities.
7
+ */
8
+
9
+ import { omdMetaExpression } from "../../src/omdMetaExpression.js";
10
+ /**
11
+ * Base class for mathematical expression nodes
12
+ * Handles basic tree structure, layout calculations, and visual properties
13
+ * @extends omdMetaExpression
14
+ */
15
+ let _simplifyStep = null;
16
+ export function setSimplifyStep(fn) { _simplifyStep = fn; }
17
+
18
+ export class omdNode extends omdMetaExpression {
19
+ static nextId = 1;
20
+ /**
21
+ * Creates a tree node from math.js AST data
22
+ * @param {Object} astNodeData - The AST node from math.js parser
23
+ */
24
+ constructor(astNodeData) {
25
+ super();
26
+
27
+ this.astNodeData = astNodeData; // The AST node from math.js
28
+
29
+ this.type = "omdNode";
30
+ this.id = omdNode.nextId++;
31
+ this.argumentNodeList = {};
32
+
33
+ // Preserve provenance from AST if it exists, otherwise initialize empty array
34
+ this.provenance = astNodeData && astNodeData.provenance ? [...astNodeData.provenance] : [];
35
+ this.isExplainHighlighted = false; // Initialize the lock
36
+
37
+ this.parent = null;
38
+ this.svgElement = null;
39
+ this.x = 0;
40
+ this.y = 0;
41
+ this.width = 0;
42
+ this.height = 0;
43
+ this.fontSize = 32;
44
+ }
45
+
46
+ /**
47
+ * Creates a deep clone of this node.
48
+ * @returns {omdNode} A new node that is a deep clone of this one.
49
+ */
50
+ clone() {
51
+ // A more robust deep clone for astNodeData might be needed if it contains complex objects.
52
+ const newAstNodeData = JSON.parse(JSON.stringify(this.astNodeData));
53
+ const clone = new this.constructor(newAstNodeData);
54
+ // A clone gets a new ID, but its provenance points back to the node it was cloned from.
55
+ // This is the crucial link for tracking history across simplification steps.
56
+ clone.provenance.push(this.id);
57
+ return clone;
58
+ }
59
+
60
+ /**
61
+ * Recursively walks a cloned node tree and sets the provenance of each node
62
+ * to point back to the corresponding node in the original tree.
63
+ * @param {omdNode} originalNode - The corresponding node from the original tree.
64
+ * @private
65
+ */
66
+ _syncProvenanceFrom(originalNode) {
67
+ // This function is called on a node in a CLONED tree.
68
+ // `originalNode` is the corresponding node from the ORIGINAL tree.
69
+ if (!originalNode) return;
70
+
71
+ // Base case: Add the original's ID to this cloned node's provenance.
72
+ // The clone gets its own unique ID from the constructor, so this creates the link.
73
+ // Don't replace existing provenance, just add to it.
74
+ if (!this.provenance.includes(originalNode.id)) {
75
+ this.provenance.push(originalNode.id);
76
+ }
77
+
78
+ // Recursive step: Recurse into all meaningful children.
79
+ // We use `argumentNodeList` as the source of truth for children that
80
+ // are part of the expression's structure (e.g., left/right, args).
81
+ if (originalNode.argumentNodeList && this.argumentNodeList) {
82
+ // Iterate over the original's keys. Using Object.keys() is safer than a
83
+ // for...in loop as it only includes own properties.
84
+ for (const key of Object.keys(originalNode.argumentNodeList)) {
85
+ const originalChild = originalNode.argumentNodeList[key];
86
+ const cloneChild = this.argumentNodeList[key];
87
+
88
+ // Ensure the corresponding child exists on the clone before recursing.
89
+ if (originalChild && cloneChild) {
90
+ if (Array.isArray(originalChild) && Array.isArray(cloneChild)) {
91
+ for (let i = 0; i < originalChild.length; i++) {
92
+ // The optional chain `?` handles cases where an item in the array might be null/undefined.
93
+ cloneChild[i]?._syncProvenanceFrom(originalChild[i]);
94
+ }
95
+ } else {
96
+ // Handle children that are single nodes.
97
+ cloneChild._syncProvenanceFrom(originalChild);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Overridable method used to determine value of omdNode
106
+ */
107
+ parseValue() {
108
+ }
109
+
110
+ parseType() {
111
+ }
112
+
113
+ /**
114
+ * Gerard: Uses this method to initiate the layout of all elements in tree
115
+ */
116
+ initialize() {
117
+ this.computeDimensions();
118
+ this.updateLayout();
119
+ }
120
+
121
+ /**
122
+ * Calculates dimensions for this node and its children
123
+ * Override in subclasses for specific dimension calculations
124
+ */
125
+ computeDimensions() {
126
+ }
127
+
128
+ /**
129
+ * Updates the layout/positioning of child nodes
130
+ * Override in subclasses for specific layout behavior
131
+ */
132
+ updateLayout() {
133
+ }
134
+
135
+ /**
136
+ * Gets the vertical position that should be used for alignment with other nodes.
137
+ * By default, this is the vertical center. Subclasses can override this.
138
+ * @returns {number} The y-coordinate for alignment.
139
+ */
140
+ getAlignmentBaseline() {
141
+ return this.height / 2;
142
+ }
143
+
144
+ /**
145
+ * @param {omdNode} newNode - The new node that will take this node's place.
146
+ * @param {object} options - Configuration for the replacement.
147
+ * @param {boolean} [options.updateLayout=true] - If true, the layout of the entire
148
+ * tree will be recalculated upwards from the point of replacement. This can be
149
+ * set to false for batch operations to improve performance.
150
+ * @returns {boolean} - True if the replacement was successful, false otherwise.
151
+ */
152
+ replaceWith(newNode, options = { updateLayout: true }) {
153
+ if (!this.parent) {
154
+ console.error("Cannot replace a node with no parent.");
155
+ return false;
156
+ }
157
+
158
+ const parent = this.parent;
159
+ const childIndex = parent.childList.indexOf(this);
160
+
161
+ const revertChanges = () => {
162
+ parent.childList[childIndex] = this;
163
+ newNode.parent = null;
164
+ this.parent = parent;
165
+ };
166
+
167
+ if (childIndex === -1) {
168
+ console.error("Node not found in parent's childList.", this);
169
+ return false;
170
+ }
171
+
172
+ parent.childList[childIndex] = newNode;
173
+ newNode.parent = parent;
174
+ this.parent = null;
175
+
176
+ if (!this.replaceNodeInParent(newNode)) {
177
+ revertChanges();
178
+ console.error("Failed to replace specific references. Reverting changes.");
179
+ return false;
180
+ }
181
+
182
+ if (options.updateLayout) {
183
+ this.updateSvg(newNode);
184
+ newNode.updateLayoutUpwards();
185
+ }
186
+
187
+ return true;
188
+ }
189
+
190
+ /**
191
+ * Helper method to replace this node with a new node in the parent's specific properties.
192
+ * @param {omdNode} newNode - The new node.
193
+ * @returns {boolean} - True if successful.
194
+ * @private
195
+ */
196
+ replaceNodeInParent(newNode) {
197
+ const parent = newNode.parent;
198
+ if (!parent || !parent.argumentNodeList) return false;
199
+
200
+ for (const key in parent.argumentNodeList) {
201
+ const property = parent.argumentNodeList[key];
202
+ if (property === this) {
203
+ parent.argumentNodeList[key] = newNode;
204
+ if (Object.prototype.hasOwnProperty.call(parent, key)) {
205
+ parent[key] = newNode;
206
+ }
207
+ return true;
208
+ }
209
+ if (Array.isArray(property) && property.includes(this)) {
210
+ const index = property.indexOf(this);
211
+ property[index] = newNode;
212
+ if (parent[key] === property) {
213
+ parent[key][index] = newNode;
214
+ }
215
+ return true;
216
+ }
217
+ }
218
+ return false;
219
+ }
220
+
221
+ /**
222
+ * Helper method to update the SVG representation in the DOM.
223
+ * @param {omdNode} newNode - The new node.
224
+ * @private
225
+ */
226
+ updateSvg(newNode) {
227
+ const parent = newNode.parent;
228
+ if (parent && parent.svgObject && this.svgObject && newNode.svgObject) {
229
+ try {
230
+ parent.svgObject.replaceChild(newNode.svgObject, this.svgObject);
231
+ } catch (e) {
232
+ console.error("SVG replacement failed, attempting fallback.", e);
233
+ try {
234
+ parent.svgObject.removeChild(this.svgObject);
235
+ parent.svgObject.appendChild(newNode.svgObject);
236
+ } catch (fallbackError) {
237
+ console.error("SVG fallback replacement also failed:", fallbackError);
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Traverses up the tree from this node's parent to re-calculate dimensions and layouts.
245
+ */
246
+ updateLayoutUpwards() {
247
+ const ancestors = [];
248
+ let current = this.parent;
249
+ while (current) {
250
+ ancestors.push(current);
251
+ current = current.parent;
252
+ }
253
+ for (const ancestor of ancestors) {
254
+ if (typeof ancestor.computeDimensions === 'function') ancestor.computeDimensions();
255
+ }
256
+ for (let i = ancestors.length - 1; i >= 0; i--) {
257
+ if (typeof ancestors[i].updateLayout === 'function') ancestors[i].updateLayout();
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Determines if the node represents a constant numerical value.
263
+ * @returns {boolean}
264
+ */
265
+ isConstant() {
266
+ return false;
267
+ }
268
+
269
+ /**
270
+ * Retrieves the numerical value of a constant node.
271
+ * Throws an error if the node is not constant.
272
+ * @returns {number}
273
+ */
274
+ getValue() {
275
+ throw new Error("Node is not a constant expression");
276
+ }
277
+
278
+ /**
279
+ * Retrieves the rational value of a constant node as a numerator/denominator pair.
280
+ * Throws an error if the node is not constant.
281
+ * @returns {{num: number, den: number}}
282
+ */
283
+ getRationalValue() {
284
+ throw new Error("Node is not a constant rational expression");
285
+ }
286
+
287
+ /**
288
+ * Simplifies this standalone node if it's not part of a sequence
289
+ * @returns {Promise<Object>} Result with {success: boolean, foldedCount: number, newRoot: omdNode|null, message: string}
290
+ */
291
+ simplify() {
292
+ if (!_simplifyStep) throw new Error("simplifyStep not set");
293
+ try {
294
+ const { foldedCount, newRoot } = _simplifyStep(this);
295
+ if (foldedCount > 0) {
296
+ return {
297
+ success: true,
298
+ foldedCount,
299
+ newRoot,
300
+ message: `Simplified! Applied ${foldedCount} simplification step(s)`
301
+ };
302
+ } else {
303
+ return {
304
+ success: false,
305
+ foldedCount: 0,
306
+ newRoot: null,
307
+ message: 'No simplifications available'
308
+ };
309
+ }
310
+ } catch (error) {
311
+ return {
312
+ success: false,
313
+ foldedCount: 0,
314
+ newRoot: null,
315
+ message: `Simplification error: ${error.message}`
316
+ };
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Converts the omdNode and its children back into a math.js AST node.
322
+ * This method must be implemented by all subclasses.
323
+ * @returns {Object} A math.js-compatible AST node.
324
+ */
325
+ toMathJSNode() {
326
+ throw new Error(`toMathJSNode() must be implemented by ${this.type}`);
327
+ }
328
+
329
+ /**
330
+ * @returns {string} A string representation of the node.
331
+ */
332
+ toString() {
333
+ try {
334
+ // Use toMathJSNode to get math.js compatible AST, then convert to string
335
+ const mathJSNode = this.toMathJSNode();
336
+ return mathJSNode.toString();
337
+ } catch (error) {
338
+ // Fallback to simple class name if conversion fails
339
+ return `[${this.type}]`;
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Render the node to SVG.
345
+ * @returns {SVGElement} The rendered SVG element
346
+ */
347
+ render() {
348
+ if (!this.svgElement) {
349
+ this.svgElement = this.renderSelf();
350
+ }
351
+ return this.svgElement;
352
+ }
353
+
354
+ /**
355
+ * Abstract method - Must be implemented by subclasses.
356
+ * Creates the specific SVG representation for this node type.
357
+ * @returns {SVGElement}
358
+ */
359
+ renderSelf() {
360
+ throw new Error(`renderSelf() must be implemented by ${this.type}`);
361
+ }
362
+
363
+ /**
364
+ * Set the font size for rendering.
365
+ * @param {number} size - The font size in pixels
366
+ */
367
+ setFontSize(size) {
368
+ this.fontSize = size;
369
+ // Update all children
370
+ if (this.childList) {
371
+ this.childList.forEach(child => {
372
+ if (child && typeof child.setFontSize === 'function') {
373
+ child.setFontSize(size);
374
+ }
375
+ });
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Move the node to a specific position.
381
+ * @param {number} x - The x coordinate
382
+ * @param {number} y - The y coordinate
383
+ */
384
+ moveTo(x, y) {
385
+ const dx = x - this.x;
386
+ const dy = y - this.y;
387
+
388
+ this.x = x;
389
+ this.y = y;
390
+
391
+ // Update SVG position if rendered
392
+ if (this.svgElement) {
393
+ this.svgElement.setAttribute('transform', `translate(${this.x}, ${this.y})`);
394
+ }
395
+
396
+ // Move all children relatively
397
+ if (this.childList) {
398
+ this.childList.forEach(child => {
399
+ if (child && typeof child.moveTo === 'function') {
400
+ child.moveTo(child.x + dx, child.y + dy);
401
+ }
402
+ });
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Make the node visible.
408
+ */
409
+ show() {
410
+ this.visible = true;
411
+ if (this.svgElement) {
412
+ this.svgElement.style.display = 'block';
413
+ }
414
+ if (this.svgObject) {
415
+ this.svgObject.style.display = 'block';
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Hide the node.
421
+ */
422
+ hide() {
423
+ this.visible = false;
424
+ if (this.svgElement) {
425
+ this.svgElement.style.display = 'none';
426
+ }
427
+ if (this.svgObject) {
428
+ this.svgObject.style.display = 'none';
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Get the depth of the node in the tree.
434
+ * @returns {number} The depth (0 for root)
435
+ */
436
+ getDepth() {
437
+ let depth = 0;
438
+ let current = this.parent;
439
+ while (current) {
440
+ depth++;
441
+ current = current.parent;
442
+ }
443
+ return depth;
444
+ }
445
+
446
+ /**
447
+ * Find the nearest parent node of a specific type.
448
+ * @param {string} type - The node type to search for
449
+ * @returns {omdNode|null} The parent node or null if not found
450
+ */
451
+ findParentOfType(type) {
452
+ let current = this.parent;
453
+ while (current) {
454
+ if (current.type === type || current.type === type) {
455
+ return current;
456
+ }
457
+ current = current.parent;
458
+ }
459
+ return null;
460
+ }
461
+
462
+ /**
463
+ * Create a node from a math.js AST.
464
+ * Factory method that creates the appropriate node subclass based on the AST type.
465
+ * @param {Object} ast - The math.js AST object
466
+ * @returns {omdNode} The appropriate node subclass instance
467
+ * @static
468
+ */
469
+ static fromAST(ast) {
470
+ // This should ideally be implemented to use the node factory
471
+ // For now, throw an error indicating it should use the helper
472
+ throw new Error('Use omdHelpers.createNodeFromAST() instead');
473
+ }
474
+
475
+ /**
476
+ * Validates the provenance integrity of this node and its descendants
477
+ * @param {Map} [nodeMap] - Optional map of all known nodes for validation
478
+ * @returns {Array} Array of validation issues found
479
+ */
480
+ validateProvenance(nodeMap = null) {
481
+ const issues = [];
482
+ const allNodes = this.findAllNodes();
483
+
484
+ // Create a set of all valid node IDs if nodeMap not provided
485
+ const validIds = nodeMap ?
486
+ new Set(nodeMap.keys()) :
487
+ new Set(allNodes.map(n => n.id));
488
+
489
+ allNodes.forEach(node => {
490
+ // Check for duplicate IDs in provenance
491
+ if (node.provenance && node.provenance.length > 0) {
492
+ const uniqueProvenance = new Set(node.provenance);
493
+ if (uniqueProvenance.size !== node.provenance.length) {
494
+ issues.push({
495
+ type: 'duplicate_provenance',
496
+ nodeId: node.id,
497
+ nodeType: node.type,
498
+ provenance: node.provenance
499
+ });
500
+ }
501
+
502
+ // Check for invalid provenance references
503
+ node.provenance.forEach(id => {
504
+ if (!validIds.has(id)) {
505
+ issues.push({
506
+ type: 'invalid_provenance_reference',
507
+ nodeId: node.id,
508
+ nodeType: node.type,
509
+ invalidId: id
510
+ });
511
+ }
512
+ });
513
+
514
+ // Check for self-reference in provenance
515
+ if (node.provenance.includes(node.id)) {
516
+ issues.push({
517
+ type: 'self_reference_provenance',
518
+ nodeId: node.id,
519
+ nodeType: node.type
520
+ });
521
+ }
522
+ }
523
+ });
524
+
525
+ return issues;
526
+ }
527
+
528
+ setHighlight(highlightOn = true, color = omdColor.highlightColor) {
529
+ // If this node is already highlighted for explanation, keep that color
530
+ if (this.backRect && this.backRect.fillColor === omdColor.explainColor) {
531
+ return;
532
+ }
533
+
534
+ // Otherwise proceed with normal highlighting
535
+ if (this.isExplainHighlighted) return; // Respect the lock
536
+
537
+ if (this.backRect) {
538
+ this.backRect.setFillColor(highlightOn ? color : omdColor.lightGray);
539
+ this.backRect.setOpacity(1.0);
540
+ }
541
+ }
542
+
543
+ lowlight() {
544
+ // If this node is highlighted for explanation, keep that color
545
+ if (this.backRect && this.backRect.fillColor === omdColor.explainColor) {
546
+ return;
547
+ }
548
+
549
+ if (this.isExplainHighlighted) return; // Respect the lock
550
+ super.lowlight();
551
+ }
552
+
553
+ setFillColor(color) {
554
+ if (this.isExplainHighlighted) return; // Respect the lock
555
+ // ... (rest of the method) ...
556
+ }
557
557
  }