@tscircuit/rectdiff 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/index.d.ts +97 -12
  2. package/dist/index.js +714 -81
  3. package/lib/RectDiffPipeline.ts +79 -13
  4. package/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts +284 -0
  5. package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +213 -0
  6. package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +129 -0
  7. package/lib/solvers/GapFillSolver/edge-constants.ts +48 -0
  8. package/lib/solvers/GapFillSolver/getBoundsFromCorners.ts +10 -0
  9. package/lib/solvers/GapFillSolver/projectToUncoveredSegments.ts +92 -0
  10. package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +32 -0
  11. package/lib/solvers/RectDiffSolver.ts +1 -33
  12. package/package.json +9 -6
  13. package/tests/board-outline.test.ts +1 -1
  14. package/tsconfig.json +4 -0
  15. package/vite.config.ts +6 -0
  16. package/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +0 -28
  17. package/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +0 -83
  18. package/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +0 -100
  19. package/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +0 -75
  20. package/lib/solvers/rectdiff/gapfill/detection.ts +0 -3
  21. package/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +0 -27
  22. package/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +0 -44
  23. package/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +0 -43
  24. package/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +0 -42
  25. package/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +0 -57
  26. package/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +0 -128
  27. package/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +0 -78
  28. package/lib/solvers/rectdiff/gapfill/engine.ts +0 -7
  29. package/lib/solvers/rectdiff/gapfill/types.ts +0 -60
  30. package/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +0 -253
@@ -0,0 +1,129 @@
1
+ import {
2
+ BasePipelineSolver,
3
+ definePipelineStep,
4
+ type PipelineStep,
5
+ } from "@tscircuit/solver-utils"
6
+ import type { SimpleRouteJson } from "lib/types/srj-types"
7
+ import type { CapacityMeshNode } from "lib/types/capacity-mesh-types"
8
+ import type { GraphicsObject } from "graphics-debug"
9
+ import { FindSegmentsWithAdjacentEmptySpaceSolver } from "./FindSegmentsWithAdjacentEmptySpaceSolver"
10
+ import { ExpandEdgesToEmptySpaceSolver } from "./ExpandEdgesToEmptySpaceSolver"
11
+
12
+ export class GapFillSolverPipeline extends BasePipelineSolver<{
13
+ meshNodes: CapacityMeshNode[]
14
+ }> {
15
+ findSegmentsWithAdjacentEmptySpaceSolver?: FindSegmentsWithAdjacentEmptySpaceSolver
16
+ expandEdgesToEmptySpaceSolver?: ExpandEdgesToEmptySpaceSolver
17
+
18
+ override pipelineDef: PipelineStep<any>[] = [
19
+ definePipelineStep(
20
+ "findSegmentsWithAdjacentEmptySpaceSolver",
21
+ FindSegmentsWithAdjacentEmptySpaceSolver,
22
+ (gapFillPipeline) => [
23
+ {
24
+ meshNodes: gapFillPipeline.inputProblem.meshNodes,
25
+ },
26
+ ],
27
+ {
28
+ onSolved: () => {
29
+ // Gap fill solver completed
30
+ },
31
+ },
32
+ ),
33
+ definePipelineStep(
34
+ "expandEdgesToEmptySpace",
35
+ ExpandEdgesToEmptySpaceSolver,
36
+ (gapFillPipeline: GapFillSolverPipeline) => [
37
+ {
38
+ inputMeshNodes: gapFillPipeline.inputProblem.meshNodes,
39
+ segmentsWithAdjacentEmptySpace:
40
+ gapFillPipeline.findSegmentsWithAdjacentEmptySpaceSolver!.getOutput()
41
+ .segmentsWithAdjacentEmptySpace,
42
+ },
43
+ ],
44
+ {
45
+ onSolved: () => {
46
+ // Gap fill solver completed
47
+ },
48
+ },
49
+ ),
50
+ ] as const
51
+
52
+ override getOutput(): { outputNodes: CapacityMeshNode[] } {
53
+ const expandedSegments =
54
+ this.expandEdgesToEmptySpaceSolver?.getOutput().expandedSegments ?? []
55
+ const expandedNodes = expandedSegments.map((es) => es.newNode)
56
+
57
+ return {
58
+ outputNodes: [...this.inputProblem.meshNodes, ...expandedNodes],
59
+ }
60
+ }
61
+
62
+ override initialVisualize(): GraphicsObject {
63
+ const graphics: Required<GraphicsObject> = {
64
+ title: "GapFillSolverPipeline - Initial",
65
+ coordinateSystem: "cartesian" as const,
66
+ rects: [],
67
+ points: [],
68
+ lines: [],
69
+ circles: [],
70
+ arrows: [],
71
+ texts: [],
72
+ }
73
+
74
+ for (const node of this.inputProblem.meshNodes) {
75
+ graphics.rects.push({
76
+ center: node.center,
77
+ width: node.width,
78
+ height: node.height,
79
+ stroke: "rgba(0, 0, 0, 0.3)",
80
+ fill: "rgba(100, 100, 100, 0.1)",
81
+ layer: `z${node.availableZ.join(",")}`,
82
+ label: [
83
+ `node ${node.capacityMeshNodeId}`,
84
+ `z:${node.availableZ.join(",")}`,
85
+ ].join("\n"),
86
+ })
87
+ }
88
+
89
+ return graphics
90
+ }
91
+
92
+ override finalVisualize(): GraphicsObject {
93
+ const graphics: Required<GraphicsObject> = {
94
+ title: "GapFillSolverPipeline - Final",
95
+ coordinateSystem: "cartesian" as const,
96
+ rects: [],
97
+ points: [],
98
+ lines: [],
99
+ circles: [],
100
+ arrows: [],
101
+ texts: [],
102
+ }
103
+
104
+ const { outputNodes } = this.getOutput()
105
+ const expandedSegments =
106
+ this.expandEdgesToEmptySpaceSolver?.getOutput().expandedSegments ?? []
107
+ const expandedNodeIds = new Set(
108
+ expandedSegments.map((es) => es.newNode.capacityMeshNodeId),
109
+ )
110
+
111
+ for (const node of outputNodes) {
112
+ const isExpanded = expandedNodeIds.has(node.capacityMeshNodeId)
113
+ graphics.rects.push({
114
+ center: node.center,
115
+ width: node.width,
116
+ height: node.height,
117
+ stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
118
+ fill: isExpanded ? "rgba(0, 200, 0, 0.3)" : "rgba(100, 100, 100, 0.1)",
119
+ layer: `z${node.availableZ.join(",")}`,
120
+ label: [
121
+ `${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
122
+ `z:${node.availableZ.join(",")}`,
123
+ ].join("\n"),
124
+ })
125
+ }
126
+
127
+ return graphics
128
+ }
129
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The four normalized edges of a rect
3
+ */
4
+ export const EDGES = [
5
+ {
6
+ facingDirection: "x-",
7
+ dx: -1,
8
+ dy: 0,
9
+ startX: -0.5,
10
+ startY: 0.5,
11
+ endX: -0.5,
12
+ endY: -0.5,
13
+ },
14
+ {
15
+ facingDirection: "x+",
16
+ dx: 1,
17
+ dy: 0,
18
+ startX: 0.5,
19
+ startY: 0.5,
20
+ endX: 0.5,
21
+ endY: -0.5,
22
+ },
23
+ {
24
+ facingDirection: "y-",
25
+ dx: 0,
26
+ dy: -1,
27
+ startX: -0.5,
28
+ startY: -0.5,
29
+ endX: 0.5,
30
+ endY: -0.5,
31
+ },
32
+ {
33
+ facingDirection: "y+",
34
+ dx: 0,
35
+ dy: 1,
36
+ startX: 0.5,
37
+ startY: 0.5,
38
+ endX: -0.5,
39
+ endY: 0.5,
40
+ },
41
+ ] as const
42
+
43
+ export const EDGE_MAP = {
44
+ "x-": EDGES.find((e) => e.facingDirection === "x-")!,
45
+ "x+": EDGES.find((e) => e.facingDirection === "x+")!,
46
+ "y-": EDGES.find((e) => e.facingDirection === "y-")!,
47
+ "y+": EDGES.find((e) => e.facingDirection === "y+")!,
48
+ } as const
@@ -0,0 +1,10 @@
1
+ export const getBoundsFromCorners = (
2
+ corners: { x: number; y: number }[],
3
+ ): { minX: number; minY: number; maxX: number; maxY: number } => {
4
+ return {
5
+ minX: Math.min(...corners.map((c) => c.x)),
6
+ minY: Math.min(...corners.map((c) => c.y)),
7
+ maxX: Math.max(...corners.map((c) => c.x)),
8
+ maxY: Math.max(...corners.map((c) => c.y)),
9
+ }
10
+ }
@@ -0,0 +1,92 @@
1
+ import type { SegmentWithAdjacentEmptySpace } from "./FindSegmentsWithAdjacentEmptySpaceSolver"
2
+
3
+ const EPS = 1e-4
4
+
5
+ export function projectToUncoveredSegments(
6
+ primaryEdge: SegmentWithAdjacentEmptySpace,
7
+ overlappingEdges: SegmentWithAdjacentEmptySpace[],
8
+ ): Array<SegmentWithAdjacentEmptySpace> {
9
+ const isHorizontal = Math.abs(primaryEdge.start.y - primaryEdge.end.y) < EPS
10
+ const isVertical = Math.abs(primaryEdge.start.x - primaryEdge.end.x) < EPS
11
+ if (!isHorizontal && !isVertical) return []
12
+
13
+ const axis: "x" | "y" = isHorizontal ? "x" : "y"
14
+ const perp: "x" | "y" = isHorizontal ? "y" : "x"
15
+ const lineCoord = primaryEdge.start[perp]
16
+
17
+ const p0 = primaryEdge.start[axis]
18
+ const p1 = primaryEdge.end[axis]
19
+ const pMin = Math.min(p0, p1)
20
+ const pMax = Math.max(p0, p1)
21
+
22
+ const clamp = (v: number) => Math.max(pMin, Math.min(pMax, v))
23
+
24
+ // 1) project each overlapping edge to an interval on the primary edge
25
+ const intervals: Array<{ s: number; e: number }> = []
26
+ for (const e of overlappingEdges) {
27
+ if (e === primaryEdge) continue
28
+
29
+ // only consider edges parallel + colinear (within EPS) with the primary edge
30
+ const eIsHorizontal = Math.abs(e.start.y - e.end.y) < EPS
31
+ const eIsVertical = Math.abs(e.start.x - e.end.x) < EPS
32
+ if (axis === "x" && !eIsHorizontal) continue
33
+ if (axis === "y" && !eIsVertical) continue
34
+ if (Math.abs(e.start[perp] - lineCoord) > EPS) continue
35
+
36
+ const eMin = Math.min(e.start[axis], e.end[axis])
37
+ const eMax = Math.max(e.start[axis], e.end[axis])
38
+
39
+ const s = clamp(eMin)
40
+ const t = clamp(eMax)
41
+ if (t - s > EPS) intervals.push({ s, e: t })
42
+ }
43
+
44
+ if (intervals.length === 0) {
45
+ // nothing covers the primary edge -> entire edge is uncovered
46
+ return [
47
+ {
48
+ ...primaryEdge,
49
+ start: { ...primaryEdge.start },
50
+ end: { ...primaryEdge.end },
51
+ },
52
+ ]
53
+ }
54
+
55
+ // 2) merge cover intervals
56
+ intervals.sort((a, b) => a.s - b.s)
57
+ const merged: Array<{ s: number; e: number }> = []
58
+ for (const it of intervals) {
59
+ const last = merged[merged.length - 1]
60
+ if (!last || it.s > last.e + EPS) merged.push({ ...it })
61
+ else last.e = Math.max(last.e, it.e)
62
+ }
63
+
64
+ // 3) compute uncovered intervals (complement of merged within [pMin,pMax])
65
+ const uncovered: Array<{ s: number; e: number }> = []
66
+ let cursor = pMin
67
+ for (const m of merged) {
68
+ if (m.s > cursor + EPS) uncovered.push({ s: cursor, e: m.s })
69
+ cursor = Math.max(cursor, m.e)
70
+ if (cursor >= pMax - EPS) break
71
+ }
72
+ if (pMax > cursor + EPS) uncovered.push({ s: cursor, e: pMax })
73
+ if (uncovered.length === 0) return []
74
+
75
+ // 4) convert uncovered intervals back to segments on the primary edge
76
+ return uncovered
77
+ .filter((u) => u.e - u.s > EPS)
78
+ .map((u) => {
79
+ const start =
80
+ axis === "x" ? { x: u.s, y: lineCoord } : { x: lineCoord, y: u.s }
81
+ const end =
82
+ axis === "x" ? { x: u.e, y: lineCoord } : { x: lineCoord, y: u.e }
83
+
84
+ return {
85
+ parent: primaryEdge.parent,
86
+ facingDirection: primaryEdge.facingDirection,
87
+ start,
88
+ end,
89
+ z: primaryEdge.z,
90
+ }
91
+ })
92
+ }
@@ -0,0 +1,32 @@
1
+ const OFFSET_DIR_MAP = {
2
+ "x-": {
3
+ x: -1,
4
+ y: 0,
5
+ },
6
+ "x+": {
7
+ x: 1,
8
+ y: 0,
9
+ },
10
+ "y-": {
11
+ x: 0,
12
+ y: -1,
13
+ },
14
+ "y+": {
15
+ x: 0,
16
+ y: 1,
17
+ },
18
+ } as const
19
+ /**
20
+ * Visually offset a line by a given amount in a given direction
21
+ */
22
+ export const visuallyOffsetLine = (
23
+ line: Array<{ x: number; y: number }>,
24
+ dir: "x-" | "x+" | "y-" | "y+",
25
+ amt: number,
26
+ ) => {
27
+ const offset = OFFSET_DIR_MAP[dir]
28
+ return line.map((p) => ({
29
+ x: p.x + offset.x * amt,
30
+ y: p.y + offset.y * amt,
31
+ }))
32
+ }
@@ -14,11 +14,6 @@ import {
14
14
  } from "./rectdiff/engine"
15
15
  import { rectsToMeshNodes } from "./rectdiff/rectsToMeshNodes"
16
16
  import { overlaps } from "./rectdiff/geometry"
17
- import type { GapFillOptions } from "./rectdiff/gapfill/types"
18
- import {
19
- findUncoveredPoints,
20
- calculateCoverage,
21
- } from "./rectdiff/gapfill/engine"
22
17
 
23
18
  /**
24
19
  * A streaming, one-step-per-iteration solver for capacity mesh generation.
@@ -82,34 +77,6 @@ export class RectDiffSolver extends BaseSolver {
82
77
  return { meshNodes: this._meshNodes }
83
78
  }
84
79
 
85
- /** Get coverage percentage (0-1). */
86
- getCoverage(sampleResolution: number = 0.05): number {
87
- return calculateCoverage(
88
- { sampleResolution },
89
- {
90
- bounds: this.state.bounds,
91
- layerCount: this.state.layerCount,
92
- obstaclesByLayer: this.state.obstaclesByLayer,
93
- placedByLayer: this.state.placedByLayer,
94
- },
95
- )
96
- }
97
-
98
- /** Find uncovered points for debugging gaps. */
99
- getUncoveredPoints(
100
- sampleResolution: number = 0.05,
101
- ): Array<{ x: number; y: number; z: number }> {
102
- return findUncoveredPoints(
103
- { sampleResolution },
104
- {
105
- bounds: this.state.bounds,
106
- layerCount: this.state.layerCount,
107
- obstaclesByLayer: this.state.obstaclesByLayer,
108
- placedByLayer: this.state.placedByLayer,
109
- },
110
- )
111
- }
112
-
113
80
  /** Get color based on z layer for visualization. */
114
81
  private getColorForZLayer(zLayers: number[]): {
115
82
  fill: string
@@ -244,6 +211,7 @@ export class RectDiffSolver extends BaseSolver {
244
211
  height: p.rect.height,
245
212
  fill: colors.fill,
246
213
  stroke: colors.stroke,
214
+ layer: `z${p.zLayers.join(",")}`,
247
215
  label: `free\nz:${p.zLayers.join(",")}`,
248
216
  })
249
217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/rectdiff",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -13,14 +13,17 @@
13
13
  "devDependencies": {
14
14
  "@biomejs/biome": "^2.3.5",
15
15
  "@react-hook/resize-observer": "^2.0.2",
16
- "@tscircuit/solver-utils": "^0.0.7",
16
+ "@tscircuit/math-utils": "^0.0.29",
17
+ "@tscircuit/solver-utils": "^0.0.9",
17
18
  "@types/bun": "latest",
19
+ "@types/rbush": "^4.0.0",
18
20
  "@types/react": "^18",
19
21
  "@types/react-dom": "^18",
20
22
  "@types/three": "^0.181.0",
23
+ "@vitejs/plugin-react": "^4",
21
24
  "biome": "^0.3.3",
22
25
  "bun-match-svg": "^0.0.14",
23
- "graphics-debug": "^0.0.70",
26
+ "graphics-debug": "^0.0.76",
24
27
  "rbush": "^4.0.1",
25
28
  "react": "18",
26
29
  "react-cosmos": "^6.2.3",
@@ -28,13 +31,13 @@
28
31
  "react-dom": "18",
29
32
  "three": "^0.181.1",
30
33
  "tsup": "^8.5.1",
31
- "vite": "^6.0.11",
32
- "@vitejs/plugin-react": "^4"
34
+ "vite": "^6.0.11"
33
35
  },
34
36
  "peerDependencies": {
35
37
  "typescript": "^5"
36
38
  },
37
39
  "dependencies": {
38
- "D": "^1.0.0"
40
+ "D": "^1.0.0",
41
+ "flatbush": "^4.5.0"
39
42
  }
40
43
  }
@@ -11,7 +11,7 @@ test("board outline snapshot", async () => {
11
11
  // Run to completion
12
12
  solver.solve()
13
13
 
14
- const viz = solver.visualize()
14
+ const viz = solver.rectDiffSolver!.visualize()
15
15
  const svg = getSvgFromGraphicsObject(viz)
16
16
 
17
17
  await expect(svg).toMatchSvgSnapshot(import.meta.path)
package/tsconfig.json CHANGED
@@ -7,6 +7,10 @@
7
7
  "moduleDetection": "force",
8
8
  "jsx": "react-jsx",
9
9
  "allowJs": true,
10
+ "paths": {
11
+ "lib/*": ["./lib/*"]
12
+ },
13
+ "baseUrl": ".",
10
14
 
11
15
  // Bundler mode
12
16
  "moduleResolution": "bundler",
package/vite.config.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import { defineConfig } from "vite"
2
+ import path from "node:path"
2
3
  import react from "@vitejs/plugin-react"
3
4
 
4
5
  // https://vitejs.dev/config/
5
6
  export default defineConfig({
6
7
  plugins: [react()],
8
+ resolve: {
9
+ alias: {
10
+ lib: path.resolve(__dirname, "lib"),
11
+ },
12
+ },
7
13
  })
@@ -1,28 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts
2
- import { rectsEqual } from "../../../../../utils/rectsEqual"
3
- import { rectsOverlap } from "../../../../../utils/rectsOverlap"
4
- import type { GapRegion } from "../types"
5
-
6
- export function deduplicateGaps(gaps: GapRegion[]): GapRegion[] {
7
- const result: GapRegion[] = []
8
-
9
- for (const gap of gaps) {
10
- // Check if we already have a gap at the same location with overlapping layers
11
- const existing = result.find(
12
- (g) =>
13
- rectsEqual(g.rect, gap.rect) ||
14
- (rectsOverlap(g.rect, gap.rect) &&
15
- gap.zLayers.some((z) => g.zLayers.includes(z))),
16
- )
17
-
18
- if (!existing) {
19
- result.push(gap)
20
- } else if (gap.zLayers.length > existing.zLayers.length) {
21
- // Replace with the one that has more layers
22
- const idx = result.indexOf(existing)
23
- result[idx] = gap
24
- }
25
- }
26
-
27
- return result
28
- }
@@ -1,83 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts
2
- import type { XYRect } from "../../types"
3
- import type { GapRegion, LayerContext } from "../types"
4
- import { EPS } from "../../geometry"
5
- import { findGapsOnLayer } from "./findGapsOnLayer"
6
- import { rectsOverlap } from "../../../../../utils/rectsOverlap"
7
- import { deduplicateGaps } from "./deduplicateGaps"
8
-
9
- /**
10
- * Find gaps across all layers and return GapRegions with z-layer info.
11
- */
12
- export function findAllGaps(
13
- {
14
- scanResolution,
15
- minWidth,
16
- minHeight,
17
- }: {
18
- scanResolution: number
19
- minWidth: number
20
- minHeight: number
21
- },
22
- ctx: LayerContext,
23
- ): GapRegion[] {
24
- const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx
25
-
26
- // Find gaps on each layer
27
- const gapsByLayer: XYRect[][] = []
28
- for (let z = 0; z < layerCount; z++) {
29
- const obstacles = obstaclesByLayer[z] ?? []
30
- const placed = placedByLayer[z] ?? []
31
- const gaps = findGapsOnLayer({ bounds, obstacles, placed, scanResolution })
32
- gapsByLayer.push(gaps)
33
- }
34
-
35
- // Convert to GapRegions with z-layer info
36
- const allGaps: GapRegion[] = []
37
-
38
- for (let z = 0; z < layerCount; z++) {
39
- for (const gap of gapsByLayer[z]!) {
40
- // Filter out gaps that are too small
41
- if (gap.width < minWidth - EPS || gap.height < minHeight - EPS) continue
42
-
43
- // Check if this gap exists on adjacent layers too
44
- const zLayers = [z]
45
-
46
- // Look up
47
- for (let zu = z + 1; zu < layerCount; zu++) {
48
- const hasOverlap = gapsByLayer[zu]!.some((g) => rectsOverlap(g, gap))
49
- if (hasOverlap) zLayers.push(zu)
50
- else break
51
- }
52
-
53
- // Look down (if z > 0 and not already counted)
54
- for (let zd = z - 1; zd >= 0; zd--) {
55
- const hasOverlap = gapsByLayer[zd]!.some((g) => rectsOverlap(g, gap))
56
- if (hasOverlap && !zLayers.includes(zd)) zLayers.unshift(zd)
57
- else break
58
- }
59
-
60
- allGaps.push({
61
- rect: gap,
62
- zLayers: zLayers.sort((a, b) => a - b),
63
- centerX: gap.x + gap.width / 2,
64
- centerY: gap.y + gap.height / 2,
65
- area: gap.width * gap.height,
66
- })
67
- }
68
- }
69
-
70
- // Deduplicate gaps that are essentially the same across layers
71
- const deduped = deduplicateGaps(allGaps)
72
-
73
- // Sort by priority: prefer larger gaps and multi-layer gaps
74
- deduped.sort((a, b) => {
75
- // Prefer multi-layer gaps
76
- const layerDiff = b.zLayers.length - a.zLayers.length
77
- if (layerDiff !== 0) return layerDiff
78
- // Then prefer larger area
79
- return b.area - a.area
80
- })
81
-
82
- return deduped
83
- }
@@ -1,100 +0,0 @@
1
- // lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts
2
- import type { XYRect } from "../../types"
3
- import { EPS } from "../../geometry"
4
-
5
- import { mergeUncoveredCells } from "./mergeUncoveredCells"
6
-
7
- /**
8
- * Sweep-line algorithm to find maximal uncovered rectangles on a single layer.
9
- */
10
- export function findGapsOnLayer({
11
- bounds,
12
- obstacles,
13
- placed,
14
- scanResolution,
15
- }: {
16
- bounds: XYRect
17
- obstacles: XYRect[]
18
- placed: XYRect[]
19
- scanResolution: number
20
- }): XYRect[] {
21
- const blockers = [...obstacles, ...placed]
22
-
23
- // Collect all unique x-coordinates
24
- const xCoords = new Set<number>()
25
- xCoords.add(bounds.x)
26
- xCoords.add(bounds.x + bounds.width)
27
-
28
- for (const b of blockers) {
29
- if (b.x > bounds.x && b.x < bounds.x + bounds.width) {
30
- xCoords.add(b.x)
31
- }
32
- if (b.x + b.width > bounds.x && b.x + b.width < bounds.x + bounds.width) {
33
- xCoords.add(b.x + b.width)
34
- }
35
- }
36
-
37
- // Also add intermediate points based on scan resolution
38
- for (let x = bounds.x; x <= bounds.x + bounds.width; x += scanResolution) {
39
- xCoords.add(x)
40
- }
41
-
42
- const sortedX = Array.from(xCoords).sort((a, b) => a - b)
43
-
44
- // Similarly for y-coordinates
45
- const yCoords = new Set<number>()
46
- yCoords.add(bounds.y)
47
- yCoords.add(bounds.y + bounds.height)
48
-
49
- for (const b of blockers) {
50
- if (b.y > bounds.y && b.y < bounds.y + bounds.height) {
51
- yCoords.add(b.y)
52
- }
53
- if (
54
- b.y + b.height > bounds.y &&
55
- b.y + b.height < bounds.y + bounds.height
56
- ) {
57
- yCoords.add(b.y + b.height)
58
- }
59
- }
60
-
61
- for (let y = bounds.y; y <= bounds.y + bounds.height; y += scanResolution) {
62
- yCoords.add(y)
63
- }
64
-
65
- const sortedY = Array.from(yCoords).sort((a, b) => a - b)
66
-
67
- // Build a grid of cells and mark which are uncovered
68
- const uncoveredCells: Array<{ x: number; y: number; w: number; h: number }> =
69
- []
70
-
71
- for (let i = 0; i < sortedX.length - 1; i++) {
72
- for (let j = 0; j < sortedY.length - 1; j++) {
73
- const cellX = sortedX[i]!
74
- const cellY = sortedY[j]!
75
- const cellW = sortedX[i + 1]! - cellX
76
- const cellH = sortedY[j + 1]! - cellY
77
-
78
- if (cellW <= EPS || cellH <= EPS) continue
79
-
80
- // Check if this cell is covered by any blocker
81
- const cellCenterX = cellX + cellW / 2
82
- const cellCenterY = cellY + cellH / 2
83
-
84
- const isCovered = blockers.some(
85
- (b) =>
86
- cellCenterX >= b.x - EPS &&
87
- cellCenterX <= b.x + b.width + EPS &&
88
- cellCenterY >= b.y - EPS &&
89
- cellCenterY <= b.y + b.height + EPS,
90
- )
91
-
92
- if (!isCovered) {
93
- uncoveredCells.push({ x: cellX, y: cellY, w: cellW, h: cellH })
94
- }
95
- }
96
- }
97
-
98
- // Merge adjacent uncovered cells into maximal rectangles
99
- return mergeUncoveredCells(uncoveredCells)
100
- }