v1.4.2 — fix drag hysteresis body-cross rect shape
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.
This commit is contained in:
@@ -3,3 +3,7 @@ __pycache__/
|
||||
*.pyo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local agent notes (keep out of GitHub)
|
||||
進度.md
|
||||
**/進度.md
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+6
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user