@tscircuit/rectdiff 0.0.22 → 0.0.24
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 +291 -80
- package/lib/RectDiffPipeline.ts +42 -35
- package/lib/buildFinalRectDiffVisualization.ts +46 -0
- 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 +16 -5
- package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +12 -2
- package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +53 -13
- package/lib/solvers/RectDiffSeedingSolver/layers.ts +9 -5
- package/lib/utils/buildOutlineGraphics.ts +39 -0
- package/lib/utils/expandRectFromSeed.ts +11 -1
- 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/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
- package/tests/solver/bugreport01-be84eb/__snapshots__/bugreport01-be84eb.snap.svg +2 -2
- package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +2 -2
- package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
- package/tests/solver/bugreport07-d3f3be/__snapshots__/bugreport07-d3f3be.snap.svg +1 -1
- package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
- package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +2 -2
- package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +2 -2
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
- package/tests/solver/bugreport11-b2de3c/bugreport11-b2de3c-clearance-equivalence.test.ts +52 -0
- package/tests/solver/bugreport12-35ce1c/__snapshots__/bugreport12-35ce1c.snap.svg +1 -1
- package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
- package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +2 -2
- package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
- package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
- package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +1 -1
- package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
- 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 +1 -1
- 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/tests/solver/interaction/__snapshots__/interaction.snap.svg +1 -1
- package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
- package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +1 -1
package/lib/RectDiffPipeline.ts
CHANGED
|
@@ -10,17 +10,24 @@ import type { GraphicsObject } from "graphics-debug"
|
|
|
10
10
|
import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline"
|
|
11
11
|
import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
|
|
12
12
|
import { createBaseVisualization } from "./rectdiff-visualization"
|
|
13
|
+
import { buildFinalRectDiffVisualization } from "./buildFinalRectDiffVisualization"
|
|
13
14
|
import { computeInverseRects } from "./solvers/RectDiffSeedingSolver/computeInverseRects"
|
|
15
|
+
import { buildZIndexMap } from "./solvers/RectDiffSeedingSolver/layers"
|
|
16
|
+
import { buildObstacleClearanceGraphics } from "./utils/renderObstacleClearance"
|
|
17
|
+
import { mergeGraphics } from "graphics-debug"
|
|
14
18
|
|
|
15
19
|
export interface RectDiffPipelineInput {
|
|
16
20
|
simpleRouteJson: SimpleRouteJson
|
|
17
21
|
gridOptions?: Partial<GridFill3DOptions>
|
|
22
|
+
obstacleClearance?: number
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput> {
|
|
21
26
|
rectDiffGridSolverPipeline?: RectDiffGridSolverPipeline
|
|
22
27
|
gapFillSolver?: GapFillSolverPipeline
|
|
23
28
|
boardVoidRects: XYRect[] | undefined
|
|
29
|
+
zIndexByName?: Map<string, number>
|
|
30
|
+
layerNames?: string[]
|
|
24
31
|
|
|
25
32
|
override pipelineDef: PipelineStep<any>[] = [
|
|
26
33
|
definePipelineStep(
|
|
@@ -28,9 +35,21 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
28
35
|
RectDiffGridSolverPipeline,
|
|
29
36
|
(rectDiffPipeline: RectDiffPipeline) => [
|
|
30
37
|
{
|
|
31
|
-
|
|
38
|
+
bounds: rectDiffPipeline.inputProblem.simpleRouteJson.bounds,
|
|
39
|
+
obstacles: rectDiffPipeline.inputProblem.simpleRouteJson.obstacles,
|
|
40
|
+
connections:
|
|
41
|
+
rectDiffPipeline.inputProblem.simpleRouteJson.connections,
|
|
42
|
+
outline: rectDiffPipeline.inputProblem.simpleRouteJson.outline
|
|
43
|
+
? { outline: rectDiffPipeline.inputProblem.simpleRouteJson.outline }
|
|
44
|
+
: undefined,
|
|
45
|
+
layerCount: rectDiffPipeline.inputProblem.simpleRouteJson.layerCount,
|
|
32
46
|
gridOptions: rectDiffPipeline.inputProblem.gridOptions,
|
|
33
47
|
boardVoidRects: rectDiffPipeline.boardVoidRects,
|
|
48
|
+
layerNames: rectDiffPipeline.layerNames,
|
|
49
|
+
zIndexByName: rectDiffPipeline.zIndexByName,
|
|
50
|
+
minTraceWidth:
|
|
51
|
+
rectDiffPipeline.inputProblem.simpleRouteJson.minTraceWidth,
|
|
52
|
+
obstacleClearance: rectDiffPipeline.inputProblem.obstacleClearance,
|
|
34
53
|
},
|
|
35
54
|
],
|
|
36
55
|
),
|
|
@@ -53,6 +72,12 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
53
72
|
]
|
|
54
73
|
|
|
55
74
|
override _setup(): void {
|
|
75
|
+
const { zIndexByName, layerNames } = buildZIndexMap({
|
|
76
|
+
obstacles: this.inputProblem.simpleRouteJson.obstacles,
|
|
77
|
+
layerCount: this.inputProblem.simpleRouteJson.layerCount,
|
|
78
|
+
})
|
|
79
|
+
this.zIndexByName = zIndexByName
|
|
80
|
+
this.layerNames = layerNames
|
|
56
81
|
if (this.inputProblem.simpleRouteJson.outline) {
|
|
57
82
|
this.boardVoidRects = computeInverseRects(
|
|
58
83
|
{
|
|
@@ -86,17 +111,23 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
86
111
|
}
|
|
87
112
|
|
|
88
113
|
override initialVisualize(): GraphicsObject {
|
|
89
|
-
const
|
|
114
|
+
const base = createBaseVisualization(
|
|
90
115
|
this.inputProblem.simpleRouteJson,
|
|
91
116
|
"RectDiffPipeline - Initial",
|
|
92
117
|
)
|
|
118
|
+
const clearance = buildObstacleClearanceGraphics({
|
|
119
|
+
srj: this.inputProblem.simpleRouteJson,
|
|
120
|
+
clearance: this.inputProblem.obstacleClearance,
|
|
121
|
+
})
|
|
93
122
|
|
|
94
123
|
// Show initial mesh nodes from grid pipeline if available
|
|
95
124
|
const initialNodes =
|
|
96
125
|
this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []
|
|
97
126
|
|
|
98
|
-
|
|
99
|
-
|
|
127
|
+
const nodeRects: GraphicsObject = {
|
|
128
|
+
title: "Initial Nodes",
|
|
129
|
+
coordinateSystem: "cartesian",
|
|
130
|
+
rects: initialNodes.map((node) => ({
|
|
100
131
|
center: node.center,
|
|
101
132
|
width: node.width,
|
|
102
133
|
height: node.height,
|
|
@@ -107,41 +138,17 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
107
138
|
`node ${node.capacityMeshNodeId}`,
|
|
108
139
|
`z:${node.availableZ.join(",")}`,
|
|
109
140
|
].join("\n"),
|
|
110
|
-
})
|
|
141
|
+
})),
|
|
111
142
|
}
|
|
112
143
|
|
|
113
|
-
return
|
|
144
|
+
return mergeGraphics(mergeGraphics(base, clearance), nodeRects)
|
|
114
145
|
}
|
|
115
146
|
|
|
116
147
|
override finalVisualize(): GraphicsObject {
|
|
117
|
-
|
|
118
|
-
this.inputProblem.simpleRouteJson,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const { meshNodes: outputNodes } = this.getOutput()
|
|
123
|
-
const initialNodeIds = new Set(
|
|
124
|
-
(this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []).map(
|
|
125
|
-
(n) => n.capacityMeshNodeId,
|
|
126
|
-
),
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
for (const node of outputNodes) {
|
|
130
|
-
const isExpanded = !initialNodeIds.has(node.capacityMeshNodeId)
|
|
131
|
-
graphics.rects!.push({
|
|
132
|
-
center: node.center,
|
|
133
|
-
width: node.width,
|
|
134
|
-
height: node.height,
|
|
135
|
-
stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
|
|
136
|
-
fill: isExpanded ? "rgba(0, 200, 0, 0.3)" : "rgba(100, 100, 100, 0.1)",
|
|
137
|
-
layer: `z${node.availableZ.join(",")}`,
|
|
138
|
-
label: [
|
|
139
|
-
`${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
|
|
140
|
-
`z:${node.availableZ.join(",")}`,
|
|
141
|
-
].join("\n"),
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return graphics
|
|
148
|
+
return buildFinalRectDiffVisualization({
|
|
149
|
+
srj: this.inputProblem.simpleRouteJson,
|
|
150
|
+
meshNodes: this.getOutput().meshNodes,
|
|
151
|
+
obstacleClearance: this.inputProblem.obstacleClearance,
|
|
152
|
+
})
|
|
146
153
|
}
|
|
147
154
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mergeGraphics, type GraphicsObject } from "graphics-debug"
|
|
2
|
+
import type { CapacityMeshNode } from "./types/capacity-mesh-types"
|
|
3
|
+
import type { SimpleRouteJson } from "./types/srj-types"
|
|
4
|
+
import { getColorForZLayer } from "./utils/getColorForZLayer"
|
|
5
|
+
import { buildOutlineGraphics } from "./utils/buildOutlineGraphics"
|
|
6
|
+
import { buildObstacleClearanceGraphics } from "./utils/renderObstacleClearance"
|
|
7
|
+
|
|
8
|
+
type BuildFinalVisualizationParams = {
|
|
9
|
+
srj: SimpleRouteJson
|
|
10
|
+
meshNodes: CapacityMeshNode[]
|
|
11
|
+
obstacleClearance?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const buildFinalRectDiffVisualization = ({
|
|
15
|
+
srj,
|
|
16
|
+
meshNodes,
|
|
17
|
+
obstacleClearance,
|
|
18
|
+
}: BuildFinalVisualizationParams): GraphicsObject => {
|
|
19
|
+
const outline = buildOutlineGraphics({ srj })
|
|
20
|
+
const clearance = buildObstacleClearanceGraphics({
|
|
21
|
+
srj,
|
|
22
|
+
clearance: obstacleClearance,
|
|
23
|
+
})
|
|
24
|
+
const rects = meshNodes.map((node) => ({
|
|
25
|
+
center: node.center,
|
|
26
|
+
width: node.width,
|
|
27
|
+
height: node.height,
|
|
28
|
+
stroke: getColorForZLayer(node.availableZ).stroke,
|
|
29
|
+
fill: node._containsObstacle
|
|
30
|
+
? "#fca5a5"
|
|
31
|
+
: getColorForZLayer(node.availableZ).fill,
|
|
32
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
33
|
+
label: `node ${node.capacityMeshNodeId}\nz:${node.availableZ.join(",")}`,
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
const nodesGraphic: GraphicsObject = {
|
|
37
|
+
title: "RectDiffPipeline - Final",
|
|
38
|
+
coordinateSystem: "cartesian",
|
|
39
|
+
rects,
|
|
40
|
+
lines: [],
|
|
41
|
+
points: [],
|
|
42
|
+
texts: [],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return mergeGraphics(mergeGraphics(nodesGraphic, outline), clearance)
|
|
46
|
+
}
|
|
@@ -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
|
|
|
@@ -423,10 +436,8 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
423
436
|
points.push({
|
|
424
437
|
x: cand.x,
|
|
425
438
|
y: cand.y,
|
|
426
|
-
fill: "#9333ea",
|
|
427
|
-
stroke: "#6b21a8",
|
|
428
439
|
label: `z:${cand.z}`,
|
|
429
|
-
}
|
|
440
|
+
})
|
|
430
441
|
}
|
|
431
442
|
}
|
|
432
443
|
|
|
@@ -4,6 +4,8 @@ import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
|
|
|
4
4
|
import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
|
|
5
5
|
import type RBush from "rbush"
|
|
6
6
|
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
7
|
+
const quantize = (value: number, precision = 1e-6) =>
|
|
8
|
+
Math.round(value / precision) * precision
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Compute candidate seed points for a given grid size.
|
|
@@ -83,13 +85,14 @@ export function computeCandidates3D(params: {
|
|
|
83
85
|
? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b))
|
|
84
86
|
: [Infinity]),
|
|
85
87
|
)
|
|
88
|
+
const distance = quantize(d)
|
|
86
89
|
|
|
87
90
|
const k = `${x.toFixed(6)}|${y.toFixed(6)}`
|
|
88
91
|
const cand: Candidate3D = {
|
|
89
92
|
x,
|
|
90
93
|
y,
|
|
91
94
|
z: anchorZ,
|
|
92
|
-
distance
|
|
95
|
+
distance,
|
|
93
96
|
zSpanLen: bestSpan.length,
|
|
94
97
|
}
|
|
95
98
|
const prev = out.get(k)
|
|
@@ -104,6 +107,13 @@ export function computeCandidates3D(params: {
|
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
const arr = Array.from(out.values())
|
|
107
|
-
arr.sort(
|
|
110
|
+
arr.sort(
|
|
111
|
+
(a, b) =>
|
|
112
|
+
b.zSpanLen! - a.zSpanLen! ||
|
|
113
|
+
b.distance - a.distance ||
|
|
114
|
+
a.z - b.z ||
|
|
115
|
+
a.x - b.x ||
|
|
116
|
+
a.y - b.y,
|
|
117
|
+
)
|
|
108
118
|
return arr
|
|
109
119
|
}
|
|
@@ -4,6 +4,8 @@ import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
|
|
|
4
4
|
import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
|
|
5
5
|
import type RBush from "rbush"
|
|
6
6
|
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
7
|
+
const quantize = (value: number, precision = 1e-6) =>
|
|
8
|
+
Math.round(value / precision) * precision
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Compute exact uncovered segments along a 1D line.
|
|
@@ -15,14 +17,23 @@ function computeUncoveredSegments(params: {
|
|
|
15
17
|
minSegmentLength: number
|
|
16
18
|
}): Array<{ start: number; end: number; center: number }> {
|
|
17
19
|
const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params
|
|
20
|
+
const lineStartQ = quantize(lineStart)
|
|
21
|
+
const lineEndQ = quantize(lineEnd)
|
|
22
|
+
const normalizedIntervals = coveringIntervals
|
|
23
|
+
.map((i) => {
|
|
24
|
+
const s = quantize(i.start)
|
|
25
|
+
const e = quantize(i.end)
|
|
26
|
+
return { start: Math.min(s, e), end: Math.max(s, e) }
|
|
27
|
+
})
|
|
28
|
+
.filter((i) => i.end > i.start + EPS)
|
|
18
29
|
|
|
19
|
-
if (
|
|
20
|
-
const center = (
|
|
21
|
-
return [{ start:
|
|
30
|
+
if (normalizedIntervals.length === 0) {
|
|
31
|
+
const center = (lineStartQ + lineEndQ) / 2
|
|
32
|
+
return [{ start: lineStartQ, end: lineEndQ, center }]
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
// Sort intervals by start position
|
|
25
|
-
const sorted = [...
|
|
36
|
+
const sorted = [...normalizedIntervals].sort((a, b) => a.start - b.start)
|
|
26
37
|
|
|
27
38
|
// Merge overlapping intervals
|
|
28
39
|
const merged: Array<{ start: number; end: number }> = []
|
|
@@ -45,8 +56,8 @@ function computeUncoveredSegments(params: {
|
|
|
45
56
|
const uncovered: Array<{ start: number; end: number; center: number }> = []
|
|
46
57
|
|
|
47
58
|
// Check gap before first interval
|
|
48
|
-
if (merged[0]!.start >
|
|
49
|
-
const start =
|
|
59
|
+
if (merged[0]!.start > lineStartQ + EPS) {
|
|
60
|
+
const start = lineStartQ
|
|
50
61
|
const end = merged[0]!.start
|
|
51
62
|
if (end - start >= minSegmentLength) {
|
|
52
63
|
uncovered.push({ start, end, center: (start + end) / 2 })
|
|
@@ -63,9 +74,9 @@ function computeUncoveredSegments(params: {
|
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
// Check gap after last interval
|
|
66
|
-
if (merged[merged.length - 1]!.end <
|
|
77
|
+
if (merged[merged.length - 1]!.end < lineEndQ - EPS) {
|
|
67
78
|
const start = merged[merged.length - 1]!.end
|
|
68
|
-
const end =
|
|
79
|
+
const end = lineEndQ
|
|
69
80
|
if (end - start >= minSegmentLength) {
|
|
70
81
|
uncovered.push({ start, end, center: (start + end) / 2 })
|
|
71
82
|
}
|
|
@@ -111,7 +122,11 @@ export function computeEdgeCandidates3D(params: {
|
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
function pushIfFree(p: { x: number; y: number; z: number }) {
|
|
114
|
-
const
|
|
125
|
+
const qx = quantize(p.x)
|
|
126
|
+
const qy = quantize(p.y)
|
|
127
|
+
const { z } = p
|
|
128
|
+
const x = qx
|
|
129
|
+
const y = qy
|
|
115
130
|
if (
|
|
116
131
|
x < bounds.x + EPS ||
|
|
117
132
|
y < bounds.y + EPS ||
|
|
@@ -125,13 +140,19 @@ export function computeEdgeCandidates3D(params: {
|
|
|
125
140
|
const hard = [
|
|
126
141
|
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
127
142
|
...(hardPlacedByLayer[z] ?? []),
|
|
128
|
-
]
|
|
143
|
+
].map((b) => ({
|
|
144
|
+
x: quantize(b.x),
|
|
145
|
+
y: quantize(b.y),
|
|
146
|
+
width: quantize(b.width),
|
|
147
|
+
height: quantize(b.height),
|
|
148
|
+
}))
|
|
129
149
|
const d = Math.min(
|
|
130
150
|
distancePointToRectEdges({ x, y }, bounds),
|
|
131
151
|
...(hard.length
|
|
132
152
|
? hard.map((b) => distancePointToRectEdges({ x, y }, b))
|
|
133
153
|
: [Infinity]),
|
|
134
154
|
)
|
|
155
|
+
const distance = quantize(d)
|
|
135
156
|
|
|
136
157
|
const k = key({ x, y, z })
|
|
137
158
|
if (dedup.has(k)) return
|
|
@@ -148,14 +169,26 @@ export function computeEdgeCandidates3D(params: {
|
|
|
148
169
|
obstacleIndexByLayer,
|
|
149
170
|
additionalBlockersByLayer: hardPlacedByLayer,
|
|
150
171
|
})
|
|
151
|
-
out.push({
|
|
172
|
+
out.push({
|
|
173
|
+
x,
|
|
174
|
+
y,
|
|
175
|
+
z,
|
|
176
|
+
distance,
|
|
177
|
+
zSpanLen: span.length,
|
|
178
|
+
isEdgeSeed: true,
|
|
179
|
+
})
|
|
152
180
|
}
|
|
153
181
|
|
|
154
182
|
for (let z = 0; z < layerCount; z++) {
|
|
155
183
|
const blockers = [
|
|
156
184
|
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
157
185
|
...(hardPlacedByLayer[z] ?? []),
|
|
158
|
-
]
|
|
186
|
+
].map((b) => ({
|
|
187
|
+
x: quantize(b.x),
|
|
188
|
+
y: quantize(b.y),
|
|
189
|
+
width: quantize(b.width),
|
|
190
|
+
height: quantize(b.height),
|
|
191
|
+
}))
|
|
159
192
|
|
|
160
193
|
// 1) Board edges — find exact uncovered segments along each edge
|
|
161
194
|
|
|
@@ -372,6 +405,13 @@ export function computeEdgeCandidates3D(params: {
|
|
|
372
405
|
}
|
|
373
406
|
|
|
374
407
|
// Strong multi-layer preference then distance.
|
|
375
|
-
out.sort(
|
|
408
|
+
out.sort(
|
|
409
|
+
(a, b) =>
|
|
410
|
+
b.zSpanLen! - a.zSpanLen! ||
|
|
411
|
+
b.distance - a.distance ||
|
|
412
|
+
a.z - b.z ||
|
|
413
|
+
a.x - b.x ||
|
|
414
|
+
a.y - b.y,
|
|
415
|
+
)
|
|
376
416
|
return out
|
|
377
417
|
}
|