@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.
- package/README.md +257 -251
- package/README.old.md +137 -137
- package/canvas/core/canvasConfig.js +202 -202
- package/canvas/drawing/segment.js +167 -167
- package/canvas/drawing/stroke.js +385 -385
- package/canvas/events/eventManager.js +444 -444
- package/canvas/events/pointerEventHandler.js +262 -262
- package/canvas/index.js +48 -48
- package/canvas/tools/PointerTool.js +71 -71
- package/canvas/tools/tool.js +222 -222
- package/canvas/utils/boundingBox.js +377 -377
- package/canvas/utils/mathUtils.js +258 -258
- package/docs/api/configuration-options.md +198 -198
- package/docs/api/eventManager.md +82 -82
- package/docs/api/focusFrameManager.md +144 -144
- package/docs/api/index.md +105 -105
- package/docs/api/main.md +62 -62
- package/docs/api/omdBinaryExpressionNode.md +86 -86
- package/docs/api/omdCanvas.md +83 -83
- package/docs/api/omdConfigManager.md +112 -112
- package/docs/api/omdConstantNode.md +52 -52
- package/docs/api/omdDisplay.md +87 -87
- package/docs/api/omdEquationNode.md +174 -174
- package/docs/api/omdEquationSequenceNode.md +258 -258
- package/docs/api/omdEquationStack.md +192 -192
- package/docs/api/omdFunctionNode.md +82 -82
- package/docs/api/omdGroupNode.md +78 -78
- package/docs/api/omdHelpers.md +87 -87
- package/docs/api/omdLeafNode.md +85 -85
- package/docs/api/omdNode.md +201 -201
- package/docs/api/omdOperationDisplayNode.md +117 -117
- package/docs/api/omdOperatorNode.md +91 -91
- package/docs/api/omdParenthesisNode.md +133 -133
- package/docs/api/omdPopup.md +191 -191
- package/docs/api/omdPowerNode.md +131 -131
- package/docs/api/omdRationalNode.md +144 -144
- package/docs/api/omdSequenceNode.md +128 -128
- package/docs/api/omdSimplification.md +78 -78
- package/docs/api/omdSqrtNode.md +144 -144
- package/docs/api/omdStepVisualizer.md +146 -146
- package/docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/docs/api/omdStepVisualizerLayout.md +70 -70
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/docs/api/omdToolbar.md +130 -130
- package/docs/api/omdTranscriptionService.md +95 -95
- package/docs/api/omdTreeDiff.md +169 -169
- package/docs/api/omdUnaryExpressionNode.md +137 -137
- package/docs/api/omdUtilities.md +82 -82
- package/docs/api/omdVariableNode.md +123 -123
- package/docs/api/selectTool.md +74 -74
- package/docs/api/simplificationEngine.md +97 -97
- package/docs/api/simplificationRules.md +76 -76
- package/docs/api/simplificationUtils.md +64 -64
- package/docs/api/transcribe.md +43 -43
- package/docs/api-reference.md +85 -85
- package/docs/index.html +453 -453
- package/docs/index.md +38 -38
- package/docs/omd-objects.md +258 -258
- package/index.js +79 -79
- package/jsvg/index.js +3 -0
- package/jsvg/jsvg.js +898 -898
- package/jsvg/jsvgComponents.js +357 -358
- package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
- package/npm-docs/README.md +251 -251
- package/npm-docs/api/api-reference.md +85 -85
- package/npm-docs/api/configuration-options.md +198 -198
- package/npm-docs/api/eventManager.md +82 -82
- package/npm-docs/api/expression-nodes.md +561 -561
- package/npm-docs/api/focusFrameManager.md +144 -144
- package/npm-docs/api/index.md +105 -105
- package/npm-docs/api/main.md +62 -62
- package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
- package/npm-docs/api/omdCanvas.md +83 -83
- package/npm-docs/api/omdConfigManager.md +112 -112
- package/npm-docs/api/omdConstantNode.md +52 -52
- package/npm-docs/api/omdDisplay.md +87 -87
- package/npm-docs/api/omdEquationNode.md +174 -174
- package/npm-docs/api/omdEquationSequenceNode.md +258 -258
- package/npm-docs/api/omdEquationStack.md +192 -192
- package/npm-docs/api/omdFunctionNode.md +82 -82
- package/npm-docs/api/omdGroupNode.md +78 -78
- package/npm-docs/api/omdHelpers.md +87 -87
- package/npm-docs/api/omdLeafNode.md +85 -85
- package/npm-docs/api/omdNode.md +201 -201
- package/npm-docs/api/omdOperationDisplayNode.md +117 -117
- package/npm-docs/api/omdOperatorNode.md +91 -91
- package/npm-docs/api/omdParenthesisNode.md +133 -133
- package/npm-docs/api/omdPopup.md +191 -191
- package/npm-docs/api/omdPowerNode.md +131 -131
- package/npm-docs/api/omdRationalNode.md +144 -144
- package/npm-docs/api/omdSequenceNode.md +128 -128
- package/npm-docs/api/omdSimplification.md +78 -78
- package/npm-docs/api/omdSqrtNode.md +144 -144
- package/npm-docs/api/omdStepVisualizer.md +146 -146
- package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
- package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/npm-docs/api/omdToolbar.md +130 -130
- package/npm-docs/api/omdTranscriptionService.md +95 -95
- package/npm-docs/api/omdTreeDiff.md +169 -169
- package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
- package/npm-docs/api/omdUtilities.md +82 -82
- package/npm-docs/api/omdVariableNode.md +123 -123
- package/npm-docs/api/selectTool.md +74 -74
- package/npm-docs/api/simplificationEngine.md +97 -97
- package/npm-docs/api/simplificationRules.md +76 -76
- package/npm-docs/api/simplificationUtils.md +64 -64
- package/npm-docs/api/transcribe.md +43 -43
- package/npm-docs/guides/equations.md +854 -854
- package/npm-docs/guides/factory-functions.md +354 -354
- package/npm-docs/guides/getting-started.md +318 -318
- package/npm-docs/guides/quick-examples.md +525 -525
- package/npm-docs/guides/visualizations.md +682 -682
- package/npm-docs/index.html +12 -0
- package/npm-docs/json-schemas.md +826 -826
- package/omd/config/omdConfigManager.js +279 -267
- package/omd/core/index.js +158 -158
- package/omd/core/omdEquationStack.js +546 -546
- package/omd/core/omdUtilities.js +113 -113
- package/omd/display/omdDisplay.js +969 -962
- package/omd/display/omdToolbar.js +501 -501
- package/omd/nodes/omdBinaryExpressionNode.js +459 -459
- package/omd/nodes/omdConstantNode.js +141 -141
- package/omd/nodes/omdEquationNode.js +1327 -1327
- package/omd/nodes/omdFunctionNode.js +351 -351
- package/omd/nodes/omdGroupNode.js +67 -67
- package/omd/nodes/omdLeafNode.js +76 -76
- package/omd/nodes/omdNode.js +556 -556
- package/omd/nodes/omdOperationDisplayNode.js +321 -321
- package/omd/nodes/omdOperatorNode.js +108 -108
- package/omd/nodes/omdParenthesisNode.js +292 -292
- package/omd/nodes/omdPowerNode.js +235 -235
- package/omd/nodes/omdRationalNode.js +295 -295
- package/omd/nodes/omdSqrtNode.js +307 -307
- package/omd/nodes/omdUnaryExpressionNode.js +227 -227
- package/omd/nodes/omdVariableNode.js +122 -122
- package/omd/simplification/omdSimplification.js +140 -140
- package/omd/simplification/omdSimplificationEngine.js +887 -887
- package/omd/simplification/package.json +5 -5
- package/omd/simplification/rules/binaryRules.js +1037 -1037
- package/omd/simplification/rules/functionRules.js +111 -111
- package/omd/simplification/rules/index.js +48 -48
- package/omd/simplification/rules/parenthesisRules.js +19 -19
- package/omd/simplification/rules/powerRules.js +143 -143
- package/omd/simplification/rules/rationalRules.js +725 -725
- package/omd/simplification/rules/sqrtRules.js +48 -48
- package/omd/simplification/rules/unaryRules.js +37 -37
- package/omd/simplification/simplificationRules.js +31 -31
- package/omd/simplification/simplificationUtils.js +1055 -1055
- package/omd/step-visualizer/omdStepVisualizer.js +947 -947
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
- package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
- package/omd/utils/aiNextEquationStep.js +106 -106
- package/omd/utils/omdNodeOverlay.js +638 -638
- package/omd/utils/omdPopup.js +1203 -1203
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
- package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
- package/omd/utils/omdTranscriptionService.js +123 -123
- package/omd/utils/omdTreeDiff.js +733 -733
- package/package.json +59 -56
- package/readme.html +184 -120
- package/src/index.js +74 -74
- package/src/json-schemas.md +576 -576
- package/src/omd-json-samples.js +147 -147
- package/src/omdApp.js +391 -391
- package/src/omdAppCanvas.js +335 -335
- package/src/omdBalanceHanger.js +199 -199
- package/src/omdColor.js +13 -13
- package/src/omdCoordinatePlane.js +541 -541
- package/src/omdExpression.js +115 -115
- package/src/omdFactory.js +150 -150
- package/src/omdFunction.js +114 -114
- package/src/omdMetaExpression.js +290 -290
- package/src/omdNaturalExpression.js +563 -563
- package/src/omdNode.js +383 -383
- package/src/omdNumber.js +52 -52
- package/src/omdNumberLine.js +114 -112
- package/src/omdNumberTile.js +118 -118
- package/src/omdOperator.js +72 -72
- package/src/omdPowerExpression.js +91 -91
- package/src/omdProblem.js +259 -259
- package/src/omdRatioChart.js +251 -251
- package/src/omdRationalExpression.js +114 -114
- package/src/omdSampleData.js +215 -215
- package/src/omdShapes.js +512 -512
- package/src/omdSpinner.js +151 -151
- package/src/omdString.js +49 -49
- package/src/omdTable.js +498 -498
- package/src/omdTapeDiagram.js +244 -244
- package/src/omdTerm.js +91 -91
- package/src/omdTileEquation.js +349 -349
- package/src/omdUtils.js +84 -84
- package/src/omdVariable.js +51 -51
package/omd/nodes/omdNode.js
CHANGED
|
@@ -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
|
}
|