/* GeoGebra - Dynamic Mathematics for Everyone http://www.geogebra.org This file is part of GeoGebra. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation. */ package org.geogebra.common.geogebra3D.kernel3D.algos; import java.util.ArrayList; import java.util.TreeMap; import org.geogebra.common.geogebra3D.kernel3D.geos.GeoLine3D; import org.geogebra.common.geogebra3D.kernel3D.geos.GeoPoint3D; import org.geogebra.common.geogebra3D.kernel3D.geos.GeoPolygon3D; import org.geogebra.common.geogebra3D.kernel3D.geos.GeoSegment3D; import org.geogebra.common.kernel.Construction; import org.geogebra.common.kernel.Kernel; import org.geogebra.common.kernel.StringTemplate; import org.geogebra.common.kernel.Matrix.CoordMatrix; import org.geogebra.common.kernel.Matrix.CoordMatrixUtil; import org.geogebra.common.kernel.Matrix.Coords; import org.geogebra.common.kernel.algos.AlgoPolygonOperation.PolyOperation; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoPoly; import org.geogebra.common.kernel.geos.GeoPolygon; import org.geogebra.common.kernel.kernelND.GeoLineND; import org.geogebra.common.kernel.kernelND.GeoSegmentND; import org.geogebra.common.kernel.kernelND.HasSegments; import org.geogebra.common.util.clipper.Clipper; import org.geogebra.common.util.clipper.Clipper.ClipType; import org.geogebra.common.util.clipper.Clipper.PolyFillType; import org.geogebra.common.util.clipper.Clipper.PolyType; import org.geogebra.common.util.clipper.DefaultClipper; import org.geogebra.common.util.clipper.Path; import org.geogebra.common.util.clipper.Paths; import org.geogebra.common.util.clipper.Point.DoublePoint; /** * * Input: Two polygons * * Output: Polygon that is the result of an intersection, union or difference * operation on the input polygons. * * @author thilina */ public abstract class AlgoPolygonOperations3D extends AlgoElement3D { /** first input polygon */ protected GeoPolygon inPoly0; /** second input polygon */ protected GeoPolygon inPoly1; /** output polygons */ protected OutputHandler<GeoPolygon3D> outputPolygons; /** output points */ protected OutputHandler<GeoPoint3D> outputPoints; /** output segments */ protected OutputHandler<GeoSegment3D> outputSegments; // clipper library compatible types private Path subject; private Path clip; private Paths solution; // auxiliary objects for getting corresponding coords values in 2D coords // w.r.t input poly 0 private Coords tmpCoord; private CoordMatrix matrix; private Coords retCoords; // 3d line for polygon in two different but not parallel case private GeoLine3D planeIntsctGeoLine3D; private TreeMap<Double, Coords> newCoords; private GeoPoint3D tmpPoint; private ArrayList<Integer> intersectSegmentIndex; /** polygon operation type */ protected PolyOperation operationType; /** output labels */ protected String[] labels; private boolean silent; /** * special constructor without operation type for CmdDifference. after this * constructor initiatePolyOperation() must be called * * @param cons * construction * @param labels * labels * @param inPoly0 * input polygon 1 * @param inPoly1 * input polygon 2 * */ public AlgoPolygonOperations3D(Construction cons, String[] labels, GeoPolygon inPoly0, GeoPolygon inPoly1) { super(cons); this.inPoly0 = inPoly0; this.inPoly1 = inPoly1; this.labels = labels; this.silent = cons.isSuppressLabelsActive(); } /** * after the constructor with no operation type, this method should be * called for successful completion of the constructor * * after calling this call setInputOutput() for setting input output * dependencies * * @param opType * the enum type of operation INTERSECTION, UNION, DIFFERENCE, * XOR */ public void initiatePolyOperation(PolyOperation opType) { this.operationType = opType; tmpCoord = new Coords(4); subject = new Path(inPoly0.getPointsLength()); clip = new Path(inPoly1.getPointsLength()); solution = new Paths(); planeIntsctGeoLine3D = new GeoLine3D(getConstruction()); newCoords = new TreeMap<Double, Coords>( Kernel.doubleComparator(Kernel.STANDARD_PRECISION)); tmpPoint = new GeoPoint3D(getConstruction()); intersectSegmentIndex = new ArrayList<Integer>(); createOutput(); setInputOutput(); compute(); // set labels if (labels == null) { outputPolygons.setLabels(null); outputPoints.setLabels(null); outputSegments.setLabels(null); } else { int labelsLength = labels.length; if (labelsLength > 1) { // set default outputPolygons.setLabels(null); outputSegments.setLabels(null); outputPoints.setLabels(null); } else if (labels.length == 1 && labels[0] != null && !labels[0].equals("")) { outputPolygons.setIndexLabels(labels[0]); } } update(); } /** * constructor for retrieving saved 3d polygon intersections * * @param cons * construction * @param labels * labels * @param inPoly0 * input polygon 1 * @param inPoly1 * input polygon 2 * @param operationType * the enum type of operation INTERSECTION, UNION, DIFFERENCE,XOR */ public AlgoPolygonOperations3D(Construction cons, String[] labels, GeoPoly inPoly0, GeoPoly inPoly1, PolyOperation operationType) { super(cons); this.operationType = operationType; this.inPoly0 = (GeoPolygon) inPoly0; this.inPoly1 = (GeoPolygon) inPoly1; tmpCoord = new Coords(4); subject = new Path(this.inPoly0.getPointsLength()); clip = new Path(this.inPoly1.getPointsLength()); solution = new Paths(); planeIntsctGeoLine3D = new GeoLine3D(getConstruction()); newCoords = new TreeMap<Double, Coords>( Kernel.doubleComparator(Kernel.STANDARD_PRECISION)); tmpPoint = new GeoPoint3D(getConstruction()); intersectSegmentIndex = new ArrayList<Integer>(); createOutput(); this.labels = labels; silent = cons.isSuppressLabelsActive(); } /** * @param outputSizes * size of polygon, point, segmen output */ protected void initialize(int[] outputSizes) { setInputOutput(); compute(false); // set labels if (labels == null) { outputPolygons.setLabels(null); outputPoints.setLabels(null); outputSegments.setLabels(null); } else { int labelsLength = labels.length; if (labelsLength > 1) { if (outputSizes != null) { // set output sizes outputPolygons.adjustOutputSize(outputSizes[0], false); outputPoints.adjustOutputSize(outputSizes[1], false); outputSegments.adjustOutputSize(outputSizes[2], false); // set labels int i1 = 0; int i2 = 0; while (i1 < outputSizes[0]) { outputPolygons.getElement(i1).setLabel(labels[i2]); i1++; i2++; } i1 = 0; while (i1 < outputSizes[1]) { outputPoints.getElement(i1).setLabel(labels[i2]); i1++; i2++; } i1 = 0; while (i1 < outputSizes[2]) { outputSegments.getElement(i1).setLabel(labels[i2]); i1++; i2++; } } else { // set default outputPolygons.setLabels(null); outputSegments.setLabels(null); outputPoints.setLabels(null); } } else if (labels.length == 1 && labels[0] != null && !labels[0].equals("")) { outputPolygons.setIndexLabels(labels[0]); } } update(); } @Override protected void getCmdOutputXML(StringBuilder sb, StringTemplate tpl) { sb.append("\t<outputSizes val=\""); sb.append(outputPolygons.size()); sb.append(","); sb.append(outputPoints.size()); sb.append(","); sb.append(outputSegments.size()); sb.append("\""); sb.append("/>\n"); // common method super.getCmdOutputXML(sb, tpl); } /** * create outputHandlers for output polygons, points, and segments and * initiate them */ private final void createOutput() { outputPolygons = new OutputHandler<GeoPolygon3D>( new elementFactory<GeoPolygon3D>() { @Override public GeoPolygon3D newElement() { GeoPolygon3D p = new GeoPolygon3D(cons, true); p.setParentAlgorithm(AlgoPolygonOperations3D.this); if (outputPolygons.size() > 0) { p.setAllVisualProperties( outputPolygons.getElement(0), false); } p.setViewFlags(inPoly0.getViewSet()); p.setNotFixedPointsLength(true); return p; } }); outputPolygons.adjustOutputSize(1, false); outputPoints = new OutputHandler<GeoPoint3D>( new elementFactory<GeoPoint3D>() { @Override public GeoPoint3D newElement() { GeoPoint3D newPoint = new GeoPoint3D(cons); newPoint.setCoords(0, 0, 1); newPoint.setParentAlgorithm( AlgoPolygonOperations3D.this); newPoint.setAuxiliaryObject(true); newPoint.setViewFlags(inPoly0.getViewSet()); return newPoint; } }); outputPoints.adjustOutputSize(1, false); outputSegments = new OutputHandler<GeoSegment3D>( new elementFactory<GeoSegment3D>() { @Override public GeoSegment3D newElement() { GeoSegment3D segment = (GeoSegment3D) outputPolygons .getElement(0) .createSegment(cons, outputPoints.getElement(0), outputPoints.getElement(0), true); segment.setAuxiliaryObject(true); segment.setViewFlags(inPoly0.getViewSet()); return segment; } }); } @Override protected void setInputOutput() { input = new GeoElement[2]; input[0] = inPoly0; input[1] = inPoly1; // set dependencies for (int i = 0; i < input.length; i++) { input[i].addAlgorithm(this); } cons.addToAlgorithmList(this); setDependencies(); } @Override public final void compute() { compute(!silent); } /** * @param updateLabels * whether to set labels (should be false ion silent mode) */ protected void compute(boolean updateLabels) { // one or more input polygons are undefined, terminate immediately if (!this.inPoly0.isDefined() || !this.inPoly1.isDefined()) { // Log.debug("one of the input polygons is not defined."); setOutputUndefined(); return; } Coords[] res = CoordMatrixUtil.intersectPlanes( this.inPoly0.getCoordSys().getMatrixOrthonormal(), this.inPoly1.getCoordSys().getMatrixOrthonormal()); // Log.debug("res[0]: " + res[0].getX() + " , " + res[0].getY() + " , " // + res[0].getZ() + " , " + res[0].getW()); // Log.debug("res[1]: " + res[1].getX() + " , " + res[1].getY() + " , " // + res[1].getZ() + " , " + res[1].getW()); // both the input polygons are on the same plane if (res[1].isZero() && !res[0].isZero()) { // Log.debug("the two input polygons are on the same plane."); // Log.debug(a + "X + " + b + "Y + " + c + "Z + " + d + " = 0"); // add subject polygon subject.clear(); for (int i = 0; i < this.inPoly0.getPointsLength(); i++) { DoublePoint point = new DoublePoint( this.inPoly0.getPoint(i).getX(), this.inPoly0.getPoint(i).getY()); // Log.debug("SubjectPoly-> x:" // + this.inPoly0.getPoint(i).getX() + " y:" // + this.inPoly0.getPoint(i).getY()); subject.add(point); } // add clip polygon matrix = this.inPoly0.getCoordSys().getMatrixOrthonormal(); clip.clear(); for (int i = 0; i < this.inPoly1.getPointsLength(); i++) { this.inPoly1.getPoint3D(i).projectPlaneInPlaneCoords(matrix, tmpCoord); DoublePoint point = new DoublePoint(tmpCoord.getX(), tmpCoord.getY()); // Log.debug("ClipPoly-> x:" + tmpCoord.getX() + " y:" // + tmpCoord.getY()); clip.add(point); } // initializing clipper DefaultClipper clipper = new DefaultClipper( Clipper.STRICTLY_SIMPLE); clipper.addPath(clip, PolyType.CLIP, true); clipper.addPath(subject, PolyType.SUBJECT, true); boolean solutionValid = false; solution.clear(); // calculating output polygons switch (operationType) { default: case INTERSECTION: solutionValid = clipper.execute(ClipType.INTERSECTION, solution, PolyFillType.EVEN_ODD, PolyFillType.EVEN_ODD); break; case UNION: solutionValid = clipper.execute(ClipType.UNION, solution, PolyFillType.EVEN_ODD, PolyFillType.EVEN_ODD); break; case DIFFERENCE: solutionValid = clipper.execute(ClipType.DIFFERENCE, solution, PolyFillType.EVEN_ODD, PolyFillType.EVEN_ODD); break; case XOR: solutionValid = clipper.execute(ClipType.XOR, solution, PolyFillType.EVEN_ODD, PolyFillType.EVEN_ODD); break; } // assign output calculated using clipper library appropriately if (!solutionValid) { // if there is no output setOutputUndefined(); } else { // adjust output sizes outputPolygons.adjustOutputSize(solution.size(), false); int pointCount = 0; for (Path path : solution) { pointCount += path.size(); } outputPoints.adjustOutputSize(pointCount, false); outputSegments.adjustOutputSize(pointCount, false); // assigning coords to output points pointCount = 0; for (Path path : solution) { for (int i = 0; i < path.size(); i++) { GeoPoint3D point = outputPoints.getElement(pointCount); DoublePoint calcPoint = path.get(i); retCoords = this.inPoly0.getCoordSys() .getPoint(calcPoint.getX(), calcPoint.getY()); point.setCoords(retCoords.getX(), retCoords.getY(), retCoords.getZ(), 1); // Log.debug("x: " + retCoords.getX() + " y: " // + retCoords.getY() // + " z: " + z); pointCount++; } } if (updateLabels) { outputPoints.updateLabels(); } GeoPoint3D[] points = new GeoPoint3D[pointCount]; points = outputPoints.getOutput(points); int pointIndex = 0; int polygonIndex = 0; int segmentIndex = 0; for (Path path : solution) { GeoPolygon3D polygon = outputPolygons .getElement(polygonIndex); polygonIndex++; GeoPoint3D[] polyPoints = new GeoPoint3D[path.size()]; GeoSegment3D[] polySegments = new GeoSegment3D[path.size()]; for (int i = 0; i < path.size(); i++) { GeoSegment3D segment = outputSegments .getElement(segmentIndex); segment.setPoints(points[pointIndex + i], points[pointIndex + (i + 1) % path.size()]); segment.setCoord(points[pointIndex + i], points[pointIndex + (i + 1) % path.size()]); segment.update(); polyPoints[i] = points[pointIndex + i]; polySegments[i] = segment; segmentIndex++; } pointIndex += path.size(); // assign points to poly without creating segments polygon.setPoints(polyPoints, null, false); polygon.setSegments(polySegments); polygon.calcArea(); } // Log.debug("ending computing intersection between two polygons // in the same plane. "); } } // the two input polygons are in two different parallel planes else if (res[1].isZero() && res[0].isZero()) { // Log.debug("two different parallel planes"); setOutputUndefined(); } // the two input polygons are in two different planes (not parallel) else { // Log.debug("two different planes (not parallel)"); // calculating output polygons if (this.operationType == PolyOperation.INTERSECTION) { // sets the intersection line of planes this.planeIntsctGeoLine3D.setCoord(res[0], res[1].normalize()); this.newCoords.clear(); // finding intersection points between plane intersection line // and the polygons intersectionsCoords(this.inPoly0, this.planeIntsctGeoLine3D, newCoords); intersectionsCoords(this.inPoly1, this.planeIntsctGeoLine3D, newCoords); // affect new computed points Coords[] coords = new Coords[1]; coords = newCoords.values().toArray(coords); this.intersectSegmentIndex.clear(); tmpCoord.setW(1); // ensure homeogeneous coords for (int i = 0; i < coords.length - 1; i++) { // check whether the mid point of two consecutive intersect // points is on both polygons tmpCoord.setAdd3(coords[i], coords[i + 1]).mulInside3(0.5); tmpPoint.setCoords(tmpCoord); if (this.inPoly0.isInRegion(tmpPoint) && this.inPoly1.isInRegion(tmpPoint)) { this.intersectSegmentIndex.add(i); } } int noOfSegs = this.intersectSegmentIndex.size(); // Log.debug("no Of Intersection Segments: " + noOfSegs); outputPolygons.adjustOutputSize(noOfSegs, false); outputPoints.adjustOutputSize(noOfSegs * 2, false); outputSegments.adjustOutputSize(noOfSegs, false); int pointIndex = 0; for (int i = 0; i < noOfSegs; i++) { outputPoints.getElement(pointIndex) .setCoords(coords[intersectSegmentIndex.get(i)]); outputPoints.getElement(pointIndex + 1).setCoords( coords[intersectSegmentIndex.get(i) + 1]); GeoPoint3D[] polyPoints = new GeoPoint3D[2]; polyPoints[0] = outputPoints.getElement(pointIndex); polyPoints[1] = outputPoints.getElement(pointIndex + 1); pointIndex = pointIndex + 2; GeoSegment3D[] polySegments = new GeoSegment3D[1]; outputSegments.getElement(i).setPoints(polyPoints[0], polyPoints[1]); polySegments[0] = outputSegments.getElement(i); polySegments[0].update(); // assign points to poly without creating segments outputPolygons.getElement(i).setPoints(polyPoints, null, false); outputPolygons.getElement(i).setSegments(polySegments); outputPolygons.getElement(i).calcArea(); // Log.debug("poly " + i + " defined : " // + outputPolygons.getElement(i).isDefined()); } } // for Difference, Xor, Union no output if the input polygons are in // two different planes else { setOutputUndefined(); } } if (updateLabels) { outputSegments.updateLabels(); outputPolygons.updateLabels(); } } private void setOutputUndefined() { outputPolygons.adjustOutputSize(1, false); outputPoints.adjustOutputSize(1, false); outputSegments.adjustOutputSize(1, false); outputSegments.updateLabels(); outputPolygons.updateLabels(); outputPolygons.setUndefined(); } // /////////////////////////////////////////////////////////////////////// /* * auxiliary methods for finding intersection segments when the two input * polygons are on two different not-parallel planes */ private Coords o1, d1; private void setIntersectionLine(GeoLineND line) { o1 = line.getPointInD(3, 0).getInhomCoordsInSameDimension(); d1 = line.getPointInD(3, 1).getInhomCoordsInSameDimension().sub(o1); } /** * calc intersection coords * * @param hasSegments * @param line * @param newCoords1 * */ private void intersectionsCoords(GeoPolygon poly, GeoLineND line, TreeMap<Double, Coords> newCoords1) { // check if the line is contained by the polygon plane switch (AlgoIntersectCS1D2D.getConfigLinePlane(line, poly)) { case GENERAL: // intersect line/interior of polygon intersectionsCoordsGeneral(poly, line, newCoords1); break; case CONTAINED: // intersect line/segments intersectionsCoordsContained(poly, line, newCoords1); break; case PARALLEL: // no intersection break; } } /** * calc intersection coords when line is contained in polygon's plane * * @param p * @param line * @param newCoords1 */ private void intersectionsCoordsContained(HasSegments p, GeoLineND line, TreeMap<Double, Coords> newCoords1) { // line origin and direction setIntersectionLine(line); for (int i = 0; i < p.getSegments().length; i++) { GeoSegmentND seg = p.getSegments()[i]; Coords o2 = seg.getPointInD(3, 0).getInhomCoordsInSameDimension(); Coords d2 = seg.getPointInD(3, 1).getInhomCoordsInSameDimension() .sub(o2); Coords[] project = CoordMatrixUtil.nearestPointsFromTwoLines(o1, d1, o2, d2); // check if projection is intersection point if (project != null && project[0].equalsForKernel(project[1], Kernel.STANDARD_PRECISION)) { double t1 = project[2].get(1); // parameter on line double t2 = project[2].get(2); // parameter on segment if (line.respectLimitedPath(t1) && seg.respectLimitedPath(t2)) { newCoords1.put(t1, project[0]); } } } } /** * calc intersection coords when line is not contained in polygon's plane * * @param p * @param line * @param newCoords * */ private static void intersectionsCoordsGeneral(GeoPolygon p, GeoLineND line, TreeMap<Double, Coords> newCoords) { Coords globalCoords = new Coords(4); Coords inPlaneCoords = new Coords(4); Coords singlePoint = AlgoIntersectCS1D2D.getIntersectLinePlane(line, p, globalCoords, inPlaneCoords); // check if projection is intersection point if (singlePoint != null) { newCoords.put(0d, singlePoint); } } }