@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.
- package/dist/index.d.ts +97 -12
- package/dist/index.js +714 -81
- package/lib/RectDiffPipeline.ts +79 -13
- package/lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts +284 -0
- package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +213 -0
- package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +129 -0
- package/lib/solvers/GapFillSolver/edge-constants.ts +48 -0
- package/lib/solvers/GapFillSolver/getBoundsFromCorners.ts +10 -0
- package/lib/solvers/GapFillSolver/projectToUncoveredSegments.ts +92 -0
- package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +32 -0
- package/lib/solvers/RectDiffSolver.ts +1 -33
- package/package.json +9 -6
- package/tests/board-outline.test.ts +1 -1
- package/tsconfig.json +4 -0
- package/vite.config.ts +6 -0
- 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,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.
|
|
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/
|
|
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.
|
|
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
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
|
-
}
|