diff --git a/.github/workflows/sign-windows-launcher.yml b/.github/workflows/sign-windows-launcher.yml
new file mode 100644
index 00000000..7a1d309e
--- /dev/null
+++ b/.github/workflows/sign-windows-launcher.yml
@@ -0,0 +1,86 @@
+name: Sign Windows Launcher
+
+# Signs the committed launch4j launcher that the installer embeds. Run it only after launch4j
+# regenerates the launcher, then commit the signed diylc-x64.exe from the artifact over
+# diylc/diylc-swing/deploy/win/diylc-x64.exe so Sign Windows Artifacts packs it.
+
+on:
+ workflow_dispatch:
+ inputs:
+ signing-policy-slug:
+ description: 'SignPath signing policy'
+ type: choice
+ default: test-signing
+ options:
+ - test-signing
+ - release-signing
+
+permissions:
+ # write is needed only to delete the unsigned launcher once SignPath has fetched it
+ actions: write
+ contents: read
+
+env:
+ SIGNPATH_ORGANIZATION_ID: 95470a00-59e6-430c-9c81-7393ad16d4e8
+ SIGNPATH_PROJECT_SLUG: diy-layout-creator
+
+jobs:
+ launcher:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v7
+
+ # re-signing would stack a second signature on top of the committed one
+ - name: Check the committed launcher is unsigned
+ run: |
+ if python3 diylc/diylc-swing/deploy/win/signpath/is-signed.py \
+ diylc/diylc-swing/deploy/win/diylc-x64.exe; then
+ echo "::error::deploy/win/diylc-x64.exe is already signed. Commit an unsigned launcher from launch4j first."
+ exit 1
+ fi
+
+ - name: Upload unsigned launcher
+ id: upload-unsigned-launcher
+ uses: actions/upload-artifact@v7
+ with:
+ name: unsigned-launcher
+ path: diylc/diylc-swing/deploy/win/diylc-x64.exe
+ if-no-files-found: error
+ retention-days: 1
+
+ - name: Sign launcher
+ uses: signpath/github-action-submit-signing-request@v3
+ with:
+ api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
+ organization-id: ${{ env.SIGNPATH_ORGANIZATION_ID }}
+ project-slug: ${{ env.SIGNPATH_PROJECT_SLUG }}
+ signing-policy-slug: ${{ inputs.signing-policy-slug }}
+ artifact-configuration-slug: windows-launcher
+ github-artifact-id: ${{ steps.upload-unsigned-launcher.outputs.artifact-id }}
+ wait-for-completion: true
+ output-artifact-directory: signed
+
+ - name: Delete unsigned launcher artifact
+ if: always() && steps.upload-unsigned-launcher.outputs.artifact-id != ''
+ env:
+ GH_TOKEN: ${{ github.token }}
+ ARTIFACT_ID: ${{ steps.upload-unsigned-launcher.outputs.artifact-id }}
+ run: gh api -X DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}"
+
+ - name: Verify signed launcher
+ run: |
+ python3 diylc/diylc-swing/deploy/win/signpath/is-signed.py signed/diylc-x64.exe
+ sudo apt-get update
+ sudo apt-get install -y osslsigncode
+ # chain validation fails against the self-signed test certificate by design; what
+ # matters is that a signature is present and names the expected subject
+ osslsigncode verify -in signed/diylc-x64.exe || true
+
+ - name: Upload signed launcher
+ uses: actions/upload-artifact@v7
+ with:
+ name: diylc-windows-launcher-signed
+ path: signed/diylc-x64.exe
+ if-no-files-found: error
diff --git a/.github/workflows/sign-windows.yml b/.github/workflows/sign-windows.yml
new file mode 100644
index 00000000..5d7bc51f
--- /dev/null
+++ b/.github/workflows/sign-windows.yml
@@ -0,0 +1,133 @@
+name: Sign Windows Artifacts
+
+# Builds and signs the Inno Setup installer; the zip package is not built or signed here. The
+# launcher the installer embeds is signed separately and committed by the Sign Windows Launcher
+# workflow, because Inno Setup cannot sign its payload after the fact and the launcher changes
+# only when launch4j regenerates it.
+
+on:
+ workflow_dispatch:
+ inputs:
+ signing-policy-slug:
+ description: 'SignPath signing policy'
+ type: choice
+ default: test-signing
+ options:
+ - test-signing
+ - release-signing
+
+permissions:
+ # write is needed only to delete the unsigned installer once SignPath has fetched it
+ actions: write
+ contents: read
+
+env:
+ SIGNPATH_ORGANIZATION_ID: 95470a00-59e6-430c-9c81-7393ad16d4e8
+ SIGNPATH_PROJECT_SLUG: diy-layout-creator
+
+jobs:
+ windows:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v7
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v6
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ cache: maven
+
+ # Nothing downstream fails when the committed launcher is unsigned: the installed
+ # application would just silently ship an unsigned executable.
+ - name: Check the committed launcher carries a signature
+ env:
+ POLICY: ${{ inputs.signing-policy-slug }}
+ run: |
+ if python3 diylc/diylc-swing/deploy/win/signpath/is-signed.py \
+ diylc/diylc-swing/deploy/win/diylc-x64.exe; then
+ exit 0
+ fi
+ if [ "$POLICY" = "release-signing" ]; then
+ echo "::error::deploy/win/diylc-x64.exe is unsigned. Run the Sign Windows Launcher workflow with the release policy, commit the signed launcher, then re-run this workflow."
+ exit 1
+ fi
+ echo "::warning::deploy/win/diylc-x64.exe is unsigned; the installed application will ship an unsigned launcher."
+
+ - name: Resolve project version
+ working-directory: diylc
+ run: |
+ echo "DIYLC_VERSION=$(mvn -q -N help:evaluate -Dexpression=project.version -DforceStdout)" >> "$GITHUB_ENV"
+
+ # amake/innosetup runs Inno Setup under wine as its own user (uid 1000), not as the runner
+ # (uid 1001), so it cannot write its output into a target directory the runner owns. Docker
+ # Desktop hides this on a developer machine; on a Linux runner it surfaces as "Error 5:
+ # Access denied" copying Setup.e32.
+ - name: Build Windows installer
+ working-directory: diylc
+ run: |
+ mkdir -p diylc-swing/target
+ chmod a+rwx diylc-swing/target
+ mvn -B -DskipTests -P win64-installer package
+
+ # upload-artifact zips the staged installer and SignPath signs the exe inside that zip; the
+ # action then downloads and unpacks the signed zip into signed/. The artifact configuration
+ # matches a fixed path, so the versioned installer travels under a stable name and is
+ # renamed back afterwards.
+ - name: Stage installer for signing
+ run: |
+ mkdir -p staging
+ cp "diylc/diylc-swing/target/diylc-${DIYLC_VERSION}-win64.exe" staging/diylc-setup.exe
+
+ - name: Upload unsigned installer
+ id: upload-unsigned-installer
+ uses: actions/upload-artifact@v7
+ with:
+ name: unsigned-installer
+ path: staging/diylc-setup.exe
+ if-no-files-found: error
+ retention-days: 1
+
+ - name: Sign installer
+ uses: signpath/github-action-submit-signing-request@v3
+ with:
+ api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
+ organization-id: ${{ env.SIGNPATH_ORGANIZATION_ID }}
+ project-slug: ${{ env.SIGNPATH_PROJECT_SLUG }}
+ signing-policy-slug: ${{ inputs.signing-policy-slug }}
+ artifact-configuration-slug: windows-installer
+ github-artifact-id: ${{ steps.upload-unsigned-installer.outputs.artifact-id }}
+ wait-for-completion: true
+ output-artifact-directory: signed
+
+ # SignPath can only fetch the installer from a GitHub artifact, so the unsigned upload cannot
+ # be skipped; it is deleted as soon as signing is over, whether or not signing succeeded.
+ - name: Delete unsigned installer artifact
+ if: always() && steps.upload-unsigned-installer.outputs.artifact-id != ''
+ env:
+ GH_TOKEN: ${{ github.token }}
+ ARTIFACT_ID: ${{ steps.upload-unsigned-installer.outputs.artifact-id }}
+ run: gh api -X DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}"
+
+ - name: Verify signed installer
+ run: |
+ python3 diylc/diylc-swing/deploy/win/signpath/is-signed.py signed/diylc-setup.exe
+ sudo apt-get update
+ sudo apt-get install -y osslsigncode
+ # chain validation fails against the self-signed test certificate by design; what
+ # matters is that a signature is present and names the expected subject
+ osslsigncode verify -in signed/diylc-setup.exe || true
+
+ - name: Restore signed installer under its release name
+ run: |
+ mkdir -p release
+ cp signed/diylc-setup.exe "release/diylc-${DIYLC_VERSION}-win64.exe"
+
+ - name: Upload signed installer
+ uses: actions/upload-artifact@v7
+ with:
+ name: diylc-windows-installer-signed
+ path: release/diylc-${{ env.DIYLC_VERSION }}-win64.exe
+ if-no-files-found: error
diff --git a/diylc/.gitignore b/diylc/.gitignore
index 60898bc3..256c2939 100644
--- a/diylc/.gitignore
+++ b/diylc/.gitignore
@@ -35,6 +35,7 @@ Thumbs.db
# Package Files #
*.jar
+!lib/*.jar
*.war
*.ear
*.zip
diff --git a/diylc/diylc-core/pom.xml b/diylc/diylc-core/pom.xml
index 442b5f99..c785d7f4 100644
--- a/diylc/diylc-core/pom.xml
+++ b/diylc/diylc-core/pom.xml
@@ -7,7 +7,7 @@
org.diylc
diylc-parent
- 6.8.0
+ 6.9.0
diylc-core
@@ -97,10 +97,10 @@
org.diylc.local
- gerber-writer-api-1.0.0
+ gerber-writer-api-1.1.0
1.0
system
- ${project.basedir}/../lib/gerber-writer-api-1.0.0.jar
+ ${project.basedir}/../lib/gerber-writer-api-1.1.0.jar
org.diylc.local
diff --git a/diylc/diylc-core/src/main/java/org/diylc/netlist/SpiceSumarizer.java b/diylc/diylc-core/src/main/java/org/diylc/netlist/SpiceSumarizer.java
index 42e5598b..ed0a67fd 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/netlist/SpiceSumarizer.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/netlist/SpiceSumarizer.java
@@ -2,16 +2,26 @@ package org.diylc.netlist;
import org.diylc.common.ComponentType;
import org.diylc.core.IDIYComponent;
+import org.diylc.core.measures.AbstractMeasure;
import org.diylc.presenter.ComponentProcessor;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.Format;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.stream.Collectors;
import static org.diylc.netlist.AbstractNetlistAnalyzer.find;
public class SpiceSumarizer {
+ // measures format with the user's locale for display, but SPICE only accepts '.' as the decimal
+ // separator
+ private static final Format VALUE_FORMAT =
+ new DecimalFormat("0.####", DecimalFormatSymbols.getInstance(Locale.ROOT));
+
public static String summarize(Netlist netlist, boolean useHTML) throws TreeException {
// grab all components that are in the netlist
@@ -119,14 +129,23 @@ public class SpiceSumarizer {
sb.append(typeName);
}
} else {
- if (c.getValue().toString().contains(" ")) {
- sb.append("\"").append(c.getValue()).append("\"");
+ String value = formatValue(c.getValue());
+ if (value.contains(" ")) {
+ sb.append("\"").append(value).append("\"");
} else {
- sb.append(c.getValue());
+ sb.append(value);
}
}
}
+ private static String formatValue(Object value) {
+ if (value instanceof AbstractMeasure) {
+ AbstractMeasure> measure = (AbstractMeasure>) value;
+ return VALUE_FORMAT.format(measure.getValue()) + measure.getUnit();
+ }
+ return value.toString();
+ }
+
private static String formatSpiceNode(int i) {
return String.format("N%03d" , i);
}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/serialization/ISerializationAliasProvider.java b/diylc/diylc-core/src/main/java/org/diylc/serialization/ISerializationAliasProvider.java
new file mode 100644
index 00000000..c522e509
--- /dev/null
+++ b/diylc/diylc-core/src/main/java/org/diylc/serialization/ISerializationAliasProvider.java
@@ -0,0 +1,44 @@
+/*
+
+ 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.serialization;
+
+import com.thoughtworks.xstream.XStream;
+
+import org.diylc.core.annotations.Indexed;
+
+/**
+ * Registers XStream aliases for classes and fields that were renamed after they shipped, so that
+ * the old names keep resolving. Implementations live next to the classes they alias and are
+ * applied to every {@link XStream} instance prepared by {@link ProjectFileManager#configure(XStream)}.
+ *
+ * @author Branislav Stojkovic
+ */
+@Indexed
+public interface ISerializationAliasProvider {
+
+ /**
+ * Registers aliases on the provided {@link XStream} instance.
+ *
+ * @param xStream
+ */
+ void registerAliases(XStream xStream);
+}
diff --git a/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java b/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java
index ac107343..028cf348 100644
--- a/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java
+++ b/diylc/diylc-core/src/main/java/org/diylc/serialization/ProjectFileManager.java
@@ -133,6 +133,14 @@ public class ProjectFileManager {
xStream.addImmutableType(org.diylc.core.measures.Inductance.class, true);
xStream.addImmutableType(org.diylc.core.measures.Size.class, true);
xStream.addPermission(AnyTypePermission.ANY);
+ try {
+ for (ISerializationAliasProvider provider : ReflectionUtils
+ .getIndexedInstances(ISerializationAliasProvider.class)) {
+ provider.registerAliases(xStream);
+ }
+ } catch (ReflectiveOperationException e) {
+ LOG.error("Could not register serialization aliases", e);
+ }
}
public void startNewFile() {
diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml
index 410c65df..70c4e135 100644
--- a/diylc/diylc-core/src/main/resources/update.xml
+++ b/diylc/diylc-core/src/main/resources/update.xml
@@ -582,7 +582,7 @@
8
0
- 2026-10-09 00:00:00.000 CET
+ 2026-10-16 00:00:00.000 CET
@@ -593,6 +593,14 @@
NEW_FEATURE
Holding 'Shift' when starting to drag components or control points keeps the whole drag strictly horizontal or vertical, in the direction of the initial gesture. Holding 'Shift' while drawing a new wire or two-point component does the same
+
+ NEW_FEATURE
+ Added Rectangular shape to LED component in addition to Round; added ability to set LED lead spacing
+
+
+ IMPROVEMENT
+ Upgraded AI Models to the latest GPT-6 generation across all tiers
+
IMPROVEMENT
'S1 Switch' can now be rotated to match the orientation of the potentiometer it is mounted on, so the switching matrix stays correct relative to the pot lugs
@@ -648,5 +656,25 @@
https://github.com/bancika/diy-layout-creator/releases
+
+
+ 6
+ 9
+ 0
+
+ 2026-10-17 00:00:00.000 CET
+
+
+
+ BUG_FIX
+ Gerber export on computers set to a language that uses a decimal comma (e.g. German, French, Serbian) produced files in which drill holes, pads and traces had the wrong size - a 0.8mm hole came out as 0mm and traces and the board outline had no width, so the files could not be used for fabrication
+
+
+ BUG_FIX
+ Zooming out until the drawing is smaller than the window made the toolbar above the canvas grow taller and squeeze the canvas; the toolbar now keeps its height at every zoom level
+
+
+ https://github.com/bancika/diy-layout-creator/releases
+
diff --git a/diylc/diylc-library/pom.xml b/diylc/diylc-library/pom.xml
index 8c802441..8c1ff92a 100644
--- a/diylc/diylc-library/pom.xml
+++ b/diylc/diylc-library/pom.xml
@@ -7,7 +7,7 @@
org.diylc
diylc-parent
- 6.8.0
+ 6.9.0
diylc-library
@@ -73,10 +73,10 @@
org.diylc.local
- gerber-writer-api-1.0.0
+ gerber-writer-api-1.1.0
1.0
system
- ${project.basedir}/../lib/gerber-writer-api-1.0.0.jar
+ ${project.basedir}/../lib/gerber-writer-api-1.1.0.jar
org.diylc.local
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/AbstractLeadedComponent.java b/diylc/diylc-library/src/main/java/org/diylc/components/AbstractLeadedComponent.java
index 373769f2..5701ef43 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/AbstractLeadedComponent.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/AbstractLeadedComponent.java
@@ -79,7 +79,9 @@ public abstract class AbstractLeadedComponent extends AbstractLabeledComponen
protected Color leadColor = LEAD_COLOR;
protected Display display = Display.NAME;
private boolean flipStanding = false;
- private LabelOriantation labelOriantation = LabelOriantation.Directional;
+ @Deprecated
+ private LabelOrientation labelOriantation;
+ private LabelOrientation labelOrientation = LabelOrientation.Directional;
protected boolean moveLabel = false;
// parameters for adjusting the label control point
@@ -361,13 +363,13 @@ public abstract class AbstractLeadedComponent extends AbstractLabeledComponen
if (getMoveLabel()) {
g2d.setTransform(oldTransform);
g2d.translate(getPoints()[2].getX(), getPoints()[2].getY());
- if (getLabelOriantation() != LabelOriantation.Horizontal) {
+ if (getLabelOrientation() != LabelOrientation.Horizontal) {
g2d.rotate(theta);
}
StringUtils.drawCenteredText(g2d, label, offset, 0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
g2d.setTransform(oldTransform);
} else {
- if (isStanding() || getLabelOriantation() == LabelOriantation.Horizontal) {
+ if (isStanding() || getLabelOrientation() == LabelOrientation.Horizontal) {
g2d.setTransform(oldTransform);
double x = (getPoints()[0].getX() + getPoints()[1].getX() - length) / 2.0;
double y = (getPoints()[0].getY() + getPoints()[1].getY() - width) / 2.0;
@@ -381,7 +383,7 @@ public abstract class AbstractLeadedComponent extends AbstractLabeledComponen
}
drawingObserver.stopTracking();
}
-// if (getLabelOriantation() == LabelOriantation.Horizontal) {
+// if (getLabelOrientation() == LabelOrientation.Horizontal) {
//
// drawCenteredText(g2d, label, getNewPoints()[2].getX(), getNewPoints()[2].getX(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// g2d.drawString(label, (int) (x + (length - textRect.getWidth()) / 2 + offset),
@@ -698,14 +700,20 @@ public abstract class AbstractLeadedComponent extends AbstractLabeledComponen
}
@EditableProperty(name = "Label Orientation")
- public LabelOriantation getLabelOriantation() {
- if (labelOriantation == null)
- labelOriantation = LabelOriantation.Directional;
- return labelOriantation;
+ public LabelOrientation getLabelOrientation() {
+ if (labelOrientation == null) {
+ if (labelOriantation != null) {
+ labelOrientation = labelOriantation;
+ labelOriantation = null;
+ } else {
+ labelOrientation = LabelOrientation.Directional;
+ }
+ }
+ return labelOrientation;
}
- public void setLabelOriantation(LabelOriantation labelOriantation) {
- this.labelOriantation = labelOriantation;
+ public void setLabelOrientation(LabelOrientation labelOrientation) {
+ this.labelOrientation = labelOrientation;
}
@Override
@@ -785,7 +793,7 @@ public abstract class AbstractLeadedComponent extends AbstractLabeledComponen
this.hideShortLeads = hideShortLeads;
}
- public enum LabelOriantation {
+ public enum LabelOrientation {
Directional, Horizontal
}
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CopperTrace.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CopperTrace.java
index b65b1e41..a8a7dfe6 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CopperTrace.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/CopperTrace.java
@@ -187,9 +187,9 @@ public class CopperTrace extends AbstractLeadedComponent implements ILayer
@Deprecated
@Override
- public LabelOriantation getLabelOriantation() {
+ public LabelOrientation getLabelOrientation() {
// TODO Auto-generated method stub
- return super.getLabelOriantation();
+ return super.getLabelOrientation();
}
@Deprecated
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Jumper.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Jumper.java
index 3e2dfbbf..2c9f8d37 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Jumper.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Jumper.java
@@ -163,9 +163,9 @@ public class Jumper extends AbstractLeadedComponent implements IContinuity
@Deprecated
@Override
- public LabelOriantation getLabelOriantation() {
+ public LabelOrientation getLabelOrientation() {
// TODO Auto-generated method stub
- return super.getLabelOriantation();
+ return super.getLabelOrientation();
}
@Override
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Line.java b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Line.java
index ad8203fe..1779870d 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Line.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/connectivity/Line.java
@@ -291,7 +291,7 @@ public class Line extends AbstractLeadedComponent {
@Deprecated
@Override
- public org.diylc.components.AbstractLeadedComponent.LabelOriantation getLabelOriantation() {
- return super.getLabelOriantation();
+ public org.diylc.components.AbstractLeadedComponent.LabelOrientation getLabelOrientation() {
+ return super.getLabelOrientation();
}
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/CapacitorDatasheetService.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/CapacitorDatasheetService.java
index 9241ff8c..3f2981d8 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/CapacitorDatasheetService.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/CapacitorDatasheetService.java
@@ -20,8 +20,10 @@ package org.diylc.components.passive;
import java.awt.Color;
import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
@@ -37,7 +39,10 @@ public class CapacitorDatasheetService {
private static CapacitorDatasheetService instance;
- private static NumberFormat format = new DecimalFormat("##.###");
+ // lookup keys are matched against the datasheet files, which always use '.' as the decimal
+ // separator
+ private static NumberFormat format =
+ new DecimalFormat("##.###", DecimalFormatSymbols.getInstance(Locale.ROOT));
private Map cache = new HashMap();
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/passive/Resistor.java b/diylc/diylc-library/src/main/java/org/diylc/components/passive/Resistor.java
index c6c9d2aa..2a4a2c11 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/passive/Resistor.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/passive/Resistor.java
@@ -197,7 +197,7 @@ public class Resistor extends AbstractLeadedComponent implements IGe
@Override
protected int getLabelOffset(int bodyLength, int bodyWidth, int labelLength) {
- if (value == null || getColorCode() == ResistorColorCode.NONE || getLabelOriantation() != AbstractLeadedComponent.LabelOriantation.Directional)
+ if (value == null || getColorCode() == ResistorColorCode.NONE || getLabelOrientation() != AbstractLeadedComponent.LabelOrientation.Directional)
return 0;
Color[] bands = value.getColorCode(colorCode);
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/BridgeRectifier.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/BridgeRectifier.java
index e4c8296b..db3a16bf 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/BridgeRectifier.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/BridgeRectifier.java
@@ -37,7 +37,7 @@ import org.diylc.awt.StringUtils;
import org.diylc.common.*;
import org.diylc.components.AbstractLabeledComponent;
import org.diylc.components.RoundedPolygon;
-import org.diylc.components.AbstractLeadedComponent.LabelOriantation;
+import org.diylc.components.AbstractLeadedComponent.LabelOrientation;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
@@ -107,7 +107,9 @@ public class BridgeRectifier extends AbstractLabeledComponent implements
private Voltage voltage;
private Current current;
transient private Area[] body;
- private LabelOriantation labelOriantation = LabelOriantation.Directional;
+ @Deprecated
+ private LabelOrientation labelOriantation;
+ private LabelOrientation labelOrientation = LabelOrientation.Directional;
public BridgeRectifier() {
super();
@@ -457,7 +459,7 @@ public class BridgeRectifier extends AbstractLabeledComponent implements
AffineTransform oldTransform = g2d.getTransform();
- if (getLabelOriantation() == LabelOriantation.Directional
+ if (getLabelOrientation() == LabelOrientation.Directional
&& (getOrientation() == Orientation.DEFAULT || getOrientation() == Orientation._180)) {
double centerX = bounds.getX() + bounds.width / 2;
double centerY = bounds.getY() + bounds.height / 2;
@@ -671,14 +673,20 @@ public class BridgeRectifier extends AbstractLabeledComponent implements
}
@EditableProperty(name = "Label Orientation")
- public LabelOriantation getLabelOriantation() {
- if (labelOriantation == null)
- labelOriantation = LabelOriantation.Directional;
- return labelOriantation;
+ public LabelOrientation getLabelOrientation() {
+ if (labelOrientation == null) {
+ if (labelOriantation != null) {
+ labelOrientation = labelOriantation;
+ labelOriantation = null;
+ } else {
+ labelOrientation = LabelOrientation.Directional;
+ }
+ }
+ return labelOrientation;
}
- public void setLabelOriantation(LabelOriantation labelOriantation) {
- this.labelOriantation = labelOriantation;
+ public void setLabelOrientation(LabelOrientation labelOrientation) {
+ this.labelOrientation = labelOrientation;
}
@Override
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/LED.java b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/LED.java
index ca3da9e9..a4cc0b02 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/LED.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/semiconductors/LED.java
@@ -23,6 +23,7 @@ package org.diylc.components.semiconductors;
import java.awt.Color;
import java.awt.Graphics2D;
+import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
@@ -35,6 +36,7 @@ import org.diylc.core.CreationMethod;
import org.diylc.core.IDIYComponent;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
+import org.diylc.core.annotations.PositiveNonZeroMeasureValidator;
import org.diylc.core.gerber.IGerberComponentSimple;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
@@ -47,10 +49,14 @@ public class LED extends AbstractLeadedComponent implements IGerberCompo
private static final long serialVersionUID = 1L;
public static Size DEFAULT_SIZE = new Size(5d, SizeUnit.mm);
+ public static Size DEFAULT_WIDTH = new Size(2d, SizeUnit.mm);
+ public static Size PIN_SPACING = new Size(2.54d, SizeUnit.mm);
public static Color BODY_COLOR = Color.decode("#5DFC0A");
public static Color BORDER_COLOR = BODY_COLOR.darker();
private String value = "";
+ private BodyShape shape = BodyShape.ROUND;
+ private Size pinSpacing = PIN_SPACING;
public LED() {
super();
@@ -67,6 +73,35 @@ public class LED extends AbstractLeadedComponent implements IGerberCompo
this.value = value;
}
+ @EditableProperty
+ public BodyShape getShape() {
+ if (shape == null) {
+ shape = BodyShape.ROUND;
+ }
+ return shape;
+ }
+
+ public void setShape(BodyShape shape) {
+ this.shape = shape;
+ }
+
+ @Override
+ protected int calculatePinSpacing(Rectangle shapeRect) {
+ return (int) getPinSpacing().convertToPixels();
+ }
+
+ @EditableProperty(name = "Lead Spacing")
+ public Size getPinSpacing() {
+ if (pinSpacing == null) {
+ pinSpacing = PIN_SPACING;
+ }
+ return pinSpacing;
+ }
+
+ public void setPinSpacing(Size pinSpacing) {
+ this.pinSpacing = pinSpacing;
+ }
+
@Override
public String getControlPointNodeName(int index) {
switch (index) {
@@ -97,7 +132,7 @@ public class LED extends AbstractLeadedComponent implements IGerberCompo
@Override
protected Size getDefaultWidth() {
- return DEFAULT_SIZE;
+ return DEFAULT_WIDTH;
}
@Override
@@ -107,6 +142,10 @@ public class LED extends AbstractLeadedComponent implements IGerberCompo
@Override
protected Shape getBodyShape() {
+ if (getShape() == BodyShape.RECTANGLE) {
+ return new Rectangle2D.Double(0, 0, getClosestOdd(getLength().convertToPixels()),
+ getClosestOdd(getWidth().convertToPixels()));
+ }
int size = getClosestOdd((int) (getLength().convertToPixels() * 1.2));
Area area = new Area(new Ellipse2D.Double(0, 0, size, size));
area.intersect(new Area(new Rectangle2D.Double(0, 0, getLength().convertToPixels() * 1.15, size)));
@@ -115,7 +154,7 @@ public class LED extends AbstractLeadedComponent implements IGerberCompo
@Override
protected void decorateComponentBody(Graphics2D g2d, boolean outlineMode) {
- if (!outlineMode) {
+ if (!outlineMode && getShape() == BodyShape.ROUND) {
int size = getClosestOdd((int) (getLength().convertToPixels() * 1.2));
int innerSize = getClosestOdd(getLength().convertToPixels());
int x = (size - innerSize) / 2;
@@ -125,14 +164,47 @@ public class LED extends AbstractLeadedComponent implements IGerberCompo
}
}
+ // the border always follows the body color; the inherited borderColor field stays only so
+ // that older files and older builds keep reading it
+ @Override
+ public Color getBorderColor() {
+ Color bodyColor = getBodyColor();
+ return bodyColor == null ? null : bodyColor.darker();
+ }
+
+ @Deprecated
+ @Override
+ public void setBorderColor(Color borderColor) {
+ super.setBorderColor(borderColor);
+ }
+
@EditableProperty(name = "Size")
@Override
public Size getLength() {
return super.getLength();
}
+ // only rectangular LEDs use the width; a round body is sized by its diameter alone
+ @EditableProperty(name = "Width", validatorClass = PositiveNonZeroMeasureValidator.class,
+ sortOrder = 102)
@Override
public Size getWidth() {
return super.getWidth();
}
+
+ public enum BodyShape {
+
+ ROUND("Round"), RECTANGLE("Rectangle");
+
+ private String label;
+
+ private BodyShape(String label) {
+ this.label = label;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+ }
}
diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/shapes/TapeMeasure.java b/diylc/diylc-library/src/main/java/org/diylc/components/shapes/TapeMeasure.java
index c0867e68..d91f13fe 100644
--- a/diylc/diylc-library/src/main/java/org/diylc/components/shapes/TapeMeasure.java
+++ b/diylc/diylc-library/src/main/java/org/diylc/components/shapes/TapeMeasure.java
@@ -344,8 +344,8 @@ public class TapeMeasure extends AbstractLeadedComponent {
@Deprecated
@Override
- public org.diylc.components.AbstractLeadedComponent.LabelOriantation getLabelOriantation() {
- return org.diylc.components.AbstractLeadedComponent.LabelOriantation.Directional;
+ public org.diylc.components.AbstractLeadedComponent.LabelOrientation getLabelOrientation() {
+ return org.diylc.components.AbstractLeadedComponent.LabelOrientation.Directional;
}
@Deprecated
diff --git a/diylc/diylc-library/src/main/java/org/diylc/serialization/ComponentAliasProvider.java b/diylc/diylc-library/src/main/java/org/diylc/serialization/ComponentAliasProvider.java
new file mode 100644
index 00000000..ed83a411
--- /dev/null
+++ b/diylc/diylc-library/src/main/java/org/diylc/serialization/ComponentAliasProvider.java
@@ -0,0 +1,41 @@
+/*
+
+ 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.serialization;
+
+import com.thoughtworks.xstream.XStream;
+
+import org.diylc.components.AbstractLeadedComponent;
+
+public class ComponentAliasProvider implements ISerializationAliasProvider {
+
+ @Override
+ public void registerAliases(XStream xStream) {
+ // the enum shipped misspelled and its class name is saved wherever the value is held as an
+ // Object (variants, defaults in config.xml); configs from before the package alias carry the
+ // full name. The alias registered last is also the one written, so files stay readable by
+ // older builds
+ xStream.alias("org.diylc.components.AbstractLeadedComponent$LabelOriantation",
+ AbstractLeadedComponent.LabelOrientation.class);
+ xStream.alias("diylc.AbstractLeadedComponent$LabelOriantation",
+ AbstractLeadedComponent.LabelOrientation.class);
+ }
+}
diff --git a/diylc/diylc-library/src/test/java/org/diylc/netlist/SpiceSumarizerTests.java b/diylc/diylc-library/src/test/java/org/diylc/netlist/SpiceSumarizerTests.java
new file mode 100644
index 00000000..36e29de2
--- /dev/null
+++ b/diylc/diylc-library/src/test/java/org/diylc/netlist/SpiceSumarizerTests.java
@@ -0,0 +1,65 @@
+/*
+
+ 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.netlist;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.diylc.components.passive.Resistor;
+import org.diylc.core.IDIYComponent;
+import org.diylc.core.measures.Resistance;
+import org.diylc.core.measures.ResistanceUnit;
+
+public class SpiceSumarizerTests {
+
+ private Locale defaultLocale;
+
+ @Before
+ public void setUp() {
+ defaultLocale = Locale.getDefault();
+ }
+
+ @After
+ public void tearDown() {
+ Locale.setDefault(defaultLocale);
+ }
+
+ @Test
+ public void testDecimalValueIgnoresLocale() throws TreeException {
+ Locale.setDefault(Locale.GERMANY);
+
+ Resistor resistor = new Resistor();
+ resistor.setName("R1");
+ resistor.setValue(new Resistance(4.7d, ResistanceUnit.K));
+
+ Netlist netlist = new Netlist(Arrays.>asList(resistor));
+ netlist.add(new Group(new Node(resistor, 0), new Node(resistor, 1)));
+
+ String spice = SpiceSumarizer.summarize(netlist, false);
+ Assert.assertTrue(spice, spice.contains(" 4.7K "));
+ }
+}
diff --git a/diylc/diylc-library/src/test/java/org/diylc/serialization/ComponentAliasProviderTest.java b/diylc/diylc-library/src/test/java/org/diylc/serialization/ComponentAliasProviderTest.java
new file mode 100644
index 00000000..04d09631
--- /dev/null
+++ b/diylc/diylc-library/src/test/java/org/diylc/serialization/ComponentAliasProviderTest.java
@@ -0,0 +1,115 @@
+/*
+
+ 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.serialization;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.io.xml.DomDriver;
+
+import org.diylc.components.AbstractLeadedComponent.LabelOrientation;
+import org.diylc.components.passive.Resistor;
+import org.diylc.components.semiconductors.BridgeRectifier;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Files saved before {@code LabelOriantation} was renamed must keep loading. The field is migrated
+ * and saved under its new name; the enum keeps its shipped class name on disk, because older builds
+ * discard a whole config file that names a class they cannot resolve.
+ */
+public class ComponentAliasProviderTest {
+
+ private static final String OLD_ENUM_NAME = "diylc.AbstractLeadedComponent_-LabelOriantation";
+ private static final String OLD_FULL_ENUM_NAME =
+ "org.diylc.components.AbstractLeadedComponent_-LabelOriantation";
+
+ // a fresh instance has not seen any class yet, which is the state it is in while reading
+ // config.xml at startup
+ private static XStream createXStream() {
+ XStream xStream = new XStream(new DomDriver("UTF-8"));
+ ProjectFileManager.configure(xStream);
+ return xStream;
+ }
+
+ // files saved before the rename carry only the misspelled field
+ private static String toOldFieldName(String xml) {
+ Assert.assertTrue(xml.contains("labelOrientation"));
+ return xml.replace("labelOrientation", "labelOriantation");
+ }
+
+ @Test
+ public void leadedComponentMigratesOldFieldName() {
+ Resistor resistor = new Resistor();
+ resistor.setLabelOrientation(LabelOrientation.Horizontal);
+ String oldXml = toOldFieldName(createXStream().toXML(resistor));
+
+ Resistor loaded = (Resistor) createXStream().fromXML(oldXml);
+ Assert.assertEquals(LabelOrientation.Horizontal, loaded.getLabelOrientation());
+
+ String savedXml = createXStream().toXML(loaded);
+ Assert.assertTrue(savedXml.contains("Horizontal"));
+ Assert.assertFalse(savedXml.contains("labelOriantation"));
+ }
+
+ @Test
+ public void bridgeRectifierMigratesOldFieldName() {
+ BridgeRectifier rectifier = new BridgeRectifier();
+ rectifier.setLabelOrientation(LabelOrientation.Horizontal);
+ String oldXml = toOldFieldName(createXStream().toXML(rectifier));
+
+ BridgeRectifier loaded = (BridgeRectifier) createXStream().fromXML(oldXml);
+ Assert.assertEquals(LabelOrientation.Horizontal, loaded.getLabelOrientation());
+
+ String savedXml = createXStream().toXML(loaded);
+ Assert.assertTrue(savedXml.contains("Horizontal"));
+ Assert.assertFalse(savedXml.contains("labelOriantation"));
+ }
+
+ @Test
+ public void enumValueHeldAsObjectReadsOldClassName() {
+ String xml = "";
+
+ Map, ?> loaded = (Map, ?>) createXStream().fromXML(xml);
+ Assert.assertEquals(LabelOrientation.Horizontal, loaded.get("Label Orientation"));
+ }
+
+ @Test
+ public void enumValueHeldAsObjectReadsOldFullClassName() {
+ String xml = "";
+
+ Map, ?> loaded = (Map, ?>) createXStream().fromXML(xml);
+ Assert.assertEquals(LabelOrientation.Horizontal, loaded.get("Label Orientation"));
+ }
+
+ @Test
+ public void enumValueHeldAsObjectWritesOldClassName() {
+ Map values = new HashMap();
+ values.put("Label Orientation", LabelOrientation.Horizontal);
+
+ String xml = createXStream().toXML(values);
+ Assert.assertTrue(xml.contains("<" + OLD_ENUM_NAME + ">Horizontal" + OLD_ENUM_NAME + ">"));
+ }
+}
diff --git a/diylc/diylc-processor/pom.xml b/diylc/diylc-processor/pom.xml
index ee1af64a..6fa04cce 100644
--- a/diylc/diylc-processor/pom.xml
+++ b/diylc/diylc-processor/pom.xml
@@ -7,7 +7,7 @@
org.diylc
diylc-parent
- 6.8.0
+ 6.9.0
diylc-processor
diff --git a/diylc/diylc-swing/deploy/win/signpath/is-signed.py b/diylc/diylc-swing/deploy/win/signpath/is-signed.py
new file mode 100644
index 00000000..44248ff2
--- /dev/null
+++ b/diylc/diylc-swing/deploy/win/signpath/is-signed.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""Exits 0 if the given PE file carries an Authenticode signature, 1 if it does not.
+
+`osslsigncode verify` also fails on an untrusted chain, which is the normal result for the
+SignPath test certificate, so it cannot answer this question on its own. This reads the PE
+Certificate Table data directory instead, which says whether a signature is present without
+making any claim about its validity.
+"""
+
+import struct
+import sys
+
+
+def has_signature(path):
+ with open(path, "rb") as f:
+ data = f.read()
+
+ pe = struct.unpack_from(" 0
+
+
+if __name__ == "__main__":
+ signed = has_signature(sys.argv[1])
+ print("signed" if signed else "unsigned")
+ sys.exit(0 if signed else 1)
diff --git a/diylc/diylc-swing/pom.xml b/diylc/diylc-swing/pom.xml
index a43d01b1..0796c83b 100644
--- a/diylc/diylc-swing/pom.xml
+++ b/diylc/diylc-swing/pom.xml
@@ -7,7 +7,7 @@
org.diylc
diylc-parent
- 6.8.0
+ 6.9.0
diylc-swing
@@ -145,10 +145,10 @@
org.diylc.local
- gerber-writer-api-1.0.0
+ gerber-writer-api-1.1.0
1.0
system
- ${project.basedir}/../lib/gerber-writer-api-1.0.0.jar
+ ${project.basedir}/../lib/gerber-writer-api-1.1.0.jar
org.diylc.local
diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/actionbar/ActionBarPlugin.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/actionbar/ActionBarPlugin.java
index 6b3c1e01..586a52ea 100644
--- a/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/actionbar/ActionBarPlugin.java
+++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/gui/actionbar/ActionBarPlugin.java
@@ -93,7 +93,17 @@ public class ActionBarPlugin implements IPlugIn {
public JPanel getActionPanel() {
if (actionPanel == null) {
- actionPanel = new JPanel();
+ actionPanel = new JPanel() {
+
+ private static final long serialVersionUID = 1L;
+
+ // shares a vertical BoxLayout with the canvas on Mac; without a height cap it absorbs
+ // half of the spare space whenever the zoomed-out canvas is smaller than the viewport
+ @Override
+ public Dimension getMaximumSize() {
+ return new Dimension(Integer.MAX_VALUE, getPreferredSize().height);
+ }
+ };
actionPanel.setOpaque(false);
actionPanel.setLayout(new GridBagLayout());
diff --git a/diylc/lib/gerber-writer-api-1.0.0.jar b/diylc/lib/gerber-writer-api-1.0.0.jar
deleted file mode 100644
index 09c27e27..00000000
Binary files a/diylc/lib/gerber-writer-api-1.0.0.jar and /dev/null differ
diff --git a/diylc/lib/gerber-writer-api-1.1.0.jar b/diylc/lib/gerber-writer-api-1.1.0.jar
new file mode 100644
index 00000000..502ca791
Binary files /dev/null and b/diylc/lib/gerber-writer-api-1.1.0.jar differ
diff --git a/diylc/pom.xml b/diylc/pom.xml
index 37fd8dbd..340bfc19 100644
--- a/diylc/pom.xml
+++ b/diylc/pom.xml
@@ -6,7 +6,7 @@
org.diylc
diylc-parent
- 6.8.0
+ 6.9.0
pom
DIY Layout Creator