Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions lib/utils/buildRowRuns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { Placed3D } from "../rectdiff-types"
import { EPS } from "./rectdiff-geometry"
import { dedupeSortedEdges } from "./dedupeSortedEdges"
import { getCellLayerRuns } from "./getCellLayerRuns"

/** Build horizontal runs for layered rectangles. */
export function buildRowRuns(placed: Placed3D[]) {
const xEdges = dedupeSortedEdges(
placed.flatMap((placement) => [
placement.rect.x,
placement.rect.x + placement.rect.width,
]),
)
const yEdges = dedupeSortedEdges(
placed.flatMap((placement) => [
placement.rect.y,
placement.rect.y + placement.rect.height,
]),
)

const runs: Array<{
minX: number
maxX: number
minY: number
maxY: number
zLayers: number[]
}> = []

for (let yi = 0; yi < yEdges.length - 1; yi++) {
const minY = yEdges[yi]!
const maxY = yEdges[yi + 1]!
if (maxY - minY <= EPS) {
continue
}

const openRuns = new Map<
string,
{
minX: number
maxX: number
minY: number
maxY: number
zLayers: number[]
}
>()

for (let xi = 0; xi < xEdges.length - 1; xi++) {
const minX = xEdges[xi]!
const maxX = xEdges[xi + 1]!
if (maxX - minX <= EPS) {
continue
}

const layerRuns = getCellLayerRuns({
minX,
maxX,
minY,
maxY,
placed,
})

const seenKeys = new Set<string>()

for (const zLayers of layerRuns) {
const key = zLayers.join(",")
seenKeys.add(key)
const existing = openRuns.get(key)

if (existing && Math.abs(existing.maxX - minX) <= EPS) {
existing.maxX = maxX
continue
}

if (existing) {
runs.push(existing)
}

openRuns.set(key, {
minX,
maxX,
minY,
maxY,
zLayers,
})
}

for (const [key, run] of openRuns) {
if (seenKeys.has(key)) {
continue
}
runs.push(run)
openRuns.delete(key)
}
}

for (const run of openRuns.values()) {
runs.push(run)
}
}

return runs
}
11 changes: 11 additions & 0 deletions lib/utils/canonicalizeLayeredRects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Placed3D, Rect3d } from "../rectdiff-types"
import { buildRowRuns } from "./buildRowRuns"
import { mergeRunsVertically } from "./mergeRunsVertically"

/** Rebuild placements into rectangles grouped by shared zLayers. */
export function canonicalizeLayeredRects(placed: Placed3D[]): Rect3d[] {
if (placed.length === 0) {
return []
}
return mergeRunsVertically(buildRowRuns(placed))
}
16 changes: 16 additions & 0 deletions lib/utils/dedupeSortedEdges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { EPS } from "./rectdiff-geometry"

/** Dedupe sorted edge values with EPS tolerance. */
export function dedupeSortedEdges(edges: number[]): number[] {
const sorted = edges.slice().sort((a, b) => a - b)
const out: number[] = []

for (const edge of sorted) {
const last = out[out.length - 1]
if (last === undefined || Math.abs(edge - last) > EPS) {
out.push(edge)
}
}

return out
}
123 changes: 120 additions & 3 deletions lib/utils/finalizeRects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,131 @@ import {
obstacleToXYRect,
obstacleZs,
} from "../solvers/RectDiffSeedingSolver/layers"
import { canonicalizeLayeredRects } from "./canonicalizeLayeredRects"
import { intersect1D, subtractRect2D, EPS } from "./rectdiff-geometry"

/** Finalize free-space and obstacle rectangles. */
export function finalizeRects(params: {
placed: Placed3D[]
obstacles: Obstacle[]
boardVoidRects: XYRect[]
zIndexByName: Map<string, number>
obstacleClearance?: number
}): Rect3d[] {
// Convert all placed (free space) nodes to output format
const out: Rect3d[] = params.placed.map((p) => ({
const promotedRects = canonicalizeLayeredRects(params.placed).filter(
(rect) => rect.zLayers.length > 1,
)

let freePlacements: Placed3D[] = params.placed.map((placement) => ({
rect: { ...placement.rect },
zLayers: placement.zLayers.slice().sort((a, b) => a - b),
}))

for (const promoted of promotedRects) {
const promotedRect: XYRect = {
x: promoted.minX,
y: promoted.minY,
width: promoted.maxX - promoted.minX,
height: promoted.maxY - promoted.minY,
}

const nextPlacements: Placed3D[] = []

for (const placement of freePlacements) {
const sharedZ = placement.zLayers.filter((z) =>
promoted.zLayers.includes(z),
)
if (sharedZ.length === 0) {
nextPlacements.push(placement)
continue
}

const Xi = intersect1D(
[placement.rect.x, placement.rect.x + placement.rect.width],
[promotedRect.x, promotedRect.x + promotedRect.width],
)
const Yi = intersect1D(
[placement.rect.y, placement.rect.y + placement.rect.height],
[promotedRect.y, promotedRect.y + promotedRect.height],
)
let overlapCore: {
x: number
y: number
width: number
height: number
} | null = null
if (Xi && Yi && Xi[1] - Xi[0] > EPS && Yi[1] - Yi[0] > EPS) {
overlapCore = {
x: Xi[0],
y: Yi[0],
width: Xi[1] - Xi[0],
height: Yi[1] - Yi[0],
}
}
if (!overlapCore) {
nextPlacements.push(placement)
continue
}

const outsideParts = subtractRect2D(placement.rect, promotedRect)
for (const part of outsideParts) {
if (part.width > EPS && part.height > EPS) {
nextPlacements.push({
rect: part,
zLayers: placement.zLayers.slice(),
})
}
}

const unaffectedZ = placement.zLayers.filter(
(z) => !promoted.zLayers.includes(z),
)
if (
unaffectedZ.length > 0 &&
overlapCore.width > EPS &&
overlapCore.height > EPS
) {
nextPlacements.push({
rect: overlapCore,
zLayers: unaffectedZ,
})
}
}

nextPlacements.push({
rect: promotedRect,
zLayers: promoted.zLayers.slice(),
})

const deduped = new Map<string, Placed3D>()
for (const placement of nextPlacements) {
if (placement.rect.width <= EPS || placement.rect.height <= EPS) continue
placement.zLayers = Array.from(new Set(placement.zLayers)).sort(
(a, b) => a - b,
)
if (placement.zLayers.length === 0) continue

const key = [
placement.rect.x.toFixed(9),
placement.rect.y.toFixed(9),
placement.rect.width.toFixed(9),
placement.rect.height.toFixed(9),
].join(":")
const existing = deduped.get(key)
if (!existing) {
deduped.set(key, placement)
continue
}
for (const z of placement.zLayers) {
if (!existing.zLayers.includes(z)) existing.zLayers.push(z)
}
existing.zLayers.sort((a, b) => a - b)
}

freePlacements = Array.from(deduped.values())
}

const out: Rect3d[] = freePlacements.map((p) => ({
minX: p.rect.x,
minY: p.rect.y,
maxX: p.rect.x + p.rect.width,
Expand Down Expand Up @@ -44,7 +159,9 @@ export function finalizeRects(params: {
entry = { rect, layers: new Set() }
layersByKey.set(key, entry)
}
zLayers.forEach((layer: number) => entry!.layers.add(layer))
zLayers.forEach((layer: number) => {
entry!.layers.add(layer)
})
}

for (const { rect, layers } of layersByKey.values()) {
Expand Down
29 changes: 29 additions & 0 deletions lib/utils/getCellLayerRuns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Placed3D } from "../rectdiff-types"
import { containsPoint } from "./rectdiff-geometry"
import { splitIntoContiguousLayerRuns } from "./splitIntoContiguousLayerRuns"

/** Collect contiguous zLayer runs for a cell. */
export function getCellLayerRuns(params: {
minX: number
maxX: number
minY: number
maxY: number
placed: Placed3D[]
}): number[][] {
const { minX, maxX, minY, maxY, placed } = params
const midX = (minX + maxX) / 2
const midY = (minY + maxY) / 2
const zLayers = new Set<number>()

for (const placement of placed) {
if (!containsPoint(placement.rect, { x: midX, y: midY })) {
continue
}

for (const z of placement.zLayers) {
zLayers.add(z)
}
}

return splitIntoContiguousLayerRuns(Array.from(zLayers))
}
84 changes: 84 additions & 0 deletions lib/utils/mergeRunsVertically.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { Rect3d } from "../rectdiff-types"
import { EPS } from "./rectdiff-geometry"

/** Merge adjacent runs into rectangles. */
export function mergeRunsVertically(
runs: Array<{
minX: number
maxX: number
minY: number
maxY: number
zLayers: number[]
}>,
): Rect3d[] {
const sorted = runs.slice().sort((a, b) => {
if (Math.abs(a.minY - b.minY) > EPS) {
return a.minY - b.minY
}
if (Math.abs(a.maxY - b.maxY) > EPS) {
return a.maxY - b.maxY
}
if (Math.abs(a.minX - b.minX) > EPS) {
return a.minX - b.minX
}
if (Math.abs(a.maxX - b.maxX) > EPS) {
return a.maxX - b.maxX
}
return a.zLayers.join(",").localeCompare(b.zLayers.join(","))
})

const out: Rect3d[] = []
const active = new Map<string, Rect3d>()

for (const run of sorted) {
const key = [
run.minX.toFixed(9),
run.maxX.toFixed(9),
run.zLayers.join(","),
].join(":")
const existing = active.get(key)

if (existing && Math.abs(existing.maxY - run.minY) <= EPS) {
existing.maxY = run.maxY
continue
}

if (existing) {
out.push(existing)
}

active.set(key, {
minX: run.minX,
minY: run.minY,
maxX: run.maxX,
maxY: run.maxY,
zLayers: run.zLayers.slice(),
})
}

for (const rect of active.values()) {
out.push(rect)
}

return out
.filter(
(rect) => rect.maxX - rect.minX > EPS && rect.maxY - rect.minY > EPS,
)
.sort((a, b) => {
const aKey = a.zLayers.join(",")
const bKey = b.zLayers.join(",")
if (aKey !== bKey) {
return aKey.localeCompare(bKey)
}
if (Math.abs(a.minY - b.minY) > EPS) {
return a.minY - b.minY
}
if (Math.abs(a.minX - b.minX) > EPS) {
return a.minX - b.minX
}
if (Math.abs(a.maxY - b.maxY) > EPS) {
return a.maxY - b.maxY
}
return a.maxX - b.maxX
})
}
Loading
Loading