Skip to content

Updated Circuit Editor to V2 and version to Alpha v0.3.0 - #12

Merged
FacioErgoSum merged 27 commits into
mainfrom
circuit-editor
Jul 22, 2026
Merged

FacioErgoSum merged 27 commits into
mainfrom
circuit-editor

Conversation

@FacioErgoSum

Copy link
Copy Markdown
Contributor

No description provided.

New greenfield module at src/renderer/src/circuit/ per docs/circuit-view-tech-spec.md:
- core/model: circuit.json v2 schema, parse/serialize, v1+Wokwi migration,
  unknown-key preservation (B3)
- core/routing: wireRouting port with Wokwi '*' journey semantics (B2),
  parametric junction math
- core/nets: DSU nets across both views — buses (B8 seam), junction-by-identity
  (B9), net labels, implicit-connection seam; canonical keys (B10)
- core/commands + core/store: pure Commands, undo/redo with gesture merging,
  duplicate-wire guard (B5), delete cascades with junction repair
- core/geometry + core/refdes: pin transforms, pin-grid snapping, spatial hash,
  refdes assignment
- views/CircuitView: read-only M0 preview mounted in the Circuit tab behind
  localStorage flag tinystudio.circuitV2 (legacy DiagramEditor untouched)
- 35 core tests (npm run test:circuit, esbuild + node:test); typecheck clean

Docs: architecture comparison (circuit-architecture-and-roadmap.md), tech spec
with resolved decisions (circuit-view-tech-spec.md), agent progress log
(circuit-editor-progress.md).
…ey, pending-junction typing

- core/clipboard.ts: buildClipboard (fixpoint filtering of junction riders),
  parseClipboard (shape-detected JSON), materializePaste (fresh refdes ids,
  endpoint rewrite, placement offset)
- commands.composite() gains an optional mergeKey so multi-part drag frames
  collapse into one undo step like single-part gestures
- model: PendingJunctionEnd + isPendingJunction for v1-migrated free-point
  endpoints awaiting geometric resolution
- 18 new tests (53 total)
- parts/svg.ts: namespaceSvgIds / svgNs / stripSvgSize / escapeXml — pure
  string transforms, unit-tested; full sanitizer arrives with M2 packs
- views/exportImage.ts: composeSceneSvg (namespaced per part instance),
  exportSvg / exportPng @2x with watermark
- index.ts re-exports clipboard + svg helpers
- views/partsAdapter.ts: legacy-partsLibrary geometry glue (pin transforms,
  endpoint resolution incl. junction chains, frozen-bend reroutes,
  snap-by-first-pin per spec §6.3) — swapped for the M2 registry later
- views/canvas/Canvas.tsx: camera (wheel zoom, pan, fit), selection +
  marquee, wire draw (pin→pin/junction tap via {wire,t} tAtPoint, Shift
  straight, Esc), selected-wire handles (segment/vertex/endpoint;
  endpoint re-targets to pin or junction, reverts on empty space —
  v2 has no free endpoints), part drag single+multi with frozen bends
  (between-two-moved wires translate), label drag, R/context rotate,
  arrow nudge (Shift=5x), Del cascade, Ctrl+Z/Y undo/redo,
  Ctrl+C/X/V/D clipboard via core/clipboard, pending-junction resolution
  on first render (closes the M0 migration gap)
- views/palette/Palette.tsx, views/inspector/Inspector.tsx: legacy rail +
  inspector ported; inspector renames refdes (validated), edits attrs,
  moves/rotates with reroutes
- views/CircuitView.tsx: shell — store owner, debounced (250ms) save via
  onChange with external-edit echo/skip guard, toolbar, zoom cluster,
  status pills, PartsEditor modal
…s log

EditorPanel v2 branch: CircuitV2View migrates diagram.json → circuit.json
(+ diagram.json.bak) on first open, then mounts the editor on the
circuit.json hidden buffer with updateFileContent as the debounced sink.
diagram.json intentionally left in place until M4 (legacy editor still
default-on behind the flag).
Fritzing part roots carry x=0px/y=0px + width/height; injecting placement
attributes duplicated them, making the composed SVG invalid XML — inline
DOM rendering tolerated it, the standalone .svg and the PNG rasterizer's
<img> load did not. prepareSvgForEmbed strips prolog/doctype and root
geometry attrs (viewBox kept). Also: #id selectors in <style> blocks are
now namespaced, and var(--token) colors are inlined at export time so
builtin board art survives outside the app stylesheet.
- parts/breadboard.ts: parametric generator (mini 17 / half+ 30 / full+ 63
  cols), every hole on the GRID_BB pitch so snap-by-first-pin placement
  makes seating an exact coordinate match; per-column bank buses + rail
  buses via breadboardBuses(); crisp generated SVG (body, channel, rail
  stripes, row letters, column numbers)
- partsAdapter: implicitSeats() derives pin-in-hole connections (never
  stored, spec §4/§7.3) via SpatialHash; seatedPartsOn() for sticky
  boards; holeAt() nearest-hole lookup
- CircuitView: registers breadboards as builtins pre-parse; net model now
  runs with busesFor + implicit seats
- Canvas: breadboards skip per-pin hit divs (>60 pins) — press-without-
  displacement on a board is a hole interaction (arm/finalize wires on
  holes), drag moves the board WITH its seated parts, hover shows a hole
  highlight + tooltip (hole name · net members), green seat marks render
  for every derived connection; wire preview snaps to hovered holes
- 4 new test groups (60 total): hole grid alignment, pin counts/naming,
  bus shapes, bus+implicit net integration
…odel

- parts/symbols.ts: generated IC-box symbols so schematic never blocks on
  missing art (authored art preferred, cached, pins on the major grid)
- partsAdapter goes view-generic (ViewId param, bb defaults + aliases);
  ratsnest() bridges nets connected globally but unrouted in the active
  view (bb treats breadboard seating as routed)
- Canvas: view prop — per-view placements/wires/snap grids, ink orthogonal
  schematic wires, flip (F/Inspector, sch), dashed ratsnest layer, bb-only
  seating UI; remounts per view so gesture state can't leak
- Shell: Breadboard|Schematic toggle, unplaced-parts tray (click to place),
  routed-here status counter, per-view part placement; Inspector edits the
  active view's placement incl. mirror
- 63/63 tests, typecheck clean
…CSV export; boards render under everything; resistor bands follow value
…rcuitEditor

Circuit View v2 is now the only editor mounted by EditorPanel's CircuitView -
no more tinystudio.circuitV2 flag or CircuitV1View branch. diagram.json
projects still auto-migrate to circuit.json on open (unchanged). Removes
src/renderer/src/components/DiagramEditor.tsx (2009 lines, legacy breadboard
editor) and the already-dead CircuitEditor.tsx static viewer (B21).

npm run typecheck (node+web) clean; test:circuit 95/95.
SimPanel gets an auto-rerun checkbox: once on, doc edits re-run the active
analysis after a 400ms debounce (single in-flight run, guarded by a ref).
Breadboard hole tooltips now append the net's solved DC (.op) voltage
alongside the existing 'hole · net members' text, via a new
simVoltageForNet(netIdx) lookup threaded CircuitView -> Canvas.

npm run typecheck (node+web) clean; test:circuit 95/95.
generateNetlist now records elementOfPart (lowercased SPICE device names per
part id, e.g. R1 -> ['rr1']) alongside the existing nodeOfNet. New pure
mapSimIssue/mapSimIssues (core/netlist.ts) scan raw ngspice error/warning
lines for those device names and node names (word-boundary, case-insensitive,
ground '0' excluded) and return the implicated part ids + net indices.

SimPanel's error box now renders clickable part/net chips (+ a 'select all')
under any run failure; CircuitView wires them to canvas selection via a new
selectSimIssue, expanding net hits to their member parts.

+6 tests (elementOfPart, device-name match, node-name match incl. ground
skip, n1/n10 word-boundary, multi-line de-dup). npm run typecheck (node+web)
clean; test:circuit 100/100.
Closes the last M2/M4-leftover item on sim.probes (spec Sec10.3): three
placeable builtin probes, CircuitLab-flag style, seatable on breadboards
like any part:
  - sim-probe-v      1 pin  - voltage probe (ngspice already reports every
                               node, so this needs no SPICE element)
  - sim-probe-vdiff  2 pins - differential voltage; core/probes.ts computes
                               a synthetic vdiff(<id>) = v(+) - v(-) vector
                               (real and imaginary, for AC) after each run
  - sim-probe-i      2 pins - current probe, wired in series; emits a real
                               0V series voltage source (V<id> DC 0) so
                               ngspice reports i(v<id>) without disturbing
                               the circuit

core/netlist.ts: EMITTERS regexes for the three types (anchored with \b, not
$, since the match key has a trailing ' <family>'); elementOfPart already
built into the last commit's error-mapping change. core/probes.ts adds
probesIn/diffProbeVector(s)/probeLabelFor, all pure and tested. SimPanel/Plot
now merge diff-probe vectors into results and prefer a probe's attrs.label
over the raw v(node)/i(vXX)/vdiff(id) name in the DC table and plot legend.
refdes.ts gets an explicit probe->'P' prefix entry (matches the prior
fallback, now documented).

+9 unit tests (probes.test.ts) +1 real-engine integration test (sim.test.ts:
verifies i(vamm1) against the analytic divider current). npm run typecheck
(node+web) clean; test:circuit 107/107.
…-connect

implicitSeats() previously seated each of a part's pins independently against
the nearest hole within SEAT_RADIUS, so a part with one leg near a hole and
another off-grid would report a single spurious connection while the rest of
its pins silently dangled unconnected. Now a part seats as a rigid body —
ALL of its pins must land within SEAT_RADIUS at the part's current placement
for ANY of them to seat; otherwise none do (matches Fritzing: a component
either fully plugs in or just rests on top). Pairs naturally with the
existing snap-by-first-pin grid snap, so well-formed grid-pitched parts still
seat on the first touch.

Single computation point (CircuitView.tsx memo), consumed by net building,
hole seat marks, sticky-board drag, and rigid board rotation — all get the
fix for free. npm run typecheck (node+web) clean; test:circuit 107/107
(implicitSeats depends on the legacy partsLibrary registry like the rest of
partsAdapter.ts, so it isn't unit-tested in isolation, consistent with the
rest of that module).
Closes the last M2-scope gap noted since M1 ('leg pins resolve to the drawn
leg end; real rubber-band legs are a Phase 2 renderer feature').

Import (both importers, mirrored):
  - scripts/fritzing-import.mjs and circuit/parts/fzpz.ts now read each
    connector's legId and tag the resolved pin name into a new
    PartView.legs: string[] (lib/partsLibrary.ts). Geometry itself needed no
    change — core/geometry.pinWorld and partsAdapter.pinWorldOf were already
    leg-offset aware (Placement.legs existed in the model since M0/M1 but
    nothing ever produced or consumed it).

Canvas (bb view only, per the model's 'legs bb only' contract):
  - A pin tagged as a leg can be dragged independent of the part body
    (beginLegDrag in views/canvas/Canvas.tsx): live-dispatches merged
    placePart commands per move (same pattern as the existing label-offset
    drag), so wires re-anchor to the tip in real time via the normal
    doc -> resolve()/wireGeometry() recompute — no separate drag-override
    path needed. A real (>=3px) drag suppresses the trailing click so it
    doesn't also arm/complete a wire from that pin; a plain click still
    works normally. Double-click a bent leg to reset it to rest.
  - Visual: a simple ink line from the rest pin to the dragged tip, overlaid
    on the existing static Fritzing leg art (which stays at rest position) —
    real per-instance SVG leg-path warping is future work, noted in code.
  - implicitSeats() (M2's rigid seat rule, previous commit) already reads
    pin world position through the leg-aware pinWorldOf, so a stretched leg
    can now actually reach an adjacent hole and seat, matching Fritzing.

Not covered: Inspector UI for leg offsets (canvas drag + reset covers the
interaction), and this specific gesture code has no automated test (Canvas.tsx
render/gesture code isn't unit-tested anywhere in this suite; verified by
typecheck + code review only — the importer half touches DOM-only code paths
the existing fzpz.test.ts also intentionally leaves untested).

npm run typecheck (node+web) clean; test:circuit 107/107 (unaffected).
Closes the last M2-scope item: GitHub-distributed parts packs (spec §5.4,
Arduino Boards-Manager pattern), targeting the app's actual current pipeline
(legacy PartDef v1 + lib/userParts.ts persistence) rather than the unbuilt v2
registry the spec assumes.

- circuit/parts/packs.ts: fetchIndex/fetchManifest (shape-validated, relative
  URLs resolved against their own manifest/index), installPack (per-part
  fetch + saveUserPart, continues past individual failures, marks the pack
  installed only if >=1 part landed), index-URL list + installed-version
  settings in localStorage. DEFAULT_INDEX_URL points at the tinyparts repo
  the M2 decision log already named.
- circuit/views/packs/PackManager.tsx: modal (Components rail -> new package
  icon) — add/remove index URLs, per-pack Install/Update with progress and
  toast summaries.
- scripts/make-pack-index.mjs: wraps a scripts/fritzing-import.mjs output
  directory into pack.json + index.json — re-shapes the existing bulk
  importer's manifest, no new conversion logic.
- docs/tinyparts-pack-setup.md: full guide for Geoff to create/host the
  tinyparts repo by hand (layout, exact JSON schemas, the generator command,
  local testing, ATTRIBUTION requirement per the M0 licensing decision, and
  an explicit 'not done' list — no hash verification, no launch-time update
  check, no zip hosting).

Verified two ways: 7 unit tests (packs.test.ts — index/manifest validation,
relative-URL resolution, partial-failure install, settings) against mocked
fetch/localStorage, AND a real end-to-end smoke test (not part of the
suite): ran make-pack-index.mjs against the app's actual bundled 29-part
catalogue, served the output over a plain local HTTP server, and installed
it through the real packs.ts client — 29/29 installed, 0 failures.

npm run typecheck (node+web) clean; test:circuit 114/114.
Both milestones now fully closed per the tech spec's Section 15 table. Only
M5 (KiCad/Wokwi interop) remains untouched. Full rundown of all seven
commits, verification method (sandbox esbuild-linux workaround for
test:circuit), and known gaps for the next agent.
- Default theme to light mode (main.tsx)
- Top/bottom bar recolored blue -> green via new --bar-accent token,
  kept independent of --brand so buttons/selection/focus rings are
  untouched (base.css, Header.tsx, StatusBar.tsx)
- Docs panel tabs are now controlled via editorSlice.docsTab; opening a
  folder or example (activateWorkspace in fileCommands.ts) switches off
  Examples onto Docs automatically
- Per-tab accent colors: Examples yellow, Docs unchanged blue, Studio AI
  grape (#9c6c9c) (DocsPanel.tsx)
- Examples tab: removed redundant "Examples" header, added generous
  spacing/padding around cards (ExamplesContent.tsx)
- AI panel toolbar: dropped "Studio AI" label, shows just the model name
…-deps

Fixes electron binary not being fetched on postinstall.
@netlify

netlify Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for tinystudios ready!

Name Link
🔨 Latest commit 2cc5a65
🔍 Latest deploy log https://app.netlify.com/projects/tinystudios/deploys/6a6138be2f95520008915f9c
😎 Deploy Preview https://deploy-preview-12--tinystudios.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@FacioErgoSum
FacioErgoSum merged commit a28d8bf into main Jul 22, 2026
4 of 5 checks passed
@github-actions

Copy link
Copy Markdown

Thanks for your interest in tinyStudio! 🙏

This project is in early alpha and is not accepting external contributions right now,
so this pull request is being closed automatically. Please feel free to fork and experiment.

Contributions will open up as the project stabilizes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant