From 07f0942f54e97f8245470f6421e24ae6b8d1c287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 14 Apr 2026 19:52:44 +0530 Subject: [PATCH 1/4] Add node solver input repro snapshot test --- .../node-solver-input-repro.snap.svg | 44 +++++++++ .../node-solver-input-repro.test.ts | 90 +++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 tests/solver/repros/node-solver-input/__snapshots__/node-solver-input-repro.snap.svg create mode 100644 tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts diff --git a/tests/solver/repros/node-solver-input/__snapshots__/node-solver-input-repro.snap.svg b/tests/solver/repros/node-solver-input/__snapshots__/node-solver-input-repro.snap.svg new file mode 100644 index 0000000..d553978 --- /dev/null +++ b/tests/solver/repros/node-solver-input/__snapshots__/node-solver-input-repro.snap.svg @@ -0,0 +1,44 @@ +Layer z=0Layer z=1Layer z=2Layer z=3 \ No newline at end of file diff --git a/tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts b/tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts new file mode 100644 index 0000000..21e2da2 --- /dev/null +++ b/tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts @@ -0,0 +1,90 @@ +import { expect, test } from "bun:test" +import inputProblems from "../node-solver-input/node-solver-input.json" +import { + getBounds, + getSvgFromGraphicsObject, + mergeGraphics, + stackGraphicsVertically, + type GraphicsObject, + type Rect, +} from "graphics-debug" +import { RectDiffPipeline } from "lib/RectDiffPipeline" +import { makeCapacityMeshNodeWithLayerInfo } from "tests/fixtures/makeCapacityMeshNodeWithLayerInfo" +import { makeSimpleRouteOutlineGraphics } from "tests/fixtures/makeSimpleRouteOutlineGraphics" + +test("node-solver input repro snapshot", async () => { + const problem = inputProblems[0]! + const solver = new RectDiffPipeline(problem) + const outline = makeSimpleRouteOutlineGraphics(problem.simpleRouteJson) + + solver.solve() + + const { meshNodes } = solver.getOutput() + const rectsByCombo = makeCapacityMeshNodeWithLayerInfo(meshNodes) + const allGraphicsObjects: GraphicsObject[] = [] + + for (let z = 0; z < problem.simpleRouteJson.layerCount; z++) { + const layerRects: Rect[] = [] + + for (const [key, rects] of rectsByCombo) { + const layers = key + .split(",") + .map((value) => Number.parseInt(value, 10)) + .filter((value) => !Number.isNaN(value)) + + if (layers.includes(z)) { + layerRects.push(...rects) + } + } + + let labelY = 0 + + if (layerRects.length > 0) { + let maxY = -Infinity + + for (const rect of layerRects) { + const top = rect.center.y + rect.height * (2 / 3) + + if (top > maxY) maxY = top + } + + labelY = maxY + } + + const graphics: GraphicsObject = { + title: `RectDiffPipeline - z${z}`, + texts: [ + { + anchorSide: "top_right", + text: `Layer z=${z}`, + x: 0, + y: labelY, + fontSize: 0.5, + }, + ], + coordinateSystem: "cartesian", + rects: layerRects, + points: [], + lines: [], + } + + allGraphicsObjects.push(mergeGraphics(graphics, outline)) + } + + const stackedGraphics = stackGraphicsVertically(allGraphicsObjects) + const bounds = getBounds(stackedGraphics) + const boundsWidth = Math.max(1, bounds.maxX - bounds.minX) + const boundsHeight = Math.max(1, bounds.maxY - bounds.minY) + const svgWidth = 640 + const svgHeight = Math.max( + svgWidth, + Math.ceil((boundsHeight / boundsWidth) * svgWidth), + ) + + const svg = getSvgFromGraphicsObject(stackedGraphics, { + svgWidth, + svgHeight, + }) + + await expect(svg).toMatchSvgSnapshot(import.meta.path) +}) From caceea64aeab397835d3e13d0cd5a64af78722cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 14 Apr 2026 20:00:18 +0530 Subject: [PATCH 2/4] Add SRJ fixture for node solver repro --- .../repros/node-solver-input/node-solver-input-repro.test.ts | 2 +- tests/solver/repros/node-solver-input/node-solver-input.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 tests/solver/repros/node-solver-input/node-solver-input.json diff --git a/tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts b/tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts index 21e2da2..c959242 100644 --- a/tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts +++ b/tests/solver/repros/node-solver-input/node-solver-input-repro.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import inputProblems from "../node-solver-input/node-solver-input.json" +import inputProblems from "./node-solver-input.json" import { getBounds, getSvgFromGraphicsObject, diff --git a/tests/solver/repros/node-solver-input/node-solver-input.json b/tests/solver/repros/node-solver-input/node-solver-input.json new file mode 100644 index 0000000..7c07f68 --- /dev/null +++ b/tests/solver/repros/node-solver-input/node-solver-input.json @@ -0,0 +1 @@ +[{"simpleRouteJson":{"bounds":{"maxX":10,"maxY":5,"minX":-10,"minY":-5},"obstacles":[{"type":"rect","width":1,"center":{"x":-2.15,"y":2.54},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_0","connectivity_net8","source_trace_3","source_port_10","source_port_0","pcb_smtpad_0","pcb_port_0","pcb_smtpad_10","pcb_port_10"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":-2.15,"y":1.27},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_1","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":-2.15,"y":0},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_2","connectivity_net24","source_trace_9","source_port_16","source_trace_1","source_port_2","source_net_1","pcb_smtpad_2","pcb_port_2","pcb_smtpad_16","pcb_port_16"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":-2.15,"y":-1.27},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_3","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":-2.15,"y":-2.54},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_4","connectivity_net74","source_port_4","pcb_smtpad_4","pcb_port_4"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":2.15,"y":-2.54},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_5","connectivity_net13","source_trace_5","source_port_12","source_port_5","pcb_smtpad_5","pcb_port_5","pcb_smtpad_12","pcb_port_12"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":2.15,"y":-1.27},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_6","connectivity_net21","source_trace_8","source_port_15","source_port_6","pcb_smtpad_6","pcb_port_6","pcb_smtpad_15","pcb_port_15"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":2.15,"y":0},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_7","connectivity_net18","source_trace_7","source_port_14","source_port_7","pcb_smtpad_7","pcb_port_7","pcb_smtpad_14","pcb_port_14"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":2.15,"y":1.27},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_8","connectivity_net31","source_trace_12","source_port_19","source_port_8","pcb_smtpad_8","pcb_port_8","pcb_smtpad_19","pcb_port_19"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":2.15,"y":2.54},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_9","connectivity_net28","source_trace_11","source_port_18","source_port_9","pcb_smtpad_9","pcb_port_9","pcb_smtpad_18","pcb_port_18"],"zLayers":[0]},{"type":"rect","width":0.54,"center":{"x":-2.1500000000000004,"y":4.16},"height":0.64,"layers":["top"],"connectedTo":["pcb_smtpad_10","connectivity_net8","source_trace_3","source_port_10","source_port_0","pcb_smtpad_0","pcb_port_0","pcb_smtpad_10","pcb_port_10"],"zLayers":[0]},{"type":"rect","width":0.54,"center":{"x":-1.1300000000000006,"y":4.16},"height":0.64,"layers":["top"],"connectedTo":["pcb_smtpad_11","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"zLayers":[0]},{"type":"rect","width":0.64,"center":{"x":3.97,"y":-3.2861261293105146},"height":0.54,"layers":["top"],"connectedTo":["pcb_smtpad_12","connectivity_net13","source_trace_5","source_port_12","source_port_5","pcb_smtpad_5","pcb_port_5","pcb_smtpad_12","pcb_port_12"],"zLayers":[0]},{"type":"rect","width":0.64,"center":{"x":3.97,"y":-4.306126129310514},"height":0.54,"layers":["top"],"connectedTo":["pcb_smtpad_13","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":4.15,"y":0.8238738706894861},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_14","connectivity_net18","source_trace_7","source_port_14","source_port_7","pcb_smtpad_7","pcb_port_7","pcb_smtpad_14","pcb_port_14"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":4.15,"y":-0.4461261293105139},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_15","connectivity_net21","source_trace_8","source_port_15","source_port_6","pcb_smtpad_6","pcb_port_6","pcb_smtpad_15","pcb_port_15"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":4.15,"y":-1.716126129310514},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_16","connectivity_net24","source_trace_9","source_port_16","source_trace_1","source_port_2","source_net_1","pcb_smtpad_2","pcb_port_2","pcb_smtpad_16","pcb_port_16"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":8.45,"y":-1.716126129310514},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_17","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":8.45,"y":-0.4461261293105139},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_18","connectivity_net28","source_trace_11","source_port_18","source_port_9","pcb_smtpad_9","pcb_port_9","pcb_smtpad_18","pcb_port_18"],"zLayers":[0]},{"type":"rect","width":1,"center":{"x":8.45,"y":0.8238738706894861},"height":0.6,"layers":["top"],"connectedTo":["pcb_smtpad_19","connectivity_net31","source_trace_12","source_port_19","source_port_8","pcb_smtpad_8","pcb_port_8","pcb_smtpad_19","pcb_port_19"],"zLayers":[0]},{"type":"rect","width":20,"center":{"x":0,"y":0},"height":10,"layers":["bottom"],"connectedTo":["source_net_0","unnamedsubcircuit760_connectivity_net0","connectivity_net26","source_trace_10","source_port_17","source_trace_6","source_port_13","source_trace_4","source_port_11","source_trace_2","source_port_3","source_trace_0","source_port_1","source_net_0","pcb_smtpad_1","pcb_port_1","pcb_smtpad_3","pcb_port_3","pcb_smtpad_11","pcb_port_11","pcb_smtpad_13","pcb_port_13","pcb_smtpad_17","pcb_port_17"],"isCopperPour":true,"zLayers":[3]},{"type":"rect","width":20,"center":{"x":0,"y":0},"height":10,"layers":["inner2"],"connectedTo":["source_net_1","unnamedsubcircuit760_connectivity_net1","connectivity_net24","source_trace_9","source_port_16","source_trace_1","source_port_2","source_net_1","pcb_smtpad_2","pcb_port_2","pcb_smtpad_16","pcb_port_16"],"isCopperPour":true,"zLayers":[2]},{"obstacleId":"escape-via-obstacle:escape-via:source_trace_9:pcb_port_16:inner2:0","type":"rect","layers":["top","inner1","inner2"],"zLayers":[0,1,2],"center":{"x":3.3500000000000005,"y":-1.716126129310514},"width":0.3,"height":0.3,"connectedTo":["source_trace_9"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_1:pcb_port_2:inner2:2","type":"rect","layers":["top","inner1","inner2"],"zLayers":[0,1,2],"center":{"x":-1.3499999999999999,"y":0},"width":0.3,"height":0.3,"connectedTo":["source_net_1"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_0:pcb_port_11:bottom:3","type":"rect","layers":["top","inner1","inner2","bottom"],"zLayers":[0,1,2,3],"center":{"x":-0.5600000000000005,"y":4.16},"width":0.3,"height":0.3,"connectedTo":["source_net_0"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_0:pcb_port_13:bottom:4","type":"rect","layers":["top","inner1","inner2","bottom"],"zLayers":[0,1,2,3],"center":{"x":3.3500000000000005,"y":-4.306126129310514},"width":0.3,"height":0.3,"connectedTo":["source_net_0"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_0:pcb_port_1:bottom:6","type":"rect","layers":["top","inner1","inner2","bottom"],"zLayers":[0,1,2,3],"center":{"x":-1.3499999999999999,"y":1.27},"width":0.3,"height":0.3,"connectedTo":["source_net_0"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_0:pcb_port_3:bottom:8","type":"rect","layers":["top","inner1","inner2","bottom"],"zLayers":[0,1,2,3],"center":{"x":-1.3499999999999999,"y":-1.27},"width":0.3,"height":0.3,"connectedTo":["source_net_0"]},{"obstacleId":"escape-via-obstacle:escape-via:source_net_0:pcb_port_17:bottom:9","type":"rect","layers":["top","inner1","inner2","bottom"],"zLayers":[0,1,2,3],"center":{"x":7.6499999999999995,"y":-1.716126129310514},"width":0.3,"height":0.3,"connectedTo":["source_net_0"]}],"layerCount":4,"connections":[{"pointsToConnect":[{"x":-1.1300000000000006,"y":4.16,"layer":"top","pointId":"pcb_port_11","pcb_port_id":"pcb_port_11"},{"x":-0.5600000000000005,"y":4.16,"layer":"top","pointId":"escape-via:source_net_0:pcb_port_11:bottom:3","terminalVia":{"toLayer":"bottom","viaDiameter":0.3}}],"name":"source_net_0_mst0","rootConnectionName":"source_net_0"},{"pointsToConnect":[{"x":3.3500000000000005,"y":-4.306126129310514,"layer":"top","pointId":"escape-via:source_net_0:pcb_port_13:bottom:4","terminalVia":{"toLayer":"bottom","viaDiameter":0.3}},{"x":3.97,"y":-4.306126129310514,"layer":"top","pointId":"pcb_port_13","pcb_port_id":"pcb_port_13"}],"name":"source_net_0_mst1","rootConnectionName":"source_net_0"},{"pointsToConnect":[{"x":7.6499999999999995,"y":-1.716126129310514,"layer":"top","pointId":"escape-via:source_net_0:pcb_port_17:bottom:9","terminalVia":{"toLayer":"bottom","viaDiameter":0.3}},{"x":8.45,"y":-1.716126129310514,"layer":"top","pointId":"pcb_port_17","pcb_port_id":"pcb_port_17"}],"name":"source_net_0_mst2","rootConnectionName":"source_net_0"},{"pointsToConnect":[{"x":-2.15,"y":1.27,"layer":"top","pointId":"pcb_port_1","pcb_port_id":"pcb_port_1"},{"x":-1.3499999999999999,"y":1.27,"layer":"top","pointId":"escape-via:source_net_0:pcb_port_1:bottom:6","terminalVia":{"toLayer":"bottom","viaDiameter":0.3}}],"name":"source_net_0_mst3","rootConnectionName":"source_net_0"},{"pointsToConnect":[{"x":-2.15,"y":-1.27,"layer":"top","pointId":"pcb_port_3","pcb_port_id":"pcb_port_3"},{"x":-1.3499999999999999,"y":-1.27,"layer":"top","pointId":"escape-via:source_net_0:pcb_port_3:bottom:8","terminalVia":{"toLayer":"bottom","viaDiameter":0.3}}],"name":"source_net_0_mst4","rootConnectionName":"source_net_0"},{"name":"source_trace_12","pointsToConnect":[{"x":8.45,"y":0.8238738706894861,"layer":"top","pointId":"pcb_port_19","pcb_port_id":"pcb_port_19"},{"x":2.15,"y":1.27,"layer":"top","pointId":"pcb_port_8","pcb_port_id":"pcb_port_8"}],"source_trace_id":"source_trace_12","rootConnectionName":"source_trace_12"},{"name":"source_trace_11","pointsToConnect":[{"x":8.45,"y":-0.4461261293105139,"layer":"top","pointId":"pcb_port_18","pcb_port_id":"pcb_port_18"},{"x":2.15,"y":2.54,"layer":"top","pointId":"pcb_port_9","pcb_port_id":"pcb_port_9"}],"source_trace_id":"source_trace_11","rootConnectionName":"source_trace_11"},{"pointsToConnect":[{"x":3.3500000000000005,"y":-1.716126129310514,"layer":"top","pointId":"escape-via:source_trace_9:pcb_port_16:inner2:0","terminalVia":{"toLayer":"inner2","viaDiameter":0.3}},{"x":4.15,"y":-1.716126129310514,"layer":"top","pointId":"pcb_port_16","pcb_port_id":"pcb_port_16"}],"name":"source_trace_9__source_net_1_mst0","rootConnectionName":"source_trace_9__source_net_1","mergedConnectionNames":{"$ref":"$.simpleRouteJson.connections[8].mergedConnectionNames"}},{"pointsToConnect":[{"x":-2.15,"y":0,"layer":"top","pointId":"pcb_port_2","pcb_port_id":"pcb_port_2"},{"x":-1.3499999999999999,"y":0,"layer":"top","pointId":"escape-via:source_net_1:pcb_port_2:inner2:2","terminalVia":{"toLayer":"inner2","viaDiameter":0.3}}],"name":"source_trace_9__source_net_1_mst1","rootConnectionName":"source_trace_9__source_net_1","mergedConnectionNames":["source_trace_9","source_net_1"]},{"name":"source_trace_8","pointsToConnect":[{"x":4.15,"y":-0.4461261293105139,"layer":"top","pointId":"pcb_port_15","pcb_port_id":"pcb_port_15"},{"x":2.15,"y":-1.27,"layer":"top","pointId":"pcb_port_6","pcb_port_id":"pcb_port_6"}],"source_trace_id":"source_trace_8","rootConnectionName":"source_trace_8"},{"name":"source_trace_7","pointsToConnect":[{"x":4.15,"y":0.8238738706894861,"layer":"top","pointId":"pcb_port_14","pcb_port_id":"pcb_port_14"},{"x":2.15,"y":0,"layer":"top","pointId":"pcb_port_7","pcb_port_id":"pcb_port_7"}],"source_trace_id":"source_trace_7","rootConnectionName":"source_trace_7"},{"name":"source_trace_5","pointsToConnect":[{"x":3.97,"y":-3.2861261293105146,"layer":"top","pointId":"pcb_port_12","pcb_port_id":"pcb_port_12"},{"x":2.15,"y":-2.54,"layer":"top","pointId":"pcb_port_5","pcb_port_id":"pcb_port_5"}],"source_trace_id":"source_trace_5","rootConnectionName":"source_trace_5"},{"name":"source_trace_3","pointsToConnect":[{"x":-2.1500000000000004,"y":4.16,"layer":"top","pointId":"pcb_port_10","pcb_port_id":"pcb_port_10"},{"x":-2.15,"y":2.54,"layer":"top","pointId":"pcb_port_0","pcb_port_id":"pcb_port_0"}],"source_trace_id":"source_trace_3","rootConnectionName":"source_trace_3"}],"minTraceWidth":0.15}}] \ No newline at end of file From 5aa95ff387e58a28bf4f5f54eba96d25627108bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Tue, 14 Apr 2026 20:22:25 +0530 Subject: [PATCH 3/4] Preserve multi-layer regions during overlap carving --- lib/utils/finalizeRects.ts | 1 - lib/utils/rectdiff-geometry.ts | 17 +++ lib/utils/resizeSoftOverlaps.ts | 124 ++++++++++++++++-- .../rectDiffGridSolverPipeline.snap.svg | 2 +- .../__snapshots__/bugreport08-e3ec95.snap.svg | 2 +- .../__snapshots__/bugreport10-71239a.snap.svg | 2 +- .../__snapshots__/bugreport11-b2de3c.snap.svg | 2 +- .../__snapshots__/bugreport22-2a75ce.snap.svg | 2 +- .../__snapshots__/bugreport24-05597c.snap.svg | 2 +- .../node-solver-input-repro.snap.svg | 6 +- .../node-solver-input-repro.test.ts | 7 + 11 files changed, 147 insertions(+), 20 deletions(-) diff --git a/lib/utils/finalizeRects.ts b/lib/utils/finalizeRects.ts index 2cf12a5..71a129e 100644 --- a/lib/utils/finalizeRects.ts +++ b/lib/utils/finalizeRects.ts @@ -12,7 +12,6 @@ export function finalizeRects(params: { zIndexByName: Map obstacleClearance?: number }): Rect3d[] { - // Convert all placed (free space) nodes to output format const out: Rect3d[] = params.placed.map((p) => ({ minX: p.rect.x, minY: p.rect.y, diff --git a/lib/utils/rectdiff-geometry.ts b/lib/utils/rectdiff-geometry.ts index f2deaec..08c76bf 100644 --- a/lib/utils/rectdiff-geometry.ts +++ b/lib/utils/rectdiff-geometry.ts @@ -85,3 +85,20 @@ export function subtractRect2D(A: XYRect, B: XYRect): XYRect[] { return out.filter((r) => r.width > EPS && r.height > EPS) } + +export function intersectRect2D(A: XYRect, B: XYRect): XYRect | null { + const Xi = intersect1D([A.x, A.x + A.width], [B.x, B.x + B.width]) + const Yi = intersect1D([A.y, A.y + A.height], [B.y, B.y + B.height]) + if (!Xi || !Yi) return null + + const [minX, maxX] = Xi + const [minY, maxY] = Yi + if (maxX - minX <= EPS || maxY - minY <= EPS) return null + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + } +} diff --git a/lib/utils/resizeSoftOverlaps.ts b/lib/utils/resizeSoftOverlaps.ts index ce52ad5..6b701ee 100644 --- a/lib/utils/resizeSoftOverlaps.ts +++ b/lib/utils/resizeSoftOverlaps.ts @@ -1,6 +1,11 @@ import type { RTreeRect } from "lib/types/capacity-mesh-types" import type { Placed3D } from "../rectdiff-types" -import { overlaps, subtractRect2D, EPS } from "./rectdiff-geometry" +import { + overlaps, + subtractRect2D, + intersectRect2D, + EPS, +} from "./rectdiff-geometry" import type RBush from "rbush" import { rectToTree } from "./rectToTree" @@ -30,18 +35,40 @@ export function resizeSoftOverlaps( if (sharedZ.length === 0) continue if (!overlaps(old.rect, newR)) continue - // Carve the overlap on the shared layers + // Carve only the shared layers. Non-overlapped pieces keep the full + // original layer set, and the overlapped core keeps only unaffected layers. const parts = subtractRect2D(old.rect, newR) + const overlapCore = intersectRect2D(old.rect, newR) + const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z)) + + if ( + process.env.RECTDIFF_DEBUG_OVERLAPS === "1" && + old.zLayers.length > 1 && + newZs.length === 1 + ) { + console.log( + "[resizeSoftOverlaps] carving multi-layer node", + JSON.stringify( + { + oldRect: old.rect, + oldZs: old.zLayers, + newcomerRect: newR, + newcomerZs: newZs, + sharedZ, + unaffectedZ, + partsCount: parts.length, + hasOverlapCore: Boolean(overlapCore), + }, + null, + 2, + ), + ) + } // We will replace `old` entirely; re-add unaffected layers (same rect object). removeIdx.push(i) - const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z)) - if (unaffectedZ.length > 0) { - toAdd.push({ rect: old.rect, zLayers: unaffectedZ }) - } - - // Re-add carved pieces for affected layers, dropping tiny slivers + // Outside the overlap, the old node still exists on all of its layers. const minW = Math.min( params.options.minSingle.width, params.options.minMulti.width, @@ -52,9 +79,86 @@ export function resizeSoftOverlaps( ) for (const p of parts) { if (p.width + EPS >= minW && p.height + EPS >= minH) { - toAdd.push({ rect: p, zLayers: sharedZ.slice() }) + toAdd.push({ rect: p, zLayers: old.zLayers.slice() }) } } + + // Inside the overlap, only the old node's non-shared layers remain. + if ( + overlapCore && + unaffectedZ.length > 0 && + overlapCore.width + EPS >= minW && + overlapCore.height + EPS >= minH + ) { + toAdd.push({ rect: overlapCore, zLayers: unaffectedZ }) + } + + // If the overlap core is too small to keep as a standalone unaffected node, + // the newcomer fully consumes that shared region for practical purposes. + if ( + process.env.RECTDIFF_DEBUG_OVERLAPS === "1" && + overlapCore && + unaffectedZ.length > 0 && + (overlapCore.width + EPS < minW || overlapCore.height + EPS < minH) + ) { + console.log( + "[resizeSoftOverlaps] dropped tiny overlap core", + JSON.stringify( + { + overlapCore, + unaffectedZ, + minW, + minH, + }, + null, + 2, + ), + ) + } + } + + for (const p of toAdd) { + p.zLayers = Array.from(new Set(p.zLayers)).sort((a, b) => a - b) + if (p.zLayers.length === 0) { + throw new Error("resizeSoftOverlaps produced an empty zLayers placement") + } + } + + const mergedToAdd: typeof params.placed = [] + const seenReplacements = new Map() + for (const p of toAdd) { + const key = [ + p.rect.x.toFixed(9), + p.rect.y.toFixed(9), + p.rect.width.toFixed(9), + p.rect.height.toFixed(9), + ].join(":") + const existing = seenReplacements.get(key) + if (!existing) { + seenReplacements.set(key, { + rect: p.rect, + zLayers: p.zLayers.slice(), + }) + continue + } + + for (const z of p.zLayers) { + if (!existing.zLayers.includes(z)) existing.zLayers.push(z) + } + existing.zLayers.sort((a, b) => a - b) + } + + for (const merged of seenReplacements.values()) { + if ( + process.env.RECTDIFF_DEBUG_OVERLAPS === "1" && + merged.zLayers.length > 1 + ) { + console.log( + "[resizeSoftOverlaps] merged replacement", + JSON.stringify(merged, null, 2), + ) + } + mergedToAdd.push(merged) } // Remove fully overlapped nodes and keep indexes in sync @@ -81,7 +185,7 @@ export function resizeSoftOverlaps( }) // Add replacements - for (const p of toAdd) { + for (const p of mergedToAdd) { params.placed.push(p) for (const z of p.zLayers) { if (params.placedIndexByLayer) { diff --git a/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg b/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg index 62703ef..790b237 100644 --- a/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +++ b/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg @@ -1,4 +1,4 @@ -Layer z=0Layer z=1Layer z=2Layer z=3