<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Matthew Armstrong</title>
    <description>The latest articles on DEV Community by Matthew Armstrong (@open-madscience).</description>
    <link>https://dev.to/open-madscience</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4171433%2F8deeb383-d1eb-4ec0-955e-612d43f55925.png</url>
      <title>DEV Community: Matthew Armstrong</title>
      <link>https://dev.to/open-madscience</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/open-madscience"/>
    <language>en</language>
    <item>
      <title>OvalFFR</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Sun, 01 Nov 2026 21:36:16 +0000</pubDate>
      <link>https://dev.to/open-madscience/ovalffr-5618</link>
      <guid>https://dev.to/open-madscience/ovalffr-5618</guid>
      <description>&lt;p&gt;&lt;strong&gt;Oval/slot fixed foveated rendering for OpenXR VR games, with a control panel. Built to cut GPU load for games streamed through ALVR.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Work in progress (v0.5.0).&lt;/strong&gt; The control panel, the OpenXR layer plumbing and the Direct3D 12 code are in place, but the Direct3D 12 part (the foveation itself, &lt;em&gt;Smooth low-res edge&lt;/em&gt;, and the new in-headset fps box, zone highlight and VRS check) has &lt;strong&gt;not yet been run in a real game or on a GPU&lt;/strong&gt;, and the dual-GPU fix has never touched a real Windows registry. Direct3D 11 and Vulkan are not implemented. Read What works and what is untested before relying on it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fid8wb5wjl3tn209zcmup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fid8wb5wjl3tn209zcmup.png" alt="Control panel" width="796" height="1012"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The screenshot uses simulated test data (a fake game report saying SteamVR is on the integrated card, Auto shrinking and smoothing running). Auto foveation and Smooth low-res edge are ticked here to show their status rows; both are **off by default&lt;/em&gt;&lt;em&gt;. No real game, headset or GPU was involved.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Variable Rate Shading lets the GPU shade a block of pixels once instead of once per pixel. OvalFFR keeps a sharp &lt;strong&gt;oval (or slot) in the middle&lt;/strong&gt; of each eye's image and shades the rest more coarsely:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Zone&lt;/th&gt;
&lt;th&gt;Shading&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full detail&lt;/td&gt;
&lt;td&gt;1x1&lt;/td&gt;
&lt;td&gt;inside the oval/slot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middle ring&lt;/td&gt;
&lt;td&gt;2x2&lt;/td&gt;
&lt;td&gt;a gentle step around it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low-res edge&lt;/td&gt;
&lt;td&gt;2x2 or 4x4&lt;/td&gt;
&lt;td&gt;everything else, &lt;strong&gt;including the corners&lt;/strong&gt;, which the lens barely shows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The oval is centred on each lens from the headset's field of view. The panel's &lt;strong&gt;Horizontal&lt;/strong&gt; and &lt;strong&gt;Vertical&lt;/strong&gt; sliders set the sharp area's width and height, and the picture shows exactly which part of the view is low-res. The default setting is estimated to cut pixel-shading work by about &lt;strong&gt;49 %&lt;/strong&gt; (a unit test keeps it at 10 % or more); real fps gain is smaller, because geometry and memory work are not reduced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just use ALVR's foveated encoding?&lt;/strong&gt; That compresses the frame &lt;em&gt;after&lt;/em&gt; the game has rendered it, to save Wi-Fi bandwidth. It does not reduce the game's GPU work. OvalFFR reduces the rendering itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start (click and launch)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Double-click the launcher for your system:

&lt;ul&gt;
&lt;li&gt;Windows: &lt;code&gt;OvalFFR-Windows.bat&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Linux: &lt;code&gt;OvalFFR-Linux.sh&lt;/code&gt; (choose "Run")&lt;/li&gt;
&lt;li&gt;Mac: &lt;code&gt;OvalFFR-Mac.command&lt;/code&gt; (first time: right-click, Open)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Install&lt;/strong&gt;. The panel installs the layer and reads it back to confirm.&lt;/li&gt;
&lt;li&gt;Start your game through ALVR as usual.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Check&lt;/strong&gt;. It should say the layer was loaded by your game and that foveation is &lt;strong&gt;active&lt;/strong&gt;. If it says &lt;em&gt;pass-through&lt;/em&gt;, the reason is shown (for example "app does not use D3D12").&lt;/li&gt;
&lt;li&gt;Move the sliders while playing. Changes apply within about a second. &lt;strong&gt;Uninstall&lt;/strong&gt; removes everything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Needs Python 3.8+ with tkinter (included with the python.org installers; Linux: &lt;code&gt;sudo apt install python3-tk&lt;/code&gt;). Without tkinter the launchers fall back to text mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it doing anything? (fps box, zones, A/B test)
&lt;/h2&gt;

&lt;p&gt;New in 0.5.0, because "is it actually working?" is the first thing to find out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In the headset&lt;/strong&gt; (also visible in the SteamVR mirror window on the PC). Keys work from any window, as long as the VR game has focus in the headset:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Keys&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ctrl+Alt+O&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;small fps box under the lens centre: &lt;code&gt;72 FPS / 90 HZ  FFR ON&lt;/code&gt;, and below it the estimated saving and the VRS check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ctrl+Alt+Z&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;show the zones&lt;/strong&gt;: yellow = 2x2, red = 4x4, &lt;strong&gt;purple = OvalFFR asked for low-res but the image the headset gets is full detail there&lt;/strong&gt; (VRS did not reach it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ctrl+Alt+F&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;foveated rendering on/off (quick before/after)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Ctrl+Alt+[&lt;/strong&gt; / &lt;strong&gt;]&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;sharp area 5% smaller / bigger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ctrl+Alt+E&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;low-res edge 2x2 / 4x4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Ctrl+Alt+S&lt;/strong&gt; / &lt;strong&gt;A&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Smooth low-res edge / Auto foveation on/off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ctrl+Alt+T&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;A/B test&lt;/strong&gt;: FFR off, on, off, on, 10 s each (first 3 s of each ignored), then shows e.g. &lt;code&gt;A/B: OFF 64 ON 72 FPS +12%&lt;/code&gt;. Press again to stop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every key press shows what changed for 3 seconds. Setting changes are saved, so the panel follows them. Ctrl+Alt is AltGr on UK/European keyboards, so avoid typing AltGr+letter in other programs while a VR game runs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwn3mcm7r40ze81ccdquj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwn3mcm7r40ze81ccdquj.png" alt="Zones and fps box, simulated" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The C++ copy of the shader on a made-up scene. Not a game or a headset.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The VRS check&lt;/strong&gt; answers "is it doing anything?" directly. While the zones or Smooth low-res edge are on, the layer looks at one low-res block in every 16x16 pixels of the image the headset gets and counts how many are exact copies (what VRS produces).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;About &lt;strong&gt;90 % or more&lt;/strong&gt; means VRS really reaches the picture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Near 0 %&lt;/strong&gt; (lots of purple) means the game draws that image at full detail. Usually that's because it renders into its own buffer and post-processes before handing the image over. VRS may still save work in the game's own buffer in that case, but OvalFFR cannot see that, and the fps numbers are the judge.&lt;/li&gt;
&lt;li&gt;Flat single-colour areas look the same either way, so the figure is never exactly 0 %.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On the desktop:&lt;/strong&gt; the panel's &lt;strong&gt;Mini fps window&lt;/strong&gt; button opens a small always-on-top box with the live fps. Click it for more: late frames, spare time, the VRS check, buttons for every setting, &lt;em&gt;Show zones in headset&lt;/em&gt;, &lt;em&gt;Run A/B test&lt;/em&gt;, and the table of fps per setting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8zk9lamk20e97szze93.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8zk9lamk20e97szze93.png" alt="Mini fps window, simulated data" width="527" height="707"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fps log&lt;/strong&gt; (&lt;code&gt;fps_log.csv&lt;/code&gt; in the settings folder) gets &lt;strong&gt;one row per setting, averaged over 2 minutes&lt;/strong&gt;, plus a row whenever you change the setting after at least 20 s. Loading screens are left out, and the A/B test writes its own two rows. Text mode: &lt;code&gt;--cli fps&lt;/code&gt; prints the per-setting table, and &lt;code&gt;--cli fps send zones&lt;/code&gt; sends a command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If every setting gives the same fps and it equals the headset's refresh rate&lt;/strong&gt;, the game already runs at full speed and fps cannot rise. FFR then only buys headroom (the "spare time" figure, a rough measure). To see a difference, raise the render resolution in ALVR or SteamVR until the fps drops, then compare.&lt;/p&gt;

&lt;h2&gt;
  
  
  What works and what is untested
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mask maths (oval/slot, 3 zones, savings estimate, lens centring)&lt;/td&gt;
&lt;td&gt;Unit-tested on Linux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenXR layer: negotiation, hooks, config hot reload, status/log&lt;/td&gt;
&lt;td&gt;Tested on Linux against a &lt;strong&gt;fake&lt;/strong&gt; OpenXR runtime (mock backend)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python preview equals the C++ mask&lt;/td&gt;
&lt;td&gt;Tested (&lt;code&gt;tests/test_mask_parity.py&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control panel&lt;/td&gt;
&lt;td&gt;Run on Linux under a virtual display; screenshot above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Installer (files, manifest, read-back, uninstall)&lt;/td&gt;
&lt;td&gt;Tested on Linux in a throw-away home folder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge cases (odd configs, NaN, huge sizes, FOV jitter, quad views, overlay apps)&lt;/td&gt;
&lt;td&gt;Tested on Linux, including fuzzing and AddressSanitizer/UBSan. See &lt;a href="https://github.com/The-Dorkknight/2026-10-31_1545_alvr-oval-ffr/blob/main/docs/EDGE_CASES.md" rel="noopener noreferrer"&gt;docs/EDGE_CASES.md&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto foveation: decision logic (simulated games) and layer wiring (fake runtime, closed loop)&lt;/td&gt;
&lt;td&gt;Tested on Linux. &lt;strong&gt;Not run in a real game; no fps measured&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smooth low-res edge: the maths (a line-by-line C++ copy of the shader) and the layer wiring (fake runtime)&lt;/td&gt;
&lt;td&gt;Tested on Linux. The shader passes the Khronos HLSL front end (glslang), &lt;strong&gt;not&lt;/strong&gt; Microsoft's compiler. &lt;strong&gt;Never run on a GPU; cost not measured&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fps box, zone highlight, VRS check (C++ copy of the shader), live fps, fps log, commands, focus handling, A/B test (fake runtime)&lt;/td&gt;
&lt;td&gt;Tested on Linux. &lt;strong&gt;The D3D12 drawing and the Ctrl+Alt keys (Windows only) have never run&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mini fps window, panel following outside changes&lt;/td&gt;
&lt;td&gt;Run on Linux under a virtual display (self-test + screenshot)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dual-GPU: verdict logic, registry logic with undo&lt;/td&gt;
&lt;td&gt;Tested on Linux with an in-memory stand-in for the registry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dual-GPU: the real Windows registry write and the real graphics-card enumeration&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not run&lt;/strong&gt; (Windows-only code, never executed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux wrapper script for laptops with two GPUs&lt;/td&gt;
&lt;td&gt;Run with fake &lt;code&gt;lspci&lt;/code&gt; output; &lt;strong&gt;never on a real hybrid laptop&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows layer DLL&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Cross-compiled&lt;/strong&gt; with mingw-w64; exports checked; &lt;strong&gt;never loaded by a real OpenXR loader&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows registry install&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not run&lt;/strong&gt; (written from the OpenXR loader documentation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct3D 12 VRS backend&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Compile-checked only&lt;/strong&gt;; vtable slots verified by static asserts; not run on a GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct3D 11, Vulkan&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not implemented&lt;/strong&gt; (the layer reports pass-through). Effort estimate for Linux/Vulkan: &lt;a href="https://github.com/The-Dorkknight/2026-10-31_1545_alvr-oval-ffr/blob/main/docs/LINUX_VULKAN_PLAN.md" rel="noopener noreferrer"&gt;docs/LINUX_VULKAN_PLAN.md&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS, Windows launchers&lt;/td&gt;
&lt;td&gt;Not run (Linux launcher was run for real)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real games, real fps numbers, ALVR end to end&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Not tested&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low-end check (7th gen Core i3)&lt;/td&gt;
&lt;td&gt;Not run. The app has no GPU requirement; the layer is a pass-through without VRS tier 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Known limits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GPU needs &lt;strong&gt;Variable Rate Shading tier 2&lt;/strong&gt; (NVIDIA GTX 16 / RTX 20 or newer, AMD RX 6000 or newer, Intel Arc/Xe). Without it nothing changes and nothing breaks.&lt;/li&gt;
&lt;li&gt;Only games that use OpenXR &lt;strong&gt;with Direct3D 12&lt;/strong&gt;. OpenVR-only games are not seen by an OpenXR layer.&lt;/li&gt;
&lt;li&gt;Games that create their render targets &lt;em&gt;before&lt;/em&gt; starting the OpenXR session, or move descriptors with &lt;code&gt;CopyDescriptors&lt;/code&gt;, may show no effect.&lt;/li&gt;
&lt;li&gt;A game with its own dynamic resolution may spend the saved time on a sharper picture instead of more fps. Pin its resolution when comparing.&lt;/li&gt;
&lt;li&gt;Fixed foveation is easier to notice than eye-tracked foveation. If you see the edge, raise the sliders or the ring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Details and sources: &lt;a href="https://github.com/The-Dorkknight/2026-10-31_1545_alvr-oval-ffr/blob/main/docs/RESEARCH_NOTES.md" rel="noopener noreferrer"&gt;docs/RESEARCH_NOTES.md&lt;/a&gt;. How it is built: &lt;a href="https://github.com/The-Dorkknight/2026-10-31_1545_alvr-oval-ffr/blob/main/docs/ARCHITECTURE.md" rel="noopener noreferrer"&gt;docs/ARCHITECTURE.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto foveation (experimental, off by default)
&lt;/h2&gt;

&lt;p&gt;Tick &lt;strong&gt;Auto foveation (experimental)&lt;/strong&gt;. Your sliders then become the &lt;em&gt;sharpest&lt;/em&gt; the oval will go. If the game keeps missing display refreshes, the layer shrinks the sharp area in small, slow steps down to the floor you set; when frames are fine again it grows back. If shrinking does not help (the game is limited by the CPU or the network, not the GPU) it gives the quality back and stops trying for a while. The panel shows the live percentage. Text mode: &lt;code&gt;--cli set auto=1 auto_floor=50&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Honest limits: the decision logic and a closed-loop run against a fake runtime are tested; it has &lt;strong&gt;not&lt;/strong&gt; been run in a real game and &lt;strong&gt;no fps gain has been measured&lt;/strong&gt;. It judges by &lt;code&gt;xrWaitFrame&lt;/code&gt; timing, and I do not know yet how SteamVR/ALVR pacing looks to it. Other speed-up ideas that were researched and &lt;strong&gt;not&lt;/strong&gt; built (peripheral frame generation, ...) are explained in &lt;a href="https://github.com/The-Dorkknight/2026-10-31_1545_alvr-oval-ffr/blob/main/docs/PERF_IDEAS.md" rel="noopener noreferrer"&gt;docs/PERF_IDEAS.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smooth low-res edge (experimental, off by default)
&lt;/h2&gt;

&lt;p&gt;The low-res edge looks blocky because Variable Rate Shading shades one colour per 2x2 or 4x4 block and copies it to every pixel of the block. Tick &lt;strong&gt;Smooth low-res edge (experimental)&lt;/strong&gt; and, after the game has drawn each eye, a small extra GPU pass blends those blocks into a smooth picture, like stretching a small image with bilinear filtering instead of nearest-neighbour. The idea: if the edge no longer &lt;em&gt;looks&lt;/em&gt; low-res, you can afford a &lt;strong&gt;smaller sharp area or the stronger 4x4 edge&lt;/strong&gt;. That is where the extra fps comes from. The smoothing itself saves nothing and costs a little.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4kugxoj3tnd7qbl9aesa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4kugxoj3tnd7qbl9aesa.png" alt="Smooth low-res edge, simulated" width="800" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Made by the C++ copy of the shader on a synthetic scene, magnified 3x. Not a screenshot from a game or a GPU.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What it will and will not touch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;sharp area is never written&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It only changes a block whose pixels are all identical copies. Blocks with an object's outline running through them, film grain or other real detail are left alone. If a game draws into its own buffer and post-processes before handing the image over, the blocks are no longer exact copies, so the pass changes nothing there (and still costs its time).&lt;/li&gt;
&lt;li&gt;Neighbouring blocks with clearly different colour count as an edge and are not blended in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strength&lt;/strong&gt; (0-100%) sets how far each pixel moves towards the smooth version.&lt;/li&gt;
&lt;li&gt;Text mode: &lt;code&gt;--cli set smooth=1 smooth_strength=60&lt;/code&gt;. The panel's status area shows &lt;em&gt;Smooth low-res edge: running&lt;/em&gt;, or why it is unavailable (for example no &lt;code&gt;d3dcompiler_47.dll&lt;/code&gt;). Any failure switches off only the smoothing, never the game.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measured on synthetic images (not hardware): the difference to the full-detail picture in the low-res zones drops by &lt;strong&gt;88 %&lt;/strong&gt; on a soft wavy scene and a lit ball scene. Outlines move by at most 0.005 (on a 0-1 scale). A full-detail image is left 100% unchanged (noise) or 99.99% unchanged (smooth scene).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost: not measured.&lt;/strong&gt; It copies each eye image once and runs one full-screen pass. My rough estimate is about 150 MB of memory traffic per frame for two 2000x2000 eyes, which I'd guess at roughly 0.3-0.5 ms on a mid-range card. Treat that as a guess until it is measured. The shader is compiled the first time it is needed, which may cause one short hitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laptops with two graphics cards
&lt;/h2&gt;

&lt;p&gt;On a laptop with an integrated and a gaming GPU, the &lt;strong&gt;VR runtime (SteamVR, which hosts ALVR) decides which card every OpenXR game must use&lt;/strong&gt;. Forcing only the game does not help if SteamVR itself started on the integrated chip. So OvalFFR does two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detects it.&lt;/strong&gt; When a game starts, the layer compares the card the runtime picked with the fastest card Windows reports, and the panel shows a green or red line in the &lt;em&gt;Laptop with two graphics cards&lt;/em&gt; box (also in &lt;code&gt;status-&amp;lt;pid&amp;gt;.txt&lt;/code&gt; as &lt;code&gt;gpu_verdict&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixes it (Windows).&lt;/strong&gt; &lt;em&gt;Use gaming GPU for SteamVR&lt;/em&gt; sets Windows' own per-program "High performance" preference (the same switch as Settings &amp;gt; System &amp;gt; Display &amp;gt; Graphics) for the SteamVR and ALVR programs it finds, after you confirm the list. It reads every change back, remembers what was there before, and &lt;strong&gt;Undo GPU changes&lt;/strong&gt; puts it all back exactly. &lt;em&gt;Add program...&lt;/em&gt; does the same for any &lt;code&gt;.exe&lt;/code&gt; (for example the game). Restart SteamVR afterwards.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Text mode: &lt;code&gt;python3 src/main.py --cli gpu status&lt;/code&gt;, &lt;code&gt;gpu force&lt;/code&gt;, &lt;code&gt;gpu add C:\path\game.exe&lt;/code&gt;, &lt;code&gt;gpu undo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt; nothing is written to your system. Start ALVR or SteamVR through &lt;code&gt;tools/ovalffr-gpu-linux.sh&lt;/code&gt;, for example &lt;code&gt;./tools/ovalffr-gpu-linux.sh alvr_dashboard&lt;/code&gt;. It looks at your graphics cards and, for that one launch only, sets NVIDIA's PRIME offload variables (&lt;code&gt;__NV_PRIME_RENDER_OFFLOAD=1&lt;/code&gt;, &lt;code&gt;__VK_LAYER_NV_optimus=NVIDIA_only&lt;/code&gt;) or Mesa's &lt;code&gt;DRI_PRIME=1&lt;/code&gt;. &lt;code&gt;--list&lt;/code&gt; shows what it found, &lt;code&gt;--dry-run&lt;/code&gt; shows the command. Flatpak programs need &lt;code&gt;flatpak override&lt;/code&gt; instead (the script header has the command).&lt;/p&gt;

&lt;p&gt;Honest limits: only &lt;code&gt;vrserver.exe&lt;/code&gt; is backed by a user report that this fixed a wrong-card problem; the other SteamVR programs are the same family but not individually confirmed. NVIDIA's and AMD's own control panels also have per-program settings, and I did not find out which one wins. None of this has been run on a real laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settings
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Foveated rendering on&lt;/td&gt;
&lt;td&gt;Master switch. Off = the layer does nothing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Horizontal / Vertical&lt;/td&gt;
&lt;td&gt;Size of the full-detail area as a % of the view width / height (20-100). With Auto on, this is the largest it will be.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shape&lt;/td&gt;
&lt;td&gt;Oval (lowest cost) to slot (rounded rectangle, keeps more of the frame sharp).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middle ring&lt;/td&gt;
&lt;td&gt;Thickness of the 2x2 step around the sharp area.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low-res edge strength&lt;/td&gt;
&lt;td&gt;2x2 gentle, or 4x4 strong (needs a GPU that supports it; otherwise 2x2 is used).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Centre on each lens&lt;/td&gt;
&lt;td&gt;Use the headset's field of view to find each lens axis.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto / Never below&lt;/td&gt;
&lt;td&gt;Auto foveation on/off, and the smallest the sharp area may shrink to (40-100% of your sliders).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smooth low-res edge / Strength&lt;/td&gt;
&lt;td&gt;Blend the blocky low-res pixels after the game draws (off by default), and how strongly (0-100%).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Show fps in the headset&lt;/td&gt;
&lt;td&gt;The small fps box (Ctrl+Alt+O). Off by default.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The settings live in &lt;code&gt;config.ini&lt;/code&gt; (Windows &lt;code&gt;%LOCALAPPDATA%\OvalFFR&lt;/code&gt;, Linux &lt;code&gt;~/.config/ovalffr&lt;/code&gt;, Mac &lt;code&gt;~/Library/Application Support/OvalFFR&lt;/code&gt;). The layer writes one &lt;code&gt;status-&amp;lt;process id&amp;gt;.txt&lt;/code&gt; per VR program that loads it, and &lt;code&gt;layer.log&lt;/code&gt;, there too.&lt;/p&gt;

&lt;p&gt;Text mode: &lt;code&gt;python3 src/main.py --cli show&lt;/code&gt;, &lt;code&gt;--cli set h=60 v=45&lt;/code&gt;, &lt;code&gt;--cli install&lt;/code&gt;, &lt;code&gt;--cli status&lt;/code&gt;, &lt;code&gt;--cli fps&lt;/code&gt;. The mini window on its own: &lt;code&gt;python3 src/main.py --mini&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build and test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cmake &lt;span class="nt"&gt;-S&lt;/span&gt; layer &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="nt"&gt;-DCMAKE_BUILD_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build &lt;span class="nt"&gt;-j&lt;/span&gt;
./build/core_test
./build/fuzz_test
./build/smooth_test
./build/layer_test ./build/libXR_APILAYER_OVALFFR_oval_ffr.so &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;OVALFFR_BUILD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;/build python3 &lt;span class="nt"&gt;-m&lt;/span&gt; unittest discover &lt;span class="nt"&gt;-s&lt;/span&gt; tests &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Windows DLL on Linux: configure with a mingw-w64 toolchain (the vendored Microsoft headers are used automatically). The prebuilt DLL and Linux layer in &lt;code&gt;prebuilt/&lt;/code&gt; were made this way. Check the vtable slots with &lt;code&gt;tests/check_d3d12_vtable.c&lt;/code&gt; (command inside the file).&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/2026-10-31_1545_alvr-oval-ffr" rel="noopener noreferrer"&gt;The-Dorkknight/2026-10-31_1545_alvr-oval-ffr on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>opensource</category>
      <category>performance</category>
    </item>
    <item>
      <title>angry-putty</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Sun, 01 Nov 2026 21:32:54 +0000</pubDate>
      <link>https://dev.to/open-madscience/angry-putty-3lof</link>
      <guid>https://dev.to/open-madscience/angry-putty-3lof</guid>
      <description>&lt;h1&gt;
  
  
  &lt;strong&gt;WORK IN PROGRESS: MANY FEATURES MAY BE BROKEN&lt;/strong&gt;
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Angry Putty 0.1.6-wip&lt;/strong&gt; · © Matthew Armstrong · source-available under the&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/LICENSE.md" rel="noopener noreferrer"&gt;PolyForm Noncommercial License 1.0.0&lt;/a&gt;: free for personal, non-commercial use;&lt;br&gt;
no commercial use without permission. Provided &lt;strong&gt;as is, with no warranty and no liability&lt;/strong&gt;.&lt;br&gt;
Only scan networks and log in to computers you own or have permission to use.&lt;br&gt;
&lt;strong&gt;Read &lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/DISCLAIMER.md" rel="noopener noreferrer"&gt;DISCLAIMER.md&lt;/a&gt; before use.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwms9b5cyn2xxgq0px5m.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwms9b5cyn2xxgq0px5m.webp" alt="Angry Putty: an angry blob of putty shouting a scan range at a terminal" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Angry Putty&lt;/strong&gt; is an &lt;a href="https://angryip.org/" rel="noopener noreferrer"&gt;Angry IP Scanner&lt;/a&gt;-style network scanner and a&lt;br&gt;
&lt;a href="https://www.chiark.greenend.org.uk/~sgtatham/putty/" rel="noopener noreferrer"&gt;PuTTY&lt;/a&gt;-style terminal in one small window,&lt;br&gt;
plus firmware that turns a cheap &lt;strong&gt;ESP32-S3&lt;/strong&gt; board into a USB dongle: plug it into a PC or&lt;br&gt;
Raspberry Pi and it tells you that computer's address and lets you SSH into it from your phone.&lt;/p&gt;
&lt;h3&gt;
  
  
  Credit where it's due
&lt;/h3&gt;

&lt;p&gt;The clever part of Angry Putty was done by other people. I have taken their ideas and tried to&lt;br&gt;
make them quicker to use and plug and play.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://angryip.org/" rel="noopener noreferrer"&gt;Angry IP Scanner&lt;/a&gt;&lt;/strong&gt; by &lt;strong&gt;Anton Keks&lt;/strong&gt; and its contributors (free, GPL-2.0):
the fast, friendly scanner that fills a table with every live device, its name and open ports.
Angry Putty's Scan tab is built on that idea.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.chiark.greenend.org.uk/~sgtatham/putty/" rel="noopener noreferrer"&gt;PuTTY&lt;/a&gt;&lt;/strong&gt; by &lt;strong&gt;Simon Tatham&lt;/strong&gt; and the PuTTY
team (free, MIT licence): one small window that remembers your sessions and gets you into SSH,
telnet or a serial port. Angry Putty's address bar and konsole follow that idea.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Angry Putty is a new program written from scratch: it contains none of their code, and it is not&lt;br&gt;
affiliated with or endorsed by either project. If you want the full-featured originals, use them.&lt;br&gt;
The heavy lifting inside Angry Putty is also done by other people's libraries (paramiko, pyte,&lt;br&gt;
esptool, ESP-IDF, TinyUSB, lwIP, libssh2, xterm.js); see &lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/THIRD_PARTY.md" rel="noopener noreferrer"&gt;THIRD_PARTY.md&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Status in 0.1.6-wip&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scanner: your own networks as the default range, ping, open ports, names, MAC&lt;/td&gt;
&lt;td&gt;Works (tested on Linux)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;name.local&lt;/code&gt; and "find every &lt;code&gt;.local&lt;/code&gt; device" (mDNS), also on Windows without Bonjour&lt;/td&gt;
&lt;td&gt;Works against a test responder; not yet tried on a real LAN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address bar that remembers the last address and a history; &lt;code&gt;http://ip&lt;/code&gt; opens the browser&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Konsole tabs: SSH (host-key checking), telnet, raw TCP, serial&lt;/td&gt;
&lt;td&gt;SSH tested against OpenSSH 9.6; telnet/serial unit-tested only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saved sessions, "open in system terminal"&lt;/td&gt;
&lt;td&gt;Works on Linux; Windows/Mac terminals not tried&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flash the dongle from the app&lt;/td&gt;
&lt;td&gt;Works: used on an ESP32-S3-Zero from Linux Mint (13-15 Oct 2026); 0.1.5 keeps the dongle's settings when re-flashing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dongle firmware: USB network adapter + hotspot + web page + browser konsole (SSH, telnet, serial)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Works on an ESP32-S3-Zero&lt;/strong&gt; (0.1.2): hotspot, phone page, and the plugged-in computer's address on &lt;strong&gt;Linux Mint and Windows 11&lt;/strong&gt; with no driver install. SSH through the dongle not yet tried&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1.1: status LED, USB log, restart reasons and crash records, captive portal, capped WiFi power&lt;/td&gt;
&lt;td&gt;LED, USB log and hotspot &lt;strong&gt;seen working on the board&lt;/strong&gt; (0.1.1); crash records tested in Espressif's ESP32-S3 emulator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone/laptop SSH apps through the dongle (port 2222)&lt;/td&gt;
&lt;td&gt;Written, not yet run on hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1.4: GadgetDesk option (off by default): cast the Pi's desktop to a phone on 8088, touch pad (double tap = HDMI desktop on/off), on/off from GadgetDesk&lt;/td&gt;
&lt;td&gt;Builds; gesture logic tested on a PC; works end to end against GadgetDesk with the mock dongle. &lt;strong&gt;Not flashed or touched yet&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Scan
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2lg5xdwaizepae53vcid.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2lg5xdwaizepae53vcid.webp" alt="Scan tab" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;Range&lt;/strong&gt; box fills in with your own networks (a big office network is trimmed to the /24 around you).
&lt;strong&gt;Defaults&lt;/strong&gt; lists every network card, the dongle hotspot and common home ranges.&lt;/li&gt;
&lt;li&gt;You can type: &lt;code&gt;192.168.1.0/24&lt;/code&gt;, &lt;code&gt;192.168.1.10-50&lt;/code&gt;, &lt;code&gt;10.0.0.1-10.0.1.20&lt;/code&gt;, single addresses,
host names, &lt;code&gt;printer.local&lt;/code&gt;, or &lt;code&gt;*.local&lt;/code&gt; to find every device that announces itself
(Pis, printers, Klipper/Moonraker, ESPHome, Macs...). Mix them with commas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start scan&lt;/strong&gt;. A host counts as alive if it answers ping &lt;em&gt;or&lt;/em&gt; any probed port answers
(even a refusal proves something is there). Untick &lt;strong&gt;Ping&lt;/strong&gt; on networks that block it.&lt;/li&gt;
&lt;li&gt;Double-click a row: SSH if port 22 is open, otherwise its web page. Right-click for SSH,
telnet, &lt;code&gt;http://&lt;/code&gt;, &lt;code&gt;https://&lt;/code&gt;, copy IP/name, or save it as a session. &lt;strong&gt;Export CSV&lt;/strong&gt; saves the table.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(The screenshot uses the built-in &lt;code&gt;--demo&lt;/code&gt; data, so the addresses are simulated.)&lt;/p&gt;
&lt;h3&gt;
  
  
  Connect: the address bar and konsole
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhp5w5j5s4fdqxgowj4no.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhp5w5j5s4fdqxgowj4no.webp" alt="SSH konsole" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type in &lt;strong&gt;Connect to&lt;/strong&gt; and press Enter. It remembers the last address and keeps a history.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You type&lt;/th&gt;
&lt;th&gt;It does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;192.168.1.20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SSH (or the protocol picked on the left)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pi@raspberrypi.local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SSH as &lt;code&gt;pi&lt;/code&gt;; &lt;code&gt;.local&lt;/code&gt; names are looked up with Angry Putty's own mDNS if the system cannot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh://pi@192.168.4.1:2222&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SSH on another port (this one goes through the dongle)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;telnet://192.168.1.9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;telnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http://192.168.1.50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;opens it in your web browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;COM3&lt;/code&gt;, &lt;code&gt;/dev/ttyUSB0&lt;/code&gt;, &lt;code&gt;serial:///dev/ttyACM0?baud=9600&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;serial konsole&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The konsole is a real terminal (colours, &lt;code&gt;htop&lt;/code&gt;, &lt;code&gt;nano&lt;/code&gt;, &lt;code&gt;vim&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe163en703g06e4vrjqjn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe163en703g06e4vrjqjn.webp" alt="htop in the konsole" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PuTTY habits: selecting text copies it, right-click pastes; Ctrl+Shift+C / Ctrl+Shift+V also work.&lt;/li&gt;
&lt;li&gt;The mouse wheel scrolls back. &lt;strong&gt;A+ / A-&lt;/strong&gt; change the font size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System terminal&lt;/strong&gt; opens the same connection in your own terminal with the system &lt;code&gt;ssh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;SSH logs in with your SSH agent, your keys in &lt;code&gt;~/.ssh&lt;/code&gt; (&lt;code&gt;id_ed25519&lt;/code&gt;, &lt;code&gt;id_ecdsa&lt;/code&gt;, &lt;code&gt;id_rsa&lt;/code&gt;),
then asks for a password. &lt;strong&gt;Passwords are never stored.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Host keys: the first time you are shown the SHA256 fingerprint (the same one &lt;code&gt;ssh-keygen -lf&lt;/code&gt;
prints). A &lt;strong&gt;changed&lt;/strong&gt; key gets a loud warning and is never stored over the old one.
Trusted keys go into Angry Putty's own &lt;code&gt;known_hosts&lt;/code&gt;; your &lt;code&gt;~/.ssh/known_hosts&lt;/code&gt; is read, never written.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Screenshots: a local OpenSSH test server, &lt;code&gt;127.0.0.1:2222&lt;/code&gt;.)&lt;/p&gt;
&lt;h3&gt;
  
  
  Sessions
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqp7brh1ilfkp8ray5641.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqp7brh1ilfkp8ray5641.webp" alt="Sessions tab" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Save a host as a named session (SSH, telnet, raw, serial, http, https) with port, user,&lt;br&gt;
baud rate and an optional key file. Double-click to open; &lt;strong&gt;Open in system terminal&lt;/strong&gt; uses your own terminal.&lt;/p&gt;

&lt;p&gt;Settings live in &lt;code&gt;~/.config/angry-putty/&lt;/code&gt; (Linux), &lt;code&gt;~/Library/Application Support/angry-putty/&lt;/code&gt; (macOS)&lt;br&gt;
or &lt;code&gt;%APPDATA%\angry-putty\&lt;/code&gt; (Windows).&lt;/p&gt;
&lt;h3&gt;
  
  
  The dongle
&lt;/h3&gt;

&lt;p&gt;The dongle is an ESP32-S3 board running Angry Putty's firmware. Plug it into the computer you&lt;br&gt;
want to reach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; [ PC or Raspberry Pi ] ==USB== [ ESP32-S3 dongle ] ~~~ WiFi hotspot "AngryPutty-XXXX" ~~~ [ phone / laptop ]
   gets 10.66.0.2                10.66.0.1 | 192.168.4.1                                   browser: http://192.168.4.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;It appears to that computer as a &lt;strong&gt;USB network adapter&lt;/strong&gt; and gives it the address 10.66.0.2.
It offers no gateway and no DNS, so the computer's normal internet is not touched.&lt;/li&gt;
&lt;li&gt;It reads the computer's DHCP request, so the page can also show its &lt;strong&gt;host name&lt;/strong&gt; (e.g. &lt;code&gt;raspberrypi&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;It runs its own &lt;strong&gt;WiFi hotspot&lt;/strong&gt; &lt;code&gt;AngryPutty-XXXX&lt;/code&gt; (default password &lt;code&gt;angryputty&lt;/code&gt;; change it in Settings).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="http://192.168.4.1" rel="noopener noreferrer"&gt;http://192.168.4.1&lt;/a&gt;&lt;/strong&gt; shows the computer's address and a &lt;strong&gt;browser konsole&lt;/strong&gt;: SSH (the dongle
itself is the SSH client), telnet, raw TCP, or the &lt;strong&gt;serial console&lt;/strong&gt; wired to the Pi's GPIO pins.
Extra keys (Esc, Tab, Ctrl, arrows) are there for phones.&lt;/li&gt;
&lt;li&gt;Any SSH app can log in through &lt;strong&gt;192.168.4.1 port 2222&lt;/strong&gt;; the computer's web page (port 80, e.g.
Mainsail or OctoPrint) is at &lt;strong&gt;192.168.4.1:8080&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GadgetDesk option (plug-in, 0.1.4, off by default).&lt;/strong&gt; Turn it on with the desktop app's
&lt;strong&gt;Turn GadgetDesk on&lt;/strong&gt; button (Dongle tab, 0.1.5) or &lt;strong&gt;GadgetDesk desktop&lt;/strong&gt; in the
dongle's Settings to cast a Pi's GadgetDesk web
desktop to your phone at &lt;strong&gt;192.168.4.1:8088&lt;/strong&gt; (an &lt;strong&gt;Open :8088&lt;/strong&gt; button appears) and to use the
touch pad below. With it off, the dongle has no 8088 forward and no touch pad, as in 0.1.3.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Touch pad&lt;/strong&gt; (part of the GadgetDesk option): a short wire or a piece of foil tape on &lt;strong&gt;GPIO4&lt;/strong&gt; (any of GPIO1-14 except 3,&lt;br&gt;
set on the page). With GadgetDesk on the Pi: &lt;strong&gt;double tap&lt;/strong&gt; = HDMI desktop on/off, &lt;strong&gt;hold&lt;/strong&gt; = off.&lt;br&gt;
Switch the pad off and on from the page's &lt;strong&gt;Touch pad&lt;/strong&gt; card or from GadgetDesk (its Touch button);&lt;br&gt;
the card also shows the live reading so you can set the sensitivity. It only touches the pin: no&lt;br&gt;
extra parts, and nothing else happens when GadgetDesk isn't there (3 red LED flashes).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Little OLED screen (0.1.6, optional).&lt;/strong&gt; Wire a 4-pin I2C OLED (SSD1306 / SSD1315 / SH1106,&lt;br&gt;
128x64 or 128x32) to &lt;strong&gt;3V3, GND, SDA = GPIO8, SCL = GPIO9&lt;/strong&gt; and the dongle shows the computer's&lt;br&gt;
address in big, full-brightness digits, then the hotspot, the ways in from a phone and its own&lt;br&gt;
details, a few seconds each. Nothing happens if no screen is wired. Wiring, pages, settings and&lt;br&gt;
troubleshooting: &lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/docs/OLED.md" rel="noopener noreferrer"&gt;docs/OLED.md&lt;/a&gt;. &lt;strong&gt;Not run on a real screen yet.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fangry-putty%2Fmain%2Fdocs%2Fimages%2Foled-screens.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fangry-putty%2Fmain%2Fdocs%2Fimages%2Foled-screens.webp" alt="The four OLED pages drawn on a PC by the firmware's own drawing code (not a photo)" width="800" height="683"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fangry-putty%2Fmain%2Fdocs%2Fimages%2Fdongle-touch-card.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fangry-putty%2Fmain%2Fdocs%2Fimages%2Fdongle-touch-card.webp" alt="The dongle page's Touch pad card: switch button, pin, live reading, last gesture" width="388" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Figxv05birzxbd8e2mpq5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Figxv05birzxbd8e2mpq5.webp" alt="Dongle web page on a phone" width="390" height="2463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Taken with &lt;code&gt;tools/mock_dongle.py&lt;/code&gt;, a pretend dongle on a PC, which shows a purple MOCK banner;&lt;br&gt;
the computer shown is simulated, the SSH session behind it is real.)&lt;/p&gt;

&lt;h4&gt;
  
  
  The status LED
&lt;/h4&gt;

&lt;p&gt;On the ESP32-S3-Zero the colour LED (GPIO21) shows what the dongle is doing. The same table is on&lt;br&gt;
the dongle's page under &lt;strong&gt;What the LED means&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;LED&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Red, green, blue once at power-on&lt;/td&gt;
&lt;td&gt;Colour test. If you see &lt;strong&gt;green&lt;/strong&gt; first, set &lt;em&gt;LED colour order&lt;/em&gt; in the page's Settings.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blue, 1 blink every 2 s&lt;/td&gt;
&lt;td&gt;Hotspot on, nothing on the USB port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blue, 2 blinks every 2 s&lt;/td&gt;
&lt;td&gt;Computer connected by USB, waiting for it to ask for an address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady green&lt;/td&gt;
&lt;td&gt;The computer has its address: ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;White flicker&lt;/td&gt;
&lt;td&gt;Konsole traffic, or someone joined the WiFi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 purple flashes after the colour test&lt;/td&gt;
&lt;td&gt;The last restart was a &lt;strong&gt;crash&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 orange flashes after the colour test&lt;/td&gt;
&lt;td&gt;The last restart was a &lt;strong&gt;power dip&lt;/strong&gt; (brownout)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast red blinking&lt;/td&gt;
&lt;td&gt;The WiFi hotspot failed to start&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Other boards: choose a plain LED, another pin, or no LED in Settings.&lt;/p&gt;

&lt;h4&gt;
  
  
  When something goes wrong: health, log and USB log
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;The page's &lt;strong&gt;Dongle health&lt;/strong&gt; card shows the last restart reason (power on, CRASH, brownout...),
how many crashes and power dips there have been, the last crash recorded (task and code address),
WiFi channel and power, and free memory.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Log&lt;/strong&gt; section shows the firmware's log live, and &lt;strong&gt;Before the last restart&lt;/strong&gt; shows the
last few KB written before a crash or restart.&lt;/li&gt;
&lt;li&gt;Plugged into a computer, the dongle's USB port is also a serial port called &lt;strong&gt;Angry Putty log&lt;/strong&gt;
carrying the same log (the ESP32-S3-Zero has no USB-serial chip, so this is the only way to read
it over a cable). In the app: &lt;strong&gt;Dongle&lt;/strong&gt; tab &amp;gt; &lt;strong&gt;USB log&lt;/strong&gt;. Any serial terminal works too (type
&lt;code&gt;help&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;prev&lt;/code&gt; or &lt;code&gt;reboot&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffo7ffy77lpxhzliyfopz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffo7ffy77lpxhzliyfopz.webp" alt="USB log window" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the dongle misbehaves, send the log (the window's &lt;strong&gt;Save...&lt;/strong&gt; button) and the health card:&lt;br&gt;
crash addresses can be decoded to the exact line with &lt;code&gt;firmware/debug/angry-putty-&amp;lt;version&amp;gt;.elf.gz&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Flash the dongle
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngwv2nxv9647j8if3ibd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngwv2nxv9647j8if3ibd.webp" alt="Dongle tab" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need an &lt;strong&gt;ESP32-S3&lt;/strong&gt; board with a USB port wired to the chip's own USB (most S3 boards;&lt;br&gt;
4 MB flash or more). ESP8266, the original ESP32, ESP32-C3 and C6 &lt;strong&gt;cannot&lt;/strong&gt; do this: they have&lt;br&gt;
no native USB that can act as a network adapter. The app checks the chip and says so.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hold &lt;strong&gt;BOOT&lt;/strong&gt;, plug the board into the computer running Angry Putty (or tap &lt;strong&gt;RESET&lt;/strong&gt; while
holding BOOT), let go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dongle&lt;/strong&gt; tab &amp;gt; &lt;strong&gt;Detect chip&lt;/strong&gt; (it picks the ESP32-S3's port itself and checks the chip and its
flash size) &amp;gt; &lt;strong&gt;Flash firmware&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Unplug it and plug it in again &lt;strong&gt;without&lt;/strong&gt; BOOT (remove any BOOT jumper or wire on GPIO0). The
LED should show red, green, blue. From now on its USB port is a network adapter plus the log
port, so to flash it again you always hold BOOT while plugging it in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Re-flashing keeps the dongle's settings (WiFi name and password, LED, GadgetDesk option). Tick&lt;br&gt;
&lt;strong&gt;Erase everything first&lt;/strong&gt; to go back to the defaults.&lt;/p&gt;

&lt;h4&gt;
  
  
  Use it
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Plug the dongle into the PC or Pi. On a Pi, use a normal USB-A port (or the Zero's USB OTG port with an adapter).&lt;/li&gt;
&lt;li&gt;Join the &lt;code&gt;AngryPutty-XXXX&lt;/code&gt; WiFi from your phone or laptop (password &lt;code&gt;angryputty&lt;/code&gt;).
A phone will say &lt;strong&gt;Sign in to network&lt;/strong&gt; (like hotel WiFi): tap it, or open &lt;a href="http://192.168.4.1" rel="noopener noreferrer"&gt;http://192.168.4.1&lt;/a&gt;
in your browser. The phone stays connected and keeps using mobile data for the internet.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;&lt;a href="http://192.168.4.1" rel="noopener noreferrer"&gt;http://192.168.4.1&lt;/a&gt;&lt;/strong&gt; (or press &lt;strong&gt;Refresh&lt;/strong&gt; in the app's Dongle tab).&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;SSH&lt;/strong&gt;, type the user name and password. Or connect any SSH app to &lt;code&gt;192.168.4.1&lt;/code&gt; port &lt;code&gt;2222&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;SSH must be switched on in the computer.&lt;/strong&gt; On Raspberry Pi OS: Raspberry Pi Imager's settings,&lt;br&gt;
or &lt;code&gt;sudo raspi-config&lt;/code&gt; &amp;gt; Interface Options &amp;gt; SSH. On Windows: Settings &amp;gt; Optional features &amp;gt;&lt;br&gt;
OpenSSH Server. No SSH? Use the serial console:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dongle (ESP32-S3)&lt;/th&gt;
&lt;th&gt;Raspberry Pi header&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TX (GPIO17 by default)&lt;/td&gt;
&lt;td&gt;pin 10, GPIO15 (RXD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RX (GPIO18 by default)&lt;/td&gt;
&lt;td&gt;pin 8, GPIO14 (TXD)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;pin 6 (GND)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;3.3 V logic on both sides; never connect the Pi's 5 V pins. Enable the console with&lt;br&gt;
&lt;code&gt;sudo raspi-config&lt;/code&gt; &amp;gt; Interface Options &amp;gt; Serial Port (login shell: yes). The default pins are&lt;br&gt;
arbitrary: set the ones your board has free in the page's &lt;strong&gt;Settings&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Build the firmware yourself
&lt;/h4&gt;

&lt;p&gt;See &lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/firmware/README.md" rel="noopener noreferrer"&gt;firmware/README.md&lt;/a&gt;. In short, with ESP-IDF 5.4:&lt;br&gt;
&lt;code&gt;cd firmware &amp;amp;&amp;amp; idf.py set-target esp32s3 build&lt;/code&gt;. GitHub Actions also builds it on every push.&lt;/p&gt;

&lt;h2&gt;
  
  
  More
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/HOW_IT_WORKS.md" rel="noopener noreferrer"&gt;HOW_IT_WORKS.md&lt;/a&gt;: what each part reads, changes and never does.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/RESEARCH_NOTES.md" rel="noopener noreferrer"&gt;RESEARCH_NOTES.md&lt;/a&gt;: the research, decisions, what was tested and how, ideas,
and the lessons carried over from Skipper Flipper.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/docs/HARDWARE_LOG.md" rel="noopener noreferrer"&gt;docs/HARDWARE_LOG.md&lt;/a&gt;: every test on real hardware, with results.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/firmware/README.md" rel="noopener noreferrer"&gt;firmware/README.md&lt;/a&gt;: building and flashing the dongle firmware by hand.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/THIRD_PARTY.md" rel="noopener noreferrer"&gt;THIRD_PARTY.md&lt;/a&gt;, &lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;, &lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;, &lt;a href="https://github.com/The-Dorkknight/angry-putty/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tests: &lt;code&gt;python -m pytest&lt;/code&gt; (add &lt;code&gt;xvfb-run&lt;/code&gt; on Linux for the GUI test; set&lt;br&gt;
&lt;code&gt;ANGRY_PUTTY_TEST_SSH=user:password@host:port&lt;/code&gt; to include a live SSH login).&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/angry-putty" rel="noopener noreferrer"&gt;The-Dorkknight/angry-putty on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>github</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>Pasta Inspector</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Sun, 01 Nov 2026 20:48:24 +0000</pubDate>
      <link>https://dev.to/open-madscience/pasta-inspector-24ee</link>
      <guid>https://dev.to/open-madscience/pasta-inspector-24ee</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WORK IN PROGRESS: MANY FEATURES MAY BE BROKEN.&lt;/strong&gt; Version &lt;code&gt;0.6.0-wip&lt;/code&gt;. Proof of concept: the detector has only been tested on simulated prints, and the firmware has not yet been run on a real ESP32 board.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Licence:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/LICENSE.md" rel="noopener noreferrer"&gt;PolyForm Noncommercial 1.0.0&lt;/a&gt;. Source-available, non-commercial use only. Provided as-is, with &lt;strong&gt;no warranty and no liability&lt;/strong&gt;. Pasta Inspector is &lt;strong&gt;not a safety device&lt;/strong&gt;: it can miss a failed print, and it can stop a good one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fpasta-inspector.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fpasta-inspector.webp" width="800" alt="The Pasta Inspector, a detective with a magnifying glass, crouching on a printer bed to examine a Benchy printed in spaghetti and meatballs next to a good one" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A standalone, offline "spaghetti detector" for Klipper printers that runs &lt;strong&gt;entirely on a ~£15 ESP32-S3 camera board&lt;/strong&gt;, or &lt;strong&gt;on the Klipper host itself with any USB webcam&lt;/strong&gt;. There is no Docker, no cloud and no account. The camera talks to Moonraker over your own &lt;strong&gt;WiFi&lt;/strong&gt;, or over a &lt;strong&gt;USB cable&lt;/strong&gt; plugged into the printer (for example an Elegoo Neptune 4 running OpenNept4une). When it sees filament where the G-code says there shouldn't be any, it works out &lt;strong&gt;which part failed&lt;/strong&gt;. If it is sure, it &lt;strong&gt;excludes just that part&lt;/strong&gt; and the rest of the plate carries on. If not, it &lt;strong&gt;pauses the print&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two versions, one program
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;ESP32 camera&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Klipper host + any USB webcam&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What you need&lt;/td&gt;
&lt;td&gt;A ~£15 ESP32-S3 camera board&lt;/td&gt;
&lt;td&gt;The computer that already runs Klipper (a Pi, thin client, mini PC) and any USB webcam, for example a 3–5 megapixel one or the one crowsnest already uses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Folder&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/tree/main/esp32" rel="noopener noreferrer"&gt;&lt;code&gt;esp32/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/tree/main/klipper-host" rel="noopener noreferrer"&gt;&lt;code&gt;klipper-host/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set up&lt;/td&gt;
&lt;td&gt;Flash it with the Flasher, then the steps below&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bash pasta-inspector/klipper-host/install.sh&lt;/code&gt; (&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/KLIPPER_HOST.md" rel="noopener noreferrer"&gt;docs/KLIPPER_HOST.md&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs the other one?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No.&lt;/strong&gt; No ESP32 is involved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra&lt;/td&gt;
&lt;td&gt;Can talk to the printer over a USB cable instead of WiFi&lt;/td&gt;
&lt;td&gt;Can ask a pre-trained neural network for a second opinion (optional, &lt;code&gt;install.sh --ai&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both run the &lt;strong&gt;same program&lt;/strong&gt; from &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/tree/main/shared" rel="noopener noreferrer"&gt;&lt;code&gt;shared/&lt;/code&gt;&lt;/a&gt;: the detector, the printer-sensor watch, the web page, every setting and the Moonraker link. A fix there reaches both. &lt;em&gt;The Klipper-host version is new in 0.6 and has not yet been run on a real printer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fweb-ui.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fweb-ui.webp" alt="On-device web page showing the live overlay, status and settings" width="800" height="1274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Screenshots are of the included simulator (pretend printer + camera), not of real hardware.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fhow-it-works.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fhow-it-works.svg" alt="Pipeline: camera frame and Klipper object outlines go into a comparison; new material outside every outline leads to EXCLUDE_OBJECT or PAUSE" width="900" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reads the G-code's own idea of where plastic belongs.&lt;/strong&gt; Klipper's &lt;code&gt;[exclude_object]&lt;/code&gt; gives every object's outline. Moonraker also reports where the toolhead is, how much filament has been used and the print state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projects those outlines into the camera image.&lt;/strong&gt; This uses a one-off calibration where you click 4 bed points. Each outline is swept upwards to the current print height.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compares every frame with a reference image.&lt;/strong&gt; It masks out the toolhead along the path it has just travelled and treats its shadow specially. The threshold adapts to the camera's noise, and lighting changes and small camera shakes are allowed for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flags new material outside every outline&lt;/strong&gt;, and &lt;strong&gt;changes to the finished lower part of each object&lt;/strong&gt;. Spaghetti, a blob dragged across the bed, or a part knocked loose all count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Waits until it's sure.&lt;/strong&gt; A change has to stay in the same place for 3 frames (by default) before it counts. Shadows, reflections and noise move around, so they never do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works out which part failed&lt;/strong&gt;, from two clues:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;where the mess is:&lt;/strong&gt; next to one part and near no other&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;when it grew:&lt;/strong&gt; only while one part was being printed. Klipper says which part that is. This is the clue that still works when parts stand close together.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acts:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;If it is sure which part failed and other parts remain, it sends &lt;code&gt;EXCLUDE_OBJECT NAME=...&lt;/code&gt; and the rest prints on.&lt;/li&gt;
&lt;li&gt;Otherwise it sends &lt;code&gt;PAUSE&lt;/code&gt;, or &lt;code&gt;EMERGENCY STOP&lt;/code&gt; / a console warning, whichever you choose. It also does that after 3 excluded parts in one print, when trouble comes back within a minute of an exclusion, and when the failed part is the only one still being printed.&lt;/li&gt;
&lt;li&gt;It only acts on fresh information: if the link to the printer is down, or the printer's status is more than a few seconds old, it does not judge the print at all, and an action that could not be sent is tried again when the link is back.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can switch excluding off, or change the limit, under &lt;em&gt;Detector&lt;/em&gt; (&lt;strong&gt;If one part fails and the others are fine&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At the start of every print it checks its own calibration,&lt;/strong&gt; so a camera that got nudged still lines up. It does this once the first layers are down (not while the printer is still heating and probing):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the slicer script is installed, each print carries &lt;strong&gt;4 small reference marks&lt;/strong&gt; on its first layer. The camera finds them and corrects its calibration.&lt;/li&gt;
&lt;li&gt;If there are no marks (no slicer script, a print too big to leave room, or marks it can't see), it uses &lt;strong&gt;the print itself&lt;/strong&gt;: the edges of the parts' first layers, which Klipper's outlines say where to expect.&lt;/li&gt;
&lt;li&gt;If neither can be trusted, it changes nothing. The same goes when watching only starts well into a print (the camera was switched on late): the status then says the check was skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dry run is on by default.&lt;/strong&gt; The camera only logs what it &lt;em&gt;would&lt;/em&gt; have done until you switch dry run off.&lt;/p&gt;

&lt;p&gt;It runs at the 1–5 fps you'd expect from an ESP32-S3. The detector itself takes about 1 ms per frame on a PC. On the ESP32 it hasn't been measured yet, but we expect a few tens of milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How well does it work?&lt;/strong&gt; On 8,640 randomised simulated prints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4 false alarms on 2,760 clean prints&lt;/strong&gt; with shadows, lighting jumps, camera shake, slightly-off calibration and a position report that runs ahead of the toolhead. All four were pauses, on tightly packed plates in the hardest conditions.&lt;/li&gt;
&lt;li&gt;With parts spread over the bed:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spaghetti found 98–100%&lt;/strong&gt; of the time, typically within 2.5–8.5 s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knocked-off parts found 71–85%&lt;/strong&gt; of the time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small blobs found 45–91%&lt;/strong&gt; of the time.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excluding just the failed part&lt;/strong&gt; (spaghetti, several parts on the bed):

&lt;ul&gt;
&lt;li&gt;Parts spread out: the rest of the plate was saved in 39–59% of prints. Nearly all the others were paused.&lt;/li&gt;
&lt;li&gt;Parts packed 8 mm apart: saved in 36–48%, and in 24–27% in hard conditions. Without the timing clue it was 1%.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;good part was excluded in 0–2%&lt;/strong&gt; of prints (4% on a bed-slinger in hard conditions).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tightly packed plates hide failures.&lt;/strong&gt; With 9 parts 8 mm apart, spaghetti was found 82–87% of the time and took about a minute. Knocked-off parts and small blobs were mostly missed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blind to mess on top of a part's own outline.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A camera knocked 6 px after calibration&lt;/strong&gt; caused false alarms on 41% of clean prints. Using the print's own outline to re-align: 2.5%. With reference marks from the slicer script: 0%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are simulated, not real-world, numbers. Details are in &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/ENGINE_TESTS.md" rel="noopener noreferrer"&gt;docs/ENGINE_TESTS.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exists and what doesn't
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;G-code-aware detector (C++, shared by firmware and PC tools)&lt;/td&gt;
&lt;td&gt;Written. Unit tests, a randomised simulated-print benchmark, sanitiser and fuzz runs: see &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/ENGINE_TESTS.md" rel="noopener noreferrer"&gt;ENGINE_TESTS.md&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESP32-S3 firmware (camera, Moonraker link, web UI, settings)&lt;/td&gt;
&lt;td&gt;Written. Built and linked for both boards with the real ESP32 compiler and the official Arduino core; the images ship with the Flasher (&lt;code&gt;esp32/firmware/prebuilt/&lt;/code&gt;). &lt;strong&gt;Not yet flashed or run on a real board&lt;/strong&gt;; the PlatformIO build first runs on GitHub when you publish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-device web UI (calibrate, status, settings, dry run)&lt;/td&gt;
&lt;td&gt;Written, tested against the simulator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;USB link&lt;/strong&gt; (camera's native USB port ↔ a small bridge service on the printer's computer)&lt;/td&gt;
&lt;td&gt;Written. The bridge and the protocol are tested end to end on a PC with a pretend camera and a fake Moonraker. &lt;strong&gt;Not run on a real camera or printer&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Slicer script&lt;/strong&gt; (PrusaSlicer, OrcaSlicer, SuperSlicer, Cura): object outlines + reference marks&lt;/td&gt;
&lt;td&gt;Written. Tested on G-code from PrusaSlicer 2.7.2 and on hand-made Cura-style and Orca-style files. &lt;strong&gt;Not tested on real OrcaSlicer or Cura output, and no processed file has been printed&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Calibration check at the start of each print&lt;/strong&gt;, from printed reference marks or from the print's own outline&lt;/td&gt;
&lt;td&gt;Written, tested on simulated prints only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Camera setup tab&lt;/strong&gt; in the desktop tool: guided calibration on the live picture for any camera position, and a slicer script written for that camera's view&lt;/td&gt;
&lt;td&gt;Written. Its logic is tested with a stand-in camera. &lt;strong&gt;Not used with a real camera yet&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Calibration print&lt;/strong&gt; (bed-sized STL, click its squares to calibrate, tower for the camera angle, skew check for Klipper)&lt;/td&gt;
&lt;td&gt;Written. The STL is checked to be closed and the skew numbers against Klipper's formula. &lt;strong&gt;Not printed yet&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Klipper-host version&lt;/strong&gt; (&lt;code&gt;klipper-host&lt;/code&gt;): the firmware built for Linux, with a USB webcam via crowsnest or &lt;code&gt;/dev/video*&lt;/code&gt;, installer with a systemd service and Moonraker's update manager&lt;/td&gt;
&lt;td&gt;Written. Runs end to end against a pretend printer and webcam on a PC, also under ThreadSanitizer and AddressSanitizer, and was reviewed independently. CI is set up to build and test it on x86 and ARM (it first runs when the repository is published). &lt;strong&gt;Not yet run on a Raspberry Pi, with a real webcam or with a real printer&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Klipper macros (&lt;code&gt;PASTA_INSPECTOR_SNAP&lt;/code&gt;, &lt;code&gt;PASTA_INSPECTOR_REFS&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Written. Templates checked with Jinja2, &lt;strong&gt;not run in Klipper&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elegoo Neptune 4 / 4 Pro guide (OpenNept4une, USB)&lt;/td&gt;
&lt;td&gt;Written from documentation, &lt;strong&gt;not tried on a printer&lt;/strong&gt;: &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/NEPTUNE4.md" rel="noopener noreferrer"&gt;docs/NEPTUNE4.md&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop tool (live view, calibration, record, replay, tuning)&lt;/td&gt;
&lt;td&gt;Written, tested against the simulator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PC simulator and fake Moonraker for bench testing&lt;/td&gt;
&lt;td&gt;Written, tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;The printer's own sensors&lt;/strong&gt; (filament sensors, hotend and part fan tachometers, hotend heater power), both versions&lt;/td&gt;
&lt;td&gt;Written. Found by name and watched while printing; tested against Klipper-shaped messages and a pretend printer. &lt;strong&gt;Not run against a real Klipper or sensor&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Neural network second opinion&lt;/strong&gt; (Klipper host only, optional): PrintGuard's pre-trained network, downloaded on request, run by Pasta Inspector itself&lt;/td&gt;
&lt;td&gt;Written. Gives the same numbers as ONNX Runtime on the same file. &lt;strong&gt;Its accuracy on real prints is untested here&lt;/strong&gt;; by default it only advises. On the ESP32: no network fits, see &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/RESEARCH_NOTES.md" rel="noopener noreferrer"&gt;RESEARCH_NOTES&lt;/a&gt; section 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram/Discord/phone notifications&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not started.&lt;/strong&gt; Actions appear in the Klipper console and the camera's event log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saving snapshots of detections&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Not started&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hardware
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Either a USB webcam on the Klipper host&lt;/strong&gt; (see &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/KLIPPER_HOST.md" rel="noopener noreferrer"&gt;docs/KLIPPER_HOST.md&lt;/a&gt;; then skip to step 3 below), &lt;strong&gt;or:&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An ESP32-S3 camera board with PSRAM.&lt;/strong&gt; Pin maps are included for:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seeed XIAO ESP32S3 Sense&lt;/strong&gt; (default)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Espressif ESP32-S3-EYE&lt;/strong&gt;, and boards that copy its pinout, such as the common "Freenove ESP32-S3-WROOM CAM"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The old ESP32-CAM (non-S3) is &lt;strong&gt;not&lt;/strong&gt; supported.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A USB data cable&lt;/strong&gt;, if you want to use USB instead of WiFi. On boards with two USB sockets, use the one marked USB or OTG (the ESP32-S3's own USB), not the UART one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rigid mount&lt;/strong&gt; that sees the whole bed. A mount on the printer frame is best. On a bed-slinger (Ender-3 style), either mount the camera on the bed or tick the &lt;em&gt;bed-slinger&lt;/em&gt; option (see Known limitations).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steady lighting.&lt;/strong&gt; An always-on LED strip helps a lot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Klipper printer with Moonraker&lt;/strong&gt; (Mainsail/Fluidd setups, Forge-X/ZMOD on the Flashforge AD5M, etc.) and &lt;code&gt;[exclude_object]&lt;/code&gt; enabled.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Set it up, step by step
&lt;/h2&gt;

&lt;p&gt;Using a webcam on the Klipper host instead of a camera board? &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/KLIPPER_HOST.md" rel="noopener noreferrer"&gt;docs/KLIPPER_HOST.md&lt;/a&gt; replaces steps 1 and 2; then go on with step 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Flash the camera
&lt;/h3&gt;

&lt;p&gt;Open the &lt;strong&gt;Flasher&lt;/strong&gt;: in the &lt;code&gt;esp32&lt;/code&gt; folder, double-click &lt;code&gt;Flasher.bat&lt;/code&gt; (Windows), &lt;code&gt;Flasher.command&lt;/code&gt; (macOS) or &lt;code&gt;Flasher.sh&lt;/code&gt; (Linux), or run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 esp32/flasher.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick your board (XIAO ESP32S3 Sense, or ESP32-S3-EYE / Freenove ESP32-S3 CAM), plug the camera in by USB and press &lt;strong&gt;Flash&lt;/strong&gt;. The firmware for both boards ships in &lt;code&gt;esp32/firmware/prebuilt/&lt;/code&gt;, so nothing is built or downloaded, except Espressif's esptool, which the Flasher installs into a private folder (&lt;code&gt;~/PastaInspector/esp-venv&lt;/code&gt;) when you press &lt;strong&gt;Install esptool&lt;/strong&gt; the first time. It finds the camera's port by itself, checks the firmware file against its recorded SHA-256, and asks before it writes anything. Tick &lt;strong&gt;Erase everything first&lt;/strong&gt; the first time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The shipped firmware has been built and linked, but not yet run on a board.&lt;/strong&gt; Its status is in &lt;code&gt;esp32/firmware/prebuilt/&amp;lt;board&amp;gt;/build-info.json&lt;/code&gt; and on the Flasher's screen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux:&lt;/strong&gt; if the flash says &lt;em&gt;permission denied&lt;/em&gt;, run &lt;code&gt;sudo usermod -aG dialout $USER&lt;/code&gt;, then log out and back in. The window needs Tk: &lt;code&gt;sudo apt install python3-tk&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Board not found:&lt;/strong&gt; hold &lt;strong&gt;BOOT&lt;/strong&gt;, tap &lt;strong&gt;RESET&lt;/strong&gt;, release &lt;strong&gt;BOOT&lt;/strong&gt;, and press Flash again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers can still build and flash with PlatformIO: &lt;code&gt;python3 pasta_inspector.py flash&lt;/code&gt; (or &lt;code&gt;flash eye&lt;/code&gt;). It installs PlatformIO for your user the first time (a big download). &lt;code&gt;esp32/tools/build_firmware.py&lt;/code&gt; builds the shipped images without PlatformIO, from the official arduino-esp32 2.0.17 package.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Over USB to the printer already?&lt;/strong&gt; Stop the bridge first (&lt;code&gt;sudo systemctl stop pasta-inspector-bridge&lt;/code&gt; on the printer's computer): while it runs it keeps the camera's port to itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also flash a firmware built on GitHub: each zip on the repository's &lt;em&gt;Releases&lt;/em&gt; page comes with a &lt;code&gt;FLASHING.txt&lt;/code&gt; that gives the addresses and the command.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Connect it to your WiFi and printer
&lt;/h3&gt;

&lt;p&gt;On first boot the camera makes its own WiFi network called &lt;strong&gt;&lt;code&gt;PastaInspector-XXXX-YY&lt;/code&gt;&lt;/strong&gt;, with password &lt;strong&gt;&lt;code&gt;spaghetti&lt;/code&gt;&lt;/strong&gt; (the two letters at the end are chosen once at random, so cameras in one room are easy to tell apart).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Join that network and open &lt;strong&gt;&lt;a href="http://192.168.4.1" rel="noopener noreferrer"&gt;http://192.168.4.1&lt;/a&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Fill in &lt;strong&gt;Device &amp;amp; printer&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;your WiFi name and password&lt;/li&gt;
&lt;li&gt;the printer's IP address (Moonraker, port &lt;strong&gt;7125&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Save device settings&lt;/strong&gt;, then &lt;strong&gt;Reboot&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After the reboot the camera is at &lt;strong&gt;&lt;a href="http://pasta-inspector.local" rel="noopener noreferrer"&gt;http://pasta-inspector.local&lt;/a&gt;&lt;/strong&gt;, or at its IP address, which your router can show you. Its own setup network switches off once it is on your WiFi. If it cannot get onto your WiFi when it starts (wrong password, router still booting), the setup network comes back after 20 seconds and the camera keeps trying your WiFi once a minute.&lt;/p&gt;

&lt;p&gt;A first-start notice explains the risks. Tick &lt;em&gt;I understand&lt;/em&gt; to continue.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fweb-ui-disclaimer.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fweb-ui-disclaimer.webp" alt="First-start notice" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Make Klipper report object outlines
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add this to &lt;code&gt;printer.cfg&lt;/code&gt; and restart Klipper:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nn"&gt;[exclude_object]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PAUSE&lt;/code&gt; needs &lt;code&gt;[pause_resume]&lt;/code&gt;, which Mainsail and Fluidd setups already have. &lt;code&gt;[respond]&lt;/code&gt; is optional: with it, the camera's reasons also show up in the Klipper console. Without it the camera still pauses and excludes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make your slicer label objects:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PrusaSlicer 2.7+ and OrcaSlicer&lt;/strong&gt; have a &lt;em&gt;Label objects&lt;/em&gt; option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cura&lt;/strong&gt; needs Moonraker's G-code processing (&lt;code&gt;[file_manager] enable_object_processing: True&lt;/code&gt; in &lt;code&gt;moonraker.conf&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;See &lt;a href="https://docs.mainsail.xyz/features/exclude-objects" rel="noopener noreferrer"&gt;Mainsail's exclude-objects guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Or install the slicer script, which adds the outlines itself for all of these slicers.&lt;/p&gt;

&lt;p&gt;Without object outlines the camera has nothing to compare against. The status panel then says &lt;em&gt;Objects: none&lt;/em&gt;. If even one part in a print has no outline, the camera does not judge that print at all and the status says why.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Calibrate (once, and again if the camera moves)
&lt;/h3&gt;

&lt;p&gt;Every camera sits somewhere different: above, at the front, off to one side. Calibration tells it where the bed is in its picture. The easiest way is the desktop tool's &lt;strong&gt;Camera setup&lt;/strong&gt; tab (&lt;code&gt;python3 pasta_inspector.py&lt;/code&gt;, then &lt;em&gt;Connect&lt;/em&gt;), which shows the live picture and walks you through it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fgui-setup.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fgui-setup.webp" alt="Camera setup tab: live picture with the calibrated bed outline and grid, the nozzle position, and a preview of where a G-code file's parts and reference marks land" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your bed.&lt;/strong&gt; Pick the size, or type it. Tick &lt;em&gt;Bed-slinger&lt;/em&gt; if the bed moves and the camera is fixed to the frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mark four spots.&lt;/strong&gt; Any of the three ways works for any camera position:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bed corners:&lt;/strong&gt; click the four corners in the picture, starting front-left.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nozzle:&lt;/strong&gt; if the camera can't see all four corners, move the nozzle to four far-apart spots about 1 mm above the bed (with your printer's own controls) and click its tip each time. The tool reads X and Y from the printer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibration print:&lt;/strong&gt; press &lt;em&gt;Make print STL...&lt;/em&gt;, print it, leave it on the bed and click the centres of its four corner squares. Nothing needs to be moved by hand, and square centres are easier to hit than bed corners hidden under clips. See The calibration print.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On a bed-slinger, home the printer first, leave the bed where it stops, and use the corners; the tool remembers that bed position.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Camera angle.&lt;/strong&gt; Raise the nozzle 30 mm or more, press &lt;em&gt;Measure&lt;/em&gt; and click its tip. With the calibration print, press &lt;em&gt;Use the tower&lt;/em&gt; and click the middle of the tower's top instead. This tells the camera how far parts lean in its picture as they get taller. A camera looking straight down gives about 0. You can skip this and tune &lt;em&gt;Height shift X/Y&lt;/em&gt; later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check and save.&lt;/strong&gt; The cyan outline should sit on the edge of the bed, and the pink &lt;strong&gt;+&lt;/strong&gt; should follow the nozzle. With the calibration print, the orange outline should sit on the print, including the small tab and the tower's top. Then &lt;em&gt;Save to the camera&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slicer script&lt;/strong&gt; (optional): see below.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can also calibrate without the desktop tool: press &lt;strong&gt;Calibrate bed&lt;/strong&gt; in the camera's web page and click the four bed corners (front-left, front-right, back-right, back-left), or type the coordinates of four other spots you know. Set the bed size under &lt;em&gt;Detector → Advanced&lt;/em&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  The calibration print
&lt;/h4&gt;

&lt;p&gt;A bed-sized print that takes about 5 g of filament: a thin border 10 mm inside the bed edge, four 20 mm squares near the corners (numbered 1 front-left, 2 front-right, 3 back-right, 4 back-left), a small tab just right of square 1 so you can tell which corner is which, and a hollow 20 mm tower in the middle. Everything except the tower is 0.6 mm tall.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make it&lt;/strong&gt; in the &lt;em&gt;Camera setup&lt;/em&gt; tab (&lt;em&gt;Make print STL...&lt;/em&gt;: set the gap from the bed edge and the tower height), or from the command line: &lt;code&gt;python3 pasta_inspector.py calprint stl --bed 220x220&lt;/code&gt; (&lt;code&gt;--margin 15&lt;/code&gt; if clips or a wiper are in the way, &lt;code&gt;--tower 0&lt;/code&gt; for no tower).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slice it&lt;/strong&gt; with your usual settings. Leave it where the slicer puts it (centred) and do not rotate it: the camera setup assumes each square is where the file put it. Use PETG or ABS if the bed stays hot, since PLA softens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skew check (optional).&lt;/strong&gt; With a steel rule, measure between the outer corners of the corner squares (the corner nearest each bed corner): AC is square 1 to square 3, BD is square 4 to square 2, AD is square 1 to square 2. These are Klipper's letters. Type them into &lt;em&gt;Skew...&lt;/em&gt; or run &lt;code&gt;python3 pasta_inspector.py calprint skew AC BD AD&lt;/code&gt;. You get the skew angle and the &lt;code&gt;SET_SKEW XY=...&lt;/code&gt; line for Klipper's &lt;code&gt;[skew_correction]&lt;/code&gt;. The camera cannot measure skew itself: perspective hides it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How much it helps:&lt;/strong&gt; in simulated prints, a perfect calibration found no more failures and gave no fewer false alarms than an ordinary hand-clicked one, because the camera re-aligns at the start of every print anyway (see &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/RESEARCH_NOTES.md" rel="noopener noreferrer"&gt;RESEARCH_NOTES.md&lt;/a&gt;, section 11). The print makes setup easier and gives you a skew check. It does not make detection more accurate. &lt;strong&gt;It has not been printed yet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next, start a print and check that the &lt;strong&gt;green&lt;/strong&gt; areas sit on the parts as they grow. If they lean the wrong way as the print gets taller, adjust &lt;em&gt;Height shift X/Y&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Watch a few prints in dry run, then arm it
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Leave &lt;strong&gt;Dry run&lt;/strong&gt; ticked for a few prints. Any detections appear in the event log as &lt;code&gt;DRY RUN would pause...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When you trust it, untick &lt;strong&gt;Dry run&lt;/strong&gt;. The header chip turns &lt;strong&gt;ARMED&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  USB instead of WiFi
&lt;/h2&gt;

&lt;p&gt;The ESP32-S3 has its own USB port, so one cable to the printer can carry both power and data. A small service on the printer's computer (the &lt;strong&gt;bridge&lt;/strong&gt;) passes messages between the camera and Moonraker, and puts the camera's web page on &lt;strong&gt;port 7130&lt;/strong&gt; of the printer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;camera ──USB──&amp;gt; printer's computer: pasta-inspector-bridge ──&amp;gt; Moonraker (127.0.0.1:7125)
                                     └─ web page at http://&amp;lt;printer-ip&amp;gt;:7130
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the printer's computer (over SSH), as the normal user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Dorkknight/pasta-inspector.git
bash pasta-inspector/esp32/bridge/install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer sets up the service, adds you to the &lt;code&gt;dialout&lt;/code&gt; group, copies the optional Klipper macros and adds &lt;code&gt;[include pasta_inspector.cfg]&lt;/code&gt; to &lt;code&gt;printer.cfg&lt;/code&gt; (a backup is kept). &lt;code&gt;--uninstall&lt;/code&gt; undoes it.&lt;/p&gt;

&lt;p&gt;The bridge only opens USB devices made by Espressif (the ESP32's maker), never Klipper's own boards, and sends nothing to a port until a Pasta Inspector has said hello on it. It passes on to Moonraker only the few requests the camera needs (see &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Then plug the camera in and open &lt;code&gt;http://&amp;lt;printer-ip&amp;gt;:7130&lt;/code&gt;. &lt;em&gt;Link mode&lt;/em&gt; under &lt;em&gt;Device &amp;amp; printer&lt;/em&gt; chooses between &lt;strong&gt;Auto&lt;/strong&gt; (USB when the bridge answers, otherwise WiFi), &lt;strong&gt;WiFi only&lt;/strong&gt; and &lt;strong&gt;USB only&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The full walk-through for the &lt;strong&gt;Elegoo Neptune 4 / 4 Pro with OpenNept4une&lt;/strong&gt; is in &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/NEPTUNE4.md" rel="noopener noreferrer"&gt;docs/NEPTUNE4.md&lt;/a&gt;. The same steps should apply to any Klipper host with a free USB port (a Raspberry Pi, for example); none has been tried yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slicer script: outlines and reference marks
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;slicer/pasta_inspector_post.py&lt;/code&gt; is a post-processing script for &lt;strong&gt;PrusaSlicer, OrcaSlicer, SuperSlicer and Cura&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best way to set it up&lt;/strong&gt; is step 5 of the desktop tool's &lt;em&gt;Camera setup&lt;/em&gt; tab: &lt;strong&gt;Write the slicer script&lt;/strong&gt;. It writes a copy of the script with &lt;strong&gt;your camera's view built in&lt;/strong&gt; (to &lt;code&gt;~/PastaInspector/slicer/&lt;/code&gt;), installs it into Cura if Cura is on that computer, and gives you the line to paste into PrusaSlicer/OrcaSlicer (&lt;em&gt;Output options → Post-processing scripts&lt;/em&gt;). The reference marks are then printed &lt;strong&gt;where your camera can see them&lt;/strong&gt;, and made bigger (up to 20 mm) if they would be too small in its picture. &lt;em&gt;Preview a G-code file&lt;/em&gt; shows on the live picture where a file's parts and marks will land. Do it again whenever you re-calibrate.&lt;/p&gt;

&lt;p&gt;The same from a terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 pasta_inspector.py slicer &lt;span class="nt"&gt;--camera&lt;/span&gt; http://pasta-inspector.local    &lt;span class="c"&gt;# or http://&amp;lt;printer-ip&amp;gt;:7130 over USB&lt;/span&gt;
python3 pasta_inspector.py slicer                                        &lt;span class="c"&gt;# without a camera view&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Cura (4.9 or newer): &lt;em&gt;Extensions → Post Processing → Modify G-Code → Add a script → Pasta Inspector&lt;/em&gt;. If you copy the script into Cura's &lt;code&gt;scripts&lt;/code&gt; folder by hand, name it &lt;code&gt;PastaInspector.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each sliced file then gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Object outlines for Klipper&lt;/strong&gt; (&lt;code&gt;EXCLUDE_OBJECT_DEFINE&lt;/code&gt; and start/end markers), worked out from each object's own moves, unless the slicer already wrote them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4 reference marks:&lt;/strong&gt; 10 mm squares, one layer thick, printed about 14 mm outside the corners of your parts, just before the first part starts. With your camera's view built in, corners the camera can't see are swapped for free spots it can. They are ordinary Klipper objects named &lt;code&gt;PASTA_INSPECTOR_REF_1..4&lt;/code&gt;. The camera reads their positions from Klipper, looks for them in the image once the first layer is down, and corrects its calibration for that print: a shift, or with all 4 marks a shift plus a small turn. Corrections larger than 24 px are refused, and the status then tells you to recalibrate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optionally&lt;/strong&gt; (&lt;code&gt;--snap-every N&lt;/code&gt;), a call to the &lt;code&gt;PASTA_INSPECTOR_SNAP&lt;/code&gt; macro every N layers, which parks the toolhead for a moment so the camera gets a clear view. This is mainly for bed-slingers. It is left out of files printed one object at a time ("complete objects") and of spiral-vase files, where parking would run into a finished part or leave a blob; the macro itself also refuses to move when the nozzle is lower than it was at an earlier call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Options: &lt;code&gt;--no-refs&lt;/code&gt;, &lt;code&gt;--ref-size 8&lt;/code&gt;, &lt;code&gt;--ref-margin 20&lt;/code&gt;, &lt;code&gt;--bed 225x225&lt;/code&gt; (for files that do not state their bed size; if the file states one too, marks are kept on the part of the bed both agree on), &lt;code&gt;--no-outlines&lt;/code&gt;, &lt;code&gt;--snap-every 5&lt;/code&gt;, &lt;code&gt;--camera view.json&lt;/code&gt;, &lt;code&gt;--no-camera&lt;/code&gt;, &lt;code&gt;--strict&lt;/code&gt;. Running the script twice on one file does nothing the second time.&lt;/p&gt;

&lt;p&gt;When the script has to skip something, it says why in &lt;code&gt;; Pasta Inspector note:&lt;/code&gt; lines at the top of the G-code file. Slicers only show a script's messages when it fails, so look there; or add &lt;code&gt;--strict&lt;/code&gt;, which makes the export fail with the reason instead. If anything unexpected happens while it works, it leaves your file exactly as the slicer wrote it.&lt;/p&gt;

&lt;p&gt;Files sliced with object labels for Marlin (&lt;code&gt;M486&lt;/code&gt;) are understood too: the script turns them into Klipper's labels.&lt;/p&gt;

&lt;p&gt;If the print leaves no room for marks, or none where the camera can see, the script says so in a note at the top of the file and prints none. The camera then lines itself up on the parts' own outlines instead. With a camera view the script also warns when a part is partly outside the camera's picture, where it can't be watched.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fslicer-marks.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fslicer-marks.webp" alt="First layer of a processed file: three parts with four small square reference marks around them" width="769" height="665"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script leaves the marks out, and writes the reason at the top of the file, whenever it can't be sure they are safe: with a raft, a round bed, an unknown bed size, or a file that never says where the nozzle is before the first part. It keeps them clear of skirts, brims, purge lines and wipe towers that are in the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the first layer of a processed file in your slicer's or Mainsail/Fluidd's G-code viewer before you print it.&lt;/strong&gt; The marks are extra moves added to your file, and no processed file has been printed yet. In particular, a purge line drawn by a &lt;code&gt;PRINT_START&lt;/code&gt; macro is not in the file, so the script cannot avoid it: if a mark lands on yours, move the marks with &lt;code&gt;--ref-margin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Reference marks can also be given by hand, without the slicer script: &lt;code&gt;PASTA_INSPECTOR_REFS POINTS=[[20,20],[200,20],[200,200],[20,200]] SIZE=10&lt;/code&gt; (from &lt;code&gt;klipper/pasta_inspector.cfg&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The desktop tool, tab by tab
&lt;/h2&gt;

&lt;p&gt;Start it with &lt;code&gt;python3 pasta_inspector.py&lt;/code&gt;. Type the camera's address, or press &lt;strong&gt;Use simulator&lt;/strong&gt;, then &lt;strong&gt;Connect&lt;/strong&gt;. The version and build date are always shown in the title and status bar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Camera setup&lt;/strong&gt;: the guided calibration and slicer-script export described in step 4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live&lt;/strong&gt;: the camera view with the overlay, status and event log, the &lt;em&gt;Detection enabled&lt;/em&gt; and &lt;em&gt;Dry run&lt;/em&gt; switches, and a quick four-corner calibration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;green&lt;/strong&gt;: plastic allowed by the G-code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;teal&lt;/strong&gt;: finished part bodies (watched)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;blue&lt;/strong&gt;: ignored (toolhead / excluded parts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;orange&lt;/strong&gt;: suspicious, not yet confirmed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;red&lt;/strong&gt;: anomaly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dark&lt;/strong&gt;: off the bed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fgui-live.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fgui-live.webp" alt="Live tab: spaghetti next to BRACKET_2 has been detected and the object excluded" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detector&lt;/strong&gt;: every detector setting, with load/send to the camera and open/save as JSON.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fgui-detector.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fgui-detector.webp" alt="Detector tab" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Record&lt;/strong&gt;: saves frames, plus the printer state at that moment, into a session folder. Each frame is about 19 KB. Record good prints &lt;em&gt;and&lt;/em&gt; failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replay&lt;/strong&gt;: runs the exact firmware detector (&lt;code&gt;sc_replay&lt;/code&gt;) over a recorded session, either with the session's own settings or with the Detector tab's. It lists which frames would have triggered and lets you scrub through the overlay. This is the way to tune thresholds without printing anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fgui-replay.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fgui-replay.webp" alt="Replay tab showing two actions over a recorded session" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Help&lt;/strong&gt;: the setup steps in short form.&lt;/p&gt;

&lt;p&gt;The camera's own web page works on a phone too:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fweb-ui-phone.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fpasta-inspector%2Fmain%2Fdocs%2Fimages%2Fweb-ui-phone.webp" alt="Web UI on a phone" width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without hardware
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 pasta_inspector.py sim            &lt;span class="c"&gt;# pretend camera + printer at http://127.0.0.1:8080&lt;/span&gt;
python3 pasta_inspector.py sim &lt;span class="nt"&gt;--auto&lt;/span&gt;     &lt;span class="c"&gt;# spaghetti appears by itself after ~20 s (dry run: only logged)&lt;/span&gt;
python3 pasta_inspector.py sim &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="nt"&gt;--live&lt;/span&gt;   &lt;span class="c"&gt;# ... and the pretend printer really gets the command&lt;/span&gt;
python3 pasta_inspector.py                &lt;span class="c"&gt;# desktop tool -&amp;gt; "Use simulator"&lt;/span&gt;
python3 pasta_inspector.py host-demo      &lt;span class="c"&gt;# Linux: the Klipper-host version against a pretend printer + webcam&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simulator serves the real web page and runs the real C++ detector. Like a new camera it starts in dry run. Its bar at the top can add spaghetti, flicker the lights or restart the print.&lt;/p&gt;

&lt;p&gt;To bench-test a &lt;strong&gt;real camera without a real printer&lt;/strong&gt;, run the fake Moonraker on your PC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 pasta_inspector.py fake-printer   &lt;span class="c"&gt;# listens on port 7125&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set the camera's printer host to your PC's IP.&lt;/li&gt;
&lt;li&gt;Calibrate on a sheet of paper.&lt;/li&gt;
&lt;li&gt;Drop some filament on it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;EXCLUDE_OBJECT&lt;/code&gt; / &lt;code&gt;PAUSE&lt;/code&gt; commands are printed in the terminal instead of reaching a printer.&lt;/p&gt;

&lt;h2&gt;
  
  
  More docs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/HOW_IT_WORKS.md" rel="noopener noreferrer"&gt;HOW_IT_WORKS.md&lt;/a&gt;: the detector, what it reads, what it changes and what it never does&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/KLIPPER_HOST.md" rel="noopener noreferrer"&gt;docs/KLIPPER_HOST.md&lt;/a&gt;: running it on the Klipper host with a USB webcam, instead of on a camera board&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/DETECTION_METHODS.md" rel="noopener noreferrer"&gt;docs/DETECTION_METHODS.md&lt;/a&gt;: research into further ways to catch failures (printer signals, camera methods, machine learning, licences)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/NEPTUNE4.md" rel="noopener noreferrer"&gt;docs/NEPTUNE4.md&lt;/a&gt;: Elegoo Neptune 4 / 4 Pro with OpenNept4une, over USB&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/ENGINE_TESTS.md" rel="noopener noreferrer"&gt;docs/ENGINE_TESTS.md&lt;/a&gt;: how the detector was tested, and the numbers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/API.md" rel="noopener noreferrer"&gt;docs/API.md&lt;/a&gt;: the camera's HTTP API, and the USB link protocol&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/docs/RESEARCH_NOTES.md" rel="noopener noreferrer"&gt;docs/RESEARCH_NOTES.md&lt;/a&gt;: what was looked up, tried and measured, how sure each point is, and what is still unknown. A copy is attached to every release&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/pasta-inspector/blob/main/THIRD_PARTY.md" rel="noopener noreferrer"&gt;THIRD_PARTY.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copyright Matthew Armstrong (&lt;a href="https://github.com/The-Dorkknight" rel="noopener noreferrer"&gt;The-Dorkknight&lt;/a&gt;). Licensed under the PolyForm Noncommercial License 1.0.0. This is not legal advice.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/pasta-inspector" rel="noopener noreferrer"&gt;The-Dorkknight/pasta-inspector on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>iot</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Skipper Flipper</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Sun, 01 Nov 2026 18:33:08 +0000</pubDate>
      <link>https://dev.to/open-madscience/skipper-flipper-4ec2</link>
      <guid>https://dev.to/open-madscience/skipper-flipper-4ec2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fak9gyz0w5blqezzcakp0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fak9gyz0w5blqezzcakp0.webp" alt="Building the tools in the Skipper Flipper GUI" width="799" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An experimental ESP32 pseudo Klipper port.&lt;/em&gt; (Repository and package name: &lt;code&gt;esp-printhost&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPL-3.0&lt;/strong&gt; · Linux (Mint / Ubuntu) · C++17 core + Python tools · &lt;a href="https://The-Dorkknight.github.io/esp-printhost/" rel="noopener noreferrer"&gt;Project page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Licensed under the GNU General Public License v3.0, see &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/LICENSE" rel="noopener noreferrer"&gt;LICENSE&lt;/a&gt;. &lt;strong&gt;No warranty and no liability:&lt;/strong&gt; this is provided as is. You use it at your own risk, on your own hardware, and the author is not responsible for damage to boards, printers or anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is
&lt;/h2&gt;

&lt;p&gt;An idea for reviving old, donated 3D printers: a cheap ESP32-S3 becomes the "brain" (WiFi, web UI, text config, motion planning) and the printer's original board becomes a simple slave running a modified Klipper MCU firmware. The full plan is in &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/DESIGN.md" rel="noopener noreferrer"&gt;docs/DESIGN.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target hardware: ESP32-S3 only&lt;/strong&gt; for the host ("master"). &lt;strong&gt;Status (29 October 2026): mostly working, needs tuning and proper testing.&lt;/strong&gt; On real hardware (&lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/HARDWARE_LOG.md" rel="noopener noreferrer"&gt;docs/HARDWARE_LOG.md&lt;/a&gt;), installed in a WEEDO Tina 2 with a BTT SKR Pico running plain Klipper v0.13.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the WiFi pages, console and &lt;code&gt;printer.cfg&lt;/code&gt; editor; firmware updates over WiFi;&lt;/li&gt;
&lt;li&gt;the Klipper link, clock sync, board configuration and recovery from board power cycles;&lt;/li&gt;
&lt;li&gt;temperatures, heating, PID tuning and Klipper's heater safety checks;&lt;/li&gt;
&lt;li&gt;the motor test, TMC2209 set-up, stallGuard and &lt;strong&gt;full homing of X, Y and Z&lt;/strong&gt; (2026-10-27);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;a whole print file, run end to end&lt;/strong&gt; (2026-10-27): in mid-air, because the Tina 2's flexible Z gave no reliable stop to set a Z offset from;&lt;/li&gt;
&lt;li&gt;the micro SD card, the little OLED, touch buttons through the case, the hotend fan;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the print benchmark&lt;/strong&gt; (2026-10-29): 12 layers of a print path (9,617 lines, about a million steps) through the whole motion pipeline and the real board, drivers off, in 121.8 s, with the ESP32's printer task busy about 44 % of one core, of which about 14 % is real work and the rest mostly waiting for the board's acknowledgements (&lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/BENCHMARK.md" rel="noopener noreferrer"&gt;docs/BENCHMARK.md&lt;/a&gt;);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;integer motion maths&lt;/strong&gt; (INT32/64 test build, 2026-10-29): input shaping with pressure advance costs 10 % of one core at 100 mm/s instead of 19 %; the shaping work itself is about 3.7 times faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not yet:&lt;/strong&gt; a real printed part; input shaping, pressure advance, bed mesh and the other tuning features on a moving printer; the part-cooling fan and the third fan output (possibly a broken wire). &lt;strong&gt;Next test bench:&lt;/strong&gt; a QIDI X-Smart 3 (CoreXY) with an SKR Pico; the Tina 2 is retired as the bench (no sharp Z stop, and its tiny Y motor overheats). The whole story, including every mistake: &lt;strong&gt;&lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/paper/A_FRANKENSTEIN_EXPERIENCE.md" rel="noopener noreferrer"&gt;A Frankenstein Experience&lt;/a&gt;&lt;/strong&gt; (&lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/paper/A_FRANKENSTEIN_EXPERIENCE.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;New features by date, and whether each has been on hardware, are in &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Klipper wire protocol (CRC16, VLQ ints, framing, resync, data dictionary)&lt;/td&gt;
&lt;td&gt;Unit-tested; &lt;strong&gt;works on hardware&lt;/strong&gt; (ESP32-S3 to SKR Pico)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESP32-S3 firmware (&lt;code&gt;firmware/esp32s3&lt;/code&gt;): WiFi access point, status page, console, &lt;code&gt;printer.cfg&lt;/code&gt; editor, test bench, printer page, the Klipper host's jobs (heaters, TMC2209, moves, homing, printing from flash). Shipped ready to flash&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;On hardware&lt;/strong&gt;: link, heating, PID, motor test, TMC2209, stallGuard, full homing (2026-10-27), a print file run in mid-air (2026-10-27), WiFi updates, the print and motion benchmarks (2026-10-29). Not yet: a real printed part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;skr-pico-uart&lt;/code&gt; printer-board firmware (&lt;strong&gt;plain Klipper v0.13.0&lt;/strong&gt;, serial on UART0; the ESP32-S3 firmware is the only bespoke part)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Works on hardware&lt;/strong&gt;: link, heater, thermistor, steppers, TMC UART, DIAG pins (with the 2026-10-10 build, which also held the since-removed, never-used segment queue; &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/research/segq-retired.md" rel="noopener noreferrer"&gt;why it went&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Motion planner + G-code engine (lookahead, junction deviation, trapezoids, pressure advance, G-code state)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;On hardware&lt;/strong&gt; for homing, a print file in mid-air and the benchmarks; checked in simulation for exact positions, speed and acceleration limits. Never yet used for a real part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delta kinematics (&lt;code&gt;kinematics: delta&lt;/code&gt;, &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/DELTA.md" rel="noopener noreferrer"&gt;docs/DELTA.md&lt;/a&gt;); DECKHAND needs its own delta build&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;PC-tested only&lt;/strong&gt; (head path vs the arm geometry, three-endstop homing); never on a real delta; no delta calibration yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pasta Inspector dongle support (&lt;code&gt;/websocket&lt;/code&gt;, Moonraker-style: exclude object, pause, M112)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Engine PC-tested; firmware written but not compiled or tried with a real dongle&lt;/strong&gt; (&lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/PASTA_DONGLE.md" rel="noopener noreferrer"&gt;docs/PASTA_DONGLE.md&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other board presets (generic RP2040, Creality 4.2.x, RAMPS mega2560, Ender-3 1.1.x, USB SKR Pico)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Compile only&lt;/strong&gt;; never run on hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flasher app (&lt;code&gt;flasher.py&lt;/code&gt;): Slave tab (printer board) and Master tab (ESP32-S3)&lt;/td&gt;
&lt;td&gt;ESP32-S3 flashing &lt;strong&gt;used on hardware&lt;/strong&gt; on Linux Mint. Other paths (SD card, avrdude, Windows/macOS) tested with mocks only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firmware build tool (CLI + Slave tab "My own build")&lt;/td&gt;
&lt;td&gt;Works on Linux (sandbox and the author's Linux Mint laptop); Windows needs WSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mcu_probe&lt;/code&gt;, &lt;code&gt;fake_mcu.py&lt;/code&gt;, one-command setup, Tools window&lt;/td&gt;
&lt;td&gt;Working on Linux Mint. Probe tried on an SKR Pico over USB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real printing&lt;/td&gt;
&lt;td&gt;A print file &lt;strong&gt;ran end to end on the Tina 2&lt;/strong&gt; (2026-10-27) but in mid-air (Z offset not found); upload, print from flash or SD card, pause / resume / cancel are simulated. &lt;strong&gt;No real part printed yet&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Micro SD card (&lt;code&gt;firmware/esp32s3/main/sdcard.c&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Works on hardware&lt;/strong&gt; (2026-10-19: mount, file list, upload; printing from it not yet tried): SPI card for big G-code files, file list / print / delete on the Printer page, &lt;code&gt;SDCARD_PRINT_FILE FILENAME=&lt;/code&gt;, M20/M23/M24 (the command side is unit-tested). See &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/SD_CARD.md" rel="noopener noreferrer"&gt;docs/SD_CARD.md&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buttons on the case (&lt;code&gt;firmware/esp32s3/main/panel.cpp&lt;/code&gt;, &lt;code&gt;host/src/panel.cpp&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Touch works on hardware&lt;/strong&gt; (2026-10-26: Preheat and Home from staple pads melted into the Tina 2's case); up to four touch pads (bolts on wires) or switches: Preheat, Home and E-stop (both on a 1 s hold), Unload, and more. CAPTAIN and SKIPPER. See &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/BUTTONS.md" rel="noopener noreferrer"&gt;docs/BUTTONS.md&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tuning (&lt;code&gt;host/src/shaping.cpp&lt;/code&gt;, &lt;code&gt;resonance.cpp&lt;/code&gt;, &lt;code&gt;bedmesh.cpp&lt;/code&gt;, &lt;code&gt;machine_pro.cpp&lt;/code&gt;; Tune page)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Built and PC-tested; the shaping and pressure-advance maths timed on the ESP32 (2026-10-29), not yet used on a moving printer&lt;/strong&gt; (2026-10-24): input shaping, pressure advance, accelerometer search and shaper calibration (matches Klipper's), bed mesh, screw tilt, arcs, G10/G11, exclude object, tuning tower. USB accelerometer: compile-only. CAPTAIN and SKIPPER only. See &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/TUNING.md" rel="noopener noreferrer"&gt;docs/TUNING.md&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Little OLED display (&lt;code&gt;firmware/esp32s3/main/oled.cpp&lt;/code&gt;, &lt;code&gt;components/ktft_oled&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Works on hardware&lt;/strong&gt; (2026-10-26: temperatures and the shutdown screen on a 0.96" OLED in the Tina 2); built 2026-10-23: SSD1306 / SSD1315 / SH1106, 128x64 or 128x32, on GPIO8/9: temperatures, progress bar, HEATING / HOMING / EXTRUDING / LOADING, "no board" with the WiFi name and password. See &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/OLED.md" rel="noopener noreferrer"&gt;docs/OLED.md&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three builds by board memory: DECKHAND / CAPTAIN / SKIPPER (&lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/TIERS.md" rel="noopener noreferrer"&gt;docs/TIERS.md&lt;/a&gt;), Flasher "Flash for my board"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;CAPTAIN runs on hardware&lt;/strong&gt; (2026-10-20: picked by the Flasher, 2 MB PSRAM found and used); WiFi firmware updates work (2026-10-20); the important-events log works (2026-10-28); not yet tried there: bigger look-ahead with PSRAM, slicer upload (OctoPrint-style), home WiFi, thumbnails, print history, event log, printer.cfg backups, crash reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Print benchmark, motion benchmark, INT32/64 test build (&lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/BENCHMARK.md" rel="noopener noreferrer"&gt;docs/BENCHMARK.md&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;On hardware&lt;/strong&gt; (2026-10-28/29): both benchmarks run to the end on the Tina 2 with the SKR Pico and the drivers off; results in the important-events log. The integer build has not moved motors yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fans&lt;/td&gt;
&lt;td&gt;Hotend fan &lt;strong&gt;confirmed on hardware&lt;/strong&gt;; part-cooling fan and the third fan output not yet (possibly a broken wire, will update when tested)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total checks: 1,825 C++ unit checks (run twice: with double and with integer motion maths), the OLED layout tests and 192 Python tests (including power-cycle, stallGuard and printing runs against a simulated board), all passing in the sandbox. Passing tests mean the maths and bookkeeping are right, not that a printer will behave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;p&gt;The GUI building the tools and running the unit tests:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwdyfovyfzcafenw0peq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwdyfovyfzcafenw0peq.webp" alt="Running mcu_probe against the fake MCU" width="799" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both screenshots use the &lt;strong&gt;simulated&lt;/strong&gt; board, not real hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flash a board (Linux, macOS, Windows)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 flasher.py          (Windows: double-click Flasher.bat; macOS: Flasher.command)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need Python 3.8+ with Tk (Linux: &lt;code&gt;sudo apt install python3-tk&lt;/code&gt;; on Windows and macOS the installer from python.org includes Tk). No toolchain is needed: firmware for every board preset (five boards, plus a serial variant of the SKR Pico) ships in &lt;code&gt;firmware/prebuilt/&lt;/code&gt;. Pick the &lt;strong&gt;Slave&lt;/strong&gt; tab, choose your board, press &lt;strong&gt;Flash...&lt;/strong&gt; and confirm. RP2040 boards: hold BOOTSEL while plugging in USB. STM32 boards such as the Creality 4.2.x: put the SD card in this computer, &lt;strong&gt;Browse&lt;/strong&gt; to it, flash, then move the card to the printer and power-cycle it. AVR boards need &lt;code&gt;avrdude&lt;/code&gt; installed. The &lt;strong&gt;Master&lt;/strong&gt; tab flashes an ESP32-S3 with esptool (Install esptool, then &lt;strong&gt;Use shipped firmware&lt;/strong&gt;, pick the port, Flash). The port box fills itself with the ESP32-S3's port when it can tell which one it is (otherwise pick it). The shipped image starts a WiFi network &lt;code&gt;SkipperFlipper-XXXX&lt;/code&gt; (WPA2, default password &lt;code&gt;skipperflipper&lt;/code&gt;, change it before real use) with a page at &lt;code&gt;http://192.168.4.1/&lt;/code&gt;, a live log at &lt;code&gt;/console&lt;/code&gt; and &lt;code&gt;printer.cfg&lt;/code&gt; at &lt;code&gt;/config&lt;/code&gt;; it talks Klipper to the printer board over UART (identify, clock sync, &lt;code&gt;printer.cfg&lt;/code&gt;, heaters, TMC2209 drivers, motor tests at &lt;code&gt;/test&lt;/code&gt;, homing and running a print file from the Printer page: all on hardware with the SKR Pico; a real printed part not yet). Logs appear on the chip's native USB port. &lt;strong&gt;ESP32-S3 only&lt;/strong&gt;: other ESP32 chips (original, S2, C3) are not supported, and esptool refuses to write if it finds a different chip. You can also flash your own &lt;code&gt;.bin&lt;/code&gt;. To connect the ESP32-S3 to an SKR Pico with four jumper wires, see &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/WIRING_SKR_PICO.md" rel="noopener noreferrer"&gt;docs/WIRING_SKR_PICO.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdxsgu81xrdozf74pqjjp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdxsgu81xrdozf74pqjjp.webp" alt="The Flasher, Slave tab with the Creality 4.2.x preset" width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only the &lt;code&gt;skr-pico-uart&lt;/code&gt; firmware (SKR Pico) has run on a board&lt;/strong&gt;, where it has done everything above; the other presets have never been flashed. Read &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/FIRMWARE.md" rel="noopener noreferrer"&gt;docs/FIRMWARE.md&lt;/a&gt; first, and do not flash a printer you cannot recover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start (Linux Mint / Ubuntu / Debian)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/The-Dorkknight/esp-printhost
&lt;span class="nb"&gt;cd &lt;/span&gt;esp-printhost
&lt;span class="nb"&gt;sudo &lt;/span&gt;python3 printhost.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one command installs what is missing (build tools, cmake, zlib, python3-tk, acl), gives you access to USB serial ports without logging out, stops ModemManager and brltty from grabbing printer boards, builds &lt;code&gt;mcu_probe&lt;/code&gt;, runs the tests, and opens the GUI as your normal user. It is safe to re-run. &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/HOW_IT_WORKS.md" rel="noopener noreferrer"&gt;HOW_IT_WORKS.md&lt;/a&gt; lists exactly what it changes on your system.&lt;/p&gt;

&lt;p&gt;Options: &lt;code&gt;--check&lt;/code&gt; (report only, change nothing), &lt;code&gt;--no-gui&lt;/code&gt;, &lt;code&gt;--clean&lt;/code&gt;, &lt;code&gt;--yes&lt;/code&gt;, &lt;code&gt;--version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If anything fails it prints the path of a log file in &lt;code&gt;~/esp-printhost-logs/&lt;/code&gt;. Attach that when you open an issue.&lt;/p&gt;

&lt;p&gt;macOS: the C++ library and tests build (&lt;code&gt;python3 host/tools/build.py&lt;/code&gt;), but &lt;code&gt;mcu_probe&lt;/code&gt; is Linux-only (it uses Linux's &lt;code&gt;termios2&lt;/code&gt; for 250000 baud) and the one-command setup is Linux-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the GUI
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build tools&lt;/strong&gt; compiles everything and runs the unit tests. Output is colour-coded per tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fake MCU: Start&lt;/strong&gt; runs a pretend Klipper board (no hardware). Its port appears in the Port box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run probe&lt;/strong&gt; connects, reads the board's pin list into the &lt;strong&gt;Blink pin&lt;/strong&gt; dropdown, and prints uptime and state. Tick &lt;strong&gt;Blink pin&lt;/strong&gt; and pick a pin to also toggle it 10 times.&lt;/li&gt;
&lt;li&gt;The output box is selectable (Ctrl+C, right-click, &lt;strong&gt;Copy all&lt;/strong&gt;, &lt;strong&gt;Save as...&lt;/strong&gt;). Everything is also logged to &lt;code&gt;~/esp-printhost-logs/&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it without hardware
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 host/tools/fake_mcu.py            &lt;span class="c"&gt;# prints /dev/pts/N&lt;/span&gt;
./host/build/mcu_probe /dev/pts/N 250000 &lt;span class="nt"&gt;--pins&lt;/span&gt; &lt;span class="nt"&gt;--blink&lt;/span&gt; PA5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it on a real board
&lt;/h2&gt;

&lt;p&gt;You need a board running &lt;strong&gt;stock Klipper MCU firmware&lt;/strong&gt; (the Flasher ships plain Klipper v0.13.0 builds in &lt;code&gt;firmware/prebuilt/&lt;/code&gt;, or build your own with the Firmware tab). Stop Klipper or anything else using the port, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./host/build/mcu_probe /dev/serial/by-id/&amp;lt;your-board&amp;gt; 250000 &lt;span class="nt"&gt;--pins&lt;/span&gt; &lt;span class="nt"&gt;--blink&lt;/span&gt; &amp;lt;pin&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--pins&lt;/code&gt; lists the pin names the board accepts. The probe validates your pin name before touching the board, and if the board is already configured it &lt;strong&gt;resets it&lt;/strong&gt; first (the USB device re-appears after about two seconds). Do not run it on a board that is in the middle of a print.&lt;/p&gt;

&lt;p&gt;Tested so far: BTT SKR Pico (RP2040, Klipper v0.10.0-181). The probe identified it, read 31 pin names, reset it, configured &lt;code&gt;gpio24&lt;/code&gt; and sent 10 toggles with 0 CRC errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It has not printed a real part yet.&lt;/strong&gt; Homing and a whole print file have run on the Tina 2, but in mid-air: first layer, Z offset, extrusion and cooling are untested in practice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heaters: stay with the printer.&lt;/strong&gt; Klipper's heater safety is ported and has shut the board down correctly on hardware ("ADC out of range"), but this is experimental firmware: keep a hand near the power switch and never leave it running unattended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tuning features have not moved a printer&lt;/strong&gt;: input shaping, pressure advance, accelerometer calibration, bed mesh, screw tilt, arcs, firmware retraction, exclude object and tuning tower are built and PC-tested; the shaping maths has been timed on the ESP32, nothing more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The INT32/64 test build&lt;/strong&gt; has passed the PC tests (within one step of the &lt;code&gt;double&lt;/code&gt; maths everywhere) and both benchmarks on the ESP32, but has not driven motors with its integer shaper yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending waits for the board&lt;/strong&gt;: about 30 % of one core in the print benchmark is the ESP32 waiting for the SKR Pico's acknowledgements, where Klipper keeps sending. Fine at the Tina 2's speeds; it is the next speed work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delta is untested on a real printer&lt;/strong&gt; and has no calibration (&lt;code&gt;DELTA_CALIBRATE&lt;/code&gt;); the DECKHAND delta image is a separate build and its flash size is not measured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Pasta Inspector dongle link is untested&lt;/strong&gt;: the websocket server has not been built with ESP-IDF or run against a real ESP32-CAM; CAPTAIN and SKIPPER only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only the SKR Pico has run.&lt;/strong&gt; The other board presets are compile-only, and their bootloader details are assumptions. Small 8-bit boards give Klipper a much smaller move queue than the SKR Pico's 1,024 commands; this host has not been tried with one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flash is nearly full&lt;/strong&gt; on DECKHAND (about 6 KB free) and CAPTAIN (about 14 KB); new features may need something trimmed.&lt;/li&gt;
&lt;li&gt;The part-cooling fan and the third fan output are not confirmed (possibly a broken wire). The cause of one early SKR Pico lock-up after a shutdown (2026-10-13) is still unknown.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mcu_probe&lt;/code&gt; is Linux-only.&lt;/li&gt;
&lt;li&gt;The GitHub workflows in &lt;code&gt;.github/workflows/&lt;/code&gt; have not been checked for this release.&lt;/li&gt;
&lt;li&gt;Presets for boards with clone chips (some Creality 4.2.x) are flagged unverified; check your chip.&lt;/li&gt;
&lt;li&gt;Design documents describe intent, not finished work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;printhost.py          one-command setup + launcher
flasher.py            the Flasher window (also Flasher.bat / Flasher.command)
HOW_IT_WORKS.md       what it reads, changes and never does
docs/                 DESIGN, PROTOCOL, ENGINE, FIRMWARE, TUNING, TASKS, project page
firmware/esp32s3/       ESP32-S3 firmware (ESP-IDF project) + build.sh
firmware/prebuilt-esp32s3/  shipped ESP32-S3 images (DECKHAND / CAPTAIN / SKIPPER; test-int32/ for the INT32/64 test build)
firmware/boards/      board presets (.config + .json)
firmware/prebuilt/    shipped plain-Klipper firmware per board (+ pin tables); refresh with skipper_fw.py bundle
host/                 portable C++17 core (the same code runs on the PC and, as an ESP-IDF component, on the ESP32-S3)
  include/ph/         wire, msgproto, json_mini, config_ini, gcode, motion, planner, engine
  src/  test/         implementation + 1,825 unit checks
  tools/              build.py, skipper_fw.py, printhost_gui.py (+ fw_panel, skipper_theme),
                      mcu_probe.cpp, fake_mcu.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Roadmap
&lt;/h2&gt;

&lt;p&gt;See &lt;a href="https://github.com/The-Dorkknight/esp-printhost/blob/main/docs/TASKS.md" rel="noopener noreferrer"&gt;docs/TASKS.md&lt;/a&gt;. Next, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make the integer input shaper the standard in every build (keeping the &lt;code&gt;double&lt;/code&gt; planner), now that it measures about 3.7 times faster on the ESP32.&lt;/li&gt;
&lt;li&gt;Send without waiting for each acknowledgement, the way Klipper does, to win back most of the 30 % of a core spent waiting.&lt;/li&gt;
&lt;li&gt;Move to the QIDI X-Smart 3 test bench and print a first real part.&lt;/li&gt;
&lt;li&gt;Try the tuning features (input shaping, pressure advance, bed mesh) on a moving printer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The look is a 1928 black-and-white cartoon (visuals only).&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/esp-printhost" rel="noopener noreferrer"&gt;The-Dorkknight/esp-printhost on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>iot</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>SoloTrace</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Sat, 31 Oct 2026 18:12:19 +0000</pubDate>
      <link>https://dev.to/open-madscience/solotrace-fl8</link>
      <guid>https://dev.to/open-madscience/solotrace-fl8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff4mswiv5fk4rezhoegz5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff4mswiv5fk4rezhoegz5.webp" alt="gui context menu" width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An easy PCB designer for &lt;strong&gt;single-sided, through-hole, quick-prototype boards&lt;/strong&gt;. A small library of real full-size parts, traces up to 10 mm wide set with a slider, &lt;strong&gt;every corner chamfered&lt;/strong&gt; (no pure 90° traces), an autorouter that avoids the "tangled string" look, and output for printing (toner transfer), Gerber fabs and a &lt;strong&gt;gantry diode laser (Elegoo Phecda) with a negative option and adjustable trace gap&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exists and what does not
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Place, wire, trace, trim and jumper tools, undo/redo, pan/zoom, flip view&lt;/td&gt;
&lt;td&gt;Works in tests and the screenshots; lightly tested by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right-click menus (trim, split, tidy, width, rotate, duplicate, wire from a pin, route one net...)&lt;/td&gt;
&lt;td&gt;Works in tests and the screenshots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rigid wires (Shift + Wire): a direct power trunk that the rest of the net branches off&lt;/td&gt;
&lt;td&gt;Works in tests and on the demo; first suggested from real use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-route retries (best of up to 6 net orders), jumpers optional globally or per net&lt;/td&gt;
&lt;td&gt;Works on the test boards; slower (up to about 12 s plus one try)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cut tool: one click removes a piece of trace from joint to joint (Shift: between two points)&lt;/td&gt;
&lt;td&gt;Works in tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Favourites &amp;amp; recently placed parts; Shift + click turns a part while placing&lt;/td&gt;
&lt;td&gt;Works in tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branches stay joined when a trunk is widened, tidied or dragged; DRC warns about hanging trace ends&lt;/td&gt;
&lt;td&gt;Works in tests and on rebuilds of real boards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Board &amp;gt; Keep traces out from under parts&lt;/td&gt;
&lt;td&gt;Works on the test boards; can add jumpers on cramped boards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pin labels (B C E, G D S, K A, IN GND OUT, optocoupler and DIP layouts) and part values on the board&lt;/td&gt;
&lt;td&gt;Works; layouts are the common ones for each shape, &lt;strong&gt;check your datasheet&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dragging a trace section sideways (neighbours follow, 45° rules kept)&lt;/td&gt;
&lt;td&gt;Works in tests; lightly tested by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90 through-hole parts in a form-factor tree with a live preview, footprint wizard, KiCad &lt;code&gt;.kicad_mod&lt;/code&gt; import&lt;/td&gt;
&lt;td&gt;Works; some parts excluded on purpose (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trace width slider 0.4–10 mm (segment, trace or net) + current readout + "fits up to" hint&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wide traces leave pads straight and end on the pad centre (auto-tidy)&lt;/td&gt;
&lt;td&gt;Works on the test boards; may leave a trace as it was when the tidy shape does not fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deadbug (surface-solder, no-drill) pads, per part or per pad&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copper islands (Island tool): round or rectangular pads you drop, drag and resize; pins on an island join it&lt;/td&gt;
&lt;td&gt;Works in tests and the screenshots; new in 0.4.0, not yet used on a real board&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ground fill for RF / Manhattan boards: the board is ground copper with a gap round every other net, floating fill removed&lt;/td&gt;
&lt;td&gt;Works in tests, the examples and an independent Gerber reader; new in 0.4.0, &lt;strong&gt;never etched yet&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chamfered corners everywhere + DRC&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-place and auto-route with jumper fallback&lt;/td&gt;
&lt;td&gt;Works on the three demo boards and random test boards; &lt;strong&gt;pure Python, can be slow&lt;/strong&gt; on big boards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gerber + drill zip, 1:1 PDF/SVG, BOM CSV, netlist text&lt;/td&gt;
&lt;td&gt;Written and re-read by an independent Gerber library in tests; &lt;strong&gt;not opened in a fab's own viewer&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laser G-code (negative / isolation / positive, test cards, frame file)&lt;/td&gt;
&lt;td&gt;Geometry checked in tests; &lt;strong&gt;never run on a real Elegoo Phecda&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows and macOS launchers, GitHub workflows&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not run&lt;/strong&gt; (written but untested on those systems)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Floating slider pop-up beside the cursor&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not built&lt;/strong&gt;: the slider is in the right-hand panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full PathFinder-style negotiated routing, topological router, C++ speed-up&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not built&lt;/strong&gt; (see &lt;a href="https://github.com/The-Dorkknight/solotrace/blob/main/DESIGN_NOTES.md" rel="noopener noreferrer"&gt;DESIGN_NOTES.md&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soldermask and copper-pour layers, multi-layer boards, SMD&lt;/td&gt;
&lt;td&gt;Out of scope&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Quick tour: your first board in 5 minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;File &amp;gt; Open example &amp;gt; 555 LED flasher&lt;/strong&gt; (or &lt;strong&gt;File &amp;gt; New board&lt;/strong&gt; and set a size).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Place parts:&lt;/strong&gt; pick from &lt;em&gt;Favourites &amp;amp; recent&lt;/em&gt; at the top of the parts panel, or open a group in the parts panel on the left (the little arrows show the sub-types, such as &lt;em&gt;Resistors standing&lt;/em&gt;), pick a part, check the drawing under the list, then click on the board. Or type in the search box (&lt;code&gt;dip 8&lt;/code&gt;, &lt;code&gt;to-220&lt;/code&gt;, &lt;code&gt;standing&lt;/code&gt;). &lt;code&gt;R&lt;/code&gt; or Shift + click turns the part, &lt;code&gt;Esc&lt;/code&gt; stops placing. Star parts you use a lot with the button under the preview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire:&lt;/strong&gt; press &lt;code&gt;W&lt;/code&gt;, click one pin, then another (or drag from pin to pin). A yellow rubber-band line shows the connection. Hold &lt;strong&gt;Shift&lt;/strong&gt; for a &lt;strong&gt;rigid wire&lt;/strong&gt; (see below). Click a rubber-band line to name its net or give it a class (Signal 0.8 mm, Power 2 mm, High current 5 mm; change them in Board &amp;gt; Board size and rules).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-place, then Auto-route&lt;/strong&gt; (toolbar). A progress bar with &lt;strong&gt;Cancel&lt;/strong&gt; shows while it works. Routes use 0°, 45° and 90° moves, and a 90° turn is always built as two 45° turns (a chamfer). If a trace can't get through, a &lt;strong&gt;wire-link jumper&lt;/strong&gt; (blue dashed) is added on the component side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label:&lt;/strong&gt; select a part and type its &lt;em&gt;Value / type&lt;/em&gt; (NE555, BC547, 10k...), and pick its &lt;em&gt;Pin labels&lt;/em&gt; (B C E, G D S, IN GND OUT...).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tweak:&lt;/strong&gt; drag a trace section sideways to move it, use &lt;strong&gt;Trim&lt;/strong&gt; (&lt;code&gt;X&lt;/code&gt;) to cut a section out, and &lt;strong&gt;right-click&lt;/strong&gt; anything for its menu.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check&lt;/strong&gt;, then &lt;strong&gt;Export&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Quick and dirty RF (Manhattan / deadbug):&lt;/strong&gt; &lt;em&gt;File &amp;gt; Open example &amp;gt; Manhattan RF amplifier (copper islands)&lt;/em&gt;. Press &lt;code&gt;I&lt;/code&gt; and click to drop copper islands, sit the parts' pins on them, and tick &lt;em&gt;Board &amp;gt; Ground fill&lt;/em&gt;. No traces needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature by feature
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Library and parts.&lt;/strong&gt; 90 parts, sorted by shape (form factor) rather than by maker code. Each group has an arrow that opens its sub-types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Sub-types&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chips (DIP)&lt;/td&gt;
&lt;td&gt;Narrow (7.62 mm rows) DIP-4 to DIP-28, wide (15.24 mm rows) DIP-28 and DIP-40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transistors &amp;amp; regulators&lt;/td&gt;
&lt;td&gt;TO-92, TO-220 (2-pin, 3-pin, with heatsink outline), TO-247 / TO-3P&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resistors, diodes &amp;amp; inductors&lt;/td&gt;
&lt;td&gt;Resistors lying flat (1/4 W to 5 W), &lt;strong&gt;resistors standing (1/4 W at 2.54 and 5.08 mm, 1/2 W and 1 W at 5.08 mm)&lt;/strong&gt;, diodes lying flat and standing, axial and radial inductors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacitors&lt;/td&gt;
&lt;td&gt;Ceramic disc, film / box, electrolytic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LEDs&lt;/td&gt;
&lt;td&gt;3 mm and 5 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pots, switches &amp;amp; crystals&lt;/td&gt;
&lt;td&gt;Inline pots (2.54 and 5.08 mm), triangle trimmer, 6 mm tactile switch, HC-49 crystal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connectors&lt;/td&gt;
&lt;td&gt;Pin headers 1-row and 2-row, screw terminals, JST-XH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Board bits&lt;/td&gt;
&lt;td&gt;Wire links, mounting holes, solder and surface pads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My parts&lt;/td&gt;
&lt;td&gt;Parts you made or imported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click a part and a drawing of it (copper, holes, outline, pin numbers, pitch and drill) appears under the list, in the sidebar, with no pop-up. Names say what the part is ("Electrolytic cap, 5 mm pitch"), not an obscure model number. Left out on purpose because their sizes vary too much between makers: relay, USB-B, barrel jack, buzzer, fuse holder, panel pots with lugs, TO-126, bridge rectifiers. Make those with the wizard. Body outlines, the inductor, pot and trimmer layouts, and the standing-part pitches are generic: check them against your real part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your own parts.&lt;/strong&gt; &lt;em&gt;Parts &amp;gt; New footprint...&lt;/em&gt; opens the wizard: type a pad table (name, x, y, pad size, drill), or click on the canvas to drop pads, or start from a DIP or header preset, then save. Saved parts live in &lt;code&gt;~/.solotrace/parts&lt;/code&gt; (&lt;em&gt;Parts &amp;gt; Open my parts folder&lt;/em&gt;). &lt;em&gt;Parts &amp;gt; Import KiCad .kicad_mod...&lt;/em&gt; reads the text format of a KiCad footprint (through-hole pads and silkscreen lines only).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trace width.&lt;/strong&gt; Press &lt;code&gt;T&lt;/code&gt; and click along the board to draw a trace (&lt;code&gt;Enter&lt;/code&gt;, a double-click or &lt;em&gt;Finish trace here&lt;/em&gt; on the right-click menu finishes it; corners are chamfered for you). To change a width, click a &lt;strong&gt;trace section&lt;/strong&gt; on the drawing, or click a &lt;strong&gt;yellow rubber-band line&lt;/strong&gt; to set the width of a whole net, then drag the &lt;strong&gt;slider&lt;/strong&gt; in the right panel (0.4–10 mm; preset buttons 0.4, 0.8, 1.5, 2.5, 5, 10). Choose whether it applies to the section, the whole trace or the whole net. The panel shows roughly how many amps that width carries (IPC-2221 outer-layer formula; board settings give copper weight and temperature rise; a guide only) and how wide the trace can go where it is before it breaks the clearance rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rigid wires (power trunks).&lt;/strong&gt; A net with more than two pins is normally routed by joining the nearest pins first, so a ground net can end up as one long chain (transistor to capacitor to terminal). If you want a direct trace between two particular pins, hold &lt;strong&gt;Shift&lt;/strong&gt; while wiring them (click or drag), or right-click a pin &amp;gt; &lt;em&gt;Rigid wire from this pin&lt;/em&gt;, or right-click an existing rubber band &amp;gt; &lt;em&gt;Make ... rigid&lt;/em&gt;. The router routes every rigid pair first, as its own pin-to-pin trace, then connects the rest of the net to the nearest point of that trunk, so a capacitor becomes a short branch. "Rigid" fixes &lt;em&gt;which&lt;/em&gt; two pins are joined, not the path: the trunk bends round other parts like any trace, can be moved by a re-route, and you can drag or cut it. Rigid rubber bands are thicker and orange. Once routed, the trunk is drawn in the same copper colour as everything else, so joints look like the one piece of copper they will be, with a thin dashed orange line down its middle to show it is a rigid wire. Branches stop inside the trunk instead of poking out of its far side, and when you drag a trunk sideways the branches joined to it move with it and stay joined. The net panel lists the rigid pairs, each with a &lt;em&gt;make normal&lt;/em&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copper islands (Island tool, &lt;code&gt;I&lt;/code&gt;).&lt;/strong&gt; Free-standing pads for Manhattan / deadbug building. Click on the board to drop one (5.08 mm square to start), or drag to draw one any size; Shift + click switches new islands between rectangular and round. With &lt;em&gt;Select&lt;/em&gt;, drag an island to move it (0.635 mm steps, so it can sit under pins) and drag one of its white corner squares to resize it while the opposite corner stays put. The right panel takes an exact width, height and corner cut (rectangles get 45 degree corners like everything else; a round island with a different width and height is an oval), offers square/circle sizes from 2.54 to 15.24 mm, and has &lt;em&gt;Turn 90&lt;/em&gt;, &lt;em&gt;Duplicate&lt;/em&gt; (&lt;code&gt;Ctrl+D&lt;/code&gt;) and &lt;em&gt;Delete&lt;/em&gt;; arrow keys nudge it. Right-click an island for the same choices, or right-click empty board &amp;gt; &lt;em&gt;Add a copper island here&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;An island has no net of its own: it &lt;strong&gt;joins the net of whatever touches it&lt;/strong&gt;, a pin, a trace or a jumper pad, and islands that touch each other share a net. So sit a part's pins on islands (deadbug pads make that easy) and the connection is made, with no trace. The panel says which net an island is on and which pins sit on it. If pins of two different nets end up on one island, that is a short and &lt;em&gt;Check&lt;/em&gt; reports it. The router treats islands as copper and routes round them, and they appear in every export.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ground fill (Board &amp;gt; Ground fill).&lt;/strong&gt; For quick and dirty RF and fast analogue: the whole board becomes copper, used as ground, and every other net is cut free from it with a gap. Settings are under &lt;em&gt;Board &amp;gt; Ground fill settings...&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ground net:&lt;/strong&gt; guessed (a net called GND, 0V, VSS, ground... otherwise the net with the most pins), or pick one; the net panel also has &lt;em&gt;Use this net as the ground fill&lt;/em&gt;. The ground net is joined by the fill, so Auto-route does not route it, and its old automatic traces are removed when you turn the fill on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gap&lt;/strong&gt; round everything else (default 0.6 mm, never less than the board's copper-to-copper gap). For laser etching keep it at least 3 to 4 line intervals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fatten other nets' copper&lt;/strong&gt; by up to this much (default 1 mm): routed traces and pads grow into wide, low-inductance patches, but only where that keeps the gap from other nets, ground copper and the board edge. Copper islands you placed keep their size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove floating fill&lt;/strong&gt; (on by default): a piece of fill that no ground pin sits on, boxed in by other nets, is cut away rather than left as floating copper. A ground pin that sits on a piece of fill cut off from the rest gets a yellow rubber band; join it with a trace or a jumper.&lt;/li&gt;
&lt;li&gt;Turning the fill on offers to make every pad a &lt;strong&gt;deadbug pad&lt;/strong&gt;, since such boards are usually built with the parts soldered straight onto the copper.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fill is drawn in the editor and is in the Gerber, the PDF/SVG prints and the laser G-code. In negative laser mode the laser then burns only the gaps (much faster); isolation mode burns the same thing on such a board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Values and pin labels.&lt;/strong&gt; Select a part: &lt;em&gt;Value / type&lt;/em&gt; at the top of the right panel takes anything (NE555, BC547, IRF540, 7805, 10k, 100uF 25V) and is shown next to the part name on the board; double-click a part to jump to it. &lt;em&gt;Pin labels&lt;/em&gt; offers the common layouts for that shape, not specific part numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;Layouts offered&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TO-92&lt;/td&gt;
&lt;td&gt;E B C, C B E, B C E, D G S, G D S, S G D, small regulator OUT GND IN / IN GND OUT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TO-220 3-pin&lt;/td&gt;
&lt;td&gt;power transistor B C E, MOSFET G D S, IGBT G C E, regulator IN GND OUT, adjustable regulator ADJ OUT IN, negative regulator GND IN OUT, triac MT1 MT2 G, dual diode A K A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TO-220 2-pin&lt;/td&gt;
&lt;td&gt;diode K A / A K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TO-247&lt;/td&gt;
&lt;td&gt;G D S, G C E, B C E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIP-4 / DIP-6&lt;/td&gt;
&lt;td&gt;optocoupler A K E C; optocoupler with base pin; opto-triac&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIP-8&lt;/td&gt;
&lt;td&gt;555-style timer, dual op-amp, single op-amp, dual optocoupler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIP-14 / 16 / 20&lt;/td&gt;
&lt;td&gt;logic GND and VCC pins; quad op-amp (DIP-14)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pots, connectors&lt;/td&gt;
&lt;td&gt;1 W 3, CCW W CW; + -, - +, AC AC, IN OUT, V+ GND V-, IN GND OUT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Or type your own in pin order, separated by spaces (&lt;code&gt;.&lt;/code&gt; skips a pin). Diodes and LEDs show K A, electrolytics + -, without asking. Pin orders differ between makers, so &lt;strong&gt;always check the datasheet&lt;/strong&gt; of the part you have. Labels are shown in the editor; they are not yet written into the Gerber silkscreen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wide traces are tidied.&lt;/strong&gt; When a trace is 1.2 mm or wider, SoloTrace makes it leave each pad with a straight vertical or horizontal run (about the pad radius plus the trace width) before it turns, ends it exactly on the pad centre, and makes the chamfers bigger where there is room. This happens after auto-routing and, with &lt;em&gt;Auto-tidy when the width changes&lt;/em&gt; ticked (the default), whenever you change a width. If the tidier shape would touch other copper the trace is left as it was, and the clearance problem is marked in red. &lt;em&gt;Tidy&lt;/em&gt; (right panel or right-click) does it on demand; &lt;em&gt;Edit &amp;gt; Tidy all traces&lt;/em&gt; does the whole board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cutting a piece out of a trace.&lt;/strong&gt; Press &lt;code&gt;C&lt;/code&gt; and click a trace: the piece under the mouse goes, from the joint before it to the joint after it (a pad, a place where another trace meets it, a jumper pad). The rest of the trace and of the net stays. While you hover, the piece that would go is shown in red. Right-clicking a trace only opens its menu; rest the mouse on &lt;em&gt;Cut this piece&lt;/em&gt; to see the piece in red, and choose it to remove it. To cut between two points of your own choosing, Shift + click the first point and Shift + click the second (or right-click &amp;gt; &lt;em&gt;Cut between two points&lt;/em&gt;); &lt;code&gt;Esc&lt;/code&gt; cancels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moving, trimming and splitting traces.&lt;/strong&gt; With &lt;em&gt;Select&lt;/em&gt;, drag a trace section sideways: it moves parallel to itself and the sections either side stretch to follow at their own angle, so no 90° corner appears. Ends on pads stay put (a 45° link is added). &lt;strong&gt;Trim&lt;/strong&gt; (&lt;code&gt;X&lt;/code&gt;, or right-click &amp;gt; &lt;em&gt;Trim this section&lt;/em&gt;) cuts one section out and leaves the rest as separate pieces; &lt;em&gt;Split trace here&lt;/em&gt; breaks a trace in two without removing copper. A trace you have moved or trimmed counts as hand-drawn, so Auto-route keeps it. &lt;code&gt;Ctrl+Z&lt;/code&gt; undoes any of this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right-click menus.&lt;/strong&gt; Right-click (without dragging) on a trace, pin, part, rubber-band line, jumper or empty board for the actions that fit: widths, trim, split, tidy, highlight or re-route the net, rotate either way, duplicate, lock, pad styles, wire or trace from a pin, disconnect, delete, and Undo/Redo. Right-drag still pans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small helpers.&lt;/strong&gt; Hover over copper to light up its whole net (the net name shows in the corner). Arrow keys nudge the selected part by 1.27 mm (&lt;code&gt;Shift&lt;/code&gt;: 2.54 mm), &lt;code&gt;Ctrl+D&lt;/code&gt; duplicates it, &lt;code&gt;Shift+R&lt;/code&gt; turns it back. &lt;em&gt;File &amp;gt; Open recent&lt;/em&gt; lists your last boards. While a board has unsaved changes it is saved every 2 minutes to &lt;code&gt;~/.solotrace/autosave.solotrace&lt;/code&gt;; &lt;em&gt;File &amp;gt; Recover autosave&lt;/em&gt; opens it (the file is removed when you quit normally). Hover over a toolbar button for a hint. The status bar shows the cursor position in mm and a green &lt;em&gt;all routed&lt;/em&gt; or orange &lt;em&gt;N to route&lt;/em&gt; badge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pads: through-hole or deadbug.&lt;/strong&gt; Pick &lt;em&gt;Through-hole&lt;/em&gt;, &lt;em&gt;Deadbug&lt;/em&gt; or &lt;em&gt;Both&lt;/em&gt; in the toolbar for new parts, or change it for a placed part in the right panel, or for one pad ("This pad only"). Deadbug pads have &lt;strong&gt;no hole&lt;/strong&gt; and are drawn as a long oval that sticks out from the part so you can bend the lead flat and solder it on the copper side. "Both" keeps the hole &lt;em&gt;and&lt;/em&gt; the extra copper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-place.&lt;/strong&gt; Simulated annealing that moves, rotates and swaps unlocked parts to shorten the ratsnest and cut crossings. Tick &lt;strong&gt;Lock&lt;/strong&gt; on a part to keep it where it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-route.&lt;/strong&gt; A grid router that remembers its heading, so it prefers straight runs and clean 45° bends instead of staircases. Wide nets are routed first, and it prefers not to run under part bodies. If a net is blocked it tears up the nets in the way and retries; as a last resort it adds a jumper, unless you unticked &lt;strong&gt;Jumpers&lt;/strong&gt; in the tool bar or for that net. The whole job is tried with up to six different net orders (about 12 seconds at most, plus the try in progress) and the best result is kept; it stops early when a try routes everything without jumpers. Traces you drew by hand are kept. &lt;em&gt;Board &amp;gt; Remove automatic routes&lt;/em&gt; undoes only the automatic ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jumpers.&lt;/strong&gt; Press &lt;code&gt;J&lt;/code&gt; to add one by hand. They are plain wire links on the component side, in 2.54 mm steps from 5.08 mm. They are listed in the DRC window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;View.&lt;/strong&gt; &lt;code&gt;M&lt;/code&gt; flips between the &lt;strong&gt;component side&lt;/strong&gt; (how you place parts) and the &lt;strong&gt;copper side&lt;/strong&gt; (how the etched board looks from below). Wheel zooms, right or middle drag pans, right-click opens the menu, &lt;code&gt;F&lt;/code&gt; fits the board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DRC.&lt;/strong&gt; &lt;em&gt;Check&lt;/em&gt; lists trace ends that are not joined to anything ("dangling"), copper too close to other copper or the board edge, parts sticking out of the board, traces thinner than the minimum, turns sharper than 45°, drilled pads with too little copper ring, jumpers that are the wrong length or cross each other, and pins that are not connected yet. Click a row to jump to it on the board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exports (Export menu).&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gerber + drill (zip)&lt;/td&gt;
&lt;td&gt;Copper (bottom), board edge, silkscreen, Excellon drill. Single layer; &lt;strong&gt;no soldermask layer&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1:1 PDF / SVG, mirrored&lt;/td&gt;
&lt;td&gt;Toner transfer or photo-resist film: the copper side as you will see it. Always print at 100 % and &lt;strong&gt;measure a known distance&lt;/strong&gt; before etching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1:1 PDF, design view&lt;/td&gt;
&lt;td&gt;Component-side print&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bill of materials (CSV)&lt;/td&gt;
&lt;td&gt;Parts grouped by name and value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netlist text&lt;/td&gt;
&lt;td&gt;Round-trip text format for a quick board from a text editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laser G-code (Elegoo Phecda)&lt;/td&gt;
&lt;td&gt;See below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Laser etching with the Elegoo Phecda (gantry diode laser)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Read this first.&lt;/strong&gt; A diode laser cannot remove bare copper. The method is: &lt;strong&gt;paint&lt;/strong&gt; the copper, let the laser &lt;strong&gt;burn the paint away&lt;/strong&gt; where copper must go, &lt;strong&gt;etch&lt;/strong&gt;, then clean the paint off. Nobody has run these files on a Phecda yet. Treat every default as a starting guess and use the test cards.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finish the board and run DRC. Open &lt;strong&gt;Export &amp;gt; Laser G-code (Elegoo Phecda)...&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mode&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Negative&lt;/strong&gt; (default): burns everything that is &lt;em&gt;not&lt;/em&gt; copper, so paint is left only on the traces and pads. Slowest, cleanest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt;: burns only a band of &lt;code&gt;gap&lt;/code&gt; mm around the copper. Fast; leftover paint between bands must be cleared by hand or by the etchant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Positive&lt;/strong&gt;: burns the copper shapes themselves (for a negative-working resist or marking).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adjust&lt;/strong&gt; (all live-previewed):

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Trace adjust&lt;/em&gt; (mm per side): positive grows the copper, negative thins it and widens every gap between traces. Use this to correct for the laser's spot size and your etch.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Gap&lt;/em&gt; (isolation band width) and &lt;em&gt;Border&lt;/em&gt; (keep paint at the board edge).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Line interval&lt;/em&gt; (default 0.08 mm; the module's spot is about 0.06–0.13 mm depending on model), &lt;em&gt;feed&lt;/em&gt; (default 1500 mm/min), &lt;em&gt;power&lt;/em&gt; (default 70 % of S max), &lt;em&gt;passes&lt;/em&gt;, &lt;em&gt;crosshatch&lt;/em&gt; (second pass at 90°), &lt;em&gt;bidirectional&lt;/em&gt; scanning, &lt;em&gt;M4 dynamic power&lt;/em&gt; (default) or &lt;em&gt;M3 with overscan&lt;/em&gt;, and &lt;em&gt;drill dots&lt;/em&gt; (a tiny burn at every hole centre as a drilling guide).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save G-code...&lt;/strong&gt;. Also use &lt;strong&gt;Frame file...&lt;/strong&gt; (traces the board outline at 1 % power so you can line up the paint) and, on first use, &lt;strong&gt;Speed/power test...&lt;/strong&gt; and &lt;strong&gt;Trace/gap test...&lt;/strong&gt; (a grid of squares and a ladder of fine traces and gaps).&lt;/li&gt;
&lt;li&gt;In LaserGRBL or LightBurn: jog the laser to the &lt;strong&gt;lower-left corner of the board as seen from the copper side&lt;/strong&gt;, set work zero there, open the file, check the preview, run the frame, then burn. The exporter mirrors X for you because the design view is the component side.&lt;/li&gt;
&lt;li&gt;Controller assumptions: GRBL-style, &lt;code&gt;$30&lt;/code&gt; equal to the S max in the dialog (default 1000) and &lt;code&gt;$32=1&lt;/code&gt; (laser mode). The Elegoo manual names LaserGRBL and LightBurn but does not say "GRBL"; see &lt;a href="https://github.com/The-Dorkknight/solotrace/blob/main/docs/research/phecda-laser-notes.md" rel="noopener noreferrer"&gt;docs/research/phecda-laser-notes.md&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Examples to look at without installing anything: &lt;a href="https://github.com/The-Dorkknight/solotrace/tree/main/examples" rel="noopener noreferrer"&gt;examples/&lt;/a&gt; has four boards, each with its &lt;code&gt;.solotrace&lt;/code&gt; file, Gerber zip, 1:1 copper-side SVG and PDF, BOM and laser G-code in both modes, plus &lt;a href="https://github.com/The-Dorkknight/solotrace/tree/main/examples/test-cards" rel="noopener noreferrer"&gt;test cards&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  More
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/solotrace/blob/main/HOW_IT_WORKS.md" rel="noopener noreferrer"&gt;HOW_IT_WORKS.md&lt;/a&gt;: what it reads and writes, the coordinates, the router and the laser engine in plain English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/solotrace/blob/main/DESIGN_NOTES.md" rel="noopener noreferrer"&gt;DESIGN_NOTES.md&lt;/a&gt;: decisions, rejected ideas, measurements, what is untested&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/solotrace/tree/main/docs/research" rel="noopener noreferrer"&gt;docs/research/&lt;/a&gt;: the original research and plan, and the Phecda notes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/solotrace/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/solotrace/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/solotrace/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Project page (after you enable GitHub Pages): &lt;code&gt;https://The-Dorkknight.github.io/solotrace/&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/solotrace" rel="noopener noreferrer"&gt;The-Dorkknight/solotrace on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>A POINT of contention</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:43:06 +0000</pubDate>
      <link>https://dev.to/open-madscience/a-point-of-contention-3o03</link>
      <guid>https://dev.to/open-madscience/a-point-of-contention-3o03</guid>
      <description>&lt;p&gt;&lt;strong&gt;STL to STEP conversion shouldn't be that HARD: a proposed hybrid STL format that is CAD-friendly and slicer-compatible&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Converting an STL back to a STEP file means guessing the original CAD faces. This paper proposes a normal binary STL that also records them, so slicers still open it and converters no longer have to guess. It is also a public, dated description of the format, and a list of the ideas that didn't work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Matthew Armstrong · GitHub &lt;a href="https://github.com/The-Dorkknight" rel="noopener noreferrer"&gt;The-Dorkknight&lt;/a&gt; · United Kingdom\&lt;br&gt;
First published 15 October 2026 · Version 1.0\&lt;br&gt;
Text and figures: &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener noreferrer"&gt;CC BY-SA 4.0&lt;/a&gt;. Code that comes with it: &lt;a href="https://polyformproject.org/licenses/noncommercial/1.0.0/" rel="noopener noreferrer"&gt;PolyForm Noncommercial 1.0.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;📄 &lt;strong&gt;&lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/blob/main/paper/a-point-of-contention-v1.0.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/strong&gt; · 🌐 &lt;a href="https://The-Dorkknight.github.io/a-point-of-contention/" rel="noopener noreferrer"&gt;Web page&lt;/a&gt; · 🔧 &lt;a href="https://github.com/The-Dorkknight/hardpoint-stl" rel="noopener noreferrer"&gt;The FreeCAD and Fusion plug-ins&lt;/a&gt; · About this repository&lt;/p&gt;

&lt;p&gt;This page is the full paper (the same text and figures as the PDF).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contents:&lt;/strong&gt; Abstract · Why I wrote this · The problem: what STL loses, and how slicers read it · Prior art: what I found (3–10 October 2026) · How the idea was judged · The ideas that didn't work · The format, in one picture · Does it help? · What works, and what doesn't yet · Defensive disclosure: the hardpoint STL and variations · What I'd test next (feel free to use this plan) · Working together · How this was made · Code, data and how to reproduce · Licence, and keeping it free · Disclaimers · How to cite · References · Appendix A: a real hardpoint STL&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Status: nothing in this paper has been run in real FreeCAD, in Fusion, or in a slicer.&lt;/strong&gt; The format and its reference code were tested on Linux, using test parts built with OpenCASCADE and the file-reading code that slicers use (not the slicers themselves). The FreeCAD and Fusion exporters were only tested against stand-ins for those programs. The conversion results come from one converter of mine. If you open the example files in your slicer before I do, please tell me what happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not legal advice.&lt;/strong&gt; I am not a lawyer. Where this paper talks about prior art, it says what I found, where I found it, and when. A patent attorney is the person to ask about what it means.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plain STL: 1,103 faces&lt;/th&gt;
&lt;th&gt;Hardpoint STL: 8 faces&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8yjh82mrtjp57v985e1.webp" alt="The knob converted from a plain STL: many flat strips and a dark faceted area" width="800" height="610"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F455d0jrnqihjla5ijo06.webp" alt="The knob converted from its hardpoint twin: one cylinder, one sphere, a smooth rounded edge" width="800" height="610"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The same knob, made from the same triangles, converted to STEP. Left: from a plain STL, which came out with 1,103 faces. Right: from the hardpoint version of the file, which came out with 8 faces. The original CAD model has 4.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;An STL file is just a pile of triangles. It has lost everything the CAD program knew: which triangles made up one flat face, which made up a round hole, and where the corners were. Turning an STL back into a real CAD solid means guessing all of that again, and the guesses go wrong most often on coarse meshes and curved edges.&lt;/p&gt;

&lt;p&gt;This paper describes a way to keep that information in the file without breaking it for anyone else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The idea:&lt;/strong&gt; write the CAD facts into the two places a binary STL already has spare room that slicers ignore: the 80-byte header, and the 2-byte "attribute" field after every triangle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What goes in:&lt;/strong&gt; which face each triangle belongs to (as a small colour number, like colouring a map so that neighbouring areas differ), what kind of surface the face is, and which triangle corners are CAD vertices ("hardpoints"). A checksum in the header shows whether the mesh was edited afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What a slicer sees:&lt;/strong&gt; the same triangles, the same file size, the same &lt;code&gt;.stl&lt;/code&gt; extension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How much it helps:&lt;/strong&gt; in my tests, a converter given the labelled file turned a knob into a STEP solid with 8 faces. From the same triangles without labels it made 1,103 faces (the original CAD model has 4). On a clean, fine bracket the gain was small: 21 faces down to 17, against 16 in the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What didn't work:&lt;/strong&gt; data after the last triangle, a 9-bit face number, hiding the whole STEP file inside the STL, remembering how freeform faces were cut up, and a handful of mistakes along the way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Section 8 describes the format and its variations in enough detail to build them. It is written as prior art, so the method stays free for everyone to use.&lt;/p&gt;




&lt;h2&gt;
  
  
  0. Why I wrote this
&lt;/h2&gt;

&lt;p&gt;How it started:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I convert STL files into STEP files. Most of the converter's effort goes on working out again what the CAD program already knew.&lt;/li&gt;
&lt;li&gt;The CAD program could say it at the moment it writes the STL. Instead it throws the answer away.&lt;/li&gt;
&lt;li&gt;A binary STL has two spare places that slicers ignore. I'm a hobbyist in the UK with a laptop, and I wanted to see if the answer could be kept there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I built the &lt;strong&gt;hardpoint STL&lt;/strong&gt;, plus an exporter for FreeCAD and one for Fusion. First I researched how slicers read STL files. Then I tried to find every way a file like this could break them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why publish all of this, including the failures?&lt;/strong&gt; Four reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Protection.&lt;/strong&gt; I looked, and found no earlier description of putting CAD face labels inside an STL (section 2). If that is right, a dated, detailed public description is a cheap way to keep the idea free for hobbyists. That is what a &lt;em&gt;defensive publication&lt;/em&gt; is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saving you time.&lt;/strong&gt; Every dead end here is one you don't have to walk down. The best one began as a question of mine that turned out to have a simple numerical answer (section 4.3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting other people involved.&lt;/strong&gt; The format has room for more exporters (CadQuery, Onshape, SolidWorks) and more readers than I can write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selfishly:&lt;/strong&gt; by my calculations, a third paper makes me about 0.7 % more employable. The first was worth 22.7 % and the second 3.14 %, so the returns are falling. That's probably fine. Publish too many and I start to look like I have a lot of free time, and nobody wants to hire someone who looks that available.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How this was made.&lt;/strong&gt; The ideas, the questions and the rules are mine. The code, the tests, the measurements and the first draft of this write-up were done with an AI assistant (Claude, by Anthropic), working to my design. All sources are linked so you can check them, and the code is included so you can re-run every number that can be re-run (section 12).&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The problem: what STL loses, and how slicers read it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 What gets lost
&lt;/h3&gt;

&lt;p&gt;A CAD solid is made of faces: planes, cylinders, spheres, cones, tori and freeform surfaces, joined along edges and corners. Exporting it as STL cuts every face into triangles and writes only the triangles. A flat face and a rounded edge of the same bracket both end up as nothing but triangles.&lt;/p&gt;

&lt;p&gt;A mesh-to-CAD converter has to work out again which triangles form one flat face (and how flat is flat enough?) and which form a curve. When the mesh is coarse, or a curve meets a flat face very gently, it can't always tell.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 What I set myself
&lt;/h3&gt;

&lt;p&gt;The design had to meet six requirements, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Recognised as an STL.&lt;/strong&gt; The extension stays &lt;code&gt;.stl&lt;/code&gt; and the file loads in any slicer without a plug-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identical to a normal STL in a slicer.&lt;/strong&gt; Same triangles, same file size, nothing a slicer acts on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Makes STL-to-STEP conversion easier.&lt;/strong&gt; It carries what a converter otherwise guesses: faces, surface types, corners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writable from inside CAD programs.&lt;/strong&gt; FreeCAD's and Fusion's own Python can't be assumed to have NumPy, so the writer uses the standard library only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never makes a conversion worse.&lt;/strong&gt; Labels are hints. The reader checks each one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs on low-end hardware.&lt;/strong&gt; The target machine is a 7th-generation Core i3.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1.3 What STL readers do with a file
&lt;/h3&gt;

&lt;p&gt;A binary STL has an 80-byte header, a 4-byte triangle count, then 50 bytes per triangle: 12 for the normal, 36 for three corners, and 2 for an "attribute byte count" that slicers ignore. So there are exactly two places that carry no meaning to a slicer. Anything after the last triangle is a different matter.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;th&gt;Used by&lt;/th&gt;
&lt;th&gt;Binary or text test&lt;/th&gt;
&lt;th&gt;Triangle count&lt;/th&gt;
&lt;th&gt;Extra bytes at the end&lt;/th&gt;
&lt;th&gt;How this is known&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;admesh &lt;code&gt;stlinit.cpp&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;OrcaSlicer; PrusaSlicer-family slicers share this code&lt;/td&gt;
&lt;td&gt;Binary if any of the 128 bytes after offset 84 is above 127&lt;/td&gt;
&lt;td&gt;Taken from the file size; the header count only logs a warning&lt;/td&gt;
&lt;td&gt;Rejected unless a multiple of 50, then read as phantom triangles&lt;/td&gt;
&lt;td&gt;Read &lt;a href="https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer/main/deps_src/admesh/stlinit.cpp" rel="noopener noreferrer"&gt;OrcaSlicer's source&lt;/a&gt; (PrusaSlicer's and Bambu Studio's copies not read)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;numpy-stl&lt;/td&gt;
&lt;td&gt;Cura, through &lt;a href="https://raw.githubusercontent.com/Ultimaker/Uranium/main/plugins/FileHandlers/STLReader/STLReader.py" rel="noopener noreferrer"&gt;Uranium's STLReader&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Automatic mode&lt;/td&gt;
&lt;td&gt;From the header&lt;/td&gt;
&lt;td&gt;Load fails with an error&lt;/td&gt;
&lt;td&gt;Read Uranium's source; ran the library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;trimesh&lt;/td&gt;
&lt;td&gt;many Python tools&lt;/td&gt;
&lt;td&gt;Binary only if the size is exactly 84 + 50 × count, otherwise tried as text&lt;/td&gt;
&lt;td&gt;From the header&lt;/td&gt;
&lt;td&gt;Not binary, then fails as text (0 faces)&lt;/td&gt;
&lt;td&gt;Read &lt;a href="https://raw.githubusercontent.com/mikedh/trimesh/main/trimesh/exchange/stl.py" rel="noopener noreferrer"&gt;trimesh's source&lt;/a&gt;; ran the library&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Other findings that shaped the format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The admesh text test misreads simple files.&lt;/strong&gt; A cube at whole-millimetre coordinates with zero normals has no byte above 127 in that window, so it is read as text and imports empty. This is &lt;a href="https://github.com/OrcaSlicer/OrcaSlicer/issues/15993" rel="noopener noreferrer"&gt;OrcaSlicer issue 15993&lt;/a&gt;, open when I checked on 3 October 2026. A probe copying the test's logic reproduced it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The header must not start with &lt;code&gt;solid&lt;/code&gt;.&lt;/strong&gt; Some software then treats the file as text (&lt;a href="https://en.wikipedia.org/wiki/STL_(file_format)" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two colour conventions already use the attribute field.&lt;/strong&gt; VisCAM and SolidView: bits 0–4 blue, 5–9 green, 10–14 red, bit 15 = 1 means the colour is valid. Materialise Magics: a &lt;code&gt;COLOR=&lt;/code&gt; string in the header, bits 0–4 red, 5–9 green, 10–14 blue, bit 15 = 0 means the triangle has its own colour (both as described on Wikipedia).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;trimesh drops the attribute field during clean-up.&lt;/strong&gt; With &lt;code&gt;process=True&lt;/code&gt; it survives vertex merging, but removing sliver triangles rebuilds the mesh. On one test part 5,572 triangles became 5,564 and the field was gone.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Prior art: what I found (3–10 October 2026)
&lt;/h2&gt;

&lt;p&gt;Using the attribute bytes of an STL for something other than "nothing" is old: the two colour conventions above are the best-known uses. What I looked for was a file that keeps &lt;strong&gt;CAD face identity, surface types and CAD vertices&lt;/strong&gt; inside an ordinary STL.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I found no description of that.&lt;/strong&gt; The searches were ordinary web and code searches by me and my AI assistant, over a few days. They are not a professional prior-art search, and a miss here proves little.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing mesh-to-STEP tools&lt;/strong&gt; (&lt;a href="https://github.com/Crypto69/stlToSolid" rel="noopener noreferrer"&gt;stlToSolid&lt;/a&gt;, &lt;a href="https://pypi.org/project/breptile/0.1.0/" rel="noopener noreferrer"&gt;breptile&lt;/a&gt;) segment the mesh and fit primitives from the triangles alone. As far as I could tell neither reads hints from the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The standard container for "a mesh plus the real CAD in one file"&lt;/strong&gt; is 3MF, which is a zip. It is not an STL, and I did not test how slicers treat an extra file inside one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether any of this affects anyone's rights is a question for lawyers, not me.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. How the idea was judged
&lt;/h2&gt;

&lt;p&gt;I can't build exact CAD parts by hand, so I had a CAD kernel make them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test parts.&lt;/strong&gt; Two parts built with OpenCASCADE (the kernel FreeCAD uses): a bracket (rounded corners, three holes, a boss, a countersink) and a knob (cylinder, sphere, torus fillet). The bracket was meshed at two settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twin files.&lt;/strong&gt; Each mesh was written twice with identical triangles: once labelled, once as a plain STL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The converter.&lt;/strong&gt; Both files went through STL2STEP, an earlier converter of mine (since superseded; its code is not in this repository), default settings. I counted the faces in the STEP result, the largest distance from the mesh, and the volume against the original CAD solid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The format itself.&lt;/strong&gt; Round trips (write, read back, compare faces, types and hardpoints), tamper checks, and the loaders of section 1.3.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What this gets wrong&lt;/strong&gt; (please read before quoting a number):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three test parts, built for the tests. No real downloaded models.&lt;/li&gt;
&lt;li&gt;One converter. Another converter may use the labels better or worse.&lt;/li&gt;
&lt;li&gt;Linux only. The writer was timed on a cloud machine, not on a Core i3.&lt;/li&gt;
&lt;li&gt;The FreeCAD and Fusion code ran against stand-ins with the same attribute names, filled with OpenCASCADE geometry. That checks the plug-ins' own logic and nothing about how the real programs behave.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. The ideas that didn't work
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 The table
&lt;/h3&gt;

&lt;p&gt;Seven ideas were built or tested and then dropped. Each row says what the evidence was, so nobody repeats the experiment blind.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Idea&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Store extra data after the last triangle&lt;/td&gt;
&lt;td&gt;Loaders fail or read garbage&lt;/td&gt;
&lt;td&gt;numpy-stl raised an error, trimesh loaded 0 faces, admesh's logic reads phantom triangles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A global face number in 9 bits&lt;/td&gt;
&lt;td&gt;Replaced by colours&lt;/td&gt;
&lt;td&gt;Capped at 511 faces; colours need 2 to 5 values for any count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hide the STEP file itself in the STL&lt;/td&gt;
&lt;td&gt;It doesn't fit as text, and only sometimes when compressed&lt;/td&gt;
&lt;td&gt;Section 4.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remember how a freeform face was cut into patches (a second, 3-bit "patch colour")&lt;/td&gt;
&lt;td&gt;Faster but worse, so removed&lt;/td&gt;
&lt;td&gt;Organic part: 83 s down to 19 s, but 20 areas left as facets instead of 2 and maximum deviation 0.16 mm instead of 0.12 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Let the converter's normal detection handle cone and torus faces&lt;/td&gt;
&lt;td&gt;It chopped them into cylinder and sphere pieces&lt;/td&gt;
&lt;td&gt;Knob: 131 STEP faces and 3 failed faces, against 8 faces when treated as freeform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Label a whole-body mesh by nearest triangle centre&lt;/td&gt;
&lt;td&gt;Mislabelled triangles beside face borders&lt;/td&gt;
&lt;td&gt;About 1 % wrong on the knob, which turned into 11 false flats and 315 STEP faces. Point-to-triangle distance gave 100 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carry the attribute field through trimesh's clean-up&lt;/td&gt;
&lt;td&gt;Lost whenever slivers are removed&lt;/td&gt;
&lt;td&gt;5,572 triangles became 5,564 with no attribute data. Matching by position replaced it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A likely reason saved patch layouts gave a worse result (not proven): the fit of a patch depends on its border curves, and the converter smooths those borders step by step over several passes. Starting from the final layout skips the steps that made it fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Mistakes along the way
&lt;/h3&gt;

&lt;p&gt;These are in the order they happened. Most were the AI assistant's, and they were caught by tests I had asked for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A missing triangle.&lt;/strong&gt; The knob's sphere has a pole where one triangle collapses to a line once nearby points are welded. The writer rightly dropped it (1 of 2,959). The tests then compared labels with the wrong triangles, because the output order no longer matched the input. The encoder now reports which input triangle sits at each output position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. About 1 % of triangles mislabelled.&lt;/strong&gt; Fusion's fallback labels a whole-body mesh from per-face meshes. First version: nearest triangle centre. It looked fine in a picture and mislabelled about 1 % of triangles beside face borders, which a converter read as 11 flat faces and 315 STEP faces. Point-to-triangle distance fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A flat disc shown as rough facets.&lt;/strong&gt; The converter's preview showed the top of the bracket's boss as dark left-as-facets. The rule that called a planar face a facet was "three edges or fewer"; a flat disc bounded by one circle qualifies. It now also requires every edge to be straight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A renamed OpenCASCADE method.&lt;/strong&gt; The test code called &lt;code&gt;TopoDS.Edge_s&lt;/code&gt;, which doesn't exist in the OCP 8 build installed. A small compatibility helper hides the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A changelog that claimed too much.&lt;/strong&gt; I compared my changelog with the public repository of the converter on the day I packaged it. Five bullets under the published version had been added after it was published. They moved to the new version's section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Screenshots taken at the wrong moment.&lt;/strong&gt; Two documentation screenshots showed a job queue card and a sandbox folder name. They were retaken in a clean state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. A plug-in never tried in its real program.&lt;/strong&gt; I could not install FreeCAD where this was built: its release downloads, conda-forge, the Ubuntu package source, the PPA and Flathub were all blocked, and I did not route around the block. So the workbench ran only against stand-ins. In its place, &lt;code&gt;tests/freecad_selfcheck.py&lt;/code&gt; runs inside the real program in a few seconds. It has not been run either. I keep repeating that because it is the most useful thing a reader could check.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Could the whole STEP file be hidden in the STL?
&lt;/h3&gt;

&lt;p&gt;Partway through I asked: would it be easier to hide a STEP file inside the STL as plain text? It was worth checking, because it would give the exact CAD model back with no fitting at all.&lt;/p&gt;

&lt;p&gt;It doesn't fit. The room in an STL grows with the triangle count (2 bytes each), while the size of a STEP file grows with the number of faces:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;STEP file&lt;/th&gt;
&lt;th&gt;STEP compressed&lt;/th&gt;
&lt;th&gt;Attribute room (2 bytes × triangles)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bracket, 1,180 triangles at 0.05 mm&lt;/td&gt;
&lt;td&gt;48.1 KB&lt;/td&gt;
&lt;td&gt;6.6 KB&lt;/td&gt;
&lt;td&gt;2.3 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knob, 2,958 triangles&lt;/td&gt;
&lt;td&gt;9.3 KB&lt;/td&gt;
&lt;td&gt;2.1 KB&lt;/td&gt;
&lt;td&gt;5.8 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Simple parts with big flat faces fit worst. A text (ASCII) STL has no comment syntax; from reading OrcaSlicer's reader, extra text lines inflate its triangle count and the load fails (read, not run). A compressed STEP would also need every attribute bit, including bit 15, and would compete with the labels for the same bytes. It could be an optional extra: store it when it fits, fall back to labels when it doesn't. That is a variation in section 8.6, not a result.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The format, in one picture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpmdqv1iidltl3lj8y83l.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpmdqv1iidltl3lj8y83l.webp" alt="Byte layout of a binary STL with the header and the 2 attribute bytes of each triangle highlighted, and the meaning of each of the 16 attribute bits" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drawn from the specification by &lt;code&gt;tools/make_illustrations.py&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Header&lt;/strong&gt; (80 bytes of ASCII, padded with spaces):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HARDPOINT-STL 1 u=mm tol=0.05 ang=15 crc=e67609a0 by=cad-example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HARDPOINT-STL 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Marker and format version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Units: &lt;code&gt;mm&lt;/code&gt;, &lt;code&gt;in&lt;/code&gt;, &lt;code&gt;cm&lt;/code&gt; or &lt;code&gt;m&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tol&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How far triangles may be from the true surface, in those units; 0 = unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ang&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Meshing or sharp-edge angle in degrees; 0 = unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;crc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CRC-32 of the 48 geometry bytes (normal and corners) of every triangle, in file order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;by&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What wrote the file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Attribute field&lt;/strong&gt; (16 bits per triangle, little-endian):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bits&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0–4&lt;/td&gt;
&lt;td&gt;Face colour, 0 to 31. Faces that touch along an edge never share a colour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5–6&lt;/td&gt;
&lt;td&gt;Reserved, 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;1 = this triangle is labelled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8–10&lt;/td&gt;
&lt;td&gt;Surface type: 0 unknown, 1 plane, 2 cylinder, 3 sphere, 4 cone, 5 torus, 6 freeform, 7 keep as facets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11–13&lt;/td&gt;
&lt;td&gt;Hardpoint flags for corner 1, 2 and 3 of the triangle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Reserved, 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Always 0 (some viewers read bit 15 = 1 as "this field is a colour")&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl4fgdev4ca0v181gb62m.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl4fgdev4ca0v181gb62m.webp" alt="One hardpoint STL drawn three ways: the mesh a slicer sees, the face colours stored in the file, and the surface types with hardpoints" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The bracket example (&lt;code&gt;examples/bracket_hardpoint.stl&lt;/code&gt;): 1,180 triangles, 16 faces labelled with 3 colours, 25 hardpoints. Drawn from the file itself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Why these choices (every one follows from section 1.3 or from a measurement):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Chosen&lt;/th&gt;
&lt;th&gt;Rejected&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where the labels live&lt;/td&gt;
&lt;td&gt;Header and attribute fields&lt;/td&gt;
&lt;td&gt;Data after the last triangle&lt;/td&gt;
&lt;td&gt;A trailer fails in numpy-stl and trimesh and becomes phantom triangles in admesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How a face is identified&lt;/td&gt;
&lt;td&gt;A 5-bit colour; a face is the edge-connected triangles sharing it&lt;/td&gt;
&lt;td&gt;A global face number&lt;/td&gt;
&lt;td&gt;Colours label any number of faces. Test parts needed 2 to 5 colours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surface parameters (plane, axis, radius)&lt;/td&gt;
&lt;td&gt;Not stored; the reader fits them&lt;/td&gt;
&lt;td&gt;A parameter table&lt;/td&gt;
&lt;td&gt;No room without a trailer. Fitting is cheap once the faces are known, and a reader must verify anyway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAD vertices&lt;/td&gt;
&lt;td&gt;3 bits per triangle, one per corner&lt;/td&gt;
&lt;td&gt;A separate vertex list&lt;/td&gt;
&lt;td&gt;No room. Needed where only two faces meet but a line runs into an arc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detecting edits&lt;/td&gt;
&lt;td&gt;CRC-32 of the triangle data in the header&lt;/td&gt;
&lt;td&gt;No check&lt;/td&gt;
&lt;td&gt;Stale labels on an edited mesh would mislead the converter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Labelled" flag&lt;/td&gt;
&lt;td&gt;Bit 7, and a labelled triangle is written first&lt;/td&gt;
&lt;td&gt;A separate guard&lt;/td&gt;
&lt;td&gt;Bit 7 of the first attribute byte also defeats the admesh text misread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bit 15&lt;/td&gt;
&lt;td&gt;Always 0&lt;/td&gt;
&lt;td&gt;Free use&lt;/td&gt;
&lt;td&gt;VisCAM-style viewers read bit 15 = 1 as a colour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust&lt;/td&gt;
&lt;td&gt;Every label is checked against tolerance&lt;/td&gt;
&lt;td&gt;Believe the file&lt;/td&gt;
&lt;td&gt;A wrong or stale label must not produce a wrong solid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  6. Does it help?
&lt;/h2&gt;

&lt;p&gt;Labels helped most on a coarse mesh and on a part with a sphere and a rounded edge. On a clean, fine mesh of flats and cylinders they changed little.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsbagxj08pqaog2cn0hbz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsbagxj08pqaog2cn0hbz.webp" alt="STEP face counts for three test parts from a plain STL and from its hardpoint twin, with the face count of the original CAD model marked" width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Volume error is the STEP solid against the original CAD solid (bracket 24,490.97 mm³, knob 12,347.01 mm³). Converter: STL2STEP 0.2.0-wip, Classic engine, default settings.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Triangles&lt;/th&gt;
&lt;th&gt;Plain: STEP faces&lt;/th&gt;
&lt;th&gt;Plain: volume error&lt;/th&gt;
&lt;th&gt;Plain: max deviation&lt;/th&gt;
&lt;th&gt;Hardpoint: STEP faces&lt;/th&gt;
&lt;th&gt;Hardpoint: volume error&lt;/th&gt;
&lt;th&gt;Hardpoint: max deviation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bracket, 0.05 mm mesh&lt;/td&gt;
&lt;td&gt;1,180&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;−0.013 %&lt;/td&gt;
&lt;td&gt;0.050 mm&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;+0.001 %&lt;/td&gt;
&lt;td&gt;0.049 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same bracket, coarse 0.3 mm mesh&lt;/td&gt;
&lt;td&gt;578&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;−0.075 %&lt;/td&gt;
&lt;td&gt;0.109 mm&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;+0.002 %&lt;/td&gt;
&lt;td&gt;0.086 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knob, 0.05 mm mesh&lt;/td&gt;
&lt;td&gt;2,958&lt;/td&gt;
&lt;td&gt;1,103&lt;/td&gt;
&lt;td&gt;−0.18 %&lt;/td&gt;
&lt;td&gt;0.129 mm&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;+0.007 %&lt;/td&gt;
&lt;td&gt;0.060 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;On the coarse bracket the plain conversion found 55 "flat" faces where the CAD model has 7.&lt;/li&gt;
&lt;li&gt;On the knob, the plain conversion took 14 s and the labelled one 2 s. The converter's second engine showed the same pattern in face counts: 21 to 17, 125 to 17 and 77 to 8.&lt;/li&gt;
&lt;li&gt;The CAD models have 16 faces (bracket) and 4 (knob), so even with labels the converter returns slightly more than the original. Cone and torus faces are labelled but the converter fits them as freeform surfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The knob in the converter's own 3D preview, from the plain STL and from its hardpoint twin (same triangles; blue = flat, green = cylinder or sphere, grey = freeform surface, dark = left as facets):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plain STL: 1,103 faces&lt;/th&gt;
&lt;th&gt;Hardpoint STL: 8 faces&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8yjh82mrtjp57v985e1.webp" alt="The knob converted from a plain STL: many flat strips and a dark faceted area" width="800" height="610"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F455d0jrnqihjla5ijo06.webp" alt="The knob converted from its hardpoint twin: one cylinder, one sphere, a smooth rounded edge" width="800" height="610"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Other measurements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Colours needed:&lt;/strong&gt; 2 on the knob, 3 on the bracket's 16 faces, 5 on an organic part's 69 fitted regions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round trip:&lt;/strong&gt; the reference reader rebuilt exactly the faces, types and hardpoints written, on all three parts (25 of 25 and 4 of 4 CAD vertices).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loaders:&lt;/strong&gt; numpy-stl returned triangles identical to the plain twin; trimesh loaded each file as watertight; the admesh text test classed each as binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain inputs unchanged:&lt;/strong&gt; five parts on the Classic engine and four on the second engine gave the same face counts, deviation and volume as before the feature was added.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writer speed, pure Python:&lt;/strong&gt; 4,894 triangles in 0.1 s and 15,384 in 0.2 s. The whole-body fallback added 0.4 s and 2.0 s. Measured on a cloud machine, not on a Core i3.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. What works, and what doesn't yet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tested here&lt;/th&gt;
&lt;th&gt;Simulation or stand-in only&lt;/th&gt;
&lt;th&gt;Not tested / unknown&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;Write, read, tamper check, &lt;code&gt;verify&lt;/code&gt;, &lt;code&gt;strip&lt;/code&gt;; examples pass&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loaders&lt;/td&gt;
&lt;td&gt;numpy-stl, trimesh; admesh text test (logic copied from the source)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;The slicer programs themselves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FreeCAD export&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Logic against a stand-in filled with OpenCASCADE geometry&lt;/td&gt;
&lt;td&gt;Real FreeCAD (0.20 to current); whether older versions accept the meshing call used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fusion export&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Per-face and whole-body routes against a stand-in&lt;/td&gt;
&lt;td&gt;Real Fusion; whether per-face meshes close; winding on reversed faces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Converter&lt;/td&gt;
&lt;td&gt;Three parts, two engines&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Real downloaded models; large meshes (labels tested up to 5,120 triangles)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Systems&lt;/td&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Windows, macOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Colour-aware viewers&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Whether a viewer using the Magics convention tints faces without &lt;code&gt;COLOR=&lt;/code&gt; in the header&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Known limits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any program that re-saves the STL writes its own header and zero attribute fields, so the labels vanish. That is also why the file falls back to a plain STL, which is the safe direction.&lt;/li&gt;
&lt;li&gt;Text (ASCII) STL cannot carry labels.&lt;/li&gt;
&lt;li&gt;More than 32 faces that all touch each other cannot be coloured; the writer refuses with an error. No test part has hit this.&lt;/li&gt;
&lt;li&gt;Shapes with shared faces (FreeCAD compsolids) may give a segment count that differs from the face count; types are then written as unknown.&lt;/li&gt;
&lt;li&gt;Fusion bodies inside occurrences are exported in their own component's coordinates, not the assembly's.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  8. Defensive disclosure: the hardpoint STL and variations
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;This section is written to be prior art. It describes the method and its variations in enough detail for a person skilled in CAD programming and mesh file formats to build them. I dedicate the methods described in this paper to the public, and I will not seek patent protection for them. Where a variation is untested, it says so; untested variations are disclosed as possibilities, not as results.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  8.1 The core method
&lt;/h3&gt;

&lt;p&gt;A method of storing computer-aided-design face information in a triangle-mesh file in the binary STL format, such that the file remains a valid binary STL, comprising:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep the file a binary STL.&lt;/strong&gt; Write an 80-byte header, a 4-byte little-endian triangle count, and for each triangle 50 bytes: a normal (3 × float32), three corners (9 × float32) and a 2-byte attribute word. The file size is exactly 84 + 50 × the triangle count. The geometry bytes are those a conventional export of the same mesh would give.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the file-level facts in the header.&lt;/strong&gt; Begin the 80-byte header with an ASCII marker and a version number, followed by space-separated &lt;code&gt;key=value&lt;/code&gt; fields: units, mesh tolerance, mesh angle, a checksum of the geometry bytes, and the writer's name. The header does not begin with the word &lt;code&gt;solid&lt;/code&gt;. Readers ignore keys they do not know.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the per-triangle facts in the attribute word.&lt;/strong&gt; For each triangle, store (a) a face colour in 5 bits, (b) a "labelled" flag, (c) a surface-type code in 3 bits, and (d) three bits flagging which of the triangle's three corners are CAD vertices (hardpoints). Leave remaining bits as reserved zeros, and keep the most significant bit zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identify faces by colouring, not numbering.&lt;/strong&gt; Assign each CAD face a colour so that any two faces whose triangles share an edge have different colours. A face is then recovered as a set of edge-connected triangles of equal colour. A small number of colours labels any number of faces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make labels checkable.&lt;/strong&gt; The checksum covers the 48 geometry bytes of every triangle in file order, so a reader detects that the mesh was changed after labelling, and then treats the file as an unlabelled STL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make labels hints.&lt;/strong&gt; A reader checks each labelled face against its own tolerance and falls back to unaided detection for any face that fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  8.2 The reference writer (as built)
&lt;/h3&gt;

&lt;p&gt;The reference writer is &lt;code&gt;hstl.encode&lt;/code&gt;, about 100 lines of Python (standard library only), so it can run inside FreeCAD's and Fusion's own interpreters.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Round and weld.&lt;/strong&gt; Round every coordinate to float32 (after an optional scale, e.g. ×10 for a program working in centimetres). Merge points closer than a welding tolerance, by default 2 × 10⁻⁷ of the part's bounding-box diagonal (at least 10⁻⁹). A CAD kernel gives the two faces along an edge points that differ in the last digits, so exact matching leaves the mesh open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drop collapsed triangles.&lt;/strong&gt; A triangle with two corners welded together is left out, and the writer returns which input triangle each output position holds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the mesh.&lt;/strong&gt; Count edges used by one triangle (open) and edges used by more than two, or by two triangles in the same direction (bad). Report the mesh as watertight when there are none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Colour the faces.&lt;/strong&gt; Build a graph whose nodes are faces and whose edges join faces sharing a mesh edge. Colour it greedily in smallest-last order: repeatedly remove the node of lowest remaining degree, then assign colours in reverse removal order, each the smallest not used by a neighbour. Refuse, with an error, if 32 colours are not enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flag the hardpoints.&lt;/strong&gt; For each CAD vertex, find the welded mesh point nearest to it within 50 welding tolerances; set the flag bit of every triangle corner using that point. Report how many vertices found no point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put a labelled triangle first.&lt;/strong&gt; Swap one labelled triangle into position 0. Its attribute word has bit 7 set, which makes the sniffing test of section 1.3 see a byte above 127 near the start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the records.&lt;/strong&gt; Recompute each normal from the welded corners as a unit vector. Pack the attribute word as (colour) | 0x80 | (surface type &amp;lt;&amp;lt; 8) | (corner flags &amp;lt;&amp;lt; 11). Triangles without a face get zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checksum and header.&lt;/strong&gt; CRC-32 over the 48 geometry bytes of every record in order; write the header with it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  8.3 The reference reader
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Recognise.&lt;/strong&gt; Read the 84-byte prefix. The file is a hardpoint STL if the header starts with the marker, the file size is 84 + 50 × count, and the version is known. Otherwise treat it as a plain STL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify.&lt;/strong&gt; Recompute the checksum. On a mismatch, ignore the labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weld&lt;/strong&gt; corners with identical float32 coordinates (a writer has already welded within tolerance, so exact matching suffices).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group&lt;/strong&gt; triangles that share an edge and have the same colour into faces. A triangle with the labelled flag clear belongs to no face.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt; each face by majority vote over its triangles' type codes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collect&lt;/strong&gt; the corners flagged as hardpoints.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  8.4 How a converter should use the labels
&lt;/h3&gt;

&lt;p&gt;This is the behaviour of the converter I measured with, written as a recipe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the header gives units other than millimetres and the user did not choose units, use the file's.&lt;/li&gt;
&lt;li&gt;Load and clean the mesh as usual. Skip labels if the mesh was reduced first.&lt;/li&gt;
&lt;li&gt;Match each cleaned triangle to the file's nearest triangle centre. Require at least 98 % to match within 0.25 × the header tolerance, otherwise ignore the labels.&lt;/li&gt;
&lt;li&gt;Handle each face by its type:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;If it passes&lt;/th&gt;
&lt;th&gt;If it fails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plane&lt;/td&gt;
&lt;td&gt;Least-squares plane; every corner within 1.5 × tolerance&lt;/td&gt;
&lt;td&gt;A true plane&lt;/td&gt;
&lt;td&gt;Unaided detection inside that face&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cylinder, sphere&lt;/td&gt;
&lt;td&gt;The usual fit; if its sanity rules reject it, a least-squares fit of the labelled kind with every corner within tolerance&lt;/td&gt;
&lt;td&gt;An analytic cylinder or sphere&lt;/td&gt;
&lt;td&gt;Unaided detection inside that face&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cone, torus, freeform&lt;/td&gt;
&lt;td&gt;None up front&lt;/td&gt;
&lt;td&gt;Freeform patch fitting, without a search for primitives&lt;/td&gt;
&lt;td&gt;The existing behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep as facets&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Kept as exact triangles&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown or unlabelled&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Unaided detection inside that area&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Map each hardpoint to the nearest mesh vertex within tolerance, and split border curves there, so a line running into an arc is fitted as a line and an arc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second cylinder check exists because the usual fit rejects shallow rounds, which it cannot tell from flat strips. A CAD label can.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.5 Getting the labels out of CAD programs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;FreeCAD&lt;/th&gt;
&lt;th&gt;Fusion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mesh&lt;/td&gt;
&lt;td&gt;One mesh of the whole shape with one segment per face: &lt;code&gt;MeshPart.meshFromShape(Shape, LinearDeflection, AngularDeflection, Relative=False, Segments=True)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Each face meshed on its own through &lt;code&gt;face.meshManager.createMeshCalculator()&lt;/code&gt;, with a surface tolerance (cm) and a maximum normal deviation (radians)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triangle to face&lt;/td&gt;
&lt;td&gt;Segment &lt;em&gt;i&lt;/em&gt; is face &lt;em&gt;i&lt;/em&gt; in explorer order (checked in FreeCAD's source)&lt;/td&gt;
&lt;td&gt;Known by construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surface type&lt;/td&gt;
&lt;td&gt;Class name of &lt;code&gt;face.Surface&lt;/code&gt; (Plane, Cylinder, Sphere, Cone, Toroid; anything else freeform)&lt;/td&gt;
&lt;td&gt;Last part of &lt;code&gt;face.geometry.objectType&lt;/code&gt; (Plane, Cylinder, Sphere, Cone, Torus; anything else freeform)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardpoints&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shape.Vertexes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;body.vertices&lt;/code&gt;, each &lt;code&gt;.geometry&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coordinates&lt;/td&gt;
&lt;td&gt;mm; the object's global placement is applied&lt;/td&gt;
&lt;td&gt;cm × 10; the body's own component coordinates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;If something is off&lt;/td&gt;
&lt;td&gt;Segment count differs from the face count: faces are still labelled, types are written as unknown&lt;/td&gt;
&lt;td&gt;Per-face meshes don't close: mesh the whole body and give each triangle the face of the per-face triangle its middle point lies on&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The whole-body fallback uses true point-to-triangle distance (nearest centre mislabelled about 1 %, section 4.2). It searches a uniform grid of cells, in growing shells, until the nearest triangle found is closer than the edge of the searched block.&lt;/p&gt;

&lt;p&gt;FreeCAD's source shows that &lt;code&gt;meshFromShape&lt;/code&gt; runs one incremental mesh over the whole shape, reads the triangulation back face by face in explorer order, flips reversed faces, and with &lt;code&gt;Segments=True&lt;/code&gt; adds one segment per face. For Fusion, whether per-face meshes share identical points along a common edge is not stated in Autodesk's pages; the writer reports open and wrongly oriented edges and the add-in switches to the whole-body route when there are any.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.6 Variations (disclosed; mostly untested)
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Where the bits go&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the reserved bits (5, 6 and 14) for: a "smooth border" flag for faces that meet tangentially, such as fillets; a body number for multi-body files; or a per-triangle "keep sharp normal" flag. Raise the version number.&lt;/li&gt;
&lt;li&gt;Widen the face colour to 6 or 7 bits, or the surface type to 4 bits, by taking reserved bits. Use bit 15 only if no colour-convention viewer is a concern.&lt;/li&gt;
&lt;li&gt;Flag edges (3 bits per triangle, one per side) instead of corners, to mark sharp or tangent edges.&lt;/li&gt;
&lt;li&gt;Identify a face by a run of consecutive triangles in file order plus a run-length code in the attribute words, instead of by colour.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Carrying more than flags&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spread a small parameter record (a plane's normal and offset, a cylinder's axis, point and radius) across the attribute words of consecutive triangles of a face, 16 bits at a time, with the surface type saying how many triangles carry it. The reader must still verify it against the mesh.&lt;/li&gt;
&lt;li&gt;Store a compressed STEP, IGES or other CAD file across the attribute words when it fits (section 4.3), with a header key saying so, and fall back to labels when it doesn't. The room is 2 bytes per triangle, so this suits fine meshes of simple parts least, and coarse meshes of complex parts least.&lt;/li&gt;
&lt;li&gt;Store a second checksum over the attribute words, or a checksum of the labels only, so a program that edits colours but not geometry can be told apart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Header&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Further &lt;code&gt;key=value&lt;/code&gt; fields: part name, source file name, body count, a flag that hardpoints include two-face vertices, a creation date. Readers ignore keys they don't know.&lt;/li&gt;
&lt;li&gt;Another checksum (CRC-64, SHA-256 truncated) in place of CRC-32; hash the attribute words too.&lt;/li&gt;
&lt;li&gt;A marker after the 80-byte header, such as a first record whose attribute word is the format marker, for programs that rewrite headers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Writers&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any program that knows the CAD faces and can mesh them: CadQuery, build123d, Onshape, SolidWorks, Blender (from face groups), OpenSCAD (from its own object tree).&lt;/li&gt;
&lt;li&gt;A STEP-to-hardpoint-STL command: mesh a STEP file with OpenCASCADE and read the triangulation back face by face. No CAD plug-in needed.&lt;/li&gt;
&lt;li&gt;A converter that, after fitting a mesh, writes the labels back into the input's attribute words, copying the triangles byte for byte. (I built this; after converting, the labelled copy converted again to the same result. 12 faces on a bracket, 136 on an organic part.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Readers&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A viewer that colours a file by face or surface type, or checks a file against the format.&lt;/li&gt;
&lt;li&gt;A slicer that uses the faces, for example to give round holes different perimeter settings from flat walls.&lt;/li&gt;
&lt;li&gt;Tools that strip the labels (&lt;code&gt;hstl.py strip&lt;/code&gt; writes a plain STL with identical triangles).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Other carriers (untested)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Per-face groups in OBJ or PLY, or per-triangle properties in 3MF, to carry the same facts. Those formats can hold groups natively; the point of this paper's method is that an unmodified STL carries them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8.7 The reference implementation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hstl.py&lt;/code&gt;&lt;/strong&gt; (about 600 lines, standard library only): &lt;code&gt;pack_word&lt;/code&gt;, &lt;code&gt;unpack_word&lt;/code&gt;, &lt;code&gt;make_header&lt;/code&gt;, &lt;code&gt;parse_header&lt;/code&gt;, &lt;code&gt;geometry_crc&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;decode&lt;/code&gt;, &lt;code&gt;colour&lt;/code&gt;, &lt;code&gt;encode&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;transfer_faces&lt;/code&gt;, &lt;code&gt;describe&lt;/code&gt;, &lt;code&gt;verify&lt;/code&gt;, &lt;code&gt;strip&lt;/code&gt;. Command line: &lt;code&gt;python hstl.py info|verify part.stl&lt;/code&gt;, &lt;code&gt;python hstl.py strip part.stl plain.stl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FreeCAD workbench&lt;/strong&gt; (&lt;code&gt;HardpointSTL&lt;/code&gt;): &lt;code&gt;hardpoint_fc.py&lt;/code&gt; (export logic, no GUI code) and &lt;code&gt;hardpoint_stl_fc.py&lt;/code&gt; (the command and dialog).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fusion add-in&lt;/strong&gt; (&lt;code&gt;HardpointSTL&lt;/code&gt;): &lt;code&gt;hardpoint_fusion.py&lt;/code&gt; (no &lt;code&gt;adsk&lt;/code&gt; imports) and &lt;code&gt;HardpointSTL.py&lt;/code&gt; (the command).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests, examples and illustration scripts&lt;/strong&gt; as in section 12.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Appendix A shows a real header and records.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. What I'd test next (feel free to use this plan)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Slicers.&lt;/strong&gt; Load &lt;code&gt;bracket_hardpoint.stl&lt;/code&gt; and &lt;code&gt;bracket_plain.stl&lt;/code&gt; into Cura, PrusaSlicer, OrcaSlicer and Bambu Studio and compare the sliced G-code. If they differ, the format has failed requirement 2 and I want to know.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FreeCAD.&lt;/strong&gt; Run &lt;code&gt;tests/freecad_selfcheck.py&lt;/code&gt; in FreeCAD (0.21, 1.0, 1.1) and send the output. Then export a part you actually modelled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fusion.&lt;/strong&gt; Run &lt;em&gt;Export hardpoint STL&lt;/em&gt; and note whether it took the per-face or the whole-body route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real models.&lt;/strong&gt; Export real parts from CAD and compare converter results with and without labels. All my test parts were built for the tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Colour-aware viewers.&lt;/strong&gt; Open a labelled file in a viewer that shows per-triangle STL colours and see whether faces get tinted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cone and torus.&lt;/strong&gt; A converter that builds cone and torus faces from the labels should turn countersinks and rounded edges into one exact face each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Big meshes.&lt;/strong&gt; Time the writer on a 500,000-triangle export on a low-end machine, and write a native version only if it is slow.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  10. Working together
&lt;/h2&gt;

&lt;p&gt;Nothing here came from one person working alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The readers I could study:&lt;/strong&gt; OrcaSlicer's admesh code, Uranium's STL reader and trimesh are all public, and I could read them. The person who reported OrcaSlicer issue 15993 handed me the reason bit 7 is in the format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The colour conventions&lt;/strong&gt; of VisCAM, SolidView and Materialise Magics decided what bit 15 must not be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CAD programs' own source and documentation:&lt;/strong&gt; FreeCAD is open source and its mesher is readable. Autodesk's API pages told me what Fusion would do, and where they stay quiet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Earlier mesh-to-STEP tools:&lt;/strong&gt; stlToSolid and breptile showed what is possible without any hints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What I added:&lt;/strong&gt; putting these pieces together, building the exporters, measuring the result, and writing down every way it went wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Collaboration in the open does three things a patent never will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's faster.&lt;/strong&gt; One person's dead end saves everyone else the trip. Checking whether a STEP file would fit cost me an afternoon of measuring. With this paper it costs you a table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It keeps the idea free.&lt;/strong&gt; A published idea counts as prior art. The more of us write down what we try, with dates and enough detail to rebuild it, the harder it is for anyone to claim what hobbyists worked out together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It gives better answers.&lt;/strong&gt; I only tested three parts. More people exporting, converting and comparing results will tell us far more.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;An open invitation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run the checks in section 9&lt;/strong&gt; and post your results, good or bad, as an issue or discussion on the repository. Photos of broken meshes are especially welcome. Failure is data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write an exporter&lt;/strong&gt; for the CAD program you use. &lt;code&gt;hstl.encode&lt;/code&gt; takes triangles, a face number per triangle, a type per face and a list of CAD vertices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a reader&lt;/strong&gt;, or make a converter that uses the labels better than mine does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write up your own experiments&lt;/strong&gt; the same way: dated, public, enough detail to rebuild them. Link back here and I'll link to you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everyone who contributes gets credited in the next version.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. How this was made
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;My part:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the idea, the six requirements in section 1.2 and the rule that a labelled file must stay a normal STL;&lt;/li&gt;
&lt;li&gt;the instruction to research first, and the question about hiding a STEP file;&lt;/li&gt;
&lt;li&gt;deciding what to publish, and when.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The AI assistant's part&lt;/strong&gt; (working to that design):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the research into how readers behave, the format design, the code, the tests, the measurements and the drawings;&lt;/li&gt;
&lt;li&gt;the first draft of this write-up;&lt;/li&gt;
&lt;li&gt;its share of the mistakes (1 to 6 in section 4.2).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every measurement in this paper was made by running the code in this repository, or the converter named in section 6, on a Linux cloud machine between 4 and 10 October 2026. None was made on real FreeCAD, Fusion or slicer software.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Code, data and how to reproduce
&lt;/h2&gt;

&lt;p&gt;Two repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This paper:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/a-point-of-contention" rel="noopener noreferrer"&gt;&lt;code&gt;a-point-of-contention&lt;/code&gt;&lt;/a&gt;. Its front page is this paper, generated from &lt;code&gt;paper/paper.md&lt;/code&gt;. It also holds the reference reader and writer (&lt;code&gt;code/hstl.py&lt;/code&gt;, Python 3, standard library only), the four example files (&lt;code&gt;data/&lt;/code&gt;: two parts, each as a hardpoint STL and as a plain STL with the same triangles), the full research notes (&lt;code&gt;notes/DESIGN_NOTES.md&lt;/code&gt;) and the specification (&lt;code&gt;notes/HARDPOINT_STL.md&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The program:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/hardpoint-stl" rel="noopener noreferrer"&gt;&lt;code&gt;hardpoint-stl&lt;/code&gt;&lt;/a&gt;. The FreeCAD workbench and Fusion add-in (&lt;code&gt;plugins/&lt;/code&gt;), the installers, the tests (&lt;code&gt;test_hstl.py&lt;/code&gt; and &lt;code&gt;test_install.py&lt;/code&gt; use the standard library only; &lt;code&gt;test_hardpoint_cad.py&lt;/code&gt; needs &lt;code&gt;cadquery-ocp&lt;/code&gt;, &lt;code&gt;numpy&lt;/code&gt;, &lt;code&gt;trimesh&lt;/code&gt;, and optionally &lt;code&gt;numpy-stl&lt;/code&gt;; &lt;code&gt;freecad_selfcheck.py&lt;/code&gt; runs inside real FreeCAD) and the scripts that regenerate the example files and drawings (&lt;code&gt;tools/make_examples.py&lt;/code&gt;, &lt;code&gt;tools/make_illustrations.py&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not included in either:&lt;/strong&gt; the converter I measured with. The numbers in section 6 can't be re-run from these repositories. The format checks, round trips and loader checks can.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  13. Licence, and keeping it free
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text and figures:&lt;/strong&gt; &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener noreferrer"&gt;Creative Commons Attribution-ShareAlike 4.0&lt;/a&gt;. Anyone may copy, share and adapt it. Adapted versions must credit this paper and stay under the same licence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://polyformproject.org/licenses/noncommercial/1.0.0/" rel="noopener noreferrer"&gt;PolyForm Noncommercial 1.0.0&lt;/a&gt;. Free for non-commercial use. It is source-available, not open source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The methods:&lt;/strong&gt; dedicated to the public (section 8). A licence on text or code can't stop anyone &lt;em&gt;using&lt;/em&gt; a method, and I don't want it to. The point of publishing is that the format stays free for everyone to implement, including commercially, in their own code: from today's date this paper is a dated public description of it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  14. Disclaimers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Untested in the real programs.&lt;/strong&gt; Nothing here was run in real FreeCAD, Fusion or a slicer. Statements about what those programs do come from their source code or documentation, as cited, and may be wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measurements.&lt;/strong&gt; The results in section 6 come from three test parts built for the tests and one converter. They are not a general claim about the format or about anyone else's converter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prior art.&lt;/strong&gt; My search for earlier work was informal (section 2). Nothing here is a statement that the idea is new, or about the validity of anyone's rights, and nothing here is legal advice. Whether and how a defensive publication counts as prior art is a question for a patent attorney.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other people's work.&lt;/strong&gt; Product, company and file-format names belong to their owners and are used only to identify them. This project is not affiliated with any of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your files.&lt;/strong&gt; The exporters write a new file where you choose and don't change your CAD document. A hardpoint STL is meant to be an ordinary STL; if you find one that isn't, please say so before you print it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  15. How to cite
&lt;/h2&gt;

&lt;p&gt;Armstrong, M. (2026). &lt;em&gt;A POINT of contention: STL to STEP conversion shouldn't be that HARD. A proposed hybrid STL format that is CAD-friendly and slicer-compatible. A hobbyist's field notes on the hardpoint STL, including a defensive publication of the format.&lt;/em&gt; Version 1.0, 15 October 2026. &lt;a href="https://github.com/The-Dorkknight/a-point-of-contention" rel="noopener noreferrer"&gt;https://github.com/The-Dorkknight/a-point-of-contention&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;OrcaSlicer, &lt;code&gt;deps_src/admesh/stlinit.cpp&lt;/code&gt; (binary or text test, count from file size). &lt;a href="https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer/main/deps_src/admesh/stlinit.cpp" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/OrcaSlicer/OrcaSlicer/main/deps_src/admesh/stlinit.cpp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OrcaSlicer issue 15993, small binary STLs imported as empty objects. &lt;a href="https://github.com/OrcaSlicer/OrcaSlicer/issues/15993" rel="noopener noreferrer"&gt;https://github.com/OrcaSlicer/OrcaSlicer/issues/15993&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ultimaker Uranium, &lt;code&gt;STLReader.py&lt;/code&gt; (Cura loads STL through numpy-stl). &lt;a href="https://raw.githubusercontent.com/Ultimaker/Uranium/main/plugins/FileHandlers/STLReader/STLReader.py" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/Ultimaker/Uranium/main/plugins/FileHandlers/STLReader/STLReader.py&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;trimesh, &lt;code&gt;exchange/stl.py&lt;/code&gt; (exact-length check, attribute field kept). &lt;a href="https://raw.githubusercontent.com/mikedh/trimesh/main/trimesh/exchange/stl.py" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/mikedh/trimesh/main/trimesh/exchange/stl.py&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;STL (file format), Wikipedia: binary layout, the &lt;code&gt;solid&lt;/code&gt; rule, the two colour conventions. &lt;a href="https://en.wikipedia.org/wiki/STL_(file_format)" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/STL_(file_format)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;FreeCAD source: &lt;code&gt;src/Mod/MeshPart/App/Mesher.cpp&lt;/code&gt;, &lt;code&gt;AppMeshPartPy.cpp&lt;/code&gt;; &lt;code&gt;src/Mod/Part/App/TopoShape.cpp&lt;/code&gt;, &lt;code&gt;BRepMesh.cpp&lt;/code&gt;, &lt;code&gt;Tools.cpp&lt;/code&gt;; &lt;code&gt;src/Mod/Mesh/App/Mesh.pyi&lt;/code&gt;. &lt;a href="https://github.com/FreeCAD/FreeCAD" rel="noopener noreferrer"&gt;https://github.com/FreeCAD/FreeCAD&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Autodesk Fusion API: &lt;a href="https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/TriangleMeshCalculator.htm" rel="noopener noreferrer"&gt;TriangleMeshCalculator&lt;/a&gt;, &lt;a href="https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/TriangleMesh.htm" rel="noopener noreferrer"&gt;TriangleMesh&lt;/a&gt;, &lt;a href="https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/BRepFace.htm" rel="noopener noreferrer"&gt;BRepFace&lt;/a&gt;, &lt;a href="https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/BRepBody.htm" rel="noopener noreferrer"&gt;BRepBody&lt;/a&gt;, &lt;a href="https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/FileDialog.htm" rel="noopener noreferrer"&gt;FileDialog&lt;/a&gt;, &lt;a href="https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/files/SelectionFilters_UM.htm" rel="noopener noreferrer"&gt;selection filters&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Autodesk forum, "How can I capture facets from a surface using the API" (per-face meshing example; silent on whether neighbouring faces line up). &lt;a href="https://forums.autodesk.com/t5/fusion-api-and-scripts-forum/how-can-i-capture-facets-from-a-surfaces-using-the-api/td-p/7070657" rel="noopener noreferrer"&gt;https://forums.autodesk.com/t5/fusion-api-and-scripts-forum/how-can-i-capture-facets-from-a-surfaces-using-the-api/td-p/7070657&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;stlToSolid, mesh to STEP by segmentation and primitive fitting. &lt;a href="https://github.com/Crypto69/stlToSolid" rel="noopener noreferrer"&gt;https://github.com/Crypto69/stlToSolid&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;breptile, mesh to STEP. &lt;a href="https://pypi.org/project/breptile/0.1.0/" rel="noopener noreferrer"&gt;https://pypi.org/project/breptile/0.1.0/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;numpy-stl. &lt;a href="https://github.com/WoLpH/numpy-stl" rel="noopener noreferrer"&gt;https://github.com/WoLpH/numpy-stl&lt;/a&gt; · trimesh. &lt;a href="https://github.com/mikedh/trimesh" rel="noopener noreferrer"&gt;https://github.com/mikedh/trimesh&lt;/a&gt; · cadquery-ocp (OpenCASCADE 8.0.1). &lt;a href="https://github.com/CadQuery/OCP" rel="noopener noreferrer"&gt;https://github.com/CadQuery/OCP&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pages and source files opened between 3 and 10 October 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  Appendix A: a real hardpoint STL
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;examples/bracket_hardpoint.stl&lt;/code&gt; (59,084 bytes = 84 + 50 × 1,180).&lt;/p&gt;

&lt;p&gt;The header, as written (80 bytes, padded with spaces):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HARDPOINT-STL 1 u=mm tol=0.05 ang=15 crc=e67609a0 by=cad-example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The first triangle's 50-byte record, in hexadecimal:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00 00 80 bf  00 00 00 00  00 00 00 00        normal  (-1, 0, 0)
00 00 f0 c1  00 00 80 c1  00 00 20 41        corner 1 (-30, -16, 10)
00 00 f0 c1  00 00 80 41  00 00 00 00        corner 2 (-30,  16,  0)
00 00 f0 c1  00 00 80 c1  00 00 00 00        corner 3 (-30, -16,  0)
80 39                                         attribute word 0x3980
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Reading the attribute word &lt;code&gt;0x3980&lt;/code&gt; = &lt;code&gt;0011 1001 1000 0000&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bits&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0–4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;face colour 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;labelled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8–10&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;surface type 1: plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11–13&lt;/td&gt;
&lt;td&gt;7 (all three set)&lt;/td&gt;
&lt;td&gt;all three corners are CAD vertices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;as required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;python src/hstl.py info examples/bracket_hardpoint.stl&lt;/code&gt; prints:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hardpoint STL version 1, 1180 triangles, units mm, written by cad-example
  16 faces (1 cone, 8 cylinder, 7 plane), 25 hardpoints
  meshed to 0.05 mm, 15 degrees
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;and &lt;code&gt;python src/hstl.py verify examples/bracket_hardpoint.stl&lt;/code&gt; ends with &lt;code&gt;RESULT: PASS&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  About this repository
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/blob/main/paper/paper.md" rel="noopener noreferrer"&gt;&lt;code&gt;paper/paper.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The paper (this page is generated from it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/blob/main/paper/a-point-of-contention-v1.0.pdf" rel="noopener noreferrer"&gt;&lt;code&gt;paper/a-point-of-contention-v1.0.pdf&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The paper as a PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/tree/main/paper/figures" rel="noopener noreferrer"&gt;&lt;code&gt;paper/figures/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Figures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/blob/main/code/hstl.py" rel="noopener noreferrer"&gt;&lt;code&gt;code/hstl.py&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Reference reader and writer (Python, standard library only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/tree/main/data" rel="noopener noreferrer"&gt;&lt;code&gt;data/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Example files, each as a hardpoint STL and a plain STL with the same triangles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/tree/main/notes" rel="noopener noreferrer"&gt;&lt;code&gt;notes/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The full research notes, the format specification, and how to archive this paper as prior art&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/tree/main/tools" rel="noopener noreferrer"&gt;&lt;code&gt;tools/&lt;/code&gt;&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/tree/main/tests" rel="noopener noreferrer"&gt;&lt;code&gt;tests/&lt;/code&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Rebuild the PDF, web page and README; checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/tree/main/docs" rel="noopener noreferrer"&gt;&lt;code&gt;docs/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The web version for GitHub Pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The FreeCAD workbench and Fusion add-in live in the separate&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/hardpoint-stl" rel="noopener noreferrer"&gt;&lt;code&gt;hardpoint-stl&lt;/code&gt;&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;To rebuild: &lt;code&gt;python3 tools/build_paper.py&lt;/code&gt; (pandoc and Chromium), then &lt;code&gt;python3 tools/make_readme.py&lt;/code&gt;.&lt;br&gt;
GitHub's &lt;strong&gt;Cite this repository&lt;/strong&gt; button uses &lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/blob/main/CITATION.cff" rel="noopener noreferrer"&gt;&lt;code&gt;CITATION.cff&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Join in:&lt;/strong&gt; run the checks in section 9 and post results with photos as an issue or discussion, write an exporter&lt;br&gt;
for another CAD program, make a converter that uses the labels better, or point out prior art I missed.&lt;br&gt;
Everyone who contributes gets credited in the next version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Licence:&lt;/strong&gt; paper text and figures &lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/blob/main/LICENSE.md" rel="noopener noreferrer"&gt;CC BY-SA 4.0&lt;/a&gt;; code in &lt;code&gt;code/&lt;/code&gt; and &lt;code&gt;tools/&lt;/code&gt;&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/a-point-of-contention/blob/main/LICENSE-CODE.md" rel="noopener noreferrer"&gt;PolyForm Noncommercial 1.0.0&lt;/a&gt; (source-available, not open source); the methods described are&lt;br&gt;
dedicated to the public as a defensive publication. Not legal advice.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Fin-ally Supported</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:52:44 +0000</pubDate>
      <link>https://dev.to/open-madscience/fin-ally-supported-363n</link>
      <guid>https://dev.to/open-madscience/fin-ally-supported-363n</guid>
      <description>&lt;p&gt;&lt;strong&gt;Shark-fin supports for long, flat, tilted overhangs: a hobbyist's field guide and a defensive publication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Matthew Armstrong&lt;/strong&gt; (&lt;a href="https://github.com/The-Dorkknight" rel="noopener noreferrer"&gt;@The-Dorkknight&lt;/a&gt;)\&lt;br&gt;
Version 1.0 · First published 19 October 2026 · Text and figures: CC BY-SA 4.0 · Code: PolyForm Noncommercial 1.0.0\&lt;br&gt;
Repository: &lt;a href="https://github.com/The-Dorkknight/fin-ally-supported" rel="noopener noreferrer"&gt;https://github.com/The-Dorkknight/fin-ally-supported&lt;/a&gt; · Software: &lt;a href="https://github.com/The-Dorkknight/cura-shark-support" rel="noopener noreferrer"&gt;https://github.com/The-Dorkknight/cura-shark-support&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📄 &lt;strong&gt;&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/blob/main/paper/fin-ally-supported-v1.0.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/strong&gt; · 🌐 &lt;a href="https://The-Dorkknight.github.io/fin-ally-supported/" rel="noopener noreferrer"&gt;Web page&lt;/a&gt; · 🔧 &lt;a href="https://github.com/The-Dorkknight/cura-shark-support" rel="noopener noreferrer"&gt;The plugin&lt;/a&gt; · About this repository&lt;/p&gt;

&lt;p&gt;This page is the full paper (the same text and figures as the PDF).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contents:&lt;/strong&gt; Why this paper exists · The problem · What already exists · Shark fins in one page · The method in detail · How it got here: the mistakes, in order · Numbers · Defensive disclosure: the method and its variations · Test plan (please print this) · Limitations and open questions · Fin-ally, a social species · Code, data and how to reproduce · How this was made · Licence, and keeping it free · Disclaimers · How to cite · References&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Read this first.&lt;/strong&gt; Every number in this paper comes from geometry code, slicer estimates or G-code. &lt;strong&gt;Nothing&lt;br&gt;
has been printed and broken yet.&lt;/strong&gt; Where a result is a slicer estimate or a simulation it says so. Section 8 is a&lt;br&gt;
test plan for anyone with a printer and a free evening.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Abstract
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu7ddh7yox30uypbt1hnw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu7ddh7yox30uypbt1hnw.png" alt="One fin, side view, labelled" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Printing a long box tilted onto one of its long edges makes its walls strong along their length, but turns its base&lt;br&gt;
into a long, flat overhang at 30–55°. That is near, or just past, the angle where slicers start adding support, and over 150 mm it tends to sag and&lt;br&gt;
curl.&lt;br&gt;
Normal slicer supports fix it by filling the space under the whole face with a forest that welds itself to the&lt;br&gt;
part. This paper describes &lt;strong&gt;shark-fin supports&lt;/strong&gt;: a row of thin vertical plates that run &lt;em&gt;downhill&lt;/em&gt; under the face,&lt;br&gt;
follow its slope, touch it only at small teeth, stop short of the top with a blunt flat tip, and stand on a crossbar&lt;br&gt;
foot and a one-layer brim pad. They are cheap (on the test box, 1.68 m of filament and about 40 minutes for five&lt;br&gt;
fins, slicer estimate) and should come off in one piece each. The method is implemented as an open Cura plugin with a&lt;br&gt;
toolbar button that lets you click the face you want supported. Section 7 is a &lt;strong&gt;defensive disclosure&lt;/strong&gt;: the method&lt;br&gt;
and a long list of variations are published as prior art, to make it much harder for anyone to patent them and&lt;br&gt;
charge hobbyists for them. They are dedicated to the public.&lt;/p&gt;




&lt;h2&gt;
  
  
  0. Why this paper exists
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;To keep it free.&lt;/strong&gt; A published method is prior art, which makes it much harder for anyone to get a valid
patent on it later. This paper puts the shark fin, its teeth, its blunt top, its feet and its many cousins on the
public record with a date, so that a company filing on them later has this to get past first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;To write down the dead ends.&lt;/strong&gt; Every version so far had a mistake in it that looked like a good idea at the
time. Section 5 lists them, so nobody has to make them twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;To bring makers together.&lt;/strong&gt; A plugin is a tool; a paper is an invitation. Print the test plan, post the
results, fork the code, prove me wrong. Everyone who contributes gets credit in the next version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selfish reasons.&lt;/strong&gt; Publishing things like this tends to bring more interesting 3D printing tools and tricks
back to me. It also makes me roughly 0.3 % more employable per paper, which, compounded over a few more
papers, is a very efficient use of an evening.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. The problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 Tilting for strength
&lt;/h3&gt;

&lt;p&gt;FDM parts are weakest between layers. A long box printed flat is strong along its length only in the layers&lt;br&gt;
themselves; printed on its end it is strong in the wrong direction and wobbly. A common compromise is to &lt;strong&gt;stand&lt;br&gt;
the box on one long edge and tilt it&lt;/strong&gt;, so the long walls run along the layers and the base and one side become&lt;br&gt;
sloping faces (Figure 1).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwb192pizam628nkkjyk9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwb192pizam628nkkjyk9.png" alt="The 35° test box and its fins" width="635" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. The 35° test box (grey, see-through) and its fins (orange).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The test part used throughout is a 150 × 40 × 25 mm box rotated 35° about one long bottom edge, so its base&lt;br&gt;
becomes a &lt;strong&gt;55° overhang&lt;/strong&gt; (in Cura's convention: 0° is a vertical wall, 90° a flat ceiling). It is deliberately&lt;br&gt;
boring. Boring parts make honest tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Why "it's under the angle" is not enough
&lt;/h3&gt;

&lt;p&gt;Slicers and rules of thumb treat overhangs up to about 45–55° as printable without support. That is true for a short&lt;br&gt;
ledge. Over a long face several things add up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each layer's edge sticks out a little further than the one below, and a long free edge cools unevenly and curls
upwards, which the nozzle then hits;&lt;/li&gt;
&lt;li&gt;the lowest part of the face is printed first and has nothing above it to hold it flat;&lt;/li&gt;
&lt;li&gt;a long, thin, freshly printed edge sags under its own weight and the drag of the nozzle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The usual result is a wavy, curled base, or a print knocked loose halfway through.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.3 What slicers offer
&lt;/h3&gt;

&lt;p&gt;Turning on normal supports fills the space under the whole base with support. It works, but it uses a lot of&lt;br&gt;
plastic and time, it bonds along every layer of the face, and on a tilted part it leaves a large scarred surface.&lt;br&gt;
Tree supports use less material but branch to many contact points and can be awkward to remove from a long flat&lt;br&gt;
face. Neither knows that this particular overhang only needs &lt;strong&gt;a few stiff ribs&lt;/strong&gt; to stop it curling.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What already exists
&lt;/h2&gt;

&lt;p&gt;This section is an informal prior-art search, not a professional one. It is here to give credit and to be clear&lt;br&gt;
about what is new.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slicer supports.&lt;/strong&gt; Cura, PrusaSlicer and OrcaSlicer offer grid/line supports, tree or "organic" supports, support
blockers and enforcers, painted supports, and "support meshes": any shape the user loads can be marked to print as
support (&lt;a href="https://help.prusa3d.com/en/article/support-material_1698/" rel="noopener noreferrer"&gt;Prusa Knowledge Base&lt;/a&gt;; &lt;a href="https://printago.io/guides/prusaslicer-supports" rel="noopener noreferrer"&gt;Printago guide&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hand-drawn sacrificial ribs.&lt;/strong&gt; Designers have long added thin ribs to a model in CAD to hold up an overhang and
snapped them off afterwards (&lt;a href="https://www.hubs.com/knowledge-base/supports-3d-printing-technology-overview" rel="noopener noreferrer"&gt;Hubs overview of supports&lt;/a&gt;). That is the same idea as a fin, made by hand,
usually as a plain plate that bonds along its full edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ribbed support vaults&lt;/strong&gt; (Tricard, Claux and Lefebvre, &lt;em&gt;Computer Graphics Forum&lt;/em&gt;, 2019) grow rib-like internal
walls inside hollowed FFF prints to hold up internal overhangs (&lt;a href="https://www.unilim.fr/pages_perso/frederic.claux/blog/2019/2019-06-21_Ribbed_support_vaults.html" rel="noopener noreferrer"&gt;project page&lt;/a&gt;). They are walls that
support from below, but inside the object and merging into it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teeth on metal supports.&lt;/strong&gt; In metal powder-bed printing, block supports often meet the part through small
teeth, whose size, shape and penetration affect the strength of the support (&lt;a href="https://www.materialise.com/en/inspiration/articles/optimal-metal-supports" rel="noopener noreferrer"&gt;Materialise&lt;/a&gt;). The fins'
teeth borrow from that habit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;US 9,469,057 (3D Systems, priority 2012).&lt;/strong&gt; Claims a method of printing an FFF support whose upper portion
contacts the part at discrete "fine points", with a retract, lift, move, lower sequence between them
(&lt;a href="https://patents.justia.com/patent/9469057" rel="noopener noreferrer"&gt;Justia&lt;/a&gt;; &lt;a href="https://patents.google.com/patent/US9469057B2/en" rel="noopener noreferrer"&gt;Google Patents&lt;/a&gt;, which lists it as active until about 2034). Worth knowing:
in the top layers of a fin, each tooth tip is a small separate island in the slice, and slicers usually retract
and travel (and may lift) between islands. &lt;strong&gt;How this patent relates to Shark Support has not been assessed.&lt;/strong&gt;
If it matters to you, ask a patent professional; nothing here is legal advice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As far as this search found, nobody describes &lt;strong&gt;downhill fins generated automatically under a flat tilted face,&lt;br&gt;
contacting through teeth, cut short with a blunt top, braced by a crossbar foot and brim pad, with a click-to-pick&lt;br&gt;
face selection&lt;/strong&gt;. If you know of earlier work, please open an issue: the next version will credit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Shark fins in one page
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fho9by7bq0r71r23ed7c9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fho9by7bq0r71r23ed7c9.png" alt="Plan view of the fins" width="799" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each flat face that faces downwards:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Downhill.&lt;/strong&gt; Work out the direction a ball would roll down the face, flattened onto the bed. Fins run that way,
spaced evenly across the face (30 mm by default, 4 mm in from the sides). Figure 2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure.&lt;/strong&gt; Along each fin line, cast vertical rays up from the bed every 0.5 mm to find the part's underside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shape&lt;/strong&gt; each fin as a thin vertical plate (2.4 mm thick at the bed, 1.2 mm at the top) whose top edge sits 1 mm
under the part, with &lt;strong&gt;teeth every 3 mm&lt;/strong&gt; that reach up to 0.15 mm short of it. The &lt;strong&gt;first two teeth&lt;/strong&gt; from the
low end reach 0.1 mm &lt;em&gt;into&lt;/em&gt; the part, so the start of the overhang, where curling begins, is anchored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop early.&lt;/strong&gt; The fin follows the part only to &lt;strong&gt;85 %&lt;/strong&gt; of its height; the part bridges the rest. The fin ends
in a &lt;strong&gt;5 mm flat top&lt;/strong&gt;, then goes &lt;strong&gt;straight down&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stand it up&lt;/strong&gt; with a 4 mm high crossbar foot under the tip and a 4 mm wide, one-layer brim pad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Print it as a solid little part&lt;/strong&gt; next to the model, not as Cura "support".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Figure 10. One fin of the test box, side view, drawn from the code's output and labelled.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The method in detail
&lt;/h2&gt;

&lt;p&gt;This section is written so that someone could rebuild the method from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Finding the face
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Automatic mode.&lt;/strong&gt; Triangles of the model that face downwards are grouped into flat faces: two triangles join when&lt;br&gt;
they share an edge and their normals differ by less than 3°. A face qualifies when its overhang angle α (the angle&lt;br&gt;
between its outward normal and the horizontal, so 0° is a wall and 90° a ceiling) lies between 20° and 70°, its area&lt;br&gt;
is at least 300 mm², and it spans at least 15 mm downhill, measured in plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Picked mode.&lt;/strong&gt; The user clicks the face. The slicer reports the 3D point under the mouse (in Cura, its picking&lt;br&gt;
render pass). The plugin finds the triangle that contains the point (the projection of the point lies inside the&lt;br&gt;
triangle within a small tolerance, and the point is within 1 mm, or 1 % of the model size, of its plane), checks&lt;br&gt;
that it faces downwards and is not a flat ceiling, and uses the flat face that triangle belongs to. A picked face&lt;br&gt;
ignores the automatic angle and size limits: the user has said it needs help.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Direction and layout
&lt;/h3&gt;

&lt;p&gt;For a face with unit normal &lt;strong&gt;n&lt;/strong&gt;, gravity &lt;strong&gt;g&lt;/strong&gt; = (0, 0, −1) projected into the face's plane is&lt;br&gt;
&lt;strong&gt;d&lt;/strong&gt; = &lt;strong&gt;g&lt;/strong&gt; − (&lt;strong&gt;g&lt;/strong&gt;·&lt;strong&gt;n&lt;/strong&gt;)&lt;strong&gt;n&lt;/strong&gt;, the steepest descent. Its horizontal part, normalised, is the &lt;em&gt;downhill&lt;/em&gt; direction&lt;br&gt;
&lt;strong&gt;h&lt;/strong&gt;; &lt;strong&gt;c&lt;/strong&gt; = &lt;strong&gt;ẑ&lt;/strong&gt; × &lt;strong&gt;h&lt;/strong&gt; is &lt;em&gt;across&lt;/em&gt;. Projecting the face's corners onto &lt;strong&gt;c&lt;/strong&gt; gives its width; fins sit at&lt;br&gt;
evenly spaced positions across it, a side margin from each edge, at most a set spacing apart (one fin in the middle&lt;br&gt;
if the face is narrow). A flat ceiling has no downhill and is skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Measuring the underside
&lt;/h3&gt;

&lt;p&gt;Along each fin line, at steps of 0.5 mm in &lt;strong&gt;h&lt;/strong&gt;, a vertical ray goes up from the bed. For a closed mesh the first&lt;br&gt;
surface a ray meets from below always faces downwards, so only downward triangles are indexed, in a uniform grid in&lt;br&gt;
plan. Each hit gives the height of the part above the bed and which triangle was hit. The fin keeps the &lt;strong&gt;longest&lt;br&gt;
continuous run&lt;/strong&gt; of samples that hit its own face and are at least the &lt;em&gt;start height&lt;/em&gt; (4 mm) above the bed, so it&lt;br&gt;
never climbs onto a neighbouring face or across a gap. Below the start height the face is short enough to print on&lt;br&gt;
its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 The top edge: relief, teeth and anchors
&lt;/h3&gt;

&lt;p&gt;Let s run along the fin from its low end s₀, and z(s) be the underside height. The fin's top edge is&lt;br&gt;
z(s) − &lt;em&gt;relief&lt;/em&gt; (1 mm). Teeth are placed from the low end: the first tooth's ramp starts at s₀, then one every&lt;br&gt;
&lt;em&gt;pitch&lt;/em&gt; (3 mm), and a last one near the end. Each tooth ramps up over 1 mm, stays flat for 0.6 mm, and ramps down.&lt;br&gt;
At its flat top it reaches z(s) − &lt;em&gt;gap&lt;/em&gt; (0.15 mm) or, for the first &lt;em&gt;N&lt;/em&gt; anchor teeth (2), z(s) + &lt;em&gt;overlap&lt;/em&gt;&lt;br&gt;
(0.1 mm, half of a 0.2 mm layer). The anchors therefore print touching the part's outer wall in the same layers&lt;br&gt;
(Figure 6); the other tips stop just short.&lt;/p&gt;

&lt;p&gt;Why teeth and not a plain edge: a plain edge 0.15 mm under a sloping face touches it along every layer. Teeth turn&lt;br&gt;
that into a dotted line, so the fin snaps off with a short pull and leaves small, evenly spaced marks.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5 Stopping short, with a blunt top
&lt;/h3&gt;

&lt;p&gt;The fin follows the part only up to a height z_cut = (1 − &lt;em&gt;top cut&lt;/em&gt;) × the highest edge height it would otherwise&lt;br&gt;
reach (top cut 15 %, so 85 %). Teeth near the cut are flattened to z_cut so nothing pokes above it. From there the&lt;br&gt;
fin runs &lt;strong&gt;flat for 5 mm&lt;/strong&gt; (&lt;em&gt;tip length&lt;/em&gt;), then ends.&lt;/p&gt;

&lt;p&gt;Why: a sharp tip means the last layers of the fin are a few millimetres long or less. The printer brakes and&lt;br&gt;
accelerates for each tiny move, which is slow and prints badly. With a 5 mm flat top, no layer of the fin is shorter&lt;br&gt;
than 5 mm. And the top 15 % of a long tilted face is the part closest to its own upper edge, which is already held&lt;br&gt;
by the rest of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.6 The back edge
&lt;/h3&gt;

&lt;p&gt;By default the fin goes &lt;strong&gt;straight down&lt;/strong&gt; from the end of the flat top. Variants (Figure 9) lean the back out at a&lt;br&gt;
set angle (20° from vertical) for a wider base, or make it round (a quarter ellipse) or concave. Each point of the&lt;br&gt;
back is checked against the part above it and lowered if needed, and the back never goes below a 0.6 mm stub.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fou57ij21xxivvjnlot32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fou57ij21xxivvjnlot32.png" alt="Back-edge variants" width="799" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.7 Thickness
&lt;/h3&gt;

&lt;p&gt;The fin is a "ribbon" of columns along s. Each column has a bottom width (2.4 mm), a top width that tapers linearly&lt;br&gt;
with height from 2.4 to 1.2 mm, and a top height. Neighbouring columns are joined into a closed, outward-facing&lt;br&gt;
mesh. Thicker at the bed, where bending is worst; thin at the top, where the teeth are.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.8 Crossbar foot
&lt;/h3&gt;

&lt;p&gt;A bar across the fin, 2 mm thick and 4 mm high, its length ¼ of the fin's footprint (at least 10 mm), under the&lt;br&gt;
middle of the flat top, where the fin is tallest. Fins with a footprint over 100 mm get a second bar halfway&lt;br&gt;
between the low end and the 85 % cut.&lt;br&gt;
A bar is lowered if the part comes within the relief above it, and skipped if that leaves less than 1.5 mm.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.9 Brim pad
&lt;/h3&gt;

&lt;p&gt;A rectangle one first layer thick, 4 mm wider than the fin on every side, plus one around each crossbar. Where the&lt;br&gt;
part itself comes close to the bed at the low end, the pad is trimmed until the part is at least 1 mm above it, so&lt;br&gt;
it never welds to the part's own edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.10 Solid part, not "support"
&lt;/h3&gt;

&lt;p&gt;Cura can print any mesh as support, but its support settings are global: density, walls and pattern cannot be set&lt;br&gt;
per object, so a 1.2 mm fin printed as support would come out as a few sparse lines. And in the Cura releases&lt;br&gt;
checked, the setting called "support brim" prints a brim &lt;em&gt;inside&lt;/em&gt; support areas, not around them. So by default the&lt;br&gt;
fins are a &lt;strong&gt;separate ordinary object&lt;/strong&gt; with three per-object settings: infill 100 %, support off, and "union&lt;br&gt;
overlapping volumes" on, so a fin, its crossbar and its pad print as one piece. The gaps between fin and part are&lt;br&gt;
in the geometry itself. A "print as support mesh" switch is kept for comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.11 In the slicer
&lt;/h3&gt;

&lt;p&gt;In Cura the method is a &lt;strong&gt;toolbar button&lt;/strong&gt; under move, scale and rotate (Figure 7). Its panel has &lt;em&gt;Pick face&lt;/em&gt;,&lt;br&gt;
&lt;em&gt;Auto&lt;/em&gt;, &lt;em&gt;Remove all&lt;/em&gt; and &lt;em&gt;Settings&lt;/em&gt;. Picking a face that already has fins replaces them; &lt;em&gt;Auto&lt;/em&gt; replaces a model's&lt;br&gt;
earlier fins. All of it is undoable. For other slicers a command line tool writes the fins as an STL to load next to&lt;br&gt;
the part.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi7sovqgiwont7y7jhuii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi7sovqgiwont7y7jhuii.png" alt="Picking a face in Cura" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 7. Cura (from source) with the Shark Support panel open: a mouse click on the underside, sent by the test&lt;br&gt;
driver (xdotool, virtual display), made these fins.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj58d468u6im61rhlklar.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj58d468u6im61rhlklar.png" alt="Fins in Cura" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 8. The same box after Auto, from behind: five fins with their feet and pads.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.12 All the numbers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Overhang angle range (auto)&lt;/td&gt;
&lt;td&gt;20–70°&lt;/td&gt;
&lt;td&gt;Which faces &lt;em&gt;Auto&lt;/em&gt; supports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smallest face, shortest slope (auto)&lt;/td&gt;
&lt;td&gt;300 mm², 15 mm&lt;/td&gt;
&lt;td&gt;Faces smaller than this are left alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fin spacing, side margin&lt;/td&gt;
&lt;td&gt;30 mm, 4 mm&lt;/td&gt;
&lt;td&gt;Across the face&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start height&lt;/td&gt;
&lt;td&gt;4 mm&lt;/td&gt;
&lt;td&gt;Fins start where the face is this high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thickness at bed / top&lt;/td&gt;
&lt;td&gt;2.4 / 1.2 mm&lt;/td&gt;
&lt;td&gt;Linear taper with height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relief&lt;/td&gt;
&lt;td&gt;1 mm&lt;/td&gt;
&lt;td&gt;Gap under the part between teeth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tooth pitch, tip, ramp&lt;/td&gt;
&lt;td&gt;3, 0.6, 1 mm&lt;/td&gt;
&lt;td&gt;Tooth shape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tip gap&lt;/td&gt;
&lt;td&gt;0.15 mm&lt;/td&gt;
&lt;td&gt;Ordinary teeth stop this far under the part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anchor teeth, overlap&lt;/td&gt;
&lt;td&gt;2, 0.1 mm&lt;/td&gt;
&lt;td&gt;First teeth from the low end reach into the part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top cut&lt;/td&gt;
&lt;td&gt;15 %&lt;/td&gt;
&lt;td&gt;Fin stops at 85 % of its height&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tip length&lt;/td&gt;
&lt;td&gt;5 mm&lt;/td&gt;
&lt;td&gt;Flat blunt top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Back&lt;/td&gt;
&lt;td&gt;vertical&lt;/td&gt;
&lt;td&gt;Or leaning (20°), round, concave&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crossbar&lt;/td&gt;
&lt;td&gt;¼ of footprint × 4 mm high × 2 mm&lt;/td&gt;
&lt;td&gt;Under the tip; second one over 100 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brim pad&lt;/td&gt;
&lt;td&gt;4 mm wide, one first layer&lt;/td&gt;
&lt;td&gt;Around fin and crossbar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ray step&lt;/td&gt;
&lt;td&gt;0.5 mm&lt;/td&gt;
&lt;td&gt;Profile resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  5. How it got here: the mistakes, in order
&lt;/h2&gt;

&lt;p&gt;Most of the versions below were built, tested in code and looked at; two were design dead ends caught before the&lt;br&gt;
first release. Each taught something.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffa92aaolsfnf80s5sssa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffa92aaolsfnf80s5sssa.png" alt="Four versions of one fin" width="798" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3 is drawn with today's code and each version's settings, so tooth positions follow today's rule.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;0.1.0: the actual shark.&lt;/strong&gt; A round back edge, because it looked like a fin and seemed sturdier. It was
neither: the crossbar already stops the fin tipping over, and the round back was a strip of plastic sticking out
past the part. Measured later: 2.27 m of filament against 1.74 m without it (Figure 4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.1.0: teeth centred along the fin.&lt;/strong&gt; The row of teeth was centred, so the first contact could sit up to 15 %
of the way in, leaving the start of the overhang, where curling begins, unheld. Fixed in 0.1.1 by starting at the
low end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before 0.1.0 (design): "support brim" is not a brim.&lt;/strong&gt; The plan was to use Cura's support brim for the 4 mm pad. Reading Cura's
settings showed it prints inside support, not around it. The pad became geometry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before 0.1.0 (design): fins as Cura support.&lt;/strong&gt; Printed at the global support density, a fin is a few lines. Fins became solid
objects (section 4.10).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.1.1: the triangle.&lt;/strong&gt; Straight back: 23 % less filament (27 % less fin volume). Still a sharp tip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.1.2: more teeth, two anchors.&lt;/strong&gt; Pitch halved from 6 to 3 mm. The first two teeth reach 0.1 mm into the part;
the rest stop 0.15 mm short (was 0.2). Almost free: 1.76 m instead of 1.74 m. With 3 mm pitch the valleys
between teeth are only 0.4 mm long, so the top edge is effectively a saw blade. If it grips too hard, 4 mm pitch
opens the valleys again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.2.0: the blunt top.&lt;/strong&gt; Looking at a Cura preview made the problem obvious: the tip of a triangle is a column
of tiny, slow layers. Cutting at 85 % with a 5 mm flat top fixed it and saved time: 1.68 m, about 40 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.2.0: the leaning back, measured and demoted.&lt;/strong&gt; The first sketch of the new shape had the back leaning out to
the bed. Measured, it cost more than going straight down (1.97 m, about 42 minutes). It is now an option for when
a wider base matters more than plastic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin plumbing, for other plugin authors:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Uranium's signals hold &lt;em&gt;weak&lt;/em&gt; references, so connecting a lambda silently does nothing. Connect a method.&lt;/li&gt;
&lt;li&gt;Cura caches "is this a support mesh" per object and only refreshes it when a setting change is announced;
setting it directly leaves the cache stale.&lt;/li&gt;
&lt;li&gt;A plugin's first log lines can appear before Cura's log file is open; log again once the app has started.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UM.Dialog&lt;/code&gt; takes only visual items as children: a &lt;code&gt;Connections&lt;/code&gt; object in the wrong place makes the whole
dialog fail to load.&lt;/li&gt;
&lt;li&gt;Toolbar icons are SVGs that Cura recolours by inserting a &lt;code&gt;fill&lt;/code&gt; attribute into the &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; tag. An icon that
already has one there becomes invalid.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6. Numbers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgx4but2c7g9dem0lypon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgx4but2c7g9dem0lypon.png" alt="Filament and time per version" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All from PrusaSlicer 2.7.2's command line on the fins alone: 0.2 mm layers, 0.4 mm nozzle, two perimeters,&lt;br&gt;
PrusaSlicer's default speeds. These are &lt;strong&gt;slicer estimates&lt;/strong&gt;, not stopwatch times.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Fins (5)&lt;/th&gt;
&lt;th&gt;Filament&lt;/th&gt;
&lt;th&gt;Time (estimate)&lt;/th&gt;
&lt;th&gt;Volume (code)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.1.0 round back&lt;/td&gt;
&lt;td&gt;sharp, to the top&lt;/td&gt;
&lt;td&gt;2.27 m&lt;/td&gt;
&lt;td&gt;48 min&lt;/td&gt;
&lt;td&gt;4.7 cm³&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1.1 triangle&lt;/td&gt;
&lt;td&gt;sharp, to the top&lt;/td&gt;
&lt;td&gt;1.74 m&lt;/td&gt;
&lt;td&gt;42 min&lt;/td&gt;
&lt;td&gt;3.4 cm³&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1.2 more teeth&lt;/td&gt;
&lt;td&gt;sharp, to the top&lt;/td&gt;
&lt;td&gt;1.76 m&lt;/td&gt;
&lt;td&gt;43 min&lt;/td&gt;
&lt;td&gt;3.5 cm³&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.2.0, leaning back&lt;/td&gt;
&lt;td&gt;blunt, 85 %&lt;/td&gt;
&lt;td&gt;1.97 m&lt;/td&gt;
&lt;td&gt;42 min&lt;/td&gt;
&lt;td&gt;4.0 cm³&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.2.0 default&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;blunt, 85 %&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.68 m&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40 min&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.3 cm³&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Volumes are without brim pads and crossbars; filament and time include them. The 0.1.0 and 0.1.1 volumes are&lt;br&gt;
redrawn with today's code, so their tooth positions follow today's rule.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the slicer print what the code drew?&lt;/strong&gt; Figure 5 plots every extrusion in the real G-code within 0.5 mm of the&lt;br&gt;
middle fin. The fin is solid, follows the part with a stepped gap, stops below the top of the face with a flat top,&lt;br&gt;
and goes straight down at the back. Figure 6 zooms in on the low end: the anchor teeth's lines run into the part's&lt;br&gt;
outer wall, while further up there is a clear gap at every tooth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6547l9nwa49dt8fb740o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6547l9nwa49dt8fb740o.png" alt="G-code slice through the middle fin" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 5. Every extrusion in the PrusaSlicer 2.7.2 G-code within 0.5 mm of the middle fin, seen from the side.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7evpwe5yf0pinrv6h0ug.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7evpwe5yf0pinrv6h0ug.png" alt="G-code zoom on the anchor teeth" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 6. The low end of the same fin, zoomed in.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed of the code.&lt;/strong&gt; A 196,608-triangle version of the test box took about 2 s to process (1.6–2.3 s across&lt;br&gt;
runs) on the development machine; the 12-triangle box takes 0.02 s. Not yet timed on a 7th-generation Core i3, which is the target for&lt;br&gt;
everything in this project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A box on its corner.&lt;/strong&gt; Rolled a further 12° onto a corner, both faces next to the standing edge qualify, and the&lt;br&gt;
same code makes ten fins; with the anchors switched off, the closest tooth is exactly 0.15 mm under the part&lt;br&gt;
(Figure 11).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjglpmix8rf6sarnmfis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjglpmix8rf6sarnmfis.png" alt="Box on a corner" width="674" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 11. The box rolled 12° onto a corner: two faces, ten fins.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Defensive disclosure: the method and its variations
&lt;/h2&gt;

&lt;p&gt;The following are disclosed as part of this publication, alone and in any combination, for FDM/FFF and for any&lt;br&gt;
other layer-wise process where they make sense. They are dedicated to the public.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.1 The core.&lt;/strong&gt; A support structure for a downward-facing surface of an object being built layer by layer,&lt;br&gt;
comprising one or more upright plates ("fins") generated from the object's geometry, each running substantially&lt;br&gt;
along the downhill direction of the surface in plan, whose upper edge follows the surface at a clearance, with&lt;br&gt;
contact features ("teeth") that approach or meet the surface at intervals, the plates standing on the build&lt;br&gt;
surface, optionally braced by a foot and widened at the base by a pad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.2 Finding where fins go.&lt;/strong&gt; Automatic detection of flat or near-flat downward faces by angle band, area and&lt;br&gt;
reach; grouping by normal tolerance; user selection by clicking a point on the surface, by painting, by drawing a&lt;br&gt;
line or region, or by selecting a face in CAD; selection of faces by curl risk (length of free edge, cooling time,&lt;br&gt;
distance from the bed, material); faces of any size, including faces normally considered printable; curved or&lt;br&gt;
faceted surfaces approximated piecewise; surfaces of multiple objects at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.3 Layout.&lt;/strong&gt; Fins along the downhill direction; at an angle to it; fanned from a point; in a grid or herringbone;&lt;br&gt;
crossing pairs (X); spacing fixed, scaled with overhang length, with angle, with face width, with material, or set&lt;br&gt;
so that the unsupported span between fins stays below a limit; denser near the low edge where curling starts;&lt;br&gt;
offset between neighbouring faces; joined across faces into one structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.4 Contact.&lt;/strong&gt; Teeth of any profile (trapezoid, triangle, rounded, stepped); points, ridges, pins, cones, fingers;&lt;br&gt;
teeth whose pitch varies along the fin (denser at the low end); anchor teeth that touch or overlap the part,&lt;br&gt;
anywhere along the fin and in any number, with any overlap; teeth that stop short at a gap of any size, fixed or&lt;br&gt;
proportional to the layer height; teeth printed with a different material, extruder, temperature, speed, flow or&lt;br&gt;
cooling; teeth with a breakaway neck or perforation; teeth that touch only in alternate layers; an interface layer,&lt;br&gt;
dissolvable or not, between teeth and part; contact made from the side rather than from below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.5 Profile.&lt;/strong&gt; A top edge that follows the surface at a relief; fins that stop at any fraction of the surface's&lt;br&gt;
height (the part bridging the rest), measured by height, by length along the slope or by the angle of the&lt;br&gt;
remaining span; a blunt top of any length or a minimum layer length, so that no layer of the support is shorter&lt;br&gt;
than a set length; a flat, rounded or stepped top; backs that are vertical, leaning at any angle, round, concave,&lt;br&gt;
stepped, or shaped to the minimum stable footprint; fins that start at a set height or reach down to the bed edge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.6 Section and structure.&lt;/strong&gt; Thickness constant, tapered, stepped or thickened at the base; solid, hollow,&lt;br&gt;
double-walled, corrugated, perforated, lattice-filled or sparse-infilled fins; fins with stiffening ribs or a&lt;br&gt;
flange along the top; fins printed as part of the model, as a separate object, as a slicer "support mesh", or&lt;br&gt;
directly as toolpaths at G-code level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.7 Feet and pads.&lt;/strong&gt; A crossbar of any length, height and position (under the tip, at the middle, at the low end,&lt;br&gt;
several); T, X, H or gusset-shaped feet; triangular gussets between fin and bed; a pad of any width and thickness,&lt;br&gt;
one or more layers, trimmed clear of the part, shared between neighbouring fins, or replaced by a raft or by mouse&lt;br&gt;
ears; tie bars linking fins to each other above the bed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.8 Workflow.&lt;/strong&gt; Generating fins in a slicer, a CAD program, a mesh editor, a plugin, a command line tool or a&lt;br&gt;
web service; replacing earlier fins when the same face is picked again; regenerating fins when the object is moved;&lt;br&gt;
removing all fins at once; previewing before adding; combining fins with tree or normal supports elsewhere on the&lt;br&gt;
same part; automatically turning off the slicer's own supports under the faces that have fins.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Test plan (please print this)
&lt;/h2&gt;

&lt;p&gt;Print the 35° test box (&lt;code&gt;data/tilted_box_35deg.stl&lt;/code&gt;, 150 × 40 × 25 mm) four ways, same filament, same settings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No support.&lt;/strong&gt; Photograph the base, measure curl at the low edge and mid-face with a feeler gauge or a ruler on
a flat surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slicer supports&lt;/strong&gt; (normal, then tree), touching build plate only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shark fins, defaults.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shark fins, anchors off&lt;/strong&gt; (&lt;code&gt;Anchor teeth&lt;/code&gt; 0).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each, record: filament and time (slicer estimate and real), whether it finished, curl or sag of the base,&lt;br&gt;
time to remove the supports by hand, and the state of the base afterwards (photo). Then, if you are keen: tooth pitch&lt;br&gt;
3 vs 4 vs 6 mm; top cut 0, 15 and 25 %; a 45° box. Post your results as a GitHub issue or discussion with photos.&lt;br&gt;
Everyone who does gets credited in version 1.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Fin-ally, a social species
&lt;/h2&gt;

&lt;p&gt;Infant sharks do, do, do support prints well.&lt;/p&gt;

&lt;p&gt;(With apologies to a certain children's song, which is now going to be in your head until at least Thursday.)&lt;/p&gt;

&lt;p&gt;The serious point behind the joke: this method grew one review at a time. A fin that looked right in a diagram&lt;br&gt;
looked wrong in a slicer preview; a "more efficient" back edge turned out to cost more when measured. Each change&lt;br&gt;
came from someone looking at the output and saying "that bit's daft". The fastest way to find the next daft bit is&lt;br&gt;
more people looking. Print it, break it, measure it, and tell the rest of us. Makers, like fins, are stronger when&lt;br&gt;
they are spaced sensibly and braced against each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Code, data and how to reproduce
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The plugin&lt;/strong&gt; (Cura toolbar tool, menu, installer, command line tool, 22 geometry tests):
&lt;a href="https://github.com/The-Dorkknight/cura-shark-support" rel="noopener noreferrer"&gt;https://github.com/The-Dorkknight/cura-shark-support&lt;/a&gt;, version 0.2.1-wip (fin shapes as in 0.2.0).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In this repository:&lt;/strong&gt; &lt;code&gt;code/shark_core.py&lt;/code&gt; (the geometry, numpy only), &lt;code&gt;data/&lt;/code&gt; (the test boxes and their fins as
STL), &lt;code&gt;tools/make_figures.py&lt;/code&gt; (redraws Figures 1–4 and 9–11 from the code), &lt;code&gt;tools/gcode_side_view.py&lt;/code&gt; (draws
Figures 5–6 from any G-code), &lt;code&gt;tools/build_paper.py&lt;/code&gt; (builds the web page and PDF).&lt;/li&gt;
&lt;li&gt;Figures 5 and 6: PrusaSlicer 2.7.2, &lt;code&gt;--layer-height 0.2 --first-layer-height 0.2 --nozzle-diameter 0.4
--perimeters 2 --fill-density 15%&lt;/code&gt;, the box and its fins merged. Figures 7 and 8: Cura from source, run by the
plugin's own test driver under a virtual display.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. How this was made
&lt;/h2&gt;

&lt;p&gt;The geometry code, plugin, tests, measurements and literature search were done with an AI assistant (Claude,&lt;br&gt;
by Anthropic) working to my design intent, reviewed and steered by me at each step: the shark fin, the point&lt;br&gt;
contacts, the 4 mm brim, the crossbar, the triangle, the anchors, the blunt top and the toolbar button were all my&lt;br&gt;
calls; the assistant did the typing, the maths and the measuring, and owned up to its own mistakes listed in&lt;br&gt;
section 5. I think saying so plainly makes the work more trustworthy, not less.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Licence, and keeping it free
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text and figures:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/blob/main/paper/LICENSE.md" rel="noopener noreferrer"&gt;CC BY-SA 4.0&lt;/a&gt;. Share and adapt, credit me, keep it under the same licence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; PolyForm Noncommercial 1.0.0 (&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/blob/main/LICENSE-CODE.md" rel="noopener noreferrer"&gt;LICENSE-CODE.md&lt;/a&gt;). Free for hobbyists; commercial use
needs permission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The methods&lt;/strong&gt; in sections 3, 4 and 7 are dedicated to the public. This paper is a defensive publication: its
purpose is to be prior art, to make it much harder for anyone to patent these methods and restrict their use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  14. Disclaimers
&lt;/h2&gt;

&lt;p&gt;Not legal advice: what counts as prior art, and whether something infringes a patent, is decided by patent offices&lt;br&gt;
and courts. Not engineering certification: printed supports can fail, and parts can detach from the bed. Use at&lt;br&gt;
your own risk. Not affiliated with UltiMaker, Prusa Research, 3D Systems, Materialise or anyone else named here.&lt;/p&gt;

&lt;h2&gt;
  
  
  15. How to cite
&lt;/h2&gt;

&lt;p&gt;Armstrong, M. (2026). &lt;em&gt;Fin-ally Supported: Shark-fin supports for long, flat, tilted overhangs. A hobbyist's field&lt;br&gt;
guide and a defensive publication.&lt;/em&gt; Version 1.0, first published 19 October 2026.&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported" rel="noopener noreferrer"&gt;https://github.com/The-Dorkknight/fin-ally-supported&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prusa Research, "Support material", Prusa Knowledge Base.
&lt;a href="https://help.prusa3d.com/en/article/support-material_1698/" rel="noopener noreferrer"&gt;https://help.prusa3d.com/en/article/support-material_1698/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Printago, "How to Add Supports in PrusaSlicer (Including Organic and Custom)".
&lt;a href="https://printago.io/guides/prusaslicer-supports" rel="noopener noreferrer"&gt;https://printago.io/guides/prusaslicer-supports&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hubs, "Supports in 3D printing: a technology overview".
&lt;a href="https://www.hubs.com/knowledge-base/supports-3d-printing-technology-overview" rel="noopener noreferrer"&gt;https://www.hubs.com/knowledge-base/supports-3d-printing-technology-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;T. Tricard, F. Claux, S. Lefebvre, "Ribbed Support Vaults for 3D Printing of Hollowed Objects",
&lt;em&gt;Computer Graphics Forum&lt;/em&gt;, 2019. &lt;a href="https://www.unilim.fr/pages_perso/frederic.claux/blog/2019/2019-06-21_Ribbed_support_vaults.html" rel="noopener noreferrer"&gt;https://www.unilim.fr/pages_perso/frederic.claux/blog/2019/2019-06-21_Ribbed_support_vaults.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Materialise, "How to create optimal support structures for metal 3D printing".
&lt;a href="https://www.materialise.com/en/inspiration/articles/optimal-metal-supports" rel="noopener noreferrer"&gt;https://www.materialise.com/en/inspiration/articles/optimal-metal-supports&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;3D Systems, Inc., US 9,469,057 B2, "Support structures and deposition techniques for 3D printing",
filed 17 May 2013, issued 18 October 2016. &lt;a href="https://patents.justia.com/patent/9469057" rel="noopener noreferrer"&gt;https://patents.justia.com/patent/9469057&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Patents entry for US9469057B2 (status as listed on 19 October 2026).
&lt;a href="https://patents.google.com/patent/US9469057B2/en" rel="noopener noreferrer"&gt;https://patents.google.com/patent/US9469057B2/en&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Software: The-Dorkknight/cura-shark-support, version 0.2.1-wip. &lt;a href="https://github.com/The-Dorkknight/cura-shark-support" rel="noopener noreferrer"&gt;https://github.com/The-Dorkknight/cura-shark-support&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About this repository
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/blob/main/paper/paper.md" rel="noopener noreferrer"&gt;&lt;code&gt;paper/paper.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The paper (this page is generated from it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/blob/main/paper/fin-ally-supported-v1.0.pdf" rel="noopener noreferrer"&gt;&lt;code&gt;paper/fin-ally-supported-v1.0.pdf&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The paper as a PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/tree/main/paper/figures" rel="noopener noreferrer"&gt;&lt;code&gt;paper/figures/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;All figures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/blob/main/code/shark_core.py" rel="noopener noreferrer"&gt;&lt;code&gt;code/shark_core.py&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The fin geometry (Python, numpy only), as used by the plugin 0.2.0 and 0.2.1-wip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/tree/main/data" rel="noopener noreferrer"&gt;&lt;code&gt;data/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The test boxes and their fins as STL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/tree/main/tools" rel="noopener noreferrer"&gt;&lt;code&gt;tools/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Redraw the figures (&lt;code&gt;make_figures.py&lt;/code&gt;), plot G-code (&lt;code&gt;gcode_side_view.py&lt;/code&gt;), build the PDF and web page (&lt;code&gt;build_paper.py&lt;/code&gt;), write this page (&lt;code&gt;make_readme.py&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/tree/main/docs" rel="noopener noreferrer"&gt;&lt;code&gt;docs/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The web version for GitHub Pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To rebuild everything: &lt;code&gt;python3 tools/make_figures.py&lt;/code&gt; (numpy, matplotlib, Pillow), then&lt;br&gt;
&lt;code&gt;python3 tools/build_paper.py&lt;/code&gt; (pandoc, Chromium), then &lt;code&gt;python3 tools/make_readme.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;GitHub's &lt;strong&gt;Cite this repository&lt;/strong&gt; button uses &lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/blob/main/CITATION.cff" rel="noopener noreferrer"&gt;&lt;code&gt;CITATION.cff&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Join in:&lt;/strong&gt; print the test plan (section 8), post results with photos as an issue or discussion, fork it, port it to&lt;br&gt;
another slicer, or point out prior art I missed. Everyone who contributes gets credited in the next version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Licence:&lt;/strong&gt; paper text and figures &lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/blob/main/LICENSE.md" rel="noopener noreferrer"&gt;CC BY-SA 4.0&lt;/a&gt;; code in &lt;code&gt;code/&lt;/code&gt; and &lt;code&gt;tools/&lt;/code&gt;&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported/blob/main/LICENSE-CODE.md" rel="noopener noreferrer"&gt;PolyForm Noncommercial 1.0.0&lt;/a&gt; (source-available, not open source); the methods described are&lt;br&gt;
dedicated to the public as a defensive publication. Not legal advice.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Shark Support for UltiMaker Cura</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:42:30 +0000</pubDate>
      <link>https://dev.to/open-madscience/shark-support-for-ultimaker-cura-5ep0</link>
      <guid>https://dev.to/open-madscience/shark-support-for-ultimaker-cura-5ep0</guid>
      <description>&lt;h1&gt;
  
  
  &lt;strong&gt;WORK IN PROGRESS: MANY FEATURES MAY BE BROKEN&lt;/strong&gt; (proof of concept, 0.2.1-wip)
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fcura-shark-support%2Fmain%2Fdocs%2Fimages%2Ficon-512.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fcura-shark-support%2Fmain%2Fdocs%2Fimages%2Ficon-512.png" alt="Shark Support icon" width="512" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source-available · Non-commercial.&lt;/strong&gt; Copyright Matthew Armstrong. Licensed under the&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/cura-shark-support/blob/main/LICENSE.md" rel="noopener noreferrer"&gt;PolyForm Noncommercial License 1.0.0&lt;/a&gt;. No warranty, no liability: use at your own risk.&lt;/p&gt;

&lt;p&gt;Shark Support adds thin &lt;strong&gt;shark-fin supports&lt;/strong&gt; under big, flat, tilted faces. The typical case: you print a long&lt;br&gt;
box tilted onto one long edge so it is strong along its length. Its base becomes a long 30–55° overhang that is&lt;br&gt;
fine in theory but sags or curls over that length. Shark Support puts a row of fins under it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each fin is a thin vertical plate that follows the slope of the part, &lt;strong&gt;stops at 85 % of its height&lt;/strong&gt; (the part
bridges the last bit), has a &lt;strong&gt;blunt 5 mm flat top&lt;/strong&gt; (no tiny, slow top layers) and a &lt;strong&gt;straight back&lt;/strong&gt; down to
the bed&lt;/li&gt;
&lt;li&gt;it &lt;strong&gt;only touches the part at small teeth&lt;/strong&gt;, a dotted line of points every 3 mm that snaps off cleanly, starting
right at the fin's low end. The &lt;strong&gt;first two teeth really touch&lt;/strong&gt; (0.1 mm into the part) to hold the start of the
overhang; the rest stop 0.15 mm short&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;crossbar foot&lt;/strong&gt; under the tip (¼ of the fin's length, 4 mm high) and a &lt;strong&gt;4 mm one-layer brim pad&lt;/strong&gt; keep tall
fins standing&lt;/li&gt;
&lt;li&gt;in Cura it is a &lt;strong&gt;button in the left toolbar&lt;/strong&gt;: &lt;strong&gt;pick the face&lt;/strong&gt; you want supported with the mouse, or press
&lt;strong&gt;Auto&lt;/strong&gt; for every big tilted flat face, and &lt;strong&gt;Remove all&lt;/strong&gt; to clear them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyz29kuixhehkkl4lwz33.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyz29kuixhehkkl4lwz33.webp" alt="Shark fins under a tilted box in Cura" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Real screenshot: the plugin running inside Cura (5.15 development source, see Testing), with&lt;br&gt;
fins under the 35° test box and the Shark Support panel open from its toolbar button.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjuoijy4wbox76ns5iqu7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjuoijy4wbox76ns5iqu7.webp" alt="One fin, side view" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Use it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Load your model and &lt;strong&gt;tilt it the way you will print it&lt;/strong&gt; (Rotate tool, &lt;em&gt;Lay flat&lt;/em&gt;, etc.).&lt;/li&gt;
&lt;li&gt;Select it, then click the &lt;strong&gt;shark-fin button&lt;/strong&gt; at the bottom of the left toolbar (under move, scale, rotate,
mirror, per-model settings and support blocker). Its panel opens:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fcura-shark-support%2Fmain%2Fdocs%2Fimages%2Ftoolbar-panel.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2FThe-Dorkknight%2Fcura-shark-support%2Fmain%2Fdocs%2Fimages%2Ftoolbar-panel.webp" alt="Shark Support toolbar button and panel" width="400" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick face&lt;/strong&gt;, then &lt;strong&gt;click the underside face&lt;/strong&gt; that needs support. Fins appear under that one face. Pick
 more faces the same way; picking a face again replaces its fins (handy after changing settings). Press the
 button again to stop picking. Turn the view so you can see the underside (a low side view works).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto&lt;/strong&gt;: fins under every big tilted flat face of the selected model(s), replacing their earlier fins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove all&lt;/strong&gt;: removes every Shark fin object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings...&lt;/strong&gt;: the numbers below.

&lt;ol&gt;
&lt;li&gt;A message says how many fins were made. New objects called &lt;em&gt;Shark fins (your model...)&lt;/em&gt; appear.&lt;/li&gt;
&lt;li&gt;Slice and print as usual. Normal support for that model is switched &lt;strong&gt;off&lt;/strong&gt; (a per-model setting) so Cura does
not add its own supports under the same face. You can change that in the settings.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same actions are in the menu &lt;strong&gt;Extensions &amp;gt; Shark Support&lt;/strong&gt;. If you move or rotate the model afterwards, use&lt;br&gt;
&lt;strong&gt;Remove all&lt;/strong&gt; and add the fins again: they are a separate object and do not follow the model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9uouisy3zt4vttr25aww.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9uouisy3zt4vttr25aww.webp" alt="Picking a face with the mouse" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Settings (Settings... in the panel, or Extensions &amp;gt; Shark Support &amp;gt; Settings...)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7zrpygciynt7o632cugb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7zrpygciynt7o632cugb.webp" alt="Settings dialog" width="780" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Overhang angle from / to&lt;/td&gt;
&lt;td&gt;20° / 70°&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Auto&lt;/em&gt; puts fins under faces in this range (Cura's angles: 0° = wall, 90° = ceiling). A face you pick gets fins whatever its angle, as long as it faces down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smallest face / Shortest slope&lt;/td&gt;
&lt;td&gt;300 mm² / 15 mm&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;Auto&lt;/em&gt; leaves smaller faces alone (picked faces ignore this)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fin spacing / Side margin&lt;/td&gt;
&lt;td&gt;30 mm / 4 mm&lt;/td&gt;
&lt;td&gt;Distance between fins across the face, and from its sides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Start height&lt;/td&gt;
&lt;td&gt;4 mm&lt;/td&gt;
&lt;td&gt;Fins start where the face is this high (low down it needs no help)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thickness at bed / at top&lt;/td&gt;
&lt;td&gt;2.4 / 1.2 mm&lt;/td&gt;
&lt;td&gt;Fins taper; with a 0.4 mm nozzle that is 6 lines at the bed, 3 at the top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gap between teeth&lt;/td&gt;
&lt;td&gt;1 mm&lt;/td&gt;
&lt;td&gt;The fin edge stays this far under the part except at the teeth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tooth spacing / tip length&lt;/td&gt;
&lt;td&gt;3 / 0.6 mm&lt;/td&gt;
&lt;td&gt;The contact points. The first tooth starts at the fin's low end, the last sits at the tall end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tip gap&lt;/td&gt;
&lt;td&gt;0.15 mm&lt;/td&gt;
&lt;td&gt;All teeth except the anchors stop this far under the part. Smaller = stronger contact, harder removal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anchor teeth / overlap&lt;/td&gt;
&lt;td&gt;2 / 0.1 mm&lt;/td&gt;
&lt;td&gt;The first teeth from the low end reach this far &lt;em&gt;into&lt;/em&gt; the part so they bond slightly and hold the start of the overhang. 0 turns it off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top left free&lt;/td&gt;
&lt;td&gt;15 %&lt;/td&gt;
&lt;td&gt;The fin stops this far below the top of the face; the part bridges the rest. 0 runs it to the top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blunt tip length&lt;/td&gt;
&lt;td&gt;5 mm&lt;/td&gt;
&lt;td&gt;The fin's flat top. No layer of the fin is shorter than this, so the printer never slows down for tiny top moves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Back lean&lt;/td&gt;
&lt;td&gt;20°&lt;/td&gt;
&lt;td&gt;Only with &lt;em&gt;Lean the back out&lt;/em&gt; on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crossbar length / height&lt;/td&gt;
&lt;td&gt;0.25 × fin / 4 mm&lt;/td&gt;
&lt;td&gt;The foot across the fin under its tip (a second one in the middle for fins over 100 mm)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brim width / thickness&lt;/td&gt;
&lt;td&gt;4 mm / first layer&lt;/td&gt;
&lt;td&gt;One-layer pad around each fin and crossbar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lean the back out to the bed&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;The back slopes out 20° instead of going straight down: wider base, but more plastic and time (test box: 1.97 m and 42 min instead of 1.68 m and 40 min). The command line also offers round and concave backs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Print fins as a Cura support mesh&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;Experimental: Cura then prints them with your support pattern and density&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Extensions &amp;gt; Shark Support &amp;gt; Run self-check&lt;/strong&gt; builds a test box in memory, runs the plugin's own code against it&lt;br&gt;
(including face picking and the toolbar button),&lt;br&gt;
and shows PASS/FAIL per check (also saved to &lt;code&gt;shark_support_selfcheck.txt&lt;/code&gt; in your temp folder). Run it once after&lt;br&gt;
installing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Without Cura (PrusaSlicer, OrcaSlicer, anything)
&lt;/h2&gt;

&lt;p&gt;The geometry code needs only numpy, so there is a command line tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 shark_cli.py part.stl                   # writes part_fins.stl
python3 shark_cli.py part.stl --spacing 25 --merged part_with_fins.stl
python3 shark_cli.py part.stl --back slope       # back leaning out instead of straight down
python3 shark_cli.py --help                     # every setting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The STL must already be oriented for printing (Z up, sitting on the bed). Import the fins STL as a second part at&lt;br&gt;
the same position (PrusaSlicer: &lt;em&gt;Add part&lt;/em&gt;; or slice the &lt;code&gt;--merged&lt;/code&gt; file). The repository has examples in&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/cura-shark-support/tree/main/examples" rel="noopener noreferrer"&gt;&lt;code&gt;examples/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9z4dooykdjlfmpfzc3xc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9z4dooykdjlfmpfzc3xc.webp" alt="Real G-code from PrusaSlicer, sliced through one fin" width="668" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Real G-code: the merged test box sliced by PrusaSlicer 2.7.2 on the command line, showing every extrusion within&lt;br&gt;
0.5 mm of the middle fin. The fin is solid, touches the part only at the teeth, stops below the top of the face&lt;br&gt;
with a flat top, and goes straight down at the back.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What exists and what does not
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exists (0.2.1-wip)&lt;/th&gt;
&lt;th&gt;Does not exist yet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fins under flat tilted faces: picked with the mouse, or found automatically&lt;/td&gt;
&lt;td&gt;Fins under curved or organic surfaces (it needs flat faces)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fins cut at 85 %, blunt flat top, straight back, teeth every 3 mm, 2 anchor teeth, crossbar, brim pad&lt;/td&gt;
&lt;td&gt;Painting exactly where on a face fins go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cura toolbar button with panel, menu, settings dialog, self-check, undo&lt;/td&gt;
&lt;td&gt;Fins that follow the model when it is moved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Installer that checks Cura's log; &lt;code&gt;.curapackage&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Live preview while editing settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Command line tool for any slicer&lt;/td&gt;
&lt;td&gt;PrusaSlicer / OrcaSlicer plugin (they have no Python plugin API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experimental "Cura support mesh" mode&lt;/td&gt;
&lt;td&gt;A real print test (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What was tested
&lt;/h2&gt;

&lt;p&gt;Be aware of exactly what has and has not been tried:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Geometry:&lt;/strong&gt; 22 automatic tests (&lt;code&gt;python3 tests/test_core.py&lt;/code&gt;): closed outward meshes, fins never reach into the
part (only the anchor teeth reach in, exactly 0.1 mm; with anchors off every tip is 0.15 mm below it), boxes tilted 35° and 45°, turned on the bed, standing on a corner,
the 85 % cut and 5 mm flat top, picking a face (and refusing a top face or a miss), triangle-soup input, spacing, long fins, no fins on a plain cube. A 196,000-triangle mesh took about 2 s on the
development machine (not timed on a 7th-gen Core i3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inside Cura:&lt;/strong&gt; Cura could not be downloaded in the build sandbox, so it was run &lt;strong&gt;from Cura's and Uranium's
source (5.15 development branch, plugin SDK 8.13)&lt;/strong&gt; under a virtual display, with stand-ins only for Cura's C++
parts. There the plugin loaded, its &lt;strong&gt;toolbar button&lt;/strong&gt; appeared and opened its panel, &lt;strong&gt;a real mouse click
(sent with xdotool) on the box's underside made fins for that face&lt;/strong&gt;, picking it again replaced them, &lt;em&gt;Auto&lt;/em&gt;
replaced those with automatic fins with the right per-object settings, the menu and dialog opened, the self-check passed, support-mesh mode and &lt;em&gt;Remove all fins&lt;/em&gt; worked, and Cura's package
manager listed it as installed, both after the installer and after Cura's own &lt;code&gt;.curapackage&lt;/code&gt; route. &lt;strong&gt;Slicing in
Cura was not tested&lt;/strong&gt; (the slicing engine is one of the C++ parts that could not run).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slicing:&lt;/strong&gt; the fins were sliced for real with &lt;strong&gt;PrusaSlicer 2.7.2&lt;/strong&gt; (command line), picture above. For the
test box the fins take 1.68 m of filament and about 40 minutes on their own (PrusaSlicer defaults, 0.2 mm
layers); the 0.1.2 fins, which ran to the top with a sharp tip, took 1.74 m and about 43 minutes. The zoom below shows the anchor teeth meeting the part's outer wall in the G-code while the other
teeth leave a gap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbpod23p0gy3d529prpz2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbpod23p0gy3d529prpz2.webp" alt="G-code zoom on the anchor teeth" width="787" height="638"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not tested:&lt;/strong&gt; a real print, Cura 5.0–5.10 releases (face picking uses a different path there, written to
match Cura 5.0's own support blocker but not run), Windows and Mac (launchers and installer), Flatpak Cura.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Research paper
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/The-Dorkknight/cura-shark-support/blob/main/paper/paper.md" rel="noopener noreferrer"&gt;Fin-ally Supported: Shark-fin supports for long, flat, tilted overhangs&lt;/a&gt;&lt;/strong&gt; (version 1.0, first&lt;br&gt;
published 19 October 2026) · &lt;a href="https://github.com/The-Dorkknight/cura-shark-support/blob/main/paper/fin-ally-supported-v1.0.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything behind this plugin written up: the problem, what already exists (including a live patent on "fine point"&lt;br&gt;
supports and how this differs), the method in enough detail to rebuild it, every mistake made on the way, the&lt;br&gt;
numbers, a test plan, and a defensive disclosure of the method and its variations, dedicated to the public so&lt;br&gt;
nobody can patent them. It is published on its own at&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported" rel="noopener noreferrer"&gt;The-Dorkknight/fin-ally-supported&lt;/a&gt;; &lt;code&gt;paper/&lt;/code&gt; is a copy that&lt;br&gt;
travels with the plugin. Text and figures: CC BY-SA 4.0. Infant sharks do, do, do support prints well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to cite:&lt;/strong&gt; Armstrong, M. (2026). &lt;em&gt;Fin-ally Supported: Shark-fin supports for long, flat, tilted overhangs. A&lt;br&gt;
hobbyist's field guide and a defensive publication.&lt;/em&gt; Version 1.0, 19 October 2026.&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/fin-ally-supported" rel="noopener noreferrer"&gt;https://github.com/The-Dorkknight/fin-ally-supported&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  More
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/cura-shark-support/blob/main/HOW_IT_WORKS.md" rel="noopener noreferrer"&gt;HOW_IT_WORKS.md&lt;/a&gt;: what the plugin reads, changes and never touches&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/cura-shark-support/blob/main/DESIGN_NOTES.md" rel="noopener noreferrer"&gt;DESIGN_NOTES.md&lt;/a&gt;: research notes, decisions, measurements, ideas&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/cura-shark-support/tree/main/paper" rel="noopener noreferrer"&gt;paper/&lt;/a&gt;: the research paper (Markdown and PDF)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/cura-shark-support/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/cura-shark-support/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/cura-shark-support/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/cura-shark-support/blob/main/THIRD_PARTY.md" rel="noopener noreferrer"&gt;THIRD_PARTY.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not affiliated with or endorsed by UltiMaker. "Cura" and "UltiMaker" are names of their owners.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/cura-shark-support" rel="noopener noreferrer"&gt;The-Dorkknight/cura-shark-support on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>software</category>
      <category>softwaredevelopment</category>
      <category>tools</category>
    </item>
    <item>
      <title>FREE (open) Lizard Scanner</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Mon, 19 Oct 2026 20:45:24 +0000</pubDate>
      <link>https://dev.to/open-madscience/free-open-lizard-scanner-503c</link>
      <guid>https://dev.to/open-madscience/free-open-lizard-scanner-503c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgewdnnbxsqyadosnstxb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgewdnnbxsqyadosnstxb.jpg" alt="A lizard revolutionary waving a " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Paused for a month or two.&lt;/strong&gt; Calibration turned out to be more boring than cleaning, and after weeks of&lt;br&gt;
mad science projects the cleaning has been neglected for long enough. The app works as far as described&lt;br&gt;
below; the notes say exactly where it stopped. Issues and ideas are still welcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The paper:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/docs/paper/down-the-lizard-hole.pdf" rel="noopener noreferrer"&gt;Down the Lizard Hole&lt;/a&gt;, the whole reverse-engineering&lt;br&gt;
story in plain English (also in its own repository,&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/down-the-lizard-hole" rel="noopener noreferrer"&gt;down-the-lizard-hole&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WORK IN PROGRESS: MANY FEATURES MAY BE BROKEN.&lt;/strong&gt; Version 0.5.2-wip. Proof of concept.&lt;br&gt;
The camera control, the light buttons and the diagnostics have run on the owner's real CR-Scan&lt;br&gt;
Lizard. The first real wall test (0.4) failed because the projector throws &lt;strong&gt;stripes, not dots&lt;/strong&gt;;&lt;br&gt;
0.5 rebuilds depth for the stripes and was checked on that wall test's saved data and one saved&lt;br&gt;
picture (walls back within 2 mm). The first live test (19 October) gave a flat wall where the&lt;br&gt;
calibration was good, with wrong-stripe streaks that 0.5.1 removes; a second calibration did worse for&lt;br&gt;
reasons not yet known. See &lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/docs/research/real-scanner/2026-10-18-stripes.md" rel="noopener noreferrer"&gt;the findings&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Licence:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/LICENSE.md" rel="noopener noreferrer"&gt;PolyForm Noncommercial 1.0.0&lt;/a&gt;. Source-available, non-commercial.&lt;br&gt;
Free for personal and other non-commercial use; commercial use needs permission. No warranty&lt;br&gt;
and no liability: it is provided as is. Not made by, or affiliated with, Creality.&lt;/p&gt;

&lt;p&gt;A standalone app for the &lt;strong&gt;Creality CR-Scan Lizard&lt;/strong&gt; 3D scanner that does not need Creality's&lt;br&gt;
software: a live infrared picture, &lt;strong&gt;automagic buttons instead of sliders&lt;/strong&gt; for the light, a&lt;br&gt;
&lt;strong&gt;wall test&lt;/strong&gt; that gives the scanner a rough depth calibration, a live depth view, point-cloud&lt;br&gt;
export and recordings. It is built on the bones of&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/scantastic" rel="noopener noreferrer"&gt;Scantastic&lt;/a&gt; and the Lizard engine from&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/who-are-yousb" rel="noopener noreferrer"&gt;Who are yoUSB?&lt;/a&gt;, and is laid out so the Lizard&lt;br&gt;
can move into Scantastic later (&lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/INTEGRATION.md" rel="noopener noreferrer"&gt;INTEGRATION.md&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpi4q20guoa3z9m7kvju0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpi4q20guoa3z9m7kvju0.webp" alt="Lizard Scanner showing the depth view of a ball in front of a wall" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Screenshots are of the built-in simulated scanner, not a real Lizard.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What exists, and what does not
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Works (on the simulated scanner)&lt;/th&gt;
&lt;th&gt;Not yet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Finding the Lizard's two cameras (USB bcdc:000a / 000b) and starting them (Linux)&lt;/td&gt;
&lt;td&gt;Tried on a real Lizard with this app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live picture, "pattern only" view, camera A + B side by side&lt;/td&gt;
&lt;td&gt;Full 3D scans (many views joined into a mesh): that is Scantastic's job, see &lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/INTEGRATION.md" rel="noopener noreferrer"&gt;INTEGRATION.md&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AUTO-TUNE, three presets, Brighter / Dimmer, flash capped at 1 ms&lt;/td&gt;
&lt;td&gt;Lens distortion correction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagnostics: 10 checks and a report zip&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall test: rough depth calibration from 3 to 5 walls, saved as you go, all in one folder, one-click zip&lt;/td&gt;
&lt;td&gt;Creality's own factory calibration (not found)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live depth view, save points as .ply&lt;/td&gt;
&lt;td&gt;Windows and macOS camera paths (written, never run)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recording sessions and playing them back&lt;/td&gt;
&lt;td&gt;Colour (the Lizard's pictures are infrared)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light test, "What is camera B?" test&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The picture
&lt;/h3&gt;

&lt;p&gt;Plug the Lizard in. On Linux the picture starts by itself within a few seconds (the app&lt;br&gt;
looks for it every 2.5 s). The green tag on the picture says how many pictures a second&lt;br&gt;
arrive and how much of the view shows the projector's stripes. The projector (camera A's light)&lt;br&gt;
flashes once per picture, so every picture carries its pattern; the red glow looks like flicker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cdkve0ovggv8rkirg4f.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cdkve0ovggv8rkirg4f.webp" alt="The live infrared picture with the light buttons" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;View&lt;/strong&gt; row switches between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Picture&lt;/strong&gt;: the infrared picture with the projector's stripes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern only&lt;/strong&gt;: the stripes with the background light taken away. What the depth is made from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Depth&lt;/strong&gt;: needs a wall test first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A + B&lt;/strong&gt;: both cameras side by side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The light: automagic buttons
&lt;/h3&gt;

&lt;p&gt;Creality's own app only ever changes two settings, exposure and gain. Here they are a&lt;br&gt;
ladder of 11 light levels instead of two sliders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AUTO-TUNE&lt;/strong&gt; walks the ladder until the stripes are bright but not blown out, from whatever
is in front of the scanner. Press it again to stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shiny / white&lt;/strong&gt;, &lt;strong&gt;Normal&lt;/strong&gt;, &lt;strong&gt;Dark / black&lt;/strong&gt;: presets. Normal is what Creality's app
uses (1 ms, gain 65); Shiny / white is its dimmest setting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dimmer / Brighter&lt;/strong&gt;: one step of the ladder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The ring's brightness is the flash length.&lt;/strong&gt; The white ring is camera B's light, the infrared&lt;br&gt;
projector camera A's. Both cameras start at Creality's 1 ms, never on their own automatic exposure,&lt;br&gt;
and camera B follows these buttons whenever it runs. The ring and the projector flash for as&lt;br&gt;
long as the camera exposes, and at Creality's 1 ms the ring is already very bright. So the&lt;br&gt;
ladder never flashes longer than 1 ms; above that it adds gain (picture amplification), which&lt;br&gt;
brightens the picture without touching the lights. The bar under the buttons shows the rungs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Allow longer flashes&lt;/strong&gt;: an advanced switch, with a warning, for dark objects. Brighter then
goes up to 6 ms (the camera's own starting value, never more); those rungs show red. It
switches itself off when the app closes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera's own settings (long flashes)&lt;/strong&gt;: back to the camera's own automatic exposure. Only
with longer flashes allowed, because the camera then picks its own (often long) flash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every raw setting is still there under &lt;em&gt;Every camera setting (advanced)&lt;/em&gt;; setting a flash longer&lt;br&gt;
than 1 ms there asks first.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The wall test: rough depth calibration
&lt;/h3&gt;

&lt;p&gt;The Lizard's camera A sees a fixed pattern from its projector: dense vertical stripes, each a string&lt;br&gt;
of beads whose width wobbles. When the scanner moves closer to a wall, the stripes slide sideways in&lt;br&gt;
the picture, by an amount that depends on the distance (and, on the real scanner, a little on where&lt;br&gt;
in the picture). The beads' wobble tells one stripe from the next. Measure that slide at a few known&lt;br&gt;
distances and every picture afterwards can be turned into depth. The original Kinect worked the same&lt;br&gt;
way with dots.&lt;/p&gt;

&lt;p&gt;You need a flat, plain, matt wall and a tape measure. About five minutes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press &lt;strong&gt;WALL TEST&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Point the scanner straight at the wall. Measure to the wall from one fixed point on the scanner
(the front glass is easiest; any point works if you use the same one every time; depth is then
counted from that point) and type the distance in millimetres.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;RECORD THIS WALL&lt;/strong&gt; and hold still for two seconds. (Before the first wall the app
runs AUTO-TUNE by itself.)&lt;/li&gt;
&lt;li&gt;Repeat at five distances: 18, 23, 28, 33 and 38 cm are suggested (Creality gives the
Lizard's working range as 15 to 40 cm). Three is the least; spreading the walls over the
whole range matters, because depth gets less certain beyond the nearest and farthest wall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing to press to save.&lt;/strong&gt; Every wall is saved the moment it is recorded, and from the third
wall on the calibration is fitted and saved by itself; the Depth view turns on. The list shows
how well each wall fits, and the window warns about a wall at an angle (one that disagrees with
the others is left out) or a scanner turned to the wall. &lt;em&gt;FIT AND SAVE AGAIN&lt;/em&gt; is only needed after
typing a picture width.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Everything for one scanner's calibration is in one folder&lt;/strong&gt;, &lt;code&gt;calibration/&amp;lt;serial&amp;gt;/&lt;/code&gt;: the fit&lt;br&gt;
(&lt;code&gt;calibration.json&lt;/code&gt;), the wall patterns and K map (&lt;code&gt;reference.npz&lt;/code&gt;), the measured shift maps, the report,&lt;br&gt;
and &lt;code&gt;walls/&lt;/code&gt; with three of the camera's own full-size pictures of each wall. &lt;strong&gt;ZIP CALIBRATION&lt;/strong&gt; (in the&lt;br&gt;
wall test, or &lt;em&gt;Zip this scanner's calibration&lt;/em&gt; under Depth) packs it into one file next to it, with the&lt;br&gt;
serial hidden when the Capture box says so. &lt;em&gt;Load saved walls&lt;/em&gt; brings the walls back to fit again, also&lt;br&gt;
from an older version of the app: a 0.4 calibration is refitted for the stripes without new walls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wysaonfxitedkya9yhn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wysaonfxitedkya9yhn.webp" alt="The wall test after four walls" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Optional: tick &lt;em&gt;I measured the picture's width&lt;/em&gt; and type how wide the picture is on the wall&lt;br&gt;
at a known distance. Without it, the lens is guessed from Creality's spec (200 mm across),&lt;br&gt;
which only affects sideways sizes in exported points, not depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Depth and points
&lt;/h3&gt;

&lt;p&gt;The Depth view colours the scene green in the sweet spot (18 to 35 cm), through yellow to red&lt;br&gt;
outside it, with shading so shapes show. &lt;strong&gt;Save this view as points (.ply)&lt;/strong&gt; writes the current&lt;br&gt;
depth as a point cloud in millimetres, which MeshLab, CloudCompare or Blender can open.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Capturing frames, recording and playing back
&lt;/h3&gt;

&lt;p&gt;Every saved picture is stamped with &lt;strong&gt;when it arrived, which camera, the exposure, the gain, the&lt;br&gt;
auto-exposure mode and every other camera setting in force&lt;/strong&gt;, plus its brightness, fine detail, how much&lt;br&gt;
of it shows the projector's pattern, its sharpness, the light-ladder rung and the stream's own picture&lt;br&gt;
number. Every setting change (light buttons, AUTO-TUNE, tests) is logged as it happens, so pictures from&lt;br&gt;
before and after a change never get mixed up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Save frame now&lt;/strong&gt; (Capture panel): one picture from each running camera.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RECORD&lt;/strong&gt;: every picture from every running camera (camera B joins when the A + B view starts it).
&lt;em&gt;every&lt;/em&gt; saves only every Nth picture for long recordings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notes&lt;/strong&gt;: type e.g. "covered camera A" and press Add note; it is stamped on the next picture and kept in
the timeline. With nothing recording, it goes on the next capture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hide the scanner's serial&lt;/strong&gt; (on by default) masks it in captures and in test reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each capture is a folder &lt;code&gt;captures/capture_&amp;lt;date&amp;gt;_&amp;lt;time&amp;gt;_&amp;lt;name&amp;gt;/&lt;/code&gt;: the camera's own JPEGs (no quality lost)&lt;br&gt;
in &lt;code&gt;frames/&lt;/code&gt;, &lt;code&gt;frames.csv&lt;/code&gt; (one row per picture with all the stamps), &lt;code&gt;events.jsonl&lt;/code&gt; (settings snapshots,&lt;br&gt;
every change, notes) and &lt;code&gt;session.json&lt;/code&gt;. &lt;em&gt;Source &amp;gt; Play back a recording...&lt;/em&gt; plays one back as if it were&lt;br&gt;
the scanner. The capture code is shared with Who are yoUSB? (&lt;code&gt;lizard_framecap.py&lt;/code&gt;); see&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/docs/framecap-in-lizard-scanner.md" rel="noopener noreferrer"&gt;docs/framecap-in-lizard-scanner.md&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Diagnostics
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Diagnostics&lt;/strong&gt; opens one window with every check. Point the scanner at a plain wall 20 to 30 cm&lt;br&gt;
away and press &lt;strong&gt;RUN QUICK CHECKS&lt;/strong&gt; (about 10 seconds):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;USB connection&lt;/td&gt;
&lt;td&gt;Both cameras there, at USB 2 speed, on the right driver, not behind an extra hub (Linux)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera A video&lt;/td&gt;
&lt;td&gt;Pictures a second, late pictures, whether the stripe pattern is in every picture (or the lights pulse), picture sizes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video modes and settings&lt;/td&gt;
&lt;td&gt;Every mode and setting both cameras offer (saved in the report)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serial number&lt;/td&gt;
&lt;td&gt;The scanner's serial, read the way Creality's app does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Projector and room light&lt;/td&gt;
&lt;td&gt;How much of the view shows the stripe pattern, how strong it is, how much infrared is in the room (judged between the stripes; sunlight swamps them), anything blown out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Depth speed&lt;/td&gt;
&lt;td&gt;How long one depth picture takes on this computer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;and, one at a time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor map&lt;/td&gt;
&lt;td&gt;Runs camera A alone, B alone, then both, 3 s each; measures each camera's rate, stripe pattern, colour and pulse, and asks which light you saw each time. Answers "which camera does what"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is camera B?&lt;/td&gt;
&lt;td&gt;Streams camera B alone (at a 1 ms flash): colour or grey, does its light pulse. On the owner's Lizard it is the camera with the white ring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flat wall check&lt;/td&gt;
&lt;td&gt;After the wall test: how flat a wall comes out, how far away, and (with a tape-measured distance) how far off. Also spots lens bending at the edges. The best test of a calibration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light test&lt;/td&gt;
&lt;td&gt;Steps the flash from 0.1 ms to 1 ms while you watch the ring, then asks what you saw&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gain test&lt;/td&gt;
&lt;td&gt;Holds the flash at 1 ms and steps the gain: if the ring changes, the firmware ties the LEDs to gain too&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Save diagnostics report (.zip)&lt;/strong&gt; puts the results, the settings dumps, sample pictures, the&lt;br&gt;
calibration report and the light / gain test answers in one zip in &lt;code&gt;reports/&lt;/code&gt;, with the serial&lt;br&gt;
hidden if you like: ready to attach to a GitHub issue.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpsrb0abq5s35x3j38pfw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpsrb0abq5s35x3j38pfw.webp" alt="The Diagnostics window after the quick checks" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking for Creality's own calibration? See&lt;br&gt;
&lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/docs/research/calibration-hunt.md" rel="noopener noreferrer"&gt;docs/research/calibration-hunt.md&lt;/a&gt; and the read-only Windows&lt;br&gt;
script &lt;code&gt;tools/Find-CR-Scan-Calibration.bat&lt;/code&gt; (untested).&lt;/p&gt;

&lt;h3&gt;
  
  
  More screenshots
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdl4p5a8vqhjly43089ve.webp" alt="Scanner off: TV static until the Lizard is plugged in" width="800" height="529"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl79gdtdqpxzl3msdl4pa.webp" alt="Pattern only: the projector's stripes with the background light removed" width="800" height="529"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scanner off: static until the Lizard is plugged in&lt;/td&gt;
&lt;td&gt;Pattern only: what depth is made from&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvlziu37cjfphxp95nh31.webp" alt="The plain look" width="800" height="529"&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The plain look (&lt;code&gt;--plain&lt;/code&gt;, or Look: Plain)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  More
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/HOW_IT_WORKS.md" rel="noopener noreferrer"&gt;HOW_IT_WORKS.md&lt;/a&gt;: what the app reads, changes and never does; the maths.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/DESIGN_NOTES.md" rel="noopener noreferrer"&gt;DESIGN_NOTES.md&lt;/a&gt;: findings, decisions, measurements, what is untested, what next.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/INTEGRATION.md" rel="noopener noreferrer"&gt;INTEGRATION.md&lt;/a&gt;: putting the Lizard into Scantastic.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/docs/research/README.md" rel="noopener noreferrer"&gt;docs/research&lt;/a&gt;: the USB research behind the camera control.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;, &lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;, &lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;,
&lt;a href="https://github.com/The-Dorkknight/lizard-scanner/blob/main/THIRD_PARTY.md" rel="noopener noreferrer"&gt;THIRD_PARTY.md&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Required Notice: Copyright Matthew Armstrong (&lt;a href="https://github.com/The-Dorkknight" rel="noopener noreferrer"&gt;https://github.com/The-Dorkknight&lt;/a&gt;)&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/lizard-scanner" rel="noopener noreferrer"&gt;The-Dorkknight/lizard-scanner on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>hardware</category>
      <category>opensource</category>
      <category>tools</category>
    </item>
    <item>
      <title>SimpleCAD</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Mon, 19 Oct 2026 20:33:23 +0000</pubDate>
      <link>https://dev.to/open-madscience/simplecad-2kde</link>
      <guid>https://dev.to/open-madscience/simplecad-2kde</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F60db8rjun2564t1qzhiq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F60db8rjun2564t1qzhiq.png" alt="SimpleCAD in FreeCAD 1.1.4 on a 1366 px laptop screen" width="799" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source-available · Non-commercial&lt;/strong&gt; · Licensed under the &lt;a href="https://github.com/The-Dorkknight/simplecad/blob/main/LICENSE.md" rel="noopener noreferrer"&gt;PolyForm Noncommercial License 1.0.0&lt;/a&gt; · Copyright Matthew Armstrong · &lt;strong&gt;No warranty, no liability: use at your own risk.&lt;/strong&gt; Free for personal, hobby, study and other non-commercial use; commercial use needs my separate permission. Not legal advice. See &lt;a href="https://github.com/The-Dorkknight/simplecad/blob/main/THIRD_PARTY.md" rel="noopener noreferrer"&gt;THIRD_PARTY.md&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Early release (0.12.0).&lt;/strong&gt; Tested in FreeCAD 1.1.4 on Linux. SimpleFlow's RUN button has been run for real against OpenFOAM v1912 (ESI) only; OpenFOAM Foundation 13, Windows and Mac are not tested yet; see &lt;a href="https://github.com/The-Dorkknight/simplecad/blob/main/RESEARCH.md" rel="noopener noreferrer"&gt;RESEARCH.md&lt;/a&gt;. SimpleStress results are a quick check, not a certified analysis.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;FreeCAD made simple: one pack, four workbenches.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SimpleCAD&lt;/strong&gt;: a decluttered, Fusion 360-style sketch-and-solid workbench. FreeCAD's screen is full of tools most people never touch; SimpleCAD hides them (nothing is removed) and puts sketch → solid front and centre, with one big red button that always does the next obvious thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SimpleStress&lt;/strong&gt;, &lt;strong&gt;SimpleFlow&lt;/strong&gt;, &lt;strong&gt;SimpleSnap&lt;/strong&gt;: foolproof, step-by-step simulations (will it break? how does air or water move? put the parts together). Each locks you into a short numbered pipeline: you can't skip a step, every refused click explains itself in plain English, and the same big red caution-tape button runs the next thing.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Ask it&lt;/th&gt;
&lt;th&gt;You do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SimpleStress&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Will my part break?&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Pick one part or several, tap a material tile for each (PLA, PETG, ABS, steel, stainless, pine, oak, plywood...), say &lt;strong&gt;which way up it was printed&lt;/strong&gt; or &lt;strong&gt;which way the grain runs&lt;/strong&gt;, &lt;strong&gt;bolt&lt;/strong&gt; parts together with virtual M3-M6 bolts (no holes needed), &lt;strong&gt;paint&lt;/strong&gt; the faces that are glued or bolted, &lt;strong&gt;pin&lt;/strong&gt; holes, &lt;strong&gt;click&lt;/strong&gt; where it gets pushed (a small spot, shown as a twinkling red disc, or the whole face), say how hard in N, kg or lb, &lt;strong&gt;tilt&lt;/strong&gt; the push with two sliders, press RUN. You get &lt;em&gt;STRONG / PROBABLY OK / RISKY / WILL BREAK&lt;/em&gt; and a red dot on the weakest spot.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SimpleFlow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;How does air or water move round or through it?&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Choose AROUND (a wind tunnel is built for you) or THROUGH A HOLE (click the rim of each opening: IN or OUT; the water inside is worked out for you), pick air / water / oil, set the speed and tilt the wind. It picks laminar or turbulent for you and builds the CfdOF case.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SimpleSnap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Put these parts together.&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Like Lego: click a peg, hole or flat face; it lists what fits (pegs into matching holes, flat onto flat), you choose STUCK / SPINS / SLIDES, press SNAP IT. The peg goes all the way in and the part lands the right way up.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fma77fe555rpob4z2jwhq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fma77fe555rpob4z2jwhq.png" alt="SimpleStress result: stress colours, glued end, spot force" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All screenshots are real FreeCAD 1.1.4 windows (Linux, virtual display, scripted clicks), except the deck close-ups &lt;code&gt;deck.webp&lt;/code&gt; / &lt;code&gt;swirl.webp&lt;/code&gt;, which are renders.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SimpleCAD
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Brushed-aluminium instrument panel&lt;/strong&gt;: the deck is a screwed-on brushed-metal plate with engraved, recessed modules (MODE · CONFIRM · VIEW · SYSTEM), machined keys with indicator LEDs and a green phosphor readout; SimpleCAD's tool rows get the same metal and keys. The grain is a seamless generated tile (&lt;code&gt;tools/make_brushed_alu.py&lt;/code&gt;), so it's all offline. Turn the tool-row styling off with &lt;code&gt;ALU_TOOLBARS = False&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The big red button&lt;/strong&gt; (in caution tape, shortcut &lt;strong&gt;Ctrl+Enter&lt;/strong&gt;) does the next obvious thing:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;✏ SKETCH&lt;/strong&gt;: sketch on the selected face, or edit the selected sketch. With nothing selected, the three planes appear in the 3D view: click one, or press the big &lt;strong&gt;TOP / FRONT / SIDE&lt;/strong&gt; buttons. No side-panel list to hunt through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✔ FINISH SKETCH&lt;/strong&gt;: close the sketch and leave it selected, so Extrude / Revolve / Loft is one click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✔ OK – FINISH STEP&lt;/strong&gt;: when a tool like Pad or Fillet is open, press its OK button.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acid-green ACTIVE badge&lt;/strong&gt; with rising bubbles shows whether you're in SKETCH, SOLID or TOOL mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🎯 SNAP TO FACE&lt;/strong&gt;: look straight at the selected face (or the sketch you're in) and zoom to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⏎ ENTER button&lt;/strong&gt;: pulses red whenever a tool (Pad, Pocket, Fillet...) is open and ready to confirm; click it, or just press Enter on the keyboard. (Enter is left alone in sketch mode, where it confirms typed dimensions.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whole-solid tools&lt;/strong&gt; (Solid toolbar): &lt;strong&gt;New body&lt;/strong&gt; (works from the start page too), &lt;strong&gt;Duplicate&lt;/strong&gt;, &lt;strong&gt;Move / turn&lt;/strong&gt; (drag arrows and rings), &lt;strong&gt;Combine&lt;/strong&gt;, &lt;strong&gt;Subtract&lt;/strong&gt; (first click kept, the rest cut away) and &lt;strong&gt;Intersect&lt;/strong&gt;. Every result is a new body you can keep sketching on; originals are hidden, not deleted. Each says in plain English why it won't (e.g. "These solids don't touch, so they can't become one solid").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make component&lt;/strong&gt;: select bodies that belong together (a lid and its knob, a case and its clips) and they become one component (a folder that moves as one piece, like a Fusion 360 component). Nothing moves on screen. SimpleSnap snaps a component as one part. Select a component before &lt;strong&gt;New body&lt;/strong&gt; to start a body inside it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy2z7dzngb22idp0k1t1l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy2z7dzngb22idp0k1t1l.png" alt="Combine, subtract, duplicate in FreeCAD 1.1.4" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Light on old PCs&lt;/strong&gt;: the deck is drawn once and reused, and the badge bubbles and ENTER pulse only animate briefly when something changes, so FreeCAD sits at about 3 % CPU when idle (0.9.3 kept a core about 40 % busy).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sketch on a solid, snap to it&lt;/strong&gt;: a new sketch on a solid gets that solid's edges in the sketch plane as pink reference lines (like Fusion 360), so your lines snap to them and can be made coincident. &lt;strong&gt;Use solid edges&lt;/strong&gt; and &lt;strong&gt;Project&lt;/strong&gt; (click any other edge) are first in the sketch tool row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fwdlw4zfjhgvmbzip04.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fwdlw4zfjhgvmbzip04.png" alt="Sketching on a solid: its edges come in pink" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only the everyday tools&lt;/strong&gt;, on their own row under the deck, and the row swaps automatically between sketch tools and solid tools. On a smaller screen (e.g. a 1366 px laptop) the labels drop to icons only, so no tool falls off the end (hover for its name). Optional auto-extrude after finishing a sketch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⋯ More tools&lt;/strong&gt;: every Part Design / Sketcher command, plus "Show all hidden toolbars" and a jump to the full Part Design workbench.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stays put&lt;/strong&gt;: FreeCAD 1.x jumps to the Sketcher workbench when you open a sketch, and to Part Design whenever a Pad/Pocket/Fillet... dialog opens. SimpleCAD hops straight back without resetting anything (Declutter, deck and buttons stay as they were), and steps aside if FreeCAD ever keeps fighting it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tidy panels&lt;/strong&gt;: model tree, task panel, report view and Python console become tabs on the left, and the Tasks tab comes forward when a dialog opens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🧭 Navigation readout&lt;/strong&gt; (the green phosphor display) cycles navigation styles (all orbit around the centre of the screen unless noted):

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Fusion Trackpad&lt;/em&gt;: two-finger swipe pans, Shift or Option + two-finger swipe orbits, pinch zooms, two-finger double-tap fits everything; two-finger click-drag also orbits.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;TinkerCAD&lt;/em&gt;: right-drag (two-finger click-drag) orbits, middle-drag pans, wheel zooms.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;FreeCAD Touchpad&lt;/em&gt;: FreeCAD's own touchpad style.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Fusion Mouse&lt;/em&gt; and &lt;em&gt;Cursor Mouse&lt;/em&gt; (orbit around what's under the mouse) need a mouse with a middle button, so on a Mac the button skips them; they're still in &lt;strong&gt;More tools → 🧭 Navigation&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Reverse trackpad directions or restore FreeCAD's original navigation from the same menu.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🌀 Rainbow swirl&lt;/strong&gt;: the selected face gets a smooth, see-through rainbow spiral so you can always see what's active (opacity: &lt;code&gt;SWIRL_OPACITY&lt;/code&gt;). It animates gently (10 fps) and freezes to a static swirl by itself if the PC struggles; toggle it or make it static in &lt;strong&gt;More tools → 🎨 Look&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🌈 Rainbow faces&lt;/strong&gt;: every face gets its own bright, see-through colour, updated automatically after each Pad/Pocket (toggle in &lt;strong&gt;More tools → 🎨 Look&lt;/strong&gt;; transparency and colour strength at the top of &lt;code&gt;SimpleCADDeck.py&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Looks&lt;/strong&gt;: solids go even more see-through while you sketch. &lt;strong&gt;🌙 Luminous&lt;/strong&gt; (high contrast: near-black background, glowing cyan solids, neon selection) or &lt;strong&gt;☀ Clean&lt;/strong&gt; (light, soft blue-grey). Originals are backed up; &lt;strong&gt;More tools → 🎨 Look → Restore&lt;/strong&gt; puts them back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The look stays in SimpleCAD.&lt;/strong&gt; Switch to SimpleStress, SimpleFlow, Part Design or any other workbench and your solids get their own colours back and FreeCAD its own background; the look returns when you come back. SimpleSnap is the exception: it assembles the same bodies you model, so they keep SimpleCAD's look there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DECLUTTER key&lt;/strong&gt; (amber LED when on): hides every remaining toolbar, panel and the status bar (never the workbench switcher). Press again to undo. Nothing it hides is remembered by FreeCAD, so a restart, with or without SimpleCAD, always brings everything back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opens in SimpleCAD&lt;/strong&gt;: FreeCAD starts in SimpleCAD, and a new Part Design document (e.g. the Start page's &lt;em&gt;Parametric Body&lt;/em&gt;) opens in SimpleCAD too. Turn off with &lt;strong&gt;More tools → Start FreeCAD in SimpleCAD&lt;/strong&gt; (your previous start workbench comes back). The &lt;strong&gt;LUMINOUS key&lt;/strong&gt; (green LED) switches the high-contrast 3D view.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step by step
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Start FreeCAD and pick &lt;strong&gt;SimpleCAD&lt;/strong&gt; (it opens there by default).&lt;/li&gt;
&lt;li&gt;Click the big red button (or &lt;strong&gt;Ctrl+Enter&lt;/strong&gt;) with nothing selected, then click TOP / FRONT / SIDE in the 3D view.&lt;/li&gt;
&lt;li&gt;Draw, then press the button again to finish the sketch; press &lt;strong&gt;Extrude&lt;/strong&gt; (or let auto-extrude do it).&lt;/li&gt;
&lt;li&gt;While a tool (Pad, Pocket, Fillet...) is open, press &lt;strong&gt;Enter&lt;/strong&gt; or the ⏎ button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More tools&lt;/strong&gt; holds everything else, including &lt;strong&gt;Show all hidden toolbars&lt;/strong&gt; and &lt;strong&gt;About&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzuknnih6p7fx15h9myt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzuknnih6p7fx15h9myt.webp" alt="Rainbow swirl" width="620" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Customise
&lt;/h3&gt;

&lt;p&gt;Everything tweakable is at the top of &lt;code&gt;SimpleCADDeck.py&lt;/code&gt;: which tools appear, icon size, colours, themes, navigation presets, trackpad speeds and the bubble animation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The simulations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SimpleStress:&lt;/strong&gt; PARTS &amp;gt; MATERIAL &amp;gt; DIRECTION &amp;gt; BOLTS &amp;gt; HOLD &amp;gt; FORCES &amp;gt; RUN &amp;gt; RESULT&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The only solid in the document is picked automatically. Ctrl+click several solids to test them bolted together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Materials&lt;/strong&gt; are big colourful tiles (drawn in code, no image files): 3D printed (PLA, PETG, ABS, ASA, nylon, polycarbonate, resin), metal (mild steel, stainless, aluminium, brass), wood &amp;amp; board (pine, oak, plywood, MDF) and acrylic. Each shows its strength along and across its weak or strong direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DIRECTION&lt;/strong&gt; (only when it matters): how it sat on the printer (flat, on its side, on its end, or click the face that was on the bed), which way the grain runs (or click the end grain), or which way a sheet's thickness runs. A blue arrow shows it in the 3D view. The stiffness and strength then really depend on it: a PLA bracket printed on its end can be half as strong as one printed flat, and the result tells you when "layers pulled apart" is the weak spot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BOLTS&lt;/strong&gt; (several parts only): pick M3, M4, M5 or M6 and stainless A2-70 or steel 8.8, press PLACE BOLTS and click where each bolt head goes. The bolt runs straight through into the part it touches; &lt;strong&gt;holes are optional&lt;/strong&gt;. A bolt and nut are drawn in the 3D view; click one again to remove it. Each bolt is checked for stretching, shearing, its head or nut pulling through the part, and crushing the hole wall, and the result lists every part and every bolt. Parts touch only where bolted (cautious).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GLUE&lt;/strong&gt; = stuck solid. &lt;strong&gt;PIN&lt;/strong&gt; = a bolt through a round hole: it can turn but not slide off. A single pin on its own is refused (it would swing like a door).&lt;/li&gt;
&lt;li&gt;Forces: &lt;strong&gt;SPOT&lt;/strong&gt; (default): click exactly where it pushes; the force goes on a small circle there (set its size, e.g. a finger is about 15 mm, an M4 washer 9 mm), shown as a red twinkling disc. Click again to move it. &lt;strong&gt;WHOLE FACE&lt;/strong&gt;: paint faces and the push is spread over them. Set how hard in &lt;strong&gt;N, kg or lb&lt;/strong&gt; (kg/lb = the pull of that weight hanging on it; newtons always shown; your unit is remembered), PUSH IN or PULL OUT, then &lt;strong&gt;TILT&lt;/strong&gt; (0 straight, 90 sideways) and &lt;strong&gt;SWING&lt;/strong&gt; (which way it leans) with a live compass and a red arrow. Optional self-weight.&lt;/li&gt;
&lt;li&gt;The part wears its material in the 3D view: layer lines the way it was printed, wood grain along the grain, brushed metal, so a wrong direction is easy to spot.&lt;/li&gt;
&lt;li&gt;A face can't be both held and pushed. Nothing runs until something holds it and something pushes it.&lt;/li&gt;
&lt;li&gt;The mesh is coarsened automatically so older PCs (7th-gen Core i3) cope.&lt;/li&gt;
&lt;li&gt;CLEAN UP removes everything the study added, including FreeCAD's own result objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwxks2dvxshoiox51ngf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwxks2dvxshoiox51ngf.png" alt="Material tiles, and the part showing PETG's layer lines" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F92m3oind3ja484unpn2g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F92m3oind3ja484unpn2g.png" alt="Oak: the grain shown along the shelf" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqeq8d81em0hklgbgiizu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqeq8d81em0hklgbgiizu.png" alt="Which way up it was printed" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fninkq4pripofheow612q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fninkq4pripofheow612q.png" alt="A spot force: red twinkling disc where it pushes, in kg" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xob82bsbagdv403qw90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xob82bsbagdv403qw90.png" alt="Two parts bolted with two M4s" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz17eua4n50xohxjl74d0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz17eua4n50xohxjl74d0.png" alt="Bolted result: every part and bolt listed" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SimpleFlow:&lt;/strong&gt; PART &amp;gt; KIND &amp;gt; FLUID &amp;gt; FLOW &amp;gt; CHECK &amp;gt; RUN &amp;gt; RESULT&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AROUND builds a wind-tunnel box aligned with the wind (3 lengths upstream, 8 downstream, 3 each side), cuts your part out, and labels inlet, outlet and slip side walls.&lt;/li&gt;
&lt;li&gt;THROUGH A HOLE: pick the part with the hole or channel in it (a drilled block, a pipe fitting, a manifold). Every opening on a flat face is found and shown as a grey disc. Tap IN or OUT next to each, or turn on IN / OUT and click the &lt;strong&gt;rim&lt;/strong&gt; of the hole (or its inside wall near that end: the nearest end is used, never the whole wall). The fluid inside is worked out for you; if the channel also comes out somewhere you have not marked, it names that opening instead of letting the water leak. Unrelated holes (e.g. mounting holes) are ignored. Several inlets may face different ways.&lt;/li&gt;
&lt;li&gt;THE PART IS THE FLUID: if you modelled the water itself (a solid shaped like the inside of the pipe), paint the faces where it enters and leaves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujooq0slsy6c2pnzzivf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujooq0slsy6c2pnzzivf.png" alt="Water through a drilled hole: IN at the top, OUT at the bottom" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CHECK shows the Reynolds number and whether it will be laminar or turbulent, and refuses air above 100 m/s (beyond what this setup can model).&lt;/li&gt;
&lt;li&gt;RUN is one green button, like SimpleStress's: it builds the CfdOF case, meshes the part (Gmsh), runs OpenFOAM on every core, with a live progress line ("Solving, step 120 of 2000") and a red STOP that really ends the solver. When it finishes it jumps to RESULT. Nothing to open in CfdOF any more (small MESH PANEL / SOLVER PANEL buttons stay for experts). RESULT reads the solver log: SETTLED, STILL RUNNING, FINISHED BUT NOT SETTLED, or BLEW UP (with advice), plus how many cells and how long it took.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F17we81jb3lpkcl1wxb9j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F17we81jb3lpkcl1wxb9j.png" alt="SimpleFlow's RUN step: one green button" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbh9hycs6e9oxcp8kolqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbh9hycs6e9oxcp8kolqd.png" alt="Wind direction" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SimpleSnap:&lt;/strong&gt; PARTS &amp;gt; BASE &amp;gt; SNAP &amp;gt; DONE&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tick parts to add them. &lt;strong&gt;Your own bodies go into the assembly, no copies are made&lt;/strong&gt;: snapping moves the real part, so SimpleCAD and SimpleStress see it where it was snapped. Parts stay where they are; one that overlaps another is slid aside first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Components:&lt;/strong&gt; bodies that move together (a lid and its separate knob) can be added as &lt;strong&gt;one component&lt;/strong&gt; (select them, then &lt;strong&gt;+ ADD SELECTED AS ONE COMPONENT&lt;/strong&gt;), or made beforehand with SimpleCAD's &lt;strong&gt;Make component&lt;/strong&gt;. Each body on its own is its own component.&lt;/li&gt;
&lt;li&gt;An assembly already in the file is picked up again when you come back (also after reopening the file).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;START AGAIN&lt;/strong&gt; puts every part back exactly where it was (same place, same folder) and splits components SimpleSnap made back into their bodies.&lt;/li&gt;
&lt;li&gt;The base is fixed to the table (first part by default).&lt;/li&gt;
&lt;li&gt;Click a face on the part you want to move. Matching faces on other parts are listed best-first (a perfect fit is chosen for you). Choose STUCK, SPINS, SLIDES, SPINS &amp;amp; SLIDES or BALL JOINT where it makes sense, then press the big red &lt;strong&gt;SNAP IT&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTHER END&lt;/strong&gt; pushes a peg in from the other side of a through-hole; &lt;strong&gt;FLIP LAST&lt;/strong&gt;, &lt;strong&gt;UNDO LAST&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Snapping two parts that are both already attached is refused (it would jam the assembly). NEXT unlocks only when nothing is loose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frvpmyyd0sx88l2a2i4uf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frvpmyyd0sx88l2a2i4uf.png" alt="Peg into hole" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqowhybx8ggz7mbz7fyat.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqowhybx8ggz7mbz7fyat.png" alt="A lid snapped onto a base in SimpleSnap, then tested in SimpleStress: one lid, the spot exactly where it was clicked" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Files
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;InitGui.py&lt;/code&gt;, &lt;code&gt;SimpleCAD*.py&lt;/code&gt;, &lt;code&gt;icons/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;SimpleCAD itself (deck, solid tools, trackpad, swirl, metal panel)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SimpleStress/&lt;/code&gt;, &lt;code&gt;SimpleFlow/&lt;/code&gt;, &lt;code&gt;SimpleSnap/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The simulations (each: &lt;code&gt;InitGui.py&lt;/code&gt;, &lt;code&gt;*Boot.py&lt;/code&gt;, &lt;code&gt;*UI.py&lt;/code&gt; screens, &lt;code&gt;*Logic.py&lt;/code&gt; plain-Python rules, &lt;code&gt;*Backend.py&lt;/code&gt; FreeCAD calls)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;simplewiz/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The simulations' shared core: step-locking wizard, caution-tape panel, face picker, geometry, 3D overlays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;install.py&lt;/code&gt; + launchers&lt;/td&gt;
&lt;td&gt;Installer window (install, start FreeCAD, check, uninstall)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tests/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unit/UI tests (no FreeCAD needed) and &lt;code&gt;tests/freecad/&lt;/code&gt; scripts that run in real FreeCAD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RESEARCH.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sources, measurements, decisions, and what is not verified yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HOW_IT_WORKS.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What it reads, changes and never does&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Build and test
&lt;/h2&gt;

&lt;p&gt;Nothing to compile. The heavy work is done by FreeCAD, CalculiX, Gmsh, OpenFOAM and FreeCAD's assembly solver.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements-dev.txt                    &lt;span class="c"&gt;# PySide6, only for the tests&lt;/span&gt;
&lt;span class="nv"&gt;QT_QPA_PLATFORM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;offscreen python3 &lt;span class="nt"&gt;-m&lt;/span&gt; unittest discover &lt;span class="nt"&gt;-s&lt;/span&gt; tests
&lt;span class="c"&gt;# real FreeCAD (Linux, AppImage extracted to ./squashfs-root):&lt;/span&gt;
&lt;span class="nv"&gt;FREECAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./squashfs-root/AppRun tests/freecad/run_all.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Version
&lt;/h2&gt;

&lt;p&gt;One version for the whole pack. It is in SimpleCAD's &lt;strong&gt;About&lt;/strong&gt; box, the deck's tooltip and the status bar, in every simulation panel's title and status line (e.g. &lt;code&gt;SimpleStress 0.12.0 (Built 2026-10-16 00:58)&lt;/code&gt;), and in &lt;code&gt;python3 install.py --version&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/simplecad" rel="noopener noreferrer"&gt;The-Dorkknight/simplecad on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>software</category>
      <category>tools</category>
    </item>
    <item>
      <title>A Bonding Experience- The open-source new frontier of Z-delamination resistance.</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:19:35 +0000</pubDate>
      <link>https://dev.to/open-madscience/a-bonding-experience-2k13</link>
      <guid>https://dev.to/open-madscience/a-bonding-experience-2k13</guid>
      <description>&lt;h3&gt;
  
  
  The open-source new frontier of Z-delamination resistance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A hobbyist's field guide to Z-strength in FDM printing without tripping over patents, and a defensive publication of PinLoK (pinned infill).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By Matthew Armstrong (&lt;a href="https://github.com/The-Dorkknight" rel="noopener noreferrer"&gt;The-Dorkknight&lt;/a&gt;), United Kingdom. First published 13 October 2026.&lt;/p&gt;

&lt;p&gt;📄 &lt;strong&gt;Read it:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/a-bonding-experience/blob/main/PAPER.md" rel="noopener noreferrer"&gt;PAPER.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/a-bonding-experience/blob/main/a-bonding-experience-v1.0.pdf" rel="noopener noreferrer"&gt;PDF&lt;/a&gt; · &lt;a href="https://the-dorkknight.github.io/a-bonding-experience/" rel="noopener noreferrer"&gt;web page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu8ymavdity412amyun0h.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu8ymavdity412amyun0h.webp" alt="PinLoK in the model: Grid infill without and with pins" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every strength number for my own ideas here, including Shift-LoK, FlatLock and PinLoK, is theoretical:&lt;/strong&gt; it comes from a 2-D computer simulation with known blind spots (paper, section 3), not from printed and tested parts. They are not product claims and say nothing about anyone else's products or patents. Real-world numbers quoted from others are linked to their sources. &lt;strong&gt;Not legal advice.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  In one minute
&lt;/h2&gt;

&lt;p&gt;Printed parts snap along their layers. The famous fix, staggering walls by half a layer ("brick layers"), is patented in the US. I went looking for every way to get stronger layers &lt;em&gt;without&lt;/em&gt; beads of different heights, ran twelve ideas through a strength model, and wrote down what happened, failures included.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Idea&lt;/th&gt;
&lt;th&gt;Simulated result across the layers (theoretical)&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Loopholes" (variable first layer, smaller or multi-phase staggers)&lt;/td&gt;
&lt;td&gt;about the same as normal; still needs different bead heights&lt;/td&gt;
&lt;td&gt;geometry doesn't care about wording&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different layer height per wall&lt;/td&gt;
&lt;td&gt;−1 to −11 %&lt;/td&gt;
&lt;td&gt;weaker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlatLock (fat/thin walls)&lt;/td&gt;
&lt;td&gt;−4 to −17 %&lt;/td&gt;
&lt;td&gt;weaker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Squish-LoK (over-extruded squish bead)&lt;/td&gt;
&lt;td&gt;−8 to +1 %&lt;/td&gt;
&lt;td&gt;nothing to fill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Squish-KeyLoK (keyed gaps)&lt;/td&gt;
&lt;td&gt;−19 to +3 %&lt;/td&gt;
&lt;td&gt;keys snap off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Wide inner walls&lt;/strong&gt; (about 1.5 × nozzle)&lt;/td&gt;
&lt;td&gt;+2 to +9 %&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;partial success, just a slicer setting&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PinLoK pinned infill&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;+16 to +27 %&lt;/strong&gt; against solid infill of the same weight&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;best result; costs print time&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;PinLoK&lt;/strong&gt; builds on Oak Ridge National Laboratory's published Z-pinning. Every few layers the nozzle stops over half of the open gaps in Grid infill and pushes a blob of plastic down into each one; the other half get theirs next time, so the joints never line up. A &lt;strong&gt;potato mode&lt;/strong&gt; keeps each push to about a second for stock printers like the Ender 3. A late discovery: pins that end half-way through a layer, not on a layer line, did far better in the simulation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's published
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;To keep it free.&lt;/strong&gt; Once a method is published, nobody can validly patent it afterwards. Section 9 of the paper discloses PinLoK and its variations in buildable detail, and the methods are dedicated to the public.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;To bring makers together.&lt;/strong&gt; Open notebooks let tinkerers build on each other's work instead of repeating it. Print the test plan, post your results as an issue or discussion, fork it, improve it: everyone who contributes gets credited in the next version (paper, section 11).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;To save you the dead ends.&lt;/strong&gt; Most of the ideas here didn't work. Now you know without trying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selfishly:&lt;/strong&gt; more cool 3D-printing techniques come back from the community, and I am now 22.713333…% more employable. High-efficiency synergy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's in here
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Folder&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/The-Dorkknight/a-bonding-experience/blob/main/PAPER.md" rel="noopener noreferrer"&gt;&lt;code&gt;PAPER.md&lt;/code&gt;&lt;/a&gt;, the PDF, &lt;a href="https://github.com/The-Dorkknight/a-bonding-experience/tree/main/docs" rel="noopener noreferrer"&gt;&lt;code&gt;docs/&lt;/code&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;The paper as Markdown, PDF and a web page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-bonding-experience/tree/main/figures" rel="noopener noreferrer"&gt;&lt;code&gt;figures/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Every figure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-bonding-experience/tree/main/pinlok" rel="noopener noreferrer"&gt;&lt;code&gt;pinlok/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The PinLoK G-code post-processor (Python 3.8+, no dependencies): &lt;code&gt;python3 pinlok/engine.py file.gcode --mode potato&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-bonding-experience/tree/main/tests" rel="noopener noreferrer"&gt;&lt;code&gt;tests/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Its tests: &lt;code&gt;python3 -m unittest discover -s tests&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-bonding-experience/tree/main/model" rel="noopener noreferrer"&gt;&lt;code&gt;model/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The strength model and every result file (needs numpy, scipy, matplotlib)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/The-Dorkknight/a-bonding-experience/blob/main/PUBLISHING.md" rel="noopener noreferrer"&gt;&lt;code&gt;PUBLISHING.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;How this was published, and what makes it count as published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full app (LoK Walls: FlatLock, PinLoK and a locked brick-layer pattern, with installers for Cura, PrusaSlicer and OrcaSlicer) is in a separate repository, to be published after ADDMAN, the brick-layer patent holder, has been asked. I haven't contacted them yet; I intend to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cite
&lt;/h2&gt;

&lt;p&gt;See &lt;a href="https://github.com/The-Dorkknight/a-bonding-experience/blob/main/CITATION.cff" rel="noopener noreferrer"&gt;CITATION.cff&lt;/a&gt; (GitHub shows a "Cite this repository" button). Once archived, the DOI goes here: &lt;em&gt;DOI to be added&lt;/em&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/a-bonding-experience" rel="noopener noreferrer"&gt;The-Dorkknight/a-bonding-experience on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>SimpleMesh</title>
      <dc:creator>Matthew Armstrong</dc:creator>
      <pubDate>Sun, 11 Oct 2026 08:45:17 +0000</pubDate>
      <link>https://dev.to/open-madscience/simplemesh-44dc</link>
      <guid>https://dev.to/open-madscience/simplemesh-44dc</guid>
      <description>&lt;p&gt;&lt;strong&gt;WORK IN PROGRESS: MANY FEATURES MAY BE BROKEN&lt;/strong&gt; · version &lt;code&gt;0.5.0-wip&lt;/code&gt; · pre-release&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free software · GPL-3.0-only&lt;/strong&gt; · Copyright (C) 2026 Matthew Armstrong · &lt;strong&gt;No warranty, no liability: use at your own risk.&lt;/strong&gt; See &lt;a href="https://github.com/The-Dorkknight/simplemesh/blob/main/LICENSE" rel="noopener noreferrer"&gt;LICENSE&lt;/a&gt; and &lt;a href="https://github.com/The-Dorkknight/simplemesh/blob/main/THIRD_PARTY.md" rel="noopener noreferrer"&gt;THIRD_PARTY.md&lt;/a&gt;. Not legal advice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkstlo3k6krhjpp7tswd7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkstlo3k6krhjpp7tswd7.webp" alt="Easy mode: picture buttons down the left, a snowman built from shapes, colours and Print! along the bottom" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;About the screenshots:&lt;/strong&gt; they are the real Blender 4.0.2 window with SimpleMesh loaded, captured on a virtual display (software rendering) while a script moved the mouse and pressed keys (&lt;code&gt;tests/gui_session.py&lt;/code&gt;). &lt;strong&gt;No person, and no child, has used this version by hand yet&lt;/strong&gt;, so how it &lt;em&gt;feels&lt;/em&gt; is unproven. See what is tested.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A Blender add-on that turns Blender into a plain &lt;strong&gt;make-and-print workbench&lt;/strong&gt;. No scenery, no animation timeline, no game-asset tooling. It has two levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easy mode&lt;/strong&gt; (what Blender now starts in): big picture buttons aimed at a ten-year-old. Build things out of shapes, your own drawings, words and clay, colour them in, press &lt;strong&gt;Print!&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The panel&lt;/strong&gt;: big buttons across the top of the 3D view, there in every mode, for editing STL files exactly: lay flat, move, turn, size, pull a face, cut, drill, measure, mould like clay, with millimetres. It is the same idea as SimpleCAD's deck: a big red button that does the next obvious thing, a green badge that says what is active, and keys for the look.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is the Blender sibling of &lt;a href="https://github.com/The-Dorkknight/simplecad" rel="noopener noreferrer"&gt;SimpleCAD&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Easy mode
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;To make something:&lt;/strong&gt; click a picture, move the mouse, click to put it down. A shape sits on &lt;strong&gt;whatever is under the mouse&lt;/strong&gt;: the floor, or the top or side of another shape, so things stack and stick the way you expect.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Left side: things to add&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Block, Ball, Tube, Cone, Pyramid, Roof, Ring, Star, Heart&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ready-made shapes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Draw&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hold the mouse button and draw a loop on the floor. It becomes a solid shape. Wobbly lines, crossed lines and scribbles still give something solid.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Words&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Type a word or your name and get 3D letters.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Squish and pull the chosen shape (or a new ball) like clay, then press Done. Turn on &lt;strong&gt;Mirror&lt;/strong&gt; for faces and creatures.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ideas&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Start from a snowman, house, rocket, robot or car made of shapes, then change it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Load a model file somebody else made.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;To change a shape:&lt;/strong&gt; click it. Drag it to move it. It has six dots:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dot&lt;/th&gt;
&lt;th&gt;Pull it to...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;white (top corner)&lt;/td&gt;
&lt;td&gt;make it bigger or smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;red / green / blue&lt;/td&gt;
&lt;td&gt;make it longer / wider / taller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;orange&lt;/td&gt;
&lt;td&gt;spin it round&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;purple&lt;/td&gt;
&lt;td&gt;tip it over&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Along the bottom&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Undo / Redo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Go back a step. Nothing can be broken.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Copy / Bin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Another one the same / throw it away.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hole&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Turns the chosen shape into a hole (it goes red). Push it into another shape; Glue or Print! cuts it out.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Glue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sticks the shapes together into one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keeps your project so you can carry on another day (it reopens in Easy mode).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Colours&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Colour in the chosen shape. (Colours are for you; a printer prints in whatever plastic is loaded.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Print!&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cuts out the holes, glues what touches, repairs everything, sits it on the bed, tells you if it is too big, and saves the file for the printer.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bottom right: &lt;strong&gt;arrow buttons turn the view&lt;/strong&gt;, + and − zoom, the middle one shows everything. You can also drag with the right mouse button to look around.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5i9efcedcd38wb494qq5.webp" alt="Start" width="800" height="450"&gt; What Blender looks like when it starts&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7tjdzcgegiklpt53te8w.webp" alt="Stack" width="800" height="450"&gt; A ball being put on top of a block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfcex322a2rtbmtmcda3.webp" alt="Dots" width="800" height="450"&gt; The six dots on the chosen shape&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwl6lq61p9e0mlhqk2bs.webp" alt="Taller" width="800" height="450"&gt; Pulling the blue dot: taller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ff8iy0b6qivb0r0pnud.webp" alt="Draw" width="800" height="450"&gt; Draw a loop on the floor...&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8kaidiq5x2qntjdje7j.webp" alt="Ideas" width="800" height="450"&gt; Ideas: ready-made starters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;Easy&lt;/strong&gt; key on the panel turns Easy mode off and on. With it off you select models by clicking and use the panel's tools described next. To make Blender start with it off, untick &lt;em&gt;Start in Easy mode&lt;/em&gt; in the add-on's preferences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What "anything you can imagine" really means here:&lt;/strong&gt; anything you can build from those shapes, your own drawn outlines, letters and clay, stuck together and with holes cut out. That covers a lot (characters, vehicles, buildings, name tags, cookie cutters, creatures) but it is not everything: no screw threads, no exact engineering fits, and very thin clay shapes may not print.&lt;/p&gt;

&lt;h2&gt;
  
  
  The panel (big buttons)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fda1lm0jzaahfq88pbly2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fda1lm0jzaahfq88pbly2.webp" alt="The panel across the top of the 3D view: red MOULD LIKE CLAY button in caution tape, green SELECT ACTIVE badge, FIND MODEL, twelve tool keys, four system keys" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The big red button&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The next obvious thing. Normally &lt;strong&gt;MOULD LIKE CLAY&lt;/strong&gt;. While a tool is on: &lt;strong&gt;FINISH&lt;/strong&gt; that tool. While moulding: &lt;strong&gt;DONE&lt;/strong&gt;. In one of Blender's own modes: &lt;strong&gt;BACK TO THE MODELS&lt;/strong&gt;. In Blender's own Sculpt Mode before the model is ready: &lt;strong&gt;GET READY TO MOULD&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The green badge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What is active right now: SELECT, EASY, LAY FLAT, CLAY: PULL...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FIND MODEL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Centres the view on the model (same as &lt;strong&gt;F&lt;/strong&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool keys&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lay flat, Move, Turn, Size, Pull face, Cut, Drill, Measure, Open, Save, Undo, Redo. The key of the tool that is on has its light lit. While moulding they become the seven brushes plus &lt;strong&gt;Brush +&lt;/strong&gt;, &lt;strong&gt;Brush -&lt;/strong&gt; and &lt;strong&gt;Mirror&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Easy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Picture buttons on or off.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Luminous&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Switches between the dark, high-contrast look and the light, clean one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hide clutter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hides Blender's own menu rows and status bar; press again to bring them back. The panel stays.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;More tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exact numbers, shapes and holes, check and repair, view settings, &lt;strong&gt;Look&lt;/strong&gt; (below), and &lt;em&gt;Report a problem&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The panel is drawn inside the 3D view (Blender's own header rows cannot hold big buttons), ends short of the right edge so Blender's view ball stays free, and shrinks its buttons on a narrow window. Hover over a button and a line under the panel says what it does. Switch it off in &lt;strong&gt;More tools ▸ Big-button panel&lt;/strong&gt; and the tools go back to the thin strip in Blender's header.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fymruqibsr6874xbs6a4g.webp" alt="A tool is on" width="800" height="280"&gt; Lay flat is on: its light is lit, the red button says FINISH LAY FLAT&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhoj3if4zj6skccqxqao7.webp" alt="While moulding" width="800" height="280"&gt; While moulding: DONE, and the brushes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyii0pfr0jmdib6dpkndn.webp" alt="Clean look" width="800" height="280"&gt; The Clean look&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flh2kvjva4ikjf8ewox4c.webp" alt="Clutter hidden" width="800" height="280"&gt; Hide clutter: Blender's rows are gone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Look
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;More tools ▸ Look&lt;/strong&gt; (also in the strip's &lt;strong&gt;View&lt;/strong&gt; pop-up):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Luminous&lt;/strong&gt; (near-black background, cyan models, orange outline), &lt;strong&gt;Clean&lt;/strong&gt; (light background, soft blue-grey) or &lt;strong&gt;Blender default&lt;/strong&gt;. The &lt;strong&gt;Luminous&lt;/strong&gt; key on the panel switches between the first two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rainbow&lt;/strong&gt;: every model gets a colour of its own, which shows at a glance which parts are separate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the triangles&lt;/strong&gt;: the triangles of the STL drawn on the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;See-through&lt;/strong&gt;, &lt;strong&gt;Show flipped faces (red)&lt;/strong&gt;, &lt;strong&gt;Show size on the model&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv88dakoz49etlyhu4p3o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv88dakoz49etlyhu4p3o.webp" alt="Rainbow and triangles" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open&lt;/strong&gt; an STL. It arrives in millimetres at a sensible size (a 5200 mm monster is divided by 10 until it fits your bed), sitting on the bed, in the middle of the view. Its size and "fits the bed" are written in the top-left corner all the time.&lt;/li&gt;
&lt;li&gt;Pick a tool on the panel, then &lt;strong&gt;do it on the model&lt;/strong&gt;. The view shows what a click will do before you click. Orbit and zoom keep working. &lt;strong&gt;Esc&lt;/strong&gt; or right-click leaves the tool. &lt;strong&gt;Ctrl+Z&lt;/strong&gt; undoes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save&lt;/strong&gt; writes a millimetre STL as &lt;code&gt;&amp;lt;name&amp;gt;_edited.stl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Lost the model? Press &lt;strong&gt;F&lt;/strong&gt; (or &lt;em&gt;Find&lt;/em&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;th&gt;Extras&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lay flat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Move over the model: the flat face under the mouse lights up. Click: that face sits on the bed.&lt;/td&gt;
&lt;td&gt;No Edit Mode.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Move&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drag a model: it slides across the bed.&lt;/td&gt;
&lt;td&gt;Shift+drag lifts it. Arrow keys 1 mm (Shift 10, Ctrl 0.1). D = drop to bed, C = centre.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Turn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drag a model sideways: it spins in 15° steps and is put back on the bed.&lt;/td&gt;
&lt;td&gt;X / Y / Z choose the direction. Arrow keys = 90°. Type an angle + Enter. Shift = fine.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drag away from the model = bigger, towards it = smaller. Or &lt;strong&gt;type the longest side in mm + Enter&lt;/strong&gt;.&lt;/td&gt;
&lt;td&gt;X / Y / Z stretch one direction only. Ctrl = whole millimetres. Scale stays 1.0.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pull&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Point at a flat face (it lights up) and &lt;strong&gt;drag it out or in&lt;/strong&gt;: the part gets longer or shorter. Or point and type mm + Enter.&lt;/td&gt;
&lt;td&gt;Steps of 0.5 mm; Ctrl = whole mm; Shift = free. Minus key = inwards when typing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cut&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A red plane follows the mouse over the model; click to cut there. &lt;strong&gt;Or drag a line across the model&lt;/strong&gt; to cut along it.&lt;/td&gt;
&lt;td&gt;X / Y / Z direction. Type a height in mm + Enter. K = keep both / one side. L = lay the cut faces on the bed. Cuts are capped.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Drill&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A red circle follows the mouse on the surface; click to drill a round hole.&lt;/td&gt;
&lt;td&gt;Type the diameter + Enter, or + / −. T = through / blind, [ ] = depth. Ctrl = centre of the face.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Measure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Click two points.&lt;/td&gt;
&lt;td&gt;Snaps to corners. Shows the distance and its X / Y / Z parts.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Clay: mould it like clay
&lt;/h3&gt;

&lt;p&gt;Clay is for free-form changes: click a model, press the red &lt;strong&gt;MOULD LIKE CLAY&lt;/strong&gt; button (or the Clay picture in Easy mode), and the panel's keys become seven brushes. Press on the surface and drag. Choosing &lt;em&gt;Sculpt Mode&lt;/em&gt; from Blender's own mode menu ends up in the same place: SimpleMesh does the Clay set-up by itself.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Brush&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pull&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The clay follows the mouse and stretches out. Pull a lump, a horn, a handle out of a flat face.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Build up&lt;/strong&gt; / &lt;strong&gt;Push in&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Rub to raise the surface / press it in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shift&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slide an area sideways without stretching it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inflate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Swell outwards in all directions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Smooth&lt;/strong&gt; / &lt;strong&gt;Flatten&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Rub out lumps / level an area.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Brush &lt;strong&gt;Size is in millimetres&lt;/strong&gt; and stays the same at any zoom; it starts at a fifth of the model. &lt;strong&gt;Brush +&lt;/strong&gt; and &lt;strong&gt;Brush -&lt;/strong&gt; on the panel change it (or &lt;strong&gt;F&lt;/strong&gt;, then move the mouse). Holding &lt;strong&gt;Ctrl&lt;/strong&gt; does the opposite, holding &lt;strong&gt;Shift&lt;/strong&gt; smooths, &lt;strong&gt;Ctrl+Z&lt;/strong&gt; undoes a stroke. The read-out shows how many triangles the model has and warns in red when it is getting heavy. Press the red &lt;strong&gt;DONE&lt;/strong&gt; button to go back to the normal tools.&lt;/p&gt;

&lt;p&gt;Underneath it is Blender's own sculpting engine, set up for STL work: the model is first given small triangles everywhere (its shape does not change) because an STL's big flat triangles have nothing to mould; more are added under the brush as you work; mirror symmetry is off; and when you press &lt;strong&gt;Done&lt;/strong&gt; the flat areas you did not touch are merged back into big triangles so the file stays small. &lt;strong&gt;Detail&lt;/strong&gt; (the size of the new triangles, in the header row) cannot be set finer than a thirtieth of the brush: a big brush with a tiny Detail makes Blender cut hundreds of thousands of triangles in one drag and stop answering.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhq4ik757g4nyn4gsuw7v.webp" alt="Pull a face" width="800" height="450"&gt; Pull: point at a flat face...&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhor2p3y73xxva92hfcqa.webp" alt="Pull drag" width="800" height="450"&gt; ...and drag it: +10 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feu3r9opxmle3ua2ehr0a.webp" alt="Clay pull" width="800" height="450"&gt; Clay ▸ Pull: a horn pulled out of a flat face&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fktb02u9e66137rf8tr3i.webp" alt="Clay push" width="800" height="450"&gt; Clay ▸ Push in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pop-ups in Blender's header row (and under &lt;strong&gt;More tools&lt;/strong&gt; on the panel) hold the things that need numbers or are used less often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Numbers&lt;/strong&gt;: ÷1000 ÷100 ÷10 ×10 ×1000, inch ↔ mm, set the longest side, exact X/Y/Z, drop / centre / auto lay flat, 90° turns, mirror, hole and cut settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shapes&lt;/strong&gt;: add a block, cylinder, sphere or cone sized in mm. &lt;strong&gt;Make it a hole&lt;/strong&gt; turns any shape into a red wire "hole"; &lt;strong&gt;Cut the holes out&lt;/strong&gt; removes every hole from whatever it touches (no "select the cutter, then the base"). Weld together, separate loose pieces, copy, delete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repair&lt;/strong&gt;: Check (triangles, volume, surface, holes, inside-out faces), one-click Repair, Simplify.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View&lt;/strong&gt;: standard views, show everything, looks (Luminous / Clean / Blender default), show flipped faces, laptop navigation (no middle button needed), printer bed size (presets for FlashForge Adventurer 5M, Bambu Lab A1, Creality Ender 3 V2).&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fektkq7drecqrt59p7ne9.webp" alt="Cut" width="800" height="450"&gt; Cut: the plane follows the mouse&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbhj100gqcc92n8s9ajtz.webp" alt="Hole" width="800" height="450"&gt; Hole: the circle follows the mouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6v78i80xw55t2ehinnr.webp" alt="Cut by line" width="800" height="450"&gt; Cut: drag a line across&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6mf8s87wpi2rkk1ykl6e.webp" alt="Measure" width="800" height="450"&gt; Measure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvryvv0inet0uidx1l2i.webp" alt="Size" width="800" height="450"&gt; Size: drag, or type&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyf7dyrbj2lfhkjt12jza.webp" alt="Numbers" width="800" height="450"&gt; The Numbers pop-up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6odf803qxg3babrpdr3l.webp" alt="Imported" width="800" height="450"&gt; A 5200 mm STL after Open: shrunk to fit, centred&lt;/td&gt;
&lt;td&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F28plbgzzf6jfu6sc6x97.webp" alt="Open" width="800" height="450"&gt; Open: choose the file's units here&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why the tools are like this, and which existing add-ons and programs they were modelled on: &lt;a href="https://github.com/The-Dorkknight/simplemesh/blob/main/docs/DESIGN.md" rel="noopener noreferrer"&gt;docs/DESIGN.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is tested and what is not
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tested in a real Blender 4.0.2 window&lt;/strong&gt; (virtual display, simulated mouse and keys, 114 checks, &lt;code&gt;tests/gui_session.py&lt;/code&gt;; the first part drives the tools through the thin strip with the panel off, the last part drives the panel): &lt;strong&gt;Easy mode as a scripted child's session&lt;/strong&gt;: Blender starts in it; Block is picked up and put down; a Ball is put on top of the Block and lands on it; clicking selects; pulling dots makes the block taller, bigger and spins it; dragging slides it; colour, Copy, Bin; a loop drawn with the mouse becomes a solid; Ideas places a rocket; Glue makes it one closed model; Undo; the arrow buttons and right-drag turn the view; the Clay button and Done; Print! glues, repairs, sits everything on the bed and opens the save box; a saved project reopens in Easy mode. Then the strip tools: start-up tidy (one 3D view, millimetres, two workspace tabs); import of a 5200 mm STL lands centred and sensibly sized on screen; view re-centres after scaling; &lt;strong&gt;F&lt;/strong&gt; finds a model 4.7 m away; every strip tool by click/drag/keys (lay flat by hover+click, move by drag and arrow key, turn by drag and by key, size by typing and by drag, cut by click and by dragging a line, pull a face by typing and by dragging, hole with typed diameter, measure); Clay: entering from the strip, millimetre brush size, a Pull stroke stretching a flat face from 20 to 39 mm, switching to Push in, Ctrl+Z after the first stroke staying in Clay and taking back just that stroke, redo, Done merging flat areas and leaving the model watertight; Sculpt Mode entered the way Blender's own mode menu does it being noticed, set up automatically (small triangles, automatic detail, the Pull brush in millimetres) and a drag then pulling the block from 20 to 39 mm; undo of a tool step; switching tools from the strip while a tool is on; the four pop-ups and the Open file chooser appear. &lt;strong&gt;The panel&lt;/strong&gt;: its mouse watcher starts by itself; a click on the Lay flat key starts the tool, lights the key and turns the red button into FINISH LAY FLAT; the red button finishes it; clicks on the panel do not reach the model behind it; the red button starts Clay and the keys become brushes; Brush +, a brush key, a stroke in the view, the Undo key (the stroke goes, Clay stays), DONE; the Luminous key; Hide clutter and back; the Easy key, a picture button under the panel, a panel tool started from Easy mode and Easy mode taking over again afterwards; a recovery copy written while the panel is watching the mouse. &lt;strong&gt;The installer window&lt;/strong&gt; was driven the same way (&lt;code&gt;tests/test_installer_gui.py&lt;/code&gt;, 20 checks): it found and questioned two Blenders, ticking and unticking rows, refusing a program that is not Blender, Install, the two options, Remove, and Open Blender all worked, and Blender then started normally with the add-on on. Plain &lt;code&gt;python3 install.py&lt;/code&gt; and the Linux launcher script both opened the window (&lt;code&gt;tests/test_install_launch.py&lt;/code&gt;, 14 checks, starts &lt;code&gt;install.py&lt;/code&gt; as its own program and looks for its window). With tkinter switched off, the dialog-box version (zenity) was clicked through an install by hand-placed mouse clicks, and the terminal version picked Blender number 1 from its list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also tried by hand with real mouse and keyboard events&lt;/strong&gt; (sent through the display server with &lt;code&gt;xdotool&lt;/code&gt;, not Blender's own event simulation; Blender 4.0.2 started normally after the installer; these sessions are not part of the repository's tests): placing a block and pulling a dot; opening a 234-triangle bracket and a 327,680-triangle model through the real file chooser; Lay flat on the bracket; a long Clay Pull stroke on both; &lt;strong&gt;Sculpt Mode chosen from Blender's mode menu&lt;/strong&gt; (in 0.4.2 two long drags changed nothing you could see, in 0.4.3 the set-up runs and the drag pulls a horn out); Ctrl+Z after one and after two Clay strokes; Undo pressed until nothing was left, then adding a shape; Edit Mode and &lt;em&gt;Back to the models&lt;/em&gt;; &lt;strong&gt;Report a problem&lt;/strong&gt; (which caught a real error in the first version of the undo repair). With &lt;strong&gt;Blender 5.0.1&lt;/strong&gt; and version 0.4.2, placing a block, pulling a dot and a Clay Pull stroke worked the same way; nothing later than 0.4.2 was run in a 5.x window. &lt;strong&gt;For 0.5.0, on a 1366 x 768 screen&lt;/strong&gt;: the panel's keys by real clicks (Lay flat and FINISH, MOULD LIKE CLAY, Brush + three times, Build up, strokes, the Undo key, DONE, Luminous, Hide clutter, More tools and its Exact numbers pop-up, Report a problem); four fast clicks on a picture button all counting; an 82,000-triangle model opened through the file chooser and moulded with five strokes (Blender stayed under 0.9 GB and DONE took under 8 seconds on this machine's software rendering). &lt;strong&gt;The freeze&lt;/strong&gt;: with 0.4.3 the same model, five strokes and then &lt;em&gt;Finer everywhere&lt;/em&gt; left Blender not answering for several minutes at 4 GB; that button no longer exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tested without a window&lt;/strong&gt; (&lt;code&gt;tests/test_headless.py&lt;/code&gt;, Blender &lt;strong&gt;5.2.2&lt;/strong&gt; as the &lt;code&gt;bpy&lt;/code&gt; module, also with numpy disabled): all the mesh work behind the tools: import sizes and units, scaling, lay flat, every Easy shape is closed and the right size; hand-drawn outlines (clean loop, overshoot, figure-of-eight, scribble) always give a valid solid; words become closed letters; each of the five Ideas goes through Print! to one watertight model that fits the bed; glue with holes; cuts with any plane (volumes add up, caps watertight), pulling faces, refining for Clay and merging flat areas again, Clay set-up including all seven brushes on Blender 5.2's brush system, drilling through and blind holes, hole shapes, weld, separate, repair, simplify, export and re-import.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not tested:&lt;/strong&gt; any real person using it, and in particular &lt;strong&gt;no child has tried Easy mode&lt;/strong&gt;: whether a ten-year-old finds the dots, the stacking and the wording obvious is exactly what a scripted mouse cannot tell me; the Words and Save pop-up boxes themselves (the operators behind them were run); screens smaller than 1366 x 768 (the picture buttons were looked at on 1600 x 900 and 1366 x 768 only); trackpads; the two reported problems on the computer where they were reported: "a drag only changes it slightly" (0.4.3: reproduced here by choosing Sculpt Mode from Blender's own menu) and "on sculpt all controls lock up" (0.5.0: reproduced here with &lt;em&gt;Finer everywhere&lt;/em&gt;), so in both cases I fixed something that matches the description without being able to check that it is what happened there; &lt;strong&gt;the panel, the look keys and every other 0.4.3 and 0.5.0 change in a window on any Blender other than 4.0.2&lt;/strong&gt; (on 5.2 only the windowless tests, where the panel is laid out and its buttons run but nothing is drawn or clicked); Blender 3.x and 4.1 to 4.5 in any form; the panel on high-resolution (Retina / 4K scaled) screens, on two monitors or in more than one Blender window; the recovery copy over a real working session (it was written once on demand and once by its timer); Clay on models above about 330,000 triangles and on slow computers; the dense-model path above 700,000 triangles; large real-world STLs (hundreds of thousands of triangles: hovering builds a lookup of the model first, which may pause for a few seconds); the full scripted window session on Blender 3.x, 4.2+ or 5.x (only the windowless tests ran on 5.2); a real double-click on &lt;code&gt;install.py&lt;/code&gt; or the launcher in a desktop file manager (they were started from a command line on a virtual display; the 0.4.2 fix for "&lt;code&gt;install.py&lt;/code&gt; shows nothing" was not confirmed on the computer where it was reported); macOS and Windows, including the Mac and Windows double-click launchers and the "Add another Blender..." file chooser box itself; flatpak/snap/Steam Blender with the installer (the search for them is written but there was none here to find); the extension-format zip; the GitHub workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  More
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/The-Dorkknight/simplemesh/blob/main/docs/DESIGN.md" rel="noopener noreferrer"&gt;docs/DESIGN.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/simplemesh/blob/main/HOW_IT_WORKS.md" rel="noopener noreferrer"&gt;HOW_IT_WORKS.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/simplemesh/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/simplemesh/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; · &lt;a href="https://github.com/The-Dorkknight/simplemesh/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/The-Dorkknight/simplemesh" rel="noopener noreferrer"&gt;The-Dorkknight/simplemesh on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>opensource</category>
      <category>software</category>
    </item>
  </channel>
</rss>
