Skip to content

Commit c79b936

Browse files
committed
assignable via simpler obstacle consolidation
1 parent 834aa16 commit c79b936

File tree

5 files changed

+970
-116
lines changed

5 files changed

+970
-116
lines changed

examples/unassigned-obstacles/AssignableViaAutoroutingPipelineSolver/AssignableViaAutoroutingPipelineSolver03.fixture.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export const simpleRouteJson: SimpleRouteJson = {
6262
connectedTo: [],
6363
width: 0.6,
6464
height: 0.6,
65+
netIsAssignable: true,
6566
},
6667
],
6768
connections: [

lib/solvers/AssignableViaAutoroutingPipeline/AssignableViaNodeMergerSolver.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export class AssignableViaNodeMergerSolver extends BaseSolver {
7373

7474
// Collect all unique z-layers from the nodes to merge
7575
const zLayersSet = new Set<number>()
76+
let containsTarget = false
7677

7778
for (const node of nodesToMerge) {
7879
const nodeMinX = node.center.x - node.width / 2
@@ -89,6 +90,11 @@ export class AssignableViaNodeMergerSolver extends BaseSolver {
8990
for (const z of node.availableZ) {
9091
zLayersSet.add(z)
9192
}
93+
94+
// Check if any node contains a target
95+
if (node._containsTarget) {
96+
containsTarget = true
97+
}
9298
}
9399

94100
const width = maxX - minX
@@ -110,6 +116,7 @@ export class AssignableViaNodeMergerSolver extends BaseSolver {
110116
? `z${availableZ[0]}`
111117
: `z${availableZ.join(",")}`,
112118
availableZ,
119+
_containsTarget: containsTarget,
113120
_containsObstacle: false,
114121
_completelyInsideObstacle: false,
115122
}

lib/solvers/AssignableViaAutoroutingPipeline/CapacityMeshNodeSolver_OnlyTraverseLayersInAssignableObstacles.ts

Lines changed: 126 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ interface CapacityMeshNodeSolverOptions {
1010
* aren't allowed, but there may be assignable vias on the PCB as obstacles.
1111
*
1212
* Behavior:
13-
* - Outside obstacles, multi-layer nodes are *always* Z-split into single-layer nodes.
14-
* - Inside an "assignable" obstacle (obstacle.netIsAssignable === true), we keep
15-
* exactly *one* multi-layer node (capacity ~ 1 via region), i.e. we do not Z-split it.
16-
* Further nodes completely inside the same assignable obstacle will be Z-split (one per layer).
13+
* - Outside assignable obstacles, multi-layer nodes are *always* Z-split into single-layer nodes.
14+
* - During mesh subdivision, nodes overlapping with "assignable" obstacles
15+
* (obstacle.netIsAssignable === true) are NOT filtered, allowing normal subdivision.
16+
* - After the main mesh subdivision is complete:
17+
* 1. All nodes overlapping with assignable obstacles are removed
18+
* 2. Each assignable obstacle is replaced with a single multi-layer node spanning all layers
1719
* - Single-layer nodes that are larger than MAX_SIZE_FOR_SINGLE_LAYER_NODES are XY-subdivided.
18-
* - IMPORTANT: the single multi-layer node inside an assignable obstacle is **traversable**,
19-
* so we mark `_containsObstacle = false` and `_completelyInsideObstacle = false` on it.
20+
* - IMPORTANT: the multi-layer nodes created from assignable obstacles are **traversable**,
21+
* so we mark `_containsObstacle = false` and `_completelyInsideObstacle = false` on them.
2022
*/
2123
export class CapacityMeshNodeSolver_OnlyTraverseLayersInAssignableObstacles extends CapacityMeshNodeSolver2_NodeUnderObstacle {
2224
MAX_SIZE_FOR_SINGLE_LAYER_NODES = 2 // 2x2mm
@@ -40,32 +42,6 @@ export class CapacityMeshNodeSolver_OnlyTraverseLayersInAssignableObstacles exte
4042
)
4143
}
4244

43-
private isNodeCompletelyInsideSpecificObstacle(
44-
node: CapacityMeshNode,
45-
obstacle: Obstacle,
46-
): boolean {
47-
const nb = this.getNodeBounds(node)
48-
const obsLeft = obstacle.center.x - obstacle.width / 2
49-
const obsRight = obstacle.center.x + obstacle.width / 2
50-
const obsTop = obstacle.center.y - obstacle.height / 2
51-
const obsBottom = obstacle.center.y + obstacle.height / 2
52-
53-
return (
54-
nb.minX >= obsLeft &&
55-
nb.maxX <= obsRight &&
56-
nb.minY >= obsTop &&
57-
nb.maxY <= obsBottom
58-
)
59-
}
60-
61-
private getAssignableContainer(node: CapacityMeshNode): Obstacle | null {
62-
const assignables = this.getOverlappingAssignableObstacles(node)
63-
for (const o of assignables) {
64-
return o
65-
}
66-
return null
67-
}
68-
6945
shouldNodeBeXYSubdivided(node: CapacityMeshNode) {
7046
if (node._depth! >= this.MAX_DEPTH) return false
7147
if (node._containsTarget) return true
@@ -82,27 +58,135 @@ export class CapacityMeshNodeSolver_OnlyTraverseLayersInAssignableObstacles exte
8258
}
8359

8460
/**
85-
* Multi-layer nodes are filtered unless they are completely inside an
86-
* assignable obstacle (the single allowed via region per obstacle).
87-
* Single-layer nodes use the standard relaxed single-layer filtering.
61+
* Filter nodes for obstacles, but skip filtering for assignable obstacles.
62+
* Assignable obstacles will be handled separately at the end.
8863
*/
8964
shouldFilterNodeForObstacle(node: CapacityMeshNode): boolean {
9065
if (!node._containsObstacle) return false
9166

67+
// Check if this node overlaps with any assignable obstacles
68+
const assignableObstacles = this.getOverlappingAssignableObstacles(node)
69+
if (assignableObstacles.length > 0) {
70+
// Don't filter - let the node be created, we'll remove it later
71+
return false
72+
}
73+
9274
if (node.availableZ.length === 1) {
9375
return this.shouldFilterSingleLayerNodeForObstacle(node)
9476
}
9577

96-
// Multi-layer: allowed only if it is the single node inside an assignable obstacle
97-
const container = this.getAssignableContainer(node)
98-
if (container) return false
99-
78+
// Multi-layer nodes (not in assignable obstacles) should be filtered
10079
return true
10180
}
10281

82+
/**
83+
* Remove nodes inside assignable obstacles and replace with single multi-layer nodes.
84+
* The new node's bounds are extended to cover all removed nodes' XY space.
85+
*/
86+
private insertAssignableObstaclesAsNodes() {
87+
const assignableObstacles = this.srj.obstacles.filter((o) =>
88+
this.isObstacleAssignable(o),
89+
)
90+
91+
// Map to track which nodes overlap with which obstacle
92+
const obstacleToNodesMap = new Map<Obstacle, CapacityMeshNode[]>()
93+
94+
for (const obstacle of assignableObstacles) {
95+
const overlappingNodes: CapacityMeshNode[] = []
96+
97+
for (const node of this.finishedNodes) {
98+
// Check if this node overlaps with this assignable obstacle
99+
const nodeOverlaps = this.getXYZOverlappingObstacles(node).some((o) => o === obstacle)
100+
if (nodeOverlaps) {
101+
overlappingNodes.push(node)
102+
}
103+
}
104+
105+
obstacleToNodesMap.set(obstacle, overlappingNodes)
106+
}
107+
108+
// Collect all nodes to remove
109+
const nodesToRemove = new Set<CapacityMeshNode>()
110+
for (const nodes of obstacleToNodesMap.values()) {
111+
for (const node of nodes) {
112+
nodesToRemove.add(node)
113+
}
114+
}
115+
116+
// Remove the nodes
117+
this.finishedNodes = this.finishedNodes.filter((node) => !nodesToRemove.has(node))
118+
119+
// Add a single multi-layer node for each assignable obstacle
120+
for (const obstacle of assignableObstacles) {
121+
const overlappingNodes = obstacleToNodesMap.get(obstacle) || []
122+
const availableZ = Array.from({ length: this.srj.layerCount }, (_, i) => this.srj.layerCount - i - 1)
123+
124+
// Calculate bounding box that covers all removed nodes
125+
let minX = obstacle.center.x - obstacle.width / 2
126+
let maxX = obstacle.center.x + obstacle.width / 2
127+
let minY = obstacle.center.y - obstacle.height / 2
128+
let maxY = obstacle.center.y + obstacle.height / 2
129+
130+
for (const node of overlappingNodes) {
131+
const nodeMinX = node.center.x - node.width / 2
132+
const nodeMaxX = node.center.x + node.width / 2
133+
const nodeMinY = node.center.y - node.height / 2
134+
const nodeMaxY = node.center.y + node.height / 2
135+
136+
minX = Math.min(minX, nodeMinX)
137+
maxX = Math.max(maxX, nodeMaxX)
138+
minY = Math.min(minY, nodeMinY)
139+
maxY = Math.max(maxY, nodeMaxY)
140+
}
141+
142+
const width = maxX - minX
143+
const height = maxY - minY
144+
const centerX = (minX + maxX) / 2
145+
const centerY = (minY + maxY) / 2
146+
147+
// Check if this extended area contains any target points
148+
let containsTarget = false
149+
for (const conn of this.srj.connections) {
150+
for (const point of conn.pointsToConnect) {
151+
if (
152+
point.x >= minX &&
153+
point.x <= maxX &&
154+
point.y >= minY &&
155+
point.y <= maxY
156+
) {
157+
containsTarget = true
158+
break
159+
}
160+
}
161+
if (containsTarget) break
162+
}
163+
164+
// Create a multi-layer node with extended bounds
165+
const node: CapacityMeshNode = {
166+
capacityMeshNodeId: `assignable_via_${obstacle.center.x}_${obstacle.center.y}`,
167+
center: { x: centerX, y: centerY },
168+
width,
169+
height,
170+
layer: availableZ.length === 1 ? `z${availableZ[0]}` : `z${availableZ.join(",")}`,
171+
availableZ,
172+
_depth: 0,
173+
_containsTarget: containsTarget,
174+
_containsObstacle: false,
175+
_completelyInsideObstacle: false,
176+
} as any
177+
178+
// Store the obstacle reference for later use
179+
;(node as any)._assignedViaObstacle = obstacle
180+
181+
this.finishedNodes.push(node)
182+
}
183+
}
184+
103185
_step() {
104186
const nextNode = this.unfinishedNodes.pop()
105187
if (!nextNode) {
188+
// Main subdivision complete, now insert assignable obstacles as nodes
189+
this.insertAssignableObstaclesAsNodes()
106190
this.solved = true
107191
return
108192
}
@@ -115,17 +199,9 @@ export class CapacityMeshNodeSolver_OnlyTraverseLayersInAssignableObstacles exte
115199
for (const childNode of childNodes) {
116200
const shouldBeXYSubdivided = this.shouldNodeBeXYSubdivided(childNode)
117201

118-
// Detect an assignable container that fully contains this node and is not yet claimed
119-
const assignableContainer =
120-
childNode.availableZ.length > 1 && !shouldBeXYSubdivided
121-
? this.getAssignableContainer(childNode)
122-
: null
123-
124-
// Z-subdivide multi-layer nodes except when this is the *one* allowed via region
202+
// Z-subdivide all multi-layer nodes (no special handling for assignable obstacles)
125203
const shouldBeZSubdivided =
126-
childNode.availableZ.length > 1 &&
127-
!shouldBeXYSubdivided &&
128-
!assignableContainer
204+
childNode.availableZ.length > 1 && !shouldBeXYSubdivided
129205

130206
if (shouldBeXYSubdivided) {
131207
unfinishedNewNodes.push(childNode)
@@ -148,21 +224,11 @@ export class CapacityMeshNodeSolver_OnlyTraverseLayersInAssignableObstacles exte
148224
continue
149225
}
150226

151-
// Not XY-subdivided and not Z-subdivided:
152-
// - a single-layer node that passes filtering, or
153-
// - the *single* multi-layer node inside an assignable obstacle
227+
// Not XY-subdivided and not Z-subdivided: single-layer node that passes filtering
154228
if (
155229
!this.shouldFilterNodeForObstacle(childNode) ||
156230
childNode._containsTarget
157231
) {
158-
if (assignableContainer) {
159-
// >>> IMPORTANT FIX <<<
160-
// The multi-layer node inside an assignable obstacle is traversable.
161-
// Mark it as *not* containing an obstacle.
162-
childNode._containsObstacle = false
163-
childNode._completelyInsideObstacle = false
164-
;(childNode as any)._assignedViaObstacle = assignableContainer
165-
}
166232
finishedNewNodes.push(childNode)
167233
}
168234
}

lib/solvers/AssignableViaAutoroutingPipeline/SingleLayerNodeMergerSolver_OnlyMergeTargets.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export class SingleLayerNodeMergerSolver_OnlyMergeTargets extends BaseSolver {
3434
this.absorbedNodeIds = new Set()
3535
const unprocessedNodesWithArea: Array<[CapacityMeshNode, number]> = []
3636
for (const node of nodes) {
37+
// Skip assignable via nodes - pass them through unchanged
38+
if ((node as any)._assignedViaObstacle) {
39+
this.newNodes.push(node)
40+
this.absorbedNodeIds.add(node.capacityMeshNodeId)
41+
continue
42+
}
43+
3744
// Only process nodes that contain targets
3845
if (!node._containsTarget) {
3946
this.newNodes.push(node)

0 commit comments

Comments
 (0)