mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-13 02:32:09 +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:
@@ -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) {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user