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 @@ -