From dc9697b9c7253c7d396b748a174061f283e58be6 Mon Sep 17 00:00:00 2001 From: Geoff McIntyre Date: Fri, 3 Jul 2026 05:34:34 +0000 Subject: [PATCH 01/27] =?UTF-8?q?Circuit=20View=20v2=20=E2=80=94=20M0=20co?= =?UTF-8?q?re=20skeleton=20(circuit-editor=20branch)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- docs/circuit-architecture-and-roadmap.md | 282 ++++++++++ docs/circuit-editor-progress.md | 59 +++ docs/circuit-view-tech-spec.md | 490 ++++++++++++++++++ package.json | 1 + scripts/test-circuit.mjs | 50 ++ .../src/circuit/__tests__/model.test.ts | 135 +++++ .../src/circuit/__tests__/nets.test.ts | 102 ++++ .../src/circuit/__tests__/routing.test.ts | 139 +++++ .../src/circuit/__tests__/store.test.ts | 152 ++++++ src/renderer/src/circuit/core/commands.ts | 301 +++++++++++ src/renderer/src/circuit/core/geometry.ts | 121 +++++ src/renderer/src/circuit/core/model.ts | 456 ++++++++++++++++ src/renderer/src/circuit/core/nets.ts | 197 +++++++ src/renderer/src/circuit/core/refdes.ts | 57 ++ src/renderer/src/circuit/core/routing.ts | 353 +++++++++++++ src/renderer/src/circuit/core/store.ts | 145 ++++++ src/renderer/src/circuit/index.ts | 29 ++ .../src/circuit/views/CircuitView.tsx | 337 ++++++++++++ src/renderer/src/components/EditorPanel.tsx | 8 + 19 files changed, 3414 insertions(+) create mode 100644 docs/circuit-architecture-and-roadmap.md create mode 100644 docs/circuit-editor-progress.md create mode 100644 docs/circuit-view-tech-spec.md create mode 100644 scripts/test-circuit.mjs create mode 100644 src/renderer/src/circuit/__tests__/model.test.ts create mode 100644 src/renderer/src/circuit/__tests__/nets.test.ts create mode 100644 src/renderer/src/circuit/__tests__/routing.test.ts create mode 100644 src/renderer/src/circuit/__tests__/store.test.ts create mode 100644 src/renderer/src/circuit/core/commands.ts create mode 100644 src/renderer/src/circuit/core/geometry.ts create mode 100644 src/renderer/src/circuit/core/model.ts create mode 100644 src/renderer/src/circuit/core/nets.ts create mode 100644 src/renderer/src/circuit/core/refdes.ts create mode 100644 src/renderer/src/circuit/core/routing.ts create mode 100644 src/renderer/src/circuit/core/store.ts create mode 100644 src/renderer/src/circuit/index.ts create mode 100644 src/renderer/src/circuit/views/CircuitView.tsx diff --git a/docs/circuit-architecture-and-roadmap.md b/docs/circuit-architecture-and-roadmap.md new file mode 100644 index 0000000..10af7be --- /dev/null +++ b/docs/circuit-architecture-and-roadmap.md @@ -0,0 +1,282 @@ +# Circuit Design Document — Fritzing vs. tinyStudio, Fritzing Part Import, and the Road to KiCad + +**Status:** Draft for review · **Date:** 2026-07-02 · **Scope:** `fritzing-app` (upstream reference) and `tinyStudio` Circuit view (`DiagramEditor.tsx`, `wireRouting.ts`, `circuitNets.ts`, `partsLibrary.ts`, `PartsEditor.tsx`, `scripts/fritzing-import.mjs`) + +This document explains how Fritzing's breadboard/wiring/parts architecture works, how tinyStudio's circuit view works today, the exact differences between them, how a Fritzing part can be imported directly, the bugs and gaps in the current circuit designer, and a phased roadmap toward "the best breadboard view ever" with schematic generation and KiCad export — while keeping `diagram.json` Wokwi-compatible. + +--- + +## Part I — How Fritzing works + +Fritzing is a Qt/C++ desktop app built on `QGraphicsScene`. One electrical **model** drives four synchronized **views** (icon, breadboard, schematic, PCB). Everything below is verified against the `fritzing-app` source in this workspace. + +### 1.1 The part model: `.fzp` + per-view SVGs + +A Fritzing part is an XML file (`.fzp`) plus up to four SVG files (one per view). The `.fzp` contains: + +- **Metadata** — `moduleId` (unique id), title, label, author, date, tags, `taxonomy`, description. +- **Properties** — a `family` (parts in the same family are hot-swappable in the Inspector) plus free-form key/value pairs (`resistance`, `package`, `voltage` …). +- **Views** — for each of `iconView` / `breadboardView` / `schematicView` / `pcbView`, an `image` path to an SVG and one or more `layer` ids (`breadboard`, `schematic`, `copper0`, `copper1`, `silkscreen` …). PCB parts reference multiple layers inside a single SVG. +- **Connectors** — each `` has an `id` (`connector0` …), a human `name` (`V+`, `GND`), a `type` (**male** or **female** — this drives breadboard behavior, see §1.3), optional ERC metadata (``, current/voltage hints), and per-view bindings: + - `svgId` — the id of the graphic element in that view's SVG that *is* the pin (e.g. `connector0pin`). + - `terminalId` (optional) — a smaller element marking the exact wire-attach point (used mostly in schematic view; without it the terminal defaults to the center of the `svgId` element). + - `legId` (optional) — for parts with bendable rubber-band legs (LEDs, resistors in breadboard view), the SVG element that is the flexible leg. +- **Buses** — `` declares internal connectivity: a `` lists `` pins that are permanently connected inside the part. **This is how a breadboard works in Fritzing**: the breadboard part has hundreds of female connectors, and each 5-hole row (and each power rail) is a bus. +- **Subparts** (schematic only) — multi-unit parts (e.g. quad op-amps) that can be placed as separate schematic symbols. + +Key insight: **the `.fzp` stores no pin coordinates.** Geometry lives entirely in the SVGs; the `.fzp` binds connector ids to SVG element ids. Any importer must resolve those elements inside the SVG (including ancestor `transform`s) to get pin positions — which is exactly what `scripts/fritzing-import.mjs` does. + +Parts ship as `.fzpz` (a ZIP of the `.fzp` + its SVGs); sketches ship as `.fzz` (a ZIP containing a `.fz` XML plus any non-core parts used). + +### 1.2 The scene/item architecture + +- `ModelPart` / `ModelPartShared` — the in-memory part (shared data parsed once from the `.fzp`). +- `ItemBase` → `PaletteItem` → concrete classes (`Breadboard`, `Resistor`, `Wire`, `SymbolPaletteItem` for net labels/power symbols, `Dip`, `MysteryPart` …) — a `QGraphicsItem` per part *per view*. `PartFactory` picks the class from the moduleId/family. +- `ConnectorItem` — a `QGraphicsRectItem` child of each part for every connector in every view; handles hover highlight, hit testing, and holds the list of `connectedTo` ConnectorItems. +- `Bus` / `BusShared` — internal-connection groups from the `.fzp`. +- `SketchWidget` (≈10,800 lines) — the shared view controller; `BreadboardSketchWidget`, `SchematicSketchWidget`, `PcbSketchWidget` subclass it with per-view rules (`ignoreFemale()`, `canDropModelPart()`, trace widths, colors, grid defaults: 0.1 in breadboard/schematic, 0.05 in PCB). +- Every mutation is a `QUndoCommand` on a shared undo stack, and commands carry a **CrossViewType** so an action in one view replays into the others. + +### 1.3 Breadboard view mechanics (the part to learn from) + +The breadboard view has no special "breadboard mode" — it emerges from a few composable rules: + +1. **Male vs. female connectors.** Breadboard holes are *female*; component pins and wire ends are *male*. When a part is dropped or moved, `findConnectorsUnder()` hit-tests each of its male ConnectorItems against female ConnectorItems beneath it (`Breadboard::canFindConnectorsUnder()` returns false — the board itself never searches; things land *on it*). Overlap ⇒ an implicit connection is formed, with no wire drawn. Move the part away ⇒ the connection dissolves (`disconnectFromFemale`). +2. **Buses make rows live.** Inserting a pin into one hole connects it to the whole 5-hole row because the row is a bus in the breadboard's `.fzp`. Power rails are long buses. +3. **Stickiness.** The breadboard is *sticky* (`ItemBase::m_sticky`): parts dropped on it are recorded in `m_stickyList` and move with it when the board is dragged. +4. **Wires with bendpoints.** A user wire is a chain of `Wire` items — each `Wire` is one straight `QGraphicsLineItem` segment with a ConnectorItem at each end; a bendpoint is where two segments share a position. `Wire::collectChained()` walks the chain to find true endpoints. Breadboard wires may also carry **Bézier curvature** per segment (`` control points in the sketch file) for the pretty curved-jumper look. Dragging from any male/female connector spawns a temp wire (`createTempWireForDragging`); dropping on a legal target commits it. +5. **Rubber-band legs.** Parts with `legId` connectors (LED, resistor) have draggable legs that stretch to reach holes — the leg is part geometry, not a wire. +6. **Ratsnest sync.** Every connect/disconnect calls `ratsnestConnect()`; other views receive **VirtualWire** ratsnest lines grouping equal-potential connectors, which the user then routes properly in that view (schematic wires, PCB traces). This is the bridge that turns a breadboard layout into a schematic to route. + +### 1.4 Nets, ERC and routing + +- Equal-potential collection is a flood fill (`collectEqualPotential`) across: wire connections, female/male stacking, part-internal buses, and same-net symbols (net labels, power symbols via `symbolpaletteitem`). +- `RoutingStatus` tracks nets total / routed / connectors-to-route per view. +- ERC data (`erc.h`, connector `etype`, current/voltage) supports a basic electrical rule check and drives the newer simulation module (`src/simulation`, ngspice-based). +- PCB view has a full maze autorouter + DRC (`src/autoroute`), Gerber export (`svg2gerber`), and ground-plane generation. Schematic/breadboard have no autorouter — humans route; only the ratsnest guides them. +- `src/svg/kicad2svg` imports **KiCad → Fritzing** (modules and schematic symbols → part SVGs). There is no KiCad *export* in Fritzing — a gap tinyStudio can leapfrog. + +### 1.5 Sketch file format (`.fz` inside `.fzz`) + +XML `` with `` (per-view background/grid settings) and ``. Each `` has `moduleIdRef`, a per-view `` (x/y/z, and for wires `x1/y1/x2/y2` + `wireFlags`), optional `` (width, color, Bézier control points), and a `` block whose `` children record the id + modelIndex of everything each connector touches. Connections are therefore stored **explicitly and per-view**, symmetric on both sides. + +--- + +## Part II — How tinyStudio's Circuit view works + +The Circuit view is a React component tree inside an Electron (or web) app. The single source of truth is a **`diagram.json` file in the user's project**, edited live: `EditorPanel.tsx → CircuitView` loads `diagram.json` (creating a default if absent) and hands its text to `DiagramEditor`, which parses on every change and writes back serialized JSON through Redux (`updateFileContent`). Editing the JSON in the Code view and dragging parts in the Circuit view are the same operation on the same buffer. + +### 2.1 Data model (Wokwi-first) + +```jsonc +{ + "version": 1, + "editor": "tinystudio", + "author": "…", + "parts": [ { "type": "resistor", "id": "resistor_a1x", "left": 520, "top": 230, "rotate": 90, "attrs": {…} } ], + "connections": [ ["tinycore:SIG", "resistor:Pin 0", "#36c46b", ["v-22.73", "h-179.8"]] ], + "schematic": { "pos": { "partId": [x, y] }, "routes": { "": ["h…","v…"] } } +} +``` + +- **Parts** use Wokwi's `id`/`type`/`left`/`top`/`rotate`/`attrs` fields (px @ 96 DPI; 0.1 in = 9.6 px grid). +- **Connections** use Wokwi's 4-tuple `[from, to, color, journey]` with `h`/`v` instructions. Two tinyStudio extensions: a `d,` instruction for diagonal ("straight mode") segments, and endpoints may be a free point `{x, y}` (a junction tapping another wire's body) instead of `"part:pin"`. +- **`schematic`** is a per-view overlay (positions + routes) so a future schematic view can diverge from the breadboard layout without duplicating the electrical data. Pin *names* are stable across views, so connections stay valid when switching views. + +### 2.2 Parts library (`partsLibrary.ts`) + +One schema for all parts (`PartDef`): `type`, `label`, `family`, and `views.{breadboard,schematic}` each `{ svg, w, h, pins: { name: [x, y] } }` — inline SVG plus pin coordinates in px from the part's top-left. Sources: + +1. **Built-ins** — the tiny* boards, authored inline with generated SVG. +2. **Fritzing-imported catalogue** — 29 parts currently under `assets/parts/`, one JSON per part, lazy-loaded via Vite dynamic import; `index.json` is the eagerly-loaded manifest (label, family, icon, pin count) that fills the palette. +3. **User parts** — `PartsEditor` (modal: upload SVG, click to drop pins, drag to place, name them) registers a `PartDef` into the live registry via `registerPart()`. **In-memory only — not persisted** (see bug B7). + +### 2.3 The import pipeline that already exists (`scripts/fritzing-import.mjs`) + +This is the crucial piece for "take a Fritzing part and drop it straight into the circuit view." It already works, offline, against a checkout of `fritzing-parts`: + +1. Parse the `.fzp` (metadata, per-view `image`, connector list with `svgId`/`terminalId`/`legId`). +2. Load the view SVG; read `viewBox` + real-world `width`/`height` (in/mm/pt…), convert to px @ 96 DPI (`in×96`, `mm×3.7795` …). +3. For each connector, find the `svgId`/`terminalId` element (schematic prefers `terminalId` — the precise attach point; breadboard prefers `svgId`), compute its **local anchor** (rect center, circle center, path/polygon bbox center, line far-end heuristic, first-child for `` wrappers), then apply the **cumulative ancestor transform chain** and scale into canvas px. +4. Emit `PartDef` JSON + manifest entry + `_report.json` (ok / partial / failed / skipped). + +So the conversion math (the genuinely hard part: FZP→pixels with transforms and units) is done and validated. What's missing is productization — see Part IV. + +### 2.4 Wiring engine (`wireRouting.ts` — ported from the tinySchematic prototype) + +- **Absolute bendpoints.** A wire's stored instruction list is decoded into interior bend positions *fixed in world space*. When a part moves, only the leg(s) touching it re-anchor (`collectFrozen` → `buildWirePoints(source, bends, target)`); the wire body holds still. This mirrors Fritzing's behavior, where bendpoints are their own objects. +- **Orthogonal elbow routing.** `calculateOrthogonalPath` builds L-paths; the final segment approaches the pin along the major travel axis so wires dock perpendicular. Hold Shift for straight/diagonal mode (`d` instructions). +- **Junctions.** While drawing (or dragging an endpoint), landing on another wire's body clamps the endpoint onto that segment (`clampOntoSegment`) and stores a free `{x,y}` endpoint; a solder dot is rendered, and the net model merges the nets. +- **Editing.** Selected wires get Fritzing-style handles: squares mid-segment (perpendicular segment drag via `dragSegment`, inserting corner bends at anchored ends), circles at bends (`vertexDrag` keeps neighbors orthogonal), rings at endpoints (retarget to a pin, a wire body, or free space). Double-click adds/removes a bend. `simplifyWirePoints` drops collinear/duplicate points before saving. + +### 2.5 Net model (`circuitNets.ts`) + +Union-find (DSU) over endpoint keys with three edge types: physical connections, part-internal **buses** (via an optional `busesFor` callback — **currently never supplied by the editor**, see bug B8), and junction endpoints lying within 2 px of another wire's polyline. Output maps pins/connections → net index; drives the yellow equipotential hover glow and the "Nets: N" counter. This is a faithful miniature of Fritzing's `collectEqualPotential`. + +### 2.6 Rendering & UX + +Parts are absolutely-positioned `
`s with `dangerouslySetInnerHTML` SVG bodies (CSS `rotate` around center); wires are a single SVG overlay (glow → dark outline → color core → animated selection dashes, rounded fillets via quadratic arcs); pins are 16 px hit targets rendered only in edit mode. Custom camera (`{scale, tx, ty}`), wheel zoom (0.25–3×), middle/Alt-drag pan, fit-to-view. Two-column palette rail (left) and Inspector (right: name, x/y, rotation, free-form `attrs` properties, wire color, net pin count). PNG export composes a standalone SVG and rasterizes at 2×. View/edit toggle; footer status pills. + +--- + +## Part III — Architecture comparison + +| Aspect | Fritzing | tinyStudio Circuit view | +|---|---|---| +| Platform | Qt/C++, QGraphicsScene | Electron/web, React + SVG/DOM | +| Document | `.fzz` (zipped XML), instances with per-view geometry, symmetric explicit connects | `diagram.json` (Wokwi format + extensions), connections as endpoint tuples | +| Views | 4 synced views (icon/bb/schematic/pcb), cross-view undo commands | Breadboard live; schematic exists as a per-view overlay in data + code but is **not reachable in the UI** (view state is hard-locked) | +| Part format | `.fzp` XML + per-view SVGs, pins = SVG element refs | `PartDef` JSON, pins = resolved px coordinates, SVG inlined | +| Pin identity | `connector0…N` ids + names, male/female typed | pin *names* only; no male/female concept | +| Breadboard | Real part: female connectors + row buses + sticky + drop-to-connect | No breadboard part; wires connect pin-to-pin only | +| Bendpoints | Wire = chain of segment items; Bézier curves optional | Instruction list (`h/v/d`) decoded to absolute bendpoints | +| Junctions | Bendpoint-on-wire creates a shared connector | Free `{x,y}` endpoint + proximity net merge | +| Nets | Flood fill incl. buses, net labels, power symbols | DSU incl. junctions; bus support present but unused; no net labels/power symbols | +| Undo | Full QUndoStack, cross-view | **None** (every drag frame rewrites the file buffer) | +| ERC/simulation | ERC types per connector; ngspice simulation | None (Wokwi compat is the intended simulation path) | +| PCB | Full view, maze autorouter, DRC, Gerber | Out of scope (KiCad export is the plan) | +| KiCad | Import only (KiCad→Fritzing SVG) | None yet — export is roadmap Phase 4 | +| Rubber-band legs | Yes (`legId`) | No | +| Part swapping | Family-based Inspector swap | No | + +--- + +## Part IV — Importing a Fritzing part directly into the Circuit view + +### 4.1 Why it already (mostly) just works + +The importer proves the mapping is clean: + +| Fritzing | tinyStudio | Notes | +|---|---|---| +| `.fzp` title/label/family/description | `label` / `family` / `description` | `family` also groups the palette | +| filename / `moduleId` | `type` (slugged filename; moduleId kept in `source`) | moduleIds in core are often random hashes; filenames are the stable human identity | +| `breadboardView`/`schematicView` image SVG | `views.{breadboard,schematic}.svg` (minified, width/height stripped, viewBox kept) | | +| SVG real-world size | `w`/`h` in px @ 96 DPI | `in×96`, `mm×3.7795`, `pt×1.333` | +| connector `name` (fallback `id`) | pin name (deduped `name.2`, `name.3`…) | names must stay stable across views | +| `svgId`/`terminalId` element position | `pins[name] = [x, y]` | ancestor transforms accumulated; schematic prefers `terminalId` | + +Because pin coordinates are resolved to Wokwi pixel space at import time, an imported part **drops straight into `diagram.json`** and wires exactly like a hand-made or built-in part. There is no runtime dependency on Fritzing. + +### 4.2 What's still missing for "no difficulty" in-app import + +1. **`.fzpz` drag-and-drop in the app.** Today import is a build-time Node script against a repo checkout. Ship the same logic in the renderer (it's pure XML/string work; bundle `@xmldom/xmldom` or use the browser `DOMParser`, and `fflate` for the ZIP): drop a `.fzpz` onto the palette → unzip → parse `.fzp` → resolve pins from the bundled SVGs → `registerPart()` → **persist** (see B7). The only script feature needing care is path resolution (`.fzpz` bundles its SVGs with prefixed names like `svg.breadboard.foo.svg`, so section-searching isn't needed). +2. **Buses.** The importer drops `` entirely. Emit `buses: string[][]` (groups of pin names) into `PartDef` and pass a `busesFor` callback to `buildNets` (the parameter already exists). Without this, any imported part with internal common pins — most importantly *a breadboard* — nets wrongly. +3. **Female connectors.** Add `pinType: 'male' | 'female'` per pin. Needed for drop-to-connect breadboard behavior (Phase 2) and for better wire-end semantics; harmless otherwise. +4. **Bendable legs (`legId`).** Currently ignored; leg pins resolve to the drawn leg end, which is acceptable. Real rubber-band legs are a Phase 2 renderer feature; importer just needs to tag `leg: true` on those pins. +5. **SVG hygiene at import time** (fixes live rendering *and* PNG export): + - **Namespace all `id`s** (`-`) — Fritzing SVGs freely reuse ids (`connector0pin`, gradient ids), and multiple inlined parts on one page currently collide (bug B6). + - Inline `class`-based styles (some parts carry `' + const out = namespaceSvgIds(svg, 'pR1') + assert.ok(out.includes('#pR1-body{fill:blue}')) + assert.ok(out.includes('#other{}')) // not defined as an id here — untouched + assert.ok(out.includes('id="pR1-body"')) +}) diff --git a/src/renderer/src/circuit/parts/svg.ts b/src/renderer/src/circuit/parts/svg.ts index 220a0d2..e6d2daa 100644 --- a/src/renderer/src/circuit/parts/svg.ts +++ b/src/renderer/src/circuit/parts/svg.ts @@ -21,6 +21,15 @@ export function namespaceSvgIds(svg: string, ns: string): string { if (ids.size === 0) return svg const has = (id: string): boolean => ids.has(id) + // rewrite #id selectors inside