# Migration Guide This guide covers changes between the git tag `release` and the current tree. At `release`, `masque.__version__` was `3.3`; the current tree reports `3.4`. Most downstream changes are in `masque/builder/*`, but there are a few other API changes that may require code updates. ## Routing API: renamed and consolidated The routing helpers were consolidated into a single implementation in `masque/builder/pather.py`. The biggest migration point is that the old routing verbs were renamed: | Old API | New API | | --- | --- | | `Pather.path(...)` | `Pather.trace(...)` | | `Pather.path_to(...)` | `Pather.trace_to(...)` | | `Pather.mpath(...)` | `Pather.trace(...)` / `Pather.trace_to(...)` with multiple ports | | `Pather.pathS(...)` | `Pather.jog(...)` | | `Pather.pathU(...)` | `Pather.uturn(...)` | | `Pather.path_into(...)` | `Pather.trace_into(...)` | | `Pather.path_from(src, dst)` | `Pather.at(src).trace_into(dst)` | | `RenderPather.path(...)` | `Pather(..., auto_render=False).trace(...)` | | `RenderPather.path_to(...)` | `Pather(..., auto_render=False).trace_to(...)` | | `RenderPather.mpath(...)` | `Pather(..., auto_render=False).trace(...)` / `Pather(..., auto_render=False).trace_to(...)` | | `RenderPather.pathS(...)` | `Pather(..., auto_render=False).jog(...)` | | `RenderPather.pathU(...)` | `Pather(..., auto_render=False).uturn(...)` | | `RenderPather.path_into(...)` | `Pather(..., auto_render=False).trace_into(...)` | | `RenderPather.path_from(src, dst)` | `Pather(..., auto_render=False).at(src).trace_into(dst)` | There are also new convenience wrappers: - `straight(...)` for `trace_to(..., ccw=None, ...)` - `ccw(...)` for `trace_to(..., ccw=True, ...)` - `cw(...)` for `trace_to(..., ccw=False, ...)` - `jog(...)` for S-bends - `uturn(...)` for U-bends Important: `Pather.path()` is no longer the routing API. It now forwards to `Pattern.path()` and creates a geometric `Path` element. Any old routing code that still calls `pather.path(...)` must be renamed. ### Common rewrites ```python # old pather.path('VCC', False, 6_000) pather.path_to('VCC', None, x=0) pather.mpath(['GND', 'VCC'], True, xmax=-10_000, spacing=5_000) pather.pathS('VCC', offset=-2_000, length=8_000) pather.pathU('VCC', offset=4_000, length=5_000) pather.path_into('src', 'dst') pather.path_from('src', 'dst') # new pather.cw('VCC', 6_000) pather.straight('VCC', x=0) pather.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000) pather.jog('VCC', offset=-2_000, length=8_000) pather.uturn('VCC', offset=4_000, length=5_000) pather.trace_into('src', 'dst') pather.at('src').trace_into('dst') ``` If you prefer the more explicit spelling, `trace(...)` and `trace_to(...)` remain the underlying primitives: ```python pather.trace('VCC', False, 6_000) pather.trace_to('VCC', None, x=0) ``` ## `PortPather` and `.at(...)` Routing can now be written in a fluent style via `.at(...)`, which returns a `PortPather`. ```python (rpather.at('VCC') .trace(False, length=6_000) .trace_to(None, x=0) ) ``` This is additive, not required for migration. Existing code can stay with the non-fluent `Pather` methods after renaming the verbs above. Old `PortPather` helper names were also cleaned up: | Old API | New API | | --- | --- | | `save_copy(...)` | `mark(...)` | | `rename_to(...)` | `rename(...)` | Example: ```python # old pp.save_copy('branch') pp.rename_to('feed') # new pp.mark('branch') pp.rename('feed') ``` ## Imports and module layout `Pather` now provides the remaining builder/routing surface in `masque/builder/pather.py`. The old module files `masque/builder/builder.py` and `masque/builder/renderpather.py` were removed. Update imports like this: ```python # old from masque.builder.builder import Builder from masque.builder.renderpather import RenderPather # new from masque.builder import Pather builder = Pather(...) deferred = Pather(..., auto_render=False) ``` Top-level imports from `masque` also continue to work. `Pather` now defaults to `auto_render=True`, so plain construction replaces the old `Builder` behavior. Use `Pather(..., auto_render=False)` where you previously used `RenderPather`. ## `BasicTool` was replaced `BasicTool` is no longer exported. Use `AutoTool` for reusable straight, bend, transition, and S-bend primitives. ### Old `BasicTool` ```python from masque.builder.tools import BasicTool tool = BasicTool( straight=(make_straight, 'input', 'output'), bend=(lib.abstract('bend'), 'input', 'output'), transitions={ 'm2wire': (lib.abstract('via'), 'top', 'bottom'), }, ) ``` ### New `AutoTool` ```python from masque.builder import AutoTool tool = ( AutoTool() .add_straight(make_straight, 'm1wire', 'input') .add_bend(lib.abstract('bend'), 'input', 'output', clockwise=True) .add_transition(lib.abstract('via'), 'top', 'bottom') ) ``` The key differences are: - `BasicTool` -> `AutoTool` - `straight=(fn, in_name, out_name)` -> `add_straight(fn, ptype, 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: - `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: ```python class MyTool(Tool): def path(self, ccw, length, **kwargs): ... ``` 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 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 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 The other major user-visible change is that `mirror()` and `rotate()` are now treated more consistently as intrinsic transforms on low-level objects. The practical migration rule is: - use `mirror()` / `rotate()` when you want to change the object relative to its own origin - use `flip_across(...)`, `rotate_around(...)`, or container-level transforms when you want to move the object in its parent coordinate system ### Example: `Port` Old behavior: ```python port.mirror(0) # changed both offset and orientation ``` New behavior: ```python port.mirror(0) # changes orientation only port.flip_across(axis=0) # old "mirror in the parent pattern" behavior ``` ### What to audit Check code that calls: - `Port.mirror(...)` - `Ref.rotate(...)` - `Ref.mirror(...)` - `Label.rotate_around(...)` / `Label.mirror(...)` If that code expected offsets or repetition grids to move automatically, it needs updating. For whole-pattern transforms, prefer calling `Pattern.mirror()` or `Pattern.rotate_around(...)` at the container level. ## Other user-facing changes ### DXF environments If you install the DXF extra, the supported `ezdxf` baseline moved from `~=1.0.2` to `~=1.4`. Any pinned environments should be updated accordingly. ### New exports These are additive, but available now from `masque` and `masque.builder`: - `PortPather` - `AutoTool` - `boolean` ## Minimal migration checklist 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 `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`. If your code only uses `Pattern`, `Library`, `place()`, and `plug()` without the routing helpers, you may not need any changes beyond the transform audit and any stale imports.