masque/MIGRATION.md

14 KiB

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

# 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:

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.

(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:

# 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:

# 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

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

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:

class MyTool(Tool):
    def path(self, ccw, length, **kwargs):
        ...

should now become:

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:

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:

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:

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:

port.mirror(0)   # changed both offset and orientation

New behavior:

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.