Skip to content

Commit 6259ee3

Browse files
committed
fix 2D polygon volume algorithm
1 parent fa8dbe2 commit 6259ee3

File tree

2 files changed

+225
-13
lines changed

2 files changed

+225
-13
lines changed

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

Lines changed: 135 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
import com.sk89q.worldedit.util.formatting.text.TranslatableComponent;
2727
import com.sk89q.worldedit.world.World;
2828

29-
import java.math.BigDecimal;
30-
import java.math.RoundingMode;
3129
import java.util.ArrayList;
3230
import java.util.Collections;
3331
import java.util.Iterator;
@@ -209,22 +207,146 @@ public BlockVector3 getMaximumPoint() {
209207

210208
@Override
211209
public long getVolume() {
210+
if (points.size() <= 2) {
211+
return 0;
212+
}
213+
214+
List<BlockVector2> reverseOrderPoints = new ArrayList<>(points);
215+
Collections.reverse(reverseOrderPoints);
216+
long area = Math.max(getAreaClockwise(points), getAreaClockwise(reverseOrderPoints));
217+
218+
return area * (maxY - minY + 1);
219+
}
220+
221+
private long getAreaClockwise(List<BlockVector2> points) {
222+
int n = points.size();
223+
224+
int[] previousDirections = new int[n];
225+
int[] followingDirections = new int[n];
226+
for (int i = 0; i < 2; i++) {
227+
for (int j = 0; j < n; j++) {
228+
previousDirections[j] = getDirection(points, j);
229+
if (previousDirections[j] == 0) {
230+
previousDirections[j] = previousDirections[(j - 1 + n) % n];
231+
}
232+
233+
followingDirections[n - 1 - j] = getDirection(points, n - 1 - j);
234+
if (followingDirections[n - 1 - j] == 0) {
235+
followingDirections[n - 1 - j] = followingDirections[(n - j) % n];
236+
}
237+
}
238+
}
239+
212240
long area = 0;
213-
int i;
214-
int j = points.size() - 1;
215241

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;
242+
int minX = points.stream().mapToInt(BlockVector2::x).min().orElse(0);
243+
int j = n - 1;
244+
int prevIsNewDirection = isNewDirection(points, j - 1, previousDirections, followingDirections);
245+
for (int i = 0; i < n; i++) {
246+
int x1 = points.get(j).x() - minX;
247+
int z1 = points.get(j).z();
248+
int x2 = points.get(i).x() - minX;
249+
int z2 = points.get(i).z();
250+
int isNewDirectionValue = isNewDirection(points, j, previousDirections, followingDirections);
251+
area += areaInPoints(x1, z1, x2, z2, isNewDirectionValue, prevIsNewDirection);
252+
253+
prevIsNewDirection = isNewDirectionValue;
220254
j = i;
221255
}
222256

223-
return BigDecimal.valueOf(area)
224-
.multiply(BigDecimal.valueOf(0.5))
225-
.abs()
226-
.setScale(0, RoundingMode.FLOOR)
227-
.longValue() * (maxY - minY + 1);
257+
return area;
258+
}
259+
260+
private int getDirection(List<BlockVector2> points, int i) {
261+
int z1 = points.get(i).z();
262+
int z2 = points.get((i + 1) % points.size()).z();
263+
return z1 > z2 ? 1 : z1 == z2 ? 0 : -1;
264+
}
265+
266+
private int isNewDirection(List<BlockVector2> points, int i, int[] previousDirections, int[] followingDirections) {
267+
int n = points.size();
268+
269+
int x = points.get(i % n).x();
270+
int z = points.get(i % n).z();
271+
int x1 = points.get((i + 1) % n).x();
272+
int x2 = points.get((i - 1 + n) % n).x();
273+
int z1 = points.get((i + 1) % n).z();
274+
int z2 = points.get((i - 1 + n) % n).z();
275+
int previousEdgeOrientation = crossProduct(x1, z1, x, z, x2, z2);
276+
277+
x = points.get((i + 1) % n).x();
278+
z = points.get((i + 1) % n).z();
279+
x1 = points.get(i % n).x();
280+
x2 = points.get((i + 2) % n).x();
281+
z1 = points.get(i % n).z();
282+
z2 = points.get((i + 2) % n).z();
283+
int nextEdgeOrientation = crossProduct(x1, z1, x, z, x2, z2);
284+
285+
int direction = getDirection(points, i);
286+
if (direction == -1) {
287+
return direction != followingDirections[(i + 1) % n] && nextEdgeOrientation > 0 ? 1 : 0;
288+
}
289+
290+
if (direction == 1) {
291+
return direction != previousDirections[(i - 1 + n) % n] && previousEdgeOrientation < 0 ? 1 : 0;
292+
}
293+
294+
boolean prevCondition = previousDirections[(i - 1 + n) % n] == 1 && previousEdgeOrientation >= 0;
295+
boolean follCondition = followingDirections[(i + 1) % n] == -1 && nextEdgeOrientation <= 0;
296+
297+
return prevCondition && follCondition ? 2 : prevCondition || follCondition ? 1 : 0;
298+
}
299+
300+
private int crossProduct(int x1, int y1, int x, int y, int x2, int y2) {
301+
return (x1 - x) * (y2 - y) - (x2 - x) * (y1 - y);
302+
}
303+
304+
public long areaInPoints(int x1, int z1, int x2, int z2, int isNewDirectionValue, int prevIsNewDirectionValue) {
305+
if (z1 == z2) {
306+
if (isNewDirectionValue == 2 && isNewDirectionValue != prevIsNewDirectionValue) {
307+
return Math.abs(x1 - x2) - 1;
308+
}
309+
310+
return isNewDirectionValue != 0 ? Math.abs(x1 - x2) : 0;
311+
}
312+
313+
boolean isNewDirection = isNewDirectionValue == 1;
314+
boolean isIncreasing = z1 > z2;
315+
long area = 0;
316+
317+
int side = Math.min(x1, x2) + (isIncreasing ? 1 : 0);
318+
int height = isNewDirection ? next(z1 - z2) : z1 - z2;
319+
area += (long) side * height;
320+
321+
if ((isIncreasing && (x1 < x2 || isNewDirection)) || (!isIncreasing && (x1 < x2 && !isNewDirection))) {
322+
area += Math.abs(x1 - x2);
323+
}
324+
325+
int z = Math.abs(z1 - z2);
326+
int x = Math.abs(x1 - x2);
327+
int squaresInLine = x + z - gcd(x, z);
328+
if (!isIncreasing) {
329+
area -= (x * z - squaresInLine) / 2;
330+
area -= squaresInLine;
331+
} else {
332+
area += (x * z - squaresInLine) / 2;
333+
}
334+
335+
return area;
336+
}
337+
338+
private int next(int value) {
339+
return value < 0 ? value - 1 : value + 1;
340+
}
341+
342+
private int gcd(int n1, int n2) {
343+
while (n2 != 0) {
344+
int temp = n2;
345+
n2 = n1 % n2;
346+
n1 = temp;
347+
}
348+
349+
return n1;
228350
}
229351

230352
@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(name = "index={index}")
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), // simple square
46+
Arguments.of(new int[][]{{0, 0}, {2, 2}, {2, 0}}, 6), // triangle removing diagonal
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, split
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)