Skip to content

Commit 1fa3459

Browse files
fix: 2D polygon volume algorithm
Co-authored-by: Octavia Togami <otogami@gradle.com>
1 parent fa8dbe2 commit 1fa3459

File tree

2 files changed

+127
-13
lines changed

2 files changed

+127
-13
lines changed

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

Lines changed: 37 additions & 13 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;
@@ -209,22 +208,47 @@ public BlockVector3 getMaximumPoint() {
209208

210209
@Override
211210
public long getVolume() {
212-
long area = 0;
213-
int i;
211+
if (points.size() <= 2) {
212+
return 0;
213+
}
214+
215+
int concreteArea = concreteArea();
216+
int b = boundaryPoints();
217+
int i = interiorPoints(concreteArea, b);
218+
int latticeArea = i + b;
219+
220+
return latticeArea * (maxY - minY + 1);
221+
}
222+
223+
private int boundaryPoints() {
224+
int j = points.size() - 1;
225+
int boundaryPoints = 0;
226+
for (int i = 0; i < points.size(); i++) {
227+
int x = Math.abs(points.get(i).x() - points.get(j).x());
228+
int z = Math.abs(points.get(i).z() - points.get(j).z());
229+
boundaryPoints += IntMath.gcd(x, z);
230+
j = i;
231+
}
232+
233+
return boundaryPoints;
234+
}
235+
236+
// Trapezoid formula
237+
private int concreteArea() {
238+
int area = 0;
214239
int j = points.size() - 1;
215240

216-
for (i = 0; i < points.size(); ++i) {
217-
long x = points.get(j).x() + points.get(i).x();
218-
long z = points.get(j).z() - points.get(i).z();
219-
area += x * z;
241+
for (int i = 0; i < points.size(); i++) {
242+
area += (points.get(j).x() + points.get(i).x()) * (points.get(j).z() - points.get(i).z());
220243
j = i;
221244
}
222245

223-
return BigDecimal.valueOf(area)
224-
.multiply(BigDecimal.valueOf(0.5))
225-
.abs()
226-
.setScale(0, RoundingMode.FLOOR)
227-
.longValue() * (maxY - minY + 1);
246+
return Math.abs(area) / 2;
247+
}
248+
249+
// Pick's theorem
250+
private int interiorPoints(int concreteArea, int boundaryPoints) {
251+
return concreteArea - boundaryPoints / 2 + 1;
228252
}
229253

230254
@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)