mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
Added 'Right Angle Wire' and 'Right Angle Line' components that route between two points using only horizontal and vertical runs with rounded corners
This commit is contained in:
+23
-3
@@ -64,15 +64,16 @@ The formatter is `eclipse-java-google-style.xml` (Google Java Style):
|
||||
- **100-column** line limit.
|
||||
- Source encoding is **ISO-8859-1** — do not introduce non-ASCII characters into `.java` files. Use
|
||||
Unicode escapes or the resource bundles instead.
|
||||
- Braces on the same line; single-statement `if` bodies without braces are common in this codebase
|
||||
and acceptable when you are matching the surrounding style.
|
||||
- Braces on the same line, and **always braces** — an `if`, `else`, `for` or `while` body gets a
|
||||
block even when it is a single statement. Plenty of older code omits them; do not copy that when
|
||||
you are writing new code, and add them when you are already editing the lines in question.
|
||||
- Imports: `java.*` first, then third-party, then `org.diylc.*`, separated by blank lines. No
|
||||
wildcard imports.
|
||||
- Constants are `UPPER_SNAKE_CASE`. Note that component appearance constants are deliberately
|
||||
`public static` and **not** `final` (`BODY_COLOR`, `DEFAULT_WIDTH`), so they can be overridden at
|
||||
runtime; keep that convention when adding new ones.
|
||||
- `private static final Logger LOG = Logger.getLogger(TheClass.class);` — log4j 1.2, declared first
|
||||
among the static members.
|
||||
among the static members. Only declare one on a class that actually needs it; see *Logging* below.
|
||||
|
||||
### Comments
|
||||
|
||||
@@ -93,6 +94,25 @@ g2d.setColor(bodyColor);
|
||||
Javadoc on public interfaces and abstract base classes in `diylc-core` is expected and should carry
|
||||
`@author`. Concrete component classes generally carry no Javadoc — do not add it just to fill space.
|
||||
|
||||
### Logging
|
||||
|
||||
Keep logging sparse, and prefer removing a log line to adding one. A logger earns its place on a
|
||||
class that has something a maintainer would want to read back: a swallowed failure, a slow or
|
||||
one-off initialisation, an external file or service that may not be there. Do not declare a `LOG`
|
||||
field that the class never uses.
|
||||
|
||||
- `LOG.error` — something went wrong that the user or a bug report may need to explain.
|
||||
- `LOG.warn` — a recoverable surprise.
|
||||
- `LOG.info` — events that happen a handful of times per session, not per action.
|
||||
- `LOG.debug` — anything finer. Assume it is switched off in the field.
|
||||
|
||||
Never log from code on a hot path: `draw`, control-point accessors, property getters, anything the
|
||||
canvas calls on every repaint or every mouse move. The cost is real and the output buries whatever
|
||||
else is in the log.
|
||||
|
||||
Do not wrap code in `try`/`catch` only so that there is something to log. If the block cannot
|
||||
actually throw, the guard and the logger both come out.
|
||||
|
||||
### License header
|
||||
|
||||
**Every new source file starts with the GPL header**, copied verbatim from `HEADER.txt` (or from any
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
/*
|
||||
|
||||
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;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Composite;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.common.IPlugInPort;
|
||||
import org.diylc.common.LineStyle;
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.common.OrientationHV;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
import org.diylc.core.IHaveLength;
|
||||
import org.diylc.core.Project;
|
||||
import org.diylc.core.VisibilityPolicy;
|
||||
import org.diylc.core.annotations.EditableProperty;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
/**
|
||||
* Base class for connectors that run only horizontally and vertically and turn at rounded
|
||||
* 90-degree corners.
|
||||
* <p>
|
||||
* The first and the last control point are the sticky ends. Each control point in between is a free
|
||||
* bend vertex that the route reaches with a horizontal move followed by a vertical one, or the
|
||||
* other way around when {@link #getStartDirection()} is vertical. Both coordinates of a bend point
|
||||
* therefore carry meaning and the arrangement never has to be guessed: an orthogonal route of
|
||||
* <code>s</code> segments has <code>s - 2</code> degrees of freedom, and this template spends
|
||||
* exactly two per bend point. Segments of zero length collapse silently, which is how the simple L,
|
||||
* horizontal-vertical-horizontal and vertical-horizontal-vertical shapes are expressed: a bend
|
||||
* point sharing a column with the start swallows the leading segment, one sharing a row with the
|
||||
* end swallows the trailing one. All four combinations of leaving and arriving on either axis are
|
||||
* therefore reachable by dragging alone, which is why the start direction is not offered as a
|
||||
* property.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public abstract class AbstractOrthogonalComponent<T> extends AbstractTransparentComponent<T>
|
||||
implements IHaveLength {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static Size DEFAULT_SIZE = new Size(1d, SizeUnit.in);
|
||||
public static Size DEFAULT_CORNER_RADIUS = new Size(0.05d, SizeUnit.in);
|
||||
|
||||
// rounding is skipped below this radius in pixels, where a quadratic corner is indistinguishable
|
||||
// from a sharp one
|
||||
private static final double MIN_CORNER_RADIUS = 1d;
|
||||
|
||||
private static final double EPSILON = 1e-6;
|
||||
|
||||
protected Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0),
|
||||
new Point2D.Double(DEFAULT_SIZE.convertToPixels() / 2, DEFAULT_SIZE.convertToPixels()),
|
||||
new Point2D.Double(DEFAULT_SIZE.convertToPixels(), DEFAULT_SIZE.convertToPixels())};
|
||||
|
||||
protected Color color = getDefaultColor();
|
||||
protected PointCount pointCount = PointCount.THREE;
|
||||
protected OrientationHV startDirection = OrientationHV.HORIZONTAL;
|
||||
protected LineStyle style = LineStyle.SOLID;
|
||||
protected Size cornerRadius = DEFAULT_CORNER_RADIUS;
|
||||
|
||||
/**
|
||||
* Draws the specified route onto graphics.
|
||||
*
|
||||
* @param curve
|
||||
* @param g2d
|
||||
* @param componentState
|
||||
* @param drawingObserver
|
||||
*/
|
||||
protected abstract void drawCurve(Path2D curve, Graphics2D g2d, ComponentState componentState,
|
||||
IDrawingObserver drawingObserver);
|
||||
|
||||
/**
|
||||
* @return default color.
|
||||
*/
|
||||
protected abstract Color getDefaultColor();
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
Path2D path = createIconPath(width, height);
|
||||
g2d.setColor(getDefaultColor().darker());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(3));
|
||||
g2d.draw(path);
|
||||
g2d.setColor(getDefaultColor());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.draw(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param width
|
||||
* @param height
|
||||
* @return the elbow drawn in the toolbox, so that subclasses that stroke it differently do not
|
||||
* have to restate the shape.
|
||||
*/
|
||||
protected Path2D createIconPath(int width, int height) {
|
||||
Path2D path = new Path2D.Double();
|
||||
path.moveTo(1, height - 3);
|
||||
path.lineTo(width / 2d - 2, height - 3);
|
||||
path.quadTo(width / 2d, height - 3, width / 2d, height - 5);
|
||||
path.lineTo(width / 2d, 5);
|
||||
path.quadTo(width / 2d, 3, width / 2d + 2, 3);
|
||||
path.lineTo(width - 1, 3);
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
|
||||
IDrawingObserver drawingObserver) {
|
||||
if (checkPointsClipped(g2d.getClip())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Composite oldComposite = applyAlpha(g2d);
|
||||
|
||||
drawCurve(buildPath(), g2d, componentState, drawingObserver);
|
||||
|
||||
g2d.setComposite(oldComposite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the control points into the vertices of the route. Every control point is reached by a
|
||||
* move along one axis followed by a move along the other, the end point included, so the loop
|
||||
* below is the whole routing rule. Zero-length segments are dropped as they are produced.
|
||||
*
|
||||
* @return route vertices, starting with the first and ending with the last control point.
|
||||
*/
|
||||
protected List<Point2D> buildVertices() {
|
||||
Point2D[] p = getControlPoints();
|
||||
boolean horizontalFirst = getStartDirection() == OrientationHV.HORIZONTAL;
|
||||
List<Point2D> vertices = new ArrayList<Point2D>();
|
||||
addVertex(vertices, p[0]);
|
||||
for (int i = 1; i < p.length; i++) {
|
||||
Point2D previous = p[i - 1];
|
||||
Point2D next = p[i];
|
||||
if (horizontalFirst) {
|
||||
addVertex(vertices, new Point2D.Double(next.getX(), previous.getY()));
|
||||
} else {
|
||||
addVertex(vertices, new Point2D.Double(previous.getX(), next.getY()));
|
||||
}
|
||||
addVertex(vertices, next);
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the route as a path with rounded corners. The radius is clamped to half of each adjacent
|
||||
* segment so that short segments bend rather than overshoot.
|
||||
*
|
||||
* @return path following the route.
|
||||
*/
|
||||
protected Path2D buildPath() {
|
||||
List<Point2D> vertices = buildVertices();
|
||||
double radius = getCornerRadius().convertToPixels();
|
||||
Path2D path = new Path2D.Double();
|
||||
Point2D first = vertices.get(0);
|
||||
path.moveTo(first.getX(), first.getY());
|
||||
for (int i = 1; i < vertices.size() - 1; i++) {
|
||||
Point2D previous = vertices.get(i - 1);
|
||||
Point2D corner = vertices.get(i);
|
||||
Point2D next = vertices.get(i + 1);
|
||||
double r = Math.min(radius, Math.min(previous.distance(corner), corner.distance(next)) / 2);
|
||||
if (r < MIN_CORNER_RADIUS) {
|
||||
path.lineTo(corner.getX(), corner.getY());
|
||||
continue;
|
||||
}
|
||||
Point2D in = interpolate(corner, previous, r);
|
||||
Point2D out = interpolate(corner, next, r);
|
||||
path.lineTo(in.getX(), in.getY());
|
||||
path.quadTo(corner.getX(), corner.getY(), out.getX(), out.getY());
|
||||
}
|
||||
Point2D last = vertices.get(vertices.size() - 1);
|
||||
path.lineTo(last.getX(), last.getY());
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void addVertex(List<Point2D> vertices, Point2D p) {
|
||||
if (!vertices.isEmpty() && vertices.get(vertices.size() - 1).distance(p) < EPSILON) {
|
||||
return;
|
||||
}
|
||||
vertices.add(p);
|
||||
}
|
||||
|
||||
private static Point2D interpolate(Point2D from, Point2D towards, double distance) {
|
||||
double t = distance / from.distance(towards);
|
||||
return new Point2D.Double(from.getX() + (towards.getX() - from.getX()) * t, from.getY()
|
||||
+ (towards.getY() - from.getY()) * t);
|
||||
}
|
||||
|
||||
protected Point2D[] getControlPoints() {
|
||||
return this.controlPoints;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Point Count")
|
||||
public PointCount getPointCount() {
|
||||
if (pointCount == null) {
|
||||
pointCount = PointCount.THREE;
|
||||
}
|
||||
return pointCount;
|
||||
}
|
||||
|
||||
public void setPointCount(PointCount pointCount) {
|
||||
if (getPointCount() == pointCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
Point2D[] p = getControlPoints();
|
||||
Point2D start = p[0];
|
||||
Point2D end = p[p.length - 1];
|
||||
Point2D[] newPoints = new Point2D[pointCount.count];
|
||||
newPoints[0] = start;
|
||||
newPoints[pointCount.count - 1] = end;
|
||||
|
||||
// spread the bend points so that the route comes out as an even staircase: they advance evenly
|
||||
// along the axis the route travels last, and sit halfway between those steps on the other axis
|
||||
int bendCount = pointCount.count - 2;
|
||||
boolean horizontalFirst = getStartDirection() == OrientationHV.HORIZONTAL;
|
||||
for (int i = 1; i <= bendCount; i++) {
|
||||
double along = (double) i / bendCount;
|
||||
double across = (2d * i - 1) / (2d * bendCount);
|
||||
double tx = horizontalFirst ? across : along;
|
||||
double ty = horizontalFirst ? along : across;
|
||||
newPoints[i] =
|
||||
new Point2D.Double(start.getX() + (end.getX() - start.getX()) * tx, start.getY()
|
||||
+ (end.getY() - start.getY()) * ty);
|
||||
}
|
||||
|
||||
this.controlPoints = newPoints;
|
||||
this.pointCount = pointCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not an editable property on purpose. Every route shape is reachable by dragging the bend
|
||||
* points, so exposing this would only give the user a second, less discoverable way to do the
|
||||
* same thing. It is kept as state because a quarter-turn rotation swaps the two axes and the
|
||||
* transformer has to record that.
|
||||
*
|
||||
* @return axis the route travels along when it leaves the first control point.
|
||||
*/
|
||||
public OrientationHV getStartDirection() {
|
||||
if (startDirection == null) {
|
||||
startDirection = OrientationHV.HORIZONTAL;
|
||||
}
|
||||
return startDirection;
|
||||
}
|
||||
|
||||
public void setStartDirection(OrientationHV startDirection) {
|
||||
this.startDirection = startDirection;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Corner Radius")
|
||||
public Size getCornerRadius() {
|
||||
if (cornerRadius == null) {
|
||||
cornerRadius = DEFAULT_CORNER_RADIUS;
|
||||
}
|
||||
return cornerRadius;
|
||||
}
|
||||
|
||||
public void setCornerRadius(Size cornerRadius) {
|
||||
this.cornerRadius = cornerRadius;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Color")
|
||||
public Color getLeadColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setLeadColor(Color color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Style")
|
||||
public LineStyle getStyle() {
|
||||
if (style == null) {
|
||||
style = LineStyle.SOLID;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
public void setStyle(LineStyle style) {
|
||||
this.style = style;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Length")
|
||||
public Size getLength() {
|
||||
return calculateLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Size calculateLength() {
|
||||
// corner rounding shaves a fraction of the radius off each bend, which is well below the
|
||||
// precision this readout is used at
|
||||
List<Point2D> vertices = buildVertices();
|
||||
double d = 0;
|
||||
for (int i = 1; i < vertices.size(); i++) {
|
||||
d += vertices.get(i - 1).distance(vertices.get(i));
|
||||
}
|
||||
|
||||
SizeUnit unit;
|
||||
if (ConfigurationManager.getInstance().readBoolean(IPlugInPort.METRIC_KEY, true)) {
|
||||
unit = SizeUnit.mm;
|
||||
} else {
|
||||
unit = SizeUnit.in;
|
||||
}
|
||||
|
||||
return new Size(d * SizeUnit.px.getFactor() / unit.getFactor(), unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getControlPointCount() {
|
||||
return getControlPoints().length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point2D getControlPoint(int index) {
|
||||
return getControlPoints()[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControlPoint(Point2D point, int index) {
|
||||
getControlPoints()[index].setLocation(point);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isControlPointSticky(int index) {
|
||||
return index == 0 || index == getControlPointCount() - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canControlPointOverlap(int index) {
|
||||
return index > 0 && index < getControlPointCount() - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
|
||||
return VisibilityPolicy.WHEN_SELECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rectangle2D getCachingBounds() {
|
||||
double minX = Integer.MAX_VALUE;
|
||||
double maxX = Integer.MIN_VALUE;
|
||||
double minY = Integer.MAX_VALUE;
|
||||
double maxY = Integer.MIN_VALUE;
|
||||
double margin = 50; // hard coded, replace with something meaningful
|
||||
// every route vertex borrows both of its coordinates from a control point, so the control
|
||||
// points alone bound the whole route
|
||||
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 enum PointCount {
|
||||
THREE(3), FOUR(4), FIVE(5);
|
||||
|
||||
private int count;
|
||||
|
||||
private PointCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name().substring(0, 1) + name().substring(1).toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
|
||||
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.Point2D;
|
||||
|
||||
import org.diylc.common.OrientationHV;
|
||||
import org.diylc.components.AbstractOrthogonalComponent;
|
||||
import org.diylc.core.IDIYComponent;
|
||||
|
||||
/**
|
||||
* Rotates and mirrors orthogonal connectors. Quarter-turn rotation turns every horizontal run of
|
||||
* the route into a vertical one, so the start direction has to follow the points. Mirroring leaves
|
||||
* both axes in place and needs nothing beyond moving the points.
|
||||
*
|
||||
* @author Branislav Stojkovic
|
||||
*/
|
||||
public class OrthogonalComponentTransformer extends SimpleComponentTransformer {
|
||||
|
||||
@Override
|
||||
public void rotate(IDIYComponent<?> component, Point2D center, int direction) {
|
||||
super.rotate(component, center, direction);
|
||||
if (component instanceof AbstractOrthogonalComponent<?> orthogonal) {
|
||||
orthogonal.setStartDirection(orthogonal.getStartDirection() == OrientationHV.HORIZONTAL
|
||||
? OrientationHV.VERTICAL : OrientationHV.HORIZONTAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,5 +468,21 @@
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
<org.diylc.appframework.update.Version>
|
||||
<versionNumber>
|
||||
<major>6</major>
|
||||
<minor>6</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2026-09-11 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added 'Right Angle Wire' and 'Right Angle Line' components that route between two points using only horizontal and vertical runs with rounded corners</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
</a>
|
||||
</java.util.Arrays_-ArrayList>
|
||||
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
|
||||
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;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.diylc.common.OrientationHV;
|
||||
import org.diylc.components.AbstractOrthogonalComponent.PointCount;
|
||||
import org.diylc.components.transform.OrthogonalComponentTransformer;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IDrawingObserver;
|
||||
|
||||
public class AbstractOrthogonalComponentTest {
|
||||
|
||||
private static final double DELTA = 1e-6;
|
||||
|
||||
// --- routing ---
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendOnEndRow_producesHorizontalVerticalHorizontal() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 50);
|
||||
assertRoute(c, 0, 0, 60, 0, 60, 50, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendOnStartColumn_producesVerticalHorizontalVertical() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 0, 30);
|
||||
assertRoute(c, 0, 0, 0, 30, 100, 30, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendInsideTheBox_producesThreeBends() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 20);
|
||||
assertRoute(c, 0, 0, 60, 0, 60, 20, 100, 20, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendOnBoxCorner_producesSingleBend() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 100, 0);
|
||||
assertRoute(c, 0, 0, 100, 0, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendOnOppositeBoxCorner_leavesVerticallyAndArrivesHorizontally() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 0, 50);
|
||||
assertRoute(c, 0, 0, 0, 50, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_bendOutsideTheBox_detoursAround() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, -40, 90);
|
||||
assertRoute(c, 0, 0, -40, 0, -40, 90, 100, 90, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_verticalStart_swapsTheAxes() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 20);
|
||||
c.setStartDirection(OrientationHV.VERTICAL);
|
||||
assertRoute(c, 0, 0, 0, 20, 60, 20, 60, 50, 100, 50);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_collapsedRoute_keepsASingleVertex() {
|
||||
OrthogonalTestComponent c = component(10, 10, 10, 10, 10, 10);
|
||||
assertRoute(c, 10, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildVertices_everySegmentIsAxisAligned() {
|
||||
for (OrientationHV startDirection : OrientationHV.values()) {
|
||||
for (double x = -50; x <= 150; x += 25) {
|
||||
for (double y = -50; y <= 150; y += 25) {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, x, y);
|
||||
c.setStartDirection(startDirection);
|
||||
assertAxisAligned(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- point count ---
|
||||
|
||||
@Test
|
||||
public void setPointCount_keepsTheEndsAndStaysAxisAligned() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 50);
|
||||
c.setPointCount(PointCount.FIVE);
|
||||
assertEquals(5, c.getControlPointCount());
|
||||
assertPoint(0, 0, c.getControlPoint(0));
|
||||
assertPoint(100, 50, c.getControlPoint(4));
|
||||
assertAxisAligned(c);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setPointCount_backAndForth_restoresTheEnds() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 50);
|
||||
c.setPointCount(PointCount.FOUR);
|
||||
c.setPointCount(PointCount.THREE);
|
||||
assertEquals(3, c.getControlPointCount());
|
||||
assertPoint(0, 0, c.getControlPoint(0));
|
||||
assertPoint(100, 50, c.getControlPoint(2));
|
||||
}
|
||||
|
||||
// --- transformer ---
|
||||
|
||||
@Test
|
||||
public void rotate_producesTheRotatedRoute() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 20);
|
||||
AffineTransform rotate = AffineTransform.getRotateInstance(Math.PI / 2, 0, 0);
|
||||
List<Point2D> expected = new ArrayList<Point2D>();
|
||||
for (Point2D p : c.buildVertices()) {
|
||||
expected.add(rotate.transform(p, null));
|
||||
}
|
||||
|
||||
new OrthogonalComponentTransformer().rotate(c, new Point2D.Double(0, 0), 1);
|
||||
|
||||
assertEquals(OrientationHV.VERTICAL, c.getStartDirection());
|
||||
List<Point2D> actual = c.buildVertices();
|
||||
assertEquals(expected.size(), actual.size());
|
||||
for (int i = 0; i < expected.size(); i++) {
|
||||
assertPoint(expected.get(i).getX(), expected.get(i).getY(), actual.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mirror_keepsTheStartDirection() {
|
||||
OrthogonalTestComponent c = component(0, 0, 100, 50, 60, 20);
|
||||
new OrthogonalComponentTransformer().mirror(c, new Point2D.Double(50, 25), 1);
|
||||
assertEquals(OrientationHV.HORIZONTAL, c.getStartDirection());
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
private static OrthogonalTestComponent component(double startX, double startY, double endX, double endY,
|
||||
double bendX, double bendY) {
|
||||
OrthogonalTestComponent c = new OrthogonalTestComponent();
|
||||
c.setControlPoint(new Point2D.Double(startX, startY), 0);
|
||||
c.setControlPoint(new Point2D.Double(bendX, bendY), 1);
|
||||
c.setControlPoint(new Point2D.Double(endX, endY), 2);
|
||||
return c;
|
||||
}
|
||||
|
||||
private static void assertRoute(OrthogonalTestComponent c, double... expected) {
|
||||
List<Point2D> vertices = c.buildVertices();
|
||||
assertEquals(expected.length / 2, vertices.size());
|
||||
for (int i = 0; i < vertices.size(); i++) {
|
||||
assertPoint(expected[2 * i], expected[2 * i + 1], vertices.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertPoint(double x, double y, Point2D actual) {
|
||||
assertEquals(x, actual.getX(), DELTA);
|
||||
assertEquals(y, actual.getY(), DELTA);
|
||||
}
|
||||
|
||||
private static void assertAxisAligned(OrthogonalTestComponent c) {
|
||||
List<Point2D> vertices = c.buildVertices();
|
||||
for (int i = 1; i < vertices.size(); i++) {
|
||||
Point2D previous = vertices.get(i - 1);
|
||||
Point2D current = vertices.get(i);
|
||||
boolean aligned = Math.abs(previous.getX() - current.getX()) < DELTA
|
||||
|| Math.abs(previous.getY() - current.getY()) < DELTA;
|
||||
assertTrue("segment " + previous + " to " + current + " is not axis aligned", aligned);
|
||||
}
|
||||
}
|
||||
|
||||
private static class OrthogonalTestComponent extends AbstractOrthogonalComponent<Void> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
protected void drawCurve(Path2D curve, Graphics2D g2d, ComponentState componentState,
|
||||
IDrawingObserver drawingObserver) {}
|
||||
|
||||
@Override
|
||||
protected Color getDefaultColor() {
|
||||
return Color.black;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Void value) {}
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
|
||||
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.connectivity;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Polygon;
|
||||
import java.awt.Stroke;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.List;
|
||||
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.components.AbstractOrthogonalComponent;
|
||||
import org.diylc.components.transform.OrthogonalComponentTransformer;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IContinuity;
|
||||
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.core.annotations.KeywordPolicy;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
|
||||
@ComponentDescriptor(name = "Right Angle Line", author = "Branislav Stojkovic", category = "Schematic Symbols",
|
||||
instanceNamePrefix = "LN", description = "Line with rounded right angle turns and optional arrows",
|
||||
zOrder = IDIYComponent.WIRING, flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW, autoEdit = false,
|
||||
keywordPolicy = KeywordPolicy.SHOW_TAG, keywordTag = "Schematic",
|
||||
transformer = OrthogonalComponentTransformer.class, enableCache = false)
|
||||
public class OrthogonalLine extends AbstractOrthogonalComponent<Void> implements IContinuity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static Color COLOR = Color.black;
|
||||
public static Size THICKNESS = new Size(1d, SizeUnit.px);
|
||||
public static Size ARROW_SIZE = new Size(5d, SizeUnit.px);
|
||||
|
||||
private Size thickness = THICKNESS;
|
||||
private Size arrowSize = ARROW_SIZE;
|
||||
private boolean arrowStart = false;
|
||||
private boolean arrowEnd = false;
|
||||
|
||||
private transient Polygon arrow;
|
||||
|
||||
@Override
|
||||
protected Color getDefaultColor() {
|
||||
return COLOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
|
||||
g2d.setColor(COLOR);
|
||||
g2d.draw(createIconPath(width, height));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawCurve(Path2D curve, Graphics2D g2d, ComponentState componentState,
|
||||
IDrawingObserver drawingObserver) {
|
||||
float thickness = (float) getThickness().convertToPixels();
|
||||
Stroke stroke = null;
|
||||
switch (getStyle()) {
|
||||
case SOLID:
|
||||
stroke = ObjectCache.getInstance().fetchBasicStroke(thickness);
|
||||
break;
|
||||
case DASHED:
|
||||
stroke = ObjectCache.getInstance().fetchStroke(thickness, new float[] {thickness * 2, thickness * 4},
|
||||
thickness * 4, BasicStroke.CAP_SQUARE);
|
||||
break;
|
||||
case DOTTED:
|
||||
stroke = ObjectCache.getInstance().fetchStroke(thickness, new float[] {thickness, thickness * 5}, 0,
|
||||
BasicStroke.CAP_ROUND);
|
||||
break;
|
||||
}
|
||||
g2d.setStroke(stroke);
|
||||
g2d.setColor(componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
|
||||
? SELECTION_COLOR : color);
|
||||
g2d.draw(curve);
|
||||
|
||||
if (getArrowStart() || getArrowEnd()) {
|
||||
List<Point2D> vertices = buildVertices();
|
||||
// a route that collapses onto a single point has no direction to point an arrow along
|
||||
if (vertices.size() > 1) {
|
||||
if (getArrowStart()) {
|
||||
drawArrow(g2d, vertices.get(0), vertices.get(1));
|
||||
}
|
||||
if (getArrowEnd()) {
|
||||
drawArrow(g2d, vertices.getLast(), vertices.get(vertices.size() - 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The arrow head is filled in the line color with its tip on the end point, so the line running
|
||||
* underneath it does not have to be trimmed.
|
||||
*/
|
||||
private void drawArrow(Graphics2D g2d, Point2D tip, Point2D towards) {
|
||||
AffineTransform oldTx = g2d.getTransform();
|
||||
g2d.translate(tip.getX(), tip.getY());
|
||||
g2d.rotate(Math.atan2(towards.getY() - tip.getY(), towards.getX() - tip.getX()) + Math.PI / 2);
|
||||
g2d.fill(getArrow());
|
||||
g2d.setTransform(oldTx);
|
||||
}
|
||||
|
||||
public Polygon getArrow() {
|
||||
if (arrow == null) {
|
||||
arrow = new Polygon();
|
||||
int t = (int) getArrowSize().convertToPixels();
|
||||
arrow.addPoint(0, 0);
|
||||
arrow.addPoint(-t, -t * 2);
|
||||
arrow.addPoint(t, -t * 2);
|
||||
}
|
||||
return arrow;
|
||||
}
|
||||
|
||||
@EditableProperty
|
||||
public Size getThickness() {
|
||||
if (thickness == null) {
|
||||
thickness = THICKNESS;
|
||||
}
|
||||
return thickness;
|
||||
}
|
||||
|
||||
public void setThickness(Size thickness) {
|
||||
this.thickness = thickness;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Arrow Size")
|
||||
public Size getArrowSize() {
|
||||
if (arrowSize == null) {
|
||||
arrowSize = ARROW_SIZE;
|
||||
}
|
||||
return arrowSize;
|
||||
}
|
||||
|
||||
public void setArrowSize(Size arrowSize) {
|
||||
this.arrowSize = arrowSize;
|
||||
arrow = null;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Start Arrow")
|
||||
public boolean getArrowStart() {
|
||||
return arrowStart;
|
||||
}
|
||||
|
||||
public void setArrowStart(boolean arrowStart) {
|
||||
this.arrowStart = arrowStart;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "End Arrow")
|
||||
public boolean getArrowEnd() {
|
||||
return arrowEnd;
|
||||
}
|
||||
|
||||
public void setArrowEnd(boolean arrowEnd) {
|
||||
this.arrowEnd = arrowEnd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void getValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Void value) {}
|
||||
|
||||
@Override
|
||||
public boolean arePointsConnected(int index1, int index2) {
|
||||
return Math.abs(index1 - index2) == getControlPointCount() - 1;
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
|
||||
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.connectivity;
|
||||
|
||||
import java.awt.AlphaComposite;
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Composite;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Shape;
|
||||
import java.awt.Stroke;
|
||||
import java.awt.geom.Path2D;
|
||||
|
||||
import org.diylc.common.ObjectCache;
|
||||
import org.diylc.components.AbstractOrthogonalComponent;
|
||||
import org.diylc.components.transform.OrthogonalComponentTransformer;
|
||||
import org.diylc.core.ComponentState;
|
||||
import org.diylc.core.IContinuity;
|
||||
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 = "Right Angle Wire", author = "Branislav Stojkovic", category = "Connectivity",
|
||||
instanceNamePrefix = "W", description = "Wire that runs horizontally and vertically and turns at right angles",
|
||||
zOrder = IDIYComponent.WIRING, flexibleZOrder = true, bomPolicy = BomPolicy.NEVER_SHOW,
|
||||
transformer = OrthogonalComponentTransformer.class, enableCache = false)
|
||||
public class OrthogonalWire extends AbstractOrthogonalComponent<AWG> implements IContinuity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static Color COLOR = Color.green;
|
||||
public static Color STRIPE_COLOR = Color.yellow;
|
||||
public static double INSULATION_THICKNESS_PCT = 0.3;
|
||||
|
||||
protected AWG gauge = AWG._22;
|
||||
|
||||
protected boolean striped = false;
|
||||
protected Color stripeColor = STRIPE_COLOR;
|
||||
|
||||
@Override
|
||||
protected Color getDefaultColor() {
|
||||
return COLOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawCurve(Path2D curve, Graphics2D g2d, ComponentState componentState, IDrawingObserver drawingObserver) {
|
||||
int thickness = (int) (gauge.diameterIn() * Constants.PIXELS_PER_INCH * (1 + 2 * INSULATION_THICKNESS_PCT)) - 1;
|
||||
Color curveColor =
|
||||
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING ? SELECTION_COLOR
|
||||
: color;
|
||||
g2d.setColor(curveColor);
|
||||
Stroke stroke = null;
|
||||
Stroke tracingStroke = null;
|
||||
switch (getStyle()) {
|
||||
case SOLID:
|
||||
stroke = ObjectCache.getInstance().fetchBasicStroke(thickness);
|
||||
tracingStroke = stroke;
|
||||
break;
|
||||
case DASHED:
|
||||
stroke =
|
||||
ObjectCache.getInstance().fetchStroke(thickness, new float[] {thickness * 2, thickness * 3}, thickness * 4, BasicStroke.CAP_SQUARE);
|
||||
tracingStroke = ObjectCache.getInstance().fetchBasicStroke(thickness);
|
||||
break;
|
||||
case DOTTED:
|
||||
stroke = ObjectCache.getInstance().fetchStroke(thickness, new float[] {thickness / 4, thickness * 3}, 0, BasicStroke.CAP_ROUND);
|
||||
tracingStroke = ObjectCache.getInstance().fetchBasicStroke(thickness);
|
||||
break;
|
||||
}
|
||||
Shape s = stroke.createStrokedShape(curve);
|
||||
|
||||
if (stroke == tracingStroke) {
|
||||
g2d.fill(s);
|
||||
drawingObserver.stopTracking();
|
||||
} else {
|
||||
// draw without tracking
|
||||
drawingObserver.stopTracking();
|
||||
g2d.fill(s);
|
||||
// track with a simplified stroke without the dashes and dots to improve performance;
|
||||
Shape traceShape = tracingStroke.createStrokedShape(curve);
|
||||
drawingObserver.startTracking();
|
||||
Composite oldComposite = g2d.getComposite();
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.DST, 0f));
|
||||
g2d.fill(traceShape);
|
||||
g2d.setComposite(oldComposite);
|
||||
drawingObserver.stopTracking();
|
||||
}
|
||||
|
||||
if (getStriped()) {
|
||||
stroke = ObjectCache.getInstance().fetchStroke(thickness, new float[] { thickness / 2, thickness * 2 }, thickness * 10, BasicStroke.CAP_BUTT);
|
||||
Shape stripe = stroke.createStrokedShape(curve);
|
||||
g2d.setColor(getStripeColor());
|
||||
g2d.fill(stripe);
|
||||
}
|
||||
|
||||
if (componentState == ComponentState.NORMAL) {
|
||||
g2d.setColor(color.darker());
|
||||
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
|
||||
g2d.draw(s);
|
||||
}
|
||||
}
|
||||
|
||||
@EditableProperty(name = "AWG")
|
||||
public AWG getValue() {
|
||||
return gauge;
|
||||
}
|
||||
|
||||
public void setValue(AWG gauge) {
|
||||
this.gauge = gauge;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Stripe")
|
||||
public boolean getStriped() {
|
||||
return striped;
|
||||
}
|
||||
|
||||
public void setStriped(boolean striped) {
|
||||
this.striped = striped;
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Stripe Color")
|
||||
public Color getStripeColor() {
|
||||
if (stripeColor == null) {
|
||||
stripeColor = STRIPE_COLOR;
|
||||
}
|
||||
return stripeColor;
|
||||
}
|
||||
|
||||
public void setStripeColor(Color stripeColor) {
|
||||
this.stripeColor = stripeColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean arePointsConnected(int index1, int index2) {
|
||||
return Math.abs(index1 - index2) == getControlPointCount() - 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
# Orthogonal Connector
|
||||
|
||||
A wire that runs only horizontally and vertically, turning at rounded 90-degree
|
||||
corners.
|
||||
|
||||
## 1. Goal
|
||||
|
||||
`HookupWire` draws a free Bezier curve between two sticky endpoints;
|
||||
`Line` draws a single straight segment. Neither can express the routing style
|
||||
used in schematics and block diagrams, where a connection leaves a terminal on
|
||||
an axis, turns at right angles, and arrives on an axis. This document designs
|
||||
that third kind of connector.
|
||||
|
||||
The shapes that must be reachable are, at minimum, horizontal-vertical-horizontal
|
||||
and vertical-horizontal-vertical. Simple L bends and longer staircases should
|
||||
fall out of the same mechanism rather than needing a separate component.
|
||||
|
||||
## 2. How many handles does the route actually need
|
||||
|
||||
An orthogonal route is a polyline whose segments alternate between horizontal
|
||||
and vertical. Both endpoints are pinned, so counting the free numbers is easy:
|
||||
a route of `s` segments has `s - 1` corners and `s - 2` continuous degrees of
|
||||
freedom, plus **one discrete bit** saying whether the first segment leaves the
|
||||
start point horizontally or vertically.
|
||||
|
||||
| Segments | Shape | Corners | Continuous DOF | Bit |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 2 | `HV` / `VH` (an L) | 1 | 0 | yes |
|
||||
| 3 | `HVH` / `VHV` | 2 | 1 | yes |
|
||||
| 4 | `HVHV` / `VHVH` | 3 | 2 | yes |
|
||||
| 6 | `HVHVHV` / … | 5 | 4 | yes |
|
||||
|
||||
This is the crux of the problem stated in the request. A single interior control
|
||||
point carries two numbers, but the `HVH` / `VHV` case needs *one* number and
|
||||
*one bit*. One point is simultaneously one coordinate too many and one bit too
|
||||
few, so no scheme that reads only that point's position can resolve the
|
||||
arrangement without a heuristic that flips as the user drags across a diagonal.
|
||||
|
||||
The line in the table that is exactly parameterised is the four-segment one: three
|
||||
corners, two degrees of freedom, and a middle corner that is a genuine free point
|
||||
in the plane. That observation drives the whole design.
|
||||
|
||||
## 3. The chosen parameterisation
|
||||
|
||||
**Each interior control point is a real bend vertex, and the route between
|
||||
consecutive vertices is a fixed `H` then `V` pair.** For one interior point `h`
|
||||
the route is
|
||||
|
||||
```
|
||||
A ────────────┐ A = start (sticky)
|
||||
│ c1 = (h.x, A.y) (derived)
|
||||
h ─────────┐ h = the handle (draggable)
|
||||
│ c2 = (B.x, h.y) (derived)
|
||||
B B = end (sticky)
|
||||
```
|
||||
|
||||
that is `A → (h.x, A.y) → h → (B.x, h.y) → B`: four segments, alternating,
|
||||
starting horizontal and ending vertical. Both of the handle's coordinates are
|
||||
used, nothing is discarded, and there is no hidden state.
|
||||
|
||||
The requested arrangements are reached by letting segments collapse to zero
|
||||
length, which is what snap-to-grid makes trivial:
|
||||
|
||||
| Handle placed at | Route degenerates to |
|
||||
| --- | --- |
|
||||
| anywhere | `HVHV`, three visible bends |
|
||||
| `h.y == B.y` | `HVH`, two bends, vertical leg at `h.x` |
|
||||
| `h.x == A.x` | `VHV`, two bends, horizontal leg at `h.y` |
|
||||
| `h == (A.x, B.y)` | `VH`, one bend |
|
||||
| `h == (B.x, A.y)` | `HV`, one bend |
|
||||
|
||||
The handle may sit outside the box spanned by the endpoints, which gives routes
|
||||
that detour around obstacles. Nothing special is needed for that case.
|
||||
|
||||
### Generalising
|
||||
|
||||
`k` interior points chain the same template:
|
||||
|
||||
```
|
||||
A → (h1.x, A.y) → h1 → (h2.x, h1.y) → h2 → … → (B.x, hk.y) → B
|
||||
```
|
||||
|
||||
This is `2k + 2` segments and `2k` degrees of freedom, matching the `s - 2` rule
|
||||
exactly. Every additional handle buys two more bends, every stored number is
|
||||
meaningful, and the normalisation code is the same loop regardless of `k`. The
|
||||
number of handles is an enum property, mirroring `AbstractCurvedComponent`'s
|
||||
`Point Count`.
|
||||
|
||||
### The bit does not need a property
|
||||
|
||||
The template above always leaves the start horizontally, so in principle there is
|
||||
a bit to expose. In practice there is not, because the collapses in the table
|
||||
already cover every way a route can meet its endpoints:
|
||||
|
||||
| Leaves the start | Arrives at the end | Reached by |
|
||||
| --- | --- | --- |
|
||||
| horizontally | vertically | bend point anywhere |
|
||||
| horizontally | horizontally | bend point on the end row |
|
||||
| vertically | vertically | bend point on the start column |
|
||||
| vertically | horizontally | bend point on the opposite corner of the box |
|
||||
|
||||
A `Start Direction` property would therefore be a second, less discoverable way
|
||||
of doing what dragging already does, so it is not offered. The single shape it
|
||||
would add is the mirror-image staircase through a given bend point, which one
|
||||
more bend point reaches anyway.
|
||||
|
||||
The bit does survive as a plain field, because a quarter-turn rotation swaps the
|
||||
two axes and an `H V H V` route becomes a `V H V H` one, which the fixed template
|
||||
cannot express. `OrthogonalComponentTransformer` records the swap there so that
|
||||
rotating a component yields exactly the rotated route. Nothing else reads or
|
||||
writes it, and it never appears in the property editor.
|
||||
|
||||
### Why not the alternatives
|
||||
|
||||
| Scheme | Verdict |
|
||||
| --- | --- |
|
||||
| One handle, only one coordinate honoured, arrangement from an enum | Half of the handle's motion is visibly inert with no explanation, and it does not generalise past two bends without a second property. |
|
||||
| One handle, arrangement inferred from which side of the diagonal it is on | The route snaps between `HVH` and `VHV` mid-drag. Predictable to implement, unpleasant to use. |
|
||||
| Store corners raw and project them onto a legal path on every read | Over-parameterised: `2k` stored numbers for `k - 1` degrees of freedom, so the projection needs to know which point moved last, and a whole-component drag rewrites the raw values and loses the arrangement. |
|
||||
|
||||
## 4. Class layout
|
||||
|
||||
| Class | Module | Role |
|
||||
| --- | --- | --- |
|
||||
| `AbstractOrthogonalComponent<T>` | `diylc-core`, `org.diylc.components` | Sibling of `AbstractCurvedComponent`. Owns the control points, the point-count and start-direction properties, the corner radius, path construction, length and caching bounds. Declares `protected abstract void drawCurve(Path2D, Graphics2D, ComponentState, IDrawingObserver)`, the same hook `AbstractCurvedComponent` uses, so concrete subclasses stay thin. |
|
||||
| `OrthogonalComponentTransformer` | `diylc-core`, `org.diylc.components.transform` | Delegates point rotation to `SimpleComponentTransformer`, then flips `Start Direction`. Mirroring needs no flip and passes straight through. |
|
||||
| `OrthogonalWire` | `diylc-library`, `org.diylc.components.connectivity` | The layout part, shown as `Right Angle Wire` under `Connectivity`. `AWG` value, stripe support, `IContinuity`. Its body is close to a copy of `HookupWire`'s, which is the point of reusing the `drawCurve` hook. |
|
||||
| `OrthogonalLine` | `diylc-library`, `org.diylc.components.connectivity` | The schematic part, shown as `Right Angle Line` under `Schematic Symbols`. Draws the same route as a plain stroke of a given thickness, one pixel by default, with the optional arrow heads `Line` offers. |
|
||||
|
||||
The two concrete parts differ only in `drawCurve` and in the properties each adds
|
||||
on top of the base. `Right Angle Line` mirrors `Line`'s property set exactly:
|
||||
thickness, colour, style, arrow size and the two arrow toggles. It also carries
|
||||
`IContinuity`, which `Line` does not, because it sits in `Schematic Symbols`
|
||||
rather than `Shapes` and a wire drawn between two symbols is expected to show up
|
||||
in the netlist.
|
||||
|
||||
Its arrow heads take their direction from the first and the last route segment
|
||||
rather than from the straight line between the endpoints, and they are not
|
||||
trimmed out of the stroke the way `Line` trims its own. The head is filled in the
|
||||
line colour with its tip on the endpoint, so whatever runs underneath it is
|
||||
already hidden.
|
||||
|
||||
## 5. Geometry
|
||||
|
||||
`buildVertices()` returns the full vertex list including any zero-length
|
||||
collapses, then consecutive duplicates are dropped. `buildPath()` walks the
|
||||
deduplicated list and rounds each corner:
|
||||
|
||||
```
|
||||
r_i = min(cornerRadius, len(v[i-1], v[i]) / 2, len(v[i], v[i+1]) / 2)
|
||||
```
|
||||
|
||||
The corner is drawn as a quadratic Bezier whose control point is the corner
|
||||
itself, from the point `r_i` before it to the point `r_i` after it. Clamping to
|
||||
half of each adjacent segment means short segments and collapsed segments
|
||||
degrade to sharp corners instead of overshooting; an arc would look marginally
|
||||
better at large radii but the difference is invisible at the radii this
|
||||
component is for.
|
||||
|
||||
`Corner Radius` is a `Size` property, default around `0.05in`, `0` giving hard
|
||||
corners.
|
||||
|
||||
## 6. Component contract
|
||||
|
||||
| Member | Behaviour |
|
||||
| --- | --- |
|
||||
| `creationMethod` | `SINGLE_CLICK`, as `HookupWire`. `POINT_BY_POINT` only ever assigns control points `0` and `1`, so it cannot place a component whose second endpoint is not index 1. |
|
||||
| `isControlPointSticky` | `true` only for the first and last index. |
|
||||
| `canControlPointOverlap` | `true` for interior indices. |
|
||||
| `getControlPointVisibilityPolicy` | `WHEN_SELECTED` for every index. |
|
||||
| `getControlPointNodeName` | `null` for interior indices. |
|
||||
| `arePointsConnected` | `true` only for the pair `{0, count - 1}`, exactly as `HookupWire`. |
|
||||
| Selection guidelines | Draw the dashed guideline polyline through the derived vertices with tracking off, as `AbstractCurvedComponent.draw` does. |
|
||||
| Hit area | Falls out of filling the stroked path while the drawing observer is tracking. |
|
||||
| `getLength` | Exact sum of segment lengths, so `IHaveLength` is honest here in a way the Bezier approximation is not. |
|
||||
| `enableCache` | `false`, matching `HookupWire`. |
|
||||
|
||||
## 7. Defaults
|
||||
|
||||
Dropped on an empty canvas the component spans `DEFAULT_SIZE` diagonally, with
|
||||
the single handle at `((A.x + B.x) / 2, B.y)`, which renders as a plain `HVH`
|
||||
route. That is the shape most users expect from a right-angle connector, and one
|
||||
drag of the handle reaches everything else.
|
||||
|
||||
## 8. What was built
|
||||
|
||||
| File | Module |
|
||||
| --- | --- |
|
||||
| `org/diylc/components/AbstractOrthogonalComponent.java` | `diylc-core` |
|
||||
| `org/diylc/components/transform/OrthogonalComponentTransformer.java` | `diylc-core` |
|
||||
| `org/diylc/components/connectivity/OrthogonalWire.java` | `diylc-library` |
|
||||
| `org/diylc/components/connectivity/OrthogonalLine.java` | `diylc-library` |
|
||||
| `org/diylc/components/AbstractOrthogonalComponentTest.java` | `diylc-core`, tests |
|
||||
|
||||
The three open questions were settled as follows.
|
||||
|
||||
1. The component is called `Right Angle Wire` and sits in `Connectivity`, next to
|
||||
`Hookup Wire`, whose drawing code it shares through the `drawCurve` hook. Its
|
||||
property list differs from `Hookup Wire`'s only in carrying `Corner Radius`
|
||||
where the curved wire carries `Smooth`.
|
||||
2. The handle count ships configurable from the start, as `Point Count` with
|
||||
values `Three`, `Four` and `Five`, matching `AbstractCurvedComponent`'s
|
||||
property of the same name. Changing it spreads the bend points into an even
|
||||
staircase between the two ends.
|
||||
3. No snapping was added. The clean shapes are already one grid step away, and a
|
||||
snap that pulls a handle onto the start column or end row would fight
|
||||
snap-to-grid rather than complement it. This is the decision most worth
|
||||
revisiting, since with no `Start Direction` property those two alignments are
|
||||
now the only route to the two-bend shapes.
|
||||
|
||||
The test covers the routing template in both start directions, the degenerate
|
||||
shapes, the invariant that every produced segment is axis aligned, point-count
|
||||
changes, and the transformer contract that rotating the component yields exactly
|
||||
the rotated route.
|
||||
Reference in New Issue
Block a user