@tscircuit/rectdiff 0.0.21 → 0.0.23
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 +2 -2
- package/dist/index.d.ts +23 -3
- package/dist/index.js +236 -60
- package/lib/RectDiffPipeline.ts +62 -22
- package/lib/fixtures/twoNodeExpansionFixture.ts +10 -2
- package/lib/rectdiff-visualization.ts +2 -1
- package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +8 -3
- package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +48 -9
- package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +14 -6
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +41 -5
- package/lib/solvers/RectDiffSeedingSolver/computeInverseRects.ts +37 -1
- package/lib/solvers/RectDiffSeedingSolver/layers.ts +9 -5
- package/lib/utils/expandRectFromSeed.ts +11 -5
- package/lib/utils/finalizeRects.ts +17 -9
- package/lib/utils/padRect.ts +11 -0
- package/lib/utils/renderObstacleClearance.ts +50 -0
- package/package.json +1 -1
- package/pages/bugreport11.page.tsx +1 -0
- package/test-assets/bugreport-c7537683-stalling.json +1107 -0
- package/tests/board-outline.test.ts +1 -1
- package/tests/bugreport-stalling.test.ts +102 -0
- package/tests/fixtures/makeSimpleRouteOutlineGraphics.ts +5 -1
- package/tests/should-expand-node.test.ts +9 -1
- package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +2 -2
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c-clearance.snap.svg +44 -0
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +2 -2
- package/tests/solver/bugreport11-b2de3c/bugreport11-b2de3c-clearance.test.ts +97 -0
- package/tests/solver/bugreport26-66b0b2/__snapshots__/bugreport26-66b0b2.snap.svg +2 -2
- package/tests/solver/bugreport27-dd3734/__snapshots__/bugreport27-dd3734.snap.svg +2 -2
- package/tests/solver/bugreport28-18a9ef/__snapshots__/bugreport28-18a9ef.snap.svg +2 -2
- package/tests/solver/bugreport29-7deae8/__snapshots__/bugreport29-7deae8.snap.svg +2 -2
- package/tests/solver/bugreport30-2174c8/__snapshots__/bugreport30-2174c8.snap.svg +2 -2
- package/tests/solver/bugreport33-213d45/__snapshots__/bugreport33-213d45.snap.svg +2 -2
- package/tests/solver/bugreport34-e9dea2/__snapshots__/bugreport34-e9dea2.snap.svg +2 -2
- package/tests/solver/bugreport35-191db9/__snapshots__/bugreport35-191db9.snap.svg +2 -2
- package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
package/lib/RectDiffPipeline.ts
CHANGED
|
@@ -11,16 +11,22 @@ import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipe
|
|
|
11
11
|
import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
|
|
12
12
|
import { createBaseVisualization } from "./rectdiff-visualization"
|
|
13
13
|
import { computeInverseRects } from "./solvers/RectDiffSeedingSolver/computeInverseRects"
|
|
14
|
+
import { buildZIndexMap } from "./solvers/RectDiffSeedingSolver/layers"
|
|
15
|
+
import { buildObstacleClearanceGraphics } from "./utils/renderObstacleClearance"
|
|
16
|
+
import { mergeGraphics } from "graphics-debug"
|
|
14
17
|
|
|
15
18
|
export interface RectDiffPipelineInput {
|
|
16
19
|
simpleRouteJson: SimpleRouteJson
|
|
17
20
|
gridOptions?: Partial<GridFill3DOptions>
|
|
21
|
+
obstacleClearance?: number
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
|
|
21
25
|
rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
|
|
22
26
|
gapFillSolver?: GapFillSolverPipeline
|
|
23
27
|
boardVoidRects: XYRect[] | undefined
|
|
28
|
+
zIndexByName?: Map<string, number>
|
|
29
|
+
layerNames?: string[]
|
|
24
30
|
|
|
25
31
|
override pipelineDef: PipelineStep<any>[] = [
|
|
26
32
|
definePipelineStep(
|
|
@@ -28,9 +34,21 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
28
34
|
RectDiffGridSolverPipeline,
|
|
29
35
|
(rectDiffPipeline: RectDiffPipeline) => [
|
|
30
36
|
{
|
|
31
|
-
|
|
37
|
+
bounds: rectDiffPipeline.inputProblem.simpleRouteJson.bounds,
|
|
38
|
+
obstacles: rectDiffPipeline.inputProblem.simpleRouteJson.obstacles,
|
|
39
|
+
connections:
|
|
40
|
+
rectDiffPipeline.inputProblem.simpleRouteJson.connections,
|
|
41
|
+
outline: rectDiffPipeline.inputProblem.simpleRouteJson.outline
|
|
42
|
+
? { outline: rectDiffPipeline.inputProblem.simpleRouteJson.outline }
|
|
43
|
+
: undefined,
|
|
44
|
+
layerCount: rectDiffPipeline.inputProblem.simpleRouteJson.layerCount,
|
|
32
45
|
gridOptions: rectDiffPipeline.inputProblem.gridOptions,
|
|
33
46
|
boardVoidRects: rectDiffPipeline.boardVoidRects,
|
|
47
|
+
layerNames: rectDiffPipeline.layerNames,
|
|
48
|
+
zIndexByName: rectDiffPipeline.zIndexByName,
|
|
49
|
+
minTraceWidth:
|
|
50
|
+
rectDiffPipeline.inputProblem.simpleRouteJson.minTraceWidth,
|
|
51
|
+
obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance,
|
|
34
52
|
},
|
|
35
53
|
],
|
|
36
54
|
),
|
|
@@ -53,6 +71,12 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
53
71
|
]
|
|
54
72
|
|
|
55
73
|
override _setup(): void {
|
|
74
|
+
const { zIndexByName, layerNames } = buildZIndexMap({
|
|
75
|
+
obstacles: this.inputProblem.simpleRouteJson.obstacles,
|
|
76
|
+
layerCount: this.inputProblem.simpleRouteJson.layerCount,
|
|
77
|
+
})
|
|
78
|
+
this.zIndexByName = zIndexByName
|
|
79
|
+
this.layerNames = layerNames
|
|
56
80
|
if (this.inputProblem.simpleRouteJson.outline) {
|
|
57
81
|
this.boardVoidRects = computeInverseRects(
|
|
58
82
|
{
|
|
@@ -86,17 +110,23 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
86
110
|
}
|
|
87
111
|
|
|
88
112
|
override initialVisualize(): GraphicsObject {
|
|
89
|
-
const
|
|
113
|
+
const base = createBaseVisualization(
|
|
90
114
|
this.inputProblem.simpleRouteJson,
|
|
91
115
|
"RectDiffPipeline - Initial",
|
|
92
116
|
)
|
|
117
|
+
const clearance = buildObstacleClearanceGraphics({
|
|
118
|
+
srj: this.inputProblem.simpleRouteJson,
|
|
119
|
+
clearance: this.inputProblem.obstacleClearance,
|
|
120
|
+
})
|
|
93
121
|
|
|
94
122
|
// Show initial mesh nodes from grid pipeline if available
|
|
95
123
|
const initialNodes =
|
|
96
124
|
this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []
|
|
97
125
|
|
|
98
|
-
|
|
99
|
-
|
|
126
|
+
const nodeRects: GraphicsObject = {
|
|
127
|
+
title: "Initial Nodes",
|
|
128
|
+
coordinateSystem: "cartesian",
|
|
129
|
+
rects: initialNodes.map((node) => ({
|
|
100
130
|
center: node.center,
|
|
101
131
|
width: node.width,
|
|
102
132
|
height: node.height,
|
|
@@ -107,17 +137,21 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
107
137
|
`node ${node.capacityMeshNodeId}`,
|
|
108
138
|
`z:${node.availableZ.join(",")}`,
|
|
109
139
|
].join("\n"),
|
|
110
|
-
})
|
|
140
|
+
})),
|
|
111
141
|
}
|
|
112
142
|
|
|
113
|
-
return
|
|
143
|
+
return mergeGraphics(mergeGraphics(base, clearance), nodeRects)
|
|
114
144
|
}
|
|
115
145
|
|
|
116
146
|
override finalVisualize(): GraphicsObject {
|
|
117
|
-
const
|
|
147
|
+
const base = createBaseVisualization(
|
|
118
148
|
this.inputProblem.simpleRouteJson,
|
|
119
149
|
"RectDiffPipeline - Final",
|
|
120
150
|
)
|
|
151
|
+
const clearance = buildObstacleClearanceGraphics({
|
|
152
|
+
srj: this.inputProblem.simpleRouteJson,
|
|
153
|
+
clearance: this.inputProblem.obstacleClearance,
|
|
154
|
+
})
|
|
121
155
|
|
|
122
156
|
const { meshNodes: outputNodes } = this.getOutput()
|
|
123
157
|
const initialNodeIds = new Set(
|
|
@@ -126,22 +160,28 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
126
160
|
),
|
|
127
161
|
)
|
|
128
162
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
163
|
+
const nodeRects: GraphicsObject = {
|
|
164
|
+
title: "Final Nodes",
|
|
165
|
+
coordinateSystem: "cartesian",
|
|
166
|
+
rects: outputNodes.map((node) => {
|
|
167
|
+
const isExpanded = !initialNodeIds.has(node.capacityMeshNodeId)
|
|
168
|
+
return {
|
|
169
|
+
center: node.center,
|
|
170
|
+
width: node.width,
|
|
171
|
+
height: node.height,
|
|
172
|
+
stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
|
|
173
|
+
fill: isExpanded
|
|
174
|
+
? "rgba(0, 200, 0, 0.3)"
|
|
175
|
+
: "rgba(100, 100, 100, 0.1)",
|
|
176
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
177
|
+
label: [
|
|
178
|
+
`${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
|
|
179
|
+
`z:${node.availableZ.join(",")}`,
|
|
180
|
+
].join("\n"),
|
|
181
|
+
}
|
|
182
|
+
}),
|
|
143
183
|
}
|
|
144
184
|
|
|
145
|
-
return
|
|
185
|
+
return mergeGraphics(mergeGraphics(base, clearance), nodeRects)
|
|
146
186
|
}
|
|
147
187
|
}
|
|
@@ -3,6 +3,7 @@ import type { RectDiffExpansionSolverInput } from "../solvers/RectDiffExpansionS
|
|
|
3
3
|
import type { SimpleRouteJson } from "../types/srj-types"
|
|
4
4
|
import type { XYRect } from "../rectdiff-types"
|
|
5
5
|
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
6
|
+
import { buildZIndexMap } from "../solvers/RectDiffSeedingSolver/layers"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Builds a minimal RectDiffExpansionSolver snapshot with exactly two nodes
|
|
@@ -31,9 +32,13 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => {
|
|
|
31
32
|
)
|
|
32
33
|
// Start with all-empty obstacle indexes for a "clean" scenario
|
|
33
34
|
|
|
35
|
+
const { zIndexByName, layerNames } = buildZIndexMap({
|
|
36
|
+
obstacles: srj.obstacles,
|
|
37
|
+
layerCount: srj.layerCount,
|
|
38
|
+
})
|
|
39
|
+
|
|
34
40
|
return {
|
|
35
|
-
|
|
36
|
-
layerNames: ["top"],
|
|
41
|
+
layerNames,
|
|
37
42
|
layerCount,
|
|
38
43
|
bounds,
|
|
39
44
|
options: { gridSizes: [1] },
|
|
@@ -55,5 +60,8 @@ export const createTwoNodeExpansionInput = (): RectDiffExpansionSolverInput => {
|
|
|
55
60
|
totalSeedsThisGrid: 0,
|
|
56
61
|
consumedSeedsThisGrid: 0,
|
|
57
62
|
obstacleIndexByLayer,
|
|
63
|
+
zIndexByName,
|
|
64
|
+
layerNamesCanonical: layerNames,
|
|
65
|
+
obstacles: srj.obstacles,
|
|
58
66
|
}
|
|
59
67
|
}
|
|
@@ -45,6 +45,7 @@ export function createBaseVisualization(
|
|
|
45
45
|
// Draw obstacles
|
|
46
46
|
for (const obstacle of srj.obstacles ?? []) {
|
|
47
47
|
if (obstacle.type === "rect" || obstacle.type === "oval") {
|
|
48
|
+
const layerLabel = (obstacle.zLayers ?? []).join(",") || "all"
|
|
48
49
|
rects.push({
|
|
49
50
|
center: { x: obstacle.center.x, y: obstacle.center.y },
|
|
50
51
|
width: obstacle.width,
|
|
@@ -52,7 +53,7 @@ export function createBaseVisualization(
|
|
|
52
53
|
fill: "#fee2e2",
|
|
53
54
|
stroke: "#ef4444",
|
|
54
55
|
layer: "obstacle",
|
|
55
|
-
label:
|
|
56
|
+
label: `obstacle\nz:${layerLabel}`,
|
|
56
57
|
})
|
|
57
58
|
}
|
|
58
59
|
}
|
|
@@ -6,13 +6,12 @@ import { finalizeRects } from "../../utils/finalizeRects"
|
|
|
6
6
|
import { resizeSoftOverlaps } from "../../utils/resizeSoftOverlaps"
|
|
7
7
|
import { rectsToMeshNodes } from "./rectsToMeshNodes"
|
|
8
8
|
import type { XYRect, Candidate3D, Placed3D } from "../../rectdiff-types"
|
|
9
|
-
import type {
|
|
9
|
+
import type { Obstacle } from "lib/types/srj-types"
|
|
10
10
|
import RBush from "rbush"
|
|
11
11
|
import { rectToTree } from "../../utils/rectToTree"
|
|
12
12
|
import { sameTreeRect } from "../../utils/sameTreeRect"
|
|
13
13
|
|
|
14
14
|
export type RectDiffExpansionSolverInput = {
|
|
15
|
-
srj: SimpleRouteJson
|
|
16
15
|
layerNames: string[]
|
|
17
16
|
layerCount: number
|
|
18
17
|
bounds: XYRect
|
|
@@ -29,6 +28,10 @@ export type RectDiffExpansionSolverInput = {
|
|
|
29
28
|
totalSeedsThisGrid: number
|
|
30
29
|
consumedSeedsThisGrid: number
|
|
31
30
|
obstacleIndexByLayer: Array<RBush<RTreeRect>>
|
|
31
|
+
zIndexByName: Map<string, number>
|
|
32
|
+
layerNamesCanonical: string[]
|
|
33
|
+
obstacles: Obstacle[]
|
|
34
|
+
obstacleClearance?: number
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/**
|
|
@@ -132,8 +135,10 @@ export class RectDiffExpansionSolver extends BaseSolver {
|
|
|
132
135
|
|
|
133
136
|
const rects = finalizeRects({
|
|
134
137
|
placed: this.input.placed,
|
|
135
|
-
|
|
138
|
+
obstacles: this.input.obstacles,
|
|
139
|
+
zIndexByName: this.input.zIndexByName,
|
|
136
140
|
boardVoidRects: this.input.boardVoidRects,
|
|
141
|
+
obstacleClearance: this.input.obstacleClearance,
|
|
137
142
|
})
|
|
138
143
|
this._meshNodes = rectsToMeshNodes(rects)
|
|
139
144
|
this.solved = true
|
|
@@ -3,7 +3,11 @@ import {
|
|
|
3
3
|
definePipelineStep,
|
|
4
4
|
type PipelineStep,
|
|
5
5
|
} from "@tscircuit/solver-utils"
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
Obstacle,
|
|
8
|
+
SimpleRouteConnection,
|
|
9
|
+
SimpleRouteJson,
|
|
10
|
+
} from "lib/types/srj-types"
|
|
7
11
|
import type { GridFill3DOptions, XYRect } from "lib/rectdiff-types"
|
|
8
12
|
import type { CapacityMeshNode, RTreeRect } from "lib/types/capacity-mesh-types"
|
|
9
13
|
import { RectDiffSeedingSolver } from "lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver"
|
|
@@ -11,25 +15,47 @@ import { RectDiffExpansionSolver } from "lib/solvers/RectDiffExpansionSolver/Rec
|
|
|
11
15
|
import type { GraphicsObject } from "graphics-debug"
|
|
12
16
|
import RBush from "rbush"
|
|
13
17
|
import { buildObstacleIndexesByLayer } from "./buildObstacleIndexes"
|
|
18
|
+
import type { Bounds } from "@tscircuit/math-utils"
|
|
14
19
|
|
|
15
20
|
export type RectDiffGridSolverPipelineInput = {
|
|
16
|
-
|
|
21
|
+
bounds: Bounds
|
|
22
|
+
obstacles: Obstacle[]
|
|
23
|
+
connections: SimpleRouteConnection[]
|
|
24
|
+
outline?: Pick<SimpleRouteJson, "outline">
|
|
25
|
+
layerCount: number
|
|
26
|
+
minTraceWidth: number
|
|
27
|
+
obstacleClearance?: number
|
|
17
28
|
gridOptions?: Partial<GridFill3DOptions>
|
|
18
29
|
boardVoidRects?: XYRect[]
|
|
30
|
+
layerNames?: string[]
|
|
31
|
+
zIndexByName?: Map<string, number>
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridSolverPipelineInput> {
|
|
22
35
|
rectDiffSeedingSolver?: RectDiffSeedingSolver
|
|
23
36
|
rectDiffExpansionSolver?: RectDiffExpansionSolver
|
|
24
37
|
private obstacleIndexByLayer: Array<RBush<RTreeRect>>
|
|
38
|
+
private layerNames: string[]
|
|
39
|
+
private zIndexByName: Map<string, number>
|
|
25
40
|
|
|
26
41
|
constructor(inputProblem: RectDiffGridSolverPipelineInput) {
|
|
27
42
|
super(inputProblem)
|
|
28
|
-
const { obstacleIndexByLayer } =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
const { obstacleIndexByLayer, layerNames, zIndexByName } =
|
|
44
|
+
buildObstacleIndexesByLayer({
|
|
45
|
+
srj: {
|
|
46
|
+
bounds: inputProblem.bounds,
|
|
47
|
+
obstacles: inputProblem.obstacles,
|
|
48
|
+
connections: inputProblem.connections,
|
|
49
|
+
outline: inputProblem.outline?.outline,
|
|
50
|
+
layerCount: inputProblem.layerCount,
|
|
51
|
+
minTraceWidth: inputProblem.minTraceWidth,
|
|
52
|
+
},
|
|
53
|
+
boardVoidRects: inputProblem.boardVoidRects,
|
|
54
|
+
obstacleClearance: inputProblem.obstacleClearance,
|
|
55
|
+
})
|
|
32
56
|
this.obstacleIndexByLayer = obstacleIndexByLayer
|
|
57
|
+
this.layerNames = inputProblem.layerNames ?? layerNames
|
|
58
|
+
this.zIndexByName = inputProblem.zIndexByName ?? zIndexByName
|
|
33
59
|
}
|
|
34
60
|
|
|
35
61
|
override pipelineDef: PipelineStep<any>[] = [
|
|
@@ -38,10 +64,20 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridS
|
|
|
38
64
|
RectDiffSeedingSolver,
|
|
39
65
|
(pipeline: RectDiffGridSolverPipeline) => [
|
|
40
66
|
{
|
|
41
|
-
simpleRouteJson:
|
|
67
|
+
simpleRouteJson: {
|
|
68
|
+
bounds: pipeline.inputProblem.bounds,
|
|
69
|
+
obstacles: pipeline.inputProblem.obstacles,
|
|
70
|
+
connections: pipeline.inputProblem.connections,
|
|
71
|
+
outline: pipeline.inputProblem.outline?.outline,
|
|
72
|
+
layerCount: pipeline.inputProblem.layerCount,
|
|
73
|
+
minTraceWidth: pipeline.inputProblem.minTraceWidth,
|
|
74
|
+
},
|
|
42
75
|
gridOptions: pipeline.inputProblem.gridOptions,
|
|
43
76
|
obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
|
|
44
77
|
boardVoidRects: pipeline.inputProblem.boardVoidRects,
|
|
78
|
+
layerNames: pipeline.layerNames,
|
|
79
|
+
zIndexByName: pipeline.zIndexByName,
|
|
80
|
+
obstacleClearance: pipeline.inputProblem.obstacleClearance,
|
|
45
81
|
},
|
|
46
82
|
],
|
|
47
83
|
),
|
|
@@ -55,10 +91,9 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridS
|
|
|
55
91
|
}
|
|
56
92
|
return [
|
|
57
93
|
{
|
|
58
|
-
srj: pipeline.inputProblem.simpleRouteJson,
|
|
59
94
|
layerNames: output.layerNames ?? [],
|
|
60
95
|
boardVoidRects: pipeline.inputProblem.boardVoidRects ?? [],
|
|
61
|
-
layerCount: pipeline.inputProblem.
|
|
96
|
+
layerCount: pipeline.inputProblem.layerCount,
|
|
62
97
|
bounds: output.bounds!,
|
|
63
98
|
candidates: output.candidates,
|
|
64
99
|
consumedSeedsThisGrid: output.placed.length,
|
|
@@ -69,6 +104,10 @@ export class RectDiffGridSolverPipeline extends BasePipelineSolver<RectDiffGridS
|
|
|
69
104
|
expansionIndex: output.expansionIndex,
|
|
70
105
|
obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
|
|
71
106
|
options: output.options,
|
|
107
|
+
zIndexByName: pipeline.zIndexByName,
|
|
108
|
+
layerNamesCanonical: pipeline.layerNames,
|
|
109
|
+
obstacles: pipeline.inputProblem.obstacles,
|
|
110
|
+
obstacleClearance: pipeline.inputProblem.obstacleClearance,
|
|
72
111
|
},
|
|
73
112
|
]
|
|
74
113
|
},
|
|
@@ -8,15 +8,22 @@ import {
|
|
|
8
8
|
} from "lib/solvers/RectDiffSeedingSolver/layers"
|
|
9
9
|
import type { XYRect } from "lib/rectdiff-types"
|
|
10
10
|
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
11
|
+
import { padRect } from "lib/utils/padRect"
|
|
11
12
|
|
|
12
13
|
export const buildObstacleIndexesByLayer = (params: {
|
|
13
14
|
srj: SimpleRouteJson
|
|
14
15
|
boardVoidRects?: XYRect[]
|
|
16
|
+
obstacleClearance?: number
|
|
15
17
|
}): {
|
|
16
18
|
obstacleIndexByLayer: Array<RBush<RTreeRect>>
|
|
19
|
+
layerNames: string[]
|
|
20
|
+
zIndexByName: Map<string, number>
|
|
17
21
|
} => {
|
|
18
|
-
const { srj, boardVoidRects } = params
|
|
19
|
-
const { layerNames, zIndexByName } = buildZIndexMap(
|
|
22
|
+
const { srj, boardVoidRects, obstacleClearance } = params
|
|
23
|
+
const { layerNames, zIndexByName } = buildZIndexMap({
|
|
24
|
+
obstacles: srj.obstacles,
|
|
25
|
+
layerCount: srj.layerCount,
|
|
26
|
+
})
|
|
20
27
|
const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1)
|
|
21
28
|
const bounds: XYRect = {
|
|
22
29
|
x: srj.bounds.minX,
|
|
@@ -48,9 +55,10 @@ export const buildObstacleIndexesByLayer = (params: {
|
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
for (const obstacle of srj.obstacles ?? []) {
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
53
|
-
const
|
|
58
|
+
const rectBase = obstacleToXYRect(obstacle)
|
|
59
|
+
if (!rectBase) continue
|
|
60
|
+
const rect = padRect(rectBase, obstacleClearance ?? 0)
|
|
61
|
+
const zLayers = obstacleZs(obstacle, zIndexByName)
|
|
54
62
|
const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount)
|
|
55
63
|
if (invalidZs.length) {
|
|
56
64
|
throw new Error(
|
|
@@ -66,5 +74,5 @@ export const buildObstacleIndexesByLayer = (params: {
|
|
|
66
74
|
for (const z of zLayers) insertObstacle(rect, z)
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
return { obstacleIndexByLayer }
|
|
77
|
+
return { obstacleIndexByLayer, layerNames, zIndexByName }
|
|
70
78
|
}
|
|
@@ -28,6 +28,9 @@ export type RectDiffSeedingSolverInput = {
|
|
|
28
28
|
obstacleIndexByLayer: Array<RBush<RTreeRect>>
|
|
29
29
|
gridOptions?: Partial<GridFill3DOptions>
|
|
30
30
|
boardVoidRects?: XYRect[]
|
|
31
|
+
layerNames: string[]
|
|
32
|
+
zIndexByName: Map<string, number>
|
|
33
|
+
obstacleClearance?: number
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -67,7 +70,16 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
67
70
|
const srj = this.input.simpleRouteJson
|
|
68
71
|
const opts = this.input.gridOptions ?? {}
|
|
69
72
|
|
|
70
|
-
const
|
|
73
|
+
const precomputed = this.input.layerNames && this.input.zIndexByName
|
|
74
|
+
const { layerNames, zIndexByName } = precomputed
|
|
75
|
+
? {
|
|
76
|
+
layerNames: this.input.layerNames!,
|
|
77
|
+
zIndexByName: this.input.zIndexByName!,
|
|
78
|
+
}
|
|
79
|
+
: buildZIndexMap({
|
|
80
|
+
obstacles: srj.obstacles,
|
|
81
|
+
layerCount: srj.layerCount,
|
|
82
|
+
})
|
|
71
83
|
const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1)
|
|
72
84
|
|
|
73
85
|
const bounds: XYRect = {
|
|
@@ -310,7 +322,6 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
310
322
|
*/
|
|
311
323
|
override getOutput() {
|
|
312
324
|
return {
|
|
313
|
-
srj: this.srj,
|
|
314
325
|
layerNames: this.layerNames,
|
|
315
326
|
layerCount: this.layerCount,
|
|
316
327
|
bounds: this.bounds,
|
|
@@ -323,6 +334,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
323
334
|
edgeAnalysisDone: this.edgeAnalysisDone,
|
|
324
335
|
totalSeedsThisGrid: this.totalSeedsThisGrid,
|
|
325
336
|
consumedSeedsThisGrid: this.consumedSeedsThisGrid,
|
|
337
|
+
obstacles: this.srj.obstacles,
|
|
338
|
+
obstacleClearance: this.input.obstacleClearance,
|
|
326
339
|
}
|
|
327
340
|
}
|
|
328
341
|
|
|
@@ -379,6 +392,31 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
379
392
|
}
|
|
380
393
|
}
|
|
381
394
|
|
|
395
|
+
// obstacle clearance visualization (expanded)
|
|
396
|
+
if (this.input.obstacleClearance && this.input.obstacleClearance > 0) {
|
|
397
|
+
for (const obstacle of srj.obstacles ?? []) {
|
|
398
|
+
const pad = this.input.obstacleClearance
|
|
399
|
+
const expanded = {
|
|
400
|
+
x: obstacle.center.x - obstacle.width / 2 - pad,
|
|
401
|
+
y: obstacle.center.y - obstacle.height / 2 - pad,
|
|
402
|
+
width: obstacle.width + 2 * pad,
|
|
403
|
+
height: obstacle.height + 2 * pad,
|
|
404
|
+
}
|
|
405
|
+
rects.push({
|
|
406
|
+
center: {
|
|
407
|
+
x: expanded.x + expanded.width / 2,
|
|
408
|
+
y: expanded.y + expanded.height / 2,
|
|
409
|
+
},
|
|
410
|
+
width: expanded.width,
|
|
411
|
+
height: expanded.height,
|
|
412
|
+
fill: "rgba(234, 179, 8, 0.15)",
|
|
413
|
+
stroke: "rgba(202, 138, 4, 0.9)",
|
|
414
|
+
layer: "obstacle-clearance",
|
|
415
|
+
label: "clearance",
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
382
420
|
// board void rects (early visualization of mask)
|
|
383
421
|
if (this.boardVoidRects) {
|
|
384
422
|
let outlineBBox: {
|
|
@@ -423,10 +461,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
423
461
|
points.push({
|
|
424
462
|
x: cand.x,
|
|
425
463
|
y: cand.y,
|
|
426
|
-
fill: "#9333ea",
|
|
427
|
-
stroke: "#6b21a8",
|
|
428
464
|
label: `z:${cand.z}`,
|
|
429
|
-
}
|
|
465
|
+
})
|
|
430
466
|
}
|
|
431
467
|
}
|
|
432
468
|
|
|
@@ -6,6 +6,31 @@ import {
|
|
|
6
6
|
} from "../../utils/rectdiff-geometry"
|
|
7
7
|
import { isPointInPolygon } from "./isPointInPolygon"
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Simplify a polygon by reducing coordinate precision to avoid excessive grid cells.
|
|
11
|
+
* This rounds coordinates to a grid and removes duplicates.
|
|
12
|
+
*/
|
|
13
|
+
function simplifyPolygon(
|
|
14
|
+
polygon: Array<{ x: number; y: number }>,
|
|
15
|
+
precision: number,
|
|
16
|
+
): Array<{ x: number; y: number }> {
|
|
17
|
+
const round = (v: number) => Math.round(v / precision) * precision
|
|
18
|
+
const seen = new Set<string>()
|
|
19
|
+
const result: Array<{ x: number; y: number }> = []
|
|
20
|
+
|
|
21
|
+
for (const p of polygon) {
|
|
22
|
+
const rx = round(p.x)
|
|
23
|
+
const ry = round(p.y)
|
|
24
|
+
const key = `${rx},${ry}`
|
|
25
|
+
if (!seen.has(key)) {
|
|
26
|
+
seen.add(key)
|
|
27
|
+
result.push({ x: rx, y: ry })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
|
|
9
34
|
/**
|
|
10
35
|
* Decompose the empty space inside 'bounds' but outside 'polygon' into rectangles.
|
|
11
36
|
* This uses a coordinate grid approach, ideal for rectilinear polygons.
|
|
@@ -16,10 +41,21 @@ export function computeInverseRects(
|
|
|
16
41
|
): XYRect[] {
|
|
17
42
|
if (!polygon || polygon.length < 3) return []
|
|
18
43
|
|
|
44
|
+
// Simplify polygon if it has too many points to avoid O(n^2) performance issues
|
|
45
|
+
// A polygon with 350+ points (like rounded corners) creates too many grid cells
|
|
46
|
+
const MAX_POLYGON_POINTS = 100
|
|
47
|
+
const workingPolygon =
|
|
48
|
+
polygon.length > MAX_POLYGON_POINTS
|
|
49
|
+
? simplifyPolygon(
|
|
50
|
+
polygon,
|
|
51
|
+
Math.max(bounds.width, bounds.height) / MAX_POLYGON_POINTS,
|
|
52
|
+
)
|
|
53
|
+
: polygon
|
|
54
|
+
|
|
19
55
|
// 1. Collect unique sorted X and Y coordinates
|
|
20
56
|
const xs = new Set<number>([bounds.x, bounds.x + bounds.width])
|
|
21
57
|
const ys = new Set<number>([bounds.y, bounds.y + bounds.height])
|
|
22
|
-
for (const p of
|
|
58
|
+
for (const p of workingPolygon) {
|
|
23
59
|
xs.add(p.x)
|
|
24
60
|
ys.add(p.y)
|
|
25
61
|
}
|
|
@@ -21,11 +21,15 @@ export function canonicalizeLayerOrder(names: string[]) {
|
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// TODO: should not take a srj
|
|
25
|
+
export function buildZIndexMap(params: {
|
|
26
|
+
obstacles?: Obstacle[]
|
|
27
|
+
layerCount?: number
|
|
28
|
+
}) {
|
|
25
29
|
const names = canonicalizeLayerOrder(
|
|
26
|
-
(
|
|
30
|
+
(params.obstacles ?? []).flatMap((o) => o.layers ?? []),
|
|
27
31
|
)
|
|
28
|
-
const declaredLayerCount = Math.max(1,
|
|
32
|
+
const declaredLayerCount = Math.max(1, params.layerCount || names.length || 1)
|
|
29
33
|
const fallback = Array.from({ length: declaredLayerCount }, (_, i) =>
|
|
30
34
|
i === 0 ? "top" : i === declaredLayerCount - 1 ? "bottom" : `inner${i}`,
|
|
31
35
|
)
|
|
@@ -78,8 +82,8 @@ export function obstacleZs(ob: Obstacle, zIndexByName: Map<string, number>) {
|
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
export function obstacleToXYRect(ob: Obstacle): XYRect | null {
|
|
81
|
-
const w = ob.width
|
|
82
|
-
const h = ob.height
|
|
85
|
+
const w = ob.width
|
|
86
|
+
const h = ob.height
|
|
83
87
|
if (typeof w !== "number" || typeof h !== "number") return null
|
|
84
88
|
return { x: ob.center.x - w / 2, y: ob.center.y - h / 2, width: w, height: h }
|
|
85
89
|
}
|
|
@@ -286,14 +286,20 @@ export function expandRectFromSeed(params: {
|
|
|
286
286
|
for (const b of blockers) if (overlaps(r, b)) continue STRATS
|
|
287
287
|
|
|
288
288
|
// greedy expansions in 4 directions
|
|
289
|
+
// Use a minimum expansion threshold to avoid infinitesimal improvements
|
|
290
|
+
// that can occur with mixed-precision floating point coordinates
|
|
291
|
+
const MIN_EXPANSION = 1e-6
|
|
292
|
+
const MAX_ITERATIONS = 1000
|
|
289
293
|
let improved = true
|
|
290
|
-
|
|
294
|
+
let iterations = 0
|
|
295
|
+
while (improved && iterations < MAX_ITERATIONS) {
|
|
296
|
+
iterations++
|
|
291
297
|
improved = false
|
|
292
298
|
const commonParams = { bounds, blockers, maxAspect: maxAspectRatio }
|
|
293
299
|
|
|
294
300
|
collectBlockers(searchStripRight({ rect: r, bounds }))
|
|
295
301
|
const eR = maxExpandRight({ ...commonParams, r })
|
|
296
|
-
if (eR >
|
|
302
|
+
if (eR > MIN_EXPANSION) {
|
|
297
303
|
r = { ...r, width: r.width + eR }
|
|
298
304
|
collectBlockers(r)
|
|
299
305
|
improved = true
|
|
@@ -301,7 +307,7 @@ export function expandRectFromSeed(params: {
|
|
|
301
307
|
|
|
302
308
|
collectBlockers(searchStripDown({ rect: r, bounds }))
|
|
303
309
|
const eD = maxExpandDown({ ...commonParams, r })
|
|
304
|
-
if (eD >
|
|
310
|
+
if (eD > MIN_EXPANSION) {
|
|
305
311
|
r = { ...r, height: r.height + eD }
|
|
306
312
|
collectBlockers(r)
|
|
307
313
|
improved = true
|
|
@@ -309,7 +315,7 @@ export function expandRectFromSeed(params: {
|
|
|
309
315
|
|
|
310
316
|
collectBlockers(searchStripLeft({ rect: r, bounds }))
|
|
311
317
|
const eL = maxExpandLeft({ ...commonParams, r })
|
|
312
|
-
if (eL >
|
|
318
|
+
if (eL > MIN_EXPANSION) {
|
|
313
319
|
r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height }
|
|
314
320
|
collectBlockers(r)
|
|
315
321
|
improved = true
|
|
@@ -317,7 +323,7 @@ export function expandRectFromSeed(params: {
|
|
|
317
323
|
|
|
318
324
|
collectBlockers(searchStripUp({ rect: r, bounds }))
|
|
319
325
|
const eU = maxExpandUp({ ...commonParams, r })
|
|
320
|
-
if (eU >
|
|
326
|
+
if (eU > MIN_EXPANSION) {
|
|
321
327
|
r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU }
|
|
322
328
|
collectBlockers(r)
|
|
323
329
|
improved = true
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import type { Obstacle } from "lib/types/srj-types"
|
|
1
2
|
import type { Placed3D, Rect3d, XYRect } from "../rectdiff-types"
|
|
2
|
-
import type { SimpleRouteJson } from "../types/srj-types"
|
|
3
3
|
import {
|
|
4
|
-
buildZIndexMap,
|
|
5
4
|
obstacleToXYRect,
|
|
6
5
|
obstacleZs,
|
|
7
6
|
} from "../solvers/RectDiffSeedingSolver/layers"
|
|
8
7
|
|
|
9
8
|
export function finalizeRects(params: {
|
|
10
9
|
placed: Placed3D[]
|
|
11
|
-
|
|
10
|
+
obstacles: Obstacle[]
|
|
12
11
|
boardVoidRects: XYRect[]
|
|
12
|
+
zIndexByName: Map<string, number>
|
|
13
|
+
obstacleClearance?: number
|
|
13
14
|
}): Rect3d[] {
|
|
14
15
|
// Convert all placed (free space) nodes to output format
|
|
15
16
|
const out: Rect3d[] = params.placed.map((p) => ({
|
|
@@ -20,23 +21,30 @@ export function finalizeRects(params: {
|
|
|
20
21
|
zLayers: [...p.zLayers].sort((a, b) => a - b),
|
|
21
22
|
}))
|
|
22
23
|
|
|
23
|
-
const { zIndexByName } = buildZIndexMap(params.srj)
|
|
24
24
|
const layersByKey = new Map<string, { rect: XYRect; layers: Set<number> }>()
|
|
25
25
|
|
|
26
|
-
for (const obstacle of params.
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
26
|
+
for (const obstacle of params.obstacles ?? []) {
|
|
27
|
+
const baseRect = obstacleToXYRect(obstacle)
|
|
28
|
+
if (!baseRect) continue
|
|
29
|
+
const rect = params.obstacleClearance
|
|
30
|
+
? {
|
|
31
|
+
x: baseRect.x - params.obstacleClearance,
|
|
32
|
+
y: baseRect.y - params.obstacleClearance,
|
|
33
|
+
width: baseRect.width + 2 * params.obstacleClearance,
|
|
34
|
+
height: baseRect.height + 2 * params.obstacleClearance,
|
|
35
|
+
}
|
|
36
|
+
: baseRect
|
|
29
37
|
const zLayers =
|
|
30
38
|
obstacle.zLayers?.length && obstacle.zLayers.length > 0
|
|
31
39
|
? obstacle.zLayers
|
|
32
|
-
: obstacleZs(obstacle
|
|
40
|
+
: obstacleZs(obstacle, params.zIndexByName)
|
|
33
41
|
const key = `${rect.x}:${rect.y}:${rect.width}:${rect.height}`
|
|
34
42
|
let entry = layersByKey.get(key)
|
|
35
43
|
if (!entry) {
|
|
36
44
|
entry = { rect, layers: new Set() }
|
|
37
45
|
layersByKey.set(key, entry)
|
|
38
46
|
}
|
|
39
|
-
zLayers.forEach((layer) => entry!.layers.add(layer))
|
|
47
|
+
zLayers.forEach((layer: number) => entry!.layers.add(layer))
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
for (const { rect, layers } of layersByKey.values()) {
|