Compare commits

...

48 Commits

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

* Fix blank board area rendering

* Fixes

* translations

* Add length

* Update update.xml
2025-12-31 15:07:22 +01:00
Bane Stojkovic 61b3c63bbf Revert "Introduce unique component instance ID"
This reverts commit 348eb3ac00.
2025-12-31 15:04:18 +01:00
Bane Stojkovic 348eb3ac00 Introduce unique component instance ID 2025-12-31 12:29:48 +01:00
Bane Stojkovic 9972e0ee46 Update .gitignore 2025-12-31 09:59:21 +01:00
Bane Stojkovic de971b707b Update build-all-profiles.sh 2025-12-30 20:09:57 +01:00
Bane Stojkovic 0dcc06f712 Update update.xml 2025-12-30 19:44:05 +01:00
Bane Stojkovic 109b0788a8 5.8.0 release 2025-12-30 14:37:56 +01:00
Bane Stojkovic d190ebe208 Cleanup 2025-12-30 10:44:12 +01:00
Bane Stojkovic fa93af2707 'Rotary Switch (Open)' cannot be selected by clicking on the wafer for 4-pole variations 2025-12-29 22:58:15 +01:00
Bane Stojkovic 6b59966816 Minor chassis panel optimization 2025-12-29 22:39:39 +01:00
Bane Stojkovic b065c9d6fb Reorganized export and print related items in the File menu to reduce clutter 2025-12-29 16:12:36 +01:00
Bane Stojkovic 49d805a60c Improved behavior of mouse cursor during drag operations; this should also fix behavior on some linux machines that show a "forbidden" cursor while dragging 2025-12-29 15:44:59 +01:00
Bane Stojkovic a9a98bf3a4 Language improvements 2025-12-29 14:01:59 +01:00
Bane Stojkovic 840d416282 Honor label color settings for 'Tape Measure' component 2025-12-29 13:40:06 +01:00
Bane Stojkovic dc2cc4c7c8 Do not use composites in outline mode 2025-12-29 13:00:41 +01:00
Bane Stojkovic 3d51d2a38d trace ending default values 2025-12-29 12:59:03 +01:00
Bane Stojkovic 1a31819431 explorer tree 2025-12-29 12:30:44 +01:00
Bane Stojkovic 4dfb1ca630 fix default LP Switch behavior for back compatibility 2025-12-29 11:15:59 +01:00
Bane Stojkovic e9c8686960 Added new property in 'LP Switch' that allows having the middle terminals connected (as they were) or disconnected to allow more flexibility 2025-12-29 10:55:45 +01:00
Bane Stojkovic 9be97f5213 fix tests 2025-12-29 10:54:39 +01:00
Bane Stojkovic 721d582dcd Update G2DWrapper.java 2025-12-28 23:09:04 +01:00
Bane Stojkovic 443f76f3f9 Monochromatic mode 2025-12-28 23:07:50 +01:00
Bane Stojkovic 3ad9c49f53 Update update.xml 2025-12-26 23:04:28 +01:00
Bane Stojkovic 61077ed5a4 Added Silkscreen export to PDF/PNG and print capabilities 2025-12-26 23:04:26 +01:00
Bane Stojkovic 1fe3706d0f improve multiwatt icon 2025-12-26 20:24:55 +01:00
Bane Stojkovic 16df8fd173 Added multiwat IC 2025-12-26 19:53:37 +01:00
Bane Stojkovic f7806d4d3c Added trace Ending Style property 2025-12-26 12:28:16 +01:00
Bane Stojkovic 04e44e174e Improve contrast of tooltips on some Linux machines that show white text against yellow ballon 2025-12-26 12:06:31 +01:00
Bane Stojkovic f74fe19b77 Update update.xml 2025-12-26 11:57:46 +01:00
Bane Stojkovic e7487069a5 Application freezes when using underside rendering or mirroring on 'Pin Header' when sized at 1x1 2025-12-26 11:57:43 +01:00
Bane Stojkovic 9c48b80515 Use configuration for scale factor 2025-12-26 11:48:52 +01:00
Bane Stojkovic 814f4296a9 Added configuration for text scale factor in rare cases when system scale factor is not applied correctly 2025-12-25 16:27:47 +01:00
82 changed files with 5819 additions and 521 deletions
+7
View File
@@ -147,3 +147,10 @@ buildNumber.properties
# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
!/.mvn/wrapper/maven-wrapper.jar
######################
###### Custom ######
######################
# Deployment folder
deploy/
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
# Script to build all Maven profiles and copy zip files to deployment folder
# Usage: ./build-all-profiles.sh [to-deploy-folder]
set -e # Exit on error
# Parse arguments
DEPLOY_DIR="${1:-}"
# Configuration
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SWING_MODULE_DIR="${PROJECT_DIR}/diylc-swing"
DEPLOY_DIR="${DEPLOY_DIR:-${PROJECT_DIR}/deploy}"
# List of profiles to build
PROFILES=(
"universal-zip"
"linux-appimage"
"win64-zip"
"win64-installer"
"mac-x86"
"mac-arm"
)
echo "=========================================="
echo "Building all Maven profiles"
echo "=========================================="
echo "Project directory: ${PROJECT_DIR}"
echo "Swing module directory: ${SWING_MODULE_DIR}"
echo "Deployment directory: ${DEPLOY_DIR}"
echo "Tests: Skipped"
echo ""
# Create deployment directory if it doesn't exist
mkdir -p "${DEPLOY_DIR}"
# Change to project root directory
cd "${PROJECT_DIR}"
# Build each profile (clean before each one)
TARGET_DIR="${SWING_MODULE_DIR}/target"
TOTAL_ZIP_COUNT=0
for profile in "${PROFILES[@]}"; do
echo "----------------------------------------"
echo "Building profile: ${profile}"
echo "----------------------------------------"
# Clean and build from root, specifying the swing module and profile
# This ensures dependencies (diylc-core, diylc-library) are built first
mvn clean package -pl diylc-swing -am -P"${profile}" -DskipTests || {
echo "ERROR: Failed to build profile ${profile}"
echo "Continuing with next profile..."
continue
}
echo "✓ Profile ${profile} built successfully"
# Copy zip and exe files immediately after each build (before next clean)
if [ -d "${TARGET_DIR}" ]; then
echo "Looking for artifacts in ${TARGET_DIR}..."
PROFILE_FILE_COUNT=0
# Find all zip files in target directory (excluding DIYLC-notarize.zip)
while IFS= read -r -d '' zipfile; do
filename=$(basename "${zipfile}")
# Skip DIYLC-notarize.zip files
if [[ "${filename}" != "DIYLC-notarize.zip" ]] && [ -f "${zipfile}" ]; then
dest="${DEPLOY_DIR}/${filename}"
echo " → Copying: ${filename}"
cp "${zipfile}" "${dest}"
TOTAL_ZIP_COUNT=$((TOTAL_ZIP_COUNT + 1))
PROFILE_FILE_COUNT=$((PROFILE_FILE_COUNT + 1))
fi
done < <(find "${TARGET_DIR}" -maxdepth 1 -type f -name "*.zip" -print0 2>/dev/null)
# Find all exe files in target directory
while IFS= read -r -d '' exefile; do
if [ -f "${exefile}" ]; then
filename=$(basename "${exefile}")
dest="${DEPLOY_DIR}/${filename}"
echo " → Copying: ${filename}"
cp "${exefile}" "${dest}"
TOTAL_ZIP_COUNT=$((TOTAL_ZIP_COUNT + 1))
PROFILE_FILE_COUNT=$((PROFILE_FILE_COUNT + 1))
fi
done < <(find "${TARGET_DIR}" -maxdepth 1 -type f -name "*.exe" -print0 2>/dev/null)
if [ ${PROFILE_FILE_COUNT} -eq 0 ]; then
echo " ⚠ Warning: No artifacts found for profile ${profile}"
# List what's actually in the target directory for debugging
echo " Debug: Files in target directory:"
ls -la "${TARGET_DIR}"/*.{zip,exe} 2>/dev/null | head -5 || echo " (no zip/exe files found)"
else
echo " ✓ Copied ${PROFILE_FILE_COUNT} file(s) from ${profile}"
fi
else
echo " ⚠ Warning: Target directory ${TARGET_DIR} does not exist"
fi
echo ""
done
echo "=========================================="
echo "Copying summary"
echo "=========================================="
if [ ${TOTAL_ZIP_COUNT} -eq 0 ]; then
echo "No artifacts were copied"
else
echo "✓ Copied ${TOTAL_ZIP_COUNT} file(s) total to ${DEPLOY_DIR}"
fi
echo ""
echo "=========================================="
echo "Build and deployment complete!"
echo "=========================================="
echo "Deployment folder: ${DEPLOY_DIR}"
echo "Files:"
ls -lh "${DEPLOY_DIR}"/*.{zip,exe} 2>/dev/null || echo " (no zip/exe files found)"
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.7.0</version>
<version>5.8.0</version>
</parent>
<artifactId>diylc-core</artifactId>
@@ -68,5 +68,9 @@ public enum DrawOption {
/**
* Whether locked components should be rendered as transparent or not.
*/
LOCKED_ALPHA;
LOCKED_ALPHA,
/**
* When applied, all colors will be converted to blackest black or whitest white
*/
MONOCHROMATIC;
}
@@ -21,7 +21,6 @@
*/
package org.diylc.common;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import org.diylc.core.Project;
@@ -55,12 +54,7 @@ public enum EventType {
*/
FILE_STATUS_CHANGED,
/**
* Called when selection rectangle is changed. Object of type {@link Rectangle} is the only
* parameter and it contains the new selection rectangle.
*/
SELECTION_RECT_CHANGED,
/**
* Called when component selection is changed. New {@link ComponentSelection} is attached as a
* Called when component selection is changed. New list of components and control point map is attached as a
* parameter.
*/
SELECTION_CHANGED,
@@ -41,16 +41,18 @@ import org.diylc.core.IDIYComponent;
import org.diylc.core.Project;
import org.diylc.core.Template;
import org.diylc.core.Theme;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.core.measures.Size;
import org.diylc.filter.IComponentFilter;
import org.diylc.test.ITestProcessor;
/**
* Interface for communication between plug-ins and the application. Plug-ins may acquire
* information or make changes through this interface. Application events are dispatched to plug-ins
* through {@link MessageDispatcher}
*
*
* @author Branislav Stojkovic
*
*
* @see IPlugIn
* @see MessageDispatcher
* @see IMessageListener
@@ -84,18 +86,20 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
public static final String RENUMBER_ON_PASTE_KEY = "renumberOnPaste";
public static final String RULER_IN_SUBDIVISION_KEY = "rulerInSubdivision";
public static final String CACHING_ENABLED_KEY = "cachingEnabled";
public static final String FONT_SCALE_FACTOR_KEY = "fontScaleFactor";
public static final String SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY = "showResizeDimensionsTooltip";
public static final String RULER_IN_SUBDIVISION_2 = "base of 2";
public static final String RULER_IN_SUBDIVISION_10 = "base of 10";
public static final String RULER_IN_SUBDIVISION_DEFAULT = RULER_IN_SUBDIVISION_10;
public static final String SNAP_TO_NONE = "None";
public static final String SNAP_TO_GRID = "Grid";
public static final String SNAP_TO_COMPONENTS = "Components";
public static final String SNAP_TO_DEFAULT = SNAP_TO_GRID;
public static final String LOCKED_ALPHA = "lockedAlpha";
public static final String LANGUAGE = "language";
public static final String LANGUAGE_DEFAULT = "English";
@@ -108,25 +112,25 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
public static final int NOBUTTON = 0;
public static final int BUTTON1 = 1;
public static final int BUTTON2 = 2;
public static final int BUTTON3 = 3;
public static final int BUTTON3 = 3;
/**
* Returns size of the canvas that takes project dimensions into account as well as zoom level.
* Each dimension is calculated as the product of the actual size and the number of pixels per
* unit. If <code>useZoom</code> is set to true, the result is scaled by zoom factor.
*
*
* @param useZoom
* @param includeExtraSpace
*
*
* @return canvas dimensions
*/
Dimension getCanvasDimensions(boolean useZoom, boolean includeExtraSpace);
/**
* Returns an instance of {@link Cursor} that should be used at the specified location.
*
*
* Note: point coordinates are scaled for zoom factor.
*
*
* @param point
* @param ctrlDown
* @param shiftDown
@@ -137,14 +141,14 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Returns an instance of currently loaded project.
*
*
* @return
*/
Project getCurrentProject();
/**
* Loads specified {@link Project}.
*
*
* @param project
* @param freshStart
* @param filename file name when loading from a file
@@ -158,22 +162,22 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Loads a project from the specified file.
*
*
* @param fileName
*/
void loadProjectFromFile(String fileName);
/**
* Saves the current project into the specified file.
*
*
* @param fileName
* @param isBackup
*/
void saveProjectToFile(String fileName, boolean isBackup);
/**
* Exports the current project into one or more files - one per layer and potentially drill layer.
*
*
* @param fileNameBase
* @param g2d
*/
@@ -191,7 +195,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Shows a user dialog if there are changes to confirm that it's safe to proceed.
*
*
* @return true, if file actions (new, open, close) can be taken
*/
boolean allowFileAction();
@@ -200,7 +204,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
* Returns all available {@link ComponentType}s classified by category. Result is a {@link Map}
* between category name to a {@link List} of all {@link ComponentType}s that share that category
* name.
*
*
* @return
*/
Map<String, List<ComponentType>> getComponentTypes();
@@ -208,7 +212,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Draws project on the provided {@link Graphics2D}. If the provided filter is not null, it will
* be used to filter the components that are shown.
*
*
* @param g2d
* @param drawOptions specific drawing options
* @param filter
@@ -222,35 +226,35 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Returns current zoom level where <code>zoomLevel = 1.0d</code> means 100%.
*
*
* @return current zoom level
*/
double getZoomLevel();
/**
* Changes current zoom level where <code>zoomLevel = 1.0d</code> means 100%
*
*
* @param zoomLevel new zoom leve
*/
void setZoomLevel(double zoomLevel);
/**
* Returns current version number.
*
*
* @return
*/
VersionNumber getCurrentVersionNumber();
/**
* Returns few most recent versions.
*
* Returns few most recent versions.
*
* @return
*/
List<Version> getRecentUpdates();
/**
* Adds a list of components to the project.
*
*
* @param componentTransferable
* @param autoGroup
* @param assignNewNames
@@ -269,7 +273,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Sets default value for the specified property name for currently selected component types.
*
*
* @param propertyName display name for property
* @param value new default value, must not be null
*/
@@ -277,7 +281,7 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
/**
* Sets default value for the specified property name for projects.
*
*
* @param clazz class to set defaults to
* @param propertyName display name for property
* @param value new default value, must not be null
@@ -291,6 +295,15 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
*/
List<PropertyWrapper> getMutualSelectionProperties();
/**
* Available only during resizing, returns the dimensions of the component being resized.
* Otherwise returns null.
*
* @return ResizeDimensions with width, height (in mm or in depending on configuration),
* and length (if component implements IHaveLength), or null if not resizing
*/
ResizeDimensions getResizeDimensions();
void editSelection();
/**
@@ -37,16 +37,14 @@ import com.thoughtworks.xstream.annotations.XStreamOmitField;
import org.diylc.common.IPlugInPort;
import org.diylc.common.LineStyle;
import org.diylc.common.ObjectCache;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComponent<T> {
public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComponent<T> implements
IHaveLength {
private static final long serialVersionUID = 1L;
@@ -337,8 +335,8 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
// }
}
@EditableProperty
public Size getLength() {
@EditableProperty(name = "Length")
public Size calculateLength() {
double d = 0;
try {
Point2D[] p = getControlPoints();
@@ -0,0 +1,29 @@
/*
DIY Layout Creator (DIYLC).
Copyright (c) 2009-2025 held jointly by the individual authors.
This file is part of DIYLC.
DIYLC is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
DIYLC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.core;
import org.diylc.core.measures.Size;
public interface IHaveLength {
Size calculateLength();
}
@@ -0,0 +1,33 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.core.measures;
/**
* Record containing resize dimensions for a component being resized.
*
* @param width the width of the component (in mm or in, depending on configuration)
* @param height the height of the component (in mm or in, depending on configuration)
* @param length the length of the component (if it implements IHaveLength), null otherwise
*/
public record ResizeDimensions(Size width, Size height, Size length) {
}
@@ -19,7 +19,7 @@
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.common;
package org.diylc.filter;
import org.diylc.core.IDIYComponent;
@@ -19,11 +19,10 @@
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.presenter;
package org.diylc.filter;
import java.lang.reflect.Method;
import org.diylc.common.IComponentFilter;
import org.diylc.common.PCBLayer;
import org.diylc.core.IDIYComponent;
@@ -0,0 +1,23 @@
package org.diylc.filter;
import org.diylc.core.IBoard;
import org.diylc.core.IDIYComponent;
import org.diylc.core.gerber.GerberRenderMode;
import org.diylc.core.gerber.IGerberComponent;
import java.util.Set;
public class SilkScreenFilter implements IComponentFilter {
@Override
public boolean testComponent(IDIYComponent<?> component) {
if (component instanceof IBoard board) {
return board.shouldExportToGerber();
}
if (!(component instanceof IGerberComponent gerberComponent)) {
return false;
}
Set<GerberRenderMode> gerberRenderModes = gerberComponent.getGerberRenderModes();
return gerberRenderModes.contains(GerberRenderMode.Outline);
}
}
@@ -18,6 +18,7 @@
*/
package org.diylc.presenter;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.util.List;
@@ -71,4 +72,54 @@ public class CalcUtils {
return false;
return true;
}
/**
* Calculates the relative luminance (perceived brightness) of a color.
* Uses the standard formula: L = 0.299*R + 0.587*G + 0.114*B
*
* @param color the color to calculate luminance for
* @return the luminance value between 0 and 255
*/
public static double calculateLuminance(Color color) {
if (color == null) {
return 0.0;
}
return 0.299 * color.getRed() + 0.587 * color.getGreen() + 0.114 * color.getBlue();
}
/**
* Converts a color to monochrome (black or white) based on its luminance.
* Colors with luminance below the threshold (128) are converted to black,
* otherwise to white. The alpha channel is preserved if present.
*
* @param color the color to convert
* @param threshold the luminance threshold (default 128 for middle gray)
* @return black or white color, preserving alpha channel
*/
public static Color convertToMonochrome(Color color, double threshold) {
if (color == null) {
return null;
}
double luminance = calculateLuminance(color);
// Use <= to handle edge case where luminance equals threshold (should be white)
// Add small epsilon to account for floating point precision issues
Color monochromeColor = (luminance < threshold - 1e-10) ? Color.BLACK : Color.WHITE;
// Preserve alpha channel if present
if (color.getAlpha() < 255) {
monochromeColor = new Color(monochromeColor.getRed(), monochromeColor.getGreen(),
monochromeColor.getBlue(), color.getAlpha());
}
return monochromeColor;
}
/**
* Converts a color to monochrome (black or white) based on its luminance.
* Uses the default threshold of 128 (middle gray).
*
* @param color the color to convert
* @return black or white color, preserving alpha channel
*/
public static Color convertToMonochrome(Color color) {
return convertToMonochrome(color, 128.0);
}
}
@@ -26,10 +26,12 @@ import java.lang.ref.SoftReference;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.log4j.Logger;
import org.diylc.common.ComponentType;
import org.diylc.common.DrawOption;
import org.diylc.core.ComponentForRender;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
@@ -67,14 +69,14 @@ public class DrawingCache {
*/
@SuppressWarnings("unchecked")
public void bulkPrepare(Collection<ComponentForRender> components, G2DWrapper g2d, boolean outlineMode, Project project,
double zoom, double scaleFactor) {
double zoom, double scaleFactor, Set<DrawOption> drawOptions) {
// run all renders in parallel to utilize all CPU cores
components.stream().parallel().forEach(x -> {
ComponentType type = ComponentProcessor.getInstance()
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) x.getComponent().getClass());
if (type != null && type.getEnableCache()) {
renderAndCache(x.getComponent(), g2d, x.getState(), outlineMode, project, zoom, scaleFactor, x.getZOrder());
renderAndCache(x.getComponent(), g2d, x.getState(), outlineMode, project, zoom, scaleFactor, x.getZOrder(), drawOptions);
}
});
}
@@ -92,7 +94,7 @@ public class DrawingCache {
*/
@SuppressWarnings("unchecked")
public void draw(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Rectangle2D visibleRect) {
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Rectangle2D visibleRect, Set<DrawOption> drawOptions) {
ComponentType type = ComponentProcessor.getInstance()
.extractComponentTypeFrom((Class<? extends IDIYComponent<?>>) component.getClass());
@@ -100,7 +102,7 @@ public class DrawingCache {
// if we need to apply caching
Point2D firstPoint = component.getControlPoint(0);
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom, scaleFactor, zOrder);
CacheValue value = renderAndCache(component, g2d, componentState, outlineMode, project, zoom, scaleFactor, zOrder, drawOptions);
if (value == null) {
return;
@@ -180,7 +182,7 @@ public class DrawingCache {
}
private CacheValue renderAndCache(IDIYComponent<?> component, G2DWrapper g2d, ComponentState componentState,
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder) {
boolean outlineMode, Project project, double zoom, double scaleFactor, int zOrder, Set<DrawOption> drawOptions) {
if (!g2d.getClipBounds().intersects(component.getCachingBounds())) {
return null;
@@ -199,9 +201,11 @@ public class DrawingCache {
}
// only honor the cache if the component hasn't changed in the meantime
// Also check if drawOptions changed (e.g., MONOCHROMATIC affects rendering)
boolean drawOptionsChanged = value == null || !value.getDrawOptions().equals(drawOptions);
if (value == null || !value.getComponent().equalsTo(component)
|| value.getState() != componentState || value.getZoom() != zoom || value.getOutlineMode() != outlineMode
|| value.getScaleFactor() != scaleFactor) {
|| value.getScaleFactor() != scaleFactor || drawOptionsChanged) {
// figure out size and placement of buffer image
Rectangle2D rect = component.getCachingBounds();
int width = (int)Math.round(rect.getWidth() * zoom);
@@ -233,7 +237,7 @@ public class DrawingCache {
cg2d.translate(-firstPoint.getX() + dx, -firstPoint.getY() + dy);
// wrap the graphics so we can track
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom);
G2DWrapper wrapper = new G2DWrapper(cg2d, zoom, drawOptions);
// initialize wrapper
wrapper.startedDrawingComponent(zOrder);
@@ -275,7 +279,7 @@ public class DrawingCache {
// add to the cache
value = new CacheValue(component, image, wrapper.getCurrentArea(), wrapper.getContinuityPositiveAreas(),
wrapper.getContinuityNegativeAreas(), componentState, outlineMode, zoom, scaleFactor, dx, dy, rect);
wrapper.getContinuityNegativeAreas(), componentState, outlineMode, zoom, scaleFactor, dx, dy, rect, drawOptions);
imageCache.put(component, new SoftReference<DrawingCache.CacheValue>(value));
}
@@ -295,11 +299,12 @@ public class DrawingCache {
int dx;
int dy;
Rectangle2D cacheBounds;
Set<DrawOption> drawOptions;
public CacheValue(IDIYComponent<?> component, BufferedImage image, Area currentArea,
Map<String, Area> continuityPositiveAreas, Map<String, Area> continuityNegativeAreas,
ComponentState state, boolean outlineMode, double zoom, double scaleFactor, int dx, int dy,
Rectangle2D cacheBounds) {
Rectangle2D cacheBounds, Set<DrawOption> drawOptions) {
super();
this.cacheBounds = cacheBounds;
// take a copy of the component, so we can check if it changed in the meantime
@@ -317,6 +322,7 @@ public class DrawingCache {
this.dx = dx;
this.dy = dy;
this.scaleFactor = scaleFactor;
this.drawOptions = drawOptions;
}
public IDIYComponent<?> getComponent() {
@@ -366,6 +372,10 @@ public class DrawingCache {
public Rectangle2D getCacheBounds() {
return cacheBounds;
}
public Set<DrawOption> getDrawOptions() {
return drawOptions;
}
@Override
public String toString() {
@@ -54,6 +54,7 @@ import org.diylc.components.AbstractTransparentComponent;
import org.diylc.core.*;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.filter.IComponentFilter;
import org.diylc.netlist.ContinuityGraph;
import org.diylc.netlist.NetlistBuilder;
import org.diylc.utils.Constants;
@@ -183,7 +184,7 @@ public class DrawingManager {
if (externalZoom != null)
zoom *= externalZoom;
G2DWrapper g2dWrapper = new G2DWrapper(g2d, zoom);
G2DWrapper g2dWrapper = new G2DWrapper(g2d, zoom, drawOptions);
configureRenderingHints(g2d, drawOptions);
@@ -267,7 +268,7 @@ public class DrawingManager {
// give the cache a chance to prepare all the components in multiple threads
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) {
DrawingCache.Instance.bulkPrepare(componentForRenderList, g2dWrapper, outlineMode, project, zoom, scaleFactor);
DrawingCache.Instance.bulkPrepare(componentForRenderList, g2dWrapper, outlineMode, project, zoom, scaleFactor, drawOptions);
}
// componentAreaMap.clear();
@@ -561,10 +562,12 @@ public class DrawingManager {
}
}
private static void drawComponent(Project project, Set<DrawOption> drawOptions, Double scaleFactor, Rectangle2D visibleRect, IDIYComponent<?> component, G2DWrapper g2dWrapper, ComponentState state, double zoom, int i, boolean outlineMode) {
private static void drawComponent(Project project, Set<DrawOption> drawOptions, Double scaleFactor,
Rectangle2D visibleRect, IDIYComponent<?> component, G2DWrapper g2dWrapper, ComponentState state,
double zoom, int i, boolean outlineMode) {
if (drawOptions.contains(DrawOption.ENABLE_CACHING)) // go through the DrawingCache
DrawingCache.Instance.draw(component, g2dWrapper, state,
drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom, scaleFactor, i, visibleRect);
drawOptions.contains(DrawOption.OUTLINE_MODE), project, zoom, scaleFactor, i, visibleRect, drawOptions);
else // go straight to the wrapper
component.draw(g2dWrapper, state, outlineMode, project, g2dWrapper);
}
@@ -53,17 +53,11 @@ import java.awt.image.renderable.RenderableImage;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.AttributedCharacterIterator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;
import org.apache.log4j.Logger;
import org.diylc.common.DrawOption;
import org.diylc.common.ObjectCache;
import org.diylc.common.ZoomableStroke;
import org.diylc.core.IDrawingObserver;
@@ -71,7 +65,7 @@ import org.diylc.core.IDrawingObserver;
/**
* {@link Graphics2D} wrapper that keeps track of all drawing actions and creates an {@link Area}
* that corresponds to drawn objects. Before each component is drawn,
* {@link #startedDrawingComponent()} should be called. After the component is drawn, area may be
* {@link #startedDrawingComponent(int)} should be called. After the component is drawn, area may be
* retrieved using {@link #finishedDrawingComponent()}. Graphics configuration (color, font, etc) is
* reset between each two components.
*
@@ -96,7 +90,9 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
private int uniqueCounter = 0;
private Graphics2D canvasGraphics;
private final Graphics2D canvasGraphics;
private final Set<DrawOption> drawOptions;
private Stroke originalStroke;
private Color originalColor;
private Composite originalComposite;
@@ -115,12 +111,14 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
/**
* Creates a wrapper around specified {@link Graphics2D} object.
*
*
* @param canvasGraphics
* @param drawOptions
*/
public G2DWrapper(Graphics2D canvasGraphics, double zoom) {
public G2DWrapper(Graphics2D canvasGraphics, double zoom, Set<DrawOption> drawOptions) {
super();
this.canvasGraphics = canvasGraphics;
this.drawOptions = drawOptions;
String debugAreaPerformanceStr = System.getProperty(DEBUG_AREA_PERFORMANCE);
debugAreaPerformance = "true".equalsIgnoreCase(debugAreaPerformanceStr);
this.zoom = zoom;
@@ -482,7 +480,9 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
@Override
public void setComposite(Composite comp) {
canvasGraphics.setComposite(comp);
if (!drawOptions.contains(DrawOption.OUTLINE_MODE)) {
canvasGraphics.setComposite(comp);
}
// FIXME: check this.
}
@@ -758,7 +758,11 @@ class G2DWrapper extends Graphics2D implements IDrawingObserver {
@Override
public void setColor(Color c) {
canvasGraphics.setColor(c);
if (drawOptions.contains(DrawOption.MONOCHROMATIC) && c != null) {
canvasGraphics.setColor(CalcUtils.convertToMonochrome(c));
} else {
canvasGraphics.setColor(c);
}
}
@Override
@@ -59,18 +59,13 @@ import com.thoughtworks.xstream.security.AnyTypePermission;
import org.diylc.clipboard.ComponentTransferable;
import org.diylc.common.*;
import org.diylc.core.ExpansionMode;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDatasheetSupport;
import org.diylc.core.IView;
import org.diylc.core.Project;
import org.diylc.core.Template;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.IAutoCreator;
import org.diylc.core.gerber.GerberExporter;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.filter.IComponentFilter;
import org.diylc.lang.LangUtil;
import org.diylc.netlist.INetlistParser;
import org.diylc.netlist.Netlist;
@@ -250,8 +245,10 @@ public class Presenter implements IPlugInPort {
@Override
public Cursor getCursorAt(Point point, boolean ctrlDown, boolean shiftDown, boolean altDown) {
// Only change the cursor if we're not making a new component.
if (operationMode == OperationMode.HIGHLIGHT_CONNECTED_AREAS || altDown)
if (operationMode == OperationMode.HIGHLIGHT_CONNECTED_AREAS || altDown) {
return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
}
if (instantiationManager.getComponentTypeSlot() == null) {
// Scale point to remove zoom factor.
Point2D scaledPoint = scalePoint(point);
@@ -268,6 +265,7 @@ public class Presenter implements IPlugInPort {
}
}
}
return Cursor.getDefaultCursor();
}
@@ -427,7 +425,7 @@ public class Presenter implements IPlugInPort {
}
@Override
public void draw(Graphics2D g2d, Set<DrawOption> drawOptions, final IComponentFilter filter, Double externalZoom,
public void draw(Graphics2D g2d, Set<DrawOption> drawOptions, final IComponentFilter filter, Double externalZoom,
Double scaleFactor, Rectangle2D visibleRect) {
if (currentProject == null) {
return;
@@ -924,29 +922,36 @@ public class Presenter implements IPlugInPort {
messageDispatcher.dispatchMessage(EventType.REPAINT);
}
@Override
public Rectangle2D getSelectionBounds(boolean applyZoom) {
if (selectedComponents == null || selectedComponents.isEmpty())
return getComponentBounds(selectedComponents, applyZoom);
}
private Rectangle2D getComponentBounds(Collection<IDIYComponent<?>> components, boolean applyZoom) {
if (components == null || components.isEmpty())
return null;
int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
for (IDIYComponent<?> c : selectedComponents) {
for (IDIYComponent<?> c : components) {
ComponentArea compArea = drawingManager.getComponentArea(c);
if (compArea != null && compArea.getOutlineArea() != null) {
Rectangle rect = compArea.getOutlineArea().getBounds();
if (rect.x < minX)
minX = rect.x;
if (rect.x + rect.width > maxX)
maxX = rect.x + rect.width;
if (rect.y < minY)
minY = rect.y;
if (rect.y + rect.height > maxY)
maxY = rect.y + rect.height;
} else if (currentProject.getComponents().contains(c))
if (compArea == null || compArea.getOutlineArea() == null) {
LOG.debug("Area is null for " + c.getName() + " of type " + c.getClass().getName());
return null;
}
Rectangle rect = compArea.getOutlineArea().getBounds();
if (rect.x < minX)
minX = rect.x;
if (rect.x + rect.width > maxX)
maxX = rect.x + rect.width;
if (rect.y < minY)
minY = rect.y;
if (rect.y + rect.height > maxY)
maxY = rect.y + rect.height;
}
if (configManager.readBoolean(EXTRA_SPACE_KEY, true)) {
@@ -1015,8 +1020,9 @@ public class Presenter implements IPlugInPort {
}
updateSelection(matching);
messageDispatcher.dispatchMessage(EventType.REPAINT, true);
if (matching.size() > 0)
if (!matching.isEmpty()) {
messageDispatcher.dispatchMessage(EventType.SCROLL_TO, getSelectionBounds(true));
}
}
@Override
@@ -1233,11 +1239,14 @@ public class Presenter implements IPlugInPort {
Map<String, Object> params = new HashMap<String, Object>();
params.put("point", point);
test.addStep(DIYTest.DRAG_OVER, params);
}
}
if (point == null || operationMode == OperationMode.HIGHLIGHT_CONNECTED_AREAS) {
return false;
}
boolean resizeInProgress = false;
Point2D scaledPoint = scalePoint(point);
if (controlPointMap != null && !controlPointMap.isEmpty()) {
// We're dragging control point(s).
@@ -1248,6 +1257,13 @@ public class Presenter implements IPlugInPort {
if (actualD == null)
return true;
// if there's at least one component with only one control point being dragged it means that we are resizing
if (controlPointMap.size() == 1 && controlPointMap.entrySet().stream()
.anyMatch(entry -> entry.getValue().size() == 1
&& entry.getKey().getControlPointCount() > 1)) {
resizeInProgress = true;
}
previousDragPoint.setLocation(previousDragPoint.getX() + actualD.getX(), previousDragPoint.getY() + actualD.getY());
} else if (selectedComponents.isEmpty() && instantiationManager.getComponentTypeSlot() == null
&& previousDragPoint != null) {
@@ -1265,10 +1281,50 @@ public class Presenter implements IPlugInPort {
this.previousScaledPoint = scalePoint(point);
instantiationManager.updateSingleClick(previousScaledPoint, isSnapToGrid(), currentProject.getGridSpacing());
}
messageDispatcher.dispatchMessage(EventType.REPAINT);
messageDispatcher.dispatchMessage(EventType.REPAINT, false, resizeInProgress);
return true;
}
public ResizeDimensions getResizeDimensions() {
if (controlPointMap == null || controlPointMap.isEmpty()) {
return null;
}
// Find the component being resized (one with a single control point being dragged)
IDIYComponent<?> resizingComponent = null;
for (Map.Entry<IDIYComponent<?>, Set<Integer>> entry : controlPointMap.entrySet()) {
if (entry.getValue().size() == 1 && entry.getKey().getControlPointCount() > 1) {
resizingComponent = entry.getKey();
break;
}
}
if (resizingComponent == null) {
return null;
}
Rectangle2D componentBounds = getComponentBounds(controlPointMap.keySet(), false);
if (componentBounds == null) {
return null;
}
// Convert pixel dimensions to Size (mm or in based on configuration)
boolean metric = configManager.readBoolean(METRIC_KEY, true);
SizeUnit unit = metric ? SizeUnit.mm : SizeUnit.in;
Size width = Size.fromPixels(componentBounds.getWidth(), unit);
Size height = Size.fromPixels(componentBounds.getHeight(), unit);
// Get length if component implements IHaveLength
Size length = null;
if (resizingComponent instanceof IHaveLength iHaveLength) {
length = iHaveLength.calculateLength();
}
return new ResizeDimensions(width, height, length);
}
private Point2D moveComponents(Map<IDIYComponent<?>, Set<Integer>> controlPointMap, int dx, int dy, boolean snapToGrid, boolean snapToObjects) {
// After we make the transfer and snap to grid, calculate actual dx
// and dy. We'll use them to translate the previous drag point.
@@ -1580,6 +1636,7 @@ public class Presenter implements IPlugInPort {
drawingManager.clearContinuityArea();
projectFileManager.notifyFileChange();
}
messageDispatcher.dispatchMessage(EventType.REPAINT);
dragInProgress = false;
}
@@ -2166,6 +2223,10 @@ public class Presenter implements IPlugInPort {
}
Rectangle2D rect = getSelectionBounds(false);
if (rect == null) {
return null;
}
double width = rect.getWidth();
double height = rect.getHeight();
@@ -2508,7 +2569,7 @@ public class Presenter implements IPlugInPort {
}
try {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(
"import-defaults/variants.xml"));
"pending-variants.xml"));
XStream xStream = new XStream(new DomDriver());
xStream.addPermission(AnyTypePermission.ANY);
ProjectFileManager.xStreamSerializer.toXML(defaultVariantMap, out);
@@ -4128,4 +4128,204 @@
</org.diylc.core.Template>
</list>
</entry>
</map>
<entry>
<string>org.diylc.components.electromechanical.TactileMicroSwitch</string>
<list>
<org.diylc.core.Template>
<name>6x6 (e.g. Omron B3F-1000)</name>
<values>
<entry>
<string>Spacing X</string>
<org.diylc.core.measures.Size value="6.5" unit="mm"/>
</entry>
<entry>
<string>Spacing Y</string>
<org.diylc.core.measures.Size value="4.5" unit="mm"/>
</entry>
<entry>
<string>Label</string>
<awt-color hex="ffffff"/>
</entry>
<entry>
<string>Orientation</string>
<org.diylc.common.Orientation>DEFAULT</org.diylc.common.Orientation>
</entry>
<entry>
<string>Case</string>
<org.diylc.core.measures.Size value="6.0" unit="mm"/>
</entry>
<entry>
<string>Name</string>
<string>SW2</string>
</entry>
<entry>
<string>Font Size Override</string>
<null/>
</entry>
<entry>
<string>Value</string>
<string></string>
</entry>
<entry>
<string>Alpha</string>
<byte>127</byte>
</entry>
<entry>
<string>Body</string>
<awt-color hex="c0c0c0"/>
</entry>
<entry>
<string>Border</string>
<awt-color hex="595959"/>
</entry>
<entry>
<string>Display</string>
<org.diylc.common.Display>BOTH</org.diylc.common.Display>
</entry>
<entry>
<string>Pin Pairing Mode</string>
<diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>Opposite</diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>
</entry>
</values>
<points>
<point x="0.0" y="0.0"/>
<point x="0.0" y="0.175"/>
<point x="0.255" y="0.0"/>
<point x="0.255" y="0.175"/>
</points>
<defaultFlag>false</defaultFlag>
<createdOn>2026-01-02T10:01:17.805688</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>12x12 (e.g. Omron B3F-4000)</name>
<values>
<entry>
<string>Spacing X</string>
<org.diylc.core.measures.Size value="12.6" unit="mm"/>
</entry>
<entry>
<string>Spacing Y</string>
<org.diylc.core.measures.Size value="5.0" unit="mm"/>
</entry>
<entry>
<string>Label</string>
<awt-color hex="ffffff"/>
</entry>
<entry>
<string>Orientation</string>
<org.diylc.common.Orientation>DEFAULT</org.diylc.common.Orientation>
</entry>
<entry>
<string>Case</string>
<org.diylc.core.measures.Size value="12.0" unit="mm"/>
</entry>
<entry>
<string>Name</string>
<string>SW1</string>
</entry>
<entry>
<string>Font Size Override</string>
<null/>
</entry>
<entry>
<string>Value</string>
<string></string>
</entry>
<entry>
<string>Alpha</string>
<byte>127</byte>
</entry>
<entry>
<string>Body</string>
<awt-color hex="c0c0c0"/>
</entry>
<entry>
<string>Border</string>
<awt-color hex="595959"/>
</entry>
<entry>
<string>Display</string>
<org.diylc.common.Display>BOTH</org.diylc.common.Display>
</entry>
<entry>
<string>Pin Pairing Mode</string>
<diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>Opposite</diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>
</entry>
</values>
<points>
<point x="0.0" y="0.0"/>
<point x="0.0" y="0.195"/>
<point x="0.495" y="0.0"/>
<point x="0.495" y="0.195"/>
</points>
<defaultFlag>false</defaultFlag>
<createdOn>2026-01-02T10:00:31.950676</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>5x5 Low Profile SMD (e.g. Alps SKQG)</name>
<values>
<entry>
<string>Spacing X</string>
<org.diylc.core.measures.Size value="5.2" unit="mm"/>
</entry>
<entry>
<string>Spacing Y</string>
<org.diylc.core.measures.Size value="3.7" unit="mm"/>
</entry>
<entry>
<string>Label</string>
<awt-color hex="ffffff"/>
</entry>
<entry>
<string>Orientation</string>
<org.diylc.common.Orientation>DEFAULT</org.diylc.common.Orientation>
</entry>
<entry>
<string>Case</string>
<org.diylc.core.measures.Size value="5.0" unit="mm"/>
</entry>
<entry>
<string>Name</string>
<string>SW2</string>
</entry>
<entry>
<string>Font Size Override</string>
<null/>
</entry>
<entry>
<string>Value</string>
<string></string>
</entry>
<entry>
<string>Alpha</string>
<byte>127</byte>
</entry>
<entry>
<string>Border</string>
<awt-color hex="595959"/>
</entry>
<entry>
<string>Body</string>
<awt-color hex="c0c0c0"/>
</entry>
<entry>
<string>Display</string>
<org.diylc.common.Display>BOTH</org.diylc.common.Display>
</entry>
<entry>
<string>Pin Pairing Mode</string>
<diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>Diagonal</diylc.electromechanical.TactileMicroSwitch_-PinPairingMode>
</entry>
</values>
<points>
<point x="0.0" y="0.0"/>
<point x="0.0" y="0.145"/>
<point x="0.2" y="0.0"/>
<point x="0.2" y="0.145"/>
</points>
<defaultFlag>false</defaultFlag>
<createdOn>2026-01-02T10:04:33.795741</createdOn>
</org.diylc.core.Template>
</list>
</entry>
</map>
+109 -1
View File
@@ -422,17 +422,125 @@
<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>2025-12-30 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added balloon tooltip next to cursor showing component width, height and length (where available) while resizing; can be disabled from Config menu</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'Freeway 3x3-05 (Ultra) Switch' (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Configurable 'Tactile Micro Switch' dimensions and added a few most commonly used variants</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Exposed the most common pin pairing modes and added switching logic for 'Tactile Micro Switch' to allow using it for analysis</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>'Potentiometer Panel' can now render numerical lug markers for easier identification</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Auto-Wrap Label' retains line breaks from the input text</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Cliff 1/8" Jack', 'Open 1/8" Jack' and 'Open 1/4" Jack' can be rotated</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Fixed the offset of terminals on a DP4T Tele type of 'Lever Switch' to be aligned with the physical switch (attention - this will break existing diagrams and will require adjusting leads to match the new terminal positions)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Make the label of schematic symbols (e.g. 'Resistor Symbol') clickable as well</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Ability to resize 'Transformer Core' schematic symbol after it's already placed</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Clicking on 'Tactile Micro Switch' body doesn't register</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>New lines at the end of the 'Label' text get erroneously rendered as br HTML tags</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
@@ -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.8.0</version>
</parent>
<artifactId>diylc-library</artifactId>
@@ -31,7 +31,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.VerticalAlignment;
@@ -72,13 +71,18 @@ public class StringUtils {
* @return a non-empty list of strings
*/
public static List<String> wrap(String str, FontMetrics fm, int maxWidth) {
String[] split = str.split("\\<br\\>");
List<String> lines = Arrays.stream(split).flatMap(s -> splitIntoLines(s).stream()).collect(Collectors.toList());
if (lines.size() == 0)
return lines;
// First split by actual newlines to honor them, then split by <br> tags
List<String> lines = splitIntoLines(str);
List<String> allLines = new ArrayList<String>();
for (String line : lines) {
String[] split = line.split("\\<br\\>");
allLines.addAll(Arrays.asList(split));
}
if (allLines.size() == 0)
return allLines;
ArrayList<String> strings = new ArrayList<String>();
for (Iterator<String> iter = lines.iterator(); iter.hasNext();)
for (Iterator<String> iter = allLines.iterator(); iter.hasNext();)
wrapLineInto((String) iter.next(), strings, fm, maxWidth);
return strings;
}
@@ -126,8 +130,8 @@ public class StringUtils {
line = line.substring(pos).trim();
len = line.length();
}
if (len > 0)
list.add(line);
// Always add the line, even if empty, to preserve empty lines from newlines
list.add(line);
}
/**
@@ -209,12 +213,24 @@ public class StringUtils {
if (text == null)
return;
String[] parts = text.split("\\<br\\>");
// Use -1 limit to preserve trailing empty strings so we can filter them out
String[] parts = text.split("\\<br\\>", -1);
if (parts.length > 1) {
// Remove trailing empty strings to avoid rendering extra blank lines
int lastNonEmpty = parts.length - 1;
while (lastNonEmpty >= 0 && parts[lastNonEmpty].trim().isEmpty()) {
lastNonEmpty--;
}
// If all parts are empty, don't render anything
if (lastNonEmpty < 0) {
return;
}
FontMetrics fontMetrics = g2d.getFontMetrics();
Rectangle stringBounds = fontMetrics.getStringBounds(parts[0], g2d).getBounds();
for (int i = 0; i < parts.length; i++)
drawCenteredText(g2d, parts[i], x, (int)(y - stringBounds.height * (parts.length - 1) / 2d + i * stringBounds.height), horizontalAlignment, verticalAlignment);
int actualLength = lastNonEmpty + 1;
for (int i = 0; i < actualLength; i++)
drawCenteredText(g2d, parts[i], x, (int)(y - stringBounds.height * (actualLength - 1) / 2d + i * stringBounds.height), horizontalAlignment, verticalAlignment);
return;
}
@@ -43,12 +43,7 @@ import org.diylc.common.IPlugInPort;
import org.diylc.common.LineStyle;
import org.diylc.common.ObjectCache;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractLabeledComponent;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.*;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.PositiveNonZeroMeasureValidator;
import org.diylc.core.measures.Size;
@@ -63,7 +58,8 @@ import org.diylc.utils.Constants;
*
* @author Branislav Stojkovic
*/
public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponent<T> {
public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponent<T> implements
IHaveLength {
private static final long serialVersionUID = 1L;
@@ -367,8 +363,8 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
offset = -offset;
}
drawingObserver.startTracking();
if (getMoveLabel()) {
drawingObserver.startTracking();
g2d.setTransform(oldTransform);
g2d.translate(getPoints()[2].getX(), getPoints()[2].getY());
if (getLabelOriantation() != LabelOriantation.Horizontal) {
@@ -378,19 +374,18 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
g2d.setTransform(oldTransform);
} else {
if (isStanding() || getLabelOriantation() == LabelOriantation.Horizontal) {
drawingObserver.startTracking();
g2d.setTransform(oldTransform);
double x = (getPoints()[0].getX() + getPoints()[1].getX() - length) / 2.0;
double y = (getPoints()[0].getY() + getPoints()[1].getY() - width) / 2.0;
g2d.drawString(label, (int) (x + (length - textRect.getWidth()) / 2 + offset),
(int) (y + calculateLabelYOffset(shapeRect, textRect, fontMetrics)));
} else {
drawingObserver.stopTracking();
g2d.drawString(label, (int) (length - textRect.getWidth()) / 2 + offset,
calculateLabelYOffset(shapeRect, textRect, fontMetrics));
g2d.setTransform(oldTransform);
}
}
drawingObserver.stopTracking();
}
// if (getLabelOriantation() == LabelOriantation.Horizontal) {
//
@@ -434,7 +429,7 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
Stroke stroke = null;
switch (getStyle()) {
case SOLID:
stroke = ObjectCache.getInstance().fetchBasicStroke(thickness);
stroke = ObjectCache.getInstance().fetchStroke(thickness, getEndingStyle() == LineEndingStyle.Round ? BasicStroke.CAP_ROUND : BasicStroke.CAP_BUTT);
break;
case DASHED:
stroke =
@@ -641,6 +636,32 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
this.length = length;
}
@Override
public Size calculateLength() {
try {
Point2D[] points = getPoints();
if (points.length < 2) {
return null;
}
// Calculate distance between ending points
double distanceInPixels = points[0].distance(points[1]);
// Convert to Size with appropriate unit (mm or in based on configuration)
SizeUnit unit;
if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.METRIC_KEY, true)) {
unit = SizeUnit.mm;
} else {
unit = SizeUnit.in;
}
return Size.fromPixels(distanceInPixels, unit);
} catch (Exception e) {
return null;
}
}
@EditableProperty(name = "Width", defaultable = true, validatorClass = PositiveNonZeroMeasureValidator.class, sortOrder = 102)
public Size getWidth() {
return width;
@@ -747,6 +768,10 @@ public abstract class AbstractLeadedComponent<T> extends AbstractLabeledComponen
hideShortLeads = false;
return hideShortLeads;
}
protected LineEndingStyle getEndingStyle() {
return LineEndingStyle.Round;
}
public void setHideShortLeads(Boolean hideShortLeads) {
this.hideShortLeads = hideShortLeads;
@@ -0,0 +1,5 @@
package org.diylc.components;
public enum LineEndingStyle {
Round, Flat
}
@@ -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,8 +64,15 @@ public class ChassisPanel extends AbstractShape {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(color);
g2d.fillRoundRect((int)firstPoint.getX(), (int)firstPoint.getY(), (int)(secondPoint.getX() - firstPoint.getX()), (int)(secondPoint.getY() - firstPoint.getY()), radius,
radius);
if (radius == 0) {
g2d.fillRect((int) firstPoint.getX(), (int) firstPoint.getY(),
(int) (secondPoint.getX() - firstPoint.getX()),
(int) (secondPoint.getY() - firstPoint.getY()));
} else {
g2d.fillRoundRect((int) firstPoint.getX(), (int) firstPoint.getY(),
(int) (secondPoint.getX() - firstPoint.getX()),
(int) (secondPoint.getY() - firstPoint.getY()), radius, radius);
}
g2d.setComposite(oldComposite);
}
// Do not track any changes that follow because the whole rect has been
@@ -73,8 +80,15 @@ public class ChassisPanel extends AbstractShape {
drawingObserver.stopTracking();
g2d.setColor(componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
: borderColor);
g2d.drawRoundRect((int)(firstPoint.getX()), (int)(firstPoint.getY()), (int)(secondPoint.getX() - firstPoint.getX()), (int)(secondPoint.getY() - firstPoint.getY()), radius,
radius);
if (radius == 0) {
g2d.drawRect((int) (firstPoint.getX()), (int) (firstPoint.getY()),
(int) (secondPoint.getX() - firstPoint.getX()),
(int) (secondPoint.getY() - firstPoint.getY()));
} else {
g2d.drawRoundRect((int) (firstPoint.getX()), (int) (firstPoint.getY()),
(int) (secondPoint.getX() - firstPoint.getX()),
(int) (secondPoint.getY() - firstPoint.getY()), radius, radius);
}
}
@EditableProperty(name = "Radius")
@@ -28,6 +28,7 @@ import com.bancika.gerberwriter.GerberFunctions;
import org.diylc.common.ObjectCache;
import org.diylc.common.PCBLayer;
import org.diylc.components.AbstractLeadedComponent;
import org.diylc.components.LineEndingStyle;
import org.diylc.components.transform.SimpleComponentTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.CreationMethod;
@@ -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() {
@@ -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++) {
@@ -35,13 +35,10 @@ import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractLabeledComponent;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.components.guitar.LeverSwitchPositionPropertyValueSource;
import org.diylc.core.*;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.DynamicEditableProperty;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.gerber.IGerberComponentSimple;
@@ -53,22 +50,21 @@ import org.diylc.utils.Constants;
category = "Electro-Mechanical", instanceNamePrefix = "SW",
description = "4-pin tactile momentary switch", zOrder = IDIYComponent.COMPONENT,
keywordPolicy = KeywordPolicy.SHOW_VALUE, enableCache = true)
public class TactileMicroSwitch extends AbstractLabeledComponent<String> implements IGerberComponentSimple {
public class TactileMicroSwitch extends AbstractLabeledComponent<String> implements IGerberComponentSimple,
ISwitch {
private static final long serialVersionUID = 1L;
public static Color BODY_COLOR = Color.lightGray;
public static Color BORDER_COLOR = Color.gray.darker();
public static Color PIN_COLOR = METAL_COLOR;
public static Color PIN_BORDER_COLOR = PIN_COLOR.darker();
public static Color INDENT_COLOR = Color.gray.darker();
public static Color LABEL_COLOR = Color.white;
public static Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
public static int PIN_COUNT = 4;
public static Size CASE_SIZE = new Size(6d, SizeUnit.mm);
public static Size INDENT_SIZE = new Size(3d, SizeUnit.mm);
private static Size PIN_SPACING = new Size(4.5d, SizeUnit.mm);
private static Size ROW_SPACING = new Size(6.5d, SizeUnit.mm);
private static final Color BODY_COLOR = Color.lightGray;
private static final Color BORDER_COLOR = Color.gray.darker();
private static final Color PIN_COLOR = METAL_COLOR;
private static final Color PIN_BORDER_COLOR = PIN_COLOR.darker();
private static final Color INDENT_COLOR = Color.gray.darker();
private static final Color LABEL_COLOR = Color.white;
private static final Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
private static final Size CASE_SIZE = new Size(6d, SizeUnit.mm);
private static final Size PIN_SPACING = new Size(4.5d, SizeUnit.mm);
private static final Size ROW_SPACING = new Size(6.5d, SizeUnit.mm);
private String value = "";
private Orientation orientation = Orientation.DEFAULT;
@@ -80,6 +76,12 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
private Color labelColor = LABEL_COLOR;
transient private Area[] body;
private Size caseSize;
private Size pinSpacing;
private Size rowSpacing;
private PinPairingMode pinPairingMode;
private int selectedPosition;
public TactileMicroSwitch() {
super();
updateControlPoints();
@@ -146,10 +148,11 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
private void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
int PIN_COUNT = 4;
controlPoints = new Point2D[PIN_COUNT];
controlPoints[0] = firstPoint;
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
double pinSpacing = getPinSpacing().convertToPixels();
double rowSpacing = getRowSpacing().convertToPixels();
// Update control points.
double dx1;
double dy1;
@@ -198,8 +201,8 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
double y = controlPoints[0].getY();
double width;
double height;
double caseSize = CASE_SIZE.convertToPixels();
double indentSize = INDENT_SIZE.convertToPixels();
double caseSize = getCaseSize().convertToPixels();
double actuatorSize = caseSize / 2;
width = height = caseSize;
x = (controlPoints[0].getX() + controlPoints[3].getX() - width) / 2;
@@ -207,7 +210,7 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
body[0] = new Area(new Rectangle2D.Double(x, y, width, height));
width = height = indentSize;
width = height = actuatorSize;
x = (controlPoints[0].getX() + controlPoints[3].getX() - width) / 2;
y = (controlPoints[0].getY() + controlPoints[3].getY() - height) / 2;
@@ -244,7 +247,7 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : getBodyColor());
drawingObserver.stopTracking();
drawingObserver.startTracking();
g2d.fill(mainArea);
drawingObserver.stopTracking();
@@ -391,6 +394,59 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
this.labelColor = labelColor;
}
@EditableProperty(name = "Case Size")
public Size getCaseSize() {
if (caseSize == null) {
caseSize = CASE_SIZE;
}
return caseSize;
}
public void setCaseSize(Size caseSize) {
this.caseSize = caseSize;
this.body = null;
}
@EditableProperty(name = "Pin Spacing")
public Size getPinSpacing() {
if (pinSpacing == null) {
pinSpacing = PIN_SPACING;
}
return pinSpacing;
}
public void setPinSpacing(Size pinSpacing) {
this.pinSpacing = pinSpacing;
this.body = null;
updateControlPoints();
}
@EditableProperty(name = "Row Spacing")
public Size getRowSpacing() {
if (rowSpacing == null) {
rowSpacing = ROW_SPACING;
}
return rowSpacing;
}
public void setRowSpacing(Size rowSpacing) {
this.rowSpacing = rowSpacing;
this.body = null;
updateControlPoints();
}
@EditableProperty(name = "Pin Pairing Mode")
public PinPairingMode getPinPairingMode() {
if (pinPairingMode == null) {
pinPairingMode = PinPairingMode.Opposite;
}
return pinPairingMode;
}
public void setPinPairingMode(PinPairingMode pinPairingMode) {
this.pinPairingMode = pinPairingMode;
}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
@@ -418,4 +474,56 @@ public class TactileMicroSwitch extends AbstractLabeledComponent<String> impleme
return new Rectangle2D.Double(minX - margin, minY - margin, maxX - minX + 2 * margin,
maxY - minY + 2 * margin);
}
@Override
public int getPositionCount() {
return 2;
}
@Override
public String getPositionName(int position) {
return position == 0 ? "Released" : "Pressed";
}
@Override
public boolean arePointsConnected(int index1, int index2, int position) {
// Only connect pins when switch is pressed (position = 1)
if (position != 1) {
return false;
}
PinPairingMode mode = getPinPairingMode();
switch (mode) {
case Opposite:
// Pins on opposite sides (same row): 0-2 and 1-3
return (index1 == 0 && index2 == 2) || (index1 == 2 && index2 == 0) ||
(index1 == 1 && index2 == 3) || (index1 == 3 && index2 == 1);
case Diagonal:
// Diagonally opposite pins: 0-3 and 1-2
return (index1 == 0 && index2 == 3) || (index1 == 3 && index2 == 0) ||
(index1 == 1 && index2 == 2) || (index1 == 2 && index2 == 1);
case Adjacent:
// Pins next to each other (same column): 0-1 and 2-3
return (index1 == 0 && index2 == 1) || (index1 == 1 && index2 == 0) ||
(index1 == 2 && index2 == 3) || (index1 == 3 && index2 == 2);
default:
return false;
}
}
@DynamicEditableProperty(source = TactileMicroSwitchPositionPropertyValueSource.class)
@EditableProperty(name = "Selected Position")
@Override
public Integer getSelectedPosition() {
return selectedPosition;
}
public void setSelectedPosition(Integer selectedPosition) {
this.selectedPosition = selectedPosition;
}
public enum PinPairingMode {
Opposite, Diagonal, Adjacent;
}
}
@@ -0,0 +1,17 @@
package org.diylc.components.electromechanical;
import org.diylc.common.PropertyWrapper;
import org.diylc.components.guitar.LeverSwitch;
import org.diylc.core.AbstractDynamicPropertyValueSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class TactileMicroSwitchPositionPropertyValueSource extends AbstractDynamicPropertyValueSource {
@Override
public List<Object> getAvailableValues() {
return List.of("Released", "Pressed");
}
}
@@ -59,8 +59,8 @@ import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
import org.diylc.utils.SwitchUtils;
@ComponentDescriptor(name = "Freeway 3X3-03 Toggle", category = "Guitar",
author = "Branislav Stojkovic", description = "Freeway 3X3-03 Toggle Switch",
@ComponentDescriptor(name = "Freeway 3X3-03 Switch", category = "Guitar",
author = "Branislav Stojkovic", description = "Freeway 3X3-03 (Standard) Toggle Switch",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "SW",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram",
transformer = Freeway3x3_03Transformer.class, enableCache = true)
@@ -0,0 +1,493 @@
/*
*
* DIY Layout Creator (DIYLC). Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with DIYLC. If not, see
* <http://www.gnu.org/licenses/>.
*/
package org.diylc.components.guitar;
import com.kitfox.svg.SVGDiagram;
import com.kitfox.svg.SVGElement;
import com.kitfox.svg.SVGUniverse;
import com.kitfox.svg.ShapeElement;
import org.apache.log4j.Logger;
import org.apache.poi.util.IOUtils;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.awt.StringUtils;
import org.diylc.common.*;
import org.diylc.components.AbstractTransparentComponent;
import org.diylc.components.transform.Freeway3x3_03Transformer;
import org.diylc.core.*;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.DynamicEditableProperty;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static org.diylc.core.measures.SizeUnit.px;
@ComponentDescriptor(name = "Freeway 3X3-05 (Ultra) Switch", category = "Guitar",
author = "Martin Morrell", description = "Freeway 3X3-05 (Ultra) Toggle Switch",
zOrder = IDIYComponent.COMPONENT, instanceNamePrefix = "SW",
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Guitar Wiring Diagram",
transformer = Freeway3x3_03Transformer.class, enableCache = false)
public class Freeway3x3_05 extends AbstractTransparentComponent<Void> implements ISwitch {
private static final Logger LOG = Logger.getLogger(Freeway3x3_05.class);
private static final long serialVersionUID = 1L;
private static Color BASE_COLOR = FR4_COLOR;// Color.DARK_GRAY;
private static Color PAD_COLOR = COPPER_COLOR;
private static Color LABEL_COLOR = Color.WHITE;
private static Color CASE_COLOR = METAL_COLOR;
private static Color MARKER_COLOR = Color.lightGray;
private static final double DIAMETER_SWITCH = 34.0;
private static final double PAD_RADIUS = 15.3;
private static final double PAD_SIZE = 1.5;
private static final int NUM_CONTACTS = 28;
private static final double CONTACT_SPACING_RAD = Math.PI * (360.0 / NUM_CONTACTS) / 180.0;
private static final double CONTACT_SPACING_INITIAL = (CONTACT_SPACING_RAD * 14) + CONTACT_SPACING_RAD / 2;
private static final double GROUND_DIAMETER = 20;
private static final double CENTRE_LEN = 17;
private static final double CENTRE_WID = 10;
private static final double MARKER_OFFSET = 3.1;
private static final String[] PAD_NAMES = new String[] {
"1B", "2B", "3B", "6B", "5B", "4B", "CB", "TB", "6B", "5B", "4B", "1B", "2B", "3B",
"6A", "5A", "4A", "1A", "2A", "3A", "TA", "CA", "1A", "2A", "3A", "6A", "5A", "4A"
};
private static final AffineTransform SCALE_TX = AffineTransform.getScaleInstance(0.615d, 0.615d);
private Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
private Orientation orientation = Orientation.DEFAULT;
private Integer selectedPosition;
private Boolean showMarkers;
private transient SVGDiagram svgDiagram;
private transient List<Shape> pads;
private transient List<Shape> labels;
private transient Shape base;
private transient Shape caseShape;
private transient double[] xOffsetsPx;
private transient double[] yOffsetsPx;
public Freeway3x3_05() {
super();
updateControlPoints();
}
@Override
public String getControlPointNodeName(int index) {
return PAD_NAMES[index];
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
double x = controlPoints[0].getX();
double y = controlPoints[0].getY();
if (getOrientation() != Orientation.DEFAULT) {
double theta = orientation.toRadians();
g2d.rotate(theta, x, y);
}
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
double offsetX = getXOffsetsPx()[0];
double offsetY = getYOffsetsPx()[0];
//double origin = DIAMETER_SWITCH / 2;
int diameter = (int)(new Size(DIAMETER_SWITCH, SizeUnit.mm)).convertToPixels();
double originX = x - offsetX + diameter / 2.0;
double originY = y - offsetY + diameter / 2.0;
//Main circular shape
Color finalBorderColor;
if (outlineMode) {
Theme theme = (Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY,
Constants.DEFAULT_THEME);
finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: theme.getOutlineColor();
} else {
finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: BASE_COLOR.darker();
}
Shape base = new Ellipse2D.Double( originX, originY, diameter, diameter);
drawingObserver.startTracking();
g2d.setColor(BASE_COLOR);
g2d.fill(base);
drawingObserver.stopTracking();
g2d.setStroke(new BasicStroke(1f));
g2d.setColor(finalBorderColor);
g2d.draw(base);
//Ground circular shape
int gndDiameter = (int)(new Size(GROUND_DIAMETER, SizeUnit.mm)).convertToPixels();
int gndShift = (int)(new Size( (DIAMETER_SWITCH - GROUND_DIAMETER) / 2, SizeUnit.mm).convertToPixels());
Shape gnd = new Ellipse2D.Double( originX + gndShift, originY + gndShift, gndDiameter, gndDiameter);
g2d.setPaint(PAD_COLOR);
g2d.fill(gnd);
g2d.setColor(PAD_COLOR.darker());
g2d.draw(gnd);
//Centre rectangle casing block
int rectLen = (int)(new Size(CENTRE_LEN, SizeUnit.mm)).convertToPixels();
int rectWid = (int)(new Size(CENTRE_WID, SizeUnit.mm)).convertToPixels();
int rectShiftX = (int)(new Size( (DIAMETER_SWITCH - CENTRE_WID) / 2, SizeUnit.mm).convertToPixels());
int rectShiftY = (int)(new Size( (DIAMETER_SWITCH - CENTRE_LEN) / 2, SizeUnit.mm).convertToPixels());
int rectangleArc = (int)(new Size(1.0, SizeUnit.mm)).convertToPixels();
Shape casing = new RoundRectangle2D.Double(originX + rectShiftX, originY + rectShiftY, rectWid, rectLen, rectangleArc, rectangleArc);
g2d.setPaint(CASE_COLOR);
g2d.fill(casing);
g2d.setColor(CASE_COLOR.darker());
g2d.draw(casing);
//Pads
double circleShift = (int)(new Size( DIAMETER_SWITCH / 2, SizeUnit.mm).convertToPixels());
double circleCentreX = originX + circleShift;
double circleCentreY = originY + circleShift;
double radChange = CONTACT_SPACING_RAD * 0.45;
double extendedRadius = (int)new Size(PAD_RADIUS + PAD_SIZE, SizeUnit.mm).convertToPixels();
double reducedRadius = (int)new Size(PAD_RADIUS - PAD_SIZE, SizeUnit.mm).convertToPixels();
for (int i = 0; i < NUM_CONTACTS; i++) {
double rad = ((i + 0.5) * CONTACT_SPACING_RAD);
double rotExX = Math.cos(rad) * (extendedRadius * 1.01) + circleCentreX;
double rotExY = Math.sin(rad) * (extendedRadius * 1.01) + circleCentreY;
double rotExPosX = Math.cos(rad + radChange) * extendedRadius + circleCentreX;
double rotExPosY = Math.sin(rad + radChange) * extendedRadius + circleCentreY;
double rotExNegX = Math.cos(rad - radChange) * extendedRadius + circleCentreX;
double rotExNegY = Math.sin(rad - radChange) * extendedRadius + circleCentreY;
double rotReX = Math.cos(rad) * (reducedRadius * 1.01) + circleCentreX;
double rotReY = Math.sin(rad) * (reducedRadius * 1.01) + circleCentreY;
double rotRePosX = Math.cos(rad + radChange) * reducedRadius + circleCentreX;
double rotRePosY = Math.sin(rad + radChange) * reducedRadius + circleCentreY;
double rotReNegX = Math.cos(rad - radChange) * reducedRadius + circleCentreX;
double rotReNegY = Math.sin(rad - radChange) * reducedRadius + circleCentreY;
Path2D pad = new Path2D.Double();
pad.moveTo(rotExNegX, rotExNegY);
pad.quadTo(rotExX, rotExY, rotExPosX, rotExPosY);
pad.lineTo(rotRePosX, rotRePosY);
pad.quadTo(rotReX, rotReY,rotReNegX, rotReNegY);
pad.lineTo(rotExNegX, rotExNegY);
pad.closePath();
g2d.setPaint(PAD_COLOR);
g2d.fill(pad);
g2d.setColor(PAD_COLOR.darker());
g2d.draw(pad);
}
//Labels
double offset = (int) new Size(MARKER_OFFSET, SizeUnit.mm).convertToPixels();
for (int i = 0; i < NUM_CONTACTS; i++) {
Point2D p = getControlPoint(i);
double px = Math.sin(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS - MARKER_OFFSET);
double py = Math.cos(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS - MARKER_OFFSET);
g2d.setPaint(LABEL_COLOR);
StringUtils.drawCenteredText(g2d, PAD_NAMES[i],
(new Size(px + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originX,
(new Size(py + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
//Add other text
int makeLen = (int)(new Size(12.0, SizeUnit.mm)).convertToPixels();
int modelLen = (int)(new Size(8.0, SizeUnit.mm)).convertToPixels();
double mWidth = 1.8;
int modelWid = (int)(new Size(mWidth, SizeUnit.mm)).convertToPixels();
int makeShiftX = (int)(new Size( (DIAMETER_SWITCH / 2) - 7.25, SizeUnit.mm).convertToPixels());
int makeShiftY = (int)(new Size( (DIAMETER_SWITCH / 2) - 6.0, SizeUnit.mm).convertToPixels());
int modelShiftX = (int)(new Size( (DIAMETER_SWITCH / 2) + 5.25, SizeUnit.mm).convertToPixels());
int modelShiftY = (int)(new Size( (DIAMETER_SWITCH / 2) - 4.0, SizeUnit.mm).convertToPixels());
int modelArc = (int)(new Size(mWidth, SizeUnit.mm)).convertToPixels();
Shape make = new RoundRectangle2D.Double(originX + makeShiftX, originY + makeShiftY, modelWid, makeLen, modelArc, modelArc);
Shape model = new RoundRectangle2D.Double(originX + modelShiftX, originY + modelShiftY, modelWid, modelLen, modelArc, modelArc);
g2d.setPaint(BASE_COLOR);
g2d.fill(make);
g2d.fill(model);
g2d.setColor(PAD_COLOR.darker());
g2d.draw(make);
g2d.draw(model);
/*String makeTxt = "FREE-WAY·ULTRA";
String modelTxt = "3X3-05";
g2d.setPaint(LABEL_COLOR);
StringUtils.drawCenteredText(g2d, makeTxt,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originX + makeShiftX,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originY + makeShiftY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, modelTxt,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originX + modelShiftX,
(new Size(0.0, SizeUnit.mm).convertToPixels()) + originY + modelShiftY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);*/
if (getShowMarkers()) {
g2d.setColor(MARKER_COLOR);
for (int i = 0; i < getControlPointCount(); i++) {
Point2D p = getControlPoint(i);
Point2D labelPoint = new Point2D.Double(offset, 0);
double px = Math.sin(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS + MARKER_OFFSET);
double py = Math.cos(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * (PAD_RADIUS + MARKER_OFFSET);
String marker = PAD_NAMES[i];
if(marker.equals("CA")) {
marker = "A";
} else if (marker.equals("CB")) {
marker = "B";
} else if (marker.startsWith("T")) {
continue;
} else {
marker = marker.substring(0, 1);
}
StringUtils.drawCenteredText(g2d, marker,
(new Size(px + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originX,
(new Size(py + DIAMETER_SWITCH / 2.0, SizeUnit.mm).convertToPixels()) + originY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
}
g2d.setComposite(oldComposite);
}
public double[] getXOffsetsPx() {
if (xOffsetsPx == null) {
xOffsetsPx = new double[NUM_CONTACTS];
for (int i = 0; i < NUM_CONTACTS; i++) {
double x = Math.sin(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * PAD_RADIUS;
xOffsetsPx[i] = new Size(x + DIAMETER_SWITCH, SizeUnit.mm).convertToPixels();
}
}
return xOffsetsPx;
}
public double[] getYOffsetsPx() {
if (yOffsetsPx == null) {
yOffsetsPx = new double[NUM_CONTACTS];
for (int i = 0; i < NUM_CONTACTS; i++) {
double y = Math.cos(CONTACT_SPACING_INITIAL + i * CONTACT_SPACING_RAD) * PAD_RADIUS;
yOffsetsPx[i] = new Size(y + DIAMETER_SWITCH , SizeUnit.mm).convertToPixels();
}
}
return yOffsetsPx;
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
Shape base = new Ellipse2D.Double( 0, 0, width, height);
g2d.setColor(BASE_COLOR);
g2d.fill(base);
double contactOffset = 1;
Shape contacts = new Ellipse2D.Double( contactOffset, contactOffset, width - contactOffset * 2, height - contactOffset * 2);
g2d.setColor(PAD_COLOR);
g2d.fill(contacts);
double centreOffset = 4;
Shape centre = new Ellipse2D.Double( centreOffset, centreOffset, width - centreOffset * 2, height - centreOffset * 2);
g2d.setColor(BASE_COLOR);
g2d.fill(centre);
double gndOffset = 9;
Shape gnd = new Ellipse2D.Double( gndOffset, gndOffset, width - gndOffset * 2, height - gndOffset * 2);
g2d.setColor(PAD_COLOR);
g2d.fill(gnd);
double arc = 2.0;
Shape casing = new RoundRectangle2D.Double(12, 10, 8, 12, arc, arc);
g2d.setColor(CASE_COLOR);
g2d.fill(casing);
}
@Override
public int getControlPointCount() {
return NUM_CONTACTS;
}
@Override
public Point2D getControlPoint(int index) {
return controlPoints[index];
}
private void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
controlPoints = new Point2D[NUM_CONTACTS];
controlPoints[0] = firstPoint;
double[] xOffsetsPx = getXOffsetsPx();
double[] yOffsetsPx = getYOffsetsPx();
double theta = orientation.toRadians();
AffineTransform tx =
AffineTransform.getRotateInstance(theta, firstPoint.getX(), firstPoint.getY());
for (int i = 1; i < controlPoints.length; i++) {
controlPoints[i] = new Point2D.Double(firstPoint.getX() + (xOffsetsPx[i] - xOffsetsPx[0]),
firstPoint.getY() + (yOffsetsPx[i] - yOffsetsPx[0]));
tx.transform(controlPoints[i], controlPoints[i]);
}
}
@Override
public boolean isControlPointSticky(int index) {
return true;
}
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.ALWAYS;
}
@Override
public void setControlPoint(Point2D point, int index) {
controlPoints[index].setLocation(point);
}
@Override
public Void getValue() {
return null;
}
@Override
public void setValue(Void value) {}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
}
@EditableProperty
public Orientation getOrientation() {
if (orientation == null)
orientation = Orientation.DEFAULT;
return orientation;
}
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
updateControlPoints();
}
@Override
public int getPositionCount() {
return 6;
}
@Override
public String getPositionName(int position) {
return Integer.toString(position + 1);
}
@DynamicEditableProperty(source = Freeway3x4_03SwitchPositionPropertyValueSource.class)
@EditableProperty(name = "Selected Position")
@Override
public Integer getSelectedPosition() {
return selectedPosition;
}
public void setSelectedPosition(Integer selectedPosition) {
this.selectedPosition = selectedPosition;
}
@EditableProperty(name = "Markers")
public Boolean getShowMarkers() {
if (showMarkers == null) {
showMarkers = false;
}
return showMarkers;
}
public void setShowMarkers(Boolean showMarkers) {
this.showMarkers = showMarkers;
}
@Override
public boolean arePointsConnected(int index1, int index2, int position) {
if (position == 0 &&
((index1 == 0 && index2 == 6) || (index1 == 6 && index2 == 11) || (index1 == 0 && index2 == 11) ||
(index1 == 17 && index2 == 21) || (index1 == 21 && index2 == 22) || (index1 == 17 && index2 == 22))
) {
return true;
}
if (position == 1 &&
((index1 == 1 && index2 == 6) || (index1 == 6 && index2 == 12) || (index1 == 1 && index2 == 12) ||
(index1 == 18 && index2 == 21) || (index1 == 21 && index2 == 23) || (index1 == 18 && index2 == 23))
) {
return true;
}
if (position == 2 &&
((index1 == 2 && index2 == 6) || (index1 == 6 && index2 == 13) || (index1 == 2 && index2 == 13) ||
(index1 == 19 && index2 == 21) || (index1 == 21 && index2 == 24) || (index1 == 19 && index2 == 24))
) {
return true;
}
if (position == 3 &&
((index1 == 5 && index2 == 6) || (index1 == 6 && index2 == 10) || (index1 == 5 && index2 == 10) ||
(index1 == 16 && index2 == 21) || (index1 == 21 && index2 == 27) || (index1 == 16 && index2 == 27))
) {
return true;
}
if (position == 4 &&
((index1 == 4 && index2 == 6) || (index1 == 6 && index2 == 9) || (index1 == 4 && index2 == 9) ||
(index1 == 15 && index2 == 21) || (index1 == 21 && index2 == 26) || (index1 == 15 && index2 == 26))
) {
return true;
}
if (position == 5 &&
((index1 == 3 && index2 == 6) || (index1 == 6 && index2 == 8) || (index1 == 3 && index2 == 8) ||
(index1 == 14 && index2 == 21) || (index1 == 21 && index2 == 25) || (index1 == 14 && index2 == 25))
) {
return true;
}
return false;
}
}
@@ -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> {
@@ -35,9 +35,12 @@ import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.awt.StringUtils;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.transform.PotentiometerTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.CreationMethod;
@@ -66,10 +69,12 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
protected static Size PIN_SIZE = new Size(0.05d, SizeUnit.in);
protected static Size NUT_SIZE = new Size(0.4d, SizeUnit.in);
protected static Size SHAFT_SIZE = new Size(1 / 4d, SizeUnit.in);
protected static Size MARKER_OFFSET = new Size(0.05d, SizeUnit.in);
protected static Color BODY_COLOR = Color.lightGray;
protected static Color WAFER_COLOR = Color.decode("#CD8500");
protected static Color BORDER_COLOR = Color.gray;
protected static Color NUT_COLOR = Color.decode("#CBD5DB");
protected static Color MARKER_COLOR_DEFAULT = Color.gray;
protected Size bodyDiameter = BODY_DIAMETER;
@@ -83,6 +88,8 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
@Deprecated
protected boolean showShaft = false;
protected View view;
private Boolean showMarkers;
private Color markerColor;
// Array of 7 elements: 3 lug connectors, 1 pot body and 3 lugs
transient protected Area[] body = null;
@@ -353,6 +360,50 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
y = panelHeight / 2 + fontMetrics.getAscent();
g2d.drawString(getValueForDisplay(), (int) (bodyRect.getX() + x), (int) (bodyRect.getY() + y));
// Draw markers if enabled.
if (getShowMarkers()) {
g2d.setColor(outlineMode ? theme.getOutlineColor() : getMarkerColor());
// Get circle center and radius
Rectangle2D circleBounds = body[3].getBounds2D();
double centerX = circleBounds.getCenterX();
double centerY = circleBounds.getCenterY();
double radius = getBodyDiameter().convertToPixels() / 2;
// Fixed distance from circle edge (inside the circle)
double markerOffset = MARKER_OFFSET.convertToPixels() + 2;
double markerDistance = radius - markerOffset;
// Draw markers "1", "2", "3" along radial line from center to each lug, close to circle edge
// Reverse numbering when view is ShaftDown
boolean reverseOrder = getView() == View.ShaftDown;
for (int i = 0; i < 3; i++) {
Point2D lugPoint = controlPoints[i];
// Calculate direction from center to lug
double dx = lugPoint.getX() - centerX;
double dy = lugPoint.getY() - centerY;
double distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction vector
double unitX = dx / distance;
double unitY = dy / distance;
// Position marker at fixed distance from circle edge along radial line
double markerX = centerX + unitX * markerDistance;
double markerY = centerY + unitY * markerDistance;
// Determine marker number based on view
int markerNumber = reverseOrder ? (3 - i) : (i + 1);
StringUtils.drawCenteredText(g2d, Integer.toString(markerNumber), markerX, markerY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
}
}
}
@Override
@@ -474,6 +525,30 @@ public class PotentiometerPanel extends AbstractPotentiometer implements ILayere
public void setWaferColor(Color waferColor) {
this.waferColor = waferColor;
}
@EditableProperty(name = "Markers")
public Boolean getShowMarkers() {
if (showMarkers == null) {
showMarkers = false;
}
return showMarkers;
}
public void setShowMarkers(Boolean showMarkers) {
this.showMarkers = showMarkers;
}
@EditableProperty(name = "Marker Color")
public Color getMarkerColor() {
if (markerColor == null) {
markerColor = MARKER_COLOR_DEFAULT;
}
return markerColor;
}
public void setMarkerColor(Color markerColor) {
this.markerColor = markerColor;
}
@Override
public String getInternalLinkName(int index1, int index2) {
@@ -157,6 +157,6 @@ public class TransformerCore extends AbstractComponent<Void> {
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
return true;
}
}
@@ -0,0 +1,693 @@
/*
DIY Layout Creator (DIYLC).
Copyright (c) 2009-2025 held jointly by the individual authors.
This file is part of DIYLC.
DIYLC is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
DIYLC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.components.semiconductors;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.Display;
import org.diylc.common.IPlugInPort;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractTransparentComponent;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.Theme;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.gerber.IGerberComponentSimple;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Multiwatt IC", author = "Branislav Stojkovic", category = "Semiconductors",
instanceNamePrefix = "IC", description = "Multiwatt package IC typically used for audio amplifiers of high power applications",
zOrder = IDIYComponent.COMPONENT, keywordPolicy = KeywordPolicy.SHOW_VALUE,
enableCache = true)
public class MultiwattIC extends AbstractTransparentComponent<String> implements IGerberComponentSimple {
private static final long serialVersionUID = 1L;
public static Color BODY_COLOR = Color.gray;
public static Color BORDER_COLOR = Color.gray.darker();
public static Color PIN_COLOR = Color.decode("#00B2EE");
public static Color PIN_BORDER_COLOR = PIN_COLOR.darker();
public static Color LABEL_COLOR = Color.white;
public static Color TAB_COLOR = Color.lightGray;
public static Color TAB_BORDER_COLOR = TAB_COLOR.darker();
public static int EDGE_RADIUS = 6;
public static Size PIN_SIZE = new Size(0.7d, SizeUnit.mm);
public static Size THICKNESS = new Size(5d, SizeUnit.mm);
public static Size TAB_THICKNESS = new Size(1.6d, SizeUnit.mm);
// Multiwatt packages use 0.1 inch (2.54mm) spacing between pins in the same row
public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in);
// Multiwatt packages have staggered pins with 0.2 inch (5.08mm) row spacing
public static Size ROW_SPACING = new Size(0.2d, SizeUnit.in);
// Lead offset due to bent leads
public static Size LEAD_OFFSET = new Size(4d, SizeUnit.mm);
private String value = "";
private Orientation orientation = Orientation.DEFAULT;
private MultiwattType type = MultiwattType.MULTIWATT_11;
private Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
protected Display display = Display.NAME;
private Color bodyColor = BODY_COLOR;
private Color borderColor = BORDER_COLOR;
private Color labelColor = LABEL_COLOR;
private Color tabColor = TAB_COLOR;
private Color tabBorderColor = TAB_BORDER_COLOR;
transient private Area[] body;
public MultiwattIC() {
super();
updateControlPoints();
alpha = 100;
}
@EditableProperty
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@EditableProperty
public Orientation getOrientation() {
return orientation;
}
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
updateControlPoints();
// Reset body shape.
body = null;
}
@EditableProperty(name = "Type")
public MultiwattType getType() {
return type;
}
public void setType(MultiwattType type) {
this.type = type;
updateControlPoints();
// Reset body shape;
body = null;
}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
}
@EditableProperty
public Display getDisplay() {
if (display == null) {
display = Display.VALUE;
}
return display;
}
public void setDisplay(Display display) {
this.display = display;
}
@Override
public int getControlPointCount() {
return controlPoints.length;
}
@Override
public Point2D getControlPoint(int index) {
return controlPoints[index];
}
@Override
public boolean isControlPointSticky(int index) {
return true;
}
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.NEVER;
}
@Override
public void setControlPoint(Point2D point, int index) {
controlPoints[index].setLocation(point);
body = null;
}
private void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
int pinCount = type.getPinCount();
int pinsLeft = type.getPinsLeft();
int pinsRight = type.getPinsRight();
controlPoints = new Point2D.Double[pinCount];
controlPoints[0] = firstPoint;
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
// Update control points for staggered layout
double dx1, dy1, dx2, dy2;
// Left side pins (first row)
for (int i = 0; i < pinsLeft; i++) {
switch (orientation) {
case DEFAULT:
dx1 = 0;
dy1 = i * pinSpacing;
break;
case _90:
dx1 = -i * pinSpacing;
dy1 = 0;
break;
case _180:
dx1 = 0;
dy1 = -i * pinSpacing;
break;
case _270:
dx1 = i * pinSpacing;
dy1 = 0;
break;
default:
throw new RuntimeException("Unexpected orientation: " + orientation);
}
controlPoints[i] = new Point2D.Double((int) (firstPoint.getX() + dx1), (int) (firstPoint.getY() + dy1));
}
// Right side pins (second row, staggered - offset by half pin spacing)
for (int i = 0; i < pinsRight; i++) {
switch (orientation) {
case DEFAULT:
dx2 = rowSpacing;
dy2 = (i + 0.5) * pinSpacing; // Offset by half spacing for staggered effect
break;
case _90:
dx2 = -(i + 0.5) * pinSpacing; // Offset by half spacing for staggered effect
dy2 = rowSpacing;
break;
case _180:
dx2 = -rowSpacing;
dy2 = -(i + 0.5) * pinSpacing; // Offset by half spacing for staggered effect
break;
case _270:
dx2 = (i + 0.5) * pinSpacing; // Offset by half spacing for staggered effect
dy2 = -rowSpacing;
break;
default:
throw new RuntimeException("Unexpected orientation: " + orientation);
}
controlPoints[i + pinsLeft] = new Point2D.Double((int) (firstPoint.getX() + dx2), (int) (firstPoint.getY() + dy2));
}
}
public Area[] getBody() {
if (body == null) {
body = new Area[2];
double x = controlPoints[0].getX();
double y = controlPoints[0].getY();
double pinSize = PIN_SIZE.convertToPixels();
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
double tabThickness = TAB_THICKNESS.convertToPixels();
double leadOffset = LEAD_OFFSET.convertToPixels();
int pinsLeft = type.getPinsLeft();
int pinsRight = type.getPinsRight();
// Calculate height accounting for staggered pins (right row offset by half spacing)
double leftRowHeight = (pinsLeft - 1) * pinSpacing;
double rightRowHeight = (pinsRight - 1 + 0.5) * pinSpacing; // Account for half-spacing offset
double maxHeight = Math.max(leftRowHeight, rightRowHeight) + pinSpacing; // Add spacing for last pin
double bodyX, bodyY, bodyWidth, bodyHeight;
double tabX, tabY, tabWidth, tabHeight;
switch (orientation) {
case DEFAULT:
bodyWidth = rowSpacing - pinSize;
bodyHeight = maxHeight;
// Offset body to the left by lead offset (bent leads)
bodyX = x + pinSize / 2 - leadOffset;
bodyY = y - pinSpacing / 2;
// Tab on the left side (where there are more pins) inside the body bounds
tabX = bodyX;
tabY = bodyY;
tabWidth = tabThickness;
tabHeight = bodyHeight;
break;
case _90:
bodyWidth = maxHeight;
bodyHeight = rowSpacing - pinSize;
// Body positioned between the two pin rows
// Left pins extend to negative X, body starts from leftmost pin
// For _90: left pins go left (negative X), so leftmost is at x - (pinsLeft-1)*pinSpacing
// Offset body upward by lead offset (bent leads)
bodyX = x - (pinsLeft - 1) * pinSpacing - pinSpacing / 2;
bodyY = y + pinSize / 2 - leadOffset;
// Tab on the top side (where there are more pins - left row at y) inside the body bounds
tabX = bodyX;
tabY = bodyY;
tabWidth = bodyWidth;
tabHeight = tabThickness;
break;
case _180:
bodyWidth = rowSpacing - pinSize;
bodyHeight = maxHeight;
// Body positioned between the two pin rows
// For _180: left pins go up (negative Y), right pins at x - rowSpacing
// Left row is at x, right row is at x - rowSpacing
// Pins extend upward from y, so body should start from top of pin array
// Offset body to the right by lead offset (bent leads)
bodyX = x - rowSpacing + pinSize / 2 + leadOffset;
bodyY = y - (pinsLeft - 1) * pinSpacing - pinSpacing / 2;
// Tab on the right side (where there are more pins - left row at x) inside the body bounds
tabX = bodyX + bodyWidth - tabThickness;
tabY = bodyY;
tabWidth = tabThickness;
tabHeight = bodyHeight;
break;
case _270:
bodyWidth = maxHeight;
bodyHeight = rowSpacing - pinSize;
// Body positioned between the two pin rows
// For _270: left pins go right (positive X), so leftmost is at x
// Left row is at y, right row is at y - rowSpacing
// Similar to _90 but mirrored
// Offset body downward by lead offset (bent leads)
bodyX = x - pinSpacing / 2;
bodyY = y - rowSpacing + pinSize / 2 + leadOffset;
// Tab on the bottom side (where there are more pins - left row at y) inside the body bounds
tabX = bodyX;
tabY = bodyY + bodyHeight - tabThickness;
tabWidth = bodyWidth;
tabHeight = tabThickness;
break;
default:
throw new RuntimeException("Unexpected orientation: " + orientation);
}
body[0] = new Area(new Rectangle2D.Double(bodyX, bodyY, bodyWidth, bodyHeight));
body[1] = new Area(new Rectangle2D.Double(tabX, tabY, tabWidth, tabHeight));
}
return body;
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Area mainArea = getBody()[0];
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
if (!outlineMode) {
int pinSize = (int) PIN_SIZE.convertToPixels() / 2 * 2;
Rectangle2D bodyBounds = mainArea.getBounds2D();
double leadThickness = pinSize; // Lead width matches pin size
for (Point2D point : controlPoints) {
double pinX = point.getX();
double pinY = point.getY();
// Check if pin is inside the body
boolean insideBody = mainArea.contains(pinX, pinY);
if (insideBody) {
// Draw pin as square inside body
Rectangle2D rect = new Rectangle2D.Double(pinX - pinSize / 2,
pinY - pinSize / 2, pinSize, pinSize);
g2d.setColor(PIN_COLOR);
drawingObserver.startTracking();
g2d.fill(rect);
drawingObserver.stopTracking();
g2d.setColor(PIN_BORDER_COLOR);
g2d.draw(rect);
} else {
// Draw rectangular lead from body edge to pin position
double bodyX = bodyBounds.getX();
double bodyY = bodyBounds.getY();
double bodyWidth = bodyBounds.getWidth();
double bodyHeight = bodyBounds.getHeight();
// Find the closest point on body edge to the pin
double edgeX, edgeY;
// Determine which side of the body the pin is on
if (pinX < bodyX) {
// Pin is to the left of body
edgeX = bodyX;
edgeY = Math.max(bodyY, Math.min(bodyY + bodyHeight, pinY));
} else if (pinX > bodyX + bodyWidth) {
// Pin is to the right of body
edgeX = bodyX + bodyWidth;
edgeY = Math.max(bodyY, Math.min(bodyY + bodyHeight, pinY));
} else if (pinY < bodyY) {
// Pin is above body
edgeX = Math.max(bodyX, Math.min(bodyX + bodyWidth, pinX));
edgeY = bodyY;
} else {
// Pin is below body
edgeX = Math.max(bodyX, Math.min(bodyX + bodyWidth, pinX));
edgeY = bodyY + bodyHeight;
}
// Draw rectangular lead
double leadX = Math.min(edgeX, pinX) - leadThickness / 2;
double leadY = Math.min(edgeY, pinY) - leadThickness / 2;
double leadWidth = Math.abs(pinX - edgeX) + leadThickness;
double leadHeight = Math.abs(pinY - edgeY) + leadThickness;
// If lead is horizontal or vertical, draw as rectangle
if (Math.abs(pinX - edgeX) < 0.1) {
// Vertical lead
leadWidth = leadThickness;
leadX = edgeX - leadThickness / 2;
} else if (Math.abs(pinY - edgeY) < 0.1) {
// Horizontal lead
leadHeight = leadThickness;
leadY = edgeY - leadThickness / 2;
}
Rectangle2D leadRect = new Rectangle2D.Double(leadX, leadY, leadWidth, leadHeight);
g2d.setColor(PIN_COLOR);
drawingObserver.startTracking();
g2d.fill(leadRect);
drawingObserver.stopTracking();
g2d.setColor(PIN_BORDER_COLOR);
g2d.draw(leadRect);
// Draw pin at the end of the lead
Rectangle2D pinRect = new Rectangle2D.Double(pinX - pinSize / 2,
pinY - pinSize / 2, pinSize, pinSize);
g2d.setColor(PIN_COLOR);
drawingObserver.startTracking();
g2d.fill(pinRect);
drawingObserver.stopTracking();
g2d.setColor(PIN_BORDER_COLOR);
g2d.draw(pinRect);
}
}
}
drawingObserver.startTracking();
Composite oldComposite = g2d.getComposite();
if (alpha < MAX_ALPHA) {
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f * alpha / MAX_ALPHA));
}
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : getBodyColor());
g2d.fill(mainArea);
drawingObserver.stopTracking();
// Draw heat sink tab
Area tabArea = getBody()[1];
Color finalTabColor;
if (outlineMode) {
Theme theme =
(Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME);
finalTabColor = theme.getOutlineColor();
} else {
finalTabColor = tabColor;
}
g2d.setColor(finalTabColor);
drawingObserver.startTracking();
g2d.fill(tabArea);
drawingObserver.stopTracking();
g2d.setComposite(oldComposite);
if (!outlineMode) {
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.setColor(tabBorderColor);
g2d.draw(tabArea);
}
Color finalBorderColor;
if (outlineMode) {
Theme theme =
(Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME);
finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
: theme.getOutlineColor();
} else {
finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
: getBorderColor();
}
g2d.setColor(finalBorderColor);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(mainArea);
// Draw label.
g2d.setFont(project.getFont());
Color finalLabelColor;
if (outlineMode) {
Theme theme =
(Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME);
finalLabelColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? LABEL_COLOR_SELECTED
: theme.getOutlineColor();
} else {
finalLabelColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? LABEL_COLOR_SELECTED
: getLabelColor();
}
g2d.setColor(finalLabelColor);
FontMetrics fontMetrics = g2d.getFontMetrics(g2d.getFont());
String label = "";
label = (getDisplay() == Display.NAME) ? getName() : getValue();
if (getDisplay() == Display.NONE) {
label = "";
}
if (getDisplay() == Display.BOTH) {
label = getName() + " " + (getValue() == null ? "" : getValue().toString());
}
Rectangle2D rect = fontMetrics.getStringBounds(label, g2d);
int textHeight = (int) (rect.getHeight());
int textWidth = (int) (rect.getWidth());
// Center text horizontally and vertically
Rectangle bounds = mainArea.getBounds();
int x = (int) (bounds.getX() + (bounds.width - textWidth) / 2);
int y = (int) (bounds.getY() + (bounds.height - textHeight) / 2 + fontMetrics.getAscent());
g2d.drawString(label, x, y);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
int margin = 2;
int pinSize = 2 * width / 32;
int leftPinX = width / 6 + 9; // Move left pins 9px to the right (4px + 3px + 2px)
int rightPinX = 5 * width / 6;
int leadOffset = (int) (LEAD_OFFSET.convertToPixels() * width / 100); // Scale lead offset for icon
int bodyX = leftPinX + pinSize - leadOffset + 2; // Offset body to the left, then move 2px to the right
int bodyWidth = (rightPinX - bodyX - pinSize) / 2 + 2; // Thinner body, make 2px thicker
int bodyHeight = height - 2 * margin;
int bodyY = margin;
// Calculate pin spacing to use all vertical space
// Use same spacing for both rows, with front row offset by half spacing
int backPins = 4;
int frontPins = 3;
// Use the spacing that fits the back row (more pins)
int pinSpacing = (bodyHeight - pinSize) / (backPins - 1);
int backStartY = margin + pinSize / 2 + 1; // Move pins 1px down
// Front pins start with half spacing offset for staggered effect
int frontStartY = margin + pinSize / 2 + pinSpacing / 2 + 1; // Move pins 1px down
// Draw thin body between pin rows
g2d.setColor(BODY_COLOR);
g2d.fillRect(bodyX, bodyY, bodyWidth, bodyHeight);
g2d.setColor(BORDER_COLOR);
g2d.drawRect(bodyX, bodyY, bodyWidth, bodyHeight);
// Draw heat sink tab on left side (where there are more pins)
int tabWidth = pinSize * 2;
g2d.setColor(TAB_COLOR);
g2d.fillRect(bodyX, bodyY, tabWidth, bodyHeight);
g2d.setColor(TAB_BORDER_COLOR);
g2d.drawRect(bodyX, bodyY, tabWidth, bodyHeight);
// Draw 4 pins in the back (left row, more pins) as squares - inside body
g2d.setColor(PIN_COLOR);
for (int i = 0; i < backPins; i++) {
int pinY = backStartY + i * pinSpacing - pinSize / 2;
g2d.fillRect(leftPinX - pinSize, pinY, pinSize, pinSize);
g2d.setColor(PIN_BORDER_COLOR);
g2d.drawRect(leftPinX - pinSize, pinY, pinSize, pinSize);
g2d.setColor(PIN_COLOR);
}
// Draw 3 pins in the front (right row, fewer pins) with leads going left to body
// Use same spacing as back row, just offset by half spacing
g2d.setColor(PIN_COLOR);
for (int i = 0; i < frontPins; i++) {
int pinY = frontStartY + i * pinSpacing;
int pinX = rightPinX;
int bodyRightEdge = bodyX + bodyWidth;
// Draw lead from body edge to pin (horizontal lead going left)
if (pinX > bodyRightEdge) {
int leadX = bodyRightEdge;
int leadY = pinY - pinSize / 2;
int leadWidth = pinX - bodyRightEdge;
int leadHeight = pinSize;
g2d.fillRect(leadX, leadY, leadWidth, leadHeight);
g2d.setColor(PIN_BORDER_COLOR);
g2d.drawRect(leadX, leadY, leadWidth, leadHeight);
g2d.setColor(PIN_COLOR);
}
// Draw pin at the end of the lead
g2d.fillRect(pinX - pinSize / 2, pinY - pinSize / 2, pinSize, pinSize);
g2d.setColor(PIN_BORDER_COLOR);
g2d.drawRect(pinX - pinSize / 2, pinY - pinSize / 2, pinSize, pinSize);
g2d.setColor(PIN_COLOR);
}
}
@EditableProperty(name = "Body")
public Color getBodyColor() {
if (bodyColor == null) {
bodyColor = BODY_COLOR;
}
return bodyColor;
}
public void setBodyColor(Color bodyColor) {
this.bodyColor = bodyColor;
}
@EditableProperty(name = "Border")
public Color getBorderColor() {
if (borderColor == null) {
borderColor = BORDER_COLOR;
}
return borderColor;
}
public void setBorderColor(Color borderColor) {
this.borderColor = borderColor;
}
@EditableProperty(name = "Label")
public Color getLabelColor() {
if (labelColor == null) {
labelColor = LABEL_COLOR;
}
return labelColor;
}
public void setLabelColor(Color labelColor) {
this.labelColor = labelColor;
}
@EditableProperty(name = "Tab")
public Color getTabColor() {
if (tabColor == null) {
tabColor = TAB_COLOR;
}
return tabColor;
}
public void setTabColor(Color tabColor) {
this.tabColor = tabColor;
}
@EditableProperty(name = "Tab Border")
public Color getTabBorderColor() {
if (tabBorderColor == null) {
tabBorderColor = TAB_BORDER_COLOR;
}
return tabBorderColor;
}
public void setTabBorderColor(Color tabBorderColor) {
this.tabBorderColor = tabBorderColor;
}
@Override
public Rectangle2D getCachingBounds() {
double minX = Integer.MAX_VALUE;
double maxX = Integer.MIN_VALUE;
double minY = Integer.MAX_VALUE;
double maxY = Integer.MIN_VALUE;
int margin = 50;
for (int i = 0; i < getControlPointCount(); i++) {
Point2D p = getControlPoint(i);
if (p.getX() < minX)
minX = p.getX();
if (p.getX() > maxX)
maxX = p.getX();
if (p.getY() < minY)
minY = p.getY();
if (p.getY() > maxY)
maxY = p.getY();
}
return new Rectangle2D.Double(minX - margin, minY - margin, maxX - minX + 2 * margin, maxY - minY + 2 * margin);
}
public static enum MultiwattType {
// Multiwatt-11: 6 pins on left, 5 pins on right (staggered)
MULTIWATT_11(11, 6, 5),
// Multiwatt-15: 8 pins on left, 7 pins on right (staggered)
MULTIWATT_15(15, 8, 7);
private final int pinCount;
private final int pinsLeft;
private final int pinsRight;
MultiwattType(int pinCount, int pinsLeft, int pinsRight) {
this.pinCount = pinCount;
this.pinsLeft = pinsLeft;
this.pinsRight = pinsRight;
}
public int getPinCount() {
return pinCount;
}
public int getPinsLeft() {
return pinsLeft;
}
public int getPinsRight() {
return pinsRight;
}
@Override
public String toString() {
return "Multiwatt " + pinCount;
}
}
}
@@ -77,7 +77,7 @@ public class TapeMeasure extends AbstractLeadedComponent<String> {
g2d.setColor(COLOR);
g2d.drawLine(1, height - 2, width / 4, height * 3 / 4 - 1);
g2d.drawLine(width * 3 / 4, height / 4 - 1, width - 2, 1);
g2d.setBackground(Color.black);
g2d.setColor(Color.black);
g2d.fillPolygon(new int[] {1, 1, 4}, new int[] {height - 4, height - 1, height - 1}, 3);
g2d.fillPolygon(new int[] {width - 4, width - 1, width - 1}, new int[] {1, 1, 4}, 3);
g2d.rotate(-Math.PI / 4, width / 2, height / 2);
@@ -174,6 +174,7 @@ public class TapeMeasure extends AbstractLeadedComponent<String> {
g2d.rotate(theta);
g2d.setFont(font);
g2d.setColor(componentState == ComponentState.SELECTED ? SELECTION_COLOR : labelColor);
g2d.drawString(text, x, y);
}
@@ -27,18 +27,19 @@ import java.awt.geom.Point2D;
import org.diylc.common.IComponentTransformer;
import org.diylc.common.Orientation;
import org.diylc.components.electromechanical.CliffJack1_4;
import org.diylc.components.electromechanical.CliffJack1_8;
import org.diylc.core.IDIYComponent;
public class CliffJackTransformer implements IComponentTransformer {
@Override
public boolean canRotate(IDIYComponent<?> component) {
return component.getClass().equals(CliffJack1_4.class);
return component.getClass().equals(CliffJack1_4.class) || component.getClass().equals(CliffJack1_8.class);
}
@Override
public boolean canMirror(IDIYComponent<?> component) {
return component.getClass().equals(CliffJack1_4.class);
return component.getClass().equals(CliffJack1_4.class) || component.getClass().equals(CliffJack1_8.class);
}
@Override
@@ -55,8 +56,15 @@ public class CliffJackTransformer implements IComponentTransformer {
component.setControlPoint(p, index);
}
CliffJack1_4 jack = (CliffJack1_4) component;
Orientation o = jack.getOrientation();
Orientation o;
if (component instanceof CliffJack1_4) {
o = ((CliffJack1_4) component).getOrientation();
} else if (component instanceof CliffJack1_8) {
o = ((CliffJack1_8) component).getOrientation();
} else {
return;
}
int oValue = o.ordinal();
oValue += direction;
if (oValue < 0)
@@ -64,18 +72,33 @@ public class CliffJackTransformer implements IComponentTransformer {
if (oValue >= Orientation.values().length)
oValue = 0;
o = Orientation.values()[oValue];
jack.setOrientation(o);
if (component instanceof CliffJack1_4) {
((CliffJack1_4) component).setOrientation(o);
} else if (component instanceof CliffJack1_8) {
((CliffJack1_8) component).setOrientation(o);
}
}
@Override
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
CliffJack1_4 ic = (CliffJack1_4) component;
if (!(component instanceof CliffJack1_4) && !(component instanceof CliffJack1_8)) {
return;
}
IDIYComponent<?> ic = component;
if (direction == IComponentTransformer.HORIZONTAL) {
double dx = 2 * (center.getX() - ic.getControlPoint(1).getX());
double dy = 0;
Orientation o = ic.getOrientation();
Orientation o;
if (component instanceof CliffJack1_4) {
o = ((CliffJack1_4) component).getOrientation();
} else {
o = ((CliffJack1_8) component).getOrientation();
}
switch (o) {
case DEFAULT:
dy += (ic.getControlPoint(0).getY() - ic.getControlPoint(ic.getControlPointCount() - 1).getY());
@@ -98,12 +121,22 @@ public class CliffJackTransformer implements IComponentTransformer {
ic.setControlPoint(new Point2D.Double(p.getX() + dx, p.getY() + dy), i);
}
ic.setOrientation(o);
if (component instanceof CliffJack1_4) {
((CliffJack1_4) component).setOrientation(o);
} else {
((CliffJack1_8) component).setOrientation(o);
}
} else {
double dx = 0;
double dy = 2 * (center.getY() - ic.getControlPoint(1).getY());
Orientation o = ic.getOrientation();
Orientation o;
if (component instanceof CliffJack1_4) {
o = ((CliffJack1_4) component).getOrientation();
} else {
o = ((CliffJack1_8) component).getOrientation();
}
switch (o) {
case DEFAULT:
// dx += ic.getControlPoint(0).getX() - ic.getControlPoint(ic.getControlPointCount() - 1).getX();
@@ -128,7 +161,11 @@ public class CliffJackTransformer implements IComponentTransformer {
ic.setControlPoint(new Point2D.Double(p.getX() + dx, p.getY() + dy), i);
}
ic.setOrientation(o);
if (component instanceof CliffJack1_4) {
((CliffJack1_4) component).setOrientation(o);
} else {
((CliffJack1_8) component).setOrientation(o);
}
}
}
}
@@ -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);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.7.0</version>
<version>5.8.0</version>
</parent>
<artifactId>diylc-swing</artifactId>
@@ -42,6 +42,7 @@ import org.diylc.serialization.ProjectFileManager;
import org.diylc.swing.gui.MainFrame;
import org.diylc.swing.gui.TemplateDialog;
import org.diylc.swing.plugins.file.ProjectDrawingProvider;
import org.diylc.utils.SwingUtils;
/**
* Main class that runs DIYLC.
@@ -137,6 +138,12 @@ public class DIYLCStarter {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// Fix tooltip colors for Linux systems where they may be unreadable
SwingUtils.fixTooltipColors();
// Apply configurable font scaling from VM argument: -Dorg.diylc.fontScaleFactor=X
org.diylc.utils.FontScalingUtils.applyFontScaling();
} catch (Exception e) {
LOG.error("Could not set Look&Feel", e);
}
@@ -5,11 +5,7 @@ import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import javax.swing.AbstractAction;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.swingframework.CheckBoxListDialog;
@@ -69,7 +65,7 @@ public class ExportBlocksAction extends AbstractAction {
dialog.setVisible(true);
if (dialog.getSelectedButtonCaption() != "OK")
if (!Objects.equals(dialog.getSelectedButtonCaption(), "OK"))
return;
Object[] selected = dialog.getSelectedOptions();
@@ -133,4 +129,4 @@ public class ExportBlocksAction extends AbstractAction {
}, true);
}
}
}
}
@@ -5,13 +5,7 @@ import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.*;
import javax.swing.AbstractAction;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.swingframework.CheckBoxListDialog;
@@ -73,7 +67,7 @@ public class ExportVariantsAction extends AbstractAction {
ActionFactory.LOG.warn("Could not find type for: " + className);
}
Collections.sort(types, new Comparator<ComponentType>() {
types.sort(new Comparator<ComponentType>() {
@Override
public int compare(ComponentType o1, ComponentType o2) {
@@ -86,7 +80,7 @@ public class ExportVariantsAction extends AbstractAction {
dialog.setVisible(true);
if (dialog.getSelectedButtonCaption() != "OK")
if (!Objects.equals(dialog.getSelectedButtonCaption(), "OK"))
return;
Object[] selected = dialog.getSelectedOptions();
@@ -131,7 +125,7 @@ public class ExportVariantsAction extends AbstractAction {
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
ProjectFileManager.xStreamSerializer.toXML(variantPkg, out);
out.close();
ActionFactory.LOG.info("Exported variants succesfully");
ActionFactory.LOG.info("Exported variants successfully");
} catch (IOException e) {
ActionFactory.LOG.error("Could not export variants", e);
}
@@ -153,4 +147,4 @@ public class ExportVariantsAction extends AbstractAction {
}, true);
}
}
}
}
@@ -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);
}
}
});
}
}
@@ -0,0 +1,225 @@
/*
*
* DIY Layout Creator (DIYLC). Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with DIYLC. If not, see
* <http://www.gnu.org/licenses/>.
*/
package org.diylc.swing.plugins.canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Window;
import java.text.DecimalFormat;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JWindow;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import org.diylc.appframework.miscutils.IConfigurationManager;
import org.diylc.core.measures.ResizeDimensions;
import org.diylc.core.measures.Size;
/**
* A lightweight tooltip window that displays resize dimensions and tracks the mouse cursor.
*/
public class ResizeInfoTooltip {
private static final int OFFSET_X = 16;
private static final int OFFSET_Y = 16;
private static final DecimalFormat SIZE_FORMAT = new DecimalFormat("0.00");
private JWindow tooltipWindow;
private JLabel tooltipLabel;
private Component parentComponent;
private ResizeDimensions currentDimensions;
public ResizeInfoTooltip(Component parentComponent, IConfigurationManager<?> configManager) {
this.parentComponent = parentComponent;
initializeTooltip();
}
private void initializeTooltip() {
Window parentWindow = SwingUtilities.getWindowAncestor(parentComponent);
if (parentWindow == null) {
return;
}
tooltipWindow = new JWindow(parentWindow);
tooltipLabel = new JLabel() {
private static final long serialVersionUID = 1L;
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
super.paintComponent(g2d);
g2d.dispose();
}
};
// Use system tooltip font and colors
Font tooltipFont = UIManager.getFont("ToolTip.font");
if (tooltipFont != null) {
tooltipLabel.setFont(tooltipFont);
}
Color tooltipBackground = UIManager.getColor("ToolTip.background");
Color tooltipForeground = UIManager.getColor("ToolTip.foreground");
Color tooltipBorderColor = UIManager.getColor("ToolTip.border");
if (tooltipBorderColor == null) {
tooltipBorderColor = Color.GRAY;
}
tooltipLabel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(tooltipBorderColor, 1),
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
tooltipLabel.setOpaque(true);
tooltipLabel.setBackground(tooltipBackground != null ? tooltipBackground : new Color(255, 255, 220));
tooltipLabel.setForeground(tooltipForeground != null ? tooltipForeground : Color.BLACK);
tooltipWindow.add(tooltipLabel);
tooltipWindow.setFocusableWindowState(false);
tooltipWindow.setAlwaysOnTop(true);
}
/**
* Updates the tooltip with new dimensions and mouse position.
*/
public void update(ResizeDimensions dimensions, Point mousePosition) {
this.currentDimensions = dimensions;
if (dimensions == null || mousePosition == null) {
hideTooltip();
return;
}
if (tooltipWindow == null) {
initializeTooltip();
if (tooltipWindow == null) {
return;
}
}
// Format the dimensions
String text = formatDimensions(dimensions);
tooltipLabel.setText(text);
tooltipLabel.setPreferredSize(null); // Reset to calculate preferred size
tooltipWindow.pack();
// Position the tooltip near the mouse cursor
updateTooltipPosition(mousePosition);
tooltipWindow.setVisible(true);
}
/**
* Updates the tooltip position based on the current mouse position.
*/
public void updatePosition(Point mousePosition) {
if (currentDimensions != null && mousePosition != null && tooltipWindow != null && tooltipWindow.isVisible()) {
updateTooltipPosition(mousePosition);
}
}
private void updateTooltipPosition(Point mousePosition) {
if (tooltipWindow == null || parentComponent == null) {
return;
}
// Convert mouse position to screen coordinates
Point screenPoint = new Point(mousePosition);
SwingUtilities.convertPointToScreen(screenPoint, parentComponent);
// Position tooltip 16 pixels down and right from cursor
int x = screenPoint.x + OFFSET_X;
int y = screenPoint.y + OFFSET_Y;
// Ensure tooltip stays on screen
Dimension tooltipSize = tooltipWindow.getSize();
if (tooltipSize == null) {
tooltipWindow.pack();
tooltipSize = tooltipWindow.getSize();
}
if (tooltipSize != null) {
java.awt.Toolkit toolkit = java.awt.Toolkit.getDefaultToolkit();
Dimension screenSize = toolkit.getScreenSize();
// Adjust if tooltip would go off screen
if (x + tooltipSize.width > screenSize.width) {
x = screenPoint.x - tooltipSize.width - OFFSET_X;
}
if (y + tooltipSize.height > screenSize.height) {
y = screenPoint.y - tooltipSize.height - OFFSET_Y;
}
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
}
tooltipWindow.setLocation(x, y);
}
private String formatDimensions(ResizeDimensions dimensions) {
StringBuilder sb = new StringBuilder("<html>");
// Format width
Size width = dimensions.width();
String widthUnit = width.getUnit().toString();
sb.append(String.format("W: %s %s", SIZE_FORMAT.format(width.getValue()), widthUnit));
// Format height
Size height = dimensions.height();
String heightUnit = height.getUnit().toString();
sb.append(String.format("<br>H: %s %s", SIZE_FORMAT.format(height.getValue()), heightUnit));
// Format length if available
Size length = dimensions.length();
if (length != null) {
String lengthUnit = length.getUnit().toString();
sb.append(String.format("<br>L: %s %s", SIZE_FORMAT.format(length.getValue()), lengthUnit));
}
sb.append("</html>");
return sb.toString();
}
/**
* Hides the tooltip.
*/
public void hideTooltip() {
if (tooltipWindow != null) {
tooltipWindow.setVisible(false);
}
currentDimensions = null;
}
/**
* Disposes the tooltip window.
*/
public void dispose() {
if (tooltipWindow != null) {
tooltipWindow.dispose();
tooltipWindow = null;
}
}
}
@@ -158,6 +158,10 @@ public class ConfigPlugin implements IPlugIn {
ActionFactory.getInstance().createConfigAction(plugInPort, "Show Rulers", IPlugInPort.SHOW_RULERS_KEY, true),
CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createConfigAction(plugInPort, "Show Resize Dimensions",
IPlugInPort.SHOW_RESIZE_DIMENSIONS_TOOLTIP_KEY, true), CONFIG_MENU);
swingUI.injectSubmenu(SNAP_MENU, IconLoader.GraphEdgeDirected.getIcon(), CONFIG_MENU);
swingUI.injectMenuAction(
ActionFactory.getInstance().createToggleAction(IPlugInPort.SNAP_TO_NONE, IPlugInPort.SNAP_TO_KEY, SNAP_MENU, IPlugInPort.SNAP_TO_DEFAULT),
@@ -0,0 +1,96 @@
package org.diylc.swing.plugins.explorer;
import java.awt.Component;
import java.awt.Dimension;
import javax.swing.Icon;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.diylc.common.ComponentType;
import org.diylc.core.IDIYComponent;
public class ComponentTreeCellRenderer extends DefaultTreeCellRenderer {
private static final long serialVersionUID = 1L;
// private static Color[] layerColors = new Color[] { Color.gray, Color.decode("#ff6363"),
// Color.decode("#293462"), Color.decode("#138588"),
// Color.decode("#c0ca03"), Color.decode("#990000") };
private Map<String, ComponentType> componentTypes;
public ComponentTreeCellRenderer(Map<String, List<ComponentType>> componentTypeMap) {
this.componentTypes = componentTypeMap.values().stream().flatMap(x -> x.stream())
.collect(Collectors.toMap(x -> x.getInstanceClass().getCanonicalName(), x -> x));
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected,
boolean expanded, boolean leaf, int row, boolean hasFocus) {
super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
if (!(value instanceof DefaultMutableTreeNode)) {
return this;
}
DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
Object userObject = node.getUserObject();
// Handle group nodes
if (userObject instanceof ComponentTreeModel.GroupNode) {
ComponentTreeModel.GroupNode groupNode = (ComponentTreeModel.GroupNode) userObject;
setText(groupNode.toString());
setIcon(null); // No icon for group nodes
setPreferredSize(new Dimension(250, 16));
return this;
}
if (!(userObject instanceof IDIYComponent)) {
return this;
}
IDIYComponent<?> component = (IDIYComponent<?>) userObject;
ComponentType componentType = componentTypes.get(component.getClass().getCanonicalName());
if (componentType == null) {
return this;
}
// Set the component's icon
Icon icon = componentType.getIcon();
if (icon != null) {
setIcon(icon);
}
int layerId = (int) Math.round(componentType.getZOrder());
String valueForDisplay = component.getValueForDisplay();
if (selected) {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
setText(String.format("[L%s] %s", layerId, component.getName()));
} else {
setText(String.format("[L%s] %s (%s)", layerId, component.getName(), valueForDisplay));
}
} else {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
setText(String.format("<html><font color='#c0c0c0'>[L%s]</font> %s</html>",
layerId, component.getName()));
} else {
setText(String.format(
"<html><font color='#c0c0c0'>[L%s]</font> %s (<font color='#666666'>%s</font>)</html>",
layerId, component.getName(), valueForDisplay));
}
}
// Set preferred size after all properties are set (like TreePanel does)
setPreferredSize(new Dimension(250, 16));
return this;
}
}
@@ -0,0 +1,266 @@
package org.diylc.swing.plugins.explorer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import org.diylc.core.IDIYComponent;
public class ComponentTreeModel extends DefaultTreeModel {
private static final long serialVersionUID = 1L;
private DefaultMutableTreeNode root;
private List<IDIYComponent<?>> componentsRaw;
private List<IDIYComponent<?>> components;
private Set<Set<IDIYComponent<?>>> groups;
private String searchText;
private ExplorerPane.Sort sort = ExplorerPane.Sort.Z_INDEX;
public ComponentTreeModel() {
super(new DefaultMutableTreeNode("Root"));
this.root = (DefaultMutableTreeNode) getRoot();
}
public void setComponents(List<IDIYComponent<?>> components, Set<Set<IDIYComponent<?>>> groups) {
this.componentsRaw = components;
this.groups = groups != null ? groups : new HashSet<>();
refresh();
}
public void setComponents(List<IDIYComponent<?>> components) {
setComponents(components, null);
}
/**
* Returns the group that contains the given component, or null if not in a group.
*/
public Set<IDIYComponent<?>> getGroupForComponent(IDIYComponent<?> component) {
if (groups == null) {
return null;
}
for (Set<IDIYComponent<?>> group : groups) {
if (group.contains(component)) {
return group;
}
}
return null;
}
public List<IDIYComponent<?>> getComponents() {
return components;
}
public ExplorerPane.Sort getSort() {
return sort;
}
public void setSort(ExplorerPane.Sort sort) {
this.sort = sort;
refresh();
}
public void setSearchText(String searchText) {
this.searchText = searchText.toLowerCase();
refresh();
}
private void refresh() {
// Clear existing children
root.removeAllChildren();
// Sort components
if (sort == ExplorerPane.Sort.NAME) {
this.components =
componentsRaw.stream().sorted(Comparator.comparing(x -> x.toString().toLowerCase()))
.collect(Collectors.toList());
} else if (sort == ExplorerPane.Sort.TYPE) {
this.components = componentsRaw.stream()
.sorted(Comparator.comparing(x -> x.getClass().getName().toLowerCase())
.thenComparing(x -> x.toString().toLowerCase()))
.collect(Collectors.toList());
} else {
List<IDIYComponent<?>> componentsClone = new ArrayList<IDIYComponent<?>>(componentsRaw);
Collections.reverse(componentsClone);
this.components = componentsClone;
}
// Filter by search text
if (searchText != null && !searchText.trim().isEmpty()) {
this.components = this.components.stream().filter(component -> {
return component.getName().toLowerCase().contains(searchText);
}).collect(Collectors.toList());
}
// Build a map of components to their groups
Set<IDIYComponent<?>> groupedComponents = new HashSet<>();
if (groups != null) {
for (Set<IDIYComponent<?>> group : groups) {
groupedComponents.addAll(group);
}
}
// Create a list to hold all tree items (groups and ungrouped components) for sorting
List<TreeItem> allItems = new ArrayList<>();
// Create group nodes
if (groups != null && !groups.isEmpty()) {
for (Set<IDIYComponent<?>> group : groups) {
// Get ALL group components from raw list (for maxIndex calculation)
List<IDIYComponent<?>> groupComponentsRaw = componentsRaw.stream()
.filter(group::contains)
.collect(Collectors.toList());
// Apply search filter to get components to display
List<IDIYComponent<?>> groupComponents = groupComponentsRaw.stream()
.filter(component -> {
if (searchText != null && !searchText.trim().isEmpty()) {
return component.getName().toLowerCase().contains(searchText);
}
return true;
})
.collect(Collectors.toList());
if (!groupComponents.isEmpty()) {
// Sort components within the group based on sort mode
if (sort == ExplorerPane.Sort.NAME) {
groupComponents.sort(Comparator.comparing(x -> x.toString().toLowerCase()));
} else if (sort == ExplorerPane.Sort.TYPE) {
groupComponents.sort(Comparator.comparing(x -> x.getClass().getName().toLowerCase())
.thenComparing(x -> x.toString().toLowerCase()));
} else {
// For Z_INDEX, sort by index in raw list (reverse order - highest first)
groupComponents.sort((c1, c2) -> {
int idx1 = componentsRaw.indexOf(c1);
int idx2 = componentsRaw.indexOf(c2);
return Integer.compare(idx2, idx1); // Reverse: higher index first
});
}
// Store both the display components and all group components (for maxIndex calculation)
allItems.add(new TreeItem(group, groupComponents, groupComponentsRaw, true));
}
}
}
// Add ungrouped components
for (IDIYComponent<?> component : this.components) {
if (!groupedComponents.contains(component)) {
allItems.add(new TreeItem(null, Collections.singletonList(component), Collections.singletonList(component), false));
}
}
// Sort all items together based on sort mode
if (sort == ExplorerPane.Sort.Z_INDEX) {
// Sort by maximum z-order (index) - groups use max index of ALL their components (not just filtered)
Collections.sort(allItems, (item1, item2) -> {
int maxIndex1 = getMaxComponentIndex(item1.allComponents);
int maxIndex2 = getMaxComponentIndex(item2.allComponents);
// Reverse order: higher index first (since components are in reverse order)
return Integer.compare(maxIndex2, maxIndex1);
});
} else if (sort == ExplorerPane.Sort.NAME) {
Collections.sort(allItems, (item1, item2) -> {
String name1 = item1.components.get(0).getName().toLowerCase();
String name2 = item2.components.get(0).getName().toLowerCase();
return name1.compareTo(name2);
});
} else if (sort == ExplorerPane.Sort.TYPE) {
Collections.sort(allItems, (item1, item2) -> {
String type1 = item1.components.get(0).getClass().getName().toLowerCase();
String type2 = item2.components.get(0).getClass().getName().toLowerCase();
int typeCompare = type1.compareTo(type2);
if (typeCompare != 0) {
return typeCompare;
}
String name1 = item1.components.get(0).getName().toLowerCase();
String name2 = item2.components.get(0).getName().toLowerCase();
return name1.compareTo(name2);
});
}
// Add sorted items to the tree
for (TreeItem item : allItems) {
if (item.isGroup) {
DefaultMutableTreeNode groupNode = new DefaultMutableTreeNode(new GroupNode(item.group));
for (IDIYComponent<?> component : item.components) {
DefaultMutableTreeNode componentNode = new DefaultMutableTreeNode(component);
groupNode.add(componentNode);
}
root.add(groupNode);
} else {
DefaultMutableTreeNode node = new DefaultMutableTreeNode(item.components.get(0));
root.add(node);
}
}
// Notify tree model listeners
nodeStructureChanged(root);
}
/**
* Wrapper class to represent a group node in the tree.
*/
public static class GroupNode {
private final Set<IDIYComponent<?>> group;
public GroupNode(Set<IDIYComponent<?>> group) {
this.group = group;
}
public Set<IDIYComponent<?>> getGroup() {
return group;
}
@Override
public String toString() {
return "Group (" + group.size() + " components)";
}
}
public int getSize() {
return this.components == null ? 0 : this.components.size();
}
/**
* Gets the maximum index of components in the raw components list.
* This represents the maximum z-order for the group.
* Returns -1 if no valid index is found.
*/
private int getMaxComponentIndex(List<IDIYComponent<?>> groupComponents) {
int maxIndex = -1;
for (IDIYComponent<?> component : groupComponents) {
int index = componentsRaw.indexOf(component);
if (index >= 0 && index > maxIndex) {
maxIndex = index;
}
}
return maxIndex;
}
/**
* Helper class to hold either a group or a single ungrouped component for sorting.
*/
private static class TreeItem {
final Set<IDIYComponent<?>> group;
final List<IDIYComponent<?>> components; // Components to display (may be filtered)
final List<IDIYComponent<?>> allComponents; // All components for maxIndex calculation
final boolean isGroup;
TreeItem(Set<IDIYComponent<?>> group, List<IDIYComponent<?>> components,
List<IDIYComponent<?>> allComponents, boolean isGroup) {
this.group = group;
this.components = components;
this.allComponents = allComponents;
this.isGroup = isGroup;
}
}
}
@@ -0,0 +1,213 @@
package org.diylc.swing.plugins.explorer;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.util.List;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.TransferHandler;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import org.diylc.common.IPlugInPort;
import org.diylc.core.IDIYComponent;
import org.diylc.lang.LangUtil;
import org.diylc.swing.ISwingUI;
public class ComponentTreeTransferHandler extends TransferHandler {
private static final long serialVersionUID = 1L;
private static final String REORDERING_NOT_POSSIBLE = LangUtil.translate(
"Component reordering is possible only when Project Explorer is sorted by z-index.");
private final ComponentTreeModel treeModel;
private final ISwingUI swingUI;
private final IPlugInPort plugInPort;
public ComponentTreeTransferHandler(ComponentTreeModel treeModel, ISwingUI swingUI,
IPlugInPort plugInPort) {
this.treeModel = treeModel;
this.swingUI = swingUI;
this.plugInPort = plugInPort;
}
@Override
public int getSourceActions(JComponent c) {
return MOVE;
}
@Override
protected Transferable createTransferable(JComponent c) {
if (!(c instanceof JTree)) {
return null;
}
JTree tree = (JTree) c;
TreePath[] selectionPaths = tree.getSelectionPaths();
if (selectionPaths == null || selectionPaths.length == 0) {
return null;
}
// Get the first selected path
TreePath firstPath = selectionPaths[0];
DefaultMutableTreeNode node = (DefaultMutableTreeNode) firstPath.getLastPathComponent();
Object userObject = node.getUserObject();
// Only allow dragging components, not group nodes
if (!(userObject instanceof IDIYComponent)) {
return null;
}
// Store the component's group info in the transferable
IDIYComponent<?> component = (IDIYComponent<?>) userObject;
Set<IDIYComponent<?>> group = treeModel.getGroupForComponent(component);
String groupId = group != null ? Integer.toString(group.hashCode()) : "null";
int row = tree.getRowForPath(firstPath);
return new StringSelection(row + ":" + groupId);
}
@Override
public boolean canImport(TransferHandler.TransferSupport support) {
if (!support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
return false;
}
if (!(support.getComponent() instanceof JTree)) {
return false;
}
JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
TreePath dropPath = dl.getPath();
if (dropPath == null) {
return false;
}
// Only allow reordering when sorted by Z_INDEX
if (treeModel.getSort() != ExplorerPane.Sort.Z_INDEX) {
return false;
}
// Check if dropping on a group node - not allowed
DefaultMutableTreeNode dropNode = (DefaultMutableTreeNode) dropPath.getLastPathComponent();
if (dropNode.getUserObject() instanceof ComponentTreeModel.GroupNode) {
return false;
}
// Get the source component's group from the transferable
try {
Transferable transferable = support.getTransferable();
String data = (String) transferable.getTransferData(DataFlavor.stringFlavor);
String[] parts = data.split(":");
if (parts.length == 2) {
String sourceGroupId = parts[1];
// Get the target component's group
Object targetUserObject = dropNode.getUserObject();
if (targetUserObject instanceof IDIYComponent) {
IDIYComponent<?> targetComponent = (IDIYComponent<?>) targetUserObject;
Set<IDIYComponent<?>> targetGroup = treeModel.getGroupForComponent(targetComponent);
String targetGroupId = targetGroup != null ? Integer.toString(targetGroup.hashCode()) : "null";
// Only allow dragging within the same group (or both ungrouped)
if (!sourceGroupId.equals(targetGroupId)) {
return false;
}
}
}
} catch (Exception e) {
// If we can't parse the transferable, don't allow the drop
return false;
}
return true;
}
@Override
public boolean importData(TransferHandler.TransferSupport support) {
if (!canImport(support)) {
// Show error message if not sorted by Z_INDEX
if (treeModel.getSort() != ExplorerPane.Sort.Z_INDEX) {
swingUI.showMessage(REORDERING_NOT_POSSIBLE, "Error", ISwingUI.ERROR_MESSAGE);
}
return false;
}
if (!(support.getComponent() instanceof JTree)) {
return false;
}
JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation();
TreePath dropPath = dl.getPath();
if (dropPath == null) {
return false;
}
DefaultMutableTreeNode dropNode = (DefaultMutableTreeNode) dropPath.getLastPathComponent();
Object dropUserObject = dropNode.getUserObject();
// Can't drop on group nodes
if (dropUserObject instanceof ComponentTreeModel.GroupNode) {
return false;
}
if (!(dropUserObject instanceof IDIYComponent)) {
return false;
}
// Get the parent - if it's a group node, we need to calculate index within the group
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) dropNode.getParent();
if (parent != null && parent.getUserObject() instanceof ComponentTreeModel.GroupNode) {
// Dropping within a group - calculate index within the group's children
int childIndex = dl.getChildIndex();
int dropTargetIndex;
if (childIndex != -1) {
dropTargetIndex = childIndex;
} else {
// Dropping on a component within the group
dropTargetIndex = parent.getIndex(dropNode);
}
// For groups, we need to calculate z-index based on the group's components
// Get all components in the group in z-order (they're already sorted in the model)
ComponentTreeModel.GroupNode groupNode = (ComponentTreeModel.GroupNode) parent.getUserObject();
List<IDIYComponent<?>> groupComponents = treeModel.getComponents().stream()
.filter(groupNode.getGroup()::contains)
.collect(java.util.stream.Collectors.toList());
if (dropTargetIndex < groupComponents.size()) {
IDIYComponent<?> targetComponent = groupComponents.get(dropTargetIndex);
// Find the component's index in the full component list
int componentIndex = treeModel.getComponents().indexOf(targetComponent);
if (componentIndex >= 0) {
int targetZIndex = treeModel.getSize() - componentIndex;
plugInPort.moveSelectionToZIndex(targetZIndex);
}
}
} else {
// Dropping on an ungrouped component at root level
DefaultMutableTreeNode root = (DefaultMutableTreeNode) treeModel.getRoot();
int childIndex = dl.getChildIndex();
int dropTargetIndex;
if (childIndex != -1) {
dropTargetIndex = childIndex;
} else {
dropTargetIndex = root.getIndex(dropNode);
}
// Calculate the target z-index (components are in reverse order for z-index)
int targetZIndex = treeModel.getSize() - dropTargetIndex;
plugInPort.moveSelectionToZIndex(targetZIndex);
}
return true;
}
}
@@ -1,6 +1,5 @@
package org.diylc.swing.plugins.explorer;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
@@ -10,47 +9,36 @@ import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.AbstractListModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DropMode;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.JTree;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import org.diylc.common.ComponentType;
import org.diylc.common.IPlugInPort;
import org.diylc.core.IDIYComponent;
import org.diylc.lang.LangUtil;
import org.diylc.swing.ISwingUI;
import org.diylc.swing.gui.ComponentPopupMenu;
import org.diylc.swing.gui.DragDropList;
import org.diylc.swing.gui.IDragDropListListener;
import org.diylc.swing.gui.SearchTextField;
public class ExplorerPane extends JPanel {
private static final long serialVersionUID = 1L;
private static final String REORDERING_NOT_POSSIBLE = LangUtil.translate(
"Component reordering is possible only when Project Explorer is sorted by z-index.");
public static Font DEFAULT_FONT = new Font("Square721 BT", Font.PLAIN, 12);
private ISwingUI swingUI;
@@ -59,8 +47,8 @@ public class ExplorerPane extends JPanel {
private JLabel titleLabel;
private JTextField searchField;
private JScrollPane componentScrollPane;
private ComponentListModel componentListModel;
private JList<IDIYComponent<?>> componentList;
private ComponentTreeModel componentTreeModel;
private JTree componentTree;
private ComponentPopupMenu popupMenu;
private JComboBox<Sort> sortBox;
@@ -116,7 +104,7 @@ public class ExplorerPane extends JPanel {
public JTextField getSearchField() {
if (searchField == null) {
searchField = new SearchTextField(true, 'X', text -> {
getComponentListModel().setSearchText(text);
getComponentTreeModel().setSearchText(text);
});
}
return searchField;
@@ -129,7 +117,7 @@ public class ExplorerPane extends JPanel {
@Override
public void itemStateChanged(ItemEvent e) {
getComponentListModel().setSort((Sort) e.getItem());
getComponentTreeModel().setSort((Sort) e.getItem());
}
});
}
@@ -138,38 +126,37 @@ public class ExplorerPane extends JPanel {
public JScrollPane getComponentScrollPane() {
if (componentScrollPane == null) {
componentScrollPane = new JScrollPane(getComponentList(),
componentScrollPane = new JScrollPane(getComponentTree(),
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
}
return componentScrollPane;
}
public JList<IDIYComponent<?>> getComponentList() {
if (componentList == null) {
componentList = new DragDropList<IDIYComponent<?>>(new IDragDropListListener() {
public JTree getComponentTree() {
if (componentTree == null) {
componentTree = new JTree(getComponentTreeModel());
componentTree.setRootVisible(false);
componentTree.setShowsRootHandles(true);
componentTree.setFont(DEFAULT_FONT);
componentTree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
componentTree.setCellRenderer(new ComponentTreeCellRenderer(plugInPort.getComponentTypes()));
// Set row height after renderer is set, using a fixed value
componentTree.setRowHeight(24);
// Enable drag and drop
componentTree.setDragEnabled(true);
componentTree.setDropMode(DropMode.INSERT);
componentTree.setTransferHandler(new ComponentTreeTransferHandler(getComponentTreeModel(),
swingUI, plugInPort));
componentTree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public boolean dropComplete(int selectedIndex, int dropTargetIndex) {
if (getComponentListModel().getSort() != Sort.Z_INDEX) {
swingUI.showMessage(REORDERING_NOT_POSSIBLE, "Error", ISwingUI.ERROR_MESSAGE);
return false;
}
plugInPort.moveSelectionToZIndex(getComponentListModel().getSize() - dropTargetIndex);
return true;
}
});
componentList.setModel(getComponentListModel());
componentList.setFont(DEFAULT_FONT);
componentList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
componentList.setCellRenderer(new ComponentListCellRenderer(plugInPort.getComponentTypes()));
componentList.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting())
public void valueChanged(TreeSelectionEvent e) {
if (e.getNewLeadSelectionPath() == null)
return;
List<IDIYComponent<?>> selectedValuesList = componentList.getSelectedValuesList();
List<IDIYComponent<?>> selectedValuesList = getSelectedComponents();
if (selectedValuesList.size() > 0 && !new HashSet<IDIYComponent<?>>(selectedValuesList)
.equals(new HashSet<IDIYComponent<?>>(plugInPort.getSelectedComponents()))) {
plugInPort.setSelection(selectedValuesList, true);
@@ -177,7 +164,7 @@ public class ExplorerPane extends JPanel {
}
});
componentList.addMouseListener(new MouseAdapter() {
componentTree.addMouseListener(new MouseAdapter() {
private MouseEvent pressedEvent;
@@ -190,7 +177,7 @@ public class ExplorerPane extends JPanel {
@Override
public void mousePressed(MouseEvent e) {
componentList.requestFocus();
componentTree.requestFocus();
pressedEvent = e;
}
@@ -205,7 +192,7 @@ public class ExplorerPane extends JPanel {
if (plugInPort.getNewComponentTypeSlot() == null && (e.isPopupTrigger()
|| (pressedEvent != null && pressedEvent.isPopupTrigger()))) {
final List<IDIYComponent<?>> selectedValues = componentList.getSelectedValuesList();
final List<IDIYComponent<?>> selectedValues = getSelectedComponents();
getComponentPopupMenu().prepareAndShowAt(selectedValues, selectedValues, e.getX(),
e.getY());
}
@@ -214,193 +201,103 @@ public class ExplorerPane extends JPanel {
}
});
}
return componentList;
return componentTree;
}
private List<IDIYComponent<?>> getSelectedComponents() {
List<IDIYComponent<?>> selectedComponents = new ArrayList<>();
TreePath[] selectionPaths = componentTree.getSelectionPaths();
if (selectionPaths != null) {
for (TreePath path : selectionPaths) {
Object lastPathComponent = path.getLastPathComponent();
if (lastPathComponent instanceof DefaultMutableTreeNode) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) lastPathComponent;
Object userObject = node.getUserObject();
if (userObject instanceof IDIYComponent) {
selectedComponents.add((IDIYComponent<?>) userObject);
}
}
}
}
return selectedComponents;
}
public ComponentListModel getComponentListModel() {
if (componentListModel == null) {
componentListModel = new ComponentListModel();
public ComponentTreeModel getComponentTreeModel() {
if (componentTreeModel == null) {
componentTreeModel = new ComponentTreeModel();
}
return componentListModel;
return componentTreeModel;
}
public ComponentPopupMenu getComponentPopupMenu() {
if (popupMenu == null) {
popupMenu = new ComponentPopupMenu(plugInPort, getComponentList(), false);
popupMenu = new ComponentPopupMenu(plugInPort, getComponentTree(), false);
}
return popupMenu;
}
public void setComponents(List<IDIYComponent<?>> components,
Collection<IDIYComponent<?>> selectedComponents) {
this.getComponentListModel().setComponents(components);
// Get groups from the current project
Set<Set<IDIYComponent<?>>> groups = null;
if (plugInPort.getCurrentProject() != null) {
groups = plugInPort.getCurrentProject().getGroups();
}
this.getComponentTreeModel().setComponents(components, groups);
setSelection(new HashSet<IDIYComponent<?>>(selectedComponents));
// Expand all group nodes after model is updated
SwingUtilities.invokeLater(() -> {
JTree tree = getComponentTree();
for (int i = 0; i < tree.getRowCount(); i++) {
TreePath path = tree.getPathForRow(i);
if (path != null) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
if (node.getUserObject() instanceof ComponentTreeModel.GroupNode) {
tree.expandPath(path);
}
}
}
});
}
public void setSelection(Set<IDIYComponent<?>> selectedComponents) {
List<IDIYComponent<?>> selectedValuesList = componentList.getSelectedValuesList();
if (new HashSet<IDIYComponent<?>>(selectedValuesList)
List<IDIYComponent<?>> currentSelected = getSelectedComponents();
if (new HashSet<IDIYComponent<?>>(currentSelected)
.equals(new HashSet<IDIYComponent<?>>(plugInPort.getSelectedComponents())))
return;
final JList<IDIYComponent<?>> list = this.getComponentList();
final JTree tree = this.getComponentTree();
if (selectedComponents.size() == 0) {
list.clearSelection();
tree.clearSelection();
} else {
int[] indices = new int[selectedComponents.size()];
int n = 0;
List<IDIYComponent<?>> components = this.getComponentListModel().getComponents();
for (int i = 0; i < components.size(); i++) {
indices[n] = -1; // this should not happen
if (selectedComponents.contains(components.get(i)))
indices[n++] = i;
if (n == indices.length)
break;
}
list.getSelectionModel().setValueIsAdjusting(true);
list.setSelectedIndices(indices);
list.getSelectionModel().setValueIsAdjusting(false);
int firstVisibleIndex = list.getFirstVisibleIndex();
int lastVisibleIndex = list.getFirstVisibleIndex();
// if none of the selected items is visible, jump to the first one
if (!Arrays.stream(indices)
.anyMatch(index -> firstVisibleIndex <= index && index <= lastVisibleIndex))
list.ensureIndexIsVisible(indices[0]);
}
}
private class ComponentListModel extends AbstractListModel<IDIYComponent<?>> {
private static final long serialVersionUID = 1L;
private List<IDIYComponent<?>> componentsRaw;
private List<IDIYComponent<?>> components;
private String searchText;
private Sort sort = Sort.Z_INDEX;
public void setComponents(List<IDIYComponent<?>> components) {
this.componentsRaw = components;
int oldSize = getSize();
refresh(oldSize);
}
public List<IDIYComponent<?>> getComponents() {
return components;
}
public Sort getSort() {
return sort;
}
public void setSort(Sort sort) {
this.sort = sort;
refresh(getSize());
}
public void setSearchText(String searchText) {
this.searchText = searchText.toLowerCase();
refresh(getSize());
}
private void refresh(int oldSize) {
if (sort == Sort.NAME) {
this.components =
componentsRaw.stream().sorted(Comparator.comparing(x -> x.toString().toLowerCase()))
.collect(Collectors.toList());
} else if (sort == Sort.TYPE) {
this.components = componentsRaw.stream()
.sorted(Comparator.comparing(x -> x.getClass().getName().toLowerCase())
.thenComparing(x -> x.toString().toLowerCase()))
.collect(Collectors.toList());
} else {
List<IDIYComponent<?>> componentsClone = new ArrayList<IDIYComponent<?>>(componentsRaw);
Collections.reverse(componentsClone);
this.components = componentsClone;
}
if (searchText != null && !searchText.trim().isEmpty()) {
this.components = this.components.stream().filter(component -> {
return component.getName().toLowerCase().contains(searchText);
}).collect(Collectors.toList());
}
int newSize = this.components.size();
fireContentsChanged(this, 0, Math.min(newSize, oldSize));
if (oldSize > newSize)
fireIntervalRemoved(this, newSize, oldSize);
else if (newSize > oldSize)
fireIntervalAdded(this, oldSize, newSize);
}
@Override
public int getSize() {
return this.components == null ? 0 : this.components.size();
}
@Override
public IDIYComponent<?> getElementAt(int index) {
return this.components.get(index);
}
}
private static class ComponentListCellRenderer implements ListCellRenderer<IDIYComponent<?>> {
// private static Color[] layerColors = new Color[] { Color.gray, Color.decode("#ff6363"),
// Color.decode("#293462"), Color.decode("#138588"),
// Color.decode("#c0ca03"), Color.decode("#990000") };
protected DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer();
private Map<String, ComponentType> componentTypes;
public ComponentListCellRenderer(Map<String, List<ComponentType>> componentTypeMap) {
this.componentTypes = componentTypeMap.values().stream().flatMap(x -> x.stream())
.collect(Collectors.toMap(x -> x.getInstanceClass().getCanonicalName(), x -> x));
}
@Override
public Component getListCellRendererComponent(JList<? extends IDIYComponent<?>> list,
IDIYComponent<?> value, int index, boolean isSelected, boolean cellHasFocus) {
ComponentType componentType = componentTypes.get(value.getClass().getCanonicalName());
JLabel renderer = (JLabel) defaultRenderer.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus);
int layerId = (int) Math.round(componentType.getZOrder());
String valueForDisplay = value.getValueForDisplay();
if (isSelected) {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
renderer.setText(String.format("[L%s] %s", layerId, value.getName()));
} else {
renderer
.setText(String.format("[L%s] %s (%s)", layerId, value.getName(), valueForDisplay));
}
} else {
if (valueForDisplay == null || valueForDisplay.trim().isEmpty()) {
renderer.setText(String.format("<html><font color='#c0c0c0'>[L%s]</font> %s</html>",
layerId, value.getName()));
} else {
renderer.setText(String.format(
"<html><font color='#c0c0c0'>[L%s]</font> %s (<font color='#666666'>%s</font>)</html>",
layerId, value.getName(), valueForDisplay));
List<TreePath> pathsToSelect = new ArrayList<>();
DefaultMutableTreeNode root = (DefaultMutableTreeNode) getComponentTreeModel().getRoot();
for (int i = 0; i < root.getChildCount(); i++) {
DefaultMutableTreeNode child = (DefaultMutableTreeNode) root.getChildAt(i);
Object userObject = child.getUserObject();
if (userObject instanceof IDIYComponent && selectedComponents.contains(userObject)) {
pathsToSelect.add(new TreePath(child.getPath()));
}
}
return renderer;
tree.getSelectionModel().setSelectionPaths(pathsToSelect.toArray(new TreePath[0]));
// If none of the selected items is visible, scroll to the first one
if (!pathsToSelect.isEmpty()) {
TreePath firstPath = pathsToSelect.get(0);
if (tree.getRowForPath(firstPath) == -1) {
tree.scrollPathToVisible(firstPath);
}
}
}
}
private static enum Sort {
static enum Sort {
Z_INDEX("Sort by Z-index"), NAME("Sort by Name"), TYPE("Sort by Type");
private String label;
@@ -43,11 +43,13 @@ public class FileMenuPlugin implements IPlugIn, IDynamicSubmenuHandler {
private static final String FILE_TITLE = "File";
private static final String TRACE_MASK_TITLE = "Trace Mask";
private static final String SILK_SCREEN_TITLE = "Silkscreen";
private static final String INTEGRATION_TITLE = "Integration";
private static final String EXPORT_TITLE = "Export";
private static final String PROJECT_TITLE = "Project";
private ProjectDrawingProvider drawingProvider;
private TraceMaskDrawingProvider traceMaskDrawingProvider;
private SilkScreenDrawingProvider silkScreenDrawingProvider;
private ISwingUI swingUI;
private IPlugInPort plugInPort;
@@ -62,6 +64,7 @@ public class FileMenuPlugin implements IPlugIn, IDynamicSubmenuHandler {
this.plugInPort = plugInPort;
this.drawingProvider = new ProjectDrawingProvider(plugInPort, false, true, false);
this.traceMaskDrawingProvider = new TraceMaskDrawingProvider(plugInPort);
this.silkScreenDrawingProvider = new SilkScreenDrawingProvider(plugInPort);
ActionFactory actionFactory = ActionFactory.getInstance();
swingUI.injectMenuAction(actionFactory.createNewAction(plugInPort), FILE_TITLE);
@@ -73,15 +76,17 @@ public class FileMenuPlugin implements IPlugIn, IDynamicSubmenuHandler {
swingUI.injectMenuAction(actionFactory.createSaveAsAction(plugInPort, swingUI), FILE_TITLE);
swingUI.injectDynamicSubmenu("Recent Files", IconLoader.History.getIcon(), FILE_TITLE, this);
swingUI.injectMenuAction(null, FILE_TITLE);
swingUI.injectSubmenu(EXPORT_TITLE, IconLoader.Export.getIcon(), FILE_TITLE);
swingUI.injectSubmenu(PROJECT_TITLE, IconLoader.Project.getIcon(), FILE_TITLE);
swingUI.injectMenuAction(
actionFactory.createExportPDFAction(plugInPort, drawingProvider, swingUI, ""), EXPORT_TITLE);
actionFactory.createExportPDFAction(plugInPort, drawingProvider, swingUI, ""), PROJECT_TITLE);
swingUI.injectMenuAction(
actionFactory.createExportPNGAction(plugInPort, drawingProvider, swingUI, ""), EXPORT_TITLE);
actionFactory.createExportPNGAction(plugInPort, drawingProvider, swingUI, ""), PROJECT_TITLE);
swingUI.injectMenuAction(
actionFactory.createExportGerberAction(plugInPort, swingUI), EXPORT_TITLE);
actionFactory.createExportGerberAction(plugInPort, swingUI), PROJECT_TITLE);
swingUI.injectMenuAction(actionFactory.createPrintAction(drawingProvider, swingUI,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), FILE_TITLE);
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), PROJECT_TITLE);
swingUI.injectSubmenu(TRACE_MASK_TITLE, IconLoader.TraceMask.getIcon(), FILE_TITLE);
swingUI.injectMenuAction(actionFactory.createExportPDFAction(plugInPort,
traceMaskDrawingProvider, swingUI, " (mask)"), TRACE_MASK_TITLE);
@@ -92,6 +97,17 @@ public class FileMenuPlugin implements IPlugIn, IDynamicSubmenuHandler {
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | KeyEvent.SHIFT_DOWN_MASK),
TRACE_MASK_TITLE);
swingUI.injectSubmenu(SILK_SCREEN_TITLE, IconLoader.Silkscreen.getIcon(), FILE_TITLE);
swingUI.injectMenuAction(actionFactory.createExportPDFAction(plugInPort,
silkScreenDrawingProvider, swingUI, " (silkscreen)"), SILK_SCREEN_TITLE);
swingUI.injectMenuAction(actionFactory.createExportPNGAction(plugInPort,
silkScreenDrawingProvider, swingUI, " (silkscreen)"), SILK_SCREEN_TITLE);
swingUI.injectMenuAction(
actionFactory.createPrintAction(silkScreenDrawingProvider, swingUI,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | KeyEvent.ALT_DOWN_MASK),
SILK_SCREEN_TITLE);
swingUI.injectMenuAction(null, FILE_TITLE);
swingUI.injectSubmenu(INTEGRATION_TITLE, IconLoader.Node.getIcon(), FILE_TITLE);
swingUI.injectMenuAction(actionFactory.createImportBlocksAction(swingUI, plugInPort),
@@ -0,0 +1,63 @@
/*
DIY Layout Creator (DIYLC).
Copyright (c) 2009-2025 held jointly by the individual authors.
This file is part of DIYLC.
DIYLC is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
DIYLC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.swing.plugins.file;
import org.diylc.common.DrawOption;
import org.diylc.common.IPlugInPort;
import org.diylc.filter.PCBLayerFilter;
import org.diylc.filter.SilkScreenFilter;
import org.diylc.swingframework.IDrawingProvider;
import java.awt.*;
import java.util.*;
/**
* {@link IDrawingProvider} implementation that uses {@link IPlugInPort} to draw a project onto the
* canvas.
*
* @author Branislav Stojkovic
*/
public class SilkScreenDrawingProvider implements IDrawingProvider {
private IPlugInPort plugInPort;
public SilkScreenDrawingProvider(IPlugInPort plugInPort) {
super();
this.plugInPort = plugInPort;
}
@Override
public Dimension getSize() {
return plugInPort.getCanvasDimensions(false, false);
}
@Override
public void draw(int page, Graphics g, double zoomFactor) {
plugInPort.draw((Graphics2D) g, EnumSet.of(DrawOption.ANTIALIASING, DrawOption.OUTLINE_MODE, DrawOption.MONOCHROMATIC),
new SilkScreenFilter(), zoomFactor, null, null);
}
@Override
public int getPageCount() {
return 1;
}
}
@@ -37,7 +37,7 @@ import org.diylc.common.DrawOption;
import org.diylc.common.IPlugInPort;
import org.diylc.common.PCBLayer;
import org.diylc.core.IDIYComponent;
import org.diylc.presenter.PCBLayerFilter;
import org.diylc.filter.PCBLayerFilter;
/**
* {@link IDrawingProvider} implementation that uses {@link IPlugInPort} to draw a project onto the
@@ -0,0 +1,143 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.utils;
import java.awt.Font;
import java.util.Enumeration;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.IPlugInPort;
/**
* Utility class for applying a configurable font scaling factor to all UI components.
*
* The scaling factor is stored in the application configuration and can be set
* programmatically or through the configuration system.
* For example, a factor of 1.5 will make all fonts 50% larger.
*
* @author Branislav Stojkovic
*/
public class FontScalingUtils {
private static final double DEFAULT_FONT_SCALE_FACTOR = 1.0;
private static final double MIN_FONT_SCALE_FACTOR = 0.5;
private static final double MAX_FONT_SCALE_FACTOR = 3.0;
/**
* Gets the font scaling factor from ConfigurationManager.
*
* @return The scaling factor (defaults to 1.0 if not set or invalid)
*/
public static double getFontScaleFactor() {
try {
Object factorObj = ConfigurationManager.getInstance().readObject(
IPlugInPort.FONT_SCALE_FACTOR_KEY, DEFAULT_FONT_SCALE_FACTOR);
if (factorObj instanceof Number) {
double factor = ((Number) factorObj).doubleValue();
// Validate the factor is reasonable (between 0.5 and 3.0)
if (factor >= MIN_FONT_SCALE_FACTOR && factor <= MAX_FONT_SCALE_FACTOR) {
return factor;
} else {
System.err.println("Warning: Font scale factor " + factor + " is out of range ("
+ MIN_FONT_SCALE_FACTOR + "-" + MAX_FONT_SCALE_FACTOR + "). Using default "
+ DEFAULT_FONT_SCALE_FACTOR);
return DEFAULT_FONT_SCALE_FACTOR;
}
} else if (factorObj instanceof String) {
// Handle string values (for backward compatibility or manual config editing)
try {
double factor = Double.parseDouble((String) factorObj);
if (factor >= MIN_FONT_SCALE_FACTOR && factor <= MAX_FONT_SCALE_FACTOR) {
return factor;
}
} catch (NumberFormatException e) {
// Invalid string format, use default
}
}
} catch (Exception e) {
System.err.println("Warning: Could not read font scale factor from configuration: " + e.getMessage());
}
return DEFAULT_FONT_SCALE_FACTOR;
}
/**
* Sets the font scaling factor in ConfigurationManager.
*
* @param factor The scaling factor (must be between 0.5 and 3.0)
* @return true if the factor was set successfully, false if invalid
*/
public static boolean setFontScaleFactor(double factor) {
if (factor < MIN_FONT_SCALE_FACTOR || factor > MAX_FONT_SCALE_FACTOR) {
return false;
}
try {
ConfigurationManager.getInstance().writeValue(IPlugInPort.FONT_SCALE_FACTOR_KEY, factor);
return true;
} catch (Exception e) {
System.err.println("Warning: Could not write font scale factor to configuration: " + e.getMessage());
return false;
}
}
/**
* Applies the font scaling factor to all UIManager font defaults.
* This should be called once during application initialization, after setting the
* Look and Feel but before creating any UI components.
*
* The scaling factor is read from ConfigurationManager using the key
* IPlugInPort.FONT_SCALE_FACTOR_KEY. The factor should be a number between 0.5 and 3.0
* (e.g., 1.5 for 50% larger fonts). Use setFontScaleFactor() to change the value
* programmatically.
*/
public static void applyFontScaling() {
double scaleFactor = getFontScaleFactor();
// If factor is 1.0, no scaling needed
if (scaleFactor == 1.0) {
return;
}
try {
UIDefaults defaults = UIManager.getDefaults();
Enumeration<Object> keys = defaults.keys();
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
Object value = defaults.get(key);
// Check if this is a font property
if (value instanceof Font font) {
float newSize = (float) (font.getSize() * scaleFactor);
Font scaledFont = font.deriveFont(newSize);
defaults.put(key, scaledFont);
}
}
} catch (Exception e) {
System.err.println("Warning: Could not apply font scaling: " + e.getMessage());
}
}
}
@@ -72,7 +72,8 @@ public enum IconLoader {
ComponentLarge("component_large.png"), ImportNetlist("import_netlist.png"), SplashIC("splash_ic.png"),
ComponentPreferences("component_preferences.png"), ComponentReplace("component_replace.png"), Barcode("barcode.png"),
DocumentGerber("document_gerber.png"), DocumentX2("document_x2.png"), Export("export.png"), Multimeter("multimeter.png"),
Patreon("patreon.png"), Brain("brain.png"), Tables("tables.png");
Patreon("patreon.png"), Brain("brain.png"), Tables("tables.png"), Silkscreen("silkscreen.png"),
Project("project.png");
protected String name;
@@ -0,0 +1,71 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.utils;
import java.awt.Color;
import java.util.function.Function;
import javax.swing.UIManager;
/**
* Utility class for Swing UI-related operations.
*
* @author Branislav Stojkovic
*/
public class SwingUtils {
/**
* Fixes tooltip colors for Linux systems where they may be unreadable.
* Some Linux L&F themes set white text on light backgrounds, making tooltips invisible.
* This method ensures tooltips always have dark text on light background for readability.
*
* Should be called after UIManager.setLookAndFeel() to override system defaults.
*/
public static void fixTooltipColors() {
Color tooltipBg = UIManager.getColor("ToolTip.background");
Color tooltipFg = UIManager.getColor("ToolTip.foreground");
// Helper function to calculate brightness (0.0 = black, 1.0 = white)
Function<Color, Double> getBrightness = (c) -> {
return (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()) / 255.0;
};
// Check if we have a problematic color combination (light text on light background)
boolean needsFix = false;
if (tooltipFg != null && tooltipBg != null) {
double fgBrightness = getBrightness.apply(tooltipFg);
double bgBrightness = getBrightness.apply(tooltipBg);
// If both foreground and background are light, it's unreadable
if (fgBrightness > 0.7 && bgBrightness > 0.5) {
needsFix = true;
}
} else if (tooltipFg == null || tooltipBg == null) {
// Missing color definitions - set safe defaults
needsFix = true;
}
if (needsFix) {
UIManager.put("ToolTip.background", new Color(255, 255, 220)); // Light yellow
UIManager.put("ToolTip.foreground", Color.BLACK);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

@@ -195,6 +195,7 @@ Image|Bild
Immediate Only|Nur sofort
Import|Importieren
Import Building Blocks|Bausteine importieren
Import Netlist|Netlist importieren
Import Variants|Varianten importieren
In-Line D-44|Inline D-44
In-line|Inline
@@ -231,6 +232,7 @@ Loadline Editor|Kennliniendiagramm
Lock|Sperren
Log|Log
Log In|Anmelden
Look for|Suchen
Log Out|Abmelden
Lug Size|Lötpadgröße
Lugs|Lötpad
@@ -303,6 +305,7 @@ Power Rating|Nennleistung
Power Strip|Steckdosenleiste
Primary CT|Primärer Mittelabgriff
Print...|Drucken...
Project|Projekt
Project Cloud|Projekt Cloud
Public Announcement|Öffentliche Ankündigung
Radius|Radius
@@ -360,6 +363,7 @@ Shaft Up|Achse nach oben
Shape|Form
Shapes|Formen
Show Grid|Raster anzeigen
Show Resize Dimensions|Größenänderungsdimensionen anzeigen
Show Rulers|Lineale anzeigen
Shroud|Wanne
Shrouded|Abgeschirmt
@@ -457,3 +461,34 @@ Zoom|Vergrößern
base of 10|Zehntel
base of 2|Achtel
hold %s and drag to unstick from|Halten Sie X gedrückt und ziehen Sie, zum lösen von
Are you sure you want to clear the chat history for this project? This action cannot be undone!|Sind Sie sicher, dass Sie den Chat-Verlauf für dieses Projekt löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden!
Component reordering is possible only when Project Explorer is sorted by z-index.|Komponenten können nur neu angeordnet werden, wenn der Projekt-Explorer nach z-index sortiert ist.
Could not apply changes. Check the log for details.|Änderungen konnten nicht angewendet werden. Überprüfen Sie die Protokolldatei für Details.
Exporting outline for board %s to file %s|Exportiere Umriss für Platte %s in Datei %s
Failed to export the project to gerber. Please check the log for details|Fehler beim Exportieren des Projekts zu Gerber. Bitte überprüfen Sie die Protokolldatei für Details
Forgot Password?|Passwort vergessen?
Gerber Export|Gerber Export
Gerber export completed successfully. The following files were created:%s|Gerber-Export erfolgreich abgeschlossen. Die folgenden Dateien wurden erstellt:%s
Get Premium|Premium erhalten
Netlist files|Netlist-Dateien
No proximity issues detected.|Keine Näherungsprobleme erkannt.
Nothing to export.|Nichts zu exportieren.
Password|Passwort
Send|Senden
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?|Es gibt einige Komponenten, die außerhalb der Grenzen der Platten liegen.%sSie werden ignoriert, es sei denn, Sie fügen eine Plattenkomponente darunter hinzu.%sMöchten Sie fortfahren?
User Name|Benutzername
AI Circuit Analyzer|KI-Schaltungsanalysator
Cloud|Cloud
Compare With...|Vergleichen mit...
Connect to Patreon|Mit Patreon verbinden
Export|Exportieren
Export to Gerber|Als Gerber exportieren
Generate DIYLC Netlist (excl. Switches)|DIYLC-Netzliste erstellen (ohne Schalter)
Generate DIYLC Netlist (incl. Switches)|DIYLC-Netzliste erstellen (mit Schaltern)
Generate Spice Netlist (excl. Switches)|Spice-Netzliste erstellen (ohne Schalter)
Generate Spice Netlist (incl. Switches)|Spice-Netzliste erstellen (mit Schaltern)
Silkscreen|Siebdruck
Virtual Multimeter (excl. Switches)|Virtuelles Multimeter (ohne Schalter)
Virtual Multimeter (incl. Switches)|Virtuelles Multimeter (mit Schaltern)
Become a Patreon and enjoy perks|Werden Sie Patreon und genießen Sie Vorteile
Join Patreon|Patreon beitreten
@@ -300,6 +300,7 @@ Power Rating
Power Strip
Primary CT
Print...
Project
Project Cloud
Public Announcement
Radius
@@ -351,6 +352,7 @@ Shape
Shapes
Show Bracket
Show Grid
Show Resize Dimensions
Show Rulers
Shroud
Shrouded
@@ -462,4 +464,35 @@ Update Axis
Apply Variant
Save as Building Block
Select
Unlock
Unlock
Are you sure you want to clear the chat history for this project? This action cannot be undone!
Component reordering is possible only when Project Explorer is sorted by z-index.
Could not apply changes. Check the log for details.
Exporting outline for board %s to file %s
Failed to export the project to gerber. Please check the log for details
Forgot Password?
Gerber Export
Gerber export completed successfully. The following files were created:%s
Get Premium
Netlist files
No proximity issues detected.
Nothing to export.
Password
Send
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?
User Name
AI Circuit Analyzer
Cloud
Compare With...
Connect to Patreon
Export
Export to Gerber
Generate DIYLC Netlist (excl. Switches)
Generate DIYLC Netlist (incl. Switches)
Generate Spice Netlist (excl. Switches)
Generate Spice Netlist (incl. Switches)
Silkscreen
Virtual Multimeter (excl. Switches)
Virtual Multimeter (incl. Switches)
Become a Patreon and enjoy perks
Join Patreon
@@ -193,6 +193,7 @@ Image|Imagen
Immediate Only|Solo Inmediato
Import|Importar
Import Building Blocks|Importar Bloques
Import Netlist|Importar Lista de Redes
Import Variants|Importar variantes
In-Line D-44|En Lí­nea D-44
In-line|En línea
@@ -298,6 +299,7 @@ Power Rating|Potencia Nominal
Power Strip|Regleta
Primary CT|Toma Central en primario
Print...|Imprimir...
Project|Proyecto
Project Cloud|Proyectos en la Nube
Public Announcement|Anuncio Público
Radius|Radio
@@ -349,6 +351,7 @@ Shaft Up|Eje Arriba
Shape|Forma
Shapes|Formas
Show Grid|Mostrar Cuadrícula
Show Resize Dimensions|Mostrar Dimensiones de Redimensionamiento
Show Rulers|Mostrar Reglas
Shroud|Envoltura
Shrouded|Con Envoltura
@@ -457,4 +460,35 @@ Update Axis|Actualizar Ejes
Apply Variant|Aplicar Variante
Save as Building Block|Guardar como Bloque
Select|Seleccionar
Unlock|Desbloquear
Unlock|Desbloquear
Are you sure you want to clear the chat history for this project? This action cannot be undone!|¿Está seguro de que desea borrar el historial de chat de este proyecto? ¡Esta acción no se puede deshacer!
Component reordering is possible only when Project Explorer is sorted by z-index.|El reordenamiento de componentes solo es posible cuando el Explorador de Proyectos está ordenado por z-index.
Could not apply changes. Check the log for details.|No se pudieron aplicar los cambios. Verifique el registro para más detalles.
Exporting outline for board %s to file %s|Exportando el contorno de la placa %s al archivo %s
Failed to export the project to gerber. Please check the log for details|No se pudo exportar el proyecto a gerber. Por favor verifique el registro para más detalles
Forgot Password?|¿Olvidó la Contraseña?
Gerber Export|Exportar Gerber
Gerber export completed successfully. The following files were created:%s|La exportación Gerber se completó exitosamente. Se crearon los siguientes archivos:%s
Get Premium|Obtener Premium
Netlist files|Archivos de lista de red
No proximity issues detected.|No se detectaron problemas de proximidad.
Nothing to export.|Nada que exportar.
Password|Contraseña
Send|Enviar
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?|Hay algunos componentes que están fuera de los límites de las placas.%sSerán ignorados a menos que agregue un componente de placa debajo de ellos.%s¿Desea continuar?
User Name|Nombre de Usuario
AI Circuit Analyzer|Analizador de Circuitos IA
Cloud|Nube
Compare With...|Comparar con...
Connect to Patreon|Conectar con Patreon
Export|Exportar
Export to Gerber|Exportar a Gerber
Generate DIYLC Netlist (excl. Switches)|Generar Lista de Redes DIYLC (sin interruptores)
Generate DIYLC Netlist (incl. Switches)|Generar Lista de Redes DIYLC (con interruptores)
Generate Spice Netlist (excl. Switches)|Generar Lista Especial de Redes (sin interruptores)
Generate Spice Netlist (incl. Switches)|Generar Lista Especial de Redes (con interruptores)
Silkscreen|Serigrafía
Virtual Multimeter (excl. Switches)|Multímetro Virtual (sin interruptores)
Virtual Multimeter (incl. Switches)|Multímetro Virtual (con interruptores)
Become a Patreon and enjoy perks|Conviértete en Patreon y disfruta de beneficios
Join Patreon|Únete a Patreon
@@ -193,6 +193,7 @@ Image|Image
Immediate Only|Immédiatement proches seulement
Import|Importer
Import Building Blocks|Importer des blocs de construction
Import Netlist|Importer la liste de connexions
Import Variants|Importer des variantes
In-Line D-44|En ligne D-44
In-line|En ligne
@@ -226,6 +227,7 @@ Light|Lumière
Lock|Verrouiller
Log In|Se connecter
Log Out|Se déconnecter
Look for|Rechercher
Lug Size|Taille des cosses
Lugs|Cosses
Make a Donation|Faire un don
@@ -294,6 +296,7 @@ Power Rating|Puissance nominale
Power Strip|Multiprise
Primary CT|Prise centrale primaire
Print...|Imprimer...
Project|Projet
Project Cloud|Projet Cloud
Public Announcement|Annonce publique
Radius|Rayon
@@ -345,6 +348,7 @@ Shaft Up|Tige vers le haut
Shape|Forme
Shapes|Formes
Show Grid|Afficher la grille
Show Resize Dimensions|Afficher les dimensions de redimensionnement
Show Rulers|Afficher les règles
Shroud|Enveloppe
Shrouded|Enveloppé
@@ -440,17 +444,48 @@ Zoom|Zoom
base of 10|base de 10
base of 2|base de 2
hold %s and drag to unstick from|maintenez %s et faites glisser pour décoller
Add a Curve
Clear
Insert Image
Load From File
Loadline Editor
Max Current (mA)
Max Dissipation (W)
Max Voltage (V)
Save To File
Update Axis
Apply Variant
Save as Building Block
Select
Unlock
Add a Curve|Ajouter une courbe
Clear|Effacer
Insert Image|Insérer une image
Load From File|Charger depuis un fichier
Loadline Editor|Éditeur de ligne de charge
Max Current (mA)|Courant maximum (mA)
Max Dissipation (W)|Dissipation maximale (W)
Max Voltage (V)|Tension maximale (V)
Save To File|Enregistrer dans un fichier
Update Axis|Mettre à jour les axes
Apply Variant|Appliquer la variante
Save as Building Block|Enregistrer comme bloc de construction
Select|Sélectionner
Unlock|Déverrouiller
Are you sure you want to clear the chat history for this project? This action cannot be undone!|Êtes-vous sûr de vouloir effacer l'historique de chat de ce projet? Cette action ne peut pas être annulée!
Component reordering is possible only when Project Explorer is sorted by z-index.|Le réordonnancement des composants n'est possible que lorsque l'Explorateur de projet est trié par z-index.
Could not apply changes. Check the log for details.|Impossible d'appliquer les modifications. Consultez le journal pour plus de détails.
Exporting outline for board %s to file %s|Exportation du contour de la carte %s vers le fichier %s
Failed to export the project to gerber. Please check the log for details|Échec de l'exportation du projet vers gerber. Veuillez consulter le journal pour plus de détails
Forgot Password?|Mot de passe oublié?
Gerber Export|Export Gerber
Gerber export completed successfully. The following files were created:%s|L'export Gerber a été complété avec succès. Les fichiers suivants ont été créés:%s
Get Premium|Obtenir Premium
Netlist files|Fichiers de liste de connexions
No proximity issues detected.|Aucun problème de proximité détecté.
Nothing to export.|Rien à exporter.
Password|Mot de passe
Send|Envoyer
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?|Il y a des composants qui sont en dehors des limites des cartes.%sIls seront ignorés sauf si vous ajoutez un composant de carte en dessous.%sVoulez-vous continuer?
User Name|Nom d'utilisateur
AI Circuit Analyzer|Analyseur de circuit IA
Cloud|Cloud
Compare With...|Comparer avec...
Connect to Patreon|Se connecter à Patreon
Export|Exporter
Export to Gerber|Exporter vers Gerber
Generate DIYLC Netlist (excl. Switches)|Générer la Netlist DIYLC (sans commutateurs)
Generate DIYLC Netlist (incl. Switches)|Générer la Netlist DIYLC (avec commutateurs)
Generate Spice Netlist (excl. Switches)|Générer la Netlist Spice (sans commutateurs)
Generate Spice Netlist (incl. Switches)|Générer la Netlist Spice (avec commutateurs)
Silkscreen|Sérigraphie
Virtual Multimeter (excl. Switches)|Multimètre virtuel (sans commutateurs)
Virtual Multimeter (incl. Switches)|Multimètre virtuel (avec commutateurs)
Become a Patreon and enjoy perks|Devenez un Patreon et profitez d'avantages
Join Patreon|Rejoindre Patreon
@@ -196,6 +196,7 @@ Image|Immagine
Immediate Only|Solo immediato
Import|Importa
Import Building Blocks|Importa blocchi di costruzione
Import Netlist|Importa netlist
Import Variants|Importa varianti
In-Line D-44|In linea D-44
In-line|In linea
@@ -235,6 +236,7 @@ Lock|Blocco
Log|Accesso
Log In|Accedi
Log Out|Esci
Look for|Cerca
Lug Size|Dimensione dell'aletta
Lugs|Alette
Make a Donation|Fai una donazione
@@ -307,6 +309,7 @@ Power Rating|Potenza nominale
Power Strip|Ciabatta
Primary CT|CT primario
Print...|Stampa ...
Project|Progetto
Project Cloud|Progetto Cloud
Public Announcement|Annuncio pubblico
Radius|raggio
@@ -360,6 +363,7 @@ Shaft Up|Shaft Up
Shape|forma
Shapes|forme
Show Grid|Mostra griglia
Show Resize Dimensions|Mostra dimensioni ridimensionamento
Show Rulers|Mostra righelli
Shroud|Copri
Shrouded|Coperto
@@ -456,4 +460,35 @@ Y-axis|Asse Y
Zoom|Ingrandimento
base of 10|base di 10
base of 2|base di 2
hold %s and drag to unstick from|tieni premuto %s e trascina per sbloccare da
hold %s and drag to unstick from|tieni premuto %s e trascina per sbloccare da
Are you sure you want to clear the chat history for this project? This action cannot be undone!|Sei sicuro di voler cancellare la cronologia della chat per questo progetto? Questa azione non può essere annullata!
Component reordering is possible only when Project Explorer is sorted by z-index.|Il riordinamento dei componenti è possibile solo quando Project Explorer è ordinato per z-index.
Could not apply changes. Check the log for details.|Impossibile applicare le modifiche. Controlla il registro per i dettagli.
Exporting outline for board %s to file %s|Esportazione del contorno per la scheda %s nel file %s
Failed to export the project to gerber. Please check the log for details|Impossibile esportare il progetto in gerber. Controlla il registro per i dettagli
Forgot Password?|Password dimenticata?
Gerber Export|Esporta Gerber
Gerber export completed successfully. The following files were created:%s|Esportazione Gerber completata con successo. Sono stati creati i seguenti file:%s
Get Premium|Ottieni Premium
Netlist files|File netlist
No proximity issues detected.|Nessun problema di prossimità rilevato.
Nothing to export.|Niente da esportare.
Password|Password
Send|Invia
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?|Ci sono alcuni componenti che sono al di fuori dei limiti delle schede.%sVerranno ignorati a meno che non aggiungi un componente scheda sotto di loro.%sVuoi continuare?
User Name|Nome utente
AI Circuit Analyzer|Analizzatore di Circuiti IA
Cloud|Cloud
Compare With...|Confronta con...
Connect to Patreon|Connetti a Patreon
Export|Esporta
Export to Gerber|Esporta in Gerber
Generate DIYLC Netlist (excl. Switches)|Genera netlist DIYLC (escl. interruttori)
Generate DIYLC Netlist (incl. Switches)|Genera netlist DIYLC (incl. interruttori)
Generate Spice Netlist (excl. Switches)|Genera netlist Spice (escl. interruttori)
Generate Spice Netlist (incl. Switches)|Genera netlist Spice (incl. interruttori)
Silkscreen|Serigrafia
Virtual Multimeter (excl. Switches)|Multimetro Virtuale (escl. interruttori)
Virtual Multimeter (incl. Switches)|Multimetro Virtuale (incl. interruttori)
Become a Patreon and enjoy perks|Diventa un Patreon e goditi i vantaggi
Join Patreon|Unisciti a Patreon
@@ -195,6 +195,7 @@ Image|Afbeelding
Immediate Only|Alleen direct
Import|Importeren
Import Building Blocks|Bouwstenen importeren
Import Netlist|Netlist importeren
Import Variants|Varianten importeren
In-Line D-44|Inline D-44
In-line|Inline
@@ -232,6 +233,7 @@ Lock|Vastpinnen
Log|Logaritmisch
Log In|Inloggen
Log Out|Uitloggen
Look for|Zoeken
Lug Size|Solderooggrootte
Lugs|Solderogen
Make a Donation|Doneren
@@ -303,6 +305,7 @@ Power Rating|Nominale vermogen
Power Strip|Voedingslijn
Primary CT|Primaire middenaftakking
Print...|Afdrukken...
Project|Project
Project Cloud|Project cloud
Public Announcement|Openbare aankondiging
Radius|Straal
@@ -360,6 +363,7 @@ Shaft Up|As omhoog
Shape|Vorm
Shapes|Vormen
Show Grid|Raster weergeven
Show Resize Dimensions|Grootte-aanpassingsafmetingen weergeven
Show Rulers|Linialen weergeven
Shroud|Beschermrand
Shrouded|Afgeschermd
@@ -456,4 +460,35 @@ Y-axis|Y-as
Zoom|Zoom
base of 10|Tienden
base of 2|Achtsten
hold %s and drag to unstick from|houd %s vast en sleep, om los te maken van
hold %s and drag to unstick from|houd %s vast en sleep, om los te maken van
Are you sure you want to clear the chat history for this project? This action cannot be undone!|Weet u zeker dat u de chatgeschiedenis voor dit project wilt wissen? Deze actie kan niet ongedaan worden gemaakt!
Component reordering is possible only when Project Explorer is sorted by z-index.|Het opnieuw ordenen van componenten is alleen mogelijk wanneer Project Explorer is gesorteerd op z-index.
Could not apply changes. Check the log for details.|Wijzigingen konden niet worden toegepast. Controleer het logbestand voor details.
Exporting outline for board %s to file %s|Omtrek exporteren voor printplaat %s naar bestand %s
Failed to export the project to gerber. Please check the log for details|Het exporteren van het project naar gerber is mislukt. Controleer het logbestand voor details
Forgot Password?|Wachtwoord vergeten?
Gerber Export|Gerber Export
Gerber export completed successfully. The following files were created:%s|Gerber export succesvol voltooid. De volgende bestanden zijn aangemaakt:%s
Get Premium|Premium krijgen
Netlist files|Netlist bestanden
No proximity issues detected.|Geen nabijheidsproblemen gedetecteerd.
Nothing to export.|Niets te exporteren.
Password|Wachtwoord
Send|Verzenden
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?|Er zijn enkele componenten die buiten de grenzen van printplaten liggen.%sZe worden genegeerd tenzij u een printplaatcomponent eronder toevoegt.%sWilt u doorgaan?
User Name|Gebruikersnaam
AI Circuit Analyzer|AI Circuit Analysator
Cloud|Cloud
Compare With...|Vergelijken met...
Connect to Patreon|Verbinden met Patreon
Export|Exporteren
Export to Gerber|Exporteren naar Gerber
Generate DIYLC Netlist (excl. Switches)|DIYLC netlist genereren (zonder schakelaars)
Generate DIYLC Netlist (incl. Switches)|DIYLC netlist genereren (met schakelaars)
Generate Spice Netlist (excl. Switches)|Spice netlist genereren (zonder schakelaars)
Generate Spice Netlist (incl. Switches)|Spice netlist genereren (met schakelaars)
Silkscreen|Zijdezeefdruk
Virtual Multimeter (excl. Switches)|Virtuele Multimeter (zonder schakelaars)
Virtual Multimeter (incl. Switches)|Virtuele Multimeter (met schakelaars)
Become a Patreon and enjoy perks|Word een Patreon en geniet van voordelen
Join Patreon|Lid worden van Patreon
@@ -193,6 +193,7 @@ Image|Imagem
Immediate Only|Apenas Imediato
Import|Importar
Import Building Blocks|Importar Blocos
Import Netlist|Importar Lista de Redes
Import Variants|Importar variações
In-Line D-44|Em linha D-44
In-line|Em linha
@@ -298,6 +299,7 @@ Power Rating|Potência Nominal
Power Strip|Listra Eletrificada
Primary CT|CT Primário
Print...|Imprimir...
Project|Projeto
Project Cloud|Projetos na Núvem
Public Announcement|Anúncio Público
Radius|Raio
@@ -349,6 +351,7 @@ Shaft Up|Jogar para cima
Shape|Forma
Shapes|Formas
Show Grid|Mostrar Grid
Show Resize Dimensions|Mostrar Dimensões de Redimensionamento
Show Rulers|Mostrar Réguas
Shroud|Envoltura
Shrouded|Envolto
@@ -358,7 +361,7 @@ Size|Tamanho
Sizing Mode|Modo de Dimensionamento
Small-button B7G|Botão pequeno B7G
Smooth|Suave
Snap To...|Ajustarse a...
Snap To|Ajustar a
Soap Bar|Soap Bar
Solid|Sólido
South|Sul
@@ -457,4 +460,35 @@ Update Axis|Atualizar Eixos
Apply Variant|Aplicar Variante
Save as Building Block|Salvar como Bloco
Select|Selecionar
Unlock|Desbloquear
Unlock|Desbloquear
Are you sure you want to clear the chat history for this project? This action cannot be undone!|Tem certeza de que deseja limpar o histórico de chat deste projeto? Esta ação não pode ser desfeita!
Component reordering is possible only when Project Explorer is sorted by z-index.|A reordenação de componentes só é possível quando o Explorador de Projetos está ordenado por z-index.
Could not apply changes. Check the log for details.|Não foi possível aplicar as alterações. Verifique o registro para mais detalhes.
Exporting outline for board %s to file %s|Exportando o contorno da placa %s para o arquivo %s
Failed to export the project to gerber. Please check the log for details|Falha ao exportar o projeto para gerber. Por favor, verifique o registro para mais detalhes
Forgot Password?|Esqueceu a Senha?
Gerber Export|Exportar Gerber
Gerber export completed successfully. The following files were created:%s|A exportação Gerber foi concluída com sucesso. Os seguintes arquivos foram criados:%s
Get Premium|Obter Premium
Netlist files|Arquivos de lista de rede
No proximity issues detected.|Nenhum problema de proximidade detectado.
Nothing to export.|Nada para exportar.
Password|Senha
Send|Enviar
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?|Há alguns componentes que estão fora dos limites das placas.%sEles serão ignorados a menos que você adicione um componente de placa abaixo deles.%sDeseja continuar?
User Name|Nome de Usuário
AI Circuit Analyzer|Analisador de Circuito IA
Cloud|Nuvem
Compare With...|Comparar com...
Connect to Patreon|Conectar ao Patreon
Export|Exportar
Export to Gerber|Exportar para Gerber
Generate DIYLC Netlist (excl. Switches)|Gerar Lista de Redes DIYLC (sem interruptores)
Generate DIYLC Netlist (incl. Switches)|Gerar Lista de Redes DIYLC (com interruptores)
Generate Spice Netlist (excl. Switches)|Gerar Lista Especial de Redes (sem interruptores)
Generate Spice Netlist (incl. Switches)|Gerar Lista Especial de Redes (com interruptores)
Silkscreen|Serigrafia
Virtual Multimeter (excl. Switches)|Multímetro Virtual (sem interruptores)
Virtual Multimeter (incl. Switches)|Multímetro Virtual (com interruptores)
Become a Patreon and enjoy perks|Torne-se um Patreon e aproveite benefícios
Join Patreon|Juntar-se ao Patreon
@@ -191,6 +191,7 @@ Image|Slika
Immediate Only|Samo Neposredne
Import|Uvoz
Import Building Blocks|Uvezi Blokove za Gradnju
Import Netlist|Uvezi Netlistu
Import Variants|Uvezi Varijacije
In-Line D-44|U Redu D-44
In-line|U Redu
@@ -224,6 +225,7 @@ Light|Svetla
Lock|Zaključaj
Log In|Prijava
Log Out|Odjava
Look for|Traži
Lug Size|Veličina Ušica
Lugs|Ušice
Make a Donation|Doniraj
@@ -291,6 +293,7 @@ Power Rating|Snaga
Power Strip|Naponska Magistrala
Primary CT|Primarni Srednji Izvod
Print...|Štampa...
Project|Projekat
Project Cloud|Projektni Oblak
Public Announcement|Javna Objava
Radius|Poluprečnik
@@ -341,6 +344,7 @@ Shaft Up|Osovina Gore
Shape|Oblik
Shapes|Oblici
Show Grid|Prikaz Rešetke
Show Resize Dimensions|Prikaz Dimenzija Promene Veličine
Show Rulers|Prikaz Lenjira
Shroud|Kućište
Shrouded|Sa Kućištem
@@ -449,4 +453,35 @@ Update Axis|Osveži Ose
Apply Variant|Primeni Varijaciju
Save as Building Block|Sačuvaj Kao Blok
Select|Izaberi
Unlock|Otključaj
Unlock|Otključaj
Are you sure you want to clear the chat history for this project? This action cannot be undone!|Da li ste sigurni da želite da obrišete istoriju četa za ovaj projekat? Ova akcija se ne može poništiti!
Component reordering is possible only when Project Explorer is sorted by z-index.|Preuređivanje komponenti je moguće samo kada je Project Explorer sortiran po z-index-u.
Could not apply changes. Check the log for details.|Nije moguće primeniti izmene. Proverite log za više informacija.
Exporting outline for board %s to file %s|Izvoz obrisa za pločicu %s u datoteku %s
Failed to export the project to gerber. Please check the log for details|Neuspelo izvoženje projekta u gerber. Proverite log za više informacija
Forgot Password?|Zaboravili ste lozinku?
Gerber Export|Gerber Izvoz
Gerber export completed successfully. The following files were created:%s|Gerber izvoz je uspešno završen. Sledeće datoteke su kreirane:%s
Get Premium|Dobij Premium
Netlist files|Netlist datoteke
No proximity issues detected.|Nisu detektovani problemi sa blizinom.
Nothing to export.|Nema ništa za izvoz.
Password|Lozinka
Send|Pošalji
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?|Postoje neke komponente koje su van granica pločica.%sOne će biti ignorisane osim ako ne dodate komponentu pločice ispod njih.%sDa li želite da nastavite?
User Name|Korisničko Ime
AI Circuit Analyzer|AI Analizator Kola
Cloud|Oblak
Compare With...|Uporedi sa...
Connect to Patreon|Poveži sa Patreon-om
Export|Izvoz
Export to Gerber|Izvoz u Gerber
Generate DIYLC Netlist (excl. Switches)|Generiši DIYLC Netlistu (bez prekidača)
Generate DIYLC Netlist (incl. Switches)|Generiši DIYLC Netlistu (sa prekidačima)
Generate Spice Netlist (excl. Switches)|Generiši Spice Netlistu (bez prekidača)
Generate Spice Netlist (incl. Switches)|Generiši Spice Netlistu (sa prekidačima)
Silkscreen|Serigrafija
Virtual Multimeter (excl. Switches)|Virtuelni Multimetar (bez prekidača)
Virtual Multimeter (incl. Switches)|Virtuelni Multimetar (sa prekidačima)
Become a Patreon and enjoy perks|Postanite Patreon i uživajte u prednostima
Join Patreon|Pridružite se Patreon-u
@@ -0,0 +1,499 @@
#start-internal
icon=india
#end-internal
(Building Blocks)|(बिल्डिंग ब्लॉक्स)
(Favorites)|(पसंदीदा)
(Recently Used)|(हाल ही में उपयोग किए गए)
180 degrees clockwise|दक्षिणावर्त 180 डिग्री
270 degrees clockwise|दक्षिणावर्त 270 डिग्री
90 degrees clockwise|दक्षिणावर्त 90 डिग्री
A variant with that name already exists. Overwrite?|उस नाम का एक वेरिएंट पहले से मौजूद है। अधिलेखित करें?
About|के बारे में
Above|ऊपर
Access User Files|उपयोगकर्ता फ़ाइलें एक्सेस करें
Add Flexible Leads|लचीली लीड जोड़ें
Add Validation Step|सत्यापन चरण जोड़ें
All Connected|सभी जुड़े हुए
Alpha|अल्फा
Analyze|विश्लेषण करें
Analyze Guitar Diagrams|गिटार आरेखों का विश्लेषण करें
Angle|कोण
Anti-Aliasing|एंटी-अलियासिंग
Arrow Size|तीर का आकार
Author|लेखक
Auto-Create Pads|ऑटो-क्रिएट पैड
Auto-Edit Mode|ऑटो-एडिट मोड
Base|आधार
Below|नीचे
Blueprint|ब्लूप्रिंट
Board|बोर्ड
Boards|बोर्ड
Board Color|बोर्ड का रंग
Board Width|बोर्ड की चौड़ाई
Bobbin 1|बॉबिन 1
Bobbin 2|बॉबिन 2
Body|शरीर
Border|बॉर्डर
Border Thickness|बॉर्डर की मोटाई
Both|दोनों
Both Sides|दोनों तरफ
Bottom|नीचे
Bottom Left|नीचे बाएं
Bottom Right|नीचे दाएं
Bracket|ब्रैकेट
Bring Forward|आगे लाएं
Bring Selection to Front|चयन को सामने लाएं
Buffer|बफर
Center|केंद्र
Center Negative|केंद्र नकारात्मक
Center Positive|केंद्र सकारात्मक
Center Terminal|केंद्र टर्मिनल
Central|केंद्रीय
Change Password|पासवर्ड बदलें
Channel|चैनल
Chassis|चेसिस
Check Trace Proximity|ट्रेस निकटता जांचें
Circular|गोलाकार
Click on a component to highlight area directly or indirectly connected to the area below the cursor. To continue editing, this mode needs to be switched OFF|कर्सर के नीचे के क्षेत्र से सीधे या अप्रत्यक्ष रूप से जुड़े क्षेत्र को हाइलाइट करने के लिए एक घटक पर क्लिक करें। संपादन जारी रखने के लिए, इस मोड को बंद करना होगा
Click on the canvas to create a new %s or press %s to cancel|एक नया %s बनाने के लिए कैनवास पर क्लिक करें या रद्द करने के लिए %s दबाएं
Click on the canvas to set the %s control point of a new %s or press %s to cancel|एक नए %s के %s कंट्रोल पॉइंट सेट करने के लिए कैनवास पर क्लिक करें या रद्द करने के लिए %s दबाएं
Click the lightbulb icon in the bottom-right corner of the window for more info.|अधिक जानकारी के लिए विंडो के निचले दाएं कोने में लाइटबल्ब आइकन पर क्लिक करें।
Click to fetch the most recent public announcement|सबसे हाल का सार्वजनिक घोषणा प्राप्त करने के लिए क्लिक करें
Click to show recent changes and updates|हाल के परिवर्तन और अपडेट दिखाने के लिए क्लिक करें
Click to toggle to the next option|अगले विकल्प पर जाने के लिए क्लिक करें
Coil|कुंडली
Coil Border|कुंडली बॉर्डर
Coil Length|कुंडली की लंबाई
Coil Width|कुंडली की चौड़ाई
Color|रंग
Color 1|रंग 1
Color 2|रंग 2
Color code|रंग कोड
Column Spacing|कॉलम स्पेसिंग
Columns|कॉलम
Component|घटक
Component API|घटक API
Components|घटक
Config|कॉन्फ़िग
Connector|कनेक्टर
Connector (Mirrored)|कनेक्टर (मिरर किया गया)
Connectivity|कनेक्टिविटी
Contacts|संपर्क
Continuous Creation|निरंतर निर्माण
Coordinate Color|निर्देशांक रंग
Coordinate Origin|निर्देशांक मूल
Coordinates|निर्देशांक
Copy|कॉपी करें
Core|कोर
Core Border|कोर बॉर्डर
Core Thickness|कोर की मोटाई
Core Width|कोर की चौड़ाई
Could not create a new file. Check the log for details.|नई फ़ाइल नहीं बना सके। विवरण के लिए लॉग जांचें।
Could not create component. Check log for details.|घटक नहीं बना सके। विवरण के लिए लॉग जांचें।
Could not fetch public announcements.|सार्वजनिक घोषणाएं प्राप्त नहीं कर सके।
Could not save the project to file. Check the log for details.|प्रोजेक्ट को फ़ाइल में सहेज नहीं सके। विवरण के लिए लॉग जांचें।
Could not set component type slot. Check log for details.|घटक प्रकार स्लॉट सेट नहीं कर सके। विवरण के लिए लॉग जांचें।
Cover|कवर
Create B.O.M.|B.O.M. बनाएं
Create New Account|नया खाता बनाएं
Current|वर्तमान
Cut|काटें
Cut Between Holes|छेदों के बीच काटें
Cut Orientation|कट ओरिएंटेशन
DIP (Mirrored)|DIP (मिरर किया गया)
DP3T (Standard 3-Position Strat)|DP3T (स्टैंडर्ड 3-पोजीशन Strat)
DP3T (Standard 5-Position Strat)|DP3T (स्टैंडर्ड 5-पोजीशन Strat)
DP4T (4-Position Tele)|DP4T (4-पोजीशन Tele)
Dark|डार्क
Dashed|डैश किया गया
Decimals|दशमलव
Default|डिफ़ॉल्ट
Default Font|डिफ़ॉल्ट फ़ॉन्ट
Default Font Size|डिफ़ॉल्ट फ़ॉन्ट साइज़
Delete Selection|चयन हटाएं
Description|विवरण
Diameter|व्यास
Directional|दिशात्मक
Directly heated|सीधे गर्म
Display|डिस्प्ले
Display Pin #s|पिन #s दिखाएं
Dog Ear|डॉग ईयर
Dot Spacing|डॉट स्पेसिंग
Donate|दान करें
Dotted|बिंदीदार
Drag control point(s) of|कंट्रोल पॉइंट खींचें
Drag the mouse over the canvas to place a new|एक नया रखने के लिए कैनवास पर माउस खींचें
Dual Rail Humbucker|ड्यूल रेल हम्बकर
Dual Rods|ड्यूल रॉड्स
Duodecar B12C|Duodecar B12C
Duplicate|डुप्लिकेट
Edges|किनारे
Edit|संपादित करें
Edit Project Settings|प्रोजेक्ट सेटिंग्स संपादित करें
Edit Selection|चयन संपादित करें
Electrode Labels|इलेक्ट्रोड लेबल
Electro-Mechanical|इलेक्ट्रो-मैकेनिकल
Enable Cache Boost|कैश बूस्ट सक्षम करें
End Arrow|अंत तीर
Enjoying DIYLC? Click here to buy me a coffee :)|DIYLC का आनंद ले रहे हैं? मुझे कॉफी खरीदने के लिए यहां क्लिक करें :)
Error|त्रुटि
Error occurred while editing selection. Check the log for details.|चयन संपादित करते समय त्रुटि हुई। विवरण के लिए लॉग जांचें।
Exit|बाहर निकलें
Expand Selection|चयन विस्तारित करें
Explicit Dimensions|स्पष्ट आयाम
Explicit Length|स्पष्ट लंबाई
Explicit Width|स्पष्ट चौड़ाई
Export Building Blocks|बिल्डिंग ब्लॉक्स निर्यात करें
Export Grid|ग्रिड निर्यात करें
Export Variants|वेरिएंट निर्यात करें
Export to PDF|PDF में निर्यात करें
Export to PNG|PNG में निर्यात करें
Extra Working Area|अतिरिक्त कार्य क्षेत्र
Eyelet color|आईलेट रंग
FAQ|अक्सर पूछे जाने वाले प्रश्न
File|फ़ाइल
Filtertron|Filtertron
Find|खोजें
Five|पांच
Flip|फ्लिप करें
Folded|तह किया गया
Font|फ़ॉन्ट
Font Bold|फ़ॉन्ट बोल्ड
Font Italic|फ़ॉन्ट इटैलिक
Font Size|फ़ॉन्ट साइज़
Font Size Override|फ़ॉन्ट साइज़ ओवरराइड
Four|चार
Full|पूर्ण
Fuse|फ्यूज
Generate DIYLC Netlist|DIYLC नेटलिस्ट जेनरेट करें
Generate Spice Netlist|Spice नेटलिस्ट जेनरेट करें
Grid|ग्रिड
Grid Spacing|ग्रिड स्पेसिंग
Group Selection|चयन समूहित करें
Guitar|गिटार
Half|आधा
Hardware Acceleration|हार्डवेयर एक्सेलेरेशन
Heaters|हीटर
Height|ऊंचाई
Help|मदद
Hi-Quality Rendering|उच्च गुणवत्ता रेंडरिंग
Highlight Connected Areas|जुड़े क्षेत्र हाइलाइट करें
Hole|छेद
Hole Size|छेद का आकार
Hole Spacing|छेद स्पेसिंग
Hole size|छेद का आकार
Holes per strip|प्रति स्ट्रिप छेद
Horizontal|क्षैतिज
Horizontal Alignment|क्षैतिज संरेखण
Horizontal Large|क्षैतिज बड़ा
Horizontal Medium|क्षैतिज मध्यम
Horizontal Small 1|क्षैतिज छोटा 1
Horizontal Small 2|क्षैतिज छोटा 2
Horizontal X-Small|क्षैतिज एक्स-छोटा
Humbucking|हम्बकिंग
If this box is checked application will use the current value as a<br>default when creating new components of the same type|यदि यह बॉक्स चेक किया गया है, तो एप्लिकेशन समान प्रकार के नए घटक बनाते समय वर्तमान मान को <br>डिफ़ॉल्ट के रूप में उपयोग करेगा
Image|छवि
Immediate Only|केवल तत्काल
Import|आयात करें
Import Building Blocks|बिल्डिंग ब्लॉक्स आयात करें
Import Netlist|नेटलिस्ट आयात करें
Import Variants|वेरिएंट आयात करें
In-Line D-44|इन-लाइन D-44
In-line|इन-लाइन
Indent|इंडेंट
Information|जानकारी
Inline|इनलाइन
Input error for "%s": %s|"%s" के लिए इनपुट त्रुटि: %s
Inside color|अंदर का रंग
Integration|एकीकरण
Invert polarity|ध्रुवता उलटें
Label|लेबल
Label Color|लेबल रंग
Label Orientation|लेबल ओरिएंटेशन
Label Position|लेबल स्थिति
Labels|लेबल
Language|भाषा
Language selection will be applied after the application is restarted.|भाषा चयन एप्लिकेशन रीस्टार्ट होने के बाद लागू होगा।
Layer|लेयर
Layers|लेयर
Lead Color|लीड रंग
Lead Count|लीड संख्या
Lead Length|लीड लंबाई
Lead Spacing|लीड स्पेसिंग
Left|बाएं
Left-to-Right|बाएं से दाएं
Left click to instantiate this component, right click for more options|इस घटक को इंस्टेंट करने के लिए बाएं क्लिक करें, अधिक विकल्पों के लिए दाएं क्लिक करें
Legs|पैर
Length|लंबाई
Letters|अक्षर
Light|लाइट
Lock|लॉक करें
Log In|लॉग इन करें
Log Out|लॉग आउट करें
Look for|खोजें
Lug Size|लग साइज़
Lugs|लग्स
Make a Donation|दान करें
Manage Account|खाता प्रबंधित करें
Manage My Uploads|मेरे अपलोड प्रबंधित करें
Mark Common Lugs|सामान्य लग्स चिह्नित करें
Marker|मार्कर
Marker Size|मार्कर साइज़
Mini|मिनी
Miniature|लघु
Mirror Horizontally|क्षैतिज रूप से मिरर करें
Mirror Selection|चयन मिरर करें
Mirror Vertically|ऊर्ध्वाधर रूप से मिरर करें
Mirroring operation will change the circuit. Do you want to continue?|मिररिंग ऑपरेशन सर्किट को बदल देगा। क्या आप जारी रखना चाहते हैं?
Misc|विविध
Model|मॉडल
Mono|मोनो
Mount|माउंट
Mounting Lugs|माउंटिंग लग्स
Mouse Wheel Zoom|माउस व्हील ज़ूम
Moveable Label|स्थानांतरणीय लेबल
Name|नाम
Negative|नकारात्मक
New|नया
New version available|नया संस्करण उपलब्ध है
No new public announcements available.|कोई नई सार्वजनिक घोषणाएं उपलब्ध नहीं हैं।
No|नहीं
None|कोई नहीं
North|उत्तर
Noval B9A|Noval B9A
Nudge|नज
Numbers|संख्या
Nut|नट
Octal|ऑक्टल
Offset|ऑफसेट
One Side|एक तरफ
Open|खोलें
Opposing Points|विपरीत बिंदु
Orientation|ओरिएंटेशन
Outer Foil|बाहरी फॉइल
Outer Foil Mark|बाहरी फॉइल मार्क
Outline Mode|आउटलाइन मोड
Outside|बाहर
Oval horizontal|क्षैतिज अंडाकार
Oval vertical|ऊर्ध्वाधर अंडाकार
Pad Color|पैड रंग
Panel|पैनल
Passive|निष्क्रिय
Paste|पेस्ट करें
Pin Arrangement|पिन व्यवस्था
Pin Color|पिन रंग
Pin Spacing|पिन स्पेसिंग
Pinout|पिनआउट
Pins|पिन्स
Play Test|टेस्ट प्ले करें
Plugin API|प्लगइन API
Point Count|बिंदु संख्या
Polarity|ध्रुवता
Polarized|ध्रुवीकृत
Pole Color|पोल रंग
Pole Pieces|पोल पीस
Pole Pieces 1|पोल पीस 1
Pole Pieces 2|पोल पीस 2
Positions|स्थितियां
Positive|सकारात्मक
Power Rating|पावर रेटिंग
Power Strip|पावर स्ट्रिप
Primary CT|प्राइमरी CT
Print...|प्रिंट...
Project|प्रोजेक्ट
Project Cloud|प्रोजेक्ट क्लाउड
Public Announcement|सार्वजनिक घोषणा
Radius|त्रिज्या
Rail|रेल
Recent Files|हाल की फ़ाइलें
Recent Updates|हाल के अपडेट
Redo|रीडू
Renumber On Paste|पेस्ट पर री-नंबर करें
Renumber Selection|चयन री-नंबर करें
Resistance|प्रतिरोध
Reverse (standing)|रिवर्स (खड़ा)
Right|दाएं
Rim|रिम
Rods|रॉड्स
Rotate Clockwise|दक्षिणावर्त घुमाएं
Rotate Counterclockwise|वामावर्त घुमाएं
Rotate Selection|चयन घुमाएं
Round|गोल
Round WOG A|राउंड WOG A
Round WOG B|राउंड WOG B
Row Spacing|पंक्ति स्पेसिंग
Rows|पंक्तियां
Ruler Inch Subdivision...|रूलर इंच सबडिवीजन...
Same Type Only|केवल समान प्रकार
Save|सहेजें
Save As|इस रूप में सहेजें
Save as Variant|वेरिएंट के रूप में सहेजें
Scale|स्केल
Schematic Symbols|स्कीमैटिक प्रतीक
Screws|स्क्रू
Selection|चयन
Selection Size|चयन साइज़
Selection is empty.|चयन खाली है।
Search The Cloud|क्लाउड खोजें
Searchable Tree|खोज योग्य ट्री
Secondary CT|सेकेंडरी CT
Select All|सभी चुनें
Selected component(s) have reached the bottom of their layer. Do you want to force the selection to the back?|चयनित घटक अपनी लेयर के नीचे पहुंच गए हैं। क्या आप चयन को पीछे जबरदस्ती करना चाहते हैं?
Selected component(s) have reached the top of their layer. Do you want to force the selection to the top?|चयनित घटक अपनी लेयर के ऊपर पहुंच गए हैं। क्या आप चयन को ऊपर जबरदस्ती करना चाहते हैं?
Selection contains components that cannot be mirrored. Do you want to exclude them?|चयन में ऐसे घटक हैं जिन्हें मिरर नहीं किया जा सकता। क्या आप उन्हें बाहर करना चाहते हैं?
Selection contains components that cannot be rotated. Do you want to exclude them?|चयन में ऐसे घटक हैं जिन्हें घुमाया नहीं जा सकता। क्या आप उन्हें बाहर करना चाहते हैं?
Semiconductors|सेमीकंडक्टर्स
Send Backward|पीछे भेजें
Send Selection to Back|चयन को पीछे भेजें
Seven|सात
Shaft Down|शाफ्ट नीचे
Shaft Up|शाफ्ट ऊपर
Shape|आकार
Shapes|आकार
Show Bracket|ब्रैकेट दिखाएं
Show Grid|ग्रिड दिखाएं
Show Resize Dimensions|आकार बदलने के आयाम दिखाएं
Show Rulers|रूलर दिखाएं
Shroud|श्राउड
Shrouded|श्राउडेड
Single Rail|सिंगल रेल
Single Rods|सिंगल रॉड्स
Size|साइज़
Sizing Mode|साइज़िंग मोड
Small-button B7G|स्मॉल-बटन B7G
Smooth|स्मूथ
Snap To|स्नैप टू
Soap Bar|सोप बार
Solid|ठोस
South|दक्षिण
Spacing|स्पेसिंग
Square|वर्ग
Square BR-3|स्क्वायर BR-3
Standard|स्टैंडर्ड
Start Arrow|शुरुआत तीर
Start Recording|रिकॉर्डिंग शुरू करें
Stereo|स्टीरियो
Sticky Points|स्टिकी पॉइंट्स
Stop Recording|रिकॉर्डिंग रोकें
Stratocaster|Stratocaster
Strip Color|स्ट्रिप रंग
Stripe|स्ट्राइप
Stripe 1|स्ट्राइप 1
Stripe 2|स्ट्राइप 2
Stripe Color|स्ट्राइप रंग
Stripe Color 1|स्ट्राइप रंग 1
Stripe Color 2|स्ट्राइप रंग 2
Style|स्टाइल
Submit a Bug|बग सबमिट करें
Suppressor grid|सप्रेसर ग्रिड
Switched|स्विच किया गया
Switches|स्विच
Tab|टैब
Tab Border|टैब बॉर्डर
Tabbed Toolbar|टैब्ड टूलबार
Taper|टेपर
Telecaster|Telecaster
Terminal Spacing|टर्मिनल स्पेसिंग
Terminals|टर्मिनल
Test|टेस्ट
Text|टेक्स्ट
Text Color|टेक्स्ट रंग
Theme|थीम
There are unsaved changes. Would you like to save them?|असहेजित परिवर्तन हैं। क्या आप उन्हें सहेजना चाहते हैं?
Thickness|मोटाई
Three|तीन
Through Hole|थ्रू होल
Tick|टिक
Tick Width|टिक चौड़ाई
Ticks|टिक्स
Title|शीर्षक
Toolbox|टूलबॉक्स
Top|ऊपर
Top Lead|टॉप लीड
Top Left|ऊपर बाएं
Top Right|ऊपर दाएं
Top-to-Bottom|ऊपर से नीचे
Trace|ट्रेस
Trace Color|ट्रेस रंग
Trace Mask|ट्रेस मास्क
Transform Selection|चयन ट्रांसफॉर्म करें
Triangle|त्रिभुज
Tubular|ट्यूबुलर
Tubes|ट्यूब्स
Two|दो
Type|प्रकार
Ultra-miniature|अल्ट्रा-मिनिएचर
Underside Pad Color|अंडरसाइड पैड रंग
Underside Trace Color|अंडरसाइड ट्रेस रंग
Undo|अनडू
Ungroup Selection|चयन अनग्रुप करें
Unit|यूनिट
Upload A Project|एक प्रोजेक्ट अपलोड करें
User Manual|उपयोगकर्ता मैनुअल
Value|मान
Version history is not available.|संस्करण इतिहास उपलब्ध नहीं है।
Vertical|ऊर्ध्वाधर
Vertical Alignment|ऊर्ध्वाधर संरेखण
Vertical Inline|ऊर्ध्वाधर इनलाइन
Vertical Offset 1|ऊर्ध्वाधर ऑफसेट 1
Vertical Offset 2|ऊर्ध्वाधर ऑफसेट 2
View|व्यू
Visible|दृश्यमान
Voltage|वोल्टेज
Wafer|वेफर
Warning|चेतावनी
Width|चौड़ाई
Winding Spacing|वाइंडिंग स्पेसिंग
Wiring|वायरिंग
X-axis|X-अक्ष
Y-axis|Y-अक्ष
Zoom|ज़ूम
base of 10|10 का आधार
base of 2|2 का आधार
hold %s and drag to unstick from|%s दबाए रखें और खींचकर अलग करें
Add a Curve|एक कर्व जोड़ें
Clear|साफ करें
Insert Image|छवि सम्मिलित करें
Load From File|फ़ाइल से लोड करें
Loadline Editor|लोडलाइन एडिटर
Max Current (mA)|अधिकतम करंट (mA)
Max Dissipation (W)|अधिकतम डिसिपेशन (W)
Max Voltage (V)|अधिकतम वोल्टेज (V)
Save To File|फ़ाइल में सहेजें
Update Axis|अक्ष अपडेट करें
Apply Variant|वेरिएंट लागू करें
Save as Building Block|बिल्डिंग ब्लॉक के रूप में सहेजें
Select|चुनें
Unlock|अनलॉक करें
Are you sure you want to clear the chat history for this project? This action cannot be undone!|क्या आप वाकई इस प्रोजेक्ट की चैट हिस्ट्री साफ करना चाहते हैं? यह कार्रवाई पूर्ववत नहीं की जा सकती!
Component reordering is possible only when Project Explorer is sorted by z-index.|घटक पुन: क्रमबद्धता केवल तभी संभव है जब प्रोजेक्ट एक्सप्लोरर z-index द्वारा क्रमबद्ध हो।
Could not apply changes. Check the log for details.|परिवर्तन लागू नहीं कर सके। विवरण के लिए लॉग जांचें।
Exporting outline for board %s to file %s|बोर्ड %s का आउटलाइन फ़ाइल %s में निर्यात कर रहे हैं
Failed to export the project to gerber. Please check the log for details|प्रोजेक्ट को gerber में निर्यात करने में विफल। कृपया विवरण के लिए लॉग जांचें
Forgot Password?|पासवर्ड भूल गए?
Gerber Export|Gerber निर्यात
Gerber export completed successfully. The following files were created:%s|Gerber निर्यात सफलतापूर्वक पूर्ण हुआ। निम्नलिखित फ़ाइलें बनाई गईं:%s
Get Premium|प्रीमियम प्राप्त करें
Netlist files|नेटलिस्ट फ़ाइलें
No proximity issues detected.|कोई निकटता समस्या का पता नहीं चला।
Nothing to export.|निर्यात करने के लिए कुछ नहीं है।
Password|पासवर्ड
Send|भेजें
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?|कुछ घटक बोर्ड की सीमाओं के बाहर हैं।%sजब तक आप उनके नीचे एक बोर्ड घटक नहीं जोड़ते, तब तक उन्हें अनदेखा किया जाएगा।%sक्या आप जारी रखना चाहते हैं?
User Name|उपयोगकर्ता नाम
AI Circuit Analyzer|AI सर्किट विश्लेषक
Cloud|क्लाउड
Compare With...|के साथ तुलना करें...
Connect to Patreon|Patreon से कनेक्ट करें
Export|निर्यात करें
Export to Gerber|Gerber में निर्यात करें
Generate DIYLC Netlist (excl. Switches)|DIYLC नेटलिस्ट जेनरेट करें (स्विच के बिना)
Generate DIYLC Netlist (incl. Switches)|DIYLC नेटलिस्ट जेनरेट करें (स्विच के साथ)
Generate Spice Netlist (excl. Switches)|Spice नेटलिस्ट जेनरेट करें (स्विच के बिना)
Generate Spice Netlist (incl. Switches)|Spice नेटलिस्ट जेनरेट करें (स्विच के साथ)
Silkscreen|सिल्कस्क्रीन
Virtual Multimeter (excl. Switches)|वर्चुअल मल्टीमीटर (स्विच के बिना)
Virtual Multimeter (incl. Switches)|वर्चुअल मल्टीमीटर (स्विच के साथ)
Become a Patreon and enjoy perks|Patreon बनें और लाभ उठाएं
Join Patreon|Patreon में शामिल हों
@@ -0,0 +1,499 @@
#start-internal
icon=china
#end-internal
(Building Blocks)|(构建块)
(Favorites)|(收藏)
(Recently Used)|(最近使用)
180 degrees clockwise|顺时针180度
270 degrees clockwise|顺时针270度
90 degrees clockwise|顺时针90度
A variant with that name already exists. Overwrite?|已存在同名变体。是否覆盖?
About|关于
Above|上方
Access User Files|访问用户文件
Add Flexible Leads|添加柔性引线
Add Validation Step|添加验证步骤
All Connected|全部连接
Alpha|透明度
Analyze|分析
Analyze Guitar Diagrams|分析吉他图表
Angle|角度
Anti-Aliasing|抗锯齿
Arrow Size|箭头大小
Author|作者
Auto-Create Pads|自动创建焊盘
Auto-Edit Mode|自动编辑模式
Base|基座
Below|下方
Blueprint|蓝图
Board|电路板
Boards|电路板
Board Color|电路板颜色
Board Width|电路板宽度
Bobbin 1|线轴1
Bobbin 2|线轴2
Body|主体
Border|边框
Border Thickness|边框厚度
Both|两者
Both Sides|两侧
Bottom|底部
Bottom Left|左下
Bottom Right|右下
Bracket|支架
Bring Forward|前移一层
Bring Selection to Front|将选择移到最前
Buffer|缓冲区
Center|中心
Center Negative|中心负极
Center Positive|中心正极
Center Terminal|中心端子
Central|中央
Change Password|更改密码
Channel|通道
Chassis|机箱
Check Trace Proximity|检查走线间距
Circular|圆形
Click on a component to highlight area directly or indirectly connected to the area below the cursor. To continue editing, this mode needs to be switched OFF|点击元件以高亮显示与光标下方区域直接或间接连接的区域。要继续编辑,需要关闭此模式
Click on the canvas to create a new %s or press %s to cancel|在画布上点击以创建新的%s或按%s取消
Click on the canvas to set the %s control point of a new %s or press %s to cancel|在画布上点击以设置新%s的%s控制点或按%s取消
Click the lightbulb icon in the bottom-right corner of the window for more info.|点击窗口右下角的灯泡图标以获取更多信息。
Click to fetch the most recent public announcement|点击获取最新公告
Click to show recent changes and updates|点击显示最近的更改和更新
Click to toggle to the next option|点击切换到下一个选项
Coil|线圈
Coil Border|线圈边框
Coil Length|线圈长度
Coil Width|线圈宽度
Color|颜色
Color 1|颜色1
Color 2|颜色2
Color code|颜色代码
Column Spacing|列间距
Columns|列
Component|元件
Component API|元件API
Components|元件
Config|配置
Connector|连接器
Connector (Mirrored)|连接器(镜像)
Connectivity|连接器
Contacts|触点
Continuous Creation|连续创建
Coordinate Color|坐标颜色
Coordinate Origin|坐标原点
Coordinates|坐标
Copy|复制
Core|核心
Core Border|核心边框
Core Thickness|核心厚度
Core Width|核心宽度
Could not create a new file. Check the log for details.|无法创建新文件。请查看日志了解详情。
Could not create component. Check log for details.|无法创建元件。请查看日志了解详情。
Could not fetch public announcements.|无法获取公告。
Could not save the project to file. Check the log for details.|无法将项目保存到文件。请查看日志了解详情。
Could not set component type slot. Check log for details.|无法设置元件类型插槽。请查看日志了解详情。
Cover|盖子
Create B.O.M.|创建物料清单
Create New Account|创建新账户
Current|当前
Cut|剪切
Cut Between Holes|在孔之间切割
Cut Orientation|切割方向
DIP (Mirrored)|DIP(镜像)
DP3T (Standard 3-Position Strat)|DP3T(标准3档Strat)
DP3T (Standard 5-Position Strat)|DP3T(标准5档Strat)
DP4T (4-Position Tele)|DP4T(4档Tele)
Dark|深色
Dashed|虚线
Decimals|小数
Default|默认
Default Font|默认字体
Default Font Size|默认字体大小
Delete Selection|删除选择
Description|描述
Diameter|直径
Directional|定向
Directly heated|直接加热
Display|显示
Display Pin #s|显示引脚编号
Dog Ear|狗耳
Dot Spacing|点间距
Donate|捐赠
Dotted|点线
Drag control point(s) of|拖动控制点
Drag the mouse over the canvas to place a new|在画布上拖动鼠标以放置新的
Dual Rail Humbucker|双轨拾音器
Dual Rods|双杆
Duodecar B12C|Duodecar B12C
Duplicate|复制
Edges|边缘
Edit|编辑
Edit Project Settings|编辑项目设置
Edit Selection|编辑选择
Electrode Labels|电极标签
Electro-Mechanical|机电
Enable Cache Boost|启用缓存加速
End Arrow|结束箭头
Enjoying DIYLC? Click here to buy me a coffee :)|喜欢DIYLC?点击这里请我喝杯咖啡 :)
Error|错误
Error occurred while editing selection. Check the log for details.|编辑选择时发生错误。请查看日志了解详情。
Exit|退出
Expand Selection|扩展选择
Explicit Dimensions|显式尺寸
Explicit Length|显式长度
Explicit Width|显式宽度
Export Building Blocks|导出构建块
Export Grid|导出网格
Export Variants|导出变体
Export to PDF|导出为PDF
Export to PNG|导出为PNG
Extra Working Area|额外工作区
Eyelet color|孔眼颜色
FAQ|常见问题
File|文件
Filtertron|Filtertron
Find|查找
Five|五
Flip|翻转
Folded|折叠
Font|字体
Font Bold|粗体
Font Italic|斜体
Font Size|字体大小
Font Size Override|字体大小覆盖
Four|四
Full|完整
Fuse|保险丝
Generate DIYLC Netlist|生成DIYLC网表
Generate Spice Netlist|生成Spice网表
Grid|网格
Grid Spacing|网格间距
Group Selection|组合选择
Guitar|吉他
Half|一半
Hardware Acceleration|硬件加速
Heaters|加热器
Height|高度
Help|帮助
Hi-Quality Rendering|高质量渲染
Highlight Connected Areas|高亮连接区域
Hole|孔
Hole Size|孔径
Hole Spacing|孔间距
Hole size|孔径
Holes per strip|每条带孔数
Horizontal|水平
Horizontal Alignment|水平对齐
Horizontal Large|水平大
Horizontal Medium|水平中
Horizontal Small 1|水平小1
Horizontal Small 2|水平小2
Horizontal X-Small|水平超小
Humbucking|降噪
If this box is checked application will use the current value as a<br>default when creating new components of the same type|如果选中此框,应用程序将在创建相同类型的新元件时使用当前值作为<br>默认值
Image|图像
Immediate Only|仅立即
Import|导入
Import Building Blocks|导入构建块
Import Netlist|导入网表
Import Variants|导入变体
In-Line D-44|直插D-44
In-line|直插
Indent|缩进
Information|信息
Inline|内联
Input error for "%s": %s|"%s"的输入错误:%s
Inside color|内部颜色
Integration|集成
Invert polarity|反转极性
Label|标签
Label Color|标签颜色
Label Orientation|标签方向
Label Position|标签位置
Labels|标签
Language|语言
Language selection will be applied after the application is restarted.|语言选择将在应用程序重启后生效。
Layer|图层
Layers|图层
Lead Color|引线颜色
Lead Count|引线数量
Lead Length|引线长度
Lead Spacing|引线间距
Left|左
Left-to-Right|从左到右
Left click to instantiate this component, right click for more options|左键点击以实例化此元件,右键点击以获取更多选项
Legs|引脚
Length|长度
Letters|字母
Light|浅色
Lock|锁定
Log In|登录
Log Out|登出
Look for|查找
Lug Size|焊片尺寸
Lugs|焊片
Make a Donation|捐赠
Manage Account|管理账户
Manage My Uploads|管理我的上传
Mark Common Lugs|标记公共焊片
Marker|标记
Marker Size|标记大小
Mini|迷你
Miniature|微型
Mirror Horizontally|水平镜像
Mirror Selection|镜像选择
Mirror Vertically|垂直镜像
Mirroring operation will change the circuit. Do you want to continue?|镜像操作将改变电路。是否继续?
Misc|杂项
Model|型号
Mono|单声道
Mount|安装
Mounting Lugs|安装焊片
Mouse Wheel Zoom|鼠标滚轮缩放
Moveable Label|可移动标签
Name|名称
Negative|负
New|新建
New version available|新版本可用
No new public announcements available.|没有新的公告。
No|否
None|无
North|北
Noval B9A|Noval B9A
Nudge|微调
Numbers|数字
Nut|螺母
Octal|八脚
Offset|偏移
One Side|一侧
Open|打开
Opposing Points|相对点
Orientation|方向
Outer Foil|外层箔
Outer Foil Mark|外层箔标记
Outline Mode|轮廓模式
Outside|外部
Oval horizontal|水平椭圆
Oval vertical|垂直椭圆
Pad Color|焊盘颜色
Panel|面板
Passive|被动元件
Paste|粘贴
Pin Arrangement|引脚排列
Pin Color|引脚颜色
Pin Spacing|引脚间距
Pinout|引脚排列
Pins|引脚
Play Test|播放测试
Plugin API|插件API
Point Count|点数
Polarity|极性
Polarized|极性
Pole Color|极颜色
Pole Pieces|极片
Pole Pieces 1|极片1
Pole Pieces 2|极片2
Positions|位置
Positive|正
Power Rating|功率
Power Strip|电源条
Primary CT|初级中心抽头
Print...|打印...
Project|项目
Project Cloud|项目云
Public Announcement|公告
Radius|半径
Rail|轨道
Recent Files|最近文件
Recent Updates|最近更新
Redo|重做
Renumber On Paste|粘贴时重新编号
Renumber Selection|重新编号选择
Resistance|电阻
Reverse (standing)|反向(立式)
Right|右
Rim|边缘
Rods|杆
Rotate Clockwise|顺时针旋转
Rotate Counterclockwise|逆时针旋转
Rotate Selection|旋转选择
Round|圆形
Round WOG A|圆形WOG A
Round WOG B|圆形WOG B
Row Spacing|行间距
Rows|行
Ruler Inch Subdivision...|标尺英寸细分...
Same Type Only|仅相同类型
Save|保存
Save As|另存为
Save as Variant|另存为变体
Scale|比例
Schematic Symbols|原理图符号
Screws|螺丝
Selection|选择
Selection Size|选择大小
Selection is empty.|选择为空。
Search The Cloud|搜索云
Searchable Tree|可搜索树
Secondary CT|次级中心抽头
Select All|全选
Selected component(s) have reached the bottom of their layer. Do you want to force the selection to the back?|所选元件已到达其图层的底部。是否强制将选择移到后面?
Selected component(s) have reached the top of their layer. Do you want to force the selection to the top?|所选元件已到达其图层的顶部。是否强制将选择移到前面?
Selection contains components that cannot be mirrored. Do you want to exclude them?|选择包含无法镜像的元件。是否排除它们?
Selection contains components that cannot be rotated. Do you want to exclude them?|选择包含无法旋转的元件。是否排除它们?
Semiconductors|半导体
Send Backward|后移一层
Send Selection to Back|将选择移到后面
Seven|七
Shaft Down|轴向下
Shaft Up|轴向上
Shape|形状
Shapes|形状
Show Bracket|显示支架
Show Grid|显示网格
Show Resize Dimensions|显示调整大小尺寸
Show Rulers|显示标尺
Shroud|护罩
Shrouded|带护罩
Single Rail|单轨
Single Rods|单杆
Size|大小
Sizing Mode|大小模式
Small-button B7G|小按钮B7G
Smooth|平滑
Snap To|对齐到
Soap Bar|肥皂条
Solid|实线
South|南
Spacing|间距
Square|方形
Square BR-3|方形BR-3
Standard|标准
Start Arrow|开始箭头
Start Recording|开始录制
Stereo|立体声
Sticky Points|吸附点
Stop Recording|停止录制
Stratocaster|Stratocaster
Strip Color|条带颜色
Stripe|条纹
Stripe 1|条纹1
Stripe 2|条纹2
Stripe Color|条纹颜色
Stripe Color 1|条纹颜色1
Stripe Color 2|条纹颜色2
Style|样式
Submit a Bug|提交错误报告
Suppressor grid|抑制栅极
Switched|开关
Switches|开关
Tab|标签
Tab Border|标签边框
Tabbed Toolbar|标签工具栏
Taper|锥度
Telecaster|Telecaster
Terminal Spacing|端子间距
Terminals|端子
Test|测试
Text|文本
Text Color|文本颜色
Theme|主题
There are unsaved changes. Would you like to save them?|有未保存的更改。是否保存?
Thickness|厚度
Three|三
Through Hole|通孔
Tick|刻度
Tick Width|刻度宽度
Ticks|刻度
Title|标题
Toolbox|工具箱
Top|顶部
Top Lead|顶部引线
Top Left|左上
Top Right|右上
Top-to-Bottom|从上到下
Trace|走线
Trace Color|走线颜色
Trace Mask|走线掩膜
Transform Selection|变换选择
Triangle|三角形
Tubular|管状
Tubes|电子管
Two|二
Type|类型
Ultra-miniature|超微型
Underside Pad Color|底面焊盘颜色
Underside Trace Color|底面走线颜色
Undo|撤销
Ungroup Selection|取消组合
Unit|单位
Upload A Project|上传项目
User Manual|用户手册
Value|值
Version history is not available.|版本历史不可用。
Vertical|垂直
Vertical Alignment|垂直对齐
Vertical Inline|垂直内联
Vertical Offset 1|垂直偏移1
Vertical Offset 2|垂直偏移2
View|视图
Visible|可见
Voltage|电压
Wafer|晶圆
Warning|警告
Width|宽度
Winding Spacing|绕组间距
Wiring|布线
X-axis|X轴
Y-axis|Y轴
Zoom|缩放
base of 10|10进制
base of 2|2进制
hold %s and drag to unstick from|按住%s并拖动以从...脱离
Add a Curve|添加曲线
Clear|清除
Insert Image|插入图像
Load From File|从文件加载
Loadline Editor|负载线编辑器
Max Current (mA)|最大电流(mA)
Max Dissipation (W)|最大功耗(W)
Max Voltage (V)|最大电压(V)
Save To File|保存到文件
Update Axis|更新轴
Apply Variant|应用变体
Save as Building Block|另存为构建块
Select|选择
Unlock|解锁
Are you sure you want to clear the chat history for this project? This action cannot be undone!|您确定要清除此项目的聊天历史记录吗?此操作无法撤销!
Component reordering is possible only when Project Explorer is sorted by z-index.|只有在项目资源管理器按z-index排序时才能重新排序元件。
Could not apply changes. Check the log for details.|无法应用更改。请查看日志了解详情。
Exporting outline for board %s to file %s|正在将电路板%s的轮廓导出到文件%s
Failed to export the project to gerber. Please check the log for details|无法将项目导出为gerber格式。请查看日志了解详情
Forgot Password?|忘记密码?
Gerber Export|Gerber导出
Gerber export completed successfully. The following files were created:%s|Gerber导出成功完成。已创建以下文件:%s
Get Premium|获取高级版
Netlist files|网表文件
No proximity issues detected.|未检测到间距问题。
Nothing to export.|没有可导出的内容。
Password|密码
Send|发送
There are some components that are outside of the bounds of boards.%sThey will be ignored unless you add a board component underneath them.%sDo you want to continue?|有些元件超出了电路板的边界。%s除非您在它们下方添加电路板元件,否则它们将被忽略。%s是否继续?
User Name|用户名
AI Circuit Analyzer|AI电路分析器
Cloud|云
Compare With...|与...比较
Connect to Patreon|连接到Patreon
Export|导出
Export to Gerber|导出为Gerber
Generate DIYLC Netlist (excl. Switches)|生成DIYLC网表(不含开关)
Generate DIYLC Netlist (incl. Switches)|生成DIYLC网表(含开关)
Generate Spice Netlist (excl. Switches)|生成Spice网表(不含开关)
Generate Spice Netlist (incl. Switches)|生成Spice网表(含开关)
Silkscreen|丝印
Virtual Multimeter (excl. Switches)|虚拟万用表(不含开关)
Virtual Multimeter (incl. Switches)|虚拟万用表(含开关)
Become a Patreon and enjoy perks|成为Patreon支持者,享受特权
Join Patreon|加入Patreon
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>5.7.0</version>
<version>5.8.0</version>
<packaging>pom</packaging>
<name>DIY Layout Creator</name>