@teachinglab/omd 0.2.4 → 0.2.6

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.
@@ -1,308 +1,308 @@
1
- import { omdNode } from "./omdNode.js";
2
- import { getNodeForAST } from "../core/omdUtilities.js";
3
- import { omdConstantNode } from "./omdConstantNode.js";
4
- import { omdPowerNode } from "./omdPowerNode.js";
5
- import { omdBinaryExpressionNode } from "./omdBinaryExpressionNode.js";
6
- import { simplifyStep } from "../simplification/omdSimplification.js";
7
- import { jsvgPath, jsvgLine } from '@teachinglab/jsvg';
8
- /**
9
- * Represents a square root node in the mathematical expression tree
10
- * Handles rendering of radical symbol and expression under the root
11
- * @extends omdNode
12
- */
13
- export class omdSqrtNode extends omdNode {
14
- /**
15
- * Creates a square root node from AST data
16
- * @param {Object} astNodeData - The AST node containing sqrt function information
17
- */
18
- constructor(astNodeData) {
19
- super(astNodeData);
20
- this.type = "omdSqrtNode";
21
- this.args = astNodeData.args || [];
22
-
23
- this.value = this.parseValue();
24
- this.argument = this.createArgumentNode();
25
-
26
- // Populate the argumentNodeList for the mathematical child node
27
- if (this.argument) {
28
- this.argumentNodeList.argument = this.argument;
29
- }
30
-
31
- [ this.radicalPath, this.radicalLine ] = this.createRadicalElements();
32
- }
33
-
34
- parseValue() {
35
- return "sqrt";
36
- }
37
-
38
- /**
39
- * Creates node for the expression under the radical
40
- * @private
41
- */
42
- createArgumentNode() {
43
- if (this.args.length === 0) return null;
44
-
45
- const argAst = this.args[0];
46
- const ArgNodeType = getNodeForAST(argAst);
47
- let child = new ArgNodeType(argAst);
48
- this.addChild(child);
49
-
50
- return child;
51
- }
52
-
53
- /**
54
- * Creates radical symbol and line for sqrt
55
- * @private
56
- */
57
- createRadicalElements() {
58
- // Create custom radical symbol using SVG path
59
- let radicalPath = new jsvgPath();
60
- radicalPath.setStrokeColor('black');
61
- radicalPath.setStrokeWidth(2);
62
- radicalPath.setFillColor('none');
63
- this.addChild(radicalPath);
64
-
65
- // Create the horizontal line over the expression
66
- let radicalLine = new jsvgLine();
67
- radicalLine.setStrokeColor('black');
68
- radicalLine.setStrokeWidth(2);
69
- this.addChild(radicalLine);
70
-
71
- return [radicalPath, radicalLine];
72
- }
73
-
74
- /**
75
- * Calculates the dimensions of the sqrt node and its children
76
- * @override
77
- */
78
- computeDimensions() {
79
- if (!this.argument) return;
80
-
81
- const fontSize = this.getFontSize();
82
- const argFontSize = fontSize * 5/6; // Match rational node scaling
83
-
84
- // Set font size for argument and compute its dimensions
85
- this.argument.setFontSize(argFontSize);
86
- this.argument.computeDimensions();
87
-
88
- // Calculate dimensions using the expression height to size the radical
89
- const ratio = fontSize / this.getRootFontSize();
90
- const spacing = 4 * ratio;
91
-
92
- const argWidth = this.argument.width;
93
- const argHeight = this.argument.height;
94
-
95
- // Radical width is proportional to expression height
96
- const radicalWidth = Math.max(12 * ratio, argHeight * 0.3);
97
-
98
- const totalWidth = radicalWidth + spacing + argWidth + spacing;
99
- const totalHeight = argHeight + 8 * ratio; // Extra height for the radical top
100
-
101
- this.setWidthAndHeight(totalWidth, totalHeight);
102
- }
103
-
104
- /**
105
- * Updates the layout of the sqrt node and its children
106
- * @override
107
- */
108
- updateLayout() {
109
- if (!this.argument) return;
110
-
111
- const fontSize = this.getFontSize();
112
- const ratio = fontSize / this.getRootFontSize();
113
- const spacing = 4 * ratio;
114
-
115
- let currentX = 0;
116
-
117
- // Calculate radical dimensions based on expression
118
- const expressionHeight = this.argument.height;
119
- const radicalWidth = Math.max(12 * ratio, expressionHeight * 0.3);
120
-
121
- // Position the expression first to get its exact position
122
- const expressionX = currentX + radicalWidth + spacing;
123
- const expressionY = (this.height - expressionHeight) / 2;
124
-
125
- this.argument.setPosition(expressionX, expressionY);
126
- this.argument.updateLayout();
127
-
128
- // Draw the radical path using addPoint method
129
- const radicalBottom = expressionY + expressionHeight - 2 * ratio;
130
- const radicalTop = expressionY - 4 * ratio;
131
- const radicalMid = expressionY + expressionHeight * 0.7;
132
-
133
- // Clear previous points and create radical path: short diagonal down, long diagonal up
134
- this.radicalPath.clearPoints();
135
- this.radicalPath.addPoint(currentX + radicalWidth * 0.3, radicalMid);
136
- this.radicalPath.addPoint(currentX + radicalWidth * 0.6, radicalBottom);
137
- this.radicalPath.addPoint(currentX + radicalWidth, radicalTop);
138
- this.radicalPath.updatePath();
139
-
140
- // Position horizontal line above the expression
141
- const lineY = expressionY - 2 * ratio;
142
- const lineStartX = currentX + radicalWidth;
143
- const lineEndX = expressionX + this.argument.width + spacing / 2;
144
- this.radicalLine.setEndpoints(lineStartX, lineY, lineEndX, lineY);
145
- }
146
-
147
- clone() {
148
- let newAstData;
149
- if (typeof this.astNodeData.clone === 'function') {
150
- newAstData = this.astNodeData.clone();
151
- } else {
152
- newAstData = JSON.parse(JSON.stringify(this.astNodeData));
153
- }
154
- const clone = new omdSqrtNode(newAstData);
155
-
156
- // Keep the backRect from the clone, not from 'this'
157
- const backRect = clone.backRect;
158
- clone.removeAllChildren();
159
- clone.addChild(backRect);
160
-
161
- // Create new jsvg elements for the clone
162
- clone.radicalPath = new jsvgPath();
163
- clone.radicalPath.setStrokeColor('black');
164
- clone.radicalPath.setStrokeWidth(2);
165
- clone.radicalPath.setFillColor('none');
166
- clone.addChild(clone.radicalPath);
167
-
168
- clone.radicalLine = new jsvgLine();
169
- clone.radicalLine.setStrokeColor('black');
170
- clone.radicalLine.setStrokeWidth(2);
171
- clone.addChild(clone.radicalLine);
172
-
173
- if (this.argument) {
174
- clone.argument = this.argument.clone();
175
- clone.addChild(clone.argument);
176
-
177
- // Explicitly update the argumentNodeList in the cloned node
178
- clone.argumentNodeList.argument = clone.argument;
179
-
180
- // The crucial step: link the clone to its origin
181
- clone.provenance.push(this.id);
182
- }
183
-
184
- return clone;
185
- }
186
-
187
- /**
188
- * Highlights the sqrt node and its argument
189
- */
190
- highlightAll() {
191
- this.select();
192
-
193
- if (this.argument && this.argument.highlightAll) {
194
- this.argument.highlightAll();
195
- }
196
- }
197
-
198
- /**
199
- * Unhighlights the sqrt node and its argument
200
- */
201
- unhighlightAll() {
202
- this.deselect();
203
-
204
- if (this.argument && this.argument.unhighlightAll) {
205
- this.argument.unhighlightAll();
206
- }
207
- }
208
-
209
- /**
210
- * Converts the omdSqrtNode to a math.js AST node.
211
- * @returns {Object} A math.js-compatible AST node.
212
- */
213
- toMathJSNode() {
214
- const astNode = {
215
- type: 'FunctionNode',
216
- fn: { type: 'SymbolNode', name: 'sqrt', clone: function() { return {...this}; } },
217
- args: this.argument ? [this.argument.toMathJSNode()] : []
218
- };
219
-
220
- // Add a clone method to maintain compatibility with math.js's expectations.
221
- astNode.clone = function() {
222
- const clonedNode = { ...this };
223
- if (this.args) {
224
- clonedNode.args = this.args.map(arg => arg.clone());
225
- }
226
- if (this.fn && typeof this.fn.clone === 'function') {
227
- clonedNode.fn = this.fn.clone();
228
- }
229
- return clonedNode;
230
- };
231
- return astNode;
232
- }
233
- /**
234
- * Converts the square root node to a string representation.
235
- * @returns {string} The string representation.
236
- */
237
- toString() {
238
- return `sqrt(${this.argument ? this.argument.toString() : ''})`;
239
- }
240
-
241
- /**
242
- * Evaluate the root expression.
243
- * @param {Object} variables - Variable name to value mapping
244
- * @returns {number} The evaluated root
245
- */
246
- evaluate(variables = {}) {
247
- if (!this.argument || !this.argument.evaluate) {
248
- return NaN;
249
- }
250
- const radicandValue = this.argument.evaluate(variables);
251
- if (radicandValue < 0) {
252
- return NaN; // Or handle complex numbers if desired
253
- }
254
- return Math.sqrt(radicandValue);
255
- }
256
-
257
- /**
258
- * Check if this is a square root (index = 2).
259
- * @returns {boolean}
260
- */
261
- isSquareRoot() {
262
- return true;
263
- }
264
-
265
- /**
266
- * Check if this is a cube root (index = 3).
267
- * @returns {boolean}
268
- */
269
- isCubeRoot() {
270
- return false;
271
- }
272
-
273
- /**
274
- * Convert to equivalent power notation.
275
- * @returns {omdPowerNode} Equivalent power expression
276
- */
277
- toPowerForm() {
278
- if (!this.argument) return null;
279
-
280
- const powerAst = {
281
- type: 'OperatorNode', op: '^', fn: 'pow',
282
- args: [
283
- this.argument.toMathJSNode(),
284
- omdConstantNode.fromNumber(0.5).toMathJSNode()
285
- ]
286
- };
287
- return new omdPowerNode(powerAst);
288
- }
289
-
290
- /**
291
- * Create a root node from a string.
292
- * @static
293
- * @param {string} expressionString - Expression with root
294
- * @returns {omdSqrtNode}
295
- */
296
- static fromString(expressionString) {
297
- try {
298
- const ast = window.math.parse(expressionString);
299
- if (ast.type === 'FunctionNode' && ast.fn.name === 'sqrt') {
300
- return new omdSqrtNode(ast);
301
- }
302
- throw new Error("Expression is not a 'sqrt' function.");
303
- } catch (error) {
304
- console.error("Failed to create sqrt node from string:", error);
305
- throw error;
306
- }
307
- }
1
+ import { omdNode } from "./omdNode.js";
2
+ import { getNodeForAST } from "../core/omdUtilities.js";
3
+ import { omdConstantNode } from "./omdConstantNode.js";
4
+ import { omdPowerNode } from "./omdPowerNode.js";
5
+ import { omdBinaryExpressionNode } from "./omdBinaryExpressionNode.js";
6
+ import { simplifyStep } from "../simplification/omdSimplification.js";
7
+ import { jsvgPath, jsvgLine } from '@teachinglab/jsvg';
8
+ /**
9
+ * Represents a square root node in the mathematical expression tree
10
+ * Handles rendering of radical symbol and expression under the root
11
+ * @extends omdNode
12
+ */
13
+ export class omdSqrtNode extends omdNode {
14
+ /**
15
+ * Creates a square root node from AST data
16
+ * @param {Object} astNodeData - The AST node containing sqrt function information
17
+ */
18
+ constructor(astNodeData) {
19
+ super(astNodeData);
20
+ this.type = "omdSqrtNode";
21
+ this.args = astNodeData.args || [];
22
+
23
+ this.value = this.parseValue();
24
+ this.argument = this.createArgumentNode();
25
+
26
+ // Populate the argumentNodeList for the mathematical child node
27
+ if (this.argument) {
28
+ this.argumentNodeList.argument = this.argument;
29
+ }
30
+
31
+ [ this.radicalPath, this.radicalLine ] = this.createRadicalElements();
32
+ }
33
+
34
+ parseValue() {
35
+ return "sqrt";
36
+ }
37
+
38
+ /**
39
+ * Creates node for the expression under the radical
40
+ * @private
41
+ */
42
+ createArgumentNode() {
43
+ if (this.args.length === 0) return null;
44
+
45
+ const argAst = this.args[0];
46
+ const ArgNodeType = getNodeForAST(argAst);
47
+ let child = new ArgNodeType(argAst);
48
+ this.addChild(child);
49
+
50
+ return child;
51
+ }
52
+
53
+ /**
54
+ * Creates radical symbol and line for sqrt
55
+ * @private
56
+ */
57
+ createRadicalElements() {
58
+ // Create custom radical symbol using SVG path
59
+ let radicalPath = new jsvgPath();
60
+ radicalPath.setStrokeColor('black');
61
+ radicalPath.setStrokeWidth(2);
62
+ radicalPath.setFillColor('none');
63
+ this.addChild(radicalPath);
64
+
65
+ // Create the horizontal line over the expression
66
+ let radicalLine = new jsvgLine();
67
+ radicalLine.setStrokeColor('black');
68
+ radicalLine.setStrokeWidth(2);
69
+ this.addChild(radicalLine);
70
+
71
+ return [radicalPath, radicalLine];
72
+ }
73
+
74
+ /**
75
+ * Calculates the dimensions of the sqrt node and its children
76
+ * @override
77
+ */
78
+ computeDimensions() {
79
+ if (!this.argument) return;
80
+
81
+ const fontSize = this.getFontSize();
82
+ const argFontSize = fontSize * 5/6; // Match rational node scaling
83
+
84
+ // Set font size for argument and compute its dimensions
85
+ this.argument.setFontSize(argFontSize);
86
+ this.argument.computeDimensions();
87
+
88
+ // Calculate dimensions using the expression height to size the radical
89
+ const ratio = fontSize / this.getRootFontSize();
90
+ const spacing = 4 * ratio;
91
+
92
+ const argWidth = this.argument.width;
93
+ const argHeight = this.argument.height;
94
+
95
+ // Radical width is proportional to expression height
96
+ const radicalWidth = Math.max(12 * ratio, argHeight * 0.3);
97
+
98
+ const totalWidth = radicalWidth + spacing + argWidth + spacing;
99
+ const totalHeight = argHeight + 8 * ratio; // Extra height for the radical top
100
+
101
+ this.setWidthAndHeight(totalWidth, totalHeight);
102
+ }
103
+
104
+ /**
105
+ * Updates the layout of the sqrt node and its children
106
+ * @override
107
+ */
108
+ updateLayout() {
109
+ if (!this.argument) return;
110
+
111
+ const fontSize = this.getFontSize();
112
+ const ratio = fontSize / this.getRootFontSize();
113
+ const spacing = 4 * ratio;
114
+
115
+ let currentX = 0;
116
+
117
+ // Calculate radical dimensions based on expression
118
+ const expressionHeight = this.argument.height;
119
+ const radicalWidth = Math.max(12 * ratio, expressionHeight * 0.3);
120
+
121
+ // Position the expression first to get its exact position
122
+ const expressionX = currentX + radicalWidth + spacing;
123
+ const expressionY = (this.height - expressionHeight) / 2;
124
+
125
+ this.argument.setPosition(expressionX, expressionY);
126
+ this.argument.updateLayout();
127
+
128
+ // Draw the radical path using addPoint method
129
+ const radicalBottom = expressionY + expressionHeight - 2 * ratio;
130
+ const radicalTop = expressionY - 4 * ratio;
131
+ const radicalMid = expressionY + expressionHeight * 0.7;
132
+
133
+ // Clear previous points and create radical path: short diagonal down, long diagonal up
134
+ this.radicalPath.clearPoints();
135
+ this.radicalPath.addPoint(currentX + radicalWidth * 0.3, radicalMid);
136
+ this.radicalPath.addPoint(currentX + radicalWidth * 0.6, radicalBottom);
137
+ this.radicalPath.addPoint(currentX + radicalWidth, radicalTop);
138
+ this.radicalPath.updatePath();
139
+
140
+ // Position horizontal line above the expression
141
+ const lineY = expressionY - 2 * ratio;
142
+ const lineStartX = currentX + radicalWidth;
143
+ const lineEndX = expressionX + this.argument.width + spacing / 2;
144
+ this.radicalLine.setEndpoints(lineStartX, lineY, lineEndX, lineY);
145
+ }
146
+
147
+ clone() {
148
+ let newAstData;
149
+ if (typeof this.astNodeData.clone === 'function') {
150
+ newAstData = this.astNodeData.clone();
151
+ } else {
152
+ newAstData = JSON.parse(JSON.stringify(this.astNodeData));
153
+ }
154
+ const clone = new omdSqrtNode(newAstData);
155
+
156
+ // Keep the backRect from the clone, not from 'this'
157
+ const backRect = clone.backRect;
158
+ clone.removeAllChildren();
159
+ clone.addChild(backRect);
160
+
161
+ // Create new jsvg elements for the clone
162
+ clone.radicalPath = new jsvgPath();
163
+ clone.radicalPath.setStrokeColor('black');
164
+ clone.radicalPath.setStrokeWidth(2);
165
+ clone.radicalPath.setFillColor('none');
166
+ clone.addChild(clone.radicalPath);
167
+
168
+ clone.radicalLine = new jsvgLine();
169
+ clone.radicalLine.setStrokeColor('black');
170
+ clone.radicalLine.setStrokeWidth(2);
171
+ clone.addChild(clone.radicalLine);
172
+
173
+ if (this.argument) {
174
+ clone.argument = this.argument.clone();
175
+ clone.addChild(clone.argument);
176
+
177
+ // Explicitly update the argumentNodeList in the cloned node
178
+ clone.argumentNodeList.argument = clone.argument;
179
+
180
+ // The crucial step: link the clone to its origin
181
+ clone.provenance.push(this.id);
182
+ }
183
+
184
+ return clone;
185
+ }
186
+
187
+ /**
188
+ * Highlights the sqrt node and its argument
189
+ */
190
+ highlightAll() {
191
+ this.select();
192
+
193
+ if (this.argument && this.argument.highlightAll) {
194
+ this.argument.highlightAll();
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Unhighlights the sqrt node and its argument
200
+ */
201
+ unhighlightAll() {
202
+ this.deselect();
203
+
204
+ if (this.argument && this.argument.unhighlightAll) {
205
+ this.argument.unhighlightAll();
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Converts the omdSqrtNode to a math.js AST node.
211
+ * @returns {Object} A math.js-compatible AST node.
212
+ */
213
+ toMathJSNode() {
214
+ const astNode = {
215
+ type: 'FunctionNode',
216
+ fn: { type: 'SymbolNode', name: 'sqrt', clone: function() { return {...this}; } },
217
+ args: this.argument ? [this.argument.toMathJSNode()] : []
218
+ };
219
+
220
+ // Add a clone method to maintain compatibility with math.js's expectations.
221
+ astNode.clone = function() {
222
+ const clonedNode = { ...this };
223
+ if (this.args) {
224
+ clonedNode.args = this.args.map(arg => arg.clone());
225
+ }
226
+ if (this.fn && typeof this.fn.clone === 'function') {
227
+ clonedNode.fn = this.fn.clone();
228
+ }
229
+ return clonedNode;
230
+ };
231
+ return astNode;
232
+ }
233
+ /**
234
+ * Converts the square root node to a string representation.
235
+ * @returns {string} The string representation.
236
+ */
237
+ toString() {
238
+ return `sqrt(${this.argument ? this.argument.toString() : ''})`;
239
+ }
240
+
241
+ /**
242
+ * Evaluate the root expression.
243
+ * @param {Object} variables - Variable name to value mapping
244
+ * @returns {number} The evaluated root
245
+ */
246
+ evaluate(variables = {}) {
247
+ if (!this.argument || !this.argument.evaluate) {
248
+ return NaN;
249
+ }
250
+ const radicandValue = this.argument.evaluate(variables);
251
+ if (radicandValue < 0) {
252
+ return NaN; // Or handle complex numbers if desired
253
+ }
254
+ return Math.sqrt(radicandValue);
255
+ }
256
+
257
+ /**
258
+ * Check if this is a square root (index = 2).
259
+ * @returns {boolean}
260
+ */
261
+ isSquareRoot() {
262
+ return true;
263
+ }
264
+
265
+ /**
266
+ * Check if this is a cube root (index = 3).
267
+ * @returns {boolean}
268
+ */
269
+ isCubeRoot() {
270
+ return false;
271
+ }
272
+
273
+ /**
274
+ * Convert to equivalent power notation.
275
+ * @returns {omdPowerNode} Equivalent power expression
276
+ */
277
+ toPowerForm() {
278
+ if (!this.argument) return null;
279
+
280
+ const powerAst = {
281
+ type: 'OperatorNode', op: '^', fn: 'pow',
282
+ args: [
283
+ this.argument.toMathJSNode(),
284
+ omdConstantNode.fromNumber(0.5).toMathJSNode()
285
+ ]
286
+ };
287
+ return new omdPowerNode(powerAst);
288
+ }
289
+
290
+ /**
291
+ * Create a root node from a string.
292
+ * @static
293
+ * @param {string} expressionString - Expression with root
294
+ * @returns {omdSqrtNode}
295
+ */
296
+ static fromString(expressionString) {
297
+ try {
298
+ const ast = window.math.parse(expressionString);
299
+ if (ast.type === 'FunctionNode' && ast.fn.name === 'sqrt') {
300
+ return new omdSqrtNode(ast);
301
+ }
302
+ throw new Error("Expression is not a 'sqrt' function.");
303
+ } catch (error) {
304
+ console.error("Failed to create sqrt node from string:", error);
305
+ throw error;
306
+ }
307
+ }
308
308
  }
@@ -317,6 +317,9 @@ export class omdStepVisualizerInteractiveSteps {
317
317
  * @private
318
318
  */
319
319
  setupStepInteractions(stepBox) {
320
+ // Store the original background color to restore on mouseleave
321
+ const originalBackgroundColor = stepBox.div.style.backgroundColor || '';
322
+
320
323
  // Hover effects
321
324
  stepBox.div.addEventListener('mouseenter', () => {
322
325
  stepBox.div.style.backgroundColor = omdColor.mediumGray; // Slightly darker version of explainColor
@@ -327,7 +330,8 @@ export class omdStepVisualizerInteractiveSteps {
327
330
  });
328
331
 
329
332
  stepBox.div.addEventListener('mouseleave', () => {
330
- stepBox.div.style.backgroundColor = 'transparent';
333
+ // Restore the original background color instead of setting to transparent
334
+ stepBox.div.style.backgroundColor = originalBackgroundColor;
331
335
  stepBox.div.style.transform = 'translateX(0)';
332
336
 
333
337
  // Call hover callback if provided
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teachinglab/omd",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "omd",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -131,7 +131,7 @@ export class omdMetaExpression extends jsvgGroup
131
131
  this.backRect.setOpacity(1.0);
132
132
  } else {
133
133
  // Reset to the default background state
134
- this.backRect.setFillColor(omdColor.lightGray);
134
+ this.backRect.setFillColor(this._backgroundStyle?.backgroundColor ?? omdColor.lightGray);
135
135
  if (!this.defaultOpaqueBack) {
136
136
  this.backRect.setOpacity(0.01);
137
137
  }