Skip to content

Perfboard Studio v0.14.0

Choose a tag to compare

@github-actions github-actions released this 29 Sep 22:06
· 63 commits to main since this release

Added

  • A step bar: Circuit, Board, Placement, Wiring, Check, Build — and Next. The window
    had every tool and no order: nothing said the circuit comes first, that a board is
    chosen before the parts go on it, that routing joins them, that DRC says it can be built,
    or that the guide and the 3D view are what it is built from. A row under the toolbar now
    shows the six steps, where this board stands on each ("8 parts · 7 nets", "5 x 7 cm",
    "8/8 on the board", "3 connections left", "0 errors · 1 warning"), and a Next button that
    does whichever is next — a DRC error or an open net first. Pressing a step goes where it
    is done. Choosing the board is a step of its own.

  • Boards chosen by looking at them. The question of which stock board a circuit goes
    on is now a grid of pictures, each board with the circuit actually arranged on it, the
    suggested one starred and the too-small ones greyed; and New Board and Board Setup draw
    the chosen product — finger strips, corner holes, printed legend — beside the questions,
    redrawn as the size changes. The pictures are drawn by the board view itself.

  • A welcome that has somewhere to begin. Draw a New Circuit, the shipped examples by
    name with what each one is ("11 parts on a 6 x 8 cm board", the drawn-but-not-built one
    first), Import a KiCad Netlist, and the six steps named once.

  • A wire on the sheet can be dragged from pin to pin, as well as clicked at both ends.
    A drag used to take the first pin and do nothing when let go -- the gesture everybody
    tries first. The pin under the pointer is ringed before the click, and a pin is a target
    of at least eight pixels at any zoom (it was 1.2 mm: five pixels on a fitted sheet).

  • Ground and power from a pin's right-click. A pin on nothing offers the sheet's rails
    by name, and GND and +5V when there are none — where it used to take the Label tool and
    typing the name.

  • part.addMany: several parts into the design as one undo step, all or nothing.

  • The build guide in Turkish. The guide is what this application is for — the thing a
    person follows with an iron in their hand — and it was English in a Turkish window: every
    step, every check, the cut list, the parts list and the page they are printed from. It is
    now written in the window's language: "D1 1N4007 — I32 → E32, 4 delik ara", "Katot —
    bantlı uç — I32 deliğine", "K32 ↔ L32 açık devre olmalı", down to the column headings of
    the CSVs and the progress counter on the page. The panel, the exported files and a saved
    project's outputs agree, --headless --lang tr writes it too, and an agent asks for it
    with language on generate_guide. English is unchanged, word for word. The
    translation lives in the engine (phrasebook.py) because the sentences are built from
    the board rather than looked up, and a test builds every board in the repository in both
    languages and searches the Turkish for English left in it. Part names are said the same
    way in the Parts panel and the guide, from one table.

  • Route ▸ Crossings: routing with no wire at all. The engine could always be told what
    a solder trace does where it meets something it may not cross, and nothing let anybody
    tell it. A short jumper over the crossing stays the default; "one wire for the whole
    connection" and "never use wire" are the other two. The last uses solder traces only,
    whatever connection is preferred, and names each connection a trace cannot make rather
    than laying wire nobody asked for. It is remembered with the other routing choices, and
    an agent asks for the same thing with crossings on autoroute and reroute.

  • File ▸ Open Example. The examples ship with the application and nothing in the
    window pointed at them. They open as new, untitled boards — the file lives inside the
    application, so saving over it would change the example for everybody — and the empty
    board's hint mentions them when there are any.

  • Wires laid along the grid. Every wire the router laid was one straight run between
    its two ends, so a routed board was a cat's cradle of diagonals crossing each other, and
    a straight bare wire could pass 0.2 mm from another net's pad. Route ▸ Lay Wires Along
    the Grid
    (on by default) runs them square to the rows and columns of holes with as few
    bends as the board allows, keeps a bare wire off every pin, joint and run of copper, and
    keeps wires from lying on top of one another. On the shipped examples, stripped and
    routed again: no diagonal wire, nothing unrouted, no short, no DRC error, for about a
    fifth more wire. The engine's default stays straight, so every golden route is
    unchanged; RouterOptions.wire_path chooses. The MCP server's autoroute and reroute
    take the same choice as wires and default to the grid too, so an agent's board comes
    out like a person's.

  • DRC: wire-over-joint. A bare wire or a bent lead lying across a pin of another net,
    or across the soldered end of an insulated wire, is a short at the bench that neither the
    netlist nor LVS can see, because a wire is joined only at its two ends. The router already
    refused to lay one; a wire drawn by hand, or a part moved under an existing one, now gets
    an error. Six of the fifteen golden fixtures carry one, which the original engine never
    checked, so the rule joins PYTHON_ONLY_RULES and is pinned there.

Changed

  • A netlist is imported into the design, not onto whatever board is open. Its parts
    used to land on the board after a question, beside what they connect to — skipping the
    choice of board and leaving a first-pass placement nobody would keep. They now arrive in
    the design with their values and pin names, the sheet comes forward, and Choose a Board
    and Place on the Board take them from there.

  • Add a Part offers the catalog first, from the Parts panel's own tree: typing 555
    finds the NE555 and gives it its value, its pin names and U as its letter, where it used
    to list sixty-one packages with a crystal first and Y1 in the reference box. A value that
    names a catalog part in the chosen package brings its pinout too.

  • The board's tools are on the toolbar only while the board is in front. With the
    sheet in front the bar offered solder-trace and jumper tools, and a Rotate and a Mirror
    that acted on the board's selection beside the sheet's own.

  • A net is classed by its name until somebody picks a class: GND typed in New Net, a
    pin labelled GND, or N3 renamed GND is a ground net and drawn as ground symbols, where it
    used to be a signal wired to every part.

  • References on the board sit over their part, on a plate, instead of at the corner of
    the courtyard across the next row's pads -- and over every part, so a crowded board no
    longer prints "X12" as "X1" with the next part across the rest.

  • The schematic keeps its lanes out of the text. Every row has room above for its
    references and below for its values, so no trunk runs through "NE555" or "10nF"; net
    names are placed clear of each other and of the parts' text; every power glyph says which
    supply it is; and the panel draws text at the size the layout left room for, re-fitting
    the sheet as the panel finds its size until somebody zooms (it used to fit once, while
    the panel was still small, and stay there).

  • What just happened is said in the window's language. A command describes itself in
    English -- that sentence is the undo label, the journal line and what an agent reads -- so
    a Turkish window's status bar said "Add J1 PWR to the schematic" and its Edit menu "Geri
    Al Place and arrange 8 part(s)". Every sentence the commands, the planners and the
    window's own labels produce is now recognised where it is shown and said in Turkish
    ("Şemaya J1 PWR ekle", "7/7 nette 14 bağlantı yönlendirildi"), a conductor in the guide's
    own words. A test runs a session of nearly every command and fails on any sentence that
    comes back untranslated.

  • Qt's own buttons speak the window's language: OK, Cancel, Close, Yes and No are
    Tamam, İptal, Kapat, Evet and Hayır in Turkish.

  • The example boards are routed the way Ctrl+R routes, and the README shows the
    application as it is.
    tools/build_examples.py routed with the engine's default, which
    lays each wire as one straight run because that is what every golden route records — so
    the first boards a stranger opened from File ▸ Open Example were the cat's cradle of
    diagonal wire the window itself stopped producing when wires were laid along the grid.
    All six are rebuilt with the window's settings and the current router: no diagonal wire,
    every net closed, no DRC error, LVS clean. Three carry one R5' warning each, a trace laid
    beside another net's pin, which the guide turns into a check. The README's pictures and
    the assembly animation are regenerated from them, so they show wires along the grid and
    solder drawn as solder.

  • The potentiometer and the relay look like the parts in the bag in 3D. Neither has a
    borrowed model, and the generated bodies were a blue disc with a peg on it and a plain
    box. The potentiometer now has its steel cover crimped over the housing, a threaded
    bushing and a knurled shaft with a screwdriver slot; the relay a case with rounded
    corners standing on its feet. Both carry their value in print, as the real ones do —
    across the relay's top, stamped into the potentiometer's cover.

  • Solder and wire look like solder and wire in 3D. A run of solder was a round grey
    tube with bulges, a wire's end a ball on its pad, an insulated wire a coloured pipe with a
    mitre at every corner, and the leads came out of bare rings as if nothing had been
    soldered. A run is now a low bright ridge swelling into a dome on each pad; every lead on
    the solder side and both ends of every wire sit in a cone of solder, with the trimmed
    lead standing proud of it; an insulated wire is stripped at each end over its tinned
    core; and wire is bent round its corners and down into its holes.

  • Solder traces go straight instead of in staircases. Nothing in the search priced a
    bend, so a trace that stepped one hole across and one hole down, over and over, cost
    exactly what an L of the same length did — 77 one-hole steps in the solder-first routing
    of the examples. In the Solder and Lead-bend styles a bend now costs a hole's worth, and
    a hop over an obstacle 16 rather than 10, so straightening does not simply buy jumpers.
    Over the examples and the NE555, solder style: bends 273 → 186, trace length 1306 → 1335
    holes, jumpers 46 → 49, nothing left unrouted; lead-bend: bends 238 → 142, length 1127 →
    1024 holes, jumpers 46 → 41. Lead-bend also puts solder before wire as it always said it
    did, where it had left the two to the cost table. Balanced routing, and every golden
    route, are unchanged.

  • Solder-first routing lays far fewer jumpers. The planner routed ground and power
    first, and for a builder committed to solder that rail is a wall: a trace cannot cross a
    trace, so every signal beyond it hopped over with a piece of wire. A builder who chose
    the Solder or Lead-bend style — or allows no wire at all — is now also given the plans
    two other net orders produce, and the one cheapest to build wins. Over the examples and
    fixtures, 60 jumpers become 51 (NE555 4 → 1, lpb1-booster 5 → 2), and with no wire
    allowed 34 connections left for the builder become 27. No board comes out worse;
    Balanced and Wire routing, and auto-place, are exactly as they were.

  • Every export asks where it goes. The build guide, the schematic, the 1:1 PDF and
    the 3D snapshot wrote fixed names beside the board — or into whatever folder the
    application started in, for a board never saved — and replaced what was there without
    a word, behind menu items whose "…" promised a question. Each now opens the save
    dialog beside the board with the name it used to write, so accepting it does what the
    menu always did; its companion files go beside the one chosen, and a board with no
    file yet starts where the last export went.

  • The findings panel says what is wrong in words. A rule's row read
    "component-body-overlap (error)"; it reads "Parts overlap (error)" now — translated —
    with the rule's id in the tooltip and still matched by the filter, and LVS's kinds
    likewise. A test reads every rule id out of drc.py so a new one cannot appear untitled.
    The DRC, LVS and to-route counts on the status bar open the panel that explains them,
    and the DRC count no longer carries its run time, which is in its tooltip.

  • Routing takes a third less time, and auto-place two fifths less, with every route
    and placement exactly what it was. The trace searches' open list is a heap that pops exactly
    the node the original's linear scan did (lowest f, then earliest pushed); a document's
    physical nets are remembered for the last one asked about; the autorouter asks for the
    ratsnest of the one net it is routing instead of the whole board's, after every
    connection it lays; and whether a trace may pass a hole is worked out once per route.
    The placer no longer scores the pairs of parts nowhere near each other, whose every term
    is exactly zero — three million evaluations per placement of a large board. Measured on
    one machine against the code before the first of these changes: atmega328-relay autoroute
    1.75 → 1.16 s and auto-place 16.4 → 9.4 s, nano-relay 0.63 → 0.40 s and 6.1 → 3.8 s.

  • The Turkish interface has one word for routing. "Route" and "Trace" were both
    "Yol" — a menu and a drawing tool named alike — the Autoroute button read "Oto-yol"
    (a motorway), and the rest said "route et" half in English. Routing is now
    "yönlendirmek" and a trace a "lehim yolu" throughout, and the Turkish menu bar has a
    letter each: three menus shared Alt+Y and two shared Alt+Z. tests/test_i18n.py holds
    both.

Fixed

  • A reopened board no longer refuses its next part. Ids were counted back out of a
    loaded document for components, copper, cuts and nets only: a board saved with a part in
    its design refused the next Add a Part as a duplicate of "part-1", and every new board --
    which opens with its corner holes -- refused its first extra mounting hole. Every kind of
    generated id is counted now.

  • The build guide panel said where twice and showed neither. Each step read "R1 240 —
    J21 → J18, 3 holes apart" beside a Where column saying "J21 → J18, 3 holes apart" again,
    and in a dock a few hundred pixels wide what was left of it was "R1…" and a phase called
    "1. Lowes…". A step is now one line — the part or the connection first, its holes once —
    with phase headings in full, and the panel, its summary and the 3D caption speak the
    window's language: "GND · çıplak tel — C14 → B10" under "6. Lehim yüzü".

  • The Parts panel named its packages by their ids. Below "Transistors" and "Diodes"
    came "axial cylinder", "box film" and "to92", in both languages and in alphabetical order
    of the id, and every part under them was named in English in a Turkish window. The
    groups are named as the catalog's are — "Axial resistors and diodes", "Film capacitors"
    — in the order parts go on a board, and a part's name is shown in the window's language
    wherever the window shows one: "Direnç (eksenel, 3 delik açıklık)". The engine's names
    do not change (they are in the footprint golden); the panel recognises the engine's
    phrasings, and a test fails on a library part named in words it does not recognise.
    The filter finds a part by either name.

  • --headless --lang tr board.perf looked for a board called "tr". The window's
    command line was fixed for exactly this once, and the headless run kept its own copy of
    the old rule — every argument not starting with -- — so the language's value became
    the document. It reads the command line the way the window does now.

  • The build guide counted a wire's corners as pads. A wire laid along the grid keeps its
    bends in its path, and the step card called each one a pad — "bare wire · 25.4 mm · 4
    pads" for a wire soldered at two ends, which reads as two more joints to make on a wire
    that would short whatever it lay across there. A wire now has two pads however often it
    turns, and its corners are listed as its Path, which is what they are.

  • A ground symbol could be drawn inside a power symbol on the schematic. Where a
    ground rail from the row above and a power rail from the row below met at one point of a
    channel, the lane above went to whichever net's id sorted first — ground, usually — and
    the two glyphs, pointing at each other a lane apart and a column across, drew their bars
    into each other: stacked lines that read as a battery. It happened on five sheets,
    nano-relay among them. Power takes the upper lane now, so both point away, and a test
    holds every glyph on every sheet clear of every other.

  • A window a little too narrow for the toolbar lost every word on it. The toolbar
    needs 1536 px with its words in English and 1624 in Turkish, and the rule was all the
    words or none — so on a 1920 screen at 125 % a Turkish window dropped them all to be 90
    px short, and Trace, Spine, Bare and Insulated, which are one drawing differing only in
    what runs between the pads, became four identical pictures. The words now go one button
    at a time, the pictures everybody reads first (save, undo, delete), the views next to
    last and the conductor tools last of all: at 1536 px only six buttons lose theirs, and
    at 1280 px the four conductor tools still have their names.

  • Ten explanations were English in the Turkish window. The Draw menu's five tools and
    the five routing styles explain themselves on hover, and the tables they came from
    handed the text over as a variable, which the catalogue's check reads past. They are
    translated, and a new test builds the window in both languages and compares every menu
    tooltip somebody wrote, however it was built.

  • A part dragged onto the board had no shadow where it would land. Dragging from the
    Parts panel or the sheet showed only the list's icon, at a fixed size and centred on the
    pointer, while the drop puts the part's first pin in the hole under the pointer — so the
    picture was neither where the part landed nor its size. The board now draws the part at
    its real size and way round, in the holes it will take and red where it cannot go, and
    the picture travels beside the pointer instead of over it.

  • Pressing Autoroute or Auto-place a second time did nothing. Autoroute on a finished
    board said there was nothing to route, so changing the connection style or the crossings
    and pressing it again changed nothing; it now plans the board's routing again with the
    settings as they are — asking first, because it rips copper up — and says so when that
    comes out exactly as it is. Auto-place searched from the same seed every time and found
    the same answer; every press now searches from a new one, the seed is still shown with
    the result, and an unchanged result stays on the status line instead of timing out.

  • Moving a part on the schematic took every wire off the sheet. The first move fixes
    the sheet's layout so nothing else jumps, and a fixed sheet draws only the wires
    somebody drew — none — so every connection turned into a net label and read as cut,
    though the circuit was untouched. The layout's wires now come along as drawn wires in the
    same undo step, line for line with their dots, and a moved symbol drags the ends of its
    wires with it. A drawn net is named once over its longest run, as the laid-out sheet
    names it.

  • The schematic panel drew every label in the opposite corner. The drawing says
    which end of a label's text is at its point, and the SVG export reads it that way;
    the panel handed the same words to a text helper that aligns inside a box centred on
    the point, where they mean the reverse. So net names sat left of and below their
    point — across the wires the layout had kept clear of them, and off the sheet's left
    edge ("XTAL1" read "TAL1") — references hung under their symbols, values stood over
    them and pin numbers were on the wrong side of their leads. The panel and the export
    now put every label in the same place.

  • A net name with no room above its wire went back to where the wire branches. When
    no step along a run cleared the band above it, the name returned to the run's left-hand
    end — where the branches drop to the pins — with a wire through it. It now tries the
    band below the run, taken only with a clearance's worth of nothing under the text as
    well (so it cannot read as another run's name, or sit underlined on a rail glyph), and
    failing that stays above where the fewest wires cross it. On nano-relay BASE and
    LED_ON come clear; nothing else on the shipped boards moves.

  • The HC-49 crystal's footprint was a third of its length. It had the can as
    4.65 × 3.5 mm — its thickness along the leads and a height across them — where an
    HC-49/U is 11.05 × 4.65 mm on the board, so on atmega328-relay the crystal stood on
    one of U1's pins and DRC said nothing (the 3D view, drawing the real part, showed it).
    The footprint is the real size now, recorded as a divergence from the original engine's
    golden with a test of its geometry, and the example's crystal moved two holes clear
    with its four connections re-routed.

  • A 9 mm resistor (r-axial-6) was drawn with a 6.3 mm body in 3D, while KiCad's 9 mm
    model at its own pitch sat unused. It has that model now; r-axial-5 keeps the 9 mm one
    because KiCad draws nothing shorter at 12.70 mm, and a test holds every axial model's
    barrel to its footprint's length, that one exception named.

  • An electrolytic's minus stripe was shaded as dull khaki metal in 3D, because the
    model converter took its colour for the can's aluminium top. It is print on the sleeve
    now, in the colour the 2D view draws it; the minus signs KiCad prints down it keep
    theirs. A polarised borrowed part's mark is its biggest printed piece — a diode's band,
    a can's stripe — so the rule that fixed the diode's double band covers both.

  • Bare metal clipped to a flat white slab in 3D — a crystal can, a TO-220's tab —
    seen from the side. The render now ends in an exponential tone curve, exposed so the
    board's overall colour does not move; an HC-49 close-up went from 17 861 clipped
    pixels to none.

  • The window did not fit a laptop screen. The status bar's labels each insisted on
    their full width, which made the window's minimum wider than 1280 px; the toolbar with
    its words is wider still, so the view switches went behind the overflow arrow; and the
    Parts and Nets panels came up about 170 px wide, with no room for a net's name. The
    labels may now clip, the toolbar drops its words when they do not fit (every button
    keeps its name in its tooltip), and the side panels keep 240 px.

  • About thirty messages stayed English in the Turkish interface. They were built as
    f-strings, which the catalogue scan never reads — "Cannot place there", "Nothing to
    route", the unrouted-connections report, the auto-place confirmation, the export
    errors, the About box. They are translated now, the auto-place confirmation says what
    was done rather than "router cost 812 (seed 0)", and tests/test_i18n.py reads every
    argument that reaches the status bar or a message box for English no t() touches.

  • Selecting a net lit its ratsnest and nothing else. On a finished board there is
    no ratsnest left, so choosing GND in the Nets panel changed nothing anybody could see.
    The net's copper now glows in the ratsnest's highlight colour, on either face, judged by
    what the copper actually joins — a wire drawn by hand carries no net, and it lights up
    if it lands on the net's pins.

  • Exporting the build guide or saving a project froze the window. The 3D picture for
    every step was drawn behind a wait cursor, on the belief that it took under a second;
    atmega328-relay's 83 steps take about forty, and Windows calls a window that has not
    answered for five "Not Responding". A dialog now counts the pictures as they are drawn,
    and Skip the Pictures writes the guide without them — never with only some.
    Exporting the schematic, which takes seconds, shows a wait cursor instead of nothing,
    and the 3D panel redraws after the last edit of a burst rather than after every one
    (a third of a second each on atmega328-relay, so a held arrow key lagged behind).

  • Pressing Board on the toolbar closed the board. The Board, Schematic and 3D buttons,
    and Ctrl+1–4, were the panels' own open/close switches, lit while a panel was open
    rather than in front. The board and the sheet start stacked in one tab group, so both
    were lit, and pressing Board to get to the board put it away. They now bring their view
    forward and never close it; opening and closing a view is View ▸ Panels, where a check
    mark says which are open.

  • Copper on the far face was hard to follow in 2D. On the component side -- the side
    the editor opens on, and so most of the copper on most boards -- a run was drawn only as
    thin hatch strokes over a busy grid of pads, and zoomed out it thinned to nothing. A faint
    solid band now goes under the hatch, so the run reads as one length of copper while the
    hatch still says "on the other side".

  • The router could short two nets through a wire's soldered end. An insulated wire is
    soldered down at both ends, but it does not block the copper plane, so nothing marked
    those holes as taken. Where two insulated hops met on an empty pad, the next net's solder
    trace ran straight through it: routing atmega328-relay solder-first tied +5V to XTAL1 at
    L19. LVS reported the short and DRC did not. The router now treats every wire end like a
    pin — a trace may not run through one and a bare wire or a bent lead may not lie across
    one — and no golden route moves. The random-netlist property test, which only ever asked
    the default style (the one that never hops), now runs all four.

  • The 3D view cut the board off. The camera fitted a bounding sphere, zoomed in by a
    fixed 1.35 and then tilted, in a window it had not been given yet, so every portrait board
    lost its near edge off the bottom of the frame and its far edge off the top when flipped
    (atmega328-relay's corners landed at y −87…914 in a 950 px picture). It now fits what is
    drawn, after the tilt, to nine tenths of the window's own shape: the panel on its first
    real resize, the offscreen render and the guide's step images once their window has a
    size.

  • An LED's flat stood on its anode in 3D. KiCad numbers the cathode pin 1 and puts the
    flange's flat on it; this application numbers the anode pin 1. The three LED meshes are
    now converted turned end for end (tools/import_kicad_models.py, re-run against KiCad
    10), and tests/test_model_leads.py measures which side the flat is on.

  • Diodes had two cathode bands in 3D: the borrowed mesh's own, and one more printed at
    the very end of the barrel. The mesh's band is kept, where the real part has it, and
    painted in the colour the 2D view draws it.

  • The underside legend labelled the wrong columns in 3D. It was reflected about the
    hole span on top of the camera turning the board over, so it was mirrored twice: column A
    carried "AH". Each glyph is now reflected about its own centre and stays under the hole
    it names — and a one-edge legend stays on the same physical edge as in the 2D view.

  • Pin headers had nothing on the solder side in 3D. The borrowed pin is cut at the
    board surface like every model, and the part of it that goes through the board was the
    one thing a header did not get.

  • R did nothing while the schematic panel was open and a part was selected on the
    board.
    The board's Rotate and the sheet's Turn were both shortcuts on R, and Qt answers
    two live shortcuts on one key by firing neither. R now belongs to the board window-wide,
    and the sheet takes it back while it has the keyboard — R and Shift+R turn its selected
    symbols either way.

  • A board opened after a blank launch came up as its schematic. The blank launch puts
    the sheet in front, the session remembered it, and nothing put the board back. A board
    with parts on it now opens on the board; a design with nothing placed opens on the sheet.

  • Auto-place said "~-64 mm less connection length" when the estimate had grown. The
    winner is chosen on what the board costs to build, not on straight-line length, so the
    estimate can rise; it is now worded by its sign.

  • Re-route asked with Yes under Enter, and it is the one question that rips up
    copper. It goes through the same box as every other destructive question now — Cancel
    under Enter, the verb on the button — and its text is translated.

  • The empty board's hint gave the Parts panel's key for the Schematic (Ctrl+5, not
    Ctrl+2), and a test now holds the hint to the action's real shortcut.

  • A header's pin marks had an invisible outline in 2D: "#00000060" is read by Qt as
    #AARRGGBB, a fully transparent blue. A test now refuses an eight-digit colour string
    anywhere in the interface.

  • The Undo button grew with the command it named — "Undo Autoroute 7 nets (14
    connections)" — and pushed every button after it along the toolbar. The button keeps
    its one word; the menu and the tooltip still name the command.

  • Borrowed parts were shaded in flat facets — an LED's dome, a can's side — because the
    meshes carry no normals. They are computed once per mesh when it is read, with a feature
    angle that keeps real edges sharp.


Windows - PerfboardStudio_..._Setup.exe. It is unsigned, so SmartScreen will
warn on first run: More info -> Run anyway. A signed build is planned.

Linux - perfboard-studio-...-x86_64.AppImage. chmod +x and run it; it needs
glibc 2.35 or newer. If it will not start, the machine has no FUSE 2: install
libfuse2, or run the file with --appimage-extract-and-run, which unpacks it
instead of mounting it.

macOS - perfboard-studio-...-arm64.dmg, Apple silicon. Drag it to Applications,
then run this before opening it:

xattr -dr com.apple.quarantine /Applications/PerfboardStudio.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first launch.
On macOS 15 and later the dialog it raises defaults to Move to Trash, which
deletes the install. Clearing the flag first avoids the dialog entirely. On an
Intel Mac, install from source.

Anywhere with Python 3.12+: pip install -U perfboard-studio, then perfboard-studio.
Same release, no installer and no SmartScreen warning; Qt and VTK are most of a
400 MB download the first time.

Checking your download - SHA256SUMS carries the SHA-256 of all three
files. sha256sum -c SHA256SUMS on Linux, shasum -a 256 -c SHA256SUMS on
macOS, Get-FileHash <file> in PowerShell. Perfboard Studio does this for you when it
fetches an update itself: Help - Check for Updates. It proves the file
arrived intact, not who built it - that second claim needs the code signing
this project has not bought yet.

This is pre-alpha software. Nobody has yet built a real board by following a
generated guide - check every measurement against the part in your hand.