Releases: medinstech/perfstudio
Release list
PerfStudio v0.6.0
Added
- A pin on an edge-connector finger is a DRC error (
edge-connector-conflict), the
third member of the family withmounting-hole-conflictandcut-track-conflictand
the most absolute of the three: a bore and a cut each destroy the pad and leave a hole,
but a finger is solid copper that was never drilled (geometry.undrilled_holes), so a
through-hole lead cannot be fitted there at all. Nothing checked it, so a part dropped on
the finger strip was accepted in silence — and the finger strip runs along the board
edge, which is exactly where a connector or a terminal block gets placed.
Changed
-
The placement ghost goes red over a hole with no pad. A mounting bore, an
edge-connector finger and a cut track all leave a position on the grid with nothing to
solder a lead into, and DRC has always called each of them an error — but the ghost
stayed green right up to the click, so the part went down and the only thing that ever
said so was a line in the DRC panel afterwards. The status line now names it too. The
placement is still not refused: a mounting hole can be added over a part that was
already there, so refusing would only make the same board harder to reach while leaving
it reachable. -
The blank-board guidance appears until you have placed your first part, then stops.
It is for the first launch; repeating it on every launch afterwards is the application
explaining its own front door to somebody who has been through it a hundred times — and
there was nowhere to click it away, because the block is transparent to the mouse by
design. -
Two solder traces running side by side are one row in the findings panel, not
sixteen.solder-trace-proximityfires once per pad per trace, so eight pads of
perfectly ordinary parallel routing put sixteen copies of one sentence in the panel and
pushed everything else off the bottom — which is how the most valuable rule in the tool
became the one people scroll past. Gathered by the run now (C4–J4 · 8 pads), with
every pad still there one level down.- The rule itself is unchanged, and deliberately: it is a warning, not an error —
nothing is refused and the board is legal — and DRC's output is compared byte-for-byte
against the reference implementation this port is proved against
(test_matches_typescript_golden_drc). Grouping in the engine was written, measured
at 8 findings → 4 onrandom-09, and reverted for that reason. The noise was never in
the engine; it was in the panel.
- The rule itself is unchanged, and deliberately: it is a warning, not an error —
-
Auto-place stops turning parts for nothing. The annealer accepts any move whose
cost delta is<= 0, and a rotation's delta is exactly zero for every part the cost
function cannot tell apart turned — one on no net, or one whose courtyard is square. So
it turned parts for no reason at all: on thedensefixture it turned 11, and 5 of
those cost 0.00 to turn back. That is not free to whoever is holding the iron — every
rotation is an orientation to get right at the bench and a polarity line in the build
guide, and a plan reading "11 turned" describes work the tool did rather than noise it
made. When the placer has no preference, the user's own orientation is now the one
kept: across eight fixtures and eight seeds, parts turned went from 46 to 30 with the
routed cost identical to the last decimal.- Applied to the WINNER only, after
_pick_besthas chosen, and checked against the
router. Tidying every candidate before the choice changes the boards the router is
shown and therefore which one wins — measured ondense, where doing it that way
left the mean routed cost 31.5 → 35.9 while the best was unchanged. - Letting a rotation nudge the anchor at the same time — the obvious fix for a part
that can only be turned if it also moves a hole or two — was tried and measured
worse: summed mean routed cost 254.1 → 261.0 at half the rotations nudging, 260.0 at
a quarter, 262.6 at 0.15, with ne555 losing most of it. That result is written down at
the proposal site so nobody re-derives it.
- Applied to the WINNER only, after
Windows - PerfStudio_..._Setup.exe. It is unsigned, so SmartScreen will
warn on first run: More info -> Run anyway. A signed build is planned.
Linux - perfstudio-...-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 - perfstudio-...-arm64.dmg, Apple silicon. Drag it to Applications,
then run this before opening it:
xattr -dr com.apple.quarantine /Applications/PerfStudio.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 -e . from a clone, then
perfstudio.
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.
PerfStudio v0.5.0
Added
-
A part can be given a value (double-click it, or
F2). This was the one field on
the document no human could reach: every part the window placed was created with
value=""and nothing could change it afterwards, while an agent on the MCP server has
been able to pass one toplace_componentsince that server existed.guide._bom
groups on exactly this field, so the tool's own build guide printed "Resistor × 4"
where it meant "10k × 4".- The dialog carries only what
component.updatecarries — reference, value, lock.
Rotation is a command of its own, and putting it here would make one press of OK into
two entries on the undo stack for what the user experienced as one edit. - The Parts panel has a value box as well, applied to each part as it is placed. A
board is populated in runs — five 10k resistors, then three 100nF — and naming each
one afterwards through its own dialog is the same work done once per part instead of
once per run.
- The dialog carries only what
-
Right-click menus on the board, the Nets panel and the findings panel. There were
none anywhere: everything a part can be told to do lived in the menu bar at the top of a
window a metre wide, while the part itself was under the pointer in the middle of the
board. Every menu is built from the sameQActionobjects the menu bar holds, so an
action greyed out in one place is greyed out in the other. Right-click already means
"finish" inside a board mode, and the guards for that are described in
BoardView.contextMenuEvent. -
The window remembers its layout — geometry, dock sizes and positions, board colour,
the ratsnest, ruler and hatch toggles, and the preferred connection style. All of it
reset on every launch, and the cost was paid by whoever used the tool most. The 3D panel
deliberately still starts closed: restoring it open would build VTK's whole pipeline
during startup to show a board nobody has looked at yet. -
The build guide, in the window (
Ctrl+4). The soldering order is the thing this
application is for, and the only way to see one was to export four files and go and
find them — so the order the tool had worked out was invisible while the board was being
designed, which is when it is worth knowing. Picking a step selects its parts on the 2D
board, brings its holes into view, and moves the 3D assembly slider with it. Closed by
default and rebuilt only while open, because building a guide runs DRC and LVS. -
The export offers to open what it wrote, rather than ending at a line in the status
bar naming a file in a directory the user then has to go and find. -
A language menu (View ▸ Language). The Turkish catalogue has existed the whole time
and could be selected only by an environment variable or a command-line flag. Applied at
the next start, and it says so: every label is translated once as the window is built,
and the widgets a live rebuild missed would be exactly the ones nobody would notice had
stayed English. -
Files can be dropped on the window — a
.perfto open it (through the same
unsaved-work guard the Open menu item uses) or a.netto import it. Nothing happened
before, which reads as the application refusing that kind of file rather than refusing
drops. -
The findings panel has a filter box, like the parts and nets panels. A board
mid-layout carries a hundred proximity warnings, and "show me the errors" is how anybody
reads a list that long. -
Copy, paste and duplicate a block of board (
Ctrl+C/Ctrl+V/Ctrl+D). A
perfboard project repeats itself in a way a PCB does not — eight identical channels,
the same RC pair at every op-amp — and until now the only way to build the second one
was to place every part again by hand.- A block is parts AND the copper between them, placed by one new command,
block.place. Two commands would put a state on the undo stack nobody chose: the
parts down with their wiring gone, oneCtrl+Zfrom a board that looks finished and
is not. The copper is validated against a document the new parts have already
joined, which is what lets a pasted lead bend name the part it is a leg of. - JSON on the system clipboard, not a variable on the window, so a block crosses
documents and crosses two running copies of the application — which is the "channel 1
into that other board" case. It is readable, so a block can be pasted into a bug
report, for the same reason the project file is diffable. - A copy of R1 is not R1. Pasted parts get fresh references counted from the board,
and pasted copper carries no net claim: copper that keptnet_idwould tell LVS
the new block is wired to a schematic that has never heard of it. Unclaimed copper is
also the one kind rip-up and the stale-conductor cleanup both promise never to touch. - Three things are deliberately left behind, and each is reported rather than silent: a
lead bend whose part was not in the selection (it would be a leg of nothing), the
lock (a pasted part is one you are still positioning), and copper that the offset
would have pushed off the board. - Duplicate does not touch the system clipboard. Duplicating a part is a board
operation; it has no business throwing away what somebody copied elsewhere.
- A block is parts AND the copper between them, placed by one new command,
-
Stripboard is a board type this application can actually design on, rather than a
string the data model accepted and nothing understood.board.typeandstrip_axis
have been inmodel.py,persist.pyand the.perfformat since the first commit, a
cut.addcommand has existed the whole time, and nothing read any of it: the
connectivity engine did not know a strip joins the holes along it, so a stripboard
loaded as a pad-per-hole board with a decorative field set.- A cut destroys the copper AT a hole (
stripboard.py). That is how a track is
actually broken — a spot-face cutter or a drill bit turned by hand in the hole, which
takes the pad with it. The alternative model, a cut between two holes, describes a
knife scored across the track: a real technique, much harder to do reliably at 2.54 mm
and much harder to inspect, and one that would leave a cut without an address in an
application where every message is addressed. - Connectivity gained a fourth rule beside the three it has always had: on
stripboard the BOARD joins holes, and nobody soldered those connections. Only holes
something is soldered into take part — a strip physically joins all thirty holes in
its row, and registering the twenty-six nobody used would put every empty pad on the
board into a net, which is exactly what the module's existing note says not to do.
Gated on the board type, which is why fifteen golden fixtures still reproduce byte
for byte. - The autorouter for a stripboard subtracts before it adds (
striproute.py). Two
pins of different nets on one strip are shorted by the board itself, so the first
pass is cuts and the second is links; the links go over the COMPONENT side, because
the solder side is one sheet of parallel copper and a wire laid across it there shorts
every strip it crosses. Pins it cannot separate — adjacent, with no hole between them
to drill — are reported by name rather than routed around, because the fix is to move
a part and that is the user's decision. - The cuts and the links commit as one command (
stripboard.apply). Separately,
oneCtrl+Zleaves a board cut apart with nothing linking it, or linked with nothing
cut — which is a short across two nets, and a state nobody designed. - DRC reports a pin standing in a cut hole (
cut-track-conflict), an error for the
same reasonmounting-hole-conflictis: the pad is gone, so the board cannot work
rather than probably will not. - Both renderers draw the strips, and the 2D view marks each cut. A stripboard drawn
as a grid of separate pads is a picture of a different board, on a view whose whole
job is to be checked against the real one. - The build guide cuts first and measures each one. The cuts are made from the
copper side with a drill, and once a part is over a hole there is no way back to it —
so they are phase 0, and each gets a blocking isolation probe, because a cut that did
not go all the way through looks exactly like one that did. - Board Setup chooses the type and which way the strips run, and Draw ▸ Cut Track
(X) makes and un-makes cuts by clicking. Not done: the placer does not yet reward
putting a net's pins on one strip, which on stripboard is most of the design — said
here rather than left to be discovered.
- A cut destroys the copper AT a hole (
-
Something now checks what the render looks like (PLAN.md §10 asks for visual
regression). The headless run has always produced PNGs and CI has always kept them as
artefacts, and the only thing asserted about one was that it began with the PNG magic
bytes — so a render that lost every pad, drew the board inside out or came out blank
passed the build, and somebody had to open the artefact and look.- Not a pixel diff. Antialiasing and Qt's own version move individual pixels across
platforms and this suite runs on three; a per-pixel golden would fail on macOS for
reasons that have nothing to do with the board, and a test that fails for the wrong
reason gets switched off.tests/test_render_golden.pycompares the mean colour of
each cell of a 6 × 6 grid against a checked-in signature: re-rendering the board
moves it by 0.0 of 255, rendering the other face by 27.5, and rendering it with every
part and conductor removed by 22.6 — against a tolerance of 3. - The first attempt measured how much of each cell...
- Not a pixel diff. Antialiasing and Qt's own version move individual pixels across
PerfStudio v0.4.0
Added
-
Installers, on all three platforms, from one PyInstaller spec. Pushing a
v*tag
builds a Windows installer, a Linux AppImage and a macOS disk image, smoke-tests each
one by unpacking it and asking the binary inside what version it is, and attaches them
to the release. Nothing has been tagged yet, so nothing has been published.- The tag is checked against the source rather than trusted. A
v0.4.0tag on a
tree that still says0.4.0.dev0publishes an installer that disagrees with its own
file properties, and nothing downstream would notice — so the workflow refuses that,
refuses a version that still carries a.devNsuffix, and refuses one with no
CHANGELOG.mdsection. The release notes are that section, so the two cannot say
different things about what shipped. - Nothing is code-signed, and the release notes say so along with the click-through
each platform needs. A Windows EV certificate is ~$300/year and Apple notarization
$99/year (PLAN.md §12). The macOS bundle is signed ad-hoc, which is not a trust
decision — it is the minimum Apple silicon will execute at all. - Ubuntu 22.04 and macOS 15 are pinned rather than
-latest: a PyInstaller bundle
carries Python and Qt but links against the host's glibc, and glibc is forward
compatible only, so a 24.04 build would require 2.39 and rule out Debian 12 and every
enterprise distribution still in service. - The Windows installer speaks English and Turkish, because the application does,
and an installer that could only speak English would be the one part of the product
that does not. All three platforms claim.perf, so double-clicking a board opens it.
- The tag is checked against the source rather than trusted. A
-
Four example circuits, each shipped as both the netlist and the finished board.
There was one netlist and no board.examples/now carries the NE555 astable, an LM317
adjustable supply, a one-transistor guitar booster and an Arduino I/O shield — the
.neta schematic tool exports, and the.perfthat importing, placing and routing it
produces.- Chosen for what they make the tool say rather than for variety. The LM317 is a
TO-220, soheat-proximityhas a hot part to measure from, and that board carries
the set's onesolder-trace-proximitywarning — R5' doing its job and becoming a
measurement checkpoint. The booster is on FR-2, so the guide drops the iron 30 °C
andpad-lifting-riskcan fire at all. The shield is two headers, which is where lead
bends and short traces do nearly all the work. tests/test_examples.pyasserts on every commit that all four load without warnings,
round-trip byte-identically, match their schematics under LVS and carry no DRC error.
A broken example on the front page is worse than no example.
- Chosen for what they make the tool say rather than for variety. The LM317 is a
-
An application icon, drawn in code by
tools/make_assets.pyand committed as a
.pngand an.ico— because unlike the toolbar, an installer and a.desktopentry
need real files to point at. Every colour comes fromui/boardcolors, so the mark is
the same green and the same gold as the board in the editor. -
A demo animation of the build order (
tools/make_demo.py), generated by playing the
guide back throughdocument_at_step— the same function behind the 3D panel's assembly
slider, so it cannot show an order the guide does not actually prescribe. -
Copper on the face you are not looking at is hatched.
View ▸ Hatch Copper on the Far Side, on by default. The board is opaque: a solder-side trace drawn solid while you are
looking at the component side says this is in front of you, which is exactly the
misreading_paint_body_shadowalready exists to prevent for part bodies — and the one
that gets a board soldered on the wrong face. A conductor and a part now say "I am on the
other side" the same way, in the one visual word this application already had for it.- Stroked into a fillable shape rather than dashed. A dash already means a top jumper
inCONDUCTOR_STYLE, and giving one mark two meanings costs more than it saves. The
outline around the hatch is what keeps a run traceable end to end at low zoom, where a
0.9 mm trace is a few pixels wide and hatching alone cannot read. - The joints stay solid. Where a conductor is soldered down does not change with the
face you look from — the hole goes through the board — and it is what someone counts
pads against while tracing a run. A test pins that a hatched trace still marks every
hole it contacts. - The hatch brush carries the inverse of the painter transform, so it holds its spacing on
screen while the board zooms instead of turning solid zoomed in and vanishing zoomed out
— the same correction the body shadow needs, for the same reason. - Off is a real option, not a concession: someone tracing a dense solder side may simply
want to see it plainly. On is the default because the default has to be the reading that
cannot mislead.
- Stroked into a fillable shape rather than dashed. A dash already means a top jumper
-
The router can try every style and keep the best.
Route ▸ Preferred Connection ▸ Try each and keep the best,style: "best"over MCP. Picking a routing style meant
guessing — before seeing a single route — whether this particular board comes out better
with solder or with wire. Planning is pure and a plan is cheap, so the tool can stop
guessing and measure: it routes the board once per style and keeps the one that is least
work to build. Costs about two ordinary routes (607 ms against 273 ms on the NE555
fixture), becausebalanced's own rip-up passes are the slow ones.- It does not compare costs, and that is the whole trick. Each style's plan carries a
total_costquoted in that style's own currency: thewiretable prices a solder step
at 4 and an insulated wire at 6, so its plans are cheap by its own definition of
cheap, andmin(total_cost)would pick wire on every board ever. The comparison is on
physical facts instead — traces, wires, millimetres of wire, holes at R5' bridging
risk — which mean the same thing whichever table produced them. A test asserts
score_plannever mentionstotal_cost. - An unrouted connection is a gate, not a term. A plan that leaves one can never win
on being tidier elsewhere, however large the gap: PLAN.md §13 names "it routed most of
it and left four connections" as the trap every previous perfboard autorouter fell into. - A wire costs more to build than a trace, and by a fixed amount before a single
millimetre exists — measure, cut, strip, tin, dress, solder twice. The first version of
this scoring priced them the same, which undercharges the one primitive with real
preparation behind it and makes every comparison meaningless. - Every loser is kept and reported, with its measurements, in the status bar's tooltip
and incomparisonover MCP. The winner is chosen on an exchange rate between wires and
bridging risk that the user is entitled to disagree with, and they cannot disagree with
numbers they were never shown — so all four styles stay pickable by hand. - Ties fall to the earlier style tried, so
balancedkeeps its place and the sweep
reduces to today's behaviour when nothing beats it. On both the NE555 and dense
fixtures, nothing does. - Headless prints the whole table, so a change to any cost table shows up in CI as a
different winner rather than as a silently different board.
- It does not compare costs, and that is the whole trick. Each style's plan carries a
-
Resistors wear their colour code, in both views. A resistor is the commonest part on
almost any board and every one of them was an anonymous beige blob — so "is the 10k in
the right place" could not be answered by looking, which is the one job the 3D view has.
The bands are decoded fromComponentInstance.value, which is already in the document,
so nothing new is stored and the bands cannot disagree with the netlist.- It never guesses. A wrong band is worse than no band: somebody would read it and
fit the wrong part. The parser understands the way schematics actually write a
resistance —470,470R,4R7,10k,4k7,2.2k,2M2,1kΩ, where the unit
letter stands in for the decimal point — and returns nothing at all for anything else.
100nF,10uH,2A,NE555and dense.perf's placeholderv12all decode to no
bands rather than to a plausible resistance, and that is the load-bearing test. - A diode is not a resistor, though they share the
axial-cylinderarchetype. A
polarized axial body keeps its cathode stripe and is never banded, the same split
style_foralready makes. - Both views read
bodies.resistor_bands, so the editor and the 3D view cannot print
different parts. The library icons still draw three generic bands, and correctly: that
list shows footprints, which have no value to decode — it is a picture of a resistor,
where the board is a picture of a 10k.
- It never guesses. A wrong band is worse than no band: somebody would read it and
-
Parts are lit like the material they are made of.
BodyStyle.metallicand
BodyStyle.lenswere documented as shading hints for both renderers and read by almost
neither — the HC-49 crystal, the one part in the registry that is literally a metal can,
hardcoded its own metal shading while carrying the flag that says so, and no LED was ever
lit as a lens. Two sources of truth for one fact, in the module that exists to prevent
exactly that.bodies.surface_foris now the single answer, and both views ask it.- The 2D view gained a highlight swept across the short axis of a body, because that is
the direction a cylinder curves in. A flat fill is what made every part read as a
sticker printed on the board rather than an object standing on it. - A LED now looks lit, a crystal can looks like metal, and a DIP sti...
- The 2D view gained a highlight swept across the short axis of a body, because that is