UNPKG

@teachinglab/omd

Version:

omd

245 lines (213 loc) 9.78 kB
import { omdEquationNode } from '../nodes/omdEquationNode.js'; import { omdColor } from '../../src/omdColor.js'; import { jsvgLine } from '@teachinglab/jsvg'; /** * Handles visual layout, positioning, and visibility management for step visualizations */ export class omdStepVisualizerLayout { constructor(stepVisualizer) { this.stepVisualizer = stepVisualizer; } /** * Updates the layout of visual elements relative to the sequence */ updateVisualLayout() { if (this.stepVisualizer.stepDots.length === 0) return; // Position visual container to the right of the sequence // Add extra offset based on equation background padding (if any) const baseRight = (this.stepVisualizer.sequenceWidth || this.stepVisualizer.width); // Use EFFECTIVE padding (after pill clamping) to avoid overlap when pills are wider const extraPaddingX = this._getMaxEquationEffectivePaddingX(); const visualX = baseRight + this.stepVisualizer.visualSpacing + extraPaddingX; this.stepVisualizer.visualContainer.setPosition(visualX, 0); // Position dots based on visible equations const visibleSteps = this.stepVisualizer.steps.filter(s => s.visible !== false); let currentY = 0; const verticalPadding = 15 * this.stepVisualizer.getFontSize() / this.stepVisualizer.getRootFontSize(); visibleSteps.forEach((step, visIndex) => { if (step instanceof omdEquationNode) { const dotIndex = this.findDotIndexForEquation(step); if (dotIndex >= 0 && dotIndex < this.stepVisualizer.stepDots.length) { const dot = this.stepVisualizer.stepDots[dotIndex]; // Center dot vertically with the equation let equationCenter; if (step.equalsSign && step.equalsSign.ypos !== undefined) { equationCenter = step.equalsSign.ypos + (step.equalsSign.height / 2); } else { equationCenter = step.getAlignmentBaseline ? step.getAlignmentBaseline() : step.height / 2; } const dotY = currentY + equationCenter; const dotX = (this.stepVisualizer.dotRadius * 3) / 2; dot.setPosition(dotX, dotY); } } currentY += step.height; if (visIndex < visibleSteps.length - 1) { currentY += verticalPadding; } }); this.updateAllLinePositions(); // Update container dimensions let containerWidth = this.stepVisualizer.dotRadius * 3; let containerHeight = this.stepVisualizer.height; const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes(); if (textBoxes.length > 0) { const textBoxWidth = 280; containerWidth = Math.max(containerWidth, textBoxWidth + this.stepVisualizer.dotRadius * 2 + 10 + 20); // Calculate the maximum extent of any text box to prevent clipping textBoxes.forEach(textBox => { if (textBox.interactiveSteps) { const dimensions = textBox.interactiveSteps.getDimensions(); const layoutGroup = textBox.interactiveSteps.getLayoutGroup(); // Calculate the bottom of this text box const textBoxBottom = layoutGroup.ypos + dimensions.height; containerHeight = Math.max(containerHeight, textBoxBottom + 20); // Add some buffer } }); } if (this.stepVisualizer.stepDots.length > 0) { const maxRadius = Math.max(...this.stepVisualizer.stepDots.map(d=>d.radius||this.stepVisualizer.dotRadius)); const containerWidth = maxRadius * 3; const maxDotY = Math.max(...this.stepVisualizer.stepDots.map(dot => dot.ypos + this.stepVisualizer.dotRadius)); containerHeight = Math.max(containerHeight, maxDotY); } this.stepVisualizer.visualContainer.setWidthAndHeight(containerWidth, containerHeight); this.updateVisualZOrder(); } /** * Computes the maximum horizontal padding (x) among visible equations, if configured. * This allows dots to shift further right when pill background padding is added. * @returns {number} * @private */ _getMaxEquationEffectivePaddingX() { try { const steps = this.stepVisualizer.steps || []; let maxPadX = 0; steps.forEach(step => { if (step instanceof omdEquationNode && step.visible !== false) { if (typeof step.getEffectiveBackgroundPaddingX === 'function') { const px = Number(step.getEffectiveBackgroundPaddingX()); maxPadX = Math.max(maxPadX, isNaN(px) ? 0 : px); } } }); return maxPadX; } catch (_) { return 0; } } /** * Finds the dot index for a given equation */ findDotIndexForEquation(equation) { return this.stepVisualizer.stepDots.findIndex(dot => dot.equationRef === equation); } /** * Updates the z-order of visual elements */ updateVisualZOrder() { if (!this.stepVisualizer.visualContainer) return; // Lines behind (z-index 1) this.stepVisualizer.stepLines.forEach(line => { if (line && line.svgObject) { line.svgObject.style.zIndex = '1'; if (line.parentNode !== this.stepVisualizer.visualContainer) { this.stepVisualizer.visualContainer.addChild(line); } } }); // Dots in front (z-index 2) this.stepVisualizer.stepDots.forEach(dot => { if (dot && dot.svgObject) { dot.svgObject.style.zIndex = '2'; if (dot.parentNode !== this.stepVisualizer.visualContainer) { this.stepVisualizer.visualContainer.addChild(dot); } } }); // Text boxes on top (z-index 3) const textBoxes = this.stepVisualizer.textBoxManager.getStepTextBoxes(); textBoxes.forEach(textBox => { if (textBox && textBox.svgObject) { textBox.svgObject.style.zIndex = '3'; if (textBox.parentNode !== this.stepVisualizer.visualContainer) { this.stepVisualizer.visualContainer.addChild(textBox); } } }); } /** * Updates all line positions to connect dot centers */ updateAllLinePositions() { this.stepVisualizer.stepLines.forEach(line => { const fromDot = this.stepVisualizer.stepDots[line.fromDotIndex]; const toDot = this.stepVisualizer.stepDots[line.toDotIndex]; if (fromDot && toDot) { line.setEndpoints(fromDot.xpos, fromDot.ypos, toDot.xpos, toDot.ypos); } }); } /** * Updates visibility of visual elements based on equation visibility */ updateVisualVisibility() { const sv = this.stepVisualizer; // Update dot visibility first, which is the source of truth sv.stepDots.forEach(dot => { if (dot.equationRef && dot.equationRef.visible !== false) { dot.show(); dot.visible = true; // Use the dot's own visibility property } else { dot.hide(); dot.visible = false; } }); // Remove all old lines from the container and the array sv.stepLines.forEach(line => { // Remove the line if it is currently a child of the visualContainer if (line.parent === sv.visualContainer) { sv.visualContainer.removeChild(line); } }); sv.stepLines = []; // Get the dots that are currently visible const visibleDots = sv.stepDots.filter(dot => dot.visible); // Re-create connecting lines only between the visible dots for (let i = 0; i < visibleDots.length - 1; i++) { const fromDot = visibleDots[i]; const toDot = visibleDots[i + 1]; const line = new jsvgLine(); line.setStrokeColor(omdColor.stepColor); line.setStrokeWidth(sv.lineWidth); line.fromDotIndex = sv.stepDots.indexOf(fromDot); line.toDotIndex = sv.stepDots.indexOf(toDot); sv.visualContainer.addChild(line); sv.stepLines.push(line); } // After creating the lines, update their positions this.updateAllLinePositions(); } /** * Updates the clickability of a dot */ updateDotClickability(dot) { if (this.stepVisualizer.dotsClickable) { dot.svgObject.style.cursor = "pointer"; dot.svgObject.onclick = (event) => { try { const idx = this.stepVisualizer.stepDots.indexOf(dot); if (idx < 0) return; // orphan dot, ignore this.stepVisualizer._handleDotClick(dot, idx); event.stopPropagation(); } catch (error) { console.error('Error in dot click handler:', error); } }; } else { dot.svgObject.style.cursor = "default"; dot.svgObject.onclick = null; } } }