diff --git a/curvebuilding-core/src/main/java/me/kous500/curvebuilding/commands/bc/CurveLayout.java b/curvebuilding-core/src/main/java/me/kous500/curvebuilding/commands/bc/CurveLayout.java index 1538a7b..8c32880 100644 --- a/curvebuilding-core/src/main/java/me/kous500/curvebuilding/commands/bc/CurveLayout.java +++ b/curvebuilding-core/src/main/java/me/kous500/curvebuilding/commands/bc/CurveLayout.java @@ -31,8 +31,11 @@ static final class TooLongException extends Exception { } } - /** A step along a segment: where the curve is, which way it goes, and how far it is from the segment start. */ - private record Sample(Vector3 point, double angle, double distance) { + /** + * A step along a segment: where the curve is, which way is across it on the xz plane (of length 1), and how far it + * is from the segment start. + */ + private record Sample(Vector3 point, Vector3 across, double distance) { } private final Selection selection; @@ -121,15 +124,23 @@ private static List samples(BezierSegment segment, double fineness) { // At 0 a handle can be at the point itself, where the curve has no direction yet double at = t == 0 ? 0.00001 : t; Vector3 point = segment.point(at); - distance += Math.sqrt((point.x() - before.x()) * (point.x() - before.x()) - + (point.y() - before.y()) * (point.y() - before.y()) - + (point.z() - before.z()) * (point.z() - before.z())); - samples.add(new Sample(point, segment.angle(at), distance)); + distance += before.distance(point); + samples.add(new Sample(point, across(segment.direction(at)), distance)); before = point; } return samples; } + /** + * {@code direction} on the xz plane, turned a quarter turn the way that takes +z to +x, and of length 1. +x when it + * does not move on the xz plane, as for a curve towards +z. + */ + private static Vector3 across(Vector3 direction) { + double length = Math.hypot(direction.x(), direction.z()); + if (length == 0) return Vector3.at(1, 0, 0); + return Vector3.at(direction.z() / length, 0, -direction.x() / length); + } + /** * Lays row {@code y} of the cross section, from its middle outwards on both sides. The middle column is laid from * both sides, so it counts twice. @@ -167,8 +178,8 @@ private void layRow(List samples, int y, Vector3 middle) throws MaxChang private void layColumn(List samples, int side, int y, Vector3 column) throws MaxChangedBlocksException { BlockVector3 before = null; for (Sample sample : samples) { - Vector3 across = Vector3.at(side * Math.cos(-sample.angle()), y, side * Math.sin(-sample.angle())); - BlockVector3 target = BlockVector3.round(sample.point().add(across)); + Vector3 fromCurve = sample.across().multiply(side).add(0, y, 0); + BlockVector3 target = BlockVector3.round(sample.point().add(fromCurve)); if (!target.equals(before)) { // How far into the repeating selection this block is diff --git a/curvebuilding-core/src/main/java/me/kous500/curvebuilding/curve/BezierSegment.java b/curvebuilding-core/src/main/java/me/kous500/curvebuilding/curve/BezierSegment.java index ff4020e..a2a77df 100644 --- a/curvebuilding-core/src/main/java/me/kous500/curvebuilding/curve/BezierSegment.java +++ b/curvebuilding-core/src/main/java/me/kous500/curvebuilding/curve/BezierSegment.java @@ -90,15 +90,13 @@ public Vector3 blockCenter(double t) { } /** - * The direction of the curve at {@code t} on the xz plane, as the angle in radians from +z towards +x, from - * {@code -Math.PI} to {@code Math.PI}. 0 when the curve does not move on the xz plane there. + * The direction that the curve goes at {@code t}: its derivative divided by 3, the steps between the points + * weighted by the quadratic Bernstein polynomials. Zero at an end whose handle is at its point. * * @param t where on the segment, from 0 to 1 - * @return the angle in radians + * @return the direction, not scaled to any length */ - public double angle(double t) { - // The direction of the curve, its derivative divided by 3: the steps between the points, weighted by the - // quadratic Bernstein polynomials + public Vector3 direction(double t) { double u = 1 - t; double quadratic0 = u * u; double quadratic1 = 2 * u * t; @@ -108,13 +106,9 @@ public double angle(double t) { Vector3 step1 = p2.subtract(p1); Vector3 step2 = p3.subtract(p2); - Vector3 direction = step0.multiply(quadratic0) + return step0.multiply(quadratic0) .add(step1.multiply(quadratic1)) .add(step2.multiply(quadratic2)); - - // atan2 gives Math.PI or -Math.PI for a zero vector when z is -0.0 - if (direction.x() == 0 && direction.z() == 0) return 0; - return Math.atan2(direction.x(), direction.z()); } /** @@ -134,9 +128,7 @@ public double length(double fineness) { Vector3 before = blockCenter(0); for (int step = 1; step <= fineness; step++) { Vector3 p = blockCenter(step / fineness); - length += Math.sqrt((p.x() - before.x()) * (p.x() - before.x()) - + (p.y() - before.y()) * (p.y() - before.y()) - + (p.z() - before.z()) * (p.z() - before.z())); + length += before.distance(p); before = p; } return length; diff --git a/curvebuilding-core/src/main/java/me/kous500/curvebuilding/preview/ParticleLayout.java b/curvebuilding-core/src/main/java/me/kous500/curvebuilding/preview/ParticleLayout.java index f61c2a7..701d90c 100644 --- a/curvebuilding-core/src/main/java/me/kous500/curvebuilding/preview/ParticleLayout.java +++ b/curvebuilding-core/src/main/java/me/kous500/curvebuilding/preview/ParticleLayout.java @@ -132,15 +132,16 @@ private void addCross(Vector3 block, Kind kind) { // Each line of the cross goes from a corner of the block to its center, sqrt(3) / 2 blocks, and has at least // its two ends int density = Math.max(1, (int) (settings.pointDensity() * Math.sqrt(3) / 2)); + Vector3 center = block.add(0.5, 0.5, 0.5); for (int x = 0; x <= 1; x++) { for (int y = 0; y <= 1; y++) { for (int z = 0; z <= 1; z++) { + Vector3 corner = block.add(x, y, z); + Vector3 toCenter = center.subtract(corner); for (int i = 0; i <= density; i++) { - double ax = 0.5 * x + (double) (density * x + (-x * 2 + 1) * i) / density * 0.5; - double ay = 0.5 * y + (double) (density * y + (-y * 2 + 1) * i) / density * 0.5; - double az = 0.5 * z + (double) (density * z + (-z * 2 + 1) * i) / density * 0.5; - particles.add(new Particle(block.add(ax, ay, az), kind)); + Vector3 position = corner.add(toCenter.multiply((double) i / density)); + particles.add(new Particle(position, kind)); } } } @@ -153,12 +154,11 @@ private void addHandleLine(@Nullable Vector3 point, @Nullable Vector3 handle) { int distance = (int) point.distance(handle); if (distance > settings.maxLength()) return; + Vector3 start = point.add(0.5, 0.5, 0.5); + Vector3 toHandle = handle.subtract(point); List candidates = new ArrayList<>(); for (double t = 0; t <= 1; t += 1.0 / (distance * settings.lineDensity())) { - candidates.add(Vector3.at( - (1 - t) * point.x() + t * handle.x() + 0.5, - (1 - t) * point.y() + t * handle.y() + 0.5, - (1 - t) * point.z() + t * handle.z() + 0.5)); + candidates.add(start.add(toHandle.multiply(t))); } addDots(candidates, point, Kind.HANDLE_LINE); } diff --git a/curvebuilding-core/src/test/java/me/kous500/curvebuilding/commands/bc/CurveLayoutTest.java b/curvebuilding-core/src/test/java/me/kous500/curvebuilding/commands/bc/CurveLayoutTest.java index ec5b979..032d044 100644 --- a/curvebuilding-core/src/test/java/me/kous500/curvebuilding/commands/bc/CurveLayoutTest.java +++ b/curvebuilding-core/src/test/java/me/kous500/curvebuilding/commands/bc/CurveLayoutTest.java @@ -85,6 +85,16 @@ void crossSectionIsLaidAcrossTheCurve() throws Exception { assertEquals(5 * 9, blocks.size()); } + @Test + void crossSectionTurnsWithTheCurve() throws Exception { + CurveLayout.Selection selection = new CurveLayout.Selection(1, 3, 3, Vector3.at(0, 1, 1)); + BezierSegment line = straight(Vector3.at(0, 64, 0), Vector3.at(0, 64, 4)); + NavigableMap blocks = layout(selection, NO_ARGUMENTS, 0, line).blocks(); + + assertEquals(Set.of(-1, 0, 1), xs(blocks)); + assertEquals(5 * 9, blocks.size()); + } + /** Which of the four blocks of {@link #FOUR_ALONG_X} each block of the curve gets, from the start of the curve. */ private static List blocksUsed(int offset, BezierSegment... segments) throws Exception { List used = new ArrayList<>(); diff --git a/curvebuilding-core/src/test/java/me/kous500/curvebuilding/curve/BezierSegmentTest.java b/curvebuilding-core/src/test/java/me/kous500/curvebuilding/curve/BezierSegmentTest.java index 64333d7..6c288ac 100644 --- a/curvebuilding-core/src/test/java/me/kous500/curvebuilding/curve/BezierSegmentTest.java +++ b/curvebuilding-core/src/test/java/me/kous500/curvebuilding/curve/BezierSegmentTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import java.util.List; @@ -70,22 +69,24 @@ void rejectsPositionOutsideCurve(double t) { } @Nested - class Angle { - // dx and dz of the direction; the angle goes from +z towards +x - @ParameterizedTest - @CsvSource({ - "0, 1, 0", - "1, 0, 1.5707963267948966", - "0, -1, 3.141592653589793", - "-1, 0, -1.5707963267948966", - "1, 1, 0.7853981633974483", - "-1, -1, -2.356194490192345", - "0, 0, 0", - }) - void followsTheCurveOnTheXzPlane(double dx, double dz, double expected) { - BezierSegment segment = straight(Vector3.at(0, 64, 0), Vector3.at(dx * 10, 70, dz * 10)); - // The middle, as a straight segment without handles has no direction at its ends - assertEquals(expected, segment.angle(0.5), 1e-12); + class Direction { + @Test + void endsGoTowardsTheirHandles() { + assertVector(0, 10, 0, ARCH.direction(0)); + assertVector(0, -10, 0, ARCH.direction(1)); + } + + @Test + void midpointUsesQuadraticBernsteinWeights() { + // weights at t = 0.5 are 1/4, 1/2, 1/4 on the steps (0, 10, 0), (10, 0, 0) and (0, -10, 0) + assertVector(5, 0, 0, ARCH.direction(0.5)); + } + + @Test + void endWithoutHandleHasNoDirection() { + BezierSegment segment = straight(Vector3.at(0, 64, 0), Vector3.at(10, 64, 0)); + assertVector(0, 0, 0, segment.direction(0)); + assertVector(5, 0, 0, segment.direction(0.5)); } }