Skip to content

Commit 324aacb

Browse files
fix: 2D polygon volume algorithm (#2811)
Co-authored-by: Octavia Togami <otogami@gradle.com>
1 parent eb3f609 commit 324aacb

2 files changed

Lines changed: 125 additions & 9 deletions

File tree

worldedit-core/src/main/java/com/sk89q/worldedit/regions/Polygonal2DRegion.java

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,14 @@
1919

2020
package com.sk89q.worldedit.regions;
2121

22+
import com.google.common.math.IntMath;
2223
import com.sk89q.worldedit.math.BlockVector2;
2324
import com.sk89q.worldedit.math.BlockVector3;
2425
import com.sk89q.worldedit.regions.iterator.FlatRegion3DIterator;
2526
import com.sk89q.worldedit.regions.iterator.FlatRegionIterator;
2627
import com.sk89q.worldedit.util.formatting.text.TranslatableComponent;
2728
import com.sk89q.worldedit.world.World;
2829

29-
import java.math.BigDecimal;
30-
import java.math.RoundingMode;
3130
import java.util.ArrayList;
3231
import java.util.Collections;
3332
import java.util.Iterator;
@@ -220,11 +219,37 @@ public BlockVector3 getMaximumPoint() {
220219

221220
@Override
222221
public long getVolume() {
222+
if (points.size() <= 2) {
223+
return 0;
224+
}
225+
226+
long concreteArea = concreteArea();
227+
int b = boundaryPoints();
228+
long i = interiorPoints(concreteArea, b);
229+
long latticeArea = i + b;
230+
231+
return latticeArea * (maxY - minY + 1);
232+
}
233+
234+
private int boundaryPoints() {
235+
int j = points.size() - 1;
236+
int boundaryPoints = 0;
237+
for (int i = 0; i < points.size(); i++) {
238+
int x = Math.abs(points.get(i).x() - points.get(j).x());
239+
int z = Math.abs(points.get(i).z() - points.get(j).z());
240+
boundaryPoints += IntMath.gcd(x, z);
241+
j = i;
242+
}
243+
244+
return boundaryPoints;
245+
}
246+
247+
// Trapezoid formula
248+
private long concreteArea() {
223249
long area = 0;
224-
int i;
225250
int j = points.size() - 1;
226251

227-
for (i = 0; i < points.size(); ++i) {
252+
for (int i = 0; i < points.size(); i++) {
228253
BlockVector2 iPoint = points.get(i);
229254
BlockVector2 jPoint = points.get(j);
230255
long x = jPoint.x() + (long) iPoint.x();
@@ -233,11 +258,12 @@ public long getVolume() {
233258
j = i;
234259
}
235260

236-
return BigDecimal.valueOf(area)
237-
.multiply(BigDecimal.valueOf(0.5))
238-
.abs()
239-
.setScale(0, RoundingMode.FLOOR)
240-
.longValue() * (maxY - minY + 1);
261+
return Math.abs(area) / 2;
262+
}
263+
264+
// Pick's theorem
265+
private long interiorPoints(long concreteArea, long boundaryPoints) {
266+
return concreteArea - boundaryPoints / 2 + 1;
241267
}
242268

243269
@Override
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* WorldEdit, a Minecraft world manipulation toolkit
3+
* Copyright (C) sk89q <http://www.sk89q.com>
4+
* Copyright (C) WorldEdit team and contributors
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
package com.sk89q.worldedit.regions;
21+
22+
import com.sk89q.worldedit.math.BlockVector2;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.Arguments;
25+
import org.junit.jupiter.params.provider.MethodSource;
26+
27+
import java.util.ArrayList;
28+
import java.util.List;
29+
30+
import static org.junit.jupiter.api.Assertions.assertEquals;
31+
32+
public class Polygonal2DRegionTest {
33+
34+
@ParameterizedTest
35+
@MethodSource("areaTestData")
36+
void testArea(int[][] coordinates, long expectedVolume) {
37+
List<BlockVector2> points = toPoints(coordinates);
38+
Polygonal2DRegion region = new Polygonal2DRegion(null, points, 0, 0);
39+
40+
assertEquals(expectedVolume, region.getVolume());
41+
}
42+
43+
static List<Arguments> areaTestData() {
44+
return List.of(
45+
Arguments.of(new int[][]{{0, 0}, {0, 1}, {1, 1}, {1, 0}}, 4), // square
46+
Arguments.of(new int[][]{{0, 0}, {2, 2}, {2, 0}}, 6), // triangle
47+
Arguments.of(new int[][]{{6, 3}, {6, 1}, {0, 0}}, 10), // polygon with separated parts
48+
Arguments.of(new int[][]{{-1, 1}, {4, 1}, {4, -3}, {-1, -3}}, 30), // x < 0
49+
Arguments.of(new int[][]{
50+
{0, 9}, {6, 9}, {6, 0}, {1, 2}, {4, 4}, {3, 7}, {0, 5},
51+
}, 47), // concave
52+
Arguments.of(new int[][]{
53+
{0, 4}, {2, 6}, {4, 6}, {6, 4}, {6, 2}, {4, 0}, {2, 0}, {0, 2},
54+
}, 37), // octagon
55+
Arguments.of(new int[][]{
56+
{0, 0}, {2, 2}, {2, 4}, {0, 6}, {6, 6}, {4, 4}, {4, 2}, {6, 0},
57+
}, 33), // hourglass
58+
Arguments.of(new int[][]{
59+
{0, 5}, {11, 5}, {11, 0}, {9, 0}, {9, 4}, {7, 4}, {7, 1}, {6, 1},
60+
{6, 2}, {4, 2}, {4, 1}, {3, 1}, {3, 0}, {1, 0}, {1, 3}, {0, 3},
61+
}, 60), // checks if new direction is well assigned
62+
Arguments.of(new int[][]{
63+
{0, 5}, {2, 3}, {5, 3}, {7, 1}, {0, 1},
64+
}, 24), // horizontal and downwards
65+
Arguments.of(new int[][]{
66+
{0, 0}, {2, 2}, {4, 2}, {6, 4}, {6, 0},
67+
}, 21), // horizontal and upwards
68+
Arguments.of(new int[][]{
69+
{0, 5}, {3, 5}, {2, 3}, {5, 3}, {7, 5}, {7, 1}, {0, 1},
70+
}, 34), // horizontal with upwards and downwards
71+
Arguments.of(new int[][]{
72+
{0, 5}, {3, 5}, {2, 3}, {4, 3}, {5, 3}, {7, 5}, {7, 1}, {0, 1},
73+
}, 34), // horizontal, upwards, downwards, redundant point
74+
Arguments.of(new int[][]{
75+
{1, 3}, {3, 3}, {4, 5}, {6, 8}, {7, 6}, {9, 6}, {12, 8}, {11, 6}, {11, 4},
76+
{10, 2}, {8, 0}, {6, 1}, {9, 4}, {6, 3}, {4, 1}, {3, 0}, {1, 1},
77+
}, 55) // complex polygon
78+
);
79+
}
80+
81+
private static List<BlockVector2> toPoints(int[]... coordinates) {
82+
List<BlockVector2> points = new ArrayList<>();
83+
for (int[] coordinate : coordinates) {
84+
points.add(BlockVector2.at(coordinate[0], coordinate[1]));
85+
}
86+
87+
return points;
88+
}
89+
90+
}

0 commit comments

Comments
 (0)