UNPKG

@teachinglab/omd

Version:

omd

308 lines (261 loc) 10.5 kB
import { omdNode } from "./omdNode.js"; import { getNodeForAST } from "../core/omdUtilities.js"; import { omdConstantNode } from "./omdConstantNode.js"; import { omdPowerNode } from "./omdPowerNode.js"; import { omdBinaryExpressionNode } from "./omdBinaryExpressionNode.js"; import { simplifyStep } from "../simplification/omdSimplification.js"; import { jsvgPath, jsvgLine } from '@teachinglab/jsvg'; /** * Represents a square root node in the mathematical expression tree * Handles rendering of radical symbol and expression under the root * @extends omdNode */ export class omdSqrtNode extends omdNode { /** * Creates a square root node from AST data * @param {Object} astNodeData - The AST node containing sqrt function information */ constructor(astNodeData) { super(astNodeData); this.type = "omdSqrtNode"; this.args = astNodeData.args || []; this.value = this.parseValue(); this.argument = this.createArgumentNode(); // Populate the argumentNodeList for the mathematical child node if (this.argument) { this.argumentNodeList.argument = this.argument; } [ this.radicalPath, this.radicalLine ] = this.createRadicalElements(); } parseValue() { return "sqrt"; } /** * Creates node for the expression under the radical * @private */ createArgumentNode() { if (this.args.length === 0) return null; const argAst = this.args[0]; const ArgNodeType = getNodeForAST(argAst); let child = new ArgNodeType(argAst); this.addChild(child); return child; } /** * Creates radical symbol and line for sqrt * @private */ createRadicalElements() { // Create custom radical symbol using SVG path let radicalPath = new jsvgPath(); radicalPath.setStrokeColor('black'); radicalPath.setStrokeWidth(2); radicalPath.setFillColor('none'); this.addChild(radicalPath); // Create the horizontal line over the expression let radicalLine = new jsvgLine(); radicalLine.setStrokeColor('black'); radicalLine.setStrokeWidth(2); this.addChild(radicalLine); return [radicalPath, radicalLine]; } /** * Calculates the dimensions of the sqrt node and its children * @override */ computeDimensions() { if (!this.argument) return; const fontSize = this.getFontSize(); const argFontSize = fontSize * 5/6; // Match rational node scaling // Set font size for argument and compute its dimensions this.argument.setFontSize(argFontSize); this.argument.computeDimensions(); // Calculate dimensions using the expression height to size the radical const ratio = fontSize / this.getRootFontSize(); const spacing = 4 * ratio; const argWidth = this.argument.width; const argHeight = this.argument.height; // Radical width is proportional to expression height const radicalWidth = Math.max(12 * ratio, argHeight * 0.3); const totalWidth = radicalWidth + spacing + argWidth + spacing; const totalHeight = argHeight + 8 * ratio; // Extra height for the radical top this.setWidthAndHeight(totalWidth, totalHeight); } /** * Updates the layout of the sqrt node and its children * @override */ updateLayout() { if (!this.argument) return; const fontSize = this.getFontSize(); const ratio = fontSize / this.getRootFontSize(); const spacing = 4 * ratio; let currentX = 0; // Calculate radical dimensions based on expression const expressionHeight = this.argument.height; const radicalWidth = Math.max(12 * ratio, expressionHeight * 0.3); // Position the expression first to get its exact position const expressionX = currentX + radicalWidth + spacing; const expressionY = (this.height - expressionHeight) / 2; this.argument.setPosition(expressionX, expressionY); this.argument.updateLayout(); // Draw the radical path using addPoint method const radicalBottom = expressionY + expressionHeight - 2 * ratio; const radicalTop = expressionY - 4 * ratio; const radicalMid = expressionY + expressionHeight * 0.7; // Clear previous points and create radical path: short diagonal down, long diagonal up this.radicalPath.clearPoints(); this.radicalPath.addPoint(currentX + radicalWidth * 0.3, radicalMid); this.radicalPath.addPoint(currentX + radicalWidth * 0.6, radicalBottom); this.radicalPath.addPoint(currentX + radicalWidth, radicalTop); this.radicalPath.updatePath(); // Position horizontal line above the expression const lineY = expressionY - 2 * ratio; const lineStartX = currentX + radicalWidth; const lineEndX = expressionX + this.argument.width + spacing / 2; this.radicalLine.setEndpoints(lineStartX, lineY, lineEndX, lineY); } clone() { let newAstData; if (typeof this.astNodeData.clone === 'function') { newAstData = this.astNodeData.clone(); } else { newAstData = JSON.parse(JSON.stringify(this.astNodeData)); } const clone = new omdSqrtNode(newAstData); // Keep the backRect from the clone, not from 'this' const backRect = clone.backRect; clone.removeAllChildren(); clone.addChild(backRect); // Create new jsvg elements for the clone clone.radicalPath = new jsvgPath(); clone.radicalPath.setStrokeColor('black'); clone.radicalPath.setStrokeWidth(2); clone.radicalPath.setFillColor('none'); clone.addChild(clone.radicalPath); clone.radicalLine = new jsvgLine(); clone.radicalLine.setStrokeColor('black'); clone.radicalLine.setStrokeWidth(2); clone.addChild(clone.radicalLine); if (this.argument) { clone.argument = this.argument.clone(); clone.addChild(clone.argument); // Explicitly update the argumentNodeList in the cloned node clone.argumentNodeList.argument = clone.argument; // The crucial step: link the clone to its origin clone.provenance.push(this.id); } return clone; } /** * Highlights the sqrt node and its argument */ highlightAll() { this.select(); if (this.argument && this.argument.highlightAll) { this.argument.highlightAll(); } } /** * Unhighlights the sqrt node and its argument */ unhighlightAll() { this.deselect(); if (this.argument && this.argument.unhighlightAll) { this.argument.unhighlightAll(); } } /** * Converts the omdSqrtNode to a math.js AST node. * @returns {Object} A math.js-compatible AST node. */ toMathJSNode() { const astNode = { type: 'FunctionNode', fn: { type: 'SymbolNode', name: 'sqrt', clone: function() { return {...this}; } }, args: this.argument ? [this.argument.toMathJSNode()] : [] }; // Add a clone method to maintain compatibility with math.js's expectations. astNode.clone = function() { const clonedNode = { ...this }; if (this.args) { clonedNode.args = this.args.map(arg => arg.clone()); } if (this.fn && typeof this.fn.clone === 'function') { clonedNode.fn = this.fn.clone(); } return clonedNode; }; return astNode; } /** * Converts the square root node to a string representation. * @returns {string} The string representation. */ toString() { return `sqrt(${this.argument ? this.argument.toString() : ''})`; } /** * Evaluate the root expression. * @param {Object} variables - Variable name to value mapping * @returns {number} The evaluated root */ evaluate(variables = {}) { if (!this.argument || !this.argument.evaluate) { return NaN; } const radicandValue = this.argument.evaluate(variables); if (radicandValue < 0) { return NaN; // Or handle complex numbers if desired } return Math.sqrt(radicandValue); } /** * Check if this is a square root (index = 2). * @returns {boolean} */ isSquareRoot() { return true; } /** * Check if this is a cube root (index = 3). * @returns {boolean} */ isCubeRoot() { return false; } /** * Convert to equivalent power notation. * @returns {omdPowerNode} Equivalent power expression */ toPowerForm() { if (!this.argument) return null; const powerAst = { type: 'OperatorNode', op: '^', fn: 'pow', args: [ this.argument.toMathJSNode(), omdConstantNode.fromNumber(0.5).toMathJSNode() ] }; return new omdPowerNode(powerAst); } /** * Create a root node from a string. * @static * @param {string} expressionString - Expression with root * @returns {omdSqrtNode} */ static fromString(expressionString) { try { const ast = window.math.parse(expressionString); if (ast.type === 'FunctionNode' && ast.fn.name === 'sqrt') { return new omdSqrtNode(ast); } throw new Error("Expression is not a 'sqrt' function."); } catch (error) { console.error("Failed to create sqrt node from string:", error); throw error; } } }