Compare commits

...

85 Commits

Author SHA1 Message Date
Bane Stojkovic 5d8555e316 Update ChatbotPane.java 2026-03-26 22:49:38 +01:00
Bane Stojkovic 614652ca97 release 2026-03-26 22:47:14 +01:00
Bane Stojkovic af3887b9ac Clicking on a component type in the left-hand component tree sometimes does not register and does not instantiate the component 2026-03-26 20:33:16 +01:00
Bane Stojkovic 150e03c023 Pressing the 'Esc' key closes selection editor dialog 2026-03-26 19:57:18 +01:00
Bane Stojkovic cd0cb776b0 Right click (or two finger tap on mac) after selecting a component with a regular click opens editor dialog in addition to the context menu 2026-03-26 19:51:56 +01:00
Bane Stojkovic ebd66eea4f minor improvements 2026-02-27 14:13:46 +01:00
Bane Stojkovic 7d24621c18 Cleanup of code that doesn't work for fixing tooltip colors on linux mint (kde bug) 2026-02-22 22:34:10 +01:00
Bane Stojkovic a7442e14e4 Better error handling for AI tools< 2026-02-21 22:36:32 +01:00
Bane Stojkovic 03af702933 Redo unit conversion 2026-02-20 22:31:40 +01:00
Bane Stojkovic 303e59303b Added a new section in the output of 'AI Circuit Analyzer' which provides insights into the operation of the circuit and individual components 2026-02-20 22:12:19 +01:00
Bane Stojkovic 508d1db513 Added 'Auto-Unit Conversion' configuration which automatically converts between metric and imperial units based on the ruler unit preference 2026-02-17 20:44:50 +01:00
Bane Stojkovic c4efe7d20f Update draft-github-release.sh 2026-02-15 22:52:53 +01:00
Bane Stojkovic fccda215fc release 2026-02-15 22:49:47 +01:00
Bane Stojkovic d89061b355 Windows installer prompts for installation folder instead of forcing one 2026-02-15 22:26:27 +01:00
Bane Stojkovic f0c482d2d5 Added ability to export chat history from 'AI Assistant' to a HTML file 2026-02-15 20:40:05 +01:00
Bane Stojkovic f5fedc1a24 Update update.xml 2026-02-14 23:17:06 +01:00
Bane Stojkovic b3a1187c1c Improved rendering of 'Switch (Latching)' symbol to clearly make visual contact between the terminals 2026-02-13 22:26:23 +01:00
Bane Stojkovic 7e79143079 Ensure no duplicate IDs when loading 2026-02-11 16:59:16 +01:00
Bane Stojkovic d976959e95 Using "Duplicate Selection" results in duplicates of component ids, breaking Project Explorer panel and causing other issues downstream 2026-02-11 16:47:08 +01:00
Bane Stojkovic 8b69903893 Consolidate a few keyboard shortcuts for consistency 2026-02-09 23:15:35 +01:00
Bane Stojkovic 391d161c59 Add support for logging all component types to JSON 2026-02-09 16:18:05 +01:00
Bane Stojkovic d970fa10a7 Create component_index.prompt 2026-02-09 16:17:19 +01:00
Bane Stojkovic b6816e6a62 typo 2026-02-09 16:17:15 +01:00
Bane Stojkovic 3272efc8bf Typo fix 2026-02-09 16:14:35 +01:00
Bane Stojkovic 0aed29f072 Remove shapes from BOM 2026-02-06 22:56:08 +01:00
Bane Stojkovic 7e2319746e Bring back calculate length for curved components 2026-02-06 22:55:40 +01:00
Bane Stojkovic 1cf776ace5 Resize dimensions do not show for 'Chassis Panel' component when dragging a corner point 2026-01-30 12:52:00 +01:00
Bane Stojkovic b9da7510a8 5.10 release 2026-01-20 20:37:04 +01:00
Bane Stojkovic 6fde016ed4 All building blocks of the same type get added to the same group 2026-01-20 20:34:32 +01:00
Bane Stojkovic a32ad6b733 fix issue with null objects in groups 2026-01-19 23:22:19 +01:00
Bane Stojkovic be16910767 fix copy paste groups 2026-01-19 23:15:46 +01:00
Bane Stojkovic 457f47782f Added E-C-B BJT pinout to TO-92 transistor package 2026-01-16 22:29:14 +01:00
Bane Stojkovic b69729c14f Trimmer improvements 2026-01-16 20:37:21 +01:00
Clive Hayward a88d8f5e03 Added Box style multi-turn potentiometer component. (#987)
* Added Box style multi-turn potentiometer component.

Implements box style multi-turn trimmer potentiometer (e.g. Bourns PV36W/X) with inline pins for stripboard layouts.

Features:
- 3 inline pins on 0.1" spacing (CCW, Wiper, CW)
- Configurable resistance value and orientation
- Blue body (4.82mm × 9.53mm) with brass adjustment screw
- Screw positioned in lower left corner with horizontal slot
- Label positioned outside body for readability
- Pins centered in body width for all orientations
- Side profile icon showing brass screw and three pins
- Locked pin movement - component moves as single unit

Dimensions based on common box-style multi-turn potentiometers.
Default orientation is vertical (_90) with pins horizontal.

Category: Passive
Instance prefix: VR

* Handles mirroring and rotational transforms.

* Handles mirroring and rotational transforms.
2026-01-16 20:19:21 +01:00
Bane Stojkovic f4223f5773 Merge branch 'master' of https://github.com/bancika/diy-layout-creator 2026-01-16 20:08:34 +01:00
Bane Stojkovic 513cc67362 Update update.xml 2026-01-16 20:08:27 +01:00
Martin Morrell d8f2a3b6e0 Add 'M' and 'N' potentiometer taper options (#990) 2026-01-16 20:07:33 +01:00
Bane Stojkovic 7900128d7c Tactile Micro Switch' editor doesn't show up on double click 2026-01-16 20:03:01 +01:00
Bane Stojkovic 1cef0c0977 Explorer group (#992)
* show groups in explorer

* introduce component ids. Refactor how groups are modeled in the Project

* Group naming added

* cleanup

* release
2026-01-10 20:44:52 +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 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
126 changed files with 10440 additions and 3633 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/
+17
View File
@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="RegressionTestRunner" type="Application" factoryName="Application" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="org.diylc.RegressionTestRunner" />
<module name="diylc-swing" />
<option name="PROGRAM_PARAMETERS" value="$PROJECT_DIR$/../diylc-regression-data TEST" />
<option name="VM_PARAMETERS" value="--add-exports java.desktop/com.apple.eawt.event=ALL-UNNAMED --add-exports java.desktop/com.apple.eio=ALL-UNNAMED --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" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="org.diylc.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
+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.7.0</version>
<version>5.12.0</version>
</parent>
<artifactId>diylc-core</artifactId>
@@ -29,6 +29,7 @@ import java.io.Serializable;
import java.util.List;
import java.util.Set;
import org.diylc.core.ComponentGroup;
import org.diylc.core.IDIYComponent;
/**
@@ -44,19 +45,21 @@ public class ComponentTransferable implements Transferable, Serializable {
public static final DataFlavor listFlavor = new DataFlavor(ComponentTransferable.class, "application/diylc");
private List<IDIYComponent<?>> components;
private Set<Set<IDIYComponent<?>>> groups;
private Set<ComponentGroup> groups;
private String name;
public ComponentTransferable() {
super();
}
public ComponentTransferable(List<IDIYComponent<?>> components) {
public ComponentTransferable(List<IDIYComponent<?>> components, String name) {
super();
this.components = components;
this.name = name;
this.groups = null;
}
public ComponentTransferable(List<IDIYComponent<?>> components, Set<Set<IDIYComponent<?>>> groups) {
public ComponentTransferable(List<IDIYComponent<?>> components, Set<ComponentGroup> groups) {
super();
this.components = components;
this.groups = groups;
@@ -84,7 +87,11 @@ public class ComponentTransferable implements Transferable, Serializable {
return components;
}
public Set<Set<IDIYComponent<?>>> getGroups() {
public Set<ComponentGroup> getGroups() {
return groups;
}
public String getName() {
return name;
}
}
@@ -17,15 +17,11 @@
*/
package org.diylc.clipboard;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
import org.diylc.core.ComponentGroup;
import org.diylc.core.IDIYComponent;
public class ComponentTransferableFactory {
@@ -40,13 +36,19 @@ public class ComponentTransferableFactory {
return instance;
}
public ComponentTransferable build(Collection<IDIYComponent<?>> selectedComponents, Set<Set<IDIYComponent<?>>> groups) {
Set<IDIYComponent<?>> originalComponentSet = new HashSet<IDIYComponent<?>>(selectedComponents);
public ComponentTransferable build(Collection<IDIYComponent<?>> selectedComponents, Set<ComponentGroup> groups) {
List<IDIYComponent<?>> originalComponents = new ArrayList<IDIYComponent<?>>(selectedComponents);
Map<UUID, UUID> idMap = new HashMap<>();
List<IDIYComponent<?>> clonedComponents = originalComponents.stream(
).map(x -> {
try {
return x.clone();
IDIYComponent<?> clone = x.clone();
clone.setId(UUID.randomUUID());
idMap.put(x.getId(), clone.getId());
return clone;
} catch (CloneNotSupportedException e) {
LOG.error(e);
return null;
@@ -54,22 +56,18 @@ public class ComponentTransferableFactory {
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
Set<Set<IDIYComponent<?>>> clonedGroups = new HashSet<Set<IDIYComponent<?>>>();
for (Set<IDIYComponent<?>> group : groups) {
if (group.isEmpty())
continue;
if (originalComponentSet.contains(group.iterator().next())) {
Set<IDIYComponent<?>> clonedGroup = new HashSet<IDIYComponent<?>>();
clonedGroups.add(clonedGroup);
for (int i = 0; i < originalComponents.size(); i++) {
if (group.contains(originalComponents.get(i))) {
clonedGroup.add(clonedComponents.get(i));
}
}
}
}
Set<ComponentGroup> clonedGroups = groups.stream()
.map(group -> {
Set<UUID> newIds = group.getComponentIds().stream()
.map(idMap::get)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// Only create group if it has at least one valid ID
return newIds.isEmpty() ? null : new ComponentGroup(newIds, group.getName());
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
return new ComponentTransferable(clonedComponents, clonedGroups);
}
@@ -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,
@@ -0,0 +1,52 @@
/*
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.common;
import java.util.Set;
import org.diylc.core.ComponentGroup;
/**
* Interface for component grouping operations.
*
* @author Branislav Stojkovic
*/
public interface IGroupProcessor {
/**
* Returns the set of component groups in the current project. Each group is a set of components
* that are grouped together.
*
* @return set of component groups
*/
Set<ComponentGroup> getComponentGroups();
/**
* Groups all selected components.
*/
void groupSelectedComponents();
/**
* Ungroups all selected components.
*/
void ungroupSelectedComponents();
}
@@ -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,22 @@ 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 AUTO_UNIT_CONVERSION_KEY = "autoUnitConversion";
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 +114,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 +143,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 +164,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 +197,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 +206,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 +214,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 +228,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 +275,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 +283,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 +297,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();
/**
@@ -31,7 +31,7 @@ import org.diylc.core.ExpansionMode;
import org.diylc.core.IDIYComponent;
import org.diylc.core.measures.Size;
public interface ISelectionProcessor {
public interface ISelectionProcessor extends IGroupProcessor {
/**
* Returns the current {@link ComponentTransferable}.
@@ -89,16 +89,6 @@ public interface ISelectionProcessor {
*/
void rotateSelection(int direction);
/**
* Groups all selected components.
*/
void groupSelectedComponents();
/**
* Ungroups all selected components.
*/
void ungroupSelectedComponents();
/**
* Mirrors selected components in the given axis.
*
@@ -24,8 +24,11 @@ package org.diylc.common;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.core.IDynamicPropertySource;
import org.diylc.core.IPropertyValidator;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
/**
* Entity class for editable properties extracted from component objects. Represents a single
@@ -65,7 +68,8 @@ public class PropertyWrapper implements Cloneable {
public void readFrom(Object object) throws IllegalArgumentException, IllegalAccessException,
InvocationTargetException, SecurityException, NoSuchMethodException {
this.ownerObject = object;
this.value = getGetter().invoke(object);
Object value = getGetter().invoke(object);
this.value = value;
}
// public void readUniqueFrom(IDIYComponent component)
@@ -30,6 +30,7 @@ import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.UUID;
import java.util.stream.Collectors;
import org.diylc.core.IDIYComponent;
@@ -52,6 +53,7 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
private static final long serialVersionUID = 1L;
protected UUID id = UUID.randomUUID();
protected String name = "";
public static Color SELECTION_COLOR = Color.red;
@@ -62,6 +64,7 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
public static Color LIGHT_METAL_COLOR = Color.decode("#EEEEEE");
public static Color COPPER_COLOR = Color.decode("#DA8A67");
public static Color GOLD_COLOR = Color.decode("#F4B301");
public static Color BRASS_COLOR = Color.decode("#B8860B");
public static Color FR4_COLOR = Color.decode("#6B9B6B");
public static Color FR4_LIGHT_COLOR = Color.decode("#BABE71");
public static Color PHENOLIC_COLOR = Color.decode("#F8EBB3");
@@ -78,6 +81,19 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
this.name = name;
}
@Override
public UUID getId() {
if (id == null) {
id = UUID.randomUUID();
}
return id;
}
@Override
public void setId(UUID id) {
this.id = id;
}
@Override
public boolean canControlPointOverlap(int index) {
return false;
@@ -186,6 +202,10 @@ public abstract class AbstractComponent<T> implements IDIYComponent<T> {
field.setAccessible(true);
Object value = field.get(this);
if (field.getName().equalsIgnoreCase("id")) {
value = getId();
}
// Deep copy point arrays.
// TODO: something nicer
if (value != null && value.getClass().isArray()
@@ -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,12 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
// }
}
@EditableProperty
@EditableProperty(name = "Length")
public Size getLength() {
return calculateLength();
}
public Size calculateLength() {
double d = 0;
try {
Point2D[] p = getControlPoints();
@@ -0,0 +1,43 @@
package org.diylc.core;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class ComponentGroup implements Serializable {
private Set<UUID> componentIds;
private String name;
public ComponentGroup(Set<UUID> componentIds) {
this.componentIds = componentIds;
}
public ComponentGroup(Set<UUID> componentIds, String name) {
this.componentIds = componentIds;
this.name = name;
}
public static ComponentGroup from(Collection<IDIYComponent<?>> components, String name) {
Set<UUID> componentIds = components.stream()
.map(IDIYComponent::getId)
.filter(id -> id != null)
.collect(Collectors.toSet());
return new ComponentGroup(componentIds, name);
}
public Set<UUID> getComponentIds() {
return componentIds;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -25,6 +25,7 @@ import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.UUID;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
@@ -55,6 +56,10 @@ public interface IDIYComponent<T> extends Serializable, Cloneable {
public static final int WIRING = 5;
public static final int TEXT = 6;
UUID getId();
void setId(UUID id);
/**
* @return component instance name.
*/
@@ -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();
}
@@ -23,13 +23,9 @@ package org.diylc.core;
import java.awt.Font;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import org.diylc.appframework.update.VersionNumber;
import org.diylc.core.annotations.EditableProperty;
@@ -67,6 +63,7 @@ public class Project implements Serializable, Cloneable {
private Integer dotSpacing;
private List<IDIYComponent<?>> components;
private Set<Set<IDIYComponent<?>>> groups;
private Set<ComponentGroup> groupsEx;
private Set<Integer> lockedLayers;
private Set<IDIYComponent<?>> lockedComponents;
private Set<Integer> hiddenLayers;
@@ -75,6 +72,7 @@ public class Project implements Serializable, Cloneable {
public Project() {
components = new ArrayList<IDIYComponent<?>>();
groups = new HashSet<Set<IDIYComponent<?>>>();
groupsEx = new HashSet<>();
lockedLayers = new HashSet<Integer>();
hiddenLayers = new HashSet<Integer>();
lockedComponents = new HashSet<IDIYComponent<?>>();
@@ -153,22 +151,35 @@ public class Project implements Serializable, Cloneable {
/**
* List of components sorted by z-order ascending.
*
* s
* @return
*/
public List<IDIYComponent<?>> getComponents() {
return components;
}
/**
* Set of grouped components.
*
* @return
*/
public Set<Set<IDIYComponent<?>>> getGroups() {
return groups;
public Set<ComponentGroup> getGroupsEx() {
if (groupsEx == null) {
groupsEx = new HashSet<>();
if (groups != null && !groups.isEmpty()) {
for (Set<IDIYComponent<?>> group : groups) {
Set<IDIYComponent<?>> nonNullIds =
group.stream().filter(Objects::nonNull).collect(Collectors.toSet());
if (!nonNullIds.isEmpty()) {
ComponentGroup newGroup = ComponentGroup.from(group, null);
groupsEx.add(newGroup);
}
}
groups.clear();
}
}
groupsEx.removeAll(groupsEx.stream().
filter(x -> x.getComponentIds() == null ||
x.getComponentIds().stream().noneMatch(Objects::nonNull)).collect(Collectors.toSet()));
return groupsEx;
}
public Set<IDIYComponent<?>> getLockedComponents() {
if (lockedComponents == null)
lockedComponents = new HashSet<IDIYComponent<?>>();
@@ -292,6 +303,11 @@ public class Project implements Serializable, Cloneable {
return false;
} else if (!groups.equals(other.groups))
return false;
if (groupsEx == null) {
if (other.groupsEx != null)
return false;
} else if (!groupsEx.equals(other.groupsEx))
return false;
if (lockedLayers == null) {
if (other.lockedLayers != null)
return false;
@@ -356,12 +372,15 @@ public class Project implements Serializable, Cloneable {
}
}
for (Set<IDIYComponent<?>> group : this.groups) {
Set<IDIYComponent<?>> cloneGroup = new HashSet<IDIYComponent<?>>();
for (IDIYComponent<?> component : group) {
cloneGroup.add(cloneMap.get(component));
for (ComponentGroup group : this.getGroupsEx()) {
// Filter out null IDs when cloning groups
Set<UUID> validIds = group.getComponentIds().stream()
.filter(id -> id != null)
.collect(Collectors.toSet());
// Only add group if it has valid component IDs
if (!validIds.isEmpty()) {
project.getGroupsEx().add(new ComponentGroup(validIds, group.getName()));
}
project.groups.add(cloneGroup);
}
for (IDIYComponent<?> component : this.getLockedComponents()) {
@@ -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) {
}
@@ -46,11 +46,21 @@ public class Size extends AbstractMeasure<SizeUnit> implements Comparable<Size>
return getValue() * getUnit().getFactor() / SizeUnit.in.getFactor() * Constants.PIXELS_PER_INCH;
}
public double convertToUnits(SizeUnit unit) {
/**
* Converts this size to the given unit. Returns a new Size with the same
* physical length expressed in the target unit.
*/
public Size convertToUnits(SizeUnit unit) {
if (unit == getUnit()) {
return this.getValue();
return new Size(roundTo4Decimals(getValue()), unit);
}
return convertToPixels() * SizeUnit.px.getFactor();
double lengthInBase = getValue() * getUnit().getFactor();
double valueInTargetUnit = lengthInBase / unit.getFactor();
return new Size(roundTo4Decimals(valueInTargetUnit), unit);
}
private static double roundTo4Decimals(double value) {
return Math.round(value * 10000) / 10000.0;
}
@Override
@@ -23,18 +23,35 @@ package org.diylc.core.measures;
import org.diylc.utils.Constants;
import java.util.Map;
public enum SizeUnit implements Unit {
px(25.4d / Constants.PIXELS_PER_INCH), mm(1d), cm(10d), m(1e3d), in(25.4d), ft(25.4d * 12), yd(9144d);
px(25.4d / Constants.PIXELS_PER_INCH, SizeUnitType.Digital),
mm(1d, SizeUnitType.Metric),
cm(10d, SizeUnitType.Metric),
m(1e3d, SizeUnitType.Metric),
in(25.4d, SizeUnitType.Imperial),
ft(25.4d * 12, SizeUnitType.Imperial),
yd(9144d, SizeUnitType.Imperial);
double factor;
SizeUnitType type;
private SizeUnit(double factor) {
private SizeUnit(double factor, SizeUnitType type) {
this.factor = factor;
this.type = type;
}
@Override
public double getFactor() {
return factor;
}
public SizeUnitType getType() {
return type;
}
public static final Map<SizeUnit, SizeUnit> ConversionMapping =
Map.of(mm, in, cm, in, m, yd, in, mm, ft, cm, yd, m);
}
@@ -0,0 +1,5 @@
package org.diylc.core.measures;
public enum SizeUnitType {
Metric, Imperial, Digital
}
@@ -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);
}
}
@@ -38,6 +38,7 @@ import org.diylc.core.ISwitch;
import org.diylc.netlist.*;
import org.diylc.plugins.chatbot.model.*;
import org.diylc.plugins.cloud.model.IServiceAPI;
import org.diylc.plugins.cloud.service.CloudException;
import org.diylc.plugins.cloud.service.NotLoggedInException;
import org.diylc.presenter.ComponentProcessor;
import org.diylc.presenter.ContinuityArea;
@@ -62,6 +63,8 @@ public class ChatbotService {
private final static Logger LOG = Logger.getLogger(ChatbotService.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
public static final String SERVER_ERROR_PLEASE_TRY_AGAIN = "Server error, please try again.";
static {
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
@@ -114,7 +117,7 @@ public class ChatbotService {
return service;
}
public String promptChatbot(String prompt) throws NotLoggedInException {
public String promptChatbot(String prompt) throws NotLoggedInException, CloudException {
if (!plugInPort.getCloudService().isLoggedIn())
throw new NotLoggedInException();
@@ -126,13 +129,13 @@ public class ChatbotService {
File aiProjectFile = getAiProjectFile();
try {
return getService().promptChatbot(
plugInPort.getCloudService().getCurrentUsername(),
String response = getService().promptChatbot(plugInPort.getCloudService().getCurrentUsername(),
plugInPort.getCloudService().getCurrentToken(),
plugInPort.getCloudService().getMachineId(),
fileName,
aiProjectFile,
prompt);
plugInPort.getCloudService().getMachineId(), fileName, aiProjectFile, prompt);
return Optional.ofNullable(response).orElse(SERVER_ERROR_PLEASE_TRY_AGAIN);
} catch (Exception e) {
LOG.error("Chatbot error", e);
throw new CloudException(SERVER_ERROR_PLEASE_TRY_AGAIN);
} finally {
// Clean up the temp file immediately after use
if (aiProjectFile != null && aiProjectFile.exists()) {
@@ -141,7 +144,7 @@ public class ChatbotService {
}
}
public String analyzeCircuit() throws NotLoggedInException {
public String analyzeCircuit() throws NotLoggedInException, CloudException {
if (!plugInPort.getCloudService().isLoggedIn())
throw new NotLoggedInException();
@@ -156,9 +159,15 @@ public class ChatbotService {
String jsonResult = getService().analyzeCircuit(plugInPort.getCloudService().getCurrentUsername(),
plugInPort.getCloudService().getCurrentToken(),
plugInPort.getCloudService().getMachineId(), fileName, aiProjectFile);
if (jsonResult == null || jsonResult.trim().isEmpty()) {
throw new CloudException(SERVER_ERROR_PLEASE_TRY_AGAIN);
}
return JsonToHtmlConverter.convertToHtml(jsonResult);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (Exception e) {
LOG.error("AI Analyzer error", e);
throw new CloudException(SERVER_ERROR_PLEASE_TRY_AGAIN);
} finally {
// Clean up the temp file immediately after use
if (aiProjectFile != null && aiProjectFile.exists()) {
@@ -188,6 +197,7 @@ public class ChatbotService {
- Coordinates are represented in pixels assuming 200px/in resolution.
- Each component will either have a set of terminals that are connectable to other components or one or two points defining its position.
- Netlists of connected terminals are defined in the 'nets' structure.
- Connective components (wires, jumpers, traces, etc) are not part of the netlist, they are there to understand physical diagram but their conductive properties are already taken into account when constructing the netlist
- Analyze labels - they could provide clues about nearby terminals and what they represent.
""");
if (aiProject.switches() != null && !aiProject.switches().isEmpty()) {
@@ -33,6 +33,7 @@ public class JsonToHtmlConverter {
.append("</head>\n<body>\n");
// Process each section
processSection(root, "analysis", html, "#008000");
processSection(root, "critical", html, "#c00");
processSection(root, "warnings", html, "#e65c00");
processSection(root, "optimizations", html, "#0066c0");
@@ -18,13 +18,8 @@
package org.diylc.presenter;
import java.io.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.IConfigurationManager;
@@ -145,6 +140,7 @@ public class BuildingBlockManager {
clone.setName(instantiationManager.createUniqueName(ComponentProcessor.getInstance()
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) clone.getClass()),
testComponents));
clone.setId(UUID.randomUUID());
testComponents.add(clone);
clones.add(clone);
} catch (CloneNotSupportedException e) {
@@ -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);
}
}
@@ -28,6 +28,10 @@ import java.awt.image.BufferedImage;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import org.apache.log4j.Logger;
@@ -63,6 +67,8 @@ public class ComponentProcessor {
private static final Logger LOG = Logger.getLogger(ComponentProcessor.class);
private static final boolean LOG_COMPONENT_TYPE_DETAILS = false;
private static ComponentProcessor instance;
private Map<String, List<PropertyWrapper>> propertyCache;
@@ -219,6 +225,18 @@ public class ComponentProcessor {
return cloneProperties(properties);
}
private static String formatPropertyType(Class<?> type) {
return type == null ? null : type.getSimpleName();
}
private static List<String> getPossibleValues(Class<?> type) {
if (type == null || !type.isEnum())
return null;
return Arrays.stream(type.getEnumConstants())
.map(Object::toString)
.collect(Collectors.toList());
}
private List<PropertyWrapper> cloneProperties(List<PropertyWrapper> properties) {
List<PropertyWrapper> result = new ArrayList<PropertyWrapper>(properties.size());
for (PropertyWrapper propertyWrapper : properties) {
@@ -243,6 +261,7 @@ public class ComponentProcessor {
Set<Class<?>> componentTypeClasses = null;
try {
componentTypeClasses = reflections.getTypesAnnotatedWith(ComponentDescriptor.class, false);
List<Map<String, Object>> componentSnapshots = LOG_COMPONENT_TYPE_DETAILS ? new ArrayList<>() : null;
for (Class<?> clazz : componentTypeClasses) {
if (!Modifier.isAbstract(clazz.getModifiers()) && IDIYComponent.class.isAssignableFrom(clazz)) {
@@ -262,6 +281,42 @@ public class ComponentProcessor {
componentTypes.put(componentType.getCategory(), nestedList);
}
nestedList.add(componentType);
if (LOG_COMPONENT_TYPE_DETAILS) {
try {
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("name", componentType.getName());
snapshot.put("description", componentType.getDescription());
snapshot.put("creationMethod", componentType.getCreationMethod());
snapshot.put("category", componentType.getCategory());
List<PropertyWrapper> props = propertyCache.get(clazz.getName());
snapshot.put("properties", props != null ? props.stream()
.map(p -> {
Map<String, Object> prop = new LinkedHashMap<>();
prop.put("name", p.getName());
prop.put("type", formatPropertyType(p.getType()));
List<String> possibleValues = getPossibleValues(p.getType());
if (possibleValues != null) {
prop.put("possibleValues", possibleValues);
}
return prop;
})
.collect(Collectors.toList()) : Collections.emptyList());
componentSnapshots.add(snapshot);
} catch (Exception e) {
LOG.warn("Failed to log component type details for " + clazz.getName(), e);
}
}
}
}
if (LOG_COMPONENT_TYPE_DETAILS && componentSnapshots != null && !componentSnapshots.isEmpty()) {
try {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
LOG.info("Component types: " + mapper.writeValueAsString(componentSnapshots));
} catch (Exception e) {
LOG.warn("Failed to log component types JSON", e);
}
}
@@ -22,6 +22,7 @@ import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
@@ -38,7 +39,7 @@ public class DatasheetService {
private static DatasheetService instance;
private static Map<String, List<String[]>> datasheetCache = new HashMap<String, List<String[]>>();
private static Map<String, List<String[]>> datasheetCache = new ConcurrentHashMap<>();
private static Map<String, Map<String, List<String[]>>> lookupCache =
new HashMap<String, Map<String, List<String[]>>>();
@@ -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
@@ -24,13 +24,7 @@ package org.diylc.presenter;
import java.awt.geom.Point2D;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.ConfigurationManager;
@@ -40,6 +34,7 @@ import org.diylc.clipboard.ComponentTransferable;
import org.diylc.common.*;
import org.diylc.core.Angle;
import org.diylc.core.ComponentGroup;
import org.diylc.core.CreationMethod;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
@@ -68,9 +63,9 @@ public class InstantiationManager {
public static final ComponentType clipboardType = new ComponentType("Clipboard contents",
"Components from the clipboard", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
public static final ComponentType blockType = new ComponentType("Building block",
"Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
// public static final ComponentType blockType = new ComponentType("Building block",
// "Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
// false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0);
public InstantiationManager() {}
@@ -213,7 +208,10 @@ public class InstantiationManager {
// Update the component type slot so the app knows that something's
// being instantiated.
this.componentTypeSlot = autoGroup ? blockType : clipboardType;
String slotName = Optional.ofNullable(componentTransferable.getName()).orElse("Building Block");
this.componentTypeSlot = autoGroup ? new ComponentType(slotName,
"Components from the building block", CreationMethod.SINGLE_CLICK, "Multi", "", "", null, null, 0, false, null,
false, null, KeywordPolicy.NEVER_SHOW, null, false, null, 0) : clipboardType;
if (snapToGrid) {
scaledPoint = new Point2D.Double(scaledPoint.getX(), scaledPoint.getY());
@@ -222,9 +220,14 @@ public class InstantiationManager {
// Update the location according to mouse location
updateSingleClick(scaledPoint, snapToGrid, gridSpacing);
// copy group information if available
// copy group information if available, but filter out invalid groups
if (componentTransferable.getGroups() != null) {
currentProject.getGroups().addAll(componentTransferable.getGroups());
for (ComponentGroup group : componentTransferable.getGroups()) {
if (group.getComponentIds() != null && !group.getComponentIds().isEmpty() &&
group.getComponentIds().stream().anyMatch(id -> id != null)) {
currentProject.getGroupsEx().add(group);
}
}
}
}
@@ -273,7 +276,7 @@ public class InstantiationManager {
private List<IDIYComponent<?>> instantiateComponent(ComponentType componentType, Point2D point,
Project currentProject) throws InstantiationException, IllegalAccessException, NoSuchMethodException,
SecurityException, IllegalArgumentException, InvocationTargetException {
LOG.info("Instatiating component of type: " + componentType.getInstanceClass().getName());
LOG.info("Instantiating component of type: " + componentType.getInstanceClass().getName());
// Instantiate the component.
IDIYComponent<?> component;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,131 @@
You are helping me generate Appendix A ? Component Model Reference for an ebook about DIY Layout Creator (DIYLC).
I will provide you with a JSON file that defines all component types used by DIYLC.
Your task
Convert the JSON into a single DOCX appendix that documents all components defined in the JSON, with no omissions and no inferred or invented information.
Structure rules (must follow exactly)
1. Document structure
Output format: DOCX
Document title (H1):
Appendix A ? Component Model Reference
Group components by category
Each category is an H2 heading
Category order: alphabetical
Component order within category: alphabetical
2. Component entry format
Do NOT use H3 or any heading for component names.
For each component, render the following:
Component Name (bold, normal paragraph)
Description: <exact description text from JSON>
Creation method: <human-friendly label>
Creation method mapping:
POINT_BY_POINT ? Point-by-point
SINGLE_CLICK ? Single-click
Missing value ? (not specified)
3. Properties table
If the component defines properties, render a table immediately after the metadata:
| Property | Type | Possible values |
Table formatting requirements:
No visible borders (markdown-style look)
Column widths:
Property: 30%
Type: 30%
Possible values: 40%
Header row:
Bold text
Very light gray background (#EEEEEE)
Data rows:
Alternating row background
Very very light gray (#F7F7F7) on every second row
Row height:
As compact as possible
No extra vertical padding
Content rules:
Property name ? JSON name
Type ? JSON type
Possible values:
Populate only if the JSON defines an enum/list
Leave empty otherwise
If a component has no properties, write:
This component has no configurable properties.
4. Styling consistency
Font: Calibri
Base text size: 11 pt
Match the visual style of previously generated ebook chapters
No decorative elements
Clean, technical, reference-style layout
Strict constraints
? Do NOT summarize components
? Do NOT invent descriptions, properties, or values
? Do NOT rename properties or types
? Do NOT add commentary or interpretation
? Do NOT introduce additional columns
Everything must come directly from the JSON.
Final output
Produce one DOCX file
File name:
Appendix A ? Component Model Reference.docx
When finished, provide a download link to the DOCX file.
Acknowledge when you are ready to receive the JSON file.
@@ -36,6 +36,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@@ -54,6 +55,7 @@ import com.thoughtworks.xstream.mapper.MapperWrapper;
import com.thoughtworks.xstream.security.AnyTypePermission;
import org.diylc.common.EventType;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
import org.diylc.presenter.Presenter;
import org.diylc.test.DIYTest;
@@ -112,6 +114,7 @@ public class ProjectFileManager {
xStream.autodetectAnnotations(true);
xStream.alias("point", java.awt.geom.Point2D.class);
xStream.alias("point", java.awt.geom.Point2D.Double.class);
xStream.alias("group", org.diylc.core.ComponentGroup.class);
xStream.alias("font", java.awt.Font.class);
xStream.alias("project", Project.class);
xStream.aliasPackage("diylc", "org.diylc.components");
@@ -278,9 +281,29 @@ public class ProjectFileManager {
LOG.warn("The opened file is missing the following properties: " + String.join(", ", missingFields));
warnings.add("The project references unknown component properties, most likely because it was created with a newer version of DIYLC.");
}
ensureUniqueComponentIds(project);
fis.close();
return project;
}
/**
* Ensures no two components share the same identifier. Assigns a new UUID to any
* component that has a null id or an id already seen (duplicate).
*/
private void ensureUniqueComponentIds(Project project) {
if (project == null || project.getComponents() == null) {
return;
}
Set<UUID> seenIds = new HashSet<UUID>();
for (IDIYComponent<?> component : project.getComponents()) {
UUID id = component.getId();
if (id == null || seenIds.contains(id)) {
component.setId(UUID.randomUUID());
LOG.warn("Detected duplicated componentId: " + id);
}
seenIds.add(component.getId());
}
}
private VersionNumber readV3Version(String fileName) throws Exception {
File fXmlFile = new File(fileName);
@@ -46,8 +46,8 @@ public class VersionReader {
sbHtml.append(" </release>\n");
System.out.println(sbMd.toString());
System.out.println();
System.out.println(sbHtml.toString());
// System.out.println();
// System.out.println(sbHtml.toString());
in.close();
}
} catch (Exception e) {
@@ -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>
+233 -285
View File
@@ -1,289 +1,5 @@
<java.util.Arrays_-ArrayList>
<a class="org.diylc.appframework.update.Version-array">
<org.diylc.appframework.update.Version>
<versionNumber>
<major>4</major>
<minor>41</minor>
<build>1</build>
</versionNumber>
<releaseDate>2025-03-08 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Do not mirror bottom layers for Gerber export as fabrication service providers expect a top view for everything</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Selecting components by drawing a rectangle around them works glitchy</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>0</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-03-09 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Switched to AppImage distribution for Linux system, abandoning Flatpak as a means of distribution. DIYLC AppImage will be available alongside with other executables on the official DIYLC GitHub page.</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Upgraded Windows installer version and Windows EXE launcher version</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed alignment issues of round elements (e.g. solder pads and holes) in exported Gerber files</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed the issue with opening DIY files on Mac OS</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>1</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-03-24 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability to render the underside of boards along with the top side for easier validation, building and debugging</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Configurable 'High DPI Rendering' feature which makes the diagram smoother on high-resolution displays but can affect performance</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Significantly improved rendering performance</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Increased initial memory allocation to 1GB for smoother performance</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Upgraded source code structure to modern standards and introduced Maven</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved robustness of series/parallel graph construction code used in circuit analysis</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Omit boards, wires, traces, terminals, etc from Spice netlist output</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Updated the windows installer (hopefully) to avoid false positive anti-virus detection</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Moving selected components with a mouse doesn't register file modification action and doesn't enable Undo action</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Rotating selected components sometimes wacks the alignment off the grid</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Ctrl+X (Command+X) shortcut for Cut action doesn't work because 'Project Explorer' steals the focus on X key press</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>1</minor>
<build>1</build>
</versionNumber>
<releaseDate>2025-03-27 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Include windows ZIP package</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Windows version doesn't start in some cases</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>2</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-04-12 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'AI Assistant' that can help with general electronics questions, questions about DIYLC usage and circuit-related questions</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>'Virtual Multimeter' from 'Analyze' menu traces the circuit finding all possible paths of electricity between two points designated by the probe symbol</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability to display switch lug markings in order to illustrate how it works internally</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added a 'Save' button to all analysis screens which saves results of a circuit analysis to an HTML file</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability control if the board underside display should render as transparent or not</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added DIY Cloud password reset page</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Moved 'Analyze' functions from 'File' menu to their own menu for faster access</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Moved 'Highlight Connected Areas' feature from 'Config' menu to 'Analyze' menu</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Fully configurable colors for 'Mini Toggle Switch' components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Removed 'DP3T' options from 'Mini Toggle Switch' because that is covered by 'Slide Switch' component and does not belong here</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Include components in the underside display that have at least one control point laying in the board, as opposed to all</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Applying a variant to a 'Hookup Wire' or 'Curved Trace' makes it lose its shape</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Copy operation doesn't work when certain component types are selected</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Board underside display does not work if there's a Label or PCB Text component on the board</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Using board underside display sometimes makes other components semi-transparent</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed Windows ZIP package issue with the bundled JRE</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>3</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-04-21 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved AI Assistant quality of responses, especially when analyzing a circuit</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>When saving a project to the new file, retain all the AI Assistant chat history for the new project file</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Added ability to be logged into the same DIYLC Cloud account from several machines at the same time</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Confirmation dialog before deleting AI Assistant chat history</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Output current subscription statistics at the end of the chat for usage transparency</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved styling and usability of chatbot tool</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Language selection doesn't work on some OSs</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>'Connect to Patreon' option doesn't work if the user name contains a space or another non-alphanumerical character</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>4</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-05-13 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>'AI Analyzer' feature added which uses AI to scan the circuit diagram and report issues and suggestions</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Significantly improve rendering performance of most components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Open Jack 1/4"' and 'Potentiometer Panel' now tolerate connections anywhere on the lugs instead of dead center only</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Increased maximum number of switching combinations to 256 for circuit analysis</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>'Pilot Lamp Holder' component can sometimes hang the application</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed the issue with embedding image files into DIY projects</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed the issue with some components (e.g. DIP IC) jumping when showing underside of a board</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>
@@ -422,17 +138,249 @@
<minor>8</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-12-21 00:00:00.000 CET</releaseDate>
<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>2026-01-10 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability to name groups of components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Show groups of components as single items in the Project Explorer list</description>
</org.diylc.appframework.update.Change>
<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>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>5</major>
<minor>10</minor>
<build>0</build>
</versionNumber>
<releaseDate>2026-01-20 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'M' and 'N' potentiometer taper options (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added E-C-B BJT pinout to TO-92 transistor package</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed the performance issue that happens when groups of components are copy-pasted and the project size grows uncontrollably</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>All building blocks of the same type get added to the same group</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>'Tactile Micro Switch' editor doesn't show up on double click</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>11</minor>
<build>0</build>
</versionNumber>
<releaseDate>2026-02-15 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability to export chat history from 'AI Assistant' to a HTML file</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Upgraded AI models for AI-powered tools and improved overall quality of the responses</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Windows installer prompts for installation folder instead of forcing one</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Shape components shouldn't appear in the BOM</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved rendering of 'Switch (Latching)' symbol to clearly make visual contact between the terminals</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Consolidate a few keyboard shortcuts for consistency</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Using "Duplicate Selection" results in duplicates of component ids, breaking Project Explorer panel and causing other issues downstream</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Calculated length disappeared from Hookup Wire and similar curved components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Resize dimensions do not show for 'Chassis Panel' component when dragging a corner point</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>12</minor>
<build>0</build>
</versionNumber>
<releaseDate>2026-03-26 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'Auto-Unit Conversion' configuration which automatically converts between metric and imperial units based on the ruler unit preference</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added a new section in the output of 'AI Circuit Analyzer' which provides insights into the operation of the circuit and individual components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Better error handling for AI tools</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Pressing the 'Esc' key closes selection editor dialog</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Right click (or two finger tap on mac) after selecting a component with a regular click opens editor dialog in addition to the context menu</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Clicking on a component type in the left-hand component tree sometimes does not register and does not instantiate the component</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
@@ -4773,5 +4773,289 @@
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>4</major>
<minor>41</minor>
<build>1</build>
</versionNumber>
<releaseDate>2025-03-08 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Do not mirror bottom layers for Gerber export as fabrication service providers expect a top view for everything</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Selecting components by drawing a rectangle around them works glitchy</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>0</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-03-09 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Switched to AppImage distribution for Linux system, abandoning Flatpak as a means of distribution. DIYLC AppImage will be available alongside with other executables on the official DIYLC GitHub page.</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Upgraded Windows installer version and Windows EXE launcher version</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed alignment issues of round elements (e.g. solder pads and holes) in exported Gerber files</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed the issue with opening DIY files on Mac OS</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>1</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-03-24 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability to render the underside of boards along with the top side for easier validation, building and debugging</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Configurable 'High DPI Rendering' feature which makes the diagram smoother on high-resolution displays but can affect performance</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Significantly improved rendering performance</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Increased initial memory allocation to 1GB for smoother performance</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Upgraded source code structure to modern standards and introduced Maven</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved robustness of series/parallel graph construction code used in circuit analysis</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Omit boards, wires, traces, terminals, etc from Spice netlist output</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Updated the windows installer (hopefully) to avoid false positive anti-virus detection</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Moving selected components with a mouse doesn't register file modification action and doesn't enable Undo action</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Rotating selected components sometimes wacks the alignment off the grid</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Ctrl+X (Command+X) shortcut for Cut action doesn't work because 'Project Explorer' steals the focus on X key press</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>1</minor>
<build>1</build>
</versionNumber>
<releaseDate>2025-03-27 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Include windows ZIP package</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Windows version doesn't start in some cases</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>2</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-04-12 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'AI Assistant' that can help with general electronics questions, questions about DIYLC usage and circuit-related questions</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>'Virtual Multimeter' from 'Analyze' menu traces the circuit finding all possible paths of electricity between two points designated by the probe symbol</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability to display switch lug markings in order to illustrate how it works internally</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added a 'Save' button to all analysis screens which saves results of a circuit analysis to an HTML file</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability control if the board underside display should render as transparent or not</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added DIY Cloud password reset page</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Moved 'Analyze' functions from 'File' menu to their own menu for faster access</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Moved 'Highlight Connected Areas' feature from 'Config' menu to 'Analyze' menu</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Fully configurable colors for 'Mini Toggle Switch' components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Removed 'DP3T' options from 'Mini Toggle Switch' because that is covered by 'Slide Switch' component and does not belong here</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Include components in the underside display that have at least one control point laying in the board, as opposed to all</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Applying a variant to a 'Hookup Wire' or 'Curved Trace' makes it lose its shape</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Copy operation doesn't work when certain component types are selected</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Board underside display does not work if there's a Label or PCB Text component on the board</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Using board underside display sometimes makes other components semi-transparent</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed Windows ZIP package issue with the bundled JRE</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>3</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-04-21 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved AI Assistant quality of responses, especially when analyzing a circuit</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>When saving a project to the new file, retain all the AI Assistant chat history for the new project file</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Added ability to be logged into the same DIYLC Cloud account from several machines at the same time</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Confirmation dialog before deleting AI Assistant chat history</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Output current subscription statistics at the end of the chat for usage transparency</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Improved styling and usability of chatbot tool</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Language selection doesn't work on some OSs</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>'Connect to Patreon' option doesn't work if the user name contains a space or another non-alphanumerical character</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>4</minor>
<build>0</build>
</versionNumber>
<releaseDate>2025-05-13 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>'AI Analyzer' feature added which uses AI to scan the circuit diagram and report issues and suggestions</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Significantly improve rendering performance of most components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Open Jack 1/4"' and 'Potentiometer Panel' now tolerate connections anywhere on the lugs instead of dead center only</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Increased maximum number of switching combinations to 256 for circuit analysis</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>'Pilot Lamp Holder' component can sometimes hang the application</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed the issue with embedding image files into DIY projects</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Fixed the issue with some components (e.g. DIP IC) jumping when showing underside of a board</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>
@@ -0,0 +1,242 @@
/*
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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class SizeTest {
private static final double DELTA = 1e-9;
// --- convertToUnits: same unit ---
@Test
public void convertToUnits_sameUnit_returnsNewSizeWithValueRoundedTo4Decimals() {
Size size = new Size(1.23456789, SizeUnit.mm);
Size result = size.convertToUnits(SizeUnit.mm);
assertNotSame(size, result);
assertEquals(SizeUnit.mm, result.getUnit());
assertEquals(1.2346, result.getValue(), DELTA);
}
@Test
public void convertToUnits_sameUnit_roundsTo4Decimals() {
assertEquals(2.3457, new Size(2.3456789, SizeUnit.cm).convertToUnits(SizeUnit.cm).getValue(), DELTA);
assertEquals(0.5, new Size(0.5, SizeUnit.in).convertToUnits(SizeUnit.in).getValue(), DELTA);
}
// --- convertToUnits: metric ↔ metric ---
@Test
public void convertToUnits_mmToCm() {
Size size = new Size(10.0, SizeUnit.mm);
Size result = size.convertToUnits(SizeUnit.cm);
assertEquals(SizeUnit.cm, result.getUnit());
assertEquals(1.0, result.getValue(), DELTA);
}
@Test
public void convertToUnits_cmToMm() {
Size size = new Size(2.5, SizeUnit.cm);
Size result = size.convertToUnits(SizeUnit.mm);
assertEquals(SizeUnit.mm, result.getUnit());
assertEquals(25.0, result.getValue(), DELTA);
}
@Test
public void convertToUnits_mmToM() {
Size size = new Size(1500.0, SizeUnit.mm);
Size result = size.convertToUnits(SizeUnit.m);
assertEquals(SizeUnit.m, result.getUnit());
assertEquals(1.5, result.getValue(), DELTA);
}
@Test
public void convertToUnits_mToMm() {
Size size = new Size(0.5, SizeUnit.m);
Size result = size.convertToUnits(SizeUnit.mm);
assertEquals(SizeUnit.mm, result.getUnit());
assertEquals(500.0, result.getValue(), DELTA);
}
// --- convertToUnits: imperial ↔ imperial ---
@Test
public void convertToUnits_inToFt() {
Size size = new Size(24.0, SizeUnit.in);
Size result = size.convertToUnits(SizeUnit.ft);
assertEquals(SizeUnit.ft, result.getUnit());
assertEquals(2.0, result.getValue(), DELTA);
}
@Test
public void convertToUnits_ftToIn() {
Size size = new Size(1.0, SizeUnit.ft);
Size result = size.convertToUnits(SizeUnit.in);
assertEquals(SizeUnit.in, result.getUnit());
assertEquals(12.0, result.getValue(), DELTA);
}
// --- convertToUnits: metric ↔ imperial ---
@Test
public void convertToUnits_mmToIn() {
Size size = new Size(25.4, SizeUnit.mm);
Size result = size.convertToUnits(SizeUnit.in);
assertEquals(SizeUnit.in, result.getUnit());
assertEquals(1.0, result.getValue(), DELTA);
}
@Test
public void convertToUnits_inToMm() {
Size size = new Size(1.0, SizeUnit.in);
Size result = size.convertToUnits(SizeUnit.mm);
assertEquals(SizeUnit.mm, result.getUnit());
assertEquals(25.4, result.getValue(), DELTA);
}
@Test
public void convertToUnits_cmToIn() {
Size size = new Size(2.54, SizeUnit.cm);
Size result = size.convertToUnits(SizeUnit.in);
assertEquals(SizeUnit.in, result.getUnit());
assertEquals(1.0, result.getValue(), DELTA);
}
// --- convertToUnits: rounding to 4 decimal places ---
@Test
public void convertToUnits_resultIsRoundedTo4Decimals() {
Size size = new Size(1.0, SizeUnit.mm);
Size result = size.convertToUnits(SizeUnit.in);
// 1 mm = 1/25.4 in ≈ 0.03937007874015748...
assertEquals(0.0394, result.getValue(), DELTA);
}
@Test
public void convertToUnits_roundingDown() {
Size size = new Size(1.23441, SizeUnit.mm);
Size result = size.convertToUnits(SizeUnit.mm);
assertEquals(1.2344, result.getValue(), DELTA);
}
@Test
public void convertToUnits_roundingUp() {
Size size = new Size(1.23446, SizeUnit.mm);
Size result = size.convertToUnits(SizeUnit.mm);
assertEquals(1.2345, result.getValue(), DELTA);
}
// --- parseSize ---
@Test
public void parseSize_mm() {
Size size = Size.parseSize("10.5mm");
assertEquals(10.5, size.getValue(), DELTA);
assertEquals(SizeUnit.mm, size.getUnit());
}
@Test
public void parseSize_cm() {
Size size = Size.parseSize("2.54cm");
assertEquals(2.54, size.getValue(), DELTA);
assertEquals(SizeUnit.cm, size.getUnit());
}
@Test
public void parseSize_in() {
Size size = Size.parseSize("1in");
assertEquals(1.0, size.getValue(), DELTA);
assertEquals(SizeUnit.in, size.getUnit());
}
@Test
public void parseSize_caseInsensitive() {
Size size = Size.parseSize("5 MM");
assertEquals(5.0, size.getValue(), DELTA);
assertEquals(SizeUnit.mm, size.getUnit());
}
@Test(expected = IllegalArgumentException.class)
public void parseSize_unknownUnit_throws() {
Size.parseSize("10 xyz");
}
// --- fromPixels ---
@Test
public void fromPixels_roundTripWithConvertToPixels() {
Size original = new Size(1.0, SizeUnit.in);
double pixels = original.convertToPixels();
Size fromPixels = Size.fromPixels(pixels, SizeUnit.in);
assertEquals(original.getValue(), fromPixels.getValue(), 0.01);
assertEquals(original.getUnit(), fromPixels.getUnit());
}
// --- scale ---
@Test
public void scale() {
Size size = new Size(10.0, SizeUnit.mm);
Size scaled = size.scale(2.0);
assertEquals(20.0, scaled.getValue(), DELTA);
assertEquals(SizeUnit.mm, scaled.getUnit());
}
// --- compareTo ---
@Test
public void compareTo_samePhysicalSize_returnsZero() {
Size a = new Size(25.4, SizeUnit.mm);
Size b = new Size(1.0, SizeUnit.in);
assertEquals(0, a.compareTo(b));
}
@Test
public void compareTo_smaller_returnsNegative() {
Size a = new Size(1.0, SizeUnit.mm);
Size b = new Size(1.0, SizeUnit.cm);
assertTrue(a.compareTo(b) < 0);
}
@Test
public void compareTo_larger_returnsPositive() {
Size a = new Size(10.0, SizeUnit.mm);
Size b = new Size(1.0, SizeUnit.mm);
assertTrue(a.compareTo(b) > 0);
}
// --- clone ---
@Test
public void clone_returnsEqualSize() throws CloneNotSupportedException {
Size size = new Size(3.14, SizeUnit.cm);
Size cloned = size.clone();
assertNotSame(size, cloned);
assertEquals(size.getValue(), cloned.getValue(), DELTA);
assertEquals(size.getUnit(), cloned.getUnit());
}
}
@@ -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.7.0</version>
<version>5.12.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
}
@@ -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();
@@ -64,17 +64,31 @@ 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);
drawingObserver.stopTracking();
}
// Do not track any changes that follow because the whole rect has been
// tracked so far.
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;
@@ -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;
@@ -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() {
@@ -48,7 +48,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,
description = "A pair of flexible leads twisted tightly together", zOrder = IDIYComponent.WIRING,
flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW,
transformer = SimpleComponentTransformer.class, enableCache = true)
public class TwistedWire extends AbstractCurvedComponent<Void> implements IContinuity {
@@ -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++) {
@@ -110,8 +110,8 @@ public class SwitchLatchingSymbol
offsetX = terminalSize;
offsetY = 0;
} else {
offsetX = terminalSize * 2 / 3;
offsetY = terminalSize / 2;
offsetX = terminalSize / 3;
offsetY = terminalSize / 3;
}
Point2D endPoint = new Point2D.Double(
@@ -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,31 @@
/*
*
* 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 org.diylc.core.AbstractDynamicPropertyValueSource;
import java.util.List;
public class TactileMicroSwitchPositionPropertyValueSource extends AbstractDynamicPropertyValueSource {
@Override
public List<Object> getAvailableValues() {
return List.of(0, 1);
}
}
@@ -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;
}
}
@@ -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
}
}
@@ -321,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:
@@ -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> {
@@ -0,0 +1,398 @@
/*
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.passive;
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.*;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.measures.Resistance;
import org.diylc.core.measures.ResistanceUnit;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
import java.awt.*;
import java.awt.geom.Point2D;
import java.io.Serial;
//@ComponentDescriptor(name = "Box Multi-Turn Pot", author = "Clive", category = "Passive",
// instanceNamePrefix = "VR",
// description = "Box style multi-turn potentiometer (e.g. Bourns PV36)",
// zOrder = IDIYComponent.COMPONENT,
// transformer = org.diylc.components.transform.BoxTrimmerTransformer.class)
//<org.diylc.appframework.update.Change>
// <changeType>NEW_FEATURE</changeType>
// <description>Added Box style multi-turn potentiometer component (thank you chayward)</description>
// </org.diylc.appframework.update.Change>
public class BoxTrimmer extends AbstractTransparentComponent<Resistance> {
@Serial
private static final long serialVersionUID = 1L;
public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in);
public static Size BODY_WIDTH = new Size(0.19d, SizeUnit.in); // 2x 2.41mm = 4.82mm ≈ 0.19"
public static Size BODY_LENGTH = new Size(0.375d, SizeUnit.in); // 9.53mm from datasheet
public static Color BODY_COLOR = Color.decode("#4477BB");
public static Color BORDER_COLOR = BODY_COLOR.darker();
public static Color PIN_COLOR = METAL_COLOR;
public static Color SCREW_COLOR = BRASS_COLOR;
protected String name;
private Resistance value = new Resistance(10d, ResistanceUnit.K);
private Orientation orientation = Orientation._90; // Default to vertical (90 degrees)
private Display display = Display.NAME;
private Color bodyColor = BODY_COLOR;
private Color borderColor = BORDER_COLOR;
private boolean screwFlippedHorizontally = false; // Track if screw flipped left-right
private boolean screwFlippedVertically = false; // Track if screw flipped top-bottom
private Point2D[] controlPoints = new Point2D[3];
public BoxTrimmer() {
super();
updateControlPoints();
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
Theme theme = (Theme) ConfigurationManager.getInstance()
.readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME);
int pinSpacing = (int) PIN_SPACING.convertToPixels();
int bodyWidth = (int) BODY_WIDTH.convertToPixels();
int bodyLength = (int) BODY_LENGTH.convertToPixels();
// Calculate positions - body is centered on the middle pin
int centerX = (int) controlPoints[1].getX();
int centerY = (int) controlPoints[1].getY();
int bodyX, bodyY, bodyW, bodyH;
// Position body based on orientation - fits tightly around pins
if (orientation == Orientation.DEFAULT || orientation == Orientation._180) {
// Horizontal - body is wide, pins are below/above
bodyW = bodyLength;
bodyH = bodyWidth;
bodyX = centerX - bodyLength / 2;
bodyY = centerY - bodyWidth / 2;
} else {
// Vertical - body is tall, pins are left/right
bodyW = bodyWidth;
bodyH = bodyLength;
bodyX = centerX - bodyWidth / 2;
bodyY = centerY - bodyLength / 2;
}
// Draw body
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 : bodyColor);
g2d.fillRoundRect(bodyX, bodyY, bodyW, bodyH, bodyW / 10, bodyH / 10);
g2d.setComposite(oldComposite);
// Draw border
Color finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ?
SELECTION_COLOR :
(outlineMode ? theme.getOutlineColor() : borderColor);
g2d.setColor(finalBorderColor);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.drawRoundRect(bodyX, bodyY, bodyW, bodyH, bodyW / 10, bodyH / 10);
// Draw adjustment screw (slotted screw head) - position based on orientation
if (!outlineMode) {
// Brass/gold color for screw head
// Color brassColor = new Color(184, 134, 11); // Dark goldenrod
int screwDiameter = Math.min(bodyW, bodyH) / 3;
int screwX, screwY;
// Position screw based on orientation and flip state
// The screw should be in the corner near pin 0, but can flip horizontally or vertically
screwY = switch (orientation) {
case DEFAULT -> {
screwX = screwFlippedHorizontally ? bodyX + bodyW - screwDiameter : bodyX;
yield screwFlippedVertically ? bodyY : bodyY + bodyH - screwDiameter;
}
case _90 -> {
screwX = screwFlippedHorizontally ? bodyX + bodyW - screwDiameter : bodyX;
yield screwFlippedVertically ? bodyY : bodyY + bodyH - screwDiameter;
}
case _180 -> {
screwX = screwFlippedHorizontally ? bodyX : bodyX + bodyW - screwDiameter;
yield screwFlippedVertically ? bodyY : bodyY + bodyH - screwDiameter;
}
case _270 -> {
screwX = screwFlippedHorizontally ? bodyX + bodyW - screwDiameter : bodyX;
yield screwFlippedVertically ? bodyY + bodyH - screwDiameter : bodyY;
}
default -> {
screwX = bodyX;
yield bodyY + bodyH - screwDiameter;
}
};
// Draw screw head circle
g2d.setColor(SCREW_COLOR);
g2d.fillOval(screwX, screwY, screwDiameter, screwDiameter);
// Draw slot in screw - centered horizontally through the circle
g2d.setColor(SCREW_COLOR.darker());
g2d.drawOval(screwX, screwY, screwDiameter, screwDiameter);
int slotLength = (int) (screwDiameter * 0.7);
int slotThickness = Math.max(1, screwDiameter / 10);
int slotCenterX = screwX + screwDiameter / 2;
int slotCenterY = screwY + screwDiameter / 2;
g2d.fillRect(slotCenterX - slotLength / 2, slotCenterY - slotThickness / 2, slotLength,
slotThickness);
}
// Draw pins
g2d.setColor(outlineMode ? theme.getOutlineColor() : PIN_COLOR);
for (Point2D p : controlPoints) {
g2d.fillOval((int) p.getX() - 2, (int) p.getY() - 2, 4, 4);
}
// Draw label - position outside body for readability
Color labelColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ?
LABEL_COLOR_SELECTED :
LABEL_COLOR;
g2d.setColor(labelColor);
g2d.setFont(project.getFont());
String label = display == Display.NAME ?
getName() :
display == Display.VALUE ?
getValue().toString() :
display == Display.BOTH ? getName() + " " + getValue() : "";
if (!label.isEmpty()) {
// Position label outside the component body based on orientation
int labelX, labelY;
java.awt.FontMetrics fm = g2d.getFontMetrics();
if (orientation == Orientation.DEFAULT || orientation == Orientation._180) {
// Horizontal orientation - label below body
labelX = bodyX + bodyW / 2;
labelY = bodyY + bodyH + fm.getHeight();
} else {
// Vertical orientation - label to the right of body
labelX = bodyX + bodyW + fm.stringWidth(label) / 2 + 5; // 5 pixel gap
labelY = bodyY + bodyH / 2;
}
drawCenteredText(g2d, label, labelX, labelY);
}
}
private void drawCenteredText(Graphics2D g2d, String text, int x, int y) {
java.awt.FontMetrics fm = g2d.getFontMetrics();
int w = fm.stringWidth(text);
int h = fm.getHeight();
g2d.drawString(text, x - w / 2, y + fm.getAscent() - h / 2);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
// Side profile view of PV36 trimmer
int bodyWidth = width - 8;
int bodyHeight = height - 10;
int bodyX = 4;
int bodyY = 2;
// Draw blue body (side profile - rectangular)
g2d.setColor(BODY_COLOR);
g2d.fillRect(bodyX, bodyY, bodyWidth, bodyHeight);
g2d.setColor(BORDER_COLOR);
g2d.drawRect(bodyX, bodyY, bodyWidth, bodyHeight);
// Draw brass adjustment screw on top left
Color brassColor = new Color(184, 134, 11);
int screwSize = Math.min(bodyWidth, bodyHeight) / 3;
g2d.setColor(brassColor);
g2d.fillOval(bodyX + 2, bodyY - screwSize / 2, screwSize, screwSize);
// Draw slot in screw
g2d.setColor(new Color(80, 80, 80));
int slotLen = (int) (screwSize * 0.6);
g2d.drawLine(bodyX + 2 + screwSize / 2 - slotLen / 2, bodyY,
bodyX + 2 + screwSize / 2 + slotLen / 2, bodyY);
// Draw three pins at bottom (light gray)
g2d.setColor(PIN_COLOR);
int pinSpacing = bodyWidth / 4;
for (int i = 0; i < 3; i++) {
int pinX = bodyX + pinSpacing * (i + 1) - 1;
int pinY = bodyY + bodyHeight;
g2d.fillRect(pinX, pinY, 2, 6);
}
}
@EditableProperty
public Resistance getValue() {
return value;
}
public void setValue(Resistance value) {
this.value = value;
}
@EditableProperty
public Orientation getOrientation() {
return orientation;
}
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
updateControlPoints();
}
public boolean getScrewFlippedHorizontally() {
return screwFlippedHorizontally;
}
public void setScrewFlippedHorizontally(boolean flipped) {
this.screwFlippedHorizontally = flipped;
}
public boolean getScrewFlippedVertically() {
return screwFlippedVertically;
}
public void setScrewFlippedVertically(boolean flipped) {
this.screwFlippedVertically = flipped;
}
@EditableProperty
public Display getDisplay() {
return display;
}
public void setDisplay(Display display) {
this.display = display;
}
@EditableProperty(name = "Body")
public Color getBodyColor() {
return bodyColor;
}
public void setBodyColor(Color color) {
this.bodyColor = color;
}
@EditableProperty(name = "Border")
public Color getBorderColor() {
return borderColor;
}
public void setBorderColor(Color color) {
this.borderColor = color;
}
@Override
public int getControlPointCount() {
return 3;
}
@Override
public Point2D getControlPoint(int index) {
return controlPoints[index];
}
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.WHEN_SELECTED;
}
@Override
public void setControlPoint(Point2D point, int index) {
// Only allow moving via the first control point
if (index == 0) {
controlPoints[0] = new Point2D.Double(point.getX(), point.getY());
updateControlPoints();
}
}
private void updateControlPoints() {
int pinSpacing = (int) PIN_SPACING.convertToPixels();
double x = controlPoints[0] != null ? controlPoints[0].getX() : 0;
double y = controlPoints[0] != null ? controlPoints[0].getY() : 0;
controlPoints[0] = new Point2D.Double(x, y);
// Pin arrangement depends on orientation
// Pin 0 is always CCW, Pin 1 is Wiper, Pin 2 is CW
switch (orientation) {
case DEFAULT: // Horizontal, pins go right from pin 0
controlPoints[1] = new Point2D.Double(x + pinSpacing, y);
controlPoints[2] = new Point2D.Double(x + 2 * pinSpacing, y);
break;
case _90: // Vertical, pins go down from pin 0
controlPoints[1] = new Point2D.Double(x, y + pinSpacing);
controlPoints[2] = new Point2D.Double(x, y + 2 * pinSpacing);
break;
case _180: // Horizontal mirrored, pins go left from pin 0
controlPoints[1] = new Point2D.Double(x - pinSpacing, y);
controlPoints[2] = new Point2D.Double(x - 2 * pinSpacing, y);
break;
case _270: // Vertical mirrored, pins go up from pin 0
controlPoints[1] = new Point2D.Double(x, y - pinSpacing);
controlPoints[2] = new Point2D.Double(x, y - 2 * pinSpacing);
break;
}
}
@Override
public String getControlPointNodeName(int index) {
return new String[] {"CCW (1)", "Wiper (2)", "CW (3)"}[index];
}
@Override
public String getName() {
return name != null && !name.trim().isEmpty() ? name : super.getName();
}
@Override
@EditableProperty(defaultable = false)
public void setName(String name) {
this.name = name;
}
@Override
public boolean isControlPointSticky(int index) {
// First control point is sticky so component can be placed from side panel
// But setControlPoint ensures all pins move together
return index == 0;
}
}
@@ -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) {
@@ -23,7 +23,7 @@ package org.diylc.components.passive;
public enum Taper {
LIN, LOG, REV_LOG, W, S;
LIN, LOG, REV_LOG, W, S, M, N;
@Override
public String toString() {
@@ -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;
}
}
}
@@ -2,7 +2,7 @@ package org.diylc.components.semiconductors;
public enum TransistorPinout {
BJT_EBC, BJT_CBE, BJT_BCE, JFET_DSG, JFET_GSD, JFET_DGS, JFET_SGD, JFET_GDS, MOSFET_DSG, MOSFET_GSD, MOSFET_DGS, MOSFET_SGD, MOSFET_GDS, REGULATOR_IGO, REGULATOR_OGI, REGULATOR_GOI, REGULATOR_AOI, REGULATOR_GIO;
BJT_EBC, BJT_CBE, BJT_BCE, BJT_ECB, JFET_DSG, JFET_GSD, JFET_DGS, JFET_SGD, JFET_GDS, MOSFET_DSG, MOSFET_GSD, MOSFET_DGS, MOSFET_SGD, MOSFET_GDS, REGULATOR_IGO, REGULATOR_OGI, REGULATOR_GOI, REGULATOR_AOI, REGULATOR_GIO;
@Override
public String toString() {
@@ -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, transformer = SimpleComponentTransformer.class)
bomPolicy = BomPolicy.NEVER_SHOW, 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,
flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW,
transformer = SimpleComponentTransformer.class)
public class Polygon extends AbstractShape {
@@ -38,8 +38,8 @@ import org.diylc.core.measures.Size;
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,
instanceNamePrefix = "RECT", description = "Rectangular area, with or without rounded edges",
zOrder = IDIYComponent.COMPONENT, flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW,
transformer = SimpleComponentTransformer.class)
public class Rectangle extends AbstractShapeWithDimensions {
@@ -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);
}
@@ -0,0 +1,234 @@
package org.diylc.components.transform;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import org.diylc.common.IComponentTransformer;
import org.diylc.common.Orientation;
import org.diylc.components.passive.BoxTrimmer;
import org.diylc.core.IDIYComponent;
public class BoxTrimmerTransformer implements IComponentTransformer {
private enum Corner { TL, TR, BL, BR }
// ---------- public API ----------
@Override
public boolean mirroringChangesCircuit() {
// Pins are by index (0=CCW,1=Wiper,2=CW); mirror doesn't swap indices.
return false;
}
@Override
public boolean canMirror(IDIYComponent<?> component) {
return component instanceof BoxTrimmer;
}
@Override
public boolean canRotate(IDIYComponent<?> component) {
return component instanceof BoxTrimmer;
}
@Override
public void rotate(IDIYComponent<?> component, Point2D center, int direction) {
if (!(component instanceof BoxTrimmer t)) return;
if (direction != 1 && direction != -1) return;
// capture old state
Orientation oldO = t.getOrientation();
Corner oldCorner = flagsToCorner(oldO, t.getScrewFlippedHorizontally(), t.getScrewFlippedVertically());
// rotate CP0 about center (clockwise for direction=+1)
Point2D p0 = t.getControlPoint(0);
AffineTransform rot = AffineTransform.getRotateInstance((Math.PI / 2) * -direction, center.getX(), center.getY());
Point2D p0r = new Point2D.Double();
rot.transform(p0, p0r);
// compute new orientation and new corner
Orientation newO = rotateOrientation(oldO, direction);
Corner newCorner = (direction == 1) ? rotateCornerCW(oldCorner) : rotateCornerCCW(oldCorner);
// decode corner back to flags in new orientation
boolean[] hv = cornerToFlags(newO, newCorner);
// apply (CP0 first, then orientation regenerates pins)
t.setControlPoint(p0r, 0);
t.setOrientation(newO);
t.setScrewFlippedHorizontally(hv[0]);
t.setScrewFlippedVertically(hv[1]);
}
@Override
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
if (!(component instanceof BoxTrimmer t)) return;
Orientation oldO = t.getOrientation();
Corner oldCorner = flagsToCorner(oldO, t.getScrewFlippedHorizontally(), t.getScrewFlippedVertically());
Point2D p0 = t.getControlPoint(0);
Point2D p0m;
Orientation newO;
Corner newCorner;
if (direction == IComponentTransformer.HORIZONTAL) {
// mirror left-right
p0m = new Point2D.Double(2 * center.getX() - p0.getX(), p0.getY());
newO = mirrorOrientationHorizontal(oldO);
newCorner = mirrorCornerHorizontal(oldCorner);
} else {
// mirror top-bottom
p0m = new Point2D.Double(p0.getX(), 2 * center.getY() - p0.getY());
newO = mirrorOrientationVertical(oldO);
newCorner = mirrorCornerVertical(oldCorner);
}
boolean[] hv = cornerToFlags(newO, newCorner);
t.setControlPoint(p0m, 0);
t.setOrientation(newO);
t.setScrewFlippedHorizontally(hv[0]);
t.setScrewFlippedVertically(hv[1]);
}
// ---------- orientation index + transforms (table-driven) ----------
private static int oIdx(Orientation o) {
return switch (o) {
case DEFAULT -> 0;
case _90 -> 1;
case _180 -> 2;
case _270 -> 3;
};
}
// rotate orientation clockwise / counterclockwise
// index order: DEFAULT, _90, _180, _270
private static final Orientation[] ORIENTATIONS = {
Orientation.DEFAULT, Orientation._90, Orientation._180, Orientation._270
};
private static Orientation rotateOrientation(Orientation o, int direction) {
int i = oIdx(o);
int ni = (direction == 1) ? (i + 1) : (i + 3); // -1 == +3 mod 4
return ORIENTATIONS[ni & 3];
}
// mirror tables: new orientation index after mirror
private static final int[] ORIENT_MIRROR_H = { 2, 1, 0, 3 }; // DEFAULT<->_180, _90 stays, _270 stays
private static final int[] ORIENT_MIRROR_V = { 0, 3, 2, 1 }; // _90<->_270, DEFAULT stays, _180 stays
private static Orientation mirrorOrientationHorizontal(Orientation o) {
return ORIENTATIONS[ORIENT_MIRROR_H[oIdx(o)]];
}
private static Orientation mirrorOrientationVertical(Orientation o) {
return ORIENTATIONS[ORIENT_MIRROR_V[oIdx(o)]];
}
// ---------- corner mapping (table-driven, matches BoxTrimmer.draw()) ----------
/**
* CORNER_BY_FLAGS[orientationIndex][hvIndex] -> Corner
* hvIndex encodes (h,v) as:
* bit0 = h (0/1)
* bit1 = v (0/1)
* so:
* 0: h=0,v=0
* 1: h=1,v=0
* 2: h=0,v=1
* 3: h=1,v=1
*
* This matches draw() logic:
* - DEFAULT, _90: normal (h: left->right, v: bottom->top)
* - _180: horizontal meaning inverted
* - _270: vertical meaning inverted
*/
private static final Corner[][] CORNER_BY_FLAGS = {
// DEFAULT
{ Corner.BL, Corner.BR, Corner.TL, Corner.TR },
// _90
{ Corner.BL, Corner.BR, Corner.TL, Corner.TR },
// _180
{ Corner.BR, Corner.BL, Corner.TR, Corner.TL },
// _270
{ Corner.TL, Corner.TR, Corner.BL, Corner.BR }
};
private static final boolean[][][] FLAGS_BY_CORNER = buildFlagsByCorner();
private static boolean[][][] buildFlagsByCorner() {
boolean[][][] out = new boolean[4][4][2]; // [ori][corner][h/v]
for (int oi = 0; oi < 4; oi++) {
for (int hv = 0; hv < 4; hv++) {
Corner c = CORNER_BY_FLAGS[oi][hv];
boolean h = (hv & 1) != 0;
boolean v = (hv & 2) != 0;
out[oi][c.ordinal()][0] = h;
out[oi][c.ordinal()][1] = v;
}
}
return out;
}
private static Corner flagsToCorner(Orientation o, boolean h, boolean v) {
int oi = oIdx(o);
int hv = (h ? 1 : 0) | (v ? 2 : 0);
return CORNER_BY_FLAGS[oi][hv];
}
private static boolean[] cornerToFlags(Orientation o, Corner c) {
int oi = oIdx(o);
boolean h = FLAGS_BY_CORNER[oi][c.ordinal()][0];
boolean v = FLAGS_BY_CORNER[oi][c.ordinal()][1];
return new boolean[] { h, v };
}
// ---------- corner transforms (table-driven) ----------
// ordinal order: TL=0, TR=1, BL=2, BR=3
private static final Corner[] CORNER_ROT_CW = {
Corner.TR, // TL -> TR
Corner.BR, // TR -> BR
Corner.TL, // BL -> TL
Corner.BL // BR -> BL
};
private static final Corner[] CORNER_ROT_CCW = {
Corner.BL, // TL -> BL
Corner.TL, // TR -> TL
Corner.BR, // BL -> BR
Corner.TR // BR -> TR
};
private static final Corner[] CORNER_MIRROR_H = {
Corner.TR, // TL <-> TR
Corner.TL,
Corner.BR, // BL <-> BR
Corner.BL
};
private static final Corner[] CORNER_MIRROR_V = {
Corner.BL, // TL <-> BL
Corner.BR, // TR <-> BR
Corner.TL,
Corner.TR
};
private static Corner rotateCornerCW(Corner c) {
return CORNER_ROT_CW[c.ordinal()];
}
private static Corner rotateCornerCCW(Corner c) {
return CORNER_ROT_CCW[c.ordinal()];
}
private static Corner mirrorCornerHorizontal(Corner c) {
return CORNER_MIRROR_H[c.ordinal()];
}
private static Corner mirrorCornerVertical(Corner c) {
return CORNER_MIRROR_V[c.ordinal()];
}
}
@@ -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);
}
}
}
}
@@ -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);
}
}
@@ -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);
}
}
+10 -1
View File
@@ -1,7 +1,12 @@
; Default installation directory (user can change on "Select Destination Location" page).
; Override at compile time: iscc /DDefaultInstallDir="D:\Apps\DIYLC" install.iss
; Silent install: install.exe /S /D=C:\MyPath\DIYLC
#define DefaultInstallDir "{pf}\DIYLC"
[Setup]
AppName=DIY Layout Creator
AppVersion={#diylcver}
DefaultDirName={pf}\DIYLC
DefaultDirName={#DefaultInstallDir}
DefaultGroupName=DIY Layout Creator
UninstallDisplayIcon={app}\diylc.exe
Compression=zip
@@ -9,7 +14,11 @@ SolidCompression=yes
OutputDir=..\..\target
OutputBaseFilename=diylc-{#diylcver}-win64
ChangesAssociations=yes
; Show directory selection so user can choose install folder
DisableDirPage=no
AlwaysShowDirOnReadyPage=yes
; Suggest previous install path when upgrading
UsePreviousAppDir=yes
; Only allow the installer to run on x64-compatible systems,
; and enable 64-bit install mode.
ArchitecturesAllowed=x64compatible
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.7.0</version>
<version>5.12.0</version>
</parent>
<artifactId>diylc-swing</artifactId>
@@ -137,6 +137,9 @@ public class DIYLCStarter {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// 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);
}
@@ -1,7 +1,9 @@
package org.diylc.swing.actions;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.Objects;
import javax.swing.AbstractAction;
import javax.swing.KeyStroke;
@@ -26,7 +28,7 @@ public class FindAction extends AbstractAction {
putValue(AbstractAction.NAME, "Find");
putValue(AbstractAction.SMALL_ICON, IconLoader.SearchBox.getIcon());
putValue(AbstractAction.ACCELERATOR_KEY,
KeyStroke.getKeyStroke(KeyEvent.VK_F, ActionEvent.CTRL_MASK));
KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
}
@Override
@@ -35,13 +37,13 @@ public class FindAction extends AbstractAction {
FindDialog dialog = DialogFactory.getInstance().createFindDialog();
dialog.setVisible(true);
if (dialog.getSelectedButtonCaption() == FindDialog.OK) {
if (Objects.equals(dialog.getSelectedButtonCaption(), FindDialog.OK)) {
String criteria = dialog.getCriteria();
plugInPort.selectMatching(criteria);
if (plugInPort.getSelectedComponents().size() == 0) {
if (plugInPort.getSelectedComponents().isEmpty()) {
swingUI.showMessage("No matching components found.", "Find",
ISwingUI.INFORMATION_MESSAGE);
}
}
}
}
}
@@ -13,6 +13,7 @@ import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.util.List;
import java.util.Optional;
public class AiAnalyzeAction extends AbstractAction {
@@ -41,7 +42,9 @@ public class AiAnalyzeAction extends AbstractAction {
@Override
public void failed(Exception e) {
swingUI.showMessage(e.getMessage(), "AI Circuit Analyzer", ISwingUI.ERROR_MESSAGE);
String message =
Optional.ofNullable(e.getCause()).map(Throwable::getMessage).orElse(e.getMessage());
swingUI.showMessage(message, "AI Circuit Analyzer", ISwingUI.ERROR_MESSAGE);
}
@Override
@@ -36,7 +36,7 @@ public class CopyAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
ActionFactory.LOG.info("Copy triggered");
clipboard.setContents(ComponentTransferableFactory.getInstance()
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroups()),
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroupsEx()),
clipboardOwner);
}
}
}
@@ -36,7 +36,7 @@ public class CutAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
ActionFactory.LOG.info("Cut triggered");
clipboard.setContents(ComponentTransferableFactory.getInstance()
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroups()),
.build(plugInPort.getSelectedComponents(), plugInPort.getCurrentProject().getGroupsEx()),
clipboardOwner);
plugInPort.deleteSelectedComponents();
}
@@ -32,7 +32,7 @@ public class NudgeAction extends AbstractAction {
putValue(AbstractAction.NAME, "Nudge");
putValue(AbstractAction.SMALL_ICON, IconLoader.FitToSize.getIcon());
putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Q,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
ActionEvent.ALT_MASK));
}
@Override
@@ -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);
}
}
}
}
@@ -65,8 +65,8 @@ public class ImportAction extends AbstractAction {
}
}, InMemoryConfigurationManager.getInstance());
putValue(AbstractAction.NAME, "Import");
putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_I,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
// putValue(AbstractAction.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_I,
// Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
putValue(AbstractAction.SMALL_ICON, IconLoader.ElementInto.getIcon());
}
@@ -88,7 +88,7 @@ public class ImportAction extends AbstractAction {
// presenter
plugInPort.pasteComponents(ComponentTransferableFactory.getInstance().build(
presenter.getCurrentProject().getComponents(),
presenter.getCurrentProject().getGroups()), false, false);
presenter.getCurrentProject().getGroupsEx()), false, false);
// Cleanup components in the temp presenter, don't need
// them anymore
presenter.selectAll(0);
@@ -113,7 +113,7 @@ public class ImportNetlistAction extends AbstractAction {
if (!outputWarnings.isEmpty())
ActionFactory.LOG.warn("Component creation produced warnings:\n"
+ String.join("\n", outputWarnings));
plugInPort.pasteComponents(new ComponentTransferable(components), false, false);
plugInPort.pasteComponents(new ComponentTransferable(components, "Imported Netlist"), false, false);
}
} catch (Exception e) {
swingUI.showMessage("Could not import netlist file: " + e.getMessage(), "Error",
@@ -130,4 +130,4 @@ public class ImportNetlistAction extends AbstractAction {
}, true);
}
}
}
}
@@ -35,6 +35,10 @@ import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import javax.swing.JComboBox;
import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.IPlugInPort;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.swingframework.DoubleTextField;
import org.diylc.common.PropertyWrapper;
@@ -55,7 +59,8 @@ public class MeasureEditor extends Container {
@SuppressWarnings("unchecked")
public MeasureEditor(final PropertyWrapper property) {
setLayout(new BorderLayout());
final AbstractMeasure<?> measure = ((AbstractMeasure<?>) property.getValue());
AbstractMeasure<?> measure = ((AbstractMeasure<?>) property.getValue());
measure = convertUnitsIfNeeded(measure);
valueField = new DoubleTextField(measure == null ? null : measure.getValue());
oldBg = valueField.getBackground();
valueField.addPropertyChangeListener(DoubleTextField.VALUE_PROPERTY, new PropertyChangeListener() {
@@ -122,6 +127,26 @@ public class MeasureEditor extends Container {
}
}
private AbstractMeasure<?> convertUnitsIfNeeded(AbstractMeasure<?> value) {
if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.AUTO_UNIT_CONVERSION_KEY, true)
&& value instanceof Size sizeValue) {
SizeUnit preferredUnit;
if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.METRIC_KEY, true)) {
preferredUnit = SizeUnit.mm;
}
else {
preferredUnit = SizeUnit.in;
}
if (preferredUnit.getType() != sizeValue.getUnit().getType()) {
SizeUnit targetUnit = SizeUnit.ConversionMapping.get(sizeValue.getUnit());
if (targetUnit != null) {
value = sizeValue.convertToUnits(targetUnit);
}
}
}
return value;
}
@Override
public void requestFocus() {
this.valueField.requestFocus();
@@ -27,6 +27,8 @@ import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.List;
@@ -35,6 +37,7 @@ import javax.swing.BorderFactory;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.KeyStroke;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
@@ -87,6 +90,19 @@ public class PropertyEditorDialog extends ButtonDialog {
layoutGui();
setLocationRelativeTo(owner);
KeyStroke escapeStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
Action escapeAction = new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
getButton(ButtonDialog.CANCEL).doClick();
}
};
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeStroke,
"PropertyEditorDialog.escape");
getRootPane().getActionMap().put("PropertyEditorDialog.escape", escapeAction);
}
@Override
@@ -156,8 +172,6 @@ public class PropertyEditorDialog extends ButtonDialog {
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
getButton(OK).doClick();
} else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
getButton(CANCEL).doClick();
}
}
});
@@ -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));
}
}
@@ -41,6 +41,7 @@ import org.apache.log4j.Logger;
import org.diylc.appframework.miscutils.IConfigListener;
import org.diylc.appframework.miscutils.IConfigurationManager;
import org.diylc.appframework.miscutils.Utils;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.swingframework.ruler.IRulerListener;
import org.diylc.swingframework.ruler.Ruler.InchSubdivision;
import org.diylc.swingframework.ruler.RulerScrollPane;
@@ -53,7 +54,6 @@ import org.diylc.common.BadPositionException;
import org.diylc.common.EventType;
import org.diylc.common.IPlugIn;
import org.diylc.common.IPlugInPort;
import org.diylc.core.IDIYComponent;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.presenter.Presenter;
@@ -68,6 +68,8 @@ public class CanvasPlugin implements IPlugIn{
private RulerScrollPane scrollPane;
private CanvasPanel canvasPanel;
private ComponentPopupMenu popupMenu;
private ResizeInfoTooltip resizeInfoTooltip;
private boolean resizingInProgress = false;
private IPlugInPort plugInPort;
private ISwingUI swingUI;
@@ -193,6 +195,7 @@ public class CanvasPlugin implements IPlugIn{
public CanvasPanel getCanvasPanel() {
if (canvasPanel == null) {
canvasPanel = new CanvasPanel(plugInPort, configManager);
resizeInfoTooltip = new ResizeInfoTooltip(canvasPanel, configManager);
canvasPanel.addMouseListener(new MouseAdapter() {
private MouseEvent pressedEvent;
@@ -271,10 +274,21 @@ public class CanvasPlugin implements IPlugIn{
public void mouseMoved(MouseEvent e) {
if (scrollPane.isMouseScrollMode())
return;
// if needed uncomment this code, it forces the cursor to refresh when it's stuck
// canvasPanel.setCursor(null);
canvasPanel.setCursor(plugInPort.getCursorAt(e.getPoint(), Utils.isMac() ? e.isMetaDown() : e.isControlDown(),
e.isShiftDown(), e.isAltDown()));
plugInPort.mouseMoved(e.getPoint(), Utils.isMac() ? e.isMetaDown() : e.isControlDown(),
e.isShiftDown(), e.isAltDown());
// Update tooltip position if resizing is in progress and tooltip is enabled, otherwise hide it
if (resizeInfoTooltip != null) {
if (resizingInProgress && configManager.readBoolean(IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true)) {
resizeInfoTooltip.updatePosition(e.getPoint());
} else {
resizeInfoTooltip.hideTooltip();
}
}
}
});
@@ -503,11 +517,13 @@ public class CanvasPlugin implements IPlugIn{
canvasPanel.repaint();
// Refresh selection bounds after we're done with painting to ensure we have traced the
// component areas
boolean panToSelection = params.length > 0 && Boolean.TRUE.equals(params[0]);
this.resizingInProgress = params.length > 1 && Boolean.TRUE.equals(params[1]);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (params.length > 0 && Boolean.TRUE.equals(params[0])) {
if (panToSelection) {
Rectangle2D selectionBounds = plugInPort.getSelectionBounds(true);
if (selectionBounds != null) {
Rectangle visibleRect = scrollPane.getVisibleRect();
@@ -517,8 +533,28 @@ public class CanvasPlugin implements IPlugIn{
);
}
}
if (configManager.readBoolean(IPlugInPort.SHOW_RULERS_KEY, true))
if (configManager.readBoolean(IPlugInPort.SHOW_RULERS_KEY, true)) {
scrollPane.setSelectionRectangle(plugInPort.getSelectionBounds(true));
}
if (CanvasPlugin.this.resizingInProgress && getCanvasPanel().getMousePosition() != null) {
// Only show tooltip if enabled in configuration
if (configManager.readBoolean(IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true)) {
ResizeDimensions resizeDimensions = plugInPort.getResizeDimensions();
if (resizeInfoTooltip != null) {
resizeInfoTooltip.update(resizeDimensions, getCanvasPanel().getMousePosition());
}
} else {
// Hide tooltip if disabled
if (resizeInfoTooltip != null) {
resizeInfoTooltip.hideTooltip();
}
}
} else {
// Hide tooltip when not resizing
if (resizeInfoTooltip != null) {
resizeInfoTooltip.hideTooltip();
}
}
}
});
break;
@@ -561,25 +597,4 @@ public class CanvasPlugin implements IPlugIn{
double extraSpace = CanvasPlugin.this.plugInPort.getExtraSpace();
getScrollPane().setZeroLocation(new Point2D.Double(extraSpace, extraSpace));
}
class LockAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private IDIYComponent<?> component;
private boolean locked;
public LockAction(IDIYComponent<?> component, boolean locked) {
super();
this.locked = locked;
this.component = component;
putValue(AbstractAction.NAME, component.getName());
}
@Override
public void actionPerformed(ActionEvent e) {
plugInPort.lockComponent(component, locked);
}
}
}
@@ -21,11 +21,16 @@
*/
package org.diylc.swing.plugins.canvas;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.SwingUtilities;
import org.diylc.common.IPlugInPort;
@@ -50,13 +55,16 @@ class CanvasSourceListener implements DragSourceListener {
}
@Override
public void dragEnter(DragSourceDragEvent dsde) {}
public void dragEnter(DragSourceDragEvent dsde) {
updateCursor(dsde);
}
@Override
public void dragExit(DragSourceEvent dse) {}
@Override
public void dragOver(DragSourceDragEvent dsde) {
updateCursor(dsde);
Point p = dsde.getDragSourceContext().getComponent().getMousePosition();
if (p != null) {
dsde.getDragSourceContext().getComponent().firePropertyChange("dragPoint", p.x, p.y);
@@ -64,5 +72,26 @@ class CanvasSourceListener implements DragSourceListener {
}
@Override
public void dropActionChanged(DragSourceDragEvent dsde) {}
public void dropActionChanged(DragSourceDragEvent dsde) {
updateCursor(dsde);
}
/**
* Updates the cursor during drag operations using the presenter as the single source of truth.
* If the presenter returns DEFAULT_CURSOR, uses MOVE_CURSOR to prevent "No" cursor issues.
*/
private void updateCursor(DragSourceDragEvent dsde) {
Point mousePos = dsde.getDragSourceContext().getComponent().getMousePosition();
if (mousePos != null) {
// Use presenter as single source of truth for cursor
Cursor requestedCursor = presenter.getCursorAt(mousePos, false, false, false);
Cursor dragCursor = requestedCursor;
// If DEFAULT_CURSOR, use MOVE_CURSOR to prevent "No" cursor during drag operations
if (requestedCursor.getType() == Cursor.DEFAULT_CURSOR) {
dragCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
}
dsde.getDragSourceContext().setCursor(null);
dsde.getDragSourceContext().setCursor(dragCursor);
}
}
}
@@ -21,15 +21,13 @@
*/
package org.diylc.swing.plugins.canvas;
import java.awt.Point;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.*;
import java.awt.dnd.*;
import org.diylc.common.IPlugInPort;
import javax.swing.*;
/**
* {@link DropTargetListener} for {@link CanvasPanel}.
@@ -106,10 +104,35 @@ class CanvasTargetListener implements DropTargetListener {
@Override
public void drop(DropTargetDropEvent dtde) {
presenter.dragEnded(dtde.getLocation());
updateCursor(dtde);
}
@Override
public void dropActionChanged(DropTargetDragEvent dtde) {
presenter.dragActionChanged(dtde.getDropAction());
}
/**
* Updates the cursor during drag operations using the presenter as the single source of truth.
* If the presenter returns DEFAULT_CURSOR, uses MOVE_CURSOR to prevent "No" cursor issues.
*/
private void updateCursor(DropTargetDropEvent dsde) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Point mousePos = dsde.getDropTargetContext().getComponent().getMousePosition();
if (mousePos != null) {
// Use presenter as single source of truth for cursor
Cursor requestedCursor = presenter.getCursorAt(mousePos, false, false, false);
Cursor dragCursor = requestedCursor;
// If DEFAULT_CURSOR, use MOVE_CURSOR to prevent "No" cursor during drag operations
if (requestedCursor.getType() == Cursor.DEFAULT_CURSOR) {
dragCursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
}
dsde.getDropTargetContext().getComponent().setCursor(null);
dsde.getDropTargetContext().getComponent().setCursor(dragCursor);
}
}
});
}
}

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