/** * */ package cz.cuni.mff.peckam.java.origamist.math; import static cz.cuni.mff.peckam.java.origamist.math.MathHelper.EPSILON; import static java.lang.Math.abs; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; import java.util.Queue; import java.util.Set; import javax.vecmath.Point3d; import javax.vecmath.Vector2d; import javax.vecmath.Vector3d; import org.apache.log4j.Logger; import cz.cuni.mff.peckam.java.origamist.modelstate.ModelSegment; import cz.cuni.mff.peckam.java.origamist.modelstate.ModelTriangle; import cz.cuni.mff.peckam.java.origamist.utils.ChangeNotification; import cz.cuni.mff.peckam.java.origamist.utils.ObservableList.ChangeTypes; import cz.cuni.mff.peckam.java.origamist.utils.Observer; /** * A (possibly non-convex and hole-containing, but connected) polygon in 3D space. All the triangles the polygon * consists of must lie in the same plane. * * @author Martin Pecka * * @param T The type of the triangles this polygon consists of. */ public class Polygon3d<T extends Triangle3d> { /** * The triangles the polygon consists of. This is a linked set, because we need to maintain the triangles' order for * morphing. */ protected LinkedHashSet<T> triangles = new LinkedHashSet<T>(); /** The read-only view of triangles. */ protected Set<T> trianglesRO = Collections.unmodifiableSet(triangles); /** The plane the polygon lies in. */ protected Plane3d plane = null; /** A list of observers of the triangles property. */ protected List<Observer<T>> trianglesObservers = new LinkedList<Observer<T>>(); /** * Create a new polygon consisting of the given triangles. * * @param triangles The triangles the polygon consists of. The list can be modified by this function. * @throws IllegalStateException If the resulting polygon either wouldn't be connected or one of the triangles * doesn't lie in the same plane as the first triangle does. In the case this exception is thrown, the * polygon's state will remain the same as before calling this function (eg. this will not try to add * all "valid" triangles from the given list, but it either accepts all or none of them). */ public Polygon3d(T... triangles) throws IllegalStateException { this(Arrays.asList(triangles)); } /** * Create a new polygon consisting of the given triangles. * * @param triangles The triangles the polygon consists of. The list can be modified by this function. * @throws IllegalStateException If the resulting polygon either wouldn't be connected or one of the triangles * doesn't lie in the same plane as the first triangle does. In the case this exception is thrown, the * polygon's state will remain the same as before calling this function (eg. this will not try to add * all "valid" triangles from the given list, but it either accepts all or none of them). */ public Polygon3d(List<T> triangles) throws IllegalStateException { this(new LinkedHashSet<T>(triangles)); } /** * Create a new polygon consisting of the given triangles. * * @param triangles The triangles the polygon consists of. The set can be modified by this function. * @throws IllegalStateException If the resulting polygon either wouldn't be connected or one of the triangles * doesn't lie in the same plane as the first triangle does. In the case this exception is thrown, the * polygon's state will remain the same as before calling this function (eg. this will not try to add * all "valid" triangles from the given list, but it either accepts all or none of them). */ public Polygon3d(Set<T> triangles) throws IllegalStateException { if (triangles.size() > 0) { addTriangles(triangles); } } /** * Add all the given triangles to the polygon. * * @param triangles The triangles to add. The list can be modified by this function. * @throws IllegalStateException If the resulting polygon either wouldn't be connected or one of the triangles * doesn't lie in the same plane as the layer does. In the case this exception is thrown, the polygon's * state will remain the same as before calling this function (eg. this will not try to add all "valid" * triangles from the given list, but it either accepts all or none of them). */ public void addTriangles(T... triangles) throws IllegalStateException { addTriangles(Arrays.asList(triangles)); } /** * Add all the given triangles to the polygon. * * @param triangles The triangles to add. The list can be modified by this function. * @throws IllegalStateException If the resulting polygon either wouldn't be connected or one of the triangles * doesn't lie in the same plane as the layer does. In the case this exception is thrown, the polygon's * state will remain the same as before calling this function (eg. this will not try to add all "valid" * triangles from the given list, but it either accepts all or none of them). */ public void addTriangles(List<T> triangles) throws IllegalStateException { addTriangles(new LinkedHashSet<T>(triangles)); } /** * Add all the given triangles to the polygon. * * @param triangles The triangles to add. The set can be modified by this function. * @throws IllegalStateException If the resulting polygon either wouldn't be connected or one of the triangles * doesn't lie in the same plane as the layer does. In the case this exception is thrown, the polygon's * state will remain the same as before calling this function (eg. this will not try to add all "valid" * triangles from the given list, but it either accepts all or none of them). */ static int i = 0; @SuppressWarnings("unchecked") public void addTriangles(Set<T> triangles) throws IllegalStateException { if (triangles == null || triangles.size() == 0) return; // be sure not to add a triangle for the second time triangles.removeAll(this.triangles); // TODO should be done using epsilonEquals if (triangles.size() == 0) return; // nothing new to add if (plane == null) plane = triangles.iterator().next().getPlane(); i++; // check that all the new triangles lie in the polygon's plane for (T t : triangles) { Vector3d cross = new Vector3d(); cross.cross(plane.getNormal(), t.getNormal()); // if the normals are parallel, then the cross product will be zero if (!cross.epsilonEquals(new Vector3d(), EPSILON)) { throw new IllegalStateException( "Adding a triangle to polygon but the triangle doesn't lie in the polygon's plane."); } } // backup for the case that the resulting polygon is invalid LinkedHashSet<T> oldTriangles = new LinkedHashSet<T>(this.triangles); this.triangles.addAll(triangles); T borderTriangle = null; // the triangle from the "old" polygon which neighbors with a new triangle if (oldTriangles.size() == 0) { // if we have had no triangles in the polygon yet, fake the borderTriangle with any new triangle borderTriangle = triangles.iterator().next(); } else { outer: for (T t : triangles) { for (Triangle3d n : t.getNeighbors()) { if (oldTriangles.contains(n)) { borderTriangle = (T) n; break outer; } } } } if (borderTriangle == null) { this.triangles = oldTriangles; throw new IllegalStateException( "Trying to add triangles to polygon, but none of them neighbors to the polygon."); } // check if the polygon is connected (doesn't consist of two or more parts) // this can be done by recursively traversing the neighbors of any one triangle and checking that we visited all // triangles this way (in a connected space there always exists a path between any two points) // two triangles are not considered being neighbors if they only touch in a vertex HashSet<T> visited = new HashSet<T>(oldTriangles); // we suppose that the old polygon was connected Queue<T> toVisit = new LinkedList<T>(); toVisit.add(borderTriangle); T t; while ((t = toVisit.poll()) != null) { visited.add(t); for (T n : getNeighbors(t)) { if (!visited.contains(n)) toVisit.add(n); } } if (visited.size() != this.triangles.size()) { // if we didn't manage to visit all triangles, the resulting polygon wouldn't be connected this.triangles = oldTriangles; throw new IllegalStateException("Trying to construct a non-connected polygon."); } if (!additionalAddTrianglesCheck(triangles)) { this.triangles = oldTriangles; throw new IllegalStateException( "The triangles newly added to this polygon don't conform to the rules for new triangles."); } for (Observer<T> observer : trianglesObservers) { for (T triangle : triangles) { observer.changePerformed(new ChangeNotification<T>(this.triangles, triangle, ChangeTypes.ADD)); } } } /** * Add the given triangle to the polygon. * * For adding just one triangle, this is more efficient than {@link Polygon3d#addTriangles(Set)}. * * @param triangle The triangle to add. * @throws IllegalStateException If the resulting polygon either wouldn't be connected or the triangle doesn't lie * in the same plane as the layer does. In the case this exception is thrown, the polygon's state will * remain the same as before calling this function. */ public void addTriangle(T triangle) throws IllegalStateException { if (triangle == null) return; // be sure not to add the triangle for the second time if (this.triangles.contains(triangle)) // TODO should be checked using epsilonEquals return; // nothing new to add if (plane == null) { plane = triangle.getPlane(); } else { // check that the new triangle lies in the polygon's plane Vector3d cross = new Vector3d(); cross.cross(plane.getNormal(), triangle.getNormal()); // if the normals are parallel, then the cross product will be zero if (!cross.epsilonEquals(new Vector3d(), EPSILON)) { throw new IllegalStateException( "Adding a triangle to polygon but the triangle doesn't lie in the polygon's plane."); } } // backup for the case that the resulting polygon is invalid LinkedHashSet<T> oldTriangles = new LinkedHashSet<T>(this.triangles); this.triangles.add(triangle); boolean neighborsToOldPolygon = false; if (oldTriangles.size() == 0) { // if we have had no triangles in the polygon yet, set this flag to true, since it's useless in this case neighborsToOldPolygon = true; } else { for (Triangle3d n : triangle.getNeighbors()) { if (oldTriangles.contains(n)) { neighborsToOldPolygon = true; break; } } } if (!neighborsToOldPolygon) { this.triangles = oldTriangles; throw new IllegalStateException( "Trying to add a triangle to polygon, but it doesn't neighbor to the polygon."); } HashSet<T> set = new HashSet<T>(1); set.add(triangle); if (!additionalAddTrianglesCheck(set)) { this.triangles = oldTriangles; throw new IllegalStateException( "The triangle newly added to this polygon doesn't conform to the rules for new triangles."); } for (Observer<T> observer : trianglesObservers) { observer.changePerformed(new ChangeNotification<T>(triangles, triangle, ChangeTypes.ADD)); } } /** * Remove all the given triangles from the polygon. Triangles not present in the polygon are ignored. * * @param triangles The triangles to remove. The set can be modified by this function. * @throws IllegalStateException If the resulting polygon wouldn't be connected. In the case this exception is * thrown, the polygon's state will remain the same as before calling this function (eg. this will not * try to remove all "valid" triangles from the given list, but it either accepts all or none of them). */ public void removeTriangles(Set<T> triangles) throws IllegalStateException { if (triangles == null || triangles.size() == 0) return; // ignore triangles not present in the polygon try { triangles.retainAll(this.triangles); // TODO should be done using epsilonEquals } catch (UnsupportedOperationException e) { Iterator<T> it = triangles.iterator(); while (it.hasNext()) { T t = it.next(); if (!this.triangles.contains(t)) it.remove(); } } if (triangles.size() == 0) return; // nothing to remove if (triangles.size() == this.triangles.size()) { // we want to remove all triangles this.triangles.clear(); plane = null; for (Observer<T> observer : trianglesObservers) { for (T triangle : triangles) { observer.changePerformed(new ChangeNotification<T>(this.triangles, triangle, ChangeTypes.REMOVE)); } } return; } // backup for the case that the resulting polygon is invalid LinkedHashSet<T> oldTriangles = new LinkedHashSet<T>(this.triangles); this.triangles.removeAll(triangles); // check if the polygon is connected (doesn't consist of two or more parts) // this can be done by recursively traversing the neighbors of any one triangle and checking that we visited all // triangles this way (in a connected space there always exists a path between any two points) // two triangles are not considered being neighbors if they only touch in a vertex HashSet<T> visited = new HashSet<T>(this.triangles.size()); Queue<T> toVisit = new LinkedList<T>(); toVisit.add(this.triangles.iterator().next()); T t; while ((t = toVisit.poll()) != null) { visited.add(t); List<T> neighbors = getNeighbors(t); for (T n : neighbors) { if (!visited.contains(n)) toVisit.add(n); } } if (visited.size() != this.triangles.size()) { // if we didn't manage to visit all triangles, the resulting polygon wouldn't be connected this.triangles = oldTriangles; throw new IllegalStateException("Trying to construct a non-connected polygon."); } if (!additionalRemoveTrianglesCheck(triangles)) { this.triangles = oldTriangles; throw new IllegalStateException( "The triangles removed from this polygon don't conform to the rules for removed triangles."); } for (Observer<T> observer : trianglesObservers) { for (T triangle : triangles) { observer.changePerformed(new ChangeNotification<T>(this.triangles, triangle, ChangeTypes.REMOVE)); } } } /** * Remove the given triangle from the polygon. If it is not present in the polygon, nothing happens. * * This function is no more effective than calling {@link Polygon3d#removeTriangles(Set)} with a one-element set. * * @param triangle The triangle to remove. * @throws IllegalStateException If the resulting polygon wouldn't be connected. In the case this exception is * thrown, the polygon's state will remain the same as before calling this function. */ public void removeTriangle(T triangle) throws IllegalStateException { HashSet<T> set = new HashSet<T>(1); set.add(triangle); removeTriangles(set); } /** * "Cut" the given triangle by the given segment and return the triangles that are created by the cut. * * @param segment The triangle to cut and the segment to cut with. * @return The newly created triangles. * * @throws IllegalArgumentException If the segment doesn't define a cut of the triangle or if the triangle isn't one * of this polygon's triangles. */ public List<T> subdivideTriangle(IntersectionWithTriangle<T> segment) throws IllegalArgumentException { if (!triangles.contains(segment.triangle)) { // TODO should be checked using epsilonEquals throw new IllegalArgumentException( "Polygon3d#subdivideTriangle(): Trying to subdivide a triangle not present in this polygon."); } if (!segment.triangle.sidesContain(segment.p) || !segment.triangle.sidesContain(segment.p2)) { throw new IllegalArgumentException( "Polygon3d#subdivideTriangle(): Trying to subdivide a triangle by an invalid cut segment."); } List<T> triangles = segment.triangle.subdivideTriangle(segment); if (triangles.size() == 0) assert false : "Polygon3d#subdivideTriangle(): 0 triangles after triangle subdivision."; if (triangles.size() == 1) // no subdividing is needed return triangles; this.triangles.remove(segment.triangle); for (Observer<T> observer : trianglesObservers) { observer.changePerformed(new ChangeNotification<T>(triangles, segment.triangle, ChangeTypes.REMOVE)); } this.triangles.addAll(triangles); for (Observer<T> observer : trianglesObservers) { for (T triangle : triangles) { observer.changePerformed(new ChangeNotification<T>(triangles, triangle, ChangeTypes.ADD)); } } return triangles; } /** * Performs additional checks on the newly added triangles. * * This is intended to be used by subclasses to specify more precisely the rules for adding new triangles. * * @param triangles The newly triangles. * @return <code>true</code> if all the checks were ok. */ protected boolean additionalAddTrianglesCheck(Set<T> triangles) { return true; } /** * Performs additional checks on the removed triangles. * * This is intended to be used by subclasses to specify more precisely the rules for removing triangles. * The check is called in the time when this.triangles and this.neighbors don't contain the removed triangles. * * @param triangles The removed triangles. * @return <code>true</code> if all the checks were ok. */ protected boolean additionalRemoveTrianglesCheck(Set<T> triangles) { return true; } /** * Return all triangles that have a common edge with the given triangle. * * @param triangle The triangle to find neighbors for. * @return All triangles that have a common edge with the given triangle. */ @SuppressWarnings("unchecked") public List<T> getNeighbors(T triangle) { List<T> result = new LinkedList<T>(); for (Triangle3d n : triangle.getNeighbors()) { if (this.triangles.contains(n)) result.add((T) n); } return result; } /** * @return An unmodifiable set of the triangles this polygon consists of. The set's iterator iterates the triangles * in the order they were added/subdivided in this triangle (the source list is a {@link LinkedHashSet}). */ public Set<T> getTriangles() { return trianglesRO; } /** * Tell whether this polygon contains the given point. * * @param point The point to check. * @return <code>true</code> if this polygon contains the given point. */ public boolean contains(Point3d point) { // TODO maybe ineffective if (!plane.contains(point)) return false; for (T t : triangles) { if (t.contains(point)) return true; } return false; } /** * Return the segments that are the intersection of the given line and this polygon. If a part of the intersection * would be a point, then a segment with zero direction vector will appear in the list. * * All segments will have their direction vector pointing in the same direction (the same direction where points the * line's direction vector) and will be ordered along this vector. * No two segments will have a common point (this means segments with a common point will be joined). * * @param line The line we search intersections with. * @return the segments that are the intersection of the given line and this polygon. If a part of the intersection * would be a point, then a segment with zero direction vector will appear in the list. */ public List<? extends Segment3d> getIntersections(Line3d line) { // connect all segments that can be connected into one new segment List<IntersectionWithTriangle<T>> intersections = getIntersectionsWithTriangles(line); return joinNeighboringSegments(intersections); } /** * Return the intersections of the given stripe and this polygon. * * @param stripe The stripe to find intersections with. * @return The intersections of the given stripe and this polygon. <code>null</code> if the stripe is parallel to * this polygon or has its direction vector parallel to this polygon. */ public List<? extends Segment3d> getIntersections(Stripe3d stripe) { Line3d stripePlaneAndPolygonPlaneInt = stripe.getPlane().getIntersection(getPlane()); if (stripePlaneAndPolygonPlaneInt == null) return null; // the stripe and the polygon are parallel // the stripe's direction vector is parallel to the polygon if (MathHelper.vectorQuotient3d(stripePlaneAndPolygonPlaneInt.getVector(), stripe.getLine1().getVector()) != null) return null; Line3d segmentPoint1 = stripe.getHalfspace1().getPlane().getIntersection(stripePlaneAndPolygonPlaneInt); Line3d segmentPoint2 = stripe.getHalfspace2().getPlane().getIntersection(stripePlaneAndPolygonPlaneInt); // we can assume the computed segment points are regular points and that they exist - an intersection line of // two non-parallel planes always exists; in addition the intersection line isn't parallel to any of the // stripe's border lines (and lies in the same plane), so these lines must intersect Segment3d intersectionSegment = new Segment3d(segmentPoint1.p, segmentPoint2.p); return getIntersections(intersectionSegment); } /** * Return the single segment defining the intersection of the given stripe with this polygon (this segment joins the * first and last segment returned by {@link Polygon3d#getIntersections}). * * @param stripe The stripe to find the intersection with. * @return The intersection of the given stripe and this polygon. <code>null</code> if the stripe is parallel to * this polygon (and if it lies in the same plane as the polygon) or if the stripe doesn't intersect with * it. */ public Segment3d getIntersectionSegment(Stripe3d stripe) { List<? extends Segment3d> ints = getIntersections(stripe); if (ints == null || ints.size() == 0) return null; Point3d int1 = ints.get(0).getP1(); Point3d int2 = ints.get(ints.size() - 1).getP2(); return new Segment3d(int1, int2); } /** * Take the list of segments lying on one line. They all must point in the same direction and must be ordered as * they go along the line. This method returns the list of segments such, that no two segments will have a common * point (segments with common points will be joined). * * @param segments The list of segments to join. * @return The list of joined segments. */ public List<? extends Segment3d> joinNeighboringSegments(List<IntersectionWithTriangle<T>> segments) { LinkedList<Segment3d> result = new LinkedList<Segment3d>(); for (IntersectionWithTriangle<T> s : segments) { if (result.size() == 0 || !result.getLast().p2.epsilonEquals(s.p, EPSILON)) { result.add(new Segment3d(s.p, s.p2)); } else { Segment3d last = result.getLast(); last = new Segment3d(last.p, s.p2); result.removeLast(); result.add(last); } } return result; } /** * Return the segments that are the intersection of the given line and this polygon. If a part of the intersection * would be a point, then a segment with zero direction vector will appear in the list. * * All segments will have their direction vector pointing in the same direction (the same direction where points the * line's direction vector) and will be ordered along this vector. * Each segment will end at intersection with a triangle. * * Be aware that if you provide a segment as the argument and the segment starts or ends within a triangle, it will * be virtually extended to intersect a side of the triangle. This is implemented as a specific need of the * Origamist project to be robust against rounding errors. * * @param line The line we search intersections with. * @return the segments that are the intersection of the given line and this polygon. If a part of the intersection * would be a point, then a segment with zero direction vector will appear in the list. */ public List<IntersectionWithTriangle<T>> getIntersectionsWithTriangles(final Line3d line) { // idea: find intersections with triangles and sort them as they go along the line final Hashtable<Point3d, Double> parameters = new Hashtable<Point3d, Double>(triangles.size()); List<IntersectionWithTriangle<T>> intersections = new LinkedList<IntersectionWithTriangle<T>>(); for (T t : triangles) { Segment3d intersection = t.getIntersection(line); // a check if the 2D segment also intersects with the 2D triangle if (intersection != null && line instanceof ModelSegment && t instanceof ModelTriangle) { Triangle2d t2 = ((ModelTriangle) t).getOriginalPosition(); Segment2d s2 = ((ModelSegment) line).getOriginal(); // if the segment doesn't intersect the 2D triangle, skip this intersection Segment2d intersection2 = t2.getIntersection(s2); if (intersection2 == null) { intersection = null; } else if (intersection2.v.epsilonEquals(new Vector2d(), EPSILON) != intersection.v.epsilonEquals( new Vector3d(), EPSILON)) { // the 3D intersection is point and the 2D one is not, or vice versa intersection = null; } } // this is important to be robust against rounding errors - in most cases we don't handle segments starting // or ending inside triangles, and if we encounter such a segment, it is probably due to rounding errors if (line instanceof Segment3d && intersection != null && (!t.sidesContain(intersection.p) || !t.sidesContain(intersection.p2))) { intersection = t.getIntersection(new Line3d(line)); } if (intersection != null) { // the nearest point search is performed to cancel the segment "magnetizing" heuristic in // Triangle3d#getIntersection() Double p1 = line.getParameterForPoint(line.getNearestPoint(intersection.p)); Double p2 = line.getParameterForPoint(line.getNearestPoint(intersection.p2)); if (p1 != null && p2 != null) { if (parameters.get(intersection.p) == null) parameters.put(intersection.p, p1); if (parameters.get(intersection.p2) == null) parameters.put(intersection.p2, p2); if (p1 > p2) { // here we don't want to compare using epsilon-equals intersection = new Segment3d(intersection.p2, intersection.p); } intersections.add(new IntersectionWithTriangle<T>(t, intersection)); } else { Logger.getLogger(getClass()) .warn("Polygon3d#getIntersectionsWithTriangles(): couldn't locate intersection segment points on the intersecting line. The line was " + line + ", the segment was " + intersection); } } } if (intersections.size() <= 1) return intersections; // sort the segments according to the parameters of the border points // this means the segments will be ordered "as they go along the line" Collections.sort(intersections, new Comparator<IntersectionWithTriangle<T>>() { @Override public int compare(IntersectionWithTriangle<T> o1, IntersectionWithTriangle<T> o2) { double diff = parameters.get(o1.p) - parameters.get(o2.p); if (diff < -EPSILON) { return -1; } else if (diff > EPSILON) { return 1; } else { // if the segments have the same starts, one of them can be a zero-length segment - consider those // as less than "full"-length segments double maxDiff = parameters.get(o1.p2) - parameters.get(o2.p2); return (maxDiff < -EPSILON ? -1 : (maxDiff > EPSILON ? 1 : 0)); } } }); return intersections; } /** * Check if the given line or segment intersects with this layer. * * @param line The line to check. * @return True if the line intersects with this layer. */ public boolean liesInThisLayer(Line3d line) { double dot = plane.getNormal().dot(line.getVector()); // we want the line to lie in the layer's plane, so it must be perpendicular to it's normal if (abs(dot) > EPSILON) return false; // the line must lie in the layer's plane if (!plane.contains(line.getPoint())) return false; Segment3d intersection; for (T t : triangles) { intersection = t.getIntersection(line); if (intersection != null && !intersection.getVector().epsilonEquals(new Vector3d(), EPSILON)) return true; } return false; } /** * Split this polygon to two or more polygons by the given line. * * This method requires that the line has either no intersection with a triangle or goes through its edge. * * The resulting polygons will be the maximal ones that can be connected. * * @param line The line to split around. * @param part1 The polygons that are in the direction of the cross product of the line direction vector and the * polygon plane's normal. * @param part2 The rest of polygons. * @return <code>part1</code>. * * @throws IllegalArgumentException If the line goes through the inside of a triangle. */ public List<Polygon3d<T>> splitPolygon(Line3d line, List<Polygon3d<T>> part1, List<Polygon3d<T>> part2) throws IllegalArgumentException { Vector3d direction = new Vector3d(); direction.cross(line.getVector(), getNormal()); Point3d dirPoint = new Point3d(direction); Point3d p2 = new Point3d(line.p); p2.add(line.v); HalfSpace3d hs = HalfSpace3d.createPerpendicularToTriangle(line.p, p2, dirPoint); Set<T> part1triangles = new HashSet<T>(); Set<T> part2triangles = new HashSet<T>(); for (T triangle : triangles) { Segment3d intersection = triangle.getIntersection(line); if (intersection == null || (intersection.v.epsilonEquals(new Vector3d(), EPSILON) && triangle.isVertex(intersection.p)) || intersection.overlaps(triangle.getS1()) || intersection.overlaps(triangle.getS2()) || intersection.overlaps(triangle.getS3()) || (intersection.v.epsilonEquals(new Vector3d(), EPSILON) && !triangle.sidesContain(intersection.p) && !triangle .sidesContain(intersection.p2))) { if (hs.contains(triangle.p1) && hs.contains(triangle.p2) && hs.contains(triangle.p3)) { part1triangles.add(triangle); } else { part2triangles.add(triangle); } } else { // it is not an error to have intersection not being the triangle's vertex, it is the case when the line // goes through a vertex; // on the other side, if the intersection is a line and doesn't overlap a triangle's edge, it is an // error throw new IllegalArgumentException( "Polygon3d#splitLayer: a line going through the interior of a triangle detected."); } } assert part1triangles.size() + part2triangles.size() == triangles.size() : part1triangles.size() + ", " + part2triangles.size() + ": " + triangles.size(); assert triangles.size() > 0; Hashtable<Set<T>, List<Polygon3d<T>>> parts = new Hashtable<Set<T>, List<Polygon3d<T>>>(2); parts.put(part1triangles, part1); parts.put(part2triangles, part2); for (Entry<Set<T>, List<Polygon3d<T>>> e : parts.entrySet()) { Set<T> triangles = e.getKey(); List<Polygon3d<T>> polygons = e.getValue(); while (!triangles.isEmpty()) { Iterator<T> it = triangles.iterator(); T triangle = it.next(); it.remove(); it = null; Queue<T> queue = new LinkedList<T>(); queue.add(triangle); List<T> polygonTriangles = new LinkedList<T>(); while ((triangle = queue.poll()) != null) { polygonTriangles.add(triangle); triangles.remove(triangle); List<T> tNeighbors = getNeighbors(triangle); for (T n : tNeighbors) { if (triangles.contains(n)) { queue.add(n); } } } polygons.add(new Polygon3d<T>(polygonTriangles)); } } return part1; } /** * Rotate all triangles in this layer around the given axis by the given angle. * * @param axis The axis to rotate around. * @param angle The angle to rotate the triangles by. */ public void rotate(Line3d axis, double angle) { List<T> oldTriangles = new LinkedList<T>(); oldTriangles.addAll(triangles); // hashset needs the triangles to not change their hashcode, but setting points to something else will change it // - on the other side, a LinkedList doesn't care about the hashcode // so we need to remove all triangles, change the vertices and then add them back again - this will also "reset" // the neighbors map removeTriangles(new HashSet<T>(triangles)); for (T t : oldTriangles) { t.setPoints(MathHelper.rotate(t.getP1(), axis, angle), MathHelper.rotate(t.getP2(), axis, angle), MathHelper.rotate(t.getP3(), axis, angle)); } addTriangles(oldTriangles); } /** * @return * @see cz.cuni.mff.peckam.java.origamist.math.Plane3d#getNormal() */ public Vector3d getNormal() { return plane.getNormal(); } /** * @return The plane the polygon lies in. */ public Plane3d getPlane() { return plane; } /** * @return the trianglesObservers */ public List<Observer<T>> getTrianglesObservers() { return trianglesObservers; } /** * Add new observer of the triangles property. * * @param observer The observer to add. */ public void addTrianglesObserver(Observer<T> observer) { trianglesObservers.add(observer); } /** * Remove the given observer of the triangles property. * * @param observer The observer to remove. */ public void removeTrianglesObserver(Observer<T> observer) { trianglesObservers.remove(observer); } /** * Remove all observers of the triangles property. */ public void clearTrianglesObservers() { trianglesObservers.clear(); } @Override public String toString() { return "Polygon3d [" + triangles + "]"; } }