diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java index 007c773525..a6c39707dc 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java @@ -50,10 +50,12 @@ import com.sk89q.worldedit.internal.annotation.Direction; import com.sk89q.worldedit.internal.annotation.MultiDirection; import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.ConvexPolyhedralRegion; import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.regions.RegionOperationException; import com.sk89q.worldedit.regions.RegionSelector; +import com.sk89q.worldedit.regions.selector.ConvexPolyhedralRegionSelector; import com.sk89q.worldedit.regions.selector.CuboidRegionSelector; import com.sk89q.worldedit.regions.selector.ExtendingCuboidRegionSelector; import com.sk89q.worldedit.regions.selector.RegionSelectorType; @@ -154,7 +156,27 @@ public void pos(Actor actor, World world, LocalSession session, regionSelector.selectPrimary(pos1, ActorSelectorLimits.forActor(actor)); for (BlockVector3 vector : pos2) { - regionSelector.selectSecondary(vector, ActorSelectorLimits.forActor(actor)); + boolean changed = regionSelector.selectSecondary(vector, ActorSelectorLimits.forActor(actor)); + if (!changed && regionSelector instanceof ConvexPolyhedralRegionSelector) { + ConvexPolyhedralRegion convex = (ConvexPolyhedralRegion) regionSelector.getIncompleteRegion(); + if (convex.getVertices().contains(vector)) { + actor.printInfo(TranslatableComponent.of( + "worldedit.selection.convex.error.duplicate", + TextComponent.of(vector.toString()) + )); + } else { + ActorSelectorLimits limits = ActorSelectorLimits.forActor(actor); + limits.getPolyhedronVertexLimit().ifPresent(limit -> { + int total = convex.getVertices().size(); + if (total >= limit) { + actor.printInfo(TranslatableComponent.of( + "worldedit.select.convex.limit-message", + TextComponent.of(limit) + )); + } + }); + } + } } session.dispatchCUISelection(actor); @@ -205,8 +227,30 @@ public void pos2(Actor actor, World world, LocalSession session, } } - if (!session.getRegionSelector(world).selectSecondary(coordinates, ActorSelectorLimits.forActor(actor))) { - actor.printError(TranslatableComponent.of("worldedit.pos.already-set")); + RegionSelector selector = session.getRegionSelector(world); + if (!selector.selectSecondary(coordinates, ActorSelectorLimits.forActor(actor))) { + if (selector instanceof ConvexPolyhedralRegionSelector) { + ConvexPolyhedralRegion convex = (ConvexPolyhedralRegion) selector.getIncompleteRegion(); + if (convex.getVertices().contains(coordinates)) { + actor.printInfo(TranslatableComponent.of( + "worldedit.selection.convex.error.duplicate", + TextComponent.of(coordinates.toString()) + )); + } else { + ActorSelectorLimits limits = ActorSelectorLimits.forActor(actor); + limits.getPolyhedronVertexLimit().ifPresent(limit -> { + int total = convex.getVertices().size(); + if (total >= limit) { + actor.printInfo(TranslatableComponent.of( + "worldedit.select.convex.limit-message", + TextComponent.of(limit) + )); + } + }); + } + } else { + actor.printError(TranslatableComponent.of("worldedit.pos.already-set")); + } return; } @@ -245,8 +289,31 @@ public void hpos2(Player player, LocalSession session) { Location pos = player.getBlockTrace(300); if (pos != null) { - if (!session.getRegionSelector(player.getWorld()).selectSecondary(pos.toVector().toBlockPoint(), ActorSelectorLimits.forActor(player))) { - player.printError(TranslatableComponent.of("worldedit.hpos.already-set")); + RegionSelector selector = session.getRegionSelector(player.getWorld()); + BlockVector3 bp = pos.toVector().toBlockPoint(); + if (!selector.selectSecondary(bp, ActorSelectorLimits.forActor(player))) { + if (selector instanceof ConvexPolyhedralRegionSelector) { + ConvexPolyhedralRegion convex = (ConvexPolyhedralRegion) selector.getIncompleteRegion(); + if (convex.getVertices().contains(bp)) { + player.printInfo(TranslatableComponent.of( + "worldedit.selection.convex.error.duplicate", + TextComponent.of(bp.toString()) + )); + } else { + ActorSelectorLimits limits = ActorSelectorLimits.forActor(player); + limits.getPolyhedronVertexLimit().ifPresent(limit -> { + int total = convex.getVertices().size(); + if (total >= limit) { + player.printInfo(TranslatableComponent.of( + "worldedit.select.convex.limit-message", + TextComponent.of(limit) + )); + } + }); + } + } else { + player.printError(TranslatableComponent.of("worldedit.hpos.already-set")); + } return; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/DistanceWand.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/DistanceWand.java index 5d62d7abd0..73fee00053 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/DistanceWand.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/DistanceWand.java @@ -26,8 +26,11 @@ import com.sk89q.worldedit.extension.platform.permission.ActorSelectorLimits; import com.sk89q.worldedit.function.mask.Mask; import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.ConvexPolyhedralRegion; import com.sk89q.worldedit.regions.RegionSelector; +import com.sk89q.worldedit.regions.selector.ConvexPolyhedralRegionSelector; import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; /** @@ -65,6 +68,27 @@ public boolean actPrimary(Platform server, LocalConfiguration config, Player pla BlockVector3 blockPoint = target.toVector().toBlockPoint(); if (selector.selectSecondary(blockPoint, ActorSelectorLimits.forActor(player))) { selector.explainSecondarySelection(player, session, blockPoint); + } else if (selector instanceof ConvexPolyhedralRegionSelector) { + ConvexPolyhedralRegion convex = (ConvexPolyhedralRegion) selector.getIncompleteRegion(); + + if (convex.getVertices().contains(blockPoint)) { + player.printInfo(TranslatableComponent.of( + "worldedit.selection.convex.error.duplicate", + TextComponent.of(blockPoint.toString()) + )); + return true; + } + + ActorSelectorLimits limits = ActorSelectorLimits.forActor(player); + limits.getPolyhedronVertexLimit().ifPresent(limit -> { + int total = convex.getVertices().size(); + if (total >= limit) { + player.printInfo(TranslatableComponent.of( + "worldedit.select.convex.limit-message", + TextComponent.of(limit) + )); + } + }); } return true; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/SelectionWand.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/SelectionWand.java index 34c4096e7a..fce0bfe36b 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/SelectionWand.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/SelectionWand.java @@ -26,9 +26,13 @@ import com.sk89q.worldedit.extension.platform.Platform; import com.sk89q.worldedit.extension.platform.permission.ActorSelectorLimits; import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.ConvexPolyhedralRegion; import com.sk89q.worldedit.regions.RegionSelector; +import com.sk89q.worldedit.regions.selector.ConvexPolyhedralRegionSelector; import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.util.formatting.text.TextComponent; +import com.sk89q.worldedit.util.formatting.text.TranslatableComponent; import javax.annotation.Nullable; @@ -52,6 +56,27 @@ public boolean actPrimary(Platform server, LocalConfiguration config, Player pla if (selector.selectSecondary(blockPoint, ActorSelectorLimits.forActor(player))) { selector.explainSecondarySelection(player, session, blockPoint); + } else if (selector instanceof ConvexPolyhedralRegionSelector) { + ConvexPolyhedralRegion convex = (ConvexPolyhedralRegion) selector.getIncompleteRegion(); + + if (convex.getVertices().contains(blockPoint)) { + player.printInfo(TranslatableComponent.of( + "worldedit.selection.convex.error.duplicate", + TextComponent.of(blockPoint.toString()) + )); + return true; + } + + ActorSelectorLimits limits = ActorSelectorLimits.forActor(player); + limits.getPolyhedronVertexLimit().ifPresent(limit -> { + int total = convex.getVertices().size(); + if (total >= limit) { + player.printInfo(TranslatableComponent.of( + "worldedit.select.convex.limit-message", + TextComponent.of(limit) + )); + } + }); } return true; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegion.java b/worldedit-core/src/main/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegion.java index 4c1671fcc9..2ed784eca9 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegion.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegion.java @@ -338,6 +338,14 @@ public Collection getVertices() { return ret; } + public boolean isBacklogVertex(BlockVector3 vertex) { + return vertexBacklog.contains(vertex); + } + + public Collection getBacklogVertices() { + return new ArrayList<>(vertexBacklog); + } + public Collection getTriangles() { return triangles; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/regions/selector/ConvexPolyhedralRegionSelector.java b/worldedit-core/src/main/java/com/sk89q/worldedit/regions/selector/ConvexPolyhedralRegionSelector.java index af0dd9c344..4759c71223 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/regions/selector/ConvexPolyhedralRegionSelector.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/regions/selector/ConvexPolyhedralRegionSelector.java @@ -131,7 +131,7 @@ public boolean selectSecondary(BlockVector3 position, SelectorLimits limits) { Optional vertexLimit = limits.getPolyhedronVertexLimit(); - if (vertexLimit.isPresent() && region.getVertices().size() > vertexLimit.get()) { + if (vertexLimit.isPresent() && region.getVertices().size() >= vertexLimit.get()) { return false; } @@ -169,7 +169,12 @@ public long getVolume() { @Override public void learnChanges() { - pos1 = region.getVertices().iterator().next(); + if (!region.getTriangles().isEmpty()) { + Triangle t = region.getTriangles().iterator().next(); + pos1 = t.getVertex(0).toBlockPoint(); + } else { + pos1 = region.getVertices().iterator().next(); + } } @Override @@ -186,8 +191,20 @@ public String getTypeName() { public List getSelectionInfoLines() { List ret = new ArrayList<>(); - ret.add(TranslatableComponent.of("worldedit.selection.convex.info.vertices", TextComponent.of(region.getVertices().size()))); - ret.add(TranslatableComponent.of("worldedit.selection.convex.info.triangles", TextComponent.of(region.getTriangles().size()))); + int vertexCount = region.getVertices().size(); + int triangleCount = region.getTriangles().size(); + ret.add(TranslatableComponent.of("worldedit.selection.convex.info.vertices", TextComponent.of(vertexCount))); + ret.add(TranslatableComponent.of("worldedit.selection.convex.info.triangles", TextComponent.of(triangleCount))); + + if (triangleCount == 0) { + if (vertexCount < 3) { + ret.add(TranslatableComponent.of("worldedit.selection.convex.error.too-few")); + } else { + ret.add(TranslatableComponent.of("worldedit.selection.convex.error.all-collinear")); + } + } else if (triangleCount == 2) { + ret.add(TranslatableComponent.of("worldedit.selection.convex.info.planar")); + } return ret; } @@ -212,7 +229,25 @@ public void explainSecondarySelection(Actor player, LocalSession session, BlockV session.describeCUI(player); - player.printInfo(TranslatableComponent.of("worldedit.selection.convex.explain.secondary", TextComponent.of(pos.toString()))); + if (region.isBacklogVertex(pos)) { + if (region.getTriangles().isEmpty()) { + player.printInfo(TranslatableComponent.of( + "worldedit.selection.convex.info.backlog.collinear", + TextComponent.of(pos.toString()) + )); + } else { + player.printInfo(TranslatableComponent.of( + "worldedit.selection.convex.info.backlog.coplanar", + TextComponent.of(pos.toString()) + )); + } + } else { + player.printInfo(TranslatableComponent.of("worldedit.selection.convex.explain.secondary", TextComponent.of(pos.toString()))); + } + + if (!region.isDefined() && region.getVertices().size() >= 3) { + player.printInfo(TranslatableComponent.of("worldedit.selection.convex.error.all-collinear")); + } } @Override diff --git a/worldedit-core/src/main/resources/lang/strings.json b/worldedit-core/src/main/resources/lang/strings.json index e01e89e97a..1aa647deb5 100644 --- a/worldedit-core/src/main/resources/lang/strings.json +++ b/worldedit-core/src/main/resources/lang/strings.json @@ -419,6 +419,12 @@ "worldedit.selection.convex.info.triangles": "Triangles: {0}", "worldedit.selection.convex.explain.primary": "Started new selection with vertex {0}.", "worldedit.selection.convex.explain.secondary": "Added vertex {0} to the selection.", + "worldedit.selection.convex.info.backlog.collinear": "Point is collinear with existing vertices, added to backlog ({0}).", + "worldedit.selection.convex.info.backlog.coplanar": "Point is coplanar with existing vertices, added to backlog ({0}).", + "worldedit.selection.convex.error.duplicate": "Vertex already exists at {0}.", + "worldedit.selection.convex.error.all-collinear": "Cannot form a region with only collinear points.", + "worldedit.selection.convex.info.planar": "Selection is planar (2D), add a non-coplanar point for 3D volume.", + "worldedit.selection.convex.error.too-few": "Need at least 3 non-collinear vertices to define a region.", "worldedit.selection.cuboid.info.pos1": "Position 1: {0}", "worldedit.selection.cuboid.info.pos2": "Position 2: {0}", "worldedit.selection.cuboid.explain.primary": "First position set to {0}.", diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegionSpecTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegionSpecTest.java new file mode 100644 index 0000000000..4c261e65f0 --- /dev/null +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegionSpecTest.java @@ -0,0 +1,124 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.regions; + +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.selector.ConvexPolyhedralRegionSelector; +import com.sk89q.worldedit.regions.selector.limit.SelectorLimits; +import com.sk89q.worldedit.world.World; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ConvexPolyhedralRegionSpecTest { + + @Test + void allCollinearPoints_areUndefined() { + ConvexPolyhedralRegion region = new ConvexPolyhedralRegion((World) null); + assertTrue(region.addVertex(BlockVector3.at(0, 0, 0))); + assertTrue(region.addVertex(BlockVector3.at(10, 0, 0))); + // Third collinear point should be backlogged; no triangles should exist + assertTrue(region.addVertex(BlockVector3.at(20, 0, 0))); + assertFalse(region.isDefined()); + assertEquals(0, region.getTriangles().size()); + } + + @Test + void coplanarPoints_form2DDefinedRegion() { + ConvexPolyhedralRegion region = new ConvexPolyhedralRegion((World) null); + // Three non-collinear, coplanar points define a 2D region (two triangles) + assertTrue(region.addVertex(BlockVector3.at(0, 0, 0))); + assertTrue(region.addVertex(BlockVector3.at(10, 0, 0))); + assertTrue(region.addVertex(BlockVector3.at(0, 0, 10))); + assertTrue(region.isDefined()); + assertEquals(2, region.getTriangles().size()); + + // Add another coplanar point; should remain planar (still two triangles) + assertTrue(region.addVertex(BlockVector3.at(10, 0, 10))); + assertEquals(2, region.getTriangles().size()); + } + + @Test + void vertexLimitEnforced_includesBacklog() { + ConvexPolyhedralRegionSelector selector = new ConvexPolyhedralRegionSelector((com.sk89q.worldedit.world.World) null); + SelectorLimits limits = new SelectorLimits() { + @Override + public Optional getPolygonVertexLimit() { + return Optional.empty(); + } + + @Override + public Optional getPolyhedronVertexLimit() { + // Limit total (vertices + backlog) to 3 + return Optional.of(3); + } + }; + + // First two vertices + assertTrue(selector.selectPrimary(BlockVector3.at(0, 0, 0), limits)); + assertTrue(selector.selectSecondary(BlockVector3.at(10, 0, 0), limits)); + // Third collinear point goes to backlog but counts towards limit + assertTrue(selector.selectSecondary(BlockVector3.at(20, 0, 0), limits)); + // Fourth should be rejected due to limit (>= check) + assertFalse(selector.selectSecondary(BlockVector3.at(0, 1, 0), limits)); + } + + @Test + void duplicateVertex_isRejected() { + ConvexPolyhedralRegion region = new ConvexPolyhedralRegion((World) null); + BlockVector3 a = BlockVector3.at(1, 2, 3); + assertTrue(region.addVertex(a)); + assertFalse(region.addVertex(a)); + } + + @Test + void hullBecomes3D_withFourNonCoplanarPoints() { + ConvexPolyhedralRegion region = new ConvexPolyhedralRegion((World) null); + assertTrue(region.addVertex(BlockVector3.at(0, 0, 0))); + assertTrue(region.addVertex(BlockVector3.at(10, 0, 0))); + assertTrue(region.addVertex(BlockVector3.at(0, 0, 10))); + // Non-coplanar fourth point + assertTrue(region.addVertex(BlockVector3.at(0, 10, 0))); + assertTrue(region.isDefined()); + assertTrue(region.getTriangles().size() > 2, "3D hull should have more than 2 triangles"); + } + + @Test + void backlogProcessingOrder_isMaintained() { + ConvexPolyhedralRegion region = new ConvexPolyhedralRegion((World) null); + BlockVector3 a = BlockVector3.at(0, 0, 0); + BlockVector3 b = BlockVector3.at(10, 0, 0); + BlockVector3 c = BlockVector3.at(5, 0, 0); // collinear -> backlog + BlockVector3 d = BlockVector3.at(0, 0, 10); // non-collinear + + assertTrue(region.addVertex(a)); + assertTrue(region.addVertex(b)); + assertTrue(region.addVertex(c)); + assertTrue(region.getBacklogVertices().contains(c)); + + // Adding d will trigger backlog processing; c should remain backlogged until non-coplanar point added later + assertTrue(region.addVertex(d)); + assertTrue(region.getBacklogVertices().contains(c)); + } +}