Skip to content

Rendering

Drawing a layout as a picture, for looking at rather than for loading. Layout defects are visual and coordinates are not: a sizes that divides wrongly, a gap that eats a row and a labelled that mis-splits are otherwise assertable one frame at a time.

doc = py2tosc.load("mixer.tosc")
Path("mixer.svg").write_text(py2tosc.to_svg(doc))

Nothing reads SVG back, which is why this is render rather than a convert target: convert means the same layout in another encoding and implies the reverse works.

What it promises is that a reader can see where every control is, how big it is and what kind it is -- not that it matches a screenshot. TouchOSC cannot be scripted, so there is no oracle for pixel fidelity. A Lua script can set any property at runtime, so what is drawn is the document rather than the running state.

Rules live in one <style> inside the SVG, so the file travels alone and draws through an <img src>. Classes are prefixed p2t- so an inline SVG cannot leak selectors into the page. Per-control values are emitted as custom properties rather than baked into coordinates: a fader's bar is driven by --v.

to_svg(doc, clip=True) cuts a control off at the edge of the one holding it, as TouchOSC does. Off by default: an overflowing control is a defect the picture is being drawn to find.

to_html wraps the same SVG in a page carrying the layout's name, its control count and whatever validate found. The output extension picks the wrapper.

$ py2tosc render mixer.tosc -o mixer.html

Pages of a pager are laid out side by side in HTML as well, not behind tabs. pagers.tosc holds four pagers and :target matches one element per document, so independent tabs would need radio inputs positioned over the SVG at each pager's coordinates plus a second rendering mode where pages stack.

Provisional under the stability policy, like py2tosc.ui.

to_svg

to_svg(doc: Document, *, clip: bool = False) -> str

Draw one layout as SVG.

Parameters:

Name Type Description Default
doc Document

The layout. Resolve it first if it was built by py2tosc.ui; what is drawn is whatever frames the controls carry.

required
clip bool

Whether a control is cut off at the edge of the one holding it, the way TouchOSC draws it. Off by default, and the default is the decision: a control that overflows its parent is exactly the kind of defect a picture is being drawn to find, and clipping hides it behind a layout that looks correct. Turn it on to see what the device will actually show. A pager is never clipped either way, since its pages are laid out past its own frame on purpose.

False

Returns:

Type Description
str

The SVG, as text.

Source code in src/py2tosc/render.py
def to_svg(doc: Document, *, clip: bool = False) -> str:
    """Draw one layout as SVG.

    Args:
        doc: The layout. Resolve it first if it was built by `py2tosc.ui`;
            what is drawn is whatever frames the controls carry.
        clip: Whether a control is cut off at the edge of the one holding it,
            the way TouchOSC draws it. Off by default, and the default is the
            decision: a control that overflows its parent is exactly the kind
            of defect a picture is being drawn to find, and clipping hides it
            behind a layout that looks correct. Turn it on to see what the
            device will actually show. A pager is never clipped either way,
            since its pages are laid out past its own frame on purpose.

    Returns:
        The SVG, as text.
    """
    width, height = _extent(doc.root, 0.0, 0.0)
    out = [
        (
            f'<svg xmlns="http://www.w3.org/2000/svg" '
            f'viewBox="0 0 {_n(width)} {_n(height)}" '
            f'width="{_n(width)}" height="{_n(height)}">'
        ),
        f"  <style>{STYLESHEET}  </style>",
    ]
    out += _node(doc.root, 0, clip, [0])
    out.append("</svg>")
    return "\n".join(out) + "\n"

to_html

to_html(doc: Document, *, clip: bool = False) -> str

Draw one layout as a page with the picture in it.

The same SVG to_svg produces, with a page around it -- not a second renderer. That is what keeps anything added later, a runtime included, additive rather than a second code path to keep in step.

What the page adds is the context a bare picture has nowhere to put: what the layout is called, how many controls it holds, and what validate says about it. A layout with a control the editor will refuse is worth knowing about while looking at the picture rather than afterwards.

Parameters:

Name Type Description Default
doc Document

The layout, resolved.

required
clip bool

As to_svg.

False

Returns:

Type Description
str

A complete HTML document, as text.

Source code in src/py2tosc/render.py
def to_html(doc: Document, *, clip: bool = False) -> str:
    """Draw one layout as a page with the picture in it.

    The same SVG `to_svg` produces, with a page around it -- not a second
    renderer. That is what keeps anything added later, a runtime included,
    additive rather than a second code path to keep in step.

    What the page adds is the context a bare picture has nowhere to put: what
    the layout is called, how many controls it holds, and what `validate` says
    about it. A layout with a control the editor will refuse is worth knowing
    about while looking at the picture rather than afterwards.

    Args:
        doc: The layout, resolved.
        clip: As `to_svg`.

    Returns:
        A complete HTML document, as text.
    """
    name = doc.root.get("name") or "layout"
    controls = len(list(doc.walk()))
    issues = doc.validate()

    reported = ""
    if issues:
        listed = "".join(f"<li>{escape(str(issue))}</li>" for issue in issues)
        reported = f'<ul class="issues">{listed}</ul>'

    return (
        "<!doctype html>\n"
        '<html lang="en">\n<head>\n<meta charset="utf-8">\n'
        '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
        f"<title>{escape(str(name))}</title>\n"
        f"<style>{PAGE_STYLE}</style>\n</head>\n<body>\n"
        f"<header>\n<h1>{escape(str(name))}</h1>\n"
        f'<div class="meta">{controls} controls'
        f"{f', {len(issues)} issue(s)' if issues else ''}</div>\n"
        f"{reported}\n</header>\n"
        f"<figure>\n{to_svg(doc, clip=clip)}</figure>\n"
        "</body>\n</html>\n"
    )

STYLESHEET module-attribute

STYLESHEET = f'
.{PREFIX}-shape {
    fill: var(--fill, none);
    fill-opacity: 0.45;
    stroke: var(--line, none);
}
.{PREFIX}-shape.{PREFIX}-outlined { stroke-width: 2; }
.{PREFIX}-shape.{PREFIX}-track { fill-opacity: 0.15; }
.{PREFIX}-mark { stroke: var(--line, #888); stroke-width: 1; fill: none; }
.{PREFIX}-bar {
    fill: var(--line, #888);
    fill-opacity: 0.55;
    transform-box: fill-box;
    transform-origin: bottom;
    transform: scaleY(var(--v, 0));
}
.{PREFIX}-bar.{PREFIX}-across {
    transform-origin: left;
    transform: scaleX(var(--v, 0));
}
/* The handle, and the reason a fader at zero is still legible: it has real
   thickness and rides `--v` up the track, where a bar scaled to nothing
   leaves an empty box. `--span` is the travel, which is static. */
.{PREFIX}-cursor {
    fill: var(--line, #888);
    transform: translateY(calc((1 - var(--v, 0)) * var(--span, 0px)));
}
.{PREFIX}-cursor.{PREFIX}-across {
    transform: translateX(calc(var(--v, 0) * var(--span, 0px)));
}
.{PREFIX}-rule { stroke: var(--line, #888); stroke-width: 1; opacity: 0.35; }
.{PREFIX}-step { fill: var(--line, #888); fill-opacity: 0.55; }
.{PREFIX}-page-name { fill: #888; }
.{PREFIX}-cap { fill: none; stroke: var(--line, #888); stroke-width: 1.5; }
.{PREFIX}-dial { fill: none; stroke: var(--line, #888); stroke-width: 2; }
.{PREFIX}-cross { stroke: var(--line, #888); stroke-width: 1; opacity: 0.8; }
.{PREFIX}-text {
    fill: var(--ink, #111);
    font-family: var(--face, system-ui, sans-serif);
    dominant-baseline: middle;
}
.{PREFIX}-hidden { opacity: 0.25; }
.{PREFIX}-hidden .{PREFIX}-shape { stroke-dasharray: 4 3; }
'