| 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();