@@ -120,10 +120,42 @@ def hex_setup(self):
120120 shapely .points (coordinates ), self .route_radius , quad_segs = 16 )
121121 self .route_tree = STRtree (route_disks )
122122 self .route_point_tree = STRtree (shapely .points (coordinates ))
123+ self .route_outline = (
124+ self .outline_polygon if self .outline_polygon is not None
125+ else sg .box (0 , 0 , * self .size ))
126+ outlines = self .layers ['GML' ].lines
127+ if outlines :
128+ self .route_outline = max (
129+ (sg .Polygon (line ) for line in outlines ), key = lambda p : p .area )
130+ self .edge_block_cache = {}
123131 self .blocked = {layer : self .layer_blocks (layer ) for layer in ('GTL' , 'GBL' )}
124132 self .routes = []
125133 self .route_widths = []
126134
135+ def edge_blocks (self , width ):
136+ """Reserve trace radius and edge clearance against the perimeter.
137+
138+ Convex insets contain the segments between allowed centers. Concave
139+ outlines need an extra half-step margin to protect those segments.
140+ """
141+ clearance = float (getattr (self , "hex_edge_clearance" , 0 ))
142+ assert math .isfinite (clearance ) and clearance >= 0
143+ key = (width , clearance )
144+ if key not in self .edge_block_cache :
145+ segment_margin = (
146+ 0 if self .route_outline .equals (self .route_outline .convex_hull )
147+ else self .hr )
148+ interior = self .route_outline .buffer (
149+ - (clearance + width / 2 + segment_margin ))
150+ blocked = self .gr .zeros (np .uint8 ) | (self .gr .valid == 0 )
151+ points = shapely .points ([h .to_plane () for h in self .route_hexes ])
152+ inside = shapely .covers (interior , points )
153+ for h , allowed in zip (self .route_hexes , inside ):
154+ if not allowed :
155+ blocked [h .q , h .r ] = 1
156+ self .edge_block_cache [key ] = blocked
157+ return self .edge_block_cache [key ].copy ()
158+
127159 def layer_blocks (
128160 self , nm , width = None , exempt_points = (),
129161 exempt_geometries = ()):
@@ -155,7 +187,7 @@ def layer_blocks(
155187 layer_poly = so .unary_union (
156188 copper + drill_keepouts + self .keepouts +
157189 self .route_keepouts [nm ]).buffer (0 )
158- blocked = self .gr . zeros ( np . uint8 ) | ( self . gr . valid == 0 )
190+ blocked = self .edge_blocks ( width )
159191 for i in self .route_tree .query (
160192 layer_poly , predicate = "intersects" ):
161193 h = self .route_hexes [i ]
@@ -169,7 +201,7 @@ def layer_blocks(
169201 fixed_geometry = fixed_geometry .buffer (geometry_expansion )
170202 layer_poly = so .unary_union (
171203 [fixed_geometry ] + drill_keepouts ).buffer (0 )
172- blocked = self .gr . zeros ( np . uint8 ) | ( self . gr . valid == 0 )
204+ blocked = self .edge_blocks ( width )
173205 for i in self .route_tree .query (layer_poly , predicate = "intersects" ):
174206 h = self .route_hexes [i ]
175207 blocked [h .q , h .r ] = 1
@@ -247,6 +279,11 @@ def _hex_route_pad_endpoints(self, a, b):
247279 self .pad_hex_cells (b )
248280 if isinstance (b , PadEndpoint )
249281 else frozenset ((tuple (Hex .from_xy (* target .xy )),)))
282+ edge_blocked = self .edge_blocks (self .trace )
283+ for endpoint , cells in ((a , source_cells ), (b , target_cells )):
284+ if not isinstance (endpoint , PadEndpoint ):
285+ assert all (not edge_blocked [q , r ] for q , r in cells ), (
286+ "Route endpoint violates board-edge clearance" )
250287 exempt_geometries = tuple (
251288 endpoint .draw .boundary
252289 for endpoint in (a , b )
@@ -313,6 +350,9 @@ def hex_route(self, a, b):
313350 target = b
314351 a = Hex .from_xy (* source .xy )
315352 b = Hex .from_xy (* target .xy )
353+ edge_blocked = self .edge_blocks (self .trace )
354+ assert not edge_blocked [a .q , a .r ] and not edge_blocked [b .q , b .r ], (
355+ "Route endpoint violates board-edge clearance" )
316356
317357 wavefront = set ([tuple (a )])
318358 dirs = [Hex (dq ,dr ) for (dq , dr ) in axial_direction_vectors ]
@@ -381,6 +421,11 @@ def hex_route_net(self, terminals, width=None):
381421 else frozenset ((tuple (Hex .from_xy (* draw .xy )),)))
382422 for terminal , draw in zip (terminals , draws )
383423 ]
424+ edge_blocked = self .edge_blocks (route_width )
425+ for terminal , cells in zip (terminals , endpoint_cells ):
426+ if not isinstance (terminal , PadEndpoint ):
427+ assert all (not edge_blocked [q , r ] for q , r in cells ), (
428+ "Route endpoint violates board-edge clearance" )
384429 exempt_geometries = tuple (
385430 terminal .draw .boundary
386431 for terminal in terminals
0 commit comments