From 265c05952b84bdfd13d3b033e99d1e616e52f84f Mon Sep 17 00:00:00 2001 From: Ahmed Alshaybani Date: Tue, 20 Oct 2026 05:51:32 +0200 Subject: [PATCH 1/2] test: reproduce board109 outline sliver explosion --- test-assets/board109-outline.json | 396 ++++++++++++++++++ .../compute-inverse-rects-board109.snap.svg | 44 ++ tests/compute-inverse-rects-board109.test.ts | 45 ++ tests/fixtures/getBoard109OutlineSnapshot.ts | 102 +++++ 4 files changed, 587 insertions(+) create mode 100644 test-assets/board109-outline.json create mode 100644 tests/__snapshots__/compute-inverse-rects-board109.snap.svg create mode 100644 tests/compute-inverse-rects-board109.test.ts create mode 100644 tests/fixtures/getBoard109OutlineSnapshot.ts diff --git a/test-assets/board109-outline.json b/test-assets/board109-outline.json new file mode 100644 index 00000000..9710e17b --- /dev/null +++ b/test-assets/board109-outline.json @@ -0,0 +1,396 @@ +{ + "bounds": { + "minX": -31, + "maxX": 31, + "minY": -31, + "maxY": 31 + }, + "outline": [ + { + "x": 30, + "y": 0 + }, + { + "x": 29.935767697158106, + "y": 1.962093876904292 + }, + { + "x": 29.74334584121431, + "y": 3.915785766601547 + }, + { + "x": 29.423558412096913, + "y": 5.852709660483847 + }, + { + "x": 28.97777478867205, + "y": 7.764571353075622 + }, + { + "x": 28.40790388485317, + "y": 9.643183959094848 + }, + { + "x": 27.716385975338603, + "y": 11.480502970952694 + }, + { + "x": 26.906182245980652, + "y": 13.268660706570037 + }, + { + "x": 25.98076211353316, + "y": 14.999999999999998 + }, + { + "x": 24.944088369076358, + "y": 16.667106990588067 + }, + { + "x": 23.800600208737055, + "y": 18.26284287026162 + }, + { + "x": 22.555194224369323, + "y": 19.780374453002064 + }, + { + "x": 21.213203435596427, + "y": 21.213203435596423 + }, + { + "x": 19.780374453002064, + "y": 22.555194224369323 + }, + { + "x": 18.26284287026162, + "y": 23.800600208737055 + }, + { + "x": 16.66710699058807, + "y": 24.944088369076354 + }, + { + "x": 15.000000000000004, + "y": 25.980762113533157 + }, + { + "x": 13.268660706570037, + "y": 26.906182245980652 + }, + { + "x": 11.480502970952696, + "y": 27.716385975338603 + }, + { + "x": 9.64318395909485, + "y": 28.407903884853166 + }, + { + "x": 7.764571353075622, + "y": 28.97777478867205 + }, + { + "x": 5.85270966048385, + "y": 29.423558412096913 + }, + { + "x": 3.915785766601551, + "y": 29.74334584121431 + }, + { + "x": 1.9620938769042982, + "y": 29.935767697158106 + }, + { + "x": 1.83697019872103e-15, + "y": 30 + }, + { + "x": -1.9620938769042944, + "y": 29.935767697158106 + }, + { + "x": -3.915785766601548, + "y": 29.74334584121431 + }, + { + "x": -5.852709660483846, + "y": 29.423558412096913 + }, + { + "x": -7.764571353075619, + "y": 28.97777478867205 + }, + { + "x": -9.643183959094848, + "y": 28.40790388485317 + }, + { + "x": -11.480502970952685, + "y": 27.716385975338607 + }, + { + "x": -13.268660706570033, + "y": 26.906182245980652 + }, + { + "x": -14.999999999999995, + "y": 25.98076211353316 + }, + { + "x": -16.66710699058807, + "y": 24.944088369076354 + }, + { + "x": -18.26284287026162, + "y": 23.800600208737055 + }, + { + "x": -19.780374453002064, + "y": 22.555194224369323 + }, + { + "x": -21.213203435596423, + "y": 21.213203435596427 + }, + { + "x": -22.55519422436932, + "y": 19.780374453002068 + }, + { + "x": -23.800600208737052, + "y": 18.262842870261625 + }, + { + "x": -24.94408836907635, + "y": 16.667106990588074 + }, + { + "x": -25.98076211353316, + "y": 14.999999999999998 + }, + { + "x": -26.906182245980645, + "y": 13.268660706570051 + }, + { + "x": -27.716385975338603, + "y": 11.480502970952697 + }, + { + "x": -28.407903884853166, + "y": 9.643183959094852 + }, + { + "x": -28.977774788672047, + "y": 7.76457135307563 + }, + { + "x": -29.423558412096913, + "y": 5.852709660483845 + }, + { + "x": -29.74334584121431, + "y": 3.9157857666015596 + }, + { + "x": -29.935767697158106, + "y": 1.9620938769042935 + }, + { + "x": -30, + "y": 3.67394039744206e-15 + }, + { + "x": -29.935767697158106, + "y": -1.962093876904286 + }, + { + "x": -29.74334584121431, + "y": -3.915785766601553 + }, + { + "x": -29.423558412096916, + "y": -5.852709660483837 + }, + { + "x": -28.97777478867205, + "y": -7.764571353075624 + }, + { + "x": -28.40790388485317, + "y": -9.643183959094847 + }, + { + "x": -27.716385975338607, + "y": -11.48050297095269 + }, + { + "x": -26.906182245980645, + "y": -13.268660706570044 + }, + { + "x": -25.980762113533164, + "y": -14.999999999999993 + }, + { + "x": -24.944088369076365, + "y": -16.66710699058806 + }, + { + "x": -23.800600208737055, + "y": -18.26284287026162 + }, + { + "x": -22.555194224369323, + "y": -19.780374453002064 + }, + { + "x": -21.213203435596437, + "y": -21.213203435596412 + }, + { + "x": -19.78037445300207, + "y": -22.55519422436932 + }, + { + "x": -18.262842870261625, + "y": -23.80060020873705 + }, + { + "x": -16.667106990588067, + "y": -24.944088369076358 + }, + { + "x": -15.000000000000014, + "y": -25.980762113533153 + }, + { + "x": -13.26866070657004, + "y": -26.90618224598065 + }, + { + "x": -11.480502970952685, + "y": -27.716385975338607 + }, + { + "x": -9.643183959094854, + "y": -28.407903884853166 + }, + { + "x": -7.764571353075619, + "y": -28.97777478867205 + }, + { + "x": -5.8527096604838595, + "y": -29.42355841209691 + }, + { + "x": -3.915785766601549, + "y": -29.74334584121431 + }, + { + "x": -1.962093876904282, + "y": -29.935767697158106 + }, + { + "x": -5.510910596163089e-15, + "y": -30 + }, + { + "x": 1.962093876904271, + "y": -29.93576769715811 + }, + { + "x": 3.915785766601538, + "y": -29.743345841214314 + }, + { + "x": 5.852709660483849, + "y": -29.423558412096913 + }, + { + "x": 7.764571353075609, + "y": -28.977774788672054 + }, + { + "x": 9.643183959094845, + "y": -28.40790388485317 + }, + { + "x": 11.480502970952674, + "y": -27.71638597533861 + }, + { + "x": 13.268660706570031, + "y": -26.906182245980656 + }, + { + "x": 15.000000000000004, + "y": -25.980762113533157 + }, + { + "x": 16.667106990588056, + "y": -24.944088369076365 + }, + { + "x": 18.262842870261597, + "y": -23.800600208737073 + }, + { + "x": 19.78037445300207, + "y": -22.555194224369316 + }, + { + "x": 21.21320343559642, + "y": -21.21320343559643 + }, + { + "x": 22.555194224369323, + "y": -19.78037445300206 + }, + { + "x": 23.80060020873705, + "y": -18.262842870261625 + }, + { + "x": 24.944088369076344, + "y": -16.66710699058809 + }, + { + "x": 25.980762113533153, + "y": -15.000000000000014 + }, + { + "x": 26.90618224598065, + "y": -13.268660706570042 + }, + { + "x": 27.716385975338607, + "y": -11.480502970952687 + }, + { + "x": 28.407903884853166, + "y": -9.643183959094856 + }, + { + "x": 28.977774788672043, + "y": -7.764571353075647 + }, + { + "x": 29.42355841209691, + "y": -5.852709660483861 + }, + { + "x": 29.74334584121431, + "y": -3.9157857666015508 + }, + { + "x": 29.935767697158106, + "y": -1.962093876904284 + } + ], + "minTraceWidth": 0.15, + "minBoardEdgeClearance": 0.3 +} diff --git a/tests/__snapshots__/compute-inverse-rects-board109.snap.svg b/tests/__snapshots__/compute-inverse-rects-board109.snap.svg new file mode 100644 index 00000000..08713bea --- /dev/null +++ b/tests/__snapshots__/compute-inverse-rects-board109.snap.svg @@ -0,0 +1,44 @@ +Board 109: 19985 void rectangles14696 slivers at or below EPS (1e-9 mm)Upper-right detail: 39 intersecting rectangles \ No newline at end of file diff --git a/tests/compute-inverse-rects-board109.test.ts b/tests/compute-inverse-rects-board109.test.ts new file mode 100644 index 00000000..264cc7ac --- /dev/null +++ b/tests/compute-inverse-rects-board109.test.ts @@ -0,0 +1,45 @@ +import { expect, test } from "bun:test" +import { computeInverseRects } from "../lib/solvers/RectDiffSeedingSolver/computeInverseRects" +import { containsPoint, EPS } from "../lib/utils/rectdiff-geometry" +import board109Outline from "../test-assets/board109-outline.json" +import { getBoard109OutlineSnapshot } from "./fixtures/getBoard109OutlineSnapshot" + +// Captured from tscircuit-autorouter's bugreport109-book-reading-clip-lamp.srj.json. +test("board109's circular outline does not generate floating-point sliver blockers", async () => { + const clearance = board109Outline.minBoardEdgeClearance + const rects = computeInverseRects( + { + x: board109Outline.bounds.minX - clearance, + y: board109Outline.bounds.minY - clearance, + width: + board109Outline.bounds.maxX - + board109Outline.bounds.minX + + clearance * 2, + height: + board109Outline.bounds.maxY - + board109Outline.bounds.minY + + clearance * 2, + }, + board109Outline.outline, + { minGridSize: board109Outline.minTraceWidth }, + ) + + await expect(getBoard109OutlineSnapshot(rects)).toMatchSvgSnapshot( + import.meta.path, + ) + + // The original 96-vertex outline generated 19,985 rectangles, including + // 14,696 thinner than EPS. A compact perimeter decomposition is sufficient. + expect(rects.length).toBeLessThan(512) + expect(rects.every((rect) => rect.width > EPS && rect.height > EPS)).toBe( + true, + ) + + for (let index = 0; index < 96; index++) { + const angle = (index * Math.PI * 2) / 96 + const exterior = { x: 30.8 * Math.cos(angle), y: 30.8 * Math.sin(angle) } + const interior = { x: 29 * Math.cos(angle), y: 29 * Math.sin(angle) } + expect(rects.some((rect) => containsPoint(rect, exterior))).toBe(true) + expect(rects.some((rect) => containsPoint(rect, interior))).toBe(false) + } +}) diff --git a/tests/fixtures/getBoard109OutlineSnapshot.ts b/tests/fixtures/getBoard109OutlineSnapshot.ts new file mode 100644 index 00000000..3774ab9e --- /dev/null +++ b/tests/fixtures/getBoard109OutlineSnapshot.ts @@ -0,0 +1,102 @@ +import { getSvgFromGraphicsObject } from "graphics-debug" +import type { Point } from "@tscircuit/math-utils" +import type { XYRect } from "../../lib/rectdiff-types" +import { EPS } from "../../lib/utils/rectdiff-geometry" +import board109Outline from "../../test-assets/board109-outline.json" + +/** Show every void rectangle intersecting a small section of the circular edge. */ +export function getBoard109OutlineSnapshot(rects: XYRect[]): string { + const detailBounds = { minX: 20, minY: 20, maxX: 23, maxY: 23 } + const detailRects = rects + .filter( + (rect) => + rect.x <= detailBounds.maxX && + rect.x + rect.width >= detailBounds.minX && + rect.y <= detailBounds.maxY && + rect.y + rect.height >= detailBounds.minY, + ) + .map((rect) => { + const minX = Math.max(detailBounds.minX, rect.x) + const maxX = Math.min(detailBounds.maxX, rect.x + rect.width) + const minY = Math.max(detailBounds.minY, rect.y) + const maxY = Math.min(detailBounds.maxY, rect.y + rect.height) + return { + center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 }, + width: maxX - minX, + height: maxY - minY, + fill: "#fee2e2", + stroke: "#f97316", + label: `void: ${rect.width.toExponential(2)} x ${rect.height.toExponential(2)} mm`, + } + }) + const sliverCount = rects.filter( + (rect) => rect.width <= EPS || rect.height <= EPS, + ).length + + return getSvgFromGraphicsObject( + { + coordinateSystem: "cartesian", + rects: detailRects, + lines: [ + ...detailRects.map((rect) => ({ + points: [ + { + x: rect.center.x - rect.width / 2, + y: rect.center.y - rect.height / 2, + }, + { + x: rect.center.x + rect.width / 2, + y: rect.center.y - rect.height / 2, + }, + { + x: rect.center.x + rect.width / 2, + y: rect.center.y + rect.height / 2, + }, + { + x: rect.center.x - rect.width / 2, + y: rect.center.y + rect.height / 2, + }, + { + x: rect.center.x - rect.width / 2, + y: rect.center.y - rect.height / 2, + }, + ], + strokeColor: "#f97316", + strokeWidth: 0.0075, + })), + { + points: board109Outline.outline.filter( + (point: Point) => point.x > 19.5 && point.y > 19.5, + ), + strokeColor: "#2563eb", + strokeWidth: 0.025, + label: "physical board outline", + }, + ], + texts: [ + { + x: 20, + y: 24.1, + text: `Board 109: ${rects.length} void rectangles`, + fontSize: 0.12, + anchorSide: "center_left", + }, + { + x: 20, + y: 23.8, + text: `${sliverCount} slivers at or below EPS (1e-9 mm)`, + fontSize: 0.12, + anchorSide: "center_left", + }, + { + x: 20, + y: 23.5, + text: `Upper-right detail: ${detailRects.length} intersecting rectangles`, + fontSize: 0.12, + anchorSide: "center_left", + }, + ], + }, + { svgWidth: 800, svgHeight: 800 }, + ) +} From bd79b78e3242c19ce3a763ba969c2c546fd890f2 Mon Sep 17 00:00:00 2001 From: ShiboSoftwareDev <144173452+ShiboSoftwareDev@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:34:04 +0200 Subject: [PATCH 2/2] test: snapshot board109 with the native seeding visualization --- .../compute-inverse-rects-board109.snap.svg | 4 +- tests/compute-inverse-rects-board109.test.ts | 2 +- tests/fixtures/getBoard109OutlineSnapshot.ts | 133 +++++------------- 3 files changed, 39 insertions(+), 100 deletions(-) diff --git a/tests/__snapshots__/compute-inverse-rects-board109.snap.svg b/tests/__snapshots__/compute-inverse-rects-board109.snap.svg index 08713bea..356ff432 100644 --- a/tests/__snapshots__/compute-inverse-rects-board109.snap.svg +++ b/tests/__snapshots__/compute-inverse-rects-board109.snap.svg @@ -1,4 +1,4 @@ -Board 109: 19985 void rectangles14696 slivers at or below EPS (1e-9 mm)Upper-right detail: 39 intersecting rectangles