Repository navigation
Perfboard Studio v0.11.0
Changed
-
The application is called Perfboard Studio; its package and command are
perfboard-studioand its Python module isperfboard_studio. It was PerfStudio
through ten releases. Two things were wrong with that name, and the second is the one
that decided it. The first is how it reads: one letter separates "perf" from "perv",
and not every eye lands on the right one. The second is what the abbreviation already
means in software — "perf" is performance. It is the name of Linux's profiler, and
AMD shipped a graphics profiler called GPU PerfStudio for years. PLAN.md §12 picked
"perf" for open-source discoverability, and discoverability is exactly what it cost:
a search for "perfstudio" lands among profiling tools, not among perfboard.The identifier is
perfboard-studiorather than the bareperfboard, and that is
a second decision rather than a longer spelling of the first.perfboardis the name
of the board, not of this program; taking the generic word as a package name confuses
the product with the material it works on. It is also a word this repository writes in
prose on nearly every page, which would have made the name impossible to change again
without reading every line by hand.What it means for an existing install:
uv tool install perfboard-studio, orpipx install perfboard-studio. The old
perfstudiodistribution is not renamed in place — PyPI has no such operation — so
it stays where it is and stops at v0.10.0.- The command is
perfboard-studioand the MCP server isperfboard-studio-mcp. A
client configured withpython -m perfstudio.mcphas to be pointed at
python -m perfboard_studio.mcp. - The environment variables are
PERFBOARD_STUDIO_LANG,PERFBOARD_STUDIO_DOCUMENT,
PERFBOARD_STUDIO_LOGand the threePERFBOARD_STUDIO_BLESS_*. - The window's stored settings are keyed on the application name, so the first start
after upgrading comes up with the default layout, the default board colour and an
empty recent-files list. Nothing on disk is lost; the old keys are simply not read.
The document format did not move.
DOCUMENT_FORMAT_VERSIONis still 1, the
extension is still.perf— it was always short for perfboard, never for the
application — and every golden fixture intools/diffcheck/golden/is byte-identical
across the rename. A board written by v0.10.0 opens in this build unchanged, and the
differential proof against the TypeScript engine is untouched. -
The board is the board people actually buy, checked against a photograph of one.
Somebody held a 5 × 7 cm double-sided board up against the screen and asked whether it
matched. The geometry did — 70 × 50 mm, the grid, the four corner screw holes in the
border, the finger strips down both short edges — and two things did not:- The pads are TINNED, not gold.
_GOLDclaimed to be "what the green, blue and
black prototyping boards are finished with", and on the photograph they are plainly
not: the pads there are neutral and light, at a blue/red ratio of about 1.0 where
gold's is 0.4. It is HASL — hot-air levelled solder over copper._TINNEDreplaces it
for every masked scheme, kept warmer and lighter than the grey a solder run is drawn
in, because those two are nearly the same metal on a real board and telling them apart
is what this application is for. The 2D render signatures were re-blessed after
reading the diff: ink coverage identical to three decimals, every cell moved about ten
levels — a colour change and not a geometry one, which is exactly what it should be. - The legend was taken off the green board in the same pass and put straight back:
the 400-pixel product shot resolved no printing, a photograph of the same product at a
usable size shows the column letters plainly along the border under the bottom row,
beside the part number and "5X7CM". A thumbnail too small to resolve a print is
evidence that the print cannot be SEEN, not that the board has none — which is now
written atboard_from_presetand in the test, because the same mistake is available
to anyone reading the same photograph.
- The pads are TINNED, not gold.
-
The corner screw hole is AT the corner position, centred, and the pad it replaces is
gone. It used to be pushed as far diagonally into the printed border as it would fit,
to spare that pad — and one offset was used for both axes, so on a 5 x 7, whose two
borders are 3.41 and 5.79 mm, the bore ended up 0.20 mm from one edge of the board and
floating 2.6 mm from the other, with the corner pad jammed against it. Nothing was
centred in anything, and it is the corner somebody looking at the board looks at first.- The board is manufactured with a screw hole at that position INSTEAD of a pad, so the
pad is consumed — throughconsumed_holes, which DRC, both renderers and the guide
already read, with no special case for corners. - A board whose border is too thin to hold a screw steps the hole one position inward
rather than drilling a notch out of its own edge:CORNER_HOLE_WEB_MMis 1 mm of board
and the corner position leaves 0.31 mm of it on the 20 x 30 and 0.88 mm on the 7 x 9.
Both are now measured by a test rather than left to the eye. It also means every
double-sided preset gets its four screw holes; the six largest boards used to get none
at all, because the old arithmetic gave up and returned zero. - The finger strip already trimmed itself clear of the screws, but by taking the run
from the first clear finger to the last — which assumed the obstruction is always at an
END. An inset screw obstructs one in the middle, and first-to-last quietly put it back;
it takes the longest contiguous clear run now.
- The board is manufactured with a screw hole at that position INSTEAD of a pad, so the
-
A mounting bore takes the copper it is drilled through, including a finger. A hole
put one position in from the corner — which is what Board Features offers by default —
reaches the edge-connector strip, and the finger it went through was still drawn intact
in both views: a 3.2 mm hole sitting on top of a contact the board no longer has. A bore
does not distinguish between the two shapes of copper it destroys, so
geometry.surviving_finger_holesanswers that for both renderers the way
consumed_holesalready answered it for a round pad. -
The printed legend is not printed into a hole. Ink goes ON the substrate and a bore
takes the substrate away, so a letter under one is not faint, it is absent — which is
what a real board shows.geometry.printed_label_is_clearis asked by the 2D legend and
the 3D one, in the same units, because a legend that agreed with the board in one view
and not the other is the disagreement this pair keeps producing. -
The board in 3D has holes in it. Which sounds like a statement of the obvious and
was not true: the substrate was one solid cube and every hole on it was faked by laying
a dark cylinder over the top. A dark disc on green reads as a mark printed on the board,
not as something you can push a lead through — and it was worst on a mounting bore,
which has no copper ring around it to explain the darkness, so a screw hole read as a
sticker. The plate is punched now: both faces, every hole, and the wall of the bore
visible inside it.- Affordably, which is why the fake existed. A boolean subtraction per hole is
nearly two thousand of them on a 945-hole board, and that argument still holds. This
is neither: a face is ONE TILE — a pitch square with its hole taken out — glyphed at
every hole, so both faces of any board cost one source and two actors. The tiles are
exactly a pitch across, so neighbours share an edge and the surface is watertight;
what is left between the outermost tiles and the board's edge is the printed border,
drawn as rectangles, and a bore in it is cut out of them. - A mounting bore takes whole tiles and lays one patch over them, and the tiles it
takes are exactly the holesgeometry.consumed_holesreports the copper gone from —
one bore, one answer, in the renderer and in DRC. It was also being drawn at its hole
ADDRESS rather than atmounting_hole_centre_mm, so every corner hole appeared back
on the grid in the middle of four pads that are perfectly intact. CLAUDE.md has said
not to do that since the feature was written. - An edge-connector finger keeps a solid tile: it is a contact with no bore, and
drilling through it is what made a finger look like a long pad.
- Affordably, which is why the fake existed. A boolean subtraction per hole is
-
The parts look like the parts. Every model was looked at on its own, close up, which
is how most of these were found: at the whole-board zoom a wrong shape reads as a small
coloured blob and nothing more.- A resistor was a flat-ended tube; it has the moulded shoulder at each end now,
with the barrel shortened by what the domes add back so the part still measures the
length its footprint says it does. - A film capacitor was a plain orange brick. The dipped case is a slab with
half-round ends, so it is one now. - A disc ceramic was a coin with a sharp rim; the dipped case is a lens, thicker in
the middle, and that is the same one solid. - A TO-92 was a squashed cylinder — which is an ellipse, so it has two flats and
marks nothing. It is the D-shaped case now, with the flat facing its row of pins, and
getting a transistor round the wrong way is the classic way to spend an evening. - A TO-220's tab was a slab the width of the package sitting on top of it. The tab
is sheet metal the plastic is moulded around: thin, flush with the back face, and with
the bolt hole through it — the side a heatsink goes on. - An HC-49 crystal was a flat-topped tube. It has the pressed cap and the welded lip
at its base, which is also what says which way up it goes. - A DIP's pins are flat blades rather than round wire, it has the semicircular notch
at the pin-1 end as well as the dot, and the dot is a dimple rather than a headlamp:
at a tenth of the package in the accent colour it read as a component of its own. - An electrolytic gained the crimped rim and the vent scored into its top. It had
been given a WHITE top — and two of them on a board then read as screw heads, which is
exactly what the first person to see it called them.
- A resistor was a flat-ended tube; it has the moulded shoulder at each end now,
-
The 2D and the 3D board draw the same board. Three things had them disagreeing, and
all three were the 2D view or the renderer being wrong rather than a difference of
opinion:- A corner mounting hole was drawn on the grid in 2D.
MountingHoleItemread the
hole ADDRESS and ignored the offset — the very thing CLAUDE.md says to go through
mounting_hole_centre_mmfor — so a bore that sits in the printed border was drawn a
whole pad's width away from where 3D and DRC both put it.view2d.mm_to_screenis the
mapping it needed: the same reflectionhole_to_screenapplies, for the things that
are not on the grid. - A bore in the border blinded the hole beside it. The patch of plate laid over a
mounting bore takes whole tiles, and it was grown OUT to tile edges — so a corner bore
that touches no tile at all swallowed its neighbour, leaving a pad drawn over solid
board with no hole through it. The patch now takes the tiles the bore actually reaches
and, for whatever of it lies outside the tiled grid, a rectangle clipped so it cannot
reach a tile the bore never touched. - The copper was clipping. VTK's default specular POWER is 1.0, which is not a
highlight — it adds the specular term flat across the whole surface. A pad carrying
0.4 of it rendered a fifth brighter than its own colour and blew out: measured at
(255, 255, 125) against the#c8a951the 2D view paints from the same table, which
also flattened the shading off the metal and made the board a grid of flat yellow
rings.COPPER_SPECULAR_POWERis on the pads, the strips and the fingers now, and
test_the_board_s_copper_is_never_blown_outrenders a board and counts the clipped
pixels.
- A corner mounting hole was drawn on the grid in 2D.
-
A part is now the height its footprint says it is, and that is measured. VTK applies
an actor's scale BEFORE its orientation, and the turn that stands a cylinder up maps the
source's own z onto world y — so a scale written to flatten the width of an upright can
flattened its LENGTH instead. The crystal came out a quarter short with its cap floating
in the air above it, and the render looked like a part, just not that part.
test_every_part_stands_exactly_as_tall_as_its_footprint_saysmeasures every archetype
against the number the height-limit rule works from. -
No mesh library, and that is still the decision. The only comprehensive set of
through-hole models is KiCad's, under CC-BY-SA-4.0: a share-alike licence in a
repository that is Apache-2.0, for assets that would then have to be shipped, versioned
and kept in step with the footprints. PLAN.md D6 declined it and the reason has not
changed — and there is a second one now, which is thatbodies.pyderives the 2D
footprint and the 3D solid from the sameBodySpec, so a mesh would be a third
description of a part free to disagree with both. -
The repository page describes the project, and says where everything is. Three
things had drifted apart from what is actually here.- GitHub's language bar read 14% TypeScript, HTML, NSIS and JavaScript. All of it
came from things nobody installs:packages/(the retired reference engine),
prototypes/(the Qt spikeui/came out of),tools/bench-3d(the M0 WebGL
harness for the Tauri architecture that was not taken) and the golden fixtures. They
arelinguist-vendoredin.gitattributesnow — notlinguist-generated, which
would collapse them in a diff, and a readable golden diff is the point of the
goldens. Only the statistics change. - Both READMEs now say where the rest of the documentation is. They linked
PLAN.mdfour times without mentioning it is written in Turkish, and never linked
the changelog, the security policy or the code of conduct at all. One table names all
nine, with what each is for; a nav line under the status callout links to the install
commands, which were 110 lines down the page. prototypes/andtools/bench-3dsay what they are. A visitor clicking into
either found a working application built on an architecture this project does not
use, with nothing to say it was a spike.
- GitHub's language bar read 14% TypeScript, HTML, NSIS and JavaScript. All of it
-
Both READMEs open the way
cycloidgen's does, because they came from one hand.
The other Medinstech desktop application had already settled a house style and this one
had never been held next to it: the Medinstech wordmark linking to the site, with a
light and a dark variant behind a<picture>; a centred title, one-line positioning
and a centred description; fourflat-squareshields in the brand's#0d00ff, two of
them live (the workflow's status, the latest release) rather than hand-written text
that goes stale; a centred row of jumps with the two that matter in bold; then the
screenshot and a caption saying what to look at in it. The old## What it doesis
gone — its paragraph IS the centred description now and its three numbered points are
six bullets, which is the shape the other page states its case in.- A
Jump toline under them, and in the Turkish file one heading needed an
explicit ASCII anchor: GitHub lowercasesİtoiplus a COMBINING DOT ABOVE, so
the automatic anchor forİki yüzholds a character nobody can see and nobody would
type correctly.
- A
-
The card GitHub unfurls a link into is the product. It was the generic
auto-generated one.tools/make_social_preview.pydraws
docs/images/social-preview.pngfromui/theme.py's colours and the editor
screenshot, so it cannot become a picture of an older version of either. It is
committed even though GitHub will not take it from a commit — there is no API to set a
social preview — because "which image is up there?" should still be answerable from the
tree. -
The installers come first, and
pip installis no longer the headline. It is a
desktop application and the three platform builds — none of which had been downloaded
once — were a paragraph below apip install perfboard-studiothat does not work for most of
the people this is for: an Intel Mac's system Python is 3.9, and Debian, Ubuntu and
Fedora refuse a system-widepip installoutright under PEP 668. The releases table
is the first thing under Running it now, and PyPI is a subsection under it, spelled
uv tool install/pipx install— an application in an environment of its own, which
is the shape of thing this is. It is still the right answer for the two platforms with
no build, an Intel Mac and Linux on ARM, and that is what the section says it is for.
Fixed
-
Every lead now goes down its hole and out the other side. A part in the 3D view
hovered over the holes it is meant to be soldered into: an axial lead ran horizontally
to the pin position and stopped there in mid-air, a radial capacitor had no legs at all,
and a DIP had no pins — it was a black block resting on the board. The lead turns down
at the pin, disappears into the hole (the bore is opaque, as a board is) and reappears
trimmed on the solder side, which is the only evidence that face has that anything came
through it. Every archetype, because the omission was per-builder, and a DIP's pins run
down the OUTSIDE of its two long sides the way the real package's do.- One instanced actor per component, not one per pin: a 2×20 header has forty, and the
pad grid is instanced for exactly this reason. _upright_cylinderhands the glyph its polydata rather than a pipeline connection.
A connection holds a RAW pointer back to the source that produced it, and the source
there is a local — connecting one segfaults the interpreter outright, which is how
this was found rather than reasoned about.
- One instanced actor per component, not one per pin: a 2×20 header has forty, and the
-
The holes read as holes rather than as buttons. The dark bore stood 0.10 mm PROUD of
the copper at both faces, so at any grazing angle — which is most of them, since this
view is orbited — every hole showed a black cap above its own pad, occluding the pads on
the rows behind it. A board of 700 holes read as a grid of black buttons. The bore now
stops just short of the copper on both faces (BORE_UNDER_PAD_MM), so the ring is the
topmost thing at every hole. Mounting bores use the same span, because a screw hole
drawn to a different depth from the grid around it reads as a different kind of thing.pad_zand the newbore_span_zare pure and asserted directly, as the pad plane
already was: what a photograph shows and no test could see is exactly what needs one.
-
pnpm buildworks from a clean clone. The roottsconfig.jsonthattsc -blooks
for was never committed, so the command failed withTS6053: File 'tsconfig.json' not found— andpnpm build && node tools/diffcheck/generate.mjsis the documented way to
regenerate the golden fixtures this Python engine is proved byte-for-byte against. The
solution file compiles nothing itself; it names the four packages, in whatever order
their ownreferencesimply.tools/bench-3dis deliberately not among them: it is
composite: falseandnoEmit, which a solution build cannot reference. Verified by
regenerating every fixture and finding the tree unchanged. -
The MCP recipe in both READMEs was two unrelated halves.
pip install -e ".[mcp]"
installs a clone in editable mode;uvx --from "perfboard-studio[mcp]"fetches from PyPI and
ignores it entirely. Printed as consecutive lines of one block they read as a two-step
setup where either alone is the whole thing — which is whatdocs/MCP.mdsays, and now
what the READMEs say too. -
Three stale numbers. Both READMEs announced v0.9.0 as the current release with
v0.10.0 on PyPI and the releases page, and counted "~2070 tests" against 2084.
PLAN.md's front matter still read "planlama tamamlandı, geliştirme başlamadı" and
called the name temporary, and its §11 put the MCP tool count at 50 where §13 and
docs/MCP.mdsay 51 —test_mcp.pyreads §13's capitalised spelling, so §11's escaped
the check written for exactly this. -
A net name is no longer drawn on the wire it names.
Label.atfor a net was the
trunk's y and the trunk's left end, so the baseline WAS the wire: the line ran through
every descender and the branch dropping into that end ran up through the first letter.
Measured across every board in the repository, 41 of 41 net names were drawn on a
wire — VIN, VOUT and ADJ on the LM317 sheet, OUT, THRESH, DISCH and CTRL on the NE555
one. References, values and pin numbers were already clear; this was the whole defect
and it was in every sheet the tool has ever produced.- The name now sits in a band above the run, and it is the BAND that carries the
guarantee rather than any search:NET_LABEL_CLEARANCE_MM + NET_LABEL_MMis shorter
thanTRACK_PITCH_MM, so it cannot reach the lane above, and its own trunk is below
it. No horizontal run can be in it — which is the reading that matters, since text and
a horizontal wire lie ALONG each other rather than crossing, and a line drawn through
a word is the one overlap that makes it unreadable. Same shape of rule as
RAIL_GLYPH_MM's, and cheap for the same reason. - What is left is vertical branches, and
_net_label_atsearches for those: it starts
at the left-hand end of the run, where a reader looks for the name, and only slides
RIGHT ALONG THE SAME RUN in half grid squares when the band there is occupied — never
to another wire, never off the run. Twenty-nine of the 41 do not move at all. Three
end up crossed by another net, and two by a branch of the net they name, which is not
ambiguous about anything and is left alone. - The size is one fact with two consumers. The layout has to know how much room a
name takes in order to keep a wire out of it, and only a renderer knows the real size,
soSheetInk.net_mmreadsschematic.NET_LABEL_MMrather than naming its own — a
sheet that drew the name at some other size would be filling a gap that was measured
for a different piece of text. The panel draws text at a fixed PIXEL size on purpose
and treats this as the nominal it was always laid out against. - The two schematic goldens were re-blessed after reading the diff, and the diff is the
argument: every net name moved up 0.89 mm and right 0.76 mm, two of them further along
their own trunk, and not one symbol, wire, rail or junction moved at all.
- The name now sits in a band above the run, and it is the BAND that carries the
-
A bless run on Windows no longer writes CRLF into the goldens.
test_guide_golden
passednewline="\n"and the other three golden writers did not, so re-blessing a
schematic, an exported sheet or a render signature from this platform rewrote the line
endings of a file every other platform leaves alone..gitattributesnormalises it away
at commit, which is exactly why it went unnoticed. -
A BFS layer is a hint about distance, and the sheet was treating it as a constraint.
Everything one hop from the root went into one column. Onlm317-supplyten of the
eleven parts hang directly off U1, so the sheet came out 129 x 239 mm — nearly twice
as tall as it was wide, on a circuit that fits across a page with room to spare. Nothing
was being respected by that arrangement: a schematic has no precedence order, so a part
moved one column further out only makes its own wire span one more channel._split_tall_layerscaps a layer at about the side of a square —
max(3, ceil(sqrt(n))), the same arithmetic the loose-parts block already used — and
chunks the overflow into columns of its own, consecutively in the reference order the
layer already carries, so R1 stays beside R2.- It fires only where a layer was genuinely over-full. The fourteen random fixtures are
untouched; the five real circuits went from a worst aspect of 1.84 to 0.92. LM317 is
now 185 x 132 mm and the NE555 went from 4 columns by 7 rows to 5 by 4, which reads
left to right the way a schematic should. - The goldens were re-blessed after checking that every element count is identical —
same symbols, wires, rails, junctions and labels, only positions moved. test_the_pdf_is_a_pdf_and_takes_its_orientation_from_the_sheetbuilt its tall case
fromlm317-supplyand its guard fired, correctly, which is why the guard was there.
It builds both sheets fromSchematicDrawing(width=..., height=...)now: what is under
test issvg_to_pdfreading a shape and picking a page, so the shape is the input and
no future layout change can take the tall case away again.
-
A pin no net reaches is marked, so "not connected" reads differently from "not drawn".
An unwired pin was a plain lead stopping in mid-air — which is also exactly what a pin
whose wire the sheet failed to draw would look like, and a reader could not tell them
apart. Ten of the eleven parts onarduino-io-shieldhave one; the NE555's is U1 pin 4.- A statement of fact, not of intent, and that is the difference from KiCad's marker
of the same shape. There somebody places one to say "I meant to leave this open"; here
nothing is placed by hand, so what this can honestly say is that the netlist does not
mention the pin. - Not a defect and no note. Unused pins on a header are the ordinary case, and a
sheet that added ten notes for one connector would bury the note that matters. LVS is
where a connection that was supposed to exist gets reported. Drawn in the dim ink for
the same reason. schematic.no_connect_armsowns the two strokes, the same shape asrail_glyph_bars:
the panel and the exported sheet ask rather than each deciding.NO_CONNECT_MMis
small enough that the cross cannot reach the body or the pin next door, which is
asserted rather than eyeballed.- The golden text dump gained a
no-connectline. A thing that is generated and not
dumped is a thing that can quietly stop being generated, which is what the goldens are
for; the NE555 diff was exactly one line.
- A statement of fact, not of intent, and that is the difference from KiCad's marker
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.