mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
better curvature
This commit is contained in:
@@ -289,11 +289,18 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
|
||||
}
|
||||
|
||||
/**
|
||||
* Bows the curve to one side instead of leaving the intermediate points on the straight line
|
||||
* joining the ends, so that a freshly created component reads as a curve that can be reshaped
|
||||
* rather than as a rigid line. The sine envelope vanishes at both ends and peaks in the middle,
|
||||
* which yields a symmetric arc for any point count. The offset always points down the screen so
|
||||
* that the wire sags the same way no matter which end was drawn first.
|
||||
* Waves the curve from side to side instead of leaving the intermediate points on the straight
|
||||
* line joining the ends, so that a freshly created component reads as a slack wire that can be
|
||||
* reshaped rather than as a rigid line. Successive handles go to opposite sides, which turns the
|
||||
* default four point curve into an S, while the points that lie on the curve itself stay on the
|
||||
* straight line and the sine envelope fades the excursions out towards both ends. The first
|
||||
* excursion always points down the screen so that the shape does not depend on which end was
|
||||
* drawn first.
|
||||
* <p>
|
||||
* Keeping the on-curve points on the line is what makes the wave survive: a handle either side
|
||||
* of one of them then comes out as its exact mirror, which is the continuity rule that the
|
||||
* smoothing in {@link #draw} enforces, so that pass leaves the shape alone instead of pulling
|
||||
* one handle across to satisfy it.
|
||||
*/
|
||||
@Override
|
||||
protected Point2D getStretchedPoint(Point2D first, Point2D second, int index) {
|
||||
@@ -301,7 +308,7 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
|
||||
double dx = second.getX() - first.getX();
|
||||
double dy = second.getY() - first.getY();
|
||||
double length = Math.hypot(dx, dy);
|
||||
if (length == 0) {
|
||||
if (length == 0 || isOnCurve(index)) {
|
||||
return p;
|
||||
}
|
||||
if (dx < 0 || (dx == 0 && dy > 0)) {
|
||||
@@ -309,10 +316,44 @@ public abstract class AbstractCurvedComponent<T> extends AbstractTransparentComp
|
||||
dy = -dy;
|
||||
}
|
||||
double t = (double) index / (getControlPointCount() - 1);
|
||||
double offset = CREATION_CURVATURE * length * Math.sin(Math.PI * t);
|
||||
double offset = getStretchedSide(index) * CREATION_CURVATURE * length * Math.sin(Math.PI * t);
|
||||
return new Point2D.Double(p.getX() - dy / length * offset, p.getY() + dx / length * offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index
|
||||
* @return true if the control point at the specified index lies on the curve rather than pulling
|
||||
* it from the side, which follows from how {@link #draw} chains the segments together.
|
||||
*/
|
||||
private boolean isOnCurve(int index) {
|
||||
if (index == 0 || index == getControlPointCount() - 1) {
|
||||
return true;
|
||||
}
|
||||
if (getPointCount() == PointCount.FIVE) {
|
||||
return index == 2;
|
||||
}
|
||||
if (getPointCount() == PointCount.SEVEN) {
|
||||
return index == 3;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index
|
||||
* @return which side of the straight line the handle at the specified index is pushed to,
|
||||
* counting handles rather than control points so that an on-curve point in between does
|
||||
* not break the alternation.
|
||||
*/
|
||||
private int getStretchedSide(int index) {
|
||||
int handleCount = 0;
|
||||
for (int i = 1; i < index; i++) {
|
||||
if (!isOnCurve(i)) {
|
||||
handleCount++;
|
||||
}
|
||||
}
|
||||
return handleCount % 2 == 0 ? 1 : -1;
|
||||
}
|
||||
|
||||
private Point2D findThirdPoint(Point2D p0, Point2D p) {
|
||||
return new Point2D.Double(2 * p0.getX() - p.getX(), 2 * p0.getY() - p.getY());
|
||||
}
|
||||
|
||||
@@ -481,6 +481,14 @@
|
||||
<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>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Double click on a component type in the tree or the toolbar to instantiate it at once, even for those that are intended to be placed point-by-point</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>All curved components ('Curved Trace', 'Hookup Wire', 'Twisted Leads') are now getting created between the two points by default instead of getting created with some preset shape and length</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
|
||||
+79
-9
@@ -48,33 +48,103 @@ public class AbstractCurvedComponentTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_bowsTheIntermediatePointsOffTheChord() {
|
||||
public void stretchBetween_movesTheIntermediatePointsOffTheChord() {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
assertTrue("first handle should not sit on the chord", c.getControlPoint(1).getY() > 1);
|
||||
assertTrue("second handle should not sit on the chord", c.getControlPoint(2).getY() > 1);
|
||||
assertTrue("first handle should not sit on the chord",
|
||||
Math.abs(c.getControlPoint(1).getY()) > 1);
|
||||
assertTrue("second handle should not sit on the chord",
|
||||
Math.abs(c.getControlPoint(2).getY()) > 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_bowIsSymmetric() {
|
||||
public void stretchBetween_putsConsecutivePointsOnOppositeSides() {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
assertEquals(c.getControlPoint(1).getY(), c.getControlPoint(2).getY(), DELTA);
|
||||
assertTrue("the first excursion should point down the screen", c.getControlPoint(1).getY() > 0);
|
||||
assertTrue("the second excursion should point the other way", c.getControlPoint(2).getY() < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_alternatesSidesForEveryPointCount() {
|
||||
for (PointCount pointCount : PointCount.values()) {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.setPointCount(pointCount);
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
double previous = 0;
|
||||
for (int i = 1; i < c.getControlPointCount() - 1; i++) {
|
||||
double current = c.getControlPoint(i).getY();
|
||||
// points that lie on the curve stay on the chord and do not take part in the alternation
|
||||
if (Math.abs(current) < DELTA) {
|
||||
continue;
|
||||
}
|
||||
if (previous != 0) {
|
||||
assertTrue(pointCount + " handle " + i + " should sit opposite the previous one",
|
||||
previous * current < 0);
|
||||
}
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_keepsTheOnCurvePointsOnTheStraightLine() {
|
||||
for (PointCount pointCount : new PointCount[] {PointCount.FIVE, PointCount.SEVEN}) {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.setPointCount(pointCount);
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
assertEquals(pointCount + " should keep its middle point on the chord", 0,
|
||||
c.getControlPoint(middleIndex(pointCount)).getY(), DELTA);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The smoothing in {@code draw} mirrors the handles around a point that lies on the curve, and
|
||||
* rewrites one of them when they are not already mirrored. Placing them mirrored to begin with
|
||||
* is what keeps that pass from pulling the freshly created wave out of shape.
|
||||
*/
|
||||
@Test
|
||||
public void stretchBetween_placesHandlesTheWayTheSmoothingRuleWants() {
|
||||
for (PointCount pointCount : new PointCount[] {PointCount.FIVE, PointCount.SEVEN}) {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.setPointCount(pointCount);
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 60));
|
||||
int middle = middleIndex(pointCount);
|
||||
Point2D before = c.getControlPoint(middle - 1);
|
||||
Point2D on = c.getControlPoint(middle);
|
||||
Point2D after = c.getControlPoint(middle + 1);
|
||||
assertEquals(pointCount + " handles should mirror horizontally", 2 * on.getX() - before.getX(),
|
||||
after.getX(), DELTA);
|
||||
assertEquals(pointCount + " handles should mirror vertically", 2 * on.getY() - before.getY(),
|
||||
after.getY(), DELTA);
|
||||
}
|
||||
}
|
||||
|
||||
private static int middleIndex(PointCount pointCount) {
|
||||
return pointCount == PointCount.FIVE ? 2 : 3;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_waveIsSymmetricAboutTheMiddle() {
|
||||
CurvedTestComponent c = new CurvedTestComponent();
|
||||
c.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
assertEquals(-c.getControlPoint(1).getY(), c.getControlPoint(2).getY(), DELTA);
|
||||
assertEquals(100 - c.getControlPoint(2).getX(), c.getControlPoint(1).getX(), DELTA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_sagsTheSameWayRegardlessOfDrawingDirection() {
|
||||
public void stretchBetween_wavesTheSameWayRegardlessOfDrawingDirection() {
|
||||
CurvedTestComponent forward = new CurvedTestComponent();
|
||||
forward.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(100, 0));
|
||||
CurvedTestComponent backward = new CurvedTestComponent();
|
||||
backward.stretchBetween(new Point2D.Double(100, 0), new Point2D.Double(0, 0));
|
||||
assertEquals(forward.getControlPoint(1).getY(), backward.getControlPoint(2).getY(), DELTA);
|
||||
assertEquals(forward.getControlPoint(2).getY(), backward.getControlPoint(1).getY(), DELTA);
|
||||
for (int i = 1; i < forward.getControlPointCount() - 1; i++) {
|
||||
assertEquals(forward.getControlPoint(i).getY(), backward.getControlPoint(i).getY(), DELTA);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stretchBetween_scalesTheBowWithTheDistance() {
|
||||
public void stretchBetween_scalesTheWaveWithTheDistance() {
|
||||
CurvedTestComponent shortWire = new CurvedTestComponent();
|
||||
shortWire.stretchBetween(new Point2D.Double(0, 0), new Point2D.Double(50, 0));
|
||||
CurvedTestComponent longWire = new CurvedTestComponent();
|
||||
|
||||
Reference in New Issue
Block a user