From 7b2366981636c4fdb81b4a82677aa8203e7da5dc Mon Sep 17 00:00:00 2001 From: kous500 <75741517+kous500@users.noreply.github.com> Date: Sat, 14 Nov 2026 23:38:22 +0900 Subject: [PATCH] fix: Lay the cross section of //bc across the curve at its ends At an end whose handle is at its point, the curve has no direction, and //bc laid the cross section along +x there. It stepped along the curve by adding 1 / fineness to t, which reached t = 1 only for some lengths, such as most short ones with a fineness of 1: then the end of a curve along x got its cross section along the curve, past its end. - BezierSegment.tangent gives the direction also where it is zero: at an end without a handle, the curve leaves towards, or arrives from, the other handle. - //bc takes its steps as whole steps divided by the fineness, so the end of a segment is always one, and uses tangent for the direction there instead of starting at t = 0.00001. --- .../commands/bc/CurveLayout.java | 9 +++--- .../curvebuilding/curve/BezierSegment.java | 19 ++++++++++++ .../commands/bc/CurveLayoutTest.java | 13 ++++++++ .../curve/BezierSegmentTest.java | 30 +++++++++++++++++++ 4 files changed, 66 insertions(+), 5 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 8c32880..a21488a 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 @@ -120,12 +120,11 @@ private static List samples(BezierSegment segment, double fineness) { List samples = new ArrayList<>(); Vector3 before = segment.p0(); double distance = 0; - for (double t = 0; t <= 1; t += 1.0 / 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); + for (int step = 0; step <= fineness; step++) { + double t = step / fineness; + Vector3 point = segment.point(t); distance += before.distance(point); - samples.add(new Sample(point, across(segment.direction(at)), distance)); + samples.add(new Sample(point, across(segment.tangent(t)), distance)); before = point; } return samples; 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 a2a77df..7d9959d 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 @@ -111,6 +111,25 @@ public Vector3 direction(double t) { .add(step2.multiply(quadratic2)); } + /** + * The direction that the curve goes at {@code t}, like {@link #direction}, but also where that is zero, as at an + * end whose handle is at its point. There the curve leaves {@code p0} towards {@code p2}, or arrives at {@code p3} + * from {@code p1}, as {@link #direction} does just beside it; when that is zero too, from {@code p0} to + * {@code p3}. + * + * @param t where on the segment, from 0 to 1 + * @return the direction, not scaled to any length; zero when none of these tells it, as when all four positions + * are at one place + */ + public Vector3 tangent(double t) { + Vector3 direction = direction(t); + if (direction.lengthSq() != 0) return direction; + + Vector3 besideTheEnd = t < 0.5 ? p2.subtract(p0) : p3.subtract(p1); + if (besideTheEnd.lengthSq() != 0) return besideTheEnd; + return p3.subtract(p0); + } + /** * The length of the curve, measured along {@code fineness} straight steps. * 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 032d044..139a547 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,19 @@ void crossSectionIsLaidAcrossTheCurve() throws Exception { assertEquals(5 * 9, blocks.size()); } + @Test + void crossSectionIsAcrossTheCurveAtItsEnd() throws Exception { + // With one step per block, the last step is at the end, where the segment has no handle + CurveLayout.Selection selection = new CurveLayout.Selection(1, 1, 3, Vector3.at(0, 0, 1)); + BezierSegment line = straight(Vector3.at(0, 64, 0), Vector3.at(4, 64, 0)); + CurveLayout layout = new CurveLayout(selection, NO_ARGUMENTS, NEVER_AIR, 0, 10000, 1); + layout.add(line); + NavigableMap blocks = layout.blocks(); + + assertEquals(Set.of(0, 1, 2, 3, 4), xs(blocks)); + assertEquals(5 * 3, blocks.size()); + } + @Test void crossSectionTurnsWithTheCurve() throws Exception { CurveLayout.Selection selection = new CurveLayout.Selection(1, 3, 3, Vector3.at(0, 1, 1)); 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 6c288ac..7b00c14 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 @@ -90,6 +90,36 @@ void endWithoutHandleHasNoDirection() { } } + @Nested + class Tangent { + @Test + void isTheDirectionWhereThereIsOne() { + assertVector(5, 0, 0, ARCH.tangent(0.5)); + assertVector(0, 10, 0, ARCH.tangent(0)); + } + + @Test + void straightSegmentGoesFromEndToEndAtItsEnds() { + BezierSegment segment = straight(Vector3.at(0, 64, 0), Vector3.at(10, 64, 0)); + assertVector(10, 0, 0, segment.tangent(0)); + assertVector(10, 0, 0, segment.tangent(1)); + } + + @Test + void endWithoutHandleGoesTowardsTheOtherHandle() { + Vector3 start = Vector3.at(0, 64, 0); + BezierSegment segment = new BezierSegment(start, start, Vector3.at(0, 64, 10), Vector3.at(10, 64, 10)); + assertVector(0, 0, 10, segment.tangent(0)); + assertVector(10, 0, 0, segment.tangent(1)); + } + + @Test + void segmentAtOnePlaceHasNoDirection() { + Vector3 a = Vector3.at(3, 64, 3); + assertVector(0, 0, 0, new BezierSegment(a, a, a, a).tangent(0)); + } + } + @Nested class Length { @ParameterizedTest