diff --git a/lib/RectDiffPipeline.ts b/lib/RectDiffPipeline.ts index d9b3a32..4fafba1 100644 --- a/lib/RectDiffPipeline.ts +++ b/lib/RectDiffPipeline.ts @@ -60,7 +60,6 @@ export class RectDiffPipeline extends BasePipelineSolver } override initialVisualize(): GraphicsObject { - console.log("RectDiffPipeline - initialVisualize") const graphics = createBaseVisualization( this.inputProblem.simpleRouteJson, "RectDiffPipeline - Initial", diff --git a/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts b/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts index 129e459..2e9811b 100644 --- a/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +++ b/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts @@ -18,6 +18,7 @@ import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ" import { allLayerNode } from "../../utils/buildHardPlacedByLayer" import { isFullyOccupiedAtPoint } from "lib/utils/isFullyOccupiedAtPoint" import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps" +import { getColorForZLayer } from "lib/utils/getColorForZLayer" import RBush from "rbush" import type { RTreeRect } from "lib/types/capacity-mesh-types" @@ -336,23 +337,6 @@ export class RectDiffSeedingSolver extends BaseSolver { } } - /** Get color based on z layer for visualization. */ - private getColorForZLayer(zLayers: number[]): { - fill: string - stroke: string - } { - const minZ = Math.min(...zLayers) - const colors = [ - { fill: "#dbeafe", stroke: "#3b82f6" }, - { fill: "#fef3c7", stroke: "#f59e0b" }, - { fill: "#d1fae5", stroke: "#10b981" }, - { fill: "#e9d5ff", stroke: "#a855f7" }, - { fill: "#fed7aa", stroke: "#f97316" }, - { fill: "#fecaca", stroke: "#ef4444" }, - ] as const - return colors[minZ % colors.length]! - } - /** Visualization focused on the grid seeding phase. */ override visualize(): GraphicsObject { const rects: NonNullable = [] @@ -460,7 +444,7 @@ export class RectDiffSeedingSolver extends BaseSolver { // current placements (streaming) during grid fill if (this.placed?.length) { for (const placement of this.placed) { - const colors = this.getColorForZLayer(placement.zLayers) + const colors = getColorForZLayer(placement.zLayers) rects.push({ center: { x: placement.rect.x + placement.rect.width / 2, diff --git a/lib/utils/getColorForZLayer.ts b/lib/utils/getColorForZLayer.ts new file mode 100644 index 0000000..40f580d --- /dev/null +++ b/lib/utils/getColorForZLayer.ts @@ -0,0 +1,17 @@ +export const getColorForZLayer = ( + zLayers: number[], +): { + fill: string + stroke: string +} => { + const minZ = Math.min(...zLayers) + const colors = [ + { fill: "#dbeafe", stroke: "#3b82f6" }, + { fill: "#fef3c7", stroke: "#f59e0b" }, + { fill: "#d1fae5", stroke: "#10b981" }, + { fill: "#e9d5ff", stroke: "#a855f7" }, + { fill: "#fed7aa", stroke: "#f97316" }, + { fill: "#fecaca", stroke: "#ef4444" }, + ] as const + return colors[minZ % colors.length]! +} diff --git a/tests/fixtures/getPerLayerVisualizations.ts b/tests/fixtures/getPerLayerVisualizations.ts new file mode 100644 index 0000000..7f6f0f8 --- /dev/null +++ b/tests/fixtures/getPerLayerVisualizations.ts @@ -0,0 +1,130 @@ +import type { GraphicsObject, Line, Point, Rect } from "graphics-debug" + +export function getPerLayerVisualizations( + graphics: GraphicsObject, +): Map { + const rects = (graphics.rects ?? []) as NonNullable + const lines = (graphics.lines ?? []) as NonNullable + const points = (graphics.points ?? []) as NonNullable + + const zValues = new Set() + + const addZValuesFromLayer = (layer: string) => { + if (!layer.startsWith("z")) return + const rest = layer.slice(1) + if (!rest) return + for (const part of rest.split(",")) { + const value = Number.parseInt(part, 10) + if (!Number.isNaN(value)) zValues.add(value) + } + } + + for (const rect of rects) addZValuesFromLayer(rect.layer!) + for (const line of lines) addZValuesFromLayer(line.layer!) + for (const point of points) addZValuesFromLayer(point.layer!) + + const result = new Map() + if (!zValues.size) return result + + const sortedZ = Array.from(zValues).sort((a, b) => a - b) + + const commonRects: NonNullable = [] + const perLayerRects: { layers: number[]; rect: Rect }[] = [] + + for (const rect of rects) { + const layer = rect.layer! + if (layer.startsWith("z")) { + const rest = layer.slice(1) + if (rest) { + const layers = rest + .split(",") + .map((part) => Number.parseInt(part, 10)) + .filter((value) => !Number.isNaN(value)) + if (layers.length) { + perLayerRects.push({ layers, rect }) + continue + } + } + } + commonRects.push(rect) + } + + const commonLines: NonNullable = [] + const perLayerLines: { layers: number[]; line: Line }[] = [] + + for (const line of lines) { + const layer = line.layer! + if (layer.startsWith("z")) { + const rest = layer.slice(1) + if (rest) { + const layers = rest + .split(",") + .map((part) => Number.parseInt(part, 10)) + .filter((value) => !Number.isNaN(value)) + if (layers.length) { + perLayerLines.push({ layers, line }) + continue + } + } + } + commonLines.push(line) + } + + const commonPoints: NonNullable = [] + const perLayerPoints: { layers: number[]; point: Point }[] = [] + + for (const point of points) { + const layer = point.layer! + if (layer.startsWith("z")) { + const rest = layer.slice(1) + if (rest) { + const layers = rest + .split(",") + .map((part) => Number.parseInt(part, 10)) + .filter((value) => !Number.isNaN(value)) + if (layers.length) { + perLayerPoints.push({ layers, point }) + continue + } + } + } + commonPoints.push(point) + } + + const allCombos: number[][] = [[]] + for (const z of sortedZ) { + const withZ = allCombos.map((combo) => [...combo, z]) + allCombos.push(...withZ) + } + + for (const combo of allCombos.filter((c) => c.length > 0)) { + const key = `z${combo.join(",")}` + + const layerRects: NonNullable = [...commonRects] + const layerLines: NonNullable = [...commonLines] + const layerPoints: NonNullable = [...commonPoints] + + const intersects = (layers: number[]) => + layers.some((layer) => combo.includes(layer)) + + for (const { layers, rect } of perLayerRects) { + if (intersects(layers)) layerRects.push(rect) + } + for (const { layers, line } of perLayerLines) { + if (intersects(layers)) layerLines.push(line) + } + for (const { layers, point } of perLayerPoints) { + if (intersects(layers)) layerPoints.push(point) + } + + result.set(key, { + title: `${graphics.title ?? ""} - z${combo.join(",")}`, + coordinateSystem: graphics.coordinateSystem, + rects: layerRects, + lines: layerLines, + points: layerPoints, + }) + } + + return result +} diff --git a/tests/fixtures/makeCapacityMeshNodeWithLayerInfo.ts b/tests/fixtures/makeCapacityMeshNodeWithLayerInfo.ts new file mode 100644 index 0000000..89b9a2e --- /dev/null +++ b/tests/fixtures/makeCapacityMeshNodeWithLayerInfo.ts @@ -0,0 +1,33 @@ +import type { Rect } from "graphics-debug" +import type { CapacityMeshNode } from "lib/types/capacity-mesh-types" +import { getColorForZLayer } from "lib/utils/getColorForZLayer" + +export const makeCapacityMeshNodeWithLayerInfo = ( + nodes: CapacityMeshNode[], +): Map => { + const map = new Map() + + for (const node of nodes) { + if (!node.availableZ.length) continue + const key = node.availableZ.join(",") + const colors = getColorForZLayer(node.availableZ) + const rect: Rect = { + center: node.center, + width: node.width, + height: node.height, + layer: `z${key}`, + stroke: "black", + fill: node._containsObstacle ? "red" : colors.fill, + label: "node", + } + + const existing = map.get(key) + if (existing) { + existing.push(rect) + } else { + map.set(key, [rect]) + } + } + + return map +} diff --git a/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg b/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg new file mode 100644 index 0000000..39b7e92 --- /dev/null +++ b/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg @@ -0,0 +1,44 @@ +Layer z=0Layer z=1Layer z=2Layer z=3 \ No newline at end of file diff --git a/tests/solver/rectDiffGridSolverPipeline.test.ts b/tests/solver/rectDiffGridSolverPipeline.test.ts new file mode 100644 index 0000000..b4e1024 --- /dev/null +++ b/tests/solver/rectDiffGridSolverPipeline.test.ts @@ -0,0 +1,88 @@ +import { expect, test } from "bun:test" +import srj from "test-assets/bugreport11-b2de3c.json" +import { + getBounds, + getSvgFromGraphicsObject, + stackGraphicsVertically, + type GraphicsObject, + type Rect, +} from "graphics-debug" +import { RectDiffPipeline } from "lib/RectDiffPipeline" +import { makeCapacityMeshNodeWithLayerInfo } from "tests/fixtures/makeCapacityMeshNodeWithLayerInfo" + +test("RectDiffPipeline mesh layer snapshots", async () => { + const solver = new RectDiffPipeline({ + simpleRouteJson: srj.simple_route_json, + }) + + solver.solve() + + const { meshNodes } = solver.getOutput() + const rectsByCombo = makeCapacityMeshNodeWithLayerInfo(meshNodes) + const allGraphicsObjects: GraphicsObject[] = [] + + // Generate a snapshot for each z-layer + for (const z of [0, 1, 2, 3]) { + const layerRects: Rect[] = [] + + for (const [key, rects] of rectsByCombo) { + const layers = key + .split(",") + .map((value) => Number.parseInt(value, 10)) + .filter((value) => !Number.isNaN(value)) + + if (layers.includes(z)) { + layerRects.push(...rects) + } + } + + let labelY = 0 + + if (layerRects.length > 0) { + let maxY = -Infinity + + for (const rect of layerRects) { + const top = rect.center.y + rect.height * (2 / 3) + + if (top > maxY) maxY = top + } + + labelY = maxY + } + + const graphics: GraphicsObject = { + title: `RectDiffPipeline - z${z}`, + texts: [ + { + anchorSide: "top_right", + text: `Layer z=${z}`, + x: 0, + y: labelY, + fontSize: 0.5, + }, + ], + coordinateSystem: "cartesian", + rects: layerRects, + points: [], + lines: [], + } + + allGraphicsObjects.push(graphics) + } + + const stackedGraphics = stackGraphicsVertically(allGraphicsObjects) + const bounds = getBounds(stackedGraphics) + const boundsWidth = Math.max(1, bounds.maxX - bounds.minX) + const boundsHeight = Math.max(1, bounds.maxY - bounds.minY) + const svgWidth = 640 + const svgHeight = Math.max( + svgWidth, + Math.ceil((boundsHeight / boundsWidth) * svgWidth), + ) + + const svg = getSvgFromGraphicsObject(stackedGraphics, { + svgWidth, + svgHeight, + }) + await expect(svg).toMatchSvgSnapshot(import.meta.path) +})