diff --git a/src/main/java/com/sk89q/worldedit/cui/SelectionPolygonEvent.java b/src/main/java/com/sk89q/worldedit/cui/SelectionPolygonEvent.java new file mode 100644 index 000000000..cea73b392 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/cui/SelectionPolygonEvent.java @@ -0,0 +1,42 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q 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.cui; + +public class SelectionPolygonEvent implements CUIEvent { + protected final int[] vertices; + + public SelectionPolygonEvent(int... vertices) { + this.vertices = vertices; + } + + public String getTypeId() { + return "poly"; + } + + public String[] getParameters() { + final String[] ret = new String[vertices.length]; + + int i = 0; + for (int vertex : vertices) { + ret[i++] = String.valueOf(vertex); + } + + return ret; + } +} diff --git a/src/main/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegion.java b/src/main/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegion.java new file mode 100644 index 000000000..d59a4e0e6 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegion.java @@ -0,0 +1,325 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q 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 java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.sk89q.worldedit.LocalWorld; +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.regions.polyhedron.Edge; +import com.sk89q.worldedit.regions.polyhedron.Triangle; + +public class ConvexPolyhedralRegion extends AbstractRegion { + /** + * Vertices that are contained in the convex hull. + */ + private final Set vertices = new HashSet(); + + /** + * Triangles that form the convex hull. + */ + private final List triangles = new ArrayList(); + + /** + * Vertices that are coplanar to the first 3 vertices. + */ + private final Set vertexBacklog = new HashSet(); + + /** + * Minimum point of the axis-aligned bounding box. + */ + private Vector minimumPoint; + + /** + * Maximum point of the axis-aligned bounding box. + */ + private Vector maximumPoint; + + /** + * Accumulator for the barycenter of the polyhedron. Divide by vertices.size() to get the actual center. + */ + private Vector centerAccum = Vector.ZERO; + + /** + * The last triangle that caused a {@link #contains(Vector)} to classify a point as "outside". Used for optimization. + */ + private Triangle lastTriangle; + + /** + * Constructs an empty mesh, containing no vertices or triangles. + * + * @param world + */ + public ConvexPolyhedralRegion(LocalWorld world) { + super(world); + } + + /** + * Constructs an independent copy of the given region. + * + * @param world + */ + public ConvexPolyhedralRegion(ConvexPolyhedralRegion region) { + this(region.world); + vertices.addAll(region.vertices); + triangles.addAll(region.triangles); + vertexBacklog.addAll(region.vertexBacklog); + + minimumPoint = region.minimumPoint; + maximumPoint = region.maximumPoint; + centerAccum = region.centerAccum; + lastTriangle = region.lastTriangle; + } + + /** + * Clears the region, removing all vertices and triangles. + */ + public void clear() { + vertices.clear(); + triangles.clear(); + vertexBacklog.clear(); + + minimumPoint = null; + maximumPoint = null; + centerAccum = Vector.ZERO; + lastTriangle = null; + } + + /** + * Add a vertex to the region. + * + * @param vertex + * @return true, if something changed. + */ + public boolean addVertex(Vector vertex) { + lastTriangle = null; // Probably not necessary + + if (vertices.contains(vertex)) { + return false; + } + + if (vertices.size() == 3) { + if (vertexBacklog.contains(vertex)) { + return false; + } + + if (containsRaw(vertex)) { + return vertexBacklog.add(vertex); + } + } + + vertices.add(vertex); + + centerAccum = centerAccum.add(vertex); + + /*if (contains(vertex)) { + return true; + }*/ + + lastTriangle = null; + + if (minimumPoint == null) { + minimumPoint = maximumPoint = vertex; + } else { + minimumPoint = Vector.getMinimum(minimumPoint, vertex); + maximumPoint = Vector.getMaximum(maximumPoint, vertex); + } + + + switch (vertices.size()) { + case 0: + case 1: + case 2: + // Incomplete, can't make a mesh yet + return true; + + case 3: + // Generate minimal mesh to start from + final Vector[] v = vertices.toArray(new Vector[vertices.size()]); + + triangles.add((new Triangle(v[0], v[1], v[2]))); + triangles.add((new Triangle(v[0], v[2], v[1]))); + return true; + } + + // Look for triangles that face the vertex and remove them + final Set borderEdges = new LinkedHashSet(); + for (Iterator it = triangles.iterator(); it.hasNext(); ) { + final Triangle triangle = it.next(); + + // If the triangle can't be seen, it's not relevant + if (!triangle.above(vertex)) { + continue; + } + + // Remove the triangle from the mesh + it.remove(); + + // ...and remember its edges + for (int i = 0; i < 3; ++i) { + final Edge edge = triangle.getEdge(i); + if (borderEdges.remove(edge)) { + continue; + } + + borderEdges.add(edge); + } + } + + // Add triangles between the remembered edges and the new vertex. + for (Edge edge : borderEdges) { + triangles.add(edge.createTriangle(vertex)); + } + + if (!vertexBacklog.isEmpty()) { + final List vertexBacklog2 = new ArrayList(vertexBacklog); + vertexBacklog.clear(); + for (Vector vertex2 : vertexBacklog2) { + addVertex(vertex2); + } + } + + return true; + } + + public boolean isDefined() { + return !triangles.isEmpty(); + } + + @Override + public Vector getMinimumPoint() { + return minimumPoint; + } + + @Override + public Vector getMaximumPoint() { + return maximumPoint; + } + + @Override + public Vector getCenter() { + return centerAccum.divide(vertices.size()); + } + + @Override + public void expand(Vector... changes) throws RegionOperationException { + } + + @Override + public void contract(Vector... changes) throws RegionOperationException { + } + + @Override + public void shift(Vector change) throws RegionOperationException { + shiftCollection(vertices, change); + shiftCollection(vertexBacklog, change); + + for (int i = 0; i < triangles.size(); ++i) { + final Triangle triangle = triangles.get(i); + + final Vector v0 = change.add(triangle.getVertex(0)); + final Vector v1 = change.add(triangle.getVertex(1)); + final Vector v2 = change.add(triangle.getVertex(2)); + + triangles.set(i, new Triangle(v0, v1, v2)); + } + + minimumPoint = change.add(minimumPoint); + maximumPoint = change.add(maximumPoint); + centerAccum = change.multiply(vertices.size()).add(centerAccum); + lastTriangle = null; + } + + private static void shiftCollection(Collection collection, Vector change) { + final List tmp = new ArrayList(collection); + collection.clear(); + for (Vector vertex : tmp) { + collection.add(change.add(vertex)); + } + } + + @Override + public boolean contains(Vector pt) { + if (!isDefined()) { + return false; + } + + final int x = pt.getBlockX(); + final int y = pt.getBlockY(); + final int z = pt.getBlockZ(); + + final Vector min = getMinimumPoint(); + final Vector max = getMaximumPoint(); + + if (x < min.getBlockX()) return false; + if (x > max.getBlockX()) return false; + if (y < min.getBlockY()) return false; + if (y > max.getBlockY()) return false; + if (z < min.getBlockZ()) return false; + if (z > max.getBlockZ()) return false; + + return containsRaw(pt); + } + + private boolean containsRaw(Vector pt) { + if (lastTriangle != null && lastTriangle.above(pt)) { + return false; + } + + for (Triangle triangle : triangles) { + if (lastTriangle == triangle) { + continue; + } + + if (triangle.above(pt)) { + lastTriangle = triangle; + return false; + } + } + + return true; + } + + public Collection getVertices() { + if (vertexBacklog.isEmpty()) { + return vertices; + } + + final List ret = new ArrayList(vertices); + ret.addAll(vertexBacklog); + + return ret; + } + + public Collection getTriangles() { + return triangles; + } + + @Override + public AbstractRegion clone() { + return new ConvexPolyhedralRegion(this); + } +} diff --git a/src/main/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegionSelector.java b/src/main/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegionSelector.java new file mode 100644 index 000000000..ba4d941ea --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/regions/ConvexPolyhedralRegionSelector.java @@ -0,0 +1,219 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q 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 java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.sk89q.worldedit.BlockVector; +import com.sk89q.worldedit.BlockVector2D; +import com.sk89q.worldedit.IncompleteRegionException; +import com.sk89q.worldedit.LocalPlayer; +import com.sk89q.worldedit.LocalSession; +import com.sk89q.worldedit.LocalWorld; +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.cui.CUIRegion; +import com.sk89q.worldedit.cui.SelectionPointEvent; +import com.sk89q.worldedit.cui.SelectionPolygonEvent; +import com.sk89q.worldedit.cui.SelectionShapeEvent; +import com.sk89q.worldedit.regions.polyhedron.Triangle; + +public class ConvexPolyhedralRegionSelector implements RegionSelector, CUIRegion { + private int maxVertices; + private final ConvexPolyhedralRegion region; + private BlockVector pos1; + + public ConvexPolyhedralRegionSelector(LocalWorld world, int maxVertices) { + this.maxVertices = maxVertices; + region = new ConvexPolyhedralRegion(world); + } + + public ConvexPolyhedralRegionSelector(RegionSelector oldSelector, int maxVertices) { + this.maxVertices = maxVertices; + if (oldSelector instanceof ConvexPolyhedralRegionSelector) { + final ConvexPolyhedralRegionSelector convexPolyhedralRegionSelector = (ConvexPolyhedralRegionSelector) oldSelector; + + pos1 = convexPolyhedralRegionSelector.pos1; + region = new ConvexPolyhedralRegion(convexPolyhedralRegionSelector.region); + } else { + final Region oldRegion; + try { + oldRegion = oldSelector.getRegion(); + } catch (IncompleteRegionException e) { + region = new ConvexPolyhedralRegion(oldSelector.getIncompleteRegion().getWorld()); + return; + } + + final int minY = oldRegion.getMinimumPoint().getBlockY(); + final int maxY = oldRegion.getMaximumPoint().getBlockY(); + + region = new ConvexPolyhedralRegion(oldRegion.getWorld()); + + for (final BlockVector2D pt : new ArrayList(oldRegion.polygonize(maxVertices < 0 ? maxVertices : maxVertices / 2))) { + region.addVertex(pt.toVector(minY)); + region.addVertex(pt.toVector(maxY)); + } + + learnChanges(); + } + } + + @Override + public boolean selectPrimary(Vector pos) { + clear(); + pos1 = pos.toBlockVector(); + return region.addVertex(pos); + } + + @Override + public boolean selectSecondary(Vector pos) { + if (maxVertices >= 0 && region.getVertices().size() > maxVertices) { + return false; + } + + return region.addVertex(pos); + } + + @Override + public BlockVector getPrimaryPosition() throws IncompleteRegionException { + return pos1; + } + + @Override + public Region getRegion() throws IncompleteRegionException { + if (!region.isDefined()) { + throw new IncompleteRegionException(); + } + + return region; + } + + @Override + public Region getIncompleteRegion() { + return region; + } + + @Override + public boolean isDefined() { + return region.isDefined(); + } + + @Override + public int getArea() { + return region.getArea(); + } + + @Override + public void learnChanges() { + pos1 = region.getVertices().iterator().next().toBlockVector(); + } + + @Override + public void clear() { + region.clear(); + } + + @Override + public String getTypeName() { + return "Convex Polyhedron"; + } + + @Override + public List getInformationLines() { + List ret = new ArrayList(); + + ret.add("Vertices: "+region.getVertices().size()); + ret.add("Triangles: "+region.getTriangles().size()); + + return ret; + } + + + @Override + public void explainPrimarySelection(LocalPlayer player, LocalSession session, Vector pos) { + session.describeCUI(player); + + player.print("Started new selection with vertex "+pos+"."); + } + + @Override + public void explainSecondarySelection(LocalPlayer player, LocalSession session, Vector pos) { + session.describeCUI(player); + + player.print("Added vertex "+pos+" to the selection."); + } + + @Override + public void explainRegionAdjust(LocalPlayer player, LocalSession session) { + session.describeCUI(player); + } + + + @Override + public int getProtocolVersion() { + return 3; + } + + @Override + public String getTypeID() { + return "polyhedron"; + } + + @Override + public void describeCUI(LocalSession session, LocalPlayer player) { + Collection vertices = region.getVertices(); + Collection triangles = region.getTriangles(); + + player.dispatchCUIEvent(new SelectionShapeEvent(getTypeID())); + + Map vertexIds = new HashMap(vertices.size()); + int lastVertexId = -1; + for (Vector vertex : vertices) { + vertexIds.put(vertex, ++lastVertexId); + session.dispatchCUIEvent(player, new SelectionPointEvent(lastVertexId, vertex, getArea())); + } + + for (Triangle triangle : triangles) { + final int[] v = new int[3]; + for (int i = 0; i < 3; ++i) { + v[i] = vertexIds.get(triangle.getVertex(i)); + } + session.dispatchCUIEvent(player, new SelectionPolygonEvent(v)); + } + } + + @Override + public String getLegacyTypeID() { + return "cuboid"; + } + + @Override + public void describeLegacyCUI(LocalSession session, LocalPlayer player) { + if (isDefined()) { + session.dispatchCUIEvent(player, new SelectionPointEvent(0, region.getMinimumPoint(), getArea())); + session.dispatchCUIEvent(player, new SelectionPointEvent(1, region.getMaximumPoint(), getArea())); + } else { + session.dispatchCUIEvent(player, new SelectionShapeEvent(getLegacyTypeID())); + } + } +} diff --git a/src/main/java/com/sk89q/worldedit/regions/polyhedron/Edge.java b/src/main/java/com/sk89q/worldedit/regions/polyhedron/Edge.java new file mode 100644 index 000000000..9b5a9d339 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/regions/polyhedron/Edge.java @@ -0,0 +1,74 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q 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.polyhedron; + +import com.sk89q.worldedit.Vector; + +public class Edge { + private final Vector start; + private final Vector end; + + public Edge(Vector start, Vector end) { + this.start = start; + this.end = end; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Edge)) { + return false; + } + + Edge otherEdge = (Edge) other; + if ((this.start == otherEdge.end) && (this.end == otherEdge.start)) { + return true; + } + + if ((this.end == otherEdge.end) && (this.start == otherEdge.start)) { + return true; + } + + return false; + } + + @Override + public int hashCode() { + return start.hashCode() ^ end.hashCode(); + } + + + @Override + public String toString() { + return "(" + this.start + "," + this.end + ")"; + } + + /** + * Generates a triangle from { this.start, this.end, vertex } + * + * @param vertex The 3rd vertex for the triangle + * @return a triangle + */ + public Triangle createTriangle(Vector vertex) { + return new Triangle(this.start, this.end, vertex); + } + public Triangle createTriangle2(Vector vertex) { + return new Triangle(this.start, vertex, this.end); + } +} diff --git a/src/main/java/com/sk89q/worldedit/regions/polyhedron/Triangle.java b/src/main/java/com/sk89q/worldedit/regions/polyhedron/Triangle.java new file mode 100644 index 000000000..b7ddd0bd4 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/regions/polyhedron/Triangle.java @@ -0,0 +1,95 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q 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.polyhedron; + +import com.sk89q.worldedit.Vector; + +public class Triangle { + private final Vector[] vertices; + private final Vector normal; + private final double b; + + /** + * Constructs a triangle with the given vertices (counter-clockwise) + * + * @param v0 + * @param v1 + * @param v2 + */ + public Triangle(Vector v0, Vector v1, Vector v2) { + vertices = new Vector[] { v0, v1, v2 }; + + this.normal = v1.subtract(v0).cross(v2.subtract(v0)).normalize(); + this.b = Math.max(Math.max(normal.dot(v0), normal.dot(v1)), normal.dot(v2)); + } + + /** + * Returns the triangle's vertex with the given index, counter-clockwise. + * + * @param index Vertex index. Valid input: 0..2 + * @return a vertex + */ + public Vector getVertex(int index) { + return vertices[index]; + } + + /** + * Returns the triangle's edge with the given index, counter-clockwise. + * + * @param index Edge index. Valid input: 0..2 + * @return an edge + */ + public Edge getEdge(int index) { + if (index == vertices.length - 1) { + return new Edge(vertices[index], vertices[0]); + } + return new Edge(vertices[index], vertices[index + 1]); + } + + /** + * Returns whether the given point is above the plane the triangle is in. + * + * @param pt + * @return + */ + public boolean below(Vector pt) { + return normal.dot(pt) < b; + } + + /** + * Returns whether the given point is above the plane the triangle is in. + * + * @param pt + * @return + */ + public boolean above(Vector pt) { + return normal.dot(pt) > b; + } + + @Override + public String toString() { + return tag+"(" + this.vertices[0] + "," + this.vertices[1] + "," + this.vertices[2] + ")"; + } + String tag = "Triangle"; + public Triangle tag(String tag) { + this.tag = tag; + return this; + } +}