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
205 changes: 205 additions & 0 deletions lib/utils/canonicalizeLayeredRects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import type { Placed3D, Rect3d } from "../rectdiff-types"
import { EPS, containsPoint } from "./rectdiff-geometry"

type RectRun = {
minX: number
maxX: number
minY: number
maxY: number
zLayers: number[]
}

const sortNumbers = (a: number, b: number) => a - b

function dedupeSortedEdges(edges: number[]): number[] {
const sorted = edges.slice().sort(sortNumbers)
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
}

function splitIntoContiguousLayerRuns(zLayers: number[]): number[][] {
if (zLayers.length === 0) return []

const sorted = Array.from(new Set(zLayers)).sort(sortNumbers)
const out: number[][] = []
let current = [sorted[0]!]

for (let i = 1; i < sorted.length; i++) {
const z = sorted[i]!
const prev = current[current.length - 1]!

if (z === prev + 1) {
current.push(z)
continue
}

out.push(current)
current = [z]
}

out.push(current)
return out
}

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))
}

function buildRowRuns(placed: Placed3D[]): RectRun[] {
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: RectRun[] = []

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, RectRun>()

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
}

function mergeRunsVertically(runs: RectRun[]): 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
})
}

export function canonicalizeLayeredRects(placed: Placed3D[]): Rect3d[] {
if (placed.length === 0) return []
return mergeRunsVertically(buildRowRuns(placed))
}
117 changes: 115 additions & 2 deletions lib/utils/finalizeRects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
obstacleToXYRect,
obstacleZs,
} from "../solvers/RectDiffSeedingSolver/layers"
import { canonicalizeLayeredRects } from "./canonicalizeLayeredRects"
import { intersectRect2D, subtractRect2D, EPS } from "./rectdiff-geometry"

export function finalizeRects(params: {
placed: Placed3D[]
Expand All @@ -12,8 +14,119 @@ export function finalizeRects(params: {
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 overlapCore = intersectRect2D(placement.rect, promotedRect)
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())
}

if (process.env.RECTDIFF_DEBUG_FINALIZE === "1") {
const countByKey = (items: Array<{ zLayers: number[] }>) => {
const counts = new Map<string, number>()
for (const item of items) {
const key = item.zLayers.join(",")
counts.set(key, (counts.get(key) ?? 0) + 1)
}
return Object.fromEntries(counts)
}

console.log(
"[finalizeRects] promoted shared regions",
JSON.stringify(
{
inputCombos: countByKey(params.placed),
promotedCombos: countByKey(promotedRects),
outputCombos: countByKey(freePlacements),
},
null,
2,
),
)
}

const out: Rect3d[] = freePlacements.map((p) => ({
minX: p.rect.x,
minY: p.rect.y,
maxX: p.rect.x + p.rect.width,
Expand Down
17 changes: 17 additions & 0 deletions lib/utils/rectdiff-geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Loading
Loading