Compare commits

..

79 Commits

Author SHA1 Message Date
Bane Stojkovic 525024ac56 Merge branch 'master' into explorer-tree 2026-01-07 19:39:38 +01:00
Bane Stojkovic 2383f9ae77 'Potentiometer Panel' can now render numerical lug markers for easier identification 2026-01-06 19:46:14 +01:00
Bane Stojkovic 14ec18dfef Update update.xml 2026-01-06 19:06:10 +01:00
Bane Stojkovic 2f492c8fa8 Ability to resize 'Transformer Core' schematic symbol after it's already placed 2026-01-06 19:06:04 +01:00
Bane Stojkovic 9db43cd2f4 Make the label of schematic symbols (e.g. 'Resistor Symbol') clickable as well 2026-01-06 18:54:08 +01:00
Bane Stojkovic 344c238f73 Fixed the offset of terminals on a DP4T Tele type of 'Lever Switch' to be aligned with the physical switch 2026-01-05 21:30:13 +01:00
Bane Stojkovic 3376aa995a 'Open 1/8" Jack' and 'Open 1/4" Jack' can be rotated 2026-01-05 21:10:10 +01:00
Bane Stojkovic ec75f1b6eb 'Cliff 1/8" Jack' can be rotated 2026-01-05 21:06:22 +01:00
Bane Stojkovic 5f2b20ea41 New lines at the end of the 'Label' text get erroneously rendered as <br> 2026-01-05 20:53:00 +01:00
Bane Stojkovic 9d130c0ff3 Minor cleamup 2026-01-05 20:17:17 +01:00
Martin Morrell 119c41dc86 Add Freeway 3x3-05 (Ultra) Switch (#989) 2026-01-05 20:10:37 +01:00
Bane Stojkovic c5613a6fea 'Auto-Wrap Label' retains line breaks from the input text 2026-01-02 22:16:17 +01:00
Bane Stojkovic 479bd50540 Label adjustment 2026-01-02 21:42:09 +01:00
Bane Stojkovic 502a4b8d39 Tactile switch improvements 2026-01-02 21:06:27 +01:00
Bane Stojkovic 1f85c07453 Configurable 'Tactile Micro Switch' dimensions and added a few most commonly used variants 2026-01-02 10:17:23 +01:00
Bane Stojkovic 53ec1e780e fix: Clicking on 'Tactile Micro Switch' body doesn't register 2026-01-01 22:29:39 +01:00
Bane Stojkovic 2de3def32e Resize tooltip (#986)
* resize tooltip

* Fix blank board area rendering

* Fixes

* translations

* Add length

* Update update.xml
2025-12-31 15:07:22 +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 348eb3ac00 Introduce unique component instance ID 2025-12-31 12:29:48 +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 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 1a31819431 explorer tree 2025-12-29 12:30:44 +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
132 changed files with 6667 additions and 739 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();
@@ -2508,7 +2569,7 @@ public class Presenter implements IPlugInPort {
}
try {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(
"import-defaults/variants.xml"));
"pending-variants.xml"));
XStream xStream = new XStream(new DomDriver());
xStream.addPermission(AnyTypePermission.ANY);
ProjectFileManager.xStreamSerializer.toXML(defaultVariantMap, out);
@@ -4128,4 +4128,204 @@
</org.diylc.core.Template>
</list>
</entry>
</map>
<entry>
<string>org.diylc.components.electromechanical.TactileMicroSwitch</string>
<list>
<org.diylc.core.Template>
<name>6x6 (e.g. Omron B3F-1000)</name>
<values>
<entry>
<string>Spacing X</string>
<org.diylc.core.measures.Size value="6.5" unit="mm"/>
</entry>
<entry>
<string>Spacing Y</string>
<org.diylc.core.measures.Size value="4.5" unit="mm"/>
</entry>
<entry>
<string>Label</string>
<awt-color hex="ffffff"/>
</entry>
<entry>
<string>Orientation</string>
<org.diylc.common.Orientation>DEFAULT</org.diylc.common.Orientation>
</entry>
<entry>
<string>Case</string>
<org.diylc.core.measures.Size value="6.0" unit="mm"/>
</entry>
<entry>
<string>Name</string>
<string>SW2</string>
</entry>
<entry>
<string>Font Size Override</string>
<null/>
</entry>
<entry>
<string>Value</string>
<string></string>
</entry>
<entry>
<string>Alpha</string>
<byte>127</byte>
</entry>
<entry>
<string>Body</string>
<awt-color hex="c0c0c0"/>
</entry>
<entry>
<string>Border</string>
<awt-color hex="595959"/>
</entry>
<entry>
<string>Display</string>
<org.diylc.common.Display>BOTH</org.diylc.common.Display>
</entry>
<entry>
<string>Pin Pairing Mode</string>
<diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>Opposite</diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>
</entry>
</values>
<points>
<point x="0.0" y="0.0"/>
<point x="0.0" y="0.175"/>
<point x="0.255" y="0.0"/>
<point x="0.255" y="0.175"/>
</points>
<defaultFlag>false</defaultFlag>
<createdOn>2026-01-02T10:01:17.805688</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>12x12 (e.g. Omron B3F-4000)</name>
<values>
<entry>
<string>Spacing X</string>
<org.diylc.core.measures.Size value="12.6" unit="mm"/>
</entry>
<entry>
<string>Spacing Y</string>
<org.diylc.core.measures.Size value="5.0" unit="mm"/>
</entry>
<entry>
<string>Label</string>
<awt-color hex="ffffff"/>
</entry>
<entry>
<string>Orientation</string>
<org.diylc.common.Orientation>DEFAULT</org.diylc.common.Orientation>
</entry>
<entry>
<string>Case</string>
<org.diylc.core.measures.Size value="12.0" unit="mm"/>
</entry>
<entry>
<string>Name</string>
<string>SW1</string>
</entry>
<entry>
<string>Font Size Override</string>
<null/>
</entry>
<entry>
<string>Value</string>
<string></string>
</entry>
<entry>
<string>Alpha</string>
<byte>127</byte>
</entry>
<entry>
<string>Body</string>
<awt-color hex="c0c0c0"/>
</entry>
<entry>
<string>Border</string>
<awt-color hex="595959"/>
</entry>
<entry>
<string>Display</string>
<org.diylc.common.Display>BOTH</org.diylc.common.Display>
</entry>
<entry>
<string>Pin Pairing Mode</string>
<diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>Opposite</diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>
</entry>
</values>
<points>
<point x="0.0" y="0.0"/>
<point x="0.0" y="0.195"/>
<point x="0.495" y="0.0"/>
<point x="0.495" y="0.195"/>
</points>
<defaultFlag>false</defaultFlag>
<createdOn>2026-01-02T10:00:31.950676</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>5x5 Low Profile SMD (e.g. Alps SKQG)</name>
<values>
<entry>
<string>Spacing X</string>
<org.diylc.core.measures.Size value="5.2" unit="mm"/>
</entry>
<entry>
<string>Spacing Y</string>
<org.diylc.core.measures.Size value="3.7" unit="mm"/>
</entry>
<entry>
<string>Label</string>
<awt-color hex="ffffff"/>
</entry>
<entry>
<string>Orientation</string>
<org.diylc.common.Orientation>DEFAULT</org.diylc.common.Orientation>
</entry>
<entry>
<string>Case</string>
<org.diylc.core.measures.Size value="5.0" unit="mm"/>
</entry>
<entry>
<string>Name</string>
<string>SW2</string>
</entry>
<entry>
<string>Font Size Override</string>
<null/>
</entry>
<entry>
<string>Value</string>
<string></string>
</entry>
<entry>
<string>Alpha</string>
<byte>127</byte>
</entry>
<entry>
<string>Border</string>
<awt-color hex="595959"/>
</entry>
<entry>
<string>Body</string>
<awt-color hex="c0c0c0"/>
</entry>
<entry>
<string>Display</string>
<org.diylc.common.Display>BOTH</org.diylc.common.Display>
</entry>
<entry>
<string>Pin Pairing Mode</string>
<diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>Diagonal</diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>
</entry>
</values>
<points>
<point x="0.0" y="0.0"/>
<point x="0.0" y="0.145"/>
<point x="0.2" y="0.0"/>
<point x="0.2" y="0.145"/>
</points>
<defaultFlag>false</defaultFlag>
<createdOn>2026-01-02T10:04:33.795741</createdOn>
</org.diylc.core.Template>
</list>
</entry>
</map>
+225 -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,225 @@
</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>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'Freeway 3x3-05 (Ultra) Switch' (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Configurable 'Tactile Micro Switch' dimensions and added a few most commonly used variants</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Exposed the most common pin pairing modes and added switching logic for 'Tactile Micro Switch' to allow using it for analysis</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>'Potentiometer Panel' can now render numerical lug markers for easier identification</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Auto-Wrap Label' retains line breaks from the input text</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Cliff 1/8" Jack', 'Open 1/8" Jack' and 'Open 1/4" Jack' can be rotated</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Fixed the offset of terminals on a DP4T Tele type of 'Lever Switch' to be aligned with the physical switch (attention - this will break existing diagrams and will require adjusting leads to match the new terminal positions)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Make the label of schematic symbols (e.g. 'Resistor Symbol') clickable as well</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Ability to resize 'Transformer Core' schematic symbol after it's already placed</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Clicking on 'Tactile Micro Switch' body doesn't register</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>New lines at the end of the 'Label' text get erroneously rendered as br HTML tags</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>
@@ -31,7 +31,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.VerticalAlignment;
@@ -72,13 +71,18 @@ public class StringUtils {
* @return a non-empty list of strings
*/
public static List<String> wrap(String str, FontMetrics fm, int maxWidth) {
String[] split = str.split("\\<br\\>");
List<String> lines = Arrays.stream(split).flatMap(s -> splitIntoLines(s).stream()).collect(Collectors.toList());
if (lines.size() == 0)
return lines;
// First split by actual newlines to honor them, then split by <br> tags
List<String> lines = splitIntoLines(str);
List<String> allLines = new ArrayList<String>();
for (String line : lines) {
String[] split = line.split("\\<br\\>");
allLines.addAll(Arrays.asList(split));
}
if (allLines.size() == 0)
return allLines;
ArrayList<String> strings = new ArrayList<String>();
for (Iterator<String> iter = lines.iterator(); iter.hasNext();)
for (Iterator<String> iter = allLines.iterator(); iter.hasNext();)
wrapLineInto((String) iter.next(), strings, fm, maxWidth);
return strings;
}
@@ -126,8 +130,8 @@ public class StringUtils {
line = line.substring(pos).trim();
len = line.length();
}
if (len > 0)
list.add(line);
// Always add the line, even if empty, to preserve empty lines from newlines
list.add(line);
}
/**
@@ -209,12 +213,24 @@ public class StringUtils {
if (text == null)
return;
String[] parts = text.split("\\<br\\>");
// Use -1 limit to preserve trailing empty strings so we can filter them out
String[] parts = text.split("\\<br\\>", -1);
if (parts.length > 1) {
// Remove trailing empty strings to avoid rendering extra blank lines
int lastNonEmpty = parts.length - 1;
while (lastNonEmpty >= 0 && parts[lastNonEmpty].trim().isEmpty()) {
lastNonEmpty--;
}
// If all parts are empty, don't render anything
if (lastNonEmpty < 0) {
return;
}
FontMetrics fontMetrics = g2d.getFontMetrics();
Rectangle stringBounds = fontMetrics.getStringBounds(parts[0], g2d).getBounds();
for (int i = 0; i < parts.length; i++)
drawCenteredText(g2d, parts[i], x, (int)(y - stringBounds.height * (parts.length - 1) / 2d + i * stringBounds.height), horizontalAlignment, verticalAlignment);
int actualLength = lastNonEmpty + 1;
for (int i = 0; i < actualLength; i++)
drawCenteredText(g2d, parts[i], x, (int)(y - stringBounds.height * (actualLength - 1) / 2d + i * stringBounds.height), horizontalAlignment, verticalAlignment);
return;
}
@@ -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;
@@ -367,8 +363,8 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
offset = -offset;
}
drawingObserver.startTracking();
if (getMoveLabel()) {
drawingObserver.startTracking();
g2d.setTransform(oldTransform);
g2d.translate(getPoints()[2].getX(), getPoints()[2].getY());
if (getLabelOriantation() != LabelOriantation.Horizontal) {
@@ -378,19 +374,18 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
g2d.setTransform(oldTransform);
} else {
if (isStanding() || getLabelOriantation() == LabelOriantation.Horizontal) {
drawingObserver.startTracking();
g2d.setTransform(oldTransform);
double x = (getPoints()[0].getX() + getPoints()[1].getX() - length) / 2.0;
double y = (getPoints()[0].getY() + getPoints()[1].getY() - width) / 2.0;
g2d.drawString(label, (int) (x + (length - textRect.getWidth()) / 2 + offset),
(int) (y + calculateLabelYOffset(shapeRect, textRect, fontMetrics)));
} else {
drawingObserver.stopTracking();
g2d.drawString(label, (int) (length - textRect.getWidth()) / 2 + offset,
calculateLabelYOffset(shapeRect, textRect, fontMetrics));
g2d.setTransform(oldTransform);
}
}
drawingObserver.stopTracking();
}
// if (getLabelOriantation() == LabelOriantation.Horizontal) {
//
@@ -434,7 +429,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 +636,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 +768,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> {
@@ -42,6 +42,7 @@ import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractMultiPartComponent;
import org.diylc.components.electromechanical.OpenJack1_4.OpenJackType;
import org.diylc.components.transform.OpenJackTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
@@ -56,7 +57,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Open 1/4\" Jack", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "Switchcraft-style open panel mount 1/4\" phono jack, stereo and mono",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", enableCache = true)
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", transformer = OpenJackTransformer.class, enableCache = true)
public class OpenJack1_4 extends AbstractMultiPartComponent<OpenJackType> {
private static final float FONT_SCALE = 0.8f;
@@ -42,6 +42,7 @@ import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractMultiPartComponent;
import org.diylc.components.electromechanical.OpenJack1_8.OpenJackType;
import org.diylc.components.transform.OpenJackTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
@@ -56,7 +57,7 @@ import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Open 1/8\" Jack", category = "Electro-Mechanical", author = "Branislav Stojkovic",
description = "Switchcraft-style open panel mount 1/8\" phono jack, stereo and mono",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", enableCache = true)
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "J", transformer = OpenJackTransformer.class, enableCache = true)
public class OpenJack1_8 extends AbstractMultiPartComponent<OpenJackType> {
private static final float FONT_SCALE = 0.6f;
@@ -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 {
@@ -35,13 +35,10 @@ import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractLabeledComponent;
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.components.guitar.LeverSwitchPositionPropertyValueSource;
import org.diylc.core.*;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.DynamicEditableProperty;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.gerber.IGerberComponentSimple;
@@ -53,22 +50,21 @@ import org.diylc.utils.Constants;
category = "Electro-Mechanical", instanceNamePrefix = "SW",
description = "4-pin tactile momentary switch", zOrder = IDIYComponent.COMPONENT,
keywordPolicy = KeywordPolicy.SHOW_VALUE, enableCache = true)
public class TactileMicroSwitch extends AbstractLabeledComponent<String> implements IGerberComponentSimple {
public class TactileMicroSwitch extends AbstractLabeledComponent<String> implements IGerberComponentSimple,
ISwitch {
private static final long serialVersionUID = 1L;
public static Color BODY_COLOR = Color.lightGray;
public static Color BORDER_COLOR = Color.gray.darker();
public static Color PIN_COLOR = METAL_COLOR;
public static Color PIN_BORDER_COLOR = PIN_COLOR.darker();
public static Color INDENT_COLOR = Color.gray.darker();
public static Color LABEL_COLOR = Color.white;
public static Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
public static int PIN_COUNT = 4;
public static Size CASE_SIZE = new Size(6d, SizeUnit.mm);
public static Size INDENT_SIZE = new Size(3d, SizeUnit.mm);
private static Size PIN_SPACING = new Size(4.5d, SizeUnit.mm);
private static Size ROW_SPACING = new Size(6.5d, SizeUnit.mm);
private static final Color BODY_COLOR = Color.lightGray;
private static final Color BORDER_COLOR = Color.gray.darker();
private static final Color PIN_COLOR = METAL_COLOR;
private static final Color PIN_BORDER_COLOR = PIN_COLOR.darker();
private static final Color INDENT_COLOR = Color.gray.darker();
private static final Color LABEL_COLOR = Color.white;
private static final Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
private static final Size CASE_SIZE = new Size(6d, SizeUnit.mm);
private static final Size PIN_SPACING = new Size(4.5d, SizeUnit.mm);
private static final Size ROW_SPACING = new Size(6.5d, SizeUnit.mm);
private String value = "";
private Orientation orientation = Orientation.DEFAULT;
@@ -80,6 +76,12 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
private Color labelColor = LABEL_COLOR;
transient private Area[] body;
private Size caseSize;
private Size pinSpacing;
private Size rowSpacing;
private PinPairingMode pinPairingMode;
private int selectedPosition;
public TactileMicroSwitch() {
super();
updateControlPoints();
@@ -146,10 +148,11 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
private void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
int PIN_COUNT = 4;
controlPoints = new Point2D[PIN_COUNT];
controlPoints[0] = firstPoint;
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
double pinSpacing = getPinSpacing().convertToPixels();
double rowSpacing = getRowSpacing().convertToPixels();
// Update control points.
double dx1;
double dy1;
@@ -198,8 +201,8 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
double y = controlPoints[0].getY();
double width;
double height;
double caseSize = CASE_SIZE.convertToPixels();
double indentSize = INDENT_SIZE.convertToPixels();
double caseSize = getCaseSize().convertToPixels();
double actuatorSize = caseSize / 2;
width = height = caseSize;
x = (controlPoints[0].getX() + controlPoints[3].getX() - width) / 2;
@@ -207,7 +210,7 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
body[0] = new Area(new Rectangle2D.Double(x, y, width, height));
width = height = indentSize;
width = height = actuatorSize;
x = (controlPoints[0].getX() + controlPoints[3].getX() - width) / 2;
y = (controlPoints[0].getY() + controlPoints[3].getY() - height) / 2;
@@ -244,7 +247,7 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : getBodyColor());
drawingObserver.stopTracking();
drawingObserver.startTracking();
g2d.fill(mainArea);
drawingObserver.stopTracking();
@@ -391,6 +394,59 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
this.labelColor = labelColor;
}
@EditableProperty(name = "Case Size")
public Size getCaseSize() {
if (caseSize == null) {
caseSize = CASE_SIZE;
}
return caseSize;
}
public void setCaseSize(Size caseSize) {
this.caseSize = caseSize;
this.body = null;
}
@EditableProperty(name = "Pin Spacing")
public Size getPinSpacing() {
if (pinSpacing == null) {
pinSpacing = PIN_SPACING;
}
return pinSpacing;
}
public void setPinSpacing(Size pinSpacing) {
this.pinSpacing = pinSpacing;
this.body = null;
updateControlPoints();
}
@EditableProperty(name = "Row Spacing")
public Size getRowSpacing() {
if (rowSpacing == null) {
rowSpacing = ROW_SPACING;
}
return rowSpacing;
}
public void setRowSpacing(Size rowSpacing) {
this.rowSpacing = rowSpacing;
this.body = null;
updateControlPoints();
}
@EditableProperty(name = "Pin Pairing Mode")
public PinPairingMode getPinPairingMode() {
if (pinPairingMode == null) {
pinPairingMode = PinPairingMode.Opposite;
}
return pinPairingMode;
}
public void setPinPairingMode(PinPairingMode pinPairingMode) {
this.pinPairingMode = pinPairingMode;
}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
@@ -418,4 +474,56 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
return new Rectangle2D.Double(minX - margin, minY - margin, maxX - minX + 2 * margin,
maxY - minY + 2 * margin);
}
@Override
public int getPositionCount() {
return 2;
}
@Override
public String getPositionName(int position) {
return position == 0 ? "Released" : "Pressed";
}
@Override
public boolean arePointsConnected(int index1, int index2, int position) {
// Only connect pins when switch is pressed (position = 1)
if (position != 1) {
return false;
}
PinPairingMode mode = getPinPairingMode();
switch (mode) {
case Opposite:
// Pins on opposite sides (same row): 0-2 and 1-3
return (index1 == 0 && index2 == 2) || (index1 == 2 && index2 == 0) ||
(index1 == 1 && index2 == 3) || (index1 == 3 && index2 == 1);
case Diagonal:
// Diagonally opposite pins: 0-3 and 1-2
return (index1 == 0 && index2 == 3) || (index1 == 3 && index2 == 0) ||
(index1 == 1 && index2 == 2) || (index1 == 2 && index2 == 1);
case Adjacent:
// Pins next to each other (same column): 0-1 and 2-3
return (index1 == 0 && index2 == 1) || (index1 == 1 && index2 == 0) ||
(index1 == 2 && index2 == 3) || (index1 == 3 && index2 == 2);
default:
return false;
}
}
@DynamicEditableProperty(source = TactileMicroSwitchPositionPropertyValueSource.class)
@EditableProperty(name = "Selected Position")
@Override
public Integer getSelectedPosition() {
return selectedPosition;
}
public void setSelectedPosition(Integer selectedPosition) {
this.selectedPosition = selectedPosition;
}
public enum PinPairingMode {
Opposite, Diagonal, Adjacent;
}
}
@@ -0,0 +1,17 @@
package org.diylc.components.electromechanical;
import org.diylc.common.PropertyWrapper;
import org.diylc.components.guitar.LeverSwitch;
import org.diylc.core.AbstractDynamicPropertyValueSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class TactileMicroSwitchPositionPropertyValueSource extends AbstractDynamicPropertyValueSource {
@Override
public List<Object> getAvailableValues() {
return List.of("Released", "Pressed");
}
}
@@ -59,8 +59,8 @@ import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
import org.diylc.utils.SwitchUtils;
@ComponentDescriptor(name = "Freeway 3X3-03 Toggle", category = "Guitar",
author = "Branislav Stojkovic", description = "Freeway 3X3-03 Toggle Switch",
@ComponentDescriptor(name = "Freeway 3X3-03 Switch", category = "Guitar",
author = "Branislav Stojkovic", description = "Freeway 3X3-03 (Standard) Toggle Switch",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "SW",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram",
transformer = Freeway3x3_03Transformer.class, enableCache = true)
@@ -0,0 +1,493 @@
/*
*
* 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.guitar;
import com.kitfox.svg.SVGDiagram;
import com.kitfox.svg.SVGElement;
import com.kitfox.svg.SVGUniverse;
import com.kitfox.svg.ShapeElement;
import org.apache.log4j.Logger;
import org.apache.poi.util.IOUtils;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.awt.StringUtils;
import org.diylc.common.*;
import org.diylc.components.AbstractTransparentComponent;
import org.diylc.components.transform.Freeway3x3_03Transformer;
import org.diylc.core.*;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.DynamicEditableProperty;
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;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static org.diylc.core.measures.SizeUnit.px;
@ComponentDescriptor(name = "Freeway 3X3-05 (Ultra) Switch", category = "Guitar",
author = "Martin Morrell", description = "Freeway 3X3-05 (Ultra) Toggle Switch",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "SW",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram",
transformer = Freeway3x3_03Transformer.class, enableCache = false)
public class Freeway3x3_05 extends AbstractTransparentComponent<Void> implements ISwitch {
private static final Logger LOG = Logger.getLogger(Freeway3x3_05.class);
private static final long serialVersionUID = 1L;
private static Color BASE_COLOR = FR4_COLOR;// Color.DARK_GRAY;
private static Color PAD_COLOR = COPPER_COLOR;
private static Color LABEL_COLOR = Color.WHITE;
private static Color CASE_COLOR = METAL_COLOR;
private static Color MARKER_COLOR = Color.lightGray;
private static final double DIAMETER_SWITCH = 34.0;
private static final double PAD_RADIUS = 15.3;
private static final double PAD_SIZE = 1.5;
private static final int NUM_CONTACTS = 28;
private static final double CONTACT_SPACING_RAD = Math.PI * (360.0 / NUM_CONTACTS) / 180.0;
private static final double CONTACT_SPACING_INITIAL = (CONTACT_SPACING_RAD * 14) + CONTACT_SPACING_RAD / 2;
private static final double GROUND_DIAMETER = 20;
private static final double CENTRE_LEN = 17;
private static final double CENTRE_WID = 10;
private static final double MARKER_OFFSET = 3.1;
private static final String[] PAD_NAMES = new String[] {
"1B", "2B", "3B", "6B", "5B", "4B", "CB", "TB", "6B", "5B", "4B", "1B", "2B", "3B",
"6A", "5A", "4A", "1A", "2A", "3A", "TA", "CA", "1A", "2A", "3A", "6A", "5A", "4A"
};
private static final AffineTransform SCALE_TX = AffineTransform.getScaleInstance(0.615d, 0.615d);
private Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
private Orientation orientation = Orientation.DEFAULT;
private Integer selectedPosition;
private Boolean showMarkers;
private transient SVGDiagram svgDiagram;
private transient List<Shape> pads;
private transient List<Shape> labels;
private transient Shape base;
private transient Shape caseShape;
private transient double[] xOffsetsPx;
private transient double[] yOffsetsPx;
public Freeway3x3_05() {
super();
updateControlPoints();
}
@Override
public String getControlPointNodeName(int index) {
return PAD_NAMES[index];
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
double x = controlPoints[0].getX();
double y = controlPoints[0].getY();
if (getOrientation() != Orientation.DEFAULT) {
double theta = orientation.toRadians();
g2d.rotate(theta, x, y);
}
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
double offsetX = getXOffsetsPx()[0];
double offsetY = getYOffsetsPx()[0];
//double origin = DIAMETER_SWITCH / 2;
int diameter = (int)(new Size(DIAMETER_SWITCH, SizeUnit.mm)).convertToPixels();
double originX = x - offsetX + diameter / 2.0;
double originY = y - offsetY + diameter / 2.0;
//Main circular shape
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
: BASE_COLOR.darker();
}
Shape base = new Ellipse2D.Double( originX, originY, diameter, diameter);
drawingObserver.startTracking();
g2d.setColor(BASE_COLOR);
g2d.fill(base);
drawingObserver.stopTracking();
g2d.setStroke(new BasicStroke(1f));
g2d.setColor(finalBorderColor);
g2d.draw(base);
//Ground circular shape
int gndDiameter = (int)(new Size(GROUND_DIAMETER, SizeUnit.mm)).convertToPixels();
int gndShift = (int)(new Size( (DIAMETER_SWITCH - GROUND_DIAMETER) / 2, SizeUnit.mm).convertToPixels());
Shape gnd = new Ellipse2D.Double( originX + gndShift, originY + gndShift, gndDiameter, gndDiameter);
g2d.setPaint(PAD_COLOR);
g2d.fill(gnd);
g2d.setColor(PAD_COLOR.darker());
g2d.draw(gnd);
//Centre rectangle casing block
int rectLen = (int)(new Size(CENTRE_LEN, SizeUnit.mm)).convertToPixels();
int rectWid = (int)(new Size(CENTRE_WID, SizeUnit.mm)).convertToPixels();
int rectShiftX = (int)(new Size( (DIAMETER_SWITCH - CENTRE_WID) / 2, SizeUnit.mm).convertToPixels());
int rectShiftY = (int)(new Size( (DIAMETER_SWITCH - CENTRE_LEN) / 2, SizeUnit.mm).convertToPixels());
int rectangleArc = (int)(new Size(1.0, SizeUnit.mm)).convertToPixels();
Shape casing = new RoundRectangle2D.Double(originX + rectShiftX, originY + rectShiftY, rectWid, rectLen, rectangleArc, rectangleArc);
g2d.setPaint(CASE_COLOR);
g2d.fill(casing);
g2d.setColor(CASE_COLOR.darker());
g2d.draw(casing);
//Pads
double circleShift = (int)(new Size( DIAMETER_SWITCH / 2, SizeUnit.mm).convertToPixels());
double circleCentreX = originX + circleShift;
double circleCentreY = originY + circleShift;
double radChange = CONTACT_SPACING_RAD * 0.45;
double extendedRadius = (int)new Size(PAD_RADIUS + PAD_SIZE, SizeUnit.mm).convertToPixels();
double reducedRadius = (int)new Size(PAD_RADIUS - PAD_SIZE, SizeUnit.mm).convertToPixels();
for (int i = 0; i < NUM_CONTACTS; i++) {
double rad = ((i + 0.5) * CONTACT_SPACING_RAD);
double rotExX = Math.cos(rad) * (extendedRadius * 1.01) + circleCentreX;
double rotExY = Math.sin(rad) * (extendedRadius * 1.01) + circleCentreY;
double rotExPosX = Math.cos(rad + radChange) * extendedRadius + circleCentreX;
double rotExPosY = Math.sin(rad + radChange) * extendedRadius + circleCentreY;
double rotExNegX = Math.cos(rad - radChange) * extendedRadius + circleCentreX;
double rotExNegY = Math.sin(rad - radChange) * extendedRadius + circleCentreY;
double rotReX = Math.cos(rad) * (reducedRadius * 1.01) + circleCentreX;
double rotReY = Math.sin(rad) * (reducedRadius * 1.01) + circleCentreY;
double rotRePosX = Math.cos(rad + radChange) * reducedRadius + circleCentreX;
double rotRePosY = Math.sin(rad + radChange) * reducedRadius + circleCentreY;
double rotReNegX = Math.cos(rad - radChange) * reducedRadius + circleCentreX;
double rotReNegY = Math.sin(rad - radChange) * reducedRadius + circleCentreY;
Path2D pad = new Path2D.Double();
pad.moveTo(rotExNegX, rotExNegY);
pad.quadTo(rotExX, rotExY, rotExPosX, rotExPosY);
pad.lineTo(rotRePosX, rotRePosY);
pad.quadTo(rotReX, rotReY,rotReNegX, rotReNegY);
pad.lineTo(rotExNegX, rotExNegY);
pad.closePath();
g2d.setPaint(PAD_COLOR);
g2d.fill(pad);
g2d.setColor(PAD_COLOR.darker());
g2d.draw(pad);
}
//Labels
double offset = (int) new Size(MARKER_OFFSET, SizeUnit.mm).convertToPixels();
for (int i = 0; i < NUM_CONTACTS; i++) {
Point2D p = getControlPoint(i);
double px = Math.sin(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS - MARKER_OFFSET);
double py = Math.cos(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS - MARKER_OFFSET);
g2d.setPaint(LABEL_COLOR);
StringUtils.drawCenteredText(g2d, PAD_NAMES[i],
(new Size(px + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originX,
(new Size(py + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
//Add other text
int makeLen = (int)(new Size(12.0, SizeUnit.mm)).convertToPixels();
int modelLen = (int)(new Size(8.0, SizeUnit.mm)).convertToPixels();
double mWidth = 1.8;
int modelWid = (int)(new Size(mWidth, SizeUnit.mm)).convertToPixels();
int makeShiftX = (int)(new Size( (DIAMETER_SWITCH / 2) - 7.25, SizeUnit.mm).convertToPixels());
int makeShiftY = (int)(new Size( (DIAMETER_SWITCH / 2) - 6.0, SizeUnit.mm).convertToPixels());
int modelShiftX = (int)(new Size( (DIAMETER_SWITCH / 2) + 5.25, SizeUnit.mm).convertToPixels());
int modelShiftY = (int)(new Size( (DIAMETER_SWITCH / 2) - 4.0, SizeUnit.mm).convertToPixels());
int modelArc = (int)(new Size(mWidth, SizeUnit.mm)).convertToPixels();
Shape make = new RoundRectangle2D.Double(originX + makeShiftX, originY + makeShiftY, modelWid, makeLen, modelArc, modelArc);
Shape model = new RoundRectangle2D.Double(originX + modelShiftX, originY + modelShiftY, modelWid, modelLen, modelArc, modelArc);
g2d.setPaint(BASE_COLOR);
g2d.fill(make);
g2d.fill(model);
g2d.setColor(PAD_COLOR.darker());
g2d.draw(make);
g2d.draw(model);
/*String makeTxt = "FREE-WAY·ULTRA";
String modelTxt = "3X3-05";
g2d.setPaint(LABEL_COLOR);
StringUtils.drawCenteredText(g2d, makeTxt,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originX + makeShiftX,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originY + makeShiftY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, modelTxt,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originX + modelShiftX,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originY + modelShiftY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);*/
if (getShowMarkers()) {
g2d.setColor(MARKER_COLOR);
for (int i = 0; i < getControlPointCount(); i++) {
Point2D p = getControlPoint(i);
Point2D labelPoint = new Point2D.Double(offset, 0);
double px = Math.sin(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS + MARKER_OFFSET);
double py = Math.cos(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS + MARKER_OFFSET);
String marker = PAD_NAMES[i];
if(marker.equals("CA")) {
marker = "A";
} else if (marker.equals("CB")) {
marker = "B";
} else if (marker.startsWith("T")) {
continue;
} else {
marker = marker.substring(0, 1);
}
StringUtils.drawCenteredText(g2d, marker,
(new Size(px + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originX,
(new Size(py + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
}
g2d.setComposite(oldComposite);
}
public double[] getXOffsetsPx() {
if (xOffsetsPx == null) {
xOffsetsPx = new double[NUM_CONTACTS];
for (int i = 0; i < NUM_CONTACTS; i++) {
double x = Math.sin(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * PAD_RADIUS;
xOffsetsPx[i] = new Size(x + DIAMETER_SWITCH, SizeUnit.mm).convertToPixels();
}
}
return xOffsetsPx;
}
public double[] getYOffsetsPx() {
if (yOffsetsPx == null) {
yOffsetsPx = new double[NUM_CONTACTS];
for (int i = 0; i < NUM_CONTACTS; i++) {
double y = Math.cos(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * PAD_RADIUS;
yOffsetsPx[i] = new Size(y + DIAMETER_SWITCH , SizeUnit.mm).convertToPixels();
}
}
return yOffsetsPx;
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
Shape base = new Ellipse2D.Double( 0, 0, width, height);
g2d.setColor(BASE_COLOR);
g2d.fill(base);
double contactOffset = 1;
Shape contacts = new Ellipse2D.Double( contactOffset, contactOffset, width - contactOffset * 2, height - contactOffset * 2);
g2d.setColor(PAD_COLOR);
g2d.fill(contacts);
double centreOffset = 4;
Shape centre = new Ellipse2D.Double( centreOffset, centreOffset, width - centreOffset * 2, height - centreOffset * 2);
g2d.setColor(BASE_COLOR);
g2d.fill(centre);
double gndOffset = 9;
Shape gnd = new Ellipse2D.Double( gndOffset, gndOffset, width - gndOffset * 2, height - gndOffset * 2);
g2d.setColor(PAD_COLOR);
g2d.fill(gnd);
double arc = 2.0;
Shape casing = new RoundRectangle2D.Double(12, 10, 8, 12, arc, arc);
g2d.setColor(CASE_COLOR);
g2d.fill(casing);
}
@Override
public int getControlPointCount() {
return NUM_CONTACTS;
}
@Override
public Point2D getControlPoint(int index) {
return controlPoints[index];
}
private void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
controlPoints = new Point2D[NUM_CONTACTS];
controlPoints[0] = firstPoint;
double[] xOffsetsPx = getXOffsetsPx();
double[] yOffsetsPx = getYOffsetsPx();
double theta = orientation.toRadians();
AffineTransform tx =
AffineTransform.getRotateInstance(theta, firstPoint.getX(), firstPoint.getY());
for (int i = 1; i < controlPoints.length; i++) {
controlPoints[i] = new Point2D.Double(firstPoint.getX() + (xOffsetsPx[i] - xOffsetsPx[0]),
firstPoint.getY() + (yOffsetsPx[i] - yOffsetsPx[0]));
tx.transform(controlPoints[i], controlPoints[i]);
}
}
@Override
public boolean isControlPointSticky(int index) {
return true;
}
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.ALWAYS;
}
@Override
public void setControlPoint(Point2D point, int index) {
controlPoints[index].setLocation(point);
}
@Override
public Void getValue() {
return null;
}
@Override
public void setValue(Void value) {}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
}
@EditableProperty
public Orientation getOrientation() {
if (orientation == null)
orientation = Orientation.DEFAULT;
return orientation;
}
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
updateControlPoints();
}
@Override
public int getPositionCount() {
return 6;
}
@Override
public String getPositionName(int position) {
return Integer.toString(position + 1);
}
@DynamicEditableProperty(source = Freeway3x4_03SwitchPositionPropertyValueSource.class)
@EditableProperty(name = "Selected Position")
@Override
public Integer getSelectedPosition() {
return selectedPosition;
}
public void setSelectedPosition(Integer selectedPosition) {
this.selectedPosition = selectedPosition;
}
@EditableProperty(name = "Markers")
public Boolean getShowMarkers() {
if (showMarkers == null) {
showMarkers = false;
}
return showMarkers;
}
public void setShowMarkers(Boolean showMarkers) {
this.showMarkers = showMarkers;
}
@Override
public boolean arePointsConnected(int index1, int index2, int position) {
if (position == 0 &&
((index1 == 0 && index2 == 6) || (index1 == 6 && index2 == 11) || (index1 == 0 && index2 == 11) ||
(index1 == 17 && index2 == 21) || (index1 == 21 && index2 == 22) || (index1 == 17 && index2 == 22))
) {
return true;
}
if (position == 1 &&
((index1 == 1 && index2 == 6) || (index1 == 6 && index2 == 12) || (index1 == 1 && index2 == 12) ||
(index1 == 18 && index2 == 21) || (index1 == 21 && index2 == 23) || (index1 == 18 && index2 == 23))
) {
return true;
}
if (position == 2 &&
((index1 == 2 && index2 == 6) || (index1 == 6 && index2 == 13) || (index1 == 2 && index2 == 13) ||
(index1 == 19 && index2 == 21) || (index1 == 21 && index2 == 24) || (index1 == 19 && index2 == 24))
) {
return true;
}
if (position == 3 &&
((index1 == 5 && index2 == 6) || (index1 == 6 && index2 == 10) || (index1 == 5 && index2 == 10) ||
(index1 == 16 && index2 == 21) || (index1 == 21 && index2 == 27) || (index1 == 16 && index2 == 27))
) {
return true;
}
if (position == 4 &&
((index1 == 4 && index2 == 6) || (index1 == 6 && index2 == 9) || (index1 == 4 && index2 == 9) ||
(index1 == 15 && index2 == 21) || (index1 == 21 && index2 == 26) || (index1 == 15 && index2 == 26))
) {
return true;
}
if (position == 5 &&
((index1 == 3 && index2 == 6) || (index1 == 6 && index2 == 8) || (index1 == 3 && index2 == 8) ||
(index1 == 14 && index2 == 21) || (index1 == 21 && index2 == 25) || (index1 == 14 && index2 == 25))
) {
return true;
}
return false;
}
}
@@ -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++) {
@@ -303,7 +321,8 @@ public class LeverSwitch extends AbstractAngledComponent<LeverSwitch.LeverSwitch
case DP4T:
controlPoints = new Point2D[10];
for (int i = 0; i < controlPoints.length; i++) {
controlPoints[i] = new Point2D.Double(x + (i % 2 == 1 ? terminalLength : 0), y + i * terminalSpacing);
double yPos = i % 2 == 1 ? y + (i - 2) * terminalSpacing : y + i * terminalSpacing;
controlPoints[i] = new Point2D.Double(x + (i % 2 == 1 ? terminalLength : 0), yPos);
}
break;
case DP5T:
@@ -319,6 +338,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 +481,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 +569,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 +628,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)
@@ -45,7 +45,7 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
@ComponentDescriptor(name = "Auto-Wrap Label", author = "Branislav Stojkovic", category = "Misc",
description = "User defined label with auto-wrapped text", instanceNamePrefix = "L", zOrder = IDIYComponent.TEXT, flexibleZOrder = true,
description = "User defined label with auto-wrapped text", instanceNamePrefix = "LAB", zOrder = IDIYComponent.TEXT, flexibleZOrder = true,
bomPolicy = BomPolicy.NEVER_SHOW, transformer = TextTransformer.class)
public class WrapLabel extends AbstractComponent<String> {
@@ -35,9 +35,12 @@ import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.awt.StringUtils;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.transform.PotentiometerTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.CreationMethod;
@@ -66,10 +69,12 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
protected static Size PIN_SIZE = new Size(0.05d, SizeUnit.in);
protected static Size NUT_SIZE = new Size(0.4d, SizeUnit.in);
protected static Size SHAFT_SIZE = new Size(1 / 4d, SizeUnit.in);
protected static Size MARKER_OFFSET = new Size(0.05d, SizeUnit.in);
protected static Color BODY_COLOR = Color.lightGray;
protected static Color WAFER_COLOR = Color.decode("#CD8500");
protected static Color BORDER_COLOR = Color.gray;
protected static Color NUT_COLOR = Color.decode("#CBD5DB");
protected static Color MARKER_COLOR_DEFAULT = Color.gray;
protected Size bodyDiameter = BODY_DIAMETER;
@@ -83,6 +88,8 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
@Deprecated
protected boolean showShaft = false;
protected View view;
private Boolean showMarkers;
private Color markerColor;
// Array of 7 elements: 3 lug connectors, 1 pot body and 3 lugs
transient protected Area[] body = null;
@@ -353,6 +360,50 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
y = panelHeight / 2 + fontMetrics.getAscent();
g2d.drawString(getValueForDisplay(), (int) (bodyRect.getX() + x), (int) (bodyRect.getY() + y));
// Draw markers if enabled.
if (getShowMarkers()) {
g2d.setColor(outlineMode ? theme.getOutlineColor() : getMarkerColor());
// Get circle center and radius
Rectangle2D circleBounds = body[3].getBounds2D();
double centerX = circleBounds.getCenterX();
double centerY = circleBounds.getCenterY();
double radius = getBodyDiameter().convertToPixels() / 2;
// Fixed distance from circle edge (inside the circle)
double markerOffset = MARKER_OFFSET.convertToPixels() + 2;
double markerDistance = radius - markerOffset;
// Draw markers "1", "2", "3" along radial line from center to each lug, close to circle edge
// Reverse numbering when view is ShaftDown
boolean reverseOrder = getView() == View.ShaftDown;
for (int i = 0; i < 3; i++) {
Point2D lugPoint = controlPoints[i];
// Calculate direction from center to lug
double dx = lugPoint.getX() - centerX;
double dy = lugPoint.getY() - centerY;
double distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction vector
double unitX = dx / distance;
double unitY = dy / distance;
// Position marker at fixed distance from circle edge along radial line
double markerX = centerX + unitX * markerDistance;
double markerY = centerY + unitY * markerDistance;
// Determine marker number based on view
int markerNumber = reverseOrder ? (3 - i) : (i + 1);
StringUtils.drawCenteredText(g2d, Integer.toString(markerNumber), markerX, markerY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
}
}
}
@Override
@@ -474,6 +525,30 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
public void setWaferColor(Color waferColor) {
this.waferColor = waferColor;
}
@EditableProperty(name = "Markers")
public Boolean getShowMarkers() {
if (showMarkers == null) {
showMarkers = false;
}
return showMarkers;
}
public void setShowMarkers(Boolean showMarkers) {
this.showMarkers = showMarkers;
}
@EditableProperty(name = "Marker Color")
public Color getMarkerColor() {
if (markerColor == null) {
markerColor = MARKER_COLOR_DEFAULT;
}
return markerColor;
}
public void setMarkerColor(Color markerColor) {
this.markerColor = markerColor;
}
@Override
public String getInternalLinkName(int index1, int index2) {
@@ -157,6 +157,6 @@ public class TransformerCore extends AbstractComponent<Void> {
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
return true;
}
}
@@ -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,18 +27,19 @@ import java.awt.geom.Point2D;
import org.diylc.common.IComponentTransformer;
import org.diylc.common.Orientation;
import org.diylc.components.electromechanical.CliffJack1_4;
import org.diylc.components.electromechanical.CliffJack1_8;
import org.diylc.core.IDIYComponent;
public class CliffJackTransformer implements IComponentTransformer {
@Override
public boolean canRotate(IDIYComponent<?> component) {
return component.getClass().equals(CliffJack1_4.class);
return component.getClass().equals(CliffJack1_4.class) || component.getClass().equals(CliffJack1_8.class);
}
@Override
public boolean canMirror(IDIYComponent<?> component) {
return component.getClass().equals(CliffJack1_4.class);
return component.getClass().equals(CliffJack1_4.class) || component.getClass().equals(CliffJack1_8.class);
}
@Override
@@ -55,8 +56,15 @@ public class CliffJackTransformer implements IComponentTransformer {
component.setControlPoint(p, index);
}
CliffJack1_4 jack = (CliffJack1_4) component;
Orientation o = jack.getOrientation();
Orientation o;
if (component instanceof CliffJack1_4) {
o = ((CliffJack1_4) component).getOrientation();
} else if (component instanceof CliffJack1_8) {
o = ((CliffJack1_8) component).getOrientation();
} else {
return;
}
int oValue = o.ordinal();
oValue += direction;
if (oValue < 0)
@@ -64,18 +72,33 @@ public class CliffJackTransformer implements IComponentTransformer {
if (oValue >= Orientation.values().length)
oValue = 0;
o = Orientation.values()[oValue];
jack.setOrientation(o);
if (component instanceof CliffJack1_4) {
((CliffJack1_4) component).setOrientation(o);
} else if (component instanceof CliffJack1_8) {
((CliffJack1_8) component).setOrientation(o);
}
}
@Override
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
CliffJack1_4 ic = (CliffJack1_4) component;
if (!(component instanceof CliffJack1_4) && !(component instanceof CliffJack1_8)) {
return;
}
IDIYComponent<?> ic = component;
if (direction == IComponentTransformer.HORIZONTAL) {
double dx = 2 * (center.getX() - ic.getControlPoint(1).getX());
double dy = 0;
Orientation o = ic.getOrientation();
Orientation o;
if (component instanceof CliffJack1_4) {
o = ((CliffJack1_4) component).getOrientation();
} else {
o = ((CliffJack1_8) component).getOrientation();
}
switch (o) {
case DEFAULT:
dy += (ic.getControlPoint(0).getY() - ic.getControlPoint(ic.getControlPointCount() - 1).getY());
@@ -98,12 +121,22 @@ public class CliffJackTransformer implements IComponentTransformer {
ic.setControlPoint(new Point2D.Double(p.getX() + dx, p.getY() + dy), i);
}
ic.setOrientation(o);
if (component instanceof CliffJack1_4) {
((CliffJack1_4) component).setOrientation(o);
} else {
((CliffJack1_8) component).setOrientation(o);
}
} else {
double dx = 0;
double dy = 2 * (center.getY() - ic.getControlPoint(1).getY());
Orientation o = ic.getOrientation();
Orientation o;
if (component instanceof CliffJack1_4) {
o = ((CliffJack1_4) component).getOrientation();
} else {
o = ((CliffJack1_8) component).getOrientation();
}
switch (o) {
case DEFAULT:
// dx += ic.getControlPoint(0).getX() - ic.getControlPoint(ic.getControlPointCount() - 1).getX();
@@ -128,7 +161,11 @@ public class CliffJackTransformer implements IComponentTransformer {
ic.setControlPoint(new Point2D.Double(p.getX() + dx, p.getY() + dy), i);
}
ic.setOrientation(o);
if (component instanceof CliffJack1_4) {
((CliffJack1_4) component).setOrientation(o);
} else {
((CliffJack1_8) component).setOrientation(o);
}
}
}
}
@@ -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);
@@ -0,0 +1,161 @@
/*
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.transform;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import org.diylc.common.IComponentTransformer;
import org.diylc.components.electromechanical.OpenJack1_4;
import org.diylc.components.electromechanical.OpenJack1_8;
import org.diylc.core.IDIYComponent;
public class OpenJackTransformer implements IComponentTransformer {
@Override
public boolean canRotate(IDIYComponent<?> component) {
return component.getClass().equals(OpenJack1_4.class) || component.getClass().equals(OpenJack1_8.class);
}
@Override
public boolean canMirror(IDIYComponent<?> component) {
return component.getClass().equals(OpenJack1_4.class) || component.getClass().equals(OpenJack1_8.class);
}
@Override
public boolean mirroringChangesCircuit() {
return false;
}
@Override
public void rotate(IDIYComponent<?> component, Point2D center, int direction) {
AffineTransform rotate = AffineTransform.getRotateInstance(Math.PI / 2 * direction, center.getX(), center.getY());
for (int index = 0; index < component.getControlPointCount(); index++) {
Point2D p = new Point2D.Double();
rotate.transform(component.getControlPoint(index), p);
component.setControlPoint(p, index);
}
Integer angle;
if (component instanceof OpenJack1_4) {
angle = ((OpenJack1_4) component).getAngle();
} else if (component instanceof OpenJack1_8) {
angle = ((OpenJack1_8) component).getAngle();
} else {
return;
}
if (angle == null) {
angle = 0;
}
int newAngle = angle + direction * 90;
// Normalize to 0-360 range
while (newAngle < 0) {
newAngle += 360;
}
while (newAngle >= 360) {
newAngle -= 360;
}
if (component instanceof OpenJack1_4) {
((OpenJack1_4) component).setAngle(newAngle);
} else if (component instanceof OpenJack1_8) {
((OpenJack1_8) component).setAngle(newAngle);
}
}
@Override
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
if (!(component instanceof OpenJack1_4) && !(component instanceof OpenJack1_8)) {
return;
}
IDIYComponent<?> ic = component;
if (direction == IComponentTransformer.HORIZONTAL) {
// Mirror control points horizontally
for (int i = 0; i < ic.getControlPointCount(); i++) {
Point2D p = ic.getControlPoint(i);
double dx = 2 * (center.getX() - p.getX());
ic.setControlPoint(new Point2D.Double(p.getX() + dx, p.getY()), i);
}
Integer angle;
if (component instanceof OpenJack1_4) {
angle = ((OpenJack1_4) component).getAngle();
} else {
angle = ((OpenJack1_8) component).getAngle();
}
if (angle == null) {
angle = 0;
}
// Mirror horizontally: reflect angle across vertical axis
int newAngle = 360 - angle;
while (newAngle >= 360) {
newAngle -= 360;
}
if (component instanceof OpenJack1_4) {
((OpenJack1_4) component).setAngle(newAngle);
} else {
((OpenJack1_8) component).setAngle(newAngle);
}
} else {
// Mirror control points vertically
for (int i = 0; i < ic.getControlPointCount(); i++) {
Point2D p = ic.getControlPoint(i);
double dy = 2 * (center.getY() - p.getY());
ic.setControlPoint(new Point2D.Double(p.getX(), p.getY() + dy), i);
}
Integer angle;
if (component instanceof OpenJack1_4) {
angle = ((OpenJack1_4) component).getAngle();
} else {
angle = ((OpenJack1_8) component).getAngle();
}
if (angle == null) {
angle = 0;
}
// Mirror vertically: reflect angle across horizontal axis
int newAngle = 180 - angle;
while (newAngle < 0) {
newAngle += 360;
}
while (newAngle >= 360) {
newAngle -= 360;
}
if (component instanceof OpenJack1_4) {
((OpenJack1_4) component).setAngle(newAngle);
} else {
((OpenJack1_8) component).setAngle(newAngle);
}
}
}
}
@@ -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());
@@ -0,0 +1,247 @@
/*
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.awt;
import static org.junit.Assert.*;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
public class StringUtilsTest {
private FontMetrics fontMetrics;
private Graphics2D g2d;
@Before
public void setUp() {
BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
g2d = image.createGraphics();
g2d.setFont(new Font("Arial", Font.PLAIN, 12));
fontMetrics = g2d.getFontMetrics();
}
@Test
public void testWrapWithNewline() {
String text = "Line 1\nLine 2\nLine 3";
int maxWidth = 1000; // Large enough to not wrap
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithCarriageReturn() {
String text = "Line 1\rLine 2\rLine 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithCarriageReturnLineFeed() {
String text = "Line 1\r\nLine 2\r\nLine 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithMixedNewlines() {
String text = "Line 1\nLine 2\rLine 3\r\nLine 4";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 4 lines", 4, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
assertEquals("Line 4", lines.get(3));
}
@Test
public void testWrapWithNewlineAndBrTag() {
String text = "Line 1\nLine 2<br>Line 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithNewlineAndWrapping() {
String text = "First line\nSecond line with very long text that should wrap";
int maxWidth = fontMetrics.stringWidth("Second line with very long text that should"); // Force wrapping
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertTrue("Should have at least 2 lines (newline + wrapped)", lines.size() >= 2);
assertEquals("First line", lines.get(0));
// The second line should be wrapped, so we should have more lines
assertTrue("Second line should be wrapped", lines.size() >= 3);
}
@Test
public void testWrapWithEmptyLines() {
String text = "Line 1\n\nLine 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines including empty line", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithNewlineAtEnd() {
String text = "Line 1\nLine 2\n";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 2 lines (no trailing empty line)", 2, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
}
@Test
public void testWrapWithNoNewlines() {
String text = "This is a single line of text";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should have 1 line", 1, lines.size());
assertEquals("This is a single line of text", lines.get(0));
}
@Test
public void testWrapWithBrTagOnly() {
String text = "Line 1<br>Line 2<br>Line 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithNewlineAndBrTagMixed() {
String text = "Line 1\nLine 2<br>Line 3\nLine 4<br>Line 5";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 5 lines", 5, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("Line 2", lines.get(1));
assertEquals("Line 3", lines.get(2));
assertEquals("Line 4", lines.get(3));
assertEquals("Line 5", lines.get(4));
}
@Test
public void testWrapWithMultipleConsecutiveNewlines() {
String text = "Line 1\n\n\nLine 4";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 4 lines with 2 empty lines", 4, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("", lines.get(1));
assertEquals("", lines.get(2));
assertEquals("Line 4", lines.get(3));
}
@Test
public void testWrapWithMultipleConsecutiveNewlinesAtStart() {
String text = "\n\nLine 3";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 3 lines with 2 empty lines at start", 3, lines.size());
assertEquals("", lines.get(0));
assertEquals("", lines.get(1));
assertEquals("Line 3", lines.get(2));
}
@Test
public void testWrapWithMultipleConsecutiveNewlinesAtEnd() {
String text = "Line 1\n\n\n";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
// Trailing newlines: the last newline at end doesn't create an empty line
// So \n\n\n at end creates 2 empty lines (from first 2 newlines)
assertEquals("Should split into 3 lines (2 empty from trailing newlines)", 3, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("", lines.get(1));
assertEquals("", lines.get(2));
}
@Test
public void testWrapWithMultipleConsecutiveNewlinesMixed() {
String text = "Line 1\n\nLine 3\n\n\nLine 6";
int maxWidth = 1000;
List<String> lines = StringUtils.wrap(text, fontMetrics, maxWidth);
assertEquals("Should split into 6 lines", 6, lines.size());
assertEquals("Line 1", lines.get(0));
assertEquals("", lines.get(1));
assertEquals("Line 3", lines.get(2));
assertEquals("", lines.get(3));
assertEquals("", lines.get(4));
assertEquals("Line 6", lines.get(5));
}
}
@@ -0,0 +1,23 @@
package org.diylc.netlist;
import org.diylc.components.guitar.Freeway3x3_05;
import org.junit.Test;
public class Freeway3x3_05Tests extends AbstractSwitchTests {
@Test
public void testFreeway3x3_05() {
Freeway3x3_05 sw = new Freeway3x3_05();
String[] validCombinations = new String[]{
"0,0,6", "0,6,11", "0,0,11", "0,17,21", "0,21,22", "0,17,22",
"1,1,6", "1,6,12", "1,1,12", "1,18,21", "1,21,23", "1,18,23",
"2,2,6", "2,6,13", "2,2,13", "2,19,21", "2,21,24", "2,19,24",
"3,5,6", "3,6,10", "3,5,10", "3,16,21", "3,21,27", "3,16,27",
"4,4,6", "4,6,9", "4,4,9", "4,15,21", "4,21,26", "4,15,26",
"5,3,6", "5,6,8", "5,3,8", "5,14,21", "5,21,25", "5,14,25",
};
testSwitch(sw, validCombinations);
}
}
@@ -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);
}
}
@@ -0,0 +1,49 @@
package org.diylc.netlist;
import org.junit.Test;
import org.diylc.components.electromechanical.TactileMicroSwitch;
import org.diylc.components.electromechanical.TactileMicroSwitch.PinPairingMode;
public class TactileMicroSwitchTests extends AbstractSwitchTests {
@Test
public void testTactileMicroSwitchOpposite() {
TactileMicroSwitch tactileSwitch = new TactileMicroSwitch();
tactileSwitch.setPinPairingMode(PinPairingMode.Opposite);
// Position 0 (Released): no connections
// Position 1 (Pressed): pins on opposite sides (same row): 0-2 and 1-3
String[] validCombinations = new String[] {
"1,0,2", "1,2,0", // Position 1: 0-2 (both directions)
"1,1,3", "1,3,1" // Position 1: 1-3 (both directions)
};
testSwitch(tactileSwitch, validCombinations);
}
@Test
public void testTactileMicroSwitchDiagonal() {
TactileMicroSwitch tactileSwitch = new TactileMicroSwitch();
tactileSwitch.setPinPairingMode(PinPairingMode.Diagonal);
// Position 0 (Released): no connections
// Position 1 (Pressed): diagonally opposite pins: 0-3 and 1-2
String[] validCombinations = new String[] {
"1,0,3", "1,3,0", // Position 1: 0-3 (both directions)
"1,1,2", "1,2,1" // Position 1: 1-2 (both directions)
};
testSwitch(tactileSwitch, validCombinations);
}
@Test
public void testTactileMicroSwitchAdjacent() {
TactileMicroSwitch tactileSwitch = new TactileMicroSwitch();
tactileSwitch.setPinPairingMode(PinPairingMode.Adjacent);
// Position 0 (Released): no connections
// Position 1 (Pressed): pins next to each other (same column): 0-1 and 2-3
String[] validCombinations = new String[] {
"1,0,1", "1,1,0", // Position 1: 0-1 (both directions)
"1,2,3", "1,3,2" // Position 1: 2-3 (both directions)
};
testSwitch(tactileSwitch, 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);
}
@@ -5,11 +5,7 @@ import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import javax.swing.AbstractAction;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.swingframework.CheckBoxListDialog;
@@ -69,7 +65,7 @@ public class ExportBlocksAction extends AbstractAction {
dialog.setVisible(true);
if (dialog.getSelectedButtonCaption() != "OK")
if (!Objects.equals(dialog.getSelectedButtonCaption(), "OK"))
return;
Object[] selected = dialog.getSelectedOptions();
@@ -133,4 +129,4 @@ public class ExportBlocksAction extends AbstractAction {
}, true);
}
}
}
}
@@ -5,13 +5,7 @@ import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.*;
import javax.swing.AbstractAction;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.swingframework.CheckBoxListDialog;
@@ -73,7 +67,7 @@ public class ExportVariantsAction extends AbstractAction {
ActionFactory.LOG.warn("Could not find type for: " + className);
}
Collections.sort(types, new Comparator<ComponentType>() {
types.sort(new Comparator<ComponentType>() {
@Override
public int compare(ComponentType o1, ComponentType o2) {
@@ -86,7 +80,7 @@ public class ExportVariantsAction extends AbstractAction {
dialog.setVisible(true);
if (dialog.getSelectedButtonCaption() != "OK")
if (!Objects.equals(dialog.getSelectedButtonCaption(), "OK"))
return;
Object[] selected = dialog.getSelectedOptions();
@@ -131,7 +125,7 @@ public class ExportVariantsAction extends AbstractAction {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
ProjectFileManager.xStreamSerializer.toXML(variantPkg, out);
out.close();
ActionFactory.LOG.info("Exported variants succesfully");
ActionFactory.LOG.info("Exported variants successfully");
} catch (IOException e) {
ActionFactory.LOG.error("Could not export variants", e);
}
@@ -153,4 +147,4 @@ public class ExportVariantsAction extends AbstractAction {
}, true);
}
}
}
}
@@ -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);
}
}
}

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