@@ -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 */
2123export 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 }
0 commit comments