mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 02:32:09 +10:00
bc5f203e21
* initial * Updates * improvements * split netlist from analysis * coordinates in grid * Update IChatbotAPI.java * cleanup * cleanup * various improvements * Datasheet model support * Include pinouts in component catalog * Update update.xml * regenerate catalog * Zooming out until the drawing is smaller than the window made the toolbar above the canvas grow taller and squeeze the canvas * refresh * edit improvements * refine wire handling * CR * catalog gen improvement * improvements * Rotating and mirroring the selection with 'Sticky Points' enabled now keeps the circuit connected * fix version reading * set dimensions and mode in any order * add AINote * add aiNote * board notes added * ai notes * ai notes * ai notes * led minor code improvement and ai note * single coil fix. guitar component notes * add support for replace operation * Update pom.xml * switch notes * ai notes for pickups * stingray * reconnect operation support * ai edit code refactor * Cliff jack node name fixes * ai notes * ai notes * ai notes * netlist improvement * simplify * solder lug improvements * tag strip enhancement * regression fixes * ai notes * battery snap improvement * more sample questions * 'IEC Socket', 'Plastic DC Jack', 'Multi-Section Capacitor' and 'Mini Relay' terminals are now labeled by function * node improvement * Regression data consolidation --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
314 lines
17 KiB
Markdown
314 lines
17 KiB
Markdown
# Component Pinout Catalog
|
|
|
|
Teaching the LLM what terminals a component type has before any instance of it exists.
|
|
|
|
## 1. Goal
|
|
|
|
`ComponentCatalogGenerator` produces `catalog_full.json` and `catalog_index.json`
|
|
offline. The files are uploaded once to the diy-fever.com backend, which reads
|
|
out only the entries it needs when serving a request. Each entry today carries
|
|
the type's identity, its category, its creation method and its editable
|
|
properties.
|
|
|
|
It carries nothing about terminals. For components already on the canvas the
|
|
model gets terminals from `AiProjectBuilder`, which emits an `AiTerminal` per
|
|
sticky control point with its index, its node name and its position. For a type
|
|
the model has never seen instantiated there is no equivalent, so when it writes
|
|
an `AiEditOperation` with `fromTerminal: "SW1.3"` it is guessing.
|
|
|
|
The guess is only safe for the trivial cases. A resistor is always two pins. An
|
|
axial electrolytic is `+` and `-` when polarized and `1`/`2` when not. A mini
|
|
toggle switch is anywhere from two to fifteen lugs depending on its `Type`, and
|
|
which lug does what changes with it.
|
|
|
|
This document plans a `pinout` block on every catalog entry that needs one,
|
|
derived from the code rather than declared by hand.
|
|
|
|
## 2. What the code actually does
|
|
|
|
The numbers below come from a probe run over the whole library: instantiate
|
|
every discovered `ComponentType`, sweep every non-read-only enum and boolean
|
|
editable property one value at a time, and record the ordered list of sticky
|
|
control points with their node names.
|
|
|
|
| Measurement | Value |
|
|
| --- | --- |
|
|
| Component types discovered by `ComponentProcessor` | 144 |
|
|
| `@ComponentDescriptor` annotations in the source tree | 153 |
|
|
| Types whose sticky pinout changes with at least one property | 51 |
|
|
| Types with more than one independent driver | 8 |
|
|
| Types implementing `ISwitch` | 12 |
|
|
|
|
The first sweep covered enums and booleans only and found 49. Adding the integer sweep of §2
|
|
brought in `TagStrip` and a second driver on `TerminalStrip`.
|
|
|
|
The eight multi-driver types are `DIL_IC` (pin count and numbering scheme),
|
|
`SIL_IC`, `SubminiTube` (lead count, folded, pin arrangement),
|
|
`SwitchLatchingSymbol` (poles and configuration), `AudioTransformer` (two
|
|
booleans), and the tube symbols `TriodeSymbol`, `PentodeSymbol` and
|
|
`DuoDiodeSymbol` (heaters and directly-heated, plus suppressor grid on the
|
|
pentode).
|
|
|
|
Four facts from the probe shape everything below.
|
|
|
|
**Sticky is the only filter that matches the project view.** Raw
|
|
`getControlPointCount()` over-counts. The moveable label anchor on most leaded
|
|
components is a control point, so a plain resistor reports three points, not
|
|
two. `AiProjectBuilder.mapComponent` already skips non-sticky points, and the
|
|
catalog must use the identical filter or the indices in the catalog will not be
|
|
the indices the model sends back in a terminal reference. Filtering on sticky
|
|
also removes the false driver "Moveable Label", which otherwise shows up on
|
|
thirty-two types that have no electrical variation at all.
|
|
|
|
**Indices are not contiguous.** `RotarySwitchOpen` and `RotarySwitchSealed`
|
|
have a non-sticky point at index 0, so their first terminal is index 1.
|
|
`TubeSocket` is the same. The pinout must carry explicit indices, not a bare
|
|
ordered list.
|
|
|
|
**Some drivers are unbounded integers.** `TerminalStrip.getTerminalCount()`,
|
|
`TagStrip.getTerminalCount()`, `PinHeader.getRowCount()` and
|
|
`PinHeader.getColumnCount()` are plain `int` editable properties that call
|
|
`updateControlPoints()`. There is no enum to enumerate. Any design that assumes
|
|
a finite value domain silently omits these.
|
|
|
|
**Some node names are unusable as pin labels.** See §6.
|
|
|
|
## 3. Design decisions
|
|
|
|
| # | Decision | Choice | Rationale |
|
|
|---|---|---|---|
|
|
| D1 | How drivers are identified | Derived by probing, not declared by annotation | An annotation is correct in spirit but rots. There is no test that fails when a new component ships without it, and 8 of 49 types need more than one annotation anyway. Derivation re-reads the truth on every build. |
|
|
| D2 | Which points count as terminals | Sticky control points only | Identical filter to `AiProjectBuilder.mapComponent`, so catalog indices and project indices agree. |
|
|
| D3 | Property domains swept | Enum, `Boolean`/`boolean`, and integer properties sampled over a small range | Covers all 49 observed cases plus the four integer drivers the enum-only sweep misses. |
|
|
| D4 | Emission shapes | `fixed`, `variants`, `parametric` | Three genuinely different situations. Forcing all three into one table is what makes the file large. |
|
|
| D5 | Regular label runs | Emitted as a rule, not a table | `DIL_IC` is 24 pin counts times 5 numbering schemes. Its labels are a formula, so it collapses to one line instead of 120 variants. Irregular label sets, such as the 15 `TransistorPinout` values, stay a table. |
|
|
| D6 | Multiple drivers | Cartesian product, deduplicated by signature, capped at 64 combinations | Product order is small once D5 removes the regular cases. The cap is a guard, not an expected path. |
|
|
| D7 | Switch behaviour | A `switching` block per pinout variant, mirroring `AiSwitch` | Knowing a DPDT-on-on-on has six lugs is useless without knowing which lugs bridge in which position. The same probe produces it from `ISwitch.arePointsConnected`. |
|
|
| D8 | Unusable node names | Fixed at source in the eight offending components | Preferred over a heuristic in the generator. Changes netlist text output, so it needs a regression run. See §6. |
|
|
| D9 | Escape hatch | `@EditableProperty(pinoutDriver = ...)` with `AUTO`, `FORCE`, `EXCLUDE` | Derivation will be wrong somewhere. The annotation stays in the design as an override, not as the mechanism. Default `AUTO` means nothing has to be touched. |
|
|
| D10 | Preventing rot | A snapshot test in `diylc-library` asserting the derived driver set per type | This is the safety the annotation was really buying. Adding a component with an undeclared new driver fails the build until the snapshot is regenerated. |
|
|
| D11 | Where the analyzer lives | `diylc-core`, beside `ComponentCatalogGenerator` | It touches only `IDIYComponent`, `PropertyWrapper` and `ComponentProcessor`. No concrete component type is referenced, so the core boundary holds. |
|
|
| D12 | Boards | No pinout block at all | `AbstractBoard.isControlPointSticky` returns `false` for every index, so the sticky filter yields zero terminals and the entry is simply omitted. Components are placed on a board by coordinate, not by terminal. |
|
|
|
|
### Explicitly out of scope
|
|
|
|
- Relative pin geometry in the catalog. The model currently places a multi-pin
|
|
component by a single anchor and then wires to terminals by reference, which
|
|
`AiEditScriptEditor` resolves against the real instance. Pin offsets would
|
|
help it reason about spacing, but that is a separate change.
|
|
- The datasheet axis. Applying a datasheet model to an electrolytic can flip
|
|
`Polarized` and therefore the pinout, because `applyModel` calls
|
|
`setPolarized(!d.getNonPolarized())`. The pinout block describes the
|
|
`Polarized` property honestly; it does not enumerate which datasheet rows set
|
|
it. Five component types are affected.
|
|
- Changing how the server prompts with this data.
|
|
|
|
## 4. The three shapes
|
|
|
|
A description of one set of terminals carries exactly one of three key groups, so each key has a
|
|
stable type wherever it appears:
|
|
|
|
| Keys | Meaning |
|
|
| --- | --- |
|
|
| `pins` | An explicit list of `{ i, label }`, used whenever the labels follow no rule or the indices are not contiguous. |
|
|
| `pinCount` plus `labels` | A count and a rule, where `labels` is `sequential` or `none`. |
|
|
| `pinCountExpression` plus `labels` plus `pinCountSamples` | A formula over numeric properties, with a few worked values as evidence. |
|
|
|
|
An `ISwitch` also carries `switching`, which always names the `positionCount` and lists `positions`
|
|
when there are few enough of them to be worth reading.
|
|
|
|
### Fixed
|
|
|
|
65 of 144 types. One description, no drivers, emitted at the root of `pinout`.
|
|
|
|
```json
|
|
"pinout": {
|
|
"pins": [ { "i": 0, "label": "A" }, { "i": 1, "label": "K" } ]
|
|
}
|
|
```
|
|
|
|
### Variants
|
|
|
|
Finite drivers. One entry per combination.
|
|
|
|
This is what the mini toggle switch actually produces, abbreviated to two of its sixteen types:
|
|
|
|
```json
|
|
"pinout": {
|
|
"drivers": ["Type"],
|
|
"variants": [
|
|
{
|
|
"when": { "Type": "SPST" },
|
|
"pinCount": 2,
|
|
"labels": "sequential",
|
|
"switching": {
|
|
"positionCount": 2,
|
|
"positions": [
|
|
{ "name": "ON1", "connected": [[0, 1]] },
|
|
{ "name": "ON2", "connected": [] }
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"when": { "Type": "DPDT" },
|
|
"pinCount": 6,
|
|
"labels": "sequential",
|
|
"switching": {
|
|
"positionCount": 2,
|
|
"positions": [
|
|
{ "name": "ON1", "connected": [[0, 1], [3, 4]] },
|
|
{ "name": "ON2", "connected": [[1, 2], [4, 5]] }
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
`when` carries enum constant names, matching the `possibleValues` already
|
|
emitted for the property and matching what `AiEditScriptEditor.parsePropertyValue`
|
|
accepts.
|
|
|
|
### Parametric
|
|
|
|
A numeric property drives the count. The generator samples it, fits a formula, and verifies the
|
|
fit against every sample before emitting it. A driver counts as numeric when it is an `int`
|
|
property, or an enum whose constants read as numbers either through `toString` or through a
|
|
`getValue` or `getCount` accessor. That last case is what collapses the dual inline package from
|
|
120 combinations to five.
|
|
|
|
```json
|
|
"pinout": {
|
|
"drivers": ["Center Terminal", "Terminals"],
|
|
"variants": [
|
|
{
|
|
"when": { "Center Terminal": "false" },
|
|
"pinCountExpression": "2 * Terminals",
|
|
"labels": "custom",
|
|
"pinCountSamples": { "10": 20, "11": 22, "12": 24 },
|
|
"example": { "when": { "Terminals": "10" }, "pins": [ { "i": 0, "label": "A1" } ] }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
One numeric driver is fitted to `a * n + b`, two to a product or a sum. Anything else, or a fit
|
|
that fails verification, falls back to enumerating the drivers instead. `labels` takes
|
|
`sequential` when the labels are `1..N`, `none` when every label is null, and `custom` otherwise,
|
|
and a custom rule brings along one worked `example` because there is no array to show.
|
|
|
|
### The record model
|
|
|
|
The analyzer builds records in `org.diylc.plugins.chatbot.model`, beside the ones that already
|
|
describe a project to the model, and Jackson serializes them. `AiPinout` holds the drivers and
|
|
either one `AiTerminals` or a list of `AiPinoutVariant`, each of which pairs a `when` map with its
|
|
own `AiTerminals`. `AiTerminals` carries the three key groups above, and its three static factories
|
|
are the only way to build one, so a description cannot end up claiming to be two shapes at once.
|
|
`AiPinLabels` replaces the label rule strings, and `AiPin`, `AiSwitching`, `AiSwitchingPosition` and
|
|
`AiPinoutExample` cover the rest.
|
|
|
|
`AiTerminals` is composed into `AiPinout` and `AiPinoutVariant` through `@JsonUnwrapped`, which
|
|
keeps the model free of duplicated components while leaving the emitted JSON flat.
|
|
|
|
## 5. Implementation
|
|
|
|
Four pieces, in order.
|
|
|
|
1. **`PinoutAnalyzer` in `org.diylc.plugins.chatbot.service`.** Entry points
|
|
`analyze(ComponentType, ComponentProcessor)` and `analyze(Class, ComponentProcessor)`, both
|
|
returning the record model described at the end of §4. Internally: read the baseline terminals; sweep candidate properties
|
|
one at a time to find drivers; split them into numeric and finite; walk the product of the
|
|
finite ones; fit a formula over the numeric ones, falling back to enumeration when nothing
|
|
fits; attach switching tables for `ISwitch`. Every instantiation and every property write is
|
|
wrapped, and a failure downgrades that property to "not a driver" rather than failing the
|
|
entry. Drivers are sorted by name, because `extractProperties` follows `Class.getMethods()` and
|
|
the JVM makes no promise about that order.
|
|
|
|
2. **`ComponentCatalogGenerator.buildFullEntry`** gains a call to the analyzer
|
|
and puts the result under `pinout`. Two existing defects get fixed while the
|
|
method is open: the index loop dereferences `comp` outside its own null
|
|
check, and the `VERSION` constant reads `6.2.0` against a pom at `6.4.0`.
|
|
|
|
3. **`pinoutDriver` on `EditableProperty`** as a new element with default
|
|
`AUTO`. Adding an element with a default is safe for a runtime-retained
|
|
annotation and touches no existing component.
|
|
|
|
4. **The node name fixes** of §6, in `diylc-library`.
|
|
|
|
### Bounds
|
|
|
|
Three caps keep one component from swamping the file. At most 64 variants, dropping drivers
|
|
smallest domain first and naming the dropped ones in `driversHeldAtDefault`. At most four rows of
|
|
formula evidence, since the full domain of a numeric enum is already published as the property's
|
|
possible values. At most 24 switch positions written out in full, which matters because a DIP
|
|
switch reports two to the power of its switch count: without the cap its entry alone came to
|
|
1.27 MB, against 98 KB for all 144 types together afterwards.
|
|
|
|
## 6. Node names that need fixing
|
|
|
|
`getControlPointNodeName` serves the netlist, and `Node.toString()` already
|
|
renders `component.getName() + "." + displayName`. Eight components prepend the
|
|
component name a second time or return a hard-coded class name, so a netlist
|
|
node reads `V1.V1.+` today.
|
|
|
|
| Component | Returns now | Should return |
|
|
| --- | --- | --- |
|
|
| `BatterySymbol` | `getName() + "." + "+"` | `+` / `-` |
|
|
| `BulbSchematicSymbol` | same | `+` / `-` |
|
|
| `BuzzerSymbol` | same | `+` / `-` |
|
|
| `CurrentSourceSymbol` | same | `+` / `-` |
|
|
| `VoltageSourceSymbol` | same | `+` / `-` |
|
|
| `PhonoJackSymbol` | `getName() + "." + index` | `Tip` / `Sleeve`, and `Ring` when the type is stereo |
|
|
| `TerminalStrip` | `"TerminalStrip" + index` | `A1`…`An` for the front row, `B1`…`Bn` for the row it is bonded to, `C1`…`Cn` for the center row |
|
|
| `TagStrip` | `"TerminalStrip" + index` | nothing, the override is removed so the inherited `1`…`N` applies |
|
|
|
|
`PhonoJackSymbol` follows `ClosedJack1_4` and `OpenJack1_4`, which already name their points this
|
|
way. `TagStrip` had a verbatim copy of `TerminalStrip`'s string despite having a single row, so
|
|
deleting its override is both the smallest fix and the right one.
|
|
|
|
This is a real defect independent of the catalog, but it changes netlist text
|
|
output for any project using those parts. It needs a regression suite run and
|
|
regeneration of the affected reference netlists, which is why it is listed last
|
|
in §5 and can ship separately.
|
|
|
|
`PCBTerminalBlock` is deliberately excluded. It returns null until the user
|
|
supplies node names in a free-text property, and null correctly means "not a
|
|
named node". The catalog emits `"labels": "none"` for it.
|
|
|
|
## 7. Testing
|
|
|
|
- **Unit, `diylc-core`.** `PinoutAnalyzerTest` runs the analyzer against thirteen hand-built stub
|
|
components: no drivers, a label anchor that must not count as a terminal, no sticky points at
|
|
all, indices that do not start at zero, unnamed terminals, an enum driver, a boolean driver, two
|
|
drivers combined, an integer driver, both annotation overrides, a setter that rejects one of its
|
|
own values, and a switch.
|
|
- **Snapshot, `diylc-library`.** `PinoutDriverSnapshotTests` runs the analyzer over every
|
|
`ComponentType` and compares the derived driver names against
|
|
`src/test/resources/pinout-drivers.txt`, one line per type. Per D10 this is what catches a new
|
|
component with an undeclared pinout driver. It also pins the 51 known varying types so a
|
|
refactor cannot quietly drop one. On failure it writes the regenerated file to `target` and
|
|
names it, so accepting a real change is a copy.
|
|
- **Regression suite.** Required for §6 only. The analyzer itself never runs at
|
|
drawing time and cannot affect rendering or netlist output.
|
|
|
|
## 8. Running the generator
|
|
|
|
`ComponentProcessor` finds components by scanning the runtime classpath for
|
|
`org.diylc.components`, and those classes ship in `diylc-library`. The generator's own class sits
|
|
in `diylc-core`, which does not depend on `diylc-library`, so launching it against the
|
|
`diylc-core` module scans an empty package set and writes a catalog with no components in it. That
|
|
is a trap worth knowing about, because nothing about the failure looked like a failure.
|
|
|
|
Three things now stand in the way of hitting it again. The generator refuses to write an empty
|
|
catalog and says what to fix. `.run/ComponentCatalogGenerator.run.xml` launches it against the
|
|
`diylc-library` module, writing into the `diylc` directory where the committed catalogs live.
|
|
`ComponentCatalogGeneratorTest` moved from `diylc-core` to `diylc-library`, where the classpath
|
|
holds real components, and it now asserts that the catalog comes out populated and that a couple
|
|
of known entries carry the pinout they should.
|
|
|
|
The generator also sorts editable properties the way the property editor does, using
|
|
`ComparatorFactory.getDefaultPropertyComparator`. Property extraction follows
|
|
`Class.getMethods()`, whose order the JVM does not promise, so without that every regeneration
|
|
produced a large meaningless diff in a file that is committed. Two consecutive runs are now
|
|
byte-identical.
|