mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
Document the Schematic View in docs/ard
Add docs/ard/ (architecture & decision records): - README.md index - schematic-view-architecture.md: end-to-end overview - modules, file map, generation/sync pipeline, the ManhattanRouter, the tab + detached-window UI, how to add a factory, testing, known limitations and gotchas - 0001..0007: decision records for the embedded SchematicView, the factory SPI, wires-as-components + routing, the second-Presenter tab, the wire layer lock + reroute-on-move, the SchematicBox control-point model, and switch handling schematic_view_plan.md now points at docs/ard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FWD3QJxbr7RuEHxyFEYeEY
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# 0001 — Store the schematic as a lazy `SchematicView` on `Project`
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
The schematic derived from a layout needs to live somewhere. Options:
|
||||
|
||||
1. A sidecar file (`project.diy.schematic`).
|
||||
2. A separate top-level entity the app manages in parallel.
|
||||
3. A field on `Project`, serialized inside the `.diy` file.
|
||||
|
||||
Constraints:
|
||||
|
||||
- One file per project is a strong existing expectation.
|
||||
- The vast majority of users will never open the Schematic View; their files must not change.
|
||||
- Schematic edits (moving a symbol) should participate in undo/redo and the modified flag with
|
||||
no bespoke plumbing.
|
||||
- Older DIYLC versions must still open new files.
|
||||
|
||||
## Decision
|
||||
|
||||
Add `private SchematicView schematicView` to `Project`, serialized via XStream with the rest
|
||||
of the project.
|
||||
|
||||
- The field stays `null` until the user first opens the view. `Project.getSchematicView()`
|
||||
returns it as-is; `Project.getOrCreateSchematicView()` lazily allocates it.
|
||||
- `SchematicView` holds the schematic `components` (symbols + wires), a
|
||||
`Map<UUID, List<UUID>> physicalToSchematicMap` (physical component id → schematic symbol
|
||||
ids, 1:N), and canvas `Size`s.
|
||||
- `Project.clone()` and `Project.equals()` include the schematic view.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **+** Single file. No new file formats, no sync-of-two-files problem.
|
||||
- **+** Undo/redo and the dirty flag work for free: the app already snapshots `Project` via
|
||||
`clone()` and diffs via `equals()`.
|
||||
- **+** Backward compatible: a `null` field serializes to nothing; XStream on old versions
|
||||
ignores the unknown element.
|
||||
- **−** Opening the Schematic View calls `getOrCreateSchematicView()`, which flips the project
|
||||
to *modified* even if the user changes nothing. This is intentional (the plan wants the
|
||||
schematic to persist once generated) but is a surprising side effect.
|
||||
- **−** `SchematicView` in `equals()` slightly enlarges the cost of every undo-snapshot
|
||||
comparison once the view exists.
|
||||
- Pin mappings are deliberately **not** stored in `SchematicView` — they are a pure function
|
||||
of the physical component and are recomputed by re-running the factory. This keeps the
|
||||
serialized form small and avoids a second source of truth.
|
||||
@@ -0,0 +1,57 @@
|
||||
# 0002 — Per-type `ISchematicFactory`, declared on `@ComponentDescriptor`, with a generic fallback
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
Every physical component type needs to become one or more schematic symbols. The mapping is
|
||||
type-specific and sometimes complex:
|
||||
|
||||
- A resistor → one resistor symbol, pin 0 → pin 0, pin 1 → pin 1.
|
||||
- A dual-triode tube socket → two triode symbols, with the heater pins shared to one section.
|
||||
- A DIP IC → a box, or (for a dual op-amp) two op-amp symbols.
|
||||
|
||||
There are hundreds of component types. Most are simple; a handful are not. We need something
|
||||
scalable that keeps per-type logic with the type, and that still produces *something* for a
|
||||
type nobody has written a mapping for.
|
||||
|
||||
## Decision
|
||||
|
||||
A strategy interface plus an annotation attribute plus a fallback:
|
||||
|
||||
```java
|
||||
interface ISchematicFactory {
|
||||
List<SchematicSymbolMapping> createSchematicSymbols(IDIYComponent<?> physicalComponent);
|
||||
}
|
||||
```
|
||||
|
||||
- `SchematicSymbolMapping` = a fully configured symbol instance + `Map<Integer,Integer>`
|
||||
pin mapping (physical control-point index → schematic control-point index) + optional
|
||||
section label. The factory returns *ready* symbols; the builder only positions them.
|
||||
- Types opt in via `@ComponentDescriptor(schematicFactory = XxxSchematicFactory.class)`.
|
||||
`ComponentProcessor` copies it to `ComponentType.getSchematicFactoryClass()`. The annotation
|
||||
default is `ISchematicFactory.class` itself, used purely as a "none declared" marker.
|
||||
- **`GenericBoxSchematicFactory`** is the fallback for any type without a declared factory: it
|
||||
inspects the sticky, node-named control points and builds a `SchematicBox` with one labeled
|
||||
terminal per node, copying name and value.
|
||||
- `AbstractSimpleSchematicFactory` is a base for the common 1:1 case — subclasses just supply
|
||||
`createSymbol()` and (optionally) `electricalPinCount()`.
|
||||
|
||||
The interface and `SchematicSymbolMapping` live in `diylc-core` (the annotation references
|
||||
the interface). Concrete factories live in `diylc-library` next to the symbol classes they
|
||||
instantiate.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **+** Per-type logic lives with the type. Adding coverage is: write a factory class, add one
|
||||
annotation attribute. No central registry to edit.
|
||||
- **+** Full control for hard cases: the factory returns concrete instances, so it decides the
|
||||
symbol class, property values, and which sections get shared pins.
|
||||
- **+** Every component gets a symbol — no gaps, no crashes on unmapped types.
|
||||
- **−** The factory must be stateless (it is instantiated once and reused). This is a
|
||||
convention, not enforced.
|
||||
- **−** `diylc-core` gains an `org.diylc.schematic` package that `diylc-library` also uses
|
||||
(split package across jars). DIYLC already does this for `org.diylc.components`, so it is
|
||||
consistent, but it is a smell.
|
||||
- Pin mappings from the factory are recomputed on demand, never serialized (see
|
||||
[0001](0001-schematic-embedded-in-project.md)).
|
||||
@@ -0,0 +1,45 @@
|
||||
# 0003 — `SchematicWire` is a real component; connections are drawn, not implied
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
Nets need to be drawn as schematic wires with 90° bends. Two options:
|
||||
|
||||
1. **Wires as components:** a `SchematicWire` `IDIYComponent` stored in the `SchematicView`,
|
||||
drawn by the normal pipeline like any symbol.
|
||||
2. **Wires rendered on the fly:** compute routes from the netlist at paint time and draw them
|
||||
in a custom overlay; nothing is stored.
|
||||
|
||||
Option 2 is tempting because "there is nothing to select or drag, so it cannot be edited".
|
||||
|
||||
## Decision
|
||||
|
||||
Wires are components (`org.diylc.components.schematic.SchematicWire`).
|
||||
|
||||
- Implements `IContinuity` (so the connection is real to any downstream analysis).
|
||||
- Stores `sourceComponentId` + `sourcePinIndex`, `targetComponentId` + `targetPinIndex`, and
|
||||
the calculated `routePoints`. Control points are the route way-points; only the two endpoints
|
||||
are sticky; visibility is `NEVER`; `canPointMoveFreely` is `false`.
|
||||
- Z-order is `IDIYComponent.WIRING`. Symbols are `IDIYComponent.COMPONENT`.
|
||||
- `clone()` is overridden to deep-copy `routePoints` (the base clone only deep-copies
|
||||
`Point2D` / `Point2D[]`, not a `List`).
|
||||
|
||||
Routing is a separate pure class, `ManhattanRouter`:
|
||||
|
||||
- Straight segment when endpoints are axis-aligned; otherwise candidate L / HVH / VHV routes.
|
||||
- Each candidate scored on length, bend count, crossings with existing segments, collinear
|
||||
overlap length, and whether it leaves each pin in the expected direction
|
||||
(`SchematicBuilder.exitDirection` — away from the symbol centroid along the dominant axis).
|
||||
- Lowest score wins; result is de-duplicated and collinear middle points removed.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **+** Wires persist in the `.diy` file, are picked up by the existing image/PDF export
|
||||
paths, and can gain user-editable bend points later without a redesign.
|
||||
- **+** Routing runs once per generation / move, not on every repaint.
|
||||
- **+** Reuses the rendering pipeline — no custom paint overlay, no second code path.
|
||||
- **−** Wires are selectable/draggable by default in a `Presenter`. Making them inert needs an
|
||||
explicit mechanism — see [0005](0005-wire-layer-lock-and-reroute.md).
|
||||
- **−** The router is a heuristic scorer over a fixed candidate set, not a real grid/A\*
|
||||
router. It avoids overlap pairwise but is not globally crossing-aware.
|
||||
@@ -0,0 +1,56 @@
|
||||
# 0004 — The Schematic tab reuses the real canvas via a second `Presenter` + `CanvasPlugin`
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
The Schematic View lives on a tab next to Layout. It must render *exactly* like the layout
|
||||
canvas: rulers, scroll bars, mouse-wheel / trackpad-gesture zoom, the same look. Early
|
||||
iterations used a plain `JScrollPane` around a lightweight custom panel with its own zoom
|
||||
buttons; the user rejected that — same UI, no extra controls.
|
||||
|
||||
`CanvasPanel.paint()` calls `plugInPort.draw(...)`, `RulerScrollPane` is built around a
|
||||
`ProjectDrawingProvider(plugInPort, ...)`, and zoom is `plugInPort.setZoomLevel(...)`. In
|
||||
other words, the canvas is inseparable from a `Presenter`. `Presenter` is a ~3000-line class
|
||||
bound to a single `currentProject`.
|
||||
|
||||
## Decision
|
||||
|
||||
`SchematicTabPlugin` owns a **second `Presenter`** and a **second `CanvasPlugin`**:
|
||||
|
||||
```java
|
||||
schematicPresenter = new Presenter(swingUI, configManager, /*importVariantsAndBlocks*/ false);
|
||||
schematicCanvasPlugin = new CanvasPlugin(swingUI, configManager);
|
||||
schematicPresenter.installPlugin(() -> schematicCanvasPlugin); // injects its RulerScrollPane
|
||||
schematicPresenter.installPlugin(SchematicChangeListener::new); // reroute-on-move listener
|
||||
```
|
||||
|
||||
- Both `CanvasPlugin`s inject their `RulerScrollPane` into the same center `BoxLayout` panel;
|
||||
`SchematicTabPlugin` toggles which one is `setVisible(true)`. `CanvasPlugin` gained a
|
||||
`getCanvasScrollComponent()` accessor for this.
|
||||
- The tab strip is a `JToolBar` with two `JToggleButton`s, injected below the scroll panes.
|
||||
- Switching to Schematic: `SchematicSynchronizer.synchronize(layoutProject, continuityAreas)`,
|
||||
then wrap `layoutProject.getOrCreateSchematicView()` in a throwaway `Project` (shares the
|
||||
same component instances) and `schematicPresenter.loadProject(wrapper, true, null)`, then
|
||||
swap visibility and `scrollToCenterAndShowContents()`.
|
||||
- `IPlugInPort` / `INetlistProcessor` gained `getContinuityAreas()` (delegates to the
|
||||
drawing manager) so the tab can build the netlist without reaching into `Presenter`
|
||||
internals.
|
||||
|
||||
The detached *Analyze → Schematic View…* window (`SchematicViewFrame` / `SchematicPanel`)
|
||||
is a separate, `Presenter`-free read-only renderer kept for PNG export.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **+** Pixel-identical canvas: rulers, scroll, wheel/gesture zoom, theme — all for free.
|
||||
- **+** Independent zoom/scroll state per tab.
|
||||
- **+** Symbol selection and drag work through the standard `Presenter`.
|
||||
- **−** A second `Presenter` is heavyweight: it builds its own `DrawingManager`,
|
||||
`ProjectFileManager`, `InstantiationManager`, `VariantManager`, `BuildingBlockManager` and
|
||||
registers config listeners. `importVariantsAndBlocks=false` skips the expensive part.
|
||||
- **−** `loadProject` on the schematic presenter calls `DrawingCache.Instance.clear()`, a
|
||||
process-wide cache — a minor cross-effect on the layout canvas.
|
||||
- **−** The schematic canvas is a full editing `Presenter`; nothing yet blocks symbol
|
||||
add/delete/paste/value-edit, and its undo stack is separate from the layout's.
|
||||
- The tab and the detached window are two independent renderers of the same data. Acceptable
|
||||
redundancy; could be unified later.
|
||||
@@ -0,0 +1,48 @@
|
||||
# 0005 — Wires are made inert by locking the `WIRING` layer; they re-route on symbol move
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
On the schematic the user may move symbols but must **not** be able to touch wires — no
|
||||
selecting, no dragging, and above all no pulling a wire endpoint off a component. Symbols stay
|
||||
fully movable. Because wires are real components ([0003](0003-wires-as-components-manhattan-routing.md)),
|
||||
a `Presenter` will happily select and drag them.
|
||||
|
||||
There is no `IDIYComponent.isSelectable()` hook. The available levers are the project's
|
||||
`lockedComponents` set and `lockedLayers` set. `Presenter.isComponentLocked()` already gates
|
||||
click-select, rubber-band select, drag, and control-point grabbing off both.
|
||||
|
||||
## Decision
|
||||
|
||||
The wrapper `Project` used by the schematic tab locks the whole `WIRING` layer:
|
||||
|
||||
```java
|
||||
wrapper.getLockedLayers().add(IDIYComponent.WIRING);
|
||||
```
|
||||
|
||||
`SchematicWire` is the only thing on that layer (symbols are on `COMPONENT`), so every wire
|
||||
becomes inert while symbols stay editable.
|
||||
|
||||
Locked components render at 0.5 alpha (`DrawOption.LOCKED_ALPHA`, on by default), which would
|
||||
fade the wires. `SchematicWire.draw()` therefore forces `AlphaComposite.SrcOver` for its own
|
||||
drawing and restores the previous composite afterwards.
|
||||
|
||||
**Re-routing on move:** `SchematicTabPlugin` installs a tiny `IPlugIn` on the schematic
|
||||
presenter that listens for `EventType.PROJECT_MODIFIED` (dispatched once by `Presenter` when
|
||||
a drag-move ends). On that event it calls `SchematicBuilder.rerouteWires(components)`, which
|
||||
looks every wire's source/target symbols up by id, reads their current pin positions, and
|
||||
re-runs `ManhattanRouter`. A `sameRoute` check skips the repaint when nothing changed; a
|
||||
`rerouting` guard prevents re-entrancy.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **+** One line makes wires completely non-interactive; symbols are untouched.
|
||||
- **+** Uses the intended DIYLC mechanism (locked layers) rather than a new concept.
|
||||
- **+** Wires follow symbols the user drags around, live.
|
||||
- **−** `rerouteWires` recomputes *all* wires on every move, not just the affected ones. Fine
|
||||
for typical net counts; not optimized.
|
||||
- **−** The opaque-composite workaround in `SchematicWire.draw()` is a wart that exists only
|
||||
because the wire is drawn on a locked layer.
|
||||
- **−** If a future change puts something other than wires on the `WIRING` layer in the
|
||||
schematic, it would be locked too.
|
||||
@@ -0,0 +1,70 @@
|
||||
# 0006 — `SchematicBox` is a rigid multi-pin component; it must not recompute in `setControlPoint`
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
`SchematicBox` is a rectangle with N terminals on its four sides. Its pin positions are a
|
||||
function of an anchor (control point 0) and the per-side node counts. The natural
|
||||
implementation — "when the anchor moves, recompute all the pins" — was wired into
|
||||
`setControlPoint`:
|
||||
|
||||
```java
|
||||
// original, buggy
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
controlPoints[index].setLocation(point);
|
||||
if (index == 0) updateControlPoints(); // rebuilds the whole control-point array
|
||||
}
|
||||
```
|
||||
|
||||
This breaks against how `Presenter` moves a rigid component. For a component whose
|
||||
`canPointMoveFreely(i)` is `false`, the drag code adds **every** control-point index to the
|
||||
move set and then applies the delta one index at a time:
|
||||
|
||||
```java
|
||||
for (Integer index : indices) { // HashSet — order not guaranteed
|
||||
Point2D old = c.getControlPoint(index);
|
||||
c.setControlPoint(new Point2D.Double(old.getX() + dx, old.getY() + dy), index);
|
||||
}
|
||||
```
|
||||
|
||||
With the recompute in place, setting index 0 rebuilds pins 1..N relative to the new anchor;
|
||||
the loop then reads those *already moved* pins and adds the delta again. Pins ended up moved
|
||||
by 2× the drag, by different amounts depending on the `HashSet` iteration order — "control
|
||||
points randomly move off the edge".
|
||||
|
||||
The same shape of bug hit `SchematicBuilder.moveAnchorTo` during initial placement: it read
|
||||
`getControlPoint(i)` inside its loop after setting index 0, so box pins were double-offset,
|
||||
which is why the first generation put every wire on one node until the user nudged the symbol.
|
||||
|
||||
`ICSymbol` — the component `SchematicBox` was modelled on — does **not** have this problem: its
|
||||
`setControlPoint` only sets the one point, and it relies on `canPointMoveFreely == false` for
|
||||
the presenter to translate all points together.
|
||||
|
||||
## Decision
|
||||
|
||||
Follow the `ICSymbol` model exactly:
|
||||
|
||||
- `SchematicBox.setControlPoint(point, index)` moves **only** that point. No recompute.
|
||||
- `updateControlPoints()` runs only from the constructor and the node-list setters
|
||||
(`setLeftNodes` etc.). When the pin count is unchanged it mutates the existing `Point2D`
|
||||
objects in place (so references held by drawing/selection code stay valid); it allocates a
|
||||
new array only when the count changes.
|
||||
- `SchematicBuilder.moveAnchorTo` snapshots every target position *before* mutating any
|
||||
control point, so a component that recomputes on set cannot be double-offset.
|
||||
|
||||
During a drag the presenter translates every pin by the same delta, which turns a valid box
|
||||
configuration into another valid one — the pins stay on the (translated) edge without any
|
||||
recompute.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **+** Boxes drag cleanly; initial generation and re-sync produce identical routing.
|
||||
- **+** `moveAnchorTo` is now robust for any current or future component that recomputes on
|
||||
`setControlPoint`.
|
||||
- **−** If someone moves *only* control point 0 of a `SchematicBox` programmatically (not via
|
||||
the presenter's rigid-drag path), the body will move but the pins will not. Nothing in the
|
||||
codebase does this, but it is a latent sharp edge.
|
||||
- **Rule of thumb:** a rigid multi-control-point component must never mutate points other than
|
||||
the one passed to `setControlPoint`. Geometry recompute belongs in explicit
|
||||
structure-changing setters, not in `setControlPoint`.
|
||||
@@ -0,0 +1,41 @@
|
||||
# 0007 — Switches are drawn as symbols, not treated as connections
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
`SchematicBuilder.isEligible()` excludes `IContinuity` implementors from the schematic — wires,
|
||||
traces, solder bridges, board strips are connections, not symbols.
|
||||
|
||||
But **`ISwitch extends IContinuity`**. So the naive check silently dropped every switch from
|
||||
the schematic, even though the netlist still contained the switch's terminals with real
|
||||
connections. Symptom: switches present in the generated netlist, invisible on the diagram.
|
||||
|
||||
The netlist is built with `NetlistBuilder.extractNetlists(includeSwitches = false, ...)`. In
|
||||
that mode `NetlistBuilder` itself treats a switch as a regular component (a node per terminal),
|
||||
using the guard `c instanceof IContinuity && !(c instanceof ISwitch)` to decide what is a pure
|
||||
connection. The schematic filter has to match that logic.
|
||||
|
||||
## Decision
|
||||
|
||||
`isEligible()` returns `true` for `ISwitch` *before* the `IContinuity` check:
|
||||
|
||||
```java
|
||||
if (component instanceof ICommonNode) return true;
|
||||
if (component instanceof ISwitch) return true; // drawn as a symbol
|
||||
if (component instanceof IContinuity) return false; // wires, traces, ...
|
||||
```
|
||||
|
||||
There is no dedicated switch factory yet, so a switch falls through to
|
||||
`GenericBoxSchematicFactory` — a labeled box with one terminal per lug — and is wired like any
|
||||
other symbol.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **+** Switches appear on the schematic with their connections, matching the netlist.
|
||||
- **+** The schematic filter and `NetlistBuilder`'s own switch logic now agree.
|
||||
- **−** A switch renders as a generic box, not a proper switch symbol. A
|
||||
`SwitchSchematicFactory` → `SwitchLatchingSymbol` (or a rotary/multi-pole symbol) is future
|
||||
work.
|
||||
- **General principle:** anywhere the code says "exclude `IContinuity`", check whether it
|
||||
should special-case `ISwitch`.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Architecture & Decision Records (ARD)
|
||||
|
||||
This folder documents non-obvious design decisions in DIYLC so that a person — or an
|
||||
AI assistant — picking up the code later can understand *why* it looks the way it does,
|
||||
not just *what* it does.
|
||||
|
||||
Each record is self-contained and uses a fixed shape:
|
||||
|
||||
- **Status** — accepted / superseded / proposed
|
||||
- **Context** — the problem and the constraints that forced a choice
|
||||
- **Decision** — what we did
|
||||
- **Consequences** — what this buys us, what it costs, and the traps to watch for
|
||||
|
||||
## Index
|
||||
|
||||
### Schematic View
|
||||
|
||||
The Schematic View turns a physical layout into a schematic diagram, on demand, reusing
|
||||
the existing rendering pipeline. Start with the architecture overview, then the records.
|
||||
|
||||
| Doc | Topic |
|
||||
|---|---|
|
||||
| [`schematic-view-architecture.md`](schematic-view-architecture.md) | End-to-end overview: modules, files, the generation pipeline, routing, the UI, how to extend it, testing, limitations |
|
||||
| [`0001-schematic-embedded-in-project.md`](0001-schematic-embedded-in-project.md) | Storing the schematic as a lazy `SchematicView` field on `Project` |
|
||||
| [`0002-schematic-factory-spi.md`](0002-schematic-factory-spi.md) | `@ComponentDescriptor.schematicFactory` + `ISchematicFactory`, with a generic box fallback |
|
||||
| [`0003-wires-as-components-manhattan-routing.md`](0003-wires-as-components-manhattan-routing.md) | `SchematicWire` as a real component; the `ManhattanRouter` |
|
||||
| [`0004-schematic-tab-second-presenter.md`](0004-schematic-tab-second-presenter.md) | The Layout \| Schematic tab reuses the real canvas via a second `Presenter` + `CanvasPlugin` |
|
||||
| [`0005-wire-layer-lock-and-reroute.md`](0005-wire-layer-lock-and-reroute.md) | Wires are made non-interactive by locking the `WIRING` layer; they re-route on symbol move |
|
||||
| [`0006-schematicbox-control-point-model.md`](0006-schematicbox-control-point-model.md) | Why `SchematicBox` must behave as a rigid multi-pin component |
|
||||
| [`0007-switch-handling.md`](0007-switch-handling.md) | Switches (`ISwitch extends IContinuity`) are drawn as symbols, not treated as wires |
|
||||
|
||||
The original feature specification and its running status live in
|
||||
[`../schematic_view_plan.md`](../schematic_view_plan.md).
|
||||
@@ -0,0 +1,251 @@
|
||||
# Schematic View — Architecture Overview
|
||||
|
||||
## What it does
|
||||
|
||||
DIYLC projects are *physical layouts* — components placed on a board with leads, traces and
|
||||
wires. The Schematic View derives a **schematic diagram** from that layout on demand: every
|
||||
real component becomes a schematic symbol, every net becomes an auto-routed wire, and the
|
||||
result is drawn with the same rendering pipeline as the layout.
|
||||
|
||||
It is **derived, not authored**: the user does not draw the schematic. They can rearrange
|
||||
symbols; wires follow. Switching to the Schematic tab (re)generates it.
|
||||
|
||||
## Design principles
|
||||
|
||||
1. **Reuse the rendering pipeline.** Schematic symbols are ordinary `IDIYComponent`s drawn by
|
||||
the same `DrawingManager` that draws layout components. Wires are components too.
|
||||
2. **Per-type mapping, with a fallback.** Each physical component type can name an
|
||||
`ISchematicFactory`; anything without one gets a generic labeled box. Every component gets
|
||||
*some* symbol.
|
||||
3. **The layout is the source of truth.** The schematic is regenerated / reconciled from the
|
||||
layout + its netlist. User edits to the schematic are limited to symbol position.
|
||||
4. **Zero cost when unused.** The `SchematicView` field on `Project` stays `null` until the
|
||||
user first opens the view, so existing `.diy` files are byte-for-byte unchanged.
|
||||
|
||||
## Module layout
|
||||
|
||||
DIYLC has three Maven modules; `diylc-library` depends on `diylc-core`, `diylc-swing` depends
|
||||
on both. The Schematic View is spread across all three:
|
||||
|
||||
```
|
||||
diylc-core data model + SPI (no rendering, no Swing)
|
||||
org.diylc.core.SchematicView embedded in Project
|
||||
org.diylc.core.annotations.ComponentDescriptor.schematicFactory()
|
||||
org.diylc.common.ComponentType.schematicFactoryClass
|
||||
org.diylc.schematic.ISchematicFactory the strategy interface
|
||||
org.diylc.schematic.SchematicSymbolMapping factory output: symbol + pin map
|
||||
org.diylc.common.INetlistProcessor.getContinuityAreas() (impl on Presenter)
|
||||
|
||||
diylc-library components + generation logic
|
||||
org.diylc.components.schematic.SchematicBox generic rectangular symbol
|
||||
org.diylc.components.schematic.SchematicWire auto-routed Manhattan wire (IContinuity)
|
||||
org.diylc.schematic.ManhattanRouter routing algorithm (pure)
|
||||
org.diylc.schematic.GenericBoxSchematicFactory default fallback
|
||||
org.diylc.schematic.AbstractSimpleSchematicFactory 1:1 pin-for-pin base
|
||||
org.diylc.schematic.{Resistor,Capacitor,Diode,Transistor,Inductor,Potentiometer}SchematicFactory
|
||||
org.diylc.schematic.SchematicBuilder initial generation
|
||||
org.diylc.schematic.SchematicSynchronizer incremental reconcile
|
||||
|
||||
diylc-swing UI
|
||||
org.diylc.swing.plugins.schematic.SchematicTabPlugin Layout | Schematic tab strip
|
||||
org.diylc.swing.plugins.schematic.SchematicMenuPlugin Analyze > Schematic View...
|
||||
org.diylc.swing.plugins.schematic.SchematicViewFrame detached viewer window
|
||||
org.diylc.swing.plugins.schematic.SchematicPanel read-only renderer for the window
|
||||
org.diylc.swing.plugins.canvas.CanvasPlugin.getCanvasScrollComponent() (new accessor)
|
||||
```
|
||||
|
||||
## Data model
|
||||
|
||||
### `SchematicView` (embedded in `Project`)
|
||||
|
||||
```java
|
||||
class SchematicView implements Serializable, Cloneable {
|
||||
List<IDIYComponent<?>> components; // symbols + wires, z-order ascending
|
||||
Map<UUID, List<UUID>> physicalToSchematicMap; // physical component id -> schematic symbol id(s)
|
||||
Size width, height, gridSpacing;
|
||||
}
|
||||
```
|
||||
|
||||
- `Project.getSchematicView()` returns the field (may be `null`).
|
||||
- `Project.getOrCreateSchematicView()` lazily creates it — **calling this makes the schematic
|
||||
persist with the project from now on**.
|
||||
- `Project.clone()` and `Project.equals()` include the schematic view, so schematic edits go
|
||||
through the normal dirty / undo-snapshot machinery.
|
||||
- Serialized via XStream alongside the rest of `Project`. Older DIYLC ignores the unknown field.
|
||||
|
||||
The `physicalToSchematicMap` is 1:N to support multi-section components (a dual triode → two
|
||||
triode symbols). Pin mappings are **not** stored — they are a pure function of the physical
|
||||
component and are recomputed by re-running the factory whenever needed.
|
||||
|
||||
### `ISchematicFactory` / `SchematicSymbolMapping`
|
||||
|
||||
```java
|
||||
interface ISchematicFactory {
|
||||
List<SchematicSymbolMapping> createSchematicSymbols(IDIYComponent<?> physicalComponent);
|
||||
}
|
||||
|
||||
class SchematicSymbolMapping {
|
||||
IDIYComponent<?> schematicSymbol; // fully configured, only needs positioning
|
||||
Map<Integer, Integer> pinMapping; // physical control-point index -> schematic control-point index
|
||||
String sectionLabel; // optional, e.g. "Section A"
|
||||
}
|
||||
```
|
||||
|
||||
A type declares its factory on the annotation:
|
||||
|
||||
```java
|
||||
@ComponentDescriptor(name = "Resistor", ..., schematicFactory = ResistorSchematicFactory.class)
|
||||
```
|
||||
|
||||
`ComponentProcessor.extractComponentTypeFrom` reads it into
|
||||
`ComponentType.getSchematicFactoryClass()`. The default (`ISchematicFactory.class` itself,
|
||||
used only as a "none" marker) means **no factory declared → use the generic box**.
|
||||
|
||||
## Generation pipeline — `SchematicBuilder.build(project, continuityAreas)`
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Project + ContinuityAreas] --> B["NetlistBuilder.extractNetlists(includeSwitches=false)"]
|
||||
B --> C[single Netlist: Groups of Nodes]
|
||||
A --> D[for each component: isEligible?]
|
||||
D -->|ICommonNode| E[clone through as-is]
|
||||
D -->|ISwitch or has sticky pins| F["resolve factory (declared or GenericBox)"]
|
||||
D -->|IContinuity and not ISwitch, or no sticky pins| G[excluded, silently]
|
||||
F --> H["factory.createSchematicSymbols() -> SymbolEntry{symbol, pinMapping}"]
|
||||
E --> I[record physicalToSchematicMap]
|
||||
H --> I
|
||||
I --> J["placeSymbols: degree-sorted grid (moveAnchorTo)"]
|
||||
J --> K["createWires: per Netlist Group, chain the schematic pins, ManhattanRouter.route each pair"]
|
||||
K --> L[sort by z-order, store into SchematicView, resize canvas to fit]
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- **Eligibility** (`SchematicBuilder.isEligible`):
|
||||
- `ICommonNode` (ground, common node) → eligible, cloned through unchanged.
|
||||
- `ISwitch` → eligible (drawn as a symbol; see [0007](0007-switch-handling.md)).
|
||||
- other `IContinuity` (wires, traces, solder bridges, board strips) → excluded.
|
||||
- no sticky control points (labels, images, shapes, boards) → excluded.
|
||||
- Exclusions are silent — no warning.
|
||||
- **Factory resolution** caches one instance per factory class. Factory exceptions fall back to
|
||||
the generic box.
|
||||
- **Placement** sorts components by connection degree (sum over netlist groups of
|
||||
`groupSize - 1`) descending, then drops them into a `ceil(sqrt(n))`-column grid. Multi-symbol
|
||||
mappings stack vertically within the cell. `moveAnchorTo` translates *every* control point so
|
||||
control point 0 lands on the target — it snapshots targets first, see
|
||||
[0006](0006-schematicbox-control-point-model.md).
|
||||
- **Wiring** walks each `Group`, resolves every `Node` (physical component + pin index) to a
|
||||
schematic `(symbol, schematicPinIndex, location)` via the factory's `pinMapping`, sorts the
|
||||
pins left-to-right/top-to-bottom, and creates a `SchematicWire` between consecutive pins
|
||||
(a chain, not a star). Each wire stores `sourceComponentId/sourcePinIndex` and
|
||||
`targetComponentId/targetPinIndex` plus the routed `routePoints`. Already-placed wire
|
||||
segments are fed to the router as obstacles.
|
||||
|
||||
## Incremental sync — `SchematicSynchronizer.synchronize(project, continuityAreas)`
|
||||
|
||||
Runs every time the user switches to the Schematic tab. If the view is empty it does a full
|
||||
`SchematicBuilder.build`. Otherwise:
|
||||
|
||||
- **Kept** components: re-run the factory to get fresh symbols + pin maps, but keep the
|
||||
*existing* positioned symbol instance, only refreshing its name/value. (Pin maps are pure
|
||||
functions of the physical component, so recomputing is safe and cheaper than persisting.)
|
||||
- **New** components (id in layout, not in map): place fresh symbols in the next free grid cell.
|
||||
- **Removed** components (id in map, not in layout): drop their symbols.
|
||||
- **Wires** are always rebuilt from scratch from the current netlist — they are cheap.
|
||||
|
||||
## Routing — `ManhattanRouter`
|
||||
|
||||
Pure, no dependencies on the rest of the system. `route(start, end, startDir, endDir,
|
||||
obstacles, grid)` returns axis-aligned way-points from `start` to `end`.
|
||||
|
||||
- Aligned endpoints → a single straight segment.
|
||||
- Otherwise it generates candidate routes: two L shapes, several
|
||||
horizontal-vertical-horizontal routes (vertical middle leg at candidate x positions) and
|
||||
several vertical-horizontal-vertical routes, using midpoint / quarter points / grid offsets.
|
||||
- Each candidate is scored: `length*0.01 + bends*6 + crossings*15 + overlapLength*0.5 +
|
||||
directionMismatchPenalty`. `startDir` / `endDir` are the directions the wire should leave
|
||||
each pin (away from the symbol centroid, dominant axis — `SchematicBuilder.exitDirection`).
|
||||
- Lowest score wins; the result is de-duplicated and collinear middle points are dropped.
|
||||
|
||||
`SchematicBuilder.rerouteWires(components)` re-routes every wire from the *current* symbol pin
|
||||
positions (looked up by component id) — used after the user moves a symbol.
|
||||
|
||||
## UI
|
||||
|
||||
### The tab — `SchematicTabPlugin`
|
||||
|
||||
The tab renders through the **real canvas**, not a bespoke widget. See
|
||||
[0004](0004-schematic-tab-second-presenter.md).
|
||||
|
||||
- The plugin owns a second `Presenter` and a second `CanvasPlugin`, installed on that
|
||||
presenter. Both `CanvasPlugin`s inject their `RulerScrollPane` into the center `BoxLayout`
|
||||
panel; the plugin shows exactly one at a time.
|
||||
- A `JToolBar` with two `JToggleButton`s (`Layout` / `Schematic`) is injected below them.
|
||||
**No zoom or refresh buttons** — rulers, scroll bars and wheel/gesture zoom come from the
|
||||
`RulerScrollPane`; refresh is automatic on tab switch.
|
||||
- Switching to **Schematic**: `SchematicSynchronizer.synchronize(...)` on the layout project,
|
||||
wrap the resulting `SchematicView` in a throwaway `Project`, `loadProject()` it into the
|
||||
schematic presenter, swap the visible scroll pane, `scrollToCenterAndShowContents()`.
|
||||
- The wrapper project **locks the `WIRING` layer** so wires are inert; a listener on the
|
||||
schematic presenter re-routes wires on `PROJECT_MODIFIED` (fires once when a drag-move ends).
|
||||
See [0005](0005-wire-layer-lock-and-reroute.md).
|
||||
|
||||
### The detached window — `SchematicMenuPlugin` / `SchematicViewFrame` / `SchematicPanel`
|
||||
|
||||
*Analyze → Schematic View…* opens a separate window that renders the `SchematicView` through
|
||||
a private `DrawingManager` (read-only, no `Presenter`) and adds PNG export. It is independent
|
||||
of the tab and is the simplest possible viewer.
|
||||
|
||||
## How to add a dedicated symbol for a component type
|
||||
|
||||
1. Write an `ISchematicFactory` in `diylc-library` under `org.diylc.schematic`. For a simple
|
||||
1:1 mapping, extend `AbstractSimpleSchematicFactory` and just return `new XxxSymbol()` from
|
||||
`createSymbol()` (override `electricalPinCount()` if not 2).
|
||||
2. Point the physical component at it: add `schematicFactory = XxxSchematicFactory.class` to
|
||||
its `@ComponentDescriptor` and import the class.
|
||||
3. If the symbol's pin order does not match the physical component's control-point order,
|
||||
build the `pinMapping` explicitly instead of using the base class.
|
||||
4. For multi-section parts (dual triode, dual op-amp) return more than one
|
||||
`SchematicSymbolMapping`, each with its own `pinMapping` and a `sectionLabel`.
|
||||
|
||||
No other wiring is needed — `SchematicBuilder` discovers the factory through `ComponentType`.
|
||||
|
||||
## Testing
|
||||
|
||||
Pure-logic pieces have unit tests in `diylc-library/src/test/java/org/diylc/schematic/`:
|
||||
|
||||
- `ManhattanRouterTest` — straight vs. bent routes, axis-alignment, endpoint fidelity.
|
||||
- `GenericBoxSchematicFactoryTest` — pin count / labels, switch eligibility.
|
||||
- `SchematicRerouteTest` — wire endpoints follow a moved symbol; `SchematicBox` drag/placement
|
||||
geometry (every pin translated by exactly the drag delta, in any index order).
|
||||
|
||||
`SchematicViewIntegrationTest` (in `diylc-swing`) exercises build → sync → render end to end
|
||||
through a real `Presenter`. **Note:** the `diylc-swing` test module currently fails to resolve
|
||||
`org.reflections` in some local setups (pre-existing — `PresenterTests` fails the same way),
|
||||
so this test may not run locally; it is expected to run in CI.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- The schematic canvas blocks wire interaction, but not symbol add / delete / paste /
|
||||
value-edit; there is no shared undo with the layout history.
|
||||
- `rerouteWires` recomputes *all* wires on every move; routing is not globally crossing-aware.
|
||||
- Placement is a degree-sorted grid, not signal-flow or true clustering.
|
||||
- Multi-section (1:N) factories for tubes, dual op-amps and multi-pole switches are not done —
|
||||
those fall back to the generic box.
|
||||
- 3-pin factories (`Transistor`, `Potentiometer`) map legs to symbol legs in index order,
|
||||
which may not match every package's pin-out.
|
||||
- No print or PDF/SVG export for the schematic (PNG only, from the detached window).
|
||||
- The schematic symbols do not follow rotation/mirroring cleanly (`SchematicBox` derives its
|
||||
body from an axis-aligned anchor).
|
||||
|
||||
## Gotchas for future work
|
||||
|
||||
- `ISwitch extends IContinuity`. Any "exclude `IContinuity`" check must special-case switches.
|
||||
- `SchematicView` participating in `Project.equals()` means opening the schematic marks the
|
||||
project modified (intended, but surprising).
|
||||
- A rigid multi-pin component (`SchematicBox`, `ICSymbol`) must **not** recompute its other
|
||||
control points inside `setControlPoint` — the presenter drags all points one at a time and a
|
||||
recompute double-applies the delta. See [0006](0006-schematicbox-control-point-model.md).
|
||||
- Component source files in this repo have mixed CRLF/LF line endings and ISO-8859-1 encoding.
|
||||
Bulk-editing them with a tool that normalizes newlines produces enormous phantom diffs —
|
||||
preserve the original newline style per file.
|
||||
@@ -1,5 +1,9 @@
|
||||
# DIYLC Schematic View — Feature Specification
|
||||
|
||||
> This is the original spec plus a running status section (§11). For how the shipped code is
|
||||
> actually put together and *why*, see [`ard/schematic-view-architecture.md`](ard/schematic-view-architecture.md)
|
||||
> and the decision records in [`ard/`](ard/README.md).
|
||||
|
||||
## Overview
|
||||
|
||||
Add the ability to generate and display a schematic diagram representation of a physical layout project at any time. The schematic is derived from the existing netlist infrastructure and rendered using DIYLC's existing rendering pipeline. Users can view, rearrange, and export the schematic to verify their circuit at a glance.
|
||||
|
||||
Reference in New Issue
Block a user