From eaa2a815862eed4653bd5d0cfb228cc661ad3855 Mon Sep 17 00:00:00 2001 From: 90-RED <90-RED@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:01:27 +0800 Subject: [PATCH] =?UTF-8?q?v1.4.2=20=E2=80=94=20fix=20drag=20hysteresis=20?= =?UTF-8?q?body-cross=20rect=20shape?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert nodeRect {x,y,w,h} to {x,y,x2,y2} in dragPathStillClear so corridor stickiness keeps anti-flicker while live avoidance fires when a node body actually hits a link. Add contract tests; ignore local 進度.md. --- .gitignore | 4 ++++ pyproject.toml | 2 +- tests/router.test.mjs | 43 +++++++++++++++++++++++++++++++++++++++++++ web/router.js | 12 ++++++++++++ web/routing.js | 8 ++++++-- 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2faa995..fe50101 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ __pycache__/ *.pyo .DS_Store Thumbs.db + +# Local agent notes (keep out of GitHub) +進度.md +**/進度.md diff --git a/pyproject.toml b/pyproject.toml index e23bbcc..4107080 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "comfyui-linkrouter" -version = "1.4.1" +version = "1.4.2" description = "Object-avoiding orthogonal link routing for ComfyUI — links automatically detour around nodes instead of passing through them." license = "Apache-2.0" readme = "README.md" diff --git a/tests/router.test.mjs b/tests/router.test.mjs index 5844421..7700772 100644 --- a/tests/router.test.mjs +++ b/tests/router.test.mjs @@ -4,6 +4,7 @@ import assert from "node:assert/strict"; import { DIR, OrthoRouter, + nodeBodyRectFromWH, pathCrossesRects, stretchedPathCrossesUnexpectedNode, } from "../web/router.js"; @@ -37,6 +38,48 @@ test("sticky drag paths allow only their own endpoint bodies", () => { assert.equal(stretchedPathCrossesUnexpectedNode(throughThirdNode, rects, 0, 2), true); }); +// Contract test for the drag-hysteresis call site (routing.js dragPathStillClear): +// nodeRect yields {x, y, w, h}; the crossing check reads x2/y2. The conversion +// must happen at the call site or the check silently passes everything. +test("nodeBodyRectFromWH converts nodeRect shape to x2/y2 crossing rects", () => { + const body = nodeBodyRectFromWH({ x: 450, y: 100, w: 200, h: 100 }); + assert.deepEqual(body, { x: 450, y: 100, x2: 650, y2: 200 }); + // Non-finite inputs degrade to finite zeros instead of poisoning comparisons. + const fallback = nodeBodyRectFromWH({ x: NaN, y: undefined, w: null, h: Infinity }); + assert.ok( + Number.isFinite(fallback.x) && Number.isFinite(fallback.y) && + Number.isFinite(fallback.x2) && Number.isFinite(fallback.y2), + ); +}); + +test("drag hysteresis crossing check requires the x2/y2 rect shape", () => { + // The dragged node body sits right on the link's horizontal segment. + const pts = [ + { x: 300, y: 130 }, + { x: 316, y: 130 }, + { x: 884, y: 130 }, + { x: 900, y: 130 }, + ]; + const source = { x: 100, y: 100, w: 200, h: 100 }; + const target = { x: 900, y: 100, w: 200, h: 100 }; + const draggedWH = { x: 450, y: 100, w: 200, h: 100 }; + + const withConversion = [source, target, draggedWH].map(nodeBodyRectFromWH); + // Body crosses the path → hysteresis must report "not clear" (force re-route). + assert.equal(stretchedPathCrossesUnexpectedNode(pts, withConversion, 0, 1), true); + + // Regression lock: the raw w/h shape misses the very same crossing — + // this is exactly what made live avoidance never fire. + const rawShape = [source, target, draggedWH].map((r) => ({ ...r })); + assert.equal(stretchedPathCrossesUnexpectedNode(pts, rawShape, 0, 1), false); + + // Margin-zone proximity without body contact stays "clear" (anti-flicker). + const nearMiss = [source, target, { x: 450, y: 240, w: 200, h: 100 }].map( + nodeBodyRectFromWH, + ); + assert.equal(stretchedPathCrossesUnexpectedNode(pts, nearMiss, 0, 1), false); +}); + test("A* treats every node body as a hard obstacle", () => { const rects = [ { x: 0, y: 0, w: 100, h: 100 }, diff --git a/web/router.js b/web/router.js index 0bfe61d..07a10f7 100644 --- a/web/router.js +++ b/web/router.js @@ -862,6 +862,18 @@ function dedupePoints(pts) { return out; } +// Convert a nodeRect-shaped rect ({x, y, w, h}) to the {x, y, x2, y2} shape +// segmentCrossesRect consumes. Feeding the w/h shape in directly makes every +// x2/y2 comparison NaN-false, silently disabling the crossing check. +export function nodeBodyRectFromWH(r) { + const f = (v) => (Number.isFinite(+v) ? +v : 0); + const x = f(r.x), + y = f(r.y), + w = f(r.w), + h = f(r.h); + return { x, y, x2: x + w, y2: y + h }; +} + // True only when an orthogonal segment enters a rectangle's open interior. // Travelling exactly along a node edge remains legal for endpoint escapes. function segmentCrossesRect(a, b, r) { diff --git a/web/routing.js b/web/routing.js index 45d5e1c..00dc804 100644 --- a/web/routing.js +++ b/web/routing.js @@ -6,7 +6,7 @@ import { app } from "../../scripts/app.js"; import { M } from "./state.js"; -import { stretchedPathCrossesUnexpectedNode } from "./router.js"; +import { nodeBodyRectFromWH, stretchedPathCrossesUnexpectedNode } from "./router.js"; import { stretchPathPure } from "./stretch.js"; import { profiler } from "./profiler.js"; import { @@ -1531,7 +1531,11 @@ export function routeAll(graph) { const dragPathStillClear = (entry, pts) => { if (!hysteresisRects) { hysteresisNodes = graph._nodes || []; - hysteresisRects = hysteresisNodes.map((n) => cachedNodeRect(n, rectCache)); + // nodeRect is {x, y, w, h}; the crossing check reads x2/y2 — convert, + // or the comparison goes NaN-false and every path counts as "clear". + hysteresisRects = hysteresisNodes.map((n) => + nodeBodyRectFromWH(cachedNodeRect(n, rectCache)), + ); } return !stretchedPathCrossesUnexpectedNode( pts,