UNPKG

@teachinglab/omd

Version:

omd

247 lines (209 loc) 9.11 kB
import { omdColor } from '../../src/omdColor.js'; import { omdEquationNode } from '../nodes/omdEquationNode.js'; import { omdStepVisualizerNodeUtils } from '../utils/omdStepVisualizerNodeUtils.js'; import { omdTreeDiff } from '../utils/omdTreeDiff.js'; /** * Step visualizer highlighting functionality using robust tree diff algorithm. * This class implements optimal substructure matching to identify truly changed nodes * between mathematical equation steps, eliminating the need for special cases. */ export class omdStepVisualizerHighlighting { constructor(stepVisualizer) { this.stepVisualizer = stepVisualizer; this.highlightedNodes = new Set(); this.educationalMode = true; // Enable highlighting of pedagogical simplifications } /** * Main entry point for highlighting nodes based on robust tree diff. * @param {number} dotIndex - Index of the dot/step. */ highlightAffectedNodes(dotIndex, isOperation = false) { this.clearHighlights(); const dot = this.stepVisualizer.stepDots[dotIndex]; if (!dot || !dot.equationRef) { console.error("Highlighting failed: No equation reference for dot", dotIndex); return; } const currentEquation = dot.equationRef; const equationIndex = this.stepVisualizer.steps.indexOf(currentEquation); const previousEquation = this._findNearestVisibleEquationAbove(equationIndex); if (!previousEquation) { const leafNodes = omdStepVisualizerNodeUtils.findLeafNodes(currentEquation); leafNodes.forEach(node => this._highlightNode(node)); return; } const previousIndex = this.stepVisualizer.steps.indexOf(previousEquation); // Use robust tree diff algorithm to find changed nodes const changedNodes = omdTreeDiff.findChangedNodes(previousEquation, currentEquation, { educationalMode: this.educationalMode }); // Apply highlighting to changed nodes changedNodes.forEach(node => this._highlightNode(node)); // Use provenance to highlight related nodes in the previous equation if (!isOperation) { this._highlightProvenanceNodes(changedNodes, previousEquation); } } /** * Highlights a single node with the standard explanation color. * @param {omdNode} node - The node to highlight. * @private */ _highlightNode(node) { if (node && typeof node.setExplainHighlight === 'function') { node.setExplainHighlight(true); this.highlightedNodes.add(node); } } /** * Finds the nearest visible equation above the given index. * @param {number} currentIndex - Index of current equation. * @returns {omdEquationNode|null} The nearest visible equation above, or null. * @private */ _findNearestVisibleEquationAbove(currentIndex) { for (let i = currentIndex - 1; i >= 0; i--) { const step = this.stepVisualizer.steps[i]; if (step instanceof omdEquationNode && step.visible !== false) { return step; } } return null; } /** * Clears all highlights managed by this class. */ clearHighlights() { this.highlightedNodes.forEach(node => { if (node && typeof node.setExplainHighlight === 'function') { node.setExplainHighlight(false); } }); this.highlightedNodes.clear(); this.stepVisualizer.stepVisualizerHighlights.clear(); } /** * Clears ALL explain highlights from the entire sequence, not just tracked ones. * This is more thorough than clearHighlights() and should be used when * the step visualizer is disabled or when we need to ensure no stale highlights remain. */ clearAllExplainHighlights() { // Clear tracked highlights first this.clearHighlights(); // Also clear any explain highlights from the entire sequence tree const rootNode = this.stepVisualizer.getRootNode ? this.stepVisualizer.getRootNode() : null; if (rootNode) { this._clearExplainHighlightsFromTree(rootNode); } } /** * Recursively clears explain highlights from an entire tree * @param {omdNode} node - The root node to start clearing from * @private */ _clearExplainHighlightsFromTree(node) { if (node && typeof node.setExplainHighlight === 'function') { node.setExplainHighlight(false); } // Recursively clear from children if (node && node.childList && Array.isArray(node.childList)) { node.childList.forEach(child => { this._clearExplainHighlightsFromTree(child); }); } // Also check argumentNodeList if it exists if (node && node.argumentNodeList) { Object.values(node.argumentNodeList).forEach(child => { this._clearExplainHighlightsFromTree(child); }); } } /** * Highlights nodes in the previous equation based on provenance from changed nodes. * This creates a visual connection between the current changes and their origins. * @param {Array} changedNodes - Array of changed nodes in the current equation * @param {omdEquationNode} previousEquation - The previous equation to highlight nodes in * @private */ _highlightProvenanceNodes(changedNodes, previousEquation) { const rootNode = previousEquation.getRootNode(); if (!rootNode || !rootNode.nodeMap) { return; } const currentIndex = this.stepVisualizer.steps.indexOf(previousEquation) + 1; let targetEquation = null; // Search backwards for the first visible equation for (let i = currentIndex - 1; i >= 0; i--) { const step = this.stepVisualizer.steps[i]; if (step instanceof omdEquationNode && step.visible !== false) { targetEquation = step; break; } } if (!targetEquation) { return; } const visited = new Set(); const nodesToProcess = []; // Start with the changed nodes' provenance changedNodes.forEach(node => { if (node.provenance && Array.isArray(node.provenance)) { node.provenance.forEach(id => { if (!visited.has(id)) { visited.add(id); nodesToProcess.push(id); } }); } }); // Process provenance IDs while (nodesToProcess.length > 0) { const id = nodesToProcess.shift(); const provenanceNode = rootNode.nodeMap.get(id); if (provenanceNode) { if (this._belongsToEquation(provenanceNode, targetEquation)) { this._highlightProvenanceNode(provenanceNode); } // Add this node's provenance to the processing queue if (provenanceNode.provenance && Array.isArray(provenanceNode.provenance)) { provenanceNode.provenance.forEach(subId => { if (!visited.has(subId)) { visited.add(subId); nodesToProcess.push(subId); } }); } } else { } } } /** * Checks if a node belongs to a specific equation by traversing up the tree * @param {omdNode} node - The node to check * @param {omdEquationNode} targetEquation - The equation to check against * @returns {boolean} True if the node belongs to the equation * @private */ _belongsToEquation(node, targetEquation) { let current = node; while (current) { if (current === targetEquation) { return true; } current = current.parent; } return false; } /** * Highlights a provenance node with secondary highlighting style * @param {omdNode} node - The node to highlight with provenance style * @private */ _highlightProvenanceNode(node) { if (node && typeof node.setExplainHighlight === 'function') { // Use a slightly different color or style for provenance if desired node.setExplainHighlight(true, omdColor.provenanceColor); this.highlightedNodes.add(node); } } }