@teachinglab/omd 0.6.1 → 0.6.3
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 +606 -547
- package/omd/core/omdUtilities.js +113 -113
- package/omd/display/omdDisplay.js +1045 -963
- 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 -57
- 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
|
@@ -1,322 +1,322 @@
|
|
|
1
|
-
import { omdNode } from "./omdNode.js";
|
|
2
|
-
import { omdVariableNode } from "./omdVariableNode.js";
|
|
3
|
-
import { getNodeForAST } from "../core/omdUtilities.js";
|
|
4
|
-
import { getMultiplicationSymbol } from "../config/omdConfigManager.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Represents a visual node for displaying an operation applied to both sides of an equation.
|
|
8
|
-
* Shows both operations with appropriate spacing between them.
|
|
9
|
-
* Example: -3 -3
|
|
10
|
-
*/
|
|
11
|
-
export class omdOperationDisplayNode extends omdNode {
|
|
12
|
-
static OPERATOR_SYMBOLS = {
|
|
13
|
-
'add': '+',
|
|
14
|
-
'subtract': '-',
|
|
15
|
-
'multiply': '×',
|
|
16
|
-
'divide': '÷'
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Creates an operation display node.
|
|
21
|
-
* @param {string} operation - The type of operation (e.g., 'add', 'subtract', 'multiply', 'divide').
|
|
22
|
-
* @param {number|string|omdNode} value - The value being applied. Can be a number, variable name, or an omdNode.
|
|
23
|
-
*/
|
|
24
|
-
constructor(operation, value) {
|
|
25
|
-
super({});
|
|
26
|
-
this.operation = operation;
|
|
27
|
-
this.value = value;
|
|
28
|
-
this.type = "omdOperationDisplayNode";
|
|
29
|
-
|
|
30
|
-
this._initializeDisplay();
|
|
31
|
-
this._createOperationElements();
|
|
32
|
-
this._disableAllInteractions();
|
|
33
|
-
this._addChildElements();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @private
|
|
38
|
-
*/
|
|
39
|
-
_initializeDisplay() {
|
|
40
|
-
this.hideBackgroundByDefault();
|
|
41
|
-
if (this.backRect) {
|
|
42
|
-
this.backRect.setOpacity(0);
|
|
43
|
-
this.backRect.setFillColor("transparent");
|
|
44
|
-
}
|
|
45
|
-
// Ensure this node never highlights
|
|
46
|
-
this._makeNodeNonHighlightable();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @private
|
|
51
|
-
*/
|
|
52
|
-
_createOperationElements() {
|
|
53
|
-
const symbol = this._getOperatorSymbol(this.operation);
|
|
54
|
-
// Render each side as a single node token: "(op+value)" e.g., "(-5)"
|
|
55
|
-
const tokenText = `${symbol}${this._valueToString(this.value)}`;
|
|
56
|
-
this.leftToken = new omdVariableNode(tokenText);
|
|
57
|
-
this.rightToken = new omdVariableNode(tokenText);
|
|
58
|
-
// Ensure tokens are fully initialized and sized
|
|
59
|
-
if (typeof this.leftToken.initialize === 'function') this.leftToken.initialize();
|
|
60
|
-
if (typeof this.rightToken.initialize === 'function') this.rightToken.initialize();
|
|
61
|
-
// Propagate font size if available
|
|
62
|
-
const fs = (typeof this.getFontSize === 'function') ? this.getFontSize() : null;
|
|
63
|
-
if (fs && typeof this.leftToken.setFontSize === 'function') this.leftToken.setFontSize(fs);
|
|
64
|
-
if (fs && typeof this.rightToken.setFontSize === 'function') this.rightToken.setFontSize(fs);
|
|
65
|
-
|
|
66
|
-
// Immediately disable highlighting for created elements
|
|
67
|
-
[this.leftToken, this.rightToken].forEach(element => {
|
|
68
|
-
this._disableHighlighting(element);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @private
|
|
74
|
-
*/
|
|
75
|
-
_disableAllInteractions() {
|
|
76
|
-
[this.leftToken, this.rightToken].forEach(element => {
|
|
77
|
-
this._disableElement(element);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* @private
|
|
83
|
-
*/
|
|
84
|
-
_addChildElements() {
|
|
85
|
-
this.addChild(this.leftToken);
|
|
86
|
-
this.addChild(this.rightToken);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Converts the operation string to its display symbol.
|
|
91
|
-
* @param {string} operation
|
|
92
|
-
* @returns {string} The symbol.
|
|
93
|
-
* @private
|
|
94
|
-
*/
|
|
95
|
-
_getOperatorSymbol(operation) {
|
|
96
|
-
return omdOperationDisplayNode.OPERATOR_SYMBOLS[operation] || '';
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Creates an appropriate omdNode for the value.
|
|
101
|
-
* @param {number|string|omdNode} value
|
|
102
|
-
* @returns {omdNode}
|
|
103
|
-
* @private
|
|
104
|
-
*/
|
|
105
|
-
_createValueElement(value) {
|
|
106
|
-
// Not used in combined-token approach, retained for compatibility
|
|
107
|
-
return new omdVariableNode(this._valueToString(value));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
_valueToString(value) {
|
|
111
|
-
if (value instanceof omdNode) {
|
|
112
|
-
return value.toString();
|
|
113
|
-
}
|
|
114
|
-
if (typeof value === 'object' && value !== null) {
|
|
115
|
-
try {
|
|
116
|
-
const NodeClass = getNodeForAST(value);
|
|
117
|
-
const node = new NodeClass(value);
|
|
118
|
-
if (typeof node.initialize === 'function') node.initialize();
|
|
119
|
-
return node.toString();
|
|
120
|
-
} catch {
|
|
121
|
-
return String(value ?? '');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return String(value ?? '');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Disables background and interactions for an element and all its children
|
|
129
|
-
* @param {omdNode} element - The element to disable
|
|
130
|
-
* @private
|
|
131
|
-
*/
|
|
132
|
-
_disableElement(element) {
|
|
133
|
-
if (!element) return;
|
|
134
|
-
|
|
135
|
-
this._hideElementBackground(element);
|
|
136
|
-
this._disableMouseInteractions(element);
|
|
137
|
-
this._disableHighlighting(element);
|
|
138
|
-
this._disableChildElements(element);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* @private
|
|
143
|
-
*/
|
|
144
|
-
_hideElementBackground(element) {
|
|
145
|
-
if (element.backRect) {
|
|
146
|
-
element.hideBackgroundByDefault();
|
|
147
|
-
element.backRect.setOpacity(0);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* @private
|
|
153
|
-
*/
|
|
154
|
-
_disableMouseInteractions(element) {
|
|
155
|
-
if (element.svgObject) {
|
|
156
|
-
element.svgObject.onmouseenter = null;
|
|
157
|
-
element.svgObject.onmouseleave = null;
|
|
158
|
-
element.svgObject.style.cursor = "default";
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* @private
|
|
164
|
-
*/
|
|
165
|
-
_disableHighlighting(element) {
|
|
166
|
-
// Override highlighting methods to prevent highlighting
|
|
167
|
-
element.setHighlight = () => {};
|
|
168
|
-
element.lowlight = () => {};
|
|
169
|
-
element.setFillColor = () => {};
|
|
170
|
-
|
|
171
|
-
// Force background to be transparent and stay that way
|
|
172
|
-
if (element.backRect) {
|
|
173
|
-
element.backRect.setOpacity(0);
|
|
174
|
-
element.backRect.setFillColor("transparent");
|
|
175
|
-
// Override the backRect methods too
|
|
176
|
-
element.backRect.setFillColor = () => {};
|
|
177
|
-
element.backRect.setOpacity = () => {};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Makes this node completely non-highlightable
|
|
183
|
-
* @private
|
|
184
|
-
*/
|
|
185
|
-
_makeNodeNonHighlightable() {
|
|
186
|
-
// Override all highlighting methods on this node
|
|
187
|
-
this.setHighlight = () => {};
|
|
188
|
-
this.lowlight = () => {};
|
|
189
|
-
this.setFillColor = () => {};
|
|
190
|
-
|
|
191
|
-
// Force background to be transparent and keep it that way
|
|
192
|
-
if (this.backRect) {
|
|
193
|
-
this.backRect.setOpacity(0);
|
|
194
|
-
this.backRect.setFillColor("transparent");
|
|
195
|
-
// Override backRect methods to prevent any changes
|
|
196
|
-
this.backRect.setFillColor = () => {};
|
|
197
|
-
this.backRect.setOpacity = () => {};
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* @private
|
|
203
|
-
*/
|
|
204
|
-
_disableChildElements(element) {
|
|
205
|
-
if (element.childList) {
|
|
206
|
-
element.childList.forEach(child => this._disableElement(child));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const childProperties = ['left', 'right', 'base', 'exponent', 'argument', 'expression', 'numerator', 'denominator'];
|
|
210
|
-
childProperties.forEach(prop => {
|
|
211
|
-
if (element[prop]) this._disableElement(element[prop]);
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Computes the dimensions for the operation display node.
|
|
217
|
-
* Calculates the total width including both operations and the gap.
|
|
218
|
-
*/
|
|
219
|
-
computeDimensions() {
|
|
220
|
-
// Ensure children are laid out before measuring
|
|
221
|
-
if (this.leftToken && typeof this.leftToken.updateLayout === 'function') this.leftToken.updateLayout();
|
|
222
|
-
if (this.rightToken && typeof this.rightToken.updateLayout === 'function') this.rightToken.updateLayout();
|
|
223
|
-
|
|
224
|
-
const leftWidth = this.leftToken ? this.leftToken.width : 0;
|
|
225
|
-
const rightWidth = this.rightToken ? this.rightToken.width : 0;
|
|
226
|
-
|
|
227
|
-
// Simple, fixed gap (keeps equals-centering predictable)
|
|
228
|
-
const gap = typeof this.gap === 'number' ? this.gap : 45;
|
|
229
|
-
this.gap = gap;
|
|
230
|
-
|
|
231
|
-
// For sequence alignment
|
|
232
|
-
this.leftClusterWidth = leftWidth;
|
|
233
|
-
|
|
234
|
-
const tallest = Math.max(this.leftToken ? this.leftToken.height : 0, this.rightToken ? this.rightToken.height : 0);
|
|
235
|
-
const verticalPadding = 6; // small, constant padding
|
|
236
|
-
|
|
237
|
-
const width = leftWidth + gap + rightWidth;
|
|
238
|
-
const height = tallest + verticalPadding * 2;
|
|
239
|
-
this.setWidthAndHeight(width, height);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Updates the layout of the operation display node.
|
|
246
|
-
* Positions both operations with appropriate spacing.
|
|
247
|
-
*/
|
|
248
|
-
updateLayout() {
|
|
249
|
-
// Update children layouts first
|
|
250
|
-
[this.leftToken, this.rightToken].forEach(element => {
|
|
251
|
-
element.updateLayout();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
this.computeDimensions();
|
|
255
|
-
|
|
256
|
-
const padding = 6;
|
|
257
|
-
const gap = this.gap || 30;
|
|
258
|
-
|
|
259
|
-
// Calculate positions for both operations using single tokens
|
|
260
|
-
const leftWidthNow = this.leftToken.width;
|
|
261
|
-
|
|
262
|
-
// Position left token
|
|
263
|
-
let x = 0;
|
|
264
|
-
this.leftToken.setPosition(x, (this.height - this.leftToken.height) / 2);
|
|
265
|
-
|
|
266
|
-
// Position right token
|
|
267
|
-
x = leftWidthNow + gap;
|
|
268
|
-
this.rightToken.setPosition(x, (this.height - this.rightToken.height) / 2);
|
|
269
|
-
|
|
270
|
-
// Show both operations
|
|
271
|
-
this.rightToken.show();
|
|
272
|
-
|
|
273
|
-
// Ensure this node remains non-highlightable after layout updates
|
|
274
|
-
this._makeNodeNonHighlightable();
|
|
275
|
-
|
|
276
|
-
super.updateLayout();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Returns the effective width of the left operation cluster for equals alignment
|
|
281
|
-
* Used by omdEquationSequenceNode to align like equations
|
|
282
|
-
* @returns {number}
|
|
283
|
-
*/
|
|
284
|
-
getLeftWidthForAlignment() {
|
|
285
|
-
// Ensure dimensions are up to date
|
|
286
|
-
if (typeof this.leftClusterWidth !== 'number') {
|
|
287
|
-
this.computeDimensions();
|
|
288
|
-
}
|
|
289
|
-
return this.leftClusterWidth || 0;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Shows only the left operation (hides the right side)
|
|
294
|
-
*/
|
|
295
|
-
showLeftOnly() {
|
|
296
|
-
this.rightToken.hide();
|
|
297
|
-
|
|
298
|
-
// Recalculate dimensions for left side only
|
|
299
|
-
const padding = 6;
|
|
300
|
-
const leftWidth = this.leftToken.width;
|
|
301
|
-
const fs2 = (typeof this.getFontSize === 'function') ? this.getFontSize() : 32;
|
|
302
|
-
const tallest = this.leftToken.height;
|
|
303
|
-
const verticalPadding = Math.ceil(fs2 * 0.35);
|
|
304
|
-
this.setWidthAndHeight(leftWidth, tallest + verticalPadding * 2);
|
|
305
|
-
|
|
306
|
-
// Reposition with only left side
|
|
307
|
-
let x = 0;
|
|
308
|
-
this.leftToken.setPosition(x, (this.height - this.leftToken.height) / 2);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
clone() {
|
|
316
|
-
const clone = new omdOperationDisplayNode(this.operation, this.value);
|
|
317
|
-
clone.provenance.push(this.id);
|
|
318
|
-
// Ensure cloned node is also non-highlightable
|
|
319
|
-
clone._makeNodeNonHighlightable();
|
|
320
|
-
return clone;
|
|
321
|
-
}
|
|
1
|
+
import { omdNode } from "./omdNode.js";
|
|
2
|
+
import { omdVariableNode } from "./omdVariableNode.js";
|
|
3
|
+
import { getNodeForAST } from "../core/omdUtilities.js";
|
|
4
|
+
import { getMultiplicationSymbol } from "../config/omdConfigManager.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents a visual node for displaying an operation applied to both sides of an equation.
|
|
8
|
+
* Shows both operations with appropriate spacing between them.
|
|
9
|
+
* Example: -3 -3
|
|
10
|
+
*/
|
|
11
|
+
export class omdOperationDisplayNode extends omdNode {
|
|
12
|
+
static OPERATOR_SYMBOLS = {
|
|
13
|
+
'add': '+',
|
|
14
|
+
'subtract': '-',
|
|
15
|
+
'multiply': '×',
|
|
16
|
+
'divide': '÷'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates an operation display node.
|
|
21
|
+
* @param {string} operation - The type of operation (e.g., 'add', 'subtract', 'multiply', 'divide').
|
|
22
|
+
* @param {number|string|omdNode} value - The value being applied. Can be a number, variable name, or an omdNode.
|
|
23
|
+
*/
|
|
24
|
+
constructor(operation, value) {
|
|
25
|
+
super({});
|
|
26
|
+
this.operation = operation;
|
|
27
|
+
this.value = value;
|
|
28
|
+
this.type = "omdOperationDisplayNode";
|
|
29
|
+
|
|
30
|
+
this._initializeDisplay();
|
|
31
|
+
this._createOperationElements();
|
|
32
|
+
this._disableAllInteractions();
|
|
33
|
+
this._addChildElements();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @private
|
|
38
|
+
*/
|
|
39
|
+
_initializeDisplay() {
|
|
40
|
+
this.hideBackgroundByDefault();
|
|
41
|
+
if (this.backRect) {
|
|
42
|
+
this.backRect.setOpacity(0);
|
|
43
|
+
this.backRect.setFillColor("transparent");
|
|
44
|
+
}
|
|
45
|
+
// Ensure this node never highlights
|
|
46
|
+
this._makeNodeNonHighlightable();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
_createOperationElements() {
|
|
53
|
+
const symbol = this._getOperatorSymbol(this.operation);
|
|
54
|
+
// Render each side as a single node token: "(op+value)" e.g., "(-5)"
|
|
55
|
+
const tokenText = `${symbol}${this._valueToString(this.value)}`;
|
|
56
|
+
this.leftToken = new omdVariableNode(tokenText);
|
|
57
|
+
this.rightToken = new omdVariableNode(tokenText);
|
|
58
|
+
// Ensure tokens are fully initialized and sized
|
|
59
|
+
if (typeof this.leftToken.initialize === 'function') this.leftToken.initialize();
|
|
60
|
+
if (typeof this.rightToken.initialize === 'function') this.rightToken.initialize();
|
|
61
|
+
// Propagate font size if available
|
|
62
|
+
const fs = (typeof this.getFontSize === 'function') ? this.getFontSize() : null;
|
|
63
|
+
if (fs && typeof this.leftToken.setFontSize === 'function') this.leftToken.setFontSize(fs);
|
|
64
|
+
if (fs && typeof this.rightToken.setFontSize === 'function') this.rightToken.setFontSize(fs);
|
|
65
|
+
|
|
66
|
+
// Immediately disable highlighting for created elements
|
|
67
|
+
[this.leftToken, this.rightToken].forEach(element => {
|
|
68
|
+
this._disableHighlighting(element);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
_disableAllInteractions() {
|
|
76
|
+
[this.leftToken, this.rightToken].forEach(element => {
|
|
77
|
+
this._disableElement(element);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @private
|
|
83
|
+
*/
|
|
84
|
+
_addChildElements() {
|
|
85
|
+
this.addChild(this.leftToken);
|
|
86
|
+
this.addChild(this.rightToken);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Converts the operation string to its display symbol.
|
|
91
|
+
* @param {string} operation
|
|
92
|
+
* @returns {string} The symbol.
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
_getOperatorSymbol(operation) {
|
|
96
|
+
return omdOperationDisplayNode.OPERATOR_SYMBOLS[operation] || '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Creates an appropriate omdNode for the value.
|
|
101
|
+
* @param {number|string|omdNode} value
|
|
102
|
+
* @returns {omdNode}
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
_createValueElement(value) {
|
|
106
|
+
// Not used in combined-token approach, retained for compatibility
|
|
107
|
+
return new omdVariableNode(this._valueToString(value));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_valueToString(value) {
|
|
111
|
+
if (value instanceof omdNode) {
|
|
112
|
+
return value.toString();
|
|
113
|
+
}
|
|
114
|
+
if (typeof value === 'object' && value !== null) {
|
|
115
|
+
try {
|
|
116
|
+
const NodeClass = getNodeForAST(value);
|
|
117
|
+
const node = new NodeClass(value);
|
|
118
|
+
if (typeof node.initialize === 'function') node.initialize();
|
|
119
|
+
return node.toString();
|
|
120
|
+
} catch {
|
|
121
|
+
return String(value ?? '');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return String(value ?? '');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Disables background and interactions for an element and all its children
|
|
129
|
+
* @param {omdNode} element - The element to disable
|
|
130
|
+
* @private
|
|
131
|
+
*/
|
|
132
|
+
_disableElement(element) {
|
|
133
|
+
if (!element) return;
|
|
134
|
+
|
|
135
|
+
this._hideElementBackground(element);
|
|
136
|
+
this._disableMouseInteractions(element);
|
|
137
|
+
this._disableHighlighting(element);
|
|
138
|
+
this._disableChildElements(element);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @private
|
|
143
|
+
*/
|
|
144
|
+
_hideElementBackground(element) {
|
|
145
|
+
if (element.backRect) {
|
|
146
|
+
element.hideBackgroundByDefault();
|
|
147
|
+
element.backRect.setOpacity(0);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
_disableMouseInteractions(element) {
|
|
155
|
+
if (element.svgObject) {
|
|
156
|
+
element.svgObject.onmouseenter = null;
|
|
157
|
+
element.svgObject.onmouseleave = null;
|
|
158
|
+
element.svgObject.style.cursor = "default";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @private
|
|
164
|
+
*/
|
|
165
|
+
_disableHighlighting(element) {
|
|
166
|
+
// Override highlighting methods to prevent highlighting
|
|
167
|
+
element.setHighlight = () => {};
|
|
168
|
+
element.lowlight = () => {};
|
|
169
|
+
element.setFillColor = () => {};
|
|
170
|
+
|
|
171
|
+
// Force background to be transparent and stay that way
|
|
172
|
+
if (element.backRect) {
|
|
173
|
+
element.backRect.setOpacity(0);
|
|
174
|
+
element.backRect.setFillColor("transparent");
|
|
175
|
+
// Override the backRect methods too
|
|
176
|
+
element.backRect.setFillColor = () => {};
|
|
177
|
+
element.backRect.setOpacity = () => {};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Makes this node completely non-highlightable
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
_makeNodeNonHighlightable() {
|
|
186
|
+
// Override all highlighting methods on this node
|
|
187
|
+
this.setHighlight = () => {};
|
|
188
|
+
this.lowlight = () => {};
|
|
189
|
+
this.setFillColor = () => {};
|
|
190
|
+
|
|
191
|
+
// Force background to be transparent and keep it that way
|
|
192
|
+
if (this.backRect) {
|
|
193
|
+
this.backRect.setOpacity(0);
|
|
194
|
+
this.backRect.setFillColor("transparent");
|
|
195
|
+
// Override backRect methods to prevent any changes
|
|
196
|
+
this.backRect.setFillColor = () => {};
|
|
197
|
+
this.backRect.setOpacity = () => {};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @private
|
|
203
|
+
*/
|
|
204
|
+
_disableChildElements(element) {
|
|
205
|
+
if (element.childList) {
|
|
206
|
+
element.childList.forEach(child => this._disableElement(child));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const childProperties = ['left', 'right', 'base', 'exponent', 'argument', 'expression', 'numerator', 'denominator'];
|
|
210
|
+
childProperties.forEach(prop => {
|
|
211
|
+
if (element[prop]) this._disableElement(element[prop]);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Computes the dimensions for the operation display node.
|
|
217
|
+
* Calculates the total width including both operations and the gap.
|
|
218
|
+
*/
|
|
219
|
+
computeDimensions() {
|
|
220
|
+
// Ensure children are laid out before measuring
|
|
221
|
+
if (this.leftToken && typeof this.leftToken.updateLayout === 'function') this.leftToken.updateLayout();
|
|
222
|
+
if (this.rightToken && typeof this.rightToken.updateLayout === 'function') this.rightToken.updateLayout();
|
|
223
|
+
|
|
224
|
+
const leftWidth = this.leftToken ? this.leftToken.width : 0;
|
|
225
|
+
const rightWidth = this.rightToken ? this.rightToken.width : 0;
|
|
226
|
+
|
|
227
|
+
// Simple, fixed gap (keeps equals-centering predictable)
|
|
228
|
+
const gap = typeof this.gap === 'number' ? this.gap : 45;
|
|
229
|
+
this.gap = gap;
|
|
230
|
+
|
|
231
|
+
// For sequence alignment
|
|
232
|
+
this.leftClusterWidth = leftWidth;
|
|
233
|
+
|
|
234
|
+
const tallest = Math.max(this.leftToken ? this.leftToken.height : 0, this.rightToken ? this.rightToken.height : 0);
|
|
235
|
+
const verticalPadding = 6; // small, constant padding
|
|
236
|
+
|
|
237
|
+
const width = leftWidth + gap + rightWidth;
|
|
238
|
+
const height = tallest + verticalPadding * 2;
|
|
239
|
+
this.setWidthAndHeight(width, height);
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Updates the layout of the operation display node.
|
|
246
|
+
* Positions both operations with appropriate spacing.
|
|
247
|
+
*/
|
|
248
|
+
updateLayout() {
|
|
249
|
+
// Update children layouts first
|
|
250
|
+
[this.leftToken, this.rightToken].forEach(element => {
|
|
251
|
+
element.updateLayout();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
this.computeDimensions();
|
|
255
|
+
|
|
256
|
+
const padding = 6;
|
|
257
|
+
const gap = this.gap || 30;
|
|
258
|
+
|
|
259
|
+
// Calculate positions for both operations using single tokens
|
|
260
|
+
const leftWidthNow = this.leftToken.width;
|
|
261
|
+
|
|
262
|
+
// Position left token
|
|
263
|
+
let x = 0;
|
|
264
|
+
this.leftToken.setPosition(x, (this.height - this.leftToken.height) / 2);
|
|
265
|
+
|
|
266
|
+
// Position right token
|
|
267
|
+
x = leftWidthNow + gap;
|
|
268
|
+
this.rightToken.setPosition(x, (this.height - this.rightToken.height) / 2);
|
|
269
|
+
|
|
270
|
+
// Show both operations
|
|
271
|
+
this.rightToken.show();
|
|
272
|
+
|
|
273
|
+
// Ensure this node remains non-highlightable after layout updates
|
|
274
|
+
this._makeNodeNonHighlightable();
|
|
275
|
+
|
|
276
|
+
super.updateLayout();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Returns the effective width of the left operation cluster for equals alignment
|
|
281
|
+
* Used by omdEquationSequenceNode to align like equations
|
|
282
|
+
* @returns {number}
|
|
283
|
+
*/
|
|
284
|
+
getLeftWidthForAlignment() {
|
|
285
|
+
// Ensure dimensions are up to date
|
|
286
|
+
if (typeof this.leftClusterWidth !== 'number') {
|
|
287
|
+
this.computeDimensions();
|
|
288
|
+
}
|
|
289
|
+
return this.leftClusterWidth || 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Shows only the left operation (hides the right side)
|
|
294
|
+
*/
|
|
295
|
+
showLeftOnly() {
|
|
296
|
+
this.rightToken.hide();
|
|
297
|
+
|
|
298
|
+
// Recalculate dimensions for left side only
|
|
299
|
+
const padding = 6;
|
|
300
|
+
const leftWidth = this.leftToken.width;
|
|
301
|
+
const fs2 = (typeof this.getFontSize === 'function') ? this.getFontSize() : 32;
|
|
302
|
+
const tallest = this.leftToken.height;
|
|
303
|
+
const verticalPadding = Math.ceil(fs2 * 0.35);
|
|
304
|
+
this.setWidthAndHeight(leftWidth, tallest + verticalPadding * 2);
|
|
305
|
+
|
|
306
|
+
// Reposition with only left side
|
|
307
|
+
let x = 0;
|
|
308
|
+
this.leftToken.setPosition(x, (this.height - this.leftToken.height) / 2);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
clone() {
|
|
316
|
+
const clone = new omdOperationDisplayNode(this.operation, this.value);
|
|
317
|
+
clone.provenance.push(this.id);
|
|
318
|
+
// Ensure cloned node is also non-highlightable
|
|
319
|
+
clone._makeNodeNonHighlightable();
|
|
320
|
+
return clone;
|
|
321
|
+
}
|
|
322
322
|
}
|