agents setup

This commit is contained in:
Bane Stojkovic
2026-09-04 15:31:00 +02:00
parent 26d6ca48fd
commit 6f51c3ca42
7 changed files with 598 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
---
name: add-component
description: Add a new component type to the DIYLC library, or substantially modify an existing one. Use when asked to create a new part (resistor variant, board, jack, switch, pickup, symbol, shape), add editable properties to a component, change how a component draws or reports connectivity, or make a component rotatable/mirrorable.
---
# Adding a component to DIYLC
Components live in `diylc-library/src/main/java/org/diylc/components/`. They are discovered at
runtime by Reflections scanning `org.diylc` — **there is no registry to update**. A class becomes a
component by carrying `@ComponentDescriptor` and implementing `IDIYComponent` (in practice, by
extending one of the abstract bases).
## Before writing anything
1. **Find the closest existing component and read it end to end.** A new axial part follows
`passive/Resistor.java`; a new board follows `boards/VeroBoard.java`; a new schematic symbol
follows `AbstractSchematicLeadedSymbol` subclasses; a new jack follows `electromechanical/`.
The new class should read like a sibling of that file.
2. **Ask whether it is really a new class.** If the request is a revision or size variant of
something that already exists, prefer adding a `Version` (or similar enum) property to the
existing class over creating a parallel class. That keeps old project files loading and keeps the
toolbox from sprawling.
3. **Pick the right package** — `passive`, `semiconductors`, `electromechanical`, `boards`, `guitar`,
`tube`, `chassis`, `shapes`, `smd`, `connectivity`, `misc`, `autocreate`. The package should match
the `category` in the descriptor.
## Choosing a base class
| Base | Use for |
| --- | --- |
| `AbstractComponent<T>` | Anything that fits nothing else |
| `AbstractLabeledComponent<T>` | Adds name/value label rendering |
| `AbstractTransparentComponent<T>` | Anything with an alpha/transparency property |
| `AbstractLeadedComponent<T>` | Two-lead axial parts drawn between two points; implement `getDefaultLength()`, `getDefaultWidth()`, `getBodyShape()` |
| `AbstractRadialComponent<T>` | Radial (two leads out the same side) parts |
| `AbstractCurvedComponent<T>` | Curves and traces defined by control points |
| `AbstractAngledComponent<T>` | Parts placed by point and angle |
| `AbstractBoard` / `AbstractProtoBoard` / `AbstractVeroBoard` | Boards |
| `AbstractMultiPartComponent<T>` | Components made of several independently drawn sections |
| `Abstract3LegSymbol`, `AbstractDiodeSymbol`, `AbstractTubeSymbol`, `AbstractGuitarPickup`, `AbstractShape` | The obvious specialisations |
Extend the most specific base that fits. Do not reimplement behaviour a base already provides.
## The class
```java
/* ... GPL header copied verbatim from HEADER.txt ... */
package org.diylc.components.passive;
import ... // java.*, then third-party, then org.diylc.*
@ComponentDescriptor(name = "My Part", author = "Branislav Stojkovic", category = "Passive",
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "X",
description = "Short sentence shown in the toolbox tooltip", zOrder = IDIYComponent.COMPONENT,
transformer = SimpleComponentTransformer.class, enableCache = true)
public class MyPart extends AbstractLeadedComponent<Resistance> {
private static final long serialVersionUID = 1L;
public static Size DEFAULT_WIDTH = new Size(1d / 2, SizeUnit.in);
public static Color BODY_COLOR = Color.decode("#82CFFD");
private Resistance value = null;
public MyPart() {
super();
this.bodyColor = BODY_COLOR;
}
@EditableProperty(validatorClass = PositiveMeasureValidator.class)
public Resistance getValue() {
return value;
}
public void setValue(Resistance value) {
this.value = value;
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) { ... }
}
```
Requirements the framework enforces:
- **No-argument public constructor**, and the class must be serializable with a
`private static final long serialVersionUID = 1L;`.
- `@ComponentDescriptor` needs `name`, `description`, `category`, `author`, `instanceNamePrefix` and
`zOrder` at minimum. Set `enableCache = true` for anything whose drawing is expensive and does not
depend on neighbouring components.
- Appearance constants are `public static` and deliberately **not final**, so they can be overridden
at runtime. Follow that.
## Editable properties
- Annotate the **getter** with `@EditableProperty`; a matching `setXYZ` must exist. The property name
comes from the getter unless you pass `name`.
- Use `sortOrder` when the order in the editor matters, `defaultable = false` for properties that
must not be remembered as a user default, and `validatorClass` (e.g.
`PositiveMeasureValidator`, `PositiveNonZeroMeasureValidator`) where a raw value would be invalid.
- Use the measure types from `org.diylc.core.measures` (`Size`, `Resistance`, `Capacitance`,
`Voltage`, `Power`, `Percentage`) rather than raw numbers — they carry units and format themselves.
- For a runtime-computed list of allowed values, add `@DynamicList(availableValueFunction = "getXyz")`
alongside `@EditableProperty`. (The `additionalOptions` example in `EditableProperty`'s own Javadoc
is stale — that attribute no longer exists.)
- Override `getValueForDisplay()` when the canvas label should combine several properties.
## Drawing
`draw(Graphics2D, ComponentState, boolean outlineMode, Project, IDrawingObserver)` runs on every
repaint, for every instance. Treat it as a hot path.
- Fetch strokes from `ObjectCache.getInstance().fetchBasicStroke(w)`, `fetchZoomableStroke(w)` or
`fetchStroke(w, dash, phase, cap)`. Do not allocate `new BasicStroke(...)` in `draw()`.
- Honour `outlineMode` and every `ComponentState` (`NORMAL`, `SELECTED`, `DRAGGING`) the way the
neighbouring components do.
- `drawIcon(g2d, width, height)` is a separate, hand-drawn miniature for the toolbox — it is not a
scaled call into `draw()`.
### Connectivity
If the component conducts, wrap the conductive drawing in
`drawingObserver.startTrackingContinuityArea(true)` / `stopTrackingContinuityArea()`, exactly as the
comparable component does. Getting this wrong silently corrupts continuity checks and the netlist
without any visible rendering difference. `startTracking()` / `stopTracking()` control whether
drawing contributes to the component's clickable area.
Also implement, where relevant:
- `getControlPointCount()`, `getControlPoint(int)`, `setControlPoint(Point2D, int)`,
`isControlPointSticky(int)`, `getControlPointNodeName(int)` — the connection points.
- `getInternalLinkName(int, int)` and `getSectionNames(int)` — internal conduction between points,
needed by the netlist and analyzers.
- `IGerberComponentSimple` or `IGerberComponentCustom` if the part should appear in Gerber output.
## Rotation and mirroring
These live in an `IComponentTransformer` under
`diylc-library/src/main/java/org/diylc/components/transform/`, referenced from the `transformer`
attribute of `@ComponentDescriptor` — never inside the component itself. Reuse
`SimpleComponentTransformer` or an existing transformer where the behaviour matches; write a new one
only when the part rotates or mirrors differently from everything that exists.
## Datasheets and variants
If the part has a catalogue of real-world values and sizes, set `enableDatasheet = true` and
`datasheetCreationStepCount`, and add the file under
`diylc-library/src/main/resources/datasheets/`. Follow the format of an existing `.datasheet` file
and add a spot-check to `DatasheetServiceTest`.
## Backward compatibility
Once a component ships, its serialized shape is a contract:
- Never rename, remove, or retype a serialized field, and never rename or move the class.
- To replace a property, keep the old field, mark the old accessors `@Deprecated`, add the new field,
and migrate lazily in the new getter — see `AbstractTransparentComponent.getAlpha()`,
`Resistor.getPowerNew()`, `AbstractFilmCapacitor.getVoltageNew()`.
- Fields added later arrive as `null` from old files; default them in the getter, not the
constructor.
## Verify
```bash
mvn -q -pl diylc-library -am test
```
Then state plainly that the component still needs a visual check in the running app — automated tests
do not cover component appearance. Offer a regression-suite run (see the `regression-test` skill) for
any change to an existing component's drawing or serialization.
+129
View File
@@ -0,0 +1,129 @@
---
name: add-plugin
description: Add or modify a DIYLC Swing plugin, menu action, toolbar item, dialog or side panel. Use when asked to add a UI feature, a menu entry, a new tool window, a background task, or to react to domain events like selection or project changes.
---
# Adding a plugin or UI feature to DIYLC
The Swing module is a shell that hosts plugins. Everything the user sees — the toolbox, the canvas,
every menu, the status bar, autosave — is an `IPlugIn`. Adding UI means adding or extending a plugin,
not reaching into frames.
## Where things go
| Path | Contents |
| --- | --- |
| `diylc-swing/src/main/java/org/diylc/swing/plugins/<name>/` | One package per plugin |
| `diylc-swing/src/main/java/org/diylc/swing/actions/` | `AbstractAction` implementations, grouped into `file`, `edit`, `analyze` sub-packages |
| `diylc-swing/src/main/java/org/diylc/swing/gui/` | `MainFrame`, dialogs, editors, shared components |
| `diylc-swing/src/main/java/org/diylc/swing/gui/MainFrame.java` | Where plugins are installed |
## Before writing anything
Read the plugin closest to what is being asked. `plugins/autosave/AutoSavePlugin.java` is the
smallest complete example (event subscription plus background work);
`plugins/statusbar/`, `plugins/layers/` and `plugins/analyze/` show menu and panel injection.
Match the one you read.
## The plugin
```java
/* ... GPL header from HEADER.txt ... */
package org.diylc.swing.plugins.myfeature;
public class MyFeaturePlugin implements IPlugIn {
private static final Logger LOG = Logger.getLogger(MyFeaturePlugin.class);
private ISwingUI swingUI;
private IPlugInPort plugInPort;
public MyFeaturePlugin(ISwingUI swingUI) {
this.swingUI = swingUI;
}
@Override
public void connect(IPlugInPort plugInPort) {
this.plugInPort = plugInPort;
swingUI.injectMenuAction(new MyAction(plugInPort), "Tools");
}
@Override
public EnumSet<EventType> getSubscribedEventTypes() {
return EnumSet.of(EventType.SELECTION_CHANGED);
}
@Override
public void processMessage(EventType eventType, Object... params) {
if (eventType == EventType.SELECTION_CHANGED) {
...
}
}
}
```
Install it in `MainFrame` alongside the others:
```java
presenter.installPlugin(() -> new MyFeaturePlugin(this));
```
Order matters for menus and for docked panels — insert the line where the resulting menu or panel
position is correct, not blindly at the end.
## Talking to the domain
Plugins reach the domain **only** through `IPlugInPort`. Never import a concrete component type into
a plugin unless the feature is genuinely component-specific, and never call into `Presenter`
directly. If `IPlugInPort` lacks what you need, adding a method there is the right move — but it also
means touching `diylc-core`, so keep the addition minimal and in the style of its neighbours.
## Reacting to events
`getSubscribedEventTypes()` returns an `EnumSet<EventType>`. Subscribe to the narrowest set that
works. `PROJECT_MODIFIED`, `REPAINT` and `MOUSE_MOVED` fire constantly — anything non-trivial in
response to those will be felt as lag. The available types are in
`diylc-core/src/main/java/org/diylc/common/EventType.java` (`PROJECT_LOADED`, `PROJECT_SAVED`,
`SELECTION_CHANGED`, `ZOOM_CHANGED`, `SLOT_CHANGED`, `LAYER_STATE_CHANGED`, `CLOUD_LOGGED_IN`, …).
## Injecting UI
- `swingUI.injectMenuAction(action, "Menu Name")` — a menu item; passing a `null` action inserts a
separator, and a missing menu is created automatically.
- `swingUI.injectSubmenu(name, icon, parentMenuName)` and
`injectDynamicSubmenu(name, icon, parentMenuName, handler)` for submenus.
- `swingUI.injectGUIComponent(component, SwingConstants.LEFT, collapsible, visibilityConfigKey)` for
a docked panel. `TOP`, `BOTTOM`, `LEFT`, `RIGHT` are valid; `CENTER` is reserved for the canvas.
Pass a config key for collapsible panels so the open/closed state persists.
- `swingUI.getOwnerFrame()` is the parent for dialogs.
## Actions
Actions are small classes extending `javax.swing.AbstractAction`, holding an `IPlugInPort`, setting
their label with `putValue(AbstractAction.NAME, ...)`, and delegating to `plugInPort` in
`actionPerformed`. See `actions/RenumberAction.java` for the minimal shape. They carry
`private static final long serialVersionUID = 1L;` like any Swing class.
## Background work
Never block the EDT. Use `swingUI.executeBackgroundTask(task, blockUI)` with an `ITask<T>`, which
handles the busy cursor and optional glass pane, or a dedicated executor for periodic work as
`AutoSavePlugin` does.
## Settings and strings
- Persisted preferences: `ConfigurationManager.getInstance().readString(KEY, default)` and
`writeValue(KEY, value)`, with the key as a `String` constant. Implement `IConfigListener` to react
to changes.
- Every user-visible string goes through `LangUtil.translate("...")`.
- Icons come from `IconLoader`.
## Verify
```bash
mvn -q -pl diylc-swing -am compile
```
Compilation is not evidence that the UI works. Say so, and offer to walk through launching the app
via `.run/DIYLCStarter.run.xml` (which carries the required `--add-opens` / `--add-exports` flags) for
a visual check.
@@ -0,0 +1,84 @@
---
name: regression-test
description: Run the DIYLC end-to-end regression suite, which renders ~700 sample .diy projects and compares PNG exports and netlists against reference outputs. Use after any change to rendering, component drawing, serialization, netlist or analyzer logic, or when asked to check for regressions or regenerate reference files.
---
# Running the DIYLC regression suite
`RegressionTestRunner` (in `diylc-swing/src/test/java/org/diylc/`) loads every sample project under
`diylc-regression-data/input`, exports a PNG and extracts netlists, and compares both against
reference files produced by a known-good build. It is the only automated coverage of component
appearance and of the file format end to end — unit tests do not touch either.
**Always ask before running the full suite**, and never run `PREPARE` without explicit instruction.
## Modes
- **`TEST`** — compares against the reference files and writes a CSV report plus visual diff images.
This is what you almost always want.
- **`PREPARE`** — *regenerates* the reference PNGs and netlists from the current build. This
overwrites the golden copy and is correct only once a human has inspected the differences and
decided they are intended. Never run it on your own initiative.
## Easiest path
If the maintainer is at their IDE, the `.run/RegressionTestRunner.run.xml` IntelliJ configuration
already has the arguments and JVM flags. Offer that first.
## From the terminal
The runner needs the `--add-opens` / `--add-exports` flags and a classpath spanning all three
modules; `mvn exec:java` will not work because it runs in Maven's own classloader and fails to find
Reflections. Build a classpath and fork a JVM:
```bash
cd diylc
SP=/tmp # or the session scratchpad
mvn -q -pl diylc-swing -am test-compile
mvn -q -pl diylc-swing dependency:build-classpath -Dmdep.outputFile=$SP/cp-swing.txt -Dmdep.includeScope=test
mvn -q -pl diylc-core dependency:build-classpath -Dmdep.outputFile=$SP/cp-core.txt -Dmdep.includeScope=test
mvn -q -pl diylc-library dependency:build-classpath -Dmdep.outputFile=$SP/cp-lib.txt -Dmdep.includeScope=test
java \
--add-exports java.desktop/com.apple.eawt.event=ALL-UNNAMED \
--add-exports java.desktop/com.apple.eio=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.text=ALL-UNNAMED \
--add-opens java.desktop/java.awt=ALL-UNNAMED \
--add-opens java.desktop/java.awt.font=ALL-UNNAMED \
--add-opens java.desktop/java.awt.geom=ALL-UNNAMED \
-cp "diylc-swing/target/test-classes:diylc-swing/target/classes:diylc-core/target/classes:diylc-library/target/classes:$(cat $SP/cp-swing.txt):$(cat $SP/cp-core.txt):$(cat $SP/cp-lib.txt)" \
org.diylc.RegressionTestRunner \
"$(cd .. && pwd)/diylc-regression-data" TEST [filter]
```
Sanity-check the log line `Presenter - Current DIYLC version: 6.4.0` near the end. If it reports
`5.0.0`, the `--add-opens` flags are missing and the run is invalid.
### Arguments
1. Path to `diylc-regression-data`
2. `TEST` or `PREPARE`
3. Optional filter — a substring matched against file paths. **Use it.** The full suite is ~700
projects; while narrowing down an issue, run one file or one directory at a time
(e.g. `splitter2`, or `guitar`).
## Reading the results
- A CSV report is written to `diylc-regression-data/reports/`, named with the timestamp and version,
listing each file as OK or failed with the reason (`Images do not match`, `Netlists do not match
with switches`, `Netlists do not match without switches`).
- Rendered output and **visual diff images**, with red rectangles around every differing region, go
to `diylc-regression-data/output/` (not under version control).
- Reports are kept as historical evidence. If a run was exploratory rather than a real verification,
delete the generated CSV afterwards so the reports directory stays meaningful — and check
`git status` before finishing, since the runner writes into the repo.
## Interpreting a failure
A difference is not automatically a bug, but it is always a change the maintainer must see. Report
which files differ and what kind of difference it is; where an image diff exists, point at the file
so it can be looked at. Do not conclude "no regressions" from a filtered run — say exactly which
subset was run.