15 KiB
Bottom-Side Components
Let a component be mounted on the bottom of a PCB, so that its silkscreen goes to the bottom Gerber layers and the canvas shows it as a part on the far side of the board.
Status: 45 parts have a Board Side property, and Gerber export sends a bottom-side part's silkscreen to the bottom layer with its text mirrored. Parts can be selected, dragged and stretched through the Underside Display mirror. The canvas draws a bottom-side part faded in the main view and solid in the mirror. The silkscreen print-out has a separate page for the bottom, and new parts can be placed through the mirror. Every step is done, and the regression suite passes on all 97 projects; the open questions below remain.
1. Current state
- Traces, pads, PCB text and ground fills carry a
PCBLayer:BottomandTop, plus the deprecatedInner 1..6, kept only so that old files load and hidden in the editor. It feedsPCBLayer.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, whosegetGerberLayer()defaults toSilkscreenTop. About 35 classes indiylc-libraryrely on that default (resistors, film and radial capacitors, diodes, LEDs, transistor packages, DIL/SIL ICs, SMD passives, switches, jacks, relays, transformers, tube sockets, ...). None of them emit copper. Their pads are separateSolderPadcomponents, which already have a layer. IBoardSideComponent.sideOf(component)is the one place that answers which side a component is on (Top for components without a side); drawing, the Gerber export and the silkscreen filter all use it. Copper that sits on a layer -CopperTrace,CurvedTrace,GroundFill,SolderPadandPCBText- implementsIPCBLayerComponent(getLayer/setLayer), whichPCBLayerFilter, the trace-mask print-out andMirrorPlacementuse instead of looking the methods up by name.- A simple component's Gerber layer follows its side: the default
IGerberComponentSimple.getGerberLayer()returnsSilkscreenBotfor anIBoardSideComponenton_1(Bottom) andSilkscreenTopotherwise.GerberExporter.drawComponent, package-private so thatGerberExporterTestcan call it, mirrors the text of bottom-side parts. org.diylc.core.IBoardSideComponent:PCBLayer getBoardSide(), returning_1or_2only. It is deliberately separate fromILayeredComponent(see Decisions). Core knows a part's side only through this interface.- Z-order is paint order only. A trace on the bottom layer already paints above the board
(
zOrder = TRACE), so the codebase already treats the physical side as separate from z-order.
2. Decisions
| Decision | Rationale |
|---|---|
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. |
| The side is a plain property, and the app never mirrors a part for the user | Seen from above, a part under the board is a mirror image, so a part with an asymmetric footprint moved to the bottom also needs Mirror Horizontally. Doing that for the user was tried as a Flip to Other Side action and dropped as menu clutter; the user sets the side and mirrors the part where needed. |
The Gerber layer follows the side in the IGerberComponentSimple default, not in each component or in the exporter |
Side and layer are decided in one place on the part, with no adjustment afterwards. None of the sided parts overrides getGerberLayer(). |
Default _2 (Top), with null meaning Top |
Fields added to existing classes deserialize as null in old files. |
| Bottom-side text is mirrored centrally in the Gerber output | Parts draw their labels readable from the top, which is backwards on the bottom silkscreen once the board is turned over. While GerberExporter.drawComponent draws a bottom-side part, GerberG2DWrapper.setMirrorText mirrors each string about the centre of its layout box, so no component changes and the canvas keeps readable labels. |
| A part without a side placed in the mirror is placed unchanged | A jumper, eyelet or wire has no side to set, so it is placed at the reflected point like any other part rather than refused. |
A SolderPad opens the solder mask on its own side |
It used to write SolderMaskBot whatever its layer, which was wrong for top-layer pads. It now writes SolderMaskTop for a pad on _2. |
3. Design
diylc-core
- Canvas rendering (done): bottom-side parts use the far-side fade from
copper-side-view.md, set centrally rather than in each component. Wherever a component is drawn - the main loop inDrawingManager,DrawingCacherendering an image, and the Underside Display mirror - the wrapper's undeclared side is set toIBoardSideComponent.sideOf(component)withG2DWrapper.setUndeclaredSide. A bottom part is therefore faded in the main view (with Fade Copper Under Board on) and solid in the mirror, the same as bottom copper. In the main loop the side is set after the locked composite, which is set on the canvas directly and would otherwise replace the fade.DrawingCachepastes a cached image as both-sides paint, because the fade is already in the image.
diylc-library
- Done. Every part that uses the
SilkscreenTopdefault implementsIBoardSideComponent: a nullablePCBLayer boardSidefield and a getter/setter pair with@EditableProperty(name = "Board Side")on the getter, which returns_2(Top) when the field isnull. The key is in every language file. 45 parts andDothave it.- On a shared base, covering every subclass:
AbstractFilmCapacitor,AbstractPotentiometer,AbstractRadialComponent,AbstractTransistorPackage,PassiveSMDComponent. - On the class itself:
Resistor,HeatSinkResistor,AxialElectrolyticCapacitor,CrystalOscillator,ToroidalInductor,AudioTransformer,MultiSectionCapacitor,DiodePlastic,DiodeGlass,BridgeRectifier,DIL_IC,SIL_IC,MultiwattIC,TubeSocket,CliffJack1_4,CliffJack1_8,ClosedJack1_4,PlasticDCJack,MiniToggleSwitch,SlideSwitch,TactileMicroSwitch,RotarySwitchOpen,RotarySwitchSealed,RotarySelectorSwitch,MiniRelay,PCBTerminalBlock. - Whether a part can be mirrored does not matter: the app never mirrors it for the user.
Dothas it too, though it has no silkscreen: a dot used as a solder joint on stripboard can be put on the copper side (seecopper-side-view.md).Labelis left out: it is text, not a mounted part, andPCBTextalready has a layer.BoardSideComponentsTestchecks that every such part starts on Top and takes Bottom, and that every part writingSilkscreenTophas a side, so a new part cannot be left out by accident.
- On a shared base, covering every subclass:
CopperTrace,CurvedTrace,GroundFill,SolderPad,PCBTextandDrillHoledo not implement it. TheirPCBLayeralready says where they are.
diylc-swing
- PCB print-outs (done):
SilkScreenDrawingProviderprints the top side, and a second page for the bottom when the bottom has silkscreen other than board outlines, so a project without bottom parts, or with only a bottomDot, still prints one page.SilkScreenFiltertakes the side: a part with a Board Side goes on its own side's page, parts without one count as top, and board outlines go on both pages so that they line up. Neither page is mirrored or captioned, the same as the trace-mask pages.
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 (done).
org.diylc.presenter.UndersideMirrorholds the reflection for a board, the region its mirror occupies (the board rectangle reflected about the pivot), and which components a mirror shows: the board and every component with a control point on it.DrawingManagerdraws the mirror with it, so drawing and hit-testing cannot drift apart. - Stage 1: selecting in the mirror (done).
DrawingManager.findMirroredComponentsAtreflects a point that falls in a mirror region and tests it again, limited to the components that board mirrors.Presenter.findComponentsAtScaledfalls back to it when nothing selectable is hit in the main view; locked and hidden parts are filtered out first, so a locked part under the mirror does not block it. 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 the selected control points are drawn on both copies. - Stage 2: dragging in the mirror (done). A drag that starts on a part in a mirror, or on a
control point of a selected part there, keeps that mirror's board in
Presenter.dragMirrorBoardand reflects every mouse point of the drag through it (UndersideMirror.reflect) before it reaches the existing drag code. The part it picks is one the mirror shows, the same one a click there selects, even where something else covers the reflected point on the board. Movement along the mirrored axis is therefore reversed, which is what the user sees, and snapping and the shift axis lock apply to the reflected point. Control-point hover (Presenter.findControlPointsAt) and the hand cursor look through the mirror when the main view has nothing. Where mirrors overlap, the topmost board's wins, here and for placing. A drag that starts on empty mirror space still draws a selection rectangle in screen space. Moving the board, or changing its Underside Offset, cancels nothing; the mirror simply follows. - Stage 3: placing in the mirror (done). While a component is being placed,
Presenter.placementMirrorBoardholds the board whose mirror the cursor is in; a single-click part follows the cursor in and out of mirrors, and a part placed point by point stays in the mirror its first point went into. The cursor is reflected before it is snapped to the grid, so the part lands on the grid, andDrawingManager.setSlotMirrorBoarddraws the part being placed through that mirror, under the cursor, seen from below like the parts already there. The board is forgotten when a project is loaded. On placement,MirrorPlacement.placeOnBottomsets anIBoardSideComponentto_1(Bottom) and anIPCBLayerComponentto layer_1; nothing is mirrored. Anything else, such as a jumper, eyelet or wire, is placed at the reflected point unchanged. A part dragged in from the component tree or the toolbox goes throughPresenter.dragOveranddragEndedinstead, which pick the mirror and put the part on the bottom the same way.
4. Open questions
- Locked bottom-side parts drawn from the cache. An uncached locked part takes the stronger of the locked alpha and the far-side alpha. A cached one is pasted at the locked alpha onto an image that already carries the far-side fade, so it ends up fainter. The same applies to bottom-side paint inside any cached component.
- 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
GerberExporterTestcovers the layer and text direction of simple components, including a stubIBoardSideComponenton top, bottom andnull.MirrorPlacementTestcovers what placing through the mirror changes, andPresenterTests.testPlaceThroughUndersideMirrorandtestPlaceUnsidedPartThroughUndersideMirrorplace a resistor and an eyelet in a mirror and check that they land on the board, the resistor on the bottom;testDropFromComponentTreeIntoUndersideMirrordoes the same for a drag and drop.testDragThroughUndersideMirrorIgnoresPartsTheMirrorDoesNotShowdrags a resistor in the mirror under a jumper that only crosses the board, andDrawingManagerTest.testPartBeingPlacedInMirrorSeenFromBelowchecks that the part being placed is drawn as the mirror draws placed parts.SilkScreenFilterTestcovers which side's page each part and board outline goes on, andSilkScreenDrawingProviderTestchecks that the bottom page appears only for a bottom part with silkscreen, not for a bottomDot.DrawingManagerTestchecks rendered pixels for a part that reports its own side: a bottom part is faded only withFADE_FAR_SIDE, also when drawn from the image cache; it is solid in the mirror, where a top part is faded; and a locked bottom part is faded once, not twice.- Load an old
.diywith 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. UndersideMirrorTestcovers the mirror region for eachBoardUndersideDisplaydirection and offset, that reflecting a point twice returns it, and which components a mirror shows.DrawingManagerTest.testComponentFoundThroughUndersideMirrorcovers the hit test, andPresenterTests.testStretchThroughUndersideMirrorandtestDragThroughUndersideMirrorcheck that a drag through a mirror below the board moves the part up on the board.- 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, faded on the main view and solid in the mirror.
- The regression suite passes on all 97 projects with the whole plan in place.
6. Next steps
- Settle the open questions above.
- Check Gerber output of a board with parts on both sides in a Gerber viewer, and try the Underside Display editing in each direction in the app, as listed under Verification.