From 146ac36a1e94907629c375de25651114d342ca07 Mon Sep 17 00:00:00 2001 From: Fff Date: Tue, 10 Nov 2026 17:04:58 -0800 Subject: [PATCH] =?UTF-8?q?fix(cube):=20=E6=8C=89=E5=87=A0=E4=BD=95?= =?UTF-8?q?=E5=B0=BA=E5=BA=A6=E4=BF=9D=E6=8C=81=E6=8E=A5=E7=BC=9D=E9=9A=90?= =?UTF-8?q?=E8=97=8F=E9=98=88=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为选择几何增加每格坐标单位参数,保留默认方块坐标调用方式 - 按尺度换算接缝距离与投影容差,保留薄层几何和部分可见线段 - 覆盖缩放、严格阈值、模型浮点边界与相机变换回归 --- .../v2/cube/geometry/OutlineVisibility.java | 43 ++++++++++++------- .../v2/cube/geometry/SelectionGeometry.java | 22 +++++++++- .../cube/geometry/OutlineVisibilityTest.java | 43 +++++++++++++++++++ 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/module.cube/src/main/java/dev/anvilcraft/lib/v2/cube/geometry/OutlineVisibility.java b/module.cube/src/main/java/dev/anvilcraft/lib/v2/cube/geometry/OutlineVisibility.java index 461c450e8..b9838477b 100644 --- a/module.cube/src/main/java/dev/anvilcraft/lib/v2/cube/geometry/OutlineVisibility.java +++ b/module.cube/src/main/java/dev/anvilcraft/lib/v2/cube/geometry/OutlineVisibility.java @@ -13,20 +13,24 @@ final class OutlineVisibility { private static final double EPSILON = 2.0E-7D; private static final int MAX_VIEW_OPERATIONS = 200_000; private final List shapes; + private final double distanceEpsilon; private final List> seams; - private OutlineVisibility(List shapes, List> seams) { + private OutlineVisibility(List shapes, List> seams, double distanceEpsilon) { this.shapes = shapes; + this.distanceEpsilon = distanceEpsilon; this.seams = seams; } static OutlineVisibility build(SelectionGeometry geometry, List segments, Runnable step) { List> seams = new ArrayList<>(segments.size()); boolean any = false; + double offset = OFFSET * geometry.unitsPerBlock(); + double epsilon = EPSILON * geometry.unitsPerBlock(); for (OutlineBuilder.Segment segment : segments) { step.run(); List intervals = new ArrayList<>(); - List nearby = geometry.intersecting(new AABB(segment.start(), segment.end()).inflate(OFFSET)); + List nearby = geometry.intersecting(new AABB(segment.start(), segment.end()).inflate(offset)); for (int i = 0; i < nearby.size(); i++) { for (int j = i + 1; j < nearby.size(); j++) { ConvexShape first = nearby.get(i), second = nearby.get(j); @@ -38,16 +42,18 @@ static OutlineVisibility build(SelectionGeometry geometry, List= OFFSET - EPSILON) continue; - double low = Math.min(a.planeOffset(), otherOffset) - EPSILON; - double high = Math.max(a.planeOffset(), otherOffset) + EPSILON; + if (distance <= epsilon || distance >= offset - epsilon) { + continue; + } + double low = Math.min(a.planeOffset(), otherOffset) - epsilon; + double high = Math.max(a.planeOffset(), otherOffset) + epsilon; Interval interval = new Interval(0, 1); interval = clip(interval, a.normal().dot(segment.start()) - high, a.normal().dot(segment.end()) - high); interval = clip(interval, low - a.normal().dot(segment.start()), low - a.normal().dot(segment.end())); - interval = clipShape(interval, segment, first, a.normal(), distance, step); - interval = clipShape(interval, segment, second, a.normal(), distance, step); + interval = clipShape(interval, segment, first, a.normal(), distance, epsilon, step); + interval = clipShape(interval, segment, second, a.normal(), distance, epsilon, step); if (interval.valid()) intervals.add(interval); } } @@ -57,16 +63,16 @@ static OutlineVisibility build(SelectionGeometry geometry, List list.size() * 40L).sum(); + return 72L + this.seams.size() * 32L + this.seams.stream().mapToLong(list -> list.size() * 40L).sum(); } PackedOutline forView(PackedOutline outline, Vec3 camera) { @@ -82,7 +88,9 @@ PackedOutline forView(PackedOutline outline, Vec3 camera) { Work work = new Work(); try { List> cones = new ArrayList<>(this.shapes.size()); - for (ConvexShape shape : this.shapes) cones.add(project(shape, camera, work)); + for (ConvexShape shape : this.shapes) { + cones.add(project(shape, camera, work, this.distanceEpsilon)); + } List visible = new ArrayList<>(); for (int index = 0; index < outline.segmentCount(); index++) { work.step(); @@ -98,7 +106,7 @@ PackedOutline forView(PackedOutline outline, Vec3 camera) { for (Vec3 plane : cone) { work.step(); double a = plane.dot(start.subtract(camera)), b = plane.dot(end.subtract(camera)); - if (Math.abs(a) <= EPSILON && Math.abs(b) <= EPSILON) { + if (Math.abs(a) <= this.distanceEpsilon && Math.abs(b) <= this.distanceEpsilon) { double direction = plane.dot(side); if (direction > EPSILON) coversLeft = false; if (direction < -EPSILON) coversRight = false; @@ -139,7 +147,7 @@ private static void append(List output, Vec3 start, Vec3 output.add(new OutlineBuilder.Segment(start.lerp(end, low), start.lerp(end, high))); } - private static List project(ConvexShape shape, Vec3 camera, Work work) { + private static List project(ConvexShape shape, Vec3 camera, Work work, double epsilon) { List planes = new ArrayList<>(); for (ConvexShape.Edge edge : shape.edges()) { work.step(); @@ -153,8 +161,11 @@ private static List project(ConvexShape shape, Vec3 camera, Work work) { min = Math.min(min, distance); max = Math.max(max, distance); } - if (max <= EPSILON && min < -EPSILON) planes.add(normal); - else if (min >= -EPSILON && max > EPSILON) planes.add(normal.scale(-1)); + if (max <= epsilon && min < -epsilon) { + planes.add(normal); + } else if (min >= -epsilon && max > epsilon) { + planes.add(normal.scale(-1)); + } } return planes; } diff --git a/module.cube/src/main/java/dev/anvilcraft/lib/v2/cube/geometry/SelectionGeometry.java b/module.cube/src/main/java/dev/anvilcraft/lib/v2/cube/geometry/SelectionGeometry.java index ee3f6d12e..a3ddb8ab9 100644 --- a/module.cube/src/main/java/dev/anvilcraft/lib/v2/cube/geometry/SelectionGeometry.java +++ b/module.cube/src/main/java/dev/anvilcraft/lib/v2/cube/geometry/SelectionGeometry.java @@ -16,8 +16,24 @@ public final class SelectionGeometry { private final Node root; private final PackedOutline fallback; private final long estimatedBytes; + private final double unitsPerBlock; public SelectionGeometry(List shapes) { + this(shapes, 1); + } + + /** + * 指定几何坐标中一格对应的单位数,使放大后的薄层仍按实际尺寸判断微小接缝。 + * 此参数只影响轮廓接缝容差,调用方仍须通过 SelectionPart 的变换还原坐标。 + * + * @param shapes 局部坐标中的闭凸体 + * @param unitsPerBlock 有限正数;普通方块坐标传 1,放大 128 倍的坐标传 128 + */ + public SelectionGeometry(List shapes, double unitsPerBlock) { + if (!Double.isFinite(unitsPerBlock) || unitsPerBlock <= 0) { + throw new IllegalArgumentException("unitsPerBlock must be finite and positive"); + } + this.unitsPerBlock = unitsPerBlock; if (shapes.isEmpty() || shapes.size() > MAX_SHAPES) throw new IllegalArgumentException("Invalid shape count"); this.shapes = List.copyOf(shapes); this.root = build(new ArrayList<>(shapes)); @@ -28,10 +44,14 @@ public SelectionGeometry(List shapes) { for (ConvexShape.Edge edge : shape.edges()) edges.add(new OutlineBuilder.Segment(edge.start(), edge.end())); } this.fallback = PackedOutline.of(edges); - this.estimatedBytes = 160L + shapes.size() * 256L + this.fallback.estimatedBytes() + this.estimatedBytes = 168L + shapes.size() * 256L + this.fallback.estimatedBytes() + shapes.stream().mapToLong(ConvexShape::estimatedBytes).sum(); } + double unitsPerBlock() { + return this.unitsPerBlock; + } + public List shapes() { return this.shapes; } public AABB bounds() { return this.root.bounds; } public PackedOutline fallbackOutline() { return this.fallback; } diff --git a/module.cube/src/regressionTest/java/dev/anvilcraft/lib/v2/cube/geometry/OutlineVisibilityTest.java b/module.cube/src/regressionTest/java/dev/anvilcraft/lib/v2/cube/geometry/OutlineVisibilityTest.java index 2a9b5efb0..d546072d3 100644 --- a/module.cube/src/regressionTest/java/dev/anvilcraft/lib/v2/cube/geometry/OutlineVisibilityTest.java +++ b/module.cube/src/regressionTest/java/dev/anvilcraft/lib/v2/cube/geometry/OutlineVisibilityTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; class OutlineVisibilityTest { private static final double SMALL = 0.05D / 16; @@ -137,6 +138,48 @@ void transformedPartsConvertTheCameraToGeometrySpace() { assertFalse(covers(build(step(SMALL)).forView(local), new Vec3(0.5, 1 + SMALL, 0.5))); } + @Test + void scaledCoordinatesPreserveSeamThresholdAndCameraTransform() { + for (double scale : new double[] {0.5d, 1, 128}) { + for (double offset : new double[] {SMALL, 0.09999d / 16, 0.1d / 16, 0.10001d / 16, (float) 16.1 / 16d - 1, 0.2d / 16}) { + List shapes = step(offset).stream() + .map(shape -> shape.transform(new Matrix4d().scaling(scale))).toList(); + SelectionGeometry geometry = new SelectionGeometry(shapes, scale); + Matrix4f transform = new Matrix4f().translation(4, -2, 1).rotateY(0.7f).scale((float) (1 / scale)); + SelectionPart part = new SelectionPart(geometry, transform); + Vec3 camera = new Vec3(0.75, 8, 0.5).scale(scale); + Vec3 localCamera = part.localPosition(transform(new Matrix4d(transform), camera)); + PackedOutline outline = OutlineBuilder.build(geometry, TimeUnit.SECONDS.toNanos(10), 10_000_000); + Vec3 seam = new Vec3(0.5, 1 + offset, 0.5).scale(scale); + assertTrue(covers(outline, seam)); + assertEquals(offset >= 0.1d / 16, covers(outline.forView(localCamera), seam)); + assertTrue(covers(outline.forView(localCamera), new Vec3(0, 1, 0.5).scale(scale))); + } + } + } + + @Test + void scaledCoordinatesPreservePartiallyVisibleSeams() { + double scale = 128; + List shapes = List.of(box(0, 0, 0, 1, 1, 1), + box(0.5, 0, 0.1, 1, 1 + SMALL, 0.9), box(1.1, 0, 0.4, 2, 1.2, 0.6)).stream() + .map(shape -> shape.transform(new Matrix4d().scaling(scale))).toList(); + PackedOutline outline = OutlineBuilder.build(new SelectionGeometry(shapes, scale), + TimeUnit.SECONDS.toNanos(10), 10_000_000); + PackedOutline visible = outline.forView(new Vec3(-3, 3, 0.5).scale(scale)); + assertTrue(covers(visible, new Vec3(1, 1 + SMALL, 0.2).scale(scale))); + assertFalse(covers(visible, new Vec3(1, 1 + SMALL, 0.5).scale(scale))); + assertTrue(covers(visible, new Vec3(1, 1 + SMALL, 0.8).scale(scale))); + } + + @Test + void rejectsInvalidCoordinateScales() { + List shapes = step(SMALL); + for (double scale : new double[] {0, -1, Double.NaN, Double.POSITIVE_INFINITY}) { + assertThrows(IllegalArgumentException.class, () -> new SelectionGeometry(shapes, scale)); + } + } + private static List step(double offset) { return List.of(box(0, 0, 0, 1, 1, 1), box(0.5, 0, 0.25, 1.5, 1 + offset, 0.75)); }