From 76cffa4d98e3c4d01d343316ebb0d7d97da906c8 Mon Sep 17 00:00:00 2001 From: kous500 <75741517+kous500@users.noreply.github.com> Date: Sat, 14 Nov 2026 22:51:16 +0900 Subject: [PATCH] chore: Lay the cross section and the particle lines with vector operations - //bc turns the direction of the curve a quarter turn on the xz plane to lay its cross section across it, instead of turning it into an angle and back with cos and sin. BezierSegment.direction gives that direction, and angle, which nothing else used, is removed. - The cross of a handle goes from each corner of the block towards its center, and the handle line from the point towards the handle, written with vectors instead of each coordinate. - The length of a segment and the distance along it use Vector3.distance. --- .../commands/bc/CurveLayout.java | 27 +++++++++----- .../curvebuilding/curve/BezierSegment.java | 20 ++++------- .../curvebuilding/preview/ParticleLayout.java | 16 ++++----- .../commands/bc/CurveLayoutTest.java | 10 ++++++ .../curve/BezierSegmentTest.java | 35 ++++++++++--------- 5 files changed, 61 insertions(+), 47 deletions(-) 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)); } }