mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 02:32:09 +10:00
phase 1
This commit is contained in:
@@ -7,9 +7,13 @@ Status: not started.
|
||||
|
||||
## 1. Current state
|
||||
|
||||
- Traces, pads, PCB text and ground fills carry a `PCBLayer` (`Bottom`, `Top`, `Inner 1..6`).
|
||||
It feeds `PCBLayer.toGerberCopperLayer()`, `PCBLayerFilter`, the trace-mask print and
|
||||
continuity (`ILayeredComponent.getLayerId()`).
|
||||
- Traces, pads, PCB text and ground fills carry a `PCBLayer`: `Bottom` and `Top`, plus the
|
||||
deprecated `Inner 1..6`, kept only so that old files load and hidden in the editor. It feeds
|
||||
`PCBLayer.toGerberCopperLayer()`, `PCBLayerFilter`, the trace-mask print and continuity
|
||||
(`ILayeredComponent.getLayerId()`).
|
||||
- `IDrawingObserver.startDrawingSide(PCBLayer)` / `stopDrawingSide()` already exist
|
||||
(`copper-side-view.md`): paint on the side facing away from the viewer is drawn faded, in both
|
||||
the main view and the Underside Display mirror.
|
||||
- Every other Gerber-aware component implements `IGerberComponentSimple`, whose
|
||||
`getGerberLayer()` defaults to `SilkscreenTop`. About 35 classes in `diylc-library` rely on that
|
||||
default (resistors, film and radial capacitors, diodes, LEDs, transistor packages, DIL/SIL ICs,
|
||||
@@ -25,45 +29,44 @@ Status: not started.
|
||||
|
||||
| Decision | Rationale |
|
||||
| --- | --- |
|
||||
| A new two-value `BoardSide` (`Top`, `Bottom`), not a reuse of `PCBLayer` | Parts mount on the outer faces only. Inner layers would be invalid choices in the editor. |
|
||||
| The side is a `PCBLayer` (`_2` Top, `_1` Bottom). No separate side enum | DIYLC only models two-sided boards. The inner layers are deprecated and hidden in the editor (`copper-side-view.md`), so only the two valid choices are offered. |
|
||||
| A part's side goes through its own `IBoardSideComponent`, never through `ILayeredComponent` | `getLayerId()` drives continuity, and a through-hole part's leads must connect to copper on both sides. Reporting a resistor as layer `_1` there would cut it off from top copper. |
|
||||
| The side is the physical fact. Z-order stays paint order and is not tied to it | Boards are drawn opaque, so painting bottom parts beneath the board would hide them. Bottom-layer traces already follow this rule. |
|
||||
| The canvas shows the side visually and does not rely on z-order for it | That is what removes the ambiguity of a "bottom" part drawn above the board. |
|
||||
| Flipping a part is an action that sets the side, mirrors the part and sends it back | A part seen from above through the board is a mirror image. Changing the side without mirroring gives a footprint that is wrong for fabrication. |
|
||||
| The Gerber layer is swapped centrally, not inside each component | Components keep emitting their `*Top` layer. One place maps it to the other side. |
|
||||
| Default `Top`, with `null` meaning `Top` | Fields added to existing classes deserialize as `null` in old files. |
|
||||
| Default `_2` (Top), with `null` meaning Top | Fields added to existing classes deserialize as `null` in old files. |
|
||||
|
||||
## 3. Design
|
||||
|
||||
### diylc-core
|
||||
|
||||
- **`org.diylc.common.BoardSide`**: enum `Top`, `Bottom`, with a `toString()` label like `PCBLayer`.
|
||||
- **`org.diylc.core.IBoardSideComponent`**: `BoardSide getBoardSide();` with Javadoc and
|
||||
`@author`, in the same shape as `ILayeredComponent`. Core knows the side only through this
|
||||
interface.
|
||||
- **`org.diylc.core.IBoardSideComponent`**: `PCBLayer getBoardSide();` with Javadoc and
|
||||
`@author`, in the same shape as `ILayeredComponent` but deliberately separate from it (see
|
||||
Decisions). It returns `_1` or `_2` only. Core knows the side only through this interface.
|
||||
- **`GerberLayer.flip()`**: maps `CopperTop`/`CopperBot`, `SilkscreenTop`/`SilkscreenBot` and
|
||||
`SolderMaskTop`/`SolderMaskBot` to each other. `Outline` and the drill layers map to themselves.
|
||||
- **Gerber dispatch**: in the simple-component branch, use
|
||||
`getGerberLayer().flip()` when the component is an `IBoardSideComponent` on `Bottom`. Merge
|
||||
`getGerberLayer().flip()` when the component is an `IBoardSideComponent` on `_1`. Merge
|
||||
`GerberExporter.drawComponent` and `GerberPathRenderer.drawComponent` into one method first, so
|
||||
the rule is written once.
|
||||
- **Canvas rendering**: `DrawingManager.drawProject` draws bottom-side components with a
|
||||
composite that sets them apart. Locked components already use a 0.5 alpha composite
|
||||
(`lockedComposite`), so the bottom-side treatment has to look different from that, for example
|
||||
a lower alpha or a tint. Pick one and check it against a locked bottom-side part.
|
||||
- **Canvas rendering**: bottom-side components use the far-side fade from `copper-side-view.md`,
|
||||
wrapping their painting in `startDrawingSide(getBoardSide())`. They are then faded in the main
|
||||
view and solid in the Underside Display mirror, the same as bottom copper.
|
||||
|
||||
### diylc-library
|
||||
|
||||
- The ~35 classes that use the `SilkscreenTop` default implement `IBoardSideComponent`. Each adds a
|
||||
nullable `BoardSide boardSide` field and a getter/setter pair, with
|
||||
`@EditableProperty(name = "Board Side")` on the getter. The getter returns `Top` when the field
|
||||
is `null`. This duplicates the field across classes in the same way the `PCBLayer` field is
|
||||
nullable `PCBLayer boardSide` field and a getter/setter pair, with
|
||||
`@EditableProperty(name = "Board Side")` on the getter. The getter returns `_2` (Top) when the
|
||||
field is `null`. This duplicates the field across classes in the same way the `PCBLayer` field is
|
||||
duplicated today. Where several of these classes share an abstract base
|
||||
(`AbstractRadialComponent`, `AbstractFilmCapacitor`, `AbstractTransistorPackage`, ...), put it on
|
||||
the base.
|
||||
- `CopperTrace`, `CurvedTrace`, `GroundFill`, `SolderPad`, `PCBText` and `DrillHole` do **not**
|
||||
implement it. Their `PCBLayer` already says where they are.
|
||||
- Before allowing `Bottom` on a class, check that its transformer can mirror it
|
||||
(`canMirror`). A class that cannot be mirrored must not offer `Bottom`, or the footprint would be
|
||||
- Before allowing Bottom on a class, check that its transformer can mirror it
|
||||
(`canMirror`). A class that cannot be mirrored must not offer Bottom, or the footprint would be
|
||||
wrong.
|
||||
|
||||
### diylc-swing
|
||||
@@ -81,6 +84,35 @@ Status: not started.
|
||||
silkscreen outline. Split it into top and bottom pages, filtered by side, the same way
|
||||
`TraceMaskDrawingProvider` gives one page per `PCBLayer`.
|
||||
|
||||
### Editing through the Underside Display mirror
|
||||
|
||||
The mirror is a second drawing of the same components, not a separate space: every component
|
||||
lives in main-view coordinates. Editing in the mirror therefore means mapping the mouse back
|
||||
through the mirror's reflection before the existing logic runs. A reflection is its own inverse,
|
||||
so drawing and hit-testing use the same transform. Each stage is useful by itself and ships in
|
||||
order.
|
||||
|
||||
- **Shared mirror geometry.** Move the transform built in `DrawingManager.drawMirroredComponent`
|
||||
into a helper in `org.diylc.presenter` (not into `Presenter`). It answers two questions: the
|
||||
reflection for a given `IBoard`, and which board's mirror region, if any, contains a model point.
|
||||
The region is the board rectangle reflected about the pivot. `drawMirroredComponent` uses the
|
||||
helper, so drawing and hit-testing cannot drift apart.
|
||||
- **Stage 1: selecting in the mirror.** In `Presenter.findComponentsAt` and the click and hover
|
||||
paths, a point that falls in a mirror region and hits nothing in main-view space is reflected
|
||||
and tested again, limited to the components that board mirrors (those whose control point lies
|
||||
inside it, as in `drawMirroredComponent`). Clicking a part in the mirror selects the original, so
|
||||
the property editor, delete, copy and the popup menu all work unchanged. Selection highlighting
|
||||
and control points are drawn on both copies.
|
||||
- **Stage 2: dragging in the mirror.** A drag that starts in a mirror region reflects every
|
||||
subsequent mouse point before it reaches the existing drag code. Movement along the mirrored axis
|
||||
is therefore reversed, which is what the user sees. Snapping applies to the reflected point. Moving
|
||||
the board, or changing its Underside Offset, cancels nothing; the mirror simply follows.
|
||||
Control-point drags (stretching a trace) go through the same mapping.
|
||||
- **Stage 3: placing in the mirror.** A component instantiated with its first point in a mirror
|
||||
region is created at the reflected point. If it is an `IBoardSideComponent`, it is created with
|
||||
side `_1` (Bottom) and mirrored, as Flip to Other Side would do. A `PCBLayer` component is created on
|
||||
`_1`. Anything else is placed at the reflected point unchanged.
|
||||
|
||||
## 4. Open questions
|
||||
|
||||
- **Bottom silkscreen text.** Component labels drawn through `IGerberComponentSimple` have to read
|
||||
@@ -94,24 +126,40 @@ Status: not started.
|
||||
- **Plain property or action only.** A plain editable property lets a user change the side without
|
||||
mirroring. Either keep both (the property for already-mirrored parts) or show the side as
|
||||
read-only and change it only through the action.
|
||||
- **Components that belong to no side.** In mirror stage 3 a jumper, eyelet or wire dropped in the
|
||||
mirror has no side to set. Confirm that placing it at the reflected point unchanged is what users
|
||||
expect, or whether such parts should be refused in the mirror.
|
||||
- **Components inside two boards.** A component whose control points fall inside two boards with
|
||||
Underside Display on appears in both mirrors. Selecting through either is fine. Check that
|
||||
dragging through one mirror while the other redraws does not confuse the user.
|
||||
|
||||
## 5. Verification
|
||||
|
||||
- Unit tests in `diylc-core` for `GerberLayer.flip()` and for the merged dispatch method, using a
|
||||
stub `IBoardSideComponent` (top, bottom and `null`).
|
||||
- Load an old `.diy` with PCB parts and confirm every component reports `Top` and that the Gerber
|
||||
- Load an old `.diy` with PCB parts and confirm every component reports `_2` (Top) and that the Gerber
|
||||
output is byte-identical to before.
|
||||
- Export a board with one part on each side and open the files in a Gerber viewer: the bottom part
|
||||
appears only in `.gbo`, mirrored, and in the right position.
|
||||
- Unit tests for the mirror geometry helper: the region for each `BoardUndersideDisplay` direction
|
||||
and offset, and that reflecting a point twice returns it.
|
||||
- In the app, with Underside Display set to each direction: select, drag and stretch a trace in
|
||||
the mirror and confirm that the original follows correctly. Place a part in the mirror and confirm
|
||||
that it is bottom-side, mirrored, faded on the main view and solid in the mirror.
|
||||
- Run the regression suite, since rendering and serialization both change.
|
||||
|
||||
## 6. Next steps
|
||||
|
||||
1. Merge the two `drawComponent` copies in `diylc-core` and add `GerberLayer.flip()` with tests.
|
||||
2. Add `BoardSide` and `IBoardSideComponent`, and swap the Gerber layer in the dispatch.
|
||||
2. Add `IBoardSideComponent` and swap the Gerber layer in the dispatch.
|
||||
3. Add the field to the library components, putting it on shared bases where they exist. Settle
|
||||
the silkscreen-text question while doing so.
|
||||
4. Add the bottom-side composite in `DrawingManager`.
|
||||
4. Wrap bottom-side painting in `startDrawingSide(getBoardSide())`.
|
||||
5. Add the Flip to Other Side action.
|
||||
6. Split the silkscreen print-out by side.
|
||||
7. Run the regression suite.
|
||||
7. Extract the mirror geometry helper, with tests, and use it in `drawMirroredComponent`.
|
||||
8. Mirror stage 1: selecting through the mirror. It does not depend on steps 1-6 and can ship
|
||||
early.
|
||||
9. Mirror stage 2: dragging through the mirror.
|
||||
10. Mirror stage 3: placing in the mirror, after Flip to Other Side exists.
|
||||
11. Run the regression suite.
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# Copper-Side View
|
||||
|
||||
Make it obvious which side of a board the canvas shows. The main view is the component side, and
|
||||
copper on the bottom of the board is drawn faded, as if seen through it. The Underside Display
|
||||
mirror is a true view from below: copper solid, components faded.
|
||||
|
||||
Status: implemented in the working tree; the open questions below and the regression run remain.
|
||||
|
||||
## 1. Background
|
||||
|
||||
- Z-order is paint order (`BOARD` < `TRACE` < `COMPONENT`), so boards, strips and traces were all
|
||||
painted with copper on top and users mistook the main view for the copper side.
|
||||
- Strips and pads are not separate components: the boards paint them inside their own `draw()`.
|
||||
- `CopperTrace`, `CurvedTrace`, `SolderPad`, `GroundFill` and `PCBText` carry a `PCBLayer` that
|
||||
defaults to `_1` (Bottom), so the model already placed them on the bottom.
|
||||
|
||||
## 2. Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
| --- | --- |
|
||||
| One rule for every view: near side solid, far side at reduced opacity, as if seen through the board | The same rule covers the main view, the mirror and, later, bottom-side components (`bottom-side-components.md`). |
|
||||
| Paint order and z-order stay unchanged. Opacity alone shows the side | Boards are opaque, so painting copper beneath the board would hide it. Editing, hit-testing and selection are untouched. |
|
||||
| Rendering-only change. No serialized field is added, renamed or retyped | Old files open with the new look and nothing to migrate. |
|
||||
| `PCBLayer` is the side; no separate side enum. Inner layers are deprecated, not removed | DIYLC models two-sided boards only. XStream resolves enums by name, so `_3`-`_8` must stay for old files to load. |
|
||||
| Far-side paint uses plain reduced opacity (`DrawingManager.FAR_SIDE_ALPHA`, 55%), not raised when selected | Simple, and reads clearly as "behind". 45% was too faint on the default phenolic strips. |
|
||||
| The fade takes the stronger of `FAR_SIDE_ALPHA` and the component's own alpha; it does not multiply them | Multiplying with the 50% locked alpha left a locked board's copper at about 22%. The cost is that a bottom part already more translucent than `FAR_SIDE_ALPHA` does not fade further. |
|
||||
| A global Config toggle, **Fade Copper Under Board**, on by default and not saved in projects | Users who prefer the old look can turn it off without a per-board property. |
|
||||
| The canvas and the layout export, PDF and print follow the toggle. Trace mask, silkscreen and Gerber are unaffected | The layout outputs show what the canvas shows; the fabrication outputs build their own `DrawOption` sets. |
|
||||
| The canvas draws no side labels. Users label the sides themselves | A caption drawn outside the mirror can sit on top of wiring, and only the user knows where a label fits. |
|
||||
| `undersideTransparency` no longer fades the whole mirror: `true` (default) fades the far side, `false` draws the mirror fully solid | The near side of a true underside view has to be solid. The field keeps its name, so old files load. |
|
||||
| Paint a component does not declare is component side, except a board's own paint in the mirror, which is drawn as near side | Components need no changes. A board's substrate is seen from both sides, so it must not fade in the mirror. |
|
||||
| Holes and trace cuts are visible from both sides (`startDrawingBothSides()`) and never fade; a board's own holes are already solid as board paint | A faded hole reads as copper, and a faded cut hides the break the user is looking for. |
|
||||
| `Jumper`, `Eyelet`, `Turret`, `Top`-layer copper and `EyeletBoard` eyelets are component side | Jumpers run over the board; eyelets and turrets are visible from the top. |
|
||||
| `ProtoBoard780` declares its traces and pads bottom side, like `PerfBoard`. `ProtoBoard` declares only the traces and pads its code paints in the underside colours. `EurorackStripboard` declares nothing | The 780-hole boards carry copper on the bottom only. `ProtoBoard` is plated through with some traces on top. Which side `EurorackStripboard`'s traces are on is not sourced. |
|
||||
| `Breadboard`, `BlankBoard`, `MarshallPerfBoard`, `TagStrip`, `TerminalStrip` and the maker boards declare nothing | They have no bottom copper to show. |
|
||||
|
||||
## 3. Design as built
|
||||
|
||||
### diylc-core
|
||||
|
||||
- `PCBLayer`: `_3`-`_8` are `@Deprecated`, in their original order so that `getId()` is stable.
|
||||
They are not remapped and keep their own continuity layer. For fading they count as Bottom.
|
||||
- `IDrawingObserver.startDrawingSide(PCBLayer)`, `startDrawingBothSides()` and
|
||||
`stopDrawingSide()` are default no-op methods, so `GerberG2DWrapper` and test observers need no
|
||||
change.
|
||||
- `G2DWrapper` keeps the viewed side, the side assumed for undeclared paint, and the side being
|
||||
painted. While the painted side differs from the viewed side and fading is on, it draws at the
|
||||
smaller of `FAR_SIDE_ALPHA` and the component's own alpha. Both-sides paint is treated as the
|
||||
viewed side. `getComposite()` returns the
|
||||
component's own composite and `setComposite()` re-applies the fade, so components that save and
|
||||
restore their composite keep working. Outline mode never fades. Only the composite changes, so
|
||||
outline and continuity areas are identical with and without fading.
|
||||
- `DrawOption.FADE_FAR_SIDE` turns fading on in the main view. `IPlugInPort.FADE_FAR_SIDE_KEY`
|
||||
(`fadeFarSide`) is the config key.
|
||||
- `DrawingManager.drawMirroredComponent` views the mirror from `_1`, with fading controlled by
|
||||
`undersideTransparency`. The reflection is built by `createMirrorTransform`.
|
||||
|
||||
### diylc-library
|
||||
|
||||
- `VeroBoard` and `TriPadBoard` declare each strip `_1`; `PerfBoard` declares each pad `_1`. The
|
||||
holes stay undeclared.
|
||||
- `ProtoBoard780` declares its traces and pads `_1`; holes, mounting holes and labels stay
|
||||
undeclared.
|
||||
- `ProtoBoard` declares its underside traces and underside mini pads `_1`.
|
||||
- `CopperTrace` and `PCBText` declare their whole canvas `draw` on `getLayer()`. `CurvedTrace` and
|
||||
`GroundFill` declare their copper fill. `SolderPad` declares the pad on its layer and its
|
||||
drilled hole as both sides.
|
||||
- `TraceCut` draws its cut and hole as both sides.
|
||||
|
||||
### diylc-swing
|
||||
|
||||
- The **Fade Copper Under Board** checkbox is in the Config menu after Extra Working Area.
|
||||
`CanvasPanel.getDrawOptions` and `ProjectDrawingProvider.draw` add `FADE_FAR_SIDE` from it.
|
||||
- `EnumEditor` hides `@Deprecated` constants unless one is the current value.
|
||||
- The language key **Fade Copper Under Board** is in every file in `lang/`.
|
||||
|
||||
## 4. Open questions
|
||||
|
||||
- **`Dot` on stripboard.** Users draw solder joints with `Dot`, which is generic and undeclared,
|
||||
so dots are solid in the main view and faded in the mirror.
|
||||
- **`EurorackStripboard`.** Unchanged until the side of its traces is sourced.
|
||||
- **AI catalog.** `ComponentCatalogGenerator` still lists the deprecated inner layers as possible
|
||||
values.
|
||||
|
||||
## 5. Verification
|
||||
|
||||
- `DrawingManagerTest` checks rendered pixels: undeclared paint is solid; bottom and inner-layer
|
||||
paint is faded only with `FADE_FAR_SIDE`; both-sides paint is solid in both views; a locked
|
||||
bottom part is faded once, not twice. In the mirror, copper is solid and the component side
|
||||
is faded, and `undersideTransparency=false` draws everything solid. The composite is restored,
|
||||
setting a composite mid-side re-fades it, outline mode never fades, and continuity areas do not
|
||||
change.
|
||||
- `InnerLayerCompatibilityTest` loads a trace saved on `_5` and checks that its layer and layer id
|
||||
are kept.
|
||||
- Still to do: run the regression suite and regenerate reference PNGs for the expected
|
||||
faded-copper changes. Netlists must not change.
|
||||
|
||||
## 6. Next steps
|
||||
|
||||
1. Settle the open questions above.
|
||||
2. Run the regression suite.
|
||||
Reference in New Issue
Block a user