Split trace layer

This commit is contained in:
Bane Stojkovic
2026-11-12 13:42:04 +01:00
parent 4aab04f751
commit 6e3f72dfb6
26 changed files with 684 additions and 83 deletions
@@ -364,22 +364,24 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
void setNewComponentTypeSlot(ComponentType componentType, Template template, String[] model, boolean forceInstatiate); 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 * Locks or unlocks the specified layer: the components whose z-order rounds to it, or the
* the change as well. * 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 * @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 * Shows or hides the specified layer: the components whose z-order rounds to it, or the
* change as well. * 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 * @param visible
*/ */
void setLayerVisibility(int layerZOrder, boolean visible); void setLayerVisibility(int layerId, boolean visible);
/** /**
* @return selection size expressed in both inches or centimeters, respectively * @return selection size expressed in both inches or centimeters, respectively
@@ -77,7 +77,8 @@ public interface ISelectionProcessor extends IGroupProcessor {
/** /**
* Selects all components in the project. * 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 * regardless of layer
*/ */
void selectAll(int layer); void selectAll(int layer);
@@ -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 <http://www.gnu.org/licenses/>.
*/
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.
* <p>
* 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<Integer> 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<Integer> layers) {
if (layers.contains(BOTTOM_TRACES) && layers.contains(TOP_TRACES)) {
layers.add(LEGACY_TRACES);
} else {
layers.remove(LEGACY_TRACES);
}
}
}
@@ -196,6 +196,17 @@ public class Project implements Serializable, Cloneable {
return hiddenLayers; 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<Integer>();
}
LayerIds.migrateLoaded(lockedLayers);
LayerIds.migrateLoaded(getHiddenLayers());
return this;
}
public VersionNumber getFileVersion() { public VersionNumber getFileVersion() {
return fileVersion; return fileVersion;
} }
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Integer> 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<Integer> 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<? extends IDIYComponent<?>>) 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;
}
}
@@ -94,8 +94,7 @@ public class GroupAndLockIndex {
if (componentType == null) { if (componentType == null) {
return false; return false;
} }
return project.getLockedLayers() return ComponentLayers.isLocked(project.getLockedLayers(), component)
.contains((int) Math.round(ComponentProcessor.getInstance().getZOrder(component)))
|| project.getLockedComponents().contains(component); || project.getLockedComponents().contains(component);
} }
@@ -488,16 +488,8 @@ public class Presenter implements IPlugInPort {
configManager.writeValue(IPlugInPort.DISABLED_CATEGORIES_KEY, new ArrayList<String>()); configManager.writeValue(IPlugInPort.DISABLED_CATEGORIES_KEY, new ArrayList<String>());
} }
@SuppressWarnings({"unchecked"})
private boolean isComponentVisible(IDIYComponent<?> component) { private boolean isComponentVisible(IDIYComponent<?> component) {
ComponentType componentType = return !ComponentLayers.isHidden(currentProject.getHiddenLayers(), component);
ComponentProcessor.getInstance().extractComponentTypeFrom(
(Class<? extends IDIYComponent<?>>) component.getClass());
// for internal types
if (componentType == null)
return true;
return !currentProject.getHiddenLayers()
.contains((int) Math.round(ComponentProcessor.getInstance().getZOrder(component)));
} }
@Override @Override
@@ -1162,7 +1154,7 @@ public class Presenter implements IPlugInPort {
Iterator<IDIYComponent<?>> i = newSelection.iterator(); Iterator<IDIYComponent<?>> i = newSelection.iterator();
while (i.hasNext()) { while (i.hasNext()) {
IDIYComponent<?> c = i.next(); IDIYComponent<?> c = i.next();
if ((int) ComponentProcessor.getInstance().getZOrder(c) != layer) { if (!ComponentLayers.isOnLayer(c, layer)) {
i.remove(); i.remove();
} }
} }
@@ -2285,14 +2277,10 @@ public class Presenter implements IPlugInPort {
} }
@Override @Override
public void setLayerLocked(int layerZOrder, boolean locked) { public void setLayerLocked(int layerId, boolean locked) {
LOG.info(String.format("setLayerLocked(%s, %s)", layerZOrder, locked)); LOG.info(String.format("setLayerLocked(%s, %s)", layerId, locked));
Project oldProject = currentProject.clone(); Project oldProject = currentProject.clone();
if (locked) { setLayerInSet(currentProject.getLockedLayers(), layerId, locked);
currentProject.getLockedLayers().add(layerZOrder);
} else {
currentProject.getLockedLayers().remove(layerZOrder);
}
groupAndLockIndex.invalidate(); groupAndLockIndex.invalidate();
updateSelection(EMPTY_SELECTION); updateSelection(EMPTY_SELECTION);
messageDispatcher.dispatchMessage(EventType.LAYER_STATE_CHANGED, currentProject.getLockedLayers()); messageDispatcher.dispatchMessage(EventType.LAYER_STATE_CHANGED, currentProject.getLockedLayers());
@@ -2301,14 +2289,10 @@ public class Presenter implements IPlugInPort {
} }
@Override @Override
public void setLayerVisibility(int layerZOrder, boolean visible) { public void setLayerVisibility(int layerId, boolean visible) {
LOG.info(String.format("setLayerVisibility(%s, %s)", layerZOrder, visible)); LOG.info(String.format("setLayerVisibility(%s, %s)", layerId, visible));
Project oldProject = currentProject.clone(); Project oldProject = currentProject.clone();
if (visible) { setLayerInSet(currentProject.getHiddenLayers(), layerId, !visible);
currentProject.getHiddenLayers().remove(layerZOrder);
} else {
currentProject.getHiddenLayers().add(layerZOrder);
}
updateSelection(EMPTY_SELECTION); updateSelection(EMPTY_SELECTION);
messageDispatcher.dispatchMessage(EventType.LAYER_VISIBILITY_CHANGED, currentProject.getHiddenLayers()); messageDispatcher.dispatchMessage(EventType.LAYER_VISIBILITY_CHANGED, currentProject.getHiddenLayers());
@@ -3225,8 +3209,22 @@ public class Presenter implements IPlugInPort {
} }
private boolean isComponentLayerLocked(IDIYComponent<?> component) { private boolean isComponentLayerLocked(IDIYComponent<?> component) {
return currentProject.getLockedLayers() return ComponentLayers.isLocked(currentProject.getLockedLayers(), component);
.contains((int) Math.round(ComponentProcessor.getInstance().getZOrder(component))); }
// the legacy trace layer stands for both trace layers
private static void setLayerInSet(Set<Integer> 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);
} }
/** /**
@@ -665,6 +665,10 @@
<changeType>IMPROVEMENT</changeType> <changeType>IMPROVEMENT</changeType>
<description>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</description> <description>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</description>
</org.diylc.appframework.update.Change> </org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>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</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change> <org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType> <changeType>BUG_FIX</changeType>
<description>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</description> <description>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</description>
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Integer> migrate(Integer... layers) {
Set<Integer> set = set(layers);
LayerIds.migrateLoaded(set);
return set;
}
private static Set<Integer> sync(Integer... layers) {
Set<Integer> set = set(layers);
LayerIds.syncLegacy(set);
return set;
}
private static Set<Integer> set(Integer... layers) {
return new HashSet<Integer>(Arrays.asList(layers));
}
}
@@ -33,6 +33,7 @@ import org.diylc.core.ComponentState;
import org.diylc.core.IConnective; import org.diylc.core.IConnective;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver; import org.diylc.core.IDrawingObserver;
import org.diylc.core.ILayeredComponent;
import org.diylc.core.Project; import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy; import org.diylc.core.VisibilityPolicy;
import org.diylc.core.annotations.AiNote; 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, zOrder = IDIYComponent.TRACE + 0.1, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false,
keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, transformer = SimpleComponentTransformer.class, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, transformer = SimpleComponentTransformer.class,
enableCache = true) enableCache = true)
public class Eyelet extends AbstractTransparentComponent<String> implements IConnective { public class Eyelet extends AbstractTransparentComponent<String>
implements IConnective, ILayeredComponent {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -132,6 +134,12 @@ public class Eyelet extends AbstractTransparentComponent<String> implements ICon
return super.getName(); return super.getName();
} }
// goes through the board, so it is on every copper layer
@Override
public int getLayerId() {
return 0;
}
@Override @Override
public int getControlPointCount() { public int getControlPointCount() {
return 1; return 1;
@@ -33,6 +33,7 @@ import org.diylc.core.ComponentState;
import org.diylc.core.IConnective; import org.diylc.core.IConnective;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver; import org.diylc.core.IDrawingObserver;
import org.diylc.core.ILayeredComponent;
import org.diylc.core.Project; import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy; import org.diylc.core.VisibilityPolicy;
import org.diylc.core.annotations.AiNote; 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, zOrder = IDIYComponent.TRACE + 0.1, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false,
keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, transformer = SimpleComponentTransformer.class, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, transformer = SimpleComponentTransformer.class,
enableCache = true) enableCache = true)
public class Turret extends AbstractTransparentComponent<String> implements IConnective { public class Turret extends AbstractTransparentComponent<String>
implements IConnective, ILayeredComponent {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -127,6 +129,12 @@ public class Turret extends AbstractTransparentComponent<String> implements ICon
return super.getName(); return super.getName();
} }
// goes through the board, so it is on every copper layer
@Override
public int getLayerId() {
return 0;
}
@Override @Override
public int getControlPointCount() { public int getControlPointCount() {
return 1; return 1;
@@ -36,6 +36,7 @@ import org.diylc.core.ComponentState;
import org.diylc.core.IConnective; import org.diylc.core.IConnective;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver; import org.diylc.core.IDrawingObserver;
import org.diylc.core.ILayeredComponent;
import org.diylc.core.Project; import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy; import org.diylc.core.VisibilityPolicy;
import org.diylc.core.annotations.AiNote; 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", bomPolicy = BomPolicy.NEVER_SHOW, keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "PCB",
transformer = SimpleComponentTransformer.class, enableCache = true) transformer = SimpleComponentTransformer.class, enableCache = true)
public class Via extends AbstractTransparentComponent<Void> public class Via extends AbstractTransparentComponent<Void>
implements IConnective, IGerberComponentCustom { implements IConnective, ILayeredComponent, IGerberComponentCustom {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -175,6 +176,12 @@ public class Via extends AbstractTransparentComponent<Void>
this.color = color; this.color = color;
} }
// goes through the board, so it is on every copper layer
@Override
public int getLayerId() {
return 0;
}
@Override @Override
public int getControlPointCount() { public int getControlPointCount() {
return 1; return 1;
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Integer> set(Integer... layers) {
return new HashSet<Integer>(Arrays.asList(layers));
}
}
@@ -37,6 +37,7 @@ import org.diylc.common.IPlugIn;
import org.diylc.common.IPlugInPort; import org.diylc.common.IPlugInPort;
import org.diylc.core.IDIYComponent; import org.diylc.core.IDIYComponent;
import org.diylc.core.IView; import org.diylc.core.IView;
import org.diylc.core.LayerIds;
import org.diylc.swing.ISwingUI; import org.diylc.swing.ISwingUI;
public class LayersMenuPlugin implements IPlugIn { public class LayersMenuPlugin implements IPlugIn {
@@ -53,14 +54,14 @@ public class LayersMenuPlugin implements IPlugIn {
visibleActionMap = new HashMap<Layer, Action>(); visibleActionMap = new HashMap<Layer, Action>();
selectAllActionMap = new HashMap<Integer, Action>(); selectAllActionMap = new HashMap<Integer, Action>();
for (Layer layer : Layer.values()) { for (Layer layer : Layer.values()) {
final int zOrder = layer.getZOrder(); final int layerId = layer.getLayerId();
AbstractAction lockAction = new AbstractAction("Lock") { AbstractAction lockAction = new AbstractAction("Lock") {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Override @Override
public void actionPerformed(ActionEvent e) { 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)); // selectAllActionMap.get(zOrder).setEnabled(!(Boolean) getValue(Action.SELECTED_KEY));
} }
}; };
@@ -73,12 +74,14 @@ public class LayersMenuPlugin implements IPlugIn {
@Override @Override
public void actionPerformed(ActionEvent e) { 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)); // selectAllActionMap.get(zOrder).setEnabled(!(Boolean) getValue(Action.SELECTED_KEY));
} }
}; };
visibleAction.putValue(IView.CHECK_BOX_MENU_ITEM, true); visibleAction.putValue(IView.CHECK_BOX_MENU_ITEM, true);
visibleAction.putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_0 + zOrder, // 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())); Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
visibleActionMap.put(layer, visibleAction); visibleActionMap.put(layer, visibleAction);
@@ -88,10 +91,10 @@ public class LayersMenuPlugin implements IPlugIn {
@Override @Override
public void actionPerformed(ActionEvent e) { 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.injectSubmenu(layer.title, null, LOCK_LAYERS_TITLE);
swingUI.injectMenuAction(visibleAction, layer.title); swingUI.injectMenuAction(visibleAction, layer.title);
@@ -116,34 +119,38 @@ public class LayersMenuPlugin implements IPlugIn {
if (eventType == EventType.LAYER_STATE_CHANGED) { if (eventType == EventType.LAYER_STATE_CHANGED) {
Set<Integer> lockedLayers = (Set<Integer>) params[0]; Set<Integer> lockedLayers = (Set<Integer>) params[0];
for (Layer layer : Layer.values()) { 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) { } else if (eventType == EventType.LAYER_VISIBILITY_CHANGED) {
Set<Integer> hiddenLayers = (Set<Integer>) params[0]; Set<Integer> hiddenLayers = (Set<Integer>) params[0];
for (Layer layer : Layer.values()) { 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 { static enum Layer {
CHASSIS("Chassis", IDIYComponent.CHASSIS), BOARD("Board", IDIYComponent.BOARD), TRACE("Trace", IDIYComponent.TRACE), COMPONENT( CHASSIS("Chassis", IDIYComponent.CHASSIS), BOARD("Board", IDIYComponent.BOARD),
"Component", IDIYComponent.COMPONENT), WIRING("Wiring", IDIYComponent.WIRING), TEXT("Text", IDIYComponent.TEXT); 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; String title;
int zOrder; int layerId;
private Layer(String title, int order) { private Layer(String title, int layerId) {
this.title = title; this.title = title;
zOrder = order; this.layerId = layerId;
} }
public String getTitle() { public String getTitle() {
return title; return title;
} }
public int getZOrder() { public int getLayerId() {
return zOrder; return layerId;
} }
} }
} }
@@ -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) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3)
Fade Copper Under Board|Kupfer unter der Platine abblenden Fade Copper Under Board|Kupfer unter der Platine abblenden
Board Side|Platinenseite Board Side|Platinenseite
Bottom Traces|Leiterbahnen unten
Top Traces|Leiterbahnen oben
@@ -727,3 +727,5 @@ Decimal (4.7K, 0.22uF, 6.3V)
RKM (4K7, 0u22, 6V3) RKM (4K7, 0u22, 6V3)
Fade Copper Under Board Fade Copper Under Board
Board Side Board Side
Bottom Traces
Top Traces
@@ -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) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3)
Fade Copper Under Board|Atenuar cobre bajo la placa Fade Copper Under Board|Atenuar cobre bajo la placa
Board Side|Lado de la placa Board Side|Lado de la placa
Bottom Traces|Trazos inferiores
Top Traces|Trazos superiores
@@ -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) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3)
Fade Copper Under Board|Estomper le cuivre sous la carte Fade Copper Under Board|Estomper le cuivre sous la carte
Board Side|Côté de la carte Board Side|Côté de la carte
Bottom Traces|Pistes du dessous
Top Traces|Pistes du dessus
@@ -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) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3)
Fade Copper Under Board|Attenua il rame sotto la scheda Fade Copper Under Board|Attenua il rame sotto la scheda
Board Side|Lato della scheda Board Side|Lato della scheda
Bottom Traces|Tracce inferiori
Top Traces|Tracce superiori
@@ -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) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3)
Fade Copper Under Board|Koper onder de print vervagen Fade Copper Under Board|Koper onder de print vervagen
Board Side|Printzijde Board Side|Printzijde
Bottom Traces|Sporen onderkant
Top Traces|Sporen bovenkant
@@ -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) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3)
Fade Copper Under Board|Esmaecer cobre sob a placa Fade Copper Under Board|Esmaecer cobre sob a placa
Board Side|Lado da placa Board Side|Lado da placa
Bottom Traces|Traçados inferiores
Top Traces|Traçados superiores
@@ -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) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3)
Fade Copper Under Board|Prigušen Bakar Ispod Ploče Fade Copper Under Board|Prigušen Bakar Ispod Ploče
Board Side|Strana Ploče Board Side|Strana Ploče
Bottom Traces|Donji Vodovi
Top Traces|Gornji Vodovi
@@ -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) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3)
Fade Copper Under Board|बोर्ड के नीचे तांबा धुंधला करें Fade Copper Under Board|बोर्ड के नीचे तांबा धुंधला करें
Board Side|बोर्ड का पक्ष Board Side|बोर्ड का पक्ष
Bottom Traces|नीचे के ट्रेस
Top Traces|ऊपर के ट्रेस
@@ -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) RKM (4K7, 0u22, 6V3)|RKM (4K7, 0u22, 6V3)
Fade Copper Under Board|淡化板下铜箔 Fade Copper Under Board|淡化板下铜箔
Board Side|板面 Board Side|板面
Bottom Traces|底层走线
Top Traces|顶层走线
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Object>(Arrays.asList(bottomTrace, via)),
new HashSet<Object>(presenter.getSelectedComponents()));
presenter.selectAll(LayerIds.TOP_TRACES);
assertEquals(new HashSet<Object>(Arrays.asList(topTrace, via, jumper)),
new HashSet<Object>(presenter.getSelectedComponents()));
}
@Test
public void testLockingBottomTracesLocksTheViaButNotTheTopSide() {
presenter.setLayerLocked(LayerIds.BOTTOM_TRACES, true);
presenter.selectAll(0);
assertEquals(new HashSet<Object>(Arrays.asList(topTrace, jumper)),
new HashSet<Object>(presenter.getSelectedComponents()));
assertEquals(new HashSet<Integer>(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<Integer>(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<PropertyWrapper> 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;
}
}
+30 -27
View File
@@ -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 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. 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 ## 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. | | 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. | | 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). | | `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. | | 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. | | 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. | | 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 ### diylc-core
- `org.diylc.presenter.ComponentLayers` (new, not in `Presenter`, which stays lean): - `org.diylc.core.LayerIds` (new): `LEGACY_TRACES = IDIYComponent.TRACE`, `BOTTOM_TRACES = 31`,
- constants `BOTTOM_TRACES = 31`, `TOP_TRACES = 32`, and `LEGACY_TRACES = IDIYComponent.TRACE`; `TOP_TRACES = 32`, `migrateLoaded(Set<Integer>)` for a set read from a file and
- `Set<Integer> getLayers(IDIYComponent<?>)`: the rounded z-order for everything outside the `syncLegacy(Set<Integer>)` for a set just changed (section 4).
Trace band; inside it, both trace ids for layer id `0` and for a composite, otherwise the id - `org.diylc.presenter.ComponentLayers` (new, not in `Presenter`, which stays lean): the layer of a
of `IBoardSideComponent.sideOf`; component is the rounded z-order outside the Trace band; inside it, both trace layers for layer
- `isHidden(Project, component)` (all its layers hidden), `isLocked(Project, component)` (any of id `0` and for a composite, otherwise the layer of `IBoardSideComponent.sideOf`. It answers
its layers locked) and `isOnLayer(component, layerId)` for Select All; `isHidden(hiddenLayers, component)` (all its layers hidden), `isLocked(lockedLayers, component)`
- `normalize(Set<Integer>)`, which applies the invariant of section 4 and is used both after (any of its layers locked) and `isOnLayer(component, layerId)` for Select All, without
loading and after every change. allocating, since drawing asks for every component on every repaint.
- `Presenter.isComponentVisible`, `isComponentLayerLocked`, `selectAll(int)` and - `Presenter.isComponentVisible`, `isComponentLayerLocked`, `selectAll(int)` and
`GroupAndLockIndex.isLocked` call the helper. Internal types without a descriptor stay visible `GroupAndLockIndex.isLocked` call the helper. Internal types without a descriptor stay visible
and unlocked, as today. and unlocked, as today.
- `Presenter.setLayerLocked` and `setLayerVisibility` call `normalize` on the changed set. - `Presenter.setLayerLocked` and `setLayerVisibility` call `syncLegacy` on the changed set; given
- `Project.readResolve()` (new) normalizes `lockedLayers` and `hiddenLayers` whenever XStream the legacy id `3`, they change both trace layers.
loads a project, so the migration does not depend on which code path opened the file. XStream - `Project.readResolve()` (new) runs `migrateLoaded` on `lockedLayers` and `hiddenLayers`
calls `readResolve` on any class that declares it. 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()` - `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 returning `0`. Continuity does not change: a component that is not layered already counts as
layer `0`. 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 Nothing about components changes, and no field is added, renamed or retyped. The only saved data
involved are the two `Set<Integer>` fields of `Project`, whose type stays the same. involved are the two `Set<Integer>` fields of `Project`, whose type stays the same.
**Invariant.** In a saved set, `3` is present exactly when `31` and `32` both are. `normalize` **Invariant.** In a saved set, `3` is present exactly when `31` and `32` both are. `migrateLoaded`
enforces it: applies the first three rows after a load, `syncLegacy` the last one after a change:
| Set as loaded or changed | Normalized | Why | | Set as loaded or changed | Normalized | Why |
| --- | --- | --- | | --- | --- | --- |
@@ -117,22 +121,21 @@ clipboard carries components, not layer sets.
## 5. Verification ## 5. Verification
- `ComponentLayersTest`: a top and a bottom trace, an inner-layer trace (Bottom), PTH and NPTH - `ComponentLayersTest` (library): a top and a bottom trace, an inner-layer trace (Bottom), PTH and
pads, a `Via`, an `Eyelet`, a `Jumper` (Top Traces) and a Trace-band composite each land on the NPTH pads, a `Via`, an `Eyelet`, a `Jumper` (Top Traces) and a Trace-band composite each land on
expected layers; a the expected layers; a Component-band part keeps its z-order layer.
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 - 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. locked; a jumper stays unlocked when only Bottom Traces is locked.
- Continuity: the netlist of a project with vias, eyelets and turrets is unchanged. - 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 - `LayerIdsTest` (core): each row of the table in section 4, and a project saved and loaded through
(`readResolve`), using an XML project with `<int>3</int>` in `hiddenLayers` and `lockedLayers`. XStream, so that `readResolve` runs.
- Presenter: Select All per trace layer; locking Bottom Traces and then moving a selected top trace - `TraceLayerTests` (swing): Select All per trace layer; locking Bottom Traces locks the via but
to the bottom leaves it locked. 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 - Regression suite: no image or netlist change expected, since no regression project hides or
locks the Trace layer. locks the Trace layer.
## 6. Next steps ## 6. Next steps
1. Build the core helper with its tests, the `ILayeredComponent` declarations on `Via`, `Eyelet` 1. Check the menu, the shortcuts and the locking in the running app, and open a project saved with
and `Turret`, then the menu and the language keys. the old Trace layer hidden or locked.
2. Changelog entry in the unreleased version, including the renumbered shortcuts.