diff --git a/diylc/diylc-library/lib/diylc-core.jar b/diylc/diylc-library/lib/diylc-core.jar
index 8b797d50..af1bcaf3 100644
Binary files a/diylc/diylc-library/lib/diylc-core.jar and b/diylc/diylc-library/lib/diylc-core.jar differ
diff --git a/diylc/diylc-library/src/org/diylc/components/connectivity/TwistedWire.java b/diylc/diylc-library/src/org/diylc/components/connectivity/TwistedWire.java
new file mode 100644
index 00000000..f09e90d1
--- /dev/null
+++ b/diylc/diylc-library/src/org/diylc/components/connectivity/TwistedWire.java
@@ -0,0 +1,322 @@
+/*
+ *
+ * DIY Layout Creator (DIYLC). Copyright (c) 2009-2018 held jointly by the individual authors.
+ *
+ * This file is part of DIYLC.
+ *
+ * DIYLC is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * DIYLC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with DIYLC. If not, see
+ * .
+ */
+package org.diylc.components.connectivity;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Stroke;
+import java.awt.geom.Area;
+import java.awt.geom.CubicCurve2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.diylc.common.LineStyle;
+import org.diylc.common.ObjectCache;
+import org.diylc.common.SimpleComponentTransformer;
+import org.diylc.components.AbstractCurvedComponent;
+import org.diylc.core.ComponentState;
+import org.diylc.core.IDIYComponent;
+import org.diylc.core.IDrawingObserver;
+import org.diylc.core.annotations.BomPolicy;
+import org.diylc.core.annotations.ComponentDescriptor;
+import org.diylc.core.annotations.EditableProperty;
+import org.diylc.utils.Constants;
+
+@ComponentDescriptor(name = "Twisted Leads", author = "Branislav Stojkovic", category = "Connectivity",
+ instanceNamePrefix = "W", description = "A pair of flexible leads twisted tighly together", zOrder = IDIYComponent.COMPONENT,
+ flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW, autoEdit = false,
+ transformer = SimpleComponentTransformer.class, continuity = true)
+public class TwistedWire extends AbstractCurvedComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ public static Color COLOR = Color.green;
+ public static Color COLOR2 = Color.blue;
+ public static double INSULATION_THICKNESS_PCT = 0.3;
+
+ protected AWG gauge = AWG._22;
+
+ private Color color2 = COLOR2;
+
+ // cached areas
+ transient private Area firstLeadArea = null;
+ transient private Area secondLeadArea = null;
+
+ @Override
+ protected Color getDefaultColor() {
+ return COLOR;
+ }
+
+ @EditableProperty(name = "Color 1")
+ public Color getLeadColor() {
+ return color;
+ }
+
+ @EditableProperty(name = "Color 2")
+ public Color getColor2() {
+ return color2;
+ }
+
+ public void setColor2(Color color2) {
+ this.color2 = color2;
+ }
+
+ @Override
+ public LineStyle getStyle() {
+ // prevent from editing
+ return super.getStyle();
+ }
+
+ @Override
+ protected void drawCurve(Path2D curve, Graphics2D g2d, ComponentState componentState, IDrawingObserver drawingObserver) {
+ int thickness =
+ (int) (Math.pow(Math.E, -1.12436 - 0.11594 * gauge.getValue()) * Constants.PIXELS_PER_INCH * (1 + 2 * INSULATION_THICKNESS_PCT)) - 1;
+ Color curveColor1 =
+ componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
+ : color;
+ Color curveColor2 =
+ componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
+ : color2;
+
+ if (firstLeadArea == null || secondLeadArea == null)
+ recalculate(curve, thickness);
+
+ g2d.setColor(curveColor1);
+ g2d.fill(firstLeadArea);
+ g2d.setColor(curveColor2);
+ g2d.fill(secondLeadArea);
+ if (componentState == ComponentState.NORMAL) {
+ g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
+ g2d.setColor(color.darker());
+ g2d.draw(firstLeadArea);
+ g2d.setColor(color2.darker());
+ g2d.draw(secondLeadArea);
+ }
+ }
+
+ @Override
+ public void drawIcon(Graphics2D g2d, int width, int height) {
+ Stroke stroke = ObjectCache.getInstance().fetchBasicStroke(3);
+ CubicCurve2D curve1 = new CubicCurve2D.Double(2, height - 2, width - 4, height - 4, 4, 4, width - 2, 2);
+ CubicCurve2D curve2 = new CubicCurve2D.Double(2, height - 2, 4, 4, width - 4, height - 4, width - 2, 2);
+
+ Area area1 = new Area(stroke.createStrokedShape(curve1));
+ Area area2 = new Area(stroke.createStrokedShape(curve2));
+ area2.subtract(area1);
+
+ g2d.setColor(COLOR2);
+ g2d.fill(area2);
+ g2d.setColor(COLOR);
+ g2d.fill(area1);
+
+ g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
+ g2d.setColor(COLOR2.darker());
+ g2d.draw(area2);
+ g2d.setColor(COLOR.darker());
+ g2d.draw(area1);
+ }
+
+ @EditableProperty(name = "AWG")
+ public AWG getGauge() {
+ return gauge;
+ }
+
+ public void setGauge(AWG gauge) {
+ this.gauge = gauge;
+
+ // invalidate cached areas
+ this.firstLeadArea = null;
+ this.secondLeadArea = null;
+ }
+
+ @Override
+ public void setControlPoint(Point point, int index) {
+ super.setControlPoint(point, index);
+
+ // invalidate cached areas
+ this.firstLeadArea = null;
+ this.secondLeadArea = null;
+ }
+
+ @Override
+ public Void getValue() {
+ return null;
+ }
+
+ @Override
+ public void setValue(Void value) {}
+
+ // hard-core math below :)
+
+ public void recalculate(Path2D path, float thickness) {
+ PathIterator iterator = path.getPathIterator(null);
+ float[] coords = new float[6];
+ Point current = new Point();
+
+ // convert all segments to cubic curves
+ List curves = new ArrayList();
+ while (!iterator.isDone()) {
+ int type = iterator.currentSegment(coords);
+ switch (type) {
+ case PathIterator.SEG_MOVETO:
+ current.setLocation(coords[0], coords[1]);
+ break;
+ case PathIterator.SEG_LINETO:
+ curves.add(new CubicCurve2D.Double(current.x, current.y, (current.x + coords[0]) / 2,
+ (current.y + coords[1]) / 2, (current.x + coords[0]) / 2, (current.y + coords[1]) / 2, coords[0],
+ coords[1]));
+ break;
+ case PathIterator.SEG_QUADTO:
+ curves.add(new CubicCurve2D.Double(current.x, current.y, coords[0], coords[1], coords[0], coords[1],
+ coords[2], coords[3]));
+ break;
+ case PathIterator.SEG_CUBICTO:
+ curves.add(new CubicCurve2D.Double(current.x, current.y, coords[0], coords[1], coords[2], coords[3],
+ coords[4], coords[5]));
+ break;
+ }
+ iterator.next();
+ }
+
+ Stroke stroke = ObjectCache.getInstance().fetchBasicStroke(thickness);
+
+ double segmentLength = thickness * 7;
+
+ // Convert to polygon
+ List polygon = new ArrayList();
+ for (CubicCurve2D curve : curves)
+ polygon.addAll(split(curve, segmentLength));
+
+ firstLeadArea = new Area();
+ secondLeadArea = new Area();
+
+ List firstCurves = new ArrayList();
+ List secondCurves = new ArrayList();
+
+ Path2D current1 = new Path2D.Double();
+ Path2D current2 = new Path2D.Double();
+ double offset = thickness * 1.5;
+ double rectSize = thickness * 4;
+
+ current1.moveTo(polygon.get(0).getX1(), polygon.get(0).getY1());
+ current2.moveTo(polygon.get(0).getX1(), polygon.get(0).getY1());
+
+ for (int i = 0; i < polygon.size(); i++) {
+ Line2D line = polygon.get(i);
+ double centerX = (line.getX1() + line.getX2()) / 2;
+ double centerY = (line.getY1() + line.getY2()) / 2;
+ double theta = Math.atan2(line.getY2() - line.getY1(), line.getX2() - line.getX1());
+
+ double sign = i % 2 == 0 ? 1 : -1;
+
+ double theta1 = theta - sign * Math.PI / 2;
+ double theta2 = theta + sign * Math.PI / 2;
+
+ current1.quadTo(centerX + offset * Math.cos(theta1),
+ centerY + offset * Math.sin(theta1), line.getX2(), line.getY2());
+ current2.quadTo(centerX + offset * Math.cos(theta2),
+ centerY + offset * Math.sin(theta2), line.getX2(), line.getY2());
+
+ if (i % 2 == 0 || i == polygon.size() - 1) {
+ firstCurves.add(current1);
+ current1 = new Path2D.Double();
+ current1.moveTo(line.getX2(), line.getY2());
+ }
+ if (i % 2 == 1 || i == polygon.size() - 1) {
+ secondCurves.add(current2);
+ current2 = new Path2D.Double();
+ current2.moveTo(line.getX2(), line.getY2());
+ }
+ }
+
+ for (Path2D p : firstCurves) {
+ firstLeadArea.add(new Area(stroke.createStrokedShape(p)));
+ }
+ for (Path2D p : secondCurves) {
+ secondLeadArea.add(new Area(stroke.createStrokedShape(p)));
+ }
+
+ for (int i = 0; i < polygon.size(); i++) {
+ Line2D line = polygon.get(i);
+ Area pointRect1 = new Area(new Rectangle2D.Double(line.getX1() - rectSize / 2, line.getY1() - rectSize / 2, rectSize, rectSize));
+
+ if (i % 2 == 1) {
+ pointRect1.intersect(firstLeadArea);
+ secondLeadArea.subtract(pointRect1);
+ } else {
+ pointRect1.intersect(secondLeadArea);
+ firstLeadArea.subtract(pointRect1);
+ }
+
+ if (i == polygon.size() - 1) {
+ Area pointRect2 = new Area(new Rectangle2D.Double(line.getX2() - rectSize / 2, line.getY2() - rectSize / 2, rectSize, rectSize));
+ if (i % 2 == 0) {
+ pointRect2.intersect(firstLeadArea);
+ secondLeadArea.subtract(pointRect2);
+ } else {
+ pointRect2.intersect(secondLeadArea);
+ firstLeadArea.subtract(pointRect2);
+ }
+ }
+ }
+ }
+
+ private List split(CubicCurve2D curve, double segmentLength) {
+ Point2D p1 = curve.getP1();
+ Point2D p2 = curve.getP2();
+ List res = new ArrayList();
+
+ double length = Double.MAX_VALUE;
+
+ if (p1.distance(p2) <= segmentLength)
+ length = calculateLength(curve, segmentLength / 10);
+
+ if (length <= segmentLength) {
+ res.add(new Line2D.Double(p1, p2));
+ } else {
+ CubicCurve2D left = new CubicCurve2D.Double();
+ CubicCurve2D right = new CubicCurve2D.Double();
+ curve.subdivide(left, right);
+ res.addAll(split(left, segmentLength));
+ res.addAll(split(right, segmentLength));
+ }
+ return res;
+ }
+
+ private double calculateLength(CubicCurve2D curve, double precision) {
+ Point2D p1 = curve.getP1();
+ Point2D p2 = curve.getP2();
+
+ double d = p1.distance(p2);
+ if (d <= precision) {
+ return d;
+ }
+
+ CubicCurve2D left = new CubicCurve2D.Double();
+ CubicCurve2D right = new CubicCurve2D.Double();
+ curve.subdivide(left, right);
+
+ return calculateLength(left, precision) + calculateLength(right, precision);
+ }
+}
diff --git a/diylc/diylc-library/src/org/diylc/components/electromechanical/FuseHolderPanel.java b/diylc/diylc-library/src/org/diylc/components/electromechanical/FuseHolderPanel.java
index 04826b9c..9522460d 100644
--- a/diylc/diylc-library/src/org/diylc/components/electromechanical/FuseHolderPanel.java
+++ b/diylc/diylc-library/src/org/diylc/components/electromechanical/FuseHolderPanel.java
@@ -323,7 +323,7 @@ public class FuseHolderPanel extends AbstractMultiPartComponent {
this.display = display;
}
- @EditableProperty
+ @EditableProperty(name = "Label")
public Color getLabelColor() {
return labelColor;
}
diff --git a/diylc/diylc-swing/lib/diylc-core.jar b/diylc/diylc-swing/lib/diylc-core.jar
index 8b797d50..af1bcaf3 100644
Binary files a/diylc/diylc-swing/lib/diylc-core.jar and b/diylc/diylc-swing/lib/diylc-core.jar differ
diff --git a/diylc/diylc-swing/library/main.jar b/diylc/diylc-swing/library/main.jar
index 92c0c53d..ac6219e8 100644
Binary files a/diylc/diylc-swing/library/main.jar and b/diylc/diylc-swing/library/main.jar differ
diff --git a/diylc/diylc-swing/update.xml b/diylc/diylc-swing/update.xml
index 543f6c14..7496d151 100644
--- a/diylc/diylc-swing/update.xml
+++ b/diylc/diylc-swing/update.xml
@@ -2361,6 +2361,10 @@
NEW_FEATURE
Ability to drag components directly from the tree to the canvas for faster process
+
+
+ NEW_FEATURE
+ "Twisted Leads" added (DIYLC v2 fans rejoice)
NEW_FEATURE