[builder] major Pather/Planner/Tool rework

This commit is contained in:
Jan Petykiewicz 2026-07-08 23:58:24 -07:00
commit 22e645e527
28 changed files with 6036 additions and 2467 deletions

View file

@ -134,11 +134,8 @@ previously used `RenderPather`.
## `BasicTool` was replaced
`BasicTool` is no longer exported. Use:
- `SimpleTool` for the simple "one straight generator + one bend cell" case
- `AutoTool` if you need transitions, multiple candidate straights/bends, or
S-bends/U-bends
`BasicTool` is no longer exported. Use `AutoTool` for reusable straight, bend,
transition, and S-bend primitives.
### Old `BasicTool`
@ -157,55 +154,32 @@ tool = BasicTool(
### New `AutoTool`
```python
from masque.builder.tools import AutoTool
from masque.builder import AutoTool
tool = AutoTool(
straights=[
AutoTool.Straight(
ptype='m1wire',
fn=make_straight,
in_port_name='input',
out_port_name='output',
),
],
bends=[
AutoTool.Bend(
abstract=lib.abstract('bend'),
in_port_name='input',
out_port_name='output',
clockwise=True,
),
],
sbends=[],
transitions={
('m2wire', 'm1wire'): AutoTool.Transition(
lib.abstract('via'),
'top',
'bottom',
),
},
default_out_ptype='m1wire',
tool = (
AutoTool()
.add_straight('m1wire', make_straight, 'input')
.add_bend(lib.abstract('bend'), 'input', 'output', clockwise=True)
.add_transition(lib.abstract('via'), 'top', 'bottom')
)
```
The key differences are:
- `BasicTool` -> `SimpleTool` or `AutoTool`
- `straight=(fn, in_name, out_name)` -> `straights=[AutoTool.Straight(...)]`
- `bend=(abstract, in_name, out_name)` -> `bends=[AutoTool.Bend(...)]`
- transition keys are now `(external_ptype, internal_ptype)` tuples
- transitions use `AutoTool.Transition(...)` instead of raw tuples
If your old `BasicTool` usage did not rely on transitions or multiple routing
options, `SimpleTool` is the closest replacement.
- `BasicTool` -> `AutoTool`
- `straight=(fn, in_name, out_name)` -> `add_straight(ptype, fn, in_name)`
- `bend=(abstract, in_name, out_name)` -> `add_bend(abstract, in_name, out_name)`
- transitions are registered with `add_transition(abstract, external_port, internal_port)`
- transitions are bidirectional by default; pass `one_way=True` to inhibit the reverse adapter
## Custom `Tool` subclasses
If you maintain your own `Tool` subclass, the interface changed:
- `Tool.path(...)` became `Tool.traceL(...)`
- `Tool.traceS(...)` and `Tool.traceU(...)` were added for native S/U routes
- `planL()` / `planS()` / `planU()` remain the planning hooks used by deferred rendering
- `primitive_offers()` is now the planning boundary
- `render()` consumes committed primitive render tokens
- `Tool.path(...)`, `traceL()`, `traceS()`, `traceU()`, `planL()`,
`planS()`, and `planU()` are no longer part of the public `Tool` API
In practice, a minimal old implementation like:
@ -218,14 +192,168 @@ class MyTool(Tool):
should now become:
```python
from collections.abc import Sequence
from typing import Any
from masque import Port
from masque.builder import RenderStep, StraightOffer, Tool
class MyTool(Tool):
def traceL(self, ccw, length, **kwargs):
def primitive_offers(self, kind, *, in_ptype=None, out_ptype=None, **kwargs):
if kind != 'straight':
return ()
def endpoint(length):
ptype = out_ptype or in_ptype
return Port((length, 0), rotation=3.141592653589793, ptype=ptype)
def commit(length):
return {'length': length}
return (StraightOffer(
in_ptype=in_ptype,
out_ptype=out_ptype or in_ptype,
endpoint_planner=endpoint,
commit_planner=commit,
),)
def render(self, batch: Sequence[RenderStep], **kwargs: Any):
...
```
If you do not implement `traceS()` or `traceU()`, the unified pather will
either fall back to the planning hooks or synthesize those routes from simpler
steps where possible.
If a tool does not provide a primitive kind, return `()` for that kind. `Pather`
will compose available primitive offers where the route family allows it.
### Primitive offers
Tools describe legal routing primitives through `Tool.primitive_offers()`.
`Pather` composes those primitive offers to implement `trace()`, `jog()`,
`uturn()`, and `trace_into()`.
For custom tools, construct the concrete offer class that matches the primitive
you are exposing:
- `StraightOffer` for non-turning length-parameterized primitives
- `BendOffer` for single-turn length-parameterized primitives
- `SOffer` for S-like jog-parameterized primitives
- `UOffer` for U-like jog-parameterized primitives
`PrimitiveOffer` is the shared base type used for generic annotations and
common callback behavior. It is not the normal class users should instantiate.
The concrete offer classes carry the semantic fields (`length_domain`,
`jog_domain`, `ccw`) so tools do not need to encode primitive identity in
strings.
Minimal straight-only example:
```python
from collections.abc import Sequence
from typing import Literal
from masque import Port
from masque.builder import RenderStep, StraightOffer, Tool
class MyTool(Tool):
def primitive_offers(
self,
kind: Literal['straight', 'bend', 's', 'u'],
*,
in_ptype=None,
out_ptype=None,
**kwargs,
):
if kind != 'straight':
return ()
def endpoint(length):
ptype = out_ptype or in_ptype
return Port((length, 0), rotation=3.141592653589793, ptype=ptype)
def commit(length):
return {'length': length}
return (StraightOffer(
in_ptype=in_ptype,
out_ptype=out_ptype or in_ptype,
endpoint_planner=endpoint,
commit_planner=commit,
),)
def render(self, batch: Sequence[RenderStep], **kwargs):
...
```
Primitive offers are local planning objects:
- `endpoint_at(parameter)` returns the local output `Port`
- `cost_at(parameter)` returns an additive scalar route-selection cost
- `bbox_at(parameter)` returns local primitive bounds when a footprint hook is supplied
- `parameterized_bbox` may carry opaque future-router footprint metadata
- `commit(parameter)` returns opaque render data consumed later by `render()`
- `(min, max)` parameter domains are half-open; `(value, value)` is a fixed singleton
- selected parameter values must be finite; domains may use infinite open bounds but not `NaN`
- `None` and `"unk"` ptypes are wildcards; concrete ptype mismatches reject an offer
Heterogeneous `StraightOffer` and `SOffer` objects may be used as ptype
adapters. Requested `out_ptype` constrains only the final route endpoint; any
intermediate ptypes are chosen by the route solver.
`Tool` subclasses must override `primitive_offers()` and return `()` themselves
for recognized unsupported kinds. There is no route-level `plan*()` fallback.
Omitted-length S/U behavior comes from direct `SOffer` and `UOffer` endpoint
domains or from composed straight/bend primitives.
Offer constructors accept split `endpoint_planner` and `commit_planner`
callbacks. Provide both callbacks or override the offer methods in a subclass;
partial callback configurations are rejected during offer construction.
When writing direct primitive offers, declare the actual endpoint ptype
produced by the offer if it can differ from the requested value; `Pather`
validates evaluated endpoints against the declared offer ptype.
Stable imports for custom tool authors live in `masque.builder`. The
`masque.builder.planner` module is an internal planner implementation; do not
import it from user code.
`trace_into()` uses the same primitive-offer route selection and now searches
bounded route topologies with up to four bend roles. This preserves the common
straight, bend, S-like, U-like, and dogleg cases while allowing routes that
need an additional bounded bend pair. Among legal bounded candidates,
`trace_into()` selects the lowest total primitive-offer cost; bend count and
step count are used only to break exact cost ties.
Explicit-length `jog()` routes may also be satisfied by composing a straight
primitive before or after an omitted-length native S primitive. `uturn()` routes
may compose a straight primitive before an omitted-length native U primitive.
These compositions are used when they are the lowest-cost legal route for the
explicit request.
`AutoTool` can attach `bbox_at()` hooks to its primitive offers by rendering the
selected primitive into a temporary pattern and measuring it. If the rendered
primitive contains reusable refs, pass the source library as `bbox_library=...`;
normal routing does not require this.
### Omitted-length routing
Single-port omitted-length calls now evaluate legal primitive routes at their
minimum legal length-like parameter, or at their intrinsic endpoint length when
the requested offset fixes the primitive geometry. Cost then selects among
those minimum-length candidates:
```python
pather.trace('A', None) # minimum straight-like route
pather.jog('A', offset=2) # minimum S-like route for that offset
pather.uturn('A', offset=4) # minimum U-like route for that offset
```
For U-turns, use explicit `length=0` to request the old zero-public-length
shape:
```python
pather.uturn('A', offset=4, length=0)
```
## Transform semantics changed
@ -279,7 +407,6 @@ If you install the DXF extra, the supported `ezdxf` baseline moved from
These are additive, but available now from `masque` and `masque.builder`:
- `PortPather`
- `SimpleTool`
- `AutoTool`
- `boolean`
@ -289,7 +416,7 @@ If your code uses the routing stack, do these first:
1. Replace `path`/`path_to`/`mpath`/`path_into` calls with
`trace`/`trace_to`/multi-port `trace`/`trace_into`.
2. Replace `BasicTool` with `SimpleTool` or `AutoTool`.
2. Replace `BasicTool` with `AutoTool`.
3. Fix imports that still reference `masque.builder.builder` or
`masque.builder.renderpather`.
4. Audit any low-level `mirror()` usage, especially on `Port` and `Ref`.