From 5c80998654d1e3aa7b15a70dc96ee8097d9611b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Mon, 27 Apr 2026 15:24:38 +0530 Subject: [PATCH 1/4] force multi-layer nodes when possible --- .../RectDiffExpansionSolver.ts | 80 +++++++++++++++++++ .../RectDiffSeedingSolver.ts | 25 ++++-- lib/utils/finalizeRects.ts | 58 ++++++++++++-- tests/__snapshots__/board-outline.snap.svg | 4 +- .../rectDiffGridSolverPipeline.snap.svg | 6 +- ...no-uno-inner2-ground-bottom-power.snap.svg | 6 +- ...no-uno-inner2-ground-inner1-power.snap.svg | 6 +- .../both-points-equivalent.snap.svg | 2 +- .../__snapshots__/bugreport01-be84eb.snap.svg | 2 +- .../__snapshots__/bugreport02-bc4361.snap.svg | 6 +- .../__snapshots__/bugreport03-fe4a17.snap.svg | 2 +- .../__snapshots__/bugreport07-d3f3be.snap.svg | 2 +- .../__snapshots__/bugreport08-e3ec95.snap.svg | 2 +- .../__snapshots__/bugreport09-618e09.snap.svg | 4 +- .../__snapshots__/bugreport10-71239a.snap.svg | 4 +- .../__snapshots__/bugreport11-b2de3c.snap.svg | 6 +- ...ort11-b2de3c-clearance-equivalence.test.ts | 2 +- .../__snapshots__/bugreport12-35ce1c.snap.svg | 2 +- .../__snapshots__/bugreport13-b9a758.snap.svg | 2 +- .../__snapshots__/bugreport16-d95f38.snap.svg | 2 +- .../__snapshots__/bugreport18-1b2d06.snap.svg | 6 +- .../__snapshots__/bugreport19.snap.svg | 6 +- .../bugreport20-obstacle-clipping.snap.svg | 6 +- .../bugreport21-board-outline.snap.svg | 4 +- .../__snapshots__/bugreport22-2a75ce.snap.svg | 2 +- .../bugreport23-LGA15x4.snap.svg | 2 +- .../__snapshots__/bugreport24-05597c.snap.svg | 2 +- .../__snapshots__/bugreport25-4b1d55.snap.svg | 2 +- .../__snapshots__/bugreport26-66b0b2.snap.svg | 2 +- .../__snapshots__/bugreport27-dd3734.snap.svg | 2 +- .../__snapshots__/bugreport28-18a9ef.snap.svg | 2 +- .../__snapshots__/bugreport29-7deae8.snap.svg | 2 +- .../__snapshots__/bugreport30-2174c8.snap.svg | 2 +- .../__snapshots__/bugreport33-213d45.snap.svg | 2 +- .../__snapshots__/bugreport34-e9dea2.snap.svg | 2 +- .../__snapshots__/bugreport35-191db9.snap.svg | 2 +- .../__snapshots__/bugreport36-bf8303.snap.svg | 2 +- .../__snapshots__/interaction.snap.svg | 2 +- .../__snapshots__/multi-point.snap.svg | 2 +- .../__snapshots__/no-better-path.snap.svg | 2 +- .../__snapshots__/offboardconnects01.snap.svg | 2 +- ...hould-return-root-connection-name.snap.svg | 2 +- .../merge-single-layer-node.snap.svg | 6 +- .../__snapshots__/transitivity.snap.svg | 4 +- 44 files changed, 211 insertions(+), 80 deletions(-) diff --git a/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts b/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts index b37a364..2a52519 100644 --- a/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +++ b/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts @@ -13,6 +13,7 @@ import type { Obstacle } from "../../types/srj-types" import RBush from "rbush" import { rectToTree } from "../../utils/rectToTree" import { sameTreeRect } from "../../utils/sameTreeRect" +import { overlaps } from "../../utils/rectdiff-geometry" export type RectDiffExpansionSolverInput = { layerNames: string[] @@ -107,6 +108,7 @@ export class RectDiffExpansionSolver extends BaseSolver { zLayers: p.zLayers, }) + const rectToUse = expanded ?? oldRect if (expanded) { // Update placement + per-layer index (replace old rect object) this.input.placed[idx] = { rect: expanded, zLayers: p.zLayers } @@ -130,9 +132,87 @@ export class RectDiffExpansionSolver extends BaseSolver { ) } + this.expandPlacementLayers(idx, rectToUse) + this.input.expansionIndex += 1 } + private expandPlacementLayers(idx: number, rect: XYRect) { + const placement = this.input.placed[idx] + if (!placement) return + + const currentLayers = placement.zLayers.slice().sort((a, b) => a - b) + if (currentLayers.length === 0) return + + const query = { + minX: rect.x, + minY: rect.y, + maxX: rect.x + rect.width, + maxY: rect.y + rect.height, + } + + const isLayerFree = (layer: number) => { + const placedIndex = this.placedIndexByLayer[layer] + if (placedIndex) { + const hits = placedIndex.search(query) + for (const hit of hits) { + if (!overlaps(rect, hit)) continue + if (hit.zLayers.length >= this.input.layerCount) return false + } + } + + return true + } + + const minLayer = currentLayers[0]! + const maxLayer = currentLayers[currentLayers.length - 1]! + let nextMin = minLayer + let nextMax = maxLayer + + while (nextMin - 1 >= 0 && isLayerFree(nextMin - 1)) { + nextMin -= 1 + } + + while (nextMax + 1 < this.input.layerCount && isLayerFree(nextMax + 1)) { + nextMax += 1 + } + + if (nextMin === minLayer && nextMax === maxLayer) return + + const nextLayers: number[] = [] + for (let z = nextMin; z <= nextMax; z += 1) { + nextLayers.push(z) + } + + for (const z of currentLayers) { + const tree = this.placedIndexByLayer[z] + if (tree) { + tree.remove(rectToTree(rect, { zLayers: currentLayers }), sameTreeRect) + tree.insert(rectToTree(rect, { zLayers: nextLayers })) + } + } + + for (const z of nextLayers) { + if (currentLayers.includes(z)) continue + const tree = this.placedIndexByLayer[z] + if (tree) { + tree.insert(rectToTree(rect, { zLayers: nextLayers })) + } + } + + this.input.placed[idx] = { rect, zLayers: nextLayers } + + resizeSoftOverlaps( + { + layerCount: this.input.layerCount, + placed: this.input.placed, + options: this.input.options, + placedIndexByLayer: this.placedIndexByLayer, + }, + idx, + ) + } + private finalizeIfNeeded() { if (this.solved) return diff --git a/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts b/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts index a35b207..06d6241 100644 --- a/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +++ b/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts @@ -102,8 +102,8 @@ export class RectDiffSeedingSolver extends BaseSolver { maxAspectRatio: 3, minSingle: { width: 2 * trace, height: 2 * trace }, minMulti: { - width: 4 * trace, - height: 4 * trace, + width: 2 * trace, + height: 2 * trace, minLayers: Math.min(2, Math.max(1, srj.layerCount || 1)), }, preferMultiLayer: true, @@ -243,11 +243,20 @@ export class RectDiffSeedingSolver extends BaseSolver { }> = [] if (span.length >= minMulti.minLayers) { - attempts.push({ - kind: "multi", - layers: span, - minReq: { width: minMulti.width, height: minMulti.height }, - }) + const spanIndex = span.indexOf(cand.z) + let lo = 0 + let hi = span.length - 1 + + while (hi - lo + 1 >= minMulti.minLayers) { + attempts.push({ + kind: "multi", + layers: span.slice(lo, hi + 1), + minReq: { width: minMulti.width, height: minMulti.height }, + }) + if (hi - lo + 1 === minMulti.minLayers) break + if (spanIndex - lo > hi - spanIndex) lo += 1 + else hi -= 1 + } } attempts.push({ kind: "single", @@ -255,7 +264,7 @@ export class RectDiffSeedingSolver extends BaseSolver { minReq: { width: minSingle.width, height: minSingle.height }, }) - const ordered = preferMultiLayer ? attempts : attempts.reverse() + const ordered = preferMultiLayer ? attempts : attempts.slice().reverse() for (const attempt of ordered) { const rect = expandRectFromSeed({ diff --git a/lib/utils/finalizeRects.ts b/lib/utils/finalizeRects.ts index da3dd55..291d294 100644 --- a/lib/utils/finalizeRects.ts +++ b/lib/utils/finalizeRects.ts @@ -1,5 +1,6 @@ import type { Obstacle } from "../types/srj-types" import type { Placed3D, Rect3d, XYRect } from "../rectdiff-types" +import { subtractRect2D } from "./rectdiff-geometry" import { obstacleToXYRect, obstacleZs, @@ -12,14 +13,8 @@ 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, - maxX: p.rect.x + p.rect.width, - maxY: p.rect.y + p.rect.height, - zLayers: [...p.zLayers].sort((a, b) => a - b), - })) + const out: Rect3d[] = [] + const obstacleRectsByLayer = new Map() const layersByKey = new Map }>() @@ -38,6 +33,11 @@ export function finalizeRects(params: { obstacle.zLayers?.length && obstacle.zLayers.length > 0 ? obstacle.zLayers : obstacleZs(obstacle, params.zIndexByName) + for (const layer of zLayers) { + const list = obstacleRectsByLayer.get(layer) + if (list) list.push(rect) + else obstacleRectsByLayer.set(layer, [rect]) + } const key = `${rect.x}:${rect.y}:${rect.width}:${rect.height}` let entry = layersByKey.get(key) if (!entry) { @@ -47,6 +47,48 @@ export function finalizeRects(params: { zLayers.forEach((layer: number) => entry!.layers.add(layer)) } + const freeRectsByKey = new Map< + string, + { rect: XYRect; layers: Set } + >() + + for (const placed of params.placed) { + for (const layer of placed.zLayers) { + let parts: XYRect[] = [placed.rect] + const blockers = obstacleRectsByLayer.get(layer) ?? [] + for (const blocker of blockers) { + const nextParts: XYRect[] = [] + for (const part of parts) { + nextParts.push(...subtractRect2D(part, blocker)) + } + parts = nextParts + if (parts.length === 0) break + } + + for (const part of parts) { + const key = `${part.x}:${part.y}:${part.width}:${part.height}` + let entry = freeRectsByKey.get(key) + if (!entry) { + entry = { rect: part, layers: new Set() } + freeRectsByKey.set(key, entry) + } + entry.layers.add(layer) + } + } + } + + for (const { rect, layers } of freeRectsByKey.values()) { + const layerList = Array.from(layers).sort((a, b) => a - b) + if (layerList.length === 0) continue + out.push({ + minX: rect.x, + minY: rect.y, + maxX: rect.x + rect.width, + maxY: rect.y + rect.height, + zLayers: layerList, + }) + } + for (const { rect, layers } of layersByKey.values()) { out.push({ minX: rect.x, diff --git a/tests/__snapshots__/board-outline.snap.svg b/tests/__snapshots__/board-outline.snap.svg index 8f9b27f..91cc710 100644 --- a/tests/__snapshots__/board-outline.snap.svg +++ b/tests/__snapshots__/board-outline.snap.svg @@ -1,4 +1,4 @@ -