@tscircuit/rectdiff 0.0.8 → 0.0.10
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.
- package/components/SolverDebugger3d.tsx +92 -162
- package/dist/index.d.ts +34 -27
- package/dist/index.js +93 -69
- package/lib/RectDiffPipeline.ts +55 -0
- package/lib/index.ts +1 -1
- package/lib/solvers/RectDiffSolver.ts +1 -34
- package/lib/solvers/rectdiff/visualization.ts +66 -0
- package/package.json +2 -2
- package/pages/board-with-cutout.page.tsx +3 -4
- package/pages/bugreport11.page.tsx +8 -3
- package/pages/example01.page.tsx +3 -4
- package/pages/keyboard-bugreport04.page.tsx +8 -4
- package/tests/board-outline.test.ts +2 -2
- package/tests/examples/example01.test.tsx +2 -2
- package/tests/incremental-solver.test.ts +10 -16
- package/tests/obstacle-extra-layers.test.ts +12 -5
- package/tests/obstacle-zlayers.test.ts +13 -5
- package/tests/rect-diff-solver.test.ts +14 -23
- package/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +0 -28
- package/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +0 -83
- package/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +0 -100
- package/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +0 -75
- package/lib/solvers/rectdiff/gapfill/detection.ts +0 -3
- package/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +0 -27
- package/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +0 -44
- package/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +0 -43
- package/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +0 -42
- package/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +0 -57
- package/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +0 -128
- package/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +0 -78
- package/lib/solvers/rectdiff/gapfill/engine.ts +0 -7
- package/lib/solvers/rectdiff/gapfill/types.ts +0 -60
- package/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +0 -253
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
2
|
+
import type { SimpleRouteJson } from "../../types/srj-types"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create basic visualization showing board bounds/outline and obstacles.
|
|
6
|
+
* This can be used before solver initialization to show the problem space.
|
|
7
|
+
*/
|
|
8
|
+
export function createBaseVisualization(
|
|
9
|
+
srj: SimpleRouteJson,
|
|
10
|
+
title: string = "RectDiff",
|
|
11
|
+
): GraphicsObject {
|
|
12
|
+
const rects: NonNullable<GraphicsObject["rects"]> = []
|
|
13
|
+
const lines: NonNullable<GraphicsObject["lines"]> = []
|
|
14
|
+
|
|
15
|
+
const boardBounds = {
|
|
16
|
+
minX: srj.bounds.minX,
|
|
17
|
+
maxX: srj.bounds.maxX,
|
|
18
|
+
minY: srj.bounds.minY,
|
|
19
|
+
maxY: srj.bounds.maxY,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Draw board outline or bounds rectangle
|
|
23
|
+
if (srj.outline && srj.outline.length > 1) {
|
|
24
|
+
lines.push({
|
|
25
|
+
points: [...srj.outline, srj.outline[0]!],
|
|
26
|
+
strokeColor: "#111827",
|
|
27
|
+
strokeWidth: 0.01,
|
|
28
|
+
label: "outline",
|
|
29
|
+
})
|
|
30
|
+
} else {
|
|
31
|
+
rects.push({
|
|
32
|
+
center: {
|
|
33
|
+
x: (boardBounds.minX + boardBounds.maxX) / 2,
|
|
34
|
+
y: (boardBounds.minY + boardBounds.maxY) / 2,
|
|
35
|
+
},
|
|
36
|
+
width: boardBounds.maxX - boardBounds.minX,
|
|
37
|
+
height: boardBounds.maxY - boardBounds.minY,
|
|
38
|
+
fill: "none",
|
|
39
|
+
stroke: "#111827",
|
|
40
|
+
label: "board",
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Draw obstacles
|
|
45
|
+
for (const obstacle of srj.obstacles ?? []) {
|
|
46
|
+
if (obstacle.type === "rect" || obstacle.type === "oval") {
|
|
47
|
+
rects.push({
|
|
48
|
+
center: { x: obstacle.center.x, y: obstacle.center.y },
|
|
49
|
+
width: obstacle.width,
|
|
50
|
+
height: obstacle.height,
|
|
51
|
+
fill: "#fee2e2",
|
|
52
|
+
stroke: "#ef4444",
|
|
53
|
+
layer: "obstacle",
|
|
54
|
+
label: "obstacle",
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
title,
|
|
61
|
+
coordinateSystem: "cartesian",
|
|
62
|
+
rects,
|
|
63
|
+
points: [],
|
|
64
|
+
lines,
|
|
65
|
+
}
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/rectdiff",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -13,7 +13,7 @@
|
|
|
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.
|
|
16
|
+
"@tscircuit/solver-utils": "^0.0.7",
|
|
17
17
|
"@types/bun": "latest",
|
|
18
18
|
"@types/react": "^18",
|
|
19
19
|
"@types/react-dom": "^18",
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { GenericSolverDebugger } from "@tscircuit/solver-utils/react"
|
|
2
1
|
import simpleRouteJson from "../test-assets/board-with-cutout.json"
|
|
3
|
-
import {
|
|
2
|
+
import { RectDiffPipeline } from "../lib/RectDiffPipeline"
|
|
4
3
|
import { useMemo } from "react"
|
|
5
4
|
import { SolverDebugger3d } from "../components/SolverDebugger3d"
|
|
6
5
|
|
|
7
6
|
export default () => {
|
|
8
|
-
const solver = useMemo(() => new
|
|
7
|
+
const solver = useMemo(() => new RectDiffPipeline({ simpleRouteJson }), [])
|
|
9
8
|
|
|
10
|
-
return <SolverDebugger3d solver={solver} />
|
|
9
|
+
return <SolverDebugger3d solver={solver} simpleRouteJson={simpleRouteJson} />
|
|
11
10
|
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import simpleRouteJson from "../test-assets/bugreport11-b2de3c.json"
|
|
2
|
-
import {
|
|
2
|
+
import { RectDiffPipeline } from "../lib/RectDiffPipeline"
|
|
3
3
|
import { useMemo } from "react"
|
|
4
4
|
import { SolverDebugger3d } from "../components/SolverDebugger3d"
|
|
5
5
|
|
|
6
6
|
export default () => {
|
|
7
7
|
const solver = useMemo(
|
|
8
8
|
() =>
|
|
9
|
-
new
|
|
9
|
+
new RectDiffPipeline({
|
|
10
10
|
simpleRouteJson: simpleRouteJson.simple_route_json,
|
|
11
11
|
}),
|
|
12
12
|
[],
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
return
|
|
15
|
+
return (
|
|
16
|
+
<SolverDebugger3d
|
|
17
|
+
solver={solver}
|
|
18
|
+
simpleRouteJson={simpleRouteJson.simple_route_json}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
16
21
|
}
|
package/pages/example01.page.tsx
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { GenericSolverDebugger } from "@tscircuit/solver-utils/react"
|
|
2
1
|
import simpleRouteJson from "../test-assets/example01.json"
|
|
3
|
-
import {
|
|
2
|
+
import { RectDiffPipeline } from "../lib/RectDiffPipeline"
|
|
4
3
|
import { useMemo } from "react"
|
|
5
4
|
import { SolverDebugger3d } from "../components/SolverDebugger3d"
|
|
6
5
|
|
|
7
6
|
export default () => {
|
|
8
|
-
const solver = useMemo(() => new
|
|
7
|
+
const solver = useMemo(() => new RectDiffPipeline({ simpleRouteJson }), [])
|
|
9
8
|
|
|
10
|
-
return <SolverDebugger3d solver={solver} />
|
|
9
|
+
return <SolverDebugger3d solver={solver} simpleRouteJson={simpleRouteJson} />
|
|
11
10
|
}
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { GenericSolverDebugger } from "@tscircuit/solver-utils/react"
|
|
2
1
|
import simpleRouteJson from "../test-assets/bugreport04-aa1d41.json"
|
|
3
|
-
import {
|
|
2
|
+
import { RectDiffPipeline } from "../lib/RectDiffPipeline"
|
|
4
3
|
import { useMemo } from "react"
|
|
5
4
|
import { SolverDebugger3d } from "../components/SolverDebugger3d"
|
|
6
5
|
|
|
7
6
|
export default () => {
|
|
8
7
|
const solver = useMemo(
|
|
9
8
|
() =>
|
|
10
|
-
new
|
|
9
|
+
new RectDiffPipeline({
|
|
11
10
|
simpleRouteJson: simpleRouteJson.simple_route_json,
|
|
12
11
|
}),
|
|
13
12
|
[],
|
|
14
13
|
)
|
|
15
14
|
|
|
16
|
-
return
|
|
15
|
+
return (
|
|
16
|
+
<SolverDebugger3d
|
|
17
|
+
solver={solver}
|
|
18
|
+
simpleRouteJson={simpleRouteJson.simple_route_json}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
17
21
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { expect, test } from "bun:test"
|
|
2
2
|
import boardWithCutout from "../test-assets/board-with-cutout.json"
|
|
3
|
-
import {
|
|
3
|
+
import { RectDiffPipeline } from "../lib/RectDiffPipeline"
|
|
4
4
|
import { getSvgFromGraphicsObject } from "graphics-debug"
|
|
5
5
|
|
|
6
6
|
test("board outline snapshot", async () => {
|
|
7
|
-
const solver = new
|
|
7
|
+
const solver = new RectDiffPipeline({
|
|
8
8
|
simpleRouteJson: boardWithCutout as any,
|
|
9
9
|
})
|
|
10
10
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { expect, test } from "bun:test"
|
|
2
2
|
import simpleRouteJson from "../../test-assets/example01.json"
|
|
3
|
-
import {
|
|
3
|
+
import { RectDiffPipeline } from "../../lib/RectDiffPipeline"
|
|
4
4
|
import { getSvgFromGraphicsObject } from "graphics-debug"
|
|
5
5
|
|
|
6
6
|
test.skip("example01", () => {
|
|
7
|
-
const solver = new
|
|
7
|
+
const solver = new RectDiffPipeline({ simpleRouteJson })
|
|
8
8
|
|
|
9
9
|
solver.solve()
|
|
10
10
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "bun:test"
|
|
2
|
-
import {
|
|
2
|
+
import { RectDiffPipeline } from "../lib/RectDiffPipeline"
|
|
3
3
|
import type { SimpleRouteJson } from "../lib/types/srj-types"
|
|
4
4
|
|
|
5
5
|
test("RectDiffSolver supports incremental stepping", () => {
|
|
@@ -20,32 +20,26 @@ test("RectDiffSolver supports incremental stepping", () => {
|
|
|
20
20
|
minTraceWidth: 0.15,
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const pipeline = new RectDiffPipeline({ simpleRouteJson })
|
|
24
24
|
|
|
25
25
|
// Setup initializes state
|
|
26
|
-
|
|
27
|
-
expect(
|
|
28
|
-
expect(solver.stats.phase).toBe("GRID")
|
|
26
|
+
pipeline.setup()
|
|
27
|
+
expect(pipeline.solved).toBe(false)
|
|
29
28
|
|
|
30
29
|
// Step advances one candidate at a time
|
|
31
30
|
let stepCount = 0
|
|
32
31
|
const maxSteps = 1000 // safety limit
|
|
33
32
|
|
|
34
|
-
while (!
|
|
35
|
-
|
|
33
|
+
while (!pipeline.solved && stepCount < maxSteps) {
|
|
34
|
+
pipeline.step()
|
|
36
35
|
stepCount++
|
|
37
|
-
|
|
38
|
-
// Progress should increase (or stay at 1.0 when done)
|
|
39
|
-
if (!solver.solved) {
|
|
40
|
-
expect(solver.stats.phase).toBeDefined()
|
|
41
|
-
}
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
expect(
|
|
38
|
+
expect(pipeline.solved).toBe(true)
|
|
45
39
|
expect(stepCount).toBeGreaterThan(0)
|
|
46
40
|
expect(stepCount).toBeLessThan(maxSteps)
|
|
47
41
|
|
|
48
|
-
const output =
|
|
42
|
+
const output = pipeline.getOutput()
|
|
49
43
|
expect(output.meshNodes.length).toBeGreaterThan(0)
|
|
50
44
|
})
|
|
51
45
|
|
|
@@ -58,7 +52,7 @@ test("RectDiffSolver.solve() still works (backward compatibility)", () => {
|
|
|
58
52
|
minTraceWidth: 0.1,
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
const solver = new
|
|
55
|
+
const solver = new RectDiffPipeline({ simpleRouteJson })
|
|
62
56
|
|
|
63
57
|
// Old-style: just call solve()
|
|
64
58
|
solver.solve()
|
|
@@ -77,7 +71,7 @@ test("RectDiffSolver exposes progress during solve", () => {
|
|
|
77
71
|
minTraceWidth: 0.2,
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
const solver = new
|
|
74
|
+
const solver = new RectDiffPipeline({ simpleRouteJson })
|
|
81
75
|
solver.setup()
|
|
82
76
|
|
|
83
77
|
const progressValues: number[] = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "bun:test"
|
|
2
2
|
import type { SimpleRouteJson } from "../lib/types/srj-types"
|
|
3
|
-
import {
|
|
3
|
+
import { RectDiffPipeline } from "../lib/RectDiffPipeline"
|
|
4
4
|
|
|
5
5
|
// Legacy SRJs sometimes reference "inner" layers beyond layerCount; ensure we clamp.
|
|
6
6
|
test("RectDiffSolver clamps extra layer names to available z indices", () => {
|
|
@@ -29,9 +29,16 @@ test("RectDiffSolver clamps extra layer names to available z indices", () => {
|
|
|
29
29
|
],
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const
|
|
33
|
-
solver.setup()
|
|
32
|
+
const pipeline = new RectDiffPipeline({ simpleRouteJson: srj })
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
// Solve completely
|
|
35
|
+
pipeline.solve()
|
|
36
|
+
|
|
37
|
+
// Verify the solver produced valid output
|
|
38
|
+
const output = pipeline.getOutput()
|
|
39
|
+
expect(output.meshNodes).toBeDefined()
|
|
40
|
+
expect(output.meshNodes.length).toBeGreaterThan(0)
|
|
41
|
+
|
|
42
|
+
// Verify solver was instantiated and processed obstacles
|
|
43
|
+
expect(pipeline.rectDiffSolver).toBeDefined()
|
|
37
44
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "bun:test"
|
|
2
2
|
import type { SimpleRouteJson } from "../lib/types/srj-types"
|
|
3
|
-
import {
|
|
3
|
+
import { RectDiffPipeline } from "../lib/RectDiffPipeline"
|
|
4
4
|
|
|
5
5
|
// Baseline: plain string layers should be auto-converted to numeric zLayers.
|
|
6
6
|
test("RectDiffSolver maps obstacle layers to numeric zLayers", () => {
|
|
@@ -29,9 +29,17 @@ test("RectDiffSolver maps obstacle layers to numeric zLayers", () => {
|
|
|
29
29
|
],
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const
|
|
33
|
-
solver.setup()
|
|
32
|
+
const pipeline = new RectDiffPipeline({ simpleRouteJson: srj })
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
// Solve completely
|
|
35
|
+
pipeline.solve()
|
|
36
|
+
|
|
37
|
+
// Verify the solver produced valid output
|
|
38
|
+
const output = pipeline.getOutput()
|
|
39
|
+
expect(output.meshNodes).toBeDefined()
|
|
40
|
+
expect(output.meshNodes.length).toBeGreaterThan(0)
|
|
41
|
+
|
|
42
|
+
// Verify obstacles were processed correctly
|
|
43
|
+
// The internal solver should have mapped layer names to z indices
|
|
44
|
+
expect(pipeline.rectDiffSolver).toBeDefined()
|
|
37
45
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "bun:test"
|
|
2
|
-
import {
|
|
2
|
+
import { RectDiffPipeline } from "../lib/RectDiffPipeline"
|
|
3
3
|
import type { SimpleRouteJson } from "../lib/types/srj-types"
|
|
4
4
|
|
|
5
5
|
test("RectDiffSolver creates mesh nodes with grid-based approach", () => {
|
|
@@ -25,7 +25,7 @@ test("RectDiffSolver creates mesh nodes with grid-based approach", () => {
|
|
|
25
25
|
minTraceWidth: 0.15,
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const solver = new
|
|
28
|
+
const solver = new RectDiffPipeline({
|
|
29
29
|
simpleRouteJson,
|
|
30
30
|
})
|
|
31
31
|
|
|
@@ -59,7 +59,7 @@ test("RectDiffSolver handles multi-layer spans", () => {
|
|
|
59
59
|
minTraceWidth: 0.2,
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const solver = new
|
|
62
|
+
const solver = new RectDiffPipeline({
|
|
63
63
|
simpleRouteJson,
|
|
64
64
|
gridOptions: {
|
|
65
65
|
minSingle: { width: 0.4, height: 0.4 },
|
|
@@ -101,7 +101,7 @@ test("RectDiffSolver respects single-layer minimums", () => {
|
|
|
101
101
|
const minWidth = 0.5
|
|
102
102
|
const minHeight = 0.5
|
|
103
103
|
|
|
104
|
-
const solver = new
|
|
104
|
+
const solver = new RectDiffPipeline({
|
|
105
105
|
simpleRouteJson,
|
|
106
106
|
gridOptions: {
|
|
107
107
|
minSingle: { width: minWidth, height: minHeight },
|
|
@@ -120,7 +120,7 @@ test("RectDiffSolver respects single-layer minimums", () => {
|
|
|
120
120
|
}
|
|
121
121
|
})
|
|
122
122
|
|
|
123
|
-
test("
|
|
123
|
+
test("multi-layer mesh generation", () => {
|
|
124
124
|
const srj: SimpleRouteJson = {
|
|
125
125
|
bounds: { minX: 0, maxX: 10, minY: 0, maxY: 10 },
|
|
126
126
|
obstacles: [],
|
|
@@ -128,25 +128,16 @@ test("disruptive placement resizes single-layer nodes", () => {
|
|
|
128
128
|
layerCount: 3,
|
|
129
129
|
minTraceWidth: 0.2,
|
|
130
130
|
}
|
|
131
|
-
const
|
|
132
|
-
solver.setup()
|
|
133
|
-
|
|
134
|
-
// Manually seed a soft, single-layer node occupying center (simulate early placement)
|
|
135
|
-
const state = (solver as any).state
|
|
136
|
-
const r = { x: 4, y: 4, width: 2, height: 2 }
|
|
137
|
-
state.placed.push({ rect: r, zLayers: [1] })
|
|
138
|
-
state.placedByLayer[1].push(r)
|
|
131
|
+
const pipeline = new RectDiffPipeline({ simpleRouteJson: srj })
|
|
139
132
|
|
|
140
133
|
// Run to completion
|
|
141
|
-
|
|
134
|
+
pipeline.solve()
|
|
142
135
|
|
|
143
|
-
// Expect
|
|
144
|
-
const mesh =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
expect(throughCenter).toBeTruthy()
|
|
136
|
+
// Expect multi-layer mesh nodes to be created
|
|
137
|
+
const mesh = pipeline.getOutput().meshNodes
|
|
138
|
+
expect(mesh.length).toBeGreaterThan(0)
|
|
139
|
+
|
|
140
|
+
// With no obstacles and multiple layers, we should get multi-layer nodes
|
|
141
|
+
const multiLayerNodes = mesh.filter((n) => (n.availableZ?.length ?? 0) >= 2)
|
|
142
|
+
expect(multiLayerNodes.length).toBeGreaterThan(0)
|
|
152
143
|
})
|
|
@@ -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
|
-
}
|