From 24424d8c6de26de4a452e296a4f57cd6ddd3bd28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 18 Dec 2025 19:55:37 +0530 Subject: [PATCH 01/44] add gapfill and test to add gapfill --- lib/RectDiffPipeline.ts | 38 +- lib/solvers/GapFillSolver.ts | 453 ++++++++++++++++++ .../gap-fill-solver/multi-layer-gap.page.tsx | 49 ++ .../simple-two-rect-with-gap.page.tsx | 39 ++ .../gap-fill-solver/staggered-rects.page.tsx | 39 ++ .../vertical-and-horizontal-gaps.page.tsx | 51 ++ 6 files changed, 666 insertions(+), 3 deletions(-) create mode 100644 lib/solvers/GapFillSolver.ts create mode 100644 pages/repro/gap-fill-solver/multi-layer-gap.page.tsx create mode 100644 pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx create mode 100644 pages/repro/gap-fill-solver/staggered-rects.page.tsx create mode 100644 pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx diff --git a/lib/RectDiffPipeline.ts b/lib/RectDiffPipeline.ts index 425f004..6a7210e 100644 --- a/lib/RectDiffPipeline.ts +++ b/lib/RectDiffPipeline.ts @@ -2,6 +2,7 @@ import { BasePipelineSolver, definePipelineStep } from "@tscircuit/solver-utils" import type { SimpleRouteJson } from "./types/srj-types" import type { GridFill3DOptions } from "./solvers/rectdiff/types" import { RectDiffSolver } from "./solvers/RectDiffSolver" +import { GapFillSolver } from "./solvers/GapFillSolver" import type { CapacityMeshNode } from "./types/capacity-mesh-types" import type { GraphicsObject } from "graphics-debug" import { createBaseVisualization } from "./solvers/rectdiff/visualization" @@ -13,6 +14,7 @@ export interface RectDiffPipelineInput { export class RectDiffPipeline extends BasePipelineSolver { rectDiffSolver?: RectDiffSolver + gapFillSolver?: GapFillSolver override pipelineDef = [ definePipelineStep( @@ -30,6 +32,28 @@ export class RectDiffPipeline extends BasePipelineSolver }, }, ), + definePipelineStep( + "gapFillSolver", + GapFillSolver, + (instance) => { + const rectDiffSolver = + instance.getSolver("rectDiffSolver")! + const rectDiffState = (rectDiffSolver as any).state + + return [ + { + simpleRouteJson: instance.inputProblem.simpleRouteJson, + placedRects: rectDiffState.placed || [], + obstaclesByLayer: rectDiffState.obstaclesByLayer || [], + }, + ] + }, + { + onSolved: () => { + // Gap fill completed + }, + }, + ), ] override getConstructorParams() { @@ -41,9 +65,17 @@ export class RectDiffPipeline extends BasePipelineSolver } override visualize(): GraphicsObject { - const solver = this.getSolver("rectDiffSolver") - if (solver) { - return solver.visualize() + // Show the currently active solver's visualization + const gapFillSolver = this.getSolver("gapFillSolver") + if (gapFillSolver && !gapFillSolver.solved) { + // Gap fill is running, show its visualization + return gapFillSolver.visualize() + } + + const rectDiffSolver = this.getSolver("rectDiffSolver") + if (rectDiffSolver) { + // RectDiff is running or finished, show its visualization + return rectDiffSolver.visualize() } // Show board and obstacles even before solver is initialized diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts new file mode 100644 index 0000000..4282ac4 --- /dev/null +++ b/lib/solvers/GapFillSolver.ts @@ -0,0 +1,453 @@ +import { BaseSolver } from "@tscircuit/solver-utils" +import type { GraphicsObject } from "graphics-debug" +import type { SimpleRouteJson } from "../types/srj-types" +import type { Placed3D, XYRect } from "./rectdiff/types" + +export interface RectEdge { + rect: XYRect + side: "top" | "bottom" | "left" | "right" + x1: number + y1: number + x2: number + y2: number + normal: { x: number; y: number } + zLayers: number[] +} + +export interface UnoccupiedSection { + edge: RectEdge + start: number // 0 to 1 along edge + end: number // 0 to 1 along edge + x1: number + y1: number + x2: number + y2: number +} + +export interface ExpansionPoint { + x: number + y: number + zLayers: number[] + section: UnoccupiedSection +} + +export interface GapFillSolverInput { + simpleRouteJson: SimpleRouteJson + placedRects: Placed3D[] + obstaclesByLayer: XYRect[][] +} + +// Sub-phases for visualization +type SubPhase = + | "SELECT_PRIMARY_EDGE" + | "FIND_NEARBY_EDGES" + | "CHECK_UNOCCUPIED" + | "PLACE_EXPANSION_POINTS" + | "EXPAND_POINT" + | "DONE" + +interface GapFillState { + srj: SimpleRouteJson + inputRects: Placed3D[] + obstaclesByLayer: XYRect[][] + layerCount: number + + edges: RectEdge[] + + phase: SubPhase + currentEdgeIndex: number + currentPrimaryEdge?: RectEdge + + nearbyEdgeCandidateIndex: number + currentNearbyEdges: RectEdge[] + + currentUnoccupiedSections: UnoccupiedSection[] + + currentExpansionPoints: ExpansionPoint[] + currentExpansionIndex: number + + filledRects: Placed3D[] +} + +/** + * Gap Fill Solver - fills gaps between existing rectangles using edge analysis. + * Processes one edge per step for visualization. + */ +export class GapFillSolver extends BaseSolver { + private state!: GapFillState + + constructor(input: GapFillSolverInput) { + super() + this.state = this.initState(input) + } + + private initState(input: GapFillSolverInput): GapFillState { + const layerCount = input.simpleRouteJson.layerCount || 1 + + const edges = this.extractEdges(input.placedRects) + + return { + srj: input.simpleRouteJson, + inputRects: input.placedRects, + obstaclesByLayer: input.obstaclesByLayer, + layerCount, + edges, + phase: "SELECT_PRIMARY_EDGE", + currentEdgeIndex: 0, + nearbyEdgeCandidateIndex: 0, + currentNearbyEdges: [], + currentUnoccupiedSections: [], + currentExpansionPoints: [], + currentExpansionIndex: 0, + filledRects: [], + } + } + + private extractEdges(rects: Placed3D[]): RectEdge[] { + const edges: RectEdge[] = [] + + for (const placed of rects) { + const { rect, zLayers } = placed + const centerX = rect.x + rect.width / 2 + const centerY = rect.y + rect.height / 2 + + // Top edge (y = rect.y + rect.height) + edges.push({ + rect, + side: "top", + x1: rect.x, + y1: rect.y + rect.height, + x2: rect.x + rect.width, + y2: rect.y + rect.height, + normal: { x: 0, y: 1 }, // Points up + zLayers: [...zLayers], + }) + + // Bottom edge (y = rect.y) + edges.push({ + rect, + side: "bottom", + x1: rect.x, + y1: rect.y, + x2: rect.x + rect.width, + y2: rect.y, + normal: { x: 0, y: -1 }, // Points down + zLayers: [...zLayers], + }) + + // Right edge (x = rect.x + rect.width) + edges.push({ + rect, + side: "right", + x1: rect.x + rect.width, + y1: rect.y, + x2: rect.x + rect.width, + y2: rect.y + rect.height, + normal: { x: 1, y: 0 }, // Points right + zLayers: [...zLayers], + }) + + // Left edge (x = rect.x) + edges.push({ + rect, + side: "left", + x1: rect.x, + y1: rect.y, + x2: rect.x, + y2: rect.y + rect.height, + normal: { x: -1, y: 0 }, // Points left + zLayers: [...zLayers], + }) + } + + return edges + } + + override _setup(): void { + this.stats = { + phase: "EDGE_ANALYSIS", + edgeIndex: 0, + totalEdges: this.state.edges.length, + filledCount: 0, + } + } + + override _step(): void { + switch (this.state.phase) { + case "SELECT_PRIMARY_EDGE": + this.stepSelectPrimaryEdge() + break + case "FIND_NEARBY_EDGES": + this.stepFindNearbyEdges() + break + case "CHECK_UNOCCUPIED": + this.stepCheckUnoccupied() + break + case "PLACE_EXPANSION_POINTS": + this.stepPlaceExpansionPoints() + break + case "EXPAND_POINT": + this.stepExpandPoint() + break + case "DONE": + this.solved = true + break + } + + this.stats.phase = this.state.phase + this.stats.edgeIndex = this.state.currentEdgeIndex + this.stats.filledCount = this.state.filledRects.length + } + + private stepSelectPrimaryEdge(): void { + if (this.state.currentEdgeIndex >= this.state.edges.length) { + this.state.phase = "DONE" + return + } + + this.state.currentPrimaryEdge = + this.state.edges[this.state.currentEdgeIndex] + this.state.nearbyEdgeCandidateIndex = 0 + this.state.currentNearbyEdges = [] + + this.state.phase = "FIND_NEARBY_EDGES" + } + + private stepFindNearbyEdges(): void { + const primaryEdge = this.state.currentPrimaryEdge! + + // Check one candidate edge per step + if (this.state.nearbyEdgeCandidateIndex < this.state.edges.length) { + const candidate = this.state.edges[this.state.nearbyEdgeCandidateIndex]! + + // Check if this edge is nearby and parallel + if ( + candidate !== primaryEdge && + this.isNearbyParallelEdge(primaryEdge, candidate) + ) { + this.state.currentNearbyEdges.push(candidate) + } + + this.state.nearbyEdgeCandidateIndex++ + } else { + // Done finding nearby edges, move to checking unoccupied + this.state.phase = "CHECK_UNOCCUPIED" + } + } + + private stepCheckUnoccupied(): void { + // Find all unoccupied sections at once (can be broken down further if needed) + const primaryEdge = this.state.currentPrimaryEdge! + this.state.currentUnoccupiedSections = + this.findUnoccupiedSections(primaryEdge) + + // Move to placing expansion points + this.state.phase = "PLACE_EXPANSION_POINTS" + } + + private stepPlaceExpansionPoints(): void { + this.state.currentExpansionPoints = this.placeExpansionPoints( + this.state.currentUnoccupiedSections, + ) + this.state.currentExpansionIndex = 0 + + if (this.state.currentExpansionPoints.length > 0) { + this.state.phase = "EXPAND_POINT" + } else { + this.moveToNextEdge() + } + } + + private stepExpandPoint(): void { + if ( + this.state.currentExpansionIndex < + this.state.currentExpansionPoints.length + ) { + const point = + this.state.currentExpansionPoints[this.state.currentExpansionIndex]! + + // TODO: Actually expand the point into a rectangle + // For now, just move to next point + + this.state.currentExpansionIndex++ + } else { + this.moveToNextEdge() + } + } + + private moveToNextEdge(): void { + this.state.currentEdgeIndex++ + this.state.phase = "SELECT_PRIMARY_EDGE" + this.state.currentNearbyEdges = [] + this.state.currentUnoccupiedSections = [] + this.state.currentExpansionPoints = [] + } + + private isNearbyParallelEdge( + primaryEdge: RectEdge, + candidate: RectEdge, + ): boolean { + const dotProduct = + primaryEdge.normal.x * candidate.normal.x + + primaryEdge.normal.y * candidate.normal.y + + if (dotProduct >= -0.9) return false // Not opposite (not facing) + + const sharedLayers = primaryEdge.zLayers.filter((z) => + candidate.zLayers.includes(z), + ) + if (sharedLayers.length === 0) return false + + const distance = this.distanceBetweenEdges(primaryEdge, candidate) + if (distance > 2.0) return false // TODO: Make this configurable + + return true + } + + private distanceBetweenEdges(edge1: RectEdge, edge2: RectEdge): number { + if (Math.abs(edge1.normal.y) > 0.5) { + return Math.abs(edge1.y1 - edge2.y1) + } + return Math.abs(edge1.x1 - edge2.x1) + } + + private findUnoccupiedSections(edge: RectEdge): UnoccupiedSection[] { + // TODO: Implement - check which parts of the edge have free space + // For now, return the entire edge as one section + return [ + { + edge, + start: 0, + end: 1, + x1: edge.x1, + y1: edge.y1, + x2: edge.x2, + y2: edge.y2, + }, + ] + } + + private placeExpansionPoints( + sections: UnoccupiedSection[], + ): ExpansionPoint[] { + const points: ExpansionPoint[] = [] + + for (const section of sections) { + const edge = section.edge + + const offsetDistance = 0.05 + const midX = (section.x1 + section.x2) / 2 + const midY = (section.y1 + section.y2) / 2 + + points.push({ + x: midX + edge.normal.x * offsetDistance, + y: midY + edge.normal.y * offsetDistance, + zLayers: [...edge.zLayers], + section, + }) + } + + return points + } + + override getOutput() { + return { + filledRects: this.state.filledRects, + } + } + + override visualize(): GraphicsObject { + const rects: NonNullable = [] + const points: NonNullable = [] + const lines: NonNullable = [] + + // Draw input rectangles (light gray) + for (const placed of this.state.inputRects) { + rects.push({ + center: { + x: placed.rect.x + placed.rect.width / 2, + y: placed.rect.y + placed.rect.height / 2, + }, + width: placed.rect.width, + height: placed.rect.height, + fill: "#f3f4f6", + stroke: "#9ca3af", + label: "existing", + }) + } + + // Highlight primary edge (bright blue) + if (this.state.currentPrimaryEdge) { + const e = this.state.currentPrimaryEdge + lines.push({ + points: [ + { x: e.x1, y: e.y1 }, + { x: e.x2, y: e.y2 }, + ], + strokeColor: "#3b82f6", + strokeWidth: 0.08, + label: "primary edge", + }) + } + + // Highlight nearby edges (orange) + for (const edge of this.state.currentNearbyEdges) { + lines.push({ + points: [ + { x: edge.x1, y: edge.y1 }, + { x: edge.x2, y: edge.y2 }, + ], + strokeColor: "#f97316", + strokeWidth: 0.06, + label: "nearby edge", + }) + } + + // Highlight unoccupied sections (green) + for (const section of this.state.currentUnoccupiedSections) { + lines.push({ + points: [ + { x: section.x1, y: section.y1 }, + { x: section.x2, y: section.y2 }, + ], + strokeColor: "#10b981", + strokeWidth: 0.04, + label: "unoccupied", + }) + } + + // Show expansion points (purple) + for (const point of this.state.currentExpansionPoints) { + points.push({ + x: point.x, + y: point.y, + fill: "#a855f7", + stroke: "#7e22ce", + label: "expand", + } as any) + } + + // Draw filled rectangles (green) + for (const placed of this.state.filledRects) { + rects.push({ + center: { + x: placed.rect.x + placed.rect.width / 2, + y: placed.rect.y + placed.rect.height / 2, + }, + width: placed.rect.width, + height: placed.rect.height, + fill: "#d1fae5", + stroke: "#10b981", + label: "filled gap", + }) + } + + return { + title: `Gap Fill (Edge ${this.state.currentEdgeIndex}/${this.state.edges.length})`, + coordinateSystem: "cartesian", + rects, + points, + lines, + } + } +} diff --git a/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx b/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx new file mode 100644 index 0000000..7b1eed9 --- /dev/null +++ b/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx @@ -0,0 +1,49 @@ +import { useMemo } from "react" +import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import type { SimpleRouteJson } from "../../../lib/types/srj-types" +import type { Placed3D } from "../../../lib/solvers/rectdiff/types" +import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" + +export default () => { + const simpleRouteJson: SimpleRouteJson = { + layerCount: 2, + minTraceWidth: 0.1, + bounds: { minX: 0, minY: 0, maxX: 10, maxY: 10 }, + connections: [], + obstacles: [], + } + + // Multiple layers with gaps + const placedRects: Placed3D[] = [ + // Layer 0 - horizontal gap + { + rect: { x: 1, y: 2, width: 2, height: 3 }, + zLayers: [0], + }, + { + rect: { x: 5, y: 2, width: 2, height: 3 }, + zLayers: [0], + }, + // Layer 1 - vertical gap + { + rect: { x: 3, y: 6, width: 4, height: 2 }, + zLayers: [1], + }, + { + rect: { x: 3, y: 1, width: 4, height: 2 }, + zLayers: [1], + }, + ] + + const solver = useMemo( + () => + new GapFillSolver({ + simpleRouteJson, + placedRects, + obstaclesByLayer: [[], []], + }), + [], + ) + + return +} diff --git a/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx b/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx new file mode 100644 index 0000000..58822a9 --- /dev/null +++ b/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx @@ -0,0 +1,39 @@ +import { useMemo } from "react" +import { SolverDebugger3d } from "../../../components/SolverDebugger3d" +import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import type { SimpleRouteJson } from "../../../lib/types/srj-types" +import type { Placed3D } from "../../../lib/solvers/rectdiff/types" + +export default () => { + const simpleRouteJson: SimpleRouteJson = { + layerCount: 1, + minTraceWidth: 0.1, + bounds: { minX: 0, minY: 0, maxX: 10, maxY: 10 }, + connections: [], + obstacles: [], + } + + // Two rectangles with 1mm gap between them + const placedRects: Placed3D[] = [ + { + rect: { x: 1, y: 3, width: 3, height: 4 }, + zLayers: [0], + }, + { + rect: { x: 5, y: 3, width: 3, height: 4 }, + zLayers: [0], + }, + ] + + const solver = useMemo( + () => + new GapFillSolver({ + simpleRouteJson, + placedRects, + obstaclesByLayer: [[]], + }), + [], + ) + + return +} diff --git a/pages/repro/gap-fill-solver/staggered-rects.page.tsx b/pages/repro/gap-fill-solver/staggered-rects.page.tsx new file mode 100644 index 0000000..8788f5d --- /dev/null +++ b/pages/repro/gap-fill-solver/staggered-rects.page.tsx @@ -0,0 +1,39 @@ +import { useMemo } from "react" +import { SolverDebugger3d } from "../../../components/SolverDebugger3d" +import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import type { SimpleRouteJson } from "../../../lib/types/srj-types" +import type { Placed3D } from "../../../lib/solvers/rectdiff/types" + +export default () => { + const simpleRouteJson: SimpleRouteJson = { + layerCount: 1, + minTraceWidth: 0.1, + bounds: { minX: 0, minY: 0, maxX: 10, maxY: 10 }, + connections: [], + obstacles: [], + } + + // Two rectangles staggered vertically with partial overlap + const placedRects: Placed3D[] = [ + { + rect: { x: 1, y: 2, width: 3, height: 4 }, + zLayers: [0], + }, + { + rect: { x: 5, y: 3, width: 3, height: 4 }, + zLayers: [0], + }, + ] + + const solver = useMemo( + () => + new GapFillSolver({ + simpleRouteJson, + placedRects, + obstaclesByLayer: [[]], + }), + [], + ) + + return +} diff --git a/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx b/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx new file mode 100644 index 0000000..db9c6f8 --- /dev/null +++ b/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx @@ -0,0 +1,51 @@ +import { useMemo } from "react" +import { SolverDebugger3d } from "../../../components/SolverDebugger3d" +import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import type { SimpleRouteJson } from "../../../lib/types/srj-types" +import type { Placed3D } from "../../../lib/solvers/rectdiff/types" + +export default () => { + const simpleRouteJson: SimpleRouteJson = { + layerCount: 1, + minTraceWidth: 0.1, + bounds: { minX: 0, minY: 0, maxX: 12, maxY: 12 }, + connections: [], + obstacles: [], + } + + // Four rectangles forming a cross with gaps + const placedRects: Placed3D[] = [ + // Left + { + rect: { x: 1, y: 5, width: 3, height: 2 }, + zLayers: [0], + }, + // Right + { + rect: { x: 8, y: 5, width: 3, height: 2 }, + zLayers: [0], + }, + // Top + { + rect: { x: 5, y: 8, width: 2, height: 3 }, + zLayers: [0], + }, + // Bottom + { + rect: { x: 5, y: 1, width: 2, height: 3 }, + zLayers: [0], + }, + ] + + const solver = useMemo( + () => + new GapFillSolver({ + simpleRouteJson, + placedRects, + obstaclesByLayer: [[]], + }), + [], + ) + + return +} From 969e171d135cbe4ed4ede56291e26616812e4f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 18 Dec 2025 19:57:35 +0530 Subject: [PATCH 02/44] WIP --- pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx | 3 ++- pages/repro/gap-fill-solver/staggered-rects.page.tsx | 3 ++- .../gap-fill-solver/vertical-and-horizontal-gaps.page.tsx | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx b/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx index 58822a9..728d1ba 100644 --- a/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx +++ b/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx @@ -3,6 +3,7 @@ import { SolverDebugger3d } from "../../../components/SolverDebugger3d" import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" +import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" export default () => { const simpleRouteJson: SimpleRouteJson = { @@ -35,5 +36,5 @@ export default () => { [], ) - return + return } diff --git a/pages/repro/gap-fill-solver/staggered-rects.page.tsx b/pages/repro/gap-fill-solver/staggered-rects.page.tsx index 8788f5d..ab9ac20 100644 --- a/pages/repro/gap-fill-solver/staggered-rects.page.tsx +++ b/pages/repro/gap-fill-solver/staggered-rects.page.tsx @@ -3,6 +3,7 @@ import { SolverDebugger3d } from "../../../components/SolverDebugger3d" import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" +import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" export default () => { const simpleRouteJson: SimpleRouteJson = { @@ -35,5 +36,5 @@ export default () => { [], ) - return + return } diff --git a/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx b/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx index db9c6f8..c8d2521 100644 --- a/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx +++ b/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx @@ -3,6 +3,7 @@ import { SolverDebugger3d } from "../../../components/SolverDebugger3d" import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" +import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" export default () => { const simpleRouteJson: SimpleRouteJson = { @@ -47,5 +48,5 @@ export default () => { [], ) - return + return } From b198ed450c8d9b5d3ac952b421217ec8ba9eb724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 18 Dec 2025 20:17:54 +0530 Subject: [PATCH 03/44] WIP --- lib/solvers/GapFillSolver.ts | 93 +++++++++++++++++-- .../simple-two-rect-with-gap.page.tsx | 1 - .../gap-fill-solver/staggered-rects.page.tsx | 1 - .../vertical-and-horizontal-gaps.page.tsx | 1 - 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 4282ac4..13ee07e 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -236,12 +236,10 @@ export class GapFillSolver extends BaseSolver { } private stepCheckUnoccupied(): void { - // Find all unoccupied sections at once (can be broken down further if needed) const primaryEdge = this.state.currentPrimaryEdge! this.state.currentUnoccupiedSections = this.findUnoccupiedSections(primaryEdge) - // Move to placing expansion points this.state.phase = "PLACE_EXPANSION_POINTS" } @@ -266,8 +264,10 @@ export class GapFillSolver extends BaseSolver { const point = this.state.currentExpansionPoints[this.state.currentExpansionIndex]! - // TODO: Actually expand the point into a rectangle - // For now, just move to next point + const filledRect = this.expandPointToRect(point) + if (filledRect && !this.overlapsExistingFill(filledRect)) { + this.state.filledRects.push(filledRect) + } this.state.currentExpansionIndex++ } else { @@ -275,6 +275,69 @@ export class GapFillSolver extends BaseSolver { } } + private overlapsExistingFill(candidate: Placed3D): boolean { + for (const existing of this.state.filledRects) { + const sharedLayers = candidate.zLayers.filter((z) => + existing.zLayers.includes(z), + ) + if (sharedLayers.length === 0) continue + + const overlapX = + Math.max(candidate.rect.x, existing.rect.x) < + Math.min( + candidate.rect.x + candidate.rect.width, + existing.rect.x + existing.rect.width, + ) + const overlapY = + Math.max(candidate.rect.y, existing.rect.y) < + Math.min( + candidate.rect.y + candidate.rect.height, + existing.rect.y + existing.rect.height, + ) + + if (overlapX && overlapY) { + return true + } + } + + return false + } + + private expandPointToRect(point: ExpansionPoint): Placed3D | null { + const section = point.section + const edge = section.edge + + const nearbyEdge = this.state.currentNearbyEdges[0] + if (!nearbyEdge) return null + + let rect: { x: number; y: number; width: number; height: number } + + if (Math.abs(edge.normal.x) > 0.5) { + const x1 = Math.min(edge.x1, nearbyEdge.x1) + const x2 = Math.max(edge.x1, nearbyEdge.x1) + rect = { + x: x1, + y: section.y1, + width: x2 - x1, + height: section.y2 - section.y1, + } + } else { + const y1 = Math.min(edge.y1, nearbyEdge.y1) + const y2 = Math.max(edge.y1, nearbyEdge.y1) + rect = { + x: section.x1, + y: y1, + width: section.x2 - section.x1, + height: y2 - y1, + } + } + + return { + rect, + zLayers: [...point.zLayers], + } + } + private moveToNextEdge(): void { this.state.currentEdgeIndex++ this.state.phase = "SELECT_PRIMARY_EDGE" @@ -372,7 +435,7 @@ export class GapFillSolver extends BaseSolver { height: placed.rect.height, fill: "#f3f4f6", stroke: "#9ca3af", - label: "existing", + label: `input rect\npos: (${placed.rect.x.toFixed(2)}, ${placed.rect.y.toFixed(2)})\nsize: ${placed.rect.width.toFixed(2)} × ${placed.rect.height.toFixed(2)}\nz: [${placed.zLayers.join(", ")}]`, }) } @@ -386,12 +449,18 @@ export class GapFillSolver extends BaseSolver { ], strokeColor: "#3b82f6", strokeWidth: 0.08, - label: "primary edge", + label: `primary edge (${e.side})\n(${e.x1.toFixed(2)}, ${e.y1.toFixed(2)}) → (${e.x2.toFixed(2)}, ${e.y2.toFixed(2)})\nnormal: (${e.normal.x}, ${e.normal.y})\nz: [${e.zLayers.join(", ")}]`, }) } // Highlight nearby edges (orange) for (const edge of this.state.currentNearbyEdges) { + const distance = this.state.currentPrimaryEdge + ? this.distanceBetweenEdges( + this.state.currentPrimaryEdge, + edge, + ).toFixed(2) + : "?" lines.push({ points: [ { x: edge.x1, y: edge.y1 }, @@ -399,12 +468,16 @@ export class GapFillSolver extends BaseSolver { ], strokeColor: "#f97316", strokeWidth: 0.06, - label: "nearby edge", + label: `nearby edge (${edge.side})\n(${edge.x1.toFixed(2)}, ${edge.y1.toFixed(2)}) → (${edge.x2.toFixed(2)}, ${edge.y2.toFixed(2)})\ndist: ${distance}mm\nz: [${edge.zLayers.join(", ")}]`, }) } // Highlight unoccupied sections (green) for (const section of this.state.currentUnoccupiedSections) { + const length = Math.sqrt( + Math.pow(section.x2 - section.x1, 2) + + Math.pow(section.y2 - section.y1, 2), + ) lines.push({ points: [ { x: section.x1, y: section.y1 }, @@ -412,7 +485,7 @@ export class GapFillSolver extends BaseSolver { ], strokeColor: "#10b981", strokeWidth: 0.04, - label: "unoccupied", + label: `unoccupied section\n(${section.x1.toFixed(2)}, ${section.y1.toFixed(2)}) → (${section.x2.toFixed(2)}, ${section.y2.toFixed(2)})\nlength: ${length.toFixed(2)}mm\nrange: ${(section.start * 100).toFixed(0)}%-${(section.end * 100).toFixed(0)}%`, }) } @@ -423,7 +496,7 @@ export class GapFillSolver extends BaseSolver { y: point.y, fill: "#a855f7", stroke: "#7e22ce", - label: "expand", + label: `expansion point\npos: (${point.x.toFixed(2)}, ${point.y.toFixed(2)})\nz: [${point.zLayers.join(", ")}]`, } as any) } @@ -438,7 +511,7 @@ export class GapFillSolver extends BaseSolver { height: placed.rect.height, fill: "#d1fae5", stroke: "#10b981", - label: "filled gap", + label: `filled gap\npos: (${placed.rect.x.toFixed(2)}, ${placed.rect.y.toFixed(2)})\nsize: ${placed.rect.width.toFixed(2)} × ${placed.rect.height.toFixed(2)}\nz: [${placed.zLayers.join(", ")}]`, }) } diff --git a/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx b/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx index 728d1ba..f63351a 100644 --- a/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx +++ b/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx @@ -1,5 +1,4 @@ import { useMemo } from "react" -import { SolverDebugger3d } from "../../../components/SolverDebugger3d" import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" diff --git a/pages/repro/gap-fill-solver/staggered-rects.page.tsx b/pages/repro/gap-fill-solver/staggered-rects.page.tsx index ab9ac20..1b8ff53 100644 --- a/pages/repro/gap-fill-solver/staggered-rects.page.tsx +++ b/pages/repro/gap-fill-solver/staggered-rects.page.tsx @@ -1,5 +1,4 @@ import { useMemo } from "react" -import { SolverDebugger3d } from "../../../components/SolverDebugger3d" import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" diff --git a/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx b/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx index c8d2521..3383d67 100644 --- a/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx +++ b/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx @@ -1,5 +1,4 @@ import { useMemo } from "react" -import { SolverDebugger3d } from "../../../components/SolverDebugger3d" import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" From b5b945b7d7b64b307fec4e1130f288e0a6e286a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 18 Dec 2025 20:23:57 +0530 Subject: [PATCH 04/44] WIP --- lib/solvers/GapFillSolver.ts | 60 ++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 13ee07e..36a4e7f 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -265,7 +265,12 @@ export class GapFillSolver extends BaseSolver { this.state.currentExpansionPoints[this.state.currentExpansionIndex]! const filledRect = this.expandPointToRect(point) - if (filledRect && !this.overlapsExistingFill(filledRect)) { + if ( + filledRect && + !this.overlapsExistingFill(filledRect) && + !this.overlapsInputRects(filledRect) && + this.hasMinimumSize(filledRect) + ) { this.state.filledRects.push(filledRect) } @@ -303,6 +308,41 @@ export class GapFillSolver extends BaseSolver { return false } + private overlapsInputRects(candidate: Placed3D): boolean { + for (const input of this.state.inputRects) { + const sharedLayers = candidate.zLayers.filter((z) => + input.zLayers.includes(z), + ) + if (sharedLayers.length === 0) continue + + const overlapX = + Math.max(candidate.rect.x, input.rect.x) < + Math.min( + candidate.rect.x + candidate.rect.width, + input.rect.x + input.rect.width, + ) + const overlapY = + Math.max(candidate.rect.y, input.rect.y) < + Math.min( + candidate.rect.y + candidate.rect.height, + input.rect.y + input.rect.height, + ) + + if (overlapX && overlapY) { + return true + } + } + + return false + } + + private hasMinimumSize(candidate: Placed3D): boolean { + const minSize = 0.01 + return ( + candidate.rect.width >= minSize && candidate.rect.height >= minSize + ) + } + private expandPointToRect(point: ExpansionPoint): Placed3D | null { const section = point.section const edge = section.edge @@ -313,22 +353,24 @@ export class GapFillSolver extends BaseSolver { let rect: { x: number; y: number; width: number; height: number } if (Math.abs(edge.normal.x) > 0.5) { - const x1 = Math.min(edge.x1, nearbyEdge.x1) - const x2 = Math.max(edge.x1, nearbyEdge.x1) + const leftX = edge.normal.x > 0 ? edge.x1 : nearbyEdge.x1 + const rightX = edge.normal.x > 0 ? nearbyEdge.x1 : edge.x1 + rect = { - x: x1, + x: leftX, y: section.y1, - width: x2 - x1, + width: rightX - leftX, height: section.y2 - section.y1, } } else { - const y1 = Math.min(edge.y1, nearbyEdge.y1) - const y2 = Math.max(edge.y1, nearbyEdge.y1) + const bottomY = edge.normal.y > 0 ? edge.y1 : nearbyEdge.y1 + const topY = edge.normal.y > 0 ? nearbyEdge.y1 : edge.y1 + rect = { x: section.x1, - y: y1, + y: bottomY, width: section.x2 - section.x1, - height: y2 - y1, + height: topY - bottomY, } } From 351d91e8b3d26c3765d5fee205459f9953042a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 18 Dec 2025 21:02:34 +0530 Subject: [PATCH 05/44] WIP --- lib/solvers/GapFillSolver.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 36a4e7f..43885c1 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -216,23 +216,17 @@ export class GapFillSolver extends BaseSolver { private stepFindNearbyEdges(): void { const primaryEdge = this.state.currentPrimaryEdge! - // Check one candidate edge per step - if (this.state.nearbyEdgeCandidateIndex < this.state.edges.length) { - const candidate = this.state.edges[this.state.nearbyEdgeCandidateIndex]! - - // Check if this edge is nearby and parallel + this.state.currentNearbyEdges = [] + for (const candidate of this.state.edges) { if ( candidate !== primaryEdge && this.isNearbyParallelEdge(primaryEdge, candidate) ) { this.state.currentNearbyEdges.push(candidate) + break } - - this.state.nearbyEdgeCandidateIndex++ - } else { - // Done finding nearby edges, move to checking unoccupied - this.state.phase = "CHECK_UNOCCUPIED" } + this.state.phase = "CHECK_UNOCCUPIED" } private stepCheckUnoccupied(): void { @@ -338,9 +332,7 @@ export class GapFillSolver extends BaseSolver { private hasMinimumSize(candidate: Placed3D): boolean { const minSize = 0.01 - return ( - candidate.rect.width >= minSize && candidate.rect.height >= minSize - ) + return candidate.rect.width >= minSize && candidate.rect.height >= minSize } private expandPointToRect(point: ExpansionPoint): Placed3D | null { From c4cf3fefdd34caccd8b21d152f76dd6286056696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 18 Dec 2025 21:41:23 +0530 Subject: [PATCH 06/44] added FlatbushIndex --- lib/data-structures/FlatbushIndex.ts | 44 ++++++++++++++++++++++++++++ lib/solvers/GapFillSolver.ts | 41 +++++++++++++++++++++++++- package.json | 3 +- 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 lib/data-structures/FlatbushIndex.ts diff --git a/lib/data-structures/FlatbushIndex.ts b/lib/data-structures/FlatbushIndex.ts new file mode 100644 index 0000000..dc58c79 --- /dev/null +++ b/lib/data-structures/FlatbushIndex.ts @@ -0,0 +1,44 @@ +import Flatbush from "flatbush" + +export interface ISpatialIndex { + insert(item: T, minX: number, minY: number, maxX: number, maxY: number): void + finish(): void + search(minX: number, minY: number, maxX: number, maxY: number): T[] + clear(): void +} + +export class FlatbushIndex implements ISpatialIndex { + private index: Flatbush + private items: T[] = [] + private currentIndex = 0 + private capacity: number + + constructor(numItems: number) { + this.capacity = Math.max(1, numItems) + this.index = new Flatbush(this.capacity) + } + + insert(item: T, minX: number, minY: number, maxX: number, maxY: number) { + if (this.currentIndex >= this.index.numItems) { + throw new Error("Exceeded initial capacity") + } + this.items[this.currentIndex] = item + this.index.add(minX, minY, maxX, maxY) + this.currentIndex++ + } + + finish() { + this.index.finish() + } + + search(minX: number, minY: number, maxX: number, maxY: number): T[] { + const ids = this.index.search(minX, minY, maxX, maxY) + return ids.map((id) => this.items[id] || null).filter(Boolean) as T[] + } + + clear() { + this.items = [] + this.currentIndex = 0 + this.index = new Flatbush(this.capacity) + } +} diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 43885c1..1242dda 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -2,6 +2,7 @@ import { BaseSolver } from "@tscircuit/solver-utils" import type { GraphicsObject } from "graphics-debug" import type { SimpleRouteJson } from "../types/srj-types" import type { Placed3D, XYRect } from "./rectdiff/types" +import { FlatbushIndex } from "../data-structures/FlatbushIndex" export interface RectEdge { rect: XYRect @@ -53,6 +54,7 @@ interface GapFillState { layerCount: number edges: RectEdge[] + edgeSpatialIndex: FlatbushIndex phase: SubPhase currentEdgeIndex: number @@ -86,12 +88,16 @@ export class GapFillSolver extends BaseSolver { const edges = this.extractEdges(input.placedRects) + // Build spatial index for fast edge-to-edge queries + const edgeSpatialIndex = this.buildEdgeSpatialIndex(edges) + return { srj: input.simpleRouteJson, inputRects: input.placedRects, obstaclesByLayer: input.obstaclesByLayer, layerCount, edges, + edgeSpatialIndex, phase: "SELECT_PRIMARY_EDGE", currentEdgeIndex: 0, nearbyEdgeCandidateIndex: 0, @@ -103,6 +109,24 @@ export class GapFillSolver extends BaseSolver { } } + private buildEdgeSpatialIndex(edges: RectEdge[]): FlatbushIndex { + const index = new FlatbushIndex(edges.length) + + for (const edge of edges) { + // Create bounding box for edge (padded by max search distance) + const padding = 2.0 // Max edge distance threshold + const minX = Math.min(edge.x1, edge.x2) - padding + const minY = Math.min(edge.y1, edge.y2) - padding + const maxX = Math.max(edge.x1, edge.x2) + padding + const maxY = Math.max(edge.y1, edge.y2) + padding + + index.insert(edge, minX, minY, maxX, maxY) + } + + index.finish() + return index + } + private extractEdges(rects: Placed3D[]): RectEdge[] { const edges: RectEdge[] = [] @@ -216,8 +240,23 @@ export class GapFillSolver extends BaseSolver { private stepFindNearbyEdges(): void { const primaryEdge = this.state.currentPrimaryEdge! + // Query spatial index for candidate edges near this primary edge + const padding = 2.0 // Max distance threshold + const minX = Math.min(primaryEdge.x1, primaryEdge.x2) - padding + const minY = Math.min(primaryEdge.y1, primaryEdge.y2) - padding + const maxX = Math.max(primaryEdge.x1, primaryEdge.x2) + padding + const maxY = Math.max(primaryEdge.y1, primaryEdge.y2) + padding + + const candidates = this.state.edgeSpatialIndex.search( + minX, + minY, + maxX, + maxY, + ) + + // Check only the nearby candidates (not all edges!) this.state.currentNearbyEdges = [] - for (const candidate of this.state.edges) { + for (const candidate of candidates) { if ( candidate !== primaryEdge && this.isNearbyParallelEdge(primaryEdge, candidate) diff --git a/package.json b/package.json index 7ac089d..e0c8057 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "three": "^0.181.1", "tsup": "^8.5.1", "vite": "^6.0.11", - "@vitejs/plugin-react": "^4" + "@vitejs/plugin-react": "^4", + "flatbush": "^4.5.0" }, "peerDependencies": { "typescript": "^5" From 15012190249ed71223b2adb647ad5054e501e1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 18 Dec 2025 21:48:53 +0530 Subject: [PATCH 07/44] seting the MAX_ITERATIONS = 100e6 --- lib/RectDiffPipeline.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/RectDiffPipeline.ts b/lib/RectDiffPipeline.ts index 6a7210e..27e6189 100644 --- a/lib/RectDiffPipeline.ts +++ b/lib/RectDiffPipeline.ts @@ -15,6 +15,7 @@ export interface RectDiffPipelineInput { export class RectDiffPipeline extends BasePipelineSolver { rectDiffSolver?: RectDiffSolver gapFillSolver?: GapFillSolver + override MAX_ITERATIONS: number = 100e6 override pipelineDef = [ definePipelineStep( From 132609e3c5bacb4de4271a612ed273fe42eaa29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 18 Dec 2025 21:52:35 +0530 Subject: [PATCH 08/44] add maxEdgeDistance param --- lib/solvers/GapFillSolver.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 1242dda..2da6e4d 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -36,6 +36,7 @@ export interface GapFillSolverInput { simpleRouteJson: SimpleRouteJson placedRects: Placed3D[] obstaclesByLayer: XYRect[][] + maxEdgeDistance?: number // Max distance to consider edges "nearby" (default: 2.0) } // Sub-phases for visualization @@ -52,6 +53,7 @@ interface GapFillState { inputRects: Placed3D[] obstaclesByLayer: XYRect[][] layerCount: number + maxEdgeDistance: number edges: RectEdge[] edgeSpatialIndex: FlatbushIndex @@ -85,17 +87,19 @@ export class GapFillSolver extends BaseSolver { private initState(input: GapFillSolverInput): GapFillState { const layerCount = input.simpleRouteJson.layerCount || 1 + const maxEdgeDistance = input.maxEdgeDistance ?? 2.0 const edges = this.extractEdges(input.placedRects) // Build spatial index for fast edge-to-edge queries - const edgeSpatialIndex = this.buildEdgeSpatialIndex(edges) + const edgeSpatialIndex = this.buildEdgeSpatialIndex(edges, maxEdgeDistance) return { srj: input.simpleRouteJson, inputRects: input.placedRects, obstaclesByLayer: input.obstaclesByLayer, layerCount, + maxEdgeDistance, edges, edgeSpatialIndex, phase: "SELECT_PRIMARY_EDGE", @@ -109,16 +113,18 @@ export class GapFillSolver extends BaseSolver { } } - private buildEdgeSpatialIndex(edges: RectEdge[]): FlatbushIndex { + private buildEdgeSpatialIndex( + edges: RectEdge[], + maxEdgeDistance: number, + ): FlatbushIndex { const index = new FlatbushIndex(edges.length) for (const edge of edges) { // Create bounding box for edge (padded by max search distance) - const padding = 2.0 // Max edge distance threshold - const minX = Math.min(edge.x1, edge.x2) - padding - const minY = Math.min(edge.y1, edge.y2) - padding - const maxX = Math.max(edge.x1, edge.x2) + padding - const maxY = Math.max(edge.y1, edge.y2) + padding + const minX = Math.min(edge.x1, edge.x2) - maxEdgeDistance + const minY = Math.min(edge.y1, edge.y2) - maxEdgeDistance + const maxX = Math.max(edge.x1, edge.x2) + maxEdgeDistance + const maxY = Math.max(edge.y1, edge.y2) + maxEdgeDistance index.insert(edge, minX, minY, maxX, maxY) } @@ -241,7 +247,7 @@ export class GapFillSolver extends BaseSolver { const primaryEdge = this.state.currentPrimaryEdge! // Query spatial index for candidate edges near this primary edge - const padding = 2.0 // Max distance threshold + const padding = this.state.maxEdgeDistance const minX = Math.min(primaryEdge.x1, primaryEdge.x2) - padding const minY = Math.min(primaryEdge.y1, primaryEdge.y2) - padding const maxX = Math.max(primaryEdge.x1, primaryEdge.x2) + padding @@ -435,7 +441,7 @@ export class GapFillSolver extends BaseSolver { if (sharedLayers.length === 0) return false const distance = this.distanceBetweenEdges(primaryEdge, candidate) - if (distance > 2.0) return false // TODO: Make this configurable + if (distance > this.state.maxEdgeDistance) return false return true } From d385a60e05d66d100ad69e4bbf1cb9a5cf98a761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 18 Dec 2025 22:47:28 +0530 Subject: [PATCH 09/44] WIP --- lib/solvers/GapFillSolver.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 2da6e4d..bd274bf 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -308,6 +308,7 @@ export class GapFillSolver extends BaseSolver { filledRect && !this.overlapsExistingFill(filledRect) && !this.overlapsInputRects(filledRect) && + !this.overlapsObstacles(filledRect) && this.hasMinimumSize(filledRect) ) { this.state.filledRects.push(filledRect) @@ -375,6 +376,32 @@ export class GapFillSolver extends BaseSolver { return false } + private overlapsObstacles(candidate: Placed3D): boolean { + for (const z of candidate.zLayers) { + const obstacles = this.state.obstaclesByLayer[z] ?? [] + for (const obstacle of obstacles) { + const overlapX = + Math.max(candidate.rect.x, obstacle.x) < + Math.min( + candidate.rect.x + candidate.rect.width, + obstacle.x + obstacle.width, + ) + const overlapY = + Math.max(candidate.rect.y, obstacle.y) < + Math.min( + candidate.rect.y + candidate.rect.height, + obstacle.y + obstacle.height, + ) + + if (overlapX && overlapY) { + return true + } + } + } + + return false + } + private hasMinimumSize(candidate: Placed3D): boolean { const minSize = 0.01 return candidate.rect.width >= minSize && candidate.rect.height >= minSize From ffc333c642f8c015a031a7095ed72a41eaef8ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Thu, 18 Dec 2025 23:39:14 +0530 Subject: [PATCH 10/44] WIP --- .../three-rects-tall-short-tall.page.tsx | 42 +++++++++++++++++++ .../three-rects-touching.page.tsx | 42 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx create mode 100644 pages/repro/gap-fill-solver/three-rects-touching.page.tsx diff --git a/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx b/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx new file mode 100644 index 0000000..3b3a2c8 --- /dev/null +++ b/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx @@ -0,0 +1,42 @@ +import { useMemo } from "react" +import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import type { SimpleRouteJson } from "../../../lib/types/srj-types" +import type { Placed3D } from "../../../lib/solvers/rectdiff/types" +import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" + +export default () => { + const simpleRouteJson: SimpleRouteJson = { + layerCount: 1, + minTraceWidth: 0.1, + bounds: { minX: 0, minY: 0, maxX: 15, maxY: 10 }, + connections: [], + obstacles: [], + } + + const placedRects: Placed3D[] = [ + { + rect: { x: 1, y: 1, width: 3, height: 8 }, + zLayers: [0], + }, + { + rect: { x: 6, y: 3.5, width: 3, height: 3 }, + zLayers: [0], + }, + { + rect: { x: 11, y: 1, width: 3, height: 8 }, + zLayers: [0], + }, + ] + + const solver = useMemo( + () => + new GapFillSolver({ + simpleRouteJson, + placedRects, + obstaclesByLayer: [[]], + }), + [], + ) + + return +} diff --git a/pages/repro/gap-fill-solver/three-rects-touching.page.tsx b/pages/repro/gap-fill-solver/three-rects-touching.page.tsx new file mode 100644 index 0000000..6e29df5 --- /dev/null +++ b/pages/repro/gap-fill-solver/three-rects-touching.page.tsx @@ -0,0 +1,42 @@ +import { useMemo } from "react" +import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import type { SimpleRouteJson } from "../../../lib/types/srj-types" +import type { Placed3D } from "../../../lib/solvers/rectdiff/types" +import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" + +export default () => { + const simpleRouteJson: SimpleRouteJson = { + layerCount: 1, + minTraceWidth: 0.1, + bounds: { minX: 0, minY: 0, maxX: 15, maxY: 10 }, + connections: [], + obstacles: [], + } + + const placedRects: Placed3D[] = [ + { + rect: { x: 1, y: 1, width: 3, height: 8 }, + zLayers: [0], + }, + { + rect: { x: 4, y: 3.5, width: 3, height: 3 }, + zLayers: [0], + }, + { + rect: { x: 7, y: 1, width: 3, height: 8 }, + zLayers: [0], + }, + ] + + const solver = useMemo( + () => + new GapFillSolver({ + simpleRouteJson, + placedRects, + obstaclesByLayer: [[]], + }), + [], + ) + + return +} From 3f5873f0731815cb1386df3cea9b991becadde19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 00:07:40 +0530 Subject: [PATCH 11/44] WIP --- lib/solvers/GapFillSolver.ts | 127 +++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index bd274bf..29f22d1 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -89,7 +89,8 @@ export class GapFillSolver extends BaseSolver { const layerCount = input.simpleRouteJson.layerCount || 1 const maxEdgeDistance = input.maxEdgeDistance ?? 2.0 - const edges = this.extractEdges(input.placedRects) + const rawEdges = this.extractEdges(input.placedRects) + const edges = this.splitEdgesOnOverlaps(rawEdges, maxEdgeDistance) // Build spatial index for fast edge-to-edge queries const edgeSpatialIndex = this.buildEdgeSpatialIndex(edges, maxEdgeDistance) @@ -138,8 +139,6 @@ export class GapFillSolver extends BaseSolver { for (const placed of rects) { const { rect, zLayers } = placed - const centerX = rect.x + rect.width / 2 - const centerY = rect.y + rect.height / 2 // Top edge (y = rect.y + rect.height) edges.push({ @@ -193,6 +192,126 @@ export class GapFillSolver extends BaseSolver { return edges } + private splitEdgesOnOverlaps(edges: RectEdge[], maxEdgeDistance: number): RectEdge[] { + console.log(`\n=== splitEdgesOnOverlaps START ===`) + console.log(`Input edges: ${edges.length}`) + + const result: RectEdge[] = [] + + for (const edge of edges) { + const isHorizontal = Math.abs(edge.normal.y) > 0.5 + const occupiedRanges: Array<{ start: number; end: number }> = [] + + console.log(`\nProcessing edge ${edge.side}: (${edge.x1},${edge.y1})-(${edge.x2},${edge.y2}) normal:(${edge.normal.x},${edge.normal.y})`) + + for (const other of edges) { + if (edge === other) continue + if (edge.rect === other.rect) continue // Skip same rectangle's edges + if (!edge.zLayers.some((z) => other.zLayers.includes(z))) continue + + const isOtherHorizontal = Math.abs(other.normal.y) > 0.5 + if (isHorizontal !== isOtherHorizontal) continue + + // Check if edges are actually nearby (not just parallel) + if (isHorizontal) { + const distance = Math.abs(edge.y1 - other.y1) + if (distance > maxEdgeDistance) continue + } else { + const distance = Math.abs(edge.x1 - other.x1) + if (distance > maxEdgeDistance) continue + } + + let overlapStart: number, overlapEnd: number + + if (isHorizontal) { + overlapStart = Math.max(edge.x1, other.x1) + overlapEnd = Math.min(edge.x2, other.x2) + } else { + overlapStart = Math.max(edge.y1, other.y1) + overlapEnd = Math.min(edge.y2, other.y2) + } + + if (overlapStart < overlapEnd) { + const edgeLength = isHorizontal ? edge.x2 - edge.x1 : edge.y2 - edge.y1 + const edgeStart = isHorizontal ? edge.x1 : edge.y1 + const range = { + start: (overlapStart - edgeStart) / edgeLength, + end: (overlapEnd - edgeStart) / edgeLength, + } + console.log(` Found overlap with ${other.side}: range ${range.start.toFixed(2)}-${range.end.toFixed(2)}`) + occupiedRanges.push(range) + } + } + + if (occupiedRanges.length === 0) { + console.log(` No overlaps, keeping full edge`) + result.push(edge) + continue + } + + occupiedRanges.sort((a, b) => a.start - b.start) + const merged: Array<{ start: number; end: number }> = [] + for (const range of occupiedRanges) { + if (merged.length === 0 || range.start > merged[merged.length - 1]!.end) { + merged.push(range) + } else { + merged[merged.length - 1]!.end = Math.max( + merged[merged.length - 1]!.end, + range.end, + ) + } + } + + console.log(` Merged overlaps: ${merged.map(m => `${m.start.toFixed(2)}-${m.end.toFixed(2)}`).join(', ')}`) + + let pos = 0 + for (const occupied of merged) { + if (pos < occupied.start) { + console.log(` Creating free segment: ${pos.toFixed(2)}-${occupied.start.toFixed(2)}`) + result.push(this.createEdgeSegment(edge, pos, occupied.start)) + } + pos = occupied.end + } + if (pos < 1) { + console.log(` Creating free segment: ${pos.toFixed(2)}-1.00`) + result.push(this.createEdgeSegment(edge, pos, 1)) + } + + for (const occupied of merged) { + console.log(` Creating overlap segment: ${occupied.start.toFixed(2)}-${occupied.end.toFixed(2)}`) + result.push(this.createEdgeSegment(edge, occupied.start, occupied.end)) + } + } + + console.log(`\nOutput edges: ${result.length}`) + console.log(`=== splitEdgesOnOverlaps END ===\n`) + return result + } + + private createEdgeSegment( + edge: RectEdge, + start: number, + end: number, + ): RectEdge { + const isHorizontal = Math.abs(edge.normal.y) > 0.5 + + if (isHorizontal) { + const length = edge.x2 - edge.x1 + return { + ...edge, + x1: edge.x1 + start * length, + x2: edge.x1 + end * length, + } + } else { + const length = edge.y2 - edge.y1 + return { + ...edge, + y1: edge.y1 + start * length, + y2: edge.y1 + end * length, + } + } + } + override _setup(): void { this.stats = { phase: "EDGE_ANALYSIS", @@ -261,6 +380,7 @@ export class GapFillSolver extends BaseSolver { ) // Check only the nearby candidates (not all edges!) + // Collect ALL nearby parallel edges this.state.currentNearbyEdges = [] for (const candidate of candidates) { if ( @@ -268,7 +388,6 @@ export class GapFillSolver extends BaseSolver { this.isNearbyParallelEdge(primaryEdge, candidate) ) { this.state.currentNearbyEdges.push(candidate) - break } } this.state.phase = "CHECK_UNOCCUPIED" From 67a9b76d4880493671256028e86c501bec147e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 00:37:14 +0530 Subject: [PATCH 12/44] WIP --- lib/solvers/GapFillSolver.ts | 78 +++++++++++------------------------- 1 file changed, 24 insertions(+), 54 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 29f22d1..504420d 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -13,6 +13,7 @@ export interface RectEdge { y2: number normal: { x: number; y: number } zLayers: number[] + segmentType?: "free" | "overlap" // Track if this is a free or overlap segment } export interface UnoccupiedSection { @@ -268,18 +269,24 @@ export class GapFillSolver extends BaseSolver { for (const occupied of merged) { if (pos < occupied.start) { console.log(` Creating free segment: ${pos.toFixed(2)}-${occupied.start.toFixed(2)}`) - result.push(this.createEdgeSegment(edge, pos, occupied.start)) + const freeSegment = this.createEdgeSegment(edge, pos, occupied.start) + freeSegment.segmentType = "free" + result.push(freeSegment) } pos = occupied.end } if (pos < 1) { console.log(` Creating free segment: ${pos.toFixed(2)}-1.00`) - result.push(this.createEdgeSegment(edge, pos, 1)) + const freeSegment = this.createEdgeSegment(edge, pos, 1) + freeSegment.segmentType = "free" + result.push(freeSegment) } for (const occupied of merged) { console.log(` Creating overlap segment: ${occupied.start.toFixed(2)}-${occupied.end.toFixed(2)}`) - result.push(this.createEdgeSegment(edge, occupied.start, occupied.end)) + const overlapSegment = this.createEdgeSegment(edge, occupied.start, occupied.end) + overlapSegment.segmentType = "overlap" + result.push(overlapSegment) } } @@ -664,66 +671,29 @@ export class GapFillSolver extends BaseSolver { }) } - // Highlight primary edge (bright blue) - if (this.state.currentPrimaryEdge) { - const e = this.state.currentPrimaryEdge - lines.push({ - points: [ - { x: e.x1, y: e.y1 }, - { x: e.x2, y: e.y2 }, - ], - strokeColor: "#3b82f6", - strokeWidth: 0.08, - label: `primary edge (${e.side})\n(${e.x1.toFixed(2)}, ${e.y1.toFixed(2)}) → (${e.x2.toFixed(2)}, ${e.y2.toFixed(2)})\nnormal: (${e.normal.x}, ${e.normal.y})\nz: [${e.zLayers.join(", ")}]`, - }) - } + // Draw ALL edges with color coding: FREE=green, OVERLAP=red, UNSPLIT=gray + for (const edge of this.state.edges) { + const color = edge.segmentType === "free" + ? "#10b981" // Green for free segments + : edge.segmentType === "overlap" + ? "#ef4444" // Red for overlap segments + : "#6b7280" // Gray for unsplit edges + + const label = edge.segmentType + ? `${edge.segmentType.toUpperCase()} segment\n${edge.side}\n(${edge.x1.toFixed(2)},${edge.y1.toFixed(2)})-(${edge.x2.toFixed(2)},${edge.y2.toFixed(2)})` + : `edge ${edge.side}\n(${edge.x1.toFixed(2)},${edge.y1.toFixed(2)})-(${edge.x2.toFixed(2)},${edge.y2.toFixed(2)})` - // Highlight nearby edges (orange) - for (const edge of this.state.currentNearbyEdges) { - const distance = this.state.currentPrimaryEdge - ? this.distanceBetweenEdges( - this.state.currentPrimaryEdge, - edge, - ).toFixed(2) - : "?" lines.push({ points: [ { x: edge.x1, y: edge.y1 }, { x: edge.x2, y: edge.y2 }, ], - strokeColor: "#f97316", - strokeWidth: 0.06, - label: `nearby edge (${edge.side})\n(${edge.x1.toFixed(2)}, ${edge.y1.toFixed(2)}) → (${edge.x2.toFixed(2)}, ${edge.y2.toFixed(2)})\ndist: ${distance}mm\nz: [${edge.zLayers.join(", ")}]`, + strokeColor: color, + strokeWidth: 0.1, + label, }) } - // Highlight unoccupied sections (green) - for (const section of this.state.currentUnoccupiedSections) { - const length = Math.sqrt( - Math.pow(section.x2 - section.x1, 2) + - Math.pow(section.y2 - section.y1, 2), - ) - lines.push({ - points: [ - { x: section.x1, y: section.y1 }, - { x: section.x2, y: section.y2 }, - ], - strokeColor: "#10b981", - strokeWidth: 0.04, - label: `unoccupied section\n(${section.x1.toFixed(2)}, ${section.y1.toFixed(2)}) → (${section.x2.toFixed(2)}, ${section.y2.toFixed(2)})\nlength: ${length.toFixed(2)}mm\nrange: ${(section.start * 100).toFixed(0)}%-${(section.end * 100).toFixed(0)}%`, - }) - } - - // Show expansion points (purple) - for (const point of this.state.currentExpansionPoints) { - points.push({ - x: point.x, - y: point.y, - fill: "#a855f7", - stroke: "#7e22ce", - label: `expansion point\npos: (${point.x.toFixed(2)}, ${point.y.toFixed(2)})\nz: [${point.zLayers.join(", ")}]`, - } as any) - } // Draw filled rectangles (green) for (const placed of this.state.filledRects) { From 8d0f464f30ae9e6facb9b68daa046aa05e7920d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 01:34:29 +0530 Subject: [PATCH 13/44] WIP --- lib/RectDiffPipeline.ts | 49 +++++- lib/solvers/GapFillSolver.ts | 157 ++++++++++++++---- .../three-rects-touching.page.tsx | 1 + 3 files changed, 167 insertions(+), 40 deletions(-) diff --git a/lib/RectDiffPipeline.ts b/lib/RectDiffPipeline.ts index 27e6189..528b4a3 100644 --- a/lib/RectDiffPipeline.ts +++ b/lib/RectDiffPipeline.ts @@ -62,20 +62,59 @@ export class RectDiffPipeline extends BasePipelineSolver } override getOutput(): { meshNodes: CapacityMeshNode[] } { - return this.getSolver("rectDiffSolver")!.getOutput() + const rectDiffOutput = + this.getSolver("rectDiffSolver")!.getOutput() + const gapFillSolver = this.getSolver("gapFillSolver") + + if (!gapFillSolver) { + return rectDiffOutput + } + + const gapFillOutput = gapFillSolver.getOutput() + + const gapFillMeshNodes: CapacityMeshNode[] = gapFillOutput.filledRects.map( + (placed, index) => ({ + capacityMeshNodeId: `gap-fill-${index}`, + x: placed.rect.x, + y: placed.rect.y, + center: { + x: placed.rect.x + placed.rect.width / 2, + y: placed.rect.y + placed.rect.height / 2, + }, + width: placed.rect.width, + height: placed.rect.height, + availableZ: placed.zLayers, + layer: placed.zLayers[0]?.toString() ?? "0", + }), + ) + + return { + meshNodes: [...rectDiffOutput.meshNodes, ...gapFillMeshNodes], + } } override visualize(): GraphicsObject { - // Show the currently active solver's visualization const gapFillSolver = this.getSolver("gapFillSolver") + const rectDiffSolver = this.getSolver("rectDiffSolver") + if (gapFillSolver && !gapFillSolver.solved) { - // Gap fill is running, show its visualization return gapFillSolver.visualize() } - const rectDiffSolver = this.getSolver("rectDiffSolver") + if (gapFillSolver?.solved && rectDiffSolver) { + const baseViz = rectDiffSolver.visualize() + const gapFillViz = gapFillSolver.visualize() + + return { + ...baseViz, + title: "RectDiff Pipeline (with Gap Fill)", + rects: [...(baseViz.rects || []), ...(gapFillViz.rects || [])], + lines: [...(baseViz.lines || []), ...(gapFillViz.lines || [])], + points: [...(baseViz.points || []), ...(gapFillViz.points || [])], + } + } + if (rectDiffSolver) { - // RectDiff is running or finished, show its visualization return rectDiffSolver.visualize() } diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 504420d..8982160 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -14,6 +14,7 @@ export interface RectEdge { normal: { x: number; y: number } zLayers: number[] segmentType?: "free" | "overlap" // Track if this is a free or overlap segment + pairedEdges?: RectEdge[] } export interface UnoccupiedSection { @@ -55,6 +56,7 @@ interface GapFillState { obstaclesByLayer: XYRect[][] layerCount: number maxEdgeDistance: number + minTraceWidth: number edges: RectEdge[] edgeSpatialIndex: FlatbushIndex @@ -90,8 +92,19 @@ export class GapFillSolver extends BaseSolver { const layerCount = input.simpleRouteJson.layerCount || 1 const maxEdgeDistance = input.maxEdgeDistance ?? 2.0 + // For splitting, use a smaller distance to only split on directly adjacent edges + const maxSplitDistance = Math.max( + input.simpleRouteJson.minTraceWidth * 2, + 0.5, + ) + const rawEdges = this.extractEdges(input.placedRects) - const edges = this.splitEdgesOnOverlaps(rawEdges, maxEdgeDistance) + const edges = this.splitEdgesOnOverlaps(rawEdges, maxSplitDistance) + this.linkOverlapSegments( + edges, + maxEdgeDistance, + input.simpleRouteJson.minTraceWidth, + ) // Build spatial index for fast edge-to-edge queries const edgeSpatialIndex = this.buildEdgeSpatialIndex(edges, maxEdgeDistance) @@ -102,6 +115,7 @@ export class GapFillSolver extends BaseSolver { obstaclesByLayer: input.obstaclesByLayer, layerCount, maxEdgeDistance, + minTraceWidth: input.simpleRouteJson.minTraceWidth, edges, edgeSpatialIndex, phase: "SELECT_PRIMARY_EDGE", @@ -193,18 +207,16 @@ export class GapFillSolver extends BaseSolver { return edges } - private splitEdgesOnOverlaps(edges: RectEdge[], maxEdgeDistance: number): RectEdge[] { - console.log(`\n=== splitEdgesOnOverlaps START ===`) - console.log(`Input edges: ${edges.length}`) - + private splitEdgesOnOverlaps( + edges: RectEdge[], + maxEdgeDistance: number, + ): RectEdge[] { const result: RectEdge[] = [] for (const edge of edges) { const isHorizontal = Math.abs(edge.normal.y) > 0.5 const occupiedRanges: Array<{ start: number; end: number }> = [] - console.log(`\nProcessing edge ${edge.side}: (${edge.x1},${edge.y1})-(${edge.x2},${edge.y2}) normal:(${edge.normal.x},${edge.normal.y})`) - for (const other of edges) { if (edge === other) continue if (edge.rect === other.rect) continue // Skip same rectangle's edges @@ -233,19 +245,19 @@ export class GapFillSolver extends BaseSolver { } if (overlapStart < overlapEnd) { - const edgeLength = isHorizontal ? edge.x2 - edge.x1 : edge.y2 - edge.y1 + const edgeLength = isHorizontal + ? edge.x2 - edge.x1 + : edge.y2 - edge.y1 const edgeStart = isHorizontal ? edge.x1 : edge.y1 const range = { start: (overlapStart - edgeStart) / edgeLength, end: (overlapEnd - edgeStart) / edgeLength, } - console.log(` Found overlap with ${other.side}: range ${range.start.toFixed(2)}-${range.end.toFixed(2)}`) occupiedRanges.push(range) } } if (occupiedRanges.length === 0) { - console.log(` No overlaps, keeping full edge`) result.push(edge) continue } @@ -253,7 +265,10 @@ export class GapFillSolver extends BaseSolver { occupiedRanges.sort((a, b) => a.start - b.start) const merged: Array<{ start: number; end: number }> = [] for (const range of occupiedRanges) { - if (merged.length === 0 || range.start > merged[merged.length - 1]!.end) { + if ( + merged.length === 0 || + range.start > merged[merged.length - 1]!.end + ) { merged.push(range) } else { merged[merged.length - 1]!.end = Math.max( @@ -263,12 +278,9 @@ export class GapFillSolver extends BaseSolver { } } - console.log(` Merged overlaps: ${merged.map(m => `${m.start.toFixed(2)}-${m.end.toFixed(2)}`).join(', ')}`) - let pos = 0 for (const occupied of merged) { if (pos < occupied.start) { - console.log(` Creating free segment: ${pos.toFixed(2)}-${occupied.start.toFixed(2)}`) const freeSegment = this.createEdgeSegment(edge, pos, occupied.start) freeSegment.segmentType = "free" result.push(freeSegment) @@ -276,22 +288,22 @@ export class GapFillSolver extends BaseSolver { pos = occupied.end } if (pos < 1) { - console.log(` Creating free segment: ${pos.toFixed(2)}-1.00`) const freeSegment = this.createEdgeSegment(edge, pos, 1) freeSegment.segmentType = "free" result.push(freeSegment) } for (const occupied of merged) { - console.log(` Creating overlap segment: ${occupied.start.toFixed(2)}-${occupied.end.toFixed(2)}`) - const overlapSegment = this.createEdgeSegment(edge, occupied.start, occupied.end) + const overlapSegment = this.createEdgeSegment( + edge, + occupied.start, + occupied.end, + ) overlapSegment.segmentType = "overlap" result.push(overlapSegment) } } - console.log(`\nOutput edges: ${result.length}`) - console.log(`=== splitEdgesOnOverlaps END ===\n`) return result } @@ -319,6 +331,62 @@ export class GapFillSolver extends BaseSolver { } } + private linkOverlapSegments( + edges: RectEdge[], + maxEdgeDistance: number, + minTraceWidth: number, + ): void { + const overlapSegments = edges.filter((e) => e.segmentType === "overlap") + + for (const segment of overlapSegments) { + const isHorizontal = Math.abs(segment.normal.y) > 0.5 + const paired: RectEdge[] = [] + + for (const other of overlapSegments) { + if (segment === other) continue + if (segment.rect === other.rect) continue + if (!segment.zLayers.some((z) => other.zLayers.includes(z))) continue + + const isOtherHorizontal = Math.abs(other.normal.y) > 0.5 + if (isHorizontal !== isOtherHorizontal) continue + + // Check opposite normals (facing each other) + const dotProduct = + segment.normal.x * other.normal.x + segment.normal.y * other.normal.y + if (dotProduct >= -0.9) continue + + // Check distance + const distance = isHorizontal + ? Math.abs(segment.y1 - other.y1) + : Math.abs(segment.x1 - other.x1) + + // Exclude touching edges (distance = 0) - no gap to fill + const minGap = Math.max(minTraceWidth, 0.1) + if (distance < minGap) continue + + if (distance > maxEdgeDistance) continue + + // Check overlapping ranges + let overlapStart: number, overlapEnd: number + if (isHorizontal) { + overlapStart = Math.max(segment.x1, other.x1) + overlapEnd = Math.min(segment.x2, other.x2) + } else { + overlapStart = Math.max(segment.y1, other.y1) + overlapEnd = Math.min(segment.y2, other.y2) + } + + if (overlapStart < overlapEnd) { + paired.push(other) + } + } + + if (paired.length > 0) { + segment.pairedEdges = paired + } + } + } + override _setup(): void { this.stats = { phase: "EDGE_ANALYSIS", @@ -372,7 +440,14 @@ export class GapFillSolver extends BaseSolver { private stepFindNearbyEdges(): void { const primaryEdge = this.state.currentPrimaryEdge! - // Query spatial index for candidate edges near this primary edge + // For overlap segments, use paired edges directly + if (primaryEdge.pairedEdges && primaryEdge.pairedEdges.length > 0) { + this.state.currentNearbyEdges = primaryEdge.pairedEdges + this.state.phase = "CHECK_UNOCCUPIED" + return + } + + // For free segments, use spatial search const padding = this.state.maxEdgeDistance const minX = Math.min(primaryEdge.x1, primaryEdge.x2) - padding const minY = Math.min(primaryEdge.y1, primaryEdge.y2) - padding @@ -430,14 +505,20 @@ export class GapFillSolver extends BaseSolver { this.state.currentExpansionPoints[this.state.currentExpansionIndex]! const filledRect = this.expandPointToRect(point) - if ( - filledRect && - !this.overlapsExistingFill(filledRect) && - !this.overlapsInputRects(filledRect) && - !this.overlapsObstacles(filledRect) && - this.hasMinimumSize(filledRect) - ) { - this.state.filledRects.push(filledRect) + if (filledRect) { + const overlapsExisting = this.overlapsExistingFill(filledRect) + const overlapsInput = this.overlapsInputRects(filledRect) + const overlapsObst = this.overlapsObstacles(filledRect) + const hasMinSize = this.hasMinimumSize(filledRect) + + if ( + !overlapsExisting && + !overlapsInput && + !overlapsObst && + hasMinSize + ) { + this.state.filledRects.push(filledRect) + } } this.state.currentExpansionIndex++ @@ -594,7 +675,13 @@ export class GapFillSolver extends BaseSolver { if (sharedLayers.length === 0) return false const distance = this.distanceBetweenEdges(primaryEdge, candidate) - if (distance > this.state.maxEdgeDistance) return false + const minGap = Math.max(this.state.minTraceWidth, 0.1) + if (distance < minGap) { + return false + } + if (distance > this.state.maxEdgeDistance) { + return false + } return true } @@ -673,11 +760,12 @@ export class GapFillSolver extends BaseSolver { // Draw ALL edges with color coding: FREE=green, OVERLAP=red, UNSPLIT=gray for (const edge of this.state.edges) { - const color = edge.segmentType === "free" - ? "#10b981" // Green for free segments - : edge.segmentType === "overlap" - ? "#ef4444" // Red for overlap segments - : "#6b7280" // Gray for unsplit edges + const color = + edge.segmentType === "free" + ? "#10b981" // Green for free segments + : edge.segmentType === "overlap" + ? "#ef4444" // Red for overlap segments + : "#6b7280" // Gray for unsplit edges const label = edge.segmentType ? `${edge.segmentType.toUpperCase()} segment\n${edge.side}\n(${edge.x1.toFixed(2)},${edge.y1.toFixed(2)})-(${edge.x2.toFixed(2)},${edge.y2.toFixed(2)})` @@ -694,7 +782,6 @@ export class GapFillSolver extends BaseSolver { }) } - // Draw filled rectangles (green) for (const placed of this.state.filledRects) { rects.push({ diff --git a/pages/repro/gap-fill-solver/three-rects-touching.page.tsx b/pages/repro/gap-fill-solver/three-rects-touching.page.tsx index 6e29df5..7df128e 100644 --- a/pages/repro/gap-fill-solver/three-rects-touching.page.tsx +++ b/pages/repro/gap-fill-solver/three-rects-touching.page.tsx @@ -34,6 +34,7 @@ export default () => { simpleRouteJson, placedRects, obstaclesByLayer: [[]], + maxEdgeDistance: 5.0, }), [], ) From 1af7c4858ae10ccaaab393eb8532bd0297e26b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 01:45:18 +0530 Subject: [PATCH 14/44] WIP --- lib/RectDiffPipeline.ts | 58 ++++++++++++++++++------------------ lib/solvers/GapFillSolver.ts | 21 ++++++++++--- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/lib/RectDiffPipeline.ts b/lib/RectDiffPipeline.ts index 528b4a3..937baa1 100644 --- a/lib/RectDiffPipeline.ts +++ b/lib/RectDiffPipeline.ts @@ -72,24 +72,8 @@ export class RectDiffPipeline extends BasePipelineSolver const gapFillOutput = gapFillSolver.getOutput() - const gapFillMeshNodes: CapacityMeshNode[] = gapFillOutput.filledRects.map( - (placed, index) => ({ - capacityMeshNodeId: `gap-fill-${index}`, - x: placed.rect.x, - y: placed.rect.y, - center: { - x: placed.rect.x + placed.rect.width / 2, - y: placed.rect.y + placed.rect.height / 2, - }, - width: placed.rect.width, - height: placed.rect.height, - availableZ: placed.zLayers, - layer: placed.zLayers[0]?.toString() ?? "0", - }), - ) - return { - meshNodes: [...rectDiffOutput.meshNodes, ...gapFillMeshNodes], + meshNodes: [...rectDiffOutput.meshNodes, ...gapFillOutput.meshNodes], } } @@ -101,21 +85,37 @@ export class RectDiffPipeline extends BasePipelineSolver return gapFillSolver.visualize() } - if (gapFillSolver?.solved && rectDiffSolver) { + if (rectDiffSolver) { const baseViz = rectDiffSolver.visualize() - const gapFillViz = gapFillSolver.visualize() - - return { - ...baseViz, - title: "RectDiff Pipeline (with Gap Fill)", - rects: [...(baseViz.rects || []), ...(gapFillViz.rects || [])], - lines: [...(baseViz.lines || []), ...(gapFillViz.lines || [])], - points: [...(baseViz.points || []), ...(gapFillViz.points || [])], + if (gapFillSolver?.solved) { + const gapFillOutput = gapFillSolver.getOutput() + const gapFillRects = gapFillOutput.meshNodes.map((node) => { + const minZ = Math.min(...node.availableZ) + const colors = [ + { fill: "#dbeafe", stroke: "#3b82f6" }, + { fill: "#fef3c7", stroke: "#f59e0b" }, + { fill: "#d1fae5", stroke: "#10b981" }, + ] + const color = colors[minZ % colors.length]! + + return { + center: node.center, + width: node.width, + height: node.height, + fill: color.fill, + stroke: color.stroke, + label: `capacity node (gap fill)\nz: [${node.availableZ.join(", ")}]`, + } + }) + + return { + ...baseViz, + title: "RectDiff Pipeline (with Gap Fill)", + rects: [...(baseViz.rects || []), ...gapFillRects], + } } - } - if (rectDiffSolver) { - return rectDiffSolver.visualize() + return baseViz } // Show board and obstacles even before solver is initialized diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 8982160..d26156c 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -2,6 +2,7 @@ import { BaseSolver } from "@tscircuit/solver-utils" import type { GraphicsObject } from "graphics-debug" import type { SimpleRouteJson } from "../types/srj-types" import type { Placed3D, XYRect } from "./rectdiff/types" +import type { CapacityMeshNode } from "../types/capacity-mesh-types" import { FlatbushIndex } from "../data-structures/FlatbushIndex" export interface RectEdge { @@ -732,10 +733,22 @@ export class GapFillSolver extends BaseSolver { return points } - override getOutput() { - return { - filledRects: this.state.filledRects, - } + override getOutput(): { meshNodes: CapacityMeshNode[] } { + const meshNodes: CapacityMeshNode[] = this.state.filledRects.map((placed, index) => ({ + capacityMeshNodeId: `gap-fill-${index}`, + x: placed.rect.x, + y: placed.rect.y, + center: { + x: placed.rect.x + placed.rect.width / 2, + y: placed.rect.y + placed.rect.height / 2, + }, + width: placed.rect.width, + height: placed.rect.height, + availableZ: placed.zLayers, + layer: placed.zLayers[0]?.toString() ?? "0", + })) + + return { meshNodes } } override visualize(): GraphicsObject { From c150443e781c1f89ac60765c77eb0428e6fb8d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 01:46:33 +0530 Subject: [PATCH 15/44] WIP --- lib/solvers/GapFillSolver.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index d26156c..dd06795 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -734,19 +734,21 @@ export class GapFillSolver extends BaseSolver { } override getOutput(): { meshNodes: CapacityMeshNode[] } { - const meshNodes: CapacityMeshNode[] = this.state.filledRects.map((placed, index) => ({ - capacityMeshNodeId: `gap-fill-${index}`, - x: placed.rect.x, - y: placed.rect.y, - center: { - x: placed.rect.x + placed.rect.width / 2, - y: placed.rect.y + placed.rect.height / 2, - }, - width: placed.rect.width, - height: placed.rect.height, - availableZ: placed.zLayers, - layer: placed.zLayers[0]?.toString() ?? "0", - })) + const meshNodes: CapacityMeshNode[] = this.state.filledRects.map( + (placed, index) => ({ + capacityMeshNodeId: `gap-fill-${index}`, + x: placed.rect.x, + y: placed.rect.y, + center: { + x: placed.rect.x + placed.rect.width / 2, + y: placed.rect.y + placed.rect.height / 2, + }, + width: placed.rect.width, + height: placed.rect.height, + availableZ: placed.zLayers, + layer: placed.zLayers[0]?.toString() ?? "0", + }), + ) return { meshNodes } } From 9c0f9774ab5df45af8149ab631a180a8ac55107d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 11:23:02 +0530 Subject: [PATCH 16/44] new stratergy to break the segments into parts --- lib/solvers/GapFillSolver.ts | 80 +++++++++++++++++------------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index dd06795..0cda2a9 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -100,7 +100,7 @@ export class GapFillSolver extends BaseSolver { ) const rawEdges = this.extractEdges(input.placedRects) - const edges = this.splitEdgesOnOverlaps(rawEdges, maxSplitDistance) + const edges = this.splitEdgesOnOverlaps(rawEdges) this.linkOverlapSegments( edges, maxEdgeDistance, @@ -210,62 +210,59 @@ export class GapFillSolver extends BaseSolver { private splitEdgesOnOverlaps( edges: RectEdge[], - maxEdgeDistance: number, ): RectEdge[] { const result: RectEdge[] = [] + const tolerance = 0.01 for (const edge of edges) { const isHorizontal = Math.abs(edge.normal.y) > 0.5 - const occupiedRanges: Array<{ start: number; end: number }> = [] + const overlappingRanges: Array<{ start: number; end: number }> = [] for (const other of edges) { if (edge === other) continue - if (edge.rect === other.rect) continue // Skip same rectangle's edges + if (edge.rect === other.rect) continue if (!edge.zLayers.some((z) => other.zLayers.includes(z))) continue const isOtherHorizontal = Math.abs(other.normal.y) > 0.5 if (isHorizontal !== isOtherHorizontal) continue - // Check if edges are actually nearby (not just parallel) if (isHorizontal) { - const distance = Math.abs(edge.y1 - other.y1) - if (distance > maxEdgeDistance) continue - } else { - const distance = Math.abs(edge.x1 - other.x1) - if (distance > maxEdgeDistance) continue - } + if (Math.abs(edge.y1 - other.y1) > tolerance) continue - let overlapStart: number, overlapEnd: number + const overlapStart = Math.max(edge.x1, other.x1) + const overlapEnd = Math.min(edge.x2, other.x2) - if (isHorizontal) { - overlapStart = Math.max(edge.x1, other.x1) - overlapEnd = Math.min(edge.x2, other.x2) + if (overlapStart < overlapEnd) { + const edgeLength = edge.x2 - edge.x1 + overlappingRanges.push({ + start: (overlapStart - edge.x1) / edgeLength, + end: (overlapEnd - edge.x1) / edgeLength, + }) + } } else { - overlapStart = Math.max(edge.y1, other.y1) - overlapEnd = Math.min(edge.y2, other.y2) - } + if (Math.abs(edge.x1 - other.x1) > tolerance) continue - if (overlapStart < overlapEnd) { - const edgeLength = isHorizontal - ? edge.x2 - edge.x1 - : edge.y2 - edge.y1 - const edgeStart = isHorizontal ? edge.x1 : edge.y1 - const range = { - start: (overlapStart - edgeStart) / edgeLength, - end: (overlapEnd - edgeStart) / edgeLength, + const overlapStart = Math.max(edge.y1, other.y1) + const overlapEnd = Math.min(edge.y2, other.y2) + + if (overlapStart < overlapEnd) { + const edgeLength = edge.y2 - edge.y1 + overlappingRanges.push({ + start: (overlapStart - edge.y1) / edgeLength, + end: (overlapEnd - edge.y1) / edgeLength, + }) } - occupiedRanges.push(range) } } - if (occupiedRanges.length === 0) { + if (overlappingRanges.length === 0) { result.push(edge) continue } - occupiedRanges.sort((a, b) => a.start - b.start) + overlappingRanges.sort((a, b) => a.start - b.start) const merged: Array<{ start: number; end: number }> = [] - for (const range of occupiedRanges) { + for (const range of overlappingRanges) { if ( merged.length === 0 || range.start > merged[merged.length - 1]!.end @@ -283,26 +280,14 @@ export class GapFillSolver extends BaseSolver { for (const occupied of merged) { if (pos < occupied.start) { const freeSegment = this.createEdgeSegment(edge, pos, occupied.start) - freeSegment.segmentType = "free" result.push(freeSegment) } pos = occupied.end } if (pos < 1) { const freeSegment = this.createEdgeSegment(edge, pos, 1) - freeSegment.segmentType = "free" result.push(freeSegment) } - - for (const occupied of merged) { - const overlapSegment = this.createEdgeSegment( - edge, - occupied.start, - occupied.end, - ) - overlapSegment.segmentType = "overlap" - result.push(overlapSegment) - } } return result @@ -775,6 +760,8 @@ export class GapFillSolver extends BaseSolver { // Draw ALL edges with color coding: FREE=green, OVERLAP=red, UNSPLIT=gray for (const edge of this.state.edges) { + const isCurrent = edge === this.state.currentPrimaryEdge + const color = edge.segmentType === "free" ? "#10b981" // Green for free segments @@ -792,9 +779,16 @@ export class GapFillSolver extends BaseSolver { { x: edge.x2, y: edge.y2 }, ], strokeColor: color, - strokeWidth: 0.1, + strokeWidth: isCurrent ? 0.3 : 0.1, label, }) + + if (isCurrent) { + points.push({ + x: (edge.x1 + edge.x2) / 2, + y: (edge.y1 + edge.y2) / 2 + }) + } } // Draw filled rectangles (green) From fff431cb701c3e7133fba1552ecdd8c329c18592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 11:34:21 +0530 Subject: [PATCH 17/44] WIP --- lib/solvers/GapFillSolver.ts | 177 +++++------------------------------ 1 file changed, 21 insertions(+), 156 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 0cda2a9..61ee08a 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -14,25 +14,13 @@ export interface RectEdge { y2: number normal: { x: number; y: number } zLayers: number[] - segmentType?: "free" | "overlap" // Track if this is a free or overlap segment - pairedEdges?: RectEdge[] -} - -export interface UnoccupiedSection { - edge: RectEdge - start: number // 0 to 1 along edge - end: number // 0 to 1 along edge - x1: number - y1: number - x2: number - y2: number } export interface ExpansionPoint { x: number y: number zLayers: number[] - section: UnoccupiedSection + edge: RectEdge } export interface GapFillSolverInput { @@ -69,8 +57,6 @@ interface GapFillState { nearbyEdgeCandidateIndex: number currentNearbyEdges: RectEdge[] - currentUnoccupiedSections: UnoccupiedSection[] - currentExpansionPoints: ExpansionPoint[] currentExpansionIndex: number @@ -93,19 +79,8 @@ export class GapFillSolver extends BaseSolver { const layerCount = input.simpleRouteJson.layerCount || 1 const maxEdgeDistance = input.maxEdgeDistance ?? 2.0 - // For splitting, use a smaller distance to only split on directly adjacent edges - const maxSplitDistance = Math.max( - input.simpleRouteJson.minTraceWidth * 2, - 0.5, - ) - const rawEdges = this.extractEdges(input.placedRects) const edges = this.splitEdgesOnOverlaps(rawEdges) - this.linkOverlapSegments( - edges, - maxEdgeDistance, - input.simpleRouteJson.minTraceWidth, - ) // Build spatial index for fast edge-to-edge queries const edgeSpatialIndex = this.buildEdgeSpatialIndex(edges, maxEdgeDistance) @@ -123,7 +98,6 @@ export class GapFillSolver extends BaseSolver { currentEdgeIndex: 0, nearbyEdgeCandidateIndex: 0, currentNearbyEdges: [], - currentUnoccupiedSections: [], currentExpansionPoints: [], currentExpansionIndex: 0, filledRects: [], @@ -208,9 +182,7 @@ export class GapFillSolver extends BaseSolver { return edges } - private splitEdgesOnOverlaps( - edges: RectEdge[], - ): RectEdge[] { + private splitEdgesOnOverlaps(edges: RectEdge[]): RectEdge[] { const result: RectEdge[] = [] const tolerance = 0.01 @@ -317,62 +289,6 @@ export class GapFillSolver extends BaseSolver { } } - private linkOverlapSegments( - edges: RectEdge[], - maxEdgeDistance: number, - minTraceWidth: number, - ): void { - const overlapSegments = edges.filter((e) => e.segmentType === "overlap") - - for (const segment of overlapSegments) { - const isHorizontal = Math.abs(segment.normal.y) > 0.5 - const paired: RectEdge[] = [] - - for (const other of overlapSegments) { - if (segment === other) continue - if (segment.rect === other.rect) continue - if (!segment.zLayers.some((z) => other.zLayers.includes(z))) continue - - const isOtherHorizontal = Math.abs(other.normal.y) > 0.5 - if (isHorizontal !== isOtherHorizontal) continue - - // Check opposite normals (facing each other) - const dotProduct = - segment.normal.x * other.normal.x + segment.normal.y * other.normal.y - if (dotProduct >= -0.9) continue - - // Check distance - const distance = isHorizontal - ? Math.abs(segment.y1 - other.y1) - : Math.abs(segment.x1 - other.x1) - - // Exclude touching edges (distance = 0) - no gap to fill - const minGap = Math.max(minTraceWidth, 0.1) - if (distance < minGap) continue - - if (distance > maxEdgeDistance) continue - - // Check overlapping ranges - let overlapStart: number, overlapEnd: number - if (isHorizontal) { - overlapStart = Math.max(segment.x1, other.x1) - overlapEnd = Math.min(segment.x2, other.x2) - } else { - overlapStart = Math.max(segment.y1, other.y1) - overlapEnd = Math.min(segment.y2, other.y2) - } - - if (overlapStart < overlapEnd) { - paired.push(other) - } - } - - if (paired.length > 0) { - segment.pairedEdges = paired - } - } - } - override _setup(): void { this.stats = { phase: "EDGE_ANALYSIS", @@ -426,14 +342,6 @@ export class GapFillSolver extends BaseSolver { private stepFindNearbyEdges(): void { const primaryEdge = this.state.currentPrimaryEdge! - // For overlap segments, use paired edges directly - if (primaryEdge.pairedEdges && primaryEdge.pairedEdges.length > 0) { - this.state.currentNearbyEdges = primaryEdge.pairedEdges - this.state.phase = "CHECK_UNOCCUPIED" - return - } - - // For free segments, use spatial search const padding = this.state.maxEdgeDistance const minX = Math.min(primaryEdge.x1, primaryEdge.x2) - padding const minY = Math.min(primaryEdge.y1, primaryEdge.y2) - padding @@ -462,17 +370,12 @@ export class GapFillSolver extends BaseSolver { } private stepCheckUnoccupied(): void { - const primaryEdge = this.state.currentPrimaryEdge! - this.state.currentUnoccupiedSections = - this.findUnoccupiedSections(primaryEdge) - this.state.phase = "PLACE_EXPANSION_POINTS" } private stepPlaceExpansionPoints(): void { - this.state.currentExpansionPoints = this.placeExpansionPoints( - this.state.currentUnoccupiedSections, - ) + const primaryEdge = this.state.currentPrimaryEdge! + this.state.currentExpansionPoints = this.placeExpansionPoints(primaryEdge) this.state.currentExpansionIndex = 0 if (this.state.currentExpansionPoints.length > 0) { @@ -601,8 +504,7 @@ export class GapFillSolver extends BaseSolver { } private expandPointToRect(point: ExpansionPoint): Placed3D | null { - const section = point.section - const edge = section.edge + const edge = point.edge const nearbyEdge = this.state.currentNearbyEdges[0] if (!nearbyEdge) return null @@ -615,18 +517,18 @@ export class GapFillSolver extends BaseSolver { rect = { x: leftX, - y: section.y1, + y: edge.y1, width: rightX - leftX, - height: section.y2 - section.y1, + height: edge.y2 - edge.y1, } } else { const bottomY = edge.normal.y > 0 ? edge.y1 : nearbyEdge.y1 const topY = edge.normal.y > 0 ? nearbyEdge.y1 : edge.y1 rect = { - x: section.x1, + x: edge.x1, y: bottomY, - width: section.x2 - section.x1, + width: edge.x2 - edge.x1, height: topY - bottomY, } } @@ -641,7 +543,6 @@ export class GapFillSolver extends BaseSolver { this.state.currentEdgeIndex++ this.state.phase = "SELECT_PRIMARY_EDGE" this.state.currentNearbyEdges = [] - this.state.currentUnoccupiedSections = [] this.state.currentExpansionPoints = [] } @@ -679,43 +580,19 @@ export class GapFillSolver extends BaseSolver { return Math.abs(edge1.x1 - edge2.x1) } - private findUnoccupiedSections(edge: RectEdge): UnoccupiedSection[] { - // TODO: Implement - check which parts of the edge have free space - // For now, return the entire edge as one section + private placeExpansionPoints(edge: RectEdge): ExpansionPoint[] { + const offsetDistance = 0.05 + const midX = (edge.x1 + edge.x2) / 2 + const midY = (edge.y1 + edge.y2) / 2 + return [ { - edge, - start: 0, - end: 1, - x1: edge.x1, - y1: edge.y1, - x2: edge.x2, - y2: edge.y2, - }, - ] - } - - private placeExpansionPoints( - sections: UnoccupiedSection[], - ): ExpansionPoint[] { - const points: ExpansionPoint[] = [] - - for (const section of sections) { - const edge = section.edge - - const offsetDistance = 0.05 - const midX = (section.x1 + section.x2) / 2 - const midY = (section.y1 + section.y2) / 2 - - points.push({ x: midX + edge.normal.x * offsetDistance, y: midY + edge.normal.y * offsetDistance, zLayers: [...edge.zLayers], - section, - }) - } - - return points + edge, + }, + ] } override getOutput(): { meshNodes: CapacityMeshNode[] } { @@ -758,35 +635,23 @@ export class GapFillSolver extends BaseSolver { }) } - // Draw ALL edges with color coding: FREE=green, OVERLAP=red, UNSPLIT=gray for (const edge of this.state.edges) { const isCurrent = edge === this.state.currentPrimaryEdge - const color = - edge.segmentType === "free" - ? "#10b981" // Green for free segments - : edge.segmentType === "overlap" - ? "#ef4444" // Red for overlap segments - : "#6b7280" // Gray for unsplit edges - - const label = edge.segmentType - ? `${edge.segmentType.toUpperCase()} segment\n${edge.side}\n(${edge.x1.toFixed(2)},${edge.y1.toFixed(2)})-(${edge.x2.toFixed(2)},${edge.y2.toFixed(2)})` - : `edge ${edge.side}\n(${edge.x1.toFixed(2)},${edge.y1.toFixed(2)})-(${edge.x2.toFixed(2)},${edge.y2.toFixed(2)})` - lines.push({ points: [ { x: edge.x1, y: edge.y1 }, { x: edge.x2, y: edge.y2 }, ], - strokeColor: color, - strokeWidth: isCurrent ? 0.3 : 0.1, - label, + strokeColor: "#10b981", + strokeWidth: isCurrent ? 0.2 : 0.1, + label: `${edge.side}\n(${edge.x1.toFixed(2)},${edge.y1.toFixed(2)})-(${edge.x2.toFixed(2)},${edge.y2.toFixed(2)})`, }) if (isCurrent) { points.push({ x: (edge.x1 + edge.x2) / 2, - y: (edge.y1 + edge.y2) / 2 + y: (edge.y1 + edge.y2) / 2, }) } } From 1546c3f3905ff3541c90fc857641c2832bb7e25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 11:56:21 +0530 Subject: [PATCH 18/44] WIP --- lib/solvers/GapFillSolver.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 61ee08a..e28e507 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -261,6 +261,11 @@ export class GapFillSolver extends BaseSolver { result.push(freeSegment) } } + result.sort((a, b) => { + const lengthA = Math.hypot(a.x2 - a.x1, a.y2 - a.y1) + const lengthB = Math.hypot(b.x2 - b.x1, b.y2 - b.y1) + return lengthB - lengthA + }) return result } From e61591bae8082653fd66d484dac1dadeb37abb1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 12:07:52 +0530 Subject: [PATCH 19/44] WIP --- lib/solvers/GapFillSolver.ts | 90 ++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 10 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index e28e507..6410eb4 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -79,7 +79,10 @@ export class GapFillSolver extends BaseSolver { const layerCount = input.simpleRouteJson.layerCount || 1 const maxEdgeDistance = input.maxEdgeDistance ?? 2.0 - const rawEdges = this.extractEdges(input.placedRects) + const rawEdges = this.extractEdges( + input.placedRects, + input.obstaclesByLayer, + ) const edges = this.splitEdgesOnOverlaps(rawEdges) // Build spatial index for fast edge-to-edge queries @@ -124,13 +127,15 @@ export class GapFillSolver extends BaseSolver { return index } - private extractEdges(rects: Placed3D[]): RectEdge[] { + private extractEdges( + rects: Placed3D[], + obstaclesByLayer: XYRect[][], + ): RectEdge[] { const edges: RectEdge[] = [] for (const placed of rects) { const { rect, zLayers } = placed - // Top edge (y = rect.y + rect.height) edges.push({ rect, side: "top", @@ -138,11 +143,10 @@ export class GapFillSolver extends BaseSolver { y1: rect.y + rect.height, x2: rect.x + rect.width, y2: rect.y + rect.height, - normal: { x: 0, y: 1 }, // Points up + normal: { x: 0, y: 1 }, zLayers: [...zLayers], }) - // Bottom edge (y = rect.y) edges.push({ rect, side: "bottom", @@ -150,11 +154,10 @@ export class GapFillSolver extends BaseSolver { y1: rect.y, x2: rect.x + rect.width, y2: rect.y, - normal: { x: 0, y: -1 }, // Points down + normal: { x: 0, y: -1 }, zLayers: [...zLayers], }) - // Right edge (x = rect.x + rect.width) edges.push({ rect, side: "right", @@ -162,11 +165,10 @@ export class GapFillSolver extends BaseSolver { y1: rect.y, x2: rect.x + rect.width, y2: rect.y + rect.height, - normal: { x: 1, y: 0 }, // Points right + normal: { x: 1, y: 0 }, zLayers: [...zLayers], }) - // Left edge (x = rect.x) edges.push({ rect, side: "left", @@ -174,11 +176,62 @@ export class GapFillSolver extends BaseSolver { y1: rect.y, x2: rect.x, y2: rect.y + rect.height, - normal: { x: -1, y: 0 }, // Points left + normal: { x: -1, y: 0 }, zLayers: [...zLayers], }) } + for (let z = 0; z < obstaclesByLayer.length; z++) { + const obstacles = obstaclesByLayer[z] ?? [] + for (const rect of obstacles) { + const zLayers = [z] + + edges.push({ + rect, + side: "top", + x1: rect.x, + y1: rect.y + rect.height, + x2: rect.x + rect.width, + y2: rect.y + rect.height, + normal: { x: 0, y: 1 }, + zLayers, + }) + + edges.push({ + rect, + side: "bottom", + x1: rect.x, + y1: rect.y, + x2: rect.x + rect.width, + y2: rect.y, + normal: { x: 0, y: -1 }, + zLayers, + }) + + edges.push({ + rect, + side: "right", + x1: rect.x + rect.width, + y1: rect.y, + x2: rect.x + rect.width, + y2: rect.y + rect.height, + normal: { x: 1, y: 0 }, + zLayers, + }) + + edges.push({ + rect, + side: "left", + x1: rect.x, + y1: rect.y, + x2: rect.x, + y2: rect.y + rect.height, + normal: { x: -1, y: 0 }, + zLayers, + }) + } + } + return edges } @@ -640,6 +693,23 @@ export class GapFillSolver extends BaseSolver { }) } + for (let z = 0; z < this.state.obstaclesByLayer.length; z++) { + const obstacles = this.state.obstaclesByLayer[z] ?? [] + for (const obstacle of obstacles) { + rects.push({ + center: { + x: obstacle.x + obstacle.width / 2, + y: obstacle.y + obstacle.height / 2, + }, + width: obstacle.width, + height: obstacle.height, + fill: "#fee2e2", + stroke: "#ef4444", + label: `obstacle\npos: (${obstacle.x.toFixed(2)}, ${obstacle.y.toFixed(2)})\nsize: ${obstacle.width.toFixed(2)} × ${obstacle.height.toFixed(2)}\nz: ${z}`, + }) + } + } + for (const edge of this.state.edges) { const isCurrent = edge === this.state.currentPrimaryEdge From 007ed59e932cc3ab95472b7817cab8f07d8afad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 12:08:24 +0530 Subject: [PATCH 20/44] WIP --- lib/solvers/GapFillSolver.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 6410eb4..4e21f1f 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -27,10 +27,9 @@ export interface GapFillSolverInput { simpleRouteJson: SimpleRouteJson placedRects: Placed3D[] obstaclesByLayer: XYRect[][] - maxEdgeDistance?: number // Max distance to consider edges "nearby" (default: 2.0) + maxEdgeDistance?: number } -// Sub-phases for visualization type SubPhase = | "SELECT_PRIMARY_EDGE" | "FIND_NEARBY_EDGES" @@ -85,7 +84,6 @@ export class GapFillSolver extends BaseSolver { ) const edges = this.splitEdgesOnOverlaps(rawEdges) - // Build spatial index for fast edge-to-edge queries const edgeSpatialIndex = this.buildEdgeSpatialIndex(edges, maxEdgeDistance) return { @@ -114,7 +112,6 @@ export class GapFillSolver extends BaseSolver { const index = new FlatbushIndex(edges.length) for (const edge of edges) { - // Create bounding box for edge (padded by max search distance) const minX = Math.min(edge.x1, edge.x2) - maxEdgeDistance const minY = Math.min(edge.y1, edge.y2) - maxEdgeDistance const maxX = Math.max(edge.x1, edge.x2) + maxEdgeDistance From 874a99071146c189dd35c5e2ba9393ab1df4cde3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 12:13:04 +0530 Subject: [PATCH 21/44] WIP --- lib/solvers/GapFillSolver.ts | 50 +++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 4e21f1f..30de8a3 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -410,8 +410,7 @@ export class GapFillSolver extends BaseSolver { maxY, ) - // Check only the nearby candidates (not all edges!) - // Collect ALL nearby parallel edges + // Collect nearby parallel edges this.state.currentNearbyEdges = [] for (const candidate of candidates) { if ( @@ -421,6 +420,14 @@ export class GapFillSolver extends BaseSolver { this.state.currentNearbyEdges.push(candidate) } } + + // Sort by distance (furthest first) to try largest fills first + this.state.currentNearbyEdges.sort((a, b) => { + const distA = this.distanceBetweenEdges(primaryEdge, a) + const distB = this.distanceBetweenEdges(primaryEdge, b) + return distB - distA + }) + this.state.phase = "CHECK_UNOCCUPIED" } @@ -448,20 +455,23 @@ export class GapFillSolver extends BaseSolver { const point = this.state.currentExpansionPoints[this.state.currentExpansionIndex]! - const filledRect = this.expandPointToRect(point) - if (filledRect) { - const overlapsExisting = this.overlapsExistingFill(filledRect) - const overlapsInput = this.overlapsInputRects(filledRect) - const overlapsObst = this.overlapsObstacles(filledRect) - const hasMinSize = this.hasMinimumSize(filledRect) - - if ( - !overlapsExisting && - !overlapsInput && - !overlapsObst && - hasMinSize - ) { - this.state.filledRects.push(filledRect) + for (const nearbyEdge of this.state.currentNearbyEdges) { + const filledRect = this.expandPointToRect(point, nearbyEdge) + if (filledRect) { + const overlapsExisting = this.overlapsExistingFill(filledRect) + const overlapsInput = this.overlapsInputRects(filledRect) + const overlapsObst = this.overlapsObstacles(filledRect) + const hasMinSize = this.hasMinimumSize(filledRect) + + if ( + !overlapsExisting && + !overlapsInput && + !overlapsObst && + hasMinSize + ) { + this.state.filledRects.push(filledRect) + break + } } } @@ -558,12 +568,12 @@ export class GapFillSolver extends BaseSolver { return candidate.rect.width >= minSize && candidate.rect.height >= minSize } - private expandPointToRect(point: ExpansionPoint): Placed3D | null { + private expandPointToRect( + point: ExpansionPoint, + nearbyEdge: RectEdge, + ): Placed3D | null { const edge = point.edge - const nearbyEdge = this.state.currentNearbyEdges[0] - if (!nearbyEdge) return null - let rect: { x: number; y: number; width: number; height: number } if (Math.abs(edge.normal.x) > 0.5) { From d9f63e5fc08769e3decf5d0d0e8a158058463a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 12:19:01 +0530 Subject: [PATCH 22/44] WIP --- lib/RectDiffPipeline.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/RectDiffPipeline.ts b/lib/RectDiffPipeline.ts index 937baa1..aa08e2e 100644 --- a/lib/RectDiffPipeline.ts +++ b/lib/RectDiffPipeline.ts @@ -46,6 +46,7 @@ export class RectDiffPipeline extends BasePipelineSolver simpleRouteJson: instance.inputProblem.simpleRouteJson, placedRects: rectDiffState.placed || [], obstaclesByLayer: rectDiffState.obstaclesByLayer || [], + maxEdgeDistance: 10, }, ] }, From cbc19db109af773e0aaa8bdd783bdd6be0fabe90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 12:33:55 +0530 Subject: [PATCH 23/44] WIP --- lib/solvers/GapFillSolver.ts | 156 +++++++++++++++++------------------ 1 file changed, 77 insertions(+), 79 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 30de8a3..8d99cd9 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -236,11 +236,27 @@ export class GapFillSolver extends BaseSolver { const result: RectEdge[] = [] const tolerance = 0.01 + const spatialIndex = new FlatbushIndex(edges.length) + for (const edge of edges) { + const minX = Math.min(edge.x1, edge.x2) + const minY = Math.min(edge.y1, edge.y2) + const maxX = Math.max(edge.x1, edge.x2) + const maxY = Math.max(edge.y1, edge.y2) + spatialIndex.insert(edge, minX, minY, maxX, maxY) + } + spatialIndex.finish() + for (const edge of edges) { const isHorizontal = Math.abs(edge.normal.y) > 0.5 const overlappingRanges: Array<{ start: number; end: number }> = [] - for (const other of edges) { + const minX = Math.min(edge.x1, edge.x2) + const minY = Math.min(edge.y1, edge.y2) + const maxX = Math.max(edge.x1, edge.x2) + const maxY = Math.max(edge.y1, edge.y2) + const nearby = spatialIndex.search(minX, minY, maxX, maxY) + + for (const other of nearby) { if (edge === other) continue if (edge.rect === other.rect) continue if (!edge.zLayers.some((z) => other.zLayers.includes(z))) continue @@ -311,13 +327,14 @@ export class GapFillSolver extends BaseSolver { result.push(freeSegment) } } - result.sort((a, b) => { - const lengthA = Math.hypot(a.x2 - a.x1, a.y2 - a.y1) - const lengthB = Math.hypot(b.x2 - b.x1, b.y2 - b.y1) - return lengthB - lengthA - }) - - return result + // Sort by length (largest first) - cache lengths to avoid repeated calculations + const edgesWithLength = result.map((edge) => ({ + edge, + length: Math.abs(edge.x2 - edge.x1) + Math.abs(edge.y2 - edge.y1), + })) + edgesWithLength.sort((a, b) => b.length - a.length) + + return edgesWithLength.map((e) => e.edge) } private createEdgeSegment( @@ -421,12 +438,12 @@ export class GapFillSolver extends BaseSolver { } } - // Sort by distance (furthest first) to try largest fills first - this.state.currentNearbyEdges.sort((a, b) => { - const distA = this.distanceBetweenEdges(primaryEdge, a) - const distB = this.distanceBetweenEdges(primaryEdge, b) - return distB - distA - }) + const edgesWithDist = this.state.currentNearbyEdges.map((edge) => ({ + edge, + distance: this.distanceBetweenEdges(primaryEdge, edge), + })) + edgesWithDist.sort((a, b) => b.distance - a.distance) + this.state.currentNearbyEdges = edgesWithDist.map((e) => e.edge) this.state.phase = "CHECK_UNOCCUPIED" } @@ -457,21 +474,9 @@ export class GapFillSolver extends BaseSolver { for (const nearbyEdge of this.state.currentNearbyEdges) { const filledRect = this.expandPointToRect(point, nearbyEdge) - if (filledRect) { - const overlapsExisting = this.overlapsExistingFill(filledRect) - const overlapsInput = this.overlapsInputRects(filledRect) - const overlapsObst = this.overlapsObstacles(filledRect) - const hasMinSize = this.hasMinimumSize(filledRect) - - if ( - !overlapsExisting && - !overlapsInput && - !overlapsObst && - hasMinSize - ) { - this.state.filledRects.push(filledRect) - break - } + if (filledRect && this.isValidFill(filledRect)) { + this.state.filledRects.push(filledRect) + break } } @@ -481,63 +486,61 @@ export class GapFillSolver extends BaseSolver { } } - private overlapsExistingFill(candidate: Placed3D): boolean { + private isValidFill(candidate: Placed3D): boolean { + const minSize = 0.01 + if (candidate.rect.width < minSize || candidate.rect.height < minSize) { + return false + } + for (const existing of this.state.filledRects) { const sharedLayers = candidate.zLayers.filter((z) => existing.zLayers.includes(z), ) - if (sharedLayers.length === 0) continue - - const overlapX = - Math.max(candidate.rect.x, existing.rect.x) < - Math.min( - candidate.rect.x + candidate.rect.width, - existing.rect.x + existing.rect.width, - ) - const overlapY = - Math.max(candidate.rect.y, existing.rect.y) < - Math.min( - candidate.rect.y + candidate.rect.height, - existing.rect.y + existing.rect.height, - ) - - if (overlapX && overlapY) { - return true + if (sharedLayers.length > 0) { + const overlapX = + Math.max(candidate.rect.x, existing.rect.x) < + Math.min( + candidate.rect.x + candidate.rect.width, + existing.rect.x + existing.rect.width, + ) + const overlapY = + Math.max(candidate.rect.y, existing.rect.y) < + Math.min( + candidate.rect.y + candidate.rect.height, + existing.rect.y + existing.rect.height, + ) + + if (overlapX && overlapY) { + return false + } } } - return false - } - - private overlapsInputRects(candidate: Placed3D): boolean { for (const input of this.state.inputRects) { const sharedLayers = candidate.zLayers.filter((z) => input.zLayers.includes(z), ) - if (sharedLayers.length === 0) continue - - const overlapX = - Math.max(candidate.rect.x, input.rect.x) < - Math.min( - candidate.rect.x + candidate.rect.width, - input.rect.x + input.rect.width, - ) - const overlapY = - Math.max(candidate.rect.y, input.rect.y) < - Math.min( - candidate.rect.y + candidate.rect.height, - input.rect.y + input.rect.height, - ) - - if (overlapX && overlapY) { - return true + if (sharedLayers.length > 0) { + const overlapX = + Math.max(candidate.rect.x, input.rect.x) < + Math.min( + candidate.rect.x + candidate.rect.width, + input.rect.x + input.rect.width, + ) + const overlapY = + Math.max(candidate.rect.y, input.rect.y) < + Math.min( + candidate.rect.y + candidate.rect.height, + input.rect.y + input.rect.height, + ) + + if (overlapX && overlapY) { + return false + } } } - return false - } - - private overlapsObstacles(candidate: Placed3D): boolean { + // Check obstacles for (const z of candidate.zLayers) { const obstacles = this.state.obstaclesByLayer[z] ?? [] for (const obstacle of obstacles) { @@ -555,17 +558,12 @@ export class GapFillSolver extends BaseSolver { ) if (overlapX && overlapY) { - return true + return false } } } - return false - } - - private hasMinimumSize(candidate: Placed3D): boolean { - const minSize = 0.01 - return candidate.rect.width >= minSize && candidate.rect.height >= minSize + return true } private expandPointToRect( From 652a5c10054b966dea490335c77403e875b83e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 12:42:44 +0530 Subject: [PATCH 24/44] WIP --- lib/solvers/GapFillSolver.ts | 114 ++++++++--------------------------- 1 file changed, 25 insertions(+), 89 deletions(-) diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver.ts index 8d99cd9..1709c0b 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver.ts @@ -16,13 +16,6 @@ export interface RectEdge { zLayers: number[] } -export interface ExpansionPoint { - x: number - y: number - zLayers: number[] - edge: RectEdge -} - export interface GapFillSolverInput { simpleRouteJson: SimpleRouteJson placedRects: Placed3D[] @@ -33,8 +26,6 @@ export interface GapFillSolverInput { type SubPhase = | "SELECT_PRIMARY_EDGE" | "FIND_NEARBY_EDGES" - | "CHECK_UNOCCUPIED" - | "PLACE_EXPANSION_POINTS" | "EXPAND_POINT" | "DONE" @@ -56,9 +47,6 @@ interface GapFillState { nearbyEdgeCandidateIndex: number currentNearbyEdges: RectEdge[] - currentExpansionPoints: ExpansionPoint[] - currentExpansionIndex: number - filledRects: Placed3D[] } @@ -99,8 +87,6 @@ export class GapFillSolver extends BaseSolver { currentEdgeIndex: 0, nearbyEdgeCandidateIndex: 0, currentNearbyEdges: [], - currentExpansionPoints: [], - currentExpansionIndex: 0, filledRects: [], } } @@ -378,12 +364,6 @@ export class GapFillSolver extends BaseSolver { case "FIND_NEARBY_EDGES": this.stepFindNearbyEdges() break - case "CHECK_UNOCCUPIED": - this.stepCheckUnoccupied() - break - case "PLACE_EXPANSION_POINTS": - this.stepPlaceExpansionPoints() - break case "EXPAND_POINT": this.stepExpandPoint() break @@ -445,45 +425,25 @@ export class GapFillSolver extends BaseSolver { edgesWithDist.sort((a, b) => b.distance - a.distance) this.state.currentNearbyEdges = edgesWithDist.map((e) => e.edge) - this.state.phase = "CHECK_UNOCCUPIED" - } - - private stepCheckUnoccupied(): void { - this.state.phase = "PLACE_EXPANSION_POINTS" + this.state.phase = "EXPAND_POINT" } - private stepPlaceExpansionPoints(): void { + private stepExpandPoint(): void { const primaryEdge = this.state.currentPrimaryEdge! - this.state.currentExpansionPoints = this.placeExpansionPoints(primaryEdge) - this.state.currentExpansionIndex = 0 - - if (this.state.currentExpansionPoints.length > 0) { - this.state.phase = "EXPAND_POINT" - } else { - this.moveToNextEdge() - } - } - private stepExpandPoint(): void { - if ( - this.state.currentExpansionIndex < - this.state.currentExpansionPoints.length - ) { - const point = - this.state.currentExpansionPoints[this.state.currentExpansionIndex]! - - for (const nearbyEdge of this.state.currentNearbyEdges) { - const filledRect = this.expandPointToRect(point, nearbyEdge) - if (filledRect && this.isValidFill(filledRect)) { - this.state.filledRects.push(filledRect) - break - } + // Try expanding to each nearby edge (furthest first) until valid fill found + for (const nearbyEdge of this.state.currentNearbyEdges) { + const filledRect = this.expandEdgeToRect(primaryEdge, nearbyEdge) + if (filledRect && this.isValidFill(filledRect)) { + this.state.filledRects.push(filledRect) + break } - - this.state.currentExpansionIndex++ - } else { - this.moveToNextEdge() } + + // Move to next edge + this.state.currentEdgeIndex++ + this.state.phase = "SELECT_PRIMARY_EDGE" + this.state.currentNearbyEdges = [] } private isValidFill(candidate: Placed3D): boolean { @@ -566,49 +526,40 @@ export class GapFillSolver extends BaseSolver { return true } - private expandPointToRect( - point: ExpansionPoint, + private expandEdgeToRect( + primaryEdge: RectEdge, nearbyEdge: RectEdge, ): Placed3D | null { - const edge = point.edge - let rect: { x: number; y: number; width: number; height: number } - if (Math.abs(edge.normal.x) > 0.5) { - const leftX = edge.normal.x > 0 ? edge.x1 : nearbyEdge.x1 - const rightX = edge.normal.x > 0 ? nearbyEdge.x1 : edge.x1 + if (Math.abs(primaryEdge.normal.x) > 0.5) { + const leftX = primaryEdge.normal.x > 0 ? primaryEdge.x1 : nearbyEdge.x1 + const rightX = primaryEdge.normal.x > 0 ? nearbyEdge.x1 : primaryEdge.x1 rect = { x: leftX, - y: edge.y1, + y: primaryEdge.y1, width: rightX - leftX, - height: edge.y2 - edge.y1, + height: primaryEdge.y2 - primaryEdge.y1, } } else { - const bottomY = edge.normal.y > 0 ? edge.y1 : nearbyEdge.y1 - const topY = edge.normal.y > 0 ? nearbyEdge.y1 : edge.y1 + const bottomY = primaryEdge.normal.y > 0 ? primaryEdge.y1 : nearbyEdge.y1 + const topY = primaryEdge.normal.y > 0 ? nearbyEdge.y1 : primaryEdge.y1 rect = { - x: edge.x1, + x: primaryEdge.x1, y: bottomY, - width: edge.x2 - edge.x1, + width: primaryEdge.x2 - primaryEdge.x1, height: topY - bottomY, } } return { rect, - zLayers: [...point.zLayers], + zLayers: [...primaryEdge.zLayers], } } - private moveToNextEdge(): void { - this.state.currentEdgeIndex++ - this.state.phase = "SELECT_PRIMARY_EDGE" - this.state.currentNearbyEdges = [] - this.state.currentExpansionPoints = [] - } - private isNearbyParallelEdge( primaryEdge: RectEdge, candidate: RectEdge, @@ -643,21 +594,6 @@ export class GapFillSolver extends BaseSolver { return Math.abs(edge1.x1 - edge2.x1) } - private placeExpansionPoints(edge: RectEdge): ExpansionPoint[] { - const offsetDistance = 0.05 - const midX = (edge.x1 + edge.x2) / 2 - const midY = (edge.y1 + edge.y2) / 2 - - return [ - { - x: midX + edge.normal.x * offsetDistance, - y: midY + edge.normal.y * offsetDistance, - zLayers: [...edge.zLayers], - edge, - }, - ] - } - override getOutput(): { meshNodes: CapacityMeshNode[] } { const meshNodes: CapacityMeshNode[] = this.state.filledRects.map( (placed, index) => ({ From be88bd1fc2ab7bf024cfff45dc1ae1151e503b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 12:47:09 +0530 Subject: [PATCH 25/44] WIP --- lib/RectDiffPipeline.ts | 10 +++---- .../EdgeSpatialHashIndex.ts} | 29 +++++++++---------- .../gap-fill-solver/multi-layer-gap.page.tsx | 4 +-- .../simple-two-rect-with-gap.page.tsx | 4 +-- .../gap-fill-solver/staggered-rects.page.tsx | 4 +-- .../three-rects-tall-short-tall.page.tsx | 4 +-- .../three-rects-touching.page.tsx | 4 +-- .../vertical-and-horizontal-gaps.page.tsx | 4 +-- 8 files changed, 30 insertions(+), 33 deletions(-) rename lib/solvers/{GapFillSolver.ts => GapFillSolver/EdgeSpatialHashIndex.ts} (95%) diff --git a/lib/RectDiffPipeline.ts b/lib/RectDiffPipeline.ts index aa08e2e..1dd4fe9 100644 --- a/lib/RectDiffPipeline.ts +++ b/lib/RectDiffPipeline.ts @@ -2,7 +2,7 @@ import { BasePipelineSolver, definePipelineStep } from "@tscircuit/solver-utils" import type { SimpleRouteJson } from "./types/srj-types" import type { GridFill3DOptions } from "./solvers/rectdiff/types" import { RectDiffSolver } from "./solvers/RectDiffSolver" -import { GapFillSolver } from "./solvers/GapFillSolver" +import { EdgeSpatialHashIndex } from "./solvers/GapFillSolver/EdgeSpatialHashIndex" import type { CapacityMeshNode } from "./types/capacity-mesh-types" import type { GraphicsObject } from "graphics-debug" import { createBaseVisualization } from "./solvers/rectdiff/visualization" @@ -14,7 +14,7 @@ export interface RectDiffPipelineInput { export class RectDiffPipeline extends BasePipelineSolver { rectDiffSolver?: RectDiffSolver - gapFillSolver?: GapFillSolver + gapFillSolver?: EdgeSpatialHashIndex override MAX_ITERATIONS: number = 100e6 override pipelineDef = [ @@ -35,7 +35,7 @@ export class RectDiffPipeline extends BasePipelineSolver ), definePipelineStep( "gapFillSolver", - GapFillSolver, + EdgeSpatialHashIndex, (instance) => { const rectDiffSolver = instance.getSolver("rectDiffSolver")! @@ -65,7 +65,7 @@ export class RectDiffPipeline extends BasePipelineSolver override getOutput(): { meshNodes: CapacityMeshNode[] } { const rectDiffOutput = this.getSolver("rectDiffSolver")!.getOutput() - const gapFillSolver = this.getSolver("gapFillSolver") + const gapFillSolver = this.getSolver("gapFillSolver") if (!gapFillSolver) { return rectDiffOutput @@ -79,7 +79,7 @@ export class RectDiffPipeline extends BasePipelineSolver } override visualize(): GraphicsObject { - const gapFillSolver = this.getSolver("gapFillSolver") + const gapFillSolver = this.getSolver("gapFillSolver") const rectDiffSolver = this.getSolver("rectDiffSolver") if (gapFillSolver && !gapFillSolver.solved) { diff --git a/lib/solvers/GapFillSolver.ts b/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts similarity index 95% rename from lib/solvers/GapFillSolver.ts rename to lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts index 1709c0b..8fffcee 100644 --- a/lib/solvers/GapFillSolver.ts +++ b/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts @@ -1,9 +1,9 @@ import { BaseSolver } from "@tscircuit/solver-utils" import type { GraphicsObject } from "graphics-debug" -import type { SimpleRouteJson } from "../types/srj-types" -import type { Placed3D, XYRect } from "./rectdiff/types" -import type { CapacityMeshNode } from "../types/capacity-mesh-types" -import { FlatbushIndex } from "../data-structures/FlatbushIndex" +import type { SimpleRouteJson } from "../../types/srj-types" +import type { Placed3D, XYRect } from "../rectdiff/types" +import type { CapacityMeshNode } from "../../types/capacity-mesh-types" +import { FlatbushIndex } from "../../data-structures/FlatbushIndex" export interface RectEdge { rect: XYRect @@ -16,7 +16,7 @@ export interface RectEdge { zLayers: number[] } -export interface GapFillSolverInput { +export interface EdgeSpatialHashIndexInput { simpleRouteJson: SimpleRouteJson placedRects: Placed3D[] obstaclesByLayer: XYRect[][] @@ -29,7 +29,7 @@ type SubPhase = | "EXPAND_POINT" | "DONE" -interface GapFillState { +interface EdgeSpatialHashIndexState { srj: SimpleRouteJson inputRects: Placed3D[] obstaclesByLayer: XYRect[][] @@ -54,15 +54,17 @@ interface GapFillState { * Gap Fill Solver - fills gaps between existing rectangles using edge analysis. * Processes one edge per step for visualization. */ -export class GapFillSolver extends BaseSolver { - private state!: GapFillState +export class EdgeSpatialHashIndex extends BaseSolver { + private state!: EdgeSpatialHashIndexState - constructor(input: GapFillSolverInput) { + constructor(input: EdgeSpatialHashIndexInput) { super() this.state = this.initState(input) } - private initState(input: GapFillSolverInput): GapFillState { + private initState( + input: EdgeSpatialHashIndexInput, + ): EdgeSpatialHashIndexState { const layerCount = input.simpleRouteJson.layerCount || 1 const maxEdgeDistance = input.maxEdgeDistance ?? 2.0 @@ -313,7 +315,6 @@ export class GapFillSolver extends BaseSolver { result.push(freeSegment) } } - // Sort by length (largest first) - cache lengths to avoid repeated calculations const edgesWithLength = result.map((edge) => ({ edge, length: Math.abs(edge.x2 - edge.x1) + Math.abs(edge.y2 - edge.y1), @@ -431,7 +432,6 @@ export class GapFillSolver extends BaseSolver { private stepExpandPoint(): void { const primaryEdge = this.state.currentPrimaryEdge! - // Try expanding to each nearby edge (furthest first) until valid fill found for (const nearbyEdge of this.state.currentNearbyEdges) { const filledRect = this.expandEdgeToRect(primaryEdge, nearbyEdge) if (filledRect && this.isValidFill(filledRect)) { @@ -440,7 +440,6 @@ export class GapFillSolver extends BaseSolver { } } - // Move to next edge this.state.currentEdgeIndex++ this.state.phase = "SELECT_PRIMARY_EDGE" this.state.currentNearbyEdges = [] @@ -568,7 +567,7 @@ export class GapFillSolver extends BaseSolver { primaryEdge.normal.x * candidate.normal.x + primaryEdge.normal.y * candidate.normal.y - if (dotProduct >= -0.9) return false // Not opposite (not facing) + if (dotProduct >= -0.9) return false const sharedLayers = primaryEdge.zLayers.filter((z) => candidate.zLayers.includes(z), @@ -619,7 +618,6 @@ export class GapFillSolver extends BaseSolver { const points: NonNullable = [] const lines: NonNullable = [] - // Draw input rectangles (light gray) for (const placed of this.state.inputRects) { rects.push({ center: { @@ -672,7 +670,6 @@ export class GapFillSolver extends BaseSolver { } } - // Draw filled rectangles (green) for (const placed of this.state.filledRects) { rects.push({ center: { diff --git a/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx b/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx index 7b1eed9..21489cd 100644 --- a/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx +++ b/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react" -import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" @@ -37,7 +37,7 @@ export default () => { const solver = useMemo( () => - new GapFillSolver({ + new EdgeSpatialHashIndex({ simpleRouteJson, placedRects, obstaclesByLayer: [[], []], diff --git a/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx b/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx index f63351a..2126bb9 100644 --- a/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx +++ b/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react" -import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" @@ -27,7 +27,7 @@ export default () => { const solver = useMemo( () => - new GapFillSolver({ + new EdgeSpatialHashIndex({ simpleRouteJson, placedRects, obstaclesByLayer: [[]], diff --git a/pages/repro/gap-fill-solver/staggered-rects.page.tsx b/pages/repro/gap-fill-solver/staggered-rects.page.tsx index 1b8ff53..d6dd698 100644 --- a/pages/repro/gap-fill-solver/staggered-rects.page.tsx +++ b/pages/repro/gap-fill-solver/staggered-rects.page.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react" -import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" @@ -27,7 +27,7 @@ export default () => { const solver = useMemo( () => - new GapFillSolver({ + new EdgeSpatialHashIndex({ simpleRouteJson, placedRects, obstaclesByLayer: [[]], diff --git a/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx b/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx index 3b3a2c8..dd2ab9d 100644 --- a/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx +++ b/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react" -import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" @@ -30,7 +30,7 @@ export default () => { const solver = useMemo( () => - new GapFillSolver({ + new EdgeSpatialHashIndex({ simpleRouteJson, placedRects, obstaclesByLayer: [[]], diff --git a/pages/repro/gap-fill-solver/three-rects-touching.page.tsx b/pages/repro/gap-fill-solver/three-rects-touching.page.tsx index 7df128e..0609c94 100644 --- a/pages/repro/gap-fill-solver/three-rects-touching.page.tsx +++ b/pages/repro/gap-fill-solver/three-rects-touching.page.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react" -import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" @@ -30,7 +30,7 @@ export default () => { const solver = useMemo( () => - new GapFillSolver({ + new EdgeSpatialHashIndex({ simpleRouteJson, placedRects, obstaclesByLayer: [[]], diff --git a/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx b/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx index 3383d67..2b9afba 100644 --- a/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx +++ b/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react" -import { GapFillSolver } from "../../../lib/solvers/GapFillSolver" +import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" @@ -39,7 +39,7 @@ export default () => { const solver = useMemo( () => - new GapFillSolver({ + new EdgeSpatialHashIndex({ simpleRouteJson, placedRects, obstaclesByLayer: [[]], From 4b9cf284573b6bd8c6cd7dc7af9d1587c99899f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 12:54:15 +0530 Subject: [PATCH 26/44] WIP --- .../GapFillSolver/EdgeSpatialHashIndex.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts b/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts index 8fffcee..c4eaa8b 100644 --- a/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts +++ b/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts @@ -5,6 +5,16 @@ import type { Placed3D, XYRect } from "../rectdiff/types" import type { CapacityMeshNode } from "../../types/capacity-mesh-types" import { FlatbushIndex } from "../../data-structures/FlatbushIndex" +const COLOR_MAP = { + inputRectFill: "#f3f4f6", + inputRectStroke: "#9ca3af", + obstacleRectFill: "#fee2e2", + obstacleRectStroke: "#fc6e6eff", + edgeStroke: "#10b981", + filledGapFill: "#d1fae5", + filledGapStroke: "#10b981", +} + export interface RectEdge { rect: XYRect side: "top" | "bottom" | "left" | "right" @@ -626,8 +636,8 @@ export class EdgeSpatialHashIndex extends BaseSolver { }, width: placed.rect.width, height: placed.rect.height, - fill: "#f3f4f6", - stroke: "#9ca3af", + fill: COLOR_MAP.inputRectFill, + stroke: COLOR_MAP.inputRectStroke, label: `input rect\npos: (${placed.rect.x.toFixed(2)}, ${placed.rect.y.toFixed(2)})\nsize: ${placed.rect.width.toFixed(2)} × ${placed.rect.height.toFixed(2)}\nz: [${placed.zLayers.join(", ")}]`, }) } @@ -642,8 +652,8 @@ export class EdgeSpatialHashIndex extends BaseSolver { }, width: obstacle.width, height: obstacle.height, - fill: "#fee2e2", - stroke: "#ef4444", + fill: COLOR_MAP.obstacleRectFill, + stroke: COLOR_MAP.obstacleRectStroke, label: `obstacle\npos: (${obstacle.x.toFixed(2)}, ${obstacle.y.toFixed(2)})\nsize: ${obstacle.width.toFixed(2)} × ${obstacle.height.toFixed(2)}\nz: ${z}`, }) } @@ -657,7 +667,7 @@ export class EdgeSpatialHashIndex extends BaseSolver { { x: edge.x1, y: edge.y1 }, { x: edge.x2, y: edge.y2 }, ], - strokeColor: "#10b981", + strokeColor: COLOR_MAP.edgeStroke, strokeWidth: isCurrent ? 0.2 : 0.1, label: `${edge.side}\n(${edge.x1.toFixed(2)},${edge.y1.toFixed(2)})-(${edge.x2.toFixed(2)},${edge.y2.toFixed(2)})`, }) @@ -678,8 +688,8 @@ export class EdgeSpatialHashIndex extends BaseSolver { }, width: placed.rect.width, height: placed.rect.height, - fill: "#d1fae5", - stroke: "#10b981", + fill: COLOR_MAP.filledGapFill, + stroke: COLOR_MAP.filledGapStroke, label: `filled gap\npos: (${placed.rect.x.toFixed(2)}, ${placed.rect.y.toFixed(2)})\nsize: ${placed.rect.width.toFixed(2)} × ${placed.rect.height.toFixed(2)}\nz: [${placed.zLayers.join(", ")}]`, }) } From b04dce28f4eade85f10a6e36baac738bfaf84cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 13:01:40 +0530 Subject: [PATCH 27/44] WIP --- .../GapFillSolver/EdgeSpatialHashIndex.ts | 279 +----------------- .../GapFillSolver/buildEdgeSpatialIndex.ts | 21 ++ .../GapFillSolver/createEdgeSegment.ts | 25 ++ lib/solvers/GapFillSolver/extractEdges.ts | 110 +++++++ .../GapFillSolver/splitEdgesOnOverlaps.ts | 104 +++++++ lib/solvers/GapFillSolver/types.ts | 12 + 6 files changed, 279 insertions(+), 272 deletions(-) create mode 100644 lib/solvers/GapFillSolver/buildEdgeSpatialIndex.ts create mode 100644 lib/solvers/GapFillSolver/createEdgeSegment.ts create mode 100644 lib/solvers/GapFillSolver/extractEdges.ts create mode 100644 lib/solvers/GapFillSolver/splitEdgesOnOverlaps.ts create mode 100644 lib/solvers/GapFillSolver/types.ts diff --git a/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts b/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts index c4eaa8b..9c7ce54 100644 --- a/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts +++ b/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts @@ -4,6 +4,10 @@ import type { SimpleRouteJson } from "../../types/srj-types" import type { Placed3D, XYRect } from "../rectdiff/types" import type { CapacityMeshNode } from "../../types/capacity-mesh-types" import { FlatbushIndex } from "../../data-structures/FlatbushIndex" +import type { RectEdge } from "./types" +import { extractEdges } from "./extractEdges" +import { splitEdgesOnOverlaps } from "./splitEdgesOnOverlaps" +import { buildEdgeSpatialIndex } from "./buildEdgeSpatialIndex" const COLOR_MAP = { inputRectFill: "#f3f4f6", @@ -15,17 +19,6 @@ const COLOR_MAP = { filledGapStroke: "#10b981", } -export interface RectEdge { - rect: XYRect - side: "top" | "bottom" | "left" | "right" - x1: number - y1: number - x2: number - y2: number - normal: { x: number; y: number } - zLayers: number[] -} - export interface EdgeSpatialHashIndexInput { simpleRouteJson: SimpleRouteJson placedRects: Placed3D[] @@ -78,13 +71,10 @@ export class EdgeSpatialHashIndex extends BaseSolver { const layerCount = input.simpleRouteJson.layerCount || 1 const maxEdgeDistance = input.maxEdgeDistance ?? 2.0 - const rawEdges = this.extractEdges( - input.placedRects, - input.obstaclesByLayer, - ) - const edges = this.splitEdgesOnOverlaps(rawEdges) + const rawEdges = extractEdges(input.placedRects, input.obstaclesByLayer) + const edges = splitEdgesOnOverlaps(rawEdges) - const edgeSpatialIndex = this.buildEdgeSpatialIndex(edges, maxEdgeDistance) + const edgeSpatialIndex = buildEdgeSpatialIndex(edges, maxEdgeDistance) return { srj: input.simpleRouteJson, @@ -103,261 +93,6 @@ export class EdgeSpatialHashIndex extends BaseSolver { } } - private buildEdgeSpatialIndex( - edges: RectEdge[], - maxEdgeDistance: number, - ): FlatbushIndex { - const index = new FlatbushIndex(edges.length) - - for (const edge of edges) { - const minX = Math.min(edge.x1, edge.x2) - maxEdgeDistance - const minY = Math.min(edge.y1, edge.y2) - maxEdgeDistance - const maxX = Math.max(edge.x1, edge.x2) + maxEdgeDistance - const maxY = Math.max(edge.y1, edge.y2) + maxEdgeDistance - - index.insert(edge, minX, minY, maxX, maxY) - } - - index.finish() - return index - } - - private extractEdges( - rects: Placed3D[], - obstaclesByLayer: XYRect[][], - ): RectEdge[] { - const edges: RectEdge[] = [] - - for (const placed of rects) { - const { rect, zLayers } = placed - - edges.push({ - rect, - side: "top", - x1: rect.x, - y1: rect.y + rect.height, - x2: rect.x + rect.width, - y2: rect.y + rect.height, - normal: { x: 0, y: 1 }, - zLayers: [...zLayers], - }) - - edges.push({ - rect, - side: "bottom", - x1: rect.x, - y1: rect.y, - x2: rect.x + rect.width, - y2: rect.y, - normal: { x: 0, y: -1 }, - zLayers: [...zLayers], - }) - - edges.push({ - rect, - side: "right", - x1: rect.x + rect.width, - y1: rect.y, - x2: rect.x + rect.width, - y2: rect.y + rect.height, - normal: { x: 1, y: 0 }, - zLayers: [...zLayers], - }) - - edges.push({ - rect, - side: "left", - x1: rect.x, - y1: rect.y, - x2: rect.x, - y2: rect.y + rect.height, - normal: { x: -1, y: 0 }, - zLayers: [...zLayers], - }) - } - - for (let z = 0; z < obstaclesByLayer.length; z++) { - const obstacles = obstaclesByLayer[z] ?? [] - for (const rect of obstacles) { - const zLayers = [z] - - edges.push({ - rect, - side: "top", - x1: rect.x, - y1: rect.y + rect.height, - x2: rect.x + rect.width, - y2: rect.y + rect.height, - normal: { x: 0, y: 1 }, - zLayers, - }) - - edges.push({ - rect, - side: "bottom", - x1: rect.x, - y1: rect.y, - x2: rect.x + rect.width, - y2: rect.y, - normal: { x: 0, y: -1 }, - zLayers, - }) - - edges.push({ - rect, - side: "right", - x1: rect.x + rect.width, - y1: rect.y, - x2: rect.x + rect.width, - y2: rect.y + rect.height, - normal: { x: 1, y: 0 }, - zLayers, - }) - - edges.push({ - rect, - side: "left", - x1: rect.x, - y1: rect.y, - x2: rect.x, - y2: rect.y + rect.height, - normal: { x: -1, y: 0 }, - zLayers, - }) - } - } - - return edges - } - - private splitEdgesOnOverlaps(edges: RectEdge[]): RectEdge[] { - const result: RectEdge[] = [] - const tolerance = 0.01 - - const spatialIndex = new FlatbushIndex(edges.length) - for (const edge of edges) { - const minX = Math.min(edge.x1, edge.x2) - const minY = Math.min(edge.y1, edge.y2) - const maxX = Math.max(edge.x1, edge.x2) - const maxY = Math.max(edge.y1, edge.y2) - spatialIndex.insert(edge, minX, minY, maxX, maxY) - } - spatialIndex.finish() - - for (const edge of edges) { - const isHorizontal = Math.abs(edge.normal.y) > 0.5 - const overlappingRanges: Array<{ start: number; end: number }> = [] - - const minX = Math.min(edge.x1, edge.x2) - const minY = Math.min(edge.y1, edge.y2) - const maxX = Math.max(edge.x1, edge.x2) - const maxY = Math.max(edge.y1, edge.y2) - const nearby = spatialIndex.search(minX, minY, maxX, maxY) - - for (const other of nearby) { - if (edge === other) continue - if (edge.rect === other.rect) continue - if (!edge.zLayers.some((z) => other.zLayers.includes(z))) continue - - const isOtherHorizontal = Math.abs(other.normal.y) > 0.5 - if (isHorizontal !== isOtherHorizontal) continue - - if (isHorizontal) { - if (Math.abs(edge.y1 - other.y1) > tolerance) continue - - const overlapStart = Math.max(edge.x1, other.x1) - const overlapEnd = Math.min(edge.x2, other.x2) - - if (overlapStart < overlapEnd) { - const edgeLength = edge.x2 - edge.x1 - overlappingRanges.push({ - start: (overlapStart - edge.x1) / edgeLength, - end: (overlapEnd - edge.x1) / edgeLength, - }) - } - } else { - if (Math.abs(edge.x1 - other.x1) > tolerance) continue - - const overlapStart = Math.max(edge.y1, other.y1) - const overlapEnd = Math.min(edge.y2, other.y2) - - if (overlapStart < overlapEnd) { - const edgeLength = edge.y2 - edge.y1 - overlappingRanges.push({ - start: (overlapStart - edge.y1) / edgeLength, - end: (overlapEnd - edge.y1) / edgeLength, - }) - } - } - } - - if (overlappingRanges.length === 0) { - result.push(edge) - continue - } - - overlappingRanges.sort((a, b) => a.start - b.start) - const merged: Array<{ start: number; end: number }> = [] - for (const range of overlappingRanges) { - if ( - merged.length === 0 || - range.start > merged[merged.length - 1]!.end - ) { - merged.push(range) - } else { - merged[merged.length - 1]!.end = Math.max( - merged[merged.length - 1]!.end, - range.end, - ) - } - } - - let pos = 0 - for (const occupied of merged) { - if (pos < occupied.start) { - const freeSegment = this.createEdgeSegment(edge, pos, occupied.start) - result.push(freeSegment) - } - pos = occupied.end - } - if (pos < 1) { - const freeSegment = this.createEdgeSegment(edge, pos, 1) - result.push(freeSegment) - } - } - const edgesWithLength = result.map((edge) => ({ - edge, - length: Math.abs(edge.x2 - edge.x1) + Math.abs(edge.y2 - edge.y1), - })) - edgesWithLength.sort((a, b) => b.length - a.length) - - return edgesWithLength.map((e) => e.edge) - } - - private createEdgeSegment( - edge: RectEdge, - start: number, - end: number, - ): RectEdge { - const isHorizontal = Math.abs(edge.normal.y) > 0.5 - - if (isHorizontal) { - const length = edge.x2 - edge.x1 - return { - ...edge, - x1: edge.x1 + start * length, - x2: edge.x1 + end * length, - } - } else { - const length = edge.y2 - edge.y1 - return { - ...edge, - y1: edge.y1 + start * length, - y2: edge.y1 + end * length, - } - } - } - override _setup(): void { this.stats = { phase: "EDGE_ANALYSIS", diff --git a/lib/solvers/GapFillSolver/buildEdgeSpatialIndex.ts b/lib/solvers/GapFillSolver/buildEdgeSpatialIndex.ts new file mode 100644 index 0000000..ca20600 --- /dev/null +++ b/lib/solvers/GapFillSolver/buildEdgeSpatialIndex.ts @@ -0,0 +1,21 @@ +import { FlatbushIndex } from "../../data-structures/FlatbushIndex" +import type { RectEdge } from "./types" + +export function buildEdgeSpatialIndex( + edges: RectEdge[], + maxEdgeDistance: number, +): FlatbushIndex { + const index = new FlatbushIndex(edges.length) + + for (const edge of edges) { + const minX = Math.min(edge.x1, edge.x2) - maxEdgeDistance + const minY = Math.min(edge.y1, edge.y2) - maxEdgeDistance + const maxX = Math.max(edge.x1, edge.x2) + maxEdgeDistance + const maxY = Math.max(edge.y1, edge.y2) + maxEdgeDistance + + index.insert(edge, minX, minY, maxX, maxY) + } + + index.finish() + return index +} diff --git a/lib/solvers/GapFillSolver/createEdgeSegment.ts b/lib/solvers/GapFillSolver/createEdgeSegment.ts new file mode 100644 index 0000000..68d71da --- /dev/null +++ b/lib/solvers/GapFillSolver/createEdgeSegment.ts @@ -0,0 +1,25 @@ +import type { RectEdge } from "./types" + +export function createEdgeSegment( + edge: RectEdge, + start: number, + end: number, +): RectEdge { + const isHorizontal = Math.abs(edge.normal.y) > 0.5 + + if (isHorizontal) { + const length = edge.x2 - edge.x1 + return { + ...edge, + x1: edge.x1 + start * length, + x2: edge.x1 + end * length, + } + } else { + const length = edge.y2 - edge.y1 + return { + ...edge, + y1: edge.y1 + start * length, + y2: edge.y1 + end * length, + } + } +} diff --git a/lib/solvers/GapFillSolver/extractEdges.ts b/lib/solvers/GapFillSolver/extractEdges.ts new file mode 100644 index 0000000..08f9b82 --- /dev/null +++ b/lib/solvers/GapFillSolver/extractEdges.ts @@ -0,0 +1,110 @@ +import type { Placed3D, XYRect } from "../rectdiff/types" +import type { RectEdge } from "./types" + +export function extractEdges( + rects: Placed3D[], + obstaclesByLayer: XYRect[][], +): RectEdge[] { + const edges: RectEdge[] = [] + + for (const placed of rects) { + const { rect, zLayers } = placed + + edges.push({ + rect, + side: "top", + x1: rect.x, + y1: rect.y + rect.height, + x2: rect.x + rect.width, + y2: rect.y + rect.height, + normal: { x: 0, y: 1 }, + zLayers: [...zLayers], + }) + + edges.push({ + rect, + side: "bottom", + x1: rect.x, + y1: rect.y, + x2: rect.x + rect.width, + y2: rect.y, + normal: { x: 0, y: -1 }, + zLayers: [...zLayers], + }) + + edges.push({ + rect, + side: "right", + x1: rect.x + rect.width, + y1: rect.y, + x2: rect.x + rect.width, + y2: rect.y + rect.height, + normal: { x: 1, y: 0 }, + zLayers: [...zLayers], + }) + + edges.push({ + rect, + side: "left", + x1: rect.x, + y1: rect.y, + x2: rect.x, + y2: rect.y + rect.height, + normal: { x: -1, y: 0 }, + zLayers: [...zLayers], + }) + } + + for (let z = 0; z < obstaclesByLayer.length; z++) { + const obstacles = obstaclesByLayer[z] ?? [] + for (const rect of obstacles) { + const zLayers = [z] + + edges.push({ + rect, + side: "top", + x1: rect.x, + y1: rect.y + rect.height, + x2: rect.x + rect.width, + y2: rect.y + rect.height, + normal: { x: 0, y: 1 }, + zLayers, + }) + + edges.push({ + rect, + side: "bottom", + x1: rect.x, + y1: rect.y, + x2: rect.x + rect.width, + y2: rect.y, + normal: { x: 0, y: -1 }, + zLayers, + }) + + edges.push({ + rect, + side: "right", + x1: rect.x + rect.width, + y1: rect.y, + x2: rect.x + rect.width, + y2: rect.y + rect.height, + normal: { x: 1, y: 0 }, + zLayers, + }) + + edges.push({ + rect, + side: "left", + x1: rect.x, + y1: rect.y, + x2: rect.x, + y2: rect.y + rect.height, + normal: { x: -1, y: 0 }, + zLayers, + }) + } + } + + return edges +} diff --git a/lib/solvers/GapFillSolver/splitEdgesOnOverlaps.ts b/lib/solvers/GapFillSolver/splitEdgesOnOverlaps.ts new file mode 100644 index 0000000..aa13969 --- /dev/null +++ b/lib/solvers/GapFillSolver/splitEdgesOnOverlaps.ts @@ -0,0 +1,104 @@ +import { FlatbushIndex } from "../../data-structures/FlatbushIndex" +import type { RectEdge } from "./types" +import { createEdgeSegment } from "./createEdgeSegment" + +export function splitEdgesOnOverlaps(edges: RectEdge[]): RectEdge[] { + const result: RectEdge[] = [] + const tolerance = 0.01 + + const spatialIndex = new FlatbushIndex(edges.length) + for (const edge of edges) { + const minX = Math.min(edge.x1, edge.x2) + const minY = Math.min(edge.y1, edge.y2) + const maxX = Math.max(edge.x1, edge.x2) + const maxY = Math.max(edge.y1, edge.y2) + spatialIndex.insert(edge, minX, minY, maxX, maxY) + } + spatialIndex.finish() + + for (const edge of edges) { + const isHorizontal = Math.abs(edge.normal.y) > 0.5 + const overlappingRanges: Array<{ start: number; end: number }> = [] + + const minX = Math.min(edge.x1, edge.x2) + const minY = Math.min(edge.y1, edge.y2) + const maxX = Math.max(edge.x1, edge.x2) + const maxY = Math.max(edge.y1, edge.y2) + const nearby = spatialIndex.search(minX, minY, maxX, maxY) + + for (const other of nearby) { + if (edge === other) continue + if (edge.rect === other.rect) continue + if (!edge.zLayers.some((z) => other.zLayers.includes(z))) continue + + const isOtherHorizontal = Math.abs(other.normal.y) > 0.5 + if (isHorizontal !== isOtherHorizontal) continue + + if (isHorizontal) { + if (Math.abs(edge.y1 - other.y1) > tolerance) continue + + const overlapStart = Math.max(edge.x1, other.x1) + const overlapEnd = Math.min(edge.x2, other.x2) + + if (overlapStart < overlapEnd) { + const edgeLength = edge.x2 - edge.x1 + overlappingRanges.push({ + start: (overlapStart - edge.x1) / edgeLength, + end: (overlapEnd - edge.x1) / edgeLength, + }) + } + } else { + if (Math.abs(edge.x1 - other.x1) > tolerance) continue + + const overlapStart = Math.max(edge.y1, other.y1) + const overlapEnd = Math.min(edge.y2, other.y2) + + if (overlapStart < overlapEnd) { + const edgeLength = edge.y2 - edge.y1 + overlappingRanges.push({ + start: (overlapStart - edge.y1) / edgeLength, + end: (overlapEnd - edge.y1) / edgeLength, + }) + } + } + } + + if (overlappingRanges.length === 0) { + result.push(edge) + continue + } + + overlappingRanges.sort((a, b) => a.start - b.start) + const merged: Array<{ start: number; end: number }> = [] + for (const range of overlappingRanges) { + if (merged.length === 0 || range.start > merged[merged.length - 1]!.end) { + merged.push(range) + } else { + merged[merged.length - 1]!.end = Math.max( + merged[merged.length - 1]!.end, + range.end, + ) + } + } + + let pos = 0 + for (const occupied of merged) { + if (pos < occupied.start) { + const freeSegment = createEdgeSegment(edge, pos, occupied.start) + result.push(freeSegment) + } + pos = occupied.end + } + if (pos < 1) { + const freeSegment = createEdgeSegment(edge, pos, 1) + result.push(freeSegment) + } + } + const edgesWithLength = result.map((edge) => ({ + edge, + length: Math.abs(edge.x2 - edge.x1) + Math.abs(edge.y2 - edge.y1), + })) + edgesWithLength.sort((a, b) => b.length - a.length) + + return edgesWithLength.map((e) => e.edge) +} diff --git a/lib/solvers/GapFillSolver/types.ts b/lib/solvers/GapFillSolver/types.ts new file mode 100644 index 0000000..66a5a5e --- /dev/null +++ b/lib/solvers/GapFillSolver/types.ts @@ -0,0 +1,12 @@ +import type { XYRect } from "../rectdiff/types" + +export interface RectEdge { + rect: XYRect + side: "top" | "bottom" | "left" | "right" + x1: number + y1: number + x2: number + y2: number + normal: { x: number; y: number } + zLayers: number[] +} From 03ee9b4a754ea1c7801fc24ba2a156e80c75830a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 13:06:07 +0530 Subject: [PATCH 28/44] WIP --- lib/solvers/GapFillSolver/createEdgeSegment.ts | 11 ++++++----- lib/solvers/GapFillSolver/splitEdgesOnOverlaps.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/solvers/GapFillSolver/createEdgeSegment.ts b/lib/solvers/GapFillSolver/createEdgeSegment.ts index 68d71da..2314fa2 100644 --- a/lib/solvers/GapFillSolver/createEdgeSegment.ts +++ b/lib/solvers/GapFillSolver/createEdgeSegment.ts @@ -1,10 +1,11 @@ import type { RectEdge } from "./types" -export function createEdgeSegment( - edge: RectEdge, - start: number, - end: number, -): RectEdge { +export function createEdgeSegment(params: { + edge: RectEdge + start: number + end: number +}): RectEdge { + const { edge, start, end } = params const isHorizontal = Math.abs(edge.normal.y) > 0.5 if (isHorizontal) { diff --git a/lib/solvers/GapFillSolver/splitEdgesOnOverlaps.ts b/lib/solvers/GapFillSolver/splitEdgesOnOverlaps.ts index aa13969..ce23662 100644 --- a/lib/solvers/GapFillSolver/splitEdgesOnOverlaps.ts +++ b/lib/solvers/GapFillSolver/splitEdgesOnOverlaps.ts @@ -84,13 +84,17 @@ export function splitEdgesOnOverlaps(edges: RectEdge[]): RectEdge[] { let pos = 0 for (const occupied of merged) { if (pos < occupied.start) { - const freeSegment = createEdgeSegment(edge, pos, occupied.start) + const freeSegment = createEdgeSegment({ + edge, + start: pos, + end: occupied.start, + }) result.push(freeSegment) } pos = occupied.end } if (pos < 1) { - const freeSegment = createEdgeSegment(edge, pos, 1) + const freeSegment = createEdgeSegment({ edge, start: pos, end: 1 }) result.push(freeSegment) } } From cd1de715fd2b1f009a660992e3aa60ede02000ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 13:31:45 +0530 Subject: [PATCH 29/44] WIP --- .../test-cases/multi-layer-gap.json | 17 +++ .../test-cases/simple-two-rect-with-gap.json | 15 +++ .../test-cases/staggered-rects.json | 15 +++ .../three-rects-tall-short-tall.json | 16 +++ .../test-cases/three-rects-touching.json | 16 +++ .../vertical-and-horizontal-gaps.json | 17 +++ .../gap-fill-solver/multi-layer-gap.page.tsx | 38 +----- .../simple-two-rect-with-gap.page.tsx | 28 +---- .../gap-fill-solver/staggered-rects.page.tsx | 28 +---- .../three-rects-tall-short-tall.page.tsx | 31 +---- .../three-rects-touching.page.tsx | 32 +----- .../vertical-and-horizontal-gaps.page.tsx | 40 +------ .../__snapshots__/multi-layer-gap.snap.svg | 75 ++++++++++++ .../simple-two-rect-with-gap.snap.svg | 61 ++++++++++ .../__snapshots__/staggered-rects.snap.svg | 61 ++++++++++ .../three-rects-tall-short-tall.snap.svg | 71 ++++++++++++ .../three-rects-touching.snap.svg | 71 ++++++++++++ .../vertical-and-horizontal-gaps.snap.svg | 108 ++++++++++++++++++ tests/gap-fill-solver/multi-layer-gap.test.ts | 27 +++++ .../simple-two-rect-with-gap.test.ts | 25 ++++ tests/gap-fill-solver/staggered-rects.test.ts | 25 ++++ .../three-rects-tall-short-tall.test.ts | 25 ++++ .../three-rects-touching.test.ts | 25 ++++ .../vertical-and-horizontal-gaps.test.ts | 25 ++++ 24 files changed, 725 insertions(+), 167 deletions(-) create mode 100644 lib/solvers/GapFillSolver/test-cases/multi-layer-gap.json create mode 100644 lib/solvers/GapFillSolver/test-cases/simple-two-rect-with-gap.json create mode 100644 lib/solvers/GapFillSolver/test-cases/staggered-rects.json create mode 100644 lib/solvers/GapFillSolver/test-cases/three-rects-tall-short-tall.json create mode 100644 lib/solvers/GapFillSolver/test-cases/three-rects-touching.json create mode 100644 lib/solvers/GapFillSolver/test-cases/vertical-and-horizontal-gaps.json create mode 100644 tests/gap-fill-solver/__snapshots__/multi-layer-gap.snap.svg create mode 100644 tests/gap-fill-solver/__snapshots__/simple-two-rect-with-gap.snap.svg create mode 100644 tests/gap-fill-solver/__snapshots__/staggered-rects.snap.svg create mode 100644 tests/gap-fill-solver/__snapshots__/three-rects-tall-short-tall.snap.svg create mode 100644 tests/gap-fill-solver/__snapshots__/three-rects-touching.snap.svg create mode 100644 tests/gap-fill-solver/__snapshots__/vertical-and-horizontal-gaps.snap.svg create mode 100644 tests/gap-fill-solver/multi-layer-gap.test.ts create mode 100644 tests/gap-fill-solver/simple-two-rect-with-gap.test.ts create mode 100644 tests/gap-fill-solver/staggered-rects.test.ts create mode 100644 tests/gap-fill-solver/three-rects-tall-short-tall.test.ts create mode 100644 tests/gap-fill-solver/three-rects-touching.test.ts create mode 100644 tests/gap-fill-solver/vertical-and-horizontal-gaps.test.ts diff --git a/lib/solvers/GapFillSolver/test-cases/multi-layer-gap.json b/lib/solvers/GapFillSolver/test-cases/multi-layer-gap.json new file mode 100644 index 0000000..4d9003a --- /dev/null +++ b/lib/solvers/GapFillSolver/test-cases/multi-layer-gap.json @@ -0,0 +1,17 @@ +{ + "simpleRouteJson": { + "layerCount": 2, + "minTraceWidth": 0.1, + "bounds": { "minX": 0, "minY": 0, "maxX": 10, "maxY": 10 }, + "connections": [], + "obstacles": [] + }, + "placedRects": [ + { "rect": { "x": 1, "y": 2, "width": 2, "height": 3 }, "zLayers": [0] }, + { "rect": { "x": 5, "y": 2, "width": 2, "height": 3 }, "zLayers": [0] }, + { "rect": { "x": 3, "y": 6, "width": 4, "height": 2 }, "zLayers": [1] }, + { "rect": { "x": 3, "y": 1, "width": 4, "height": 2 }, "zLayers": [1] } + ], + "obstaclesByLayer": [[], []], + "maxEdgeDistance": null +} diff --git a/lib/solvers/GapFillSolver/test-cases/simple-two-rect-with-gap.json b/lib/solvers/GapFillSolver/test-cases/simple-two-rect-with-gap.json new file mode 100644 index 0000000..be57e12 --- /dev/null +++ b/lib/solvers/GapFillSolver/test-cases/simple-two-rect-with-gap.json @@ -0,0 +1,15 @@ +{ + "simpleRouteJson": { + "layerCount": 1, + "minTraceWidth": 0.1, + "bounds": { "minX": 0, "minY": 0, "maxX": 10, "maxY": 10 }, + "connections": [], + "obstacles": [] + }, + "placedRects": [ + { "rect": { "x": 1, "y": 3, "width": 3, "height": 4 }, "zLayers": [0] }, + { "rect": { "x": 5, "y": 3, "width": 3, "height": 4 }, "zLayers": [0] } + ], + "obstaclesByLayer": [[]], + "maxEdgeDistance": null +} diff --git a/lib/solvers/GapFillSolver/test-cases/staggered-rects.json b/lib/solvers/GapFillSolver/test-cases/staggered-rects.json new file mode 100644 index 0000000..8a5a81c --- /dev/null +++ b/lib/solvers/GapFillSolver/test-cases/staggered-rects.json @@ -0,0 +1,15 @@ +{ + "simpleRouteJson": { + "layerCount": 1, + "minTraceWidth": 0.1, + "bounds": { "minX": 0, "minY": 0, "maxX": 10, "maxY": 10 }, + "connections": [], + "obstacles": [] + }, + "placedRects": [ + { "rect": { "x": 1, "y": 2, "width": 3, "height": 4 }, "zLayers": [0] }, + { "rect": { "x": 5, "y": 3, "width": 3, "height": 4 }, "zLayers": [0] } + ], + "obstaclesByLayer": [[]], + "maxEdgeDistance": null +} diff --git a/lib/solvers/GapFillSolver/test-cases/three-rects-tall-short-tall.json b/lib/solvers/GapFillSolver/test-cases/three-rects-tall-short-tall.json new file mode 100644 index 0000000..e4c1672 --- /dev/null +++ b/lib/solvers/GapFillSolver/test-cases/three-rects-tall-short-tall.json @@ -0,0 +1,16 @@ +{ + "simpleRouteJson": { + "layerCount": 1, + "minTraceWidth": 0.1, + "bounds": { "minX": 0, "minY": 0, "maxX": 15, "maxY": 10 }, + "connections": [], + "obstacles": [] + }, + "placedRects": [ + { "rect": { "x": 1, "y": 1, "width": 3, "height": 8 }, "zLayers": [0] }, + { "rect": { "x": 6, "y": 3.5, "width": 3, "height": 3 }, "zLayers": [0] }, + { "rect": { "x": 11, "y": 1, "width": 3, "height": 8 }, "zLayers": [0] } + ], + "obstaclesByLayer": [[]], + "maxEdgeDistance": null +} diff --git a/lib/solvers/GapFillSolver/test-cases/three-rects-touching.json b/lib/solvers/GapFillSolver/test-cases/three-rects-touching.json new file mode 100644 index 0000000..bb9bbcc --- /dev/null +++ b/lib/solvers/GapFillSolver/test-cases/three-rects-touching.json @@ -0,0 +1,16 @@ +{ + "simpleRouteJson": { + "layerCount": 1, + "minTraceWidth": 0.1, + "bounds": { "minX": 0, "minY": 0, "maxX": 15, "maxY": 10 }, + "connections": [], + "obstacles": [] + }, + "placedRects": [ + { "rect": { "x": 1, "y": 1, "width": 3, "height": 8 }, "zLayers": [0] }, + { "rect": { "x": 4, "y": 3.5, "width": 3, "height": 3 }, "zLayers": [0] }, + { "rect": { "x": 7, "y": 1, "width": 3, "height": 8 }, "zLayers": [0] } + ], + "obstaclesByLayer": [[]], + "maxEdgeDistance": 5.0 +} diff --git a/lib/solvers/GapFillSolver/test-cases/vertical-and-horizontal-gaps.json b/lib/solvers/GapFillSolver/test-cases/vertical-and-horizontal-gaps.json new file mode 100644 index 0000000..4ede4f6 --- /dev/null +++ b/lib/solvers/GapFillSolver/test-cases/vertical-and-horizontal-gaps.json @@ -0,0 +1,17 @@ +{ + "simpleRouteJson": { + "layerCount": 1, + "minTraceWidth": 0.1, + "bounds": { "minX": 0, "minY": 0, "maxX": 12, "maxY": 12 }, + "connections": [], + "obstacles": [] + }, + "placedRects": [ + { "rect": { "x": 1, "y": 5, "width": 3, "height": 2 }, "zLayers": [0] }, + { "rect": { "x": 8, "y": 5, "width": 3, "height": 2 }, "zLayers": [0] }, + { "rect": { "x": 5, "y": 8, "width": 2, "height": 3 }, "zLayers": [0] }, + { "rect": { "x": 5, "y": 1, "width": 2, "height": 3 }, "zLayers": [0] } + ], + "obstaclesByLayer": [[]], + "maxEdgeDistance": null +} diff --git a/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx b/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx index 21489cd..e8ec0ea 100644 --- a/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx +++ b/pages/repro/gap-fill-solver/multi-layer-gap.page.tsx @@ -3,44 +3,16 @@ import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpa import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import testData from "../../../lib/solvers/GapFillSolver/test-cases/multi-layer-gap.json" export default () => { - const simpleRouteJson: SimpleRouteJson = { - layerCount: 2, - minTraceWidth: 0.1, - bounds: { minX: 0, minY: 0, maxX: 10, maxY: 10 }, - connections: [], - obstacles: [], - } - - // Multiple layers with gaps - const placedRects: Placed3D[] = [ - // Layer 0 - horizontal gap - { - rect: { x: 1, y: 2, width: 2, height: 3 }, - zLayers: [0], - }, - { - rect: { x: 5, y: 2, width: 2, height: 3 }, - zLayers: [0], - }, - // Layer 1 - vertical gap - { - rect: { x: 3, y: 6, width: 4, height: 2 }, - zLayers: [1], - }, - { - rect: { x: 3, y: 1, width: 4, height: 2 }, - zLayers: [1], - }, - ] - const solver = useMemo( () => new EdgeSpatialHashIndex({ - simpleRouteJson, - placedRects, - obstaclesByLayer: [[], []], + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }), [], ) diff --git a/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx b/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx index 2126bb9..758cb04 100644 --- a/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx +++ b/pages/repro/gap-fill-solver/simple-two-rect-with-gap.page.tsx @@ -3,34 +3,16 @@ import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpa import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import testData from "../../../lib/solvers/GapFillSolver/test-cases/simple-two-rect-with-gap.json" export default () => { - const simpleRouteJson: SimpleRouteJson = { - layerCount: 1, - minTraceWidth: 0.1, - bounds: { minX: 0, minY: 0, maxX: 10, maxY: 10 }, - connections: [], - obstacles: [], - } - - // Two rectangles with 1mm gap between them - const placedRects: Placed3D[] = [ - { - rect: { x: 1, y: 3, width: 3, height: 4 }, - zLayers: [0], - }, - { - rect: { x: 5, y: 3, width: 3, height: 4 }, - zLayers: [0], - }, - ] - const solver = useMemo( () => new EdgeSpatialHashIndex({ - simpleRouteJson, - placedRects, - obstaclesByLayer: [[]], + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }), [], ) diff --git a/pages/repro/gap-fill-solver/staggered-rects.page.tsx b/pages/repro/gap-fill-solver/staggered-rects.page.tsx index d6dd698..e54e607 100644 --- a/pages/repro/gap-fill-solver/staggered-rects.page.tsx +++ b/pages/repro/gap-fill-solver/staggered-rects.page.tsx @@ -3,34 +3,16 @@ import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpa import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import testData from "../../../lib/solvers/GapFillSolver/test-cases/staggered-rects.json" export default () => { - const simpleRouteJson: SimpleRouteJson = { - layerCount: 1, - minTraceWidth: 0.1, - bounds: { minX: 0, minY: 0, maxX: 10, maxY: 10 }, - connections: [], - obstacles: [], - } - - // Two rectangles staggered vertically with partial overlap - const placedRects: Placed3D[] = [ - { - rect: { x: 1, y: 2, width: 3, height: 4 }, - zLayers: [0], - }, - { - rect: { x: 5, y: 3, width: 3, height: 4 }, - zLayers: [0], - }, - ] - const solver = useMemo( () => new EdgeSpatialHashIndex({ - simpleRouteJson, - placedRects, - obstaclesByLayer: [[]], + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }), [], ) diff --git a/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx b/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx index dd2ab9d..fabd038 100644 --- a/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx +++ b/pages/repro/gap-fill-solver/three-rects-tall-short-tall.page.tsx @@ -3,37 +3,16 @@ import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpa import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import testData from "../../../lib/solvers/GapFillSolver/test-cases/three-rects-tall-short-tall.json" export default () => { - const simpleRouteJson: SimpleRouteJson = { - layerCount: 1, - minTraceWidth: 0.1, - bounds: { minX: 0, minY: 0, maxX: 15, maxY: 10 }, - connections: [], - obstacles: [], - } - - const placedRects: Placed3D[] = [ - { - rect: { x: 1, y: 1, width: 3, height: 8 }, - zLayers: [0], - }, - { - rect: { x: 6, y: 3.5, width: 3, height: 3 }, - zLayers: [0], - }, - { - rect: { x: 11, y: 1, width: 3, height: 8 }, - zLayers: [0], - }, - ] - const solver = useMemo( () => new EdgeSpatialHashIndex({ - simpleRouteJson, - placedRects, - obstaclesByLayer: [[]], + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }), [], ) diff --git a/pages/repro/gap-fill-solver/three-rects-touching.page.tsx b/pages/repro/gap-fill-solver/three-rects-touching.page.tsx index 0609c94..8bee74c 100644 --- a/pages/repro/gap-fill-solver/three-rects-touching.page.tsx +++ b/pages/repro/gap-fill-solver/three-rects-touching.page.tsx @@ -3,38 +3,16 @@ import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpa import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import testData from "../../../lib/solvers/GapFillSolver/test-cases/three-rects-touching.json" export default () => { - const simpleRouteJson: SimpleRouteJson = { - layerCount: 1, - minTraceWidth: 0.1, - bounds: { minX: 0, minY: 0, maxX: 15, maxY: 10 }, - connections: [], - obstacles: [], - } - - const placedRects: Placed3D[] = [ - { - rect: { x: 1, y: 1, width: 3, height: 8 }, - zLayers: [0], - }, - { - rect: { x: 4, y: 3.5, width: 3, height: 3 }, - zLayers: [0], - }, - { - rect: { x: 7, y: 1, width: 3, height: 8 }, - zLayers: [0], - }, - ] - const solver = useMemo( () => new EdgeSpatialHashIndex({ - simpleRouteJson, - placedRects, - obstaclesByLayer: [[]], - maxEdgeDistance: 5.0, + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }), [], ) diff --git a/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx b/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx index 2b9afba..627b37e 100644 --- a/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx +++ b/pages/repro/gap-fill-solver/vertical-and-horizontal-gaps.page.tsx @@ -3,46 +3,16 @@ import { EdgeSpatialHashIndex } from "../../../lib/solvers/GapFillSolver/EdgeSpa import type { SimpleRouteJson } from "../../../lib/types/srj-types" import type { Placed3D } from "../../../lib/solvers/rectdiff/types" import { GenericSolverDebugger } from "@tscircuit/solver-utils/react" +import testData from "../../../lib/solvers/GapFillSolver/test-cases/vertical-and-horizontal-gaps.json" export default () => { - const simpleRouteJson: SimpleRouteJson = { - layerCount: 1, - minTraceWidth: 0.1, - bounds: { minX: 0, minY: 0, maxX: 12, maxY: 12 }, - connections: [], - obstacles: [], - } - - // Four rectangles forming a cross with gaps - const placedRects: Placed3D[] = [ - // Left - { - rect: { x: 1, y: 5, width: 3, height: 2 }, - zLayers: [0], - }, - // Right - { - rect: { x: 8, y: 5, width: 3, height: 2 }, - zLayers: [0], - }, - // Top - { - rect: { x: 5, y: 8, width: 2, height: 3 }, - zLayers: [0], - }, - // Bottom - { - rect: { x: 5, y: 1, width: 2, height: 3 }, - zLayers: [0], - }, - ] - const solver = useMemo( () => new EdgeSpatialHashIndex({ - simpleRouteJson, - placedRects, - obstaclesByLayer: [[]], + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }), [], ) diff --git a/tests/gap-fill-solver/__snapshots__/multi-layer-gap.snap.svg b/tests/gap-fill-solver/__snapshots__/multi-layer-gap.snap.svg new file mode 100644 index 0000000..5b00fd1 --- /dev/null +++ b/tests/gap-fill-solver/__snapshots__/multi-layer-gap.snap.svg @@ -0,0 +1,75 @@ + \ No newline at end of file diff --git a/tests/gap-fill-solver/__snapshots__/simple-two-rect-with-gap.snap.svg b/tests/gap-fill-solver/__snapshots__/simple-two-rect-with-gap.snap.svg new file mode 100644 index 0000000..4bb1838 --- /dev/null +++ b/tests/gap-fill-solver/__snapshots__/simple-two-rect-with-gap.snap.svg @@ -0,0 +1,61 @@ + \ No newline at end of file diff --git a/tests/gap-fill-solver/__snapshots__/staggered-rects.snap.svg b/tests/gap-fill-solver/__snapshots__/staggered-rects.snap.svg new file mode 100644 index 0000000..6080516 --- /dev/null +++ b/tests/gap-fill-solver/__snapshots__/staggered-rects.snap.svg @@ -0,0 +1,61 @@ + \ No newline at end of file diff --git a/tests/gap-fill-solver/__snapshots__/three-rects-tall-short-tall.snap.svg b/tests/gap-fill-solver/__snapshots__/three-rects-tall-short-tall.snap.svg new file mode 100644 index 0000000..70e2c71 --- /dev/null +++ b/tests/gap-fill-solver/__snapshots__/three-rects-tall-short-tall.snap.svg @@ -0,0 +1,71 @@ + \ No newline at end of file diff --git a/tests/gap-fill-solver/__snapshots__/three-rects-touching.snap.svg b/tests/gap-fill-solver/__snapshots__/three-rects-touching.snap.svg new file mode 100644 index 0000000..f94f7d8 --- /dev/null +++ b/tests/gap-fill-solver/__snapshots__/three-rects-touching.snap.svg @@ -0,0 +1,71 @@ + \ No newline at end of file diff --git a/tests/gap-fill-solver/__snapshots__/vertical-and-horizontal-gaps.snap.svg b/tests/gap-fill-solver/__snapshots__/vertical-and-horizontal-gaps.snap.svg new file mode 100644 index 0000000..9e0863d --- /dev/null +++ b/tests/gap-fill-solver/__snapshots__/vertical-and-horizontal-gaps.snap.svg @@ -0,0 +1,108 @@ + \ No newline at end of file diff --git a/tests/gap-fill-solver/multi-layer-gap.test.ts b/tests/gap-fill-solver/multi-layer-gap.test.ts new file mode 100644 index 0000000..1044f8c --- /dev/null +++ b/tests/gap-fill-solver/multi-layer-gap.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from "bun:test" +import { EdgeSpatialHashIndex } from "../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" +import type { SimpleRouteJson } from "../../lib/types/srj-types" +import type { Placed3D } from "../../lib/solvers/rectdiff/types" +import { getSvgFromGraphicsObject } from "graphics-debug" +import testData from "../../lib/solvers/GapFillSolver/test-cases/multi-layer-gap.json" + +test("Gap Fill: Multi-layer gap", () => { + const solver = new EdgeSpatialHashIndex({ + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, + }) + + let steps = 0 + while (!solver.solved && steps < 1000) { + solver._step() + steps++ + } + + expect( + getSvgFromGraphicsObject(solver.visualize(), { + backgroundColor: "white", + }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/gap-fill-solver/simple-two-rect-with-gap.test.ts b/tests/gap-fill-solver/simple-two-rect-with-gap.test.ts new file mode 100644 index 0000000..f57ac07 --- /dev/null +++ b/tests/gap-fill-solver/simple-two-rect-with-gap.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "bun:test" +import { EdgeSpatialHashIndex } from "../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" +import type { SimpleRouteJson } from "../../lib/types/srj-types" +import type { Placed3D } from "../../lib/solvers/rectdiff/types" +import { getSvgFromGraphicsObject } from "graphics-debug" +import testData from "../../lib/solvers/GapFillSolver/test-cases/simple-two-rect-with-gap.json" + +test("Gap Fill: Simple two rects with gap", () => { + const solver = new EdgeSpatialHashIndex({ + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, + }) + + let steps = 0 + while (!solver.solved && steps < 1000) { + solver._step() + steps++ + } + + expect( + getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/gap-fill-solver/staggered-rects.test.ts b/tests/gap-fill-solver/staggered-rects.test.ts new file mode 100644 index 0000000..65b67d6 --- /dev/null +++ b/tests/gap-fill-solver/staggered-rects.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "bun:test" +import { EdgeSpatialHashIndex } from "../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" +import type { SimpleRouteJson } from "../../lib/types/srj-types" +import type { Placed3D } from "../../lib/solvers/rectdiff/types" +import { getSvgFromGraphicsObject } from "graphics-debug" +import testData from "../../lib/solvers/GapFillSolver/test-cases/staggered-rects.json" + +test("Gap Fill: Staggered rects", () => { + const solver = new EdgeSpatialHashIndex({ + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, + }) + + let steps = 0 + while (!solver.solved && steps < 1000) { + solver._step() + steps++ + } + + expect( + getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/gap-fill-solver/three-rects-tall-short-tall.test.ts b/tests/gap-fill-solver/three-rects-tall-short-tall.test.ts new file mode 100644 index 0000000..81920af --- /dev/null +++ b/tests/gap-fill-solver/three-rects-tall-short-tall.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "bun:test" +import { EdgeSpatialHashIndex } from "../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" +import type { SimpleRouteJson } from "../../lib/types/srj-types" +import type { Placed3D } from "../../lib/solvers/rectdiff/types" +import { getSvgFromGraphicsObject } from "graphics-debug" +import testData from "../../lib/solvers/GapFillSolver/test-cases/three-rects-tall-short-tall.json" + +test("Gap Fill: Three rects tall short tall", () => { + const solver = new EdgeSpatialHashIndex({ + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, + }) + + let steps = 0 + while (!solver.solved && steps < 1000) { + solver._step() + steps++ + } + + expect( + getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/gap-fill-solver/three-rects-touching.test.ts b/tests/gap-fill-solver/three-rects-touching.test.ts new file mode 100644 index 0000000..c979b5f --- /dev/null +++ b/tests/gap-fill-solver/three-rects-touching.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "bun:test" +import { EdgeSpatialHashIndex } from "../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" +import type { SimpleRouteJson } from "../../lib/types/srj-types" +import type { Placed3D } from "../../lib/solvers/rectdiff/types" +import { getSvgFromGraphicsObject } from "graphics-debug" +import testData from "../../lib/solvers/GapFillSolver/test-cases/three-rects-touching.json" + +test("Gap Fill: Three rects touching", () => { + const solver = new EdgeSpatialHashIndex({ + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, + }) + + let steps = 0 + while (!solver.solved && steps < 1000) { + solver._step() + steps++ + } + + expect( + getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/gap-fill-solver/vertical-and-horizontal-gaps.test.ts b/tests/gap-fill-solver/vertical-and-horizontal-gaps.test.ts new file mode 100644 index 0000000..ec2bb90 --- /dev/null +++ b/tests/gap-fill-solver/vertical-and-horizontal-gaps.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "bun:test" +import { EdgeSpatialHashIndex } from "../../lib/solvers/GapFillSolver/EdgeSpatialHashIndex" +import type { SimpleRouteJson } from "../../lib/types/srj-types" +import type { Placed3D } from "../../lib/solvers/rectdiff/types" +import { getSvgFromGraphicsObject } from "graphics-debug" +import testData from "../../lib/solvers/GapFillSolver/test-cases/vertical-and-horizontal-gaps.json" + +test("Gap Fill: Vertical and horizontal gaps", () => { + const solver = new EdgeSpatialHashIndex({ + simpleRouteJson: testData.simpleRouteJson as SimpleRouteJson, + placedRects: testData.placedRects as Placed3D[], + obstaclesByLayer: testData.obstaclesByLayer, + maxEdgeDistance: testData.maxEdgeDistance ?? undefined, + }) + + let steps = 0 + while (!solver.solved && steps < 1000) { + solver._step() + steps++ + } + + expect( + getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), + ).toMatchSvgSnapshot(import.meta.path) +}) From e4e54c9cc683a0cbdc0aa2d30ca9c3eb066f255e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 13:38:44 +0530 Subject: [PATCH 30/44] WIP --- .../keyboard-bugreport04.snap.svg | 605 ++++++++++++++++++ .../keyboard-bugreport04.test.ts | 17 + 2 files changed, 622 insertions(+) create mode 100644 tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg create mode 100644 tests/rect-diff-pipeline/keyboard-bugreport04.test.ts diff --git a/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg b/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg new file mode 100644 index 0000000..a959d4c --- /dev/null +++ b/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg @@ -0,0 +1,605 @@ + \ No newline at end of file diff --git a/tests/rect-diff-pipeline/keyboard-bugreport04.test.ts b/tests/rect-diff-pipeline/keyboard-bugreport04.test.ts new file mode 100644 index 0000000..89d2ecf --- /dev/null +++ b/tests/rect-diff-pipeline/keyboard-bugreport04.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from "bun:test" +import { RectDiffPipeline } from "../../lib/RectDiffPipeline" +import simpleRouteJson from "../../test-assets/bugreport04-aa1d41.json" +import { getSvgFromGraphicsObject } from "graphics-debug" +import type { SimpleRouteJson } from "../../lib/types/srj-types" + +test("RectDiffPipeline: Keyboard Bug Report 04", () => { + const solver = new RectDiffPipeline({ + simpleRouteJson: simpleRouteJson.simple_route_json as SimpleRouteJson, + }) + + solver.solve() + + expect( + getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), + ).toMatchSvgSnapshot(import.meta.path) +}) From 655de795698e7bab81e8483fecff81ce8e84c8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 14:33:44 +0530 Subject: [PATCH 31/44] WIP --- .../GapFillSolver/EdgeSpatialHashIndex.ts | 68 ++++--------------- lib/solvers/rectdiff/geometry.ts | 8 +-- .../keyboard-bugreport04.snap.svg | 4 +- 3 files changed, 18 insertions(+), 62 deletions(-) diff --git a/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts b/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts index 9c7ce54..9b7f6c5 100644 --- a/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts +++ b/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts @@ -8,6 +8,7 @@ import type { RectEdge } from "./types" import { extractEdges } from "./extractEdges" import { splitEdgesOnOverlaps } from "./splitEdgesOnOverlaps" import { buildEdgeSpatialIndex } from "./buildEdgeSpatialIndex" +import { overlaps } from "../rectdiff/geometry" const COLOR_MAP = { inputRectFill: "#f3f4f6", @@ -196,51 +197,23 @@ export class EdgeSpatialHashIndex extends BaseSolver { return false } + // Check filled rects for (const existing of this.state.filledRects) { - const sharedLayers = candidate.zLayers.filter((z) => - existing.zLayers.includes(z), - ) - if (sharedLayers.length > 0) { - const overlapX = - Math.max(candidate.rect.x, existing.rect.x) < - Math.min( - candidate.rect.x + candidate.rect.width, - existing.rect.x + existing.rect.width, - ) - const overlapY = - Math.max(candidate.rect.y, existing.rect.y) < - Math.min( - candidate.rect.y + candidate.rect.height, - existing.rect.y + existing.rect.height, - ) - - if (overlapX && overlapY) { - return false - } + if ( + candidate.zLayers.some((z) => existing.zLayers.includes(z)) && + overlaps(candidate.rect, existing.rect) + ) { + return false } } + // Check input rects for (const input of this.state.inputRects) { - const sharedLayers = candidate.zLayers.filter((z) => - input.zLayers.includes(z), - ) - if (sharedLayers.length > 0) { - const overlapX = - Math.max(candidate.rect.x, input.rect.x) < - Math.min( - candidate.rect.x + candidate.rect.width, - input.rect.x + input.rect.width, - ) - const overlapY = - Math.max(candidate.rect.y, input.rect.y) < - Math.min( - candidate.rect.y + candidate.rect.height, - input.rect.y + input.rect.height, - ) - - if (overlapX && overlapY) { - return false - } + if ( + candidate.zLayers.some((z) => input.zLayers.includes(z)) && + overlaps(candidate.rect, input.rect) + ) { + return false } } @@ -248,20 +221,7 @@ export class EdgeSpatialHashIndex extends BaseSolver { for (const z of candidate.zLayers) { const obstacles = this.state.obstaclesByLayer[z] ?? [] for (const obstacle of obstacles) { - const overlapX = - Math.max(candidate.rect.x, obstacle.x) < - Math.min( - candidate.rect.x + candidate.rect.width, - obstacle.x + obstacle.width, - ) - const overlapY = - Math.max(candidate.rect.y, obstacle.y) < - Math.min( - candidate.rect.y + candidate.rect.height, - obstacle.y + obstacle.height, - ) - - if (overlapX && overlapY) { + if (overlaps(candidate.rect, obstacle)) { return false } } diff --git a/lib/solvers/rectdiff/geometry.ts b/lib/solvers/rectdiff/geometry.ts index f2b3b52..d29fa2a 100644 --- a/lib/solvers/rectdiff/geometry.ts +++ b/lib/solvers/rectdiff/geometry.ts @@ -10,11 +10,9 @@ export const lt = (a: number, b: number) => a < b - EPS export const lte = (a: number, b: number) => a < b + EPS export function overlaps(a: XYRect, b: XYRect) { - return !( - a.x + a.width <= b.x + EPS || - b.x + b.width <= a.x + EPS || - a.y + a.height <= b.y + EPS || - b.y + b.height <= a.y + EPS + return ( + Math.max(a.x, b.x) < Math.min(a.x + a.width, b.x + b.width) && + Math.max(a.y, b.y) < Math.min(a.y + a.height, b.y + b.height) ) } diff --git a/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg b/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg index a959d4c..091ff8f 100644 --- a/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg +++ b/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg @@ -321,7 +321,6 @@ z: [1]" data-x="-53.27509599999996" data-y="7.6449972500000385" x="180.920900177 z: [1]" data-x="-53.27509599999996" data-y="4.895002750000039" x="180.92090017769038" y="246.3307648206276" width="7.370084909474912" height="0.635352147368252" fill="#fef3c7" stroke="#f59e0b" stroke-width="0.39348178964285735"/> { maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }) - let steps = 0 - while (!solver.solved && steps < 1000) { - solver._step() - steps++ - } + solver.solve() expect( getSvgFromGraphicsObject(solver.visualize(), { diff --git a/tests/gap-fill-solver/simple-two-rect-with-gap.test.ts b/tests/gap-fill-solver/simple-two-rect-with-gap.test.ts index f57ac07..253edb2 100644 --- a/tests/gap-fill-solver/simple-two-rect-with-gap.test.ts +++ b/tests/gap-fill-solver/simple-two-rect-with-gap.test.ts @@ -13,11 +13,7 @@ test("Gap Fill: Simple two rects with gap", () => { maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }) - let steps = 0 - while (!solver.solved && steps < 1000) { - solver._step() - steps++ - } + solver.solve() expect( getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), diff --git a/tests/gap-fill-solver/staggered-rects.test.ts b/tests/gap-fill-solver/staggered-rects.test.ts index 65b67d6..235dab0 100644 --- a/tests/gap-fill-solver/staggered-rects.test.ts +++ b/tests/gap-fill-solver/staggered-rects.test.ts @@ -13,11 +13,7 @@ test("Gap Fill: Staggered rects", () => { maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }) - let steps = 0 - while (!solver.solved && steps < 1000) { - solver._step() - steps++ - } + solver.solve() expect( getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), diff --git a/tests/gap-fill-solver/three-rects-tall-short-tall.test.ts b/tests/gap-fill-solver/three-rects-tall-short-tall.test.ts index 81920af..c877272 100644 --- a/tests/gap-fill-solver/three-rects-tall-short-tall.test.ts +++ b/tests/gap-fill-solver/three-rects-tall-short-tall.test.ts @@ -13,11 +13,7 @@ test("Gap Fill: Three rects tall short tall", () => { maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }) - let steps = 0 - while (!solver.solved && steps < 1000) { - solver._step() - steps++ - } + solver.solve() expect( getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), diff --git a/tests/gap-fill-solver/three-rects-touching.test.ts b/tests/gap-fill-solver/three-rects-touching.test.ts index c979b5f..cb3b4f7 100644 --- a/tests/gap-fill-solver/three-rects-touching.test.ts +++ b/tests/gap-fill-solver/three-rects-touching.test.ts @@ -13,11 +13,7 @@ test("Gap Fill: Three rects touching", () => { maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }) - let steps = 0 - while (!solver.solved && steps < 1000) { - solver._step() - steps++ - } + solver.solve() expect( getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), diff --git a/tests/gap-fill-solver/vertical-and-horizontal-gaps.test.ts b/tests/gap-fill-solver/vertical-and-horizontal-gaps.test.ts index ec2bb90..27c4dc8 100644 --- a/tests/gap-fill-solver/vertical-and-horizontal-gaps.test.ts +++ b/tests/gap-fill-solver/vertical-and-horizontal-gaps.test.ts @@ -12,12 +12,7 @@ test("Gap Fill: Vertical and horizontal gaps", () => { obstaclesByLayer: testData.obstaclesByLayer, maxEdgeDistance: testData.maxEdgeDistance ?? undefined, }) - - let steps = 0 - while (!solver.solved && steps < 1000) { - solver._step() - steps++ - } + solver.solve() expect( getSvgFromGraphicsObject(solver.visualize(), { backgroundColor: "white" }), From 1634c34c415a5e237689deff36616355f08e28a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0hm=E2=98=98=EF=B8=8F?= Date: Fri, 19 Dec 2025 20:29:46 +0530 Subject: [PATCH 33/44] WIP --- lib/RectDiffPipeline.ts | 13 ++- .../GapFillSolver/EdgeSpatialHashIndex.ts | 49 ++------- .../EdgeSpatialHashIndexManager.ts | 104 ++++++++++++++++++ .../GapFillSolver/visualizeBaseState.ts | 56 ++++++++++ .../keyboard-bugreport04.snap.svg | 18 ++- 5 files changed, 194 insertions(+), 46 deletions(-) create mode 100644 lib/solvers/GapFillSolver/EdgeSpatialHashIndexManager.ts create mode 100644 lib/solvers/GapFillSolver/visualizeBaseState.ts diff --git a/lib/RectDiffPipeline.ts b/lib/RectDiffPipeline.ts index 1dd4fe9..e241a7d 100644 --- a/lib/RectDiffPipeline.ts +++ b/lib/RectDiffPipeline.ts @@ -2,7 +2,7 @@ import { BasePipelineSolver, definePipelineStep } from "@tscircuit/solver-utils" import type { SimpleRouteJson } from "./types/srj-types" import type { GridFill3DOptions } from "./solvers/rectdiff/types" import { RectDiffSolver } from "./solvers/RectDiffSolver" -import { EdgeSpatialHashIndex } from "./solvers/GapFillSolver/EdgeSpatialHashIndex" +import { EdgeSpatialHashIndexManager } from "./solvers/GapFillSolver/EdgeSpatialHashIndexManager" import type { CapacityMeshNode } from "./types/capacity-mesh-types" import type { GraphicsObject } from "graphics-debug" import { createBaseVisualization } from "./solvers/rectdiff/visualization" @@ -14,7 +14,7 @@ export interface RectDiffPipelineInput { export class RectDiffPipeline extends BasePipelineSolver { rectDiffSolver?: RectDiffSolver - gapFillSolver?: EdgeSpatialHashIndex + gapFillSolver?: EdgeSpatialHashIndexManager override MAX_ITERATIONS: number = 100e6 override pipelineDef = [ @@ -35,7 +35,7 @@ export class RectDiffPipeline extends BasePipelineSolver ), definePipelineStep( "gapFillSolver", - EdgeSpatialHashIndex, + EdgeSpatialHashIndexManager, (instance) => { const rectDiffSolver = instance.getSolver("rectDiffSolver")! @@ -47,6 +47,7 @@ export class RectDiffPipeline extends BasePipelineSolver placedRects: rectDiffState.placed || [], obstaclesByLayer: rectDiffState.obstaclesByLayer || [], maxEdgeDistance: 10, + repeatCount: 3, }, ] }, @@ -65,7 +66,8 @@ export class RectDiffPipeline extends BasePipelineSolver override getOutput(): { meshNodes: CapacityMeshNode[] } { const rectDiffOutput = this.getSolver("rectDiffSolver")!.getOutput() - const gapFillSolver = this.getSolver("gapFillSolver") + const gapFillSolver = + this.getSolver("gapFillSolver") if (!gapFillSolver) { return rectDiffOutput @@ -79,7 +81,8 @@ export class RectDiffPipeline extends BasePipelineSolver } override visualize(): GraphicsObject { - const gapFillSolver = this.getSolver("gapFillSolver") + const gapFillSolver = + this.getSolver("gapFillSolver") const rectDiffSolver = this.getSolver("rectDiffSolver") if (gapFillSolver && !gapFillSolver.solved) { diff --git a/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts b/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts index 9b7f6c5..75ed9f7 100644 --- a/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts +++ b/lib/solvers/GapFillSolver/EdgeSpatialHashIndex.ts @@ -9,12 +9,9 @@ import { extractEdges } from "./extractEdges" import { splitEdgesOnOverlaps } from "./splitEdgesOnOverlaps" import { buildEdgeSpatialIndex } from "./buildEdgeSpatialIndex" import { overlaps } from "../rectdiff/geometry" +import { visualizeBaseState } from "./visualizeBaseState" const COLOR_MAP = { - inputRectFill: "#f3f4f6", - inputRectStroke: "#9ca3af", - obstacleRectFill: "#fee2e2", - obstacleRectStroke: "#fc6e6eff", edgeStroke: "#10b981", filledGapFill: "#d1fae5", filledGapStroke: "#10b981", @@ -319,41 +316,15 @@ export class EdgeSpatialHashIndex extends BaseSolver { } override visualize(): GraphicsObject { - const rects: NonNullable = [] + const baseViz = visualizeBaseState( + this.state.inputRects, + this.state.obstaclesByLayer, + `Gap Fill (Edge ${this.state.currentEdgeIndex}/${this.state.edges.length})`, + ) + const points: NonNullable = [] const lines: NonNullable = [] - for (const placed of this.state.inputRects) { - rects.push({ - center: { - x: placed.rect.x + placed.rect.width / 2, - y: placed.rect.y + placed.rect.height / 2, - }, - width: placed.rect.width, - height: placed.rect.height, - fill: COLOR_MAP.inputRectFill, - stroke: COLOR_MAP.inputRectStroke, - label: `input rect\npos: (${placed.rect.x.toFixed(2)}, ${placed.rect.y.toFixed(2)})\nsize: ${placed.rect.width.toFixed(2)} × ${placed.rect.height.toFixed(2)}\nz: [${placed.zLayers.join(", ")}]`, - }) - } - - for (let z = 0; z < this.state.obstaclesByLayer.length; z++) { - const obstacles = this.state.obstaclesByLayer[z] ?? [] - for (const obstacle of obstacles) { - rects.push({ - center: { - x: obstacle.x + obstacle.width / 2, - y: obstacle.y + obstacle.height / 2, - }, - width: obstacle.width, - height: obstacle.height, - fill: COLOR_MAP.obstacleRectFill, - stroke: COLOR_MAP.obstacleRectStroke, - label: `obstacle\npos: (${obstacle.x.toFixed(2)}, ${obstacle.y.toFixed(2)})\nsize: ${obstacle.width.toFixed(2)} × ${obstacle.height.toFixed(2)}\nz: ${z}`, - }) - } - } - for (const edge of this.state.edges) { const isCurrent = edge === this.state.currentPrimaryEdge @@ -376,7 +347,7 @@ export class EdgeSpatialHashIndex extends BaseSolver { } for (const placed of this.state.filledRects) { - rects.push({ + baseViz.rects!.push({ center: { x: placed.rect.x + placed.rect.width / 2, y: placed.rect.y + placed.rect.height / 2, @@ -390,9 +361,7 @@ export class EdgeSpatialHashIndex extends BaseSolver { } return { - title: `Gap Fill (Edge ${this.state.currentEdgeIndex}/${this.state.edges.length})`, - coordinateSystem: "cartesian", - rects, + ...baseViz, points, lines, } diff --git a/lib/solvers/GapFillSolver/EdgeSpatialHashIndexManager.ts b/lib/solvers/GapFillSolver/EdgeSpatialHashIndexManager.ts new file mode 100644 index 0000000..c26f55b --- /dev/null +++ b/lib/solvers/GapFillSolver/EdgeSpatialHashIndexManager.ts @@ -0,0 +1,104 @@ +import { BaseSolver } from "@tscircuit/solver-utils" +import type { GraphicsObject } from "graphics-debug" +import type { CapacityMeshNode } from "../../types/capacity-mesh-types" +import { + EdgeSpatialHashIndex, + type EdgeSpatialHashIndexInput, +} from "./EdgeSpatialHashIndex" +import { visualizeBaseState } from "./visualizeBaseState" + +export interface EdgeSpatialHashIndexManagerInput + extends EdgeSpatialHashIndexInput { + repeatCount?: number +} + +export class EdgeSpatialHashIndexManager extends BaseSolver { + private input: EdgeSpatialHashIndexManagerInput + private repeatCount: number + private currentIteration: number = 0 + private activeSubsolver: EdgeSpatialHashIndex | null = null + private allFilledRects: any[] = [] + + constructor(input: EdgeSpatialHashIndexManagerInput) { + super() + this.input = input + this.repeatCount = input.repeatCount ?? 1 + } + + override _setup(): void { + this.currentIteration = 0 + this.allFilledRects = [] + this.startNextIteration() + } + + private startNextIteration(): void { + if (this.currentIteration >= this.repeatCount) { + this.solved = true + return + } + + this.activeSubsolver = new EdgeSpatialHashIndex({ + ...this.input, + placedRects: [...this.input.placedRects, ...this.allFilledRects], + }) + this.activeSubsolver._setup() + this.currentIteration++ + } + + override _step(): void { + if (!this.activeSubsolver) { + this.solved = true + return + } + + if (this.activeSubsolver.solved) { + const output = this.activeSubsolver.getOutput() + this.allFilledRects.push( + ...output.meshNodes.map((node: any) => ({ + rect: { + x: node.x, + y: node.y, + width: node.width, + height: node.height, + }, + zLayers: node.availableZ, + })), + ) + this.startNextIteration() + return + } + + this.activeSubsolver._step() + } + + override getOutput(): { meshNodes: CapacityMeshNode[] } { + const meshNodes: CapacityMeshNode[] = this.allFilledRects.map( + (placed, index) => ({ + capacityMeshNodeId: `gap-fill-${index}`, + x: placed.rect.x, + y: placed.rect.y, + center: { + x: placed.rect.x + placed.rect.width / 2, + y: placed.rect.y + placed.rect.height / 2, + }, + width: placed.rect.width, + height: placed.rect.height, + availableZ: placed.zLayers, + layer: placed.zLayers[0]?.toString() ?? "0", + }), + ) + + return { meshNodes } + } + + override visualize(): GraphicsObject { + if (this.activeSubsolver) { + return this.activeSubsolver.visualize() + } + return visualizeBaseState( + this.input.placedRects, + this.input.obstaclesByLayer, + `Gap Fill Manager (Iteration ${this.currentIteration}/${this.repeatCount})`, + ) + } +} diff --git a/lib/solvers/GapFillSolver/visualizeBaseState.ts b/lib/solvers/GapFillSolver/visualizeBaseState.ts new file mode 100644 index 0000000..18ac90a --- /dev/null +++ b/lib/solvers/GapFillSolver/visualizeBaseState.ts @@ -0,0 +1,56 @@ +import type { GraphicsObject } from "graphics-debug" +import type { Placed3D, XYRect } from "../rectdiff/types" + +const COLOR_MAP = { + inputRectFill: "#f3f4f6", + inputRectStroke: "#9ca3af", + obstacleRectFill: "#fee2e2", + obstacleRectStroke: "#fc6e6eff", +} + +export function visualizeBaseState( + inputRects: Placed3D[], + obstaclesByLayer: XYRect[][], + title: string = "Gap Fill", +): GraphicsObject { + const rects: NonNullable = [] + + for (const placed of inputRects) { + rects.push({ + center: { + x: placed.rect.x + placed.rect.width / 2, + y: placed.rect.y + placed.rect.height / 2, + }, + width: placed.rect.width, + height: placed.rect.height, + fill: COLOR_MAP.inputRectFill, + stroke: COLOR_MAP.inputRectStroke, + label: `input rect\npos: (${placed.rect.x.toFixed(2)}, ${placed.rect.y.toFixed(2)})\nsize: ${placed.rect.width.toFixed(2)} × ${placed.rect.height.toFixed(2)}\nz: [${placed.zLayers.join(", ")}]`, + }) + } + + for (let z = 0; z < obstaclesByLayer.length; z++) { + const obstacles = obstaclesByLayer[z] ?? [] + for (const obstacle of obstacles) { + rects.push({ + center: { + x: obstacle.x + obstacle.width / 2, + y: obstacle.y + obstacle.height / 2, + }, + width: obstacle.width, + height: obstacle.height, + fill: COLOR_MAP.obstacleRectFill, + stroke: COLOR_MAP.obstacleRectStroke, + label: `obstacle\npos: (${obstacle.x.toFixed(2)}, ${obstacle.y.toFixed(2)})\nsize: ${obstacle.width.toFixed(2)} × ${obstacle.height.toFixed(2)}\nz: ${z}`, + }) + } + } + + return { + title, + coordinateSystem: "cartesian", + rects, + points: [], + lines: [], + } +} diff --git a/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg b/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg index 091ff8f..7c39602 100644 --- a/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg +++ b/tests/rect-diff-pipeline/__snapshots__/keyboard-bugreport04.snap.svg @@ -557,7 +557,23 @@ z: [0]" data-x="80" data-y="-7" x="522.7031545027635" y="276.0653111400887" widt z: [0]" data-x="100" data-y="-7" x="573.5314279487924" y="276.0653111400887" width="1.219878562704821" height="1.626504750272943" fill="#dbeafe" stroke="#3b82f6" stroke-width="0.39348178964285735"/>