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);
/**
* 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
@@ -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);
@@ -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;
}
// 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() {
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) {
return false;
}
return project.getLockedLayers()
.contains((int) Math.round(ComponentProcessor.getInstance().getZOrder(component)))
return ComponentLayers.isLocked(project.getLockedLayers(), component)
|| project.getLockedComponents().contains(component);
}
@@ -488,16 +488,8 @@ public class Presenter implements IPlugInPort {
configManager.writeValue(IPlugInPort.DISABLED_CATEGORIES_KEY, new ArrayList<String>());
}
@SuppressWarnings({"unchecked"})
private boolean isComponentVisible(IDIYComponent<?> component) {
ComponentType componentType =
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)));
return !ComponentLayers.isHidden(currentProject.getHiddenLayers(), component);
}
@Override
@@ -1162,7 +1154,7 @@ public class Presenter implements IPlugInPort {
Iterator<IDIYComponent<?>> 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<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>
<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>
<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>
<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>
@@ -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));
}
}