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
224 changes: 224 additions & 0 deletions lib/solvers/NodeMergeSolver/CoverageMergeSolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { BaseSolver } from "@tscircuit/solver-utils"
import type { GraphicsObject } from "graphics-debug"
import type { Placed3D, Rect3d } from "lib/rectdiff-types"
import type { ActiveRect } from "./shared"
import { activeRectToRect3d } from "./shared"
import {
addActiveRects,
addGridLines,
addInfoText,
addRect3dOverlays,
addSourcePlacements,
} from "./visualization"

export type CoverageMergeSolverInput = {
placed: Placed3D[]
xs: number[]
ys: number[]
layerCount: number
layerMasks: number[][]
maxAspectRatio?: number | null
}

export class CoverageMergeSolver extends BaseSolver {
private mergeRowIndex = 0
private activeRects = new Map<string, ActiveRect>()
private coverageRects: Rect3d[] = []
private lastMergedRects: Rect3d[] = []

constructor(private input: CoverageMergeSolverInput) {
super()
}

override _setup() {
this.stats = { mergeRowIndex: 0 }
}

override _step() {
this.lastMergedRects = []

if (this.mergeRowIndex >= this.input.ys.length - 1) {
for (const rect of this.activeRects.values()) {
this.pushFinalizedRect(rect)
}
this.activeRects.clear()
this.solved = true
return
}

const y = this.mergeRowIndex++
const nextActiveKeys = new Set<string>()
let x = 0

while (x < this.input.xs.length - 1) {
const mask = this.input.layerMasks[y]![x]!
if (mask === 0) {
x += 1
continue
}

const xStart = x
x += 1
while (
x < this.input.xs.length - 1 &&
this.input.layerMasks[y]![x] === mask
) {
x += 1
}

const key = `${mask}:${this.input.xs[xStart]}:${this.input.xs[x]}`
const existing = this.activeRects.get(key)
const nextMinY = this.input.ys[y]!
const nextMaxY = this.input.ys[y + 1]!
if (existing && existing.maxY === nextMinY) {
const width = existing.maxX - existing.minX
const currentHeight = existing.maxY - existing.minY
const nextHeight = nextMaxY - existing.minY
const maxAspectRatio = this.input.maxAspectRatio
const exceedsAspect =
maxAspectRatio != null &&
width > 0 &&
nextHeight > 0 &&
currentHeight >= width &&
nextHeight > maxAspectRatio * width

if (exceedsAspect) {
this.pushFinalizedRect(existing)
this.activeRects.set(key, {
minX: this.input.xs[xStart]!,
maxX: this.input.xs[x]!,
minY: nextMinY,
maxY: nextMaxY,
mask,
})
} else {
existing.maxY = nextMaxY
}
} else {
if (existing) {
this.pushFinalizedRect(existing)
}
this.activeRects.set(key, {
minX: this.input.xs[xStart]!,
maxX: this.input.xs[x]!,
minY: nextMinY,
maxY: nextMaxY,
mask,
})
}
nextActiveKeys.add(key)
}

for (const [key, rect] of Array.from(this.activeRects.entries())) {
if (!nextActiveKeys.has(key)) {
this.pushFinalizedRect(rect)
this.activeRects.delete(key)
}
}

this.stats.mergeRowIndex = this.mergeRowIndex
}

override getOutput() {
return {
coverageRects: this.coverageRects,
placed: this.input.placed,
xs: this.input.xs,
ys: this.input.ys,
layerCount: this.input.layerCount,
}
}

private pushFinalizedRect(rect: ActiveRect) {
const rect3d = activeRectToRect3d(rect, this.input.layerCount)
const split = this.splitRectByAspect(rect3d)
if (split.length === 0) return
this.coverageRects.push(...split)
this.lastMergedRects.push(...split)
}

private splitRectByAspect(rect: Rect3d): Rect3d[] {
const width = rect.maxX - rect.minX
const height = rect.maxY - rect.minY
if (!Number.isFinite(width) || !Number.isFinite(height)) return []
if (width <= 0 || height <= 0) return []
const minSizeEps = 1e-6
if (width < minSizeEps || height < minSizeEps) return []

const maxAspectRatio = this.input.maxAspectRatio
if (maxAspectRatio == null || maxAspectRatio <= 0) return [rect]

const ratio = Math.max(width / height, height / width)
if (ratio <= maxAspectRatio) return [rect]

if (width >= height) {
const maxWidth = maxAspectRatio * height
if (!Number.isFinite(maxWidth) || maxWidth <= 0) return []
const parts = Math.max(1, Math.ceil(width / maxWidth))
if (parts > 10000) return []
const step = width / parts
return Array.from({ length: parts }, (_, index) => {
const minX = rect.minX + index * step
const maxX =
index === parts - 1 ? rect.maxX : rect.minX + (index + 1) * step
return {
...rect,
minX,
maxX,
}
})
}

const maxHeight = maxAspectRatio * width
if (!Number.isFinite(maxHeight) || maxHeight <= 0) return []
const parts = Math.max(1, Math.ceil(height / maxHeight))
if (parts > 10000) return []
const step = height / parts
return Array.from({ length: parts }, (_, index) => {
const minY = rect.minY + index * step
const maxY =
index === parts - 1 ? rect.maxY : rect.minY + (index + 1) * step
return {
...rect,
minY,
maxY,
}
})
}

override visualize(): GraphicsObject {
const rects: NonNullable<GraphicsObject["rects"]> = []
const lines: NonNullable<GraphicsObject["lines"]> = []
const texts: NonNullable<GraphicsObject["texts"]> = []

addSourcePlacements(rects, this.input.placed)
addGridLines(lines, this.input.xs, this.input.ys)
addRect3dOverlays(rects, this.coverageRects, "coverage")
addActiveRects(
rects,
Array.from(this.activeRects.values()),
this.input.layerCount,
)
addRect3dOverlays(rects, this.lastMergedRects, "merged")

addInfoText(
texts,
this.input.xs,
this.input.ys,
[
`phase: merge`,
`row: ${this.mergeRowIndex}/${Math.max(0, this.input.ys.length - 1)}`,
`coverage rects: ${this.coverageRects.length}`,
].join("\n"),
)

return {
title: "Coverage Partition - Merge",
coordinateSystem: "cartesian",
rects,
points: [],
lines,
texts,
}
}
}
135 changes: 135 additions & 0 deletions lib/solvers/NodeMergeSolver/MaskGenerationSolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { BaseSolver } from "@tscircuit/solver-utils"
import type { GraphicsObject } from "graphics-debug"
import type { Placed3D } from "lib/rectdiff-types"
import { getColorForZLayer } from "lib/utils/getColorForZLayer"
import { addGridLines, addInfoText, addSourcePlacements } from "./visualization"
import { maskToZLayers } from "./shared"

export type MaskGenerationSolverInput = {
placed: Placed3D[]
xs: number[]
ys: number[]
layerCount: number
layerDiffs: number[][][]
}

export class MaskGenerationSolver extends BaseSolver {
private layerMasks: number[][] = []
private maskLayerIndex = 0
private maskRowIndex = 0
private lastMaskRow: Array<{
x1: number
x2: number
y1: number
y2: number
mask: number
}> = []

constructor(private input: MaskGenerationSolverInput) {
super()
}

override _setup() {
this.layerMasks = Array.from(
{ length: Math.max(0, this.input.ys.length - 1) },
() =>
Array.from({ length: Math.max(0, this.input.xs.length - 1) }, () => 0),
)
this.stats = { maskLayerIndex: 0, maskRowIndex: 0 }
}

override _step() {
this.lastMaskRow = []

if (this.maskLayerIndex >= this.input.layerCount) {
this.solved = true
return
}

if (this.maskRowIndex >= this.input.ys.length - 1) {
this.maskLayerIndex += 1
this.maskRowIndex = 0
return
}

const z = this.maskLayerIndex
const y = this.maskRowIndex++
const diff = this.input.layerDiffs[z]!
let rowRunning = 0

for (let x = 0; x < this.input.xs.length - 1; x++) {
rowRunning += diff[y]![x]!
const cellCount = rowRunning + (y > 0 ? diff[y - 1]![x]! : 0)
diff[y]![x] = cellCount
if (cellCount > 0) {
this.layerMasks[y]![x]! |= 1 << z
this.lastMaskRow.push({
x1: this.input.xs[x]!,
x2: this.input.xs[x + 1]!,
y1: this.input.ys[y]!,
y2: this.input.ys[y + 1]!,
mask: this.layerMasks[y]![x]!,
})
}
}

this.stats.maskLayerIndex = this.maskLayerIndex
this.stats.maskRowIndex = this.maskRowIndex
}

override getOutput() {
return {
placed: this.input.placed,
xs: this.input.xs,
ys: this.input.ys,
layerCount: this.input.layerCount,
layerMasks: this.layerMasks,
}
}

override visualize(): GraphicsObject {
const rects: NonNullable<GraphicsObject["rects"]> = []
const lines: NonNullable<GraphicsObject["lines"]> = []
const texts: NonNullable<GraphicsObject["texts"]> = []

addSourcePlacements(rects, this.input.placed)
addGridLines(lines, this.input.xs, this.input.ys)

for (const cell of this.lastMaskRow) {
const colors = getColorForZLayer(
maskToZLayers(cell.mask, this.input.layerCount),
)
rects.push({
center: {
x: (cell.x1 + cell.x2) / 2,
y: (cell.y1 + cell.y2) / 2,
},
width: cell.x2 - cell.x1,
height: cell.y2 - cell.y1,
fill: colors.fill,
stroke: colors.stroke,
label: `mask\nz:${maskToZLayers(cell.mask, this.input.layerCount).join(",")}`,
})
}

addInfoText(
texts,
this.input.xs,
this.input.ys,
[
`phase: masks`,
`layer: ${Math.min(this.maskLayerIndex + 1, Math.max(1, this.input.layerCount))}/${Math.max(1, this.input.layerCount)}`,
`row: ${this.maskRowIndex}/${Math.max(0, this.input.ys.length - 1)}`,
].join("\n"),
)

return {
title: "Coverage Partition - Masks",
coordinateSystem: "cartesian",
rects,
points: [],
lines,
texts,
}
}
}
Loading
Loading