UNPKG

@teachinglab/omd

Version:

omd

291 lines (240 loc) 9.96 kB
import { omdColor } from "./omdColor.js"; import { jsvgGroup, jsvgRect } from "@teachinglab/jsvg"; export class omdMetaExpression extends jsvgGroup { constructor( V = 1 ) { // initialization super(); this.type = ""; this.value = V; this.defaultOpaqueBack = true; this.backRect = new jsvgRect(); this.backRect.setWidthAndHeight( 30,30 ); this.backRect.setCornerRadius( 5 ); this.backRect.setFillColor( this.getBackgroundColor() ); this.addChild( this.backRect ); // Old events for selection - we will replace these with provenance highlighting // this.svgObject.onmouseenter = this.select.bind(this); // this.svgObject.onmouseleave = this.deselect.bind(this); // New events for provenance highlighting this.svgObject.onmouseenter = this.highlightProvenance.bind(this); this.svgObject.onmouseleave = this.clearProvenance.bind(this); this.svgObject.style.cursor = "pointer"; } /* Gerard - ADDED */ setWidthAndHeight(w, h) { super.setWidthAndHeight(w, h); this.backRect.setWidthAndHeight(w, h); } setFontSize(fontSize) { this.fontSize = fontSize; // Propagate font size to all children that support it if (this.childList) { this.childList.forEach(child => { if (child && typeof child.setFontSize === 'function') { child.setFontSize(fontSize); } }); } } getFontSize() { if (this.fontSize) return this.fontSize; if (this.parent && typeof this.parent.getFontSize === 'function') { return this.parent.getFontSize(); } return 16; // Default fallback } getRootFontSize() { if (this.parent && (this.parent instanceof omdMetaExpression)) { return this.parent.getRootFontSize(); } // No parent, so this is the root. Return its font size or default. return this.fontSize || 16; } getBackgroundColor() { return this._backgroundStyle?.backgroundColor ?? omdColor.lightGray; } // ===== PROVENANCE HIGHLIGHTING ===== highlightProvenance(event, color = omdColor.hiliteColor, minStepNumber = -Infinity) { // Prevent event from bubbling up to parent containers and causing flickering event.stopPropagation(); const rootNode = this.getRootNode(); if (!rootNode || !rootNode.nodeMap) return; // Clear any previous highlights from the entire sequence rootNode.clearProvenanceHighlights(); // Highlight the node being hovered over this.highlight(color); // Use an iterative approach to traverse the provenance chain let nodesToProcess = [...this.provenance]; const visited = new Set(nodesToProcess); while (nodesToProcess.length > 0) { const currentId = nodesToProcess.shift(); // Get the next node to process const node = rootNode.nodeMap.get(currentId); if (node) { node.highlight(color); if (node.provenance) { node.provenance.forEach(pId => { if (!visited.has(pId)) { visited.add(pId); nodesToProcess.push(pId); } }); } } } } clearProvenance(event) { event.stopPropagation(); const rootNode = this.getRootNode(); if (rootNode) { rootNode.clearProvenanceHighlights(); } } highlight(color) { this.backRect.setFillColor(color); this.backRect.setOpacity(1.0); // also highlight children this.childList.forEach((child) => { if (child instanceof omdMetaExpression) child.highlight(color); }); } clearProvenanceHighlights() { // Preserve step visualizer highlights (explain highlights) if present if (this.isExplainHighlighted) { // Restore the explain highlight color and ensure full opacity this.backRect.setFillColor(omdColor.explainColor); this.backRect.setOpacity(1.0); } else if (this.isProvenanceHighlighted) { // Preserve step visualizer provenance highlights this.backRect.setFillColor(omdColor.explainColor); this.backRect.setOpacity(1.0); } else { // Reset to the default background state this.backRect.setFillColor(this._backgroundStyle?.backgroundColor ?? omdColor.lightGray); if (!this.defaultOpaqueBack) { this.backRect.setOpacity(0.01); } } // Recursively clear highlights for all children in the tree this.childList.forEach((child) => { if (child instanceof omdMetaExpression && typeof child.clearProvenanceHighlights === 'function') { child.clearProvenanceHighlights(); } }); } /** * Recursively applies or clears a persistent "explain" highlight. * This is used by the step visualizer to mark nodes that have changed between steps. * The highlight is preserved during hover events. * @param {boolean} isOn - True to apply the highlight, false to clear it. * @param {string} color - The color to use for the highlight. */ setExplainHighlight(isOn = true, color = omdColor.explainColor) { this.isExplainHighlighted = isOn; if (isOn) { this.backRect.setFillColor(color); this.backRect.setOpacity(1.0); } else { // Reset to default state this.backRect.setFillColor(this.getBackgroundColor()); if (!this.defaultOpaqueBack) { this.backRect.setOpacity(0.01); } } // Use the same traversal logic as hover highlighting to recurse if (this.argumentNodeList) { Object.values(this.argumentNodeList).forEach(child => { if (child && typeof child.setExplainHighlight === 'function') { child.setExplainHighlight(isOn, color); } }); } } getRootNode() { let current = this; // Traverse upwards as long as the parent is also part of the omd expression system while (current.parent && (current.parent instanceof omdMetaExpression)) { current = current.parent; } return current; } findAllNodes(nodes = []) { nodes.push(this); if (this.childList) { this.childList.forEach(child => { if (child.findAllNodes) { // Ensure child has the method child.findAllNodes(nodes); } }); } return nodes; } // ===== OLD SELECTION LOGIC (can be removed or kept for other purposes) ===== select(root) { if (root === this) return; if (!(root instanceof omdMetaExpression)) root = this; this.backRect.setFillColor( omdColor.hiliteColor ); this.backRect.setOpacity( 1.0 ); this.childList.forEach((child) => { if (child instanceof omdMetaExpression) child.select(root); }); if (this === root && this.parent instanceof omdMetaExpression) root.parent.deselect(root); } deselect(root) { if (!(root instanceof omdMetaExpression)) root = this; if (this === root && this.parent instanceof omdMetaExpression) root.parent.select(root); if (root === this && this.parent instanceof omdMetaExpression) return; this.backRect.setFillColor( this.getBackgroundColor() ); if ( this.defaultOpaqueBack == false ) this.backRect.setOpacity(0.01); this.childList.forEach((child) => { if (child !== root && child instanceof omdMetaExpression) child.deselect(root); }); } // select(root) // { // this.backRect.setFillColor( omdColor.hiliteColor ); // this.backRect.setOpacity( 1.0 ); // if (!(root instanceof omdMetaExpression)) // root = this; // this.childList.forEach((child) => { // if (child instanceof omdMetaExpression) // child.select(root); // }); // if (this === root) // this.unselectParents(root); // } // deselect(root) // { // this.backRect.setFillColor( omdColor.lightGray ); // if ( this.defaultOpaqueBack == false ) // this.backRect.setOpacity(0.01); // this.childList.forEach((child) => { // if (child !== root && child instanceof omdMetaExpression) { // child.deselect(root); // } // }); // if (this.parent instanceof omdMetaExpression) // this.parent.select(this.parent); // } // unselectParents(root) // { // if (this.parent instanceof omdMetaExpression) { // this.parent.unselectParents(root); // this.parent.deselect(root); // } // } hideBackgroundByDefault() { this.defaultOpaqueBack = false; this.backRect.setOpacity(0.01); } makeBackgroundLight() { this.backRect.setFillColor( this.getBackgroundColor() ) } makeBackgroundDark() { this.backRect.setFillColor( omdColor.mediumGray ) } }