Files

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 .java files. Use Unicode escapes or the resource bundles instead.
  • Braces on the same line, and always braces — an if, else, for or while body 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, then org.diylc.*, separated by blank lines. No wildcard imports.
  • Constants are UPPER_SNAKE_CASE. Note that component appearance constants are deliberately public static and not final (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 are AbstractTransparentComponent.getAlpha() (legacy Byte alpha → Percentage alphaPercent) and Resistor.getPowerNew() / AbstractFilmCapacitor.getVoltageNew().
  • Fields added to an existing class deserialize as null in 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 / V2FileParser reviewed.
  • Prefer a Version property 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 @EditableProperty on the getter; the property name is derived from the getter unless name is given.
  • Drawing code must fetch strokes from ObjectCache.getInstance().fetchBasicStroke(...) / fetchZoomableStroke(...) / fetchStroke(...) rather than allocating new BasicStroke(...). draw() runs on every repaint; do not allocate what can be cached or reused.
  • Conductive areas are reported to the IDrawingObserver via startTrackingContinuityArea(true) / stopTrackingContinuityArea(). Getting this wrong silently breaks continuity checks and the netlist, so mirror a neighbouring component exactly.
  • Rotation and mirroring live in an IComponentTransformer referenced from @ComponentDescriptor, never inside the component.
  • drawIcon() draws into a small fixed box for the toolbox; it is hand-drawn, not scaled from draw().

Plugins and Swing

  • Everything in the UI is an IPlugIn installed from MainFrame via presenter.installPlugin(...).
  • Plugins talk to the domain only through IPlugInPort, and react to domain changes by returning a narrow EnumSet<EventType> from getSubscribedEventTypes(). Subscribe to as little as possible — PROJECT_MODIFIED fires constantly.
  • UI is injected through ISwingUI.injectMenuAction / injectGUIComponent / injectSubmenu, not by reaching into frames directly.
  • Persisted settings go through ConfigurationManager.getInstance().readString/writeValue with a String key 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 @EditableProperty name, enum display value and component category — must be added to English.txt and translated into every other file in lang/, 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.NAME of injected actions are translated by MainFrame — pass the English literal. Everything else is yours to translate: the message and title of every showMessage / showConfirmDialog / showInputDialog, dialog titles, labels, buttons, tooltips, placeholder text and text rendered into panes.
  • Text containing values is one key with %s placeholders, formatted afterwards: String.format(LangUtil.translate("Could not open file. %s"), e.getMessage()). Never concatenate values into the string passed to translate, and never assemble a sentence from translated fragments.
  • Each new key goes into English.txt and, as English|Translation, into every other language file in lang/, 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.