From 7f4a231f3d8d094e6d2cd9670af91fe2e2ea26bd Mon Sep 17 00:00:00 2001 From: 90-RED <90-RED@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:43:21 +0800 Subject: [PATCH] =?UTF-8?q?v1.4.1=20=E2=80=94=20slot=20hover=20=C3=97=20se?= =?UTF-8?q?lection=20interaction=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/draw.js | 43 +++++++++++++++++++++++++++++++------------ web/state.js | 1 + web/ui.js | 20 +++++++++++--------- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/web/draw.js b/web/draw.js index 0d89779..7062d3b 100644 --- a/web/draw.js +++ b/web/draw.js @@ -536,6 +536,7 @@ export function drawAll(canvas, ctx) { ); const hovLinkId = M.S.hoverAnim ? M._hoverLinkId : null; const hovSlotIds = M.S.hoverAnim ? M._hoverSlotLinkIds : null; + const hovSlotNodeId = M.S.hoverAnim ? M._hoverSlotNodeId : null; // Slot-label hover (pointer on an input/output name or dot) wins over link // hover and node hover: only that slot's links animate/thicken this frame. const hovId = @@ -543,17 +544,33 @@ export function drawAll(canvas, ctx) { const hoverId = hovId !== null && !selIds.has(String(hovId)) ? hovId : null; const hasSel = M.S.selectHighlight && selIds.size > 0; const isDragging = M._nodeDragActive; - // Slot-label hover also narrows a selection: while the pointer is on a slot - // label/dot, the highlight set shrinks to just that slot's links (other - // links stay dimmed), and returns to the full selection on leave. - const related = hovSlotIds - ? (link) => hovSlotIds.has(link.id) - : (link) => - selIds.has(String(link.origin_id)) || selIds.has(String(link.target_id)); + const related = (link) => + selIds.has(String(link.origin_id)) || selIds.has(String(link.target_id)); + const isSlotLink = (link) => !!hovSlotIds && hovSlotIds.has(link.id); + // Slot-label hover on a SELECTED node narrows that node's highlight to the + // slot's links: its other related links lose the select boost/animation but + // keep FULL alpha (never dimmed). Selections on other nodes are untouched, + // and slot hover on an unselected node leaves every selection alone. + const slotNodeKey = + hovSlotIds && + hovSlotNodeId !== null && + hovSlotNodeId !== undefined && + selIds.has(String(hovSlotNodeId)) + ? String(hovSlotNodeId) + : null; + const demotedSel = (link) => + !!slotNodeKey && + !isSlotLink(link) && + (String(link.origin_id) === slotNodeKey || String(link.target_id) === slotNodeKey); const hovered = (link) => link.id === hovLinkId || - (!!hovSlotIds && hovSlotIds.has(link.id)) || + isSlotLink(link) || (hoverId !== null && (link.origin_id === hoverId || link.target_id === hoverId)); + // While any hover target is active (slot label, single link, or an + // unselected node), selection ANIMATION pauses — selected links keep their + // width boost and full alpha, but only the hovered links animate. + const hovAny = hovLinkId !== null || !!hovSlotIds || hoverId !== null; + const selAnim = M.S.selectAnim && !hovAny; // Adaptive marker density: count this frame's animating links once (cheap // predicate pass, no geometry) and scale the marker gap accordingly. M._animGapScale = animDensityScale( @@ -562,7 +579,7 @@ export function drawAll(canvas, ctx) { : routed.reduce( (n, { entry }) => n + - (((hasSel && related(entry.link)) && M.S.selectAnim) || + (((hasSel && related(entry.link) && !demotedSel(entry.link)) && selAnim) || hovered(entry.link) ? 1 : 0), @@ -641,10 +658,12 @@ export function drawAll(canvas, ctx) { const mid = pts[Math.floor(pts.length / 2)]; link._pos && ((link._pos[0] = mid.x), (link._pos[1] = mid.y)); if (cull && boundsOutside(linkBounds(cached), cull)) continue; - const isSel = hasSel && related(link); + const isSel = hasSel && related(link) && !demotedSel(link); const isHov = M.S.hoverAnim && hovered(link); let alpha = 1; - if (hasSel && !isSel) { + // Hovered links and slot-demoted selected links keep full alpha — only + // truly unrelated links dim while a selection is active. + if (hasSel && !isSel && !isHov && !demotedSel(link)) { // During drag, use a separate (less aggressive) dim value // so you can still see the canvas layout while dragging. alpha = isDragging && M.S.dragDimAlpha > 0 @@ -678,7 +697,7 @@ export function drawAll(canvas, ctx) { const hitPath = cachedCanvasPath(cached); if (hitPath) link.path = hitPath; - if (!lowQ && ((isSel && M.S.selectAnim) || isHov)) { + if (!lowQ && ((isSel && selAnim) || isHov)) { if (M.S.flowMode === "animated" && animOK) { animOn = true; if (useOverlay) M._animLinks.push({ cached, color, alpha }); diff --git a/web/state.js b/web/state.js index 9c3b4bb..e49bd64 100644 --- a/web/state.js +++ b/web/state.js @@ -108,6 +108,7 @@ export const M = { mouseClient: null, // {x, y} in client space _hoverLinkId: null, // link id under the pointer (single-link hover; ui.js) _hoverSlotLinkIds: null, // Set of link ids from the slot label/dot under the pointer (ui.js) + _hoverSlotNodeId: null, // id of the node owning the hovered slot (ui.js) // --- floating bar DOM refs --- uiBox: null, // DOM element diff --git a/web/ui.js b/web/ui.js index f064c37..58fe415 100644 --- a/web/ui.js +++ b/web/ui.js @@ -472,15 +472,16 @@ function nodeAtPoint(canvas, p) { } // Slot-label hover: pointer over an input/output name or dot on the node -// under the cursor. Returns an array of connected link ids, or null when no -// linked slot is under the pointer (callers then fall back to link/node -// hover). +// under the cursor. Returns { ids, nodeId } for the connected links, or null +// when no linked slot is under the pointer (callers then fall back to +// link/node hover). function hoverSlotLinkIdsAt(canvas) { const p = pointerGraphPos(canvas); if (!p) return null; const node = canvas.node_over || nodeAtPoint(canvas, p); if (!node) return null; - return slotLinkIdsAt(node, p.gx, p.gy); + const ids = slotLinkIdsAt(node, p.gx, p.gy); + return ids ? { ids, nodeId: node.id } : null; } // Single-link hover: pointer directly over a link's drawn path. Tolerance is @@ -509,22 +510,23 @@ export function watchHover() { // Slot-label hover takes priority over link hover and node hover: // pointing at an input/output name or dot animates/thickens only that // slot's links. Skipped during a node drag, like link hover. - const slotIds = M._nodeDragActive ? null : hoverSlotLinkIdsAt(canvas); - const slotKey = slotIds ? slotIds.join(",") : null; + const slotHit = M._nodeDragActive ? null : hoverSlotLinkIdsAt(canvas); + const slotKey = slotHit ? String(slotHit.nodeId) + ":" + slotHit.ids.join(",") : null; if (slotKey !== lastHoverSlotKey) { lastHoverSlotKey = slotKey; - M._hoverSlotLinkIds = slotIds ? new Set(slotIds) : null; + M._hoverSlotLinkIds = slotHit ? new Set(slotHit.ids) : null; + M._hoverSlotNodeId = slotHit ? slotHit.nodeId : null; canvas.setDirty(true, true); } // Single-link hover: pointing directly at a link animates/thickens just // that link. Suppressed while a slot hover is active. - const curLink = slotIds || M._nodeDragActive ? null : hoverLinkIdAt(canvas); + const curLink = slotHit || M._nodeDragActive ? null : hoverLinkIdAt(canvas); if (curLink !== lastHoverLink) { lastHoverLink = curLink; M._hoverLinkId = curLink; canvas.setDirty(true, true); } - const cur = slotIds || curLink !== null ? null : drawHoverNodeId(canvas); + const cur = slotHit || curLink !== null ? null : drawHoverNodeId(canvas); if (cur !== lastHover) { lastHover = cur; canvas.setDirty(true, true);