<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>bylr.info</title>
    <link>https://bylr.info/</link>
    <description>Recent content on bylr.info</description>
    <image>
      <url>https://bylr.info/android-chrome-512x512.png</url>
      <link>https://bylr.info/android-chrome-512x512.png</link>
    </image>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <copyright>&amp;copy; 2022-2023 Antoine Beyeler –</copyright>
    <lastBuildDate>Sat, 30 Dec 2023 00:00:00 +0000</lastBuildDate><atom:link href="https://bylr.info/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>TIL: displaying contributor avatars in GitHub changelogs</title>
      <link>https://bylr.info/articles/2023/12/30/til-displaying-contributor-avatars-in-github-changelogs/</link>
      <pubDate>Sat, 30 Dec 2023 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2023/12/30/til-displaying-contributor-avatars-in-github-changelogs/</guid>
      <description>While working on a changelog-generation script for my vsvg project, I wanted to display the list of contributors as circular avatars, just like GitHub does in multiple places.
After a few Google searches and some failed attempts, I identified a couple of key tricks.
The first is about obtaining the avatar image for a given GitHub account. Although the URL is hard to predict, adding .png to the account page acts as redirection.</description>
      <content:encoded><![CDATA[<p>While working on a  <a href="https://github.com/abey79/vsvg/blob/master/CHANGELOG.md">changelog</a>-generation <a href="https://github.com/abey79/vsvg/blob/master/scripts/changelog.py">script</a> for my <a href="https://github.com/abey79/vsvg">vsvg</a> project, I wanted to display the list of contributors as circular avatars, just like GitHub does in multiple places.</p>
<p>After a few Google searches and some failed attempts, I identified a couple of key tricks.</p>
<p>The first is about obtaining the avatar image for a given GitHub account. Although the URL  is hard to predict, adding <code>.png</code> to the account page acts as redirection. For example, this url:</p>
<p><a href="https://github.com/abey79.png">https://github.com/abey79.png</a></p>
<p>redirects to:</p>
<p><a href="https://avatars.githubusercontent.com/u/49431240?v=4">https://avatars.githubusercontent.com/u/49431240?v=4</a></p>
<p>Which corresponds to this image:</p>
<p><img loading="lazy" src="https://avatars.githubusercontent.com/u/49431240?v=4" alt="my avatar"  />
</p>
<p>The second trick is about resizing, masking, and caching that image to generate the circular avatar. It turns out that the free and open-source service <a href="https://wsrv.nl">wsrv.nl</a> does <em>exactly</em> this. You can check the <a href="https://wsrv.nl/docs/">documentation</a> for details, but this URL does the trick:</p>
<p><a href="https://wsrv.nl/?url=github.com/abey79.png?w=64&amp;h=64&amp;mask=circle&amp;fit=cover&amp;maxage=1w">https://wsrv.nl/?url=github.com/abey79.png?w=64&amp;h=64&amp;mask=circle&amp;fit=cover&amp;maxage=1w</a></p>
<p>Here is the corresponding image:</p>
<p><img loading="lazy" src="https://wsrv.nl/?url=github.com/abey79.png?w=64&amp;h=64&amp;mask=circle&amp;fit=cover&amp;maxage=1w" alt="my avatar, small and circular"  />
</p>
<p>The parameters specify a scaling to 64x64 pixels with a tight fit, a circular mask, and a cache expiration of one week.</p>
<p>For the final markdown code, I further set the image size to 32x32 pixels for a nice HiDPI look:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-md" data-lang="md">[<span style="color:#000080">&lt;img src=&#34;https://wsrv.nl/?url=github.com/abey79.png?w=64&amp;h=64&amp;mask=circle&amp;fit=cover&amp;maxage=1w&#34; width=&#34;32&#34; height=&#34;32&#34; alt=&#34;abey79&#34; /&gt;</span>](<span style="color:#008080">https://github.com/abey79</span>)
</code></pre></div><p>Here is how it looks (my blog is markdown-based too, so I literally copy/pasted the line above):</p>
<p><a href="https://github.com/abey79"><img src="https://wsrv.nl/?url=github.com/abey79.png?w=64&h=64&mask=circle&fit=cover&maxage=1w" width="32" height="32" alt="abey79" /></a></p>
<p>That&rsquo;s it! You can see it in action in a <a href="https://github.com/abey79/vsvg/blob/master/CHANGELOG.md#contributors">changelog file</a> or a <a href="https://github.com/abey79/vsvg/releases/tag/v0.3.0">GitHub release</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>On vpype layers, their names, and the AxiDraw</title>
      <link>https://bylr.info/articles/2023/03/17/layer-names/</link>
      <pubDate>Fri, 17 Mar 2023 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2023/03/17/layer-names/</guid>
      <description>This article explores the topic of layers, their name, and how they can be used to control the AxiDraw plotting process.</description>
      <content:encoded><![CDATA[<p>The <a href="https://axidraw.com">AxiDraw</a> software, including the Inkscape plug-in, the AxiDraw CLI, and the Python module, offers a pretty neat way to control the plotting process with special escape codes in layer names. <em>vpype</em> doesn&rsquo;t have an explicit support for this feature <a href="https://github.com/abey79/vpype/issues/604">yet</a>, but it can give arbitrary names to layers using the <a href="https://vpype.readthedocs.io/en/latest/reference.html#name"><code>name</code></a> command&mdash;including ones that the AxiDraw understands.</p>
<p>This article explores the topic of layers, their name, and how they can be used to control the AxiDraw plotting process.</p>
<h2 id="svg-has-layers">SVG has layers?</h2>
<p>Actually, no.</p>
<p>Instead, SVG has groups, which can be nested or omitted altogether, which is not exactly the same thing. For this reason, the Inkscape project has introduced an <a href="https://inkscape.org/namespaces/inkscape/">XML namespace</a> named <a href="https://inkscape.org/namespaces/inkscape/"><code>inkscape</code></a> to support its layer feature. Amongst other thing, it defines the <code>inkscape:groupmode=&quot;layer&quot;</code> group attribute, to mark a top-level group as layer, and the <code>inskscape:label</code> attribute, to assign a label&mdash;or name&mdash;to the layer:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-svg" data-lang="svg"><span style="color:#999;font-weight:bold;font-style:italic">&lt;?xml version=&#34;1.0&#34; encoding=&#34;utf-8&#34; ?&gt;</span>
<span style="color:#000080">&lt;svg</span> <span style="color:#008080">xmlns=</span><span style="color:#d14">&#34;http://www.w3.org/2000/svg&#34;</span>
     <span style="color:#008080">xmlns:inkscape=</span><span style="color:#d14">&#34;http://www.inkscape.org/namespaces/inkscape&#34;</span>
     <span style="color:#008080">height=</span><span style="color:#d14">&#34;10.0cm&#34;</span> <span style="color:#008080">width=</span><span style="color:#d14">&#34;10.0cm&#34;</span><span style="color:#000080">&gt;</span>
  <span style="color:#000080">&lt;g</span> <span style="color:#008080">inkscape:groupmode=</span><span style="color:#d14">&#34;layer&#34;</span> <span style="color:#008080">inkscape:label=</span><span style="color:#d14">&#34;my layer label&#34;</span> <span style="color:#000080">&gt;</span>
      <span style="color:#998;font-style:italic">&lt;!-- first layer&#39;s content --&gt;</span>
  <span style="color:#000080">&lt;/g&gt;</span>
  <span style="color:#000080">&lt;g</span> <span style="color:#008080">inkscape:groupmode=</span><span style="color:#d14">&#34;layer&#34;</span> <span style="color:#008080">inkscape:label=</span><span style="color:#d14">&#34;my other layer&#34;</span> <span style="color:#000080">&gt;</span>
      <span style="color:#998;font-style:italic">&lt;!-- second layer&#39;s content --&gt;</span>
  <span style="color:#000080">&lt;/g&gt;</span>
<span style="color:#000080">&lt;/svg&gt;</span>
</code></pre></div><p>The Inkscape extension has become the <em>de facto</em> standard for SVG layer definition and naming, and is used by both <em>vpype</em> and the AxiDraw software.</p>
<h2 id="vpype-has-layers"><em>vpype</em> has layers?</h2>
<p><a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#lines-and-layers">Yes</a>.</p>
<p>Layers are first-class citizens in <em>vpype</em>, and, like any good citizen, they are primarily identified by a non-zero, positive integer identification number, in this case called &ldquo;layer ID&rdquo;. When loading an SVG, <em>vpype</em> try to construct its layer based on their Inkscape label, if any, their SVG group <code>id</code> (<code>&lt;g id=&quot;...&quot;&gt;</code>) if any, or sequentially otherwise. For example, when <code>read</code>ing an Inkscape file with two layers named &ldquo;layer 4&rdquo; and &ldquo;some layer 6&rdquo;, <em>vpype</em> will assign layer IDs 4 and 6 to these layers<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. If the label and group ID lack numerical digits, then <em>vpype</em> will revert to using sequential IDs.</p>
<p>One of the easiest ways to visualise layers and their IDs is to use the <a href="https://vpype.readthedocs.io/en/latest/reference.html#show"><code>show</code></a> and the viewer&rsquo;s layer menu:</p>
<p><img loading="lazy" src="layer_menu.jpg" alt="vpype displaying a SVG and its layers"  />
</p>
<p>Both the layer ID and the label, if any, is shown.</p>
<p>Many <em>vpype</em> commands operate on a layer-by-layer basis. They are known as <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#layer-processors">layer processors</a>. By default, they operate on <em>all</em> layers&mdash;for example <code>translate 0 2cm</code> translates the whole document by 2 cm vertically. For more control, the exact layer(s) on which the command should operate can be specified using the <code>--layer LID</code> option (or <code>-l LID</code> for short). For example, <code>translate -l 3 0 2cm</code> will translate <em>only</em> layer 3. Likewise, <code>translate -l 3,4 0 2cm</code> will translate both layer 3 and 4.</p>
<h2 id="axidraw-escape-codes">AxiDraw escape codes</h2>
<p>The <a href="https://www.evilmadscientist.com">EMSL</a> <a href="https://wiki.evilmadscientist.com/Main_Page">wiki</a> contains a <a href="https://wiki.evilmadscientist.com/AxiDraw_Layer_Control#Syntax">thorough description</a> of the supported escape codes and their effect. Here is a summary of what can be achieved:</p>
<ul>
<li>By prefixing the layer name with a number in the range of 0 to 1000, layers can be grouped together and enabled/disabled by the AxiDraw software (this resembles, but is not to be confused with <em>vpype</em>&rsquo;s layer IDs). For example, <code>10 my layer</code> assigns the number 10 to the corresponding layer. With the &ldquo;Layer&rdquo; tab of the InkScape extension, it is possible to plot all layers prefixed with a given number.</li>
<li>The pen down position can be adjusted by adding <code>+H</code> immediately followed by a number right after the layer number, e.g. <code>10+H55 my layer</code> sets the pen down position to 55. This works without layer number as well, e.g. <code>+H55 my layer</code>.</li>
<li>Likewise, the pen down speed can be adjusted with <code>+S</code>, e.g. <code>12+S30 my layer</code>.</li>
<li>Another possibility is to set a delay at the beginning of a layer using <code>+D</code>. This enables behaviours such as letting time for the ink to dry, or for a timelapse picture to be taken.</li>
<li>A layer may be marked as &ldquo;do not plot&rdquo; by adding <code>%</code> at the beginning of the layer name. This could be used to add debug elements or decorations to an SVG that should not be physically plotted.</li>
<li>Likewise, starting the layer name with <code>!</code> will force the AxiDraw to pause, for example to allow for paper swapping.</li>
</ul>
<h2 id="naming-layers">Naming layers</h2>
<p>Equipped with this understanding of how layers work, let&rsquo;s use the <a href="https://vpype.readthedocs.io/en/latest/reference.html#name"><code>name</code></a> command to control the AxiDraw. For example, the drawing speed for layer 3 can be specified this way:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vpype  <span style="color:#0086b3">read</span> multilayer.svg  name -l <span style="color:#099">3</span> <span style="color:#d14">&#34;+S30 my layer label&#34;</span>  write output.svg 
</code></pre></div><p><em>vpype</em> 1.13 <a href="/articles/2023/03/13/annotated-release-notes-vpype-1.13/">introduced</a> a new <code>lid</code> built-in expression variable which comes in handy in this context. It can be used to match the AxiDraw layer number with <em>vpype</em>&rsquo;s layer ID:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vpype  <span style="color:#0086b3">read</span> multilayer.svg  name -l <span style="color:#099">3</span> <span style="color:#d14">&#34;%lid%+S30 my layer label&#34;</span>  write output.svg 
</code></pre></div><p>In this case, layer 3 is renamed &ldquo;3+S30 my layer label&rdquo;. The nice thing is that this also works when renaming multiple layers at once. For example:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vpype  <span style="color:#0086b3">read</span> multilayer.svg  name -l 3,4 <span style="color:#d14">&#34;%lid%+S30&#34;</span>  write output.svg 
</code></pre></div><p>Here, layer 3 and 4 are renamed &ldquo;3+S30&rdquo; and &ldquo;4+S30&rdquo;, respectively.</p>
<h2 id="empty-layers">Empty layers</h2>
<p>To achieve certain behaviours with the AxiDraw, one may need to insert any empty layer with a specific name. For example, a final empty layer named &ldquo;+D1000&rdquo; makes the AxiDraw pause for one second in pen-up position at the final drawing location, before moving the head back to the home position.</p>
<p>Interestingly, there is currently no explicit support in  <em>vpype</em> to create empty layers. The layer manipulation commands (like <a href="https://vpype.readthedocs.io/en/latest/reference.html#lmove"><code>lmove</code></a>, <a href="https://vpype.readthedocs.io/en/latest/reference.html#lcopy"><code>lcopy</code></a>, etc.) specifically remove layers they leave empty.</p>
<p>While preparing this article, I realised that one command could be used to create an empty layer: <a href="https://vpype.readthedocs.io/en/latest/reference.html#random"><code>random</code></a>. Its purpose is to generate some random lines for debugging purposes, but the number of lines it generate can luckily be controlled. So here is how to create an empty layer with ID 5:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vpype  <span style="color:#000;font-weight:bold">[</span>...<span style="color:#000;font-weight:bold">]</span>  random -l <span style="color:#099">5</span> -n <span style="color:#099">0</span>  <span style="color:#000;font-weight:bold">[</span>...<span style="color:#000;font-weight:bold">]</span>
</code></pre></div><p>The <code>-n 0</code> option tells <code>random</code> to generate 0 random lines, but it still creates the layer. This can be verified using the <a href="https://vpype.readthedocs.io/en/latest/reference.html#stat"><code>stat</code></a> command:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vpype  random -l <span style="color:#099">5</span> -n <span style="color:#099">0</span>  stat
</code></pre></div><p>This prints the following output:</p>
<pre tabindex="0"><code>========= Stats =========
Current page size: None
Layer 5
  Length: 0
  Pen-up length: 0.0
  Total length: 0.0
  Mean pen-up length: 0.0
  Median pen-up length: 0.0
  Path count: 0
  Segment count: 0
  Mean segment length: n/a
  Bounds: None
  Properties:
Totals
  Layer count: 1
  Length: 0.0
  Pen-up length: 0.0
  Total length: 0.0
  Path count: 0
  Segment count: 0
  Mean segment length: n/a
  Bounds: None
  Global properties:
=========================
</code></pre><p>Layer 5 exists, but is empty.</p>
<br/>
<p>That about wraps up what can be said on <em>vpype</em> layers, their names, and how they can interact with the AxiDraw. I&rsquo;d love to hear about your creative use of these features, in the comments or on the socials.</p>
<!--

## Putting it all together

Let wrap up by considering a complete example. We'll use [this SVG](example.svg) as starting point:

![two-color concentric patterns](example.svg)

This is a two-layer file laid out on 10x10cm page.

For the purpose of this example, let's say we want to control the pen-down height and speed for the two layers, and then create an ink-bleed stain in the middle.

```bash
vpype \
  read  example.svg \
  name "%lid%+H35+S50" \
  circle -l 3 5cm 5cm 0.1px \
  name -l 3 %lid% \
  random -l 4 -n 0 \
  name -l 4 "%lid%+D10000" \
  show
```

Here is what happens:
- After reading the input file, we first rename the two layers to set the pen-down height to 35 and the speed to 50.
- We then draw a tiny circle in the middle of a new layer with an ID of 3, which we renamed with its number[^name].
- Then, we create an empty layer with ID 4, which we rename "4+D10000" to tell the AxiDraw to make a 10-second pause before starting layer 4. This happens right at the end of plotting layer 3, *before* the pen is raised to move to layer 4. This effectively lets the pen bleeding just after drawing the small circle.

[^name]: This is technically not necessary as the [`write`](https://vpype.readthedocs.io/en/latest/reference.html#write) automatically uses the ID as name for unnamed layers.

And here is the final result, ready to be plotted with the AxiDraw:

![adfas](example.jpg)

-->
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1" role="doc-endnote">
<p>Note that <a href="/articles/2023/03/13/annotated-release-notes-vpype-1.13/"><em>vpype</em> 1.13</a> introduced a minor change on how exactly this is done.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</section>
]]></content:encoded>
    </item>
    
    <item>
      <title>Annotated Release Notes: vpype 1.13</title>
      <link>https://bylr.info/articles/2023/03/13/annotated-release-notes-vpype-1.13/</link>
      <pubDate>Mon, 13 Mar 2023 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2023/03/13/annotated-release-notes-vpype-1.13/</guid>
      <description>&lt;p&gt;&lt;em&gt;vpype&lt;/em&gt; 1.13 is out! 🎉&lt;/p&gt;
&lt;p&gt;The main improvement is Python 3.11 support, but it does come with a few other bells and whistles, so let&amp;rsquo;s dive in.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>vpype</em> 1.13 is out! 🎉</p>
<p>The main improvement is Python 3.11 support, but it does come with a few other bells and whistles, so let&rsquo;s dive in.</p>
<h3 id="python-311-support">Python 3.11 support</h3>

<blockquote class="extract">
  <p><ul>
<li>Added support for Python 3.11 and dropped support for Python 3.8 (#581)</li>
</ul>
<p>Known issue:</p>
<ul>
<li>As of PySide 6.4.2, a refresh issue arises on macOS when the viewer window is resized by a window manager (#603)</li>
</ul>
</p>
</blockquote>

<p>Python 3.11 was first released on October 22nd, 2022, and the current version is 3.11.2 as of this writing. So it was way overdue for <em>vpype</em> to support it.</p>
<p>As usual, adding support for a newer Python means dropping an older one&mdash;bye bye Python 3.8! 👋 That said, if you <em>must</em> use Python 3.8, <em>vpype</em> 1.12.1 remains available and has been a rather stable release.</p>
<p>In the process of adapting <em>vpype</em> for 3.11, I had to update PySide6 to the last release (6.4.2 as of this writing). Unfortunately, it introduces a <a href="https://bugreports.qt.io/browse/QTBUG-107666">refresh glitch</a> on macOS when using a window manager:</p>
<video preload="auto" width="100%"  autoplay loop playsinline class="html-video">
    <source src="/articles/2023/03/13/annotated-release-notes-vpype-1.13/pyside6_glitch.mp4" type="video/mp4">
  <span>Your browser doesn't support embedded videos, but don't worry, you can <a href="/articles/2023/03/13/annotated-release-notes-vpype-1.13/pyside6_glitch.mp4">download it</a> and watch it with your favorite video player!</span>
</video>
<br/>
<p>I waited for a fix, but this is taking too long, so I opted to make a release with this known issue.</p>
<h3 id="lid-built-in-expression-variable"><code>lid</code> built-in expression variable</h3>

<blockquote class="extract">
  <p><ul>
<li>Added the <code>lid</code> built-in expression variable for generator and layer processor commands (#605)</li>
</ul>
</p>
</blockquote>

<p>Many <em>vpype</em> commands operate on a layer-by-layer basis. They are known as <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#layer-processors">layer processors</a>. One example is the <a href="https://vpype.readthedocs.io/en/latest/reference.html#translate"><code>translate</code></a> command. By default, it appears to run on all layers at once, but what actually happens is that it is run once for each layer. You can control on which layer(s) it should actually run with the <code>--layer</code> option.</p>
<p>Any <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#expression-substitution">expression</a> passed to such a command is <em>also</em> evaluated once per layer. This enables per-layer customisation of the command behaviour. This feature introduces the <code>lid</code> built-in variable, which is set to the ID of the layer that&rsquo;s being currently processed.</p>
<p>Here is an example for illustration:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ vpype  <span style="color:#0086b3">read</span> file_with_many_layers.svg  name <span style="color:#d14">&#34;Layer %lid%&#34;</span>  write output.svg
</code></pre></div><p>Here, the <a href="https://vpype.readthedocs.io/en/latest/reference.html#name"><code>name</code></a> command renames all layers (<code>--layer</code> is not used). But since <code>lid</code> is set by <em>vpype</em> to the current layer ID, the output SVG has its layers labelled &ldquo;Layer 1&rdquo;, &ldquo;Layer 2&rdquo;, etc. (the exact numbers depend on the input SVG&rsquo;s actual layer structure).</p>
<h3 id="layer-id-determination">Layer ID determination</h3>

<blockquote class="extract">
  <p><ul>
<li>Fixed a design issue with the <code>read</code> command where disjoint groups of digits in layer names could be used to determine layer IDs. Only the first contiguous group of digits is used now, so a layer named &ldquo;01-layer1&rdquo; has layer ID of 1 instead of 11 (#606)</li>
</ul>
</p>
</blockquote>

<p>Speaking of layer IDs, this fix slightly changes how <em>vpype</em> attributes layer IDs when reading SVGs using the <a href="https://vpype.readthedocs.io/en/latest/reference.html#read"><code>read</code></a> command. Previously, <em>vpype</em> would strip all non-digit characters from the layer name (or, lacking one, from the corresponding group ID), and use the resulting number as ID, if any. A layer named &ldquo;01-blue3&rdquo; would thus end up with ID 13, which is a rather unexpected result! The new behaviour consists of taking the <em>first</em> contiguous group of digit, if any, and use this as layer ID instead. Now, layer &ldquo;01-blue3&rdquo; has an ID of 1.</p>
<p>BTW, I&rsquo;m working on a new article on layer names, and how they can be used to control the plotting process with the AxiDraw. <del>So stay tuned for more on this!</del> <a href="/articles/2023/03/17/layer-names/">Done!</a></p>
<h3 id="wayland-related-crash">Wayland-related crash</h3>

<blockquote class="extract">
  <p><ul>
<li>Fixed an issue on Wayland-based Linux distributions where using the viewer (e.g. with the <code>show</code> command) would crash (#607)</li>
</ul>
</p>
</blockquote>

<p><a href="https://www.qt.io">Qt</a>, the GUI library behind PySide6, has an <a href="https://doc.qt.io/qt-6/qpa.html">abstraction layer</a> dedicated to the platform its running on. This is basically what enables Qt projects to seamlessly run on macOS, Linux, or Windows. There appears to be some technicalities involved with this when using OpenGL (as <em>vpype</em> does), which led the viewer to crash on Wayland-based Linux install. The workaround consisted of &ldquo;forcing&rdquo; Qt to use the <code>xcb</code> instead of <code>wayland</code> platform by setting an environment variable:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#0086b3">export</span> <span style="color:#008080">QT_QPA_PLATFORM</span><span style="color:#000;font-weight:bold">=</span>xcb
</code></pre></div><p>With <em>vpype</em> 1.13, this is no longer needed as it automatically sets this environment variable when a Wayland-based Linux is detected.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>Project: the Automatic #plotloop Machine</title>
      <link>https://bylr.info/articles/2022/12/22/automatic-plotloop-machine/</link>
      <pubDate>Thu, 22 Dec 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/12/22/automatic-plotloop-machine/</guid>
      <description>Deep dive into the internals of my Automatic #plotloop Machine.</description>
      <content:encoded><![CDATA[<h2 id="whats-this">What&rsquo;s this?</h2>
<p>I recently built an &ldquo;Automatic #plotloop Machine&rdquo;, named after the common <a href="https://mastodon.social/tags/plotloop">social</a> <a href="https://twitter.com/hashtag/plotloop">media</a> hashtag for these animations made of individually plotted frames. This article is a thorough description of the project, covering both hardware and software aspects.</p>
<p>First, here is a video:</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/w_PPPImmEN8" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="The Automatic #plotloop Machine"></iframe>
</div>

<br/>
<p>So, this is all very nice, but why does this article need to be more than <em>six thousand five hundred word long</em>?! Sure, the machine&rsquo;s neat, but it&rsquo;s not like <em>everyone</em> wants to make one, right?</p>
<p>Well, in the making of this project, I found myself using a number of tools and techniques which, I think, might be of more general interest&mdash;that is, also for someone without aspirations for maximally complex animation production methods.</p>
<p>Rather than the seasoned maker, who might more efficiently fill knowledge gaps with a few targeted Google queries, I wrote this article for the many people in the plotter space with an artistic rather than technical background. I&rsquo;m hoping this article might provide some technical baggage to fuel their tinkering urge, which, being involved with plotters, they obviously have.</p>
<p>So, if any or all of the following topics sound like they might be useful, have a read!</p>
<ul>
<li>How to use a Raspberry Pi to automate things like taking pictures or controlling stepper motors?</li>
<li>How to mate a stepper motor with LEGO Technic contraptions?</li>
<li>How to efficiently control a RPi via ssh?</li>
<li>How to efficiently control a RPi using a web framework?</li>
<li>How to use <a href="https://github.com/abey79/vsketch"><em>vsketch</em></a> to produce plot loops (automated or otherwise)?</li>
<li>How to use <em>doit</em> to automate complex workflows?</li>
<li>Etc.</li>
</ul>
<p>This article is not a beginner tutorial either. A number of the topics covered here would, in tutorial form, warrant an even longer treatment by themselves. There should be enough information to understand how things work and understand the relevance of the tools I&rsquo;ve used. Actually applying them in a project may, however, require some more focused reading. Likewise, the code is what I&rsquo;d call &ldquo;project quality&rdquo;&mdash;it does the job, I&rsquo;m not too ashamed of it (mostly), but shouldn&rsquo;t be construed as top-notch, state-of-the-art, production-ready copy-paste material.</p>
<h2 id="overview">Overview</h2>
<p>Here is how the setup looks like in on my <a href="https://youtu.be/kELtKbjg-fo">mobile plotting station</a>:</p>
<img src="/automatic-plotloop-machine/setup.jpg"alt="overview picture of my automated plotloop setup, including an AxiDraw, an 80-mm paper roll with a lego feeder powered by a stepper motor, a raspberry pi with its camera, and a portable video light"width="100%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Here are the main components involved:</p>
<ol>
<li>An <a href="https://shop.evilmadscientist.com/908">AxiDraw SE/A3</a> from <a href="https://shop.evilmadscientist.com">Evil Mad Scientist Laboratories</a>, driven by a <a href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/">Raspberry Pi 4</a> not visible in the picture (it&rsquo;s neatly installed in the lower section of the plotting station).</li>
<li>A <a href="https://www.raspberrypi.com/products/raspberry-pi-3-model-b/">Raspberry Pi 3</a> hooked to the <a href="https://www.raspberrypi.com/products/raspberry-pi-high-quality-camera/">High Quality Camera</a> module and a <a href="https://www.schmalzhaus.com/EasyDriver/">EasyDriver</a> stepper motor driver, held by a Manfrotto <a href="https://www.manfrotto.com/global/pump-cup-with-16mm-socket-241s/">241s Pump Cup</a> and <a href="https://www.manfrotto.com/global/photo-variable-friction-arm-with-interchangeable-1-4-attach-244mini/">244 Mini Friction Arm</a>.</li>
<li>An 80-mm paper feeder contraption made of <a href="https://www.lego.com/themes/technic">LEGO Technic</a> and a stepper motor.</li>
<li>A Manfrotto <a href="https://www.bhphotovideo.com/c/product/761822-REG/Manfrotto_ML840H_Maxima_84_LED_Panel.htm">ML840H On-Camera LED light</a>, held by a couple of umbrella swivel adapters and a <a href="https://www.bhphotovideo.com/c/product/1175618-REG/phottix_ph86315_multi_clamp.html">Phottix Multi Clamp</a>.</li>
</ol>
<p>Clearly, having a bunch of photography-related gear around is convenient for this kind of project! 😄</p>
<p>Here is a schematics view of the same setup:</p>
<img src="/automatic-plotloop-machine/schematic.png"alt="schematic view of my automated plotloop setup"width="100%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>The most salient aspect is the use of two Raspberry Pis. This is entirely unnecessary&mdash;a single RPi would be entirely sufficient. This arrangement happened to be more convenient for me because my plotting station already includes a RPi (with hostname <code>axidraw.local</code>) for my day-to-day use of the AxiDraw.</p>
<p>Missing from both pictures is my computer, which I use to generate the frames and controls the plotting process by sending orders to both RPis via Wi-Fi.</p>
<p>In the next sections, I will dig into the details of many hardware and software aspects of this setup, culminating with the <a href="https://pydoit.org"><em>doit</em></a> script which orchestrates everything ranging from generating the frame data, creating a simulated animation, controlling both RPis for plotting and picture acquisition, and assembling the final GIF:</p>
<img src="/automatic-plotloop-machine/world_final.gif"alt="animated GIF of a rotating earth, automatically plotted with my machine"width="532px"style="display:block;margin-left:auto;margin-right:auto"/>
<h2 id="hardware">Hardware</h2>
<h3 id="camera">Camera</h3>
<img src="/automatic-plotloop-machine/campi.png"alt="detail view of the raspberry pi with its camera and stepper motor controller"width="100%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>The camera assembly includes a Raspberry Pi (with hostname <code>campi.local</code>), the camera module and its optics, as well as the stepper motor driver. The three boards are assembled together using custom-cut plexiglass plates and nylon spacers. The camera module is connected to the RPi&rsquo;s camera interface using the provided 200mm ribbon cable.</p>
<p>I chose the <a href="https://www.raspberrypi.com/products/raspberry-pi-high-quality-camera/">High Quality Camera</a> for the following reasons:</p>
<ul>
<li>It is made by the Raspberry Pi Foundation itself, so it has best-in-class software support out-of-the-box.</li>
<li>With 12 megapixels and a large pixel size, it is one of the highest quality camera available for the Raspberry Pi.</li>
<li>It uses interchangeable, C-mount lenses, which means that I can use a lens that&rsquo;s best suited for this setup.</li>
</ul>
<p>For the lens, I selected the <a href="https://www.sparkfun.com/products/16761">16mm telephoto</a>. With its relatively narrow field of view, it minimises the distortions and can be placed high enough to leave space for the plotter to operate. With this lens, the positioning is mainly driven by the minimum focus distance, which is approximately 24cm, measured from the front-most part of the lens.</p>
<p>Here is a sample frame and the corresponding raw image as taken by the camera (<a href="/automatic-plotloop-machine/sample_hires.jpg">high-res version</a>):</p>
<img src="/automatic-plotloop-machine/sample.png"alt="sample animation frame line-work with the corresponding raw image as taken by the camera"width="100%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Note that the image is rotated by 90° compared to the actual frame. It just happened to be easier to set up the camera this way. It will be automatically corrected before the final animation is assembled.</p>
<h3 id="stepper-motor-driver">Stepper motor driver</h3>
<p>The <code>campi.local</code> RPi is also in charge of driving the paper feeder&rsquo;s stepper motor via an <a href="https://www.schmalzhaus.com/EasyDriver/">EasyDriver</a> board. As their name imply, <a href="https://en.wikipedia.org/wiki/Stepper_motor">stepper motors</a> divide their full rotation into a number of discreet, equal steps. This makes them very convenient for use cases where accuracy and reproducibility is important, like 3D printers, plotters, and&hellip; makeshift paper feeders. Stepper drivers generate the high power electrical signals needed to run the motor based on a simple <a href="https://en.wikipedia.org/wiki/General-purpose_input/output">GPIO</a> inputs.</p>
<p>Here is a close-up of the wiring schematic:</p>
<img src="/automatic-plotloop-machine/stepper-driver.png"alt="wiring schematic of the raspberry pi, the EasyDriver stepper driver board, and the paper feeder stepper motor"height="400px"style="display:block;margin-left:auto;margin-right:auto"/>
<p>The main inputs are STEP and DIR. The motor turns by one step for each STEP pulse (transition from 0 to 1), while the rotation direction is controlled by DIR.</p>
<p>The ENABLE input controls whether the motor outputs are powered or not. When they are, the driver actively keeps the motor in its position, which tends to heat both the driver and motor itself. As the feeder spends most its time waiting for the plotter to draw the frame, it is good idea to drive ENABLE only when actually running the feeder.</p>
<p>The MS1 and MS2 inputs control the so-called &ldquo;micro-stepping&rdquo; capability of the driver. This feature increases the accuracy of the motor by further dividing the motor&rsquo;s physical steps into up to 16 micro-steps. For a low-precision paper feeder like mine, this is not useful and the wiring can be skipped<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>One nice feature of the EasyDriver board is the ability to choose either 3.3V or 5V GPIO voltage. The default is 5V, but since the RPi uses 3.3V, I set the board to this voltage using a small solder bridge over the pads at the very bottom left of the board:</p>
<img src="/automatic-plotloop-machine/easydriver.png"alt="photograph of the EasyDriver stepper motor driver board"width="605"style="display:block;margin-left:auto;margin-right:auto"/>
<h3 id="lego-structure">LEGO structure</h3>
<img src="/automatic-plotloop-machine/lego_feeder.jpg"alt="photograph of the LEGO feeder assembly"width="100%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>My LEGO building skills aren&rsquo;t anything to boast about&mdash;a skilled builder would likely do a much better job. Yet, my contraption turned out to work rather reliably thanks to a few key design decisions.</p>
<p>Most importantly, I wanted to leave the feeder &ldquo;open&rdquo; on the &ldquo;top&rdquo; side to minimise the chance of collision with the plotter or the pen during operations. The lower parallel structure is thus designed to maintain some rigidity between the paper roll part and the feed part. This actually worked much better that I anticipated!</p>
<p>These structures are really easy to build once your remember that 3<sup>2</sup> + 4<sup>2</sup> = 5<sup>2</sup>:</p>
<img src="/automatic-plotloop-machine/lego_triangle.jpg"alt="photograph of a 3-4-5 right triangle made with LEGO Technic parts"width="60%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>All my diagonals use this arrangement and provide rigidity to the structure (purely rectangular structures are prone to parallelogram deformation).</p>
<p>Using the stepper motor weight to put pressure on the feeding wheel was another successful design. This ensured practically no paper slippage.</p>
<p>Finally, remembering that the LEGO stud pitch is 8mm, a 10-stud distance is a good fit for 80mm paper rolls. These are easy to source in office supply shops.</p>
<h3 id="stepper-lego-integration">Stepper-LEGO integration</h3>
<img src="/automatic-plotloop-machine/lego_motor.jpg"alt="close-up photograph of the motor-feeder lego assembly"width="60%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>One might wonder why I used a custom stepper motor instead of native LEGO motors to drive the feeder. The decision boils down to two issues with LEGO motors:</p>
<ol>
<li>There are no stepper-type LEGO motors, so their accuracy is not great, nor is their power.</li>
<li>Interfacing LEGO motors to a RPi is more complicated than using a stepper motor (especially if you already have stepper drivers lying around).</li>
</ol>
<p>I actually tried to use a LEGO Mindstorm motor initially. I used a <a href="https://www.dexterindustries.com/brickpi/">BrickPi</a> board to interface the motor to the RPi. It worked, but as noted above these motors are not ideal for the task and ended up not being precise and reliable enough.</p>
<p>An alternative to the BrickPi board would be to use the actual Mindstorm controller. This is not ideal though, as the controller is battery powered and must be tinkered with to accept external power. Also, the RPi must connect to it via Bluetooth, and a specific Python package must be used.</p>
<p>All in all, in my experience, using a stepper motor is just easier.</p>
<p>This begs the question of how to physically interface the stepper motor with LEGO Technic parts.</p>
<p>I used a 3D-printed adapter of <a href="https://www.thingiverse.com/thing:5649176">my own design</a> to interface the 5mm axle of my motor to Technic axles. There are many such designs available online for various shapes and sizes of motor axles.</p>
<img src="/automatic-plotloop-machine/5mm_adapter.png"alt="closeup photograph and render of the 3d printed 5mm axle to LEGO Technic adapter"width="100%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>It turns out that physically assembling the motor with LEGO Technic is easy enough when using the <a href="https://reprap.org/wiki/NEMA_17_Stepper_motor">NEMA 17</a> form factor (one of the most commonly used by makers). The offset between their M3 attachments is 31mm. This is close enough to the 32mm offset between two 3-stud-apart Technic holes. Using conic screws easily takes up the remaining 1mm difference:</p>
<img src="/automatic-plotloop-machine/nema17_lego.jpg"alt="closeup photograph of how NEMA17 motor can be adapted to LEGO Technic"width="60%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Crucially, this method keeps the motor axle properly aligned with the Technic grid.</p>
<h2 id="software">Software</h2>
<h3 id="ssh-rpi">Setting up the Raspberry Pi for friction-less SSH</h3>
<p>There is ample <a href="https://www.raspberrypi.com/documentation/">documentation</a> available online on how to setup Raspberry Pis, which I won&rsquo;t reproduce here. I&rsquo;d like to focus instead on how to configure the RPi such as to minimise any friction when interacting with your RPi via SSH&mdash;either manually or automatically.</p>
<p>Here is what&rsquo;s needed to achieve this:</p>
<ol>
<li>Unless you are using wired Ethernet, your home Wi-Fi must be configured on the RPi (of course, duh).</li>
<li>SSH must be configured for key-based authentication. This removes the need to input a password when SSHing into your RPi, which is annoying with manual operations and precludes automation.</li>
<li>ZeroConf/Bonjour/Avahi is installed and active. This means that you can connect to the RPi using a URL in the form of <code>hostname.local</code> instead of using an explicit IP address, which is hard to remember and subject to change.</li>
<li>You computer must &ldquo;know&rdquo; which username to use when connecting to a given RPi.</li>
</ol>
<p>For key-based authentication, you must first generate a public/private key pair on your computer (if you haven&rsquo;t done so already). This is done using the following command:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">ssh-keygen
</code></pre></div><p>You&rsquo;ll need to answer a few questions, which can all be left as default. This will create two files in your home directory:</p>
<ul>
<li>The private key: <code>~/.ssh/id_rsa</code></li>
<li>The public key: <code>~/.ssh/id_rsa.pub</code></li>
</ul>
<p>(Actual naming may differ depending on the type of key generated.)</p>
<p>The idea is to provide your <em>public</em> key (this is important&mdash;not the private key!) to the RPi so that it can accept connections without requesting a password when the originator possesses the corresponding private key.</p>
<p>Most of the configuration, fortunately, is addressed by the official <a href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a>, which I <em>strongly</em> suggest using. Everything can be set in the &ldquo;Advanced Options&rdquo; dialog, including the hostname, the SSH public key, and the Wi-Fi credentials:</p>
<img src="/automatic-plotloop-machine/rpi-imager.png"width="60%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Again, note that the content of your <em>public</em> key (<code>~/.ssh/id_rsa.pub</code>) should be pasted in the relevant field.</p>
<p>Finally, you need to tell your computer&rsquo;s SSH that user <code>pi</code> (or whichever you chose) should be used by default when connecting to the given RPi. Create the <code>~/.ssh/config</code> file if needed, and add the following content:</p>
<pre tabindex="0"><code class="language-ssh-config" data-lang="ssh-config">Host axidraw.local
    User pi
</code></pre><p>This tells your computer&rsquo;s SSH to default to the username <code>pi</code> whenever it connects to <code>axidraw.local</code>.</p>
<p>If you already have a working RPi image, and you don&rsquo;t want to recreate one from scratch, you can do the same configurations manually as follows:</p>
<ul>
<li>Check <a href="https://linuxhint.com/setup-wifi-raspberry-pi/">this article</a> for Wi-Fi configuration.</li>
<li>For key-based SSH authentication, create or edit the <code>~/.ssh/authorized_keys2</code> file on the RPi and add the content of your public key on a new line.</li>
<li>Install ZeroConf/Bonjour/Avahi support with <code>sudo apt-get install avahi-daemon</code>.</li>
<li>Change the hostname by running <code>sudo raspi-config</code>. The configuration is in the Network Options menu.</li>
</ul>
<p>Here are the cool things that you can now do remotely without entering a password:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">scp file.svg axidraw.local:          <span style="color:#998;font-style:italic"># copy a local file to your remote user directory</span>
scp file.svg axidraw.local:files/    <span style="color:#998;font-style:italic"># copy a local file the remote ~/files/ directory</span>
scp axidraw.local:files/file.svg ./  <span style="color:#998;font-style:italic"># copy a remote file locally</span>

ssh axidraw.local     <span style="color:#998;font-style:italic"># log to your remote RPi</span>
ssh axidraw.local ls  <span style="color:#998;font-style:italic"># list the files in your home directory</span>

cat my_file.svg | ssh axidraw.local axicli -L <span style="color:#099">2</span> -d <span style="color:#099">37</span> -u <span style="color:#099">60</span> -N -m plot   <span style="color:#998;font-style:italic"># plot a file!</span>
</code></pre></div><p>This last one is particularly nice. SSH allows you to pipe the output of a local command (here <code>cat</code> just outputs the contents of <code>my_file.svg</code>) into a remote command&rsquo;s input. This is a very powerful combination that I use for this project.</p>
<h3 id="controlling-the-camera-and-the-feeder">Controlling the camera and the feeder</h3>
<p>The <code>campi.local</code> Raspberry Pi has two missions: taking a picture of the frame after it is drawn, and run the motor to feed blank paper for the next frame. One of the easiest ways to make these functionalities remote controllable is to run a small HTTP server with two end-points, one for each task.</p>
<p>This is <em>really</em> easy to do with <a href="https://fastapi.tiangolo.com">FastAPI</a> (<a href="https://flask.palletsprojects.com/">Flask</a> would also work just as well). The corresponding code is available <a href="https://github.com/abey79/sketches/tree/master/world/campi">here</a>, along with a <code>requirements.txt</code> file listing the required packages.</p>
<h4 id="taking-pictures">Taking pictures</h4>
<p>For illustration, here is a shortened version with only the image acquisition part:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">from</span> <span style="color:#555">fastapi</span> <span style="color:#000;font-weight:bold">import</span> FastAPI
<span style="color:#000;font-weight:bold">from</span> <span style="color:#555">fastapi.responses</span> <span style="color:#000;font-weight:bold">import</span> FileResponse
<span style="color:#000;font-weight:bold">from</span> <span style="color:#555">picamera2</span> <span style="color:#000;font-weight:bold">import</span> Picamera2

<span style="color:#998;font-style:italic"># create a FastAPI server</span>
app <span style="color:#000;font-weight:bold">=</span> FastAPI()

<span style="color:#998;font-style:italic"># setup and start the Pi camera for still frame acquisition</span>
picam2 <span style="color:#000;font-weight:bold">=</span> Picamera2()
still_config <span style="color:#000;font-weight:bold">=</span> picam2<span style="color:#000;font-weight:bold">.</span>create_still_configuration(controls<span style="color:#000;font-weight:bold">=</span>{<span style="color:#d14">&#34;ExposureValue&#34;</span>: <span style="color:#099">0</span>})
picam2<span style="color:#000;font-weight:bold">.</span>configure(still_config)
picam2<span style="color:#000;font-weight:bold">.</span>start()

<span style="color:#998;font-style:italic"># create a GET endpoint accepting a &#34;ev&#34; parameter and returning a JPG file</span>
<span style="color:#3c5d5d;font-weight:bold">@app</span><span style="color:#000;font-weight:bold">.</span>get(<span style="color:#d14">&#34;/img/&#34;</span>)
<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">get_picture</span>(ev: <span style="color:#0086b3">int</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#099">0</span>):
    picam2<span style="color:#000;font-weight:bold">.</span>set_controls({<span style="color:#d14">&#34;ExposureValue&#34;</span>: ev})
    array <span style="color:#000;font-weight:bold">=</span> picam2<span style="color:#000;font-weight:bold">.</span>capture_file(<span style="color:#d14">&#34;/tmp/temp.jpg&#34;</span>)
    <span style="color:#000;font-weight:bold">return</span> FileResponse(<span style="color:#d14">&#34;/tmp/temp.jpg&#34;</span>, media_type<span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;image/jpeg&#34;</span>)
</code></pre></div><p>All it takes to run the server is to the following command:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">uvicorn campi:app --host campi.local --port <span style="color:#099">8000</span>
</code></pre></div><p><a href="https://www.uvicorn.org">Uvicorn</a> is a web server implementation fo Python uses FastAPI (in our case) to handle web requests. Here, <code>campi:app</code> tells Uvicorn to use the <code>app</code> object in the <code>campi</code> module (assuming our file is named <code>campi.py</code>).</p>
<p>Note in passing how having ZeroConf setup with the RPi means that, once again, we don&rsquo;t need to mess with explicit IP addresses. (Here, specifying a <code>--host</code> other than the default <code>localhost</code> is required to allow another computer to access the server.)</p>
<p>With the server running, any other computer on the local network can acquire a photo using the following command:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">curl -s -o /tmp/test.png campi.local:8000/img/?ev<span style="color:#000;font-weight:bold">=</span><span style="color:#099">1</span>
</code></pre></div><p>A couple of learnings from the field:</p>
<ul>
<li>Like in the example above, I&rsquo;m using an exposure value of +1 with the #plotloop machine. This is because the white paper tend to lead to underexposure. With the code above, this parameter doesn&rsquo;t seem to have effect until after the second picture is taken. So, after starting the server, I always use the command above to have one picture taken with <code>ev=1</code> so the camera is primed when the machine actually uses it.</li>
<li>The server can run for a long time, taking multiple hundreds of pictures (the rotating earth loop is 280-frame long). So it&rsquo;s pretty important that the <code>/img/</code> end-point doesn&rsquo;t leak any memory. Especially uncompressed-frame-sized buffers. I actually ran into this issue with the first implementation, which used a memory buffer instead of a temporary file. After messing with <a href="https://docs.python.org/3/library/tracemalloc.html"><code>tracemalloc</code></a> for a while to figure this out, it ended up being just easier to switch implementation. For my first loop with the machine, the leak would fill my RPi&rsquo;s memory and crash every 20 images or so&mdash;it was painful process to go through the whole loop!</li>
</ul>
<h4 id="paper-feed">Moving the motor</h4>
<p>I use another end-point to control the feeder motor. This time, it includes a mandatory parameter in the URL: the approximate number of centimetre of paper to feed. Here is how it looks in the code:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#3c5d5d;font-weight:bold">@app</span><span style="color:#000;font-weight:bold">.</span>get(<span style="color:#d14">&#34;/motor/</span><span style="color:#d14">{cm}</span><span style="color:#d14">&#34;</span>)
<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">run_motor</span>(cm: <span style="color:#0086b3">int</span>):
	<span style="color:#000;font-weight:bold">...</span>
</code></pre></div><p>Using this end-point is as easy as:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">curl -s campi.local:8000/motor/5
</code></pre></div><p>The implementation is really boring. The gist of controlling a stepper motor is to toggle a GPIO up and down as many times as needed&mdash;one per step. This could easily be done manually, but I&rsquo;m using <a href="https://github.com/gavinlyonsrepo/RpiMotorLib">RpiMotorLib</a> to reduce the code to a single line. The conversion from centimetre to step count depends on your motor (mine has 400 steps per full rotation, which is fairly standard) and the feeder wheel size. I have a <a href="https://en.wikipedia.org/wiki/Magic_number_(programming)">magic number</a> in the code that I tuned by trial-and-error.</p>
<p>It&rsquo;s worth noting that the accuracy of the feeder mechanisms is not critical for this machine because the paper isn&rsquo;t moved at all between the frame being drawn and the picture taken. The feeder has thus no impact on frame-to-frame alignment&mdash;only the plotter&rsquo;s repeatability matters here (which is a non-issue with the AxiDraw ❤️).</p>
<p>Another couple of things I learned on the way:</p>
<ul>
<li>As mentioned earlier, the motor and the driver can generate quite a bit of heat and power consumption when active, even when not moving. That&rsquo;s why I manually toggle the ENABLE pin in the <code>motor</code> end-point. That way the motor is kept unpowered most of the time, and only activated when it must be moved.</li>
<li>With the Raspberry Pi, it&rsquo;s important to properly shut down the GPIO sub-system when exiting your program. It avoids some errors and minimise the risk for the hardware<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>. The proper way to do this with FastAPI is to implement a shutdown event handler:
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#3c5d5d;font-weight:bold">@app</span><span style="color:#000;font-weight:bold">.</span>on_event(<span style="color:#d14">&#34;shutdown&#34;</span>)
<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">shutdown_event</span>():
    GPIO<span style="color:#000;font-weight:bold">.</span>cleanup()
</code></pre></div></li>
</ul>
<h3 id="using-vsketch-for-plot-loops">Using <em>vsketch</em> for plot loops</h3>
<p>Unsurprisingly, I&rsquo;ve used <a href="https://github.com/abey79/vsketch"><em>vsketch</em></a> to generate the animation frames. Before diving into the actual sketch I made for this article, I want to shortly digress on how to structure a sketch to produce SVGs suitable for plot loops.</p>
<p>Here is a simple example of a plot loop sketch:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">import</span> <span style="color:#555">math</span>
<span style="color:#000;font-weight:bold">import</span> <span style="color:#555">vsketch</span>

<span style="color:#000;font-weight:bold">class</span> <span style="color:#458;font-weight:bold">PlotloopSketch</span>(vsketch<span style="color:#000;font-weight:bold">.</span>SketchClass):
    frame_count <span style="color:#000;font-weight:bold">=</span> vsketch<span style="color:#000;font-weight:bold">.</span>Param(<span style="color:#099">50</span>)
    frame <span style="color:#000;font-weight:bold">=</span> vsketch<span style="color:#000;font-weight:bold">.</span>Param(<span style="color:#099">0</span>)

    <span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">draw</span>(<span style="color:#999">self</span>, vsk: vsketch<span style="color:#000;font-weight:bold">.</span>Vsketch) <span style="color:#000;font-weight:bold">-&gt;</span> <span style="color:#000;font-weight:bold">None</span>:
        vsk<span style="color:#000;font-weight:bold">.</span>size(<span style="color:#d14">&#34;5x5cm&#34;</span>, center<span style="color:#000;font-weight:bold">=</span><span style="color:#000;font-weight:bold">False</span>)
        vsk<span style="color:#000;font-weight:bold">.</span>scale(<span style="color:#d14">&#34;cm&#34;</span>)

        radius <span style="color:#000;font-weight:bold">=</span> <span style="color:#099">2</span>
        vsk<span style="color:#000;font-weight:bold">.</span>circle(<span style="color:#099">2.5</span>, <span style="color:#099">2.5</span>, radius<span style="color:#000;font-weight:bold">=</span>radius)
        angle <span style="color:#000;font-weight:bold">=</span> <span style="color:#099">360</span> <span style="color:#000;font-weight:bold">/</span> <span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>frame_count <span style="color:#000;font-weight:bold">*</span> <span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>frame
        vsk<span style="color:#000;font-weight:bold">.</span>circle(
            <span style="color:#099">2.5</span> <span style="color:#000;font-weight:bold">+</span> radius <span style="color:#000;font-weight:bold">*</span> math<span style="color:#000;font-weight:bold">.</span>cos(math<span style="color:#000;font-weight:bold">.</span>radians(angle)),
            <span style="color:#099">2.5</span> <span style="color:#000;font-weight:bold">+</span> radius <span style="color:#000;font-weight:bold">*</span> math<span style="color:#000;font-weight:bold">.</span>sin(math<span style="color:#000;font-weight:bold">.</span>radians(angle)),
            radius<span style="color:#000;font-weight:bold">=</span><span style="color:#099">0.1</span>,
        )

    <span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">finalize</span>(<span style="color:#999">self</span>, vsk: vsketch<span style="color:#000;font-weight:bold">.</span>Vsketch) <span style="color:#000;font-weight:bold">-&gt;</span> <span style="color:#000;font-weight:bold">None</span>:
        vsk<span style="color:#000;font-weight:bold">.</span>vpype(<span style="color:#d14">&#34;linemerge linesimplify reloop linesort&#34;</span>)
</code></pre></div><p>This sketch (<a href="https://github.com/abey79/vsketch/tree/master/examples/basic_plotloop">available</a> in the <em>vsketch</em> <a href="https://github.com/abey79/vsketch">repository</a>) animates a small circle rotating around a larger circle<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>:</p>
<img src="/automatic-plotloop-machine/demo_plotloop.gif"width="200px"style="display:block;margin-left:auto;margin-right:auto"/>
<p>The first noteworthy aspect is its use of the two parameters: <code>frame_count</code> and <code>frame</code>. This makes it super easy to control the animation length and generate all the frames. For example, the frames for this GIF were generated with the following command:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">vsk save plotloop -m -p frame_count <span style="color:#099">13</span> -p frame 0..12
</code></pre></div><p>(Note the use of <code>-m</code> to use all available CPU cores.)</p>
<p>Another key element is to use <code>center=False</code> in the initial <code>vsk.size()</code> call. Without this, <em>vsketch</em> would auto-centre every frame based on the geometry and this would result in the animation wobbling around. I actually made a similar mistake with one of my first plot loop<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>:</p>
<img src="/automatic-plotloop-machine/columns_marie.gif"alt="plot loop of a pixelated 3d structure exhibiting wobbling due to inconsistent centring"width="200px"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Last but not least, notice how the algorithm generates the exact same frame for <code>frame = 0</code> and <code>frame = frame_count</code><sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>. This is obviously necessary to obtain a properly looping behaviour&mdash;but is easier said than done for all but trivial examples.</p>
<p>One way is to use periodic trigonometric functions such as sine and cosine like I did in the example above. This is also the approach I used for the rotating Earth loop.</p>
<p>Many generative artwork rely on <a href="https://en.wikipedia.org/wiki/Perlin_noise">Perlin noise</a>. Some implementations <em>may</em> offer some kind of periodicity that could be used to generate a looping set of frames. Alternatively, periodicity can be achieved by sampling the noise field along cylindrical coordinates instead of on a cartesian grid.</p>
<img src="/automatic-plotloop-machine/rect_vs_cylindrical.png"alt="schema illustrating cartesian versus cylindrical sampling of a Perlin noise field"width="100%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>An entire article could be written on this topic, and this one is long enough. Instead, I&rsquo;ve added <a href="https://github.com/abey79/vsketch/tree/master/examples/noise_bezier">another example</a> to the <em>vsketch</em> repository to illustrate this principle:</p>
<img src="/automatic-plotloop-machine/noise_plotloop.gif"alt="plot loop of a wobbly line scrolling from left to right"width="200px"style="display:block;margin-left:auto;margin-right:auto"/>
<h3 id="the-rotating-earth-sketch">The rotating Earth sketch</h3>
<p>The full sketch code is too long to be reproduced in this article&mdash;it&rsquo;s available <a href="https://github.com/abey79/sketches/blob/master/world/sketch_world.py">here</a> in my <a href="https://github.com/abey79/sketches/">sketches</a> repository on GitHub. Instead, I will provide here an overview of how it&rsquo;s implemented. You might want to open the code in another window to follow along.</p>
<h4 id="data-preprocessing">Data preprocessing</h4>
<p>First, the data. I used the <a href="https://hub.arcgis.com/datasets/esri::world-countries-generalized/about">World Country</a> boundaries from <a href="https://www.esri.com">ESRI</a>. It contains polygons for all countries in the world. By merging them with <a href="https://shapely.readthedocs.io">Shapely</a>&rsquo;s <a href="https://shapely.readthedocs.io/en/stable/manual.html#shapely.ops.unary_union"><code>unary_union()</code></a>, one can obtain the land/water boundaries. This happens in the <code>build_world()</code> function.</p>
<p>Here is how the merged countries look after the union step:</p>
<img src="/automatic-plotloop-machine/world_latlon.png"width="100%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>There is one oddity I had to deal with, which explains the magic numbers and other ugliness in that function: Antarctica. This is the only body of land sitting <em>over</em> one of the poles, which are singularities in the latitude/longitude coordinate system.</p>
<img src="/automatic-plotloop-machine/antarctica.png"width="100%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>The left is how the data looked like originally. On the right is the boundary once the artificial limit at ~80°S is removed. In lat/long coordinates, it becomes a self-intersecting polygon. This can be dealt with a simple Shapely trick: create a <code>LinearRing</code> with the (self-intersecting) boundary and apply <code>unary_union()</code> on it. This creates a <code>MultiLineString</code> containing a corresponding list of non-intersecting linear rings.</p>
<p>This is the glitch that this procedure solved:</p>
<img src="/automatic-plotloop-machine/antarctica_glitch.png"width="40%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Another important step is to filter land masses by area, to avoid the myriads of tiny isles that would clutter the result and massively increase plotting time. You can&rsquo;t just use the <code>.area</code> attribute from Shapely with lat/lon coordinates as this isn&rsquo;t an <a href="https://en.wikipedia.org/wiki/Equal-area_map">equal-area projection</a>, so I shamelessly copy/pasted <code>polygon_area()</code> from <a href="https://stackoverflow.com/a/61177081/229511">StackOverflow</a>.</p>
<p>The final preprocessing step consist of converting the lat/lon land boundaries (stored in a Shapely <code>Polygon</code> instance) into 3D points on the unit sphere (stored as a Nx3 NumPy array). This is done by the <code>project_polygon()</code> function. The projected boundaries are stored in the <code>LINES</code> global variable.</p>
<h4 id="rendering-the-earth">Rendering the Earth</h4>
<p>The Earth rendering can be broken into the following steps:</p>
<ol>
<li>Rotate the Earth data as needed.</li>
<li>Crop away the &ldquo;far side&rdquo; part of the data.</li>
<li>Project orthogonally the rest of the data, a.k.a. drop the coordinate along which the backside was cropped and use the other two for drawing.</li>
<li>Draw a circle :)</li>
</ol>
<p>For rotation, I first compute 3 angles around the X, Y, and Z axes. These angles can be manually set for testing, or generated by trigonometric functions with different frequencies. Naturally, I make sure that these function are periodic with the frame count for a properly looping behaviour.</p>
<p>I then generate 3 rotation matrices:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">rot_x <span style="color:#000;font-weight:bold">=</span> np<span style="color:#000;font-weight:bold">.</span>array(
    [
        (<span style="color:#099">1</span>, <span style="color:#099">0</span>, <span style="color:#099">0</span>),
        (<span style="color:#099">0</span>, math<span style="color:#000;font-weight:bold">.</span>cos(math<span style="color:#000;font-weight:bold">.</span>radians(rot_x_angle)), <span style="color:#000;font-weight:bold">-</span>math<span style="color:#000;font-weight:bold">.</span>sin(math<span style="color:#000;font-weight:bold">.</span>radians(rot_x_angle))),
        (<span style="color:#099">0</span>, math<span style="color:#000;font-weight:bold">.</span>sin(math<span style="color:#000;font-weight:bold">.</span>radians(rot_x_angle)), math<span style="color:#000;font-weight:bold">.</span>cos(math<span style="color:#000;font-weight:bold">.</span>radians(rot_x_angle))),
    ]
)
rot_y <span style="color:#000;font-weight:bold">=</span> np<span style="color:#000;font-weight:bold">.</span>array(
    [
        (math<span style="color:#000;font-weight:bold">.</span>cos(math<span style="color:#000;font-weight:bold">.</span>radians(rot_y_angle)), <span style="color:#099">0</span>, math<span style="color:#000;font-weight:bold">.</span>sin(math<span style="color:#000;font-weight:bold">.</span>radians(rot_y_angle))),
        (<span style="color:#099">0</span>, <span style="color:#099">1</span>, <span style="color:#099">0</span>),
        (<span style="color:#000;font-weight:bold">-</span>math<span style="color:#000;font-weight:bold">.</span>sin(math<span style="color:#000;font-weight:bold">.</span>radians(rot_y_angle)), <span style="color:#099">0</span>, math<span style="color:#000;font-weight:bold">.</span>cos(math<span style="color:#000;font-weight:bold">.</span>radians(rot_y_angle))),
    ]
)
rot_z <span style="color:#000;font-weight:bold">=</span> np<span style="color:#000;font-weight:bold">.</span>array(
    [
        (math<span style="color:#000;font-weight:bold">.</span>cos(math<span style="color:#000;font-weight:bold">.</span>radians(rot_z_angle)), <span style="color:#000;font-weight:bold">-</span>math<span style="color:#000;font-weight:bold">.</span>sin(math<span style="color:#000;font-weight:bold">.</span>radians(rot_z_angle)), <span style="color:#099">0</span>),
        (math<span style="color:#000;font-weight:bold">.</span>sin(math<span style="color:#000;font-weight:bold">.</span>radians(rot_z_angle)), math<span style="color:#000;font-weight:bold">.</span>cos(math<span style="color:#000;font-weight:bold">.</span>radians(rot_z_angle)), <span style="color:#099">0</span>),
        (<span style="color:#099">0</span>, <span style="color:#099">0</span>, <span style="color:#099">1</span>),
    ]
)
</code></pre></div><p>Finally, I combine them a single transformation matrix (did I mention I love NumPy?):</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">rot <span style="color:#000;font-weight:bold">=</span> rot_x <span style="color:#000;font-weight:bold">@</span> rot_y <span style="color:#000;font-weight:bold">@</span> rot_z
</code></pre></div><p>With that, rotating every single points of one of the land boundary line is just a matter of:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">rotated_line <span style="color:#000;font-weight:bold">=</span> (rot <span style="color:#000;font-weight:bold">@</span> line<span style="color:#000;font-weight:bold">.</span>T)<span style="color:#000;font-weight:bold">.</span>T
</code></pre></div><p>Here, <code>.T</code> is used for transpose, and is needed for NumPy <a href="https://numpy.org/doc/stable/user/basics.broadcasting.html">broadcasting</a> rules to work. Remember that the actual calculation (3x3 matrix multiplication on every single points in <code>line</code>) happens in highly optimised C code, so this operation is <em>fast</em>.</p>
<p>Cropping is actually a bit trickier because you have to account for lines that may go from the front side to the far side and back, possibly multiple times. The cropping operation on a single line may thus result in multiple &ldquo;sub-lines&rdquo;.</p>
<p>Luckily, I had already sorted out most this for <em>vpype</em>&rsquo;s <code>crop</code> command. <em>vpype</em>&rsquo;s API includes the <code>crop_half_plane()</code> function, which crops a line at a give location along one of the X or Y axis<sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>. Adapting it to the 3rd dimension was trivial. While copy/pasting the function, I took along <code>_interpolate_crop()</code>, which deals specifically with computing the intersection of a line segment with the cropping plane.</p>
<p>Once the land boundary lines are cropped along one dimension, it&rsquo;s a matter of drawing them using the other two dimensions using <code>vsk.polygons</code>. Which dimension is used for what is not very important since we&rsquo;re dealing with a sphere. I made it so that when rotation angles are set to 0, the (0°, 0°) lat/lon point (somewhere in the Atlantic ocean, off Ghana) is dead in the center of the rendered Earth.</p>
<p>In the sketch code, you&rsquo;ll also find controls to enable pixelation using <a href="https://github.com/abey79/vpype-pixelart"><em>vpype-pixelart</em></a>. I ran some trials with it but decided against it&mdash;to messy for this kind of line work.</p>
<img src="/automatic-plotloop-machine/pixelize_sim.gif"alt="simulated loop showing the rotating earth with a pixelation effect"width="200px"style="display:block;margin-left:auto;margin-right:auto"/>
<h3 id="putting-it-all-together-with-doit">Putting it all together with <em>doit</em></h3>
<p>At this point, all the #plotloop machine&rsquo;s body parts are in place and just need a beating heart to set them in motion. <a href="https://pydoit.org"><em>doit</em></a> is the perfect tool for this.</p>
<p>Although <em>doit</em> is rather easy to use, it still has a tiny bit of a learning curve. If this is your first ever encounter with it, you might want to check the <a href="/articles/2022/11/10/batch-processing-doit-vpype/">introductory article</a> I recently wrote. This project takes this to a whole new level.</p>
<p>Instead of looking at the <a href="https://github.com/abey79/sketches/blob/master/world/dodo.py"><code>dodo.py</code></a> file line by line, I&rsquo;ll first provide an overview of the workflows it implements (again, you might want to open the file in another window to follow along). Then, I&rsquo;ll go deeper into a few, hand-picked topics to highlight interesting techniques.</p>
<h4 id="the-workflows">The workflows</h4>
<p>The bulk of the <code>dodo.py</code> file implements two workflows using a bunch of tasks: one is to create a simulated animation based on the frames' SVG, and another to plot, photograph, post-process, and assemble the frames into the final animation.</p>
<p>Here is a schematic of the workflows.</p>
<img src="/automatic-plotloop-machine/workflows.svg"alt="box schematic illustrating the workflows implemented to run the plotloop machine"width="100%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Let&rsquo;s review the tasks involved in creating the simulated animation:</p>
<ul>
<li>The <code>generate</code> task generates all the frame SVGs with a single call to <em>vsketch</em>. It is basically running some version of this command:
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">vsk save . -m -p frame_count <span style="color:#099">280</span> -p frame 1..280
</code></pre></div>The outcome of this task is one SVG file per frame, numbered from 1 to 280.</li>
<li>Each of the <code>simframe:XXXX</code> sub-task takes one frame SVG and convert it to a JPG with a <code>_simulated</code> prefix using <a href="https://gitlab.gnome.org/GNOME/librsvg">librsvg</a>&rsquo;s <code>rsvg-convert</code><sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup>. The sub-tasks are named after their corresponding frame number, e.g. <code>simframe:0010</code> correspond to the frame number 10.</li>
<li>Finally, the <code>simulate</code> task combines all the simulated frames into a single animated GIF, using <a href="https://imagemagick.org">ImageMagick</a>&rsquo;s <code>convert</code> command.</li>
</ul>
<p>I&rsquo;m calling this a workflow because each of these tasks have their <code>file_dep</code> and <code>targets</code> carefully defined. As a result, <em>doit</em> understands the dependency relationship between them. From a clean slate, calling <code>doit simulate</code> will first execute <code>generate</code>, then each of the <code>simframe:XXXX</code> sub-tasks, before finally running <code>simulate</code> to produce the animation.</p>
<p>The workflow for the actually plotted animation is similarly structured, but includes an additional post-processing step:</p>
<ul>
<li>
<p>The workflow starts with the same <code>generate</code> task as before.</p>
</li>
<li>
<p>Each of the frame is then plotted and photographed by the corresponding <code>plot:XXXX</code> sub-task, which performs the following steps:</p>
<ul>
<li>Plot the frame by sending the SVG via SSH to <code>axicli</code> running on the <code>axidraw.local</code> RPi (as  described <a href="#ssh-rpi">earlier</a>).</li>
<li>Move the pen away by 3 inches to get it out of camera view (again, using SSH and <code>axicli</code>).</li>
<li>Take a picture of the frame and download the corresponding image using <code>curl</code> (as described <a href="#taking-pictures">earlier</a>).</li>
<li>Move the pen back to its original position.</li>
<li>Feed fresh paper using <code>curl</code> (as described <a href="#paper-feed">earlier</a>).</li>
</ul>
<p>This is a good example of how a single <em>doit</em> task may execute multiple CLI commands.</p>
</li>
<li>
<p>Each frame is then post-processed by the corresponding <code>postprocess:XXXX</code> sub-task using <code>convert</code>. It rotates the image in the correct orientation, crops it tightly around the earth, converts it to grayscale, and bumps its brightness and contrast a bit. This is the command used:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">convert frame_XXXX_plotted.jpg -rotate <span style="color:#099">270</span> -crop 1900x1900+605+785 <span style="color:#d14">\
</span><span style="color:#d14"></span>  -colorspace Gray -brightness-contrast 5x15 frame_XXXX_postprocessed.jpg
</code></pre></div></li>
<li>
<p>Finally, the <code>animation</code> task combines all post-processed frames into the final animated GIF using <code>convert</code>.</p>
</li>
</ul>
<p>One may wonder, why is the <code>postprocess:XXXX</code> task separate from the <code>plot:XXXX</code> task? The <code>convert</code> command could just as well be added to the list of commands <code>plot:XXXX</code> executes. The answer is to be able to tweak the post-processing step without invalidating the plotting process. If both tasks were merged, any modification to the post-processing (e.g. adjusting the cropping parameters) would require re-plotting the entire frame&mdash;a lengthy process! This issue disappears with a stand-alone <code>postprocess:XXXX</code> task, which is very powerful when fine-tuning the workflow.</p>
<h4 id="basic-doit-syntax">Basic <em>doit</em> syntax</h4>
<p>Armed with this <code>dodo.py</code> file, we are now in complete control of our workflow.</p>
<p>Here are a few example commands (I won&rsquo;t list all the output here, check my <a href="/articles/2022/11/10/batch-processing-doit-vpype/">other article</a> for a gentler introduction on how it behaves).</p>
<p>First, <em>doit</em> always provides a list of available tasks with <code>doit list</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">animation     Make the animation.
disable_xy    Disable X/Y motors
generate      Generates golden master SVGs from the list of input files.
plot          Plot the plotter-ready SVGs.
postprocess   Post-process the plotted images.
shutdown      Shutdown everything
simframe      Simulate a frame.
simulate      Make the simulated animation.
toggle        Toggle pen up/down
</code></pre></div><p>Generating the frame SVGs is a matter of:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">doit generate
</code></pre></div><p>This is not needed though, as this task is automatically run when executing other tasks depending on it. For example, this executes <code>generate</code> (if needed) and all the <code>simframe:XXXX</code> sub-tasks to produce the simulated GIF:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">doit simulate
</code></pre></div><p>A single simulated frame can be generated by specifying the frame number:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">doit simframe:0118
</code></pre></div><p>This works because I chose to name sub-tasks after the corresponding (zero-padded) frame number.</p>
<p>All the frames can be generated at once by omitting the sub-task name:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">doit simframe
</code></pre></div><p>Likewise, producing the final, plotted animation is just a matter of running the following command and waiting 9 hours 🕰:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">doit animation
</code></pre></div><h4 id="executing-a-range-of-sub-tasks">Executing a range of sub-tasks</h4>
<p>It is often useful to execute a range of sub-tasks. For example, early testing requires plotting, say, the first 10 frames to verify that everything works correctly. (Spoiler alert: it doesn&rsquo;t! The process must be repeated several times until all the glitches are worked out.)</p>
<p>Thankfully, this is made very easy thanks to bash&rsquo;s <a href="https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html">brace expansion</a> syntax (it works the same with zsh and, probably, other shells<sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup>).</p>
<p>Here is an illustration to demonstrate the idea:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#0086b3">echo</span> <span style="color:#000;font-weight:bold">{</span>1..5<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p>The braces with the <code>..</code> syntax are interpreted by bash as a range that needs expansion. Accordingly, the output of the above is:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#099">1</span> <span style="color:#099">2</span> <span style="color:#099">3</span> <span style="color:#099">4</span> <span style="color:#099">5</span>
</code></pre></div><p>The good news is that it understands zero-padding:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#0086b3">echo</span> <span style="color:#000;font-weight:bold">{</span>0001..0015<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p>This produces:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#099">0001</span> <span style="color:#099">0002</span> <span style="color:#099">0003</span> <span style="color:#099">0004</span> <span style="color:#099">0005</span> <span style="color:#099">0006</span> <span style="color:#099">0007</span> <span style="color:#099">0008</span> <span style="color:#099">0009</span> <span style="color:#099">0010</span> <span style="color:#099">0011</span> <span style="color:#099">0012</span> <span style="color:#099">0013</span> <span style="color:#099">0014</span> <span style="color:#099">0015</span>
</code></pre></div><p>Knowing this, we can instruct <em>doit</em> to plot a specific frame range with the following command:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">doit plot:<span style="color:#000;font-weight:bold">{</span>0005..0015<span style="color:#000;font-weight:bold">}</span>
</code></pre></div><p>This expends to <code>doit plot:0005 plot:0006 plot:0007 ...</code>, which <em>doit</em> interprets as a list of tasks to be executed.</p>
<p>This syntax is beautifully consistent with both the single task form (<code>doit plot:0012</code>) and <em>vsketch</em>&rsquo;s <code>-p,--param</code> option (<code>vsk save -p frame 1..280</code>). It&rsquo;s also yet another shining example of how powerful terminals can be for automation.</p>
<p>Note that, again, this is enabled by my choice of consistently naming sub-tasks after their zero-padded frame number.</p>
<h4 id="path-management">Path management</h4>
<p>This <code>dodo.py</code> file wrangles with a lot of different files. Each frame has up to 4 corresponding files (the original SVG, the simulated JPG, the plotted JPG, and the post-processed JPG), each with a specific suffix.</p>
<p>A small helper class is useful to manage this complexity. Here is how it looks:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">import</span> <span style="color:#555">pathlib</span>

PROJECT_DIR <span style="color:#000;font-weight:bold">=</span> pathlib<span style="color:#000;font-weight:bold">.</span>Path(__file__)<span style="color:#000;font-weight:bold">.</span>parent
FRAME_COUNT <span style="color:#000;font-weight:bold">=</span> <span style="color:#099">280</span>
PIXELIZE <span style="color:#000;font-weight:bold">=</span> <span style="color:#000;font-weight:bold">False</span>
PROJECT_NAME <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#34;world&#34;</span>
BASENAME <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">f</span><span style="color:#d14">&#34;</span><span style="color:#d14">{</span>PROJECT_NAME<span style="color:#d14">}</span><span style="color:#d14">_frame_count_</span><span style="color:#d14">{</span>FRAME_COUNT<span style="color:#d14">}</span><span style="color:#d14">_pixelize_</span><span style="color:#d14">{</span>PIXELIZE<span style="color:#d14">}</span><span style="color:#d14">&#34;</span>

<span style="color:#000;font-weight:bold">class</span> <span style="color:#458;font-weight:bold">FileSpec</span>:
    <span style="color:#000;font-weight:bold">def</span> __init__(<span style="color:#999">self</span>, frame: <span style="color:#0086b3">int</span>):
        <span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>frame <span style="color:#000;font-weight:bold">=</span> frame
        directory <span style="color:#000;font-weight:bold">=</span> PROJECT_DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;output&#34;</span>

        <span style="color:#998;font-style:italic"># vsketch doesn&#39;t add zero padding to frame number</span>
        <span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>source <span style="color:#000;font-weight:bold">=</span> directory <span style="color:#000;font-weight:bold">/</span> (BASENAME <span style="color:#000;font-weight:bold">+</span> <span style="color:#d14">f</span><span style="color:#d14">&#34;_frame_</span><span style="color:#d14">{</span><span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>frame<span style="color:#d14">}</span><span style="color:#d14">.svg&#34;</span>)

        <span style="color:#998;font-style:italic"># for the other file we add the zero padding to keep the order with CLI tools</span>
        base_frame <span style="color:#000;font-weight:bold">=</span> BASENAME <span style="color:#000;font-weight:bold">+</span> <span style="color:#d14">f</span><span style="color:#d14">&#34;_frame_</span><span style="color:#d14">{</span><span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>frame<span style="color:#d14">:</span><span style="color:#d14">04d</span><span style="color:#d14">}</span><span style="color:#d14">&#34;</span>
        <span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>simulated <span style="color:#000;font-weight:bold">=</span> directory <span style="color:#000;font-weight:bold">/</span> (base_frame <span style="color:#000;font-weight:bold">+</span> <span style="color:#d14">&#34;_simulated.jpg&#34;</span>)
        <span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>plotted <span style="color:#000;font-weight:bold">=</span> directory <span style="color:#000;font-weight:bold">/</span> (base_frame <span style="color:#000;font-weight:bold">+</span> <span style="color:#d14">&#34;_plotted.jpg&#34;</span>)
        <span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>postprocessed <span style="color:#000;font-weight:bold">=</span> directory <span style="color:#000;font-weight:bold">/</span> (base_frame <span style="color:#000;font-weight:bold">+</span> <span style="color:#d14">&#34;_postprocessed.jpg&#34;</span>)


FILE_SPECS <span style="color:#000;font-weight:bold">=</span> {i: FileSpec(i) <span style="color:#000;font-weight:bold">for</span> i <span style="color:#000;font-weight:bold">in</span> <span style="color:#0086b3">range</span>(<span style="color:#099">1</span>, FRAME_COUNT <span style="color:#000;font-weight:bold">+</span> <span style="color:#099">1</span>)}
</code></pre></div><p><code>FileSpec</code> instances are created based on a frame number, and contain all the paths related to the corresponding frame. This is easy to implement using the <a href="https://docs.python.org/3/library/pathlib.html"><code>pathlib</code></a> module from the standard library.</p>
<p>Note that <em>vsketch</em> doesn&rsquo;t zero-pad the frame number in the SVGs it produces. This is a bit unfortunate as it messes up alphabetical ordering for most CLI tools (for example, this creates frame ordering issues when creating a GIF from a bunch of JPGs). All the other paths are constructed with zero-padding.</p>
<p>Also notice the <code>PIXELIZE</code> global variable. Its value is forwarded as a parameter to <em>vsketch</em>, which controls whether <em>vpype-pixelize</em> should be used. As noted above, I&rsquo;ve decided against it for this project. In any case, I made sure to reflect this value in all the files' base name to avoid confusion by <em>doit</em> when changing the parameter&rsquo;s value.</p>
<p>The <code>FILE_SPECS</code> global variable contains a dictionary which maps the <code>FileSpec</code> instance to the corresponding frame number. This simplifies a lot the task implementation.</p>
<h4 id="task-dependencies">Task dependencies</h4>
<p>As I explained in my <a href="/articles/2022/11/10/batch-processing-doit-vpype/">previous article</a>, proper handling of task targets and dependencies is key for <em>doit</em> to understand the structure of the workflows and be smart about which task must be executed when.</p>
<p>Let&rsquo;s consider the <code>simframe:XXXX</code> sub-tasks as an example:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_simframe</span>():
    <span style="color:#d14">&#34;&#34;&#34;Simulate a frame.&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">for</span> frame, spec <span style="color:#000;font-weight:bold">in</span> FILE_SPECS<span style="color:#000;font-weight:bold">.</span>items():
        <span style="color:#000;font-weight:bold">yield</span> {
            <span style="color:#d14">&#34;name&#34;</span>: <span style="color:#d14">f</span><span style="color:#d14">&#34;</span><span style="color:#d14">{</span>frame<span style="color:#d14">:</span><span style="color:#d14">04d</span><span style="color:#d14">}</span><span style="color:#d14">&#34;</span>,
            <span style="color:#d14">&#34;actions&#34;</span>: [
                <span style="color:#d14">f</span><span style="color:#d14">&#34;rsvg-convert -b white -h 200 </span><span style="color:#d14">{</span>spec<span style="color:#000;font-weight:bold">.</span>source<span style="color:#d14">}</span><span style="color:#d14"> &gt; </span><span style="color:#d14">{</span>spec<span style="color:#000;font-weight:bold">.</span>simulated<span style="color:#d14">}</span><span style="color:#d14">&#34;</span>,
            ],
            <span style="color:#d14">&#34;file_dep&#34;</span>: [spec<span style="color:#000;font-weight:bold">.</span>source],
            <span style="color:#d14">&#34;targets&#34;</span>: [spec<span style="color:#000;font-weight:bold">.</span>simulated],
            <span style="color:#d14">&#34;clean&#34;</span>: <span style="color:#000;font-weight:bold">True</span>,
        }
</code></pre></div><p>Each sub-task has the source SVG file set as <code>file_dep</code>, and the simulated JPG file as <code>targets</code>. Since this stage of the workflow is structured as sub-tasks&mdash;one sub-task per frame&mdash;each sub-task focuses on a single input and output file.</p>
<p>In contrast, the <code>simulate</code> task needs <em>all</em> the simulated frames to create a single animated GIF:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_simulate</span>():
    <span style="color:#d14">&#34;&#34;&#34;Make the simulated animation.&#34;&#34;&#34;</span>
    file_list <span style="color:#000;font-weight:bold">=</span> [spec<span style="color:#000;font-weight:bold">.</span>simulated <span style="color:#000;font-weight:bold">for</span> spec <span style="color:#000;font-weight:bold">in</span> FILE_SPECS<span style="color:#000;font-weight:bold">.</span>values()]
    paths <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#34; &#34;</span><span style="color:#000;font-weight:bold">.</span>join(<span style="color:#0086b3">str</span>(file) <span style="color:#000;font-weight:bold">for</span> file <span style="color:#000;font-weight:bold">in</span> file_list)
    target <span style="color:#000;font-weight:bold">=</span> PROJECT_DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;output&#34;</span> <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">f</span><span style="color:#d14">&#34;</span><span style="color:#d14">{</span>BASENAME<span style="color:#d14">}</span><span style="color:#d14">.gif&#34;</span>
    <span style="color:#000;font-weight:bold">return</span> {
        <span style="color:#d14">&#34;actions&#34;</span>: [<span style="color:#d14">f</span><span style="color:#d14">&#34;convert -delay 5 -loop 0 </span><span style="color:#d14">{</span>paths<span style="color:#d14">}</span><span style="color:#d14"> </span><span style="color:#d14">{</span>target<span style="color:#d14">}</span><span style="color:#d14">&#34;</span>],
        <span style="color:#d14">&#34;file_dep&#34;</span>: file_list,
        <span style="color:#d14">&#34;targets&#34;</span>: [target],
        <span style="color:#d14">&#34;clean&#34;</span>: <span style="color:#000;font-weight:bold">True</span>,
    }
</code></pre></div><p>In this case, <code>file_dep</code> contains all the simulated frames, while <code>targets</code> has the animated GIF.</p>
<p>These snippets also illustrate how the <code>FileSpec</code> class and the <code>FILE_SPECS</code> global variable simplify the task implementation.</p>
<h4 id="depending-on-dodopy">Depending on <code>dodo.py</code></h4>
<p>Adding the <code>dodo.py</code> file to the dependency list might sound like a smart idea. Indeed, a modification of the <code>dodo.py</code> may potentially invalidate all generated files.</p>
<p>In our case, this can be done in the <code>generate</code> task, on which all other tasks depend:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_generate</span>():
    <span style="color:#d14">&#34;&#34;&#34;Generates golden master SVGs from the list of input files.&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">return</span> {
        <span style="color:#d14">&#34;actions&#34;</span>: [
            (
                <span style="color:#d14">f</span><span style="color:#d14">&#34;vsk save -n </span><span style="color:#d14">{</span>PROJECT_NAME<span style="color:#d14">}</span><span style="color:#d14"> -p frame_count </span><span style="color:#d14">{</span>FRAME_COUNT<span style="color:#d14">}</span><span style="color:#d14"> &#34;</span>
                <span style="color:#d14">f</span><span style="color:#d14">&#34;-p pixelize </span><span style="color:#d14">{</span>PIXELIZE<span style="color:#d14">}</span><span style="color:#d14"> -p frame 1..</span><span style="color:#d14">{</span>FRAME_COUNT<span style="color:#d14">}</span><span style="color:#d14"> -m .&#34;</span>
            )
        ],
        <span style="color:#d14">&#34;file_dep&#34;</span>: [PROJECT_DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">f</span><span style="color:#d14">&#34;sketch_</span><span style="color:#d14">{</span>PROJECT_NAME<span style="color:#d14">}</span><span style="color:#d14">.py&#34;</span>, __file__],  <span style="color:#998;font-style:italic"># &lt;-- LOOK HERE</span>
        <span style="color:#d14">&#34;targets&#34;</span>: <span style="color:#0086b3">list</span>(spec<span style="color:#000;font-weight:bold">.</span>source <span style="color:#000;font-weight:bold">for</span> spec <span style="color:#000;font-weight:bold">in</span> FILE_SPECS<span style="color:#000;font-weight:bold">.</span>values()),
        <span style="color:#d14">&#34;clean&#34;</span>: <span style="color:#000;font-weight:bold">True</span>,
    }
</code></pre></div><p>The path of the <code>dodo.py</code> file is conveniently stored in the <code>__file__</code> global variable by the Python interpreter, so it is a matter of adding it to the <code>file_dep</code>.</p>
<p>(Notice, in passing, that the sketch file is also listed as <code>file_dep</code>. This triggers a complete rebuild whenever the sketch file is modified.)</p>
<p>This technique is useful in the beginning to ensure that intermediate files potentially contaminated by bugs of an early, in-construction <code>dodo.py</code> file are properly rebuilt when the bug is fixed. It can, however, become an annoyance later on. Executing all of the <code>plot:XXXX</code> sub-tasks takes about 9 hours in total. You really don&rsquo;t want to repeat the whole thing just because of a code formatting fix or added comment in <code>dodo.py</code>! As such, the <code>dodo.py</code> is <em>not</em> listed as dependency in the file on GitHub.</p>
<h4 id="helper-tasks">Helper tasks</h4>
<p>In addition to the main workflows described so far, the <code>dodo.py</code> file includes a few helper tasks to toggle the pen up/down, disable the motor power (which is always better when the keeping the AxiDraw powered but unused for long periods of time), and shutdown everything (the RPis and the AxiDraw).</p>
<p>The code is really straightforward, but very handy to have around. It&rsquo;s basically a thin wrapper over <code>axicli</code> functionalities:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_toggle</span>():
    <span style="color:#d14">&#34;&#34;&#34;Toggle pen up/down&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">return</span> {<span style="color:#d14">&#34;actions&#34;</span>: [<span style="color:#d14">f</span><span style="color:#d14">&#34;</span><span style="color:#d14">{</span>AXICLI<span style="color:#d14">}</span><span style="color:#d14"> -m toggle&#34;</span>]}


<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_disable_xy</span>():
    <span style="color:#d14">&#34;&#34;&#34;Disable X/Y motors&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">return</span> {<span style="color:#d14">&#34;actions&#34;</span>: [<span style="color:#d14">f</span><span style="color:#d14">&#34;</span><span style="color:#d14">{</span>AXICLI<span style="color:#d14">}</span><span style="color:#d14"> -m manual -M disable_xy&#34;</span>]}


<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_shutdown</span>():
    <span style="color:#d14">&#34;&#34;&#34;Shutdown everything&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">return</span> {
        <span style="color:#d14">&#34;actions&#34;</span>: [
            <span style="color:#d14">&#34;ssh campi.local sudo poweroff&#34;</span>,
            <span style="color:#d14">&#34;ssh axidraw.local sudo poweroff&#34;</span>,
        ],
        <span style="color:#d14">&#34;task_dep&#34;</span>: [<span style="color:#d14">&#34;disable_xy&#34;</span>],
    }
</code></pre></div><p>Notice the use of <code>task_dep</code> in the <code>shutdown</code> task. It basically says that <code>disable_xy</code> should be executed whenever <code>shutdown</code> is called.</p>
<h2 id="final-words">Final words</h2>
<p>And that wraps up what I wanted to cover about this project. This was <em>waaay</em> longer than I anticipated! I hope you enjoyed it and, possibly, learned something.</p>
<p>I must once again credit <a href="https://simonwillison.net">Simon Willison</a> for the inspiration and his <a href="https://simonwillison.net/2022/Nov/6/what-to-blog-about/">exhortation</a> to blog about projects. Here, I took the advice, and pushed it to the extreme! 😅 I don&rsquo;t expect to be as thorough every time, but will certainly continue to produce <em>some</em> coverage my future endeavours.</p>
<p>Please hit me up in the comments or on <a href="https://mastodon.social/@abey79">social</a> <a href="https://twitter.com/abey79">medias</a> with any feedback you may have&mdash;or just to let me know you made it this far 🏆, it&rsquo;ll make my day! ❤️</p>
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1" role="doc-endnote">
<p>I did wire MS1 and MS2 in my setup to run a few tests, since <a href="https://github.com/gavinlyonsrepo/RpiMotorLib">RpiMotorLib</a> supports this feature.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>An I/O in output mode is at risk of having its driver damaged when short-circuited. For this reason, all I/Os are in input mode by default.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3" role="doc-endnote">
<p>The fact that it resemble an orbiting planet is actually fortuitous!&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4" role="doc-endnote">
<p>In this case, this was caused by using <em>vpype</em>&rsquo;s <code>layout</code> command on each frame. The effect is the same as <code>center=True</code> though.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5" role="doc-endnote">
<p>Or, more generally, for <code>frame = k</code> and <code>frame = k + n * frame_count</code>.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6" role="doc-endnote">
<p>The <code>crop</code> command implementation basically consists of calling <code>crop_half_plane()</code> four times to cut away geometries outside the target rectangular area.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7" role="doc-endnote">
<p>I found that <code>rsvg-convert</code> is more robust with SVG than ImageMagick.&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8" role="doc-endnote">
<p>As a reminder, a &ldquo;shell&rdquo; (like bash, zsh, tcsh, etc.) is the program that prints the terminal prompt and interprets the commands you type, launching processes as required. If a terminal window is like a small computer&rsquo;s screen, then the shell is that computer&rsquo;s operating system.&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</section>
]]></content:encoded>
    </item>
    
    <item>
      <title>Batch processing SVGs with DoIt and vpype</title>
      <link>https://bylr.info/articles/2022/11/10/batch-processing-doit-vpype/</link>
      <pubDate>Thu, 10 Nov 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/11/10/batch-processing-doit-vpype/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://pydoit.org&#34;&gt;&lt;em&gt;doit&lt;/em&gt;&lt;/a&gt; (a.k.a. PyDoIt) is a fantastic Python-based tool to automate repetitive workflows. It works particularly well alongside &lt;a href=&#34;https://vpype.readthedocs.io&#34;&gt;&lt;em&gt;vpype&lt;/em&gt;&lt;/a&gt; to address mundane plotting-related tasks. This article explains in details how to automate an SVG optimisation and conversion workflow.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://pydoit.org"><em>doit</em></a> (a.k.a. PyDoIt) is a fantastic Python-based tool to automate repetitive workflows. It works particularly well alongside <a href="https://vpype.readthedocs.io"><em>vpype</em></a> to address mundane plotting-related tasks. This article explains in details how to automate an SVG optimisation and conversion workflow.</p>
<p>Most plotter workflows involve one or more repetitive steps which, when executed manually, take time, are boring, and possibly error-prone. Here are some examples that come to mind:</p>
<ul>
<li>Optimizing SVGs using <em>vpype</em>&rsquo;s <code>linemerge reloop linesort linesimplify</code> commands.</li>
<li>Converting SVGs into a format your plotter understands (e.g. HPGL, or G-code using <a href="https://github.com/plottertools/vpype-gcode">vpype-gcode</a>).</li>
<li>Splitting multi-layer SVGs into individual layers (e.g. if this is a requirement of your plotter for multi-colour plots).</li>
<li>Making a PNG version of SVGs for archival purposes.</li>
<li>Running the <code>axicli</code> command to plot an SVG with an <a href="https://axidraw.com">Axidraw</a>.</li>
<li>Uploading optimised files to the computer/server/Raspberry Pi in control of your plotter.</li>
<li>Etc.</li>
</ul>
<p>Not only your workflow may include one or more of these steps, but you may need to apply it on a single SVG at a time, or on a bunch of them at once. Even better, you might want to apply your workflow only on SVGs which were updated or created since the last execution.</p>
<p>You can do exactly that with <em>doit</em>&mdash;let&rsquo;s see how.</p>
<h2 id="installing-doit">Installing <em>doit</em></h2>
<p>Although <a href="https://pydoit.org/install.html">its documentation</a> sadly doesn&rsquo;t mention it, <a href="https://pypa.github.io/pipx/">pipx</a> is the best way to install <em>doit</em> (as for <a href="https://vpype.readthedocs.io/en/latest/install.html"><em>vpype</em></a>):</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ pipx install doit
</code></pre></div><p>You can check that the installation was successful by running this command:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit --version
0.36.0
lib @ /Users/&lt;username&gt;/.local/pipx/venvs/doit/lib/python3.10/site-packages/doit
</code></pre></div><h2 id="basics">Basics</h2>
<p>As a starting point, let&rsquo;s assume you have a bunch of SVGs which need optimising before plotting, stored in a <code>originals</code> subdirectory. Save the optimisation commands in a <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#command-files">VPY file</a> named <code>optimize.vpy</code>, with the following content:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-perl" data-lang="perl">linemerge reloop linesort linesimplify
</code></pre></div><p>Then, create a subdirectory named <code>processed</code>, which will contain the optimised SVGs:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ mkdir processed 
</code></pre></div><p>Here is how your file hierarchy should look like:</p>
<pre tabindex="0"><code>.
├── optimize.vpy
├── originals/
│   ├── dots.svg
│   ├── halftone.svg
│   └── hline.svg
└── processed/
</code></pre><p>Our goal is to have <em>doit</em> automate the optimisation of the source SVGs in <code>originals</code>, and store the result in <code>processed</code>.</p>
<p><em>doit</em> operates by loading a description of the task(s) it must execute, typically in a file named <code>dodo.py</code><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. As the name suggests, the content of this file is Python code.</p>
<p>Create a <code>dodo.py</code> file with the following content:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">import</span> <span style="color:#555">pathlib</span>                                                            <span style="color:#998;font-style:italic"># (1)</span>

DIR <span style="color:#000;font-weight:bold">=</span> pathlib<span style="color:#000;font-weight:bold">.</span>Path(__file__)<span style="color:#000;font-weight:bold">.</span>parent                                       <span style="color:#998;font-style:italic"># (2)</span>
SOURCES <span style="color:#000;font-weight:bold">=</span> <span style="color:#0086b3">list</span>((DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;originals&#34;</span>)<span style="color:#000;font-weight:bold">.</span>glob(<span style="color:#d14">&#34;*.svg&#34;</span>))                         <span style="color:#998;font-style:italic"># (3)</span>
VPY <span style="color:#000;font-weight:bold">=</span> DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;optimize.vpy&#34;</span>                                                <span style="color:#998;font-style:italic"># (4)</span>

<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_optimize</span>():                                                      <span style="color:#998;font-style:italic"># (5)</span>
    <span style="color:#d14">&#34;&#34;&#34;optimize SVGs&#34;&#34;&#34;</span>                                                   <span style="color:#998;font-style:italic"># (6)</span>
    <span style="color:#000;font-weight:bold">for</span> source <span style="color:#000;font-weight:bold">in</span> SOURCES:                                                <span style="color:#998;font-style:italic"># (7)</span>
        optimized <span style="color:#000;font-weight:bold">=</span> DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;processed&#34;</span> <span style="color:#000;font-weight:bold">/</span> (source<span style="color:#000;font-weight:bold">.</span>stem <span style="color:#000;font-weight:bold">+</span> <span style="color:#d14">&#34;_optimized.svg&#34;</span>)  <span style="color:#998;font-style:italic"># (8)</span>
        <span style="color:#000;font-weight:bold">yield</span> {                                                           <span style="color:#998;font-style:italic"># (9)</span>
            <span style="color:#d14">&#34;name&#34;</span>: source<span style="color:#000;font-weight:bold">.</span>stem,                                          <span style="color:#998;font-style:italic"># (10)</span>
            <span style="color:#d14">&#34;actions&#34;</span>: [
                <span style="color:#d14">f</span><span style="color:#d14">&#34;vpype read &#39;</span><span style="color:#d14">{</span>source<span style="color:#d14">}</span><span style="color:#d14">&#39; -I &#39;</span><span style="color:#d14">{</span>VPY<span style="color:#d14">}</span><span style="color:#d14">&#39; write &#39;</span><span style="color:#d14">{</span>optimized<span style="color:#d14">}</span><span style="color:#d14">&#39;&#34;</span>   <span style="color:#998;font-style:italic"># (11)</span>
            ],
        }
</code></pre></div><p>Let&rsquo;s examine this code line-by-line.</p>
<ol>
<li>The <a href="https://docs.python.org/3/library/pathlib.html"><code>pathlib</code></a> built-in module is great at file wrangling. Check <a href="https://realpython.com/python-pathlib/">this Real Python article</a> for a gentle yet thorough introduction.</li>
<li>Here we use it to find our project directory, which is the parent of the present file, whose path is stored in the <code>__file__</code> variable by the Python interpreter.</li>
<li>We list all the SVGs contained in the <code>originals</code> subdirectory, and store them in the <code>SOURCES</code> variable. Note that <code>glob()</code> returns a generator, which must be converted to a <code>list</code> if <code>SOURCES</code> is to be iterated multiple times.</li>
<li>We keep the path to the <code>optimize.vpy</code> file in the <code>VPY</code> variable.</li>
<li>Python functions with name starting with <code>task_</code> are interpreted by <em>doit</em> as <a href="https://pydoit.org/tasks.html">tasks</a>. Here we have just one. Let&rsquo;s call it &ldquo;optimize&rdquo;, thus the <code>task_optimize()</code> function name.</li>
<li>The function&rsquo;s <a href="https://peps.python.org/pep-0257/">docstring</a> is used by <em>doit</em> as help string for the task, so it is useful to include one.</li>
<li>Task functions must return one or more Python dictionaries describing the task. In our case, we want to create one <a href="https://pydoit.org/tasks.html#sub-tasks">sub-tasks</a> per source SVG file.</li>
<li>For each source SVG, we derive the path for the corresponding optimised SVG. The optimised SVG are located in the <code>processed</code> subdirectory and have a <code>_optimized.svg</code> suffix to their name.</li>
<li>Using <a href="https://docs.python.org/3/reference/expressions.html#yieldexpr"><code>yield</code></a> keyword (instead of <code>return</code>) makes our function a <a href="https://docs.python.org/3/glossary.html#term-generator">generator</a> (gentle introduction available <a href="https://realpython.com/introduction-to-python-generators/">here</a>). This is a convenient way to return (er&hellip; yield) multiple objects, which is supported by <em>doit</em>. Here, we yield one dictionary per sub-task.</li>
<li>Sub-tasks must be individually named so that they can be distinguished. Here we derive the sub-task name from the source SVG filename. For example, the sub-task corresponding to <code>my_file.svg</code> will be named <code>my_file</code>, and can be referred to with <em>doit</em> as <code>optimize:my_file</code>.</li>
<li>Last but not least, the <code>&quot;actions&quot;</code> entry of the sub-task dictionary lists the actions to be performed by the task. <em>doit</em> interprets strings as shell commands, so we build a <em>vpype</em> pipeline to optimise the source SVG using our VPY and saving the result in the desired location. For example, for <code>my_file.svg</code>, the action will be <code>vpype read originals/my_file.svg -I optimize.vpy write processed/my_file_optimized.svg</code><sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</li>
</ol>
<p>Let&rsquo;s take a step back to properly understand what&rsquo;s going on.</p>
<p>The function <code>task_optimize()</code> produces a task <em>description</em>&mdash;it does not actually <em>run</em> the task. When we run <em>doit</em> (using the <code>doit</code> command), it loads the <code>dodo.py</code> file, notices that it contains a task function, and calls it to learn about that task. It&rsquo;s only <em>then</em> that it can decide which action(s) to actually execute, based on the task description. In this case, the actions are the <em>vpype</em> pipelines stored in the <code>&quot;actions&quot;</code> entries.</p>
<p>Although this <code>dodo.py</code> file is not overly complicated, it can still feel like quite some work compared to, you know, just calling <em>vpype</em> manually. I certainly felt so when first using <em>doit</em>. So let&rsquo;s see what we gained by going through this effort.</p>
<p>First and foremost, we now have a potent batch processing system. We can optimise all of our source SVGs by telling <em>doit</em> to execute the <code>optimize</code> task:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit optimize
.  optimize:dots
.  optimize:halftone
.  optimize:hline
</code></pre></div><p>Here is the result after running this command:</p>
<pre tabindex="0"><code>.
├── dodo.py
├── optimize.vpy
├── originals/
│   ├── dots.svg
│   ├── halftone.svg
│   └── hline.svg
└── processed/
    ├── dots_optimized.svg
    ├── halftone_optimized.svg
    └── hline_optimized.svg
</code></pre><p><em>doit</em> indeed created properly-named, optimised versions of the source SVGs in the <code>processed</code> directory! 🎉</p>
<p>Since we only have just one task defined, we don&rsquo;t even need to specify its name:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit
.  optimize:dots
.  optimize:halftone
.  optimize:hline
</code></pre></div><p>You can also specify a specific sub-task to execute:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit optimize:halftone
.  optimize:halftone
</code></pre></div><p>Pretty neat already&mdash;but there is a lot more to gain with a little more effort!</p>
<h2 id="handling-targets-and-dependencies">Handling targets and dependencies</h2>
<p>Playing with the commands above, you may notice that each call of the <code>optimize</code> task triggers the processing of the corresponding SVGs&mdash;even if said SVGs were already processed before. The reason for this is that <em>doit</em> doesn&rsquo;t yet know what the task inputs and outputs are, so it cannot check whether that output exists or is outdated. So, to be on the safe side, it <em>always</em> executes <em>all specified tasks</em> every time.</p>
<p>By letting <em>doit</em> know about tasks' inputs and outputs, <em>doit</em> can be much smarter about what it actually needs to do.</p>
<p>In <em>doit</em> parlance, the file(s) a task uses as input are called <em>dependencies</em> (<code>&quot;file_dep&quot;</code> entry). Likewise, the file(s) created as output are called <em>targets</em> (<code>&quot;targets&quot;</code> entry). By specifying what these are in the <code>dodo.py</code> file, <em>doit</em> can decide whether the target of a given task needs to be generated or not, saving a lot of time when repeating the workflow.</p>
<p>Update the <code>dodo.py</code> file as follows:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">import</span> <span style="color:#555">pathlib</span>

DIR <span style="color:#000;font-weight:bold">=</span> pathlib<span style="color:#000;font-weight:bold">.</span>Path(__file__)<span style="color:#000;font-weight:bold">.</span>parent
SOURCES <span style="color:#000;font-weight:bold">=</span> <span style="color:#0086b3">list</span>((DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;originals&#34;</span>)<span style="color:#000;font-weight:bold">.</span>glob(<span style="color:#d14">&#34;*.svg&#34;</span>))
VPY <span style="color:#000;font-weight:bold">=</span> DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;optimize.vpy&#34;</span>

<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_optimize</span>():
    <span style="color:#d14">&#34;&#34;&#34;optimize SVGs&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">for</span> source <span style="color:#000;font-weight:bold">in</span> SOURCES:
        optimized <span style="color:#000;font-weight:bold">=</span> DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;processed&#34;</span> <span style="color:#000;font-weight:bold">/</span> (source<span style="color:#000;font-weight:bold">.</span>stem <span style="color:#000;font-weight:bold">+</span> <span style="color:#d14">&#34;_optimized.svg&#34;</span>)
        <span style="color:#000;font-weight:bold">yield</span> {
            <span style="color:#d14">&#34;name&#34;</span>: source<span style="color:#000;font-weight:bold">.</span>stem,
            <span style="color:#d14">&#34;actions&#34;</span>: [
                <span style="color:#d14">f</span><span style="color:#d14">&#34;vpype read &#39;</span><span style="color:#d14">{</span>source<span style="color:#d14">}</span><span style="color:#d14">&#39; -I &#39;</span><span style="color:#d14">{</span>VPY<span style="color:#d14">}</span><span style="color:#d14">&#39; write &#39;</span><span style="color:#d14">{</span>optimized<span style="color:#d14">}</span><span style="color:#d14">&#39;&#34;</span>
            ],
            <span style="color:#d14">&#34;targets&#34;</span>: [optimized],         <span style="color:#998;font-style:italic"># (1)</span>
            <span style="color:#d14">&#34;file_dep&#34;</span>: [source, VPY],      <span style="color:#998;font-style:italic"># (2)</span>
        }
</code></pre></div><ol>
<li>The <code>&quot;targets&quot;</code> entry is a list of all the files generated by the sub-task. In our case, there is only one, whose path is stored in the <code>optimized</code> variable.</li>
<li>The <code>&quot;file_dep&quot;</code> entry is a list of all the files the sub-task depends on. In our case, both the source SVG and the VPY file are involved to create an optimised SVG, so we list them both.</li>
</ol>
<p>It would be easy to forget the VPY file in the <code>&quot;file_dep&quot;</code> entry. That would be a mistake. All the optimised SVGs should be regenerated when the VPY file is modified. For <em>doit</em> to realise this, we must list the VPY file as a dependency.</p>
<p>With the modification above, <em>doit</em> now knows when to run optimisation sub-tasks and when they can be skipped.</p>
<p>Let&rsquo;s experiment with a clean slate by deleting all the processed files:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ rm processed/*.svg
</code></pre></div><p><em>doit</em> must now execute all sub-tasks:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit
.  optimize:dots
.  optimize:halftone
.  optimize:hline
</code></pre></div><p>Notice the dot (<code>.</code>) prefixing each line and how the execution is relatively slow.</p>
<p>Now, this is what happens if we run <em>doit</em> again:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit
-- optimize:dots
-- optimize:halftone
-- optimize:hline
</code></pre></div><p>Execution time is now much faster and each line is now prefixed with <code>--</code>, indicating that <em>doit</em> skipped the corresponding sub-task.</p>
<p>Let&rsquo;s see what happens if one of the source file is modified.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ <span style="color:#0086b3">echo</span> <span style="color:#d14">&#34; &#34;</span> &gt;&gt; originals/halftone.svg
$ doit
-- optimize:dots
.  optimize:halftone
-- optimize:hline
</code></pre></div><p>We first append a single space to the <code>halftone.svg</code> (which is harmless on a valid SVG) to simulate a change<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>. As expected, <em>doit</em> rebuilds the of <code>halftone.svg</code> without running the other tasks! 🎉</p>
<p>We now have a setup able to automatically process large batches of files and be smart about if/when any sub-task must be repeated. You have a thousand SVGs to process? It&rsquo;s coffee time while the CPUs churn through them<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>. You add just one to the list? Instant results, thanks to <em>doit</em>!</p>
<h2 id="cleaning-up">Cleaning up</h2>
<p>The files created by the <code>optimize</code> task can be considered &ldquo;temporary&rdquo;. When missing, they are automatically recreated by <em>doit</em>, and are overwritten by a new version when the input file (or the VPY file) change. In that sense, they matter much less than the source SVGs and the <code>dodo.py</code> file, which collectively form the &ldquo;recipe&rdquo; to build the optimised SVGs<sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>.</p>
<p>The ability to delete these files may occasionally be useful. For example, to force a complete rebuild of the optimised files, to make an archive with only the true source files, or simply to free some disk space.</p>
<p><em>doit</em> provides this feature with a single modification to the <code>dodo.py</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">import</span> <span style="color:#555">pathlib</span>

DIR <span style="color:#000;font-weight:bold">=</span> pathlib<span style="color:#000;font-weight:bold">.</span>Path(__file__)<span style="color:#000;font-weight:bold">.</span>parent
SOURCES <span style="color:#000;font-weight:bold">=</span> <span style="color:#0086b3">list</span>((DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;originals&#34;</span>)<span style="color:#000;font-weight:bold">.</span>glob(<span style="color:#d14">&#34;*.svg&#34;</span>))
VPY <span style="color:#000;font-weight:bold">=</span> DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;optimize.vpy&#34;</span>

<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_optimize</span>():
    <span style="color:#d14">&#34;&#34;&#34;optimize SVGs&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">for</span> source <span style="color:#000;font-weight:bold">in</span> SOURCES:
        optimized <span style="color:#000;font-weight:bold">=</span> DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;processed&#34;</span> <span style="color:#000;font-weight:bold">/</span> (source<span style="color:#000;font-weight:bold">.</span>stem <span style="color:#000;font-weight:bold">+</span> <span style="color:#d14">&#34;_optimized.svg&#34;</span>)
        <span style="color:#000;font-weight:bold">yield</span> {
            <span style="color:#d14">&#34;name&#34;</span>: source<span style="color:#000;font-weight:bold">.</span>stem,
            <span style="color:#d14">&#34;actions&#34;</span>: [
                <span style="color:#d14">f</span><span style="color:#d14">&#34;vpype read &#39;</span><span style="color:#d14">{</span>source<span style="color:#d14">}</span><span style="color:#d14">&#39; -I &#39;</span><span style="color:#d14">{</span>VPY<span style="color:#d14">}</span><span style="color:#d14">&#39; write &#39;</span><span style="color:#d14">{</span>optimized<span style="color:#d14">}</span><span style="color:#d14">&#39;&#34;</span>
            ],
            <span style="color:#d14">&#34;targets&#34;</span>: [optimized],
            <span style="color:#d14">&#34;file_dep&#34;</span>: [source, VPY],
            <span style="color:#d14">&#34;clean&#34;</span>: <span style="color:#000;font-weight:bold">True</span>,                  <span style="color:#998;font-style:italic"># (1)</span>
        }
</code></pre></div><ol>
<li>Tell <em>doit</em> that target files should be deleted when running <code>doit clean</code>.</li>
</ol>
<p>Let&rsquo;s see this in action:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit clean
optimize:hline - removing file <span style="color:#d14">&#39;.../processed/hline_optimized.svg&#39;</span>
optimize:halftone - removing file <span style="color:#d14">&#39;.../processed/halftone_optimized.svg&#39;</span>
optimize:dots - removing file <span style="color:#d14">&#39;.../processed/dots_optimized.svg&#39;</span>
</code></pre></div><p>Works as expected! 🎉</p>
<h2 id="multiple-tasks">Multiple tasks</h2>
<p>Although <em>doit</em> already shines dealing with a single task, it reveals its true power when multiple tasks are involved&mdash;even more so when they depend on each other.</p>
<p>For the illustration purposes, let&rsquo;s imagine that we need to convert the optimised SVGs to HPGL, so that we may plot them on a shiny &lsquo;83 <a href="http://www.hpmuseum.net/display_item.php?hw=74">HP 7475a</a>. We&rsquo;ll add a second task for this<sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>.</p>
<p>First, let&rsquo;s start by creating a new <code>hpgl</code> subdirectory to store the HPGL files:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ mkdir hpgl
</code></pre></div><p>Since we cleaned the optimised SVGs in the previous steps, this how your project directory should look:</p>
<pre tabindex="0"><code>.
├── dodo.py
├── hpgl/
├── optimize.vpy
├── originals/
│   ├── dots.svg
│   ├── halftone.svg
│   └── hline.svg
└── processed/
</code></pre><p>Now, update the <code>dodo.py</code> file with the following content:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">import</span> <span style="color:#555">pathlib</span>

DIR <span style="color:#000;font-weight:bold">=</span> pathlib<span style="color:#000;font-weight:bold">.</span>Path(__file__)<span style="color:#000;font-weight:bold">.</span>parent
SOURCES <span style="color:#000;font-weight:bold">=</span> <span style="color:#0086b3">list</span>((DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;originals&#34;</span>)<span style="color:#000;font-weight:bold">.</span>glob(<span style="color:#d14">&#34;*.svg&#34;</span>))
VPY <span style="color:#000;font-weight:bold">=</span> DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;optimize.vpy&#34;</span>

<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">optimized_path</span>(source: pathlib<span style="color:#000;font-weight:bold">.</span>Path):                              <span style="color:#998;font-style:italic"># (1)</span>
    <span style="color:#d14">&#34;&#34;&#34;derive optimized path from source path&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">return</span> DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;processed&#34;</span> <span style="color:#000;font-weight:bold">/</span> (source<span style="color:#000;font-weight:bold">.</span>stem <span style="color:#000;font-weight:bold">+</span> <span style="color:#d14">&#34;_optimized.svg&#34;</span>)

<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">hpgl_path</span>(source: pathlib<span style="color:#000;font-weight:bold">.</span>Path):                                   <span style="color:#998;font-style:italic"># (2)</span>
    <span style="color:#d14">&#34;&#34;&#34;derive HPGL path from source path&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">return</span> DIR <span style="color:#000;font-weight:bold">/</span> <span style="color:#d14">&#34;hpgl&#34;</span> <span style="color:#000;font-weight:bold">/</span> (source<span style="color:#000;font-weight:bold">.</span>stem <span style="color:#000;font-weight:bold">+</span> <span style="color:#d14">&#34;.hpgl&#34;</span>)

<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_optimize</span>():
    <span style="color:#d14">&#34;&#34;&#34;optimize SVGs&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">for</span> source <span style="color:#000;font-weight:bold">in</span> SOURCES:
        optimized <span style="color:#000;font-weight:bold">=</span> optimized_path(source)                             <span style="color:#998;font-style:italic"># (3)</span>
        <span style="color:#000;font-weight:bold">yield</span> {
            <span style="color:#d14">&#34;name&#34;</span>: source<span style="color:#000;font-weight:bold">.</span>stem,
            <span style="color:#d14">&#34;actions&#34;</span>: [
                <span style="color:#d14">f</span><span style="color:#d14">&#34;vpype read &#39;</span><span style="color:#d14">{</span>source<span style="color:#d14">}</span><span style="color:#d14">&#39; -I &#39;</span><span style="color:#d14">{</span>VPY<span style="color:#d14">}</span><span style="color:#d14">&#39; write &#39;</span><span style="color:#d14">{</span>optimized<span style="color:#d14">}</span><span style="color:#d14">&#39;&#34;</span>
            ],
            <span style="color:#d14">&#34;file_dep&#34;</span>: [source, VPY],
            <span style="color:#d14">&#34;targets&#34;</span>: [optimized],
            <span style="color:#d14">&#34;clean&#34;</span>: <span style="color:#000;font-weight:bold">True</span>,
        }

<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_hpgl</span>():
    <span style="color:#d14">&#34;&#34;&#34;convert to HPGL&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">for</span> source <span style="color:#000;font-weight:bold">in</span> SOURCES:                                             <span style="color:#998;font-style:italic"># (4)</span>
        optimized <span style="color:#000;font-weight:bold">=</span> optimized_path(source)                             <span style="color:#998;font-style:italic"># (5)</span>
        hpgl <span style="color:#000;font-weight:bold">=</span> hpgl_path(source)
        <span style="color:#000;font-weight:bold">yield</span> {
            <span style="color:#d14">&#34;name&#34;</span>: source<span style="color:#000;font-weight:bold">.</span>stem,
            <span style="color:#d14">&#34;actions&#34;</span>: [
                <span style="color:#d14">f</span><span style="color:#d14">&#34;vpype read &#39;</span><span style="color:#d14">{</span>optimized<span style="color:#d14">}</span><span style="color:#d14">&#39; write -d hp7475a -p a4 -q -c &#39;</span><span style="color:#d14">{</span>hpgl<span style="color:#d14">}</span><span style="color:#d14">&#39;&#34;</span>
            ],
            <span style="color:#d14">&#34;file_dep&#34;</span>: [optimized],                                   <span style="color:#998;font-style:italic"># (6)</span>
            <span style="color:#d14">&#34;targets&#34;</span>: [hpgl],                                         <span style="color:#998;font-style:italic"># (7)</span>
            <span style="color:#d14">&#34;clean&#34;</span>: <span style="color:#000;font-weight:bold">True</span>,
        }
</code></pre></div><p>Let&rsquo;s examine the changes one-by-one.</p>
<ol>
<li>To clean things up and avoid code duplication, we factored in <code>optimized_path()</code> the code to derive the path of an optimised SVG from a source SVG.</li>
<li>We do the same to derive the path of an HPGL output from a source SVG in the <code>hpgl_path()</code> function. Note that neither of these function names start with <code>task_</code>, so they aren&rsquo;t interpreted as tasks by <em>doit</em>.</li>
<li>The only change to the <code>optimize</code> task is to use the <code>optimized_path()</code> helper function.</li>
<li>This part is interesting. The purpose of the <code>hpgl</code> task is to convert optimised SVG into HPGL files, yet we iterate over the <em>source</em> SVGs instead. The reason is, for our purposes, <code>SOURCES</code> is our master &ldquo;TODO list&rdquo;. Everything the <code>hpgl</code> task must do is indirectly due to the presence of source SVGs.</li>
<li>The source path is used <em>only</em> to derive the paths for the optimised SVG as well as the HPGL output. In particular, notice how <code>source</code> is not used anywhere in the return dictionaries.</li>
<li>The optimised SVGs is now a dependency (as opposed to a target in the <code>optimize</code> task).</li>
<li>Instead, the target is the HPGL file.</li>
</ol>
<p>These two tasks collectively form a &ldquo;pipeline&rdquo;. The output (or <em>target</em>) of the first task corresponds to the input (or <em>dependency</em>) of the second. <em>doit</em> understands that thanks to the <code>&quot;file_dep&quot;</code> and <code>&quot;targets&quot;</code> entries being properly populated&mdash;and can now be smart about it!</p>
<p>Let&rsquo;s take it for a spin by executing the <code>hpgl</code> task:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit hpgl
.  optimize:dots
.  optimize:halftone
.  optimize:hline
.  hpgl:dots
.  hpgl:halftone
.  hpgl:hline
</code></pre></div><p><em>doit</em> knows that it needs optimised SVGs to create HPGL file, so it automatically executes the <code>optimize</code> task.</p>
<p>Let&rsquo;s remove a single HPGL file to test what happens. This can be done using the <code>doit clean</code> command:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit clean hpgl:hline
hpgl:hline - removing file <span style="color:#d14">&#39;.../hpgl/hline.hpgl&#39;</span>
</code></pre></div><p>This is what happens when we run the <code>hpgl</code> task again:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit hpgl
-- optimize:dots
-- optimize:halftone
-- optimize:hline
-- hpgl:dots
-- hpgl:halftone
.  hpgl:hline
</code></pre></div><p>The optimised version of <code>hline.svg</code> is still present and up-to-date, so the corresponding task is skipped. Only the HPGL conversion is executed.</p>
<p>Now, let&rsquo;s change one of the source files, like we did earlier:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ <span style="color:#0086b3">echo</span> <span style="color:#d14">&#34; &#34;</span> &gt;&gt; originals/dots.svg  
$ doit hpgl
.  optimize:dots
-- optimize:halftone
-- optimize:hline
.  hpgl:dots
-- hpgl:halftone
-- hpgl:hline
</code></pre></div><p><em>doit</em> correctly runs both the <code>optimize</code> and <code>hpgl</code> sub-tasks for the corresponding file! 🎉</p>
<h2 id="helper-tasks">Helper tasks</h2>
<p>Tasks don&rsquo;t <em>have</em> to be part of an intricate pipeline with carefully specified targets and dependencies. They can also be just a nice little helper that encapsulate a useful shell command.</p>
<p>Consider for example this task, which can readily be added to our <code>dodo.py</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">task_show</span>():
    <span style="color:#d14">&#34;&#34;&#34;display SVG&#34;&#34;&#34;</span>
    <span style="color:#000;font-weight:bold">for</span> source <span style="color:#000;font-weight:bold">in</span> SOURCES:
        <span style="color:#000;font-weight:bold">yield</span> {
            <span style="color:#d14">&#34;name&#34;</span>: source<span style="color:#000;font-weight:bold">.</span>stem,
            <span style="color:#d14">&#34;actions&#34;</span>: [<span style="color:#d14">f</span><span style="color:#d14">&#34;vpype read </span><span style="color:#d14">{</span>source<span style="color:#d14">}</span><span style="color:#d14"> show&#34;</span>],
        }
</code></pre></div><p>Its action consist of loading the source SVG and displaying it with <em>vpype</em>. This isn&rsquo;t necessarily part of your workflow, but is convenient to have handy:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ doit show:dots
</code></pre></div><p>The corresponding SVG is displayed by the <em>vpype</em> viewer:</p>
<img src="/batch-processing-doit-vpype/dots.png"alt="*vpype* viewer display a SVGs containing many dots arranged in a circle"width="80%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>This example is taken from <a href="https://github.com/abey79/vpype-perspective"><em>vpype-perspective</em></a>, where all the README&rsquo;s figures are made from VPYs files stored in the repository&rsquo;s <a href="https://github.com/abey79/vpype-perspective/tree/main/examples/figures"><code>examples/figures</code></a> subdirectory. The conversion of these VPYs into SVGs is handled by <em>doit</em> using this <a href="https://github.com/abey79/vpype-perspective/blob/main/examples/dodo.py"><code>dodo.py</code></a> file. It&rsquo;s a nice example of what can be done with <em>doit</em>.</p>
<h2 id="final-words">Final words</h2>
<p>If you made it that far, I hope you are convinced of how useful <em>doit</em> is for workflow automation.</p>
<p>In this article, I focused on <em>vpype</em>, but <em>doit</em> can be used for entirely different things. As a matter of fact, I used it to automate my <a href="https://twitter.com/abey79/status/1528735353741484033">#plotloop machine</a>, which I&rsquo;ve described in my <a href="/articles/2022/12/22/automatic-plotloop-machine/">next article</a>.</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/w_PPPImmEN8" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="The Automatic #plotloop Machine"></iframe>
</div>

<br/>
<p>One of <em>doit</em> drawbacks is the fact that its <code>dodo.py</code> file is written in Python. Creating one requires at least <em>some</em> Python basics&mdash;or willingness to acquire them. This might put off people uninterested by code.</p>
<p>But this is also its greatest strength. You wield the full power of Python when writing your <code>dodo.py</code> file, without any of the constraints of configuration languages such as <a href="https://yaml.org">YAML</a> or <a href="https://toml.io/en/">TOML</a>. This extends the possibilities <em>much</em> further than what was covered here, and makes learning <em>doit</em> a great investment! 🎯</p>
<p>Ready to take the plunge? I&rsquo;m happy to help&mdash;just share details of your workflow in the comments 👇, on <a href="https://twitter.com/abey79">Twitter</a>/<a href="https://mastodon.social/@abey79">Mastodon</a>, or on the <a href="https://discord.com/invite/XHP3dBg">Drawingbots Discord</a>.</p>
<p><em><strong>Edit</strong>: TIL what &ldquo;dolt&rdquo; (lowercase L) means 😅, and changed DoIt (uppercase i) into <em>doit</em>, consistently with their documentation.</em></p>
<p><em><strong>Edit</strong>: Added a link to the Automatic #plotloop Machine article and updated the video.</em></p>
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1" role="doc-endnote">
<p>The file may also have a different name, or be located elsewhere, but then its path should be provided to <code>doit</code>. Using <code>dodo.py</code> is simpler because this file is automatically detected and loaded by <em>doit</em>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>The code actually generates full paths.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3" role="doc-endnote">
<p>If you are used to <code>make</code> and similar systems, you might be tempted to <code>touch originals/halftone.svg</code> to trigger a rebuild instead of modifying the file&rsquo;s content. This doesn&rsquo;t work with <em>doit</em> as it uses a local database and file hashes instead of modification date to track dependencies.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4" role="doc-endnote">
<p>By the way, you can parallelise the processing of large batches using <code>doit -n 8 optimize</code>, where <code>8</code> is the number of CPU cores to use.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5" role="doc-endnote">
<p>This bears strong similarities with software build systems, where compiled object files are created from source code by the compiler. As a matter of fact, <em>doit</em> can serve as a build system.&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6" role="doc-endnote">
<p>This example is slightly over-engineered. <em>vpype</em> can optimise and export to HPGL in one command, so technically a single <em>doit</em> task is needed. Even if multiple commands were required (<em>vpype</em> or otherwise), they can all be listed in a single <em>doit</em> task&mdash;the <code>&quot;actions&quot;</code> entry is a list which can contain multiple items. It is still a relevant illustration for the many instances were multiple <em>doit</em> tasks are indeed useful.&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</section>]]></content:encoded>
    </item>
    
    <item>
      <title>TIL: running Textual on a framebuffer terminal emulator for Linux</title>
      <link>https://bylr.info/articles/2022/10/29/til-textual-fb-term/</link>
      <pubDate>Sat, 29 Oct 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/10/29/til-textual-fb-term/</guid>
      <description>My successful (yet probably vain) attempt at running Textual (a modern TUI framework) on a Raspberry Pi without X11 by using fbpad (a framebuffer terminal emulator for Linux).</description>
      <content:encoded><![CDATA[<p>Excuse me&hellip; Running a what on what terminal what? 🤔</p>
<p>So, here is the thing. A <a href="https://www.raspberrypi.org">Raspberry PI</a> paired with a touch-screen can serve as a touch-based, human-machine interface (HMI) for things like DIY projects, robots, and whatnot. For example, this is how I control my <a href="https://axidraw.com">Axidraw</a> plotter. The current implementation of my HMI software – named <a href="https://github.com/plottertools/taxi">taxi</a> – uses <a href="https://kivy.org">Kivy</a>. Here it is in action:</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/kELtKbjg-fo" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="Mobile plotting station"></iframe>
</div>

<br/>
<p>This requires a full X11 and desktop environment (e.g. <a href="http://www.lxde.org">LXDE</a>, used by default by <a href="https://www.raspberrypi.com/software/">Raspberry Pi OS</a>), which is heavy (the full Raspberry Pi OS doesn&rsquo;t fit on a 4GB memory card) and harder to turn into kiosk mode.</p>
<p>Ever since I first learned of the modern <a href="https://en.wikipedia.org/wiki/Text-based_user_interface">TUI</a> framework <a href="https://textual.textualize.io">Textual</a>, I&rsquo;ve been thinking of using it as a lightweight, kiosk-mode, touch-based HMI with minimal requirements for the underlying OS. All it needs is a terminal!</p>
<p>My first idea was to simply use the <a href="https://en.wikipedia.org/wiki/Linux_console">Linux console</a>, which is the terminal-y thing you see at boot, and can use to login if you don&rsquo;t have a X11/desktop environment. Unfortunately, due to severe limitations in the number of glyphs it can handle (or of my comprehension of it), the results were&hellip; underwhelming:</p>
<img src="/til-textual-fb-term/console-parallels.png"alt="`python -m textual` running with the Linux console on a Debian VM"width="80%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>This screenshot was made with a <a href="https://www.debian.org">Debian</a> VM running in <a href="https://www.parallels.com">Parallels Desktop</a>, but the it&rsquo;s the same for a Raspberry Pi and a physical screen.</p>
<p>Then I learned about framebuffer terminal emulators. The <a href="https://en.wikipedia.org/wiki/Linux_framebuffer">Linux framebuffer</a> is a subsystem to display on-screen graphics over the system console without relying on a X11 server. Framebuffer terminal emulators basically emulate a terminal and &ldquo;draw&rdquo; it to the screen using the Linux framebuffer. As it turns out, they are extremely niche pieces of software, so it felt like a trip to the past to get them to run! 😅</p>
<p>For a bunch of reasons discussed below, this is most likely a dead-end in my quest. Still, I learned a few things and I surely won&rsquo;t remember any of it unless it&rsquo;s writen down.</p>
<h2 id="the-plan">The plan</h2>
<p>I found a bunch of framebuffer terminal emulators, including <a href="https://docs.kernel.org/fb/fbcon.html">fbcon</a>, <a href="https://github.com/sfzhi/fbterm">fbterm</a>, <a href="https://github.com/bleenco/bterm">bterm</a>, <a href="https://github.com/uobikiemukot/yaft">yaft</a>, and <a href="https://github.com/aligrudi/fbpad">fbpad</a>.  I tried some of them (not all), and fbpad, by <a href="http://litcave.rudi.ir">Ali Gholami Rudi</a>, was the first to yield decent results, so it&rsquo;ll be the focus of this article.</p>
<p>Here is an overview of the steps:</p>
<ol>
<li>Download a suitable TTF font.</li>
<li>Download and build ft2tf, and use it to convert the font to fbpad&rsquo;s custom format.</li>
<li>Download, configure, and build fbpad.</li>
<li>Download Textual and run it in fbpad.</li>
</ol>
<p>I&rsquo;ll assume a Debian type of OS, like Raspberry Pi OS, Ubuntu, or a Debian distro.</p>
<h2 id="preparing-the-font">Preparing the font</h2>
<p>First, let&rsquo;s download a suitable monospace font. I chose to use <a href="https://fonts.google.com/specimen/Fira+Code">Fira Code</a>.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#0086b3">cd</span> ~
wget -O fira.zip <span style="color:#d14">&#34;https://fonts.google.com/download?family=Fira%20Code&#34;</span>
unzip fira.zip -d fira
</code></pre></div><p>Then, download and build ft2tf, a font conversion tool by the same author as fbpad:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#0086b3">cd</span> ~
apt-get install libfreetype-dev
wget http://litcave.rudi.ir/ft2tf-0.9.tar.gz
tar xzf ft2tf-0.9.tar.gz
<span style="color:#0086b3">cd</span> ft2tf-0.9
make
</code></pre></div><p>Now we&rsquo;re ready to convert the font:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">./ft2tf -h18 -w10 ~/fira/static/FiraCode-Regular.ttf:6 &gt; ~/fira/fira.tf
</code></pre></div><p>The <code>-h</code> and <code>-w</code> options specify the final glyph size in the terminal, which in turn determines the column and row count based on your screen resolution. The <code>:6</code> part after the font path specifies the size at which the TTF font is scaled before conversion. I found these values to work decently well for my Raspberry Pi&rsquo;s screen (a <a href="https://www.waveshare.com/wiki/7inch_HDMI_LCD_(C)">Waveshare 7&quot; HDMI LCD</a>), but YMMV.</p>
<h2 id="configure-and-build-fbpad">Configure and build fbpad</h2>
<p>First, download fbpad:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#0086b3">cd</span> ~
git clone https://github.com/aligrudi/fbpad
<span style="color:#0086b3">cd</span> fbpad
</code></pre></div><p>Then, edit <code>conf.h</code> and change these two options:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-c" data-lang="c"><span style="color:#998;font-style:italic">// ...
</span><span style="color:#998;font-style:italic"></span>
<span style="color:#999;font-weight:bold;font-style:italic">#define TERM      &#34;xterm-256color&#34;
</span><span style="color:#999;font-weight:bold;font-style:italic">#define FR        &#34;/home/USERNAME/fira/fira.tf&#34;
</span><span style="color:#999;font-weight:bold;font-style:italic"></span>
<span style="color:#998;font-style:italic">// ...
</span></code></pre></div><p>Obviously, use your actual username. You can leave the rest unchanged.</p>
<p>Finally, compile fbpad:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">make
</code></pre></div><h2 id="running-textual-in-fbpad">Running Textual in fbpad</h2>
<p>First, grab a copy of Textual (here from source, using <a href="https://python-poetry.org">Poetry</a> – installing in a venv should work as well):</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#0086b3">cd</span> ~
git clone https://github.com/Textualize/textual.git
<span style="color:#0086b3">cd</span> textual
poetry install
</code></pre></div><p>We&rsquo;re finally ready to roll! Here is Textual color reference:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#008080">TERMCOLOR</span><span style="color:#000;font-weight:bold">=</span>truecolor ~/fbpad/fbpad poetry run textual colors
</code></pre></div><p>Note that <code>TERMCOLOR=truecolor</code> is required to have 256 colors, but it doesn&rsquo;t actually enable 24bit true color mode, which fbpad does not support.</p>
<p>Here is how it looks like in the Debian VM:</p>
<img src="/til-textual-fb-term/success-parallels.png"alt="`textual colors` running with fbpad on a Debian VM"width="80%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>The Textual demo can ben run with the following command:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#008080">TERMCOLOR</span><span style="color:#000;font-weight:bold">=</span>truecolor ~/fbpad/fbpad poetry run python -m textual
</code></pre></div><img src="/til-textual-fb-term/success-parallels-demo.png"alt="`python -m demo` running with fbpad on a Debian VM"width="80%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Finally, here is a video of how it runs on the actual Raspberry Pi:</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/9P-qb6ux4VI" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="Textual running on a X11-less Raspberry Pi with a framebuffer terminal emulator"></iframe>
</div>

<br/>
<h2 id="final-words">Final words</h2>
<p>As I mentioned before, this approach has many limitations:</p>
<ul>
<li>It&rsquo;s obviously cumbersome to setup – compiled configuration file anyone??</li>
<li>Hardly any of the framebuffer terminal emulators are active and well maintained projects.</li>
<li>None of them have mouse support – let alone handle a touch-screen<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</li>
<li>The performance is bad, without an ounce of hardware acceleration.</li>
</ul>
<p>For my purposes, I&rsquo;ll likely move on to a bare-bone X11 setup without windows manager nor Desktop environment (basically <code>startx</code> + fullscreen <code>xterm</code>), since it addresses most of these limitations. Still, it was a fun niche to dig into! 🤓</p>
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1" role="doc-endnote">
<p>Extending Textual to support a touch-screen on Linux can be done with <a href="https://python-evdev.readthedocs.io"><code>python-evdev</code></a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</section>
]]></content:encoded>
    </item>
    
    <item>
      <title>Annotated Release Notes: vpype 1.12</title>
      <link>https://bylr.info/articles/2022/10/25/annotated-release-notes-vpype-1.12/</link>
      <pubDate>Tue, 25 Oct 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/10/25/annotated-release-notes-vpype-1.12/</guid>
      <description>&lt;p&gt;&lt;em&gt;vpype&lt;/em&gt; 1.12 is out! 🎉&lt;/p&gt;
&lt;p&gt;No ground-breaking features, but an improved &amp;ldquo;quality-of-life&amp;rdquo;, especially for Apple-silicon Mac owners, and few other goodies.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s dive in.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>vpype</em> 1.12 is out! 🎉</p>
<p>No ground-breaking features, but an improved &ldquo;quality-of-life&rdquo;, especially for Apple-silicon Mac owners, and few other goodies.</p>
<p>Let&rsquo;s dive in.</p>
<h2 id="migration-to-pyside6">Migration to PySide6</h2>

<blockquote class="extract">
  <p><ul>
<li>Migrated to PySide6 (from PySide2), which simplifies installation on Apple silicon Macs (#552, #559, #567)</li>
</ul>
</p>
</blockquote>

<p>PySide2 is the official Python wrapper for <a href="https://doc.qt.io/qt-5/">Qt 5</a>, the GUI toolkit I use for the viewer. As Qt 5 doesn&rsquo;t officially support Apple-silicon Macs, PySide2 – and thus <em>vpype</em> until now – were notoriously difficult to install on these computers. This is resolved with the transition to PySide6, which wraps <a href="https://www.qt.io/product/qt6">Qt 6</a> and officially supports Apple-silicon Macs.</p>
<p>This took me waaaay too long. I actually feel bad for <a href="https://github.com/abey79/vpype/issues/320">the struggle</a> incurred to <em>vpype</em> users 😅 All things considered, the migration wasn&rsquo;t that complicated, but there were still <a href="https://github.com/abey79/vpype/pull/552">a few pitfalls</a> to figure out due to Qt 6 breaking changes around the OpenGL-based widget.</p>
<p>Migrating to PySide6 is also a major step towards supporting Python 3.11, which brings a <a href="https://docs.python.org/3.11/whatsnew/3.11.html">host of novelties</a> as well as a significant performance bumps. I&rsquo;m hoping this will happen by the next release, which means <em>vpype</em> 1.12 might well be the last to support Python 3.8.</p>
<h2 id="other-fixes-and-improvements">Other fixes and improvements</h2>

<blockquote class="extract">
  <p><ul>
<li>The <code>layout</code> command now properly handles the <code>tight</code> special case by fitting the page size around the existing geometries, accommodating for a margin if provided (#556)</li>
<li>Fixed a viewer issue where page width/height of 0 would lead to errors and a blank display (#555)</li>
</ul>
</p>
</blockquote>

<p>Using <code>layout tight</code> would formerly set the page size to 0 by 0, which is useless in itself and caused a blank display. Not only the blanking issue has been resolved, but <code>layout tight</code> is now actually useful. It sets the page size to fit exactly the current geometries, accounting for a margin if <code>--fit-to-margin MARGIN</code> is provided.</p>

<blockquote class="extract">
  <p><ul>
<li>Added <code>inch</code> unit as a synonym to <code>in</code>, useful for expressions (in which <code>in</code> is a reserved keyword) (#541)</li>
</ul>
</p>
</blockquote>

<p>This addresses an oversight introduced with expressions in <a href="/articles/2022/03/03/annotated-release-notes-vpype-1.9/"><em>vpype</em> 1.9</a>. The units available for length CLI options are also available as scaling factor in expressions. For example, this creates a 10x15 cm rectangle:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vpype rect <span style="color:#099">0</span> <span style="color:#099">0</span> 10cm <span style="color:#d14">&#39;%15*cm%&#39;</span> show
</code></pre></div><p>The expression works because the <code>cm</code> variable is made available by the interpreter, and set to the conversion factor between centimetres and pixels. This would however break for inches, because <code>in</code> is a <a href="https://docs.python.org/3/reference/lexical_analysis.html#keywords">reserved keyword</a> in Python. The variable <code>inch</code> is now available instead. Either form can be used in CLI options, but <code>inch</code> must be used in expressions:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">vpype circle 5in 5inch <span style="color:#d14">&#39;%3*inch%&#39;</span> show
</code></pre></div>
<blockquote class="extract">
  <p><ul>
<li>Fixed a viewer issue where fitting the view to the document would not adjust when page size changes (<em>vsketch</em> only) (#564)</li>
</ul>
</p>
</blockquote>

<p>This change doesn&rsquo;t directly benefits <em>vpype</em> as the page size cannot change while the viewer is active. In <em>vsketch</em>, however, the sketch code is free to set/change the page size based on GUI parameters, like in the included <code>quick_draw</code> example. In this case, when the view is fitted to the page size (i.e. as long as you don&rsquo;t zoom or scroll), the view will adjust when the page size changes.</p>

<blockquote class="extract">
  <p><ul>
<li>Updated <a href="https://github.com/meerk40t/svgelements">svgelements</a> to 1.8.4, which fixes issue with some SVG constructs used by Matplotlib exports (#549)</li>
</ul>
</p>
</blockquote>

<p>Supporting all of the SVG standard subtleties is <em>hard</em>. Not only <a href="https://github.com/meerk40t/svgelements">svgelements</a> does a great job at it, but <a href="https://github.com/tatarize">@tatarize</a>&rsquo;s reactivity when edge cases appear is unmatched. In this instance, <a href="https://drawingbots.net">Drawingbots</a>' Discord user <em>apur</em> wanted to plot <a href="https://matplotlib.org">Matplotlib</a>-generated SVGs of <a href="https://www.latex-project.org">LaTeX</a> equations. They included unusual <code>&lt;use&gt;</code> elements, which didn&rsquo;t import properly. This is now fixed and I eagerly await the next niche corner case! 🤗</p>

<blockquote class="extract">
  <p><ul>
<li>Migrated to <a href="https://plausible.io">Plausible.io</a> (from Google Analytics) for <a href="https://vpype.readthedocs.io">vpype.readthedocs.io</a> (#546)</li>
</ul>
</p>
</blockquote>

<p><a href="https://plausible.io">Plausible</a> is a privacy-focused, <a href="https://gdpr-info.eu">GDPR</a>-compliant web statistics service. As I did for this site, I migrated from Google Analytics with my projects' documentation web sites. This is a paid service, so that neither your or I are the product.</p>
<h2 id="mystery-changes">Mystery changes</h2>

<blockquote class="extract">
  <p><ul>
<li>Added new units (<code>yd</code>, <code>mi</code>, and <code>km</code>) (#541)</li>
<li>Added <code>vpype.format_length()</code> to convert pixel length into human-readable string with units (#541)</li>
</ul>
</p>
</blockquote>

<p>Yes, <em>vpype</em> supports kilometer-scale plots!<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>Seriously though, these changes are part of the WIP improvements of <em>vpype</em>&rsquo;s terminal output. This will happen in future versions, but for some reason it was easier to integrate those change early.</p>
<h2 id="developer-related-changes">Developer-related changes</h2>

<blockquote class="extract">
  <p><ul>
<li><a href="https://python-poetry.org">Poetry</a> 1.2 or later is not required (developer only) (#541)</li>
<li>A <code>justfile</code> is now provided for most common operations (install, build the documentation, etc.) (#541)</li>
</ul>
</p>
</blockquote>

<p>Shoutout to these two great dev tools: <a href="https://python-poetry.org">Poetry</a> (Python dependency management) and <a href="https://just.systems">just</a> (<a href="https://www.gnu.org/software/make/">make</a> replacement for useful commands). I use them on a daily basis!</p>
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1" role="doc-endnote">
<p>plotter not included&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</section>]]></content:encoded>
    </item>
    
    <item>
      <title>TIL: use Plausible.io with a Sphinx documentation hosted on RTD</title>
      <link>https://bylr.info/articles/2022/10/09/til-plausible-rtd/</link>
      <pubDate>Sun, 09 Oct 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/10/09/til-plausible-rtd/</guid>
      <description>Although Google Analytics is very easy to setup on a Read the Docs-based documentation website, it requires a cookie banner to be GDPR-compliant and is otherwise questionable from a privacy-preservation point-of-view. As a result, I much prefer to use and support the excellent EU-based Plausible.io for traffic metrics instead.
This article explains how to setup a Read the Docs-based documentation with Plausible.io such that metrics are enabled only on &amp;ldquo;production&amp;rdquo; builds — e.</description>
      <content:encoded><![CDATA[<p>Although Google Analytics is very easy to setup on a <a href="https://readthedocs.org"><em>Read the Docs</em></a>-based documentation website, it requires a cookie banner to be <a href="https://en.wikipedia.org/wiki/General_Data_Protection_Regulation">GDPR</a>-compliant and is otherwise questionable from a privacy-preservation point-of-view. As a result, I much prefer to use and support the excellent EU-based <a href="https://plausible.io/">Plausible.io</a> for traffic metrics instead.</p>
<p>This article explains how to setup a <em>Read the Docs</em>-based documentation with Plausible.io such that metrics are enabled only on &ldquo;production&rdquo; builds — e.g. the &ldquo;latest&rdquo; builds from the main branch and the version-tagged builds. This minimises the contamination of traffic statistics by development-related activities.</p>
<p>To achieve this, the basic idea is to customise your Sphinx template such that the <a href="https://plausible.io/docs/plausible-script">Plausible.io script</a> is only included when a <code>conf.py</code>-defined flag is set to <code>True</code>. This flag is then set based on <a href="https://docs.readthedocs.io/en/stable/environment-variables.html">environment variables provided by <em>Read the Docs</em></a>.</p>
<p>Let&rsquo;s dive in the details a step at a time.</p>
<h2 id="enabling-templates">Enabling templates</h2>
<p>If you haven&rsquo;t done so already (for example to <a href="https://bylr.info/articles/2022/05/10/api-doc-with-sphinx-autoapi/">customise your API documentation</a>), create a <code>_templates</code> sub-directory and let Sphinx know that this is where custom templates are to be found:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#998;font-style:italic"># conf.py</span>

templates_path <span style="color:#000;font-weight:bold">=</span> [<span style="color:#d14">&#34;_templates&#34;</span>]
</code></pre></div><h2 id="customising-the-template">Customising the template</h2>
<p>Then, a custom template can be created to include the Plausible.io script. Create a <code>_templates/base.html</code> file with the following content:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html"><span style="color:#998;font-style:italic">&lt;!-- _templates/base.html --&gt;</span>
{% extends &#34;!base.html&#34; %}
{% block extrahead %}
    {% if enable_plausible %}
        &lt;<span style="color:#000080">script</span> <span style="color:#008080">defer</span> <span style="color:#008080">data-domain</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;myproject.readthedocs.io&#34;</span>
                <span style="color:#008080">src</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;https://plausible.io/js/script.js&#34;</span>&gt;&lt;/<span style="color:#000080">script</span>&gt;
    {% endif %}
    {{ super() }}
{% endblock %}
</code></pre></div><p>In <code>_templates/base.html</code>, replace <code>myproject.readthedocs.io</code> by the actual domain name of your documentation. This domain name must also be enabled in your Plausible.io account.</p>
<p>Note that the <code>&lt;script&gt;</code> tag is added <em>only</em> if the template variable <code>enable_plausible</code> evaluates to <code>True</code>. This is how we can control whether or not metrics should be enabled for a given build.</p>
<p><strong>Important</strong>: I&rsquo;m using the <a href="https://pradyunsg.me/furo/quickstart/">Furo</a> theme, which uses <code>base.html</code> as main HTML file. Other themes (including the default Sphinx theme) might be using <code>layout.html</code> instead, as indicated in <a href="https://www.sphinx-doc.org/en/master/templating.html">Sphinx&rsquo;s documentation</a> on templating. This initially threw me off, so make sure to check which of your template&rsquo;s file must be extended.</p>
<h2 id="enabling-metrics-on-production-build">Enabling metrics on production build</h2>
<p>The <code>enable_plausible</code> variable must be defined for our template above to function. This is done in <code>conf.py</code> file using the <code>html_context</code> variable as follows:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#998;font-style:italic"># conf.py</span>
<span style="color:#000;font-weight:bold">import</span> <span style="color:#555">os</span>

<span style="color:#998;font-style:italic"># [...]</span>

READTHEDOCS_VERSION_TYPE <span style="color:#000;font-weight:bold">=</span> os<span style="color:#000;font-weight:bold">.</span>environ<span style="color:#000;font-weight:bold">.</span>get(<span style="color:#d14">&#34;READTHEDOCS_VERSION_TYPE&#34;</span>, <span style="color:#000;font-weight:bold">None</span>)

html_context <span style="color:#000;font-weight:bold">=</span> {
    <span style="color:#d14">&#34;enable_plausible&#34;</span>: READTHEDOCS_VERSION_TYPE <span style="color:#000;font-weight:bold">in</span> [<span style="color:#d14">&#34;branch&#34;</span>, <span style="color:#d14">&#34;tag&#34;</span>],
}
</code></pre></div><p>I use the <code>READTHEDOCS_VERSION_TYPE</code> environment variable, which is <a href="https://docs.readthedocs.io/en/stable/environment-variables.html#envvar-READTHEDOCS_VERSION_TYPE">set by <em>Read the Docs</em></a>. Its value is <code>&quot;branch&quot;</code> when the docs are built from the main branch, and <code>&quot;tag&quot;</code> when they are built from a tagged release. We want <code>enable_plausible</code> to be set to <code>True</code> in those instances. In any other case, including when <code>READTHEDOCS_VERSION_TYPE</code> is undefined (as is the case for local builds), <code>enable_plausible</code> is set to <code>False</code>.</p>
<p>And that&rsquo;s about it – these few steps are all it takes more compliant and privacy-friendly metrics thanks to <code>Plausible.io</code>. For a real-world example, you can check my <a href="https://vpype.readthedocs.io/en/latest/"><em>vpype</em></a> project (<a href="https://github.com/abey79/vpype/pull/546/files">relevant PR</a>).</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>TIL: using Sphinx substitutions to generate text snippets from code</title>
      <link>https://bylr.info/articles/2022/09/30/til-sphinx-substitutions/</link>
      <pubDate>Fri, 30 Sep 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/09/30/til-sphinx-substitutions/</guid>
      <description>Often, technical documentations include lists or other snippets of text that are strongly related to some of the project&amp;rsquo;s code. vpype&amp;rsquo;s documentation is no exception to this.
For instance, the Built-in symbols section lists the units available to expressions:
These units are related to the following piece of code:
# vpype/utils.py UNITS = { &amp;#34;px&amp;#34;: 1.0, &amp;#34;in&amp;#34;: 96.0, &amp;#34;inch&amp;#34;: 96.0, &amp;#34;ft&amp;#34;: 12.0 * 96.0, &amp;#34;yd&amp;#34;: 36.0 * 96.0, &amp;#34;mi&amp;#34;: 1760.0 * 36.</description>
      <content:encoded><![CDATA[<p>Often, technical documentations include lists or other snippets of text that are strongly related to some of the project&rsquo;s code. <a href="https://github.com/abey79/vpype"><em>vpype</em></a>&rsquo;s <a href="https://vpype.readthedocs.io/">documentation</a> is no exception to this.</p>
<p>For instance, the <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#built-in-symbols">Built-in symbols</a> section lists the units available to expressions:</p>
<img src="/til-sphinx-substitutions/doc_units.png"alt="partial screenshot of vpype&#39;s documentation showing a list of units related to code"width="95%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>These units are related to the following piece of code:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#998;font-style:italic"># vpype/utils.py</span>

UNITS <span style="color:#000;font-weight:bold">=</span> {
    <span style="color:#d14">&#34;px&#34;</span>: <span style="color:#099">1.0</span>,
    <span style="color:#d14">&#34;in&#34;</span>: <span style="color:#099">96.0</span>,
    <span style="color:#d14">&#34;inch&#34;</span>: <span style="color:#099">96.0</span>,
    <span style="color:#d14">&#34;ft&#34;</span>: <span style="color:#099">12.0</span> <span style="color:#000;font-weight:bold">*</span> <span style="color:#099">96.0</span>,
    <span style="color:#d14">&#34;yd&#34;</span>: <span style="color:#099">36.0</span> <span style="color:#000;font-weight:bold">*</span> <span style="color:#099">96.0</span>,
    <span style="color:#d14">&#34;mi&#34;</span>: <span style="color:#099">1760.0</span> <span style="color:#000;font-weight:bold">*</span> <span style="color:#099">36.0</span> <span style="color:#000;font-weight:bold">*</span> <span style="color:#099">96.0</span>,
    <span style="color:#d14">&#34;mm&#34;</span>: <span style="color:#099">96.0</span> <span style="color:#000;font-weight:bold">/</span> <span style="color:#099">25.4</span>,
    <span style="color:#d14">&#34;cm&#34;</span>: <span style="color:#099">96.0</span> <span style="color:#000;font-weight:bold">/</span> <span style="color:#099">2.54</span>,
    <span style="color:#d14">&#34;m&#34;</span>: <span style="color:#099">100.0</span> <span style="color:#000;font-weight:bold">*</span> <span style="color:#099">96.0</span> <span style="color:#000;font-weight:bold">/</span> <span style="color:#099">2.54</span>,
    <span style="color:#d14">&#34;km&#34;</span>: <span style="color:#099">100_000.0</span> <span style="color:#000;font-weight:bold">*</span> <span style="color:#099">96.0</span> <span style="color:#000;font-weight:bold">/</span> <span style="color:#099">2.54</span>,
    <span style="color:#d14">&#34;pc&#34;</span>: <span style="color:#099">16.0</span>,
    <span style="color:#d14">&#34;pt&#34;</span>: <span style="color:#099">96.0</span> <span style="color:#000;font-weight:bold">/</span> <span style="color:#099">72.0</span>,
}
</code></pre></div><p>I recently added <a href="https://github.com/abey79/vpype/pull/541">support for more units</a> and, of course, the documentation was at risk of running out of sync. Obviously, generating the list of units based on the code would be a better solution. After some Googling, here is how I did it.</p>
<p>The basic idea is to use <a href="https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#substitutions">substitutions</a>. A substitution consists of assigning a text snippet to a keyword, and subsequently use said keyword (with the <code>|keyword|</code> syntax) in the documentation&rsquo;s body. The second insight is to use the <code>rst_prolog</code> variable (within the <code>conf.py</code> file) for the definition. This being regular Python, the definition can easily be auto-generated based on the original code.</p>
<p>Here is how it looks for the case above:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#998;font-style:italic"># docs/conf.py</span>

<span style="color:#000;font-weight:bold">import</span> <span style="color:#555">vpype</span> <span style="color:#000;font-weight:bold">as</span> <span style="color:#555">vp</span>

<span style="color:#998;font-style:italic"># [...]</span>

UNIT_STRINGS <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#34;, &#34;</span><span style="color:#000;font-weight:bold">.</span>join(<span style="color:#d14">f</span><span style="color:#d14">&#34;``</span><span style="color:#d14">{</span>s<span style="color:#d14">}</span><span style="color:#d14">``&#34;</span> <span style="color:#000;font-weight:bold">for</span> s <span style="color:#000;font-weight:bold">in</span> <span style="color:#0086b3">sorted</span>(vp<span style="color:#000;font-weight:bold">.</span>UNITS<span style="color:#000;font-weight:bold">.</span>keys()) <span style="color:#000;font-weight:bold">if</span> s <span style="color:#000;font-weight:bold">!=</span> <span style="color:#d14">&#34;in&#34;</span>)

rst_prolog <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">f</span><span style="color:#d14">&#34;&#34;&#34;
</span><span style="color:#d14">.. |units| replace:: </span><span style="color:#d14">{</span>UNIT_STRINGS<span style="color:#d14">}</span><span style="color:#d14">
</span><span style="color:#d14">&#34;&#34;&#34;</span>
</code></pre></div><p>(Note that <code>in</code> is explicitly excluded from the list because it is a reserved Python keyword and cannot be used in the context of <em>vpype</em> expressions.)</p>
<p>And this is how the substitution is used in the actual documentation file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-rst" data-lang="rst"><span style="color:#999;font-weight:bold;font-style:italic">..
</span><span style="color:#999;font-weight:bold;font-style:italic">  docs/fundamentals.rst
</span><span style="color:#999;font-weight:bold;font-style:italic">
</span><span style="color:#999;font-weight:bold;font-style:italic"></span><span style="color:#099">*</span> Units constants (|units|).<span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>  These variables may be used to convert values to CSS pixels unit, which <span style="color:#000;font-style:italic">*vpype*</span> uses internally. For example, the expression <span style="color:#d14">``%(3+4)*cm%``</span> evaluates to the pixel equivalent of 7 centimeters (e.g. ~264.6 pixels). (Note that expressions may overwrite these variables, e.g. to use the <span style="color:#d14">``m``</span> variable for another purpose.)<span style="color:#a61717;background-color:#e3d2d2">
</span></code></pre></div><p><em>Et voilà!</em> Nice and easy. I certainly expect to use this technique often in the future.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Annotated Release Notes: vpype 1.11</title>
      <link>https://bylr.info/articles/2022/07/06/annotated-release-notes-vpype-1.11/</link>
      <pubDate>Wed, 06 Jul 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/07/06/annotated-release-notes-vpype-1.11/</guid>
      <description>This release further solidifies the block commands which were overhauled in vpype 1.9. It also introduces several changes revolving around the &amp;ldquo;plotting with paint&amp;rdquo; use-case, which typically requires the brush to be regularly dipped in a paint well. This can be achieved by inserting &amp;ldquo;dipping&amp;rdquo; patterns at regular intervals determined by the cumulative drawing distance. vpype 1.11 makes this process much easier.
Thanks a lot to Andee Collard for his useful feedback and providing this article&amp;rsquo;s banner!</description>
      <content:encoded><![CDATA[<p>This release further solidifies the block commands which were overhauled in <a href="/articles/2022/03/03/annotated-release-notes-vpype-1.9/"><em>vpype</em> 1.9</a>. It also introduces several changes revolving around the &ldquo;plotting with paint&rdquo; use-case, which typically requires the brush to be regularly dipped in a paint well. This can be achieved by inserting &ldquo;dipping&rdquo; patterns at regular intervals determined by the cumulative drawing distance. <em>vpype</em> 1.11 makes this process much easier.</p>
<p>Thanks a lot to <a href="https://www.andeecollard.com/">Andee Collard</a> for his useful feedback and providing this article&rsquo;s banner!</p>
<h2 id="painting-with-a-plotter">Painting with a plotter</h2>

<blockquote class="extract">
  <p><ul>
<li>Added the <code>splitdist</code> command to split layers by drawing distance (thanks to @LoicGoulefert) (#487, #501)</li>
</ul>
</p>
</blockquote>

<p>The new <a href="https://vpype.readthedocs.io/en/latest/reference.html#splitdist"><code>splitdist</code></a> command, contributed by <a href="https://compotedeplot.bigcartel.com">Loïc Goulefert</a> (thanks a lot!), is the core of the paint plotting use-case. It splits each layer into newly created layers such that their respective drawing distance is each below the specified limit.</p>
<p>This command could readily be used with a clever <a href="https://github.com/plottertools/vpype-gcode">vpype-gcode</a> profile that implements the dipping mechanism at the beginning of each layer. Alternatively, it can be combined with the <code>forlayer</code> block command to insert dipping patterns into the line work. We&rsquo;ll see an example of such a pipeline below.</p>

<blockquote class="extract">
  <p><ul>
<li>Added meters (<code>m</code>) and feet (<code>ft</code>) to the supported units (#498, #508)</li>
<li>Fixed an issue with expressions where some variable names corresponding to units (e.g. <code>m</code>) could not be used (expressions may now reuse these names) (#506)</li>
</ul>
</p>
</blockquote>

<p>These are rather large units for typical plotting workflow, but come in useful for specifying the maximum drawing distance with <code>splitdist</code>.</p>
<p>As a reminder, units are available in two contexts:</p>
<ol>
<li>Every time a command accepts a length-type argument or option (e.g. <code>translate 5mm 3cm</code> or <code>linemerge --tolerance 0.05mm</code>).</li>
<li>In <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#built-in-symbols">expressions</a> (e.g. <code>forlayer translate &quot;%_i*3*cm%&quot; 0 end</code>).</li>
</ol>
<p>In the latter case, the existence of the unit constant precluded the use of variables with the same name. This issue worsened with the addition of <code>m</code> as this is a rather common variable name (e.g. this cookbook <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#cropping-and-framing-geometries">recipe</a> uses it). To address this, they are no longer read-only and may now be overwritten. Of course, doing so renders their original value unavailable in the pipeline&rsquo;s subsequent expressions.</p>

<blockquote class="extract">
  <p><ul>
<li>Fixed an issue with blocks where certain nested commands could lead totally unexpected results (#506)</li>
<li>API: removed the faulty <code>temp_document()</code> context manager from <code>vpype_cli.State()</code> (#506)</li>
</ul>
</p>
</blockquote>

<p>The improved blocks introduced in <a href="/articles/2022/03/03/annotated-release-notes-vpype-1.9/"><em>vpype</em> 1.9</a> had a major flaw which could, in some circumstances, result in erratic results. It turns out that the new <code>splitdist</code> command triggered this issue and brought it in the spotlight. This is now fixed, and the <code>vpype_cli.State.temp_document()</code> API is a casualty of this patch (luckily, it was introduced recently and I&rsquo;m pretty sure no one used it yet besides me).</p>

<blockquote class="extract">
  <p><ul>
<li>Fixed an issue with the <code>lmove</code> command where order would not be respected in certain cases such as <code>lmove all 2</code> (the content of layer 2 was placed before that of layer 1) (#506)</li>
</ul>
</p>
</blockquote>

<p>This is yet another issue highlighted by to the &ldquo;plotting with paint&rdquo; workflow. When the source layers included the destination layer (as is the case for <code>lmove all 2</code>), the order of the source layers would not be respected (e.g. for a 3-layer pipeline and the<code>lmove all 2</code> command, layer 2 would end up with its original content, then layer 1, then layer 3). With this fix, the destination layer will now include the source layers' content in the correct order (e.g. in the previous example, layer 2 would end up with the content of layer 1, then layer 2, then layer 3).</p>
<br/>
<p>Collectively, these changes enable the &ldquo;plotting with paint&rdquo; workflow using the following pipeline:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ vpype <span style="color:#d14">\
</span><span style="color:#d14"></span>      <span style="color:#0086b3">read</span> input.svg <span style="color:#d14">\
</span><span style="color:#d14"></span>      forlayer <span style="color:#d14">\
</span><span style="color:#d14"></span>        lmove %_lid% <span style="color:#099">1</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>        splitdist 1m <span style="color:#d14">\
</span><span style="color:#d14"></span>        forlayer <span style="color:#d14">\
</span><span style="color:#d14"></span>          lmove %_lid% <span style="color:#d14">&#34;%_lid*2%&#34;</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>          <span style="color:#0086b3">read</span> -l <span style="color:#d14">&#34;%_lid*2-1%&#34;</span> dip_%_name%.svg <span style="color:#d14">\
</span><span style="color:#d14"></span>        end <span style="color:#d14">\
</span><span style="color:#d14"></span>      lmove all %_lid% <span style="color:#d14">\
</span><span style="color:#d14"></span>      name -l %_lid% %_name% <span style="color:#d14">\
</span><span style="color:#d14"></span>      color -l %_lid% %_color% <span style="color:#d14">\
</span><span style="color:#d14"></span>    end <span style="color:#d14">\
</span><span style="color:#d14"></span>    write output.svg
</code></pre></div><p>For this to work, the layers in <code>input.svg</code> must be named after their respective color and, for each such color, a file named <code>dip_COLORNAME.svg</code> must exist. For example, if <code>input.svg</code> has two layers named &ldquo;red&rdquo; and &ldquo;blue&rdquo;, then the <code>dip_red.svg</code> and <code>dip_blue.svg</code> files must exist.</p>
<p>The following figure illustrates the results for synthetic data.</p>
<figure class="square-corner">
    <img loading="lazy" src="/vpype_111/paint_workflow.svg"
         alt="(left) Input SVG with 3 layers. (middle) The three corresponding dipping pattern SVGs. (right) The output SVG with 3 layers and the visible dipping patterns interspersed within the line work."/> <figcaption>
            <p>(<em>left</em>) Input SVG with 3 layers. (<em>middle</em>) The three corresponding dipping pattern SVGs. (<em>right</em>) The output SVG with 3 layers and the visible dipping patterns interspersed within the line work.</p>
        </figcaption>
</figure>

<p>This pipeline is listed in a cookbook <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#inserting-regular-dipping-patterns-for-plotting-with-paint">recipe</a> and will be explained in details, along with the <code>forlayer</code> block command, in a future article.</p>
<h2 id="other-changes">Other changes</h2>

<blockquote class="extract">
  <p><ul>
<li>Improved the <code>linemerge</code> algorithm by making it less dependent on line order (#496)</li>
</ul>
</p>
</blockquote>

<p>The <code>linemerge</code> command is implemented using a <a href="https://en.wikipedia.org/wiki/Greedy_algorithm">greedy</a> algorithm which roughly works as follows:</p>
<ol>
<li>Pick the first available line.</li>
<li>Look for another line that can be appended.</li>
<li>If found, merge both lines and look for further line to append (back to step 2). If not, save the current line, pick the next available one, and repeat (back to step 1).</li>
</ol>
<p>By default, <code>linemerge</code> always considers both endings of each line, possibly reversing them if this enables a merge. This is not always desirable though, which is why the <code>--no-flip</code> option exists. In this case, the algorithm would only try to <em>append</em> to the current line, without trying to <em>prepend</em> as well. This oversight led to a greater dependence on line order and, occasionally, suboptimal results, as illustrated by the figure below.</p>
<figure>
    <img loading="lazy" src="/vpype_111/linemerge.svg"
         alt="(left) Initial situation. (middle) Result when both appending only. (right) Results when appending and prepending."/> <figcaption>
            <p>(<em>left</em>) Initial situation. (<em>middle</em>) Result when both appending only. (<em>right</em>) Results when appending and prepending.</p>
        </figcaption>
</figure>

<p>With this fix, <code>linemerge --no-flip</code> now tries to both append and prepend, leading to more consistent results.</p>

<blockquote class="extract">
  <p><ul>
<li>Added <code>--keep-page-size</code> option to <code>grid</code> command (#506)</li>
</ul>
</p>
</blockquote>

<p>By default, the <code>grid</code> block command sets the page size to its geometry. For example, the block <code>grid --offset 4cm 3cm 3 5 [...] end</code> sets the page size to 12x15cm. This behaviour can now be disabled with the <code>--keep-page-size</code> option.</p>
<p>This change mainly helps for the testability of the blocks feature (in this release, I&rsquo;ve added multiple tests to minimise the risk of future regression), but I figured it could have its occasional use out there.</p>

<blockquote class="extract">
  <p><ul>
<li>Added HPGL configurations for the Houston Instrument DMP-161, HP7550, Roland DXY 1xxxseries and sketchmate plotters (thanks to @jimmykl and @ithinkido) (#472, #474)</li>
</ul>
</p>
</blockquote>

<p>Thanks a lot, <a href="https://linktr.ee/jimmyis">Jimmy Kirkus-Lamont</a> and <a href="https://github.com/ithinkido">@ithinkido</a>! ❤️</p>

<blockquote class="extract">
  <p><ul>
<li>Added equality operator to <code>vpype.LineCollection</code> and <code>vpype.Document</code> (#506)</li>
</ul>
</p>
</blockquote>

<p>I can now check if two layers or documents have the same content and metadata using the equality operator <code>==</code>. This is immensely useful when writing tests. I have no idea why it took so long… 🤷</p>

<blockquote class="extract">
  <p><ul>
<li>Pinned Shapely to 1.8.2, which is the first release in a long time to have binaries for most platforms/Python release combination (including Apple-silicon Macs and Python 3.10) (#475)</li>
</ul>
</p>
</blockquote>

<p>It was quite the roller coaster ride for <a href="https://shapely.readthedocs.io/">Shapely</a> to be properly packaged for both Python 3.10 and Apple-silicon Macs, but now this is fully sorted out. That&rsquo;s one less hassle when installing <em>vpype</em>.</p>

<blockquote class="extract">
  <p><ul>
<li>Removed deprecated API (#507)</li>
</ul>
</p>
</blockquote>

<p>With <a href="/articles/2022/03/03/annotated-release-notes-vpype-1.9/"><em>vpype</em> 1.9</a>, a number of APIs migrated from the <code>vpype</code> package to the <code>vpype_cli</code> package. The former APIs still worked but emitted <a href="/articles/2022/03/03/annotated-release-notes-vpype-1.9/#other-changes">deprecation warnings</a>. They are now gone forever.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Generating beautiful Python API documentation with Sphinx AutoAPI</title>
      <link>https://bylr.info/articles/2022/05/10/api-doc-with-sphinx-autoapi/</link>
      <pubDate>Tue, 10 May 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/05/10/api-doc-with-sphinx-autoapi/</guid>
      <description>Following a recent discussion on Twitter, I decided to take yet another deep dive in my Python projects&#39; documentation and fix once and for all the issues I had with it. I first focused on the automatically-generated API reference section and this article details the results of my finding. Specifically, I&amp;rsquo;m using vsketch&amp;rsquo;s API reference, which I recently updated, as an example (documentation source.
This article addresses the following objectives:</description>
      <content:encoded><![CDATA[<p>Following a recent <a href="https://twitter.com/abey79/status/1521484512596746246?s=20&amp;t=3kEAgQUDFiJDRzz77UdTgA">discussion</a> on Twitter, I decided to take yet another deep dive in my Python projects' documentation and fix once and for all the issues I had with it. I first focused on the automatically-generated API reference section and this article details the results of my finding. Specifically, I&rsquo;m using <em>vsketch</em>&rsquo;s <a href="https://vsketch.readthedocs.io/en/latest/autoapi/vsketch/index.html">API reference</a>, which I recently updated, as an example (<a href="https://github.com/abey79/vsketch/tree/master/docs">documentation source</a>.</p>
<p>This article addresses the following objectives:</p>
<ul>
<li>Produce a beautiful API documentation based on the code docstrings that is both nice to look at and easy to navigate.</li>
<li>Support for a proper table of content navigation down to each class/module&rsquo;s member.</li>
<li>Nice looking summary tables listing modules' and classes' contents.</li>
</ul>
<img src="/sphinx-autoapi/example_doc.png"alt="Example of documentation generated with this article&#39;s techniques"width="80%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>This article targets an audience of FOSS maintainers who are unhappy with the state of their project&rsquo;s API documentation, and are frustrated with the process of improving it. A basic understanding of Sphinx as well as an existing documentation setup is assumed. This article basically contains everything I wish I was told when I first started on my API reference improvement journey. For the beginners, I&rsquo;ll provide pointers to help setting up a basic Sphinx.</p>
<p>Note that although this article is structured as a tutorial, it covers tips and techniques which are likely useful for other kinds of documentation customisation.</p>
<h2 id="basic-setup">Basic setup</h2>
<p>As stated above, the basic steps to setup a Sphinx-based documentation project are outside the scope of the present article. I suggest reviewing the following resources to get started:</p>
<ul>
<li><a href="https://twitter.com/mariatta">@Mariatta</a> made a <em>brilliant</em> <a href="https://sphinx-intro-tutorial.readthedocs.io/">tutorial</a> on how to kick-start a Sphinx documentation project.</li>
<li><a href="https://thomas-cokelaer.info">Thomas Cokelaer</a> has a very nice reStructuredText <a href="https://thomas-cokelaer.info/tutorials/sphinx/rest_syntax.html">cheat sheet</a>.</li>
<li><a href="https://simonwillison.net">Simon Willison</a> wrote another <a href="https://simonwillison.net/2018/Aug/25/restructuredtext/">cheat sheet</a> which covers &ldquo;the subset of reStructuredText worth committing to memory&rdquo;.</li>
<li>Obviously, Sphinx&rsquo;s <a href="https://www.sphinx-doc.org/en/master/">documentation</a> is an important resource. Although it is somewhat arid for the newcomer, I strongly suggest not giving up on it. I had multiple &ldquo;oh there it is!&rdquo; moments with it in the process of writing this article.</li>
<li>Finally, <a href="https://readthedocs.org">Read the Docs</a> is likely the best place to host your documentation. It&rsquo;s very simple to setup and free for open source projects.</li>
</ul>
<p>As for the theme, my preference goes for <a href="https://pradyunsg.me">Pradyun Gedam</a>&rsquo;s <a href="https://github.com/pradyunsg/furo">Furo</a>. I&rsquo;m using it with the default configuration, so the only requirement is to enable it in your <code>conf.py</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">html_theme <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#34;furo&#34;</span>
</code></pre></div><p>Note that some of the CSS provided in this article may need adjustments should you opt for a different theme.</p>
<h2 id="autoapi-setup">Autoapi setup</h2>
<p>After trying both autodoc/autosummary and Sphinx AutoAPI, I opted to use the latter. Here are the main reasons behind this choice:</p>
<ul>
<li>Autosummary does not generate TOC entries for API elements such as classes/modules and their members. This is due to a <a href="https://github.com/sphinx-doc/sphinx/issues/6316">long-standing Sphinx limitation</a>. Autoapi works around this limitation (albeit imperfectly, as we&rsquo;ll later note).</li>
<li>Autosummary defaults to <a href="https://github.com/sphinx-doc/sphinx/issues/7912">not generating anything</a> and is in my experience frustrating to setup. In contrast, autoapi produces usable output out-of-the-box.</li>
<li>Templates are easier to write thanks to the rich &ldquo;mapper&rdquo; objects AutoAPI provides after parsing your code (see the <a href="#autoapi-objects">AutoAPI objects</a> section below).</li>
</ul>
<p>Note that there are two things called &ldquo;autoapi&rdquo; floating on the Internet: the <a href="https://github.com/readthedocs/sphinx-autoapi">Sphinx AutoAPI</a> project (<a href="https://sphinx-autoapi.readthedocs.io/en/latest/">documentation</a>) is the good one. The <a href="https://autoapi.readthedocs.io">other one</a> is unmaintained and barely documented. Make sure you don&rsquo;t loose time dealing with the wrong one.</p>
<h3 id="basics">Basics</h3>
<p>Setting up Sphinx AutoAPI is covered in their <a href="https://sphinx-autoapi.readthedocs.io/en/latest/tutorials.html#python">documentation</a>. It boils down to the following steps.</p>
<ol>
<li>Install the <code>sphinx-autoapi</code> package:
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ pip install sphinx-autoapi
</code></pre></div></li>
<li>Add AutoAPI it to the Sphinx extension list:
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">extensions <span style="color:#000;font-weight:bold">=</span> [
    <span style="color:#000;font-weight:bold">...</span>,
    <span style="color:#d14">&#39;autoapi.extension&#39;</span>,
]
</code></pre></div></li>
<li>List your package directories (or the directory containing them) and set basic options:
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">autoapi_dirs <span style="color:#000;font-weight:bold">=</span> [<span style="color:#d14">&#39;../mypackage&#39;</span>]
autoapi_type <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#34;python&#34;</span>
</code></pre></div></li>
<li>Add the generated documentation to your <code>index.rst</code> toctree:
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-rst" data-lang="rst">.. <span style="color:#000;font-weight:bold">toctree</span>::<span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>   <span style="color:#458;font-weight:bold">:maxdepth:</span> <span style="color:#900;font-weight:bold">3</span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span><span style="color:#999;font-weight:bold;font-style:italic">   ...
</span><span style="color:#999;font-weight:bold;font-style:italic">   autoapi/index
</span></code></pre></div></li>
</ol>
<h3 id="setting-up-templates">Setting up templates</h3>
<p>We will customise Sphinx AutoAPI&rsquo;s default templates. The easiest is to copy Sphinx AutoAPI&rsquo;s default templates in your project to serve as a starting point.</p>
<p>First, run the following commands (adjusting for your Python version) from your documentation directory:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ mkdir _templates
$ mkdir _template/autoapi
$ cp <span style="color:#008080">$VIRTUAL_ENV</span>/lib/python3.10/site-packages/autoapi/templates/index.rst _templates/autoapi/
$ cp -r <span style="color:#008080">$VIRTUAL_ENV</span>/lib/python3.10/site-packages/autoapi/templates/python _templates/autoapi/ 
</code></pre></div><p>Then, tell Sphinx AutoAPI of its template directory in your <code>conf.py</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">autoapi_template_dir <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#34;_templates/autoapi&#34;</span>
</code></pre></div><p>A useful tip is to make a Git commit just after copying the built-in templates, such that you can track (and revert) your modifications. I&rsquo;ve used this extensively while working on my templates.</p>
<p>At this point, I suggest spending some time to become acquainted with the built-in templates and how they are organised and implemented. If you haven&rsquo;t used it before, it is also useful to review the Jinja2 templating language <a href="https://jinja.palletsprojects.com">documentation</a>.</p>
<h3 id="other-configuration-options">Other configuration options</h3>
<h4 id="autoapi_options"><code>autoapi_options</code></h4>
<p>The <code>autoapi_options</code> controls various aspect of the generated documentation, including the type of class/module members that are listed. Its default value is sensible but I still felt like customising it:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">autoapi_options <span style="color:#000;font-weight:bold">=</span> [
    <span style="color:#d14">&#34;members&#34;</span>,
    <span style="color:#d14">&#34;undoc-members&#34;</span>,
    <span style="color:#d14">&#34;show-inheritance&#34;</span>,
    <span style="color:#d14">&#34;show-module-summary&#34;</span>,
    <span style="color:#d14">&#34;imported-members&#34;</span>,
]
</code></pre></div><p>In particular, I want the summary at the top of the module&rsquo;s documentation (<code>show-module-summary</code>), but we will heavily customise it. Check <a href="https://sphinx-autoapi.readthedocs.io/en/latest/reference/config.html#confval-autoapi_options">the documentation</a> for a list of available options and their descriptions.</p>
<h4 id="autoapi_keep_files"><code>autoapi_keep_files</code></h4>
<p>Another useful option is <code>autoapi_keep_files</code>. Sphinx-autoapi generates .rst pages for the documentation during the build process, but defaults to deleting them after completion. It&rsquo;s often useful to keep them around for inspection and debugging purposes:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">autoapi_keep_files <span style="color:#000;font-weight:bold">=</span> <span style="color:#000;font-weight:bold">True</span>
</code></pre></div><h4 id="autodoc_typehints"><code>autodoc_typehints</code></h4>
<p>This is technically an autodoc setting, but Sphinx AutoAPI honours it. It controls if/where type hints are included in the documentation. The possible values are the following:</p>
<ul>
<li><code>&quot;signature&quot;</code>: type hints are included in the function signature, which appears first in the member&rsquo;s documentation</li>
<li><code>&quot;description&quot;</code>: type hints are included within the function description, when the arguments are listed</li>
<li><code>&quot;both&quot;</code>: type hints are included in both places</li>
<li><code>&quot;none&quot;</code>: type hints are not included</li>
</ul>
<p>My preference goes for <code>&quot;signature&quot;</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">autodoc_typehints <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#34;signature&#34;</span>
</code></pre></div><h3 id="autoapi-objects">AutoAPI objects</h3>
<p>Understanding the Sphinx AutoAPI objects is key to customising templates. They are one of the major difference with respect to autodoc/autosummary.</p>
<p>In order to generate the API documentation, the autodoc <em>loads</em> your actual code and uses Python&rsquo;s introspection capabilities to extract the required information from your module and class objects. In contrast, Sphinx AutoAPI <em>parses</em> your Python code and builds a collection of so-called &ldquo;mapper&rdquo; objects which describe your code and its structure. These objects are then passed on as context to the Jinja2 templating engine. Oddly, the documentation doesn&rsquo;t provide a reference about them, but their <a href="https://github.com/readthedocs/sphinx-autoapi/blob/master/autoapi/mappers/python/objects.py">implementation</a> is easy to read.</p>
<p>Here is a summary of some of the attributes that are useful when writing templates:</p>
<dl>
<dt><code>obj.name</code></dt>
<dd>Name of the mapped object, e.g. <code>&quot;MyClass&quot;</code> or <code>&quot;my_method&quot;</code>.</dd>
<dt><code>obj.id</code></dt>
<dd>Fully qualified name of the object, used for cross-referencing, e.g. <code>&quot;my_module.MyClass&quot;</code> or <code>&quot;my_module.MyClass.my_method&quot;</code>.</dd>
<dt><code>obj.summary</code></dt>
<dd>Summary of the object&rsquo;s docstring (i.e. the first line).</dd>
<dt><code>obj.docstring</code></dt>
<dd>Full docstring of the object.</dd>
<dt><code>obj.display</code></dt>
<dd>Indicates whether or not this object should be displayed, based on the options set in <code>conf.py</code> and the result of the <code>autoapi-skip-member</code> event (discussed later).</dd>
<dt><code>obj.children</code></dt>
<dd><em>(Modules and classes only)</em> List children functions, methods, attributes, etc.</dd>
<dt><code>obj.properties</code></dt>
<dd><em>(Functions and methods only)</em> List of properties, such as <code>&quot;classmethod&quot;</code>, <code>&quot;staticmethod&quot;</code>&quot;, <code>&quot;abstractmethod&quot;</code>, <code>&quot;property&quot;</code>, etc.</dd>
<dt><code>obj.obj.args</code></dt>
<dd><em>(Functions and methods only)</em> List of 4-tuples describing the function&rsquo;s arguments. The first item is the star operator if any (<code>&quot;*&quot;</code>, <code>&quot;**&quot;</code>, or <code>None</code>), the second is the argument name, the third is the argument type or <code>None</code>, and the fourth is the argument default value or <code>None</code>. This key piece of data will enable us to recreate the signatures according to our needs.</dd>
</dl>
<p>When working on your documentation, it is often useful to inspect the contents of these mapper objects using a debugger. This can be achieved by adding an handler for the <code>autoapi-skip-member</code> event and setting a conditional breakpoint:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">skip_member</span>(app, what, name, obj, skip, options):
    <span style="color:#998;font-style:italic"># conditional breakpoint here</span>
    <span style="color:#000;font-weight:bold">return</span> skip


<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">setup</span>(sphinx):
    sphinx<span style="color:#000;font-weight:bold">.</span>connect(<span style="color:#d14">&#34;autoapi-skip-member&#34;</span>, skip_member)
</code></pre></div><p>This event will be triggered for every single Python object parsed from your code. By breaking, for example, when <code>obj.name == &quot;my_module&quot;</code>, the <code>obj</code> argument and its children can be fully inspected. I use the following run configuration in my IDE for this:</p>
<ul>
<li>Execute module: <code>sphinx.cmd.build</code></li>
<li>Parameters: <code>-M html . _build</code></li>
<li>Working directory: <code>docs/</code></li>
</ul>
<h2 id="an-autosummary-like-macro">An <code>autosummary</code>-like macro</h2>
<p>By default, Sphinx AutoAPI provides a summary list of classes, functions, and attributes at the top of a module&rsquo;s documentation, which is very nice. Our objective is to add a similar table at the top of each class description, to facilitate navigation. However, Sphinx AutoAPI uses its own <code>autoapisummary</code> directive, which derives from autosummary&rsquo;s <code>autosummary</code> directive. Both suffer from the following limitations:</p>
<ul>
<li>The way callables are rendered is hard-coded and cannot be customised via templates. In particular, if <code>autodoc_typehints</code> is set to <code>&quot;signature&quot;</code> or <code>&quot;both&quot;</code>, <code>autosummary</code> will include type hints in the summary table as well. Unfortunately, this dramatically increases the length of the signature, which is detrimental to the table layout and usability. Alternatively, signatures can be entirely removed by using the <code>:nosignatures:</code> option. However, in this case, not even parenthesis are displayed, which hides the callable nature of the function. The best compromise is to have the full signature with their arguments, but without typing annotations.</li>
<li>Properties are listed as functions, including their signature. This hides the fact that, API-wise, they behave as data members (though it would still be useful to indicate that they are in fact properties).</li>
<li>There is not indication that a method is abstract, static, or class-based.</li>
</ul>
<p>To address these shortcomings, we will create a <a href="https://jinja.palletsprojects.com">Jinja2</a> template macro to replicate and improve on <code>autosummary</code>/<code>autoapisummary</code> functionality.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>Our aim is to create tables where callable have their full – but unannotated – signature, where properties are indicated as such but rendered as attributes, and where static, class, and abstract methods are marked as such. Here is an example of this:</p>
<img src="/sphinx-autoapi/demo_class_doc.png"alt="Example of documentation generated with this article&#39;s techniques"width="80%"style="display:block;margin-left:auto;margin-right:auto"/>
<h3 id="basic-macro-setup">Basic macro setup</h3>
<p>The basic insight is that a summary table can be implemented using Sphinx&rsquo;s <a href="https://docutils.sourceforge.io/docs/ref/rst/directives.html#list-table"><code>list-table</code></a>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-rst" data-lang="rst">.. <span style="color:#000;font-weight:bold">list-table</span>:: Title<span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>   <span style="color:#458;font-weight:bold">:header-rows:</span> <span style="color:#900;font-weight:bold">0</span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>   <span style="color:#458;font-weight:bold">:widths:</span> <span style="color:#900;font-weight:bold">auto</span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>   
   <span style="color:#099">*</span> - Item 1<span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>     <span style="color:#099">-</span> This is the description of the first item.<span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>   <span style="color:#099">*</span> - Name 2<span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>     - This is also a description, but this time for the second item.<span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>   <span style="color:#099">*</span> - ...<span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>     - ...<span style="color:#a61717;background-color:#e3d2d2">
</span></code></pre></div><img src="/sphinx-autoapi/basic_table.png"alt="Example of basic table."width="60%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Such a table can be generated with the following Jinja macro:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja"><span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">macro</span> <span style="color:#008080">auto_summary</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">objs</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">title</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#39;&#39;</span><span style="color:#000;font-weight:bold">)</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>

.. list-table:: <span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">title</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
   :header-rows: 0
   :widths: auto

<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">for</span> <span style="color:#008080">obj</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">objs</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
    * - obj.name
      - obj.summary
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endfor</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endmacro</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
</code></pre></div><p>To test this, create a file named <code>_templates/autoapi/macros.rst</code> and add the code above. Then, make the following edits to the <code>_templates/autoapi/python/module.rst</code> file:</p>
<ul>
<li>At the top of the file, import <code>macros.rst</code> to make it available for use:
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja"><span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">import</span> <span style="color:#d14">&#39;macros.rst&#39;</span> <span style="color:#000;font-weight:bold">as</span> <span style="color:#008080">macros</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
</code></pre></div></li>
<li>Locate where the class summary is generated:
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja">.. autoapisummary::

<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">for</span> <span style="color:#008080">klass</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">visible_classes</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">klass.id</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endfor</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
</code></pre></div></li>
<li>Replace the code above by a call to our macro:
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja"><span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">macros.auto_summary</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">visible_classes</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">title</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;Classes&#34;</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
</code></pre></div></li>
</ul>
<p>Here is the result I obtain with my project:</p>
<img src="/sphinx-autoapi/basic_class_table.png"alt="Example of basic class table."width="70%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>This is a good start, but we&rsquo;re obviously far from the result we want. To start with, no cross-reference links are generated. And, had we passed functions or methods instead of classes to our macro, no signature would have been generated.</p>
<h3 id="custom-labels">Custom labels</h3>
<p>Before fixing our macro, we must discuss these nice looking &ldquo;prop&rdquo;, &ldquo;static&rdquo;, and &ldquo;class&rdquo; tag-like labels in the example tables above. These are implemented using a custom <a href="https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html">role</a> with some CSS attached to it.</p>
<p>This StackOverflow <a href="https://stackoverflow.com/a/9707879/229511">answer</a> explains how to create a custom role and make it globally available to your documentation. Basically, just add the following to your <code>conf.py</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">rst_prolog <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#34;&#34;&#34;
</span><span style="color:#d14">.. role:: summarylabel
</span><span style="color:#d14">&#34;&#34;&#34;</span>
</code></pre></div><p>The <code>role</code> directive creates a new role which can then be used as follows:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-rst" data-lang="rst"><span style="color:#008080">:summarylabel:</span><span style="color:#008080">`My Label`</span><span style="color:#a61717;background-color:#e3d2d2">
</span></code></pre></div><p>Sphinx generates the corresponding HTML code:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html">&lt;<span style="color:#000080">span</span> <span style="color:#008080">class</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;summarylabel&#34;</span>&gt;My label&lt;/<span style="color:#000080">span</span>&gt;
</code></pre></div><p>Since it sets an HTML class named after the role, it&rsquo;s easy to adjust the label appearance using some custom CSS. Create a file named <code>_static/css/custom.css</code> in your documentation directory and add the following CSS:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-css" data-lang="css"><span style="color:#000080">span</span>.<span style="color:#458;font-weight:bold">summarylabel</span> {
    <span style="color:#000;font-weight:bold">background-color</span>: <span style="color:#900;font-weight:bold">var</span>(<span style="color:#000;font-weight:bold">--</span><span style="color:#000;font-weight:bold">color</span><span style="color:#000;font-weight:bold">-</span>foreground<span style="color:#000;font-weight:bold">-</span>secondary);
    <span style="color:#000;font-weight:bold">color</span>: <span style="color:#900;font-weight:bold">var</span>(<span style="color:#000;font-weight:bold">--</span><span style="color:#000;font-weight:bold">color</span><span style="color:#000;font-weight:bold">-</span>background<span style="color:#000;font-weight:bold">-</span>secondary);
    <span style="color:#000;font-weight:bold">font-size</span>: <span style="color:#099">70</span><span style="color:#458;font-weight:bold">%</span>;
    <span style="color:#000;font-weight:bold">padding-left</span>: <span style="color:#099">2</span><span style="color:#458;font-weight:bold">px</span>;
    <span style="color:#000;font-weight:bold">padding-right</span>: <span style="color:#099">2</span><span style="color:#458;font-weight:bold">px</span>;
    <span style="color:#000;font-weight:bold">border-radius</span>: <span style="color:#099">3</span><span style="color:#458;font-weight:bold">px</span>;
    <span style="color:#000;font-weight:bold">vertical-align</span>: <span style="color:#099">15</span><span style="color:#458;font-weight:bold">%</span>;
    <span style="color:#000;font-weight:bold">padding-bottom</span>: <span style="color:#099">2</span><span style="color:#458;font-weight:bold">px</span>;
    <span style="color:#000;font-weight:bold">filter</span>: <span style="color:#0086b3">opacity</span>(<span style="color:#099">40</span><span style="color:#458;font-weight:bold">%</span>);
}
</code></pre></div><p>Note the use of CSS variables in order to support Furo&rsquo;s dynamic night mode feature.</p>
<p>Finally, we must tell Sphinx about this CSS file in the <code>conf.py</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">html_css_files <span style="color:#000;font-weight:bold">=</span> [
    <span style="color:#d14">&#34;css/custom.css&#34;</span>,
]
</code></pre></div><h3 id="customising-the-table-appearance">Customising the table appearance</h3>
<p>A similar CSS approach can be used to customise the appearance of the summary table itself. By adding the <code>:class:</code> option to the <code>list-table</code> directive, we can tell Sphinx to attach an HTML class to the <code>&lt;table&gt;</code> element, which we can then customise with CSS:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-rst" data-lang="rst">.. <span style="color:#000;font-weight:bold">list-table</span>:: Title<span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>   <span style="color:#458;font-weight:bold">:header-rows:</span> <span style="color:#900;font-weight:bold">0</span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>   <span style="color:#458;font-weight:bold">:widths:</span> <span style="color:#900;font-weight:bold">auto</span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>   <span style="color:#458;font-weight:bold">:class:</span> <span style="color:#900;font-weight:bold">summarytable</span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>   
   <span style="color:#099">*</span> - ...<span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span>     <span style="color:#099">-</span> ...<span style="color:#a61717;background-color:#e3d2d2">
</span></code></pre></div><p>For my project, the only change I made to the default appearance is to force the table to span the entire width regardless of its contents. This can be done by adding the following code to our <code>custom.css</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-css" data-lang="css"><span style="color:#000080">table</span>.<span style="color:#458;font-weight:bold">summarytable</span> {
    <span style="color:#000;font-weight:bold">width</span>: <span style="color:#099">100</span><span style="color:#458;font-weight:bold">%</span>;
}
</code></pre></div><h3 id="putting-it-all-together">Putting it all together</h3>
<p>We are now ready to put everything together and improve our <code>auto_summary()</code> macro to our liking. Here is the final code for <code>macros.rst</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja"><span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">macro</span> <span style="color:#008080">_render_item_name</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">obj</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">sig</span><span style="color:#000;font-weight:bold">=</span><span style="color:#000;font-weight:bold">False</span><span style="color:#000;font-weight:bold">)</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
:py:obj:`<span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">obj.name</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span> &lt;<span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">obj.id</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>&gt;`
     <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">if</span> <span style="color:#008080">sig</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
       \ (
       <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">for</span> <span style="color:#008080">arg</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">obj.obj.args</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
          <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">if</span> <span style="color:#008080">arg</span><span style="color:#000;font-weight:bold">[</span><span style="color:#099">0</span><span style="color:#000;font-weight:bold">]</span> <span style="color:#999;font-weight:bold;font-style:italic">%}{{</span> <span style="color:#008080">arg</span><span style="color:#000;font-weight:bold">[</span><span style="color:#099">0</span><span style="color:#000;font-weight:bold">]|</span><span style="color:#900;font-weight:bold">replace</span><span style="color:#000;font-weight:bold">(</span><span style="color:#d14">&#39;*&#39;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#39;\*&#39;</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">}}{%</span> <span style="color:#000;font-weight:bold">endif</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}{{</span>  <span style="color:#008080">arg</span><span style="color:#000;font-weight:bold">[</span><span style="color:#099">1</span><span style="color:#000;font-weight:bold">]</span> -<span style="color:#999;font-weight:bold;font-style:italic">}}</span>
          <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">if</span> <span style="color:#000;font-weight:bold">not</span> <span style="color:#0086b3">loop</span><span style="color:#008080">.last</span>  <span style="color:#999;font-weight:bold;font-style:italic">%}</span>, <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endif</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
       <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">endfor</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
       )<span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">endif</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">endmacro</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>

<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">macro</span> <span style="color:#008080">_item</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">obj</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">sig</span><span style="color:#000;font-weight:bold">=</span><span style="color:#000;font-weight:bold">False</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">label</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#39;&#39;</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   * - <span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">_render_item_name</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">obj</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">sig</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
     - <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">if</span> <span style="color:#008080">label</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>:summarylabel:`<span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">label</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>` <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endif</span> <span style="color:#999;font-weight:bold;font-style:italic">%}{%</span> <span style="color:#000;font-weight:bold">if</span> <span style="color:#008080">obj.summary</span> <span style="color:#999;font-weight:bold;font-style:italic">%}{{</span> <span style="color:#008080">obj.summary</span> <span style="color:#999;font-weight:bold;font-style:italic">}}{%</span> <span style="color:#000;font-weight:bold">else</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>\-<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endif</span> <span style="color:#000;font-weight:bold">+</span><span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endmacro</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>

<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">macro</span> <span style="color:#008080">auto_summary</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">objs</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">title</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#39;&#39;</span><span style="color:#000;font-weight:bold">)</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
.. list-table:: <span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">title</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
   :header-rows: 0
   :widths: auto
   :class: summarytable

  <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">for</span> <span style="color:#008080">obj</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">objs</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
    <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">sig</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#008080">obj.type</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#000;font-weight:bold">[</span><span style="color:#d14">&#39;method&#39;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#39;function&#39;</span><span style="color:#000;font-weight:bold">]</span> <span style="color:#000;font-weight:bold">and</span> <span style="color:#000;font-weight:bold">not</span> <span style="color:#d14">&#39;property&#39;</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">obj.properties</span><span style="color:#000;font-weight:bold">)</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>

    <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">if</span> <span style="color:#d14">&#39;property&#39;</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">obj.properties</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
      <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">label</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#39;prop&#39;</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
    <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">elif</span> <span style="color:#d14">&#39;classmethod&#39;</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">obj.properties</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
      <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">label</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#39;class&#39;</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
    <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">elif</span> <span style="color:#d14">&#39;abstractmethod&#39;</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">obj.properties</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
      <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">label</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#39;abc&#39;</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
    <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">elif</span> <span style="color:#d14">&#39;staticmethod&#39;</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">obj.properties</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
      <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">label</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#39;static&#39;</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
    <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">else</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
      <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">label</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#d14">&#39;&#39;</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>
    <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">endif</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>

    <span style="color:#999;font-weight:bold;font-style:italic">{{</span><span style="color:#000;font-weight:bold">-</span> <span style="color:#008080">_item</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">obj</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">sig</span><span style="color:#000;font-weight:bold">=</span><span style="color:#008080">sig</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">label</span><span style="color:#000;font-weight:bold">=</span><span style="color:#008080">label</span><span style="color:#000;font-weight:bold">)</span> -<span style="color:#999;font-weight:bold;font-style:italic">}}</span>
  <span style="color:#999;font-weight:bold;font-style:italic">{%</span>- <span style="color:#000;font-weight:bold">endfor</span> -<span style="color:#999;font-weight:bold;font-style:italic">%}</span>

<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endmacro</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
</code></pre></div><p>The work is now split in three macros:</p>
<dl>
<dt><code>auto_summary()</code></dt>
<dd>This is the &ldquo;public&rdquo; macro. It generates a table based on a list of mapper objects, with an optional title. It iterates over the list of objects, and, for each of them, determines if the signature should be generated (functions and non-property methods) and if some label should be attached. It then uses <code>_item()</code> to generate each object&rsquo;s code.</dd>
<dt><code>_item()</code></dt>
<dd>This helper macro generates the code for each object, prepending a label to the summary if requested.</dd>
<dt><code>_render_item_name()</code></dt>
<dd>This helper macro focuses on generating the properly-cross-referenced object name. If the signature is requested, it iterates over the <code>obj.obj.args</code> list to produce a full (but unannotated) list of arguments.</dd>
</dl>
<h2 id="improving-the-default-templates">Improving the default templates</h2>
<p>With our <code>auto_summary()</code> macro completed, we are now ready to customise our templates, but we still have one issue to resolve before we do so.</p>
<h3 id="categorising-objects-with-a-custom-jinja2-test">Categorising objects with a custom Jinja2 test</h3>
<p>As we saw in the <a href="#autoapi-objects">AutoAPI objects</a> section, mapper objects representing modules or classes have a <code>children</code> attribute which lists the objects it contains. For example, a module&rsquo;s <code>children</code> attribute lists all the classes, functions and attributes defined within it.</p>
<p>In order to categorise these children into separate sub-lists, the built-in templates heavily use the <a href="https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.selectattr"><code>selectattr()</code></a> and <a href="https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.rejectattr"><code>rejectattr()</code></a> filters. For example, a list of classes in a module can be obtained as follows:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja"><span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">visible_children</span> <span style="color:#000;font-weight:bold">=</span>
    <span style="color:#008080">module_object.children</span><span style="color:#000;font-weight:bold">|</span><span style="color:#900;font-weight:bold">selectattr</span><span style="color:#000;font-weight:bold">(</span><span style="color:#d14">&#34;display&#34;</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">rejectattr</span><span style="color:#000;font-weight:bold">(</span><span style="color:#d14">&#34;imported&#34;</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">list</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">visible_classes</span> <span style="color:#000;font-weight:bold">=</span>
    <span style="color:#008080">visible_children</span><span style="color:#000;font-weight:bold">|</span><span style="color:#900;font-weight:bold">selectattr</span><span style="color:#000;font-weight:bold">(</span><span style="color:#d14">&#34;type&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#34;equalto&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#34;class&#34;</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">list</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
</code></pre></div><p>This code selects the visible (but not imported) children from <code>module_object</code>, and then further selects children which have their <code>type</code> set to <code>&quot;class&quot;</code>. In the code above, <a href="https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-tests.eq"><code>&quot;equalto&quot;</code></a> is known as a Jinja <a href="https://jinja.palletsprojects.com/en/3.1.x/templates/#tests">test</a>. There are many such <a href="https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-tests">built-in tests</a> in Jinja2.</p>
<p>As stated before, we aim to categorise properties as attributes instead of methods. To that end, we will have to filter methods whose <code>properties</code> attribute contains <code>&quot;property&quot;</code>. Intuition dictates that the following code achieves this:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja"><span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">property_methods</span> <span style="color:#000;font-weight:bold">=</span>
    <span style="color:#008080">all_methods</span><span style="color:#000;font-weight:bold">|</span><span style="color:#900;font-weight:bold">selectattr</span><span style="color:#000;font-weight:bold">(</span><span style="color:#d14">&#34;properties&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#34;contains&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#34;property&#34;</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">list</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
</code></pre></div><p>The bad news is that no such <code>&quot;contains&quot;</code> test exists by default in Jinja2. The good news is that it is trivial to add one.</p>
<p>First, the actual test must be written. Add the following code to your <code>conf.py</code> file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">contains</span>(seq, item):
    <span style="color:#000;font-weight:bold">return</span> item <span style="color:#000;font-weight:bold">in</span> seq
</code></pre></div><p>Then, we just need to add this test to the Jinja <a href="https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment">environment</a>. Sphinx AutoAPI provides a hook for that:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">prepare_jinja_env</span>(jinja_env) <span style="color:#000;font-weight:bold">-&gt;</span> <span style="color:#000;font-weight:bold">None</span>:
    jinja_env<span style="color:#000;font-weight:bold">.</span>tests[<span style="color:#d14">&#34;contains&#34;</span>] <span style="color:#000;font-weight:bold">=</span> contains

autoapi_prepare_jinja_env <span style="color:#000;font-weight:bold">=</span> prepare_jinja_env
</code></pre></div><p>With this in your <code>conf.py</code> file, the template code above will work as expected.</p>
<h3 id="updating-templates">Updating templates</h3>
<p>We previously replaced one of <code>module.rst</code>&rsquo;s use of <code>autoapisummary</code> by our <code>auto_summary()</code> macro (see <a href="#basic-macro-setup">Basic macro setup</a>). It is now time to generalise the use of our macro. At this stage, the details of how this is done is to a large extent up to reader&rsquo;s taste. The <a href="https://github.com/abey79/vsketch/tree/master/docs/_templates/autoapi">templates</a> of <a href="https://github.com/abey79/vsketch">vsketch</a> can serve as fully-functional example and can readily be used in another projects.</p>
<p>For the <code>module.rst</code> template, I have opted to simplify the overview&rsquo;s structure by just generating tables (without headings) for classes, functions, and attributes:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja"><span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">if</span> <span style="color:#d14">&#34;show-module-summary&#34;</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">autoapi_options</span> <span style="color:#000;font-weight:bold">and</span> <span style="color:#000;font-weight:bold">(</span><span style="color:#008080">visible_classes</span> <span style="color:#000;font-weight:bold">or</span> <span style="color:#008080">visible_functions</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">block</span> <span style="color:#008080">classes</span> <span style="color:#000;font-weight:bold">scoped</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">if</span> <span style="color:#008080">visible_classes</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">macros.auto_summary</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">visible_classes</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">title</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;Classes&#34;</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endif</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endblock</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>

<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">block</span> <span style="color:#008080">functions</span> <span style="color:#000;font-weight:bold">scoped</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">if</span> <span style="color:#008080">visible_functions</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">macros.auto_summary</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">visible_functions</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">title</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;Functions&#34;</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endif</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endblock</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>

<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">block</span> <span style="color:#008080">attributes</span> <span style="color:#000;font-weight:bold">scoped</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">if</span> <span style="color:#008080">visible_attributes</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">macros.auto_summary</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">visible_attributes</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">title</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;Attributes&#34;</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endif</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endblock</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
<span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endif</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
</code></pre></div><p>For the <code>class.rst</code> template, I chose to rework the structure of the documentation into two <a href="https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-rubric">rubrics</a>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja">   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">if</span> <span style="color:#008080">visible_methods</span> <span style="color:#000;font-weight:bold">or</span> <span style="color:#008080">visible_attributes</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   .. rubric:: Overview

   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">summary_methods</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#008080">visible_methods</span><span style="color:#000;font-weight:bold">|</span><span style="color:#900;font-weight:bold">rejectattr</span><span style="color:#000;font-weight:bold">(</span><span style="color:#d14">&#34;properties&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#34;contains&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#34;property&#34;</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">list</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">summary_attributes</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#008080">visible_attributes</span> <span style="color:#000;font-weight:bold">+</span> <span style="color:#008080">visible_methods</span><span style="color:#000;font-weight:bold">|</span><span style="color:#900;font-weight:bold">selectattr</span><span style="color:#000;font-weight:bold">(</span><span style="color:#d14">&#34;properties&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#34;contains&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#34;property&#34;</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">list</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   
   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">if</span> <span style="color:#008080">summary_attributes</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">macros.auto_summary</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">summary_attributes</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">title</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;Attributes&#34;</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">indent</span><span style="color:#000;font-weight:bold">(</span><span style="color:#099">3</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endif</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>

   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">if</span> <span style="color:#008080">summary_methods</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">macros.auto_summary</span><span style="color:#000;font-weight:bold">(</span><span style="color:#008080">summary_methods</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">title</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;Methods&#34;</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">indent</span><span style="color:#000;font-weight:bold">(</span><span style="color:#099">3</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endif</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>

   .. rubric:: Members

   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">for</span> <span style="color:#008080">attribute</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">visible_attributes</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">attribute.render</span><span style="color:#000;font-weight:bold">()|</span><span style="color:#900;font-weight:bold">indent</span><span style="color:#000;font-weight:bold">(</span><span style="color:#099">3</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endfor</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">for</span> <span style="color:#008080">method</span> <span style="color:#000;font-weight:bold">in</span> <span style="color:#008080">visible_methods</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{{</span> <span style="color:#008080">method.render</span><span style="color:#000;font-weight:bold">()|</span><span style="color:#900;font-weight:bold">indent</span><span style="color:#000;font-weight:bold">(</span><span style="color:#099">3</span><span style="color:#000;font-weight:bold">)</span> <span style="color:#999;font-weight:bold;font-style:italic">}}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endfor</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
   <span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">endif</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
</code></pre></div><p>Note the use of the custom <code>&quot;contains&quot;</code> Jinja2 test we implemented earlier.</p>
<h3 id="hiding-submodules">Hiding submodules</h3>
<p>Sphinx AutoAPI include all subpackages and submodules recursively, unless those are marked as private by prefixing their name with an underscore. In my packages' <code>__init__.py</code> file, I carefully import from submodules the objects which are meant to be public, but haven&rsquo;t necessarily marked the submodules as private. Sphinx AutoAPI has no option to control whether or not to add them (I <a href="https://github.com/readthedocs/sphinx-autoapi/issues/339">suggested</a> adding one), so I had to filter them out manually. This is done with the <code>autoapi-skip-member</code> event handler we mentioned earlier:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">skip_member</span>(app, what, name, obj, skip, options):
    <span style="color:#998;font-style:italic"># skip submodules</span>
    <span style="color:#000;font-weight:bold">if</span> what <span style="color:#000;font-weight:bold">==</span> <span style="color:#d14">&#34;module&#34;</span>:
        skip <span style="color:#000;font-weight:bold">=</span> <span style="color:#000;font-weight:bold">True</span>
    <span style="color:#000;font-weight:bold">return</span> skip

<span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">setup</span>(sphinx):
    sphinx<span style="color:#000;font-weight:bold">.</span>connect(<span style="color:#d14">&#34;autoapi-skip-member&#34;</span>, skip_member)
</code></pre></div><h3 id="hiding-members">Hiding members</h3>
<p>Likewise, it may happen that you want to hide specific members from the documentation without marking them as private. Again, the <code>autoapi-skip-member</code> event handler can do that. The following example is based from <a href="https://github.com/abey79/vsketch/blob/0d937c851ac528bf182d0b71eb42ead525848c60/docs/conf.py#L116">actual code</a> in <a href="https://github.com/abey79/vsketch"><em>vsketch</em></a>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">skip_member</span>(app, what, name, obj, skip, options):
    <span style="color:#000;font-weight:bold">if</span> <span style="color:#d14">&#34;vsketch.SketchClass&#34;</span> <span style="color:#000;font-weight:bold">in</span> name:
        <span style="color:#000;font-weight:bold">if</span> obj<span style="color:#000;font-weight:bold">.</span>name <span style="color:#000;font-weight:bold">in</span> [
            <span style="color:#d14">&#34;vsk&#34;</span>,
            <span style="color:#d14">&#34;param_set&#34;</span>,
            <span style="color:#d14">&#34;execute_draw&#34;</span>,
            <span style="color:#d14">&#34;ensure_finalized&#34;</span>,
            <span style="color:#d14">&#34;execute&#34;</span>,
            <span style="color:#d14">&#34;get_params&#34;</span>,
            <span style="color:#d14">&#34;set_param_set&#34;</span>,
        ]:
            skip <span style="color:#000;font-weight:bold">=</span> <span style="color:#000;font-weight:bold">True</span>
    <span style="color:#000;font-weight:bold">return</span> skip
</code></pre></div><p>Note that <code>name</code> is the fully qualified name of the object, so the <code>vsk</code> member has <code>name</code> set to <code>vsketch.SketchClass.vsk</code>. In contrast, <code>obj.name</code> is just the base name.</p>
<p>Also, for this to work as expected for modules, I had to change the following line in <code>module.rst</code></p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja"><span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">visible_children</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#008080">obj.children</span><span style="color:#000;font-weight:bold">|</span><span style="color:#900;font-weight:bold">selectattr</span><span style="color:#000;font-weight:bold">(</span><span style="color:#d14">&#34;short_name&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#34;in&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">obj.all</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">list</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
</code></pre></div><p>into</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-jinja" data-lang="jinja"><span style="color:#999;font-weight:bold;font-style:italic">{%</span> <span style="color:#000;font-weight:bold">set</span> <span style="color:#008080">visible_children</span> <span style="color:#000;font-weight:bold">=</span> <span style="color:#008080">obj.children</span><span style="color:#000;font-weight:bold">|</span><span style="color:#900;font-weight:bold">selectattr</span><span style="color:#000;font-weight:bold">(</span><span style="color:#d14">&#34;display&#34;</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">selectattr</span><span style="color:#000;font-weight:bold">(</span><span style="color:#d14">&#34;short_name&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#d14">&#34;in&#34;</span><span style="color:#000;font-weight:bold">,</span> <span style="color:#008080">obj.all</span><span style="color:#000;font-weight:bold">)|</span><span style="color:#900;font-weight:bold">list</span> <span style="color:#999;font-weight:bold;font-style:italic">%}</span>
</code></pre></div><p>Without this modification, objects marked as skipped would show up in the summary tables.</p>
<h3 id="ordering">Ordering</h3>
<p>By default, Sphinx AutoAPI generates the documentation in the same order as the code. This can be <a href="https://sphinx-autoapi.readthedocs.io/en/latest/reference/config.html?highlight=ordering#confval-autoapi_member_order">changed</a> to alphabetical order, I like being in control from the code.</p>
<p>In <a href="https://github.com/abey79/vsketch"><em>vsketch</em></a>, the top level content is determined by the imports in my package&rsquo;s <code>__init__.py</code> file, so the <code>import</code> statements themself matter. Since I&rsquo;m using <a href="https://pycqa.github.io/isort/">isort</a>, I had to short-circuit it in this particular case:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#998;font-style:italic"># vsketch/__init__.py</span>

<span style="color:#998;font-style:italic"># isort: skip_file</span>

<span style="color:#998;font-style:italic"># Ordered for the documentation</span>
<span style="color:#000;font-weight:bold">from</span> <span style="color:#555">.vsketch</span> <span style="color:#000;font-weight:bold">import</span> Vsketch
<span style="color:#000;font-weight:bold">from</span> <span style="color:#555">.shape</span> <span style="color:#000;font-weight:bold">import</span> Shape
<span style="color:#000;font-weight:bold">from</span> <span style="color:#555">.sketch_class</span> <span style="color:#000;font-weight:bold">import</span> SketchClass, Param, ParamType

<span style="color:#000;font-weight:bold">from</span> <span style="color:#555">.easing</span> <span style="color:#000;font-weight:bold">import</span> EASING_FUNCTIONS
<span style="color:#000;font-weight:bold">from</span> <span style="color:#555">.utils</span> <span style="color:#000;font-weight:bold">import</span> working_directory
</code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>Well, this ended up being much longer than anticipated 😅 – if you made it this far, congratulations! 🎉</p>
<p>Here is what we covered:</p>
<ul>
<li>We built on a basic Sphinx project and added AutoAPI to generate an API reference documentation.</li>
<li>We created custom templates based on the built-in templates provided by AutoAPI.</li>
<li>We reviewed the mapper objects created by AutoAPI to described our code.</li>
<li>We learned how to use a debugger to easily inspect these objects.</li>
<li>We crafted an <code>autosummary</code>-like macro named <code>auto_summary()</code> to build beautiful summary tables.</li>
<li>We customised these tables with custom CSS.</li>
<li>We used a custom Sphinx role and CSS to create tag-like labels to be used in the summary tables.</li>
<li>We learned of Jinja2&rsquo;s tests and created a custom one.</li>
<li>We controlled the visibility of submodules and members using a <code>autoapi-skip-member</code> event handler.</li>
<li>We learned how to control ordering from our code.</li>
</ul>
<p>Like any software project, improving the documentation is a never-ending endeavour. As it turns out, there is one remaining issue that has been bugging me and is yet unresolved. Due to a limitation in Sphinx, AutoAPI has a tendency to mangle the TOC ordering, especially when section headings are emitted from the templates. Check these <a href="https://github.com/readthedocs/sphinx-autoapi/issues/283">two</a> <a href="https://github.com/sphinx-doc/sphinx/issues/6316#issuecomment-1066195555">issues</a> for more information. Hopefully they&rsquo;ll get solved in the future.</p>
<p>I shall conclude by stating that, by all means, I do not consider myself a Sphinx expert — much to the contrary. I <em>did</em> spend a lot of time improving my API documentation, and figured it would be wasteful not to share my findings, especially given the relative scarcity of information on advanced Sphinx usage patterns. As a result, it is rather likely that I made mistakes and sub-optimal choices. If so, please do provide feedback, and I&rsquo;ll update this article to improve it.</p>
<p><em>Edit: updated title and intro to clarify the nature of the API discussed, i.e. Python API (2022-05-11).</em></p>
<section class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1" role="doc-endnote">
<p>Ideally, these shortcomings would be addressed using an extension and a custom directive, or even by contributing the improvement back to the Sphinx or AutoAPI projects. This is sadly beyond my skills. Also, the template method is anyway useful for highly specific requirements where writing an extension wouldn&rsquo;t be warranted.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</section>
]]></content:encoded>
    </item>
    
    <item>
      <title>Sketch: fill test pattern generator</title>
      <link>https://bylr.info/articles/2022/04/28/sketch-fill-test/</link>
      <pubDate>Thu, 28 Apr 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/04/28/sketch-fill-test/</guid>
      <description>Hatch fills or pixel art plotting requires a rather precise estimate of your particular pen/paper combo&amp;rsquo;s stroke width.
For example, this test pixel art plot would benefit from a slightly thinner pitch to avoid the visible overlap between neighbouring lines:
There is no way around experience to find the optimal pitch. I&amp;rsquo;ve created the fill_test sketch to create custom charts with test patterns precisely tuned to the pen of interest. There is indeed no point to testing a rotring isograph .</description>
      <content:encoded><![CDATA[<p>Hatch fills or <a href="https://github.com/abey79/vpype-pixelart">pixel art</a> plotting requires a rather precise estimate of your particular pen/paper combo&rsquo;s stroke width.</p>
<p>For example, this test pixel art plot would benefit from a slightly thinner pitch to avoid the visible overlap between neighbouring lines:</p>
<img src="/sketch-fill-test/obama.jpg"alt="plotted obama pixelart with visibly overlap between neighbouring lines"width="450px"style="display:block;margin-left:auto;margin-right:auto"/>
<p>There is no way around experience to find the optimal pitch. I&rsquo;ve created the <a href="https://github.com/abey79/sketches/tree/master/fill_test"><code>fill_test</code></a> sketch to create custom charts with test patterns precisely tuned to the pen of interest. There is indeed no point to testing a rotring isograph .35mm pen with a generic 0.1mm to 1.0mm chart.</p>
<p>Here is how the UI looks:</p>
<img src="/sketch-fill-test/ui.png"alt="UI of a fill test pattern generator sketch made with vsketch"width="650px"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Beyond the general layout options (page size, grid size, etc.), the <code>Smallest Width</code> and <code>Width Increment</code> parameters enable a fine-grained exploration of pitches around the nominal pen size.</p>
<p>Here is an example with a rotring isograph .35mm in my notebook, which has a slight propensity for ink soaking. For this combo, fills with .4mm pitch yield the best results:</p>
<img src="/sketch-fill-test/example.jpg"alt="example of plotted fill test patterns"width="450px"style="display:block;margin-left:auto;margin-right:auto"/>
<p>To use the sketch, download or clone my <a href="https://github.com/abey79/sketches">sketches</a> repository and execute the sketch using your existing <a href="https://github.com/abey79/vsketch"><em>vsketch</em></a> installation:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ vsk run path/to/sketchs/fill_test
</code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>How to scale a grid on a page for uniform margins?</title>
      <link>https://bylr.info/articles/2022/04/13/grid-layout/</link>
      <pubDate>Wed, 13 Apr 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/04/13/grid-layout/</guid>
      <description>The problem Several generative art algorithms, such as Truchet tiles, use a regular grid of square cells. For example, check this interactive demo from the Generative Design book or these few pieces of mine.
Now, let&amp;rsquo;s say you want to generate an iteration of your algorithm for printing or plotting such that all margins around the grid are the same for the given paper size. You can of course adjust the number of cell rows and columns, but how should you size the cell such as to achieve uniform margins?</description>
      <content:encoded><![CDATA[<h2 id="the-problem">The problem</h2>
<p>Several generative art algorithms, such as <a href="https://en.wikipedia.org/wiki/Truchet_tiles">Truchet tiles</a>, use a regular grid of square cells. For example, check this <a href="http://www.generative-gestaltung.de/2/sketches/?01_P/P_2_3_6_01">interactive demo</a> from the <a href="http://www.generative-gestaltung.de/2/">Generative Design</a> book or <a href="https://github.com/abey79/vpype-explorations#covid-in-complex-module">these</a> <a href="https://twitter.com/abey79/status/1251148503176237057?s=20&amp;t=zJlPTdagH-8hVnEKHSLOYw">few</a> <a href="https://github.com/abey79/sketches/blob/master/README.md#liquid_neon">pieces</a> of mine.</p>
<p>Now, let&rsquo;s say you want to generate an iteration of your algorithm for printing or plotting such that all margins around the grid are the same for the given paper size. You can of course adjust the number of cell rows and columns, but how should you size the cell such as to achieve uniform margins?</p>
<p>The image above illustrates the problem. For a given page of size $W \times H$ and a regular grid of $N \times M$ cells, what should be the cell size $s$ to achieve uniform margins $m$ around the grid? What is then the value of $m$?</p>
<h2 id="the-solution">The solution</h2>
<p>This is easy to solve with a bit of math. Here is the system of two equations that must be solved:</p>
<span>
  \[\begin{cases}
2 \cdot m &#43; N \cdot s = W \\
2 \cdot m &#43; M \cdot s = H
\end{cases}\]
</span>
<p>with all parameters ($N$, $M$, $W$, $H$) and the cell size $s$ being strictly positive.</p>
<p>Solving this for $m$ and $s$ is no rocket science but <a href="https://www.wolframalpha.com">Wolfram Alpha</a> can <a href="https://www.wolframalpha.com/input?i=solve+%7B2*m+%2B+N*s+%3D+W%2C+2*m+%2B+M*s+%3D+H%2C+W%3E0%2C+H%3E0%2C+M%3E0%2C+N%3E0%2C+s%3E0%7D+for+m+and+s+over+the+reals">do the job for you</a> if your high school math is rusty!</p>
<img src="/grid-layout/wolfram.png"alt="equation solved by Wolfram Alpha"width="70%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>We have the following solutions:</p>
<span>
  \[\begin{cases}
\displaystyle s = \frac{H - 2 m}{N}, \quad m &lt; \frac{H}{2} &amp; \footnotesize M = N, \; W = H \\
\\
\displaystyle s = \frac{H-W}{M-N}, \quad m = \frac{M W - H N}{2(M - N)} &amp; \footnotesize N&lt;M, \; W&lt;H \quad \textrm{or} \quad  N&gt;M, \; W&gt;H
\end{cases}\]
</span>
<p>The first solution corresponds to the special case of a square page size. In this case, the grid must be square ($N=M$) and the margins may have an arbitrary value, with the cell size varying accordingly. This is not very surprising.</p>
<p>The second solution is where things become interesting. As intuition dictates, it is valid <em>only</em> if the grid orientation (portrait or landscape) matches the paper orientation. If so, uniform margins is achieved by choosing a cell size of $s = \frac{H-W}{M-N}$.</p>
<p>Note that the resulting margin $m$ may, depending on the parameters, be negative. In this case, the grid <em>overflows</em> all around the page by a constant distance. This making plotting/printing your piece inconvenient, you will have to adjust $N$ and/or $M$ to reach a positive margin value.</p>
<h2 id="the-demo">The demo</h2>
<p>I made a <a href="https://github.com/abey79/sketches/tree/master/centred_grid">demonstration sketch</a> made with <a href="https://github.com/abey79/vsketch"><em>vsketch</em></a>. Here is how it looks:</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/4nlGM2hcV9o" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<br/>
<p>This is a simplified version of the code which can be used as a starting point for your next grid-based design:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">import</span> <span style="color:#555">itertools</span>
<span style="color:#000;font-weight:bold">import</span> <span style="color:#555">vsketch</span>

<span style="color:#000;font-weight:bold">class</span> <span style="color:#458;font-weight:bold">MySketch</span>(vsketch<span style="color:#000;font-weight:bold">.</span>SketchClass):
    N <span style="color:#000;font-weight:bold">=</span> vsketch<span style="color:#000;font-weight:bold">.</span>Param(<span style="color:#099">5</span>, <span style="color:#099">1</span>)
    M <span style="color:#000;font-weight:bold">=</span> vsketch<span style="color:#000;font-weight:bold">.</span>Param(<span style="color:#099">7</span>, <span style="color:#099">1</span>)

    <span style="color:#000;font-weight:bold">def</span> <span style="color:#900;font-weight:bold">draw</span>(<span style="color:#999">self</span>, vsk: vsketch<span style="color:#000;font-weight:bold">.</span>Vsketch) <span style="color:#000;font-weight:bold">-&gt;</span> <span style="color:#000;font-weight:bold">None</span>:
        vsk<span style="color:#000;font-weight:bold">.</span>size(<span style="color:#d14">&#34;a4&#34;</span>, landscape<span style="color:#000;font-weight:bold">=</span><span style="color:#000;font-weight:bold">False</span>, center<span style="color:#000;font-weight:bold">=</span><span style="color:#000;font-weight:bold">False</span>)   <span style="color:#998;font-style:italic"># disable auto-centering</span>

        cell_size <span style="color:#000;font-weight:bold">=</span> (vsk<span style="color:#000;font-weight:bold">.</span>height <span style="color:#000;font-weight:bold">-</span> vsk<span style="color:#000;font-weight:bold">.</span>width) <span style="color:#000;font-weight:bold">/</span> (<span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>M <span style="color:#000;font-weight:bold">-</span> <span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>N)
        margin <span style="color:#000;font-weight:bold">=</span> (<span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>M <span style="color:#000;font-weight:bold">*</span> vsk<span style="color:#000;font-weight:bold">.</span>width <span style="color:#000;font-weight:bold">-</span> <span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>N <span style="color:#000;font-weight:bold">*</span> vsk<span style="color:#000;font-weight:bold">.</span>height) <span style="color:#000;font-weight:bold">/</span> <span style="color:#099">2</span> <span style="color:#000;font-weight:bold">/</span> (<span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>M <span style="color:#000;font-weight:bold">-</span> <span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>N)
        
        <span style="color:#000;font-weight:bold">if</span> cell_size <span style="color:#000;font-weight:bold">&gt;</span> <span style="color:#099">0</span>:
            <span style="color:#998;font-style:italic"># account for the computed margin</span>
            vsk<span style="color:#000;font-weight:bold">.</span>translate(margin, margin)
            
            <span style="color:#998;font-style:italic"># draw the grid</span>
            <span style="color:#000;font-weight:bold">for</span> i, j <span style="color:#000;font-weight:bold">in</span> itertools<span style="color:#000;font-weight:bold">.</span>product(<span style="color:#0086b3">range</span>(<span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>N <span style="color:#000;font-weight:bold">+</span> <span style="color:#099">1</span>), <span style="color:#0086b3">range</span>(<span style="color:#999">self</span><span style="color:#000;font-weight:bold">.</span>M <span style="color:#000;font-weight:bold">+</span> <span style="color:#099">1</span>)):
                vsk<span style="color:#000;font-weight:bold">.</span>point(i <span style="color:#000;font-weight:bold">*</span> cell_size, j <span style="color:#000;font-weight:bold">*</span> cell_size)
        <span style="color:#000;font-weight:bold">else</span>:
            <span style="color:#998;font-style:italic"># ERROR: N and M values must be adjusted!</span>
            <span style="color:#000;font-weight:bold">pass</span>
</code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>Annotated Release Notes: vpype 1.10</title>
      <link>https://bylr.info/articles/2022/04/07/annotated-release-notes-vpype-1.10/</link>
      <pubDate>Thu, 07 Apr 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/04/07/annotated-release-notes-vpype-1.10/</guid>
      <description>I originally intended vpype 1.10 to be a &amp;lsquo;quick-and-dirty&amp;rsquo;, bug-fix-only release but it ended up being quite substantial, so let&amp;rsquo;s dive in.
New features and improvements   Improved support for layer pen width and opacity in the viewer (#448)
 The &amp;ldquo;Pen Width&amp;rdquo; and &amp;ldquo;Pen Opacity&amp;rdquo; menus are now named &amp;ldquo;Default Pen Width&amp;rdquo; and &amp;ldquo;Default Pen Opacity&amp;rdquo;. The layer opacity is now used for display by default. It can be overridden by the default pen opacity by checking the &amp;ldquo;Override&amp;rdquo; item from the &amp;ldquo;Default Pen Opacity&amp;rdquo; menu.</description>
      <content:encoded><![CDATA[<p>I originally intended <a href="https://github.com/abey79/vpype/releases/tag/1.10.0"><em>vpype</em> 1.10</a> to be a &lsquo;quick-and-dirty&rsquo;, bug-fix-only release but it ended up being quite substantial, so let&rsquo;s dive in.</p>
<h2 id="new-features-and-improvements">New features and improvements</h2>

<blockquote class="extract">
  <p><ul>
<li>
<p>Improved support for layer pen width and opacity in the viewer (#448)</p>
<ul>
<li>The &ldquo;Pen Width&rdquo; and &ldquo;Pen Opacity&rdquo; menus are now named &ldquo;Default Pen Width&rdquo; and &ldquo;Default Pen Opacity&rdquo;.</li>
<li>The layer opacity is now used for display by default. It can be overridden by the default pen opacity by checking the &ldquo;Override&rdquo; item from the &ldquo;Default Pen Opacity&rdquo; menu.</li>
<li>The layer pen width is now used for display by default as well. Likewise, it can be overridden by checking the &ldquo;Override&rdquo; item from the &ldquo;Default Pen Width&rdquo; menu.</li>
</ul>
</li>
</ul>
</p>
</blockquote>

<p>This alone is reason to upgrade, and, if we&rsquo;re being honest, it should have been done in the previous release. The display logic of the viewer is now as follows:</p>
<ul>
<li>By default, honor the layer&rsquo;s pen width and opacity if present.</li>
<li>If pen width and/or opacity is not set, revert to the value set in the menu (0.3mm / 80% by default).</li>
<li>Either or both of the displayed pen width and opacity can be forced to the value in the menu using the new &ldquo;Override&rdquo; menu item.</li>
</ul>
<p>It is worth noting that opacity is not a standalone layer property, but is part of its RGBA color property (<code>vp_color</code>). Weirdly, <em>vpype</em> 1.9&rsquo;s viewer would honor the base color (RGB), but not its alpha chanel. (See next feature, though.)</p>
<p>By the way, this article&rsquo;s cover image is a screenshot of the viewer made with this command:</p>
<img src="/vpype_110/demo_cmd.png"alt="vpype  repeat 100  circle --layer %_i&#43;1% %_i*mm% 0 1cm  color --layer %_i&#43;1% red  alpha --layer %_i&#43;1% %(_i&#43;1)*.7/_n%  end  repeat 50  circle --layer %_i&#43;101% %2*_i*mm% 3cm 1cm  color --layer %_i&#43;101% blue  penwidth --layer %_i&#43;101% %(0.05&#43;_i/100)*2.5*mm%  end  layout --fit-to-margins 1cm --landscape 21x10cm  show"width="85%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>(Yes, this is a properly syntax-highlighted <em>vpype</em> command made with a custom script and <a href="https://github.com/Textualize/rich">Rich</a>&rsquo;s new <a href="https://twitter.com/willmcgugan/status/1510621023678476291?s=20&amp;t=KegUSS_vGKzzG8GzoFsoFQ">SVG export</a>. This is very preliminary, but, in time, something to improve on and deploy more widely in the doc and elsewhere.)</p>

<blockquote class="extract">
  <p><ul>
<li>Added the <code>alpha</code> command to set layer opacity without changing the base color (#447, #451)</li>
</ul>
</p>
</blockquote>

<p>While working on the viewer improvements, I realized how inconvenient it was to set an arbitrary opacity value. Using CSS color names (e.g. <code>color red</code>) always sets opacity to 100% and there is no way around the hex notation for a custom value (e.g. <code>color #ff00007f</code> or <code>color #f007</code>). The new <code>alpha</code> command fills that gap: <code>color red  alpha 0.5</code>.</p>

<blockquote class="extract">
  <p><ul>
<li>Added HPGL configuration for the Calcomp Artisan plotter (thanks to Andee Collard and @ithinkido) (#418)</li>
</ul>
</p>
</blockquote>

<p>Good news for Calcomp Artisan&rsquo;s owners! Let this be a reminder that I welcome this kind of contribution. Though I&rsquo;d like to, I can&rsquo;t own every single type of vintage plotter! 😅</p>

<blockquote class="extract">
  <p><ul>
<li>
<p>The <code>read</code> command now better handles SVGs with missing <code>width</code> or <code>height</code> attributes (#446)</p>
<p>When the <code>width</code> or <code>height</code> attribute is missing or expressed as percent, the <code>read</code> command now attempts to use the <code>viewBox</code> attribute to set the page size, defaulting to 1000x1000px if missing. This behavior can be overridden with the <code>--display-size</code> and the <code>--display-landscape</code> parameters.</p>
</li>
</ul>
</p>
</blockquote>

<p>I recently came across a SVG with a <code>viewBox</code> defined but no <code>width</code>/<code>height</code> attributes:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-svg" data-lang="svg"><span style="color:#000080">&lt;svg</span> <span style="color:#008080">xmlns=</span><span style="color:#d14">&#34;http://www.w3.org/2000/svg&#34;</span> <span style="color:#008080">viewBox=</span><span style="color:#d14">&#34;0 0 1191.26 1684.49&#34;</span><span style="color:#000080">&gt;</span>...<span style="color:#000080">&lt;/svg&gt;</span>
</code></pre></div><p>In such an instance, using the <code>read</code> command used to default to an A4 page size, while the <code>vpype.read_svg()</code> API (and friends) would default to a 1000x1000px page size. This is both inconsistent and missing the opportunity to fallback on the <code>viewBox</code>. <em>vpype</em> now fully delegates this fallback logic to <a href="https://github.com/meerk40t/svgelements"><code>svgelements</code></a>, which does a good job at making the most of the available information. Also, if everything is missing (or <code>width</code>/<code>height</code> are expressed in percents), <em>vpype</em> consistently falls back to 1000x1000px.</p>

<blockquote class="extract">
  <p><ul>
<li>Added the <code>--dont-set-date</code> option to the <code>write</code> command (#442)</li>
</ul>
</p>
</blockquote>

<p>This one is a bit niche. <em>vpype</em> adds some metadata to the SVG, including the date and time at which it was generated (note the <code>&lt;dc:date&gt;</code> tag):</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ vpype line 1cm 1cm 5cm 3cm layout a6 write -f svg -
&lt;?xml <span style="color:#008080">version</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;1.0&#34;</span> <span style="color:#008080">encoding</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;utf-8&#34;</span> ?&gt;
&lt;svg <span style="color:#008080">xmlns</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;http://www.w3.org/2000/svg&#34;</span> xmlns:cc<span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;http://creativecommons.org/ns&#34;</span> xmlns:dc<span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;http://purl.org/dc/elements/1.1/&#34;</span> xmlns:ev<span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;http://www.w3.org/2001/xml-events&#34;</span> xmlns:inkscape<span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;http://www.inkscape.org/namespaces/inkscape&#34;</span> xmlns:rdf<span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;http://www.w3.org/1999/02/22-rdf-syntax-ns&#34;</span> xmlns:sodipodi<span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd&#34;</span> xmlns:xlink<span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;http://www.w3.org/1999/xlink&#34;</span> <span style="color:#008080">baseProfile</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;tiny&#34;</span> <span style="color:#008080">height</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;14.8cm&#34;</span> <span style="color:#008080">version</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;1.2&#34;</span> <span style="color:#008080">viewBox</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;0 0 396.85039370078744 559.3700787401575&#34;</span> <span style="color:#008080">width</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;10.5cm&#34;</span>&gt;
  &lt;metadata&gt;
    &lt;rdf:RDF&gt;
      &lt;cc:Work&gt;
        &lt;dc:format&gt;image/svg+xml&lt;/dc:format&gt;
        &lt;dc:source&gt;vpype line 1cm 1cm 5cm 3cm layout a6 write -f svg -
&lt;/dc:source&gt;
        &lt;dc:date&gt;2022-04-07T10:15:00.532842&lt;/dc:date&gt;
      &lt;/cc:Work&gt;
    &lt;/rdf:RDF&gt;
  &lt;/metadata&gt;
  &lt;defs/&gt;
  &lt;g <span style="color:#008080">fill</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;none&#34;</span> <span style="color:#008080">id</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;layer1&#34;</span> inkscape:groupmode<span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;layer&#34;</span> inkscape:label<span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;1&#34;</span> <span style="color:#008080">stroke</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;#0000ff&#34;</span> <span style="color:#008080">style</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;display:inline&#34;</span>&gt;
    &lt;line <span style="color:#008080">x1</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;122.8346&#34;</span> <span style="color:#008080">x2</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;274.0157&#34;</span> <span style="color:#008080">y1</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;241.8898&#34;</span> <span style="color:#008080">y2</span><span style="color:#000;font-weight:bold">=</span><span style="color:#d14">&#34;317.4803&#34;</span>/&gt;
  &lt;/g&gt;
&lt;/svg&gt;
</code></pre></div><p>This is all well and good until you automate the generation of SVGs under a version control system, which is what I did for <a href="https://github.com/abey79/vpype-perspective">vpype-perspective</a>&rsquo;s documentation figures. A <a href="https://pydoit.org">PyDoIt</a> <a href="https://github.com/abey79/vpype-perspective/blob/main/examples/dodo.py"><code>dodo.py</code></a> files converts any <code>.vpy</code> file it finds into corresponding SVGs – basically a Python-powered, overcharged <code>Makefile</code>. (This is a rather neat process which, by the way, should get its own article someday.) In this kind of setup, having a ever-changing date in the SVG yields many unwanted VCS diffs which can now be avoided using this option.</p>
<h2 id="bug-fixes">Bug fixes</h2>

<blockquote class="extract">
  <p><ul>
<li>Fixed an issue with <code>forlayer</code> where the <code>_n</code> variable was improperly set (#443)</li>
</ul>
</p>
</blockquote>

<p>One word: inexcusable 🙄</p>

<blockquote class="extract">
  <p><ul>
<li>Fixed an issue with <code>write</code> where layer opacity was included in the <code>stroke</code> attribute instead of using <code>stroke-opacity</code>, which, although compliant, was not compatible with Inkscape (#429)</li>
</ul>
</p>
</blockquote>

<p>This one is <a href="https://gitlab.com/inkscape/inbox/-/issues/1195">Inkscape</a>&rsquo;s <a href="https://gitlab.com/inkscape/inbox/-/issues/1195">fault</a>. Using <code>stroke-opacity</code> is &ldquo;more compatible&rdquo; anyways, so it&rsquo;s a good move regardless.</p>

<blockquote class="extract">
  <p><ul>
<li>Fixed an issue with <code>vpype --help</code> where commands from plug-ins would not be listed (#444)</li>
</ul>
</p>
</blockquote>

<p>I ran into an issue with <a href="https://click.palletsprojects.com">Click</a> where a sub-command plug-in using APIs from the top-level command&rsquo;s package (a scheme widely used by <em>vpype</em> and its plug-ins) would fail because of circular imports. The workaround I used in <em>vpype</em> 1.9 meant that plug-ins were no longer listed in <code>vpype --help</code>. This is fixed now, but this may not be the end of the story. I tried – and failed – to reproduce the original issue in a minimal demo project and I&rsquo;ll have to further dig into this someday.</p>

<blockquote class="extract">
  <p><ul>
<li>Fixed a minor issue where plug-ins would be reloaded each time <code>vpype_cli.execute()</code> is called (#444)</li>
</ul>
</p>
</blockquote>

<p>By &ldquo;minor&rdquo;, I mean that this amounted to a tiny performance hit for Python scripts using <em>vpype</em>&rsquo;s <a href="https://vpype.readthedocs.io/en/latest/api/vpype_cli.html#vpype_cli.execute"><code>execute()</code></a> API multiple times.</p>

<blockquote class="extract">
  <p><ul>
<li>Fixed a rendering inconsistency in the viewer where the ruler width could vary by one pixel depending on the OpenGL driver/GPU/OS combination (#448)</li>
</ul>
</p>
</blockquote>

<p>The ruler of <em>vpype</em>&rsquo;s viewer is supposed to be 20px wide, but it turns out that either of the horizontal or the vertical one was 21px wide instead. Which one? It depends on the platform and my Intel/AMD- and M1-based laptops disagreed on the matter! 😲</p>
<img src="/vpype_110/bad_rulers.gif"alt="animation of a rendering discrepancy"width="50%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>It took me a while to discover that this is due to drawing lines at a 0.5px offset with respect to the pixel grid, leading to unpredictable rounding behavior. This is basically what happens when drawing horizontal or vertical lines with integer coordinates, and I <a href="/articles/2022/04/05/til-aligning-horizontal-or-vertical-lines-to-the-pixel-grid-with-opengl/">wrote about it</a> a few days ago.</p>
<p>You&rsquo;d think that not one soul would care about this, but some of my tests are based on comparing newly rendered images of the viewer with previously-generated reference images, and those tests would fail on my new M1 Mac.</p>
<h2 id="api-changes">API changes</h2>

<blockquote class="extract">
  <p><ul>
<li>Added <code>vpype_cli.FloatType()</code>, <code>vpype_cli.IntRangeType()</code>, <code>vpype_cli.FloatRangeType()</code>, and <code>vpype_cli.ChoiceType()</code> (#430, #447)</li>
</ul>
</p>
</blockquote>

<p>These <code>Click</code> types provide support for <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#property-substitution">property</a> and <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#expression-substitution">expression substitution</a>. They were missing from <em>vpype</em> 1.9 because they aren&rsquo;t needed internally. Plug-ins, however, wanted them, including <a href="https://github.com/serycjon/vpype-flow-imager">flow imager</a> and my new <a href="https://github.com/abey79/vpype-perspective">vpype-perspective</a>.</p>

<blockquote class="extract">
  <p><ul>
<li>Changed <code>vpype.Document.add_to_sources()</code> to also modify the <code>vp_source</code> property (#431)</li>
</ul>
</p>
</blockquote>

<p>This will simplify the code needed to handle sources in plug-ins.</p>

<blockquote class="extract">
  <p><ul>
<li>Changed the parameter name of both <code>vpype_viewer.Engine()</code> and <code>vpype_viewer.render_image()</code> from <code>pen_width</code> and <code>pen_opacity</code> to <code>default_pen_width</code> and <code>default_pen_opacity</code> (breaking change) (#448)</li>
<li>Added <code>override_pen_width</code> and <code>override_pen_opacity</code> boolean parameters to both <code>vpype_viewer.Engine()</code> and <code>vpype_viewer.render_image()</code> (#448)</li>
<li>Added a <code>set_date:bool = True</code> argument to <code>vpype.write_svg()</code> (#442)</li>
<li>Changed the default value of <code>default_width</code> and <code>default_height</code> arguments of <code>vpype.read_svg()</code> (and friends) to <code>None</code> to allow <code>svgelement</code> better handle missing <code>width</code>/<code>height</code> attributes (#446)</li>
</ul>
</p>
</blockquote>

<p>These are the API counterparts of some of the changes described before.</p>
<h2 id="other-changes">Other changes</h2>

<blockquote class="extract">
  <p><ul>
<li>Added support for Python 3.10 and dropped support for Python 3.7 (#417)</li>
</ul>
</p>
</blockquote>

<p>Walruses <a href="https://github.com/abey79/vpype/blob/0e01b45f6e8cef0352cd369a215aeaaeade97b48/vpype/model.py#L620">have appeared</a> already! 🦭</p>

<blockquote class="extract">
  <p><ul>
<li>Updated code base with modern typing syntax (using <a href="https://github.com/asottile/pyupgrade">pyupgrade</a>) (#427)</li>
</ul>
</p>
</blockquote>

<p>I was this year old when I learned that you can use modern <a href="https://docs.python.org/3/library/typing.html">typing</a> syntax (such as <code>list[int] | None</code>) with older Python versions thanks to this statement:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#000;font-weight:bold">from</span> <span style="color:#555">__future__</span> <span style="color:#000;font-weight:bold">import</span> annotations
</code></pre></div><p>I swiftly ran <a href="https://github.com/asottile/pyupgrade">pyupgrade</a> on the entire code base to bring it up to date.</p>

<blockquote class="extract">
  <p><ul>
<li>Updated the <a href="https://vpype.readthedocs.io/en/latest/">documentation</a> template (#428)</li>
</ul>
</p>
</blockquote>

<p>It looks cleaner now IMO, though there is still a <a href="https://github.com/abey79/vpype/issues/400">whole lot</a> that could be improved.</p>

<blockquote class="extract">
  <p><ul>
<li>Updated installation instructions to use pipx (#428)</li>
</ul>
</p>
</blockquote>

<p>I have yet to get over how long it took me to realize this! 😳 I&rsquo;m sorry for everyone who has struggled with virtual environments to install <em>vpype</em>!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>TIL: aligning horizontal or vertical lines to the pixel grid with OpenGL</title>
      <link>https://bylr.info/articles/2022/04/05/til-aligning-horizontal-or-vertical-lines-to-the-pixel-grid-with-opengl/</link>
      <pubDate>Tue, 05 Apr 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/04/05/til-aligning-horizontal-or-vertical-lines-to-the-pixel-grid-with-opengl/</guid>
      <description>When I started using my new M1 Max MacBook Pro in December, a bunch of vpype&amp;rsquo;s tests started to fail. The failing tests were all image-based: an image is rendered and then compared to a previously-generated, reference image. This process is made easy thanks to this Pytest fixture.
In this case, the reference images were generated long ago on my previous, Intel/AMD-based MacBook Pro. This GIF highlights the discrepancy I&amp;rsquo;d get with images generated on my new computer (notice how the ruler&amp;rsquo;s thickness varies):</description>
      <content:encoded><![CDATA[<p>When I started using my new M1 Max MacBook Pro in December, a bunch of <a href="https://github.com/abey79/vpype"><em>vpype</em></a>&rsquo;s tests started to fail. The failing tests were all image-based: an image is rendered and then compared to a previously-generated, reference image. This process is made easy thanks to <a href="https://github.com/abey79/vpype/blob/cd95e2da1940171e33ed0001255b763fa0d5f082/tests/conftest.py#L96">this Pytest fixture</a>.</p>
<p>In this case, the reference images were generated long ago on my previous, Intel/AMD-based MacBook Pro. This GIF highlights the discrepancy I&rsquo;d get with images generated on my new computer (notice how the ruler&rsquo;s thickness varies):</p>
<img src="/til-pixel-grid/m1_render.gif"alt="animated gif highlighting rendering discrepancy with horizontal and vertical lines"width="650px"style="display:block;margin-left:auto;margin-right:auto"/>
<p>As I&rsquo;m currently working on this viewer again, I finally spent two days tracking this issue – and finally found its cause.</p>
<p>Without giving it a thought, I first used integer coordinates for those ruler lines. However, coordinates refer to pixel <em>boundaries</em> – not pixel <em>centres</em>. This means than an horizontal line with integer coordinates (e.g. <code>[(2, 2), (7, 2)]</code>) sits halfway between two consecutive rows of pixel:</p>
<img src="/til-pixel-grid/pixel_grid.png"alt="schematic of a line not aligned with the pixel grid"width="500px"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Which of the 2nd or 3rd row of pixel eventually gets drawn is up to a coin toss – or rather the rounding strategy of your particular OpenGL driver/GPU/OS combination.</p>
<p>By offsetting the coordinates by half a pixel (e.g. <code>[(2, 2.5), (7, 2.5)]</code>), one can force the line on a specific pixel row and avoid any rounding:</p>
<img src="/til-pixel-grid/pixel_grid_aligned.png"alt="schematic of a line aligned with the pixel grid"width="500px"style="display:block;margin-left:auto;margin-right:auto"/>
<p>This makes the rendering more predictable across platforms.</p>
<p>Ultimately, the fix was very simple (I just changed the ruler thickness from <code>20</code> to <code>19.5</code>), but figuring it out was tricky (<a href="https://ptb.discord.com/channels/550302843777712148/811289127609827358/960561872774500412">relevant discussions</a> on <a href="https://github.com/moderngl/moderngl">ModernGL</a>&rsquo;s Discord server). Hopefully I wont forget about it after writing this TIL.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Annotated Release Notes: vpype 1.9</title>
      <link>https://bylr.info/articles/2022/03/03/annotated-release-notes-vpype-1.9/</link>
      <pubDate>Thu, 03 Mar 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/03/03/annotated-release-notes-vpype-1.9/</guid>
      <description>vpype \ text -l1 -p 0 3.5cm &#34;Custom layer name/color/pen width&#34; \ text -l2 -p 5cm 4.5cm -s 24 &#34;Properties&#34; \ text -l2 -p 2cm 5.5cm -s 20 &#34;Expressions&#34; \ text -l1 -p 6cm 6.5cm -s 24 &#34;Better/new block processors&#34; \ text -l2 -p 3cm 8cm &#34;...and much more!&#34; \ layout -m 0.3cm -l 10x3.5cm \ penwidth -l2 0.5mm \ color -l2 &#34;%Color(226,200,0)%&#34; \ color -l1 &#34;%Color(3,118,207)%&#34; \ color -l1 blue \ show -- vpype 1.</description>
      <content:encoded><![CDATA[<!--
<img src="/vpype_19/banner_ua.png"width="60%"style="display:block;margin-left:auto;margin-right:auto"/>

vpype \
text -l1 -p 0 3.5cm "Custom layer name/color/pen width" \
text -l2 -p 5cm 4.5cm -s 24 "Properties" \
text -l2 -p 2cm 5.5cm -s 20 "Expressions" \
text -l1 -p 6cm 6.5cm -s 24 "Better/new block processors" \
text -l2 -p 3cm 8cm "...and much more!" \
layout -m 0.3cm -l 10x3.5cm \
penwidth -l2 0.5mm \
color -l2 "%Color(226,200,0)%" \
color -l1 "%Color(3,118,207)%" \
color -l1 blue \
show
-->
<p><a href="https://github.com/abey79/vpype"><em>vpype</em></a> 1.9 is finally out! 🎉</p>
<p>I recently stumbled upon a <a href="https://simonwillison.net/2022/Jan/31/release-notes/">post</a> by <a href="https://twitter.com/simonw">Simon Willison</a> where he promotes the idea of <em>annotated release notes</em>. As it turns out, this release is, by any metric I can think of, the biggest and most transformative so far. The associated <a href="https://github.com/abey79/vpype/blob/master/CHANGELOG.md#19">change log</a> is consequently rather unwieldy and calls, you guessed it 💡, for the present annotated release notes.</p>
<p>(Note: although the <a href="https://github.com/abey79/vpype/blob/master/CHANGELOG.md#19">original release notes</a> are extensively quoted in this article, I reshuffled and shortened the original material. Make sure to check the base material for an authoritative list of change.)</p>
<h2 id="properties">Properties</h2>
<h3 id="basics">Basics</h3>

<blockquote class="extract">
  <p><ul>
<li>
<p>Added support for global and per-layer <a href="(https://vpype.readthedocs.io/en/latest/fundamentals.html#properties)">properties</a> (#359)</p>
<p>This feature introduces metadata to the pipeline in the form of properties which may either be attached to specific layers (layer property) or all of them (global property). Properties are identified by a name and may be of arbitrary type (e.g. integer, floating point, color, etc.). A number of <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#system-properties">system properties</a> with a specific name (prefixed with <code>vp_</code>) and type are introduced to support some of the new features.</p>
</li>
</ul>
</p>
</blockquote>

<p>Metadata is data which says something about other data, and <em>vpype</em> lacked such a thing. Until now, what was passed from one command to the next consisted <em>exclusively</em> of paths sorted into layers, without any context such as what the color of these paths might be. One command could &ldquo;know&rdquo; about something (e.g. <code>read</code> knows, from the SVG, the color of a layer), but it could not &ldquo;tell&rdquo; the next command(s) about it.</p>
<p>This is no more, thanks to properties. They offer a generic mechanism to attach data to pipeline and layers. They are the backbone of several features introduced today, and lay the ground for future features within <em>vpype</em> or in plug-ins.</p>
<h3 id="layer-color-pen-width-and-name">Layer color, pen width, and name</h3>

<blockquote class="extract">
  <p><ul>
<li>Layer color, pen width, and name are now customizable (#359, #376, #389)
<ul>
<li>The <code>read</code> commands now sets layer color, pen width, and name based on the input SVG if possible.</li>
<li>The new <code>color</code>, <code>penwdith</code>, and <code>name</code> commands can be used to modify layer color, pen width, and name.</li>
<li>The new <code>pens</code> command can apply a predefined or custom scheme on multiple layers at once. Two common schemes are built-in: <code>rgb</code> and <code>cmyk</code>. <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#creating-a-custom-pen-configuration">Custom schemes</a> can be defined in the configuration file.</li>
<li>The <code>show</code> and <code>write</code> commands now take into account these layer properties.</li>
</ul>
</li>
</ul>
</p>
</blockquote>

<p>Supporting arbitrary layer colors, pen widths, and names, has long been amongst the most requested features. Well, thanks to properties, here they are. It happens automagically when using <code>read</code>, and the new commands can further customise these values:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ vpype <span style="color:#d14">\
</span><span style="color:#d14"></span>    rect --layer <span style="color:#099">1</span> <span style="color:#099">0</span> <span style="color:#099">0</span> 5cm 5cm <span style="color:#d14">\
</span><span style="color:#d14"></span>    color --layer <span style="color:#099">1</span> purple <span style="color:#d14">\
</span><span style="color:#d14"></span>    penwidth --layer <span style="color:#099">1</span> 0.5mm <span style="color:#d14">\
</span><span style="color:#d14"></span>    circle --layer <span style="color:#099">2</span> 4cm 4cm 2cm <span style="color:#d14">\
</span><span style="color:#d14"></span>    color --layer <span style="color:#099">2</span> orange <span style="color:#d14">\
</span><span style="color:#d14"></span>    penwidth --layer <span style="color:#099">2</span> 5mm <span style="color:#d14">\
</span><span style="color:#d14"></span>    show
</code></pre></div><img src="/vpype_19/color_penwidth.png"width="40%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>The new, high-level <code>color</code>, <code>penwidth</code>, and <code>name</code> commands are simple wrappers which change the value of specific <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#system-properties">system properties</a> (i.e. <code>vp_color</code>, <code>vp_pen_width</code>, resp. <code>vp_name</code>):</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-console" data-lang="console"><span style="color:#555">$ </span>vpype random name <span style="color:#d14">&#34;hello&#34;</span> color purple penwidth 0.1mm proplist --layer <span style="color:#099">1</span>
<span style="color:#888">listing 3 properties for layer 1
</span><span style="color:#888">  vp_color: (color) #800080
</span><span style="color:#888">  vp_name: (str) hello
</span><span style="color:#888">  vp_pen_width: (float) 0.37795275590551186
</span></code></pre></div><p>System properties differ from &ldquo;regular&rdquo; properties only in the sense that they have special meaning to <em>vpype</em>. By convention, their name is prefixed with <code>vp_</code>.</p>
<p>Special mention for the new <code>pens</code> command, which is short for <em>here is the set of pens I intend to use for this plot</em>. It sets in bulk layer colors, pen widths and/or names all at once, based on a built-in or <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#creating-a-custom-pen-configuration">custom</a> configuration. For example, this produces a CMYK SVG using the <a href="https://github.com/serycjon/vpype-flow-imager">flow imager</a> plug-in:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ vpype <span style="color:#d14">\
</span><span style="color:#d14"></span>    flow_img <span style="color:#000;font-weight:bold">[</span>...<span style="color:#000;font-weight:bold">]</span> --cmyk input.jpg <span style="color:#d14">\
</span><span style="color:#d14"></span>    pens cmyk <span style="color:#d14">\
</span><span style="color:#d14"></span>    write output.svg
</code></pre></div>
<blockquote class="extract">
  <p><ul>
<li>
<p>Introduced new commands for low-level inspection and modification of properties (#359)</p>
<ul>
<li><code>propget</code>: gets the value of a given global or layer property</li>
<li><code>proplist</code>: lists all global and/or layer properties and their value</li>
<li><code>propset</code>: sets the value of a given global or layer property</li>
<li><code>propdel</code>: deletes a given global or layer property</li>
<li><code>propclear</code>: removes all global and/or layer properties</li>
</ul>
</li>
</ul>
</p>
</blockquote>

<p>These are low-level commands to interact with properties. Although they have limited use in real-world workflows, they come in handy when learning about properties or crafting complex pipelines.</p>

<blockquote class="extract">
  <p><ul>
<li>
<p>Updated layer operation commands to handle properties (#359)</p>
<ul>
<li>When a single source layer is specified and <code>--prob</code> is not used, the <code>lcopy</code> and <code>lmove</code> commands now copy the source layer&rsquo;s properties to the destination layer (possibly overwriting existing properties).</li>
<li>When <code>--prob</code> is not used, the <code>lswap</code> command now swaps the layer properties as well.</li>
<li>These behaviors can be disabled with the <code>--no-prop</code> option.</li>
</ul>
</li>
</ul>
</p>
</blockquote>

<p>With properties, some of the layer manipulation commands became somewhat ambiguous. For example, what happens with properties when using <code>lmove all 1</code> (merges all layers into layer one) or <code>move --prob 0.5 1 2</code> (picks geometries from layer 1 with a 50% probability and moves them to layer 2)?</p>
<p>I opted for a strategy where layer properties are affected <em>only</em> for unambiguous cases. This is basically when a single layer is moved/copied and when probabilistic behaviour is not used at all. In all other cases, the layer properties are left unchanged.</p>
<h3 id="from-svg-attributes-to-properties">From SVG attributes to properties</h3>

<blockquote class="extract">
  <p><ul>
<li>
<p>Added the <code>--attr</code> option to the <code>read</code> command to (optionally) sort geometries by attributes (e.g. stroke color, stroke width, etc.) instead of by SVG layer (#378, #389)</p>
</li>
<li>
<p>The <code>read</code> and <code>write</code> commands now preserve a sub-set of SVG attributes (experimental) (#359, #389)</p>
<p>The <code>read</code> command identifies SVG attributes (e.g. <code>stroke-dasharray</code>) which are common in all geometries within each layer. These attributes are saved as layer properties with their name prefixed with <code>svg_</code> (e.g. <code>svg_stroke-dasharray</code>). The <code>write</code> command can optionally restore these attributes in the output SVG using the <code>--restore-attribs</code> option.</p>
</li>
</ul>
</p>
</blockquote>

<p>As noted, the <code>read</code> command now tries to extract SVG attributes and store them as layer properties. There are two motivations for that. First, it enables the <code>write</code> command to optionally restore these attributes in the output file, in order to achieve a higher degree of fidelity. (This feature is experimental and opt-in for the time being.) Second, it enables future features or plug-ins to do neat things such as generating hatch fills when <code>fill</code> is set to a color, or cutting paths in bits to emulate <code>stroke-dasharray</code> if defined.</p>
<p>Now, since properties are only available at the layer level (or globally), <code>read</code> discards SVG attributes that are not shared amongst <em>every</em> paths within a given layer. Let&rsquo;s take an example:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-svg" data-lang="svg"><span style="color:#000080">&lt;svg</span> <span style="color:#008080">xmlns=</span><span style="color:#d14">&#34;http://www.w3.org/2000/svg&#34;</span> <span style="color:#008080">width=</span><span style="color:#d14">&#34;650&#34;</span> <span style="color:#008080">height=</span><span style="color:#d14">&#34;650&#34;</span><span style="color:#000080">&gt;</span>
    <span style="color:#000080">&lt;circle</span> <span style="color:#008080">cx=</span><span style="color:#d14">&#34;150&#34;</span> <span style="color:#008080">cy=</span><span style="color:#d14">&#34;150&#34;</span> <span style="color:#008080">r=</span><span style="color:#d14">&#34;100&#34;</span> <span style="color:#008080">stroke=</span><span style="color:#d14">&#34;red&#34;</span> <span style="color:#008080">stroke-width=</span><span style="color:#d14">&#34;0.5mm&#34;</span> <span style="color:#008080">fill=</span><span style="color:#d14">&#34;green&#34;</span> <span style="color:#000080">/&gt;</span>
    <span style="color:#000080">&lt;rect</span> <span style="color:#008080">x=</span><span style="color:#d14">&#34;400&#34;</span> <span style="color:#008080">y=</span><span style="color:#d14">&#34;200&#34;</span> <span style="color:#008080">width=</span><span style="color:#d14">&#34;200&#34;</span> <span style="color:#008080">height=</span><span style="color:#d14">&#34;400&#34;</span> <span style="color:#008080">stroke=</span><span style="color:#d14">&#34;blue&#34;</span> <span style="color:#008080">stroke-width=</span><span style="color:#d14">&#34;0.5mm&#34;</span> <span style="color:#008080">fill=</span><span style="color:#d14">&#34;green&#34;</span> <span style="color:#000080">/&gt;</span>
    <span style="color:#000080">&lt;path</span> <span style="color:#008080">d=</span><span style="color:#d14">&#34;M250,600 l-200,0 l0,-200 z&#34;</span> <span style="color:#008080">stroke=</span><span style="color:#d14">&#34;blue&#34;</span> <span style="color:#008080">stroke-width=</span><span style="color:#d14">&#34;0.1mm&#34;</span> <span style="color:#008080">fill=</span><span style="color:#d14">&#34;green&#34;</span> <span style="color:#000080">/&gt;</span>
<span style="color:#000080">&lt;/svg&gt;</span>
</code></pre></div><p>This SVG only contains top-level elements, which <em>vpype</em> loads in layer 1 by default. The <code>fill</code> property is common to all paths and thus stored as a layer property, but the <code>stroke</code> and <code>stroke-width</code> attributes are heterogeneous and thus discarded. As a result, the <code>show</code> command uses the default color and pen width.</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-console" data-lang="console"><span style="color:#555">$ </span>vpype <span style="color:#0086b3">read</span> example.svg proplist --layer <span style="color:#099">1</span> show
<span style="color:#888">listing 1 properties for layer 1
</span><span style="color:#888">  svg_fill: (str) green
</span></code></pre></div><img src="/vpype_19/read_example.png"width="40%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>To address this issue, the <code>read</code> command has now the option to create layers based on SVG attributes instead of structure:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-console" data-lang="console"><span style="color:#555">$ </span>vpype <span style="color:#0086b3">read</span> --attr stroke --attr stroke-width example.svg proplist --layer all show
<span style="color:#888">listing 5 properties for layer 1
</span><span style="color:#888">  svg_fill: (str) green
</span><span style="color:#888">  svg_stroke: (str) red
</span><span style="color:#888">  svg_stroke-width: (str) 0.5mm
</span><span style="color:#888">  vp_color: (color) #ff0000
</span><span style="color:#888">  vp_pen_width: (float) 1.8897648
</span><span style="color:#888">listing 5 properties for layer 2
</span><span style="color:#888">  svg_fill: (str) green
</span><span style="color:#888">  svg_stroke: (str) blue
</span><span style="color:#888">  svg_stroke-width: (str) 0.5mm
</span><span style="color:#888">  vp_color: (color) #0000ff
</span><span style="color:#888">  vp_pen_width: (float) 1.8897648
</span><span style="color:#888">listing 5 properties for layer 3
</span><span style="color:#888">  svg_fill: (str) green
</span><span style="color:#888">  svg_stroke: (str) blue
</span><span style="color:#888">  svg_stroke-width: (str) 0.1mm
</span><span style="color:#888">  vp_color: (color) #0000ff
</span><span style="color:#888">  vp_pen_width: (float) 0.37795296000000006
</span></code></pre></div><img src="/vpype_19/read_attr_example.png"width="40%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>In this case, <code>read</code> creates one layer per unique combination of <code>stroke</code> and <code>stroke-width</code> attribute, resulting in a total of three layers, each assigned with the correct properties, and correctly displayed by <code>show</code>.</p>
<h3 id="source-files">Source files</h3>

<blockquote class="extract">
  <p><ul>
<li>The <code>read</code> command now records the source SVG paths in the <code>vp_source</code> and <code>vp_sources</code> system properties (see the <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#system-properties">documentation</a>) (#397, #406, #408)</li>
</ul>
</p>
</blockquote>

<p>The idea of the <code>vp_source</code> and <code>vp_sources</code> properties is to keep track of the files from which the content of the pipeline originates from. The <code>vp_source</code> property is a single path, which is overwritten by the last command importing from a file. The <code>vp_sources</code> property is a <a href="https://docs.python.org/3/tutorial/datastructures.html#sets">set</a> of <em>all</em> source files encountered so far. Both properties are <a href="https://docs.python.org/3/library/pathlib.html"><code>pathlib.Path</code></a> instances.</p>
<p>This is, for example, what happens when using <code>read</code> twice:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-console" data-lang="console"><span style="color:#555">$ </span>vpype <span style="color:#0086b3">read</span> machine_typography_01_3.svg <span style="color:#0086b3">read</span> machine_typography_02_3.svg proplist -g
<span style="color:#888">listing 5 global properties
</span><span style="color:#888">  svg_fill: (str) black
</span><span style="color:#888">  svg_stroke: (str) none
</span><span style="color:#888">  vp_page_size: (tuple) (396.850608, 559.3703808000001)
</span><span style="color:#888">  vp_source: (PosixPath) /private/tmp/MT/machine_typography_02_3.svg
</span><span style="color:#888">  vp_sources: (set) {PosixPath(&#39;/private/tmp/MT/machine_typography_01_3.svg&#39;), PosixPath(&#39;/private/tmp/MT/machine_typography_02_3.svg&#39;)}
</span></code></pre></div><p>Here, <code>vp_source</code> points to the file read by the last <code>read</code> command, but <code>vp_sources</code> contains all two source files.</p>
<p>Currently, <code>read</code> is the only command which sets these variables, but the idea is that any command involved with reading a file (SVG or otherwise) should set these properties, including plug-ins such as <a href="https://github.com/plottertools/hatched">hatched</a>, <a href="https://github.com/serycjon/vpype-flow-imager">flow imager</a>, or <a href="https://github.com/EmbroidePy/vpype-embroidery">vpype-embroidery</a>.</p>
<p>One of the most common use case is to name the output file after the input file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-console" data-lang="console"><span style="color:#555">$ </span>vpype flow_img <span style="color:#000;font-weight:bold">[</span>...<span style="color:#000;font-weight:bold">]</span> my_image.png write <span style="color:#d14">&#34;{vp_name.stem}_converted.svg&#34;</span>
</code></pre></div><p>Note the use of a property substitution pattern, which brings us to the next topic.</p>
<h3 id="property-substitution">Property substitution</h3>

<blockquote class="extract">
  <p><ul>
<li>
<p>Added <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#property-substitution">property substitution</a> to CLI user input (#395)</p>
<p>The input provided to most commands' arguments and options may now contain substitution patterns which will be replaced by the corresponding property value. Property substitution patterns are marked with curly braces (e.g. <code>{property_name}</code>) and support the same formatting capabilities as the Python&rsquo;s <a href="https://docs.python.org/3/library/string.html#formatstrings"><code>format()</code> function</a>.</p>
</li>
</ul>
</p>
</blockquote>

<p>This is where things start becoming &ldquo;meta&rdquo;! 🤯</p>
<p>As shown in the previous example, the value of a property may now be used anywhere as input using property substitution patterns.</p>
<p>Here is another example where the full path of the input file is drawn and displayed:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-console" data-lang="console"><span style="color:#555">$ </span>vpype <span style="color:#0086b3">read</span> example.svg text -p 0.5cm 0.5cm <span style="color:#d14">&#34;{vp_source}&#34;</span> show
</code></pre></div><img src="/vpype_19/prop_subst.png"width="40%"style="display:block;margin-left:auto;margin-right:auto"/>
<p>Again, <code>vp_source</code> is a <a href="https://docs.python.org/3/library/pathlib.html"><code>pathlib.Path</code></a> instance, so <code>{vp_source.name}</code> (file name) or <code>{vp_source.stem}</code> (file name without extension) could be used instead.</p>
<p>Multiple substitution patterns can be combined and mixed with regular text. For example, this creates an output file in the same directory as, and named after, the input file:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ vpype <span style="color:#0086b3">read</span> example.svg linesort <span style="color:#d14">\
</span><span style="color:#d14"></span>    write <span style="color:#d14">&#34;{vp_source.parent}/{vp_source.stem}_optimised.svg&#34;</span>
</code></pre></div><p>Of course, when using <em>vpype</em> interactively, it&rsquo;s easier to simply spell out the output file name. Instead, this kind of mechanism makes it considerably easier to write generic, reusable <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#wrapping-a-vpype-pipeline-in-a-shell-script">shell scripts</a>.</p>
<p>Note that, since property substitution internally relies on Python&rsquo;s <a href="https://docs.python.org/3/library/string.html#formatstrings"><code>str.format()</code></a> function, the number formatting mini-language is available as well (e.g. <code>{vp_pen_width:.02f}</code>). See the <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#property-substitution">documentation</a> for some examples.</p>
<p>Now, taking a step back, this feature is neat indeed, but its usefulness turns out to be limited in many non-trivial, real-world scenarios. I had hoped it would unlock several workflows I had in mind, but it just did not - or not elegantly enough. So much so that I even considered dropping the feature altogether.</p>
<p>This was a bit frustrating, to say the least. And ultimately lead to what is the next big chapter of this release.</p>
<h2 id="expressions">Expressions</h2>

<blockquote class="extract">
  <p><ul>
<li>
<p>Added <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#expression-substitution">expression substitution</a> to CLI user input (#397)</p>
<p>The input provided to most command&rsquo;s arguments and options may now contain expression patterns which are evaluated before the command is executed. Expression patterns are marked with the percent symbol <code>%</code> (e.g. <code>%3+4%</code>) and support a large subset of the Python language. <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#load-multiple-files-merging-their-layers-by-name">A</a> <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#cropping-and-framing-geometries">lot</a> <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#laying-out-multiple-svgs-on-a-grid">of</a> <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#create-interactive-scripts-with-input">examples</a> were added in the <a href="https://vpype.readthedocs.io/en/latest/cookbook.html">cookbook</a>.</p>
</li>
</ul>
</p>
</blockquote>

<p>This is possibly the most transformative feature brought to <em>vpype</em> since its inception: anything between pairs of <code>%</code> characters is now evaluated as (a sub-set of) Python code, and the result is substituted in the input before it reaches the actual command. The documentation has been updated with a whole new <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#expression-substitution">section</a> about expressions (which I&rsquo;m not going to repeat here), and the <a href="https://vpype.readthedocs.io/en/latest/cookbook.html">cookbook</a> has plenty of examples making use of them. Do check them out for a taste of what expressions are capable of!</p>
<p>This feature blurs the lines between a mere CLI tool and a programming language. This begs the question of why not using a programming language in the first place, a point <a href="https://ptb.discord.com/channels/499297341472505858/748589023731122277/939168445759631411">raised</a> by fellow Python dev and <a href="https://github.com/serycjon/vpype-flow-imager">flow imager</a> author <a href="https://github.com/serycjon">Jonas Serych</a>. <em>vpype</em> even offers a proper <a href="https://vpype.readthedocs.io/en/latest/api.html">API</a> for that!</p>
<p>Here are my thoughts about this:</p>
<ul>
<li>Users of <em>vpype</em> are often not Python developers &ndash; or developers at all. Expressions build on existing <em>vpype</em> knowledge and bring, at least through examples and recipes that can be copy/pasted/customized, tiny bits of programs which are readily useful, without the need to learn much of the Python syntax and ecosystem.</li>
<li>For many real-world cases (see the examples linked in the release notes), the resulting one-liners are more compact than the equivalent in proper code - even Python, even with <em>vpype</em> API. (Arbitrarily complex pipelines can of course be conceivably crafted as counter-examples, but this is besides the point.)</li>
</ul>

<blockquote class="extract">
  <p><ul>
<li>Added the <code>eval</code> command as placeholder for executing expressions (#397)</li>
</ul>
</p>
</blockquote>

<p>Though expressions can be used in any command&rsquo;s input, some &ldquo;space&rdquo; dedicated to them in the pipeline can be useful. Typical cases include variable initialization or <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#create-interactive-scripts-with-input">querying the user</a> for some parameter with the <code>input()</code> function. Several examples shown or linked below make use of this.</p>
<h2 id="block-processors">Block processors</h2>

<blockquote class="extract">
  <p><ul>
<li>
<p>Improved block processors (#395, #397)</p>
<ul>
<li>Simplified and improved the infrastructure underlying block processors for better extensibility.</li>
<li>The <code>begin</code> marker is now optional and implied whenever a block processor command is encountered. <em>Note</em>: the <code>end</code> marker must always be used to mark the end of a block.</li>
<li>Commands inside the block now have access to the current layer structure and its metadata.</li>
</ul>
</li>
</ul>
</p>
</blockquote>

<p>Block processors hardly got any love since the first release of <em>vpype</em> and, as far as I can tell, weren&rsquo;t used much - if at all - due to their limitations. Properties and expressions completely reverse this situation and block processors are now where the magic happens. The changes above lay the ground work for this.</p>

<blockquote class="extract">
  <p><ul>
<li>
<p>Improved the <code>grid</code> block processor (#397)</p>
<ul>
<li>The page size is now updated according to the grid size.</li>
<li>The command now sets expression variables for use in the nested pipeline.</li>
<li>Cells are now first iterated along rows instead of columns.</li>
</ul>
</li>
<li>
<p>The <code>repeat</code> block processor now sets expression variables for use in the nested pipeline (#397)</p>
</li>
<li>
<p>Added <code>forfile</code> block processor to iterate over a list of file (#397)</p>
</li>
<li>
<p>Added <code>forlayer</code> block processor to iterate over the existing layers (#397)</p>
</li>
<li>
<p>The <code>read</code> command now will ignore a missing file if <code>--no-fail</code> parameter is used (#397)</p>
</li>
<li>
<p>Changed the initial default target layer to 1 (#395)</p>
<p>Previously, the first generator command of the pipeline would default to create a new layer if the <code>--layer</code> option was not provided. This could lead to unexpected behaviour in several situation. The target layer is now layer 1. For subsequent generators, the existing behaviour of using the previous generator target layer as default remains.</p>
</li>
</ul>
</p>
</blockquote>

<p>That&rsquo;s two new block processor commands, and another two finally coming to life, plus a few changes to make them work better with real-world workflows.</p>
<p>One of the key changes is that block processors now set temporary expression variables (prefixed with <code>_</code>) that can be used in the nested pipeline. They are listed in each command&rsquo;s help text:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-console" data-lang="console"><span style="color:#555">$ </span>vpype grid --help
<span style="color:#888">Usage: vpype grid [OPTIONS] NX NY
</span><span style="color:#888"></span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span><span style="color:#888">  Creates a NX by NY grid of geometry
</span><span style="color:#888"></span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span><span style="color:#888">  [...]
</span><span style="color:#888"></span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span><span style="color:#888">  The following variables are set by `grid` and available for expressions:
</span><span style="color:#888"></span><span style="color:#a61717;background-color:#e3d2d2">
</span><span style="color:#a61717;background-color:#e3d2d2"></span><span style="color:#888">      _nx: number of columns (NX)
</span><span style="color:#888">      _ny: number of rows (NY)
</span><span style="color:#888">      _n: total number of cells (NX*NY)
</span><span style="color:#888">      _x: current column (0 to NX-1)
</span><span style="color:#888">      _y: current row (0 to NY-1)
</span><span style="color:#888">      _i: current cell (0 to _n-1)
</span><span style="color:#888">  
</span><span style="color:#888">  [...]
</span></code></pre></div><p>Another novelty is the introduction of two new block processor commands:</p>
<ul>
<li>The <code>forfile</code> command accepts a pathname pattern (e.g. <code>*.svg</code>) and executes the nested pipeline for each of the paths it expends into. It makes things like <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#batch-processing-multiple-svgs-with-forfile">batch processing files</a>, <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#merge-multiple-svgs-into-a-multilayer-file">merging multiple SVGs into a multilayer file</a>, or <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#laying-out-multiple-svgs-on-a-grid">laying out multiple files on a grid</a> a breeze.</li>
<li>The <code>forlayer</code> command executes the nested pipeline for each of the exising layers, which is useful, e.g., to <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#saving-each-layer-as-a-separate-file">export one file per layer</a>.</li>
</ul>
<p>Checks the related <a href="https://vpype.readthedocs.io/en/latest/fundamentals.html#blocks">documentation</a> for more details.</p>
<p>This example, taken from the <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#laying-out-multiple-svgs-on-a-grid">grid layout recipe</a>, demonstrates best what <em>vpype</em> 1.9 is about:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ vpype <span style="color:#d14">\
</span><span style="color:#d14"></span>   <span style="color:#0086b3">eval</span> <span style="color:#d14">&#34;files=glob(&#39;*.svg&#39;)&#34;</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>   <span style="color:#0086b3">eval</span> <span style="color:#d14">&#34;cols=6; rows=ceil(len(files)/cols)&#34;</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>   <span style="color:#0086b3">eval</span> <span style="color:#d14">&#34;names={};n=100&#34;</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>   grid -o 10cm 15cm <span style="color:#d14">&#34;%cols%&#34;</span> <span style="color:#d14">&#34;%rows%&#34;</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>       <span style="color:#0086b3">read</span> --no-fail <span style="color:#d14">&#34;%files[_i] if _i &lt; len(files) else &#39;&#39;%&#34;</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>       layout -m 0.5cm 10x15cm <span style="color:#d14">\
</span><span style="color:#d14"></span>       forlayer <span style="color:#d14">\
</span><span style="color:#d14"></span>           <span style="color:#0086b3">eval</span> <span style="color:#d14">&#34;%if _name not in names: names[_name] = n; n = n+1%&#34;</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>           lmove %_lid% <span style="color:#d14">&#34;%names[_name]%&#34;</span> <span style="color:#d14">\
</span><span style="color:#d14"></span>       end <span style="color:#d14">\
</span><span style="color:#d14"></span>   end <span style="color:#d14">\
</span><span style="color:#d14"></span>   write combined.svg
</code></pre></div><p>It creates a grid layout from multiple SVG files, combining layers using their name (e.g. all &ldquo;yellow&rdquo; layers in input files are merged in a single &ldquo;yellow&rdquo; layer in the output file). Check the <a href="https://vpype.readthedocs.io/en/latest/cookbook.html#laying-out-multiple-svgs-on-a-grid">recipe</a> for a detailed explanation.</p>
<p>This pipeline has it all:</p>
<ul>
<li>extensive use of expressions,</li>
<li>two nested blocks, using their expression variables (prefixed with <code>_</code>),</li>
<li>use of properties (via the <code>_name</code> variable set by <code>forlayer</code>, which contains the current layer&rsquo;s <code>vp_name</code> property).</li>
</ul>
<p>Here is how it looks when run on my <a href="https://github.com/abey79/sketches#machine_typography">Machine Typography</a> #ptpx project:</p>
<img src="/vpype_19/machine_typography.png"width="60%"style="display:block;margin-left:auto;margin-right:auto"/>
<h2 id="other-changes">Other changes</h2>

<blockquote class="extract">
  <p><strong>Note</strong>: This is the last version of <em>vpype</em> to support Python 3.7.</p>
</blockquote>

<p>It&rsquo;s the year of the <a href="https://realpython.com/python-walrus-operator/">walrus</a> for <em>vpype</em>! 🦭</p>

<blockquote class="extract">
  <p><ul>
<li>Added <code>pagerotate</code> command, to rotate the page layout (including geometries) by 90 degrees (#404)</li>
</ul>
</p>
</blockquote>

<p>This command is useful for plotters without native support for both portrait and landscape orientations. Your plotter support only, say, landscape orientation and you want to plot a portrait-oriented file? This pipeline does the trick:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-console" data-lang="console"><span style="color:#555">$ </span>vpype <span style="color:#0086b3">read</span> portrait_input.svg pagerotate wrote landscape_output.svg
</code></pre></div>
<blockquote class="extract">
  <p><ul>
<li>Added <code>--keep</code> option to the <code>ldelete</code> command (to delete all layers but those specified) (#383)</li>
</ul>
</p>
</blockquote>

<p>There was formerly no way to delete all layers but one. The new <code>--keep</code> option fills this gap.</p>

<blockquote class="extract">
  <p><ul>
<li>Pinned poetry-core to 1.0.8 to enable editable installs (#410)</li>
</ul>
</p>
</blockquote>

<p><a href="https://python-poetry.org">Poetry</a> finally <a href="https://github.com/python-poetry/poetry/issues/34">supports</a> <a href="https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs">editable installs</a> thanks to <a href="https://www.python.org/dev/peps/pep-0660/">PEP 660</a> 🎉</p>
<p>This change is relevant when developing jointly on <em>vpype</em> and another project that depends on it (e.g. <a href="https://github.com/abey79/vsketch"><em>vsketch</em></a>, or some plug-in). In such cases, <em>vpype</em> can now be installed in editable mode from a local checkout. Modifications made to it will immediately be available to the dependent project:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-console" data-lang="console"><span style="color:#555">$ </span><span style="color:#0086b3">cd</span> my-plugin
<span style="color:#555">$ </span><span style="color:#0086b3">source</span> venv/bin/activate
<span style="color:#555">$ </span>git clone https://github.com/abey79/vpype ../vpype
<span style="color:#555">$ </span>pip install -e ../vpype
</code></pre></div>
<blockquote class="extract">
  <p><ul>
<li>Fixed an issue with the <code>random</code> command when using non-square area (#395)</li>
</ul>
</p>
</blockquote>

<p>That&rsquo;s a two-year-old bug I can&rsquo;t believe I hadn&rsquo;t seen before 🙄 (I use <code>random</code> <em>a lot</em> when testing out stuff during development.)</p>

<blockquote class="extract">
  <p><ul>
<li>Renamed the bundled config file to <code>vpype_config.toml</code> (#359)</li>
</ul>
</p>
</blockquote>

<p>This is the config file bundled with <em>vpype</em>. It used to be called <code>hpgl_devices.toml</code>, but now it also contains the build-in configurations of the new <code>pens</code> command (<code>cmyk</code> and <code>rgb</code>). The old name didn&rsquo;t make sense anymore.</p>

<blockquote class="extract">
  <p><p>API changes:</p>
<ul>
<li>Moved all CLI-related APIs from <code>vpype</code> to <code>vpype_cli</code> (#388)</li>
<li>Updated the block processor API (breaking change) (#395)</li>
<li>&hellip;</li>
</ul>
</p>
</blockquote>

<p>This release comes with scores of changes at the API level (to many to list here). Two of these changes deserve a note though.</p>
<p>First, a fair amount of infrastructure used by the <em>vpype</em> CLI (e.g. the <code>@generator</code> decorator and friends) used to reside in the <code>vpype</code> package instead of <code>vpype_cli</code>. This is not ideal for many reasons and I&rsquo;m moving away from it. <code>vpype</code> should be a &ldquo;pure&rdquo; library, whereas <code>vpype_cli</code> should contain everything needed for its CLI (and for plug-ins). These are not yet breaking changes but will generate deprecation warnings with most plug-ins. I will ensure that they are fixed ASAP.</p>
<p>Secondly, as part of the block processor overhaul, the <code>@block_processor</code> decorator had breaking changes without backward-compatible deprecation. I am not aware of any third-party code actually using it, so this shouldn&rsquo;t cause any issue.</p>
<h2 id="whats-next">What&rsquo;s next?</h2>
<p>Congrats if you got this far! 😲🏆</p>
<p>I hope you&rsquo;ll enjoy <em>vpype</em> 1.9 as much as I sweated preparing it. 😅</p>
<p>Feedback is welcome, via <a href="https://github.com/abey79/vpype/discussions">discussions</a> for support/suggestions or <a href="https://github.com/abey79/vpype/issues">issues</a> for bugs. As always, I hang out on the <a href="https://drawingbots.net">drawingbots.net</a>&rsquo;s <a href="https://discord.com/invite/XHP3dBg">Discord server</a> and am available for a chat.</p>
<p>Contributions are most welcome too, and the documentation is one area where help is always beneficial. I&rsquo;ve gathered a few ideas of what can be done <a href="https://github.com/abey79/vpype/issues/400">here</a>.</p>
<p>To conclude, here are my probable areas of focus for the coming weeks/months:</p>
<ul>
<li>Given the scope of this release, I&rsquo;m expecting to deal with increased user support and a few kirks and bugs to address. I&rsquo;ll be available for this in the short term.</li>
<li>Property-related stuff must be ported to <a href="https://github.com/abey79/vsketch"><em>vsketch</em></a>, including a nice API to set layer name, color, pen-width, etc.</li>
<li>The next big topic for <em>vpype</em> is its UX. At least, I want to improve the look and usability of the integrated help (using the brand new <a href="https://github.com/ewels/rich-click">rich-click</a> project), and add some visual feedback during execution.</li>
<li>With support for Python 3.7 dropped, compatibility with Python 3.10 is now on the menu.</li>
<li>Whatever user feedback might steer my attention to 😉</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>First post</title>
      <link>https://bylr.info/articles/2022/02/20/first-post/</link>
      <pubDate>Sun, 20 Feb 2022 00:00:00 +0000</pubDate>
      
      <guid>https://bylr.info/articles/2022/02/20/first-post/</guid>
      <description>&lt;p&gt;Hello again, world.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Hello again, world.</p>
<p>Lately, I&rsquo;ve been meaning to document some of my personal projects and endeavours, and realised that I lacked a suitable outlet. I figured this would be a great occasion to test <a href="https://gohugo.io">Hugo</a> and <a href="https://pages.github.com">GitHub Pages</a>, so here we are!</p>
<p>This is what might ramble about in the future:</p>
<ul>
<li>Stuff about <a href="https://github.com/abey79/vpype">vpype</a>, <a href="https://github.com/abey79/vsketch">vsketch</a>, and my other plotter-related software projects.</li>
<li>Logs for some of my past and future projects (plotter-related or otherwise).</li>
<li>Posts related to <a href="https://www.home-assistant.io">Home Assistant</a> or other home-automation topics.</li>
<li>Who knows what else.</li>
</ul>
<p>Follow me on <a href="https://twitter.com/abey79">Twitter</a> for updates.</p>]]></content:encoded>
    </item>
    
    <item>
      <title>About</title>
      <link>https://bylr.info/about/</link>
      <pubDate>Thu, 10 Feb 2022 20:21:40 +0100</pubDate>
      
      <guid>https://bylr.info/about/</guid>
      <description>My name is Antoine Beyeler. I am a multidisciplinary engineer and entrepreneur with a PhD in flying robotics and interests in software, aviation, photography, generative art, plotters, and home automation. I live in Lausanne, Switzerland 🇨🇭.
I recently joined Rerun.io as software engineer to learn Rust 🦀 while developing visualisation frameworks and products for computer vision and robotics practitioners.
Earlier, with a few friends, I co-founded senseFly (now named AgEagle), which soon became the leader of fixed-wing mapping drones for surveyors, farmers, humanitarians and many others.</description>
      <content:encoded><![CDATA[<p>My name is <strong>Antoine Beyeler</strong>. I am a multidisciplinary engineer and entrepreneur with a PhD in flying robotics and interests in software, aviation, photography, generative art, plotters, and home automation. I live in Lausanne, Switzerland 🇨🇭.</p>
<p>I recently joined <a href="https://www.rerun.io">Rerun.io</a> as software engineer to learn Rust 🦀 while developing visualisation frameworks and products for computer vision and robotics practitioners.</p>
<p>Earlier, with a few friends, I co-founded <a href="https://www.sensefly.com">senseFly</a> (now named <a href="https://ageagle.com">AgEagle</a>), which soon became the leader of fixed-wing mapping drones for surveyors, farmers, humanitarians and many others. Until my departure in 2018, I lead as CTO a team of up to 45 engineers and developed multiple generations of end-to-end mapping drone systems.</p>
<p>I am also involved in advisory capacity with several Swiss start-ups.</p>
<p>On my spare time, I dabble in generative art and pen plotters, and maintain two open-source projects I created in that space:</p>
<ul>
<li><a href="https://github.com/abey79/vpype"><em>vpype</em></a>: a Swiss-Army-knife CLI to generate, manage, process, and convert vector graphic files for plotters,</li>
<li><a href="https://github.com/abey79/vsketch"><em>vsketch</em></a>: a Python plotter generative art framework and workflow automation tool.</li>
</ul>
<p>When I am not at the computer, you may find me piloting a helicopter 🚁 or behind the camera 📷 on my travels.</p>
<p><img loading="lazy" src="/self.jpg" alt="self"  />
</p>
<h2 id="about-this-site">About this site</h2>
<p>This site is built using <a href="https://gohugo.io">Hugo</a> and the <a href="https://github.com/adityatelange/hugo-PaperMod/">PaperMod</a> theme. It is hosted on <a href="https://pages.github.com">GitHub Pages</a> and deployed using a simple <a href="https://github.com/features/actions">GitHub Actions</a> <a href="https://github.com/abey79/abey79.github.io/blob/main/.github/workflows/gh-pages.yml">workflow</a>. Privacy-friendly and GDPR-compliant analytics services are provided by <a href="https://plausible.io/">Plausible</a> (a paid service, so neither you or I are the product).</p>
]]></content:encoded>
    </item>
    
    
    
  </channel>
</rss>
