This commit is contained in:
Bane Stojkovic
2026-10-09 09:38:16 +02:00
88 changed files with 11344 additions and 1934 deletions
+288
View File
@@ -0,0 +1,288 @@
# Component Category Visibility
A `Config` submenu that lets the user hide whole component categories from the
component tree and the tabbed component bar, with the filtering applied in the
`Presenter` and announced to the views through a new event.
## 1. Goal
The library ships sixteen categories (`Boards`, `Connectivity`, `Controllers`,
`Displays & Outputs`, `Electro-Mechanical`, `Guitar`, `Misc`,
`Modules & Breakouts`, `Passive`, `Robotics`, `SMD`, `Schematic Symbols`,
`Semiconductors`, `Sensors`, `Shapes`, `Tubes`). A user who only ever draws
guitar wiring has to scroll past tabs and tree nodes they will never open. The
feature is a per-user toggle list: every category visible by default, any subset
hidden, the change visible immediately in whichever browser is active.
New in `ConfigPlugin`:
```
Config
...
Auto-Edit Mode
Component Categories >
Reset All
---------------
[x] Boards
[x] Connectivity
...
[x] Tubes
Continuous Creation
...
```
Disabling a category changes **only what the two browsers list**. Nothing is
hidden on the canvas, no existing project is altered, and no `.diy` file is
affected, so the serialized format and the regression references are untouched.
## 2. Four decisions taken up front
**The filtered view is a second accessor, not a change to `getComponentTypes()`.**
That method has six callers and only two of them are browsers:
| Caller | Uses the map for | Needs every category |
| --- | --- | --- |
| `CustomTreeModel` | tree nodes | no - filter here |
| `ComponentTabbedPane` | category tabs | no - filter here |
| `CanvasPanel` | type cache, keyboard-shortcut lookup | yes |
| `ExplorerPane` | list cell renderer | yes |
| `NetlistImportDialog` | netlist type mapping | yes |
| `ExportVariantsAction` | variant export tree | yes |
Had the existing method started filtering, a hidden category would silently
break the explorer's rendering of components already on the canvas and the
netlist importer's type lookup. So `getComponentTypes()` keeps its contract and
a new `getVisibleComponentTypes()` is added for the browsers.
**Favorites, Recently Used and keyboard shortcuts stay reachable.** Only the
per-category listings are filtered. A part the user explicitly pinned does not
vanish because its category was collapsed, and `CustomTreeModel`'s favorites
branch - which dereferences `typesByClass.get(fav.getName())` without a null
check - keeps working because that map is still built from the complete type
list.
**One config key holds the disabled categories**, as a `List<String>` written
through `writeValue` / read through `readObject`, the same shape as
`FAVORITES_KEY` and `RECENT_COMPONENTS_KEY`:
```
disabledComponentCategories = ["Robotics", "Tubes"]
```
Storing the *disabled* set rather than the enabled one means an empty value is
the default state, and a category introduced in a future release shows up
enabled for every existing user without a migration. "Reset All" is then simply
writing an empty list. The alternative - one boolean key per category - would
have let `ConfigAction` be reused verbatim, at the cost of config keys derived
from display names and one listener per category inside the `Presenter`.
**The submenu sits directly under `Config`**, a sibling of `Snap To`, `Theme`
and `Toolbox`, rather than nested inside `Toolbox`.
## 3. Core: the event
`diylc-core/src/main/java/org/diylc/common/EventType.java` gains a constant at
the end of the enum, documented like its neighbours:
```java
/**
* Called when the set of enabled component categories changes. The first parameter is a
* <code>Set&lt;String&gt;</code> of disabled category names. All categories not included are
* enabled.
*/
COMPONENT_CATEGORIES_CHANGED
```
The payload mirrors `LAYER_VISIBILITY_CHANGED`, which passes the set of hidden
layers. Views are free to ignore it and re-read `getVisibleComponentTypes()`,
which is what both of them will do.
## 4. Core: `IPlugInPort`
Next to `FAVORITES_KEY`:
```java
public static final String DISABLED_CATEGORIES_KEY = "disabledComponentCategories";
```
and four methods, the accessor documented immediately after
`getComponentTypes()` so the difference between the two is impossible to miss:
```java
/**
* Returns the {@link ComponentType}s that should be offered to the user, i.e. the result of
* {@link #getComponentTypes()} without the categories the user disabled. Intended for component
* browsers; anything that needs to resolve an arbitrary component class must use
* {@link #getComponentTypes()} instead.
*/
Map<String, List<ComponentType>> getVisibleComponentTypes();
boolean isCategoryEnabled(String category);
void setCategoryEnabled(String category, boolean enabled);
void resetCategoryVisibility();
```
Keeping the set arithmetic behind these three mutators means the Swing action
never parses the stored list and never touches the config key, so the meaning of
"disabled" lives in exactly one place.
## 5. Core: `Presenter`
The disabled set is read from `configManager` on demand rather than cached in a
field. None of these calls is on a hot path - they run when a browser rebuilds
or a menu item is toggled - and a stateless read cannot go stale:
```java
@SuppressWarnings("unchecked")
private Set<String> getDisabledCategories() {
return new HashSet<String>(
(List<String>) configManager.readObject(DISABLED_CATEGORIES_KEY, new ArrayList<String>()));
}
```
`getVisibleComponentTypes()` copies the entries it keeps into a new map;
`ComponentProcessor` hands back its own cached instance, which must not be
mutated.
`setCategoryEnabled` adds or removes one name and writes a sorted
`ArrayList<String>` back, so the config file stays stable across sessions.
`resetCategoryVisibility()` writes an empty list.
The event is fired from a config listener, registered in the constructor next to
the existing `SHOW_NODE_NAME_TOOLTIPS_KEY` one, rather than from the setters:
```java
configManager.addConfigListener(IPlugInPort.DISABLED_CATEGORIES_KEY, new IConfigListener() {
@Override
public void valueChanged(String key, Object value) {
messageDispatcher.dispatchMessage(EventType.COMPONENT_CATEGORIES_CHANGED,
getDisabledCategories());
}
});
```
Any writer then refreshes the views, not just the menu. The dispatcher is
constructed with `new MessageDispatcher<EventType>(true)` - synchronous - so the
views are rebuilt on the same EDT call that toggled the checkbox, and no
`invokeLater` is needed.
## 6. Swing: the menu
Two new action classes beside `ConfigAction` and `ComponentBrowserAction` in
`org.diylc.swing.actions`:
- **`ComponentCategoryAction`** - one per category. Modelled on `ConfigAction`:
`NAME` is the category, `IView.CHECK_BOX_MENU_ITEM` is `true`,
`SELECTED_KEY` is seeded from `plugInPort.isCategoryEnabled(category)`, and a
listener on `DISABLED_CATEGORIES_KEY` re-reads that flag so "Reset All"
re-checks every item in the submenu. `actionPerformed` calls
`setCategoryEnabled`.
- **`ResetCategoriesAction`** - a plain item calling
`resetCategoryVisibility()`.
`ActionFactory` gets `createComponentCategoryAction(plugInPort, category)` and
`createResetCategoriesAction(plugInPort)` next to
`createComponentBrowserAction`.
`ConfigPlugin` gains `CATEGORIES_MENU = "Component Categories"` and, placed
alphabetically between "Auto-Edit Mode" and "Continuous Creation" to match the
ordering of the rest of the menu:
```java
swingUI.injectSubmenu(CATEGORIES_MENU, IconLoader.FolderPreferences.getIcon(), CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createResetCategoriesAction(plugInPort), CATEGORIES_MENU);
swingUI.injectMenuAction(null, CATEGORIES_MENU);
plugInPort.getComponentTypes().keySet().stream().sorted()
.forEach(category -> swingUI.injectMenuAction(
ActionFactory.getInstance().createComponentCategoryAction(plugInPort, category),
CATEGORIES_MENU));
```
The items are built from the **complete** keyset - a disabled category still
needs a checkbox to turn it back on. `injectMenuAction(null, ...)` is the
established way to add a separator, and `MainFrame.injectMenuAction` already
pushes action names through `LangUtil.translate`, so category names are
translated like every other menu label.
## 7. Swing: the component tree reacts
`CustomTreeModel` takes its categories from the filtered map while keeping the
class lookup complete:
```java
this.componentTypes = plugInPort.getVisibleComponentTypes();
...
for (Map.Entry<String, List<ComponentType>> e : plugInPort.getComponentTypes().entrySet())
for (ComponentType c : e.getValue())
typesByClass.put(c.getInstanceClass().getCanonicalName(), c);
```
and gains a `refresh()` that re-reads the visible map, rebuilds `categories`
with `(Favorites)`, `(Recently Used)` and `(Building Blocks)` back on front, and
calls the existing `updateVisibleCategories()`, which already clears the leaf
cache and fires `treeStructureChanged`. The constructor's category setup moves
into that method so there is one copy of it. `TreePanel` exposes it through its
existing `getTreeModel()`.
`ComponentTree` currently returns `null` from `getSubscribedEventTypes()` with a
TODO and does nothing in `processMessage`. Both are filled in:
```java
@Override
public EnumSet<EventType> getSubscribedEventTypes() {
return EnumSet.of(EventType.COMPONENT_CATEGORIES_CHANGED);
}
@Override
public void processMessage(EventType eventType, Object... params) {
if (eventType == EventType.COMPONENT_CATEGORIES_CHANGED) {
getTreePanel().getTreeModel().refresh();
}
}
```
Search text survives the refresh because `updateVisibleCategories()` re-applies
it; tree expansion state does not, which is acceptable for an explicit menu
action.
## 8. Swing: the tabbed toolbar reacts
`ComponentTabbedPane` builds its category tabs in the constructor, after the
three fixed tabs. That loop moves into `refreshCategoryTabs()`, which removes
every tab from index 3 onwards and re-adds one per visible category. Each
category's panel is cached in a `Map<String, Component>`, because
`createComponentPanel` instantiates a `ComponentButtonFactory` button - with its
icon and variant popup - for every type in the category; toggling a category
must not pay that cost again for the fifteen categories that did not change.
The selected tab is remembered by title and restored if it is still present. A
`refreshing` guard keeps the existing `ChangeListener` from persisting
intermediate `lastSelectedTab` indices while tabs are being removed.
`ToolBox` fills in the same `null`/TODO pair as `ComponentTree`, delegating to
`getComponentTabbedPane().refreshCategoryTabs()`.
## 9. What is deliberately not touched
`CanvasPanel`, `ExplorerPane`, `NetlistImportDialog` and `ExportVariantsAction`
keep calling `getComponentTypes()` and keep seeing every category, so keyboard
shortcuts, the project explorer's renderer, netlist import and variant export
behave identically whatever is hidden.
## 10. Verification
```bash
mvn -q -pl diylc-swing -am compile # core + library + swing
mvn -q test # reactor, since core changed
```
Then a manual pass in the app: toggle a category with the tree active and with
the tabbed toolbar active, confirm both update without reopening the menu,
confirm "Reset All" re-checks every item, confirm a favorited part from a hidden
category is still clickable, and confirm the setting survives a restart.
The change touches neither rendering, serialization nor netlist output, so a
regression-suite run is not called for.
+26 -9
View File
@@ -413,11 +413,27 @@ Small, independent, and worth folding into whichever Tier 1 item is done first.
`bomPolicy = SHOW_ONLY_TYPE_NAME`, so a Uno R3 and a Uno R4 WiFi both print as "Arduino Uno".
A maker's BOM is a shopping list and the variant is the part they buy. Worth deciding on a
uniform approach before the roadmap roughly triples the number of variants.
3. **Editable-property set drifts across the family.** `Headers` exists on `ArduinoNano` and
`RaspberryPiZero` but not on `ArduinoUno`, `Teensy` or `ESP32DevKit`; there is no "show pin
labels" toggle anywhere; `WemosD1Mini` hardcodes `SILK_PIN_NAMES_LEFT` / `SILK_PIN_NAMES_RIGHT`
instead of using the shared `drawPinLabels`. Settling a uniform Version / Headers / Show Labels
/ Board Color set now stops ten more boards inheriting the inconsistency.
3. **Editable-property set drifts across the family** -- **mostly withdrawn; see the peripherals
plan's §11.2 for the decision.** Two of the three facts here have not held up. `Headers` is now
on seven boards including `Teensy` and `ESP32DevKit`, and the three that lack it -- `ArduinoUno`,
`ArduinoMega` and the full-size `RaspberryPi` -- are exactly the boards that ship with their
headers soldered, so the distribution is the product line rather than drift; adding it to the Uno
would offer a choice the part does not have. The "show pin labels" toggle is rejected outright,
chiefly because a new boolean deserializes as `false` and would strip the silkscreen from every
existing file. The last clause was the real one and is now **fixed**, though not the way it
was written. `WemosD1Mini` did hardcode `SILK_PIN_NAMES_LEFT` / `SILK_PIN_NAMES_RIGHT`, and they
were `PIN_NAMES` with the annotations stripped -- the right one also **reversed**, because that
row's control points run bottom to top. The labels were correct on screen, so what this cost was
not a visible defect but a duplicate that had to be kept in step by hand and would have drifted
silently the first time a pin was renamed.
Moving it to the shared `drawPinLabels` would have been the wrong fix: that helper stands its
labels on end, and the D1 mini prints its names flat between its rows, which its 0.9" row
spacing has room for. So the drawing stays local and only the *source* changed -- text from
`getSilkPinLabel(i)` and position from `getRelativeOffsets()`, both indexed by control point, so
the row direction is followed rather than mirrored and the reversed array disappears rather than
being corrected. The render is byte-identical with and without headers.
`silkLabelsComeFromTheNodeNames` and `theRightRowRunsBottomToTop` cover it.
4. **Pin-name conventions differ.** `ESP32DevKit` uses `GND_1`, `ArduinoNano` uses `GND1`. And
`AbstractMakerBoard.getDisplayPinLabel` truncates at the first `_` and `(`, so `ArduinoNano`'s
`D3 (~)` silently loses its PWM marker on the silkscreen. Pick one convention before adding
@@ -442,10 +458,11 @@ Each item is an independent commit; none depends on another except where sequenc
1. **BOM treatment of `Version`** (§8.2). Options: leave as-is; switch these boards to a BOM
policy that includes the variant; or expose the variant through a value-like property. Affects
every board in the package, so worth deciding once.
2. **Uniform property set** (§8.3). Which of `Headers` and a new "Show Pin Labels" toggle should
exist on all boards, and whether retrofitting `Headers` onto `ArduinoUno` / `Teensy` /
`ESP32DevKit` is wanted. New boolean fields deserialize as `false` in old files, so the default
must be chosen to preserve the current look.
2. ~~**Uniform property set** (§8.3)~~ -- **decided in the peripherals plan's §11.2.** Neither
`Headers` nor a "Show Pin Labels" toggle is universalised: a maker component gets a property
when the real part gives the buyer a choice, and a drawing preference is not a property. The
serialization warning recorded here is what kills the toggle rather than merely constraining
its default.
3. **ESP32-C6 in the same pass as C3?** §5.2 Item 7 is written for both since they share a body.
If time-boxing, ship C3 first and add C6 as a one-branch follow-up.
4. **Nano pin-label verbosity** (§5.2 Item 6). Recommendation as written: annotate bus pins on all
+258
View File
@@ -0,0 +1,258 @@
# Maker Peripherals — `Displays & Outputs`
Status: **13 components shipped**, in the as-yet-unreleased `6.7.0` block of
`diylc-core/src/main/resources/update.xml`.
Scope: `diylc-library`, package `org.diylc.components.displays`.
Companion: `maker-board-roadmap.md`, which covers `org.diylc.components.micro`.
Nothing in this package has been in a release, so defaults, control-point counts and serialized
field names can still be changed freely. **That stops the moment 6.7.0 ships.**
## 1. What is in the palette
| Class | Variants | Other properties |
|---|---|---|
| `CharacterLCD` | 16x2, 20x4 × I2C backpack, parallel HD44780 | Backlight Color, Screen |
| `OLEDDisplay` | 0.96" SSD1306 128x64, 0.91" SSD1306 128x32, 1.3" SH1106 128x64 × I2C, 7-pin SPI, 6-pin SPI | Pixel Color, Screen |
| `TFTDisplay` | 2.8" / 2.4" ILI9341, 0.96" / 1.8" ST7735, 1.54" ST7789, 1.28" GC9A01 round | Screen |
| `Nokia5110LCD` | single part (PCD8544 84x48) | Backlight Color, Screen |
| `SevenSegmentDisplay` | 1-digit 0.56", 4-digit 0.36", 4-digit 0.56", 4-digit TM1637 module | Common, Punctuation, LED / Board / Body colour |
| `LEDMatrix` | MAX7219 single 8x8, compact 8x8, 4-in-1 32x8, 8-in-1 64x8 | Dot Color |
| `LEDBarGraph` | 8, 10, 12 segments | Orientation, body / border colour |
| `WS2812BBreakout` | single LED | — |
| `WS2812BStick` | 8 LEDs | — |
| `WS2812BJewel` | 7 LEDs | — |
| `WS2812BRing` | 12, 16, 24 LEDs | — |
| `WS2812BStrip` | 30 / 60 / 144 LED·m⁻¹ × 1–144 LEDs | — |
| `WS2812BPanel` | Adafruit 8x8 NeoMatrix | — |
Twelve extend `AbstractMakerBoard`. `LEDBarGraph` is the exception: a DIP-outline part, so it
follows `DIL_IC` on `AbstractLabeledComponent` with its own transformer and inherits none of the
maker helpers. It is still in this category because that is where someone looks for a bar graph.
## 2. Conventions this package follows
These are the rules worth knowing before touching anything here. Most were learned by getting them
wrong once.
**Variants.** A `Version`-style enum on the existing class where the footprint family is shared; a
new class where it is not. The test is structural, not cosmetic: `WS2812BJewel` is a separate class
from `WS2812BRing` because it is a disc rather than an annulus, with a centre LED and its own pad
arrangement — five variant-specific structures is a different part.
**Figures live on the variant.** Hardcoded `Size` constants and `String[]` arrays in the component
class, per-variant wherever a variant changes them. No pin-definition resource, and no pixel
literals in `draw()` — every dimension derives from a `Size`.
**A different silkscreen is not a variant; a different pin count is.** The same two pads on an
SSD1306 carry the clock and data lines in both interfaces — the module's BS jumpers choose the
protocol, not the pins — so `SCL`/`SCK`/`CLK`/`D0` are one pin and `SDA`/`MOSI`/`D1` are another,
and boards that print them differently are the same board. Node names pick one spelling and move on.
A 6-pin SPI board is the opposite case: its chip select is tied low on the PCB rather than brought
out, so it cannot share a bus, and that is a variant because it reaches the netlist.
**Say whether a figure is measured or derived.** Active areas computed from a nominal diagonal at a
panel's aspect ratio are derivations, and are commented as such so nothing is built on them. Where a
part's own measurement contradicts a derivation the measurement wins and carries its own field — but
check first that it really contradicts it. A supplied figure sitting within half a millimetre of
what an existing rule already produces is more likely a reading than a departure, and has twice
turned out to be one; query it before it earns a mechanism.
**Orientation is sourced per variant, never standardised.** Which edge a board's header leaves by is
a fact about the part. Control point zero is always the leftmost pin, so with the pin order fixed a
top header and a bottom header are mirror images, not the same board turned round. `TFTDisplay`
divides three and three, and every constant states its own edge — the short constructor takes the
flag too, so none can inherit a default. Standardising this family onto one orientation was tried
and had to be undone: it had moved two boards away from the truth.
**Do not let one property stand in for another.** `isRound()` was serving as "the header is at the
bottom" and broke the moment a rectangular board had a bottom header; `isDualRow()` was serving as
"this is a bare package". Carry both questions explicitly even while the answers agree.
**Pins are drawn and covered, never skipped.** `drawPinHeader` is what registers a conductive area,
so a header hidden under a module or a moulding is drawn first and painted over. Leaving the call
out looks identical and silently drops those pins from continuity and the netlist.
**Silk where it fits, nowhere else.** Pin names go through `getSilkPinLabel`, so a name is written
once. A row with no clear strip beside it gets no labels rather than crowded ones — the matrix's
covered rows, the LCD's I2C backpack and the TFTs' SD rows all go unlabelled deliberately. A
`SILK_NAMES` array exists only where the node name is actively wrong on the board, not merely long.
**The glass prints the part's own spec.** `Display.VALUE` puts the variant string on the lit area,
generated from the properties rather than typed, carrying no prose and no language. `Display.NONE`
gives the unpowered part. No invented demo content, ever.
**BOM.** Every class sets `bomPolicy = SHOW_ONLY_TYPE_NAME` and returns the variant string from
`getVariantLabel()`, because `BomMaker` groups rows on type name plus value — without it every
variant of a class collapses into one line and the BOM cannot say which part to buy.
**Shared arrays are shared by identity.** Where two variants have the same pinout they reference one
array constant and the test asserts `assertSame`, so a correction to one reaches both. Two copies
that happen to match would pass every name check and drift the day one is corrected.
**Nested types at the bottom of the file.** Where a variant needs more than a couple of extra
figures they go into a nested value class rather than onto the enum constructor —
`OLEDDisplay.Layout`, `TFTDisplay.Glass`, `SevenSegmentDisplay.Module`. A constructor past about
fifteen arguments is the signal.
## 3. Figures that are not measurements
Kept visible so no one mistakes them for sourced data and builds on them.
- **`SevenSegmentDisplay` digit widths** — 8.1 mm on both 0.56" packages (unverified) and 5.2 mm on
the 0.36" (inferred from the 0.56" ratio, so resting on an unchecked figure). **Deliberately left
as inferences:** they size the drawn glyph and nothing else, and the glyph is already a declared
departure from the package at `DIGIT_DRAW_SCALE` 0.88. An error here is cosmetic and bounded,
unlike a wrong pin array.
- **`SevenSegmentDisplay.Module` hole figures** — 1.778 and 1.524 mm, which are the 14 and 12 pixel
offsets the class drew before they were figures at all, converted exactly so the drawing did not
change.
- **`Nokia5110LCD`** — the 3.5 mm clearance from the bottom edge to the pin row.
- **`WS2812BStrip`** — tape width (10 mm / 12 mm) is a stated norm rather than a measurement, and
the end-pad lead-in derives from the pad and package footprints.
- **`TFTDisplay`** — the SD rows' horizontal placement is centred across the board by derivation;
only their offset from the edge was given.
- **`OLEDDisplay`** — the 6-pin SPI boards borrow the 7-pin boards' outlines. A 6-pin module is a
PCB of its own with one fewer pad, and nothing but its outline would say whether it matches.
## 4. Testing
Per-class tests in `diylc-library/src/test/java/org/diylc/components/displays/`, using
`MakerBoardTestSupport` (`assertRow`, `assertRowSpacing`, `assertBoardSize`, `assertDrawsCleanly`,
`assertScreenTextIsDrawn`, `renderPixels`).
What these tests are for, beyond the obvious count-and-dimension checks:
- **Assert relationships, not transcriptions.** A board is exactly as wide as its modules; a glass
clears the mounting holes; no hole is drilled where a pad is; a lit area that is measured really
does disagree with the rule it replaced. A test that restates the constant passes while the
component is wrong.
- **No duplicate node names,** on every variant. The cost is legibility, not connectivity: nodes are
identified by component and control point index and the netlist graph is keyed on position, so two
pads named `GND` stay two nodes that merely print the same, and a reader cannot tell which one a
connection reached. They are invisible on the canvas too, since `getDisplayPinLabel` cuts the
suffix off and both silk as `GND`. `Teensy` is the one board that still repeats a bare name.
- **A drawing smoke test** over every variant, state and orientation.
- When a refactor is meant to change nothing, **render before and after and compare pixels.** The
`digitCount` work was verified that way, and it caught a sub-pixel hole shift the unit tests
missed.
`MakerComponentsTest` carries the generic discovery and icon coverage. A class ships by moving from
`unreleasedMakerComponentClasses` to `releasedMakerComponentClasses` and uncommenting its
`@ComponentDescriptor`.
## 5. Release
The slice is worth shipping on its own: it is archetype-complete, part of it (7-segment, character
LCD, bar graph, addressable strips) reaches builds with no microcontroller in them, and the slices
after it are large and differently shaped, so batching would mean a much later and far less
reviewable release. A release is also the only validation loop that reaches people who own the
parts.
### 5.1 What shipping makes permanent
Only this list needs checking beforehand, because only this list is a contract afterwards:
- class names, and serialized field names and types
- **control-point counts**
- **control-point positions relative to point 0** — these are stored per instance, so moving a pin
relative to the body misplaces every wire in an existing file
Everything else is fixable in a later release without breaking a file: bezel placement, colours,
silk, hole positions, digit widths. Every figure in §3 falls here, which is why none of them blocks
a release.
### 5.2 Pre-release checklist
1. **Audit the pin layout of all 13** — count, node names, and each pin's position relative to the
body. Not the cosmetics; those are §5.1's second list.
2. **Settle `TFTDisplay`'s 2.8" SD row.** It is the least verified thing in the package: the row was
added on the argument that the "(Touch + SD)" label implied a missing header, and its 3.0 mm
offset was read from "the same offset as the bottom row" rather than measured. Four control
points that become permanent.
3. **Re-check `TFTDisplay` generally.** It is where the churn concentrated — both ILI9341s went
14 to 18 control points, and the 0.96", 1.8" and 1.54" each had every pin move relative to the
body when their orientation was corrected.
4. **Render a contact sheet of all 13 and look at it.** Cheap, and it caught several real errors
that the unit tests passed straight over.
Regression samples (§6.3) are deliberately *not* on this list. They protect against future change,
which is what a released contract makes dangerous — so they are the first task after shipping, not
a blocker on it.
### 5.3 Release notes
One `NEW_FEATURE` per palette entry in the `6.7.0` block, part name then variant list. **A released
block is never edited.** A new variant of a component that is new in the same block goes onto that
component's existing line rather than being logged as an `IMPROVEMENT` — there is no earlier release
for it to improve on.
## 6. Next steps
### 6.1 Parts worth adding, ranked by how often they appear in builds
1. **1.3" 240x240 ST7789**, and the 1.14" and 2.0" ST7789 the Pico ecosystem buys. `TFTDisplay` now
absorbs offset panels, either header edge and second rows, so each is one `Controller` constant.
Needs per board: outline, header offset and edge, pin names in printed order, hole inset and
drill, bezel height, and whether the lit area is centred or measured.
2. **MAX7219 8-digit 7-segment module** — the clock and counter staple, and this package already
models a MAX7219 cascade for the matrix. Unblocked: `DisplayType` now carries digit count, pin
array and module board figures, so this is a constant.
3. **6-digit TM1637 and 2-digit 0.56"** — the rest of the clock and thermometer range. Same.
4. **E-paper, 1.54" / 2.13" / 2.9" SSD1680.** Once judged niche; that call is stale, as ESP32 badge
builds and the Waveshare and Good Display modules made it ordinary. The flexible-cable mounting
is real new drawing work, which is the only reason it is not higher.
5. **WS2812B 16x16 and flexible 8x32 matrices** — the one addressable form the package lacks.
6. **The bare WS2812B 5050 LED**, as against a breakout. People solder these directly.
7. **16x4 character LCD.** Also the RGB-backlight 1602, which is *not* the free colour default it
looks like: it is an 18-pin part whose three backlight anodes are real pins, so it is an
`LCDInterface` constant with its own array.
8. **Nixie tubes — IN-12, IN-14, B7971.** The strongest audience overlap on this list: DIYLC already
has a `Tubes` category, and the people in it build Nixie clocks.
9. **2.42" SSD1309 OLED**, 14- and 16-segment alphanumeric, HT16K33 backpacks. Real, second tier.
Explicitly rejected: a tint property on `TFTDisplay`. A 240x320 colour panel has no tint to set, and
offering one invites a drawing that says something false about the hardware.
### 6.2 The "& Outputs" half of the name is unfulfilled
Every member of this category emits light. **There is no analog panel meter, no VU meter, no speaker
or driver, and no magic-eye tube anywhere in the library** — `misc/Buzzer` and
`electromechanical/PilotLampHolder` are the whole non-display output range.
This is a bigger gap than anything in §6.1, and it reaches the audience DIYLC already has: a VU
meter and a loudspeaker appear in amp and effect builds containing no microcontroller at all,
`Dial Scale` is already next door in `misc`, and a moving-coil meter is a simpler drawing than any
display here. Whether these belong in this category or in `Electro-Mechanical` is not an interesting
question; that they are absent is the finding.
### 6.3 Regression coverage — the standing risk
**No `.diy` file among the 1418 in `diylc-regression-data` references `org.diylc.components.micro`
or `org.diylc.components.displays`,** so 23 components have no rendering or netlist baseline. Unit
tests guard the figures we chose; they cannot catch a pin dropping out of the netlist or a component
misbehaving in a real project. Samples were declined for this slice; the case is stronger now that
orientations, pin arrays and geometry have all moved.
### 6.4 Later slices
**`sensors` (9 drafts).** Ship the four that are the unrivalled answer in their function — HC-SR04,
DHT11/22, PIR HC-SR501, MPU6050 — and leave the five commodity KY-style three-pin boards, which
carry most of the sourcing cost for the least distinctiveness. **Do not** collapse the tail into a
parameterised "three-pin sensor module": that is the generic configurable board that was already
built and reverted, and it fails for the same reason — people search the palette for a product name.
**`modules` (21 drafts).** The largest and least homogeneous batch, and the worst geometry drift in
the tree (`ULN2003Driver` has 24 raw pixel offsets, `PCA9685ServoDriver` 22, `L298NMotorDriver` 18).
Split by function rather than shipping as one category. Settle one overlap first: the drafted
`ActiveBuzzerModule` (KY-012) against the existing `misc/Buzzer`.
**`robotics` (4 drafts).** Blocked on an architectural decision, not on part selection.
`AbstractMotor` is a parallel fork of `AbstractMakerBoard`, re-implementing `rotatePoints`,
`drawPins`, `getFinalBorderColor` and `getCachingBounds` plus its own `PIN_COLOR`, `PIN_SIZE` and
silk fonts. Four components do not justify a second hierarchy: either lift the shared behaviour into
a common base or take the duplication deliberately.
**i18n.** Component display names and enum labels are plain strings, absent from
`diylc-swing/src/main/resources/lang/*.txt`, as they are for the shipped boards. No work required
here; noted for a future pass.