Compare commits

..

70 Commits

Author SHA1 Message Date
Bane Stojkovic 5dcc3f01d5 Update update.xml 2025-12-31 15:06:48 +01:00
Bane Stojkovic 7a25a09a16 Merge branch 'master' into resize-tooltip 2025-12-31 15:04:30 +01:00
Bane Stojkovic 61b3c63bbf Revert "Introduce unique component instance ID"
This reverts commit 348eb3ac00.
2025-12-31 15:04:18 +01:00
Bane Stojkovic 405309825d Add length 2025-12-31 15:04:07 +01:00
Bane Stojkovic 6ba947cbfa translations 2025-12-31 14:35:22 +01:00
Bane Stojkovic 5a9cc6fff2 Merge branch 'master' into resize-tooltip 2025-12-31 12:31:58 +01:00
Bane Stojkovic 348eb3ac00 Introduce unique component instance ID 2025-12-31 12:29:48 +01:00
Bane Stojkovic d7360699c8 Fixes 2025-12-31 12:29:15 +01:00
Bane Stojkovic 465ddbd032 Fix blank board area rendering 2025-12-31 12:27:16 +01:00
Bane Stojkovic efe07e3ee7 Merge branch 'master' into resize-tooltip 2025-12-31 10:03:32 +01:00
Bane Stojkovic 9972e0ee46 Update .gitignore 2025-12-31 09:59:21 +01:00
Bane Stojkovic de971b707b Update build-all-profiles.sh 2025-12-30 20:09:57 +01:00
Bane Stojkovic 0dcc06f712 Update update.xml 2025-12-30 19:44:05 +01:00
Bane Stojkovic 109b0788a8 5.8.0 release 2025-12-30 14:37:56 +01:00
Bane Stojkovic 66c5ce5ad2 resize tooltip 2025-12-30 14:14:53 +01:00
Bane Stojkovic d190ebe208 Cleanup 2025-12-30 10:44:12 +01:00
Bane Stojkovic fa93af2707 'Rotary Switch (Open)' cannot be selected by clicking on the wafer for 4-pole variations 2025-12-29 22:58:15 +01:00
Bane Stojkovic 6b59966816 Minor chassis panel optimization 2025-12-29 22:39:39 +01:00
Bane Stojkovic b065c9d6fb Reorganized export and print related items in the File menu to reduce clutter 2025-12-29 16:12:36 +01:00
Bane Stojkovic 49d805a60c Improved behavior of mouse cursor during drag operations; this should also fix behavior on some linux machines that show a "forbidden" cursor while dragging 2025-12-29 15:44:59 +01:00
Bane Stojkovic a9a98bf3a4 Language improvements 2025-12-29 14:01:59 +01:00
Bane Stojkovic 840d416282 Honor label color settings for 'Tape Measure' component 2025-12-29 13:40:06 +01:00
Bane Stojkovic dc2cc4c7c8 Do not use composites in outline mode 2025-12-29 13:00:41 +01:00
Bane Stojkovic 3d51d2a38d trace ending default values 2025-12-29 12:59:03 +01:00
Bane Stojkovic 4dfb1ca630 fix default LP Switch behavior for back compatibility 2025-12-29 11:15:59 +01:00
Bane Stojkovic e9c8686960 Added new property in 'LP Switch' that allows having the middle terminals connected (as they were) or disconnected to allow more flexibility 2025-12-29 10:55:45 +01:00
Bane Stojkovic 9be97f5213 fix tests 2025-12-29 10:54:39 +01:00
Bane Stojkovic 721d582dcd Update G2DWrapper.java 2025-12-28 23:09:04 +01:00
Bane Stojkovic 443f76f3f9 Monochromatic mode 2025-12-28 23:07:50 +01:00
Bane Stojkovic 3ad9c49f53 Update update.xml 2025-12-26 23:04:28 +01:00
Bane Stojkovic 61077ed5a4 Added Silkscreen export to PDF/PNG and print capabilities 2025-12-26 23:04:26 +01:00
Bane Stojkovic 1fe3706d0f improve multiwatt icon 2025-12-26 20:24:55 +01:00
Bane Stojkovic 16df8fd173 Added multiwat IC 2025-12-26 19:53:37 +01:00
Bane Stojkovic f7806d4d3c Added trace Ending Style property 2025-12-26 12:28:16 +01:00
Bane Stojkovic 04e44e174e Improve contrast of tooltips on some Linux machines that show white text against yellow ballon 2025-12-26 12:06:31 +01:00
Bane Stojkovic f74fe19b77 Update update.xml 2025-12-26 11:57:46 +01:00
Bane Stojkovic e7487069a5 Application freezes when using underside rendering or mirroring on 'Pin Header' when sized at 1x1 2025-12-26 11:57:43 +01:00
Bane Stojkovic 9c48b80515 Use configuration for scale factor 2025-12-26 11:48:52 +01:00
Bane Stojkovic 814f4296a9 Added configuration for text scale factor in rare cases when system scale factor is not applied correctly 2025-12-25 16:27:47 +01:00
Bane Stojkovic 6fa0e7d56a Improved sensitivity of 'Multimeter Probe' that required pixel-level precision to detect connectivity; improved rendering of the probe in outline mode 2025-12-25 11:36:51 +01:00
Bane Stojkovic 12726338ef Fixed underside rendering and mirroring position issues that affected only SPDT 'Mini Toggle Switch' types 2025-12-25 10:59:31 +01:00
Bane Stojkovic f3a8e3cfb1 release 5.7.9 2025-12-21 22:07:21 +01:00
Bane Stojkovic 1ac8ba8a24 Merge pull request #980 from mm-rnd/Lever_4P3T
Lever 4P3T
2025-12-21 22:03:47 +01:00
Bane Stojkovic 7b5bb3a152 Merge branch 'master' into Lever_4P3T 2025-12-21 22:01:05 +01:00
Martin Morrell ce7753744a Lever 2502N import switch (#981)
* Add 4P3T AxLabs Lever Switch variant

* Add 2502N Lever Switch

* Bridge is now pos1 and neck pos5 as expected

Re-worked acording to the diagram here:
https://guitarpartscenter.eu/en_US/p/ALPHA-SW-2502N-5-Way-Lever-Switch-ADWH/23036
2025-12-21 21:58:20 +01:00
Martin Morrell 3adaa16537 Add 4P3T AxLabs Lever Switch variant 2025-12-20 07:46:36 +00:00
Bane Stojkovic aa4f4c9c66 'Drill Hole' changes color to red when selected 2025-12-19 20:20:07 +01:00
Bane Stojkovic ef01d52905 Update update.xml 2025-12-19 20:10:05 +01:00
Bane Stojkovic 892de712b2 Merge branch 'master' of https://github.com/bancika/diy-layout-creator 2025-12-19 20:09:53 +01:00
Martin Morrell 79aa25f14c Update positon names to match switch positions (#977) 2025-12-19 20:07:12 +01:00
Bane Stojkovic 165b61329c Merge branch 'master' of https://github.com/bancika/diy-layout-creator 2025-12-19 20:02:33 +01:00
Bane Stojkovic c251e5ace2 flip S1 position names 2025-12-19 20:02:24 +01:00
Martin Morrell 550e221265 Fix S1 Switch markers (#978) 2025-12-19 20:02:10 +01:00
Martin Morrell 7110897e9d Add 6P5T AxLabs Lever Switch variant (#979) 2025-12-19 19:54:15 +01:00
Bane Stojkovic 74badd8a01 Update DrillHole.java 2025-08-11 22:29:03 +02:00
Bane Stojkovic 95eccffa4a 'Connect to Patreon' option says that it connected account with username 'a b c' but doesn't do anything 2025-08-11 19:25:00 +02:00
Bane Stojkovic 35a190b02f Added 'Drill Hole' component 2025-06-21 22:45:46 +02:00
Bane Stojkovic 92457ae0fb Increase maximum size of the backup folder from 64MB to 1GB for deeper history 2025-06-18 21:35:14 +02:00
Bane Stojkovic 73d3dd1cd4 fix: Alignment issues of some visual elements in the resulting Gerber files, e.g. DIL IC pin designators 2025-06-16 23:33:56 +02:00
Bane Stojkovic cb9cd46261 fix: When a switch position is selected (i.e. set to a value other than None), DIYLC will treat the terminals that are internally connected in that switching logic as connected in analysis tools 2025-06-06 23:52:28 +02:00
Bane Stojkovic 4bd3943345 bigger cloud search window 2025-06-06 00:09:28 +02:00
Bane Stojkovic 30bdc7fc3d disable patreon connect when not logged in 2025-06-05 00:05:17 +02:00
Bane Stojkovic a1d31c1ae2 add join patreon link at the top and randomly show 2025-06-05 00:05:04 +02:00
Bane Stojkovic 90fb3095bd More intuitive user account creation dialog for DIYLC Cloud - showing exact requirements for password and marking optional entries 2025-06-04 23:33:50 +02:00
Bane Stojkovic 0c468369cd Simplify zoom in/out keyboard shortcuts to only plus/minus keys and zero numerical key for zoom reset to 100 2025-06-02 23:02:33 +02:00
Bane Stojkovic f88b0c642a mac build naming. Enable windows arm 2025-05-30 23:47:04 +02:00
Bane Stojkovic 1b8bdbe7ed Reduce logging 2025-05-29 23:45:40 +02:00
Bane Stojkovic 185b5da4d5 Bundle Java JRE with Linux AppImage to avoid having to install it 2025-05-29 23:32:27 +02:00
Bane Stojkovic cd92464467 fix auto-edit 2025-05-26 22:35:18 +02:00
Bane Stojkovic 59e18c6be2 Feature/compare (#929)
* add compare tool

* improvements

* diff improvements

* license

* diff logic and tests fixed

* update license info

* add isPolarized to component interface

* handle comparison of non-polarized components
2025-05-26 14:57:47 +02:00
110 changed files with 4450 additions and 456 deletions
+7
View File
@@ -147,3 +147,10 @@ buildNumber.properties
# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
!/.mvn/wrapper/maven-wrapper.jar
######################
###### Custom ######
######################
# Deployment folder
deploy/
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
# Script to build all Maven profiles and copy zip files to deployment folder
# Usage: ./build-all-profiles.sh [to-deploy-folder]
set -e # Exit on error
# Parse arguments
DEPLOY_DIR="${1:-}"
# Configuration
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SWING_MODULE_DIR="${PROJECT_DIR}/diylc-swing"
DEPLOY_DIR="${DEPLOY_DIR:-${PROJECT_DIR}/deploy}"
# List of profiles to build
PROFILES=(
"universal-zip"
"linux-appimage"
"win64-zip"
"win64-installer"
"mac-x86"
"mac-arm"
)
echo "=========================================="
echo "Building all Maven profiles"
echo "=========================================="
echo "Project directory: ${PROJECT_DIR}"
echo "Swing module directory: ${SWING_MODULE_DIR}"
echo "Deployment directory: ${DEPLOY_DIR}"
echo "Tests: Skipped"
echo ""
# Create deployment directory if it doesn't exist
mkdir -p "${DEPLOY_DIR}"
# Change to project root directory
cd "${PROJECT_DIR}"
# Build each profile (clean before each one)
TARGET_DIR="${SWING_MODULE_DIR}/target"
TOTAL_ZIP_COUNT=0
for profile in "${PROFILES[@]}"; do
echo "----------------------------------------"
echo "Building profile: ${profile}"
echo "----------------------------------------"
# Clean and build from root, specifying the swing module and profile
# This ensures dependencies (diylc-core, diylc-library) are built first
mvn clean package -pl diylc-swing -am -P"${profile}" -DskipTests || {
echo "ERROR: Failed to build profile ${profile}"
echo "Continuing with next profile..."
continue
}
echo "✓ Profile ${profile} built successfully"
# Copy zip and exe files immediately after each build (before next clean)
if [ -d "${TARGET_DIR}" ]; then
echo "Looking for artifacts in ${TARGET_DIR}..."
PROFILE_FILE_COUNT=0
# Find all zip files in target directory (excluding DIYLC-notarize.zip)
while IFS= read -r -d '' zipfile; do
filename=$(basename "${zipfile}")
# Skip DIYLC-notarize.zip files
if [[ "${filename}" != "DIYLC-notarize.zip" ]] && [ -f "${zipfile}" ]; then
dest="${DEPLOY_DIR}/${filename}"
echo " → Copying: ${filename}"
cp "${zipfile}" "${dest}"
TOTAL_ZIP_COUNT=$((TOTAL_ZIP_COUNT + 1))
PROFILE_FILE_COUNT=$((PROFILE_FILE_COUNT + 1))
fi
done < <(find "${TARGET_DIR}" -maxdepth 1 -type f -name "*.zip" -print0 2>/dev/null)
# Find all exe files in target directory
while IFS= read -r -d '' exefile; do
if [ -f "${exefile}" ]; then
filename=$(basename "${exefile}")
dest="${DEPLOY_DIR}/${filename}"
echo " → Copying: ${filename}"
cp "${exefile}" "${dest}"
TOTAL_ZIP_COUNT=$((TOTAL_ZIP_COUNT + 1))
PROFILE_FILE_COUNT=$((PROFILE_FILE_COUNT + 1))
fi
done < <(find "${TARGET_DIR}" -maxdepth 1 -type f -name "*.exe" -print0 2>/dev/null)
if [ ${PROFILE_FILE_COUNT} -eq 0 ]; then
echo " ⚠ Warning: No artifacts found for profile ${profile}"
# List what's actually in the target directory for debugging
echo " Debug: Files in target directory:"
ls -la "${TARGET_DIR}"/*.{zip,exe} 2>/dev/null | head -5 || echo " (no zip/exe files found)"
else
echo " ✓ Copied ${PROFILE_FILE_COUNT} file(s) from ${profile}"
fi
else
echo " ⚠ Warning: Target directory ${TARGET_DIR} does not exist"
fi
echo ""
done
echo "=========================================="
echo "Copying summary"
echo "=========================================="
if [ ${TOTAL_ZIP_COUNT} -eq 0 ]; then
echo "No artifacts were copied"
else
echo "✓ Copied ${TOTAL_ZIP_COUNT} file(s) total to ${DEPLOY_DIR}"
fi
echo ""
echo "=========================================="
echo "Build and deployment complete!"
echo "=========================================="
echo "Deployment folder: ${DEPLOY_DIR}"
echo "Files:"
ls -lh "${DEPLOY_DIR}"/*.{zip,exe} 2>/dev/null || echo " (no zip/exe files found)"
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.4.0</version>
<version>5.8.0</version>
</parent>
<artifactId>diylc-core</artifactId>
@@ -68,5 +68,9 @@ public enum DrawOption {
/**
* Whether locked components should be rendered as transparent or not.
*/
LOCKED_ALPHA;
LOCKED_ALPHA,
/**
* When applied, all colors will be converted to blackest black or whitest white
*/
MONOCHROMATIC;
}
@@ -21,7 +21,6 @@
*/
package org.diylc.common;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import org.diylc.core.Project;
@@ -55,12 +54,7 @@ public enum EventType {
*/
FILE_STATUS_CHANGED,
/**
* Called when selection rectangle is changed. Object of type {@link Rectangle} is the only
* parameter and it contains the new selection rectangle.
*/
SELECTION_RECT_CHANGED,
/**
* Called when component selection is changed. New {@link ComponentSelection} is attached as a
* Called when component selection is changed. New list of components and control point map is attached as a
* parameter.
*/
SELECTION_CHANGED,
@@ -41,16 +41,18 @@ import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
import org.diylc.core.Template;
import org.diylc.core.Theme;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.core.measures.Size;
import org.diylc.filter.IComponentFilter;
import org.diylc.test.ITestProcessor;
/**
* Interface for communication between plug-ins and the application. Plug-ins may acquire
* information or make changes through this interface. Application events are dispatched to plug-ins
* through {@link MessageDispatcher}
*
*
* @author Branislav Stojkovic
*
*
* @see IPlugIn
* @see MessageDispatcher
* @see IMessageListener
@@ -84,18 +86,20 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
public static final String RENUMBER_ON_PASTE_KEY = "renumberOnPaste";
public static final String RULER_IN_SUBDIVISION_KEY = "rulerInSubdivision";
public static final String CACHING_ENABLED_KEY = "cachingEnabled";
public static final String FONT_SCALE_FACTOR_KEY = "fontScaleFactor";
public static final String SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY = "showResizeDimensionsTooltip";
public static final String RULER_IN_SUBDIVISION_2 = "base of 2";
public static final String RULER_IN_SUBDIVISION_10 = "base of 10";
public static final String RULER_IN_SUBDIVISION_DEFAULT = RULER_IN_SUBDIVISION_10;
public static final String SNAP_TO_NONE = "None";
public static final String SNAP_TO_GRID = "Grid";
public static final String SNAP_TO_COMPONENTS = "Components";
public static final String SNAP_TO_DEFAULT = SNAP_TO_GRID;
public static final String LOCKED_ALPHA = "lockedAlpha";
public static final String LANGUAGE = "language";
public static final String LANGUAGE_DEFAULT = "English";
@@ -108,25 +112,25 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
public static final int NOBUTTON = 0;
public static final int BUTTON1 = 1;
public static final int BUTTON2 = 2;
public static final int BUTTON3 = 3;
public static final int BUTTON3 = 3;
/**
* Returns size of the canvas that takes project dimensions into account as well as zoom level.
* Each dimension is calculated as the product of the actual size and the number of pixels per
* unit. If <code>useZoom</code> is set to true, the result is scaled by zoom factor.
*
*
* @param useZoom
* @param includeExtraSpace
*
*
* @return canvas dimensions
*/
Dimension getCanvasDimensions(boolean useZoom, boolean includeExtraSpace);
/**
* Returns an instance of {@link Cursor} that should be used at the specified location.
*
*
* Note: point coordinates are scaled for zoom factor.
*
*
* @param point
* @param ctrlDown
* @param shiftDown
@@ -137,14 +141,14 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Returns an instance of currently loaded project.
*
*
* @return
*/
Project getCurrentProject();
/**
* Loads specified {@link Project}.
*
*
* @param project
* @param freshStart
* @param filename file name when loading from a file
@@ -158,22 +162,22 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Loads a project from the specified file.
*
*
* @param fileName
*/
void loadProjectFromFile(String fileName);
/**
* Saves the current project into the specified file.
*
*
* @param fileName
* @param isBackup
*/
void saveProjectToFile(String fileName, boolean isBackup);
/**
* Exports the current project into one or more files - one per layer and potentially drill layer.
*
*
* @param fileNameBase
* @param g2d
*/
@@ -191,7 +195,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Shows a user dialog if there are changes to confirm that it's safe to proceed.
*
*
* @return true, if file actions (new, open, close) can be taken
*/
boolean allowFileAction();
@@ -200,7 +204,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
* Returns all available {@link ComponentType}s classified by category. Result is a {@link Map}
* between category name to a {@link List} of all {@link ComponentType}s that share that category
* name.
*
*
* @return
*/
Map<String, List<ComponentType>> getComponentTypes();
@@ -208,7 +212,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Draws project on the provided {@link Graphics2D}. If the provided filter is not null, it will
* be used to filter the components that are shown.
*
*
* @param g2d
* @param drawOptions specific drawing options
* @param filter
@@ -222,35 +226,35 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Returns current zoom level where <code>zoomLevel = 1.0d</code> means 100%.
*
*
* @return current zoom level
*/
double getZoomLevel();
/**
* Changes current zoom level where <code>zoomLevel = 1.0d</code> means 100%
*
*
* @param zoomLevel new zoom leve
*/
void setZoomLevel(double zoomLevel);
/**
* Returns current version number.
*
*
* @return
*/
VersionNumber getCurrentVersionNumber();
/**
* Returns few most recent versions.
*
* Returns few most recent versions.
*
* @return
*/
List<Version> getRecentUpdates();
/**
* Adds a list of components to the project.
*
*
* @param componentTransferable
* @param autoGroup
* @param assignNewNames
@@ -269,7 +273,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Sets default value for the specified property name for currently selected component types.
*
*
* @param propertyName display name for property
* @param value new default value, must not be null
*/
@@ -277,7 +281,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Sets default value for the specified property name for projects.
*
*
* @param clazz class to set defaults to
* @param propertyName display name for property
* @param value new default value, must not be null
@@ -291,6 +295,15 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
*/
List<PropertyWrapper> getMutualSelectionProperties();
/**
* Available only during resizing, returns the dimensions of the component being resized.
* Otherwise returns null.
*
* @return ResizeDimensions with width, height (in mm or in depending on configuration),
* and length (if component implements IHaveLength), or null if not resizing
*/
ResizeDimensions getResizeDimensions();
void editSelection();
/**
@@ -37,16 +37,14 @@ import com.thoughtworks.xstream.annotations.XStreamOmitField;
import org.diylc.common.IPlugInPort;
import org.diylc.common.LineStyle;
import org.diylc.common.ObjectCache;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComponent<T> {
public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComponent<T> implements
IHaveLength {
private static final long serialVersionUID = 1L;
@@ -337,8 +335,8 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
// }
}
@EditableProperty
public Size getLength() {
@EditableProperty(name = "Length")
public Size calculateLength() {
double d = 0;
try {
Point2D[] p = getControlPoints();
@@ -0,0 +1,29 @@
/*
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 org.diylc.core.measures.Size;
public interface IHaveLength {
Size calculateLength();
}
@@ -300,7 +300,7 @@ public class GerberG2DWrapper extends Graphics2D
@Override
public AffineTransform getTransform() {
return tx;
return new AffineTransform(tx);
}
@Override
@@ -0,0 +1,33 @@
/*
*
* 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.measures;
/**
* Record containing resize dimensions for a component being resized.
*
* @param width the width of the component (in mm or in, depending on configuration)
* @param height the height of the component (in mm or in, depending on configuration)
* @param length the length of the component (if it implements IHaveLength), null otherwise
*/
public record ResizeDimensions(Size width, Size height, Size length) {
}
@@ -19,7 +19,7 @@
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.common;
package org.diylc.filter;
import org.diylc.core.IDIYComponent;
@@ -19,11 +19,10 @@
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.presenter;
package org.diylc.filter;
import java.lang.reflect.Method;
import org.diylc.common.IComponentFilter;
import org.diylc.common.PCBLayer;
import org.diylc.core.IDIYComponent;
@@ -0,0 +1,23 @@
package org.diylc.filter;
import org.diylc.core.IBoard;
import org.diylc.core.IDIYComponent;
import org.diylc.core.gerber.GerberRenderMode;
import org.diylc.core.gerber.IGerberComponent;
import java.util.Set;
public class SilkScreenFilter implements IComponentFilter {
@Override
public boolean testComponent(IDIYComponent<?> component) {
if (component instanceof IBoard board) {
return board.shouldExportToGerber();
}
if (!(component instanceof IGerberComponent gerberComponent)) {
return false;
}
Set<GerberRenderMode> gerberRenderModes = gerberComponent.getGerberRenderModes();
return gerberRenderModes.contains(GerberRenderMode.Outline);
}
}
@@ -101,23 +101,7 @@ public class NetlistBuilder {
// if there are no switches, make one with 1 position so we get 1 result back
if (switches.isEmpty())
switches.add(new ISwitch() {
@Override
public String getPositionName(int position) {
return "Default";
}
@Override
public int getPositionCount() {
return 1;
}
@Override
public boolean arePointsConnected(int index1, int index2, int position) {
return false;
}
});
switches.add(new TrivialSwitch());
// construct all possible combinations
int[] positions = new int[switches.size()];
@@ -418,7 +402,9 @@ public class NetlistBuilder {
// handle direct connections or connections through a switch
// when not using "includeSwitches" flag
if (c instanceof IContinuity &&
(!(c instanceof ISwitch) || switchPositions.isEmpty())) {
// ugly way to check if running in "exclude switches" mode.
(!(c instanceof ISwitch) || (switchPositions.size() == 1 &&
switchPositions.keySet().stream().findFirst().get() instanceof TrivialSwitch))) {
for (int i = 0; i < c.getControlPointCount() - 1; i++)
for (int j = i + 1; j < c.getControlPointCount(); j++)
if (((IContinuity) c).arePointsConnected(i, j))
@@ -493,4 +479,23 @@ public class NetlistBuilder {
return area;
}
}
private static class TrivialSwitch implements ISwitch {
@Override
public String getPositionName(int position) {
return "Default";
}
@Override
public int getPositionCount() {
return 1;
}
@Override
public boolean arePointsConnected(int index1, int index2, int position) {
return false;
}
}
}
@@ -18,6 +18,7 @@
*/
package org.diylc.presenter;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.util.List;
@@ -71,4 +72,54 @@ public class CalcUtils {
return false;
return true;
}
/**
* Calculates the relative luminance (perceived brightness) of a color.
* Uses the standard formula: L = 0.299*R + 0.587*G + 0.114*B
*
* @param color the color to calculate luminance for
* @return the luminance value between 0 and 255
*/
public static double calculateLuminance(Color color) {
if (color == null) {
return 0.0;
}
return 0.299 * color.getRed() + 0.587 * color.getGreen() + 0.114 * color.getBlue();
}
/**
* Converts a color to monochrome (black or white) based on its luminance.
* Colors with luminance below the threshold (128) are converted to black,
* otherwise to white. The alpha channel is preserved if present.
*
* @param color the color to convert
* @param threshold the luminance threshold (default 128 for middle gray)
* @return black or white color, preserving alpha channel
*/
public static Color convertToMonochrome(Color color, double threshold) {
if (color == null) {
return null;
}
double luminance = calculateLuminance(color);
// Use <= to handle edge case where luminance equals threshold (should be white)
// Add small epsilon to account for floating point precision issues
Color monochromeColor = (luminance < threshold - 1e-10) ? Color.BLACK : Color.WHITE;
// Preserve alpha channel if present
if (color.getAlpha() < 255) {
monochromeColor = new Color(monochromeColor.getRed(), monochromeColor.getGreen(),
monochromeColor.getBlue(), color.getAlpha());
}
return monochromeColor;
}
/**
* Converts a color to monochrome (black or white) based on its luminance.
* Uses the default threshold of 128 (middle gray).
*
* @param color the color to convert
* @return black or white color, preserving alpha channel
*/
public static Color convertToMonochrome(Color color) {
return convertToMonochrome(color, 128.0);
}
}
@@ -26,10 +26,12 @@ import java.lang.ref.SoftReference;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
import org.diylc.common.ComponentType;
import org.diylc.common.DrawOption;
import org.diylc.core.ComponentForRender;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
@@ -67,14 +69,14 @@ public class DrawingCache {
*/
@SuppressWarnings("unchecked")
public void bulkPrepare(Collection<ComponentForRender> components, G2DWrapper g2d, boolean outlineMode, Project project,
double zoom, double scaleFactor) {
double zoom, double scaleFactor, Set<DrawOption> drawOptions) {
// run all renders in parallel to utilize all CPU cores
components.stream().parallel().forEach(x -> {
ComponentType type = ComponentProcessor.getInstance()
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) x.getComponent().getClass());
if (type != null && type.getEnableCache()) {
renderAndCache(x.getComponent(), g2d, x.getState(), outlineMode, project, zoom, scaleFactor, x.getZOrder());
renderAndCache(x.getComponent(), g2d, x.getState(), outlineMode, project, zoom, scaleFactor, x.getZOrder(), drawOptions);
}
});
}
@@ -92,7 +94,7 @@ public class DrawingCache {
*/
@SuppressWarnings("unchecked")
public void draw(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Rectangle2D visibleRect) {
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Rectangle2D visibleRect, Set<DrawOption> drawOptions) {
ComponentType type = ComponentProcessor.getInstance()
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
@@ -100,7 +102,7 @@ public class DrawingCache {
// if we need to apply caching
Point2D firstPoint = component.getControlPoint(0);
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom, scaleFactor, zOrder);
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom, scaleFactor, zOrder, drawOptions);
if (value == null) {
return;
@@ -180,7 +182,7 @@ public class DrawingCache {
}
private CacheValue renderAndCache(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder) {
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Set<DrawOption> drawOptions) {
if (!g2d.getClipBounds().intersects(component.getCachingBounds())) {
return null;
@@ -199,9 +201,11 @@ public class DrawingCache {
}
// only honor the cache if the component hasn't changed in the meantime
// Also check if drawOptions changed (e.g., MONOCHROMATIC affects rendering)
boolean drawOptionsChanged = value == null || !value.getDrawOptions().equals(drawOptions);
if (value == null || !value.getComponent().equalsTo(component)
|| value.getState() != componentState || value.getZoom() != zoom || value.getOutlineMode() != outlineMode
|| value.getScaleFactor() != scaleFactor) {
|| value.getScaleFactor() != scaleFactor || drawOptionsChanged) {
// figure out size and placement of buffer image
Rectangle2D rect = component.getCachingBounds();
int width = (int)Math.round(rect.getWidth() * zoom);
@@ -233,7 +237,7 @@ public class DrawingCache {
cg2d.translate(-firstPoint.getX() + dx, -firstPoint.getY() + dy);
// wrap the graphics so we can track
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom);
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom, drawOptions);
// initialize wrapper
wrapper.startedDrawingComponent(zOrder);
@@ -275,7 +279,7 @@ public class DrawingCache {
// add to the cache
value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(),
wrapper.getContinuityNegativeAreas(), componentState, outlineMode, zoom, scaleFactor, dx, dy, rect);
wrapper.getContinuityNegativeAreas(), componentState, outlineMode, zoom, scaleFactor, dx, dy, rect, drawOptions);
imageCache.put(component, new SoftReference<DrawingCache.CacheValue>(value));
}
@@ -295,11 +299,12 @@ public class DrawingCache {
int dx;
int dy;
Rectangle2D cacheBounds;
Set<DrawOption> drawOptions;
public CacheValue(IDIYComponent<?> component, BufferedImage image, Area currentArea,
Map<String, Area> continuityPositiveAreas, Map<String, Area> continuityNegativeAreas,
ComponentState state, boolean outlineMode, double zoom, double scaleFactor, int dx, int dy,
Rectangle2D cacheBounds) {
Rectangle2D cacheBounds, Set<DrawOption> drawOptions) {
super();
this.cacheBounds = cacheBounds;
// take a copy of the component, so we can check if it changed in the meantime
@@ -317,6 +322,7 @@ public class DrawingCache {
this.dx = dx;
this.dy = dy;
this.scaleFactor = scaleFactor;
this.drawOptions = drawOptions;
}
public IDIYComponent<?> getComponent() {
@@ -366,6 +372,10 @@ public class DrawingCache {
public Rectangle2D getCacheBounds() {
return cacheBounds;
}
public Set<DrawOption> getDrawOptions() {
return drawOptions;
}
@Override
public String toString() {
@@ -54,6 +54,7 @@ import org.diylc.components.AbstractTransparentComponent;
import org.diylc.core.*;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.filter.IComponentFilter;
import org.diylc.netlist.ContinuityGraph;
import org.diylc.netlist.NetlistBuilder;
import org.diylc.utils.Constants;
@@ -183,7 +184,7 @@ public class DrawingManager {
if (externalZoom != null)
zoom *= externalZoom;
G2DWrapper g2dWrapper = new G2DWrapper(g2d, zoom);
G2DWrapper g2dWrapper = new G2DWrapper(g2d, zoom, drawOptions);
configureRenderingHints(g2d, drawOptions);
@@ -267,7 +268,7 @@ public class DrawingManager {
// give the cache a chance to prepare all the components in multiple threads
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) {
DrawingCache.Instance.bulkPrepare(componentForRenderList, g2dWrapper, outlineMode, project, zoom, scaleFactor);
DrawingCache.Instance.bulkPrepare(componentForRenderList, g2dWrapper, outlineMode, project, zoom, scaleFactor, drawOptions);
}
// componentAreaMap.clear();
@@ -561,10 +562,12 @@ public class DrawingManager {
}
}
private static void drawComponent(Project project, Set<DrawOption> drawOptions, Double scaleFactor, Rectangle2D visibleRect, IDIYComponent<?> component, G2DWrapper g2dWrapper, ComponentState state, double zoom, int i, boolean outlineMode) {
private static void drawComponent(Project project, Set<DrawOption> drawOptions, Double scaleFactor,
Rectangle2D visibleRect, IDIYComponent<?> component, G2DWrapper g2dWrapper, ComponentState state,
double zoom, int i, boolean outlineMode) {
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) // go through the DrawingCache
DrawingCache.Instance.draw(component, g2dWrapper, state,
drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom, scaleFactor, i, visibleRect);
drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom, scaleFactor, i, visibleRect, drawOptions);
else // go straight to the wrapper
component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
}
@@ -53,17 +53,11 @@ import java.awt.image.renderable.RenderableImage;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.AttributedCharacterIterator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;
import org.apache.log4j.Logger;
import org.diylc.common.DrawOption;
import org.diylc.common.ObjectCache;
import org.diylc.common.ZoomableStroke;
import org.diylc.core.IDrawingObserver;
@@ -71,7 +65,7 @@ import org.diylc.core.IDrawingObserver;
/**
* {@link Graphics2D} wrapper that keeps track of all drawing actions and creates an {@link Area}
* that corresponds to drawn objects. Before each component is drawn,
* {@link #startedDrawingComponent()} should be called. After the component is drawn, area may be
* {@link #startedDrawingComponent(int)} should be called. After the component is drawn, area may be
* retrieved using {@link #finishedDrawingComponent()}. Graphics configuration (color, font, etc) is
* reset between each two components.
*
@@ -96,7 +90,9 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
private int uniqueCounter = 0;
private Graphics2D canvasGraphics;
private final Graphics2D canvasGraphics;
private final Set<DrawOption> drawOptions;
private Stroke originalStroke;
private Color originalColor;
private Composite originalComposite;
@@ -115,12 +111,14 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
/**
* Creates a wrapper around specified {@link Graphics2D} object.
*
*
* @param canvasGraphics
* @param drawOptions
*/
public G2DWrapper(Graphics2D canvasGraphics, double zoom) {
public G2DWrapper(Graphics2D canvasGraphics, double zoom, Set<DrawOption> drawOptions) {
super();
this.canvasGraphics = canvasGraphics;
this.drawOptions = drawOptions;
String debugAreaPerformanceStr = System.getProperty(DEBUG_AREA_PERFORMANCE);
debugAreaPerformance = "true".equalsIgnoreCase(debugAreaPerformanceStr);
this.zoom = zoom;
@@ -482,7 +480,9 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
@Override
public void setComposite(Composite comp) {
canvasGraphics.setComposite(comp);
if (!drawOptions.contains(DrawOption.OUTLINE_MODE)) {
canvasGraphics.setComposite(comp);
}
// FIXME: check this.
}
@@ -758,7 +758,11 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
@Override
public void setColor(Color c) {
canvasGraphics.setColor(c);
if (drawOptions.contains(DrawOption.MONOCHROMATIC) && c != null) {
canvasGraphics.setColor(CalcUtils.convertToMonochrome(c));
} else {
canvasGraphics.setColor(c);
}
}
@Override
@@ -59,18 +59,13 @@ import com.thoughtworks.xstream.security.AnyTypePermission;
import org.diylc.clipboard.ComponentTransferable;
import org.diylc.common.*;
import org.diylc.core.ExpansionMode;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDatasheetSupport;
import org.diylc.core.IView;
import org.diylc.core.Project;
import org.diylc.core.Template;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.IAutoCreator;
import org.diylc.core.gerber.GerberExporter;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.filter.IComponentFilter;
import org.diylc.lang.LangUtil;
import org.diylc.netlist.INetlistParser;
import org.diylc.netlist.Netlist;
@@ -250,8 +245,10 @@ public class Presenter implements IPlugInPort {
@Override
public Cursor getCursorAt(Point point, boolean ctrlDown, boolean shiftDown, boolean altDown) {
// Only change the cursor if we're not making a new component.
if (operationMode == OperationMode.HIGHLIGHT_CONNECTED_AREAS || altDown)
if (operationMode == OperationMode.HIGHLIGHT_CONNECTED_AREAS || altDown) {
return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
}
if (instantiationManager.getComponentTypeSlot() == null) {
// Scale point to remove zoom factor.
Point2D scaledPoint = scalePoint(point);
@@ -268,6 +265,7 @@ public class Presenter implements IPlugInPort {
}
}
}
return Cursor.getDefaultCursor();
}
@@ -427,7 +425,7 @@ public class Presenter implements IPlugInPort {
}
@Override
public void draw(Graphics2D g2d, Set<DrawOption> drawOptions, final IComponentFilter filter, Double externalZoom,
public void draw(Graphics2D g2d, Set<DrawOption> drawOptions, final IComponentFilter filter, Double externalZoom,
Double scaleFactor, Rectangle2D visibleRect) {
if (currentProject == null) {
return;
@@ -924,29 +922,36 @@ public class Presenter implements IPlugInPort {
messageDispatcher.dispatchMessage(EventType.REPAINT);
}
@Override
public Rectangle2D getSelectionBounds(boolean applyZoom) {
if (selectedComponents == null || selectedComponents.isEmpty())
return getComponentBounds(selectedComponents, applyZoom);
}
private Rectangle2D getComponentBounds(Collection<IDIYComponent<?>> components, boolean applyZoom) {
if (components == null || components.isEmpty())
return null;
int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
for (IDIYComponent<?> c : selectedComponents) {
for (IDIYComponent<?> c : components) {
ComponentArea compArea = drawingManager.getComponentArea(c);
if (compArea != null && compArea.getOutlineArea() != null) {
Rectangle rect = compArea.getOutlineArea().getBounds();
if (rect.x < minX)
minX = rect.x;
if (rect.x + rect.width > maxX)
maxX = rect.x + rect.width;
if (rect.y < minY)
minY = rect.y;
if (rect.y + rect.height > maxY)
maxY = rect.y + rect.height;
} else if (currentProject.getComponents().contains(c))
if (compArea == null || compArea.getOutlineArea() == null) {
LOG.debug("Area is null for " + c.getName() + " of type " + c.getClass().getName());
return null;
}
Rectangle rect = compArea.getOutlineArea().getBounds();
if (rect.x < minX)
minX = rect.x;
if (rect.x + rect.width > maxX)
maxX = rect.x + rect.width;
if (rect.y < minY)
minY = rect.y;
if (rect.y + rect.height > maxY)
maxY = rect.y + rect.height;
}
if (configManager.readBoolean(EXTRA_SPACE_KEY, true)) {
@@ -1015,8 +1020,9 @@ public class Presenter implements IPlugInPort {
}
updateSelection(matching);
messageDispatcher.dispatchMessage(EventType.REPAINT, true);
if (matching.size() > 0)
if (!matching.isEmpty()) {
messageDispatcher.dispatchMessage(EventType.SCROLL_TO, getSelectionBounds(true));
}
}
@Override
@@ -1233,11 +1239,14 @@ public class Presenter implements IPlugInPort {
Map<String, Object> params = new HashMap<String, Object>();
params.put("point", point);
test.addStep(DIYTest.DRAG_OVER, params);
}
}
if (point == null || operationMode == OperationMode.HIGHLIGHT_CONNECTED_AREAS) {
return false;
}
boolean resizeInProgress = false;
Point2D scaledPoint = scalePoint(point);
if (controlPointMap != null && !controlPointMap.isEmpty()) {
// We're dragging control point(s).
@@ -1248,6 +1257,13 @@ public class Presenter implements IPlugInPort {
if (actualD == null)
return true;
// if there's at least one component with only one control point being dragged it means that we are resizing
if (controlPointMap.size() == 1 && controlPointMap.entrySet().stream()
.anyMatch(entry -> entry.getValue().size() == 1
&& entry.getKey().getControlPointCount() > 1)) {
resizeInProgress = true;
}
previousDragPoint.setLocation(previousDragPoint.getX() + actualD.getX(), previousDragPoint.getY() + actualD.getY());
} else if (selectedComponents.isEmpty() && instantiationManager.getComponentTypeSlot() == null
&& previousDragPoint != null) {
@@ -1265,10 +1281,50 @@ public class Presenter implements IPlugInPort {
this.previousScaledPoint = scalePoint(point);
instantiationManager.updateSingleClick(previousScaledPoint, isSnapToGrid(), currentProject.getGridSpacing());
}
messageDispatcher.dispatchMessage(EventType.REPAINT);
messageDispatcher.dispatchMessage(EventType.REPAINT, false, resizeInProgress);
return true;
}
public ResizeDimensions getResizeDimensions() {
if (controlPointMap == null || controlPointMap.isEmpty()) {
return null;
}
// Find the component being resized (one with a single control point being dragged)
IDIYComponent<?> resizingComponent = null;
for (Map.Entry<IDIYComponent<?>, Set<Integer>> entry : controlPointMap.entrySet()) {
if (entry.getValue().size() == 1 && entry.getKey().getControlPointCount() > 1) {
resizingComponent = entry.getKey();
break;
}
}
if (resizingComponent == null) {
return null;
}
Rectangle2D componentBounds = getComponentBounds(controlPointMap.keySet(), false);
if (componentBounds == null) {
return null;
}
// Convert pixel dimensions to Size (mm or in based on configuration)
boolean metric = configManager.readBoolean(METRIC_KEY, true);
SizeUnit unit = metric ? SizeUnit.mm : SizeUnit.in;
Size width = Size.fromPixels(componentBounds.getWidth(), unit);
Size height = Size.fromPixels(componentBounds.getHeight(), unit);
// Get length if component implements IHaveLength
Size length = null;
if (resizingComponent instanceof IHaveLength iHaveLength) {
length = iHaveLength.calculateLength();
}
return new ResizeDimensions(width, height, length);
}
private Point2D moveComponents(Map<IDIYComponent<?>, Set<Integer>> controlPointMap, int dx, int dy, boolean snapToGrid, boolean snapToObjects) {
// After we make the transfer and snap to grid, calculate actual dx
// and dy. We'll use them to translate the previous drag point.
@@ -1580,6 +1636,7 @@ public class Presenter implements IPlugInPort {
drawingManager.clearContinuityArea();
projectFileManager.notifyFileChange();
}
messageDispatcher.dispatchMessage(EventType.REPAINT);
dragInProgress = false;
}
@@ -2166,6 +2223,10 @@ public class Presenter implements IPlugInPort {
}
Rectangle2D rect = getSelectionBounds(false);
if (rect == null) {
return null;
}
double width = rect.getWidth();
double height = rect.getHeight();
+181 -1
View File
@@ -290,7 +290,7 @@
<minor>5</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-05-13 00:00:00.000 CET</releaseDate>
<releaseDate>2025-05-26 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
@@ -313,6 +313,10 @@
<changeType>BUG_FIX</changeType>
<description>X or Q keys sometimes do not register in secondary dialogs</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>'Auto-Edit Mode' doesn't do what it's supposed to</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>If the internet connection is present, but unstable, the application starts but nothing can be clicked until the connection restores</description>
@@ -320,5 +324,181 @@
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>5</major>
<minor>6</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-08-11 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'Drill Hole' component that can be used for board mounting holes, chassis holes, faceplace holes, etc</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Simplify zoom in/out keyboard shortcuts to only plus/minus keys and zero numerical key for zoom reset to 100</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Bundle Java JRE with Linux AppImage to avoid having to install it</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Updated naming conventions Mac build packages to replace 'arm' with 'silicon' and 'x86' to 'intel' for clarity</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Allow Windows 64 bit installer to run on ARM-based Windows machine (through emulation)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>More intuitive user account creation dialog for DIYLC Cloud - showing exact requirements for password and marking optional entries</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved project search algorithm in DIYLC cloud</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>When a switch position is selected (i.e. set to a value other than None), DIYLC will treat the terminals that are internally connected in that switching logic as connected in analysis tools</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Increase maximum size of the backup folder from 64MB to 1GB for deeper history</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Alignment issues of some visual elements in the resulting Gerber files, e.g. DIL IC pin designators</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>'Connect to Patreon' option says that it connected account with username 'a b c' but doesn't do anything</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>5</major>
<minor>7</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-12-21 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added '6P5T (5-Position 6 Pole AxLabs)' lever switch type (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added '4P3T (3-Position 4 Pole AxLabs)' lever switch type (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'DP3T (Import 5-Position 2502N)' lever switch type (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Corrected 'S1 Switch' up/down position designators (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Corrected 'Mini Toggle Switch' position names for center-OFF variants to be in the correct order (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Drill Hole' changes color to red when selected</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>5</major>
<minor>8</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-12-30 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added Silkscreen export to PDF/PNG and print capabilities</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'Multiwatt IC' package with 11 and 15 pin varieties</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added new property in 'LP Switch' that allows having the middle terminals connected (as they were) or disconnected to allow more flexibility</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'Ending Style' property to 'Copper Trace' and 'Curved Trace' to allow choosing round or flat style</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added Chinese simplified (thank you chenjingyuanku) and Hindi UI translation and updated all other languages with new translations</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added configuration for text scale factor in rare cases when system scale factor is not applied correctly; requires adding a configuration property to config.xml with key 'fontScaleFactor' and a desired float value</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved behavior of mouse cursor during drag operations; this should also fix behavior on some linux machines that show a "forbidden" cursor while dragging</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved sensitivity of 'Multimeter Probe' that required pixel-level precision to detect connectivity; improved rendering of the probe in outline mode</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improve contrast of tooltips on some Linux machines that show white text against yellow balloon</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Honor label color settings for 'Tape Measure' component</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Reorganized export and print related items in the File menu to reduce clutter</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed underside rendering and mirroring position issues that affected only SPDT 'Mini Toggle Switch' types</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Application freezes when using underside rendering or mirroring on 'Pin Header' when sized at 1x1</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>'Rotary Switch (Open)' cannot be selected by clicking on the wafer for 4-pole variations</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>5</major>
<minor>9</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-12-30 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added balloon tooltip next to cursor showing component width, height and length (where available) while resizing; can be disabled from Config menu</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
</a>
</java.util.Arrays_-ArrayList>
@@ -3,6 +3,7 @@ package org.diylc.presenter;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.util.Arrays;
import java.util.List;
@@ -142,4 +143,116 @@ public class CalcUtilsTest {
point2.setLocation(10.0, 21.0);
assertFalse(CalcUtils.pointsMatch(point1, point2, 0.5));
}
@Test
public void testCalculateLuminance() {
// Test pure black
assertEquals(0.0, CalcUtils.calculateLuminance(Color.BLACK), 0.001);
// Test pure white
assertEquals(255.0, CalcUtils.calculateLuminance(Color.WHITE), 0.001);
// Test pure red
double redLuminance = CalcUtils.calculateLuminance(Color.RED);
assertEquals(76.245, redLuminance, 0.001); // 0.299 * 255
// Test pure green
double greenLuminance = CalcUtils.calculateLuminance(Color.GREEN);
assertEquals(149.685, greenLuminance, 0.001); // 0.587 * 255
// Test pure blue
double blueLuminance = CalcUtils.calculateLuminance(Color.BLUE);
assertEquals(29.07, blueLuminance, 0.001); // 0.114 * 255
// Test middle gray (128, 128, 128)
Color gray = new Color(128, 128, 128);
double grayLuminance = CalcUtils.calculateLuminance(gray);
assertEquals(128.0, grayLuminance, 0.001);
// Test null color
assertEquals(0.0, CalcUtils.calculateLuminance(null), 0.001);
// Test custom color
Color custom = new Color(100, 150, 200);
double customLuminance = CalcUtils.calculateLuminance(custom);
double expected = 0.299 * 100 + 0.587 * 150 + 0.114 * 200;
assertEquals(expected, customLuminance, 0.001);
}
@Test
public void testConvertToMonochrome() {
// Test pure black -> should stay black
Color result = CalcUtils.convertToMonochrome(Color.BLACK);
assertEquals(Color.BLACK, result);
// Test pure white -> should stay white
result = CalcUtils.convertToMonochrome(Color.WHITE);
assertEquals(Color.WHITE, result);
// Test dark color -> should become black
Color darkBlue = new Color(0, 0, 50);
result = CalcUtils.convertToMonochrome(darkBlue);
assertEquals(Color.BLACK, result);
// Test light color -> should become white
Color lightYellow = new Color(255, 255, 200);
result = CalcUtils.convertToMonochrome(lightYellow);
assertEquals(Color.WHITE, result);
// Test middle gray -> should become white (threshold is 128, gray is exactly 128)
Color gray = new Color(128, 128, 128);
result = CalcUtils.convertToMonochrome(gray);
assertEquals(Color.WHITE, result);
// Test color just below threshold -> should become black
Color darkGray = new Color(127, 127, 127);
result = CalcUtils.convertToMonochrome(darkGray);
assertEquals(Color.BLACK, result);
// Test color just above threshold -> should become white
Color lightGray = new Color(129, 129, 129);
result = CalcUtils.convertToMonochrome(lightGray);
assertEquals(Color.WHITE, result);
// Test with custom threshold
Color mediumColor = new Color(100, 100, 100);
result = CalcUtils.convertToMonochrome(mediumColor, 50.0);
assertEquals(Color.WHITE, result); // Luminance ~100, threshold 50, so white
result = CalcUtils.convertToMonochrome(mediumColor, 150.0);
assertEquals(Color.BLACK, result); // Luminance ~100, threshold 150, so black
// Test null color
assertNull(CalcUtils.convertToMonochrome(null));
}
@Test
public void testConvertToMonochromeWithAlpha() {
// Test color with alpha channel - alpha should be preserved
// Red (255, 0, 0) has luminance ~76, which is < 128, so it becomes BLACK
Color transparentRed = new Color(255, 0, 0, 128);
Color result = CalcUtils.convertToMonochrome(transparentRed);
assertEquals(Color.BLACK.getRed(), result.getRed());
assertEquals(Color.BLACK.getGreen(), result.getGreen());
assertEquals(Color.BLACK.getBlue(), result.getBlue());
assertEquals(128, result.getAlpha());
// Test light color with alpha - should become WHITE
Color transparentLight = new Color(255, 255, 200, 128);
result = CalcUtils.convertToMonochrome(transparentLight);
assertEquals(Color.WHITE.getRed(), result.getRed());
assertEquals(Color.WHITE.getGreen(), result.getGreen());
assertEquals(Color.WHITE.getBlue(), result.getBlue());
assertEquals(128, result.getAlpha());
// Test fully transparent color
Color transparent = new Color(100, 150, 200, 0);
result = CalcUtils.convertToMonochrome(transparent);
assertEquals(0, result.getAlpha());
// Test opaque color - alpha should remain 255
Color opaque = new Color(50, 50, 50);
result = CalcUtils.convertToMonochrome(opaque);
assertEquals(255, result.getAlpha());
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.4.0</version>
<version>5.8.0</version>
</parent>
<artifactId>diylc-library</artifactId>
@@ -43,12 +43,7 @@ import org.diylc.common.IPlugInPort;
import org.diylc.common.LineStyle;
import org.diylc.common.ObjectCache;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractLabeledComponent;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.PositiveNonZeroMeasureValidator;
import org.diylc.core.measures.Size;
@@ -63,7 +58,8 @@ import org.diylc.utils.Constants;
*
* @author Branislav Stojkovic
*/
public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponent<T> {
public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponent<T> implements
IHaveLength {
private static final long serialVersionUID = 1L;
@@ -434,7 +430,7 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
Stroke stroke = null;
switch (getStyle()) {
case SOLID:
stroke = ObjectCache.getInstance().fetchBasicStroke(thickness);
stroke = ObjectCache.getInstance().fetchStroke(thickness, getEndingStyle() == LineEndingStyle.Round ? BasicStroke.CAP_ROUND : BasicStroke.CAP_BUTT);
break;
case DASHED:
stroke =
@@ -641,6 +637,32 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
this.length = length;
}
@Override
public Size calculateLength() {
try {
Point2D[] points = getPoints();
if (points.length < 2) {
return null;
}
// Calculate distance between ending points
double distanceInPixels = points[0].distance(points[1]);
// Convert to Size with appropriate unit (mm or in based on configuration)
SizeUnit unit;
if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.METRIC_KEY, true)) {
unit = SizeUnit.mm;
} else {
unit = SizeUnit.in;
}
return Size.fromPixels(distanceInPixels, unit);
} catch (Exception e) {
return null;
}
}
@EditableProperty(name = "Width", defaultable = true, validatorClass = PositiveNonZeroMeasureValidator.class, sortOrder = 102)
public Size getWidth() {
return width;
@@ -747,6 +769,10 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
hideShortLeads = false;
return hideShortLeads;
}
protected LineEndingStyle getEndingStyle() {
return LineEndingStyle.Round;
}
public void setHideShortLeads(Boolean hideShortLeads) {
this.hideShortLeads = hideShortLeads;
@@ -0,0 +1,5 @@
package org.diylc.components;
public enum LineEndingStyle {
Round, Flat
}
@@ -41,7 +41,7 @@ import org.diylc.core.annotations.EditableProperty;
@ComponentDescriptor(name = "Blank Board", category = "Boards", author = "Branislav Stojkovic",
zOrder = IDIYComponent.BOARD, instanceNamePrefix = "Board", description = "Blank circuit board",
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false, transformer = SimpleComponentTransformer.class)
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, transformer = SimpleComponentTransformer.class)
public class BlankBoard extends AbstractBoard {
private static final long serialVersionUID = 1L;
@@ -69,18 +69,18 @@ public class BlankBoard extends AbstractBoard {
}
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
if (componentState != ComponentState.DRAGGING) {
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(boardColor);
if (getType() == Type.SQUARE)
g2d.fillRect((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY()));
else
g2d.fillOval((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY()));
g2d.setComposite(oldComposite);
// if (componentState != ComponentState.DRAGGING) {
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(boardColor);
if (getType() == Type.SQUARE)
g2d.fillRect((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY()));
else
g2d.fillOval((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(finalSecondPoint.getX() - firstPoint.getX()), (int)(finalSecondPoint.getY() - firstPoint.getY()));
g2d.setComposite(oldComposite);
// }
// Do not track any changes that follow because the whole board has been
// tracked so far.
drawingObserver.stopTracking();
@@ -50,7 +50,7 @@ import org.diylc.core.measures.SizeUnit;
@ComponentDescriptor(name = "Breadboard", category = "Boards", author = "Branislav Stojkovic",
description = "Prototyping solderless breadboard", instanceNamePrefix = "BB",
zOrder = IDIYComponent.BOARD, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false,
zOrder = IDIYComponent.BOARD, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME,
keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, enableCache = true, transformer = ProtoBoardTransformer.class)
public class Breadboard extends AbstractProtoBoard {
@@ -52,7 +52,7 @@ import org.diylc.core.measures.SizeUnit;
author = "Branislav Stojkovic",
description = "David Haillant's eurorack format stripboard for prototyping and building simple modules",
instanceNamePrefix = "BB", zOrder = IDIYComponent.BOARD,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME,
keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, enableCache = true,
transformer = ProtoBoardTransformer.class)
public class EurorackStripboard extends AbstractProtoBoard {
@@ -42,7 +42,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Eyelet Board", category = "Boards", author = "Branislav Stojkovic",
zOrder = IDIYComponent.BOARD, instanceNamePrefix = "Board", description = "Perforated board with eyelets",
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
transformer = AbstractBoardTransformer.class, enableCache = true)
public class EyeletBoard extends AbstractBoard {
@@ -44,7 +44,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Marshall Style Perf Board", category = "Boards", author = "Branislav Stojkovic",
zOrder = IDIYComponent.BOARD, instanceNamePrefix = "Board",
description = "Perforated board as found on some Marshall and Trainwreck amps",
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false, keywordPolicy = KeywordPolicy.SHOW_TAG,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TAG,
keywordTag = "Perf Board", transformer = AbstractBoardTransformer.class)
public class MarshallPerfBoard extends AbstractBoard {
@@ -44,7 +44,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Perf Board w/ Pads", category = "Boards", author = "Branislav Stojkovic",
zOrder = IDIYComponent.BOARD, instanceNamePrefix = "Board", description = "Perforated board with solder pads",
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false, keywordPolicy = KeywordPolicy.SHOW_TAG,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TAG,
keywordTag = "Perf Board", transformer = AbstractBoardTransformer.class, enableCache = true)
public class PerfBoard extends AbstractBoard implements ILayeredComponent {
@@ -47,7 +47,7 @@ import org.diylc.core.measures.SizeUnit;
@ComponentDescriptor(name = "P-0+o (Proto) Board", category = "Boards", author = "Branislav Stojkovic",
description = "A prototyping board for modular synths, central bus with V+ GND and V-", instanceNamePrefix = "BB",
zOrder = IDIYComponent.BOARD, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false,
zOrder = IDIYComponent.BOARD, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME,
keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, enableCache = true, transformer = ProtoBoardTransformer.class)
public class ProtoBoard extends AbstractProtoBoard {
@@ -46,7 +46,7 @@ import org.diylc.core.measures.SizeUnit;
author = "Branislav Stojkovic",
description = "Prototyping board similar to Radio Shack 276-168B or MPJA 33304",
instanceNamePrefix = "BB", zOrder = IDIYComponent.BOARD,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME,
keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, enableCache = true,
transformer = ProtoBoardTransformer.class)
public class ProtoBoard780 extends AbstractProtoBoard {
@@ -42,7 +42,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Vero Board", category = "Boards", author = "Branislav Stojkovic",
zOrder = IDIYComponent.BOARD, instanceNamePrefix = "Board",
description = "Perforated FR4 board with copper strips connecting all holes in a row",
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, autoEdit = false, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
transformer = VeroBoardTransformer.class, enableCache = true)
public class VeroBoard extends AbstractVeroBoard implements ILayeredComponent {
@@ -64,8 +64,15 @@ public class ChassisPanel extends AbstractShape {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(color);
g2d.fillRoundRect((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(secondPoint.getX() - firstPoint.getX()), (int)(secondPoint.getY() - firstPoint.getY()), radius,
radius);
if (radius == 0) {
g2d.fillRect((int) firstPoint.getX(), (int) firstPoint.getY(),
(int) (secondPoint.getX() - firstPoint.getX()),
(int) (secondPoint.getY() - firstPoint.getY()));
} else {
g2d.fillRoundRect((int) firstPoint.getX(), (int) firstPoint.getY(),
(int) (secondPoint.getX() - firstPoint.getX()),
(int) (secondPoint.getY() - firstPoint.getY()), radius, radius);
}
g2d.setComposite(oldComposite);
}
// Do not track any changes that follow because the whole rect has been
@@ -73,8 +80,15 @@ public class ChassisPanel extends AbstractShape {
drawingObserver.stopTracking();
g2d.setColor(componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
: borderColor);
g2d.drawRoundRect((int)(firstPoint.getX()), (int)(firstPoint.getY()), (int)(secondPoint.getX() - firstPoint.getX()), (int)(secondPoint.getY() - firstPoint.getY()), radius,
radius);
if (radius == 0) {
g2d.drawRect((int) (firstPoint.getX()), (int) (firstPoint.getY()),
(int) (secondPoint.getX() - firstPoint.getX()),
(int) (secondPoint.getY() - firstPoint.getY()));
} else {
g2d.drawRoundRect((int) (firstPoint.getX()), (int) (firstPoint.getY()),
(int) (secondPoint.getX() - firstPoint.getX()),
(int) (secondPoint.getY() - firstPoint.getY()), radius, radius);
}
}
@EditableProperty(name = "Radius")
@@ -28,6 +28,7 @@ import com.bancika.gerberwriter.GerberFunctions;
import org.diylc.common.ObjectCache;
import org.diylc.common.PCBLayer;
import org.diylc.components.AbstractLeadedComponent;
import org.diylc.components.LineEndingStyle;
import org.diylc.components.transform.SimpleComponentTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.CreationMethod;
@@ -47,7 +48,7 @@ import org.diylc.core.measures.SizeUnit;
@ComponentDescriptor(name = "Copper Trace", author = "Branislav Stojkovic", category = "Connectivity",
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "Trace",
description = "Straight copper trace", zOrder = IDIYComponent.TRACE, bomPolicy = BomPolicy.NEVER_SHOW,
autoEdit = false, keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "PCB",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "PCB",
transformer = SimpleComponentTransformer.class)
public class CopperTrace extends AbstractLeadedComponent<Void> implements ILayeredComponent, IGerberComponentSimple {
@@ -58,6 +59,7 @@ public class CopperTrace extends AbstractLeadedComponent<Void> implements ILayer
private Size thickness = THICKNESS;
private PCBLayer layer = PCBLayer._1;
private LineEndingStyle endingStyle = LineEndingStyle.Round;
public CopperTrace() {
super();
@@ -202,6 +204,19 @@ public class CopperTrace extends AbstractLeadedComponent<Void> implements ILayer
return false;
}
@EditableProperty(name = "Ending Style")
@Override
public LineEndingStyle getEndingStyle() {
if (endingStyle == null) {
endingStyle = LineEndingStyle.Round;
}
return endingStyle;
}
public void setEndingStyle(LineEndingStyle endingStyle) {
this.endingStyle = endingStyle;
}
@Override
public Set<GerberRenderMode> getGerberRenderModes() {
return EnumSet.of(GerberRenderMode.Normal);
@@ -17,8 +17,7 @@
*/
package org.diylc.components.connectivity;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.*;
import java.awt.geom.Path2D;
import java.util.EnumSet;
import java.util.Set;
@@ -29,6 +28,8 @@ import org.diylc.common.LineStyle;
import org.diylc.common.ObjectCache;
import org.diylc.common.PCBLayer;
import org.diylc.components.AbstractCurvedComponent;
import org.diylc.components.AbstractLeadedComponent;
import org.diylc.components.LineEndingStyle;
import org.diylc.components.transform.SimpleComponentTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
@@ -46,7 +47,7 @@ import org.diylc.core.measures.SizeUnit;
@ComponentDescriptor(name = "Curved Trace", author = "Branislav Stojkovic", category = "Connectivity",
instanceNamePrefix = "Trace", description = "Curved copper trace with two control points",
zOrder = IDIYComponent.TRACE, bomPolicy = BomPolicy.NEVER_SHOW, autoEdit = false,
zOrder = IDIYComponent.TRACE, bomPolicy = BomPolicy.NEVER_SHOW,
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "PCB", transformer = SimpleComponentTransformer.class,
enableCache = true)
public class CurvedTrace extends AbstractCurvedComponent<Void> implements ILayeredComponent, IGerberComponentSimple {
@@ -58,6 +59,7 @@ public class CurvedTrace extends AbstractCurvedComponent<Void> implements ILayer
protected Size size = SIZE;
private PCBLayer layer = PCBLayer._1;
private LineEndingStyle endingStyle = LineEndingStyle.Round;
@Override
protected Color getDefaultColor() {
@@ -67,7 +69,7 @@ public class CurvedTrace extends AbstractCurvedComponent<Void> implements ILayer
@Override
protected void drawCurve(Path2D curve, Graphics2D g2d, ComponentState componentState, IDrawingObserver drawingObserver) {
float thickness = (float) size.convertToPixels();
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(thickness));
g2d.setStroke(ObjectCache.getInstance().fetchStroke(thickness, getEndingStyle() == LineEndingStyle.Round ? BasicStroke.CAP_ROUND : BasicStroke.CAP_BUTT));
Color curveColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
: color;
@@ -97,6 +99,18 @@ public class CurvedTrace extends AbstractCurvedComponent<Void> implements ILayer
public void setLayer(PCBLayer layer) {
this.layer = layer;
}
@EditableProperty(name = "Ending Style")
public LineEndingStyle getEndingStyle() {
if (endingStyle == null) {
endingStyle = LineEndingStyle.Round;
}
return endingStyle;
}
public void setEndingStyle(LineEndingStyle endingStyle) {
this.endingStyle = endingStyle;
}
@Override
public int getLayerId() {
@@ -40,7 +40,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Hookup Wire", author = "Branislav Stojkovic", category = "Connectivity",
instanceNamePrefix = "W", description = "Flexible wire with two control points", zOrder = IDIYComponent.WIRING,
flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW, autoEdit = false,
flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW,
transformer = SimpleComponentTransformer.class, enableCache = false)
public class HookupWire extends AbstractCurvedComponent<AWG> implements IContinuity {
@@ -28,21 +28,20 @@ import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation45;
import org.diylc.components.AbstractTransparentComponent;
import org.diylc.components.transform.SimpleComponentTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Multimeter Probe", category = "Connectivity",
author = "Branislav Stojkovic", description = "Multimeter Probe", instanceNamePrefix = "Probe",
@@ -59,6 +58,7 @@ public class MultimeterProbe extends AbstractTransparentComponent<Color> {
public static Size HANDLE_DIAMETER = new Size(0.2d, SizeUnit.in);
public static Size HANDLE_LENGTH = new Size(0.4d, SizeUnit.in);
public static Color HANDLE_COLOR = Color.red;
public static Size SENSITIVITY = new Size(0.05d, SizeUnit.in);
private Point2D.Double point = new Point2D.Double(0, 0);
private Color handleColor = HANDLE_COLOR;
@@ -77,27 +77,49 @@ public class MultimeterProbe extends AbstractTransparentComponent<Color> {
Shape[] shapes = getBody();
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
if (!outlineMode) {
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(PROBE_COLOR);
g2d.fill(shapes[0]);
g2d.setColor(handleColor);
g2d.fill(shapes[1]);
g2d.setComposite(oldComposite);
// draw an invisible conductive circle around the point to increase sensitivity
g2d.setColor(Constants.TRANSPARENT_COLOR);
drawingObserver.startTrackingContinuityArea(true);
double sensitivity = SENSITIVITY.convertToPixels();
g2d.fillOval((int) (point.getX() - sensitivity), (int) (point.getY() - sensitivity),
(int) (sensitivity * 2), (int) (sensitivity * 2));
drawingObserver.stopTrackingContinuityArea();
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: handleColor.darker());
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : PROBE_COLOR);
g2d.fill(shapes[0]);
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : handleColor);
g2d.fill(shapes[1]);
g2d.setComposite(oldComposite);
drawingObserver.stopTracking();
Theme theme = (Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY,
Constants.DEFAULT_THEME);
Color handleBorderColor;
if (outlineMode) {
handleBorderColor = theme.getOutlineColor();
} else {
handleBorderColor = componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: handleColor.darker();
}
g2d.setColor(handleBorderColor);
g2d.draw(shapes[1]);
g2d.setColor(
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: PROBE_COLOR.darker());
Color probeBorderColor;
if (outlineMode) {
probeBorderColor = theme.getOutlineColor();
} else {
probeBorderColor = componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: PROBE_COLOR.darker();
}
g2d.setColor(probeBorderColor);
g2d.draw(shapes[0]);
}
@@ -50,7 +50,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "PCB Terminal Block", category = "Connectivity",
author = "Branislav Stojkovic", description = "Horizontal PCB terminal block with 5mm pitch",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "TB", autoEdit = false,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "TB",
enableCache = true, transformer = PCBTerminalBlockTransformer.class)
public class PCBTerminalBlock extends AbstractTransparentComponent<PCBTerminalBlockCount> implements IGerberComponentSimple {
@@ -52,7 +52,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Solder Pad", category = "Connectivity", author = "Branislav Stojkovic",
description = "Copper solder pad, round or square", instanceNamePrefix = "Pad",
zOrder = IDIYComponent.TRACE + 0.1, bomPolicy = BomPolicy.NEVER_SHOW, autoEdit = false,
zOrder = IDIYComponent.TRACE + 0.1, bomPolicy = BomPolicy.NEVER_SHOW,
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "PCB",
transformer = SimpleComponentTransformer.class, enableCache = true)
public class SolderPad extends AbstractTransparentComponent<Void>
@@ -49,7 +49,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Twisted Leads", author = "Branislav Stojkovic",
category = "Connectivity", instanceNamePrefix = "W",
description = "A pair of flexible leads twisted tighly together", zOrder = IDIYComponent.WIRING,
flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW, autoEdit = false,
flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW,
transformer = SimpleComponentTransformer.class, enableCache = true)
public class TwistedWire extends AbstractCurvedComponent<Void> implements IContinuity {
@@ -53,7 +53,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "9V Battery Snap", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "", zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "BTR",
autoEdit = false, transformer = BatterySnapTransformer.class)
transformer = BatterySnapTransformer.class)
public class BatterySnap9V extends AbstractTransparentComponent<String> {
private static final long serialVersionUID = 1L;
@@ -58,7 +58,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Cliff 1/4\" Jack", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "Cliff-style closed panel mount 1/4\" phono jack",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", autoEdit = false, transformer = CliffJackTransformer.class,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", transformer = CliffJackTransformer.class,
enableCache = true)
public class CliffJack1_4 extends AbstractMultiPartComponent<JackType> implements IGerberComponentSimple {
@@ -54,7 +54,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Cliff 1/8\" Jack", category = "Electro-Mechanical",
author = "Branislav Stojkovic", description = "Cliff-style closed 1/8\" phono jack",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", autoEdit = false,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J",
transformer = CliffJackTransformer.class, enableCache = true)
public class CliffJack1_8 extends AbstractMultiPartComponent<JackType> implements IGerberComponentSimple {
@@ -59,7 +59,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Closed 1/4\" Jack", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "Enclosed panel mount 1/4\" phono jack", zOrder = IDIYComponent.COMPONENT,
instanceNamePrefix = "J", autoEdit = false, transformer = ClosedJackTransformer.class, enableCache = true)
instanceNamePrefix = "J", transformer = ClosedJackTransformer.class, enableCache = true)
public class ClosedJack1_4 extends AbstractMultiPartComponent<JackType> implements IGerberComponentSimple {
private static final long serialVersionUID = 1L;
@@ -0,0 +1,229 @@
/*
*
* 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.components.electromechanical;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import com.bancika.gerberwriter.GerberFunctions;
import org.diylc.components.AbstractTransparentComponent;
import org.diylc.components.transform.SimpleComponentTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.annotations.PositiveNonZeroMeasureValidator;
import org.diylc.core.gerber.GerberLayer;
import org.diylc.core.gerber.IGerberComponentCustom;
import org.diylc.core.gerber.IGerberDrawingObserver;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
@ComponentDescriptor(name = "Drill Hole", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "Drill hole marker for PCB, chassis or faceplate", instanceNamePrefix = "Hole",
zOrder = IDIYComponent.BOARD + 0.1, bomPolicy = BomPolicy.NEVER_SHOW,
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "PCB",
transformer = SimpleComponentTransformer.class, enableCache = true)
public class DrillHole extends AbstractTransparentComponent<Void>
implements IGerberComponentCustom {
private static final long serialVersionUID = 1L;
public static Size DIAMETER = new Size(3.0d, SizeUnit.mm);
public static Color COLOR = Color.black;
public static Size STROKE_WIDTH = new Size(1.0d, SizeUnit.px);
public static Size CROSSHAIR_WIDTH = new Size(1.0d, SizeUnit.px);
private Size diameter = DIAMETER;
private Color color = COLOR;
private Point2D.Double point = new Point2D.Double(0, 0);
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver,
IGerberDrawingObserver gerberDrawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
double holeDiameter = getDiameter().convertToPixels();
double borderWidth = STROKE_WIDTH.convertToPixels(); // Use constant stroke width
// Export to Gerber drill layer
if (gerberDrawingObserver != null) {
GerberLayer drillLayer = GerberLayer.DrillNonPlated;
gerberDrawingObserver.startGerberOutput(drillLayer, GerberFunctions.COMPONENT_DRILL, false);
}
Composite oldComposite = g2d.getComposite();
if (getAlpha() < MAX_ALPHA) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * getAlpha() / MAX_ALPHA));
}
// Draw white circle (hole)
g2d.setColor(Color.white);
g2d.fill(new Ellipse2D.Double(point.getX() - holeDiameter / 2, point.getY() - holeDiameter / 2,
holeDiameter, holeDiameter));
// Draw colored border
g2d.setColor(
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: color);
g2d.setStroke(new BasicStroke((float) borderWidth));
g2d.draw(new Ellipse2D.Double(point.getX() - holeDiameter / 2, point.getY() - holeDiameter / 2,
holeDiameter, holeDiameter));
// Draw crosshairs
double crosshairLength = holeDiameter * 0.4; // 40% of diameter for crosshair length (increased from 30%)
double crosshairWidth = CROSSHAIR_WIDTH.convertToPixels(); // Use constant crosshair width
g2d.setStroke(new BasicStroke((float) crosshairWidth));
// Horizontal line
g2d.draw(new Line2D.Double(
point.getX() - crosshairLength / 2, point.getY(),
point.getX() + crosshairLength / 2, point.getY()));
// Vertical line
g2d.draw(new Line2D.Double(
point.getX(), point.getY() - crosshairLength / 2,
point.getX(), point.getY() + crosshairLength / 2));
drawingObserver.stopTracking();
if (gerberDrawingObserver != null) {
gerberDrawingObserver.stopGerberOutput();
}
g2d.setComposite(oldComposite);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
this.draw(g2d, componentState, outlineMode, project, drawingObserver, null);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
int diameter = getClosestOdd(width * 3 / 4); // Increased from width/2 to 3/4 of width
int borderWidth = Math.max(1, (int) STROKE_WIDTH.convertToPixels()); // Use constant stroke width
int crosshairLength = diameter * 2 / 5; // 40% of diameter (increased from 1/3)
int crosshairWidth = Math.max(1, (int) CROSSHAIR_WIDTH.convertToPixels()); // Use constant crosshair width
// Draw white circle
g2d.setColor(Color.white);
g2d.fillOval((width - diameter) / 2, (height - diameter) / 2, diameter, diameter);
// Draw colored border
g2d.setColor(COLOR);
g2d.setStroke(new BasicStroke(borderWidth));
g2d.drawOval((width - diameter) / 2, (height - diameter) / 2, diameter, diameter);
// Draw crosshairs
g2d.setStroke(new BasicStroke(crosshairWidth));
// Horizontal line
g2d.drawLine(
(width - crosshairLength) / 2, height / 2,
(width + crosshairLength) / 2, height / 2);
// Vertical line
g2d.drawLine(
width / 2, (height - crosshairLength) / 2,
width / 2, (height + crosshairLength) / 2);
}
@EditableProperty(validatorClass = PositiveNonZeroMeasureValidator.class)
public Size getDiameter() {
return diameter;
}
public void setDiameter(Size diameter) {
this.diameter = diameter;
}
@EditableProperty(name = "Color")
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
@Override
public String getName() {
return super.getName();
}
@Override
public int getControlPointCount() {
return 1;
}
@Override
public boolean isControlPointSticky(int index) {
return true;
}
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.NEVER;
}
@Override
public Point2D getControlPoint(int index) {
return point;
}
@Override
public void setControlPoint(Point2D point, int index) {
this.point.setLocation(point);
}
@Override
public Void getValue() {
return null;
}
@Override
public String getControlPointNodeName(int index) {
return null;
}
@Override
public void setValue(Void value) {}
@Override
public Rectangle2D getCachingBounds() {
double diameter = getDiameter().convertToPixels();
return new Rectangle2D.Double(point.getX() - diameter, point.getY() - diameter,
diameter * 2, diameter * 2);
}
}
@@ -53,7 +53,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Fuse Holder (Panel)", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "Panel mounted fuse holder", zOrder = IDIYComponent.COMPONENT,
instanceNamePrefix = "FH", autoEdit = false, enableCache = true)
instanceNamePrefix = "FH", enableCache = true)
public class FuseHolderPanel extends AbstractMultiPartComponent<String> {
private static final long serialVersionUID = 1L;
@@ -53,7 +53,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "IEC Socket", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "Panel mounted IEC power socket", zOrder = IDIYComponent.COMPONENT,
instanceNamePrefix = "IEC", autoEdit = false, enableCache = true)
instanceNamePrefix = "IEC", enableCache = true)
public class IECSocket extends AbstractMultiPartComponent<String> {
private static final long serialVersionUID = 1L;
@@ -59,7 +59,7 @@ import org.diylc.utils.SwitchUtils;
@ComponentDescriptor(name = "Mini Toggle Switch", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "Panel mounted mini toggle switch", zOrder = IDIYComponent.COMPONENT,
instanceNamePrefix = "SW", autoEdit = false, enableCache = true, transformer = MiniToggleSwitchTransformer.class)
instanceNamePrefix = "SW", enableCache = true, transformer = MiniToggleSwitchTransformer.class)
public class MiniToggleSwitch extends AbstractTransparentComponent<ToggleSwitchType> implements ISwitch, ILayeredComponent, IGerberComponentSimple {
private static final long serialVersionUID = 1L;
@@ -502,10 +502,17 @@ public class MiniToggleSwitch extends AbstractTransparentComponent<ToggleSwitchT
}
@Override
public String getPositionName(int position) {
if (switchType.name().endsWith("_off") && position == 2)
return "OFF";
return "ON" + Integer.toString(position + 1);
public String getPositionName(int position) {
int pos = position;
if (switchType.name().endsWith("_off")) {
int midPosition = (getPositionCount() - 1) / 2;
if (position == midPosition) {
return "OFF";
} else if (position > midPosition) {
--pos;
}
}
return "ON" + Integer.toString(pos + 1);
}
@DynamicEditableProperty(source = ToggleSwitchPositionPropertyValueSource.class)
@@ -56,7 +56,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Neutrik 1/4\" Jack", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "1/4\" mono/stereo phono jack based on Neutrik NMJx series, PCB or Panel-mount",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", autoEdit = false, transformer = NeutrikJack1_4Transformer.class,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", transformer = NeutrikJack1_4Transformer.class,
enableCache = true)
public class NeutrikJack1_4 extends AbstractMultiPartComponent<JackType> {
@@ -374,13 +374,13 @@ public class RotarySwitchOpen extends AbstractAngledComponent<RotarySwitchOpenTy
// Draw body
if (componentState != ComponentState.DRAGGING) {
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : getColor());
drawingObserver.startTracking();
g2d.fill(body[0]);
drawingObserver.stopTracking();
}
g2d.setColor(finalBorderColor);
drawingObserver.startTracking();
g2d.draw(body[0]);
drawingObserver.stopTracking();
// Draw pins
for (int i = 1; i <= secondLevelStart; i++) {
@@ -59,7 +59,7 @@ import org.diylc.utils.SwitchUtils;
@ComponentDescriptor(name = "Slide Switch", category = "Electro-Mechanical",
author = "Branislav Stojkovic", description = "Panel mounted slide switch",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "SW", autoEdit = false,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "SW",
enableCache = true, transformer = MiniToggleSwitchTransformer.class)
public class SlideSwitch extends AbstractTransparentComponent<SlideSwitchType> implements ISwitch, IGerberComponentSimple {
@@ -51,7 +51,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Humbucker Pickup", category = "Guitar", author = "Branislav Stojkovic",
description = "Double-coil humbucker guitar pickup (PAF, Mini Humbuckers, Filtertrons)",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP", autoEdit = false,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram", enableCache = true,
transformer = GuitarPickupTransformer.class)
public class HumbuckerPickup extends AbstractGuitarPickup {
@@ -52,7 +52,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Jazz Bass Pickup", category = "Guitar", author = "Branislav Stojkovic",
description = "Single coil pickup for Jazz Bass and similar guitars",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP", autoEdit = false,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram", enableCache = true,
transformer = GuitarPickupTransformer.class)
public class JazzBassPickup extends AbstractSingleOrHumbuckerPickup {
@@ -63,15 +63,18 @@ public class LPSwitch extends AbstractTransparentComponent<String> implements IS
private static Size LENGTH = new Size(1.3d, SizeUnit.in);
private static Size BASE_LENGTH = new Size(18d, SizeUnit.mm);
private static Size WAFER_THICKNESS = new Size(0.05d, SizeUnit.in);
private static Size TERMINAL_SPACING = new Size(0.2d, SizeUnit.in);
private static Size TERMINAL_SPACING_CONNECTED = new Size(0.2d, SizeUnit.in);
private static Size TERMINAL_SPACING_DISCONNECTED = new Size(0.1d, SizeUnit.in);
private String value = "";
private Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0), new Point2D.Double(0, 0), new Point2D.Double(0, 0), new Point2D.Double(0, 0)};
transient Shape[] body;
private Orientation orientation = Orientation.DEFAULT;
private MiddleTerminalConfiguration middleTerminalConfiguration;
public LPSwitch() {
super();
middleTerminalConfiguration = MiddleTerminalConfiguration.Connected;
updateControlPoints();
}
@@ -127,7 +130,7 @@ public class LPSwitch extends AbstractTransparentComponent<String> implements IS
g2d.setColor(finalBorderColor);
g2d.draw(body[1]);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(2));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(3));
g2d.setColor(METAL_COLOR);
drawingObserver.startTracking();
g2d.draw(body[2]);
@@ -178,24 +181,44 @@ public class LPSwitch extends AbstractTransparentComponent<String> implements IS
}
}
int terminalSpacing = (int) TERMINAL_SPACING.convertToPixels();
GeneralPath terminalPath = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
terminalPath.moveTo(x - waferThickness * 3 / 2, bodyY + 1);
terminalPath.lineTo(x - waferThickness * 3 / 2, bodyY + baseLength);
terminalPath.lineTo(x, y + length);
terminalPath.moveTo(x + waferThickness * 3 / 2, bodyY + 1);
terminalPath.lineTo(x + waferThickness * 3 / 2, bodyY + baseLength);
terminalPath.lineTo(x, y + length);
if (getMiddleTerminalConfiguration() == MiddleTerminalConfiguration.Connected) {
int terminalSpacing = (int) TERMINAL_SPACING_CONNECTED.convertToPixels();
terminalPath.moveTo(x - waferThickness * 3 / 2, bodyY + 1);
terminalPath.lineTo(x - waferThickness * 3 / 2, bodyY + baseLength);
terminalPath.lineTo(x, y + length);
terminalPath.moveTo(x - waferThickness * 5 / 2, bodyY + 1);
terminalPath.lineTo(x - waferThickness * 5 / 2, bodyY + baseLength);
terminalPath.lineTo(x - terminalSpacing, y + length);
terminalPath.moveTo(x + waferThickness * 3 / 2, bodyY + 1);
terminalPath.lineTo(x + waferThickness * 3 / 2, bodyY + baseLength);
terminalPath.lineTo(x, y + length);
terminalPath.moveTo(x + waferThickness * 5 / 2, bodyY + 1);
terminalPath.lineTo(x + waferThickness * 5 / 2, bodyY + baseLength);
terminalPath.lineTo(x + terminalSpacing, y + length);
terminalPath.moveTo(x - waferThickness * 5 / 2, bodyY + 1);
terminalPath.lineTo(x - waferThickness * 5 / 2, bodyY + baseLength);
terminalPath.lineTo(x - terminalSpacing, y + length);
terminalPath.moveTo(x + waferThickness * 5 / 2, bodyY + 1);
terminalPath.lineTo(x + waferThickness * 5 / 2, bodyY + baseLength);
terminalPath.lineTo(x + terminalSpacing, y + length);
} else {
int terminalSpacing = (int) TERMINAL_SPACING_DISCONNECTED.convertToPixels();
terminalPath.moveTo(x - waferThickness * 3 / 2, bodyY + 1);
terminalPath.lineTo(x - waferThickness * 3 / 2, bodyY + baseLength);
terminalPath.lineTo(x- terminalSpacing, y + length);
terminalPath.moveTo(x + waferThickness * 3 / 2, bodyY + 1);
terminalPath.lineTo(x + waferThickness * 3 / 2, bodyY + baseLength);
terminalPath.lineTo(x + terminalSpacing, y + length);
terminalPath.moveTo(x - waferThickness * 5 / 2, bodyY + 1);
terminalPath.lineTo(x - waferThickness * 5 / 2, bodyY + baseLength);
terminalPath.lineTo(x - terminalSpacing * 2, y + length);
terminalPath.moveTo(x + waferThickness * 5 / 2, bodyY + 1);
terminalPath.lineTo(x + waferThickness * 5 / 2, bodyY + baseLength);
terminalPath.lineTo(x + terminalSpacing * 2, y + length);
}
body[2] = terminalPath;
// Rotate if needed
@@ -219,14 +242,37 @@ public class LPSwitch extends AbstractTransparentComponent<String> implements IS
@SuppressWarnings("incomplete-switch")
private void updateControlPoints() {
Point2D[] newControlPoints = null;
if (getMiddleTerminalConfiguration() == MiddleTerminalConfiguration.Connected && controlPoints.length == 5) {
newControlPoints = new Point2D[4];
} else if (getMiddleTerminalConfiguration() == MiddleTerminalConfiguration.Disconnected && controlPoints.length == 4) {
newControlPoints = new Point2D[5];
newControlPoints[4] = new Point2D.Double();
}
if (newControlPoints != null) {
for (int i = 0; i < newControlPoints.length && i < controlPoints.length; i++) {
newControlPoints[i] = controlPoints[i];
}
controlPoints = newControlPoints;
}
double x = controlPoints[0].getX();
double y = controlPoints[0].getY();
int terminalSpacing = (int) TERMINAL_SPACING.convertToPixels();
int length = (int) LENGTH.convertToPixels();
controlPoints[1].setLocation(x - terminalSpacing, y + length);
controlPoints[2].setLocation(x, y + length);
controlPoints[3].setLocation(x + terminalSpacing, y + length);
if (getMiddleTerminalConfiguration() == MiddleTerminalConfiguration.Connected) {
int terminalSpacing = (int) TERMINAL_SPACING_CONNECTED.convertToPixels();
controlPoints[1].setLocation(x - terminalSpacing, y + length);
controlPoints[2].setLocation(x, y + length);
controlPoints[3].setLocation(x + terminalSpacing, y + length);
} else {
int terminalSpacing = (int) TERMINAL_SPACING_DISCONNECTED.convertToPixels();
controlPoints[1].setLocation(x - terminalSpacing * 2, y + length);
controlPoints[2].setLocation(x - terminalSpacing, y + length);
controlPoints[3].setLocation(x + terminalSpacing, y + length);
controlPoints[4].setLocation(x + terminalSpacing * 2, y + length);
}
// Rotate if needed
if (orientation != Orientation.DEFAULT) {
@@ -331,8 +377,24 @@ public class LPSwitch extends AbstractTransparentComponent<String> implements IS
// Invalidate the body
body = null;
}
// @Override
@EditableProperty(name = "Middle Terminals")
public MiddleTerminalConfiguration getMiddleTerminalConfiguration() {
if (middleTerminalConfiguration == null) {
middleTerminalConfiguration = MiddleTerminalConfiguration.Connected;
}
return middleTerminalConfiguration;
}
public void setMiddleTerminalConfiguration(
MiddleTerminalConfiguration middleTerminalConfiguration) {
this.middleTerminalConfiguration = middleTerminalConfiguration;
updateControlPoints();
// Invalidate the body
body = null;
}
// @Override
// public String getControlPointNodeName(int index) {
// // we don't want the switch to produce any nodes, it just makes connections
// return null;
@@ -347,27 +409,34 @@ public class LPSwitch extends AbstractTransparentComponent<String> implements IS
@Override
public String getPositionName(int position) {
switch (position) {
case 0:
return "Treble";
case 1:
return "Middle";
case 2:
return "Rhythm";
}
return null;
return switch (position) {
case 0 -> "Treble";
case 1 -> "Middle";
case 2 -> "Rhythm";
default -> null;
};
}
@Override
public boolean arePointsConnected(int index1, int index2, int position) {
switch (position) {
case 0:
return index1 == 1 && index2 == 2;
case 1:
return index1 > 0 && index2 > 0 && index1 < index2;
case 2:
return index1 == 2 && index2 == 3;
if (getMiddleTerminalConfiguration() == MiddleTerminalConfiguration.Connected) {
return switch (position) {
case 0 -> index1 == 1 && index2 == 2;
case 1 -> index1 > 0 && index2 > 0 && index1 < index2;
case 2 -> index1 == 2 && index2 == 3;
default -> false;
};
} else {
return switch (position) {
case 0 -> index1 == 1 && index2 == 2;
case 1 -> (index1 == 1 && index2 == 2) || (index1 == 3 && index2 == 4);
case 2 -> index1 == 3 && index2 == 4;
default -> false;
};
}
return false;
}
public enum MiddleTerminalConfiguration {
Connected, Disconnected
}
}
@@ -151,8 +151,13 @@ public class LeverSwitch extends AbstractAngledComponent<LeverSwitch.LeverSwitch
g2d.draw(body[2]);
if (getShowMarkers()) {
String[] markers =
SwitchUtils.getSwitchingMarkers(this, getControlPointCount(), false);
String[] markers;
if(type == LeverSwitchType.DP3T_5pos_2502N) {
markers = new String[]{"1", "2", "3", "4", "5", "6", "7", "8"};
} else {
markers = SwitchUtils.getSwitchingMarkers(this, getControlPointCount(), false);
}
double theta = getAngle().getValueRad();
g2d.setColor(MARKER_COLOR);
@@ -203,11 +208,13 @@ public class LeverSwitch extends AbstractAngledComponent<LeverSwitch.LeverSwitch
|| type == LeverSwitchType.DP3T_5pos) {
x += terminalLength;
yOffset = 7;
} else if (type == LeverSwitchType.DP3T_5pos_Import) {
} else if (type == LeverSwitchType.DP3T_5pos_Import || type == LeverSwitchType.DP3T_5pos_2502N) {
yOffset = 10;
} else if (type == LeverSwitchType._6_WAY_OG) {
x += terminalLength;
yOffset = 8;
} else if (type == LeverSwitchType._4P3T) {
yOffset = 8;
} else {
yOffset = 12;
}
@@ -226,11 +233,21 @@ public class LeverSwitch extends AbstractAngledComponent<LeverSwitch.LeverSwitch
- (waferLength - terminalSpacing * yOffset) / 2, waferThickness,
waferLength));
if (type == LeverSwitchType._4P5T) {
if (type == LeverSwitchType._4P5T || type == LeverSwitchType._6P5T) {
waferArea.add(new Area(new Rectangle2D.Double(x - terminalLength / 2 - waferThickness / 2 + waferSpacing, y
- (waferLength - terminalSpacing * 12) / 2, waferThickness,
waferLength)));
}
if (type == LeverSwitchType._6P5T) {
waferArea.add(new Area(new Rectangle2D.Double(x - terminalLength / 2 - waferThickness / 2 + waferSpacing * 2, y
- (waferLength - terminalSpacing * 12) / 2, waferThickness,
waferLength)));
}
if (type == LeverSwitchType._4P3T) {
waferArea.add(new Area(new Rectangle2D.Double(x - terminalLength / 2 - waferThickness / 2 + waferSpacing, y
- (waferLength - terminalSpacing * 8) / 2, waferThickness,
waferLength)));
}
double theta = getAngle().getValueRad();
@@ -294,6 +311,7 @@ public class LeverSwitch extends AbstractAngledComponent<LeverSwitch.LeverSwitch
}
break;
case DP3T_5pos_Import:
case DP3T_5pos_2502N:
controlPoints = new Point2D[8];
terminalSpacing *= 1.5;
for (int i = 0; i < controlPoints.length; i++) {
@@ -319,6 +337,21 @@ public class LeverSwitch extends AbstractAngledComponent<LeverSwitch.LeverSwitch
controlPoints[i + 12] = new Point2D.Double(x + waferSpacing, y + i * terminalSpacing + (i >= 6 ? terminalSpacing : 0));
}
break;
case _6P5T:
controlPoints = new Point2D[36];
for (int i = 0; i < controlPoints.length / 3; i++) {
controlPoints[i] = new Point2D.Double(x, y + i * terminalSpacing + (i >= 6 ? terminalSpacing : 0));
controlPoints[i + 12] = new Point2D.Double(x + waferSpacing, y + i * terminalSpacing + (i >= 6 ? terminalSpacing : 0));
controlPoints[i + 24] = new Point2D.Double(x + waferSpacing * 2, y + i * terminalSpacing + (i >= 6 ? terminalSpacing : 0));
}
break;
case _4P3T:
controlPoints = new Point2D[16];
for (int i = 0; i < controlPoints.length / 2; i++) {
controlPoints[i] = new Point2D.Double(x, y + i * terminalSpacing + (i >= 4 ? terminalSpacing : 0));
controlPoints[i + 8] = new Point2D.Double(x + waferSpacing, y + i * terminalSpacing + (i >= 4 ? terminalSpacing : 0));
}
break;
}
// Rotate if needed
@@ -447,7 +480,16 @@ public class LeverSwitch extends AbstractAngledComponent<LeverSwitch.LeverSwitch
|| (index2 == 11 && index2 - index1 == 5 - position);
case _4P5T:
return ((index1 == 0 || index1 == 12) && index2 - index1 == position + 1)
|| ((index2 == 11 || index2 == 23) && index2 - index1 == 5 - position);
|| ((index2 == 11 || index2 == 23) && index2 - index1 == 5 - position);
case _6P5T:
return ((index1 == 0 || index1 == 12 || index1 == 24) && index2 - index1 == position + 1)
|| ((index2 == 11 || index2 == 23 || index2 == 35) && index2 - index1 == 5 - position);
case _4P3T:
return ((index1 == 0 || index1 == 8) && index2 - index1 == position + 1)
|| ((index2 == 7 || index2 == 15) && index2 - index1 == 3 - position);
case DP3T_5pos_2502N:
positionConnections = DP3T_5pos_2502N_CONNECTIONS.get(position);
break;
}
if (positionConnections != null) {
@@ -526,6 +568,32 @@ public class LeverSwitch extends AbstractAngledComponent<LeverSwitch.LeverSwitch
) // position 6
);
private static final List<List<int[]>> DP3T_5pos_2502N_CONNECTIONS = Arrays.asList(
Arrays.asList(
new int[] { 0, 3 }
), // position 1
Arrays.asList(
new int[] { 0, 3 },
new int[] { 1, 3 },
new int[] { 4, 7 },
new int[] { 5, 7 }
), // position 2
Arrays.asList(
new int[] { 0, 3 },
new int[] { 2, 3 },
new int[] { 6, 7 }
), // position 3
Arrays.asList(
new int[] { 1, 3 },
new int[] { 2, 3 },
new int[] { 5, 7 }
), // position 4
Arrays.asList(
new int[] { 2, 3 },
new int[] { 6, 7 }
) // position 5
);
@DynamicEditableProperty(source = LeverSwitchPositionPropertyValueSource.class)
@EditableProperty(name = "Selected Position")
@Override
@@ -559,7 +627,10 @@ public class LeverSwitch extends AbstractAngledComponent<LeverSwitch.LeverSwitch
_4P5T("4P5T (Super/Mega)", 5),
DP4T("DP4T (4-Position Tele)", 4),
_6_WAY_OG("DP4T (6-Position Oak Grigsby)", 6),
DP5T("DP5T", 5);
DP5T("DP5T", 5),
_6P5T("6P5T (5-Position 6 Pole AxLabs)", 5),
_4P3T("4P3T (3-Position 4 Pole AxLabs)", 3),
DP3T_5pos_2502N("DP3T (Import 5-Position 2502N)", 5);
private final int positionCount;
private String title;
@@ -53,7 +53,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "P-90 Single Coil Pickup", category = "Guitar", author = "Branislav Stojkovic",
description = "Single coil P-90 guitar pickup, both \"dog ear\" and \"soap bar\"",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP", autoEdit = false,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram", enableCache = true)
public class P90Pickup extends AbstractSingleOrHumbuckerPickup {
@@ -52,7 +52,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "P- Bass Pickup", category = "Guitar", author = "Branislav Stojkovic",
description = "Split-coil pickup for P-Bass and similar guitars",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP", autoEdit = false,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram", enableCache = true,
transformer = GuitarPickupTransformer.class)
public class PBassPickup extends AbstractSingleOrHumbuckerPickup {
@@ -53,7 +53,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "S1 Switch", category = "Guitar", author = "Branislav Stojkovic",
description = "Fender S1 4 pole pushbutton switch", zOrder = IDIYComponent.COMPONENT,
instanceNamePrefix = "SW", autoEdit = false, enableCache = true)
instanceNamePrefix = "SW", enableCache = true)
public class S1Switch extends AbstractTransparentComponent<Void> implements ISwitch {
private static final long serialVersionUID = 1L;
@@ -208,9 +208,9 @@ public class S1Switch extends AbstractTransparentComponent<Void> implements ISwi
double dy = 0;
if (i % 3 == 1) {
dy = -1;
} else if (i % 3 == 0) {
dy = 1;
} else if (i % 3 == 0) {
dy = -1;
}
Stroke stroke = g2d.getStroke();
@@ -280,9 +280,9 @@ public class S1Switch extends AbstractTransparentComponent<Void> implements ISwi
@Override
public String getPositionName(int position) {
if (position == 0) {
return "Up";
return "Down";
}
return "Down";
return "Up";
}
@EditableProperty(name = "Markers")
@@ -54,7 +54,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Single Coil Pickup", category = "Guitar", author = "Branislav Stojkovic",
description = "Single coil guitar pickup, both Strat and Tele style",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP", autoEdit = false,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram", enableCache = true,
transformer = GuitarPickupTransformer.class)
public class SingleCoilPickup extends AbstractSingleOrHumbuckerPickup {
@@ -40,7 +40,7 @@ import java.awt.geom.*;
@ComponentDescriptor(name = "Stingray Bass Pickup", category = "Guitar", author = "JD",
description = "Humbucker Bass Pickup for Musicman Stingray and similar guitars",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP", autoEdit = false,
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "PKP",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram",
enableCache = true, transformer = GuitarPickupTransformer.class)
@@ -0,0 +1,693 @@
/*
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.components.semiconductors;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.Display;
import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractTransparentComponent;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.gerber.IGerberComponentSimple;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Multiwatt IC", author = "Branislav Stojkovic", category = "Semiconductors",
instanceNamePrefix = "IC", description = "Multiwatt package IC typically used for audio amplifiers of high power applications",
zOrder = IDIYComponent.COMPONENT, keywordPolicy = KeywordPolicy.SHOW_VALUE,
enableCache = true)
public class MultiwattIC extends AbstractTransparentComponent<String> implements IGerberComponentSimple {
private static final long serialVersionUID = 1L;
public static Color BODY_COLOR = Color.gray;
public static Color BORDER_COLOR = Color.gray.darker();
public static Color PIN_COLOR = Color.decode("#00B2EE");
public static Color PIN_BORDER_COLOR = PIN_COLOR.darker();
public static Color LABEL_COLOR = Color.white;
public static Color TAB_COLOR = Color.lightGray;
public static Color TAB_BORDER_COLOR = TAB_COLOR.darker();
public static int EDGE_RADIUS = 6;
public static Size PIN_SIZE = new Size(0.7d, SizeUnit.mm);
public static Size THICKNESS = new Size(5d, SizeUnit.mm);
public static Size TAB_THICKNESS = new Size(1.6d, SizeUnit.mm);
// Multiwatt packages use 0.1 inch (2.54mm) spacing between pins in the same row
public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in);
// Multiwatt packages have staggered pins with 0.2 inch (5.08mm) row spacing
public static Size ROW_SPACING = new Size(0.2d, SizeUnit.in);
// Lead offset due to bent leads
public static Size LEAD_OFFSET = new Size(4d, SizeUnit.mm);
private String value = "";
private Orientation orientation = Orientation.DEFAULT;
private MultiwattType type = MultiwattType.MULTIWATT_11;
private Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
protected Display display = Display.NAME;
private Color bodyColor = BODY_COLOR;
private Color borderColor = BORDER_COLOR;
private Color labelColor = LABEL_COLOR;
private Color tabColor = TAB_COLOR;
private Color tabBorderColor = TAB_BORDER_COLOR;
transient private Area[] body;
public MultiwattIC() {
super();
updateControlPoints();
alpha = 100;
}
@EditableProperty
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@EditableProperty
public Orientation getOrientation() {
return orientation;
}
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
updateControlPoints();
// Reset body shape.
body = null;
}
@EditableProperty(name = "Type")
public MultiwattType getType() {
return type;
}
public void setType(MultiwattType type) {
this.type = type;
updateControlPoints();
// Reset body shape;
body = null;
}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
}
@EditableProperty
public Display getDisplay() {
if (display == null) {
display = Display.VALUE;
}
return display;
}
public void setDisplay(Display display) {
this.display = display;
}
@Override
public int getControlPointCount() {
return controlPoints.length;
}
@Override
public Point2D getControlPoint(int index) {
return controlPoints[index];
}
@Override
public boolean isControlPointSticky(int index) {
return true;
}
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.NEVER;
}
@Override
public void setControlPoint(Point2D point, int index) {
controlPoints[index].setLocation(point);
body = null;
}
private void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
int pinCount = type.getPinCount();
int pinsLeft = type.getPinsLeft();
int pinsRight = type.getPinsRight();
controlPoints = new Point2D.Double[pinCount];
controlPoints[0] = firstPoint;
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
// Update control points for staggered layout
double dx1, dy1, dx2, dy2;
// Left side pins (first row)
for (int i = 0; i < pinsLeft; i++) {
switch (orientation) {
case DEFAULT:
dx1 = 0;
dy1 = i * pinSpacing;
break;
case _90:
dx1 = -i * pinSpacing;
dy1 = 0;
break;
case _180:
dx1 = 0;
dy1 = -i * pinSpacing;
break;
case _270:
dx1 = i * pinSpacing;
dy1 = 0;
break;
default:
throw new RuntimeException("Unexpected orientation: " + orientation);
}
controlPoints[i] = new Point2D.Double((int) (firstPoint.getX() + dx1), (int) (firstPoint.getY() + dy1));
}
// Right side pins (second row, staggered - offset by half pin spacing)
for (int i = 0; i < pinsRight; i++) {
switch (orientation) {
case DEFAULT:
dx2 = rowSpacing;
dy2 = (i + 0.5) * pinSpacing; // Offset by half spacing for staggered effect
break;
case _90:
dx2 = -(i + 0.5) * pinSpacing; // Offset by half spacing for staggered effect
dy2 = rowSpacing;
break;
case _180:
dx2 = -rowSpacing;
dy2 = -(i + 0.5) * pinSpacing; // Offset by half spacing for staggered effect
break;
case _270:
dx2 = (i + 0.5) * pinSpacing; // Offset by half spacing for staggered effect
dy2 = -rowSpacing;
break;
default:
throw new RuntimeException("Unexpected orientation: " + orientation);
}
controlPoints[i + pinsLeft] = new Point2D.Double((int) (firstPoint.getX() + dx2), (int) (firstPoint.getY() + dy2));
}
}
public Area[] getBody() {
if (body == null) {
body = new Area[2];
double x = controlPoints[0].getX();
double y = controlPoints[0].getY();
double pinSize = PIN_SIZE.convertToPixels();
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
double tabThickness = TAB_THICKNESS.convertToPixels();
double leadOffset = LEAD_OFFSET.convertToPixels();
int pinsLeft = type.getPinsLeft();
int pinsRight = type.getPinsRight();
// Calculate height accounting for staggered pins (right row offset by half spacing)
double leftRowHeight = (pinsLeft - 1) * pinSpacing;
double rightRowHeight = (pinsRight - 1 + 0.5) * pinSpacing; // Account for half-spacing offset
double maxHeight = Math.max(leftRowHeight, rightRowHeight) + pinSpacing; // Add spacing for last pin
double bodyX, bodyY, bodyWidth, bodyHeight;
double tabX, tabY, tabWidth, tabHeight;
switch (orientation) {
case DEFAULT:
bodyWidth = rowSpacing - pinSize;
bodyHeight = maxHeight;
// Offset body to the left by lead offset (bent leads)
bodyX = x + pinSize / 2 - leadOffset;
bodyY = y - pinSpacing / 2;
// Tab on the left side (where there are more pins) inside the body bounds
tabX = bodyX;
tabY = bodyY;
tabWidth = tabThickness;
tabHeight = bodyHeight;
break;
case _90:
bodyWidth = maxHeight;
bodyHeight = rowSpacing - pinSize;
// Body positioned between the two pin rows
// Left pins extend to negative X, body starts from leftmost pin
// For _90: left pins go left (negative X), so leftmost is at x - (pinsLeft-1)*pinSpacing
// Offset body upward by lead offset (bent leads)
bodyX = x - (pinsLeft - 1) * pinSpacing - pinSpacing / 2;
bodyY = y + pinSize / 2 - leadOffset;
// Tab on the top side (where there are more pins - left row at y) inside the body bounds
tabX = bodyX;
tabY = bodyY;
tabWidth = bodyWidth;
tabHeight = tabThickness;
break;
case _180:
bodyWidth = rowSpacing - pinSize;
bodyHeight = maxHeight;
// Body positioned between the two pin rows
// For _180: left pins go up (negative Y), right pins at x - rowSpacing
// Left row is at x, right row is at x - rowSpacing
// Pins extend upward from y, so body should start from top of pin array
// Offset body to the right by lead offset (bent leads)
bodyX = x - rowSpacing + pinSize / 2 + leadOffset;
bodyY = y - (pinsLeft - 1) * pinSpacing - pinSpacing / 2;
// Tab on the right side (where there are more pins - left row at x) inside the body bounds
tabX = bodyX + bodyWidth - tabThickness;
tabY = bodyY;
tabWidth = tabThickness;
tabHeight = bodyHeight;
break;
case _270:
bodyWidth = maxHeight;
bodyHeight = rowSpacing - pinSize;
// Body positioned between the two pin rows
// For _270: left pins go right (positive X), so leftmost is at x
// Left row is at y, right row is at y - rowSpacing
// Similar to _90 but mirrored
// Offset body downward by lead offset (bent leads)
bodyX = x - pinSpacing / 2;
bodyY = y - rowSpacing + pinSize / 2 + leadOffset;
// Tab on the bottom side (where there are more pins - left row at y) inside the body bounds
tabX = bodyX;
tabY = bodyY + bodyHeight - tabThickness;
tabWidth = bodyWidth;
tabHeight = tabThickness;
break;
default:
throw new RuntimeException("Unexpected orientation: " + orientation);
}
body[0] = new Area(new Rectangle2D.Double(bodyX, bodyY, bodyWidth, bodyHeight));
body[1] = new Area(new Rectangle2D.Double(tabX, tabY, tabWidth, tabHeight));
}
return body;
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Area mainArea = getBody()[0];
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
if (!outlineMode) {
int pinSize = (int) PIN_SIZE.convertToPixels() / 2 * 2;
Rectangle2D bodyBounds = mainArea.getBounds2D();
double leadThickness = pinSize; // Lead width matches pin size
for (Point2D point : controlPoints) {
double pinX = point.getX();
double pinY = point.getY();
// Check if pin is inside the body
boolean insideBody = mainArea.contains(pinX, pinY);
if (insideBody) {
// Draw pin as square inside body
Rectangle2D rect = new Rectangle2D.Double(pinX - pinSize / 2,
pinY - pinSize / 2, pinSize, pinSize);
g2d.setColor(PIN_COLOR);
drawingObserver.startTracking();
g2d.fill(rect);
drawingObserver.stopTracking();
g2d.setColor(PIN_BORDER_COLOR);
g2d.draw(rect);
} else {
// Draw rectangular lead from body edge to pin position
double bodyX = bodyBounds.getX();
double bodyY = bodyBounds.getY();
double bodyWidth = bodyBounds.getWidth();
double bodyHeight = bodyBounds.getHeight();
// Find the closest point on body edge to the pin
double edgeX, edgeY;
// Determine which side of the body the pin is on
if (pinX < bodyX) {
// Pin is to the left of body
edgeX = bodyX;
edgeY = Math.max(bodyY, Math.min(bodyY + bodyHeight, pinY));
} else if (pinX > bodyX + bodyWidth) {
// Pin is to the right of body
edgeX = bodyX + bodyWidth;
edgeY = Math.max(bodyY, Math.min(bodyY + bodyHeight, pinY));
} else if (pinY < bodyY) {
// Pin is above body
edgeX = Math.max(bodyX, Math.min(bodyX + bodyWidth, pinX));
edgeY = bodyY;
} else {
// Pin is below body
edgeX = Math.max(bodyX, Math.min(bodyX + bodyWidth, pinX));
edgeY = bodyY + bodyHeight;
}
// Draw rectangular lead
double leadX = Math.min(edgeX, pinX) - leadThickness / 2;
double leadY = Math.min(edgeY, pinY) - leadThickness / 2;
double leadWidth = Math.abs(pinX - edgeX) + leadThickness;
double leadHeight = Math.abs(pinY - edgeY) + leadThickness;
// If lead is horizontal or vertical, draw as rectangle
if (Math.abs(pinX - edgeX) < 0.1) {
// Vertical lead
leadWidth = leadThickness;
leadX = edgeX - leadThickness / 2;
} else if (Math.abs(pinY - edgeY) < 0.1) {
// Horizontal lead
leadHeight = leadThickness;
leadY = edgeY - leadThickness / 2;
}
Rectangle2D leadRect = new Rectangle2D.Double(leadX, leadY, leadWidth, leadHeight);
g2d.setColor(PIN_COLOR);
drawingObserver.startTracking();
g2d.fill(leadRect);
drawingObserver.stopTracking();
g2d.setColor(PIN_BORDER_COLOR);
g2d.draw(leadRect);
// Draw pin at the end of the lead
Rectangle2D pinRect = new Rectangle2D.Double(pinX - pinSize / 2,
pinY - pinSize / 2, pinSize, pinSize);
g2d.setColor(PIN_COLOR);
drawingObserver.startTracking();
g2d.fill(pinRect);
drawingObserver.stopTracking();
g2d.setColor(PIN_BORDER_COLOR);
g2d.draw(pinRect);
}
}
}
drawingObserver.startTracking();
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : getBodyColor());
g2d.fill(mainArea);
drawingObserver.stopTracking();
// Draw heat sink tab
Area tabArea = getBody()[1];
Color finalTabColor;
if (outlineMode) {
Theme theme =
(Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME);
finalTabColor = theme.getOutlineColor();
} else {
finalTabColor = tabColor;
}
g2d.setColor(finalTabColor);
drawingObserver.startTracking();
g2d.fill(tabArea);
drawingObserver.stopTracking();
g2d.setComposite(oldComposite);
if (!outlineMode) {
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.setColor(tabBorderColor);
g2d.draw(tabArea);
}
Color finalBorderColor;
if (outlineMode) {
Theme theme =
(Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME);
finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
: theme.getOutlineColor();
} else {
finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
: getBorderColor();
}
g2d.setColor(finalBorderColor);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(mainArea);
// Draw label.
g2d.setFont(project.getFont());
Color finalLabelColor;
if (outlineMode) {
Theme theme =
(Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME);
finalLabelColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? LABEL_COLOR_SELECTED
: theme.getOutlineColor();
} else {
finalLabelColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? LABEL_COLOR_SELECTED
: getLabelColor();
}
g2d.setColor(finalLabelColor);
FontMetrics fontMetrics = g2d.getFontMetrics(g2d.getFont());
String label = "";
label = (getDisplay() == Display.NAME) ? getName() : getValue();
if (getDisplay() == Display.NONE) {
label = "";
}
if (getDisplay() == Display.BOTH) {
label = getName() + " " + (getValue() == null ? "" : getValue().toString());
}
Rectangle2D rect = fontMetrics.getStringBounds(label, g2d);
int textHeight = (int) (rect.getHeight());
int textWidth = (int) (rect.getWidth());
// Center text horizontally and vertically
Rectangle bounds = mainArea.getBounds();
int x = (int) (bounds.getX() + (bounds.width - textWidth) / 2);
int y = (int) (bounds.getY() + (bounds.height - textHeight) / 2 + fontMetrics.getAscent());
g2d.drawString(label, x, y);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
int margin = 2;
int pinSize = 2 * width / 32;
int leftPinX = width / 6 + 9; // Move left pins 9px to the right (4px + 3px + 2px)
int rightPinX = 5 * width / 6;
int leadOffset = (int) (LEAD_OFFSET.convertToPixels() * width / 100); // Scale lead offset for icon
int bodyX = leftPinX + pinSize - leadOffset + 2; // Offset body to the left, then move 2px to the right
int bodyWidth = (rightPinX - bodyX - pinSize) / 2 + 2; // Thinner body, make 2px thicker
int bodyHeight = height - 2 * margin;
int bodyY = margin;
// Calculate pin spacing to use all vertical space
// Use same spacing for both rows, with front row offset by half spacing
int backPins = 4;
int frontPins = 3;
// Use the spacing that fits the back row (more pins)
int pinSpacing = (bodyHeight - pinSize) / (backPins - 1);
int backStartY = margin + pinSize / 2 + 1; // Move pins 1px down
// Front pins start with half spacing offset for staggered effect
int frontStartY = margin + pinSize / 2 + pinSpacing / 2 + 1; // Move pins 1px down
// Draw thin body between pin rows
g2d.setColor(BODY_COLOR);
g2d.fillRect(bodyX, bodyY, bodyWidth, bodyHeight);
g2d.setColor(BORDER_COLOR);
g2d.drawRect(bodyX, bodyY, bodyWidth, bodyHeight);
// Draw heat sink tab on left side (where there are more pins)
int tabWidth = pinSize * 2;
g2d.setColor(TAB_COLOR);
g2d.fillRect(bodyX, bodyY, tabWidth, bodyHeight);
g2d.setColor(TAB_BORDER_COLOR);
g2d.drawRect(bodyX, bodyY, tabWidth, bodyHeight);
// Draw 4 pins in the back (left row, more pins) as squares - inside body
g2d.setColor(PIN_COLOR);
for (int i = 0; i < backPins; i++) {
int pinY = backStartY + i * pinSpacing - pinSize / 2;
g2d.fillRect(leftPinX - pinSize, pinY, pinSize, pinSize);
g2d.setColor(PIN_BORDER_COLOR);
g2d.drawRect(leftPinX - pinSize, pinY, pinSize, pinSize);
g2d.setColor(PIN_COLOR);
}
// Draw 3 pins in the front (right row, fewer pins) with leads going left to body
// Use same spacing as back row, just offset by half spacing
g2d.setColor(PIN_COLOR);
for (int i = 0; i < frontPins; i++) {
int pinY = frontStartY + i * pinSpacing;
int pinX = rightPinX;
int bodyRightEdge = bodyX + bodyWidth;
// Draw lead from body edge to pin (horizontal lead going left)
if (pinX > bodyRightEdge) {
int leadX = bodyRightEdge;
int leadY = pinY - pinSize / 2;
int leadWidth = pinX - bodyRightEdge;
int leadHeight = pinSize;
g2d.fillRect(leadX, leadY, leadWidth, leadHeight);
g2d.setColor(PIN_BORDER_COLOR);
g2d.drawRect(leadX, leadY, leadWidth, leadHeight);
g2d.setColor(PIN_COLOR);
}
// Draw pin at the end of the lead
g2d.fillRect(pinX - pinSize / 2, pinY - pinSize / 2, pinSize, pinSize);
g2d.setColor(PIN_BORDER_COLOR);
g2d.drawRect(pinX - pinSize / 2, pinY - pinSize / 2, pinSize, pinSize);
g2d.setColor(PIN_COLOR);
}
}
@EditableProperty(name = "Body")
public Color getBodyColor() {
if (bodyColor == null) {
bodyColor = BODY_COLOR;
}
return bodyColor;
}
public void setBodyColor(Color bodyColor) {
this.bodyColor = bodyColor;
}
@EditableProperty(name = "Border")
public Color getBorderColor() {
if (borderColor == null) {
borderColor = BORDER_COLOR;
}
return borderColor;
}
public void setBorderColor(Color borderColor) {
this.borderColor = borderColor;
}
@EditableProperty(name = "Label")
public Color getLabelColor() {
if (labelColor == null) {
labelColor = LABEL_COLOR;
}
return labelColor;
}
public void setLabelColor(Color labelColor) {
this.labelColor = labelColor;
}
@EditableProperty(name = "Tab")
public Color getTabColor() {
if (tabColor == null) {
tabColor = TAB_COLOR;
}
return tabColor;
}
public void setTabColor(Color tabColor) {
this.tabColor = tabColor;
}
@EditableProperty(name = "Tab Border")
public Color getTabBorderColor() {
if (tabBorderColor == null) {
tabBorderColor = TAB_BORDER_COLOR;
}
return tabBorderColor;
}
public void setTabBorderColor(Color tabBorderColor) {
this.tabBorderColor = tabBorderColor;
}
@Override
public Rectangle2D getCachingBounds() {
double minX = Integer.MAX_VALUE;
double maxX = Integer.MIN_VALUE;
double minY = Integer.MAX_VALUE;
double maxY = Integer.MIN_VALUE;
int margin = 50;
for (int i = 0; i < getControlPointCount(); i++) {
Point2D p = getControlPoint(i);
if (p.getX() < minX)
minX = p.getX();
if (p.getX() > maxX)
maxX = p.getX();
if (p.getY() < minY)
minY = p.getY();
if (p.getY() > maxY)
maxY = p.getY();
}
return new Rectangle2D.Double(minX - margin, minY - margin, maxX - minX + 2 * margin, maxY - minY + 2 * margin);
}
public static enum MultiwattType {
// Multiwatt-11: 6 pins on left, 5 pins on right (staggered)
MULTIWATT_11(11, 6, 5),
// Multiwatt-15: 8 pins on left, 7 pins on right (staggered)
MULTIWATT_15(15, 8, 7);
private final int pinCount;
private final int pinsLeft;
private final int pinsRight;
MultiwattType(int pinCount, int pinsLeft, int pinsRight) {
this.pinCount = pinCount;
this.pinsLeft = pinsLeft;
this.pinsRight = pinsRight;
}
public int getPinCount() {
return pinCount;
}
public int getPinsLeft() {
return pinsLeft;
}
public int getPinsRight() {
return pinsRight;
}
@Override
public String toString() {
return "Multiwatt " + pinCount;
}
}
}
@@ -36,7 +36,7 @@ import org.diylc.core.annotations.ComponentDescriptor;
@ComponentDescriptor(name = "Ellipse", author = "Branislav Stojkovic", category = "Shapes", instanceNamePrefix = "ELL",
description = "Elliptical area", zOrder = IDIYComponent.COMPONENT, flexibleZOrder = true,
bomPolicy = BomPolicy.SHOW_ALL_NAMES, autoEdit = false, transformer = SimpleComponentTransformer.class)
bomPolicy = BomPolicy.SHOW_ALL_NAMES, transformer = SimpleComponentTransformer.class)
public class Ellipse extends AbstractShapeWithDimensions {
private static final long serialVersionUID = 1L;
@@ -40,7 +40,7 @@ import org.diylc.core.annotations.EditableProperty;
@ComponentDescriptor(name = "Polygon", author = "Branislav Stojkovic", category = "Shapes",
instanceNamePrefix = "POLY", description = "Polygonal area", zOrder = IDIYComponent.COMPONENT,
flexibleZOrder = true, bomPolicy = BomPolicy.SHOW_ALL_NAMES, autoEdit = false,
flexibleZOrder = true, bomPolicy = BomPolicy.SHOW_ALL_NAMES,
transformer = SimpleComponentTransformer.class)
public class Polygon extends AbstractShape {
@@ -39,7 +39,7 @@ import org.diylc.core.measures.SizeUnit;
@ComponentDescriptor(name = "Rectangle", author = "Branislav Stojkovic", category = "Shapes",
instanceNamePrefix = "RECT", description = "Ractangular area, with or withouth rounded edges",
zOrder = IDIYComponent.COMPONENT, flexibleZOrder = true, bomPolicy = BomPolicy.SHOW_ALL_NAMES, autoEdit = false,
zOrder = IDIYComponent.COMPONENT, flexibleZOrder = true, bomPolicy = BomPolicy.SHOW_ALL_NAMES,
transformer = SimpleComponentTransformer.class)
public class Rectangle extends AbstractShapeWithDimensions {
@@ -53,7 +53,7 @@ import org.diylc.core.measures.SizeUnit;
@ComponentDescriptor(name = "Tape Measure", author = "Branislav Stojkovic", category = "Misc",
creationMethod = CreationMethod.POINT_BY_POINT, instanceNamePrefix = "TM", description = "Measures distance between the two points",
zOrder = IDIYComponent.COMPONENT, bomPolicy = BomPolicy.NEVER_SHOW, autoEdit = false,
zOrder = IDIYComponent.COMPONENT, bomPolicy = BomPolicy.NEVER_SHOW,
transformer = SimpleComponentTransformer.class)
public class TapeMeasure extends AbstractLeadedComponent<String> {
@@ -77,7 +77,7 @@ public class TapeMeasure extends AbstractLeadedComponent<String> {
g2d.setColor(COLOR);
g2d.drawLine(1, height - 2, width / 4, height * 3 / 4 - 1);
g2d.drawLine(width * 3 / 4, height / 4 - 1, width - 2, 1);
g2d.setBackground(Color.black);
g2d.setColor(Color.black);
g2d.fillPolygon(new int[] {1, 1, 4}, new int[] {height - 4, height - 1, height - 1}, 3);
g2d.fillPolygon(new int[] {width - 4, width - 1, width - 1}, new int[] {1, 1, 4}, 3);
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
@@ -174,6 +174,7 @@ public class TapeMeasure extends AbstractLeadedComponent<String> {
g2d.rotate(theta);
g2d.setFont(font);
g2d.setColor(componentState == ComponentState.SELECTED ? SELECTION_COLOR : labelColor);
g2d.drawString(text, x, y);
}
@@ -27,6 +27,7 @@ import java.awt.geom.Point2D;
import org.diylc.common.IComponentTransformer;
import org.diylc.common.OrientationHV;
import org.diylc.components.electromechanical.MiniToggleSwitch;
import org.diylc.components.electromechanical.ToggleSwitchType;
import org.diylc.core.IDIYComponent;
public class MiniToggleSwitchTransformer implements IComponentTransformer {
@@ -107,7 +108,8 @@ public class MiniToggleSwitchTransformer implements IComponentTransformer {
MiniToggleSwitch sw = (MiniToggleSwitch) component;
if (direction == IComponentTransformer.HORIZONTAL) {
double dx = 2 * (center.getX() - sw.getControlPoint(1).getX());
double dx = 2 * (center.getX() -
(sw.getControlPoint(0).getX() + sw.getControlPoint(sw.getControlPointCount() - 1).getX()) / 2);
double dy = 0;
for (int i = 0; i < sw.getControlPointCount(); i++) {
@@ -116,7 +118,8 @@ public class MiniToggleSwitchTransformer implements IComponentTransformer {
}
} else {
double dx = 0;
double dy = 2 * (center.getY() - sw.getControlPoint(1).getY());
double dy = 2 * (center.getY() -
(sw.getControlPoint(0).getY() + sw.getControlPoint(sw.getControlPointCount() - 1).getY()) / 2);
for (int i = 0; i < sw.getControlPointCount(); i++) {
Point2D p = sw.getControlPoint(i);
@@ -70,6 +70,20 @@ public class PinHeaderTransformer implements IComponentTransformer {
@Override
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
PinHeader header = (PinHeader) component;
int controlPointCount = header.getControlPointCount();
// Handle single-pin case
if (controlPointCount == 1) {
Point2D p = header.getControlPoint(0);
if (direction == IComponentTransformer.HORIZONTAL) {
double dx = 2 * (center.getX() - p.getX());
header.setControlPoint(new Point2D.Double(p.getX() + dx, p.getY()), 0);
} else {
double dy = 2 * (center.getY() - p.getY());
header.setControlPoint(new Point2D.Double(p.getX(), p.getY() + dy), 0);
}
return;
}
if (direction == IComponentTransformer.HORIZONTAL) {
double dx = 2 * (center.getX() - header.getControlPoint(1).getX());
@@ -1,15 +1,117 @@
package org.diylc.netlist;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import org.junit.Test;
import org.diylc.components.guitar.LPSwitch;
import org.diylc.components.guitar.LPSwitch.MiddleTerminalConfiguration;
import org.diylc.common.Orientation;
import org.diylc.core.IDIYComponent;
public class LPSwitchTests extends AbstractSwitchTests {
@Test
public void testLPSwitch() {
public void testLPSwitchConnected() {
LPSwitch lpSwitch = new LPSwitch();
// Default configuration is Connected
assertEquals(MiddleTerminalConfiguration.Connected, lpSwitch.getMiddleTerminalConfiguration());
// Connected has 4 control points (0, 1, 2, 3)
assertEquals(4, ((IDIYComponent<?>) lpSwitch).getControlPointCount());
// Position 0 (Treble): 1-2 connected
// Position 1 (Middle): 1-2, 1-3, 2-3 connected (all terminals > 0 connected)
// Position 2 (Rhythm): 2-3 connected
String[] validCombinations = new String[] {"0,1,2", "1,1,2", "1,2,3", "1,1,3", "2,2,3"};
testSwitch(lpSwitch, validCombinations);
}
}
@Test
public void testLPSwitchDisconnected() {
LPSwitch lpSwitch = new LPSwitch();
lpSwitch.setMiddleTerminalConfiguration(MiddleTerminalConfiguration.Disconnected);
assertEquals(MiddleTerminalConfiguration.Disconnected, lpSwitch.getMiddleTerminalConfiguration());
// Disconnected has 5 control points (0, 1, 2, 3, 4)
assertEquals(5, ((IDIYComponent<?>) lpSwitch).getControlPointCount());
// Position 0 (Treble): 1-2 connected
// Position 1 (Middle): 1-2 and 3-4 connected (both pairs)
// Position 2 (Rhythm): 3-4 connected
String[] validCombinations = new String[] {"0,1,2", "1,1,2", "1,3,4", "2,3,4"};
testSwitch(lpSwitch, validCombinations);
}
@Test
public void testControlPointCountChange() {
LPSwitch lpSwitch = new LPSwitch();
// Start with Connected (4 points)
assertEquals(4, ((IDIYComponent<?>) lpSwitch).getControlPointCount());
// Switch to Disconnected (5 points)
lpSwitch.setMiddleTerminalConfiguration(MiddleTerminalConfiguration.Disconnected);
assertEquals(5, ((IDIYComponent<?>) lpSwitch).getControlPointCount());
// Switch back to Connected (4 points)
lpSwitch.setMiddleTerminalConfiguration(MiddleTerminalConfiguration.Connected);
assertEquals(4, ((IDIYComponent<?>) lpSwitch).getControlPointCount());
}
@Test
public void testPositionNames() {
LPSwitch lpSwitch = new LPSwitch();
assertEquals(3, lpSwitch.getPositionCount());
assertEquals("Treble", lpSwitch.getPositionName(0));
assertEquals("Middle", lpSwitch.getPositionName(1));
assertEquals("Rhythm", lpSwitch.getPositionName(2));
assertNull(lpSwitch.getPositionName(3)); // Invalid position
assertNull(lpSwitch.getPositionName(-1)); // Invalid position
}
@Test
public void testOrientationDoesNotAffectConnectivity() {
LPSwitch lpSwitch = new LPSwitch();
// Test with default Connected configuration
String[] validCombinations = new String[] {"0,1,2", "1,1,2", "1,2,3", "1,1,3", "2,2,3"};
// Test all orientations - connectivity should remain the same
for (Orientation orientation : Orientation.values()) {
lpSwitch.setOrientation(orientation);
assertEquals(orientation, lpSwitch.getOrientation());
// Connectivity should be unchanged regardless of orientation
testSwitch(lpSwitch, validCombinations);
}
}
@Test
public void testOrientationWithDisconnectedConfiguration() {
LPSwitch lpSwitch = new LPSwitch();
lpSwitch.setMiddleTerminalConfiguration(MiddleTerminalConfiguration.Disconnected);
String[] validCombinations = new String[] {"0,1,2", "1,1,2", "1,3,4", "2,3,4"};
// Test all orientations with Disconnected configuration
for (Orientation orientation : Orientation.values()) {
lpSwitch.setOrientation(orientation);
assertEquals(orientation, lpSwitch.getOrientation());
// Connectivity should be unchanged regardless of orientation
testSwitch(lpSwitch, validCombinations);
}
}
@Test
public void testConfigurationSwitchPreservesConnectivity() {
LPSwitch lpSwitch = new LPSwitch();
// Test Connected configuration
lpSwitch.setMiddleTerminalConfiguration(MiddleTerminalConfiguration.Connected);
String[] connectedCombinations = new String[] {"0,1,2", "1,1,2", "1,2,3", "1,1,3", "2,2,3"};
testSwitch(lpSwitch, connectedCombinations);
// Switch to Disconnected
lpSwitch.setMiddleTerminalConfiguration(MiddleTerminalConfiguration.Disconnected);
String[] disconnectedCombinations = new String[] {"0,1,2", "1,1,2", "1,3,4", "2,3,4"};
testSwitch(lpSwitch, disconnectedCombinations);
// Switch back to Connected
lpSwitch.setMiddleTerminalConfiguration(MiddleTerminalConfiguration.Connected);
testSwitch(lpSwitch, connectedCombinations);
}
}
@@ -28,6 +28,34 @@ public class LeverSwitchTests extends AbstractSwitchTests {
leverSwitch.setValue(LeverSwitchType._4P5T);
testSwitch(leverSwitch, validCombinations);
}
@Test
public void testLeverSwitch6P5T() {
LeverSwitch leverSwitch = new LeverSwitch();
String[] validCombinations = new String[] {
"0,0,1", "0,6,11", "0,12,13", "0,18,23", "0,24,25", "0,30,35",
"1,0,2", "1,7,11", "1,12,14", "1,19,23", "1,24,26", "1,31,35",
"2,0,3", "2,8,11", "2,12,15", "2,20,23", "2,24,27", "2,32,35",
"3,0,4", "3,9,11", "3,12,16", "3,21,23", "3,24,28", "3,33,35",
"4,0,5", "4,10,11", "4,12,17", "4,22,23", "4,24,29", "4,34,35"
};
leverSwitch.setValue(LeverSwitchType._6P5T);
testSwitch(leverSwitch, validCombinations);
}
@Test
public void testLeverSwitch4P3T() {
LeverSwitch leverSwitch = new LeverSwitch();
String[] validCombinations = new String[] {
"0,0,1", "0,4,7", "0,8,9", "0,12,15",
"1,0,2", "1,5,7", "1,8,10", "1,13,15",
"2,0,3", "2,6,7", "2,8,11", "2,14,15",
};
leverSwitch.setValue(LeverSwitchType._4P3T);
testSwitch(leverSwitch, validCombinations);
}
@Test
public void testLeverSwitchImport() {
@@ -50,4 +78,20 @@ public class LeverSwitchTests extends AbstractSwitchTests {
leverSwitch.setValue(LeverSwitchType._6_WAY_OG);
testSwitch(leverSwitch, validCombinations);
}
@Test
public void testLeverSwitchImport2502N() {
LeverSwitch leverSwitch = new LeverSwitch();
String[] validCombinations = new String[] {
"0,0,3",
"1,0,3", "1,1,3", "1,4,7", "1,5,7",
"2,0,3", "2,2,3", "2,6,7",
"3,1,3", "3,2,3", "3,5,7",
"4,2,3", "4,6,7",
};
leverSwitch.setValue(LeverSwitchType.DP3T_5pos_2502N);
testSwitch(leverSwitch, validCombinations);
}
}
@@ -1,24 +1,58 @@
#!/bin/bash
#set -x # Enable debug mode
set -x # Enable debug mode
# Get the AppImage directory
if [ -n "$APPDIR" ]; then
HERE="$APPDIR/usr/bin"
JRE_PATH="$APPDIR/usr/lib/jre/bin/java"
JRE_DIR="$APPDIR/usr/lib/jre"
else
HERE="$(cd "$(dirname "$0")" && pwd)"
JRE_PATH="$(dirname "$HERE")/lib/jre/bin/java"
JRE_DIR="$(dirname "$HERE")/lib/jre"
fi
echo "diylc.sh: Running from directory: $HERE"
echo "diylc.sh: Looking for JRE at: $JRE_PATH"
# Use system Java
JAVA="$(which java)"
if [ -z "$JAVA" ] || [ ! -x "$JAVA" ]; then
echo "diylc.sh: ERROR - System Java not found or not executable!"
echo "diylc.sh: Please install Java 17 or later"
# List directory contents for debugging
echo "diylc.sh: Contents of $(dirname "$JRE_PATH"):"
ls -la "$(dirname "$JRE_PATH")"
# Use bundled JRE
if [ ! -f "$JRE_PATH" ]; then
echo "diylc.sh: ERROR - Bundled JRE not found at: $JRE_PATH"
exit 1
fi
echo "diylc.sh: Using system Java: $JAVA"
# Check if we're running from a mounted AppImage
if [[ "$HERE" == /tmp/.mount_* ]]; then
echo "diylc.sh: Running from mounted AppImage"
# Create a temporary directory for the JRE
TEMP_DIR="/tmp/diylc-jre-$$"
mkdir -p "$TEMP_DIR"
# Copy the entire JRE directory to the temporary location
echo "diylc.sh: Copying JRE to temporary location: $TEMP_DIR"
cp -r "$JRE_DIR"/* "$TEMP_DIR/"
chmod -R +x "$TEMP_DIR/bin"
JRE_PATH="$TEMP_DIR/bin/java"
# Clean up the temporary directory on exit
trap 'rm -rf "$TEMP_DIR"' EXIT
fi
# Verify permissions
echo "diylc.sh: Verifying JRE permissions..."
ls -la "$JRE_PATH"
if [ ! -x "$JRE_PATH" ]; then
echo "diylc.sh: ERROR - JRE is not executable at: $JRE_PATH"
echo "diylc.sh: Current permissions: $(ls -l "$JRE_PATH")"
exit 1
fi
echo "diylc.sh: Using JRE: $JRE_PATH"
cd "$HERE"
echo "diylc.sh: Current working directory: $(pwd)"
@@ -26,15 +60,17 @@ echo "diylc.sh: Current working directory: $(pwd)"
# Debug: Print command before execution
echo "diylc.sh: Executing DIYLCStarter with Java"
exec "$JAVA" \
-Xms1024m \
-Xmx4096m \
-cp "$APPDIR/usr/lib/diylc.jar" \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.text=ALL-UNNAMED \
--add-opens java.desktop/java.awt=ALL-UNNAMED \
--add-opens java.desktop/java.awt.font=ALL-UNNAMED \
--add-opens java.desktop/java.awt.geom=ALL-UNNAMED \
-Dorg.diylc.scriptRun=true \
org.diylc.DIYLCStarter "$@"
# Execute Java directly with the command array
exec "$JRE_PATH" \
-Xms1024m \
-Xmx4096m \
-cp "$(dirname "$HERE")/lib/diylc.jar" \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.text=ALL-UNNAMED \
--add-opens java.desktop/java.awt=ALL-UNNAMED \
--add-opens java.desktop/java.awt.font=ALL-UNNAMED \
--add-opens java.desktop/java.awt.geom=ALL-UNNAMED \
-Dorg.diylc.scriptRun=true \
org.diylc.DIYLCStarter "$@"
+3 -3
View File
@@ -12,11 +12,11 @@ ChangesAssociations=yes
AlwaysShowDirOnReadyPage=yes
; Only allow the installer to run on x64-compatible systems,
; and enable 64-bit install mode.
ArchitecturesAllowed={#arch}
ArchitecturesInstallIn64BitMode={#arch}
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
[Files]
Source: "diylc-{#arch}.exe"; DestDir: "{app}"; DestName: "diylc.exe"
Source: "diylc-x64.exe"; DestDir: "{app}"; DestName: "diylc.exe"
Source: "..\..\src\main\resources\icons\diylc_file.ico"; DestDir: "{app}"
Source: "..\..\target\diylc.jar"; DestDir: "{app}"
Source: "..\..\target\jre_win\*"; DestDir: "{app}\jre17"; Flags: ignoreversion recursesubdirs
+1
View File
@@ -182,6 +182,7 @@ DP3T (Standard 5-Position Strat)
DP4T (4-Position Tele)
DP4T (6-Position Oak Grigsby)
DP5T
6P5T
DPDT
DPDT (On/Off/On)
DPDT (On/On)
+64 -12
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.4.0</version>
<version>5.8.0</version>
</parent>
<artifactId>diylc-swing</artifactId>
@@ -317,12 +317,49 @@
</properties>
<build>
<plugins>
<!-- Create AppDir structure and copy files -->
<!-- Download and extract JRE -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>download-extract-jre</id>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<!-- Clean old jre folder -->
<delete dir="${jre.dest.linux}" />
<!-- Create folder to hold extracted JRE -->
<mkdir dir="${jre.dest.linux}" />
<!-- Download JRE for Linux -->
<mkdir dir="${project.build.directory}/downloads" />
<get src="${jdk.url.linux}"
dest="${project.build.directory}/downloads/${jdk.archive.linux}"
usetimestamp="true" />
<!-- Extract JRE -->
<untar src="${project.build.directory}/downloads/${jdk.archive.linux}"
dest="${jre.dest.linux}"
compression="gzip" />
<!-- Move files up one level -->
<move todir="${jre.dest.linux}">
<fileset dir="${jre.dest.linux}/jdk-${jdk.version}-jre" />
</move>
<!-- Remove empty top-level folder -->
<delete dir="${jre.dest.linux}/jdk-${jdk.version}-jre" />
</target>
</configuration>
</execution>
<!-- Create AppDir structure and copy files -->
<execution>
<id>create-appdir</id>
<phase>package</phase>
@@ -339,7 +376,22 @@
<!-- Copy app files -->
<copy file="${project.build.directory}/diylc.jar" todir="${appdir.dir}/usr/lib" />
<!-- Copy JRE -->
<copy todir="${appdir.dir}/usr/lib/jre">
<fileset dir="${jre.dest.linux}" />
</copy>
<!-- Make JRE files executable -->
<chmod perm="755">
<fileset dir="${appdir.dir}/usr/lib/jre/bin" includes="**/*" />
</chmod>
<!-- Ensure JRE binaries are executable in the source directory too -->
<chmod perm="755">
<fileset dir="${jre.dest.linux}/bin" includes="**/*" />
</chmod>
<!-- Copy AppImage specific files -->
<copy file="${project.basedir}/deploy/linux/appimage/diylc.sh" tofile="${appdir.dir}/usr/bin/diylc.sh" />
<copy file="${project.basedir}/deploy/linux/appimage/AppRun" tofile="${appdir.dir}/AppRun" />
@@ -581,7 +633,7 @@
<argument>${project.basedir}:/workspace</argument>
<argument>amake/innosetup</argument>
<argument>/Ddiylcver=${project.version}</argument>
<argument>/Darch=x64</argument>
<!--<argument>/Darch=win64 arm64</argument>-->
<argument>/workspace/deploy/win/install.iss</argument>
</arguments>
</configuration>
@@ -839,11 +891,11 @@
<echo message="Creating unsigned zip for distribution..."/>
<!-- Create unsigned zip for distribution -->
<zip destfile="${project.build.directory}/diylc-${project.version}-osx-x86-unsigned.zip">
<zip destfile="${project.build.directory}/diylc-${project.version}-osx-intel-unsigned.zip">
<zipfileset filemode="755" dir="${project.build.directory}/DIYLC.app" prefix="DIYLC.app" />
<zipfileset dir="${project.basedir}/deploy/mac" includes="README.txt" />
</zip>
<echo message="Unsigned zip created at: ${project.build.directory}/diylc-${project.version}-osx-x86-unsigned.zip"/>
<echo message="Unsigned zip created at: ${project.build.directory}/diylc-${project.version}-osx-intel-unsigned.zip"/>
<!-- First remove existing signatures -->
<echo message="Removing any existing signatures..."/>
@@ -984,10 +1036,10 @@
<echo message="Creating final signed and notarized zip package..."/>
<!-- Create final signed and notarized zip package -->
<zip destfile="${project.build.directory}/diylc-${project.version}-osx-x86.zip">
<zip destfile="${project.build.directory}/diylc-${project.version}-osx-intel.zip">
<zipfileset filemode="755" dir="${project.build.directory}/DIYLC.app" prefix="DIYLC.app" />
</zip>
<echo message="Final package created at: ${project.build.directory}/diylc-${project.version}-osx-x86.zip"/>
<echo message="Final package created at: ${project.build.directory}/diylc-${project.version}-osx-intel.zip"/>
<echo message="Mac build process completed successfully!"/>
</target>
</configuration>
@@ -1153,11 +1205,11 @@
<echo message="Creating unsigned zip for distribution..."/>
<!-- Create unsigned zip for distribution -->
<zip destfile="${project.build.directory}/diylc-${project.version}-osx-arm-unsigned.zip">
<zip destfile="${project.build.directory}/diylc-${project.version}-osx-silicon-unsigned.zip">
<zipfileset filemode="755" dir="${project.build.directory}/DIYLC.app" prefix="DIYLC.app" />
<zipfileset dir="${project.basedir}/deploy/mac" includes="README.txt" />
</zip>
<echo message="Unsigned zip created at: ${project.build.directory}/diylc-${project.version}-osx-arm-unsigned.zip"/>
<echo message="Unsigned zip created at: ${project.build.directory}/diylc-${project.version}-osx-silicon-unsigned.zip"/>
<!-- First remove existing signatures -->
<echo message="Removing any existing signatures..."/>
@@ -1298,10 +1350,10 @@
<echo message="Creating final signed and notarized zip package..."/>
<!-- Create final signed and notarized zip package -->
<zip destfile="${project.build.directory}/diylc-${project.version}-osx-arm.zip">
<zip destfile="${project.build.directory}/diylc-${project.version}-osx-silicon.zip">
<zipfileset filemode="755" dir="${project.build.directory}/DIYLC.app" prefix="DIYLC.app" />
</zip>
<echo message="Final package created at: ${project.build.directory}/diylc-${project.version}-osx-arm.zip"/>
<echo message="Final package created at: ${project.build.directory}/diylc-${project.version}-osx-silicon.zip"/>
<echo message="Mac build process completed successfully!"/>
</target>
</configuration>
@@ -42,6 +42,7 @@ import org.diylc.serialization.ProjectFileManager;
import org.diylc.swing.gui.MainFrame;
import org.diylc.swing.gui.TemplateDialog;
import org.diylc.swing.plugins.file.ProjectDrawingProvider;
import org.diylc.utils.SwingUtils;
/**
* Main class that runs DIYLC.
@@ -137,6 +138,12 @@ public class DIYLCStarter {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// Fix tooltip colors for Linux systems where they may be unreadable
SwingUtils.fixTooltipColors();
// Apply configurable font scaling from VM argument: -Dorg.diylc.fontScaleFactor=X
org.diylc.utils.FontScalingUtils.applyFontScaling();
} catch (Exception e) {
LOG.error("Could not set Look&Feel", e);
}
@@ -324,7 +324,7 @@ public class MainFrame extends JFrame implements ISwingUI {
@Override
public void injectGUIComponent(JComponent component, int position, boolean collapsible, String visibilityConfigKey) throws BadPositionException {
LOG.info(String.format("injectGUIComponent(%s, %s, %s, %s)", component.getClass().getName(), position, collapsible, visibilityConfigKey));
// LOG.info(String.format("injectGUIComponent(%s, %s, %s, %s)", component.getClass().getName(), position, collapsible, visibilityConfigKey));
if (collapsible) {
CollapsiblePanel panel = new CollapsiblePanel(position, visibilityConfigKey);
panel.add(component);
@@ -354,8 +354,8 @@ public class MainFrame extends JFrame implements ISwingUI {
@Override
public void injectMenuAction(Action action, String menuName) {
LOG.info(String.format("injectMenuAction(%s, %s)", action == null ? "Separator" : action.getValue(Action.NAME),
menuName));
// LOG.info(String.format("injectMenuAction(%s, %s)", action == null ? "Separator" : action.getValue(Action.NAME),
// menuName));
if (menuName != null)
menuName = LangUtil.translate(menuName);
@@ -55,6 +55,21 @@ public class ActionBarPlugin implements IPlugIn {
private static final Logger LOG = Logger.getLogger(ActionBarPlugin.class);
private static final SupportOption[] SUPPORT_OPTIONS = new SupportOption[] {
new SupportOption(
"Enjoying DIYLC? Click here to buy me a coffee :)",
"Donate",
IconLoader.Donate,
HelpMenuPlugin.DONATE_URL
),
new SupportOption(
"Become a Patreon and enjoy perks",
"Join Patreon",
IconLoader.Patreon,
HelpMenuPlugin.PATREON_URL
)
};
private IPlugInPort plugInPort;
private ISwingUI swingUI;
@@ -62,13 +77,12 @@ public class ActionBarPlugin implements IPlugIn {
private ActionToolbar contextActionToolbar;
private ConfigToolbar configToolbar;
private JLabel donateLabel;
private static final String DONATE_HTML = LangUtil.translate("Enjoying DIYLC? Click here to buy me a coffee :)");
private static final String DONATE_HTML_SHORT = LangUtil.translate("Donate");
private SupportOption selectedOption;
private Consumer<Boolean> highlightConnectedAreasUpdateAction;
public ActionBarPlugin(ISwingUI swingUI) {
this.swingUI = swingUI;
this.selectedOption = SUPPORT_OPTIONS[(int)(Math.random() * SUPPORT_OPTIONS.length)];
}
public JPanel getActionPanel() {
@@ -110,12 +124,9 @@ public class ActionBarPlugin implements IPlugIn {
public JLabel getDonateLabel() {
if (donateLabel == null) {
donateLabel = new JLabel(DONATE_HTML);
// Map<TextAttribute, Integer> fontAttributes = new HashMap<TextAttribute, Integer>();
// fontAttributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
// donateLabel.setFont(donateLabel.getFont().deriveFont(fontAttributes));
donateLabel = new JLabel(selectedOption.longText);
donateLabel.setForeground(Color.blue);
donateLabel.setIcon(IconLoader.Donate.getIcon());
donateLabel.setIcon(selectedOption.icon.getIcon());
donateLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
this.swingUI.getOwnerFrame().addComponentListener(new ComponentAdapter() {
@@ -126,11 +137,15 @@ public class ActionBarPlugin implements IPlugIn {
@Override
public void componentResized(ComponentEvent e) {
if (rootPane.getWidth() < 940) {
if (!DONATE_HTML_SHORT.equals(donateLabel.getText()))
donateLabel.setText(DONATE_HTML_SHORT);
if (!selectedOption.shortText.equals(donateLabel.getText())) {
donateLabel.setText(selectedOption.shortText);
donateLabel.setIcon(selectedOption.icon.getIcon());
}
} else {
if (!DONATE_HTML.equals(donateLabel.getText()))
donateLabel.setText(DONATE_HTML);
if (!selectedOption.longText.equals(donateLabel.getText())) {
donateLabel.setText(selectedOption.longText);
donateLabel.setIcon(selectedOption.icon.getIcon());
}
}
}
});
@@ -140,9 +155,11 @@ public class ActionBarPlugin implements IPlugIn {
@Override
public void mouseClicked(MouseEvent e) {
try {
Utils.openURL(HelpMenuPlugin.DONATE_URL);
Utils.openURL(selectedOption.url);
} catch (Exception e1) {
swingUI.showMessage("Web browser launching failed. Please visit " + HelpMenuPlugin.DONATE_URL + " to donate. Thank you!", "Error", ISwingUI.INFORMATION_MESSAGE);
swingUI.showMessage("Web browser launching failed. Please visit " +
selectedOption.url +
" to support DIYLC. Thank you!", "Error", ISwingUI.INFORMATION_MESSAGE);
}
}
});
@@ -225,4 +242,16 @@ public class ActionBarPlugin implements IPlugIn {
OperationMode.HIGHLIGHT_CONNECTED_AREAS);
}
}
private record SupportOption(
String longText,
String shortText,
IconLoader icon,
String url
) {
public SupportOption {
longText = LangUtil.translate(longText);
shortText = LangUtil.translate(shortText);
}
}
}
@@ -45,7 +45,7 @@ public class AutoSavePlugin implements IPlugIn {
private static final Logger LOG = Logger.getLogger(AutoSavePlugin.class);
public static long BACKUP_FREQ_MS = 60 * 1000;
public static int MAX_TOTAL_SIZE_MB = 64;
public static int MAX_TOTAL_SIZE_MB = 1024;
private ExecutorService executor;
@@ -21,6 +21,7 @@
*/
package org.diylc.swing.plugins.canvas;
import java.awt.Cursor;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.event.InputEvent;
@@ -54,7 +55,17 @@ class CanvasDragGestureListener implements DragGestureListener {
}
canvasPanel.setClickInProgress(false);
presenter.dragStarted(dge.getDragOrigin(), dge.getDragAction(), forceReSelection);
dge.startDrag(presenter.getCursorAt(dge.getDragOrigin(), false, false, false), new EmptyTransferable(), new CanvasSourceListener(
// Get the appropriate cursor from the presenter (e.g., HAND_CURSOR over components)
// Preserve the cursor context (HAND_CURSOR, CROSSHAIR_CURSOR, etc.) for drag operations
// On Linux, DEFAULT_CURSOR during drag can show "No" cursor, so use MOVE_CURSOR as fallback
Cursor requestedCursor = presenter.getCursorAt(dge.getDragOrigin(), false, false, false);
Cursor dragCursor = requestedCursor;
// Only replace DEFAULT_CURSOR with MOVE_CURSOR on Linux to prevent "No" cursor issue
// Preserve HAND_CURSOR, CROSSHAIR_CURSOR, etc. as they are appropriate for drag operations
if (requestedCursor.getType() == Cursor.DEFAULT_CURSOR) {
dragCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
}
dge.startDrag(dragCursor, new EmptyTransferable(), new CanvasSourceListener(
presenter));
}
}
@@ -146,42 +146,35 @@ public class CanvasPanel extends JComponent implements Autoscroll {
}
private void initializeActions() {
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_QUOTE, 0), "repeatLast");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "clearSlot");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_DOWN_MASK), "zoomIn");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, KeyEvent.CTRL_DOWN_MASK), "zoomIn");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.META_DOWN_MASK), "zoomIn");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, KeyEvent.CTRL_DOWN_MASK), "zoomIn");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, KeyEvent.META_DOWN_MASK), "zoomIn");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, KeyEvent.META_DOWN_MASK), "zoomIn");
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), "zoomIn");
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0), "zoomIn");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, KeyEvent.CTRL_DOWN_MASK), "zoomOut");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, KeyEvent.CTRL_DOWN_MASK), "zoomOut");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, KeyEvent.META_DOWN_MASK), "zoomOut");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, KeyEvent.META_DOWN_MASK), "zoomOut");
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0), "zoomIn");
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, 0), "zoomIn");
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), "zoomOut");
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), "zoomOut");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_0, KeyEvent.CTRL_DOWN_MASK), "resetZoom");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD0, KeyEvent.CTRL_DOWN_MASK), "resetZoom");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_0, KeyEvent.META_DOWN_MASK), "resetZoom");
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD0, KeyEvent.META_DOWN_MASK), "resetZoom");
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_0, 0), "resetZoom");
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD0, 0), "resetZoom");
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_0, 0), "resetZoom");
getInputMap(JComponent.WHEN_FOCUSED)
.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD0, 0), "resetZoom");
for (int i = 1; i <= 12; i++) {
final int x = i;
@@ -198,36 +191,6 @@ public class CanvasPanel extends JComponent implements Autoscroll {
});
}
// getActionMap().put("zoomIn", new AbstractAction() {
//
// private static final long serialVersionUID = 1L;
//
// @Override
// public void actionPerformed(ActionEvent e) {
// LOG.debug("Keyboard zoom-in triggered");
// double oldZoom = CanvasPanel.this.plugInPort.getZoomLevel();
// Double[] availableZoomLevels = CanvasPanel.this.plugInPort.getAvailableZoomLevels();
// int index = Arrays.binarySearch(availableZoomLevels, oldZoom);
// if (index < availableZoomLevels.length - 1)
// CanvasPanel.this.plugInPort.setZoomLevel(availableZoomLevels[index + 1]);
// }
// });
//
// getActionMap().put("zoomOut", new AbstractAction() {
//
// private static final long serialVersionUID = 1L;
//
// @Override
// public void actionPerformed(ActionEvent e) {
// LOG.debug("Keyboard zoom-out triggered");
// double oldZoom = CanvasPanel.this.plugInPort.getZoomLevel();
// Double[] availableZoomLevels = CanvasPanel.this.plugInPort.getAvailableZoomLevels();
// int index = Arrays.binarySearch(availableZoomLevels, oldZoom);
// if (index > 0)
// CanvasPanel.this.plugInPort.setZoomLevel(availableZoomLevels[index - 1]);
// }
// });
getActionMap().put("resetZoom", new AbstractAction() {
private static final long serialVersionUID = 1L;
@@ -41,6 +41,7 @@ import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.IConfigListener;
import org.diylc.appframework.miscutils.IConfigurationManager;
import org.diylc.appframework.miscutils.Utils;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.swingframework.ruler.IRulerListener;
import org.diylc.swingframework.ruler.Ruler.InchSubdivision;
import org.diylc.swingframework.ruler.RulerScrollPane;
@@ -53,7 +54,6 @@ import org.diylc.common.BadPositionException;
import org.diylc.common.EventType;
import org.diylc.common.IPlugIn;
import org.diylc.common.IPlugInPort;
import org.diylc.core.IDIYComponent;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.presenter.Presenter;
@@ -68,6 +68,8 @@ public class CanvasPlugin implements IPlugIn{
private RulerScrollPane scrollPane;
private CanvasPanel canvasPanel;
private ComponentPopupMenu popupMenu;
private ResizeInfoTooltip resizeInfoTooltip;
private boolean resizingInProgress = false;
private IPlugInPort plugInPort;
private ISwingUI swingUI;
@@ -193,6 +195,7 @@ public class CanvasPlugin implements IPlugIn{
public CanvasPanel getCanvasPanel() {
if (canvasPanel == null) {
canvasPanel = new CanvasPanel(plugInPort, configManager);
resizeInfoTooltip = new ResizeInfoTooltip(canvasPanel, configManager);
canvasPanel.addMouseListener(new MouseAdapter() {
private MouseEvent pressedEvent;
@@ -271,10 +274,21 @@ public class CanvasPlugin implements IPlugIn{
public void mouseMoved(MouseEvent e) {
if (scrollPane.isMouseScrollMode())
return;
// if needed uncomment this code, it forces the cursor to refresh when it's stuck
// canvasPanel.setCursor(null);
canvasPanel.setCursor(plugInPort.getCursorAt(e.getPoint(), Utils.isMac() ? e.isMetaDown() : e.isControlDown(),
e.isShiftDown(), e.isAltDown()));
plugInPort.mouseMoved(e.getPoint(), Utils.isMac() ? e.isMetaDown() : e.isControlDown(),
e.isShiftDown(), e.isAltDown());
// Update tooltip position if resizing is in progress and tooltip is enabled, otherwise hide it
if (resizeInfoTooltip != null) {
if (resizingInProgress && configManager.readBoolean(IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true)) {
resizeInfoTooltip.updatePosition(e.getPoint());
} else {
resizeInfoTooltip.hideTooltip();
}
}
}
});
@@ -503,11 +517,13 @@ public class CanvasPlugin implements IPlugIn{
canvasPanel.repaint();
// Refresh selection bounds after we're done with painting to ensure we have traced the
// component areas
boolean panToSelection = params.length > 0 && Boolean.TRUE.equals(params[0]);
this.resizingInProgress = params.length > 1 && Boolean.TRUE.equals(params[1]);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (params.length > 0 && Boolean.TRUE.equals(params[0])) {
if (panToSelection) {
Rectangle2D selectionBounds = plugInPort.getSelectionBounds(true);
if (selectionBounds != null) {
Rectangle visibleRect = scrollPane.getVisibleRect();
@@ -517,8 +533,28 @@ public class CanvasPlugin implements IPlugIn{
);
}
}
if (configManager.readBoolean(IPlugInPort.SHOW_RULERS_KEY, true))
if (configManager.readBoolean(IPlugInPort.SHOW_RULERS_KEY, true)) {
scrollPane.setSelectionRectangle(plugInPort.getSelectionBounds(true));
}
if (CanvasPlugin.this.resizingInProgress && getCanvasPanel().getMousePosition() != null) {
// Only show tooltip if enabled in configuration
if (configManager.readBoolean(IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true)) {
ResizeDimensions resizeDimensions = plugInPort.getResizeDimensions();
if (resizeInfoTooltip != null) {
resizeInfoTooltip.update(resizeDimensions, getCanvasPanel().getMousePosition());
}
} else {
// Hide tooltip if disabled
if (resizeInfoTooltip != null) {
resizeInfoTooltip.hideTooltip();
}
}
} else {
// Hide tooltip when not resizing
if (resizeInfoTooltip != null) {
resizeInfoTooltip.hideTooltip();
}
}
}
});
break;
@@ -561,25 +597,4 @@ public class CanvasPlugin implements IPlugIn{
double extraSpace = CanvasPlugin.this.plugInPort.getExtraSpace();
getScrollPane().setZeroLocation(new Point2D.Double(extraSpace, extraSpace));
}
class LockAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private IDIYComponent<?> component;
private boolean locked;
public LockAction(IDIYComponent<?> component, boolean locked) {
super();
this.locked = locked;
this.component = component;
putValue(AbstractAction.NAME, component.getName());
}
@Override
public void actionPerformed(ActionEvent e) {
plugInPort.lockComponent(component, locked);
}
}
}
@@ -21,11 +21,16 @@
*/
package org.diylc.swing.plugins.canvas;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.SwingUtilities;
import org.diylc.common.IPlugInPort;
@@ -50,13 +55,16 @@ class CanvasSourceListener implements DragSourceListener {
}
@Override
public void dragEnter(DragSourceDragEvent dsde) {}
public void dragEnter(DragSourceDragEvent dsde) {
updateCursor(dsde);
}
@Override
public void dragExit(DragSourceEvent dse) {}
@Override
public void dragOver(DragSourceDragEvent dsde) {
updateCursor(dsde);
Point p = dsde.getDragSourceContext().getComponent().getMousePosition();
if (p != null) {
dsde.getDragSourceContext().getComponent().firePropertyChange("dragPoint", p.x, p.y);
@@ -64,5 +72,26 @@ class CanvasSourceListener implements DragSourceListener {
}
@Override
public void dropActionChanged(DragSourceDragEvent dsde) {}
public void dropActionChanged(DragSourceDragEvent dsde) {
updateCursor(dsde);
}
/**
* Updates the cursor during drag operations using the presenter as the single source of truth.
* If the presenter returns DEFAULT_CURSOR, uses MOVE_CURSOR to prevent "No" cursor issues.
*/
private void updateCursor(DragSourceDragEvent dsde) {
Point mousePos = dsde.getDragSourceContext().getComponent().getMousePosition();
if (mousePos != null) {
// Use presenter as single source of truth for cursor
Cursor requestedCursor = presenter.getCursorAt(mousePos, false, false, false);
Cursor dragCursor = requestedCursor;
// If DEFAULT_CURSOR, use MOVE_CURSOR to prevent "No" cursor during drag operations
if (requestedCursor.getType() == Cursor.DEFAULT_CURSOR) {
dragCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
}
dsde.getDragSourceContext().setCursor(null);
dsde.getDragSourceContext().setCursor(dragCursor);
}
}
}
@@ -21,15 +21,13 @@
*/
package org.diylc.swing.plugins.canvas;
import java.awt.Point;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.*;
import java.awt.dnd.*;
import org.diylc.common.IPlugInPort;
import javax.swing.*;
/**
* {@link DropTargetListener} for {@link CanvasPanel}.
@@ -106,10 +104,35 @@ class CanvasTargetListener implements DropTargetListener {
@Override
public void drop(DropTargetDropEvent dtde) {
presenter.dragEnded(dtde.getLocation());
updateCursor(dtde);
}
@Override
public void dropActionChanged(DropTargetDragEvent dtde) {
presenter.dragActionChanged(dtde.getDropAction());
}
/**
* Updates the cursor during drag operations using the presenter as the single source of truth.
* If the presenter returns DEFAULT_CURSOR, uses MOVE_CURSOR to prevent "No" cursor issues.
*/
private void updateCursor(DropTargetDropEvent dsde) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Point mousePos = dsde.getDropTargetContext().getComponent().getMousePosition();
if (mousePos != null) {
// Use presenter as single source of truth for cursor
Cursor requestedCursor = presenter.getCursorAt(mousePos, false, false, false);
Cursor dragCursor = requestedCursor;
// If DEFAULT_CURSOR, use MOVE_CURSOR to prevent "No" cursor during drag operations
if (requestedCursor.getType() == Cursor.DEFAULT_CURSOR) {
dragCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
}
dsde.getDropTargetContext().getComponent().setCursor(null);
dsde.getDropTargetContext().getComponent().setCursor(dragCursor);
}
}
});
}
}
@@ -0,0 +1,225 @@
/*
*
* 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.swing.plugins.canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Window;
import java.text.DecimalFormat;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import org.diylc.appframework.miscutils.IConfigurationManager;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.core.measures.Size;
/**
* A lightweight tooltip window that displays resize dimensions and tracks the mouse cursor.
*/
public class ResizeInfoTooltip {
private static final int OFFSET_X = 16;
private static final int OFFSET_Y = 16;
private static final DecimalFormat SIZE_FORMAT = new DecimalFormat("0.00");
private JWindow tooltipWindow;
private JLabel tooltipLabel;
private Component parentComponent;
private ResizeDimensions currentDimensions;
public ResizeInfoTooltip(Component parentComponent, IConfigurationManager<?> configManager) {
this.parentComponent = parentComponent;
initializeTooltip();
}
private void initializeTooltip() {
Window parentWindow = SwingUtilities.getWindowAncestor(parentComponent);
if (parentWindow == null) {
return;
}
tooltipWindow = new JWindow(parentWindow);
tooltipLabel = new JLabel() {
private static final long serialVersionUID = 1L;
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
super.paintComponent(g2d);
g2d.dispose();
}
};
// Use system tooltip font and colors
Font tooltipFont = UIManager.getFont("ToolTip.font");
if (tooltipFont != null) {
tooltipLabel.setFont(tooltipFont);
}
Color tooltipBackground = UIManager.getColor("ToolTip.background");
Color tooltipForeground = UIManager.getColor("ToolTip.foreground");
Color tooltipBorderColor = UIManager.getColor("ToolTip.border");
if (tooltipBorderColor == null) {
tooltipBorderColor = Color.GRAY;
}
tooltipLabel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(tooltipBorderColor, 1),
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
tooltipLabel.setOpaque(true);
tooltipLabel.setBackground(tooltipBackground != null ? tooltipBackground : new Color(255, 255, 220));
tooltipLabel.setForeground(tooltipForeground != null ? tooltipForeground : Color.BLACK);
tooltipWindow.add(tooltipLabel);
tooltipWindow.setFocusableWindowState(false);
tooltipWindow.setAlwaysOnTop(true);
}
/**
* Updates the tooltip with new dimensions and mouse position.
*/
public void update(ResizeDimensions dimensions, Point mousePosition) {
this.currentDimensions = dimensions;
if (dimensions == null || mousePosition == null) {
hideTooltip();
return;
}
if (tooltipWindow == null) {
initializeTooltip();
if (tooltipWindow == null) {
return;
}
}
// Format the dimensions
String text = formatDimensions(dimensions);
tooltipLabel.setText(text);
tooltipLabel.setPreferredSize(null); // Reset to calculate preferred size
tooltipWindow.pack();
// Position the tooltip near the mouse cursor
updateTooltipPosition(mousePosition);
tooltipWindow.setVisible(true);
}
/**
* Updates the tooltip position based on the current mouse position.
*/
public void updatePosition(Point mousePosition) {
if (currentDimensions != null && mousePosition != null && tooltipWindow != null && tooltipWindow.isVisible()) {
updateTooltipPosition(mousePosition);
}
}
private void updateTooltipPosition(Point mousePosition) {
if (tooltipWindow == null || parentComponent == null) {
return;
}
// Convert mouse position to screen coordinates
Point screenPoint = new Point(mousePosition);
SwingUtilities.convertPointToScreen(screenPoint, parentComponent);
// Position tooltip 16 pixels down and right from cursor
int x = screenPoint.x + OFFSET_X;
int y = screenPoint.y + OFFSET_Y;
// Ensure tooltip stays on screen
Dimension tooltipSize = tooltipWindow.getSize();
if (tooltipSize == null) {
tooltipWindow.pack();
tooltipSize = tooltipWindow.getSize();
}
if (tooltipSize != null) {
java.awt.Toolkit toolkit = java.awt.Toolkit.getDefaultToolkit();
Dimension screenSize = toolkit.getScreenSize();
// Adjust if tooltip would go off screen
if (x + tooltipSize.width > screenSize.width) {
x = screenPoint.x - tooltipSize.width - OFFSET_X;
}
if (y + tooltipSize.height > screenSize.height) {
y = screenPoint.y - tooltipSize.height - OFFSET_Y;
}
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
}
tooltipWindow.setLocation(x, y);
}
private String formatDimensions(ResizeDimensions dimensions) {
StringBuilder sb = new StringBuilder("<html>");
// Format width
Size width = dimensions.width();
String widthUnit = width.getUnit().toString();
sb.append(String.format("W: %s %s", SIZE_FORMAT.format(width.getValue()), widthUnit));
// Format height
Size height = dimensions.height();
String heightUnit = height.getUnit().toString();
sb.append(String.format("<br>H: %s %s", SIZE_FORMAT.format(height.getValue()), heightUnit));
// Format length if available
Size length = dimensions.length();
if (length != null) {
String lengthUnit = length.getUnit().toString();
sb.append(String.format("<br>L: %s %s", SIZE_FORMAT.format(length.getValue()), lengthUnit));
}
sb.append("</html>");
return sb.toString();
}
/**
* Hides the tooltip.
*/
public void hideTooltip() {
if (tooltipWindow != null) {
tooltipWindow.setVisible(false);
}
currentDimensions = null;
}
/**
* Disposes the tooltip window.
*/
public void dispose() {
if (tooltipWindow != null) {
tooltipWindow.dispose();
tooltipWindow = null;
}
}
}
@@ -104,6 +104,7 @@ public class CloudPlugIn implements IPlugIn {
getManageAccountAction().setEnabled(false);
getChangePasswordAction().setEnabled(false);
getLogOutAction().setEnabled(false);
getConnectPatreonAction().setEnabled(false);
}
public CloudBrowserFrame getCloudBrowser() {
@@ -179,6 +180,7 @@ public class CloudPlugIn implements IPlugIn {
getUploadAction().setEnabled(true);
getManageProjectsAction().setEnabled(true);
getChangePasswordAction().setEnabled(true);
getConnectPatreonAction().setEnabled(true);
}
public void loggedOut() {
@@ -190,6 +192,7 @@ public class CloudPlugIn implements IPlugIn {
getUploadAction().setEnabled(false);
getManageProjectsAction().setEnabled(false);
getChangePasswordAction().setEnabled(false);
getConnectPatreonAction().setEnabled(false);
}
// Getters for action classes
@@ -60,7 +60,6 @@ public class ConnectPatreonAction extends AbstractAction {
return;
}
String username = plugInPort.getCloudService().getCurrentUsername();
username ="a b c";
String url = "http://diy-fever.com/diylc/api/v1/ai/patreon_login.php?username=" + URLEncoder.encode(username, StandardCharsets.UTF_8);
try {
java.awt.Desktop.getDesktop().browse(new java.net.URI(url));
@@ -37,6 +37,7 @@ import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.UIManager;
import org.diylc.swingframework.ButtonDialog;
@@ -47,6 +48,14 @@ public class UserEditDialog extends ButtonDialog {
private static final long serialVersionUID = 1L;
private static final String BULLET = "&nbsp;&nbsp;&nbsp;&#8226;&nbsp;";
private static final String PASSWORD_RULES_HTML = "<html>" + BULLET
+ "Password must be at least 8 characters long<br>" + BULLET
+ "Password must contain at least one letter<br>" + BULLET
+ "Password must contain at least one number<br>" + BULLET
+ "Passwords must match exactly<br>"
+ "</html>";
private JPanel mainPanel;
private JTextField userNameField;
@@ -65,6 +74,8 @@ public class UserEditDialog extends ButtonDialog {
private UserEntity existingEntity;
private JLabel passwordRulesLabel;
public UserEditDialog(JFrame owner, UserEntity existingEntity) {
super(owner, existingEntity == null ? "New Account" : "Manage Account", new String[] {OK, CANCEL});
@@ -117,16 +128,33 @@ public class UserEditDialog extends ButtonDialog {
gbc.gridy = 2;
mainPanel.add(new JLabel("Confirm Password:"), gbc);
// Add password rules hint pane
gbc.gridy = 3;
gbc.gridwidth = 2;
gbc.gridx = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(4, 2, 4, 2); // Add more vertical padding
mainPanel.add(getPasswordRulesLabel(), gbc);
// Reset gridwidth and gridx for remaining fields
gbc.gridwidth = 1;
gbc.gridx = 1;
gbc.insets = new Insets(2, 2, 2, 2); // Reset insets
gbc.gridy = 4;
}
gbc.gridy = 3;
gbc.gridy = existingEntity == null ? 4 : 3;
gbc.gridx = 0;
mainPanel.add(new JLabel("eMail:"), gbc);
gbc.gridy = 4;
mainPanel.add(new JLabel("Website:"), gbc);
gbc.gridy = existingEntity == null ? 5 : 4;
gbc.gridx = 0;
mainPanel.add(new JLabel("Website (optional):"), gbc);
gbc.gridy = 5;
mainPanel.add(new JLabel("Short Bio:"), gbc);
gbc.gridy = existingEntity == null ? 6 : 5;
gbc.gridx = 0;
mainPanel.add(new JLabel("Short Bio (optional):"), gbc);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
@@ -144,13 +172,13 @@ public class UserEditDialog extends ButtonDialog {
mainPanel.add(getConfirmPasswordField(), gbc);
}
gbc.gridy = 3;
gbc.gridy = existingEntity == null ? 4 : 3;
mainPanel.add(getEmailField(), gbc);
gbc.gridy = 4;
gbc.gridy = existingEntity == null ? 5 : 4;
mainPanel.add(getWebsiteField(), gbc);
gbc.gridy = 5;
gbc.gridy = existingEntity == null ? 6 : 5;
gbc.fill = GridBagConstraints.BOTH;
mainPanel.add(getBioPane(), gbc);
}
@@ -161,7 +189,16 @@ public class UserEditDialog extends ButtonDialog {
this.userName = getUserNameField().getText();
String password = new String(getPasswordField().getPassword());
String confirmPassword = new String(getConfirmPasswordField().getPassword());
if (password.equals(confirmPassword)) {
// Password validation
boolean hasValidPassword = true;
if (existingEntity == null) {
hasValidPassword = password.length() >= 8 &&
password.matches(".*[a-zA-Z].*") && // at least one letter
password.matches(".*\\d.*"); // at least one number
}
if (password.equals(confirmPassword) && hasValidPassword) {
this.password = password;
} else {
this.password = null;
@@ -342,4 +379,16 @@ public class UserEditDialog extends ButtonDialog {
}
return bioArea;
}
private JLabel getPasswordRulesLabel() {
if (passwordRulesLabel == null) {
passwordRulesLabel = new JLabel();
passwordRulesLabel.setOpaque(true);
passwordRulesLabel.setBackground(UIManager.getColor("ToolTip.background"));
passwordRulesLabel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEtchedBorder(),
BorderFactory.createEmptyBorder(2, 2, 2, 2)));
passwordRulesLabel.setText(PASSWORD_RULES_HTML);
}
return passwordRulesLabel;
}
}
@@ -81,7 +81,7 @@ public class CloudBrowserFrame extends JFrame implements ISimpleView {
super(TITLE);
this.swingUI = swingUI;
this.setIconImage(IconLoader.Cloud.getImage());
this.setPreferredSize(new Dimension(700, 640));
this.setPreferredSize(new Dimension(800, 640));
this.plugInPort = plugInPort;
this.searchSession = new SearchSession(plugInPort);
@@ -158,6 +158,10 @@ public class ConfigPlugin implements IPlugIn {
ActionFactory.getInstance().createConfigAction(plugInPort, "Show Rulers", IPlugInPort.SHOW_RULERS_KEY, true),
CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Show Resize Dimensions",
IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true), CONFIG_MENU);
swingUI.injectSubmenu(SNAP_MENU, IconLoader.GraphEdgeDirected.getIcon(), CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createToggleAction(IPlugInPort.SNAP_TO_NONE, IPlugInPort.SNAP_TO_KEY, SNAP_MENU, IPlugInPort.SNAP_TO_DEFAULT),
@@ -43,11 +43,13 @@ public class FileMenuPlugin implements IPlugIn, IDynamicSubmenuHandler {
private static final String FILE_TITLE = "File";
private static final String TRACE_MASK_TITLE = "Trace Mask";
private static final String SILK_SCREEN_TITLE = "Silkscreen";
private static final String INTEGRATION_TITLE = "Integration";
private static final String EXPORT_TITLE = "Export";
private static final String PROJECT_TITLE = "Project";
private ProjectDrawingProvider drawingProvider;
private TraceMaskDrawingProvider traceMaskDrawingProvider;
private SilkScreenDrawingProvider silkScreenDrawingProvider;
private ISwingUI swingUI;
private IPlugInPort plugInPort;
@@ -62,6 +64,7 @@ public class FileMenuPlugin implements IPlugIn, IDynamicSubmenuHandler {
this.plugInPort = plugInPort;
this.drawingProvider = new ProjectDrawingProvider(plugInPort, false, true, false);
this.traceMaskDrawingProvider = new TraceMaskDrawingProvider(plugInPort);
this.silkScreenDrawingProvider = new SilkScreenDrawingProvider(plugInPort);
ActionFactory actionFactory = ActionFactory.getInstance();
swingUI.injectMenuAction(actionFactory.createNewAction(plugInPort), FILE_TITLE);
@@ -73,15 +76,17 @@ public class FileMenuPlugin implements IPlugIn, IDynamicSubmenuHandler {
swingUI.injectMenuAction(actionFactory.createSaveAsAction(plugInPort, swingUI), FILE_TITLE);
swingUI.injectDynamicSubmenu("Recent Files", IconLoader.History.getIcon(), FILE_TITLE, this);
swingUI.injectMenuAction(null, FILE_TITLE);
swingUI.injectSubmenu(EXPORT_TITLE, IconLoader.Export.getIcon(), FILE_TITLE);
swingUI.injectSubmenu(PROJECT_TITLE, IconLoader.Project.getIcon(), FILE_TITLE);
swingUI.injectMenuAction(
actionFactory.createExportPDFAction(plugInPort, drawingProvider, swingUI, ""), EXPORT_TITLE);
actionFactory.createExportPDFAction(plugInPort, drawingProvider, swingUI, ""), PROJECT_TITLE);
swingUI.injectMenuAction(
actionFactory.createExportPNGAction(plugInPort, drawingProvider, swingUI, ""), EXPORT_TITLE);
actionFactory.createExportPNGAction(plugInPort, drawingProvider, swingUI, ""), PROJECT_TITLE);
swingUI.injectMenuAction(
actionFactory.createExportGerberAction(plugInPort, swingUI), EXPORT_TITLE);
actionFactory.createExportGerberAction(plugInPort, swingUI), PROJECT_TITLE);
swingUI.injectMenuAction(actionFactory.createPrintAction(drawingProvider, swingUI,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), FILE_TITLE);
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), PROJECT_TITLE);
swingUI.injectSubmenu(TRACE_MASK_TITLE, IconLoader.TraceMask.getIcon(), FILE_TITLE);
swingUI.injectMenuAction(actionFactory.createExportPDFAction(plugInPort,
traceMaskDrawingProvider, swingUI, " (mask)"), TRACE_MASK_TITLE);
@@ -92,6 +97,17 @@ public class FileMenuPlugin implements IPlugIn, IDynamicSubmenuHandler {
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | KeyEvent.SHIFT_DOWN_MASK),
TRACE_MASK_TITLE);
swingUI.injectSubmenu(SILK_SCREEN_TITLE, IconLoader.Silkscreen.getIcon(), FILE_TITLE);
swingUI.injectMenuAction(actionFactory.createExportPDFAction(plugInPort,
silkScreenDrawingProvider, swingUI, " (silkscreen)"), SILK_SCREEN_TITLE);
swingUI.injectMenuAction(actionFactory.createExportPNGAction(plugInPort,
silkScreenDrawingProvider, swingUI, " (silkscreen)"), SILK_SCREEN_TITLE);
swingUI.injectMenuAction(
actionFactory.createPrintAction(silkScreenDrawingProvider, swingUI,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | KeyEvent.ALT_DOWN_MASK),
SILK_SCREEN_TITLE);
swingUI.injectMenuAction(null, FILE_TITLE);
swingUI.injectSubmenu(INTEGRATION_TITLE, IconLoader.Node.getIcon(), FILE_TITLE);
swingUI.injectMenuAction(actionFactory.createImportBlocksAction(swingUI, plugInPort),
@@ -0,0 +1,63 @@
/*
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.swing.plugins.file;
import org.diylc.common.DrawOption;
import org.diylc.common.IPlugInPort;
import org.diylc.filter.PCBLayerFilter;
import org.diylc.filter.SilkScreenFilter;
import org.diylc.swingframework.IDrawingProvider;
import java.awt.*;
import java.util.*;
/**
* {@link IDrawingProvider} implementation that uses {@link IPlugInPort} to draw a project onto the
* canvas.
*
* @author Branislav Stojkovic
*/
public class SilkScreenDrawingProvider implements IDrawingProvider {
private IPlugInPort plugInPort;
public SilkScreenDrawingProvider(IPlugInPort plugInPort) {
super();
this.plugInPort = plugInPort;
}
@Override
public Dimension getSize() {
return plugInPort.getCanvasDimensions(false, false);
}
@Override
public void draw(int page, Graphics g, double zoomFactor) {
plugInPort.draw((Graphics2D) g, EnumSet.of(DrawOption.ANTIALIASING, DrawOption.OUTLINE_MODE, DrawOption.MONOCHROMATIC),
new SilkScreenFilter(), zoomFactor, null, null);
}
@Override
public int getPageCount() {
return 1;
}
}
@@ -37,7 +37,7 @@ import org.diylc.common.DrawOption;
import org.diylc.common.IPlugInPort;
import org.diylc.common.PCBLayer;
import org.diylc.core.IDIYComponent;
import org.diylc.presenter.PCBLayerFilter;
import org.diylc.filter.PCBLayerFilter;
/**
* {@link IDrawingProvider} implementation that uses {@link IPlugInPort} to draw a project onto the
@@ -63,10 +63,11 @@ public class HelpMenuPlugin implements IPlugIn {
public static String PLUGIN_URL = "https://github.com/bancika/diy-layout-creator/blob/wiki/PluginAPI.md";
public static String BUG_URL = "https://github.com/bancika/diy-layout-creator/issues";
public static String DONATE_URL = "http://diy-fever.com/donate";
public static String PATREON_URL = "http://diy-fever.com/patreon";
private IPlugInPort plugInPort;
private AboutDialog aboutDialog;
private ISwingUI swingUI;
private final ISwingUI swingUI;
public HelpMenuPlugin(ISwingUI swingUI) {
this.swingUI = swingUI;
@@ -0,0 +1,143 @@
/*
*
* 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.utils;
import java.awt.Font;
import java.util.Enumeration;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.IPlugInPort;
/**
* Utility class for applying a configurable font scaling factor to all UI components.
*
* The scaling factor is stored in the application configuration and can be set
* programmatically or through the configuration system.
* For example, a factor of 1.5 will make all fonts 50% larger.
*
* @author Branislav Stojkovic
*/
public class FontScalingUtils {
private static final double DEFAULT_FONT_SCALE_FACTOR = 1.0;
private static final double MIN_FONT_SCALE_FACTOR = 0.5;
private static final double MAX_FONT_SCALE_FACTOR = 3.0;
/**
* Gets the font scaling factor from ConfigurationManager.
*
* @return The scaling factor (defaults to 1.0 if not set or invalid)
*/
public static double getFontScaleFactor() {
try {
Object factorObj = ConfigurationManager.getInstance().readObject(
IPlugInPort.FONT_SCALE_FACTOR_KEY, DEFAULT_FONT_SCALE_FACTOR);
if (factorObj instanceof Number) {
double factor = ((Number) factorObj).doubleValue();
// Validate the factor is reasonable (between 0.5 and 3.0)
if (factor >= MIN_FONT_SCALE_FACTOR && factor <= MAX_FONT_SCALE_FACTOR) {
return factor;
} else {
System.err.println("Warning: Font scale factor " + factor + " is out of range ("
+ MIN_FONT_SCALE_FACTOR + "-" + MAX_FONT_SCALE_FACTOR + "). Using default "
+ DEFAULT_FONT_SCALE_FACTOR);
return DEFAULT_FONT_SCALE_FACTOR;
}
} else if (factorObj instanceof String) {
// Handle string values (for backward compatibility or manual config editing)
try {
double factor = Double.parseDouble((String) factorObj);
if (factor >= MIN_FONT_SCALE_FACTOR && factor <= MAX_FONT_SCALE_FACTOR) {
return factor;
}
} catch (NumberFormatException e) {
// Invalid string format, use default
}
}
} catch (Exception e) {
System.err.println("Warning: Could not read font scale factor from configuration: " + e.getMessage());
}
return DEFAULT_FONT_SCALE_FACTOR;
}
/**
* Sets the font scaling factor in ConfigurationManager.
*
* @param factor The scaling factor (must be between 0.5 and 3.0)
* @return true if the factor was set successfully, false if invalid
*/
public static boolean setFontScaleFactor(double factor) {
if (factor < MIN_FONT_SCALE_FACTOR || factor > MAX_FONT_SCALE_FACTOR) {
return false;
}
try {
ConfigurationManager.getInstance().writeValue(IPlugInPort.FONT_SCALE_FACTOR_KEY, factor);
return true;
} catch (Exception e) {
System.err.println("Warning: Could not write font scale factor to configuration: " + e.getMessage());
return false;
}
}
/**
* Applies the font scaling factor to all UIManager font defaults.
* This should be called once during application initialization, after setting the
* Look and Feel but before creating any UI components.
*
* The scaling factor is read from ConfigurationManager using the key
* IPlugInPort.FONT_SCALE_FACTOR_KEY. The factor should be a number between 0.5 and 3.0
* (e.g., 1.5 for 50% larger fonts). Use setFontScaleFactor() to change the value
* programmatically.
*/
public static void applyFontScaling() {
double scaleFactor = getFontScaleFactor();
// If factor is 1.0, no scaling needed
if (scaleFactor == 1.0) {
return;
}
try {
UIDefaults defaults = UIManager.getDefaults();
Enumeration<Object> keys = defaults.keys();
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
Object value = defaults.get(key);
// Check if this is a font property
if (value instanceof Font font) {
float newSize = (float) (font.getSize() * scaleFactor);
Font scaledFont = font.deriveFont(newSize);
defaults.put(key, scaledFont);
}
}
} catch (Exception e) {
System.err.println("Warning: Could not apply font scaling: " + e.getMessage());
}
}
}
@@ -72,7 +72,8 @@ public enum IconLoader {
ComponentLarge("component_large.png"), ImportNetlist("import_netlist.png"), SplashIC("splash_ic.png"),
ComponentPreferences("component_preferences.png"), ComponentReplace("component_replace.png"), Barcode("barcode.png"),
DocumentGerber("document_gerber.png"), DocumentX2("document_x2.png"), Export("export.png"), Multimeter("multimeter.png"),
Patreon("patreon.png"), Brain("brain.png"), Tables("tables.png");
Patreon("patreon.png"), Brain("brain.png"), Tables("tables.png"), Silkscreen("silkscreen.png"),
Project("project.png");
protected String name;
@@ -0,0 +1,71 @@
/*
*
* 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.utils;
import java.awt.Color;
import java.util.function.Function;
import javax.swing.UIManager;
/**
* Utility class for Swing UI-related operations.
*
* @author Branislav Stojkovic
*/
public class SwingUtils {
/**
* Fixes tooltip colors for Linux systems where they may be unreadable.
* Some Linux L&F themes set white text on light backgrounds, making tooltips invisible.
* This method ensures tooltips always have dark text on light background for readability.
*
* Should be called after UIManager.setLookAndFeel() to override system defaults.
*/
public static void fixTooltipColors() {
Color tooltipBg = UIManager.getColor("ToolTip.background");
Color tooltipFg = UIManager.getColor("ToolTip.foreground");
// Helper function to calculate brightness (0.0 = black, 1.0 = white)
Function<Color, Double> getBrightness = (c) -> {
return (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255.0;
};
// Check if we have a problematic color combination (light text on light background)
boolean needsFix = false;
if (tooltipFg != null && tooltipBg != null) {
double fgBrightness = getBrightness.apply(tooltipFg);
double bgBrightness = getBrightness.apply(tooltipBg);
// If both foreground and background are light, it's unreadable
if (fgBrightness > 0.7 && bgBrightness > 0.5) {
needsFix = true;
}
} else if (tooltipFg == null || tooltipBg == null) {
// Missing color definitions - set safe defaults
needsFix = true;
}
if (needsFix) {
UIManager.put("ToolTip.background", new Color(255, 255, 220)); // Light yellow
UIManager.put("ToolTip.foreground", Color.BLACK);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

@@ -195,6 +195,7 @@ Image|Bild
Immediate Only|Nur sofort
Import|Importieren
Import Building Blocks|Bausteine importieren
Import Netlist|Netlist importieren
Import Variants|Varianten importieren
In-Line D-44|Inline D-44
In-line|Inline
@@ -231,6 +232,7 @@ Loadline Editor|Kennliniendiagramm
Lock|Sperren
Log|Log
Log In|Anmelden
Look for|Suchen
Log Out|Abmelden
Lug Size|Lötpadgröße
Lugs|Lötpad
@@ -303,6 +305,7 @@ Power Rating|Nennleistung
Power Strip|Steckdosenleiste
Primary CT|Primärer Mittelabgriff
Print...|Drucken...
Project|Projekt
Project Cloud|Projekt Cloud
Public Announcement|Öffentliche Ankündigung
Radius|Radius
@@ -360,6 +363,7 @@ Shaft Up|Achse nach oben
Shape|Form
Shapes|Formen
Show Grid|Raster anzeigen
Show Resize Dimensions|Größenänderungsdimensionen anzeigen
Show Rulers|Lineale anzeigen
Shroud|Wanne
Shrouded|Abgeschirmt
@@ -457,3 +461,34 @@ Zoom|Vergrößern
base of 10|Zehntel
base of 2|Achtel
hold %s and drag to unstick from|Halten Sie X gedrückt und ziehen Sie, zum lösen von
Are you sure you want to clear the chat history for this project? This action cannot be undone!|Sind Sie sicher, dass Sie den Chat-Verlauf für dieses Projekt löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden!
Component reordering is possible only when Project Explorer is sorted by z-index.|Komponenten können nur neu angeordnet werden, wenn der Projekt-Explorer nach z-index sortiert ist.
Could not apply changes. Check the log for details.|Änderungen konnten nicht angewendet werden. Überprüfen Sie die Protokolldatei für Details.
Exporting outline for board %s to file %s|Exportiere Umriss für Platte %s in Datei %s
Failed to export the project to gerber. Please check the log for details|Fehler beim Exportieren des Projekts zu Gerber. Bitte überprüfen Sie die Protokolldatei für Details
Forgot Password?|Passwort vergessen?
Gerber Export|Gerber Export
Gerber export completed successfully. The following files were created:%s|Gerber-Export erfolgreich abgeschlossen. Die folgenden Dateien wurden erstellt:%s
Get Premium|Premium erhalten
Netlist files|Netlist-Dateien
No proximity issues detected.|Keine Näherungsprobleme erkannt.
Nothing to export.|Nichts zu exportieren.
Password|Passwort
Send|Senden
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?|Es gibt einige Komponenten, die außerhalb der Grenzen der Platten liegen.%sSie werden ignoriert, es sei denn, Sie fügen eine Plattenkomponente darunter hinzu.%sMöchten Sie fortfahren?
User Name|Benutzername
AI Circuit Analyzer|KI-Schaltungsanalysator
Cloud|Cloud
Compare With...|Vergleichen mit...
Connect to Patreon|Mit Patreon verbinden
Export|Exportieren
Export to Gerber|Als Gerber exportieren
Generate DIYLC Netlist (excl. Switches)|DIYLC-Netzliste erstellen (ohne Schalter)
Generate DIYLC Netlist (incl. Switches)|DIYLC-Netzliste erstellen (mit Schaltern)
Generate Spice Netlist (excl. Switches)|Spice-Netzliste erstellen (ohne Schalter)
Generate Spice Netlist (incl. Switches)|Spice-Netzliste erstellen (mit Schaltern)
Silkscreen|Siebdruck
Virtual Multimeter (excl. Switches)|Virtuelles Multimeter (ohne Schalter)
Virtual Multimeter (incl. Switches)|Virtuelles Multimeter (mit Schaltern)
Become a Patreon and enjoy perks|Werden Sie Patreon und genießen Sie Vorteile
Join Patreon|Patreon beitreten
@@ -300,6 +300,7 @@ Power Rating
Power Strip
Primary CT
Print...
Project
Project Cloud
Public Announcement
Radius
@@ -351,6 +352,7 @@ Shape
Shapes
Show Bracket
Show Grid
Show Resize Dimensions
Show Rulers
Shroud
Shrouded
@@ -462,4 +464,35 @@ Update Axis
Apply Variant
Save as Building Block
Select
Unlock
Unlock
Are you sure you want to clear the chat history for this project? This action cannot be undone!
Component reordering is possible only when Project Explorer is sorted by z-index.
Could not apply changes. Check the log for details.
Exporting outline for board %s to file %s
Failed to export the project to gerber. Please check the log for details
Forgot Password?
Gerber Export
Gerber export completed successfully. The following files were created:%s
Get Premium
Netlist files
No proximity issues detected.
Nothing to export.
Password
Send
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?
User Name
AI Circuit Analyzer
Cloud
Compare With...
Connect to Patreon
Export
Export to Gerber
Generate DIYLC Netlist (excl. Switches)
Generate DIYLC Netlist (incl. Switches)
Generate Spice Netlist (excl. Switches)
Generate Spice Netlist (incl. Switches)
Silkscreen
Virtual Multimeter (excl. Switches)
Virtual Multimeter (incl. Switches)
Become a Patreon and enjoy perks
Join Patreon

Some files were not shown because too many files have changed in this diff Show More