14 KiB
DIY Layout Creator — agent guidelines
DIYLC is a Java 21 Swing desktop application for drawing circuit layouts, schematics and guitar wiring diagrams. It is a mature codebase (~690 source files) with long-standing conventions and a file format that must stay readable by every version. Consistency with the surrounding code outweighs personal preference or modern idiom in every case.
The one rule that matters most
Before writing anything, read two or three existing files that do the same kind of job — the neighbouring component, the sibling plugin, the analyzer next to the one you are touching — and match them. Match their naming, their structure, their comment density, their ordering of members, their choice of helper classes. If your change looks different from its neighbours, it is wrong even if it compiles and works.
Modules
Maven reactor rooted at diylc/pom.xml, version 6.4.0, three application modules with a strict
one-way dependency chain, plus a build-time annotation processor:
| Module | Contains | Depends on |
|---|---|---|
diylc-core |
Domain model, Presenter, rendering pipeline, messaging, serialization, netlist engine, annotations |
— |
diylc-library |
Concrete component classes, transformers, datasheets, legacy file parsers, component-aware analyzers | diylc-core |
diylc-swing |
Swing shell, plugins, dialogs, editors, DIYLCStarter |
diylc-core, diylc-library |
diylc-processor |
ServiceIndexProcessor, run by javac over the three modules above; never on the runtime classpath |
— |
diylc-core must never reference a concrete component type. It knows components only through
IDIYComponent and the annotations. If core logic seems to need knowledge of a specific component,
that knowledge belongs in diylc-library (an analyzer, a transformer, a parser) or is exposed
through a new interface in core that the component implements — look at
org.diylc.core.gerber.IGerberComponentSimple for the established shape of that escape hatch.
Components, netlist analyzers and parsers, legacy file parsers and auto-creators are discovered
through a class index built at compile time: every concrete class implementing a type marked
@Indexed (IDIYComponent, INetlistAnalyzer, ...) is listed under META-INF/services/ by
diylc-processor, and ReflectionUtils.getIndexedClasses() reads that back at startup. Nothing
scans the classpath, and there is no registry to update when adding a component.
Build and verify
Run from diylc/. Before reporting a change as done:
mvn -q -pl diylc-core -am compile # after core changes
mvn -q -pl diylc-library -am test # after library changes (~20s)
mvn -q -pl diylc-swing -am compile # after swing changes
mvn -q test # whole reactor, when the change spans modules
Compile the module you touched plus everything downstream of it, and run the unit tests of any module whose tests could plausibly cover the change. Do not report success without having actually run these — quote the failure if something breaks rather than describing it.
Do not run mvn package, build-all-profiles.sh, or the regression suite unless asked; they are
slow and the deployment build needs Docker and signing certificates. The regression suite is
described in .claude/skills/regression-test/SKILL.md and is worth offering whenever a change
affects rendering, serialization or netlist output.
The app runs from the .run/DIYLCStarter.run.xml IntelliJ configuration, which carries the required
--add-opens/--add-exports JVM flags. Copy them if launching from a terminal.
Code style
The formatter is eclipse-java-google-style.xml (Google Java Style):
- Two-space indent, four spaces for continuation lines. No tabs.
- 100-column line limit.
- Source encoding is ISO-8859-1 — do not introduce non-ASCII characters into
.javafiles. Use Unicode escapes or the resource bundles instead. - Braces on the same line, and always braces — an
if,else,fororwhilebody gets a block even when it is a single statement. Plenty of older code omits them; do not copy that when you are writing new code, and add them when you are already editing the lines in question. - Imports:
java.*first, then third-party, thenorg.diylc.*, separated by blank lines. No wildcard imports. - Constants are
UPPER_SNAKE_CASE. Note that component appearance constants are deliberatelypublic staticand notfinal(BODY_COLOR,DEFAULT_WIDTH), so they can be overridden at runtime; keep that convention when adding new ones. private static final Logger LOG = Logger.getLogger(TheClass.class);— log4j 1.2, declared first among the static members. Only declare one on a class that actually needs it; see Logging below.- Nested types go at the bottom of the file, after every field, constructor and method of the enclosing class, immediately before its closing brace. A version enum or a small helper class declared halfway down the file pushes the code a reader came for out of sight; keeping them last means the outer class reads top to bottom without interruption. Several nested types keep their relative order.
Comments
Write comments only where the code cannot speak for itself: a non-obvious electrical or geometric reason, a workaround for a library bug, a compatibility constraint, a unit or coordinate-system assumption. Never restate the code.
// BAD — adds nothing
// set the body color
g2d.setColor(bodyColor);
// GOOD — explains something the reader cannot infer
// the outer foil marking is drawn on the lead that is closer to the first control point,
// which is the convention printed on most film capacitors
Javadoc on public interfaces and abstract base classes in diylc-core is expected and should carry
@author. Concrete component classes generally carry no Javadoc — do not add it just to fill space.
Logging
Keep logging sparse, and prefer removing a log line to adding one. A logger earns its place on a
class that has something a maintainer would want to read back: a swallowed failure, a slow or
one-off initialisation, an external file or service that may not be there. Do not declare a LOG
field that the class never uses.
LOG.error— something went wrong that the user or a bug report may need to explain.LOG.warn— a recoverable surprise.LOG.info— events that happen a handful of times per session, not per action.LOG.debug— anything finer. Assume it is switched off in the field.
Never log from code on a hot path: draw, control-point accessors, property getters, anything the
canvas calls on every repaint or every mouse move. The cost is real and the output buries whatever
else is in the log.
Do not wrap code in try/catch only so that there is something to log. If the block cannot
actually throw, the guard and the logger both come out.
License header
Every new source file starts with the GPL header, copied verbatim from HEADER.txt (or from any
existing class), immediately followed by the package declaration with no blank line between the
closing */ and package. This applies to Java files in all three modules, including tests.
File-format backward compatibility
DIYLC projects are XStream-serialized object graphs. Users open decade-old .diy files with the
current build, so the serialized shape of a component class is a public contract.
- Never rename or delete a serialized field, and never change its type. To replace one, keep the old
field, add a new one, mark the old accessors
@Deprecated, and migrate lazily inside the new getter. The canonical examples areAbstractTransparentComponent.getAlpha()(legacyByte alpha→Percentage alphaPercent) andResistor.getPowerNew()/AbstractFilmCapacitor.getVoltageNew(). - Fields added to an existing class deserialize as
nullin old files. Every read path must tolerate that, typically by defaulting in the getter rather than the constructor. - Never rename or move a component class that has shipped. If a component genuinely must change
identity, an alias has to be registered on the XStream side and the legacy parsers in
V1FileParser/V2FileParserreviewed. - Prefer a
Versionproperty on a single component class over a new class per revision when a board or part gains a new variant. One class with a version enum keeps old files loading and keeps the toolbox uncluttered. - Any change here calls for a regression-suite run.
Components
See .claude/skills/add-component/SKILL.md for the full recipe. The rules that apply to editing
existing components too:
- Extend the most specific abstract base that fits (
AbstractLeadedComponent,AbstractRadialComponent,AbstractBoard,AbstractTransparentComponent, …) rather than reimplementing shared behaviour. - Every editable property needs a matching getter/setter pair with
@EditablePropertyon the getter; the property name is derived from the getter unlessnameis given. - Drawing code must fetch strokes from
ObjectCache.getInstance().fetchBasicStroke(...)/fetchZoomableStroke(...)/fetchStroke(...)rather than allocatingnew BasicStroke(...).draw()runs on every repaint; do not allocate what can be cached or reused. - Conductive areas are reported to the
IDrawingObserverviastartTrackingContinuityArea(true)/stopTrackingContinuityArea(). Getting this wrong silently breaks continuity checks and the netlist, so mirror a neighbouring component exactly. - Rotation and mirroring live in an
IComponentTransformerreferenced from@ComponentDescriptor, never inside the component. drawIcon()draws into a small fixed box for the toolbox; it is hand-drawn, not scaled fromdraw().
Plugins and Swing
- Everything in the UI is an
IPlugIninstalled fromMainFrameviapresenter.installPlugin(...). - Plugins talk to the domain only through
IPlugInPort, and react to domain changes by returning a narrowEnumSet<EventType>fromgetSubscribedEventTypes(). Subscribe to as little as possible —PROJECT_MODIFIEDfires constantly. - UI is injected through
ISwingUI.injectMenuAction/injectGUIComponent/injectSubmenu, not by reaching into frames directly. - Persisted settings go through
ConfigurationManager.getInstance().readString/writeValuewith aStringkey constant. - Long-running work belongs on a background executor, never on the EDT.
Localization
Every string the user reads is localized through LangUtil.translate(...) and the dictionaries in
diylc-swing/src/main/resources/lang/. The full recipe is in the Localization section of
.claude/skills/add-plugin/SKILL.md; the rules that apply to every UI change:
- A UI change is not finished until its text is in the language files. Every new user-facing
string — menu item, dialog, message, label, tooltip, panel text, and on the component side every
new
@EditablePropertyname, enum display value and component category — must be added toEnglish.txtand translated into every other file inlang/, in the same change. Check first whether the key already exists; leave out pure identifiers (part numbers, switch codes, units). - Menu and submenu names and the
Action.NAMEof injected actions are translated byMainFrame— pass the English literal. Everything else is yours to translate: the message and title of everyshowMessage/showConfirmDialog/showInputDialog, dialog titles, labels, buttons, tooltips, placeholder text and text rendered into panes. - Text containing values is one key with
%splaceholders, formatted afterwards:String.format(LangUtil.translate("Could not open file. %s"), e.getMessage()). Never concatenate values into the string passed totranslate, and never assemble a sentence from translated fragments. - Each new key goes into
English.txtand, asEnglish|Translation, into every other language file inlang/, appended at the end with that file's own line endings. - Log and exception messages stay in English.
Testing
JUnit 4 under src/test/java in each module. Add tests where the project already tests: switching
logic, netlist and analyzer behaviour, datasheet parsing, core utilities. Component rendering and
user interaction are covered by the regression suite instead, not by unit tests. Test classes are
named *Test or *Tests and carry the license header like any other file.
Git
Do not commit or push. Make the change in the working tree and stop; the maintainer reviews and
commits. If a commit is explicitly requested, commit only what was asked for, never to a remote, and
never git add -A over unrelated dirty files.
Out of scope unless explicitly asked
diylc-server-api/— PHP backend that runs in an isolated environment on diy-fever.com and cannot be tested locally.deploy/,build-all-profiles.sh, packaging and notarization.lib/— vendored jars for dependencies not on Maven Central.diylc-regression-data/reports/— historical evidence, append-only.- Bumping the project version in the POMs.
Task recipes
Detailed, step-by-step recipes live in .claude/skills/ and are worth reading in full before
starting the corresponding kind of work:
| Skill | Covers |
|---|---|
add-component |
Creating or substantially changing a component in diylc-library |
add-plugin |
Adding UI: plugins, menu actions, panels, dialogs, background tasks |
regression-test |
Running the end-to-end regression suite and reading its report |
Reference
Chapter 13 of Mastering DIY Layout Creator is the authoritative developer guide and covers the architecture, extension points and regression suite in prose.