Skip to content

Commit de5b697

Browse files
xuanruliclaude
andcommitted
fix(lint): exclude gauge needles/pointers from connector_motion_detached
A gauge needle/pointer/tick is a one-end-anchored indicator, not a node-to-node connector; a separate gauge check owns it. Skip a connector whose element or group ancestor id/class matches needle|pointer|gauge|tick| indicator, and skip gauge-indicator geometry (endpoint pivots near an SVG arc/hub centre with the loose end radially outward). A defective radial connector points the other way so it still fires. Clears the fuzz080 gauge false positive; fuzz011 detach still fires (0 FP across the 81-sample corpus). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b217183 commit de5b697

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

packages/cli/src/commands/layout-audit.browser.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,11 +1739,22 @@
17391739
// short strokes are filtered out so only real diagram connectors count.
17401740
const CONNECTOR_MIN_SVG_PX = 100;
17411741
const CONNECTOR_MIN_LEN_PX = 60;
1742+
// Gauge needles/pointers/ticks are one-end-anchored indicators, not node-to-node
1743+
// connectors — a separate (gauge) check owns them. Skip by id/class of the line
1744+
// or any group ancestor up to the SVG.
1745+
const CONNECTOR_INDICATOR_NAME = /needle|pointer|gauge|tick|indicator/i;
17421746
const CONNECTOR_NODE_MIN_AREA = 400;
17431747
// SVG dots/markers are small; keep the floor low but above sub-pixel decoration.
17441748
const CONNECTOR_NODE_MIN_DOT_AREA = 16;
17451749
const CONNECTOR_NODE_CAP = 300;
17461750

1751+
function isIndicatorConnector(line, svg) {
1752+
for (let node = line; node && node !== svg.parentElement; node = node.parentElement) {
1753+
if (CONNECTOR_INDICATOR_NAME.test(connectorNameFor(node))) return true;
1754+
}
1755+
return false;
1756+
}
1757+
17471758
function lineScreenEndpoints(svg, line) {
17481759
if (typeof line.getScreenCTM !== "function" || typeof svg.createSVGPoint !== "function") {
17491760
return null;
@@ -1821,6 +1832,7 @@
18211832
for (const line of Array.from(svg.querySelectorAll("line, path"))) {
18221833
if (line.closest(CONNECTOR_SKIP_CONTAINERS)) continue;
18231834
if (!isVisibleElement(line, 0.05)) continue;
1835+
if (isIndicatorConnector(line, svg)) continue;
18241836
const ends =
18251837
line.tagName.toLowerCase() === "line"
18261838
? lineScreenEndpoints(svg, line)

packages/cli/src/utils/checkPipeline.connectorMotionDetached.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,56 @@ describe("detectConnectorMotionDetached", () => {
105105
expect(detectConnectorMotionDetached(frames, CANVAS)).toHaveLength(0);
106106
});
107107

108+
it("does not fire on a gauge needle: anchored at the arc centre, loose end radially outward", () => {
109+
// Arc ring centred at 900,500; needle base on the hub near centre, tip out
110+
// past the arc — a pointer, not a broken connector.
111+
const hub: ConnectorNodeBox = {
112+
selector: "#hub",
113+
left: 890,
114+
top: 490,
115+
right: 910,
116+
bottom: 510,
117+
ring: false,
118+
};
119+
const arc: ConnectorNodeBox = {
120+
selector: "#arc",
121+
left: 700,
122+
top: 300,
123+
right: 1100,
124+
bottom: 700,
125+
ring: true,
126+
};
127+
const frames = [0, 2, 4, 6, 8].map((time) =>
128+
frame(time, { selector: "#needle", ax: 900, ay: 500, bx: 900, by: 180 }, [hub, arc]),
129+
);
130+
expect(detectConnectorMotionDetached(frames, CANVAS)).toHaveLength(0);
131+
});
132+
133+
it("still fires on a radial connector drifting toward the centre (not outward)", () => {
134+
// Anchored on a peripheral node; loose end drifts inward to empty space near
135+
// a ring centre — the fuzz011 shape, opposite of a gauge pointer.
136+
const peripheral: ConnectorNodeBox = {
137+
selector: "#panel",
138+
left: 120,
139+
top: 120,
140+
right: 260,
141+
bottom: 200,
142+
ring: false,
143+
};
144+
const arc: ConnectorNodeBox = {
145+
selector: "#arc",
146+
left: 700,
147+
top: 300,
148+
right: 1100,
149+
bottom: 700,
150+
ring: true,
151+
};
152+
const frames = [0, 2, 4, 6, 8].map((time) =>
153+
frame(time, { selector: "#spoke", ax: 180, ay: 160, bx: 900, by: 500 }, [peripheral, arc]),
154+
);
155+
expect(detectConnectorMotionDetached(frames, CANVAS)).toHaveLength(1);
156+
});
157+
108158
it("still fires when the loose end sits in a ring's hollow centre", () => {
109159
// End B at the ring centre 900,500 is ~200px from its perimeter → dangling.
110160
const frames = [0, 2, 4, 6, 8].map((time) =>

0 commit comments

Comments
 (0)