Skip to content

Latest commit

 

History

History
131 lines (115 loc) · 4.54 KB

Feature-Hover-Engine.md

File metadata and controls

131 lines (115 loc) · 4.54 KB
name Hover 交互引擎 (状态管理)
description 全局状态机。检测鼠标悬停事件,解析 target 目标,并自动控制鼠标光标形态。
author ymjr
version 1.0.0
license MIT
usage 作为前置依赖运行,用于监听 Excalidraw 的鼠标悬停事件(PointerMove),主要服务于那些需要在光标悬停时改变形态或触发特效的脚本。
features
维持全局的 Hover 状态机(activeIds 和 triggerId)
解析 hover 自定义数据中的 target 目标
控制 Excalidraw 画布中鼠标指针的样式(如变手型)
dependencies
无前置依赖
为其他交互型脚本提供基础支持(如 Collapse, Mindmap, 连线高亮等)
autorun true

/*

*/
var locales = {
  zh: {
    log_unmounted: "[{id}] 🔌 已卸载",
    log_mounted: "[{id}] 🚀 挂载完毕"
  },
  en: {
    log_unmounted: "[{id}] 🔌 Unmounted",
    log_mounted: "[{id}] 🚀 Mounted successfully"
  }
};

const SCRIPT_ID = "ymjr.feature.hover-engine";

ExcalidrawAutomate.plugin._ymjr_HoverState = ExcalidrawAutomate.plugin._ymjr_HoverState || {
    triggerId: null,   
    activeIds: new Set(), 
    hoverData: null    
};

function resolveTargetIds(hitElement, App, ea) {
    const hoverData = hitElement.customData?.hover;
    if (!hoverData) return [hitElement.id];

    if (Array.isArray(hoverData.id) && hoverData.id.length > 0) return hoverData.id;
    if (typeof hoverData.id === "string") {
        if (hoverData.id === "self") return [hitElement.id];
        if (hoverData.id === "target") {
            const groupElements = ea.getElementsInTheSameGroupWithElement(
                hitElement, 
                App.scene.getNonDeletedElements()
            );
            const targetEl = groupElements.find(el => el?.customData?.hover?.target);
            return targetEl ? [targetEl.id] : [];
        }
    }
    return [hitElement.id];
}

// 强制刷新视图引擎
function triggerRedraw(App, ea) {
    ea.viewUpdateScene({ appState: { zoom: { value: App.state.zoom.value + 0.00001 } } });
}

// ====================== 核心 Hook ======================
const handleCanvasPointerMove = (ctx) => {
    const { App, event, hitElement, ea, api } = ctx;
    if (!ea || !api) return;
    if (!ea?.plugin) return;
    const currentState = ExcalidrawAutomate.plugin._ymjr_HoverState;
    const isHoverValid = hitElement && hitElement.customData?.hover;

    // 1. 处理状态更新
    if (!isHoverValid) {
        if (currentState.activeIds.size > 0) {
            currentState.activeIds.clear();
            currentState.triggerId = null;
            currentState.hoverData = null;
            triggerRedraw(App, ea); 
        }
    } else {
        if (currentState.triggerId !== hitElement.id) {
            const targetIds = resolveTargetIds(hitElement, App, ea);
            currentState.triggerId = hitElement.id;
            currentState.activeIds = new Set(targetIds);
            currentState.hoverData = hitElement.customData.hover;
            triggerRedraw(App, ea);
        }
    }

    // 2. 光标控制逻辑
    if (hitElement && App.interactiveCanvas) {
        const cd = hitElement.customData || {};
        
        // 💡 判断 hover.pointer 开关,默认开启
        let wantsHoverPointer = false;
        if (cd.hover) {
            wantsHoverPointer = cd.hover.pointer !== false; 
        }

        const needsPointer = cd.collapse || cd.Mindmap || cd.highlightLine || wantsHoverPointer;
        
        let shouldSetCursor = false;
        if (cd.onPointerDownHook) {
            const hook = cd.onPointerDownHook;
            if (!hook.withKey) shouldSetCursor = true;
            else if ((hook.withKey === 'shift' && event.shiftKey) ||
                     (hook.withKey === 'ctrl' && event.ctrlKey) ||
                     (hook.withKey === 'alt' && event.altKey)) {
                shouldSetCursor = true;
            }
        }

        if (needsPointer || shouldSetCursor) {
            App.interactiveCanvas.style.cursor = "pointer";
        }
    }
};

function mountFeature() {
    if (!window.EA_Core) return;
    if (typeof ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`] === "function") ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`]();
    window.EA_Core.registerHook(SCRIPT_ID, 'handleCanvasPointerMove', handleCanvasPointerMove, 50);

    ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`] = () => {
        if (window.EA_Core) window.EA_Core.unregisterHook(SCRIPT_ID);
        console.log(t("log_unmounted", { id: SCRIPT_ID }));
    };

    console.log(t("log_mounted", { id: SCRIPT_ID }));
}

mountFeature();