@tscircuit/rectdiff 0.0.22 → 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 +206 -54
- 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/layers.ts +9 -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/tests/board-outline.test.ts +1 -1
- package/tests/fixtures/makeSimpleRouteOutlineGraphics.ts +5 -1
- package/tests/should-expand-node.test.ts +9 -1
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c-clearance.snap.svg +44 -0
- package/tests/solver/bugreport11-b2de3c/bugreport11-b2de3c-clearance.test.ts +97 -0
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
|
|
|
@@ -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
|
}
|
|
@@ -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()) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { XYRect } from "../rectdiff-types"
|
|
2
|
+
|
|
3
|
+
export const padRect = (rect: XYRect, clearance: number): XYRect => {
|
|
4
|
+
if (!clearance || clearance <= 0) return rect
|
|
5
|
+
return {
|
|
6
|
+
x: rect.x - clearance,
|
|
7
|
+
y: rect.y - clearance,
|
|
8
|
+
width: rect.width + 2 * clearance,
|
|
9
|
+
height: rect.height + 2 * clearance,
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { SimpleRouteJson } from "lib/types/srj-types"
|
|
2
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure helper that returns clearance rect graphics; does not mutate inputs.
|
|
6
|
+
*/
|
|
7
|
+
export const buildObstacleClearanceGraphics = (params: {
|
|
8
|
+
srj: SimpleRouteJson
|
|
9
|
+
clearance: number | undefined
|
|
10
|
+
}): GraphicsObject => {
|
|
11
|
+
const { srj, clearance } = params
|
|
12
|
+
const c = clearance ?? 0
|
|
13
|
+
if (c <= 0) {
|
|
14
|
+
return {
|
|
15
|
+
title: "Obstacle Clearance",
|
|
16
|
+
coordinateSystem: "cartesian",
|
|
17
|
+
rects: [],
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const rects: NonNullable<GraphicsObject["rects"]> = []
|
|
22
|
+
|
|
23
|
+
for (const obstacle of srj.obstacles ?? []) {
|
|
24
|
+
if (obstacle.type !== "rect" && obstacle.type !== "oval") continue
|
|
25
|
+
const expanded = {
|
|
26
|
+
x: obstacle.center.x - obstacle.width / 2 - c,
|
|
27
|
+
y: obstacle.center.y - obstacle.height / 2 - c,
|
|
28
|
+
width: obstacle.width + 2 * c,
|
|
29
|
+
height: obstacle.height + 2 * c,
|
|
30
|
+
}
|
|
31
|
+
rects.push({
|
|
32
|
+
center: {
|
|
33
|
+
x: expanded.x + expanded.width / 2,
|
|
34
|
+
y: expanded.y + expanded.height / 2,
|
|
35
|
+
},
|
|
36
|
+
width: expanded.width,
|
|
37
|
+
height: expanded.height,
|
|
38
|
+
stroke: "rgba(202, 138, 4, 0.9)",
|
|
39
|
+
fill: "rgba(234, 179, 8, 0.15)",
|
|
40
|
+
layer: "obstacle-clearance",
|
|
41
|
+
label: `clearance\nz:${(obstacle.zLayers ?? []).join(",") || "all"}`,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
title: "Obstacle Clearance",
|
|
47
|
+
coordinateSystem: "cartesian",
|
|
48
|
+
rects,
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ import { makeCapacityMeshNodeWithLayerInfo } from "./fixtures/makeCapacityMeshNo
|
|
|
6
6
|
|
|
7
7
|
test("board outline snapshot", async () => {
|
|
8
8
|
const solver = new RectDiffPipeline({
|
|
9
|
-
simpleRouteJson: boardWithCutout
|
|
9
|
+
simpleRouteJson: boardWithCutout,
|
|
10
10
|
})
|
|
11
11
|
|
|
12
12
|
// Run to completion
|