viz/tree-viz/features/features-engine.js

/**
 * @fileoverview TreeFeaturesEngine - Feature-Modul für Node-Styling, Status-Management und Dead-End-Erkennung
 * 
 * WICHTIG: Status-Definitionen und -Designs sind nun zentral in status-config.js definiert.
 * Diese Datei nutzt:
 * - StatusConfig.getStatusTypes() für Priority-basierte Status
 * - StatusConfig.getStyleConfig() für Visual-Styles
 * - STYLE_CONFIG_GLOBAL für Master-Rendering-Effekte
 * 
 * Zentrale Verwaltung aller visuellen Effekte und Node-Eigenschaften:
 * - checkAndMarkDeadEnd(): Erkennung von Sackgassen im Baum
 * - getNodeStyle(): Konsistentes Styling basierend auf Node-Status
 * - updateActiveNodeTracking(): Fokus-Management für aktive Knoten
 * 
 * @author Alexander Wolf
 * @version 1.0
 */
// Make TreeFeaturesEngine global (not const, so it's accessible without window.)
var TreeFeaturesEngine = window.TreeFeaturesEngine || {
    // Safe getters for global STYLE_CONFIG and STYLE_CONFIG_GLOBAL
    // These are defined in status-config.js and must be loaded first
    getStyleConfig() {
        if (typeof window !== 'undefined' && window.STYLE_CONFIG) {
            return window.STYLE_CONFIG;
        }
        if (typeof StatusConfig !== 'undefined' && StatusConfig.getStyleConfig) {
            return StatusConfig.getStyleConfig();
        }
        return {};
    },

    getStyleConfigGlobal() {
        if (typeof window !== 'undefined' && window.STYLE_CONFIG_GLOBAL) {
            return window.STYLE_CONFIG_GLOBAL;
        }
        if (typeof StatusConfig !== 'undefined' && StatusConfig.STYLE_CONFIG_GLOBAL) {
            return StatusConfig.STYLE_CONFIG_GLOBAL;
        }
        return {};
    },

    /**
     * Identifies whether a node is a dead end and applies the corresponding status.
     * @param {string} nodeId - The ID of the node to check
     * @param {Object} nodes - Map of all nodes in the tree
     */
    checkAndMarkDeadEnd(nodeId, nodes) {
        const node = nodes.get(nodeId);
        
        if (!node) {
            return;
        }
        
        if (!node.children || node.children.length === 0) {
            return;
        }

        // REKURSIV: Prüfe und markiere alle Kinder zuerst!
        for (const childId of node.children) {
            TreeFeaturesEngine.checkAndMarkDeadEnd(childId, nodes);
        }

        // JETZT: Prüfe, ob alle Kinder DEAD_END sind (nachdem sie rekursiv geprüft wurden)
        let allChildrenAreDead = true;
        for (const childId of node.children) {
            const child = nodes.get(childId);
            const hasDeadEnd = child && child.status && child.status.has('DEAD_END');
            
            if (!hasDeadEnd) {
                allChildrenAreDead = false;
                break;
            }
        }

        // Wenn alle Kinder DEAD_END sind, markiere diesen Knoten auch
        if (allChildrenAreDead) {
            if (!node.status) node.status = new Set();
            node.status.add('DEAD_END');
        }
    },

    /**
     * Aktualisiert Active Node Tracking
     * Zentriert View auf aktiven Node mit optionaler Animation
     * (Pausiert während Pan/Zoom-Interaktionen)
     * 
     * @param {Object} activeNodeTracking - { nodeId, paused, targetX, targetY, animating, ... }
     * @param {Map} nodes
     * @param {Object} viewport - { offsetX, offsetY, scale }
     * @param {Object} canvas - { width, height }
     * @param {Object} config - { activeNodeTrackingSmooth, activeNodeTrackingDuration }
     */
    updateActiveNodePosition(activeNodeTracking, nodes, viewport, canvas, config) {
        if (!activeNodeTracking.nodeId) return;
        
        // Skip if tracking is paused (during pan/zoom)
        if (activeNodeTracking.paused) return;

        const node = nodes.get(activeNodeTracking.nodeId);
        if (!node) return;

        const targetX = canvas.width * config.activeNodeTargetX;
        const targetY = canvas.height * config.activeNodeTargetY;

        if (config.activeNodeTrackingSmooth && !activeNodeTracking.animating) {
            // Start animation
            activeNodeTracking.animating = true;
            activeNodeTracking.animationStart = performance.now();
            activeNodeTracking.animationDuration = config.activeNodeTrackingDuration || 300;
            activeNodeTracking.lastOffsetX = viewport.offsetX;
            activeNodeTracking.lastOffsetY = viewport.offsetY;
        }

        if (activeNodeTracking.animating) {
            const now = performance.now();
            const elapsed = now - activeNodeTracking.animationStart;
            const duration = activeNodeTracking.animationDuration;

            if (elapsed >= duration) {
                // Animation finished
                activeNodeTracking.animating = false;
            }

            const progress = Math.min(1, elapsed / duration);
            const easeProgress = 1 - Math.pow(1 - progress, 3);

            const startX = activeNodeTracking.lastOffsetX;
            const startY = activeNodeTracking.lastOffsetY;
            const endOffsetX = targetX - node.x * viewport.scale;
            const endOffsetY = targetY - node.y * viewport.scale;

            viewport.offsetX = startX + (endOffsetX - startX) * easeProgress;
            viewport.offsetY = startY + (endOffsetY - startY) * easeProgress;
        } else {
            // Instant centering
            viewport.offsetX = targetX - node.x * viewport.scale;
            viewport.offsetY = targetY - node.y * viewport.scale;
        }
    },

    /**
     * Setzt den Active Node (exclusive - entfernt ACTIVE von allen anderen)
     * @param {number} nodeId
     * @param {Map} nodes
     * @param {Object} activeNodeTracking
     */
    setActiveNode(nodeId, nodes, activeNodeTracking) {
        if (activeNodeTracking.nodeId === nodeId) return; // Already active

        // REMOVE ACTIVE from all other nodes (exclusive lock)
        nodes.forEach(node => {
            if (node.status && node.status.has('ACTIVE') && node.id !== nodeId) {
                node.status.delete('ACTIVE');
            }
        });

        activeNodeTracking.nodeId = nodeId;
        activeNodeTracking.animating = false;

        const node = nodes.get(nodeId);
        if (!node) return;

        // Markiere als ACTIVE (nicht nur Status, sondern auch Tracking-Info)
        if (!node.status) node.status = new Set();
        node.status.add('ACTIVE');
    },

    /**
     * Gibt Style-Objekt für einen Knoten basierend auf Status
     * Kombiniert Status (z.B. ACTIVE + DEAD_END) zu konsistentem Styling
     * 
     * @param {Object} node
     * @param {Object} statusTypes - Status-Definitionen
     * @param {Object} config - Engine config with enableStatusGlow
     * @returns {Object} { glowColor, borderColor, borderWidth, borderDash, fillColor }
     */
    getNodeStyle(node, statusTypes, config = {}) {
        const STYLE_CONFIG = this.getStyleConfig();
        // CUSTOM COLOR SUPPORT: If node has a custom color property (e.g., for Warnsdorf preview nodes)
        if (node.color && !node.status?.size) {
            const previewStyle = { ...STYLE_CONFIG.PREVIEW_NODE };
            previewStyle.color = node.color;  // Override with custom color
            return this._convertStyleToRenderFormat(previewStyle, config);
        }

        if (!node.status || node.status.size === 0) {
            return this._convertStyleToRenderFormat(STYLE_CONFIG.DEFAULT, config);
        }

        // Combo-Status prüfen (höchste Priorität)
        const hasActive = node.status.has('ACTIVE');
        const hasDeadEnd = node.status.has('DEAD_END');
        const hasWin = node.status.has('WIN');
        const hasDuplicate = node.status.has('DUPLICATE');

        // DUPLICATE: Always takes precedence
        if (hasDuplicate) {
            return this._convertStyleToRenderFormat(STYLE_CONFIG.DUPLICATE, config);
        }

        // Combo-Status
        if (hasActive && hasDeadEnd) {
            return this._convertStyleToRenderFormat(STYLE_CONFIG.ACTIVE_DEAD_END, config);
        }

        if (hasActive && hasWin) {
            return this._convertStyleToRenderFormat(STYLE_CONFIG.ACTIVE_WIN, config);
        }

        // Single status (pick first by priority order)
        // EXPLICITLY ADD INTERACTIVE STATUSES (READY, WAIT, EVALUATED)
        // Single status (pick first by priority order)
        // EXPLICITLY ADD INTERACTIVE STATUSES (READY, WAIT, EVALUATED, WIN_BLUE, WIN_RED)
        const priorityOrder = ['ACTIVE', 'NEXT_TO_EVALUATE', 'READY', 'WIN_BLUE', 'WIN_RED', 'WIN', 'LOSS', 'SOLUTION', 'DEAD_END', 'DRAW', 'PRUNED', 'EVALUATED', 'WAIT', 'NONE'];
        let selectedStyle = STYLE_CONFIG.DEFAULT;
        
        for (const statusName of priorityOrder) {
            if (node.status.has(statusName) && STYLE_CONFIG[statusName]) {
                selectedStyle = STYLE_CONFIG[statusName];
                break;
            }
        }

        return this._convertStyleToRenderFormat(selectedStyle, config);
    },

    /**
     * Konvertiert STYLE_CONFIG Format zu Renderer-Format
     * Mit Unterstützung für enableGlow und enableDash Schalter
     * Nutzt zentrale STYLE_CONFIG_GLOBAL Effekt-Schalter
     * @param {Object} style - STYLE_CONFIG object
     * @param {Object} config - Renderer config (kompatibilität, wird nicht mehr genutzt)
     * @returns {Object} - Renderer-kompatibles Format
     */
    _convertStyleToRenderFormat(style, config = {}) {
        const STYLE_CONFIG_GLOBAL = this.getStyleConfigGlobal();
        // Nutze zentrale STYLE_CONFIG_GLOBAL Schalter statt einzelner config Optionen
        const enableGlowEffects = STYLE_CONFIG_GLOBAL.enableGlowEffects;
        const enableDashEffects = STYLE_CONFIG_GLOBAL.enableDashEffects;
        
        return {
            fillColor: style.color || null,
            borderColor: style.border || '#333',
            borderWidth: style.width || 2,
            // Glow: Kombiniere globalen Schalter mit lokalem Schalter und Glow-Farbe
            glowColor: (enableGlowEffects && style.enableGlow && style.glow) ? style.glow : null,
            // Dash: Kombiniere globalen Schalter mit lokalem Schalter und Dash-Pattern
            borderDash: (enableDashEffects && style.enableDash && style.dash) ? style.dash : []
        };

    },

    /**
     * Aktualisiert Node-Status (addStatus, removeStatus, setStatus)
     * @param {Object} node
     * @param {string} status
     * @param {boolean} active - true = add, false = remove
     */
    updateNodeStatus(node, status, active = true) {
        if (!node.status) {
            node.status = new Set();
        }

        if (active) {
            node.status.add(status);
        } else {
            node.status.delete(status);
        }
    }
};

// Make globally available
if (typeof window !== 'undefined') {
    window.TreeFeaturesEngine = TreeFeaturesEngine;
}

// Export for Node.js/CommonJS
if (typeof module !== 'undefined' && module.exports) {
    module.exports = TreeFeaturesEngine;
}