diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java b/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java index a707dc42e..feb042b8e 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/IPlugInPort.java @@ -364,22 +364,24 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP void setNewComponentTypeSlot(ComponentType componentType, Template template, String[] model, boolean forceInstatiate); /** - * Locks or unlocks the specified layer. All components within +- 0.5 range will be affected by - * the change as well. + * Locks or unlocks the specified layer: the components whose z-order rounds to it, or the + * components on one side of the board for a trace layer. * - * @param layerZOrder + * @param layerId a z-order such as {@link org.diylc.core.IDIYComponent#BOARD}, or one of the + * trace layers in {@link org.diylc.core.LayerIds} * @param locked */ - void setLayerLocked(int layerZOrder, boolean locked); + void setLayerLocked(int layerId, boolean locked); /** - * Shows or hides the specified layer. All components within +- 0.5 range will be affected by the - * change as well. + * Shows or hides the specified layer: the components whose z-order rounds to it, or the + * components on one side of the board for a trace layer. * - * @param layerZOrder + * @param layerId a z-order such as {@link org.diylc.core.IDIYComponent#BOARD}, or one of the + * trace layers in {@link org.diylc.core.LayerIds} * @param visible */ - void setLayerVisibility(int layerZOrder, boolean visible); + void setLayerVisibility(int layerId, boolean visible); /** * @return selection size expressed in both inches or centimeters, respectively diff --git a/diylc/diylc-core/src/main/java/org/diylc/common/ISelectionProcessor.java b/diylc/diylc-core/src/main/java/org/diylc/common/ISelectionProcessor.java index 250a19cb7..20669c8b0 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/common/ISelectionProcessor.java +++ b/diylc/diylc-core/src/main/java/org/diylc/common/ISelectionProcessor.java @@ -77,7 +77,8 @@ public interface ISelectionProcessor extends IGroupProcessor { /** * Selects all components in the project. * - * @param int layer if > 0, designates which layer to select. If <= 0 we should select all + * @param int layer if > 0, the id of the layer to select, as for + * {@link IPlugInPort#setLayerVisibility(int, boolean)}. If <= 0 we should select all * regardless of layer */ void selectAll(int layer); diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/LayerIds.java b/diylc/diylc-core/src/main/java/org/diylc/core/LayerIds.java new file mode 100644 index 000000000..40e9687e4 --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/core/LayerIds.java @@ -0,0 +1,72 @@ +/* + + DIY Layout Creator (DIYLC). + Copyright (c) 2009-2025 held jointly by the individual authors. + + This file is part of DIYLC. + + DIYLC is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DIYLC is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DIYLC. If not, see . + +*/ +package org.diylc.core; + +import java.util.Set; + +/** + * Ids of the layers that can be hidden or locked in a project. A layer is a rounded z-order, such + * as {@link IDIYComponent#BOARD}, except that the traces are split into a bottom and a top layer. + *

+ * The traces used to be one layer, {@link IDIYComponent#TRACE}, and older DIYLC versions still + * know only that one. Project files therefore keep {@link #LEGACY_TRACES} in a hidden or locked + * set exactly when both trace layers are in it, so that an older version hides or locks all + * traces when the new one has both sides hidden or locked. + * + * @author Branislav Stojkovic + */ +public final class LayerIds { + + public static final int LEGACY_TRACES = IDIYComponent.TRACE; + public static final int BOTTOM_TRACES = IDIYComponent.TRACE * 10 + 1; + public static final int TOP_TRACES = IDIYComponent.TRACE * 10 + 2; + + private LayerIds() {} + + /** + * Brings a hidden or locked layer set read from a file up to date. The legacy trace layer, from + * an old file or written by an older version, stands for both trace layers. Both trace layers + * without it can only mean that an older version showed or unlocked its single trace layer, so + * neither side is hidden or locked any more. + */ + public static void migrateLoaded(Set layers) { + if (layers.contains(LEGACY_TRACES)) { + layers.add(BOTTOM_TRACES); + layers.add(TOP_TRACES); + } else if (layers.contains(BOTTOM_TRACES) && layers.contains(TOP_TRACES)) { + layers.remove(BOTTOM_TRACES); + layers.remove(TOP_TRACES); + } + } + + /** + * Keeps the legacy trace layer in a hidden or locked set after it has been changed: present + * exactly when both trace layers are. + */ + public static void syncLegacy(Set layers) { + if (layers.contains(BOTTOM_TRACES) && layers.contains(TOP_TRACES)) { + layers.add(LEGACY_TRACES); + } else { + layers.remove(LEGACY_TRACES); + } + } +} diff --git a/diylc/diylc-core/src/main/java/org/diylc/core/Project.java b/diylc/diylc-core/src/main/java/org/diylc/core/Project.java index 90c4da51d..a8a18ed3b 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/core/Project.java +++ b/diylc/diylc-core/src/main/java/org/diylc/core/Project.java @@ -196,6 +196,17 @@ public class Project implements Serializable, Cloneable { return hiddenLayers; } + // called by XStream after loading, whichever way the project is opened; the trace layer was split + // in two, and files from older versions name only the single legacy one + private Object readResolve() { + if (lockedLayers == null) { + lockedLayers = new HashSet(); + } + LayerIds.migrateLoaded(lockedLayers); + LayerIds.migrateLoaded(getHiddenLayers()); + return this; + } + public VersionNumber getFileVersion() { return fileVersion; } diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentLayers.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentLayers.java new file mode 100644 index 000000000..ee36b1c7a --- /dev/null +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/ComponentLayers.java @@ -0,0 +1,109 @@ +/* + + DIY Layout Creator (DIYLC). + Copyright (c) 2009-2025 held jointly by the individual authors. + + This file is part of DIYLC. + + DIYLC is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DIYLC is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DIYLC. If not, see . + +*/ +package org.diylc.presenter; + +import java.util.Set; + +import org.diylc.common.ComponentType; +import org.diylc.common.PCBLayer; +import org.diylc.components.composite.CompositeComponent; +import org.diylc.core.IBoardSideComponent; +import org.diylc.core.IDIYComponent; +import org.diylc.core.ILayeredComponent; +import org.diylc.core.LayerIds; + +/** + * Decides which layer of the Layers menu a component is on, and with that whether it is hidden or + * locked. A component is on the layer of its rounded z-order, except in the trace band, which is + * split by board side: copper and other components with a side are on the bottom or top trace + * layer, a component without a side is on the top one like the rendering assumes, and a component + * that goes through the board, reporting layer id 0, is on both. + * + * @author Branislav Stojkovic + */ +public class ComponentLayers { + + // the component is on both trace layers + private static final int BOTH_TRACES = -1; + // internal components without a descriptor are on no layer + private static final int NONE = 0; + + private ComponentLayers() {} + + /** + * @return true when every layer the component is on is hidden + */ + public static boolean isHidden(Set hiddenLayers, IDIYComponent component) { + int layer = getLayer(component); + if (layer == BOTH_TRACES) { + return hiddenLayers.contains(LayerIds.BOTTOM_TRACES) + && hiddenLayers.contains(LayerIds.TOP_TRACES); + } + return layer != NONE && hiddenLayers.contains(layer); + } + + /** + * @return true when any layer the component is on is locked; moving a component that is on both + * trace layers changes the routing of both sides + */ + public static boolean isLocked(Set lockedLayers, IDIYComponent component) { + int layer = getLayer(component); + if (layer == BOTH_TRACES) { + return lockedLayers.contains(LayerIds.BOTTOM_TRACES) + || lockedLayers.contains(LayerIds.TOP_TRACES); + } + return layer != NONE && lockedLayers.contains(layer); + } + + /** + * @return true when the component is on the layer, including a component on both trace layers + * when the layer is one of them + */ + public static boolean isOnLayer(IDIYComponent component, int layerId) { + int layer = getLayer(component); + if (layer == BOTH_TRACES) { + return layerId == LayerIds.BOTTOM_TRACES || layerId == LayerIds.TOP_TRACES; + } + return layer != NONE && layer == layerId; + } + + @SuppressWarnings("unchecked") + private static int getLayer(IDIYComponent component) { + ComponentProcessor processor = ComponentProcessor.getInstance(); + ComponentType componentType = processor + .extractComponentTypeFrom((Class>) component.getClass()); + if (componentType == null) { + return NONE; + } + int layer = (int) Math.round(processor.getZOrder(component)); + if (layer != IDIYComponent.TRACE) { + return layer; + } + // a building block may hold copper of both sides + if (component instanceof CompositeComponent + || (component instanceof ILayeredComponent layered && layered.getLayerId() == 0)) { + return BOTH_TRACES; + } + return IBoardSideComponent.sideOf(component) == PCBLayer._1 ? LayerIds.BOTTOM_TRACES + : LayerIds.TOP_TRACES; + } +} diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/GroupAndLockIndex.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/GroupAndLockIndex.java index 18c0aa783..d11159cf3 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/GroupAndLockIndex.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/GroupAndLockIndex.java @@ -94,8 +94,7 @@ public class GroupAndLockIndex { if (componentType == null) { return false; } - return project.getLockedLayers() - .contains((int) Math.round(ComponentProcessor.getInstance().getZOrder(component))) + return ComponentLayers.isLocked(project.getLockedLayers(), component) || project.getLockedComponents().contains(component); } diff --git a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java index 1b7ded933..615fe42d3 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java +++ b/diylc/diylc-core/src/main/java/org/diylc/presenter/Presenter.java @@ -488,16 +488,8 @@ public class Presenter implements IPlugInPort { configManager.writeValue(IPlugInPort.DISABLED_CATEGORIES_KEY, new ArrayList()); } - @SuppressWarnings({"unchecked"}) private boolean isComponentVisible(IDIYComponent component) { - ComponentType componentType = - ComponentProcessor.getInstance().extractComponentTypeFrom( - (Class>) component.getClass()); - // for internal types - if (componentType == null) - return true; - return !currentProject.getHiddenLayers() - .contains((int) Math.round(ComponentProcessor.getInstance().getZOrder(component))); + return !ComponentLayers.isHidden(currentProject.getHiddenLayers(), component); } @Override @@ -1162,7 +1154,7 @@ public class Presenter implements IPlugInPort { Iterator> i = newSelection.iterator(); while (i.hasNext()) { IDIYComponent c = i.next(); - if ((int) ComponentProcessor.getInstance().getZOrder(c) != layer) { + if (!ComponentLayers.isOnLayer(c, layer)) { i.remove(); } } @@ -2285,14 +2277,10 @@ public class Presenter implements IPlugInPort { } @Override - public void setLayerLocked(int layerZOrder, boolean locked) { - LOG.info(String.format("setLayerLocked(%s, %s)", layerZOrder, locked)); + public void setLayerLocked(int layerId, boolean locked) { + LOG.info(String.format("setLayerLocked(%s, %s)", layerId, locked)); Project oldProject = currentProject.clone(); - if (locked) { - currentProject.getLockedLayers().add(layerZOrder); - } else { - currentProject.getLockedLayers().remove(layerZOrder); - } + setLayerInSet(currentProject.getLockedLayers(), layerId, locked); groupAndLockIndex.invalidate(); updateSelection(EMPTY_SELECTION); messageDispatcher.dispatchMessage(EventType.LAYER_STATE_CHANGED, currentProject.getLockedLayers()); @@ -2301,14 +2289,10 @@ public class Presenter implements IPlugInPort { } @Override - public void setLayerVisibility(int layerZOrder, boolean visible) { - LOG.info(String.format("setLayerVisibility(%s, %s)", layerZOrder, visible)); + public void setLayerVisibility(int layerId, boolean visible) { + LOG.info(String.format("setLayerVisibility(%s, %s)", layerId, visible)); Project oldProject = currentProject.clone(); - if (visible) { - currentProject.getHiddenLayers().remove(layerZOrder); - } else { - currentProject.getHiddenLayers().add(layerZOrder); - } + setLayerInSet(currentProject.getHiddenLayers(), layerId, !visible); updateSelection(EMPTY_SELECTION); messageDispatcher.dispatchMessage(EventType.LAYER_VISIBILITY_CHANGED, currentProject.getHiddenLayers()); @@ -3225,8 +3209,22 @@ public class Presenter implements IPlugInPort { } private boolean isComponentLayerLocked(IDIYComponent component) { - return currentProject.getLockedLayers() - .contains((int) Math.round(ComponentProcessor.getInstance().getZOrder(component))); + return ComponentLayers.isLocked(currentProject.getLockedLayers(), component); + } + + // the legacy trace layer stands for both trace layers + private static void setLayerInSet(Set layers, int layerId, boolean inSet) { + int[] ids = layerId == LayerIds.LEGACY_TRACES + ? new int[] {LayerIds.BOTTOM_TRACES, LayerIds.TOP_TRACES} + : new int[] {layerId}; + for (int id : ids) { + if (inSet) { + layers.add(id); + } else { + layers.remove(id); + } + } + LayerIds.syncLegacy(layers); } /** diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index 4299c2ee5..8288001ac 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -665,6 +665,10 @@ IMPROVEMENT Gerber export produces files ready for any PCB fab: drill files are written in Excellon format, which every fab accepts, instead of Gerber format; holes are defined by the drill files alone instead of also being cut out of the copper layers; and solder pads are marked as component pads, or SMD pads when they have no hole, instead of as connector pads + + IMPROVEMENT + The 'Trace' layer in the Layers menu is split into 'Bottom Traces' and 'Top Traces', so the copper of one side can be hidden, locked or selected on its own; vias, eyelets, turrets and plated pads go through the board and belong to both, and jumpers are on the top. The layer shortcuts follow the menu order now, Ctrl+1 to Ctrl+7. Projects with the old 'Trace' layer hidden or locked open with both sides hidden or locked + BUG_FIX Rotated labels in pasted components and building blocks were drawn in the wrong place and at the wrong angle while following the mouse, until the components were placed diff --git a/diylc/diylc-core/src/test/java/org/diylc/core/LayerIdsTest.java b/diylc/diylc-core/src/test/java/org/diylc/core/LayerIdsTest.java new file mode 100644 index 000000000..2a33de17c --- /dev/null +++ b/diylc/diylc-core/src/test/java/org/diylc/core/LayerIdsTest.java @@ -0,0 +1,94 @@ +/* + + DIY Layout Creator (DIYLC). + Copyright (c) 2009-2025 held jointly by the individual authors. + + This file is part of DIYLC. + + DIYLC is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DIYLC is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DIYLC. If not, see . + +*/ +package org.diylc.core; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.io.xml.DomDriver; + +import org.diylc.serialization.ProjectFileManager; +import org.junit.Test; + +public class LayerIdsTest { + + private static final int LEGACY = LayerIds.LEGACY_TRACES; + private static final int BOTTOM = LayerIds.BOTTOM_TRACES; + private static final int TOP = LayerIds.TOP_TRACES; + + @Test + public void testLegacyTraceLayerStandsForBothSides() { + assertEquals(set(LEGACY, BOTTOM, TOP, IDIYComponent.BOARD), + migrate(LEGACY, IDIYComponent.BOARD)); + } + + @Test + public void testBothSidesWithoutLegacyMeansAnOlderVersionClearedTraces() { + assertEquals(set(IDIYComponent.BOARD), migrate(BOTTOM, TOP, IDIYComponent.BOARD)); + } + + @Test + public void testOneSideIsKept() { + assertEquals(set(TOP), migrate(TOP)); + } + + @Test + public void testLegacyFollowsBothSides() { + assertEquals(set(LEGACY, BOTTOM, TOP), sync(BOTTOM, TOP)); + assertEquals(set(TOP), sync(LEGACY, TOP)); + assertEquals(set(), sync(LEGACY)); + } + + @Test + public void testLoadedProjectIsMigrated() { + Project project = new Project(); + project.getHiddenLayers().add(LEGACY); + project.getLockedLayers().addAll(set(BOTTOM, TOP)); + + XStream xStream = new XStream(new DomDriver("UTF-8")); + ProjectFileManager.configure(xStream); + Project loaded = (Project) xStream.fromXML(xStream.toXML(project)); + + assertEquals(set(LEGACY, BOTTOM, TOP), loaded.getHiddenLayers()); + assertEquals(set(), loaded.getLockedLayers()); + } + + private static Set migrate(Integer... layers) { + Set set = set(layers); + LayerIds.migrateLoaded(set); + return set; + } + + private static Set sync(Integer... layers) { + Set set = set(layers); + LayerIds.syncLegacy(set); + return set; + } + + private static Set set(Integer... layers) { + return new HashSet(Arrays.asList(layers)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Eyelet.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Eyelet.java index 6edb53e88..e66bafbb0 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Eyelet.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Eyelet.java @@ -33,6 +33,7 @@ import org.diylc.core.ComponentState; import org.diylc.core.IConnective; import org.diylc.core.IDIYComponent; import org.diylc.core.IDrawingObserver; +import org.diylc.core.ILayeredComponent; import org.diylc.core.Project; import org.diylc.core.VisibilityPolicy; import org.diylc.core.annotations.AiNote; @@ -52,7 +53,8 @@ import org.diylc.utils.Constants; zOrder = IDIYComponent.TRACE + 0.1, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, transformer = SimpleComponentTransformer.class, enableCache = true) -public class Eyelet extends AbstractTransparentComponent implements IConnective { +public class Eyelet extends AbstractTransparentComponent + implements IConnective, ILayeredComponent { private static final long serialVersionUID = 1L; @@ -132,6 +134,12 @@ public class Eyelet extends AbstractTransparentComponent implements ICon return super.getName(); } + // goes through the board, so it is on every copper layer + @Override + public int getLayerId() { + return 0; + } + @Override public int getControlPointCount() { return 1; diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Turret.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Turret.java index 571559dae..0d0b77a11 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Turret.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Turret.java @@ -33,6 +33,7 @@ import org.diylc.core.ComponentState; import org.diylc.core.IConnective; import org.diylc.core.IDIYComponent; import org.diylc.core.IDrawingObserver; +import org.diylc.core.ILayeredComponent; import org.diylc.core.Project; import org.diylc.core.VisibilityPolicy; import org.diylc.core.annotations.AiNote; @@ -50,7 +51,8 @@ import org.diylc.utils.Constants; zOrder = IDIYComponent.TRACE + 0.1, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, transformer = SimpleComponentTransformer.class, enableCache = true) -public class Turret extends AbstractTransparentComponent implements IConnective { +public class Turret extends AbstractTransparentComponent + implements IConnective, ILayeredComponent { private static final long serialVersionUID = 1L; @@ -127,6 +129,12 @@ public class Turret extends AbstractTransparentComponent implements ICon return super.getName(); } + // goes through the board, so it is on every copper layer + @Override + public int getLayerId() { + return 0; + } + @Override public int getControlPointCount() { return 1; diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Via.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Via.java index 79c9b15f0..d3b2564c8 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Via.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Via.java @@ -36,6 +36,7 @@ import org.diylc.core.ComponentState; import org.diylc.core.IConnective; import org.diylc.core.IDIYComponent; import org.diylc.core.IDrawingObserver; +import org.diylc.core.ILayeredComponent; import org.diylc.core.Project; import org.diylc.core.VisibilityPolicy; import org.diylc.core.annotations.AiNote; @@ -62,7 +63,7 @@ import org.diylc.utils.Constants; bomPolicy = BomPolicy.NEVER_SHOW, keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "PCB", transformer = SimpleComponentTransformer.class, enableCache = true) public class Via extends AbstractTransparentComponent - implements IConnective, IGerberComponentCustom { + implements IConnective, ILayeredComponent, IGerberComponentCustom { private static final long serialVersionUID = 1L; @@ -175,6 +176,12 @@ public class Via extends AbstractTransparentComponent this.color = color; } + // goes through the board, so it is on every copper layer + @Override + public int getLayerId() { + return 0; + } + @Override public int getControlPointCount() { return 1; diff --git a/diylc/diylc-library/src/test/java/org/diylc/presenter/ComponentLayersTest.java b/diylc/diylc-library/src/test/java/org/diylc/presenter/ComponentLayersTest.java new file mode 100644 index 000000000..9ab6f6047 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/presenter/ComponentLayersTest.java @@ -0,0 +1,142 @@ +/* + + DIY Layout Creator (DIYLC). + Copyright (c) 2009-2025 held jointly by the individual authors. + + This file is part of DIYLC. + + DIYLC is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DIYLC is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DIYLC. If not, see . + +*/ +package org.diylc.presenter; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.diylc.common.PCBLayer; +import org.diylc.components.composite.CompositeComponent; +import org.diylc.components.connectivity.CopperTrace; +import org.diylc.components.connectivity.Eyelet; +import org.diylc.components.connectivity.Jumper; +import org.diylc.components.connectivity.SolderPad; +import org.diylc.components.connectivity.Turret; +import org.diylc.components.connectivity.Via; +import org.diylc.components.passive.Resistor; +import org.diylc.core.IDIYComponent; +import org.diylc.core.LayerIds; +import org.junit.Test; + +public class ComponentLayersTest { + + private static final int BOTTOM = LayerIds.BOTTOM_TRACES; + private static final int TOP = LayerIds.TOP_TRACES; + + @Test + public void testCopperIsOnTheLayerOfItsSide() { + assertOnlyOn(trace(PCBLayer._1), BOTTOM); + assertOnlyOn(trace(PCBLayer._2), TOP); + } + + @SuppressWarnings("deprecation") + @Test + public void testInnerLayerCopperIsOnTheBottom() { + assertOnlyOn(trace(PCBLayer._5), BOTTOM); + } + + @Test + public void testComponentsThroughTheBoardAreOnBothTraceLayers() { + assertOnBoth(pad(SolderPad.HoleType.PTH)); + assertOnBoth(new Via()); + assertOnBoth(new Eyelet()); + assertOnBoth(new Turret()); + } + + @Test + public void testNonPlatedPadIsOnItsSide() { + assertOnlyOn(pad(SolderPad.HoleType.NTPH), BOTTOM); + } + + @Test + public void testJumperIsOnTopTraces() { + assertOnlyOn(new Jumper(), TOP); + } + + @Test + public void testTraceBlockIsOnBothTraceLayers() { + CompositeComponent block = new CompositeComponent(); + block.getChildComponents().addAll(Arrays.asList(trace(PCBLayer._1), trace(PCBLayer._2))); + assertOnBoth(block); + } + + @Test + public void testOtherComponentsKeepTheirZOrderLayer() { + Resistor resistor = new Resistor(); + assertTrue(ComponentLayers.isOnLayer(resistor, IDIYComponent.COMPONENT)); + assertFalse(ComponentLayers.isOnLayer(resistor, TOP)); + assertTrue(ComponentLayers.isHidden(set(IDIYComponent.COMPONENT), resistor)); + assertTrue(ComponentLayers.isLocked(set(IDIYComponent.COMPONENT), resistor)); + } + + @Test + public void testComponentOnBothIsHiddenWithBothAndLockedWithEither() { + Via via = new Via(); + assertFalse(ComponentLayers.isHidden(set(BOTTOM), via)); + assertTrue(ComponentLayers.isHidden(set(BOTTOM, TOP), via)); + assertTrue(ComponentLayers.isLocked(set(BOTTOM), via)); + assertTrue(ComponentLayers.isLocked(set(TOP), via)); + assertFalse(ComponentLayers.isLocked(set(IDIYComponent.COMPONENT), via)); + } + + @Test + public void testJumperStaysUnlockedWithOnlyBottomTracesLocked() { + assertFalse(ComponentLayers.isLocked(set(BOTTOM), new Jumper())); + } + + private static void assertOnlyOn(IDIYComponent component, int layer) { + int other = layer == BOTTOM ? TOP : BOTTOM; + assertTrue(ComponentLayers.isOnLayer(component, layer)); + assertFalse(ComponentLayers.isOnLayer(component, other)); + assertFalse(ComponentLayers.isOnLayer(component, IDIYComponent.TRACE)); + assertTrue(ComponentLayers.isHidden(set(layer), component)); + assertFalse(ComponentLayers.isHidden(set(other), component)); + assertTrue(ComponentLayers.isLocked(set(layer), component)); + assertFalse(ComponentLayers.isLocked(set(other), component)); + } + + private static void assertOnBoth(IDIYComponent component) { + assertTrue(ComponentLayers.isOnLayer(component, BOTTOM)); + assertTrue(ComponentLayers.isOnLayer(component, TOP)); + } + + private static CopperTrace trace(PCBLayer layer) { + CopperTrace trace = new CopperTrace(); + trace.setLayer(layer); + return trace; + } + + private static SolderPad pad(SolderPad.HoleType holeType) { + SolderPad pad = new SolderPad(); + pad.setLayer(PCBLayer._1); + pad.setHoleType(holeType); + return pad; + } + + private static Set set(Integer... layers) { + return new HashSet(Arrays.asList(layers)); + } +} diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/layers/LayersMenuPlugin.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/layers/LayersMenuPlugin.java index 2719d82d2..2f16f37a0 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/layers/LayersMenuPlugin.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/layers/LayersMenuPlugin.java @@ -37,6 +37,7 @@ import org.diylc.common.IPlugIn; import org.diylc.common.IPlugInPort; import org.diylc.core.IDIYComponent; import org.diylc.core.IView; +import org.diylc.core.LayerIds; import org.diylc.swing.ISwingUI; public class LayersMenuPlugin implements IPlugIn { @@ -53,14 +54,14 @@ public class LayersMenuPlugin implements IPlugIn { visibleActionMap = new HashMap(); selectAllActionMap = new HashMap(); for (Layer layer : Layer.values()) { - final int zOrder = layer.getZOrder(); + final int layerId = layer.getLayerId(); AbstractAction lockAction = new AbstractAction("Lock") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { - LayersMenuPlugin.this.plugInPort.setLayerLocked(zOrder, (Boolean) getValue(Action.SELECTED_KEY)); + LayersMenuPlugin.this.plugInPort.setLayerLocked(layerId, (Boolean) getValue(Action.SELECTED_KEY)); // selectAllActionMap.get(zOrder).setEnabled(!(Boolean) getValue(Action.SELECTED_KEY)); } }; @@ -73,13 +74,15 @@ public class LayersMenuPlugin implements IPlugIn { @Override public void actionPerformed(ActionEvent e) { - LayersMenuPlugin.this.plugInPort.setLayerVisibility(zOrder, getValue(Action.SELECTED_KEY) == null ? true : (Boolean) getValue(Action.SELECTED_KEY)); + LayersMenuPlugin.this.plugInPort.setLayerVisibility(layerId, getValue(Action.SELECTED_KEY) == null ? true : (Boolean) getValue(Action.SELECTED_KEY)); // selectAllActionMap.get(zOrder).setEnabled(!(Boolean) getValue(Action.SELECTED_KEY)); } }; visibleAction.putValue(IView.CHECK_BOX_MENU_ITEM, true); - visibleAction.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_0 + zOrder, - Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); + // the shortcuts follow the menu order, Ctrl+1 for the first layer + visibleAction.putValue(AbstractAction.ACCELERATOR_KEY, + KeyStroke.getKeyStroke(KeyEvent.VK_1 + layer.ordinal(), + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); visibleActionMap.put(layer, visibleAction); AbstractAction selectAllAction = new AbstractAction("Select All") { @@ -88,10 +91,10 @@ public class LayersMenuPlugin implements IPlugIn { @Override public void actionPerformed(ActionEvent e) { - LayersMenuPlugin.this.plugInPort.selectAll(zOrder); + LayersMenuPlugin.this.plugInPort.selectAll(layerId); } }; - selectAllActionMap.put(zOrder, selectAllAction); + selectAllActionMap.put(layerId, selectAllAction); swingUI.injectSubmenu(layer.title, null, LOCK_LAYERS_TITLE); swingUI.injectMenuAction(visibleAction, layer.title); @@ -116,34 +119,38 @@ public class LayersMenuPlugin implements IPlugIn { if (eventType == EventType.LAYER_STATE_CHANGED) { Set lockedLayers = (Set) params[0]; for (Layer layer : Layer.values()) { - lockActionMap.get(layer).putValue(Action.SELECTED_KEY, lockedLayers.contains(layer.getZOrder())); + lockActionMap.get(layer).putValue(Action.SELECTED_KEY, lockedLayers.contains(layer.getLayerId())); } } else if (eventType == EventType.LAYER_VISIBILITY_CHANGED) { Set hiddenLayers = (Set) params[0]; for (Layer layer : Layer.values()) { - visibleActionMap.get(layer).putValue(Action.SELECTED_KEY, !hiddenLayers.contains(layer.getZOrder())); + visibleActionMap.get(layer).putValue(Action.SELECTED_KEY, !hiddenLayers.contains(layer.getLayerId())); } } } + // in menu order, which also gives the shortcuts static enum Layer { - CHASSIS("Chassis", IDIYComponent.CHASSIS), BOARD("Board", IDIYComponent.BOARD), TRACE("Trace", IDIYComponent.TRACE), COMPONENT( - "Component", IDIYComponent.COMPONENT), WIRING("Wiring", IDIYComponent.WIRING), TEXT("Text", IDIYComponent.TEXT); + CHASSIS("Chassis", IDIYComponent.CHASSIS), BOARD("Board", IDIYComponent.BOARD), + BOTTOM_TRACES("Bottom Traces", LayerIds.BOTTOM_TRACES), + TOP_TRACES("Top Traces", LayerIds.TOP_TRACES), + COMPONENT("Component", IDIYComponent.COMPONENT), WIRING("Wiring", IDIYComponent.WIRING), + TEXT("Text", IDIYComponent.TEXT); String title; - int zOrder; + int layerId; - private Layer(String title, int order) { + private Layer(String title, int layerId) { this.title = title; - zOrder = order; + this.layerId = layerId; } public String getTitle() { return title; } - public int getZOrder() { - return zOrder; + public int getLayerId() { + return layerId; } } } diff --git a/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt b/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt index d977878d8..da66ba834 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Deutsch.txt @@ -734,3 +734,5 @@ Decimal (4.7K, 0.22uF, 6.3V)|Dezimal (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) Fade Copper Under Board|Kupfer unter der Platine abblenden Board Side|Platinenseite +Bottom Traces|Leiterbahnen unten +Top Traces|Leiterbahnen oben diff --git a/diylc/diylc-swing/src/main/resources/lang/English.txt b/diylc/diylc-swing/src/main/resources/lang/English.txt index 4b28622a8..7022dee51 100644 --- a/diylc/diylc-swing/src/main/resources/lang/English.txt +++ b/diylc/diylc-swing/src/main/resources/lang/English.txt @@ -727,3 +727,5 @@ Decimal (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3) Fade Copper Under Board Board Side +Bottom Traces +Top Traces diff --git a/diylc/diylc-swing/src/main/resources/lang/Español.txt b/diylc/diylc-swing/src/main/resources/lang/Español.txt index 965d5f0c8..8e6187194 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Español.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Español.txt @@ -730,3 +730,5 @@ Decimal (4.7K, 0.22uF, 6.3V)|Decimal (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) Fade Copper Under Board|Atenuar cobre bajo la placa Board Side|Lado de la placa +Bottom Traces|Trazos inferiores +Top Traces|Trazos superiores diff --git a/diylc/diylc-swing/src/main/resources/lang/Français.txt b/diylc/diylc-swing/src/main/resources/lang/Français.txt index 760a357b0..71c2100d6 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Français.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Français.txt @@ -729,3 +729,5 @@ Decimal (4.7K, 0.22uF, 6.3V)|Décimale (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) Fade Copper Under Board|Estomper le cuivre sous la carte Board Side|Côté de la carte +Bottom Traces|Pistes du dessous +Top Traces|Pistes du dessus diff --git a/diylc/diylc-swing/src/main/resources/lang/Italiano.txt b/diylc/diylc-swing/src/main/resources/lang/Italiano.txt index 563549bc8..3c9d9873f 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Italiano.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Italiano.txt @@ -731,3 +731,5 @@ Decimal (4.7K, 0.22uF, 6.3V)|Decimale (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) Fade Copper Under Board|Attenua il rame sotto la scheda Board Side|Lato della scheda +Bottom Traces|Tracce inferiori +Top Traces|Tracce superiori diff --git a/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt b/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt index 63ab0e8f9..3b62d33e7 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Nederlands.txt @@ -735,3 +735,5 @@ Decimal (4.7K, 0.22uF, 6.3V)|Decimaal (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) Fade Copper Under Board|Koper onder de print vervagen Board Side|Printzijde +Bottom Traces|Sporen onderkant +Top Traces|Sporen bovenkant diff --git a/diylc/diylc-swing/src/main/resources/lang/Português.txt b/diylc/diylc-swing/src/main/resources/lang/Português.txt index b9a6b670f..998ad8d87 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Português.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Português.txt @@ -730,3 +730,5 @@ Decimal (4.7K, 0.22uF, 6.3V)|Decimal (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) Fade Copper Under Board|Esmaecer cobre sob a placa Board Side|Lado da placa +Bottom Traces|Traçados inferiores +Top Traces|Traçados superiores diff --git a/diylc/diylc-swing/src/main/resources/lang/Srpski.txt b/diylc/diylc-swing/src/main/resources/lang/Srpski.txt index cc9ab8e46..c6e2f2725 100644 --- a/diylc/diylc-swing/src/main/resources/lang/Srpski.txt +++ b/diylc/diylc-swing/src/main/resources/lang/Srpski.txt @@ -726,3 +726,5 @@ Decimal (4.7K, 0.22uF, 6.3V)|Decimalni (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) Fade Copper Under Board|Prigušen Bakar Ispod Ploče Board Side|Strana Ploče +Bottom Traces|Donji Vodovi +Top Traces|Gornji Vodovi diff --git a/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt b/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt index f0d502b53..d7e296d19 100644 --- a/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt +++ b/diylc/diylc-swing/src/main/resources/lang/हिंदी.txt @@ -728,3 +728,5 @@ Decimal (4.7K, 0.22uF, 6.3V)|दशमलव (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) Fade Copper Under Board|बोर्ड के नीचे तांबा धुंधला करें Board Side|बोर्ड का पक्ष +Bottom Traces|नीचे के ट्रेस +Top Traces|ऊपर के ट्रेस diff --git a/diylc/diylc-swing/src/main/resources/lang/简体中文.txt b/diylc/diylc-swing/src/main/resources/lang/简体中文.txt index 8c6dabd5e..2f7f31f58 100644 --- a/diylc/diylc-swing/src/main/resources/lang/简体中文.txt +++ b/diylc/diylc-swing/src/main/resources/lang/简体中文.txt @@ -728,3 +728,5 @@ Decimal (4.7K, 0.22uF, 6.3V)|小数 (4.7K, 0.22uF, 6.3V) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3) Fade Copper Under Board|淡化板下铜箔 Board Side|板面 +Bottom Traces|底层走线 +Top Traces|顶层走线 diff --git a/diylc/diylc-swing/src/test/java/org/diylc/TraceLayerTests.java b/diylc/diylc-swing/src/test/java/org/diylc/TraceLayerTests.java new file mode 100644 index 000000000..3ebd30ee9 --- /dev/null +++ b/diylc/diylc-swing/src/test/java/org/diylc/TraceLayerTests.java @@ -0,0 +1,116 @@ +/* + + DIY Layout Creator (DIYLC). + Copyright (c) 2009-2025 held jointly by the individual authors. + + This file is part of DIYLC. + + DIYLC is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DIYLC is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DIYLC. If not, see . + +*/ +package org.diylc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import org.diylc.common.PCBLayer; +import org.diylc.common.PropertyWrapper; +import org.diylc.components.connectivity.CopperTrace; +import org.diylc.components.connectivity.Jumper; +import org.diylc.components.connectivity.Via; +import org.diylc.core.LayerIds; +import org.diylc.core.Project; +import org.junit.Before; +import org.junit.Test; + +public class TraceLayerTests extends TestBase { + + private CopperTrace bottomTrace; + private CopperTrace topTrace; + private Via via; + private Jumper jumper; + + @Before + public void setUp() { + bottomTrace = trace(PCBLayer._1); + topTrace = trace(PCBLayer._2); + via = new Via(); + jumper = new Jumper(); + presenter.getCurrentProject().getComponents() + .addAll(Arrays.asList(bottomTrace, topTrace, via, jumper)); + } + + @Test + public void testSelectAllOfATraceLayer() { + presenter.selectAll(LayerIds.BOTTOM_TRACES); + assertEquals(new HashSet(Arrays.asList(bottomTrace, via)), + new HashSet(presenter.getSelectedComponents())); + + presenter.selectAll(LayerIds.TOP_TRACES); + assertEquals(new HashSet(Arrays.asList(topTrace, via, jumper)), + new HashSet(presenter.getSelectedComponents())); + } + + @Test + public void testLockingBottomTracesLocksTheViaButNotTheTopSide() { + presenter.setLayerLocked(LayerIds.BOTTOM_TRACES, true); + + presenter.selectAll(0); + assertEquals(new HashSet(Arrays.asList(topTrace, jumper)), + new HashSet(presenter.getSelectedComponents())); + assertEquals(new HashSet(Arrays.asList(LayerIds.BOTTOM_TRACES)), + presenter.getCurrentProject().getLockedLayers()); + } + + @Test + public void testBothSidesKeepTheLegacyLayerForOlderVersions() { + Project project = presenter.getCurrentProject(); + presenter.setLayerVisibility(LayerIds.BOTTOM_TRACES, false); + presenter.setLayerVisibility(LayerIds.TOP_TRACES, false); + assertTrue(project.getHiddenLayers().contains(LayerIds.LEGACY_TRACES)); + + presenter.setLayerVisibility(LayerIds.TOP_TRACES, true); + assertEquals(new HashSet(Arrays.asList(LayerIds.BOTTOM_TRACES)), + project.getHiddenLayers()); + } + + @Test + public void testTraceMovedToALockedSideIsLocked() { + presenter.setLayerLocked(LayerIds.BOTTOM_TRACES, true); + presenter.selectAll(0); + assertTrue(presenter.getSelectedComponents().contains(topTrace)); + + List properties = presenter.getProperties(topTrace); + for (PropertyWrapper property : properties) { + if (property.getName().equals("Layer")) { + property.setValue(PCBLayer._1); + } + } + presenter.applyProperties(topTrace, properties); + + presenter.selectAll(0); + assertFalse(presenter.getSelectedComponents().contains(topTrace)); + } + + private static CopperTrace trace(PCBLayer layer) { + CopperTrace trace = new CopperTrace(); + trace.setLayer(layer); + return trace; + } +} diff --git a/diylc/docs/plans/split-trace-layer.md b/diylc/docs/plans/split-trace-layer.md index e55c660e4..a4fa73ed2 100644 --- a/diylc/docs/plans/split-trace-layer.md +++ b/diylc/docs/plans/split-trace-layer.md @@ -5,7 +5,8 @@ that copper on one side of the board can be hidden, locked or selected without t Two-sided boards, the Underside Display editing and the Via made one trace layer too coarse: a user routing the bottom in the Underside Display wants to lock the top, and the other way round. -Status: planned, nothing built yet. +Status: done. Core, library and swing tests pass; the regression suite has not been run, and no +change is expected there (section 5). ## 1. Background @@ -37,6 +38,8 @@ Status: planned, nothing built yet. | The Visible shortcuts are renumbered to follow the menu: Ctrl+1 Chassis, Ctrl+2 Board, Ctrl+3 Bottom Traces, Ctrl+4 Top Traces, Ctrl+5 Component, Ctrl+6 Wiring, Ctrl+7 Text | Shortcuts that follow the menu order are easier to learn than a new one added out of order. Only the shortcuts move; the saved layer ids do not, so files are unaffected. | | New ids: Bottom Traces `31`, Top Traces `32` (`TRACE * 10 + PCBLayer.getId()`) | They cannot collide with a z-order. Reusing `3` for one side would make an older DIYLC treat a file with only one side hidden as all traces hidden. | | `3` stays as the legacy id meaning both trace layers, and the saved sets keep the invariant "`3` is present exactly when `31` and `32` both are" | A file saved with both sides hidden or locked still hides or locks all traces in an older DIYLC, and an older DIYLC that unhides or unlocks Trace is understood by the new one (section 4). | +| The ids and the set rules live in `org.diylc.core.LayerIds`; which layer a component is on lives in `org.diylc.presenter.ComponentLayers` | `Project` applies the rules on load and must not depend on the presenter package; layer membership needs the z-order from `ComponentProcessor`. | +| Two set rules, not one: `migrateLoaded` after a load, `syncLegacy` after a change | "Both sides set, no `3`" means opposite things in the two cases: written by an older version that cleared Trace, or just set by the user. | | Copper with a side is on the layer of that side; the deprecated inner layers count as Bottom | `IPCBLayerComponent.getBoardSide()` already maps them that way. | | A Trace-band component that reports layer id `0` is on both trace layers: a PTH `SolderPad`, and `Via`, `Eyelet` and `Turret`, which implement `ILayeredComponent` to say so. A composite whose z-order falls in the Trace band is on both as well | Layer id `0` already means "every layer" to continuity, and the three go through the board. A composite may hold copper of both sides. Core names no concrete component. | | Every other Trace-band component is on the layer of `IBoardSideComponent.sideOf`, so a component without a side, such as `Jumper`, is on Top Traces | `sideOf` already treats a component without a side as top, the same rule the rendering uses. A jumper sits on top of the board: locking Bottom Traces to work on the top must leave it movable. | @@ -48,22 +51,23 @@ Status: planned, nothing built yet. ### diylc-core -- `org.diylc.presenter.ComponentLayers` (new, not in `Presenter`, which stays lean): - - constants `BOTTOM_TRACES = 31`, `TOP_TRACES = 32`, and `LEGACY_TRACES = IDIYComponent.TRACE`; - - `Set getLayers(IDIYComponent)`: the rounded z-order for everything outside the - Trace band; inside it, both trace ids for layer id `0` and for a composite, otherwise the id - of `IBoardSideComponent.sideOf`; - - `isHidden(Project, component)` (all its layers hidden), `isLocked(Project, component)` (any of - its layers locked) and `isOnLayer(component, layerId)` for Select All; - - `normalize(Set)`, which applies the invariant of section 4 and is used both after - loading and after every change. +- `org.diylc.core.LayerIds` (new): `LEGACY_TRACES = IDIYComponent.TRACE`, `BOTTOM_TRACES = 31`, + `TOP_TRACES = 32`, `migrateLoaded(Set)` for a set read from a file and + `syncLegacy(Set)` for a set just changed (section 4). +- `org.diylc.presenter.ComponentLayers` (new, not in `Presenter`, which stays lean): the layer of a + component is the rounded z-order outside the Trace band; inside it, both trace layers for layer + id `0` and for a composite, otherwise the layer of `IBoardSideComponent.sideOf`. It answers + `isHidden(hiddenLayers, component)` (all its layers hidden), `isLocked(lockedLayers, component)` + (any of its layers locked) and `isOnLayer(component, layerId)` for Select All, without + allocating, since drawing asks for every component on every repaint. - `Presenter.isComponentVisible`, `isComponentLayerLocked`, `selectAll(int)` and `GroupAndLockIndex.isLocked` call the helper. Internal types without a descriptor stay visible and unlocked, as today. -- `Presenter.setLayerLocked` and `setLayerVisibility` call `normalize` on the changed set. -- `Project.readResolve()` (new) normalizes `lockedLayers` and `hiddenLayers` whenever XStream - loads a project, so the migration does not depend on which code path opened the file. XStream - calls `readResolve` on any class that declares it. +- `Presenter.setLayerLocked` and `setLayerVisibility` call `syncLegacy` on the changed set; given + the legacy id `3`, they change both trace layers. +- `Project.readResolve()` (new) runs `migrateLoaded` on `lockedLayers` and `hiddenLayers` + whenever XStream loads a project, so the migration does not depend on which code path opened + the file. XStream calls `readResolve` on any class that declares it. - `Via`, `Eyelet` and `Turret` (diylc-library) implement `ILayeredComponent` with `getLayerId()` returning `0`. Continuity does not change: a component that is not layered already counts as layer `0`. @@ -87,8 +91,8 @@ Status: planned, nothing built yet. Nothing about components changes, and no field is added, renamed or retyped. The only saved data involved are the two `Set` fields of `Project`, whose type stays the same. -**Invariant.** In a saved set, `3` is present exactly when `31` and `32` both are. `normalize` -enforces it: +**Invariant.** In a saved set, `3` is present exactly when `31` and `32` both are. `migrateLoaded` +applies the first three rows after a load, `syncLegacy` the last one after a change: | Set as loaded or changed | Normalized | Why | | --- | --- | --- | @@ -117,22 +121,21 @@ clipboard carries components, not layer sets. ## 5. Verification -- `ComponentLayersTest`: a top and a bottom trace, an inner-layer trace (Bottom), PTH and NPTH - pads, a `Via`, an `Eyelet`, a `Jumper` (Top Traces) and a Trace-band composite each land on the - expected layers; a - Component-band part keeps its z-order layer. +- `ComponentLayersTest` (library): a top and a bottom trace, an inner-layer trace (Bottom), PTH and + NPTH pads, a `Via`, an `Eyelet`, a `Jumper` (Top Traces) and a Trace-band composite each land on + the expected layers; a Component-band part keeps its z-order layer. - Hidden and locked rules: a via is hidden only with both sides hidden and locked with either side locked; a jumper stays unlocked when only Bottom Traces is locked. - Continuity: the netlist of a project with vias, eyelets and turrets is unchanged. -- `normalize`: each row of the table in section 4, and that a loaded project goes through it - (`readResolve`), using an XML project with `3` in `hiddenLayers` and `lockedLayers`. -- Presenter: Select All per trace layer; locking Bottom Traces and then moving a selected top trace - to the bottom leaves it locked. +- `LayerIdsTest` (core): each row of the table in section 4, and a project saved and loaded through + XStream, so that `readResolve` runs. +- `TraceLayerTests` (swing): Select All per trace layer; locking Bottom Traces locks the via but + not the top trace or the jumper; both sides hidden keeps `3`; a top trace moved to the locked + bottom through its Layer property is locked. - Regression suite: no image or netlist change expected, since no regression project hides or locks the Trace layer. ## 6. Next steps -1. Build the core helper with its tests, the `ILayeredComponent` declarations on `Via`, `Eyelet` - and `Turret`, then the menu and the language keys. -2. Changelog entry in the unreleased version, including the renumbered shortcuts. +1. Check the menu, the shortcuts and the locking in the running app, and open a project saved with + the old Trace layer hidden or locked.