/** * */ package cz.cuni.mff.peckam.java.origamist.modelstate; import static cz.cuni.mff.peckam.java.origamist.math.MathHelper.EPSILON; import static java.lang.Math.abs; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Queue; import java.util.Set; import javax.media.j3d.GeometryArray; import javax.media.j3d.LineArray; import javax.media.j3d.TriangleArray; import javax.vecmath.Point2d; import javax.vecmath.Point2f; import javax.vecmath.Point3d; import javax.vecmath.TexCoord2f; import javax.vecmath.Vector2d; import javax.vecmath.Vector3d; import org.apache.log4j.Logger; import cz.cuni.mff.peckam.java.origamist.exceptions.InvalidOperationException; import cz.cuni.mff.peckam.java.origamist.exceptions.PaperIntersectionException; import cz.cuni.mff.peckam.java.origamist.exceptions.PaperStructureException; import cz.cuni.mff.peckam.java.origamist.exceptions.PaperTearException; import cz.cuni.mff.peckam.java.origamist.math.HalfSpace3d; import cz.cuni.mff.peckam.java.origamist.math.IntersectionWithTriangle; import cz.cuni.mff.peckam.java.origamist.math.Line2d; import cz.cuni.mff.peckam.java.origamist.math.Line3d; import cz.cuni.mff.peckam.java.origamist.math.MathHelper; import cz.cuni.mff.peckam.java.origamist.math.Plane3d; import cz.cuni.mff.peckam.java.origamist.math.Polygon3d; import cz.cuni.mff.peckam.java.origamist.math.Segment2d; import cz.cuni.mff.peckam.java.origamist.math.Segment3d; import cz.cuni.mff.peckam.java.origamist.math.Stripe3d; import cz.cuni.mff.peckam.java.origamist.math.Triangle2d; import cz.cuni.mff.peckam.java.origamist.math.Triangle3d; import cz.cuni.mff.peckam.java.origamist.model.Origami; import cz.cuni.mff.peckam.java.origamist.model.Step; import cz.cuni.mff.peckam.java.origamist.model.UnitDimension; import cz.cuni.mff.peckam.java.origamist.model.UnitHelper; import cz.cuni.mff.peckam.java.origamist.model.jaxb.Unit; import cz.cuni.mff.peckam.java.origamist.utils.ChangeNotification; import cz.cuni.mff.peckam.java.origamist.utils.ObservableList; import cz.cuni.mff.peckam.java.origamist.utils.ObservableList.ChangeTypes; import cz.cuni.mff.peckam.java.origamist.utils.Observer; /** * The internal state of the model after some steps. * * @author Martin Pecka */ public class ModelState implements Cloneable { /** * Folds on this paper. */ protected ObservableList<Fold> folds = new ObservableList<Fold>(); /** Cache for arrays of the lines representing folds. */ protected LineArray[] foldLineArrays = null; /** * If true, the value of foldLineArray doesn't have to be consistent and a call to updateLineArray is needed. */ protected boolean foldLineArraysDirty = true; /** The triangles this model state consists of. */ protected ObservableList<ModelTriangle> triangles = new ObservableList<ModelTriangle>(); /** The layers of the paper. */ protected ObservableList<Layer> layers = new ObservableList<Layer>(); /** The list of markers to be displayed. */ protected ObservableList<Marker> markers = new ObservableList<Marker>(); /** The mapping of triangles to their containing layer. Automatically updated when <code>layers</code> change */ protected Hashtable<ModelTriangle, Layer> trianglesToLayers = new Hashtable<ModelTriangle, Layer>(); /** * A cache for quick finding of a 3D triangle corresponding to the given 2D triangle. Automatically updated when * <code>triangles</code> change. */ protected Hashtable<Triangle2d, ModelTriangle> paperToSpaceTriangles = new Hashtable<Triangle2d, ModelTriangle>(); /** Cache for finding 3D locations of points corresponding to 2D points on the paper. */ protected Hashtable<Point2d, Point3d> paperToSpacePoint = new Hashtable<Point2d, Point3d>(); /** * The triangles the model state consists of. Each array component contains triangles of one layer. Indices in the * array correspond to indices in the layer list. This representation can be directly used by Java3D. */ protected TriangleArray[] trianglesArrays = null; /** * If true, the value of trianglesArrays doesn't have to be consistent and a call to updateVerticesArray is needed. */ protected boolean trianglesArraysDirty = true; /** The data from markers needed for rendering. This list should be automatically handled by the markers list. */ protected List<MarkerRenderData> markerData = new LinkedList<MarkerRenderData>(); /** If true, the 3D positions of markers need to be recomputed before returning them to some caller. */ protected boolean markersDirty = true; /** * Rotation of the model (around the axis from eyes to display) in radians. */ protected double rotationAngle = 0; /** * The angle the model is viewed from (angle between eyes and the unfolded paper surface) in radians. * * PI/2 means top view, -PI/2 means bottom view */ protected double viewingAngle = Math.PI / 2.0; /** The normal of the screen. Originally it is a vector of (0,0,1) and rotation and viewing angle are applied to it. */ protected Vector3d screenNormal = null; /** * The step this state belongs to. */ protected Step step; /** * The origami model which is this the state of. */ protected Origami origami; /** * The number of steps a foldline remains visible. */ protected int stepBlendingTreshold = 5; /** * Denotes the point to measure furthest rotated point distance from. If <code>null</code>, furthest rotated point * from point calculation is disabled. */ protected ModelPoint furthestRotationCenter = null; /** The furthest point from furthestRotationCenter found by bendPaper() calls. */ protected ModelPoint furthestRotatedPoint = null; /** * Denotes the segment to measure furthest rotated point distance from. If <code>null</code>, furthest rotated point * around segment calculation is disabled. */ protected ModelSegment furthestRotationSegment = null; /** The furthest point from furthestRotationSegment found by bendPaper() calls. */ protected ModelPoint furthestRotatedPointAroundSegment = null; /** If not null, this operation is covered by this image. */ protected BufferedImage overlayImage = null; public ModelState() { addObservers(); } /** * Add all the needed observers to this state's observable fields. */ protected void addObservers() { folds.addObserver(new Observer<Fold>() { @Override public void changePerformed(ChangeNotification<? extends Fold> change) { ModelState.this.foldLineArraysDirty = true; if (change.getChangeType() == ChangeTypes.ADD) { change.getItem().lines.addObserver(new Observer<FoldLine>() { @Override public void changePerformed(ChangeNotification<? extends FoldLine> change) { ModelState.this.foldLineArraysDirty = true; } }); } } }); triangles.addObserver(new Observer<ModelTriangle>() { @Override public void changePerformed(ChangeNotification<? extends ModelTriangle> change) { ModelState.this.trianglesArraysDirty = true; ModelState.this.foldLineArraysDirty = true; ModelState.this.markersDirty = true; paperToSpacePoint.clear(); if (change.getChangeType() != ChangeTypes.ADD) { ModelTriangle t = change.getOldItem(); paperToSpaceTriangles.remove(t.originalPosition); } else if (change.getChangeType() != ChangeTypes.REMOVE) { ModelTriangle t = change.getItem(); paperToSpaceTriangles.put(t.originalPosition, t); } } }); layers.addObserver(new Observer<Layer>() { @Override public void changePerformed(ChangeNotification<? extends Layer> change) { if (change.getChangeType() != ChangeTypes.ADD) { Layer old = change.getOldItem(); for (ModelTriangle t : old.getTriangles()) { trianglesToLayers.remove(t); } old.clearTrianglesObservers(); } else if (change.getChangeType() != ChangeTypes.REMOVE) { final Layer layer = change.getItem(); for (ModelTriangle t : layer.getTriangles()) { trianglesToLayers.put(t, layer); } layer.addTrianglesObserver(new Observer<ModelTriangle>() { @Override public void changePerformed(ChangeNotification<? extends ModelTriangle> change) { if (change.getChangeType() != ChangeTypes.ADD) { trianglesToLayers.remove(change.getOldItem()); } else if (change.getChangeType() != ChangeTypes.REMOVE) { ModelTriangle triangle = change.getItem(); trianglesToLayers.put(triangle, layer); } } }); } } }); markers.addObserver(new Observer<Marker>() { @Override public void changePerformed(ChangeNotification<? extends Marker> change) { if (change.getChangeType() != ChangeTypes.ADD) markerData.remove(change.getOldItem().getRenderData()); if (change.getChangeType() != ChangeTypes.REMOVE) markerData.add(change.getItem().getRenderData()); } }); } /** * Set the step this model state belongs to. * * @param step The step to set. */ public void setStep(Step step) { this.step = step; } /** * Set the origami model this step will work with. * * @param origami The origami model. */ public void setOrigami(Origami origami) { this.origami = origami; } /** * @return The step this model state belongs to. */ public Step getStep() { return step; } /** * @return The origami model this step works with. */ public Origami getOrigami() { return origami; } /** * Takes a point defined in the 2D paper relative coordinates and returns the position of the point in the 3D model * state (also in relative coordinates). * * This method uses a cache for the points. The cache is cleared everytime a triangle is added or removed. * * @param point The 2D paper point to find the corresponding 3D point for. * @return The 3D point. The returned copy is a fresh instance, so you can alter it. * * @throws IllegalArgumentException If the given point doesn't lie in the paper. */ public Point3d locatePointFromPaperTo3D(Point2d point) throws IllegalArgumentException { if (!origami.getModel().getPaper().containsRelative(point)) throw new IllegalArgumentException("locatePointFromPaperTo3D: Given point doesn't lie in the paper: " + point); if (paperToSpacePoint.get(point) == null) { ModelTriangle containingTriangle = null; // TODO possible performance loss, try to use some kind of Voronoi diagram??? But it seems that this section // won't be preformance-bottle-neck for (ModelTriangle t : triangles) { if (t.getOriginalPosition().contains(point)) { containingTriangle = t; break; } } if (containingTriangle == null) { Logger.getLogger(getClass()).warn("locatePointFromPaperTo3D: Couldn't locate point " + point); return new Point3d(); } Vector3d barycentric = containingTriangle.getOriginalPosition().getBarycentricCoords(point); paperToSpacePoint.put(point, new Point3d(containingTriangle.interpolatePointFromBarycentric(barycentric))); } return paperToSpacePoint.get(point); } /** * Update the contents of the foldLineArrays so that it corresponds to the actual contents of the folds variable. */ @SuppressWarnings("serial") protected synchronized void updateLineArrays() { // HOW THIS METHOD WORKS // WE HAVE: a bunch of triangle edges marked as fold lines; a lot of them is duplicated by various folds going // through that triangle edge // WE WANT: a set of lines, where no two lines could be connected together to form a new narrow line not // intersected by another line; also no lines can be duplicated // WE DO: // 1) take only the newest line for each triangle edge (the newest is the latest in the edge's fold line list) // - this makes sure no lines are duplicated // 2) connect all segments into the longest narrow lines available // - this makes sure no two lines can be connected to form a new narrow line // 3) split the lines at their intersections // - this makes sure the lines aren't too long List<List<ModelSegment>> lines = new ArrayList<List<ModelSegment>>(step.getId() * 3); for (int i = 0; i < step.getId() * 3; i++) lines.add(new LinkedList<ModelSegment>()); // STEP 1 // put only the newest fold lines into lines (it means, if more folds go through a fold line, add only the one // with highest originatingStepId) int index; for (ModelTriangle t : triangles) { for (int i = 0; i < 3; i++) { List<FoldLine> foldLines = t.getFoldLines(i); if (foldLines != null && foldLines.size() > 0) { FoldLine line = foldLines.get(foldLines.size() - 1); if (line.getDirection() != null) index = line.getDirection().ordinal() * line.getFold().getOriginatingStepId(); else index = 2 * line.getFold().getOriginatingStepId(); lines.get(index).add(new ModelSegment(line)); } } } // STEP 2 // now we have a lot of lines split by the boundaries of the triangles, so join all adjacent lines together int numLines = 0; for (List<ModelSegment> list : lines) { if (list.size() == 0) continue; numLines += list.size(); // join lines that are parallel and overlap // TODO O(n^2) algorithm, couldn't we do it better? (it already has contained some optimizations, but still // it's been O(n^2)) int i = 0; for (Iterator<ModelSegment> it = list.iterator(); it.hasNext();) { if (i >= list.size() - 1) break; ModelSegment seg = it.next(); boolean merged = false; for (ModelSegment other : list.subList(i + 1, list.size())) { if (other.merge(seg)) { it.remove(); numLines--; merged = true; break; } } if (!merged) i++; } } // now flatten the joined lines into linesWhole and prepare linesIntersected for further use ModelSegment[] linesWhole = new ModelSegment[numLines]; List<List<ModelSegment>> linesIntersected = new ArrayList<List<ModelSegment>>(numLines); int i = 0; for (List<ModelSegment> list : lines) { for (final ModelSegment seg : list) { linesWhole[i] = seg; linesIntersected.add(new ArrayList<ModelSegment>(5) { { add(seg.clone()); } }); i++; } } // STEP 3 // now we have all the parts of narrow lines joined into one line; but we want to get lines split at their // intersections with other lines // this algorithm runs in O(n^2) time; it just takes pairs of the narrow lines and looks if they intersect; if // they do, it subdivides those lines into linesIntersected list and then looks for intersections for the // subdivided parts // it would be nice to use iterators and foreach loops here, but we need to add elements to the iterated lists for (i = 0; i < linesWhole.length - 1; i++) { for (int j = i + 1; j < linesWhole.length; j++) { // discard lines not intersecting on the paper if (linesWhole[i].original.getIntersection(linesWhole[j].original) == null) continue; Segment3d intersection = linesWhole[i].getIntersection(linesWhole[j]); // if the whole lines intersect, find the intersecting subsegments and slice them if (intersection != null && intersection.getVector().epsilonEquals(new Vector3d(), EPSILON)) { Point3d intPoint = intersection.getPoint(); List<ModelSegment> lineIntersected = linesIntersected.get(i); k: for (int k = 0; k < lineIntersected.size(); k++) { ModelSegment line = lineIntersected.get(k); ModelSegment split = line.split(intPoint); if (split != null || line.isBorderPoint(intPoint)) { if (split != null) { lineIntersected.add(split); numLines++; } break k; } } List<ModelSegment> otherIntersected = linesIntersected.get(j); l: for (int l = 0; l < otherIntersected.size(); l++) { ModelSegment other = otherIntersected.get(l); ModelSegment split = other.split(intPoint); if (split != null || other.isBorderPoint(intPoint)) { if (split != null) { otherIntersected.add(split); numLines++; } break l; } } } } } UnitDimension paperSize = origami.getModel().getPaper().getSize(); double ratio = UnitHelper.convertTo(Unit.REL, Unit.M, 1, paperSize.getUnit(), paperSize.getMax()); foldLineArrays = new LineArray[numLines]; i = 0; for (List<ModelSegment> lineIntersected : linesIntersected) { for (ModelSegment line : lineIntersected) { foldLineArrays[i] = new LineArray(2, GeometryArray.COORDINATES); Point3d startPoint = new Point3d(line.getP1()); startPoint.scale(ratio); foldLineArrays[i].setCoordinate(0, startPoint); Point3d endPoint = new Point3d(line.getP2()); endPoint.scale(ratio); foldLineArrays[i].setCoordinate(1, endPoint); foldLineArrays[i].setUserData(line); i++; } } foldLineArraysDirty = false; } /** * Retrurn the line arrays corresponding to the lines on the paper. * * @return The line arrays corresponding to the lines on the paper. */ public synchronized LineArray[] getLineArrays() { if (foldLineArraysDirty) updateLineArrays(); return foldLineArrays; } /** * Call this method right after a call to {@link #revertDelayedOperations()} to get the lines arrays with 3D values * updated to correspond to the new triangles. The lines will be in the same order as they were before the * {@link #revertDelayedOperations()} call. * <p> * Beware - the call to this method must be performed right after the {@link #revertDelayedOperations()}, if you eg. * call {@link #getLineArrays()} in between, an {@link IllegalStateException} will be thrown (the implementaion * requires the cached lines array to be invalid). * <p> * This method validates the line arrays cache, so only one call will work. After the first call, subsequent calls * to {@link #getLineArrays()} will return the same result. * <p> * This method may be quite ineffective with large numbers of triangles (each of the line's ends is renewed using * {@link #locatePointFromPaperTo3D(Point2d)}, which iterates over all layers). * * @return The line arrays corresponding to the lines on the paper. * * @throws IllegalStateException If the line arrays cache is valid. */ public synchronized LineArray[] getLineArraysAfterDelayedRevertion() throws IllegalStateException { if (!foldLineArraysDirty || foldLineArrays == null || foldLineArrays.length == 0) throw new IllegalStateException( "Cannot call getLineArraysAfterDelayedRevertion on a state with valid line arrays cache."); UnitDimension paperSize = origami.getModel().getPaper().getSize(); double ratio = UnitHelper.convertTo(Unit.REL, Unit.M, 1, paperSize.getUnit(), paperSize.getMax()); LineArray[] newFoldLineArrays = new LineArray[foldLineArrays.length]; for (int i = 0; i < foldLineArrays.length; i++) { ModelSegment oldSegment = (ModelSegment) foldLineArrays[i].getUserData(); newFoldLineArrays[i] = new LineArray(2, GeometryArray.COORDINATES); Point3d startPoint = new Point3d(locatePointFromPaperTo3D(oldSegment.getOriginal().getP1())); startPoint.scale(ratio); newFoldLineArrays[i].setCoordinate(0, startPoint); Point3d endPoint = new Point3d(locatePointFromPaperTo3D(oldSegment.getOriginal().getP2())); endPoint.scale(ratio); newFoldLineArrays[i].setCoordinate(1, endPoint); ModelSegment line = new ModelSegment(new Segment3d(startPoint, endPoint), oldSegment.getOriginal(), oldSegment.getDirection(), oldSegment.getOriginatingStepId()); newFoldLineArrays[i].setUserData(line); } foldLineArraysDirty = false; return foldLineArrays = newFoldLineArrays; } /** * Update the contents of the trianglesArrays so that it corresponds to the actual model state. */ protected synchronized void updateTrianglesArrays() { trianglesArrays = new TriangleArray[layers.size()]; double oneRelInMeters = origami.getModel().getPaper().getOneRelInMeters(); int index = 0; Point3d p; for (Layer layer : layers) { trianglesArrays[index] = new TriangleArray(layer.getTriangles().size() * 3, TriangleArray.COORDINATES | TriangleArray.TEXTURE_COORDINATE_2); trianglesArrays[index].setUserData(layer); int i = 0; for (ModelTriangle triangle : layer.getTriangles()) { p = (Point3d) triangle.getP1().clone(); p.scale(oneRelInMeters); trianglesArrays[index].setCoordinate(3 * i, p); trianglesArrays[index].setTextureCoordinate(0, 3 * i, new TexCoord2f(new Point2f(triangle .getOriginalPosition().getP1()))); p = (Point3d) triangle.getP2().clone(); p.scale(oneRelInMeters); trianglesArrays[index].setCoordinate(3 * i + 1, p); trianglesArrays[index].setTextureCoordinate(0, 3 * i + 1, new TexCoord2f(new Point2f(triangle .getOriginalPosition().getP2()))); p = (Point3d) triangle.getP3().clone(); p.scale(oneRelInMeters); trianglesArrays[index].setCoordinate(3 * i + 2, p); trianglesArrays[index].setTextureCoordinate(0, 3 * i + 2, new TexCoord2f(new Point2f(triangle .getOriginalPosition().getP3()))); i++; } index++; } trianglesArraysDirty = false; } /** * Return the triangle arrays. * * @return The triangle arrays. */ public synchronized TriangleArray[] getTrianglesArrays() { if (trianglesArraysDirty) updateTrianglesArrays(); return trianglesArrays; } /** * @return The list of marker data needed for rendering. */ public synchronized List<MarkerRenderData> getMarkerRenderData() { if (markersDirty) updateMarkers(); return markerData; } /** * Recompute markers 3D positions. */ protected synchronized void updateMarkers() { for (Marker m : markers) m.setPoint3d(locatePointFromPaperTo3D(m.getPoint2d())); markersDirty = false; } /** * Performs a valley/mountain fold. * * @param direction The direction of the fold - VALLEY/MOUNTAIN. * @param startPoint Starting point of the fold (in 2D paper relative coordinates). * @param endPoint Ending point of the fold (in 2D paper relative coordinates). * @param layerFilter The filter that filters the layers this fold should be made on. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values accepted by layerFilter (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If the requested operation is invalid. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ public Map<Layer, Layer> makeFold(Direction direction, Point2d startPoint, Point2d endPoint, LayerFilter layerFilter, double angle) throws InvalidOperationException, IllegalStateException { return makeFold(direction, startPoint, endPoint, null, layerFilter, angle, null, null); } /** * Performs a valley/mountain fold. * * @param direction The direction of the fold - VALLEY/MOUNTAIN. * @param startPoint Starting point of the fold (in 2D paper relative coordinates). * @param endPoint Ending point of the fold (in 2D paper relative coordinates). * @param layerFilter The filter that filters the layers this fold should be made on. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * @param neighborTest The test for including neighbors when bending. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values accepted by layerFilter (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If the requested operation is invalid. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ public Map<Layer, Layer> makeFold(Direction direction, Point2d startPoint, Point2d endPoint, LayerFilter layerFilter, double angle, NeighborInclusionTest neighborTest) throws InvalidOperationException, IllegalStateException { return makeFold(direction, startPoint, endPoint, null, layerFilter, angle, null, neighborTest); } /** * Performs a valley/mountain fold. * * @param direction The direction of the fold - VALLEY/MOUNTAIN. * @param startPoint Starting point of the fold (in 2D paper relative coordinates). * @param endPoint Ending point of the fold (in 2D paper relative coordinates). * @param refPoint A general point in the part of the paper to be bent. Pass <code>null</code> to autocompute so * that the part with less triangles will be rotated. * @param layerFilter The filter that filters the layers this fold should be made on. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values accepted by layerFilter (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If the requested operation is invalid. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ public Map<Layer, Layer> makeFold(Direction direction, Point2d startPoint, Point2d endPoint, Point2d refPoint, LayerFilter layerFilter, double angle) throws InvalidOperationException, IllegalStateException { return makeFold(direction, startPoint, endPoint, refPoint, layerFilter, angle, null, null); } /** * Performs a valley/mountain fold. * * @param direction The direction of the fold - VALLEY/MOUNTAIN. * @param startPoint Starting point of the fold (in 2D paper relative coordinates). * @param endPoint Ending point of the fold (in 2D paper relative coordinates). * @param refPoint A general point in the part of the paper to be bent. Pass <code>null</code> to autocompute so * that the part with less triangles will be rotated. * @param layerFilter The filter that filters the layers this fold should be made on. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * @param neighborTest The test for including neighbors when bending. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values accepted by layerFilter (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If the requested operation is invalid. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ public Map<Layer, Layer> makeFold(Direction direction, Point2d startPoint, Point2d endPoint, Point2d refPoint, LayerFilter layerFilter, double angle, NeighborInclusionTest neighborTest) throws InvalidOperationException, IllegalStateException { return makeFold(direction, startPoint, endPoint, refPoint, layerFilter, angle, null, neighborTest); } /** * Performs a valley/mountain fold. * * @param direction The direction of the fold - VALLEY/MOUNTAIN. * @param startPoint Starting point of the fold (in 2D paper relative coordinates). * @param endPoint Ending point of the fold (in 2D paper relative coordinates). * @param layerFilter The filter that filters the layers this fold should be made on. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * @param foundAffectedLayers Output parameter. The map of found layers affected by this fold and corresponding * intersection segments. Pass <code>null</code> if you aren't interested in this information. Note that * the list corresponds to layers before bending, so it is practically useful only if * <code>angle == 0</code>. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values accepted by layerFilter (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If the requested operation is invalid. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ public Map<Layer, Layer> makeFold(Direction direction, Point2d startPoint, Point2d endPoint, LayerFilter layerFilter, double angle, Map<Layer, Segment3d> foundAffectedLayers) throws InvalidOperationException, IllegalStateException { return makeFold(direction, startPoint, endPoint, null, layerFilter, angle, foundAffectedLayers, null); } /** * Performs a valley/mountain fold. * * @param direction The direction of the fold - VALLEY/MOUNTAIN. * @param startPoint Starting point of the fold (in 2D paper relative coordinates). * @param endPoint Ending point of the fold (in 2D paper relative coordinates). * @param refPoint A general point in the part of the paper to be bent. Pass <code>null</code> to autocompute so * that the part with less triangles will be rotated. * @param layerFilter The filter that filters the layers this fold should be made on. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * @param foundAffectedLayers Output parameter. The map of found layers affected by this fold and corresponding * intersection segments. Pass <code>null</code> if you aren't interested in this information. Note that * the list corresponds to layers before bending, so it is practically useful only if * <code>angle == 0</code>. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values accepted by layerFilter (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If the requested operation is invalid. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ public Map<Layer, Layer> makeFold(Direction direction, Point2d startPoint, Point2d endPoint, Point2d refPoint, LayerFilter layerFilter, double angle, Map<Layer, Segment3d> foundAffectedLayers) throws InvalidOperationException, IllegalStateException { return makeFold(direction, startPoint, endPoint, refPoint, layerFilter, angle, foundAffectedLayers, null); } /** * Performs a valley/mountain fold. * * @param direction The direction of the fold - VALLEY/MOUNTAIN. * @param startPoint Starting point of the fold (in 2D paper relative coordinates). * @param endPoint Ending point of the fold (in 2D paper relative coordinates). * @param layerFilter The filter that filters the layers this fold should be made on. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * @param foundAffectedLayers Output parameter. The map of found layers affected by this fold and corresponding * intersection segments. Pass <code>null</code> if you aren't interested in this information. Note that * the list corresponds to layers before bending, so it is practically useful only if * <code>angle == 0</code>. * @param neighborTest The test for including neighbors when bending. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values accepted by layerFilter (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If the requested operation is invalid. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ public Map<Layer, Layer> makeFold(Direction direction, Point2d startPoint, Point2d endPoint, LayerFilter layerFilter, double angle, Map<Layer, Segment3d> foundAffectedLayers, NeighborInclusionTest neighborTest) throws InvalidOperationException, IllegalStateException { return makeFold(direction, startPoint, endPoint, null, layerFilter, angle, foundAffectedLayers, neighborTest); } /** * Performs a valley/mountain fold. * * @param direction The direction of the fold - VALLEY/MOUNTAIN. * @param startPoint Starting point of the fold (in 2D paper relative coordinates). * @param endPoint Ending point of the fold (in 2D paper relative coordinates). * @param refPoint A general point in the part of the paper to be bent. Pass <code>null</code> to autocompute so * that the part with less triangles will be rotated. * @param layerFilter The filter that filters the layers this fold should be made on. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * @param foundAffectedLayers Output parameter. The map of found layers affected by this fold and corresponding * intersection segments. Pass <code>null</code> if you aren't interested in this information. Note that * the list corresponds to layers before bending, so it is practically useful only if * <code>angle == 0</code>. * @param neighborTest The test for including neighbors when bending. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values accepted by layerFilter (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). Only contains entries with values * accepted by layerFilter (therefore only the "primarily" rotated layers, not those rotated because they * neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If the requested operation is invalid. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ protected Map<Layer, Layer> makeFold(Direction direction, Point2d startPoint, Point2d endPoint, Point2d refPoint, LayerFilter layerFilter, double angle, Map<Layer, Segment3d> foundAffectedLayers, NeighborInclusionTest neighborTest) throws InvalidOperationException, IllegalStateException { if (angle < -EPSILON || angle > Math.PI + EPSILON) throw new IllegalArgumentException("Cannot pass angles outside <0, PI> interval to makeFold()."); Point3d start = locatePointFromPaperTo3D(startPoint); Point3d end = locatePointFromPaperTo3D(endPoint); Point3d ref = (refPoint != null ? locatePointFromPaperTo3D(refPoint) : null); ModelSegment segment = new ModelSegment(new Segment3d(start, end), new Segment2d(startPoint, endPoint), direction, step.getId()); final LinkedHashMap<Layer, ModelSegment> layerInts = getLayers(segment); final Map<Layer, Direction> foldDirections = new HashMap<Layer, Direction>(layerInts.size()); // filter out the layers we aren't interested in if (layerFilter != null) layerFilter.filter(layerInts); // cut triangles along the segment and make the appropriate fold lines for (Entry<Layer, ModelSegment> entry : layerInts.entrySet()) makeFoldInLayer(entry.getKey(), direction, entry.getValue(), foldDirections); if (foundAffectedLayers != null) foundAffectedLayers.putAll(layerInts); // bend the paper return bendPaper(segment, ref, layerInts, angle, neighborTest, foldDirections); } /** * Bends the paper. Requires that the fold line goes only along triangle edges, not through the interiors of them. * <p> * To specify the part of the paper that will be rotated, the segment's direction vector is used. Make cross product * of the normal of the layer the segment lies in and the direction vector of the segment. The cross product points * to the part of the paper that will be moved. * * @param segment The segment to bend around. Note that the direction vector of the segment specifies which part of * the paper will be rotated. * @param layerInts A map of affected layers and intersections of the fold stripe with them. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * @param foldDirections The real directions in fold lines in layers. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values being keys in layerInts (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If no or all layers are to be rotated while angle is non-zero. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ protected Map<Layer, Layer> bendPaper(ModelSegment segment, Map<Layer, ModelSegment> layerInts, double angle, Map<Layer, Direction> foldDirections) throws InvalidOperationException, IllegalStateException { return bendPaper(segment, null, layerInts, angle, null, foldDirections); } /** * Bends the paper. Requires that the fold line goes only along triangle edges, not through the interiors of them. * <p> * To specify the part of the paper that will be rotated, the segment's direction vector is used. Make cross product * of the normal of the layer the segment lies in and the direction vector of the segment. The cross product points * to the part of the paper that will be moved. * * @param segment The segment to bend around. Note that the direction vector of the segment specifies which part of * the paper will be rotated. * @param layerInts A map of affected layers and intersections of the fold stripe with them. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * @param neighborTest A callback for narrowing the set of neighbor triangles that should be bent. Pass * <code>null</code> if you don't need this callback. * @param foldDirections The real directions in fold lines in layers. If you pass <code>null</code>, you can specify * angle < 0, and this means that you know the right angle of rotation around the given segment and no * corrections should be applied on it. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values being keys in layerInts (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If no or all layers are to be rotated while angle is non-zero. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ protected Map<Layer, Layer> bendPaper(ModelSegment segment, Map<Layer, ModelSegment> layerInts, double angle, NeighborInclusionTest neighborTest, Map<Layer, Direction> foldDirections) throws InvalidOperationException, IllegalStateException { return bendPaper(segment, null, layerInts, angle, neighborTest, foldDirections); } /** * Bends the paper. Requires that the fold line goes only along triangle edges, not through the interiors of them. * <p> * To specify the part of the paper that will be rotated, imagine a plane going through <code>segment</code> and * perpendicular to the layer <code>segment</code> lies in. This is a halfspace's border plane. Then * <code>refPoint</code> specifies the halfspace containing the parts of the paper that are to be bent. * * @param segment The segment to bend around. Note that the direction vector of the segment specifies which part of * the paper will be rotated. * @param refPoint A reference point lying in the halfspace containing the parts of the paper that are to be bent. * Pass <code>null</code> to autodetermine the part as the one with less triangles. Please ensure * <code>refPoint</code> doesn't lie in the border plane as described above, this will not be checked. * @param layerInts A map of affected layers and intersections of the fold stripe with them. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * @param foldDirections The real directions in fold lines in layers. If you pass <code>null</code>, you can specify * angle < 0, and this means that you know the right angle of rotation around the given segment and no * corrections should be applied on it. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values being keys in layerInts (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If no or all layers are to be rotated while angle is non-zero. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ protected Map<Layer, Layer> bendPaper(ModelSegment segment, Point3d refPoint, Map<Layer, ModelSegment> layerInts, double angle, Map<Layer, Direction> foldDirections) throws InvalidOperationException, IllegalStateException { return bendPaper(segment, refPoint, layerInts, angle, null, foldDirections); } /** * Bends the paper. Requires that the fold line goes only along triangle edges, not through the interiors of them. * <p> * To specify the part of the paper that will be rotated, imagine a plane going through <code>segment</code> and * perpendicular to the layer <code>segment</code> lies in. This is a halfspace's border plane. Then * <code>refPoint</code> specifies the halfspace containing the parts of the paper that are to be bent. * * @param segment The segment to bend around. Note that the direction vector of the segment specifies which part of * the paper will be rotated. * @param refPoint A reference point lying in the halfspace containing the parts of the paper that are to be bent. * Pass <code>null</code> to autodetermine the part as the one with less triangles. Please ensure * <code>refPoint</code> doesn't lie in the border plane as described above, this will not be checked. * @param layerInts A map of affected layers and intersections of the fold stripe with them. * @param angle The angle the paper should be bent by (in radians). The value must be in <0, π> interval. * @param neighborTest A callback for narrowing the set of neighbor triangles that should be bent. Pass * <code>null</code> if you don't need this callback. * @param foldDirections The real directions in fold lines in layers. If you pass <code>null</code>, you can specify * angle < 0, and this means that you know the right angle of rotation around the given segment and no * corrections should be applied on it. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values being keys in layerInts (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws IllegalArgumentException If angle isn't in interval <0, π>. * @throws InvalidOperationException If no or all layers are to be rotated while angle is non-zero. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ protected Map<Layer, Layer> bendPaper(ModelSegment segment, Point3d refPoint, Map<Layer, ModelSegment> layerInts, double angle, NeighborInclusionTest neighborTest, Map<Layer, Direction> foldDirections) throws InvalidOperationException, IllegalStateException { if (foldDirections != null && (angle < -EPSILON || angle > Math.PI + EPSILON)) throw new IllegalArgumentException("Cannot pass angles outside <0, PI> interval to bendPaper()."); if (angle < EPSILON || layerInts.size() == 0) return new HashMap<Layer, Layer>(0); Layer segLayer = null; if (foldDirections != null || refPoint == null) {// find the layer the given segment lies in for (Entry<Layer, ModelSegment> entry : layerInts.entrySet()) { Segment3d intersection = entry.getValue().getIntersection(segment); if (intersection != null && !intersection.getVector().epsilonEquals(new Vector3d(), EPSILON)) { segLayer = entry.getKey(); } } if (segLayer == null) { throw new IllegalStateException("Cannot find layer in which the segment to bend over lies."); } } Point3d r = refPoint; if (refPoint == null) { assert segLayer != null; // find a reference point by taking the cross product of the normal of the layer the segment lies in, and // the direction vector of the segment; add this vector to a point on the segment to get a general point in // the halfspace that contains the layers to be bent Vector3d layerNormalSegmentDirCross = new Vector3d(); layerNormalSegmentDirCross.cross(segLayer.getNormal(), segment.getVector()); Vector3d v = new Vector3d(layerNormalSegmentDirCross); v.normalize(); r = new Point3d(v); r.add(segment.getPoint()); } double angle1 = angle; if (foldDirections != null) {// invert the rotation angle if it doesn't correspond to the real fold direction assert segLayer != null; Direction foldDir = foldDirections.get(segLayer); // the halfspace having the layer's plane as its border and defining the half of the space where the layer's // normal points HalfSpace3d halfspace = new HalfSpace3d(segLayer.getPlane()); Point3d rotated = MathHelper.rotate(r, segment, angle); boolean isInHalfspace = halfspace.contains(rotated); // mountain fold should bend the paper "under" the top plane, whereas valley fold should bend it "over" the // top plane if (foldDir == Direction.MOUNTAIN && isInHalfspace) angle1 = -angle1; else if (foldDir == Direction.VALLEY && !isInHalfspace) angle1 = -angle1; } Vector3d segNormal = getSegmentNormal(segment); Vector3d halfSpaceNormal = new Vector3d(); halfSpaceNormal.cross(segNormal, segment.getVector()); // the halfspace that contains the layers to be bent HalfSpace3d halfspace = new HalfSpace3d(halfSpaceNormal, segment.getP1()); if (!halfspace.contains(r)) halfspace.invert(); // further we will need to search in layerInts, but the layers will probably change, so we backup the old // removed layers here HashMap<Layer, Layer> newLayersToOldOnes = new HashMap<Layer, Layer>(); // fill this queue with triangles from the new layers that need to be bent Queue<ModelTriangle> queue = new LinkedList<ModelTriangle>(); for (Entry<Layer, ModelSegment> layerInt : layerInts.entrySet()) { Layer layer = layerInt.getKey(); ModelSegment splitSegment = layerInt.getValue(); List<Polygon3d<ModelTriangle>> part1 = new LinkedList<Polygon3d<ModelTriangle>>(); List<Polygon3d<ModelTriangle>> part2 = new LinkedList<Polygon3d<ModelTriangle>>(); // split the layer into at least two new ones layer.splitPolygon(splitSegment, part1, part2); // we want to have the layers to be bent in part1, so we will maybe need to swap them boolean swapParts = false; if (part1.size() > 0) { Triangle3d part1t = part1.get(0).getTriangles().iterator().next(); if (!(halfspace.contains(part1t.getP1()) && halfspace.contains(part1t.getP2()) && halfspace .contains(part1t.getP3()))) { swapParts = true; } } else { Triangle3d part2t = part2.get(0).getTriangles().iterator().next(); if (!(halfspace.contains(part2t.getP1()) && halfspace.contains(part2t.getP2()) && halfspace .contains(part2t.getP3()))) { swapParts = true; } } if (part1.size() == 0 && part2.size() > 0) // can happen if bending already bent folds swapParts = true; if (swapParts) { List<Polygon3d<ModelTriangle>> tmp = part1; part1 = part2; part2 = tmp; } // remove the old layer and add the new ones this.layers.remove(layer); for (Polygon3d<ModelTriangle> l : part1) { if (l.getTriangles().size() > 0) { Layer newL = new Layer(l); this.layers.add(newL); newLayersToOldOnes.put(newL, layer); } } for (Polygon3d<ModelTriangle> l : part2) { if (l.getTriangles().size() > 0) { Layer newL = new Layer(l); this.layers.add(newL); newLayersToOldOnes.put(newL, layer); } } // add triangles from layers from part1 to the queue for (Polygon3d<ModelTriangle> l : part1) { queue.addAll(l.getTriangles()); } } if (queue.isEmpty()) { throw new InvalidOperationException("bend.paper.nothing.to.bend"); } // to find all triangles that have to be rotated, first add all triangles in "affected" layers that lie in the // right halfspace, and then go over neighbors of all found triangles to rotate and add them, if the neighbor // doesn't lie on an opposite side of a fold line. // another constraint for not adding some triangles to trianglesToRotate can be provided in neighborTest Set<ModelTriangle> inQueue = new HashSet<ModelTriangle>(queue); Set<ModelTriangle> trianglesToRotate = new HashSet<ModelTriangle>(); ModelTriangle t; while ((t = queue.poll()) != null) { if (!trianglesToLayers.containsKey(t)) throw new IllegalStateException("Cannot find layer for triangle " + t); trianglesToRotate.add(t); // border is the intersection line in the layer of the processed triangle - if the triangle lies in a layer // without intersection line, it can be surely added to the queue Segment3d border = layerInts.get(trianglesToLayers.get(t)); if (border == null) { // this is assumed to be true Layer oldLayer = newLayersToOldOnes.get(trianglesToLayers.get(t)); if (oldLayer != null) border = layerInts.get(oldLayer); } List<ModelTriangle> neighbors = findNeighbors(t); n: for (ModelTriangle n : neighbors) { if (inQueue.contains(n)) continue; if (border != null) { Segment3d intWithNeighbor = t.getCommonEdge(n, false); // if the common edge between t and n is a part of the border line, we need to check if n lies in // the processed halfspace; if not, it is "on the other side" of the border line, so we don't want // to add it to the queue if (intWithNeighbor != null && intWithNeighbor.overlaps(border)) { if (!halfspace.contains(n.getP1()) || !halfspace.contains(n.getP2()) || !halfspace.contains(n.getP3())) { continue n; } else if (neighborTest != null && !neighborTest.includeNeighbor(t, n)) { // or neighborTest can also discard some triangles from being added to trianglesToRotate continue n; } } else if (neighborTest != null && !neighborTest.includeNeighbor(t, n)) { // or neighborTest can also discard some triangles from being added to trianglesToRotate continue n; } } // else - if no border line lies in t's layer, we automatically want to add all of its neighbors queue.add(n); inQueue.add(n); } } if (trianglesToRotate.size() == triangles.size()) { throw new InvalidOperationException("bend.paper.whole.model.rotation"); } Set<Layer> layersToRotate = new HashSet<Layer>(); // find a set of layers that contain all the triangles to be rotated for (ModelTriangle tr : trianglesToRotate) { layersToRotate.add(trianglesToLayers.get(tr)); } if (layersToRotate.size() == layers.size()) { throw new InvalidOperationException("bend.paper.whole.model.rotation"); } // HEURISTIC: If we have guessed refPoint, and we find out that more than a half of all triangles should be // rotated, we rotate the rest of the layers (so that the bigger part of paper will always stay unrotated). // However, we cannot use this heuristic if a neighbor inclusion test is specified! if (refPoint == null && neighborTest == null) { if (trianglesToRotate.size() > triangles.size() / 2) { Set<Layer> newLayersToRotate = new HashSet<Layer>(layers.size() - layersToRotate.size()); for (Layer l : layers) if (!layersToRotate.contains(l)) newLayersToRotate.add(l); layersToRotate = newLayersToRotate; angle1 = -angle1; } } if (furthestRotationCenter != null) { double max = (furthestRotatedPoint != null ? furthestRotatedPoint.distance(furthestRotationCenter) : 0); for (Layer l : layersToRotate) { for (ModelTriangle tr : l.getTriangles()) { int i = 0; for (Point3d p : tr.getVertices()) { double dist = p.distance(furthestRotationCenter); if (dist > max) { max = dist; furthestRotatedPoint = new ModelPoint(p, tr.getOriginalPosition().getVertices()[i]); } i++; } } } } if (furthestRotationSegment != null) { double max = (furthestRotatedPointAroundSegment != null ? furthestRotationSegment .distance(furthestRotatedPointAroundSegment) : 0); for (Layer l : layersToRotate) { for (ModelTriangle tr : l.getTriangles()) { int i = 0; for (Point3d p : tr.getVertices()) { double dist = furthestRotationSegment.distance(p); if (dist > max) { max = dist; furthestRotatedPointAroundSegment = new ModelPoint(p, tr.getOriginalPosition() .getVertices()[i]); } i++; } } } } Map<Layer, Layer> result = new HashMap<Layer, Layer>(layersToRotate.size()); for (Layer l : layersToRotate) { // remove, rotate, and then add the triangles back to make sure all caches and maps will hold the correct // value if (l == null) continue; triangles.removeAll(l.getTriangles()); l.rotate(segment, angle1); triangles.addAll(l.getTriangles()); Layer oldLayer = newLayersToOldOnes.get(l); if (layerInts.keySet().contains(oldLayer)) result.put(l, oldLayer); } return result; } /** * Make a reverse fold and bend the paper correspondingly. * <p> * If <code>line</code> and <code>refLine</code> are perpendicular AND <code>refLine</code> doesn't start or end on * <code>line</code>, then the part to be rotated is determined by the start point of <code>refLine</code>. * * @param direction Mountain means inside fold, valley means outside fold. * @param line The line to bend along. * @param oppositeLine The second line to fold along. Pass <code>null</code> to autocompute. If you do so, you must * also pass <code>null</code> to <code>oppositeLayerFilter</code>. * @param refLine The line around which the paper will twist (you'd press on this line with your finger when doing * the fold manually). * @param layerFilter The filter that filters the layers the fold along <code>line</code> should be made on. * @param oppositeLayerFilter The filter that filters the layers the fold along <code>oppositeLine</code> should be * made on. Pass <code>null</code> to autocompute. If you do so, you must also pass <code>null</code> to * <code>oppositeLine</code>. * * @return The layers of the paper that were rotated. The first list item containing the first layers, the second * list item containing the opposite layers. Every entry has the same meaning as the return value of * {@link #makeFold(Direction, Point2d, Point2d, LayerFilter, double, boolean)}. * * @throws InvalidOperationException If the assumptions for doing this fold aren't satisfied. */ public List<Map<Layer, Layer>> makeReverseFold(Direction direction, Segment2d line, Segment2d oppositeLine, Segment2d refLine, LayerFilter layerFilter, LayerFilter oppositeLayerFilter) throws InvalidOperationException { return makeReverseFold(direction, line, oppositeLine, refLine, layerFilter, oppositeLayerFilter, null); } /** * Make a reverse fold and bend the paper correspondingly. * <p> * If <code>line</code> and <code>refLine</code> are perpendicular AND <code>refLine</code> doesn't start or end on * <code>line</code>, then the part to be rotated is determined by the start point of <code>refLine</code>. * * @param direction Mountain means inside fold, valley means outside fold. * @param line The line to bend along. * @param oppositeLine The second line to fold along. Pass <code>null</code> to autocompute. If you do so, you must * also pass <code>null</code> to <code>oppositeLayerFilter</code>. * @param refLine The line around which the paper will twist (you'd press on this line with your finger when doing * the fold manually). * @param layerFilter The filter that filters the layers the fold along <code>line</code> should be made on. * @param oppositeLayerFilter The filter that filters the layers the fold along <code>oppositeLine</code> should be * made on. Pass <code>null</code> to autocompute. If you do so, you must also pass <code>null</code> to * <code>oppositeLine</code>. * @param newRefLine Output parameter. This line will be set to the part of refLine that was bent. If * <code>null</code>, this won't be set. * * @return The layers of the paper that were rotated. The first list item containing the first layers, the second * list item containing the opposite layers. Every entry has the same meaning as the return value of * {@link #makeFold(Direction, Point2d, Point2d, LayerFilter, double, boolean)}. * * @throws InvalidOperationException If the assumptions for doing this fold aren't satisfied. */ public List<Map<Layer, Layer>> makeReverseFold(Direction direction, Segment2d line, Segment2d oppositeLine, Segment2d refLine, LayerFilter layerFilter, LayerFilter oppositeLayerFilter, Segment2d newRefLine) throws InvalidOperationException { Segment3d line3 = new Segment3d(locatePointFromPaperTo3D(line.getP1()), locatePointFromPaperTo3D(line.getP2())); Segment3d refLine3 = new Segment3d(locatePointFromPaperTo3D(refLine.getP1()), locatePointFromPaperTo3D(refLine.getP2())); // check if line and refLine have exactly one common point in 3D Segment3d intPoint = line3.getIntersection(refLine3); if (intPoint == null || !intPoint.getVector().epsilonEquals(new Vector3d(), EPSILON)) { throw new InvalidOperationException("line.and.ref.line.must.intersect.in.exactly.one.point", intPoint == null ? "null" : intPoint.toStringAsIntersection()); } Segment2d oppositeLine2; Segment3d oppositeLine3; LayerFilter oppositeLayerFilter2; if (oppositeLine == null) { // neither oppositeLine nor oppositeAffectedLayer were specified, so we must compute them // line and refLine don't have to intersect on the paper, so we can treat them as lines oppositeLine2 = new Segment2d(new Line2d(refLine).mirror(new Line2d(line))); // check if the oppositeLine lies in the paper if (!getOrigami().getModel().getPaper().containsRelative(oppositeLine2.getP1()) || !getOrigami().getModel().getPaper().containsRelative(oppositeLine2.getP2())) { throw new InvalidOperationException("opposite.line.couldnt.be.found"); } oppositeLine3 = new Segment3d(locatePointFromPaperTo3D(oppositeLine2.getP1()), locatePointFromPaperTo3D(oppositeLine2.getP2())); final LinkedHashMap<Layer, ModelSegment> layerInts = getLayers(new ModelSegment(line3, line, null, 0)); layerFilter.filter(layerInts); oppositeLayerFilter2 = getOppositeLineLayerFilter(new ModelSegment(line3, line, null, 0), new ModelSegment( oppositeLine3, oppositeLine2, null, 0), layerInts.keySet()); } else { oppositeLine2 = oppositeLine; oppositeLayerFilter2 = oppositeLayerFilter; oppositeLine3 = new Segment3d(locatePointFromPaperTo3D(oppositeLine2.getP1()), locatePointFromPaperTo3D(oppositeLine2.getP2())); Segment3d intersection = oppositeLine3.getIntersection(line3); if (intersection == null) { throw new InvalidOperationException("line.and.opposite.line.dont.intersect"); } else { Segment3d intersection2 = refLine3.getIntersection(intersection); if (intersection2 == null) { throw new InvalidOperationException("line.and.opposite.line.dont.intersect.on.refline"); } else if (!intersection2.getVector().epsilonEquals(new Vector3d(), EPSILON)) { throw new InvalidOperationException("line.and.opposite.line.cant.be.parallel.to.refline"); } } } // ________________lineP...oppositeP_____________................. // ................l/|........|\o....................O.\......../.A // ...............i/.|........|.\p....................N.\....../.N // .ONE.SIDE.....n/..|........|..\p..ANOTHER.SIDE......E.\..../.O // .............e/...|........|...\o......................\../.T // _____________/____|........|____\s____________.......S..\/.H. // ............^...refLine.refLine..^...................IDE...ER.SIDE // ............|....................| // ..........common..............common Point3d lineP = line3.getP1(); Point3d common = line3.getP2(); if (!oppositeLine3.contains(common)) { lineP = line3.getP2(); common = line3.getP1(); } Point3d oppositeP = oppositeLine3.getP1(); if (oppositeP.epsilonEquals(common, EPSILON)) oppositeP = oppositeLine3.getP2(); Vector3d lineP_oppositeP = new Vector3d(oppositeP); lineP_oppositeP.sub(lineP); if (lineP_oppositeP.epsilonEquals(new Vector3d(), EPSILON)) // line and opposite are equal, so we need another way to determine the dividing plane's normal // TODO this may not be sufficient handling of this corner case lineP_oppositeP.cross(line3.getVector(), refLine3.getVector()); Direction dir = direction, oppositeDir = direction; {// determine the real from-screen direction of the folds // lineLayerNormal and its opposite should point "outside" the triangle defined by common, lineP and oppositeP Vector3d lineLayerNormal = getSegmentNormal(new ModelSegment(line3, line)); Vector3d oppositeLineLayerNormal = getSegmentNormal(new ModelSegment(oppositeLine3, oppositeLine2)); if (lineLayerNormal == null) { throw new InvalidOperationException("reverse.fold.no.line.normal"); } if (oppositeLineLayerNormal == null) { throw new InvalidOperationException("reverse.fold.no.opposite.line.normal"); } if (lineLayerNormal.angle(lineP_oppositeP) < Math.PI / 2d - EPSILON) lineLayerNormal.negate(); if (oppositeLineLayerNormal.angle(lineP_oppositeP) > Math.PI / 2d + EPSILON) oppositeLineLayerNormal.negate(); if (lineLayerNormal.angle(getScreenNormal()) > Math.PI / 2d + EPSILON) dir = dir.getOpposite(); if (oppositeLineLayerNormal.angle(getScreenNormal()) > Math.PI / 2d + EPSILON) oppositeDir = oppositeDir.getOpposite(); } // refLineEnd is the border point of refLine that will be rotated Point3d refLineEnd; Point2d refLineEnd2; if (refLine3.getP1().epsilonEquals(common, EPSILON) || refLine3.getP2().epsilonEquals(common, EPSILON)) { if (refLine3.getP1().epsilonEquals(common, EPSILON)) { refLineEnd = refLine3.getP2(); refLineEnd2 = refLine.getP2(); } else { refLineEnd = refLine3.getP1(); refLineEnd2 = refLine.getP1(); } } else { double angle = new Segment3d(common, lineP).getVector().angle(refLine3.getVector()); double halfPI = Math.PI / 2d; if (abs(angle - halfPI) > EPSILON) { if ((direction == Direction.MOUNTAIN && angle > halfPI + EPSILON) || (direction == Direction.VALLEY && angle < halfPI - EPSILON)) { refLineEnd = refLine3.getP1(); refLineEnd2 = refLine.getP1(); } else { refLineEnd = refLine3.getP2(); refLineEnd2 = refLine.getP2(); } } else { refLineEnd = refLine3.getP1(); refLineEnd2 = refLine.getP1(); } } if (newRefLine != null) { newRefLine.set(refLineEnd2, refLine.getPointForParameter(refLine3.getParameterForPoint(common))); } // dividing plane goes along refLine and halves the space between lineP and oppositeP Plane3d dividingPlane = new Plane3d(lineP_oppositeP, refLine3.getP1()); // this halfspace just serves to be able to distinguish layers that correspond to line from that corresponding // to opposite final HalfSpace3d halfspace = new HalfSpace3d(dividingPlane); // this test determines if both triangles lie in the same halfspace "around" refLine NeighborInclusionTest neighborTest = new NeighborInclusionTest() { @Override public boolean includeNeighbor(ModelTriangle t, ModelTriangle n) { return (halfspace.contains(t.getP1()) && halfspace.contains(t.getP2()) && halfspace.contains(t.getP3())) == (halfspace .contains(n.getP1()) && halfspace.contains(n.getP2()) && halfspace.contains(n.getP3())); } }; // DETERMINING THE ROTATION ANGLE // to determine the angle of rotation we find the image of refLineEnd in a "plane symmetry" through the plane // defined by lineP, oppositeP and common; once we know this image, we can simply compute the angle between // the source and the image around <line> and we've finished (we don't need to compute the angle of rotation // around <opposite>, it will be the same) Plane3d mirrorPlane; try { mirrorPlane = new Plane3d(lineP, oppositeP, common); } catch (IllegalArgumentException e) { // lineP, oppositeP and common don't form a triangle, so both parts of the paper are parallel Vector3d mirrorPlaneDirection = new Vector3d(); mirrorPlaneDirection.cross(line3.getVector(), refLine3.getVector()); mirrorPlaneDirection.add(common); mirrorPlane = new Plane3d(lineP, common, new Point3d(mirrorPlaneDirection)); } Vector3d mirrorPlaneNormal = new Vector3d(mirrorPlane.getNormal()); mirrorPlaneNormal.normalize(); Line3d mirrorLine = new Line3d(refLineEnd, mirrorPlaneNormal); Point3d mirrorPoint = mirrorPlane.getIntersection(mirrorLine).getPoint(); Vector3d refLineEnd_mirrorPoint = new Vector3d(mirrorPoint); refLineEnd_mirrorPoint.sub(refLineEnd); // this is the desired image Point3d mirroredRefLineEnd = new Point3d(mirrorPoint); mirroredRefLineEnd.add(refLineEnd_mirrorPoint); Point3d nearestLinePoint = line3.getNearestPoint(refLineEnd); Vector3d v1 = new Vector3d(refLineEnd); v1.sub(nearestLinePoint); Vector3d v2 = new Vector3d(mirroredRefLineEnd); v2.sub(nearestLinePoint); // and here we have the angle double angle = v1.angle(v2); List<Map<Layer, Layer>> result = new LinkedList<Map<Layer, Layer>>(); result.add(makeFold(dir, line.getP1(), line.getP2(), refLineEnd2, layerFilter, angle, null, neighborTest)); // create the opposite fold result.add(makeFold(oppositeDir, oppositeLine2.getP1(), oppositeLine2.getP2(), refLineEnd2, oppositeLayerFilter2, angle, null, neighborTest)); return result; } /** * Pull (unfold) the existing fold given by start and end. Rotate the part of paper defined by refPoint. Rotate only * layers satisfying layerFilter. * * @param start The start point of the fold. * @param end The end point of the fold. * @param refPoint The reference point which selects the halfspace containing the layers to bend. * @param layerFilter The filter that selects only those layers this operation is interested in. * * @return The layers of the paper that were rotated as keys, the old layers they are part of as values. Only * contains entries with values accepted by layerFilter (therefore only the "primarily" rotated layers, not * those rotated because they neighbor to a already rotated layer). * * @throws InvalidOperationException If the requested operation is invalid. * @throws IllegalStateException If this operation cannot find a layer for a triangle. */ public Map<Layer, Layer> pullFold(Point2d start, Point2d end, Point2d refPoint, LayerFilter layerFilter) throws InvalidOperationException, IllegalStateException { Point3d start3 = locatePointFromPaperTo3D(start); Point3d end3 = locatePointFromPaperTo3D(end); ModelSegment segment = new ModelSegment(new ModelPoint(start3, start), new ModelPoint(end3, end)); List<Layer> segmentLayers = getSegmentLayers(segment); List<Layer> layersWithDifferentNormals = new ArrayList<Layer>(2); for (Layer l : segmentLayers) { Vector3d normal = l.getNormal(); for (Layer found : layersWithDifferentNormals) { if (normal.angle(found.getNormal()) < 100 * EPSILON) { break; } } layersWithDifferentNormals.add(l); } double angle; if (layersWithDifferentNormals.size() == 1) { // need to check if we don't try to pull an already unfold fold Vector3d hsNormal = new Vector3d(); hsNormal.cross(layersWithDifferentNormals.get(0).getNormal(), segment.getVector()); HalfSpace3d hs = new HalfSpace3d(hsNormal, start3); Boolean isInHalfspace = null; for (Layer l : segmentLayers) { for (ModelTriangle t : l.getTriangles()) { for (Point3d p : t.getVertices()) { double dist = hs.getPlane().signedDistance(p); if (Math.abs(dist) <= 100 * EPSILON) continue; if (isInHalfspace == null) { isInHalfspace = hs.containsExclusive(p); } else if (isInHalfspace != hs.containsExclusive(p)) { throw new InvalidOperationException("trying.to.pull.narrow.fold"); } } } } angle = Math.PI; } else if (layersWithDifferentNormals.size() == 0) { throw new InvalidOperationException("pull.invalid.layers.selected"); } else { // there are at least 2 directions, we need a more complicated solution if (layersWithDifferentNormals.size() > 2) { double maxAngle = 0; Layer maxL1 = null; Layer maxL2 = null; int i = 1; for (Layer l1 : layersWithDifferentNormals) { if (i + 1 < layersWithDifferentNormals.size()) { for (Layer l2 : layersWithDifferentNormals.subList(i, layersWithDifferentNormals.size())) { double l1l2angle = l1.getNormal().angle(l2.getNormal()); if (l1l2angle > maxAngle) { maxAngle = l1l2angle; maxL1 = l1; maxL2 = l2; } } i++; } } layersWithDifferentNormals.clear(); layersWithDifferentNormals.add(maxL1); layersWithDifferentNormals.add(maxL2); } // now we have two layers with different normals Layer l1 = layersWithDifferentNormals.get(0); Layer l2 = layersWithDifferentNormals.get(1); angle = l1.getNormal().angle(l2.getNormal()); Vector3d l1Direction = new Vector3d(), l2Direction = new Vector3d(); l1Direction.cross(l1.getNormal(), segment.getVector()); l2Direction.cross(l2.getNormal(), segment.getVector()); l1Direction.normalize(); l2Direction.normalize(); HalfSpace3d hs1 = new HalfSpace3d(l1Direction, segment.getP1()), hs2 = new HalfSpace3d(l2Direction, segment.getP1()); if (!hs1.containsTriangles(l1.getTriangles())) l1Direction.negate(); if (!hs2.containsTriangles(l2.getTriangles())) l2Direction.negate(); Vector3d segNormal = getSegmentNormal(segment); Vector3d dividingNormal = new Vector3d(); dividingNormal.cross(segNormal, segment.getVector()); HalfSpace3d dividingHS = new HalfSpace3d(dividingNormal, segment.getP1()); if (!dividingHS.contains(locatePointFromPaperTo3D(refPoint))) dividingHS.invert(); Vector3d rotatedDirection, fixedDirection; if (dividingHS.containsTriangles(l1.getTriangles())) { rotatedDirection = l1Direction; fixedDirection = l2Direction; } else { rotatedDirection = l2Direction; fixedDirection = l1Direction; } Vector3d oppositeDirection = new Vector3d(fixedDirection); oppositeDirection.negate(); Point3d fixedPoint = new Point3d(segment.getP1()), rotatedPoint = new Point3d(segment.getP1()), oppositePoint = new Point3d( segment.getP1()); fixedPoint.add(fixedDirection); rotatedPoint.add(rotatedDirection); oppositePoint.add(oppositeDirection); double[] anglesToTry = new double[] { angle, Math.PI - angle, -angle, angle - Math.PI }; double minDistance = Double.MAX_VALUE; for (double a : anglesToTry) { Point3d r = MathHelper.rotate(rotatedPoint, segment, a); double distance = r.distance(oppositePoint); if (distance < minDistance) { minDistance = distance; angle = a; } } } Map<Layer, ModelSegment> layers = getLayers(segment); if (layerFilter != null) layerFilter.filter(layers); return bendPaper(segment, locatePointFromPaperTo3D(refPoint), layers, angle, null); } /** * A call to this method will revert all triangle rotations made by paper bending in this step. This can be used to * retrieve a model state without delayed operations from a model state with delayed operations. Note that you * shouldn't base any other computations on this model state after a call to this method because it is globally * invalid. */ public void revertDelayedOperations() { this.triangles.clear(); for (Layer l : layers) { List<ModelTriangle> triangles = new LinkedList<ModelTriangle>(l.getTriangles()); // we need to construct the hash set because removeTriangles can alter the given argument l.removeTriangles(new HashSet<ModelTriangle>(triangles)); for (ModelTriangle t : triangles) { t.setPoints((Point3d) t.beforeRotation.getP1().clone(), (Point3d) t.beforeRotation.getP2().clone(), (Point3d) t.beforeRotation.getP3().clone()); } l.addTriangles(triangles); this.triangles.addAll(triangles); } } /** * Check if the paper meets all the physical constraints (it doesn't tear or intersect). * <p> * Although this check runs asymptotically in O(n^2), where n is the number of layers, it should be practically * quite fast. * * @throws PaperStructureException If the paper structure is invalid. */ public void checkPaperPhysicalConstraints() throws PaperStructureException { // check for paper tear // just walk through all triangles, take their 2D neighbors and check if their common edges are almost equal in // 3D for (ModelTriangle t : triangles) { for (ModelTriangle n : t.getNeighbors()) { ModelTriangleEdge[] common2dEdge = t.getCommonEdge2d(n, false); if (common2dEdge != null) { Segment3d tEdge = common2dEdge[0].getSegment3d(); Segment3d nEdge = common2dEdge[1].getSegment3d(); if (abs(common2dEdge[0].getSegment2d().getLength() - common2dEdge[1].getSegment2d().getLength()) < 100 * EPSILON) { double[] distances = new double[] { tEdge.getP1().distance(nEdge.getP1()), tEdge.getP2().distance(nEdge.getP1()), tEdge.getP1().distance(nEdge.getP2()), tEdge.getP2().distance(nEdge.getP2()) }; Arrays.sort(distances); // now distances should contain distances between the common edges' border points, sorted, so, // if the edges are almost equal, two of the distances must be very small if (distances[1] > 100000 * EPSILON) throw new PaperTearException(t, n); } else { // this branch shouldn't be taken because we try to not have triangles with vertices in the // interior of another triangle's edge if (!tEdge.overlaps(nEdge)) throw new PaperTearException(t, n); } } else { Logger.getLogger(getClass()).warn( "ModelState#isModelPhysicallyCorrect(): neighboring triangles don't have common 2D segment. t=" + t + ", n = " + n); } } } // check for paper intersection // 1) this algorithm tests mutually all non-parallel layers // 2) the intersection line of the layers' planes is taken and it defines one halfspace for each layer // 3) find intersections of the two layers with their intersection line and note the intersection segments // 4a) if the intersection segments don't overlap, the layers don't intersect // 4b) otherwise constrain the halfspace by a stripe defined by the other layer's intersection segment // 5) test if the layer's triangles are only on one side of the halfspace - if not, the layers intersect (don't // test points outside the constraining stripe) ModelSegment[] layerInts = new ModelSegment[2]; Vector3d[] stripeDirections = new Vector3d[] { new Vector3d(), new Vector3d() }; Stripe3d[] stripeConstraints = new Stripe3d[2]; int i = 0; for (Layer l : layers) { if (i + 1 < layers.size()) { for (Layer l2 : layers.subList(i + 1, layers.size())) { double angle = l.getNormal().angle(l2.getNormal()); // the threshold serves as EPSILON, but is scaled the bigger the shorter the normals are (note that // we don't normalize plane normals) - to compensate the inaccuracy of computing angles for very // short vectors. double threshold = 100 * EPSILON * (1d / Math.min(l.getNormal().length(), l2.getNormal().length())); if (angle <= threshold || angle >= Math.PI - threshold) // don't check parallel layers continue; Line3d line = l.getPlane().getIntersection(l2.getPlane()); if (line == null) // shouldn't be null, because normals are different continue; layerInts[0] = l.getIntersectionSegment(line); if (layerInts[0] == null || layerInts[0].isSinglePoint()) // the line doesn't intersect the layer continue; layerInts[1] = l2.getIntersectionSegment(line); if (layerInts[1] == null || layerInts[1].isSinglePoint()) // the line doesn't intersect the layer continue; // taking only the intersection segment made by joining start of the first and end of the last // intersection subsegment may cause false-negatives for non-convex polygons; for simplicity, we // omit this // if the layers don't have a common part on the intersection line, continue if (!layerInts[0].overlaps(layerInts[1])) continue; stripeDirections[0].cross(l.getNormal(), line.getVector()); stripeDirections[1].cross(l2.getNormal(), line.getVector()); stripeDirections[0].normalize(); stripeDirections[1].normalize(); // note that we access layerInts[1] for stripeConstraints[0] and vice versa, which is correct! stripeConstraints[0] = new Stripe3d(new Line3d(layerInts[1].getP1(), stripeDirections[0]), new Line3d(layerInts[1].getP2(), stripeDirections[0])); stripeConstraints[1] = new Stripe3d(new Line3d(layerInts[0].getP1(), stripeDirections[1]), new Line3d(layerInts[0].getP2(), stripeDirections[1])); int j = 0; for (Layer layer : new Layer[] { l, l2 }) { Vector3d n = layer.getNormal(); Vector3d hsNormal = new Vector3d(); hsNormal.cross(n, line.getVector()); HalfSpace3d hs = new HalfSpace3d(hsNormal, line.getPoint()); Boolean inHalfspace = null; for (ModelTriangle t : layer.getTriangles()) { for (Point3d p : t.getVertices()) { double dist = hs.getPlane().signedDistance(p); // don't test points lying in the border plane if (Math.abs(dist) <= 100000 * EPSILON) continue; // if the point isn't in the projection of the other layer onto this one, continue if (!stripeConstraints[j].contains(p)) continue; if (inHalfspace == null) { inHalfspace = dist > 100000 * EPSILON; } else if (inHalfspace != dist > 100000 * EPSILON) { throw new PaperIntersectionException(l, l2); } } } j++; } } } i++; } } /** * Add a textual marker bound to a point on the paper. * * @param point The point the marker should "stick" to. * @param text The text to display. * @param stepsToHide Number of steps this marker should be displayed during. */ public void addMarker(Point2d point, String text, int stepsToHide) { Marker marker = new Marker(text, locatePointFromPaperTo3D(point), point, stepsToHide); markers.add(marker); } /** * Returns a list of triangles having a common point with the given triangle. * * @param t The triangle to find neighbors to. * @return The list of neighbors of t. */ protected List<ModelTriangle> findNeighbors(ModelTriangle triangle) { return triangle.getNeighbors(); } /** * Returns a list of triangles having a common point with the given triangle. * * @param t The triangle to find neighbors to. * @return The list of neighbors of t. */ protected List<Triangle2d> findNeighbors(Triangle2d triangle) { final List<ModelTriangle> list = paperToSpaceTriangles.get(triangle).getNeighbors(); List<Triangle2d> result = new LinkedList<Triangle2d>(); for (ModelTriangle t : list) result.add(t.getOriginalPosition()); return result; } /** * <p> * Returns a sorted map of layers defined by the given segment. * </p> * * <p> * <i>A layer is a part of the paper surrounded by either fold lines or paper boundaries.</i> * </p> * * <p> * This function returns the layers that intersect with a stripe defined by the given segment and that has the * direction of the average normal of the segment (see {@link #getSegmentNormal(ModelSegment)}). * </p> * * <p> * The list is sorted in the order the layers intersect with the stripe. * </p> * * <p> * The very first layer is the one that has its intersection the furthest in the direction of the normal of the * layer the segment lies in. * </p> * * @param segment The segment we search layers for. * @return A list of layers defined by the given line and the intersections with the fold stripe with those layers. */ public LinkedHashMap<Layer, ModelSegment> getLayers(final ModelSegment segment) { // find the segment's average normal final Vector3d segNormal = getSegmentNormal(segment); if (segNormal == null) return new LinkedHashMap<Layer, ModelSegment>(); // find another layers: is done by creating a stripe pointing in the found normal's direction and finding // intersections of the stripe with triangles final Line3d p1line = new Line3d(segment.getP1(), segNormal); final Line3d p2line = new Line3d(segment.getP2(), segNormal); final Stripe3d stripe = new Stripe3d(p1line, p2line); return getLayers(stripe); } /** * <p> * Returns a sorted map of layers intersecting the given stripe. * </p> * * <p> * <i>A layer is a part of the paper surrounded by either fold lines or paper boundaries.</i> * </p> * * <p> * The list is sorted in the order the layers intersect with the stripe. * </p> * * <p> * The very first layer is the one that has its intersection the furthest in the direction of the normal of the * layer the segment lies in. * </p> * * @param stripe The stripe we search layers for. * @return A list of layers intersecting with the given stripe and their intersections with it. */ protected LinkedHashMap<Layer, ModelSegment> getLayers(final Stripe3d stripe) { final LinkedHashMap<Layer, ModelSegment> result = new LinkedHashMap<Layer, ModelSegment>(); for (Layer l : layers) { final ModelSegment intSegment = l.getIntersectionSegment(stripe); if (intSegment == null || intSegment.getVector().epsilonEquals(new Vector3d(), MathHelper.EPSILON)) { continue; } else { result.put(l, intSegment); } } final List<Layer> keys = new ArrayList<Layer>(result.keySet()); // sort the layers along the stripe // we assume that all the layers intersect with the stripe, so it's no problem to sort the layers by their // intersections with one of the stripe's border line Collections.sort(keys, new Comparator<Layer>() { @Override public int compare(Layer o1, Layer o2) { final Line3d int1 = o1.getPlane().getIntersection(stripe.getLine1()); final Line3d int2 = o2.getPlane().getIntersection(stripe.getLine1()); // we can assume int1 and int2 to be regular points, because intersections with layers parallel to the // stripe are discarded // nevertheless we do the null checks here to avoid NPEs due to rounding errors if (int1 == null) { if (int2 == null) return 0; return -1; } else if (int2 == null) { return 1; } double t1 = stripe.getLine1().getParameterForPoint(int1.getPoint()); double t2 = stripe.getLine1().getParameterForPoint(int2.getPoint()); return (t1 - t2 > EPSILON) ? 1 : (t1 - t2 < -EPSILON ? -1 : 0); } }); final LinkedHashMap<Layer, ModelSegment> sortedResult = new LinkedHashMap<Layer, ModelSegment>(result.size()); for (Layer l : keys) { sortedResult.put(l, result.get(l)); } return sortedResult; } /** * Return the layers the given segment lies in. * * @param segment The segment to get layers for. * @return The layers the given segment lies in. */ protected List<Layer> getSegmentLayers(ModelSegment segment) { List<Layer> result = new LinkedList<Layer>(); // find if any layer contains the given point, remember the containing layers for (Layer l : layers) { ModelSegment intersection = l.getIntersectionSegment(segment); if (intersection != null && intersection.overlaps(segment) && !intersection.getIntersection(segment).isSinglePoint()) { result.add(l); } } return result; } /** * Return the average normal of all layers the given point lies in. * <p> * If the average normal is a zero vector, return the normal of any layer the point lies in. * <p> * If the point doesn't lie in any layer, the normal cannot be detected, so <code>null</code> will be returned and a * warning will be issued by this class' logger. * <p> * The average normal is determined as angle weighted normal, described in: G. Thurmer, C. A. Wuthrich, * "Computing vertex normals from polygonal facets" Journal of Graphics Tools, 3 1998 * * @param point The point to find normal for. * @return The normal at the point, or <code>null</code> if the point doesn't lie in any of the layers. */ public Vector3d getNormalAtPoint(ModelPoint point) { Vector3d anyNormal = null; List<Layer> containingLayers = new LinkedList<Layer>(); // find if any layer contains the given point, remember the containing layers for (Layer l : layers) { if (l.contains(point)) { containingLayers.add(l); if (anyNormal == null) { anyNormal = new Vector3d(l.getNormal()); } } } if (anyNormal == null) { Logger.getLogger(getClass()).warn("getNormalAtPoint: cannot find layer for point " + point); return null; } // if there is only 1 layer the point lies in, we save lots of computing by just returning - the weighted normal // of a single layer is just its normal if (containingLayers.size() == 1) { anyNormal.normalize(); return anyNormal; } // find all triangles that have the given point as their vertex, compute the normal of the triangle, multiply it // with the angle at the point, and add it to the result normal final Vector3d result = new Vector3d(); for (Layer l : containingLayers) { for (ModelTriangle t : l.getTriangles()) { if (t.isVertex(point)) { Point3d vertex = t.getNearestVertex(point); Segment3d[] edges = t.getVertexEdges(vertex); // it'd be more precise to project the triangle onto some 2D plane and use the 2D angle (not doing // so leads us to not having the sum of 360°), but it just isn't worth it; we don't need such // precise computation double angle = edges[0].getVector().angle(edges[1].getVector()); Vector3d normal = new Vector3d(t.getNormal()); normal.scale(angle); result.add(normal); } } } // if the found normal is non-zero, normalize it and return; otherwise, the normals have cancelled each other, // so we must return simply one of the layers' normals if (!result.epsilonEquals(new Vector3d(), EPSILON)) { result.normalize(); return result; } else { return anyNormal; } } /** * Return the average normal of the layers this segment lies in. * <p> * Basically, if the segment isn't a border of the layer it lies in, the layer's normal will be returned. * <p> * If this segment lies in no layers, <code>null</code> will be returned and a warning will be issued by this class' * logger. * * @param segment The segment to find normal for. * @return The average normal of a point on this segment. */ public Vector3d getSegmentNormal(ModelSegment segment) { final double[] trialPoints = new double[] { 0.5d, 0.75d, 0.25d, 0.1d, 0.9d, 0.01d, 0.99d }; for (double d : trialPoints) { Vector3d normal = getNormalAtPoint(segment.getPointForParameter(d)); if (normal != null) return normal; } return null; } /** * Given all triangles in one layer, divides all triangles in the layer by the given line to smaller triangles. * * @param layer The triangles in the layer with the appropriate intersection points. * @param direction The direction of the created fold. * @param segment The segments defining the fold directly in this layer. * @param foldDirections An entry will be added to this map with the given layer as key and the value being the real * fold direction on the layer's top side. * * @return The intersections of the given segment with the layer. */ protected List<? extends Segment3d> makeFoldInLayer(Layer layer, Direction direction, ModelSegment segment, Map<Layer, Direction> foldDirections) { Direction realDir = direction; // if the down side of the layer faces screen, invert the fold type if (layer.getNormal().angle(getScreenNormal()) >= Math.PI / 2d + EPSILON) realDir = realDir.getOpposite(); foldDirections.put(layer, realDir); List<IntersectionWithTriangle<ModelTriangle>> intersections = layer.getIntersectionsWithTriangles(segment); Fold fold = new Fold(); fold.originatingStepId = this.step.getId(); for (IntersectionWithTriangle<ModelTriangle> intersection : intersections) { if (intersection == null) { // no intersection with the triangle - something's weird (we loop over intersections with triangles) throw new IllegalStateException( "Invalid diagram: no intersection found in IntersectionWithTriangle in step " + step.getId()); } // also subdivides the referenced foldlines and removes references to the old ones from // intersection.triangle, so no references to it should remain in this.folds List<ModelTriangle> newTriangles = layer.subdivideTriangle(intersection); if (newTriangles.size() > 1) { triangles.remove(intersection.triangle); triangles.addAll(newTriangles); } for (ModelTriangle t : newTriangles) { int i = 0; for (Segment3d edge : t.getEdges()) { Segment3d edgeInt = edge.getIntersection(intersection); if (edgeInt != null && !edgeInt.getVector().epsilonEquals(new Vector3d(), MathHelper.EPSILON)) { // this method adds all fold lines twice - one for each triangle adjacent to the // intersection segment - but we don't care (maybe we should, it'll be more clear further) FoldLine line = new FoldLine(); line.setDirection(realDir); line.setFold(fold); line.setLine(new ModelTriangleEdge(t, i)); t.addFoldLine(i, line); fold.getLines().add(line); } i++; } } } folds.add(fold); return layer.joinNeighboringSegments(intersections); } /** * Return the layer filter that selects the layers corresponding to <code>opposite</code> line. * <p> * That means, construct a triangle from <code>line</code> and <code>opposite</code> (they are required to have a * common point) and return all layers intersecting that triangle that aren't in the affected layers of * <code>line</code>. * * TODO a lot of getLayers() usage can be cached * * @param line The original fold line. * @param opposite The <code>line</code>'s opposite line (it's image in 2D axis symmetry around an axis that has a * common point with <code>line</code>). * @param lineAffectedLayers The set of affected layers for <code>line</code>. * @return The corresponding layer filter for <code>opposite</code>. <code>null</code> if for the given * <code>line</code> and <code>opposite</code> no solution is available (eg. one of them has zero direction * vector or they together form a line). */ protected LayerFilter getOppositeLineLayerFilter(ModelSegment line, ModelSegment opposite, Set<Layer> lineAffectedLayers) { Point3d lineP = line.getP1(); Point3d common = line.getP2(); if (!opposite.contains(common)) { lineP = line.getP2(); common = line.getP1(); } Point3d oppositeP = opposite.getP1(); if (oppositeP.epsilonEquals(common, EPSILON)) oppositeP = opposite.getP2(); // try to construct a triangle // // lineP--------------oppositeP // ...l.\............/.o // ....i.\........../.p // .....n.\......../.p // ......e.\....../.o // .........\..../.s // ..........\../..i // ...........\/...t // ........common..e // not null if the given points form a triangle in 3D Triangle3d triangle = null; try { triangle = new Triangle3d(lineP, common, oppositeP); } catch (IllegalArgumentException e) {} // if the triangle could be constructed, we use this algorithm, otherwise we have another one if (triangle != null) { // construct a stripe that has one border line as the line lineP3-oppositeP3 and the second one parallel to // that one and going through common3 Vector3d stripeDir = new Vector3d(oppositeP); stripeDir.sub(lineP); stripeDir.normalize(); Line3d line1 = new Line3d(lineP, stripeDir); Line3d line2 = new Line3d(common, stripeDir); Stripe3d stripe = new Stripe3d(line1, line2); // get intersection of the constructed stripe with all layers and remove those that either are in affected // layers of line, or don't intersect with the constructed triangle // // this way we get the layers that intersect with the interior of the constructed triangle and aren't "used" // by line LinkedHashMap<Layer, ModelSegment> layerInts = getLayers(stripe); for (Iterator<Entry<Layer, ModelSegment>> it = layerInts.entrySet().iterator(); it.hasNext();) { Entry<Layer, ModelSegment> entry = it.next(); if (lineAffectedLayers.contains(entry.getKey())) { it.remove(); } else { Segment3d intersection = triangle.getIntersection(entry.getValue()); if (intersection == null || intersection.getVector().epsilonEquals(new Vector3d(), EPSILON)) it.remove(); } } Set<Layer> oppositeLayers = layerInts.keySet(); // intersections with layers as defined by opposite Set<Layer> oppositeLineLayers = getLayers(opposite).keySet(); oppositeLineLayers.retainAll(oppositeLayers); return new LayerFilter(oppositeLineLayers); } else { // if the triangle couldn't be constructed, there are several possibilities Segment3d intersection = line.getIntersection(opposite); if (intersection == null || intersection.getVector().epsilonEquals(new Vector3d(), EPSILON)) { // line and opposite either don't intersect or one of them has zero direction vector return null; } // line and opposite do overlap, which means the "triangle" is effectively a line, so we are interested only // in layers that go through that line (using affectedLayers it is possible to find some layers that go // through that line and aren't "used" by line) LinkedHashMap<Layer, ModelSegment> oppositeLayerInts = getLayers(opposite); oppositeLayerInts.keySet().removeAll(lineAffectedLayers); return new LayerFilter(oppositeLayerInts.keySet()); } } /** * Adds the given angle to the current angle of rotation. * * @param rotation The angle to add (in radians). */ public void addRotation(double rotation) { setRotation(rotationAngle + rotation); } /** * Sets the current angle of rotation to the given value. * * @param rotation The angle to set (in radians). */ public void setRotation(double rotation) { rotationAngle = rotation; while (rotationAngle > Math.PI) rotationAngle -= 2 * Math.PI; while (rotationAngle < -Math.PI) rotationAngle += 2 * Math.PI; screenNormal = null; } /** * Return the rotation of the paper. * * @return The rotation of the paper (in radians). */ public double getRotation() { return rotationAngle; } /** * Adds the given angle to the current viewing angle of the paper. * * The angle will be "cropped" to <-PI/2,PI/2> interval. * * @param angle The angle to add (in radians). */ public void addViewingAngle(double angle) { setViewingAngle(viewingAngle + angle); } /** * Changes the viewing angle from top to bottom and vice versa. */ public void flipViewingAngle() { setViewingAngle(-viewingAngle); } /** * Sets the current viewing angle to the given value. * * The angle will be "cropped" to <-PI/2,PI/2> interval. * * @param angle The angle to set (in radians). */ public void setViewingAngle(double angle) { viewingAngle = angle; if (viewingAngle > Math.PI / 2.0) viewingAngle = Math.PI / 2.0; if (viewingAngle < -Math.PI / 2.0) viewingAngle = -Math.PI / 2.0; screenNormal = null; } /** * Get the current viewing angle. * * @return The viewing angle (in radians). */ public double getViewingAngle() { return viewingAngle; } /** * @return The folds on the paper. */ public ObservableList<Fold> getFolds() { return folds; } /** * @return The list of layers. */ public ObservableList<Layer> getLayers() { return layers; } /** * This function has to be called after {@link #clone()} if you intend to base the next step on this model state. */ public void proceedToNextStep() { for (Iterator<Marker> it = markers.iterator(); it.hasNext();) { Marker marker = it.next(); if (marker.getStepsToHide() > 0) marker.setStepsToHide(marker.getStepsToHide() - 1); else it.remove(); } for (ModelTriangle t : triangles) t.resetBeforeRotation(); overlayImage = null; } @Override public ModelState clone() { ModelState result = null; try { result = (ModelState) super.clone(); } catch (CloneNotSupportedException e) { return null; } result.foldLineArrays = null; result.foldLineArraysDirty = true; result.trianglesArrays = null; result.trianglesArraysDirty = true; result.layers = new ObservableList<Layer>(layers.size()); result.folds = new ObservableList<Fold>(folds.size()); result.markers = new ObservableList<Marker>(markers.size()); result.markerData = new LinkedList<MarkerRenderData>(); result.paperToSpaceTriangles = new Hashtable<Triangle2d, ModelTriangle>(paperToSpaceTriangles.size()); result.triangles = new ObservableList<ModelTriangle>(triangles.size()); result.trianglesToLayers = new Hashtable<ModelTriangle, Layer>(trianglesToLayers.size()); result.paperToSpacePoint = new Hashtable<Point2d, Point3d>(paperToSpacePoint.size()); if (overlayImage != null) { result.overlayImage = new BufferedImage(overlayImage.getWidth(), overlayImage.getHeight(), overlayImage.getType()); result.overlayImage.createGraphics().drawImage(overlayImage, null, 0, 0); } result.addObservers(); Hashtable<ModelTriangle, ModelTriangle> newTriangles = new Hashtable<ModelTriangle, ModelTriangle>( triangles.size()); for (ModelTriangle t : triangles) { ModelTriangle newT = t.clone(); // the fold lines will be filled further again with their new instances newT.s1FoldLines = null; newT.s2FoldLines = null; newT.s3FoldLines = null; newTriangles.put(t, newT); result.triangles.add(newT); } for (ModelTriangle t : result.triangles) { List<ModelTriangle> oldNeighbors = new LinkedList<ModelTriangle>(t.getRawNeighbors()); t.getRawNeighbors().clear(); for (ModelTriangle n : oldNeighbors) { ModelTriangle newN = newTriangles.get(n); if (newN == null) throw new IllegalStateException("clone: Cannot find new triangle for old triangle " + n.getOriginalPosition()); t.getRawNeighbors().add(newN); } } for (Fold fold : folds) { Fold newFold = fold.clone(); newFold.lines.getObservers().clear(); newFold.addObservers(); for (FoldLine l : newFold.lines) { ModelTriangle newTriangle = newTriangles.get(l.getLine().getTriangle()); if (newTriangle == null) throw new IllegalStateException("clone: Cannot find new triangle for old triangle " + l.getLine().getTriangle().getOriginalPosition()); l.getLine().setTriangle(newTriangle); List<FoldLine> foldLines = newTriangle.getFoldLines(l.getLine().getIndex()); if (foldLines == null) { newTriangle.setFoldLines(l.getLine().getIndex(), new LinkedList<FoldLine>()); foldLines = newTriangle.getFoldLines(l.getLine().getIndex()); } foldLines.add(l); } result.folds.add(newFold); } for (Layer l : layers) { List<ModelTriangle> triangles = new LinkedList<ModelTriangle>(); for (ModelTriangle t : l.getTriangles()) { ModelTriangle newT = newTriangles.get(t); if (newT == null) throw new IllegalStateException("clone: Cannot find new triangle for old triangle " + t.getOriginalPosition()); triangles.add(newT); } result.layers.add(new Layer(triangles)); } for (Marker m : markers) { result.markers.add(m.clone()); } return result; } /** * Return true if the line composed of the given points goes only through connected parallel layers. * * @param p1 Start point. * @param p2 End point. * @return true if the line composed of the given points goes only through connected parallel layers. */ public boolean canChooseLine(ModelPoint p1, ModelPoint p2) { ModelSegment seg = new ModelSegment(p1, p2, null, 0); List<ModelSegment> ints = new LinkedList<ModelSegment>(); // get intersections with all layers for (Layer l : layers) { if (abs(l.getNormal().dot(seg.getVector())) <= EPSILON && l.getPlane().contains(seg.getP1())) { ints.addAll(l.getIntersections(seg)); } } // this condition should never be satisfied, but false is the correct answer anyways if (ints.size() == 0) return false; // the directions as lines Line3d dir3d = new Line3d(seg); Line2d dir2d = new Line2d(seg.getOriginal()); Vector3d zero = new Vector3d(); Vector2d zero2d = new Vector2d(); // discard all intersections not having the right direction for (Iterator<ModelSegment> it = ints.iterator(); it.hasNext();) { ModelSegment s = it.next(); if ((!s.getVector().epsilonEquals(zero, EPSILON) && !dir3d.isParallelTo(s)) || !dir3d.contains(s.getP1())) { it.remove(); continue; } if ((!s.getOriginal().getVector().epsilonEquals(zero2d, EPSILON) && !dir2d.isParallelTo(s.getOriginal())) || !dir2d.contains(s.getOriginal().getP1())) { it.remove(); continue; } } if (ints.size() == 0) return false; // now we want to find out if all the found intersections can be merged to a single line // so we sort them by 2D coordinates and then try to merge them in the sorted order // if the lines can be merged, this sort always sorts them in the way that the segments being one next to the // other in real, are succeeding in this list; on the other side, if the segments can't be merged into one line, // the order has no meaning Collections.sort(ints, new Comparator<ModelSegment>() { @Override public int compare(ModelSegment o1, ModelSegment o2) { Point2d o1p1 = o1.getOriginal().getP1(); Point2d o2p1 = o2.getOriginal().getP1(); if (o1p1.x < o2p1.x + EPSILON) return -1; if (o1p1.x + EPSILON > o2p1.x) return 1; if (o1p1.y < o2p1.y + EPSILON) return -1; if (o1p1.y + EPSILON > o2p1.y) return 1; return 0; } }); // try to merge the sorted results into one line while (ints.size() > 1 && ints.get(0).merge(ints.get(1))) ints.remove(1); // a line can only by selected if it can be merged into one piece return ints.size() == 1 && ints.get(0).epsilonEquals(seg, true); } /** * @return The normal of the screen in the local coordinate system. */ public Vector3d getScreenNormal() { if (screenNormal == null) { if (Math.abs(getViewingAngle() - Math.PI / 2d) < EPSILON) { screenNormal = new Vector3d(0, 0, 1); } else { // viewingAngle = -PI/2 screenNormal = new Vector3d(0, 0, -1); } // TODO when arbitrary viewing angle is allowed, this will need to be changed } return screenNormal; } /** * Enables measuring of the furthest point affected by a rotation. Use the given point as the origin to measure * distance from. * <p> * Use {@link #getFurthestRotatedPoint()} to get the point after some bending is performed. * * @param center The center to measure distance from. * * @see #getFurthestRotatedPoint() * @see #clearFurthestRotationPointCenter() */ public void setFurthestRotationPointCenter(Point2d center) { furthestRotationCenter = new ModelPoint(locatePointFromPaperTo3D(center), center); furthestRotatedPoint = null; } /** * @return The furthestRotationCenter. */ public ModelPoint getFurthestRotationCenter() { return furthestRotationCenter; } /** * @return The furthest point affected by a rotation (measured in 3D) from the point last set by a call to * {@link #setFurthestRotationPointCenter(Point2d)}. * * @see #setFurthestRotationPointCenter(Point2d) * @see #clearFurthestRotationPointCenter() */ public ModelPoint getFurthestRotatedPoint() { return furthestRotatedPoint; } /** * Disable computing of the furthest rotated point. * * @see #setFurthestRotationPointCenter(Point2d) * @see #getFurthestRotatedPoint() */ public void clearFurthestRotationPointCenter() { furthestRotationCenter = null; furthestRotatedPoint = null; } /** * Enables measuring of the furthest point from the given segment affected by a rotation. Use the given segment as * the origin to measure distance from. * <p> * Use {@link #getFurthestRotatedPointAroundSegment()} to get the point after some bending is performed. * * @param center The center to measure distance from. * * @see #getFurthestRotatedPointAroundSegment() * @see #clearFurthestRotationSegment() */ public void setFurthestRotationSegment(Segment2d center) { furthestRotationSegment = new ModelSegment(new Segment3d(locatePointFromPaperTo3D(center.getP1()), locatePointFromPaperTo3D(center.getP2())), center); furthestRotatedPointAroundSegment = null; } /** * @return The furthestRotationSegment. */ public ModelSegment getFurthestRotationSegment() { return furthestRotationSegment; } /** * @return The furthest point affected by a rotation (measured in 3D) from the segment last set by a call to * {@link #setFurthestRotationSegment(Segment2d)}. * * @see #setFurthestRotationSegment(Segment2d) * @see #clearFurthestRotationSegment() */ public ModelPoint getFurthestRotatedPointAroundSegment() { return furthestRotatedPointAroundSegment; } /** * Disable computing of the furthest rotated point around segment. * * @see #setFurthestRotationSegment(Segment2d) * @see #getFurthestRotatedPointAroundSegment() */ public void clearFurthestRotationSegment() { furthestRotationSegment = null; furthestRotatedPointAroundSegment = null; } /** * @return If not null, this operation is covered by this image. */ public BufferedImage getOverlayImage() { return overlayImage; } /** * @param overlayImage If not null, this operation is covered by this image. */ public void setOverlayImage(BufferedImage overlayImage) { this.overlayImage = overlayImage; } /** * A callback that tests whether a triangle's neighbor should be included in some list or so. * * @author Martin Pecka */ protected interface NeighborInclusionTest { /** * Return true if <code>neighbor</code> should be included. * * @param t A triangle. * @param neighbor Neighbor of <code>t</code>. * * @return true if <code>neighbor</code> should be included. */ boolean includeNeighbor(ModelTriangle t, ModelTriangle neighbor); } }