From 4c226b17be18ebb1c949a08370c7d30ce3f2f05b Mon Sep 17 00:00:00 2001 From: mwienand Date: Sun, 27 May 2012 11:55:15 +0200 Subject: [PATCH] [355997] IPolyShape implementations and transformation interfaces --- org.eclipse.gef4.geometry.doc/javadocOptions.txt | 2 +- .../image_src/IGeometry_Planar_Abstractions.PNG | Bin 85320 -> 0 bytes .../image_src/IGeometry_Planar_Abstractions.ucls | 20 +- .../image_src/IGeometry_Planar_Overview.PNG | Bin 74485 -> 0 bytes .../image_src/IGeometry_Planar_Overview.ucls | 163 ++-- .../reference/image_src/inheritance-hierarchy.ucls | 283 +++++ .../reference/image_src/transform-overview.ucls | 44 + .../examples/demos/BezierApproximationExample.java | 81 ++ .../geometry/examples/demos/ConvexHullExample.java | 84 ++ .../demos/CubicCurveDeCasteljauExample.java | 136 +++ .../geometry/examples/demos/RegionExample.java | 100 ++ .../examples/demos/RegionOutlineExample.java | 113 ++ .../gef4/geometry/examples/demos/RingExample.java | 128 +++ .../examples/demos/TriangulationExample.java | 109 ++ .../intersection/AbstractIntersectionExample.java | 5 +- .../intersection/BezierApproximationExample.java | 80 -- .../examples/intersection/ConvexHullExample.java | 82 -- .../intersection/CubicCurveDeCasteljauExample.java | 134 --- .../scalerotate/CubicCurveScaleRotate.java | 62 ++ .../org/eclipse/gef4/geometry/tests/AllTests.java | 14 +- .../eclipse/gef4/geometry/tests/AngleTests.java | 2 + .../gef4/geometry/tests/BezierCurveTests.java | 345 ++++++ .../gef4/geometry/tests/CurveUtilsTests.java | 4 +- .../gef4/geometry/tests/DimensionTests.java | 21 +- .../eclipse/gef4/geometry/tests/EllipseTests.java | 170 +++- .../org/eclipse/gef4/geometry/tests/LineTests.java | 8 + .../eclipse/gef4/geometry/tests/PointTests.java | 4 + .../eclipse/gef4/geometry/tests/PolygonTests.java | 23 + .../eclipse/gef4/geometry/tests/PolylineTests.java | 22 +- .../gef4/geometry/tests/RectangleTests.java | 36 +- .../eclipse/gef4/geometry/tests/RegionTests.java | 103 ++ .../org/eclipse/gef4/geometry/tests/RingTests.java | 1120 ++++++++++++++++++++ .../gef4/geometry/tests/RoundedRectangleTests.java | 214 ++++ .../eclipse/gef4/geometry/tests/StraightTests.java | 4 + .../eclipse/gef4/geometry/tests/Vector3DTests.java | 115 ++ org.eclipse.gef4.geometry/META-INF/MANIFEST.MF | 2 + .../src/org/eclipse/gef4/geometry/Point.java | 3 +- .../eclipse/gef4/geometry/euclidean/Straight.java | 4 +- .../geometry/planar/AbstractArcBasedGeometry.java | 300 ++++++ .../planar/AbstractPointListBasedGeometry.java | 73 +- .../gef4/geometry/planar/AbstractPolyShape.java | 181 ++++ .../planar/AbstractRectangleBasedGeometry.java | 96 +- .../src/org/eclipse/gef4/geometry/planar/Arc.java | 246 ++---- .../eclipse/gef4/geometry/planar/BezierCurve.java | 923 ++++++++-------- .../eclipse/gef4/geometry/planar/BezierSpline.java | 2 +- .../eclipse/gef4/geometry/planar/CubicCurve.java | 99 +-- .../org/eclipse/gef4/geometry/planar/Ellipse.java | 96 +- .../eclipse/gef4/geometry/planar/IPolyShape.java | 26 +- .../src/org/eclipse/gef4/geometry/planar/Line.java | 47 + .../src/org/eclipse/gef4/geometry/planar/Pie.java | 96 ++- .../eclipse/gef4/geometry/planar/PolyBezier.java | 208 ++++- .../org/eclipse/gef4/geometry/planar/Polygon.java | 409 +++++--- .../org/eclipse/gef4/geometry/planar/Polyline.java | 135 ++-- .../gef4/geometry/planar/QuadraticCurve.java | 101 +-- .../eclipse/gef4/geometry/planar/Rectangle.java | 345 ++++--- .../org/eclipse/gef4/geometry/planar/Region.java | 359 ++++++- .../src/org/eclipse/gef4/geometry/planar/Ring.java | 506 +++++++++- .../gef4/geometry/planar/RoundedRectangle.java | 221 +++-- .../eclipse/gef4/geometry/projective/Vector3D.java | 4 +- .../gef4/geometry/transform/IRotatable.java | 134 +++ .../eclipse/gef4/geometry/transform/IScalable.java | 207 ++++ .../gef4/geometry/transform/ITranslatable.java | 81 ++ .../eclipse/gef4/geometry/utils/CurveUtils.java | 263 +++++- .../gef4/geometry/utils/PointListUtils.java | 126 +++- .../geometry/utils/PolynomCalculationUtils.java | 4 +- 65 files changed, 7253 insertions(+), 1875 deletions(-) delete mode 100644 org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Abstractions.PNG delete mode 100644 org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Overview.PNG create mode 100644 org.eclipse.gef4.geometry.doc/reference/image_src/inheritance-hierarchy.ucls create mode 100644 org.eclipse.gef4.geometry.doc/reference/image_src/transform-overview.ucls create mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/BezierApproximationExample.java create mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/ConvexHullExample.java create mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/CubicCurveDeCasteljauExample.java create mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RegionExample.java create mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RegionOutlineExample.java create mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RingExample.java create mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/TriangulationExample.java delete mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/BezierApproximationExample.java delete mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/ConvexHullExample.java delete mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/CubicCurveDeCasteljauExample.java create mode 100644 org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/scalerotate/CubicCurveScaleRotate.java create mode 100644 org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/BezierCurveTests.java create mode 100644 org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RegionTests.java create mode 100644 org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RingTests.java create mode 100644 org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RoundedRectangleTests.java create mode 100644 org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/Vector3DTests.java create mode 100644 org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractArcBasedGeometry.java create mode 100644 org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractPolyShape.java create mode 100644 org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/IRotatable.java create mode 100644 org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/IScalable.java create mode 100644 org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/ITranslatable.java diff --git a/org.eclipse.gef4.geometry.doc/javadocOptions.txt b/org.eclipse.gef4.geometry.doc/javadocOptions.txt index 7f77240..7118fc9 100644 --- a/org.eclipse.gef4.geometry.doc/javadocOptions.txt +++ b/org.eclipse.gef4.geometry.doc/javadocOptions.txt @@ -26,4 +26,4 @@ org.eclipse.gef4.geometry.euclidean org.eclipse.gef4.geometry.planar org.eclipse.gef4.geometry.projective org.eclipse.gef4.geometry.transform -org.eclipse.gef4.geometry.utils \ No newline at end of file +org.eclipse.gef4.geometry.utils diff --git a/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Abstractions.PNG b/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Abstractions.PNG deleted file mode 100644 index 33e933d39e1d1fec1dae90857b0dd9a20d3ee99e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85320 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 P1bk_!>7RP4ViWLxBD;Y4 diff --git a/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Abstractions.ucls b/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Abstractions.ucls index 39fa299..ac6f3a7 100644 --- a/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Abstractions.ucls +++ b/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Abstractions.ucls @@ -1,5 +1,5 @@ - + @@ -55,24 +55,24 @@ - + - + + + + + - - - - - - + + diff --git a/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Overview.PNG b/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Overview.PNG deleted file mode 100644 index 089d141b289c15ffc2d3286317dad9fe683e0ed7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74485 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

pA#sz7cJPEJx7K{wl+<5}-Zhy7GX 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 z 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 diff --git a/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Overview.ucls b/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Overview.ucls index 9f4e067..caae498 100644 --- a/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Overview.ucls +++ b/org.eclipse.gef4.geometry.doc/reference/image_src/IGeometry_Planar_Overview.ucls @@ -1,5 +1,5 @@ - + @@ -21,7 +21,7 @@ - + @@ -72,16 +72,7 @@ - - - - - - - - @@ -90,7 +81,7 @@ - @@ -99,7 +90,7 @@ - @@ -108,7 +99,7 @@ - @@ -117,7 +108,7 @@ - @@ -126,7 +117,7 @@ - @@ -135,7 +126,7 @@ - @@ -144,7 +135,7 @@ - @@ -153,7 +144,7 @@ - @@ -162,7 +153,7 @@ - @@ -171,7 +162,7 @@ - @@ -180,7 +171,7 @@ - @@ -189,86 +180,108 @@ - - - - - + + + + + + + + + + + + + + + - - + + - - - - - + - - - - - - - + + + - + - - + + + + + + + + + + - - + + - - + + - + - - - - - - - - + + + + + + + + - - + + - - + + - - + + - - + + - + - - + + - - + + + + + + - - - + + + + + + + diff --git a/org.eclipse.gef4.geometry.doc/reference/image_src/inheritance-hierarchy.ucls b/org.eclipse.gef4.geometry.doc/reference/image_src/inheritance-hierarchy.ucls new file mode 100644 index 0000000..13f62da --- /dev/null +++ b/org.eclipse.gef4.geometry.doc/reference/image_src/inheritance-hierarchy.ucls @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.eclipse.gef4.geometry.doc/reference/image_src/transform-overview.ucls b/org.eclipse.gef4.geometry.doc/reference/image_src/transform-overview.ucls new file mode 100644 index 0000000..dded353 --- /dev/null +++ b/org.eclipse.gef4.geometry.doc/reference/image_src/transform-overview.ucls @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/BezierApproximationExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/BezierApproximationExample.java new file mode 100644 index 0000000..c0beca9 --- /dev/null +++ b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/BezierApproximationExample.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2011 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.examples.demos; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample; +import org.eclipse.gef4.geometry.planar.BezierCurve; +import org.eclipse.gef4.geometry.planar.IGeometry; +import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.Path; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; + +public class BezierApproximationExample extends AbstractIntersectionExample { + public static void main(String[] args) { + new BezierApproximationExample("Bezier Approximation Example"); + } + + public BezierApproximationExample(String title) { + super(title); + } + + protected AbstractControllableShape createControllableShape1(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + addControlPoint(new Point(100, 200)); + addControlPoint(new Point(150, 250)); + addControlPoint(new Point(200, 150)); + addControlPoint(new Point(250, 250)); + addControlPoint(new Point(300, 150)); + addControlPoint(new Point(350, 250)); + addControlPoint(new Point(400, 200)); + } + + @Override + public IGeometry createGeometry() { + return new BezierCurve(getControlPoints()).toPath(); + } + + @Override + public void drawShape(GC gc) { + Path curve = (Path) createGeometry(); + gc.drawPath(new org.eclipse.swt.graphics.Path(Display + .getCurrent(), curve.toSWTPathData())); + } + }; + } + + protected AbstractControllableShape createControllableShape2(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + } + + @Override + public IGeometry createGeometry() { + return new Line(new Point(), new Point(1, 1)); + } + + @Override + public void drawShape(GC gc) { + } + }; + } + + @Override + protected Point[] computeIntersections(IGeometry g1, IGeometry g2) { + return new Point[] {}; + } +} diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/ConvexHullExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/ConvexHullExample.java new file mode 100644 index 0000000..67955f7 --- /dev/null +++ b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/ConvexHullExample.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2011 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.examples.demos; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample.AbstractControllableShape; +import org.eclipse.gef4.geometry.planar.IGeometry; +import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.Polygon; +import org.eclipse.gef4.geometry.utils.PointListUtils; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; + +public class ConvexHullExample extends AbstractIntersectionExample { + public static void main(String[] args) { + new ConvexHullExample("Convex Hull Example"); + } + + public ConvexHullExample(String title) { + super(title); + } + + protected AbstractControllableShape createControllableShape1(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + addControlPoint(new Point(100, 100)); + addControlPoint(new Point(150, 400)); + addControlPoint(new Point(200, 300)); + addControlPoint(new Point(250, 150)); + addControlPoint(new Point(300, 250)); + addControlPoint(new Point(350, 200)); + addControlPoint(new Point(400, 350)); + } + + @Override + public IGeometry createGeometry() { + Polygon convexHull = new Polygon( + PointListUtils.getConvexHull(getControlPoints())); + return convexHull; + } + + @Override + public void drawShape(GC gc) { + Polygon convexHull = (Polygon) createGeometry(); + gc.drawPath(new org.eclipse.swt.graphics.Path(Display + .getCurrent(), convexHull.toPath().toSWTPathData())); + } + }; + } + + protected AbstractControllableShape createControllableShape2(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + } + + @Override + public IGeometry createGeometry() { + return new Line(-10, -10, -10, -10); + } + + @Override + public void drawShape(GC gc) { + } + }; + } + + @Override + protected Point[] computeIntersections(IGeometry g1, IGeometry g2) { + return new Point[] {}; + } +} diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/CubicCurveDeCasteljauExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/CubicCurveDeCasteljauExample.java new file mode 100644 index 0000000..aa9eda0 --- /dev/null +++ b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/CubicCurveDeCasteljauExample.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright (c) 2011 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.examples.demos; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample.AbstractControllableShape; +import org.eclipse.gef4.geometry.planar.CubicCurve; +import org.eclipse.gef4.geometry.planar.IGeometry; +import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.Rectangle; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; + +public class CubicCurveDeCasteljauExample extends AbstractIntersectionExample { + public static void main(String[] args) { + new CubicCurveDeCasteljauExample("Cubic Bezier Curve Example"); + } + + public CubicCurveDeCasteljauExample(String title) { + super(title); + } + + protected AbstractControllableShape createControllableShape1(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + addControlPoint(new Point(100, 200)); + addControlPoint(new Point(200, 100)); + addControlPoint(new Point(300, 300)); + addControlPoint(new Point(400, 200)); + } + + @Override + public IGeometry createGeometry() { + Point[] points = getControlPoints(); + + CubicCurve curve = new CubicCurve(points[0], points[1], + points[2], points[3]); + + return curve; + } + + @Override + public void drawShape(GC gc) { + CubicCurve curve = (CubicCurve) createGeometry(); + + // draw curve + gc.drawPath(new org.eclipse.swt.graphics.Path(Display + .getCurrent(), curve.toPath().toSWTPathData())); + + // draw bounds + Rectangle bounds = curve.getBounds(); + + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_DARK_GRAY)); + gc.drawRectangle(bounds.toSWTRectangle()); + + // draw lerps + Point[] points = getControlPoints(); + for (int i = 0; i < 3; i++) { + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_DARK_GREEN)); + gc.drawLine((int) points[i].x, (int) points[i].y, + (int) points[i + 1].x, (int) points[i + 1].y); + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_BLACK)); + points[i] = points[i].getTranslated(points[i + 1] + .getTranslated(points[i].getScaled(-1)).getScaled( + 0.25)); + gc.drawOval((int) (points[i].x - 2), + (int) (points[i].y - 2), 4, 4); + } + for (int i = 0; i < 2; i++) { + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_BLUE)); + gc.drawLine((int) points[i].x, (int) points[i].y, + (int) points[i + 1].x, (int) points[i + 1].y); + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_BLACK)); + points[i] = points[i].getTranslated(points[i + 1] + .getTranslated(points[i].getScaled(-1)).getScaled( + 0.25)); + gc.drawOval((int) (points[i].x - 2), + (int) (points[i].y - 2), 4, 4); + } + for (int i = 0; i < 1; i++) { + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_DARK_RED)); + gc.drawLine((int) points[i].x, (int) points[i].y, + (int) points[i + 1].x, (int) points[i + 1].y); + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_BLACK)); + points[i] = points[i].getTranslated(points[i + 1] + .getTranslated(points[i].getScaled(-1)).getScaled( + 0.25)); + gc.drawOval((int) (points[i].x - 2), + (int) (points[i].y - 2), 4, 4); + } + } + }; + } + + protected AbstractControllableShape createControllableShape2(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + } + + @Override + public IGeometry createGeometry() { + return new Line(new Point(), new Point()); + } + + @Override + public void drawShape(GC gc) { + } + }; + } + + @Override + protected Point[] computeIntersections(IGeometry g1, IGeometry g2) { + return new Point[] {}; + } +} diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RegionExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RegionExample.java new file mode 100644 index 0000000..a8a9795 --- /dev/null +++ b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RegionExample.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2011 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.examples.demos; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample.AbstractControllableShape; +import org.eclipse.gef4.geometry.planar.IGeometry; +import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.Rectangle; +import org.eclipse.gef4.geometry.planar.Region; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; + +public class RegionExample extends AbstractIntersectionExample { + public static void main(String[] args) { + new RegionExample("Region Example"); + } + + public RegionExample(String title) { + super(title); + } + + protected AbstractControllableShape createControllableShape1(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + addControlPoint(new Point(100, 100)); + addControlPoint(new Point(200, 200)); + + addControlPoint(new Point(150, 150)); + addControlPoint(new Point(250, 250)); + } + + @Override + public Region createGeometry() { + Point[] cp = getControlPoints(); + Region region = new Region(new Rectangle(cp[0], cp[1]), + new Rectangle(cp[2], cp[3])); + return region; + } + + @Override + public void drawShape(GC gc) { + Region region = createGeometry(); + + gc.setClipping(region.toSWTRegion()); + + for (int y = 0; y < 800; y += 20) { + gc.drawString( + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", + 20, y); + } + + gc.setClipping((org.eclipse.swt.graphics.Region) null); + + gc.setAlpha(128); + gc.setBackground(Display.getCurrent().getSystemColor( + SWT.COLOR_BLUE)); + for (Rectangle r : region.getShapes()) { + gc.fillRectangle(r.toSWTRectangle()); + } + gc.setAlpha(255); + } + }; + } + + protected AbstractControllableShape createControllableShape2(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + } + + @Override + public IGeometry createGeometry() { + return new Line(-10, -10, -10, -10); + } + + @Override + public void drawShape(GC gc) { + } + }; + } + + @Override + protected Point[] computeIntersections(IGeometry g1, IGeometry g2) { + return new Point[] {}; + } +} diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RegionOutlineExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RegionOutlineExample.java new file mode 100644 index 0000000..2cc7466 --- /dev/null +++ b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RegionOutlineExample.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2011 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.examples.demos; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample.AbstractControllableShape; +import org.eclipse.gef4.geometry.planar.IGeometry; +import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.Rectangle; +import org.eclipse.gef4.geometry.planar.Region; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; + +public class RegionOutlineExample extends AbstractIntersectionExample { + public static void main(String[] args) { + new RegionOutlineExample("Region Example"); + } + + public RegionOutlineExample(String title) { + super(title); + } + + protected AbstractControllableShape createControllableShape1(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + addControlPoint(new Point(100, 0)); + addControlPoint(new Point(300, 100)); + + addControlPoint(new Point(250, 200)); + addControlPoint(new Point(350, 330)); + + addControlPoint(new Point(100, 200)); + addControlPoint(new Point(190, 325)); + + addControlPoint(new Point(150, 300)); + addControlPoint(new Point(280, 380)); + } + + @Override + public Region createGeometry() { + Point[] cp = getControlPoints(); + Rectangle[] rectangles = new Rectangle[cp.length / 2]; + for (int i = 0; i < rectangles.length; i++) { + rectangles[i] = new Rectangle(cp[2 * i], cp[2 * i + 1]); + // System.out.println("R" + i + " " + cp[2 * i] + "\\" + // + cp[2 * i + 1]); + } + Region region = new Region(rectangles); + return region; + } + + @Override + public void drawShape(GC gc) { + Region region = createGeometry(); + + gc.setAlpha(128); + gc.setBackground(Display.getCurrent().getSystemColor( + SWT.COLOR_BLUE)); + for (Rectangle r : region.getShapes()) { + gc.fillRectangle(r.toSWTRectangle()); + } + + gc.setAlpha(255); + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_RED)); + for (Rectangle r : region.getShapes()) { + gc.drawRectangle(r.toSWTRectangle()); + } + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_BLACK)); + for (Line l : region.getOutlineSegments()) { + gc.drawLine((int) (l.getX1()), (int) (l.getY1()), + (int) (l.getX2()), (int) (l.getY2())); + } + } + }; + } + + protected AbstractControllableShape createControllableShape2(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + } + + @Override + public IGeometry createGeometry() { + return new Line(-10, -10, -10, -10); + } + + @Override + public void drawShape(GC gc) { + } + }; + } + + @Override + protected Point[] computeIntersections(IGeometry g1, IGeometry g2) { + return new Point[] {}; + } +} diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RingExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RingExample.java new file mode 100644 index 0000000..fcd180a --- /dev/null +++ b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/RingExample.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.examples.demos; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample; +import org.eclipse.gef4.geometry.planar.IGeometry; +import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.Polygon; +import org.eclipse.gef4.geometry.planar.Ring; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; + +public class RingExample extends AbstractIntersectionExample { + public static void main(String[] args) { + new RingExample("Ring Example"); + } + + public RingExample(String title) { + super(title); + } + + protected AbstractControllableShape createControllableShape1(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + addControlPoint(new Point(100, 100)); + addControlPoint(new Point(400, 100)); + addControlPoint(new Point(400, 200)); + + addControlPoint(new Point(400, 100)); + addControlPoint(new Point(400, 400)); + addControlPoint(new Point(300, 400)); + + addControlPoint(new Point(400, 400)); + addControlPoint(new Point(100, 400)); + addControlPoint(new Point(100, 300)); + + addControlPoint(new Point(100, 400)); + addControlPoint(new Point(100, 100)); + addControlPoint(new Point(200, 100)); + } + + @Override + public Ring createGeometry() { + Point[] cp = getControlPoints(); + Polygon[] polygons = new Polygon[cp.length / 3]; + for (int i = 0; i < polygons.length; i++) { + polygons[i] = new Polygon(cp[3 * i], cp[3 * i + 1], + cp[3 * i + 2]); + // System.out.println("R" + i + " " + cp[2 * i] + "\\" + // + cp[2 * i + 1]); + } + Ring ring = new Ring(polygons); + return ring; + } + + @Override + public void drawShape(GC gc) { + Ring ring = createGeometry(); + + gc.setAlpha(64); + gc.setBackground(Display.getCurrent().getSystemColor( + SWT.COLOR_BLUE)); + for (Polygon p : ring.getShapes()) { + gc.fillPolygon(p.toSWTPointArray()); + } + + gc.setAlpha(255); + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_RED)); + for (Polygon p : ring.getShapes()) { + gc.drawPolygon(p.toSWTPointArray()); + } + + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_BLACK)); + int lineWidth = gc.getLineWidth(); + gc.setLineWidth(lineWidth + 2); + for (Line l : ring.getOutlineSegments()) { + gc.drawLine((int) (l.getX1()), (int) (l.getY1()), + (int) (l.getX2()), (int) (l.getY2())); + } + gc.setLineWidth(lineWidth); + + // gc.setForeground(Display.getCurrent().getSystemColor( + // SWT.COLOR_BLACK)); + // for (Line l : region.getOutlineSegments()) { + // gc.drawLine((int) (l.getX1()), (int) (l.getY1()), + // (int) (l.getX2()), (int) (l.getY2())); + // } + } + }; + } + + protected AbstractControllableShape createControllableShape2(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + } + + @Override + public IGeometry createGeometry() { + return new Line(-10, -10, -10, -10); + } + + @Override + public void drawShape(GC gc) { + } + }; + } + + @Override + protected Point[] computeIntersections(IGeometry g1, IGeometry g2) { + return new Point[] {}; + } +} diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/TriangulationExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/TriangulationExample.java new file mode 100644 index 0000000..fb052a1 --- /dev/null +++ b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/demos/TriangulationExample.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.examples.demos; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.examples.intersection.AbstractIntersectionExample; +import org.eclipse.gef4.geometry.planar.IGeometry; +import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.Polygon; +import org.eclipse.gef4.geometry.planar.Polygon.NonSimplePolygonException; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; + +public class TriangulationExample extends AbstractIntersectionExample { + public static void main(String[] args) { + new TriangulationExample("Triangulation Example"); + } + + public TriangulationExample(String title) { + super(title); + } + + protected AbstractControllableShape createControllableShape1(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + addControlPoint(new Point(300 / 2, 100 / 2)); + addControlPoint(new Point(100 / 2, 200 / 2)); + addControlPoint(new Point(200 / 2, 300 / 2)); + addControlPoint(new Point(100 / 2, 500 / 2)); + addControlPoint(new Point(300 / 2, 400 / 2)); + addControlPoint(new Point(500 / 2, 600 / 2)); + addControlPoint(new Point(600 / 2, 300 / 2)); + addControlPoint(new Point(500 / 2, 400 / 2)); + addControlPoint(new Point(500 / 2, 200 / 2)); + addControlPoint(new Point(300 / 2, 200 / 2)); + } + + @Override + public Polygon createGeometry() { + Point[] cp = getControlPoints(); + Polygon p = new Polygon(getControlPoints()); + return p; + } + + @Override + public void drawShape(GC gc) { + Polygon p = createGeometry(); + + // System.out.println("p = " + p); + + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_RED)); + + Polygon[] triangulation; + try { + triangulation = p.getTriangulation(); + } catch (NonSimplePolygonException x) { + triangulation = new Polygon[] { p }; + } + for (Polygon triangle : triangulation) { + gc.drawPolygon(triangle.toSWTPointArray()); + } + + int lineWidth = gc.getLineWidth(); + gc.setLineWidth(lineWidth + 2); + gc.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_BLACK)); + + gc.drawPolygon(p.toSWTPointArray()); + + gc.setLineWidth(lineWidth); + } + }; + } + + protected AbstractControllableShape createControllableShape2(Canvas canvas) { + return new AbstractControllableShape(canvas) { + @Override + public void createControlPoints() { + } + + @Override + public IGeometry createGeometry() { + return new Line(-10, -10, -10, -10); + } + + @Override + public void drawShape(GC gc) { + } + }; + } + + @Override + protected Point[] computeIntersections(IGeometry g1, IGeometry g2) { + return new Point[] {}; + } +} diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/AbstractIntersectionExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/AbstractIntersectionExample.java index 5137201..777fda7 100644 --- a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/AbstractIntersectionExample.java +++ b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/AbstractIntersectionExample.java @@ -333,11 +333,14 @@ public abstract class AbstractIntersectionExample implements PaintListener { } infoLabel.setText(infoText); + // open the shell before creating the controllable shapes so that their + // default coordinates are not changed due to the resize of their canvas + shell.open(); + controllableShape1 = createControllableShape1(shell); controllableShape2 = createControllableShape2(shell); shell.addPaintListener(this); - shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) { diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/BezierApproximationExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/BezierApproximationExample.java deleted file mode 100644 index 2bf2bb9..0000000 --- a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/BezierApproximationExample.java +++ /dev/null @@ -1,80 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2011 itemis AG and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Matthias Wienand (itemis AG) - initial API and implementation - * - *******************************************************************************/ -package org.eclipse.gef4.geometry.examples.intersection; - -import org.eclipse.gef4.geometry.Point; -import org.eclipse.gef4.geometry.planar.BezierCurve; -import org.eclipse.gef4.geometry.planar.IGeometry; -import org.eclipse.gef4.geometry.planar.Line; -import org.eclipse.gef4.geometry.planar.Path; -import org.eclipse.swt.graphics.GC; -import org.eclipse.swt.widgets.Canvas; -import org.eclipse.swt.widgets.Display; - -public class BezierApproximationExample extends AbstractIntersectionExample { - public static void main(String[] args) { - new BezierApproximationExample("Bezier Approximation Example"); - } - - public BezierApproximationExample(String title) { - super(title); - } - - protected AbstractControllableShape createControllableShape1(Canvas canvas) { - return new AbstractControllableShape(canvas) { - @Override - public void createControlPoints() { - addControlPoint(new Point(100, 100)); - addControlPoint(new Point(150, 400)); - addControlPoint(new Point(200, 300)); - addControlPoint(new Point(250, 150)); - addControlPoint(new Point(300, 250)); - addControlPoint(new Point(350, 200)); - addControlPoint(new Point(400, 350)); - } - - @Override - public IGeometry createGeometry() { - return new BezierCurve(getControlPoints()).toPath(); - } - - @Override - public void drawShape(GC gc) { - Path curve = (Path) createGeometry(); - gc.drawPath(new org.eclipse.swt.graphics.Path(Display - .getCurrent(), curve.toSWTPathData())); - } - }; - } - - protected AbstractControllableShape createControllableShape2(Canvas canvas) { - return new AbstractControllableShape(canvas) { - @Override - public void createControlPoints() { - } - - @Override - public IGeometry createGeometry() { - return new Line(new Point(), new Point(1, 1)); - } - - @Override - public void drawShape(GC gc) { - } - }; - } - - @Override - protected Point[] computeIntersections(IGeometry g1, IGeometry g2) { - return new Point[] {}; - } -} diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/ConvexHullExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/ConvexHullExample.java deleted file mode 100644 index 4e317f6..0000000 --- a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/ConvexHullExample.java +++ /dev/null @@ -1,82 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2011 itemis AG and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Matthias Wienand (itemis AG) - initial API and implementation - * - *******************************************************************************/ -package org.eclipse.gef4.geometry.examples.intersection; - -import org.eclipse.gef4.geometry.Point; -import org.eclipse.gef4.geometry.planar.IGeometry; -import org.eclipse.gef4.geometry.planar.Line; -import org.eclipse.gef4.geometry.planar.Polygon; -import org.eclipse.gef4.geometry.utils.PointListUtils; -import org.eclipse.swt.graphics.GC; -import org.eclipse.swt.widgets.Canvas; -import org.eclipse.swt.widgets.Display; - -public class ConvexHullExample extends AbstractIntersectionExample { - public static void main(String[] args) { - new ConvexHullExample("Convex Hull Example"); - } - - public ConvexHullExample(String title) { - super(title); - } - - protected AbstractControllableShape createControllableShape1(Canvas canvas) { - return new AbstractControllableShape(canvas) { - @Override - public void createControlPoints() { - addControlPoint(new Point(100, 100)); - addControlPoint(new Point(150, 400)); - addControlPoint(new Point(200, 300)); - addControlPoint(new Point(250, 150)); - addControlPoint(new Point(300, 250)); - addControlPoint(new Point(350, 200)); - addControlPoint(new Point(400, 350)); - } - - @Override - public IGeometry createGeometry() { - Polygon convexHull = new Polygon( - PointListUtils.getConvexHull(getControlPoints())); - return convexHull; - } - - @Override - public void drawShape(GC gc) { - Polygon convexHull = (Polygon) createGeometry(); - gc.drawPath(new org.eclipse.swt.graphics.Path(Display - .getCurrent(), convexHull.toPath().toSWTPathData())); - } - }; - } - - protected AbstractControllableShape createControllableShape2(Canvas canvas) { - return new AbstractControllableShape(canvas) { - @Override - public void createControlPoints() { - } - - @Override - public IGeometry createGeometry() { - return new Line(-10, -10, -10, -10); - } - - @Override - public void drawShape(GC gc) { - } - }; - } - - @Override - protected Point[] computeIntersections(IGeometry g1, IGeometry g2) { - return new Point[] {}; - } -} diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/CubicCurveDeCasteljauExample.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/CubicCurveDeCasteljauExample.java deleted file mode 100644 index 380f1b2..0000000 --- a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/intersection/CubicCurveDeCasteljauExample.java +++ /dev/null @@ -1,134 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2011 itemis AG and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Matthias Wienand (itemis AG) - initial API and implementation - * - *******************************************************************************/ -package org.eclipse.gef4.geometry.examples.intersection; - -import org.eclipse.gef4.geometry.Point; -import org.eclipse.gef4.geometry.planar.CubicCurve; -import org.eclipse.gef4.geometry.planar.IGeometry; -import org.eclipse.gef4.geometry.planar.Line; -import org.eclipse.gef4.geometry.planar.Rectangle; -import org.eclipse.swt.SWT; -import org.eclipse.swt.graphics.GC; -import org.eclipse.swt.widgets.Canvas; -import org.eclipse.swt.widgets.Display; - -public class CubicCurveDeCasteljauExample extends AbstractIntersectionExample { - public static void main(String[] args) { - new CubicCurveDeCasteljauExample("Cubic Bezier Curve Example"); - } - - public CubicCurveDeCasteljauExample(String title) { - super(title); - } - - protected AbstractControllableShape createControllableShape1(Canvas canvas) { - return new AbstractControllableShape(canvas) { - @Override - public void createControlPoints() { - addControlPoint(new Point(100, 200)); - addControlPoint(new Point(200, 100)); - addControlPoint(new Point(300, 300)); - addControlPoint(new Point(400, 200)); - } - - @Override - public IGeometry createGeometry() { - Point[] points = getControlPoints(); - - CubicCurve curve = new CubicCurve(points[0], points[1], - points[2], points[3]); - - return curve; - } - - @Override - public void drawShape(GC gc) { - CubicCurve curve = (CubicCurve) createGeometry(); - - // draw curve - gc.drawPath(new org.eclipse.swt.graphics.Path(Display - .getCurrent(), curve.toPath().toSWTPathData())); - - // draw bounds - Rectangle bounds = curve.getBounds(); - - gc.setForeground(Display.getCurrent().getSystemColor( - SWT.COLOR_DARK_GRAY)); - gc.drawRectangle(bounds.toSWTRectangle()); - - // draw lerps - Point[] points = getControlPoints(); - for (int i = 0; i < 3; i++) { - gc.setForeground(Display.getCurrent().getSystemColor( - SWT.COLOR_DARK_GREEN)); - gc.drawLine((int) points[i].x, (int) points[i].y, - (int) points[i + 1].x, (int) points[i + 1].y); - gc.setForeground(Display.getCurrent().getSystemColor( - SWT.COLOR_BLACK)); - points[i] = points[i].getTranslated(points[i + 1] - .getTranslated(points[i].getScaled(-1)).getScaled( - 0.25)); - gc.drawOval((int) (points[i].x - 2), - (int) (points[i].y - 2), 4, 4); - } - for (int i = 0; i < 2; i++) { - gc.setForeground(Display.getCurrent().getSystemColor( - SWT.COLOR_BLUE)); - gc.drawLine((int) points[i].x, (int) points[i].y, - (int) points[i + 1].x, (int) points[i + 1].y); - gc.setForeground(Display.getCurrent().getSystemColor( - SWT.COLOR_BLACK)); - points[i] = points[i].getTranslated(points[i + 1] - .getTranslated(points[i].getScaled(-1)).getScaled( - 0.25)); - gc.drawOval((int) (points[i].x - 2), - (int) (points[i].y - 2), 4, 4); - } - for (int i = 0; i < 1; i++) { - gc.setForeground(Display.getCurrent().getSystemColor( - SWT.COLOR_DARK_RED)); - gc.drawLine((int) points[i].x, (int) points[i].y, - (int) points[i + 1].x, (int) points[i + 1].y); - gc.setForeground(Display.getCurrent().getSystemColor( - SWT.COLOR_BLACK)); - points[i] = points[i].getTranslated(points[i + 1] - .getTranslated(points[i].getScaled(-1)).getScaled( - 0.25)); - gc.drawOval((int) (points[i].x - 2), - (int) (points[i].y - 2), 4, 4); - } - } - }; - } - - protected AbstractControllableShape createControllableShape2(Canvas canvas) { - return new AbstractControllableShape(canvas) { - @Override - public void createControlPoints() { - } - - @Override - public IGeometry createGeometry() { - return new Line(new Point(), new Point()); - } - - @Override - public void drawShape(GC gc) { - } - }; - } - - @Override - protected Point[] computeIntersections(IGeometry g1, IGeometry g2) { - return new Point[] {}; - } -} diff --git a/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/scalerotate/CubicCurveScaleRotate.java b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/scalerotate/CubicCurveScaleRotate.java new file mode 100644 index 0000000..0fa634a --- /dev/null +++ b/org.eclipse.gef4.geometry.examples/src/org/eclipse/gef4/geometry/examples/scalerotate/CubicCurveScaleRotate.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2011 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.examples.scalerotate; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.planar.CubicCurve; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; + +public class CubicCurveScaleRotate extends AbstractScaleRotateExample { + + public static void main(String[] args) { + new CubicCurveScaleRotate(); + } + + public CubicCurveScaleRotate() { + super("Scale/Rotate - CubicCurve"); + } + + @Override + protected AbstractScaleRotateShape createShape(Canvas canvas) { + return new AbstractScaleRotateShape(canvas) { + @Override + public boolean contains(Point p) { + return createGeometry().getBounds().contains(p); + } + + @Override + public CubicCurve createGeometry() { + double w = getCanvas().getClientArea().width; + double h = getCanvas().getClientArea().height; + double padx = w / 10; + double pady = h / 10; + + CubicCurve me = new CubicCurve(padx, pady, w + w, h, -w, h, w + - padx, pady); + me.rotateCW(getRotationAngle(), getCenter()); + me.scale(getZoomFactor(), getCenter()); + + return me; + } + + @Override + public void draw(GC gc) { + CubicCurve me = createGeometry(); + gc.fillRectangle(me.getBounds().toSWTRectangle()); + gc.drawPath(new org.eclipse.swt.graphics.Path(Display + .getCurrent(), me.toPath().toSWTPathData())); + } + }; + } +} diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/AllTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/AllTests.java index b7abe26..cf7c4de 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/AllTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/AllTests.java @@ -17,12 +17,14 @@ import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) -@SuiteClasses({ AngleTests.class, CubicCurveTests.class, CurveUtilsTests.class, - DimensionTests.class, EllipseTests.class, LineTests.class, - PointListUtilsTests.class, PointTests.class, PolygonTests.class, - PolylineTests.class, PolynomCalculationUtilsTests.class, - PrecisionUtilsTests.class, QuadraticCurveTests.class, - RectangleTests.class, StraightTests.class, VectorTests.class }) +@SuiteClasses({ AngleTests.class, BezierCurveTests.class, + CubicCurveTests.class, CurveUtilsTests.class, DimensionTests.class, + EllipseTests.class, LineTests.class, PointListUtilsTests.class, + PointTests.class, PolygonTests.class, PolylineTests.class, + PolynomCalculationUtilsTests.class, PrecisionUtilsTests.class, + QuadraticCurveTests.class, RectangleTests.class, RegionTests.class, + RingTests.class, RoundedRectangleTests.class, StraightTests.class, + VectorTests.class, Vector3DTests.class }) public class AllTests { } diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/AngleTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/AngleTests.java index 02c0278..ead4299 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/AngleTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/AngleTests.java @@ -74,6 +74,7 @@ public class AngleTests { assertFalse(alpha.equals(delta)); assertTrue(gamma.equals(delta)); assertTrue(delta.equals(gamma)); + assertFalse(alpha.equals(null)); assertFalse(alpha.equals(new Point())); alpha = new Angle(UNRECOGNIZABLE_FRACTION / 2, AngleUnit.RAD); @@ -81,6 +82,7 @@ public class AngleTests { AngleUnit.RAD); assertTrue(alpha.equals(beta)); assertTrue(beta.equals(alpha)); + } @Test diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/BezierCurveTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/BezierCurveTests.java new file mode 100644 index 0000000..88d3679 --- /dev/null +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/BezierCurveTests.java @@ -0,0 +1,345 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.planar.BezierCurve; +import org.eclipse.gef4.geometry.planar.CubicCurve; +import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.QuadraticCurve; +import org.eclipse.gef4.geometry.planar.Rectangle; +import org.eclipse.gef4.geometry.utils.PointListUtils; +import org.eclipse.gef4.geometry.utils.PrecisionUtils; +import org.junit.Test; + +public class BezierCurveTests { + + @Test + public void test_equals() { + BezierCurve c = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + assertFalse(c.equals(null)); + assertFalse(c.equals(new Rectangle(1, 2, 3, 4))); + assertEquals(c, c); + + BezierCurve cr = new BezierCurve(10, 10, 10, 1, 1, 10, 1, 1); + assertEquals(cr, cr); + assertEquals(c, cr); + assertEquals(cr, c); + + BezierCurve ce = c.getElevated(); + BezierCurve cre = cr.getElevated(); + assertEquals(c, ce); + assertEquals(ce, c); + assertEquals(cr, cre); + assertEquals(cre, cr); + assertEquals(c, cre); + assertEquals(cre, c); + assertEquals(cr, ce); + assertEquals(ce, cr); + + BezierCurve c2 = new BezierCurve(1, 2, 3, 4); + assertFalse(c.equals(c2)); + assertFalse(c2.equals(c)); + c2 = new BezierCurve(1, 1, 1, 10, 2, 3); + assertFalse(c.equals(c2)); + assertFalse(c2.equals(c)); + c2 = new BezierCurve(1, 1, 1, 10, 10, 1, 2, 3); + assertFalse(c.equals(c2)); + assertFalse(c2.equals(c)); + c2 = new BezierCurve(1, 1, 2, 9, 9, 2, 10, 10); + assertFalse(c.equals(c2)); + assertFalse(c2.equals(c)); + } + + @Test + public void test_constructors() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + assertEquals(c0, new BezierCurve(new Point(1, 1), new Point(1, 10), + new Point(10, 1), new Point(10, 10))); + assertEquals(c0, new BezierCurve(new CubicCurve(1, 1, 1, 10, 10, 1, 10, + 10))); + BezierCurve c1 = new BezierCurve(1, 1, 10, 1, 10, 10); + assertEquals(c1, new BezierCurve(new Point(1, 1), new Point(10, 1), + new Point(10, 10))); + assertEquals(c1, new BezierCurve( + new QuadraticCurve(1, 1, 10, 1, 10, 10))); + + // getCopy() + BezierCurve c0copy = c0.getCopy(); + assertEquals(c0, c0copy); + assertNotSame(c0, c0copy); + + c0copy.setP1(new Point(100, 100)); + assertFalse(c0.equals(c0copy)); + } + + @Test + public void test_contains_Point() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + assertFalse(c0.contains(new Point(0, 0))); + assertFalse(c0.contains(new Point(3, 3))); + assertFalse(c0.contains(new Point(3, 8))); + assertFalse(c0.contains(new Point(7, 3))); + assertFalse(c0.contains(new Point(7, 8))); + assertFalse(c0.contains(new Point(11, 11))); + assertTrue(c0.contains(new Point(1, 1))); + assertTrue(c0.contains(new Point(10, 10))); + + // evaluate curve at some parameter values and check that the returned + // points are contained by the curve + for (double t = 0; t <= 1; t += 0.02) + assertTrue(c0.contains(c0.get(t))); + } + + @Test + public void test_get() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + assertEquals(new Point(1, 1), c0.get(0)); + assertEquals(new Point(10, 10), c0.get(1)); + assertEquals(new Point(5.5, 5.5), c0.get(0.5)); + } + + @Test + public void test_getBounds() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + assertEquals(new Rectangle(1, 1, 9, 9), c0.getBounds()); + } + + @Test + public void test_getClipped() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + assertEquals(new BezierCurve(1, 1), c0.getClipped(0, 0)); + assertEquals(new BezierCurve(10, 10), c0.getClipped(1, 1)); + + BezierCurve c1 = c0.getClipped(0, 0.5); + BezierCurve c2 = c0.getClipped(0.5, 1); + assertEquals(new Point(1, 1), c1.get(0)); + assertEquals(new Point(5.5, 5.5), c1.get(1)); + assertEquals(new Point(5.5, 5.5), c2.get(0)); + assertEquals(new Point(10, 10), c2.get(1)); + } + + @Test + public void test_getControlBounds() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + assertEquals(new Rectangle(1, 1, 9, 9), c0.getControlBounds()); + + BezierCurve c1 = new BezierCurve(1, 5, 5, 8, 10, 1); + assertEquals(new Rectangle(1, 1, 9, 7), c1.getControlBounds()); + } + + @Test + public void test_getDerivative() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + BezierCurve d0 = c0.getDerivative(); + assertEquals(3, d0.getPoints().length); + // TODO: check the derivative for some points on the curve + } + + @Test + public void test_getOverlap() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + BezierCurve c1 = c0.getClipped(0, 0.5); + BezierCurve c2 = c0.getClipped(0.5, 1); + + BezierCurve o01 = c0.getOverlap(c1); + assertTrue(o01 != null); + assertTrue(c1.contains(o01)); + // assertEquals(c1, o01); + + /* + * TODO: The equality check may not return true for the computed overlap + * and the real overlap. This is because the overlap is only + * approximated. + */ + + BezierCurve o02 = c0.getOverlap(c2); + assertTrue(o02 != null); + assertTrue(c2.contains(o02)); + // assertEquals(c2, o02); + + assertNull(c1.getOverlap(c2)); + } + + private void check_values_with_getters(BezierCurve c, Point... points) { + Point p1 = points[0]; + assertEquals(p1, c.getP1()); + assertTrue(PrecisionUtils.equal(p1.x, c.getX1())); + assertTrue(PrecisionUtils.equal(p1.y, c.getY1())); + + Point p2 = points[points.length - 1]; + assertEquals(p2, c.getP2()); + assertTrue(PrecisionUtils.equal(p2.x, c.getX2())); + assertTrue(PrecisionUtils.equal(p2.y, c.getY2())); + + assertTrue(PointListUtils.equals(c.getPoints(), points)); + + for (int i = 0; i < points.length; i++) + assertEquals(points[i], c.getPoint(i)); + } + + @Test + public void test_point_getters() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + check_values_with_getters(c0, new Point[] { new Point(1, 1), + new Point(1, 10), new Point(10, 1), new Point(10, 10) }); + } + + @Test + public void test_point_setters() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + check_values_with_getters(c0, new Point[] { new Point(1, 1), + new Point(1, 10), new Point(10, 1), new Point(10, 10) }); + + c0.setP1(new Point(-30, 5)); + check_values_with_getters(c0, new Point[] { new Point(-30, 5), + new Point(1, 10), new Point(10, 1), new Point(10, 10) }); + + c0.setP2(new Point(31, 11)); + check_values_with_getters(c0, new Point[] { new Point(-30, 5), + new Point(1, 10), new Point(10, 1), new Point(31, 11) }); + + c0.setPoint(1, new Point(3, -3)); + check_values_with_getters(c0, new Point[] { new Point(-30, 5), + new Point(3, -3), new Point(10, 1), new Point(31, 11) }); + + c0.setPoint(2, new Point(-3, 3)); + check_values_with_getters(c0, new Point[] { new Point(-30, 5), + new Point(3, -3), new Point(-3, 3), new Point(31, 11) }); + } + + @Test + public void test_getParameterAt() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + assertTrue(PrecisionUtils.equal(0, c0.getParameterAt(new Point(1, 1)))); + assertTrue(PrecisionUtils + .equal(1, c0.getParameterAt(new Point(10, 10)))); + assertTrue(PrecisionUtils.equal(0.5, + c0.getParameterAt(new Point(5.5, 5.5)))); + + boolean thrown = false; + try { + c0.getParameterAt(null); + } catch (NullPointerException x) { + thrown = true; + } + assertTrue(thrown); + + thrown = false; + try { + c0.getParameterAt(new Point(3, 3)); + } catch (IllegalArgumentException x) { + thrown = true; + } + assertTrue(thrown); + } + + @Test + public void test_getTranslated() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + BezierCurve t0 = c0.getTranslated(new Point(-1, 4)); + assertEquals(new BezierCurve(0, 5, 0, 14, 9, 5, 9, 14), t0); + } + + @Test + public void test_overlaps() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + BezierCurve c1 = c0.getClipped(0, 0.5); + BezierCurve c2 = c0.getClipped(0.5, 1); + assertTrue(c0.overlaps(c1)); + assertTrue(c1.overlaps(c0)); + assertTrue(c0.overlaps(c2)); + assertTrue(c2.overlaps(c0)); + assertFalse(c1.overlaps(c2)); + assertFalse(c2.overlaps(c1)); + } + + @Test + public void test_split() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + BezierCurve c1 = c0.getClipped(0, 0.5); + BezierCurve c2 = c0.getClipped(0.5, 1); + BezierCurve[] split = c0.split(0.5); + assertEquals(c1, split[0]); + assertEquals(c2, split[1]); + } + + @Test + public void test_toBezier() { + BezierCurve c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + BezierCurve[] beziers = c0.toBezier(); + assertEquals(1, beziers.length); + assertEquals(c0, beziers[0]); + } + + @Test + public void test_toCubic() { + BezierCurve c0 = new BezierCurve(1, 1); + assertNull(c0.toCubic()); + c0 = new BezierCurve(1, 1, 1, 10); + assertNull(c0.toCubic()); + c0 = new BezierCurve(1, 1, 1, 10, 10, 1); + assertNull(c0.toCubic()); + c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + assertEquals(new CubicCurve(1, 1, 1, 10, 10, 1, 10, 10), c0.toCubic()); + c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 67, 89, 10, 10); + assertEquals(new CubicCurve(1, 1, 1, 10, 10, 1, 10, 10), c0.toCubic()); + c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10, 98, 76); + assertEquals(new CubicCurve(1, 1, 1, 10, 10, 1, 98, 76), c0.toCubic()); + } + + @Test + public void test_toQuadratic() { + BezierCurve c0 = new BezierCurve(1, 1); + assertNull(c0.toQuadratic()); + c0 = new BezierCurve(1, 1, 1, 10); + assertNull(c0.toQuadratic()); + c0 = new BezierCurve(1, 1, 1, 10, 10, 1); + assertEquals(new QuadraticCurve(1, 1, 1, 10, 10, 1), c0.toQuadratic()); + c0 = new BezierCurve(1, 1, 1, 10, 67, 89, 10, 1); + assertEquals(new QuadraticCurve(1, 1, 1, 10, 10, 1), c0.toQuadratic()); + c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 98, 76); + assertEquals(new QuadraticCurve(1, 1, 1, 10, 98, 76), c0.toQuadratic()); + } + + @Test + public void test_toLine() { + BezierCurve c0 = new BezierCurve(1, 1); + assertNull(c0.toCubic()); + c0 = new BezierCurve(1, 1, 1, 10); + assertEquals(new Line(1, 1, 1, 10), c0.toLine()); + c0 = new BezierCurve(1, 1, 1, 10, 10, 1); + assertEquals(new Line(1, 1, 10, 1), c0.toLine()); + c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10); + assertEquals(new Line(1, 1, 10, 10), c0.toLine()); + c0 = new BezierCurve(1, 1, 1, 10, 10, 1, 10, 10, 98, 76); + assertEquals(new Line(1, 1, 98, 76), c0.toLine()); + } + + @Test + public void test_toLineStrip() { + BezierCurve linear = new BezierCurve(0, 0, 1, 1); + Line[] lines = linear.toLineStrip(1); + assertEquals(1, lines.length); + assertEquals(new Line(0, 0, 1, 1), lines[0]); + assertEquals(linear.toLine(), lines[0]); + + // TODO: check complicated curves, too + } + +} diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/CurveUtilsTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/CurveUtilsTests.java index b81d8d8..186cfdb 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/CurveUtilsTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/CurveUtilsTests.java @@ -154,7 +154,7 @@ public class CurveUtilsTests { } @Test - public void test_clip_with_QuadraticCurve() { + public void test_getClipped_with_QuadraticCurve() { final int numPoints = 4; final double step = 0.123456789; @@ -169,7 +169,7 @@ public class CurveUtilsTests { QuadraticCurve c = new QuadraticCurve(points); for (double t1 = 0; t1 <= 1; t1 += step) { for (double t2 = 0; t2 <= 1; t2 += step) { - QuadraticCurve cc = c.clip(t1, t2); + QuadraticCurve cc = c.getClipped(t1, t2); assertEquals(c.get(t1), cc.get(0)); assertEquals(c.get(t2), cc.get(1)); } diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/DimensionTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/DimensionTests.java index 634af06..1837d62 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/DimensionTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/DimensionTests.java @@ -45,9 +45,12 @@ public class DimensionTests { public void test_contains() { Dimension d1 = new Dimension(0.1, 0.1); Dimension d2 = new Dimension(0.2, 0.2); + Dimension d3 = new Dimension(0.1, 0.3); assertTrue(d2.contains(d1)); assertFalse(d1.contains(d2)); + assertFalse(d2.contains(d3)); + assertTrue(d3.contains(d1)); } @Test @@ -64,16 +67,20 @@ public class DimensionTests { */ @Test public void test_equals() { - Dimension p1 = new Dimension(0.1, 0.1); - Dimension p2 = new Dimension(0.2, 0.2); - assertFalse(p1.equals(p2)); + Dimension d1 = new Dimension(0.1, 0.1); + Dimension d2 = new Dimension(0.2, 0.2); + Dimension d3 = new Dimension(0.1, 0.2); + Dimension d4 = new Dimension(0.2, 0.1); + assertFalse(d1.equals(d2)); + assertFalse(d1.equals(d3)); + assertFalse(d1.equals(d4)); - p1 = new Dimension(0.2, 0.2); - assertTrue(p1.equals(p2)); + d1 = new Dimension(0.2, 0.2); + assertTrue(d1.equals(d2)); // wrong type - p1 = new Dimension(1, 1); - assertFalse(p1.equals(new org.eclipse.swt.graphics.Point(1, 1))); + d1 = new Dimension(1, 1); + assertFalse(d1.equals(new org.eclipse.swt.graphics.Point(1, 1))); } @Test diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/EllipseTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/EllipseTests.java index 83d7dcf..94039e1 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/EllipseTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/EllipseTests.java @@ -11,17 +11,19 @@ * *******************************************************************************/ package org.eclipse.gef4.geometry.tests; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.planar.CubicCurve; import org.eclipse.gef4.geometry.planar.Ellipse; +import org.eclipse.gef4.geometry.planar.IGeometry; import org.eclipse.gef4.geometry.planar.Line; import org.eclipse.gef4.geometry.planar.Rectangle; import org.junit.Test; - /** * Unit tests for {@link Ellipse}. * @@ -34,35 +36,32 @@ public class EllipseTests { .getPrecisionFraction(); @Test - public void test_contains_with_Point() { + public void test_equals() { + Ellipse e = new Ellipse(0, 0, 100, 50); + assertFalse(e.equals(null)); + assertFalse(e.equals(new Point())); + assertEquals(e, e); + assertEquals(e, new Ellipse(0, 0, 100, 50)); + assertEquals(e, new Ellipse(new Rectangle(0, 0, 100, 50))); + assertEquals(e, e.getCopy()); + assertFalse(e.equals(new Ellipse(0, 0, 100, 10))); + assertFalse(e.equals(new Ellipse(0, 0, 10, 50))); + assertFalse(e.equals(new Ellipse(10, 0, 100, 50))); + assertFalse(e.equals(new Ellipse(0, 10, 100, 50))); + } + + @Test + public void test_contains_Point() { Rectangle r = new Rectangle(34.3435, 56.458945, 123.3098, 146.578); Ellipse e = new Ellipse(r); - assertTrue(e.contains(r.getCentroid())); - - assertTrue(e.contains(r.getLeft())); - assertTrue(e.contains(r.getLeft().getTranslated(PRECISION_FRACTION * 1, - 0))); - assertFalse(e.contains(r.getLeft().getTranslated( - -PRECISION_FRACTION * 1000, 0))); + checkPointContainment(r, e); + // these things could not be tested in the general case, because of + // AWT's behavior assertTrue(e.contains(r.getTop())); - assertTrue(e.contains(r.getTop().getTranslated(0, - PRECISION_FRACTION * 100))); - assertFalse(e.contains(r.getTop().getTranslated(0, - -PRECISION_FRACTION * 100))); - assertTrue(e.contains(r.getRight())); - assertTrue(e.contains(r.getRight().getTranslated( - -PRECISION_FRACTION * 100, 0))); - assertFalse(e.contains(r.getRight().getTranslated( - PRECISION_FRACTION * 100, 0))); - assertTrue(e.contains(r.getBottom())); - assertTrue(e.contains(r.getBottom().getTranslated(0, - -PRECISION_FRACTION * 100))); - assertFalse(e.contains(r.getBottom().getTranslated(0, - PRECISION_FRACTION * 100))); for (Point p : e.getIntersections(new Line(r.getTopLeft(), r .getBottomRight()))) { @@ -72,10 +71,75 @@ public class EllipseTests { .getBottomLeft()))) { assertTrue(e.contains(p)); } + + for (CubicCurve c : e.getOutlineSegments()) { + assertTrue(e.contains(c.get(0.5))); + } + } + + private void checkPointContainment(Rectangle r, IGeometry g) { + assertFalse(g.contains(r.getTopLeft())); + assertFalse(g.contains(r.getTopRight())); + assertFalse(g.contains(r.getBottomLeft())); + assertFalse(g.contains(r.getBottomRight())); + + assertTrue(g.contains(r.getCentroid())); + + assertTrue(g.contains(r.getLeft())); + assertTrue(g.contains(r.getLeft().getTranslated(PRECISION_FRACTION * 1, + 0))); + assertFalse(g.contains(r.getLeft().getTranslated( + -PRECISION_FRACTION * 1000, 0))); + + // due to AWT's behavior, we won't check getTop() but a point very near + // to it, so that the Path() will survive these tests, too + assertTrue(g.contains(r.getTop().getTranslated(0, 1))); + assertTrue(g.contains(r.getTop().getTranslated(0, + PRECISION_FRACTION * 100))); + assertFalse(g.contains(r.getTop().getTranslated(0, + -PRECISION_FRACTION * 100))); + + // due to AWT's behavior, we won't check getRight() but a point very + // near to it, so that the Path() will survive these tests, too + assertTrue(g.contains(r.getRight().getTranslated(-1, 0))); + assertTrue(g.contains(r.getRight().getTranslated( + -PRECISION_FRACTION * 100, 0))); + assertFalse(g.contains(r.getRight().getTranslated( + PRECISION_FRACTION * 100, 0))); + + // due to AWT's behavior, we won't check getBottom() but a point very + // near to it, so that the Path() will survive these tests, too + assertTrue(g.contains(r.getBottom().getTranslated(0, -1))); + assertTrue(g.contains(r.getBottom().getTranslated(0, + -PRECISION_FRACTION * 100))); + assertFalse(g.contains(r.getBottom().getTranslated(0, + PRECISION_FRACTION * 100))); + } + + @Test + public void test_contains_Line() { + Ellipse e = new Ellipse(0, 0, 100, 50); + assertFalse(e.contains(new Line(-10, -10, 10, -10))); + assertFalse(e.contains(new Line(-10, -10, 50, 50))); + assertTrue(e.contains(new Line(1, 25, 99, 25))); + assertTrue(e.contains(new Line(0, 25, 100, 25))); } @Test - public void test_intersects_with_Line() { + public void test_getCenter() { + Ellipse e = new Ellipse(0, 0, 100, 50); + assertEquals(new Point(50, 25), e.getCenter()); + e.scale(2); + assertEquals(new Point(50, 25), e.getCenter()); + e.scale(0.5); + e.scale(2, new Point()); + assertEquals(new Point(100, 50), e.getCenter()); + e.translate(-100, -50); + assertEquals(new Point(), e.getCenter()); + } + + @Test + public void test_intersects_Line() { Rectangle r = new Rectangle(34.3435, 56.458945, 123.3098, 146.578); Ellipse e = new Ellipse(r); for (Line l : r.getOutlineSegments()) { @@ -83,8 +147,45 @@ public class EllipseTests { } } + private void checkPoints(Point[] expected, Point[] obtained) { + assertEquals(expected.length, obtained.length); + for (Point e : expected) { + boolean found = false; + for (Point o : obtained) { + if (e.equals(o)) { + found = true; + break; + } + } + assertTrue(found); + } + } + @Test - public void test_get_intersections_with_Ellipse_strict() { + public void test_getIntersections_Line() { + Ellipse e = new Ellipse(0, 0, 100, 50); + Line lh = new Line(0, 25, 100, 25); + Point[] is = e.getIntersections(lh); + checkPoints(new Point[] { new Point(0, 25), new Point(100, 25) }, is); + Line lv = new Line(50, 0, 50, 50); + is = e.getIntersections(lv); + checkPoints(new Point[] { new Point(50, 0), new Point(50, 50) }, is); + + lh = lh.getTranslated(new Point(0, -25)).toLine(); + is = e.getIntersections(lh); + checkPoints(new Point[] { new Point(50, 0) }, is); + + lv = lv.getTranslated(new Point(-50, 0)).toLine(); + is = e.getIntersections(lv); + checkPoints(new Point[] { new Point(0, 25) }, is); + + Line li = new Line(-100, 100, 0, 50); + is = e.getIntersections(li); + assertEquals(0, is.length); + } + + @Test + public void test_get_intersections_Ellipse_strict() { Rectangle r = new Rectangle(34.3435, 56.458945, 123.3098, 146.578); Ellipse e1 = new Ellipse(r); Ellipse e2 = new Ellipse(r); @@ -203,7 +304,7 @@ public class EllipseTests { } @Test - public void test_getIntersections_with_Ellipse_tolerance() { + public void test_getIntersections_Ellipse_tolerance() { Rectangle r = new Rectangle(34.3435, 56.458945, 123.3098, 146.578); Ellipse e1 = new Ellipse(r); Ellipse e2 = new Ellipse(r); @@ -245,7 +346,7 @@ public class EllipseTests { // @Ignore("This test is too strict. For a liberal test see below: test_getIntersections_with_Ellipse_Bezier_special_tolerance") @Test - public void test_getIntersections_with_Ellipse_Bezier_special() { + public void test_getIntersections_Ellipse_Bezier_special() { // 3 nearly tangential intersections Ellipse e1 = new Ellipse(126, 90, 378, 270); Ellipse e2 = new Ellipse(222, 77, 200, 200); @@ -263,7 +364,7 @@ public class EllipseTests { } @Test - public void test_getIntersections_with_Ellipse_Bezier_special_tolerance() { + public void test_getIntersections_Ellipse_Bezier_special_tolerance() { // 3 nearly tangential intersections Ellipse e1 = new Ellipse(126, 90, 378, 270); Ellipse e2 = new Ellipse(222, 77, 200, 200); @@ -279,4 +380,17 @@ public class EllipseTests { intersectionsTolerance(e1, e2); // TODO: find out the 3 expected points } + @Test + public void test_toPath() { + Rectangle r = new Rectangle(0, 0, 100, 50); + Ellipse e = new Ellipse(r); + checkPointContainment(r, e.toPath()); + } + + @Test + public void test_toString() { + Ellipse e = new Ellipse(0, 0, 100, 50); + assertEquals("Ellipse (0.0, 0.0, 100.0, 50.0)", e.toString()); + } + } diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/LineTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/LineTests.java index c6a4b53..f6ab42c 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/LineTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/LineTests.java @@ -14,6 +14,7 @@ package org.eclipse.gef4.geometry.tests; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -224,6 +225,7 @@ public class LineTests { // intersection within precision, no real intersection Line close = new Line(new Point(-5, UNRECOGNIZABLE_FRACTION), new Point(5, UNRECOGNIZABLE_FRACTION)); + // parallel so we do not return an intersection point assertNull(normal.getIntersection(close)); assertNull(close.getIntersection(normal)); @@ -254,6 +256,12 @@ public class LineTests { Line elsewhere = new Line(new Point(-5, 1), new Point(5, 10)); assertNull(normal.getIntersection(elsewhere)); assertNull(elsewhere.getIntersection(normal)); + + // single end point intersection with parallel lines: + // X-------X-------X + Line l1 = new Line(400.0, 102.48618784530387, 399.99999999999994, 100.0); + Line l2 = new Line(400.0, 51.10497237569061, 399.99999999999994, 100.0); + assertNotNull(l1.getIntersection(l2)); } @Test diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PointTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PointTests.java index 6d81c3e..945cf34 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PointTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PointTests.java @@ -126,6 +126,10 @@ public class PointTests { q = new Point(3, 6); assertTrue(p.getScaled(3, 6).equals(q)); assertTrue(q.getScaled(1f / 3f, 1f / 6f).equals(p)); + + // scale around some other point + Point c = new Point(10, 10); + assertEquals(new Point(9, 8), q.getScaled(1d / 7d, 1d / 2d, c)); } @Test diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PolygonTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PolygonTests.java index 49e40bd..e4e9ecd 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PolygonTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PolygonTests.java @@ -90,6 +90,11 @@ public class PolygonTests { assertFalse(new Polygon(new Point(), new Point(0, 5), new Point(5, 5), new Point(5, 0), new Point(2.5, 2.5)).contains(new Line(1, 2, 4, 2))); + + Polygon mouth = new Polygon(new Point(0, 5), new Point(2, 1), + new Point(4, 1), new Point(6, 5), new Point(4, 6), new Point(6, + 7), new Point(4, 10), new Point(2, 10)); + assertFalse(mouth.contains(new Line(6, 5, 6, 7))); } /** @@ -496,6 +501,24 @@ public class PolygonTests { } @Test + public void test_getTriangulation() { + Polygon p = new Polygon(150.0, 50.0, 50.0, 100.0, 23.0, 165.0, 50.0, + 250.0, 135.0, 294.0, 250.0, 300.0, 137.0, 260.0, 63.0, 168.0, + 113.0, 105.0, 136.0, 206.0, 150.0, 50.0); + + // test that it does not throw a NullPointerException + p.getTriangulation(); + assertTrue(true); + // TODO: test that the triangulation is correct + + p = new Polygon(150.0, 50.0, 50.0, 100.0, 32.0, 168.0, 50.0, 250.0, + 136.0, 298.0, 250.0, 300.0, 122.0, 252.0, 67.0, 180.0, 114.0, + 95.0, 136.0, 194.0, 150.0, 50.0); + p.getTriangulation(); + assertTrue(true); + } + + @Test public void test_intersects_Ellipse() { assertTrue(RHOMB.touches(new Ellipse(0, 0, 4, 4))); } diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PolylineTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PolylineTests.java index cf74fe0..0a6e498 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PolylineTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/PolylineTests.java @@ -12,17 +12,35 @@ *******************************************************************************/ package org.eclipse.gef4.geometry.tests; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.planar.Line; import org.eclipse.gef4.geometry.planar.Polyline; import org.junit.Test; public class PolylineTests { - private static final Polyline POLYLINE = new Polyline(new Point[] { - new Point(0, 0), new Point(1, 0), new Point(6, 5) }); + private static final Point[] POINTS = new Point[] { new Point(0, 0), + new Point(1, 0), new Point(6, 5) }; + private static final Polyline POLYLINE = new Polyline(POINTS); + + @Test + public void test_equals() { + assertEquals(POLYLINE, POLYLINE); + assertFalse(POLYLINE.equals((Object) null)); + assertFalse(POLYLINE.equals(new Line(1, 2, 3, 4))); + + List points = Arrays.asList(POINTS); + Collections.reverse(points); + assertEquals(POLYLINE, new Polyline(points.toArray(new Point[] {}))); + } @Test public void test_contains_with_Point() { diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RectangleTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RectangleTests.java index 4ac9488..5dc75a5 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RectangleTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RectangleTests.java @@ -40,10 +40,6 @@ public class RectangleTests { void action(Rectangle rect, Point tl, Point br); } - private interface IPairAction { - void action(Rectangle r1, Rectangle r2); - } - private static final double PRECISION_FRACTION = TestUtils .getPrecisionFraction(); @@ -53,32 +49,6 @@ public class RectangleTests { private static final double UNRECOGNIZABLE_FRACTION = PRECISION_FRACTION - PRECISION_FRACTION / 10; - private void forRectanglePairs(IPairAction action) { - for (double x11 = -2; x11 <= 2.4; x11 += 1.1) { - for (double y11 = -2; y11 <= 2.4; y11 += 1.1) { - Point p11 = new Point(x11, y11); - for (double x12 = -2; x12 <= 2.4; x12 += 1.1) { - for (double y12 = -2; y12 <= 2.4; y12 += 1.1) { - Point p12 = new Point(x12, y12); - Rectangle r1 = new Rectangle(p11, p12); - for (double x21 = -2; x21 <= 2.4; x21 += 1.1) { - for (double y21 = -2; y21 <= 2.4; y21 += 1.1) { - Point p21 = new Point(x21, y21); - for (double x22 = -2; x22 <= 2.4; x22 += 1.1) { - for (double y22 = -2; y22 <= 2.4; y22 += 1.1) { - Point p22 = new Point(x22, y22); - Rectangle r2 = new Rectangle(p21, p22); - action.action(r1, r2); - } - } - } - } - } - } - } - } - } - private void forRectangles(IAction action) { for (double x1 = -2; x1 <= 2; x1 += 0.4) { for (double y1 = -2; y1 <= 2; y1 += 0.4) { @@ -198,6 +168,12 @@ public class RectangleTests { assertTrue(recognizableExpanded.contains(preciseRect)); assertFalse(recognizableShrinked.contains(preciseRect)); assertFalse(recognizableShrinked.contains(recognizableExpanded)); + + // Regression test for a contains() bug that caused false positives for + // a "containing" Rectangle with smaller x and y coordinates and greater + // width and height as the "contained" one. + assertFalse(new Rectangle(0, 0, 100, 100).contains(new Rectangle(200, + 200, 1, 1))); } @Test diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RegionTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RegionTests.java new file mode 100644 index 0000000..e042f32 --- /dev/null +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RegionTests.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.eclipse.gef4.geometry.planar.Rectangle; +import org.eclipse.gef4.geometry.planar.Region; +import org.eclipse.gef4.geometry.utils.PrecisionUtils; +import org.junit.Test; + +public class RegionTests { + + @Test + public void test_constructor() { + Region region = new Region(); + assertEquals(0, region.getShapes().length); + + region = new Region(new Rectangle(0, 0, 100, 100)); + assertEquals(1, region.getShapes().length); + + region = new Region(region); + assertEquals(1, region.getShapes().length); + } + + @Test + public void test_copy_semantics() { + Rectangle r1 = new Rectangle(0, 0, 100, 100); + Region a1 = new Region(r1); + r1.setWidth(50); + + // constructor copies Rectangles: + // changing r1 does not change a1 + assertTrue(PrecisionUtils.equal(100, a1.getShapes()[0].getWidth())); + + Region a2 = a1.getCopy(); + a2.getShapes()[0].setWidth(50); + + // getCopy() copies Rectangles: + // changing a2 does not change a1 + assertTrue(PrecisionUtils.equal(100, a1.getShapes()[0].getWidth())); + + a2 = new Region(a1); + a2.getShapes()[0].setWidth(50); + + // constructor copies Rectangles: + // changing a2 does not change a1 + assertTrue(PrecisionUtils.equal(100, a1.getShapes()[0].getWidth())); + } + + @Test + public void test_cover_single_rectangle() { + Rectangle r1 = new Rectangle(100, 100, 100, 100); + Region region = new Region(r1); + + assertTrue(region.contains(r1)); + assertTrue( + "A Region of just a single Rectangle should use this Rectangle as its only internal shape.", + region.getShapes()[0].equals(r1)); + + assertFalse(region.contains(new Rectangle(0, 0, 50, 50))); + assertFalse(region.contains(new Rectangle(50, 50, 100, 100))); + } + + @Test + public void test_cover_two_distinct_rectangles() { + Rectangle r1 = new Rectangle(100, 100, 100, 100); + Rectangle r2 = new Rectangle(500, 100, 100, 100); + Region region = new Region(r1, r2); + + assertTrue(region.contains(r1)); + assertTrue(region.contains(r2)); + + assertFalse(region.contains(new Rectangle(0, 0, 50, 50))); + assertFalse(region.contains(new Rectangle(50, 50, 100, 100))); + } + + @Test + public void test_cover_two_intersecting_rectangles() { + Rectangle r1 = new Rectangle(50, 50, 50, 200); + Rectangle r2 = new Rectangle(50, 200, 200, 50); + Region region = new Region(r1, r2); + + assertTrue(region.contains(r1)); + assertTrue(region.contains(r2)); + assertTrue(region.contains(r1.getIntersected(r2))); + + assertFalse(region.contains(new Rectangle(0, 0, 10, 10))); + assertFalse(region.contains(new Rectangle(25, 25, 50, 50))); + } + +} diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RingTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RingTests.java new file mode 100644 index 0000000..1a72166 --- /dev/null +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RingTests.java @@ -0,0 +1,1120 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.Polygon; +import org.eclipse.gef4.geometry.planar.Ring; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +@RunWith(Enclosed.class) +public class RingTests { + + public static class TriangulateTriangleWithOutlinePoints { + + private Polygon[] triangulate(Polygon p, Point p1, Point p2) { + try { + Class parameterTypes[] = new Class[] { Polygon.class, + Point.class, Point.class }; + Method triangulate = Ring.class.getDeclaredMethod( + "triangulate", parameterTypes); + triangulate.setAccessible(true); + return (Polygon[]) triangulate.invoke(null, p, p1, p2); + } catch (Exception x) { + throw new IllegalStateException(x); + } + } + + Polygon p; + + @Before + public void setUp() { + p = new Polygon(new Point(100, 100), new Point(100, 300), + new Point(300, 200)); + } + + @Test + public void no_polygon() { + try { + triangulate(null, new Point(0, 0), new Point(1, 1)); + } catch (IllegalStateException x) { + Throwable cause = x; + while (cause.getCause() != null) + cause = cause.getCause(); + + assertTrue(cause.getClass().equals( + IllegalArgumentException.class)); + } + } + + @Test + public void no_triangle() { + try { + triangulate(new Polygon(0, 0, 1, 0, 1, 1, 0, 1), + new Point(0, 0), new Point(1, 1)); + } catch (IllegalStateException x) { + Throwable cause = x; + while (cause.getCause() != null) + cause = cause.getCause(); + + assertTrue(cause.getClass().equals( + IllegalArgumentException.class)); + } + } + + @Test + public void p1_not_on_polygon() { + try { + triangulate(new Polygon(0, 0, 1, 1, 0, 1), new Point(1, 0), + new Point(0, 1)); + } catch (IllegalStateException x) { + Throwable cause = x; + while (cause.getCause() != null) + cause = cause.getCause(); + + assertTrue(cause.getClass().equals( + IllegalArgumentException.class)); + } + } + + @Test + public void p2_not_on_polygon() { + try { + triangulate(new Polygon(0, 0, 1, 1, 0, 1), new Point(0, 1), + new Point(1, 0)); + } catch (IllegalStateException x) { + Throwable cause = x; + while (cause.getCause() != null) + cause = cause.getCause(); + + assertTrue(cause.getClass().equals( + IllegalArgumentException.class)); + } + } + + @Test + public void both_points_on_first_edge() { + // inner + Polygon[] r = triangulate(p, new Point(100, 150), new Point(100, + 250)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(100, 250), new Point(100, 150)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + // start - inner + r = triangulate(p, new Point(100, 100), new Point(100, 200)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(100, 200), new Point(100, 100)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + // end - inner + r = triangulate(p, new Point(100, 300), new Point(100, 200)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(100, 200), new Point(100, 300)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + // start - end + r = triangulate(p, new Point(100, 100), new Point(100, 300)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(100, 300), new Point(100, 100)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + } + + @Test + public void both_points_on_second_edge() { + // inner + Polygon[] r = triangulate(p, new Point(250, 225), new Point(150, + 275)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(150, 275), new Point(250, 225)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + // start - inner + r = triangulate(p, new Point(300, 200), new Point(200, 250)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(200, 250), new Point(300, 200)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + // end - inner + r = triangulate(p, new Point(100, 300), new Point(200, 250)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(200, 250), new Point(100, 300)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + // start - end + r = triangulate(p, new Point(100, 300), new Point(300, 200)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(300, 200), new Point(100, 300)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + } + + @Test + public void both_points_on_third_edge() { + // inner + Polygon[] r = triangulate(p, new Point(150, 125), new Point(250, + 175)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(250, 175), new Point(150, 125)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + // start - inner + r = triangulate(p, new Point(100, 100), new Point(250, 175)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(250, 175), new Point(100, 100)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + // end - inner + r = triangulate(p, new Point(150, 125), new Point(300, 200)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(300, 200), new Point(150, 125)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + // start - end + r = triangulate(p, new Point(100, 100), new Point(300, 200)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + + r = triangulate(p, new Point(300, 200), new Point(100, 100)); + assertEquals("1 resulting polygon", 1, r.length); + assertEquals("result equal to original", r[0], p); + assertNotSame("a copy is returned", r[0], p); + } + + private static boolean exists(Polygon[] list, Polygon item) { + for (Polygon p : list) + if (p.equals(item)) + return true; + return false; + } + + @Test + public void edge1_and_edge2() { + Polygon[] r = triangulate(p, new Point(100, 200), new Point(200, + 250)); + assertEquals("3 resulting polygons", 3, r.length); + assertEquals( + "isolated-vertex polygon exists", + true, + exists(r, new Polygon(new Point(100, 300), new Point(100, + 200), new Point(200, 250)))); + // TODO: test for existence of the other two pieces + + r = triangulate(p, new Point(200, 250), new Point(100, 200)); + assertEquals("3 resulting polygons", 3, r.length); + assertEquals( + "isolated-vertex polygon exists", + true, + exists(r, new Polygon(new Point(100, 300), new Point(100, + 200), new Point(200, 250)))); + // TODO: test for existence of the other two pieces + } + + @Test + public void edge1_and_edge3() { + Polygon[] r = triangulate(p, new Point(100, 200), new Point(200, + 150)); + assertEquals("3 resulting polygons", 3, r.length); + assertEquals( + "isolated-vertex polygon exists", + true, + exists(r, new Polygon(new Point(100, 100), new Point(100, + 200), new Point(200, 150)))); + // TODO: test for existence of the other two pieces + + r = triangulate(p, new Point(200, 150), new Point(100, 200)); + assertEquals("3 resulting polygons", 3, r.length); + assertEquals( + "isolated-vertex polygon exists", + true, + exists(r, new Polygon(new Point(100, 100), new Point(100, + 200), new Point(200, 150)))); + // TODO: test for existence of the other two pieces + } + + @Test + public void edge2_and_edge3() { + Polygon[] r = triangulate(p, new Point(200, 250), new Point(200, + 150)); + assertEquals("3 resulting polygons", 3, r.length); + assertEquals( + "isolated-vertex polygon exists", + true, + exists(r, new Polygon(new Point(200, 250), new Point(200, + 150), new Point(300, 200)))); + // TODO: test for existence of the other two pieces + + r = triangulate(p, new Point(200, 150), new Point(200, 250)); + assertEquals("3 resulting polygons", 3, r.length); + assertEquals( + "isolated-vertex polygon exists", + true, + exists(r, new Polygon(new Point(200, 250), new Point(200, + 150), new Point(300, 200)))); + // TODO: test for existence of the other two pieces + } + + @Test + public void edge1_vertex3() { + Polygon[] r = triangulate(p, new Point(100, 200), new Point(300, + 200)); + assertEquals("2 resulting polygons", 2, r.length); + assertEquals("left-side polygon exists", true, + exists(r, new Polygon(100, 100, 100, 200, 300, 200))); + assertEquals("right-side polygon exists", true, + exists(r, new Polygon(100, 300, 100, 200, 300, 200))); + + r = triangulate(p, new Point(300, 200), new Point(100, 200)); + assertEquals("2 resulting polygons", 2, r.length); + assertEquals("left-side polygon exists", true, + exists(r, new Polygon(100, 100, 100, 200, 300, 200))); + assertEquals("right-side polygon exists", true, + exists(r, new Polygon(100, 300, 100, 200, 300, 200))); + } + + @Test + public void edge2_vertex1() { + Polygon[] r = triangulate(p, new Point(200, 250), new Point(100, + 100)); + assertEquals("2 resulting polygons", 2, r.length); + assertEquals("left-side polygon exists", true, + exists(r, new Polygon(100, 100, 200, 250, 100, 300))); + assertEquals("right-side polygon exists", true, + exists(r, new Polygon(100, 100, 200, 250, 300, 200))); + + r = triangulate(p, new Point(100, 100), new Point(200, 250)); + assertEquals("2 resulting polygons", 2, r.length); + assertEquals("left-side polygon exists", true, + exists(r, new Polygon(100, 100, 200, 250, 100, 300))); + assertEquals("right-side polygon exists", true, + exists(r, new Polygon(100, 100, 200, 250, 300, 200))); + } + + @Test + public void edge3_vertex2() { + Polygon[] r = triangulate(p, new Point(200, 150), new Point(100, + 300)); + assertEquals("2 resulting polygons", 2, r.length); + assertEquals("left-side polygon exists", true, + exists(r, new Polygon(200, 150, 100, 300, 100, 100))); + assertEquals("right-side polygon exists", true, + exists(r, new Polygon(200, 150, 100, 300, 300, 200))); + + r = triangulate(p, new Point(100, 300), new Point(200, 150)); + assertEquals("2 resulting polygons", 2, r.length); + assertEquals("left-side polygon exists", true, + exists(r, new Polygon(200, 150, 100, 300, 100, 100))); + assertEquals("right-side polygon exists", true, + exists(r, new Polygon(200, 150, 100, 300, 300, 200))); + } + + } + + /** + *

+ * The {@link Ring#triangulate(Polygon, Line)} method is tested here. + *

+ * + *

+ * The test names indicate the various situations that are tested. Each + * situation comprises the location of the start and end point of the + * {@link Line} and the real and imaginary intersection {@link Point}s of + * the {@link Line} and the {@link Polygon}. Real {@link Point}s of + * intersection are the intersection {@link Point}s of the {@link Line} and + * the {@link Polygon}. Imaginary {@link Point}s of intersection do not lie + * on the {@link Line} but on its expansion to infinity in both directions. + *

+ * + *

+ * The first two characters indicate the location of the start and the end + * {@link Point} of the {@link Line} relative to the {@link Polygon}. 'o' + * means 'outside the polygon'. 'i' means 'inside the polygon'. 'e' means + * 'on an edge of the polygon'. 'v' means 'on a vertex of the polygon'. + *

+ * + *

+ * After that, number and type of expected intersections are stated. Real + * intersection {@link Point}s ('r' for 'real') are named before the + * imaginary ('i' for 'imaginary') intersections. The characters after 'r' + * or 'i' define the type of intersection. 'v' means 'the intersection point + * is a vertex of the polygon'. 'e' means 'the intersection point is on an + * edge of the polygon'. + *

+ * + *

+ * The postfix indicates the expected number of resulting {@link Polygon}s. + * 'ntd' means 'nothing to do' and therefore, it appears when a copy of the + * original {@link Polygon} is expected as the result. Otherwise, 's' is + * followed by the number of results. 'overlaps_edge' means that a + * {@link Polygon}s edge is overlapped by the {@link Line}. + *

+ */ + public static class TriangulateTriangleWithLine { + + private Polygon[] triangulate(Polygon p, Line s) { + try { + Class parameterTypes[] = new Class[] { Polygon.class, + Line.class }; + Method triangulate = Ring.class.getDeclaredMethod( + "triangulate", parameterTypes); + triangulate.setAccessible(true); + return (Polygon[]) triangulate.invoke(null, p, s); + } catch (Exception x) { + throw new IllegalStateException(x); + } + } + + Polygon p; + + @Before + public void setUp() { + p = new Polygon(new Point(100, 100), new Point(100, 300), + new Point(300, 200)); + } + + @Test + public void no_polygon() { + try { + triangulate(null, new Line(1, 2, 3, 4)); + } catch (IllegalStateException x) { + Throwable cause = x; + while (cause.getCause() != null) + cause = cause.getCause(); + + assertTrue(cause.getClass().equals( + IllegalArgumentException.class)); + } + } + + @Test + public void no_line() { + try { + triangulate(p, null); + } catch (IllegalStateException x) { + Throwable cause = x; + while (cause.getCause() != null) + cause = cause.getCause(); + + assertTrue(cause.getClass().equals( + IllegalArgumentException.class)); + } + } + + @Test + public void triangulate_oo_ntd() { + // p1 outside, p2 outside, nothing to do + Polygon[] r = triangulate(p, new Line(new Point(0, 0), new Point( + 400, 0))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_oo1rv_ntd() { + // p1 outside, p2 outside, 1 real intersection (vertex), nothing to + // do + Polygon[] r = triangulate(p, new Line(new Point(0, 100), new Point( + 200, 100))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(0, 300), new Point(200, 300))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(300, 100), + new Point(300, 300))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_oo_overlaps_edge_ntd() { + // p1 outside, p2 outside, overlaps edge, nothing to do + Polygon[] r = triangulate(p, new Line(new Point(0, 50), new Point( + 400, 250))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(0, 350), new Point(400, 150))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, + new Line(new Point(100, 50), new Point(100, 350))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_vv_overlaps_edge_ntd() { + // p1 on vertex, p2 on vertex, overlaps edge, nothing to do + Polygon[] r = triangulate(p, new Line(new Point(100, 100), + new Point(100, 300))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(100, 100), + new Point(300, 200))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(300, 200), + new Point(100, 300))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_ee_overlaps_edge_ntd() { + // p1 on edge, p2 on edge, overlaps edge, nothing to do + Polygon[] r = triangulate(p, new Line(new Point(100, 150), + new Point(100, 250))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(150, 125), + new Point(250, 175))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(150, 275), + new Point(250, 225))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_ev_overlaps_edge_ntd() { + // p1 on edge, p2 on vertex, overlaps edge, nothing to do + Polygon[] r = triangulate(p, new Line(new Point(100, 200), + new Point(100, 100))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(150, 125), + new Point(100, 100))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(150, 125), + new Point(300, 200))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(150, 275), + new Point(300, 200))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(150, 275), + new Point(100, 300))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(100, 200), + new Point(100, 300))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_ve_overlaps_edge_ntd() { + // p1 on vertex, p2 on edge, overlaps edge, nothing to do + Polygon[] r = triangulate(p, new Line(new Point(100, 100), + new Point(100, 200))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(100, 100), + new Point(150, 125))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(300, 200), + new Point(150, 125))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(300, 200), + new Point(150, 275))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(100, 300), + new Point(150, 275))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(100, 300), + new Point(100, 200))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_vo1rv_ntd() { + // p1 on vertex, p2 outside, 1 real intersection (vertex), nothing + // to do + Polygon[] r = triangulate(p, new Line(new Point(100, 100), + new Point(200, 100))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(100, 300), + new Point(200, 300))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(300, 200), + new Point(300, 300))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_eo1re_ntd() { + // p1 on edge, p2 outside, 1 real intersection (edge), nothing to do + Polygon[] r = triangulate(p, new Line(new Point(100, 200), + new Point(100, 0))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(200, 150), new Point(200, 0))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(200, 250), + new Point(200, 300))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_ov1rv_ntd() { + // p1 outside, p2 on vertex, 1 real intersection (vertex), nothing + // to do + Polygon[] r = triangulate(p, new Line(new Point(200, 100), + new Point(100, 100))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(200, 300), + new Point(100, 300))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(300, 300), + new Point(300, 200))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_oe1re_ntd() { + // p1 outside, p2 on edge, 1 real intersection (edge), nothing to do + Polygon[] r = triangulate(p, new Line(new Point(100, 0), new Point( + 100, 200))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(200, 0), new Point(200, 150))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + + r = triangulate(p, new Line(new Point(200, 300), + new Point(200, 250))); + assertEquals("nothing to do", 1, r.length); + assertEquals("polygon remains the same", p, r[0]); + } + + @Test + public void triangulate_oo2ree_s3() { + // p1 outside, p2 outside, 2 real intersections (edge, edge), split + // into + // 3 pieces + Polygon[] r = triangulate(p, new Line(new Point(200, 100), + new Point(200, 300))); + assertEquals("split into three", 3, r.length); + // TODO: verify that the created three polygons are those you wanted + // to + // get back + + r = triangulate(p, + new Line(new Point(50, 150), new Point(250, 300))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, + new Line(new Point(50, 250), new Point(250, 100))); + assertEquals("split into three", 3, r.length); + } + + @Test + public void triangulate_oo2rve_s2() { + // p1 outside, p2 outside, 2 real intersections (vertex, edge), + // split into 2 pieces + Polygon[] r = triangulate(p, new Line(new Point(50, 200), + new Point(350, 200))); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(new Point(50, 50), new Point(300, 300))); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(new Point(50, 350), new Point(350, 50))); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_io1re1ie_s3() { + // p1 inside, p2 outside, 1 real intersection (edge), 1 imaginary + // intersection (edge), split into 3 pieces + Polygon[] r = triangulate(p, new Line(new Point(150, 200), + new Point(150, 50))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(new Point(150, 200), + new Point(250, 100))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(new Point(150, 200), + new Point(150, 350))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(new Point(150, 200), + new Point(250, 300))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, + new Line(new Point(150, 200), new Point(50, 300))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, + new Line(new Point(150, 200), new Point(50, 100))); + assertEquals("split into three", 3, r.length); + } + + @Test + public void triangulate_io1re1iv_s2() { + // p1 inside, p2 outside, 1 real intersection (edge), 1 imaginary + // intersection (vertex), split into 2 pieces + Polygon[] r = triangulate(p, new Line(new Point(150, 200), + new Point(200, 100))); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(new Point(150, 200), + new Point(200, 300))); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, + new Line(new Point(150, 200), new Point(50, 200))); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_io1rv1ie_s2() { + // p1 inside, p2 outside, 1 real intersection (vertex), 1 imaginary + // intersection (edge), split into 2 pieces + Polygon[] r = triangulate(p, new Line(new Point(150, 200), + new Point(50, 400))); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(new Point(150, 200), new Point(50, 0))); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(new Point(150, 200), + new Point(400, 200))); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_oi1re1ie_s3() { + // p1 outside, p2 inside, 1 real intersection (edge), 1 imaginary + // intersection (edge), split into 3 pieces + Polygon[] r = triangulate(p, new Line(new Point(150, 50), + new Point(150, 200))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(new Point(250, 100), + new Point(150, 200))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(new Point(150, 350), + new Point(150, 200))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(new Point(250, 300), + new Point(150, 200))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, + new Line(new Point(50, 300), new Point(150, 200))); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, + new Line(new Point(50, 100), new Point(150, 200))); + assertEquals("split into three", 3, r.length); + } + + @Test + public void triangulate_oi1re1iv_s2() { + // p1 outside, p2 inside, 1 real intersection (edge), 1 imaginary + // intersection (vertex), split into 2 pieces + Polygon[] r = triangulate(p, new Line(new Point(200, 100), + new Point(150, 200))); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(new Point(200, 300), + new Point(150, 200))); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, + new Line(new Point(50, 200), new Point(150, 200))); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_oi1rv1ie_s2() { + // p1 outside, p2 inside, 1 real intersection (vertex), 1 imaginary + // intersection (edge), split into 2 pieces + Polygon[] r = triangulate(p, new Line(new Point(50, 400), + new Point(150, 200))); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(new Point(50, 0), new Point(150, 200))); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(new Point(400, 200), + new Point(150, 200))); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_ee2ree_s3() { + // p1 on edge, p2 on edge, 2 real intersections (edge, edge), split + // into + // 3 pieces + Polygon[] r = triangulate(p, new Line(100, 200, 200, 150)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(100, 200, 200, 250)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(200, 250, 200, 150)); + assertEquals("split into three", 3, r.length); + + // swap start and end point + r = triangulate(p, new Line(200, 150, 100, 200)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(200, 250, 100, 200)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(200, 150, 200, 250)); + assertEquals("split into three", 3, r.length); + } + + @Test + public void triangulate_ve2rve_s2() { + // p1 on vertex, p2 on edge, 2 real intersections (vertex, edge), + // split + // into 2 pieces + Polygon[] r = triangulate(p, new Line(100, 100, 200, 250)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(300, 200, 100, 200)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(100, 300, 200, 150)); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_ev2rve_s2() { + // p1 on edge, p2 on vertex, 2 real intersections (vertex, edge), + // split + // into 2 pieces + Polygon[] r = triangulate(p, new Line(200, 250, 100, 100)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(100, 200, 300, 200)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(200, 150, 100, 300)); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_ei1re1ie_s3() { + // p1 on edge, p2 inside, 1 real intersection (edge), 1 imaginary + // intersection (edge), split into 3 pieces + Polygon[] r = triangulate(p, new Line(100, 200, 150, 175)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(100, 200, 150, 225)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(200, 250, 200, 200)); + assertEquals("split into three", 3, r.length); + } + + @Test + public void triangulate_ie1re1ie_s3() { + // p1 inside, p2 on edge, 1 real intersection (edge), 1 imaginary + // intersection (edge), split into 3 pieces + Polygon[] r = triangulate(p, new Line(150, 175, 100, 200)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(150, 225, 100, 200)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(200, 200, 200, 250)); + assertEquals("split into three", 3, r.length); + } + + @Test + public void triangulate_vi1rv1ie_s2() { + // p1 on vertex, p2 inside, 1 real intersection (vertex), 1 + // imaginary + // intersection (edge), split into 2 pieces + Polygon[] r = triangulate(p, new Line(100, 100, 200, 250)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(300, 200, 100, 200)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(100, 300, 200, 150)); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_iv1rv1ie_s2() { + // p1 inside, p2 on vertex, 1 real intersection (vertex), 1 + // imaginary + // intersection (edge), split into 2 pieces + Polygon[] r = triangulate(p, new Line(200, 250, 100, 100)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(100, 200, 300, 200)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(200, 150, 100, 300)); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_ei1re1iv_s2() { + // p1 on edge, p2 inside, 1 real intersection (edge), 1 imaginary + // intersection (vertex), split into 2 pieces + Polygon[] r = triangulate(p, new Line(100, 200, 200, 200)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(200, 150, 150, 225)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(200, 250, 150, 175)); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_ie1re1iv_s2() { + // p1 inside, p2 on edge, 1 real intersection (edge), 1 imaginary + // intersection (vertex), split into 2 pieces + Polygon[] r = triangulate(p, new Line(200, 200, 100, 200)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(150, 225, 200, 150)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(150, 175, 200, 250)); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_ii2iee_s3() { + // p1 inside, p2 inside, 2 imaginary intersections (edge, edge), + // split + // into 3 pieces + Polygon[] r = triangulate(p, new Line(125, 200, 150, 175)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(200, 175, 200, 225)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(125, 200, 150, 225)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(150, 175, 125, 200)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(200, 225, 200, 175)); + assertEquals("split into three", 3, r.length); + + r = triangulate(p, new Line(150, 225, 125, 200)); + assertEquals("split into three", 3, r.length); + } + + @Test + public void triangulate_ii2iev_s2() { + // p1 inside, p2 inside, 2 imaginary intersections (edge, vertex), + // split + // into 2 pieces + Polygon[] r = triangulate(p, new Line(150, 200, 125, 150)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(150, 200, 200, 200)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(150, 200, 125, 250)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(125, 150, 150, 200)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(200, 200, 150, 200)); + assertEquals("split into two", 2, r.length); + + r = triangulate(p, new Line(125, 250, 150, 200)); + assertEquals("split into two", 2, r.length); + } + + @Test + public void triangulate_precision_error() { + Polygon t = new Polygon(100.0, 100.0, 371.1146624051138, + 197.80263683579705, 370.0, 189.99999999999997); + Line l = new Line(370.0, 190.0, 400.0, 400.0); + + // throws an exception if it fails + triangulate(t, l); + } + + } + + public static class ContainmentTests { + + @Test + public void cover_single_polygon() { + Polygon p1 = new Polygon(1, 2, 1, 3, 2, 4, 3, 4, 4, 3, 4, 2, 3, 1, + 2, 1); + Ring ring = new Ring(p1); + + assertFalse(ring.contains(new Polygon(0, 0, 1, 0, 1, 1, 0, 1))); + assertFalse(ring.contains(new Polygon(1, 1, 3, 1, 2, 2))); + assertTrue(ring.contains(new Polygon(2, 2, 2, 3, 3, 3, 3, 2))); + assertTrue(ring.contains(p1)); + } + + @Test + public void cover_two_distinct_polygons() { + Polygon p1 = new Polygon(1, 2, 1, 3, 2, 4, 3, 4, 4, 3, 4, 2, 3, 1, + 2, 1); + Polygon p2 = new Polygon(4, 4, 4, 5, 5, 5, 5, 4); + Ring ring = new Ring(p1, p2); + + assertFalse(ring.contains(new Polygon(0, 0, 1, 0, 1, 1, 0, 1))); + assertFalse(ring.contains(new Polygon(1, 1, 3, 1, 2, 2))); + assertFalse(ring.contains(new Polygon(4.5, 4.5, 4.5, 5.5, 5.5, 5.5, + 5.5, 4.5))); + assertFalse(ring.contains(new Polygon(3, 3, 5, 3, 5, 5))); + assertTrue(ring.contains(new Polygon(2, 2, 2, 3, 3, 3, 3, 2))); + assertTrue(ring.contains(new Polygon(4.1, 4.1, 4.9, 4.1, 4.9, 4.9, + 4.1, 4.9))); + assertTrue(ring.contains(p1)); + assertTrue(ring.contains(p2)); + } + + @Test + public void cover_two_intersecting_polygons() { + Polygon p1 = new Polygon(1, 2, 1, 3, 2, 4, 3, 4, 4, 3, 4, 2, 3, 1, + 2, 1); + Polygon p2 = new Polygon(2.5, 2.5, 2.5, 5, 5, 5, 5, 2.5); + Ring ring = new Ring(p1, p2); + + assertFalse(ring.contains(new Polygon(0, 0, 1, 0, 1, 1, 0, 1))); + assertFalse(ring.contains(new Polygon(1, 1, 3, 1, 2, 2))); + assertFalse(ring.contains(new Polygon(4.5, 4.5, 4.5, 5.5, 5.5, 5.5, + 5.5, 4.5))); + assertTrue(ring.contains(new Polygon(2, 2, 2, 3, 3, 3, 3, 2))); + assertTrue(ring.contains(new Polygon(3, 3, 5, 3, 5, 5))); + assertTrue(ring.contains(p1)); + assertTrue(ring.contains(p2)); + assertTrue(ring.contains(new Polygon(2, 2, 2, 3, 3, 3, 3, 2))); + } + + } + +} diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RoundedRectangleTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RoundedRectangleTests.java new file mode 100644 index 0000000..5586fc5 --- /dev/null +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/RoundedRectangleTests.java @@ -0,0 +1,214 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.eclipse.gef4.geometry.Angle; +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.planar.Arc; +import org.eclipse.gef4.geometry.planar.ICurve; +import org.eclipse.gef4.geometry.planar.IGeometry; +import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.Rectangle; +import org.eclipse.gef4.geometry.planar.RoundedRectangle; +import org.eclipse.gef4.geometry.utils.PrecisionUtils; +import org.junit.Before; +import org.junit.Test; + +public class RoundedRectangleTests { + + double x = 1, y = 2, w = 5, h = 6, aw = 1, ah = 2; + RoundedRectangle rr; + + @Before + public void setUp() { + rr = new RoundedRectangle(x, y, w, h, aw, ah); + } + + @Test + public void test_equals() { + assertEquals(rr, + new RoundedRectangle(new Rectangle(x, y, w, h), aw, ah)); + assertEquals(rr, rr.getCopy()); + assertFalse(rr.equals(null)); + assertFalse(rr.equals(new Point())); + + assertFalse(rr.equals(new RoundedRectangle(x + 10, y, w, h, aw, ah))); + assertFalse(rr.equals(new RoundedRectangle(x, y + 10, w, h, aw, ah))); + assertFalse(rr.equals(new RoundedRectangle(x, y, w + 10, h, aw, ah))); + assertFalse(rr.equals(new RoundedRectangle(x, y, w, h + 10, aw, ah))); + assertFalse(rr.equals(new RoundedRectangle(x, y, w, h, aw + 10, ah))); + assertFalse(rr.equals(new RoundedRectangle(x, y, w, h, aw, ah + 10))); + } + + @Test + public void test_getters() { + check_values_with_getters(rr, x, y, w, h, aw, ah); + } + + @Test + public void test_setters() { + // TODO: change values and test if the changes are applied correctly + RoundedRectangle rrCopy = rr.getCopy(); + + double nx = 9, ny = 8, nw = 7, nh = 6, naw = 5, nah = 4; + + rrCopy.setX(nx); + rrCopy.setY(ny); + rrCopy.setWidth(nw); + rrCopy.setHeight(nh); + rrCopy.setArcWidth(naw); + rrCopy.setArcHeight(nah); + + check_values_with_getters(rrCopy, nx, ny, nw, nh, naw, nah); + check_values_with_getters(rr, x, y, w, h, aw, ah); + } + + @Test + public void test_contains_Point() { + check_Point_containment(rr); + } + + @Test + public void test_toPath() { + check_Point_containment(rr.toPath()); + } + + @Test + public void test_getOutlineSegments() { + ICurve[] outlineSegments = rr.getOutlineSegments(); + assertEquals(8, outlineSegments.length); + + // consecutive + for (int i = 0; i < 7; i++) + assertEquals(outlineSegments[i].getP2(), + outlineSegments[i + 1].getP1()); + assertEquals(outlineSegments[7].getP2(), outlineSegments[0].getP1()); + + // position + assertEquals(new Point(x + w, y + ah), outlineSegments[0].getP1()); + assertEquals(new Point(x + w - aw, y), outlineSegments[1].getP1()); + assertEquals(new Point(x + aw, y), outlineSegments[2].getP1()); + assertEquals(new Point(x, y + ah), outlineSegments[3].getP1()); + assertEquals(new Point(x, y + h - ah), outlineSegments[4].getP1()); + assertEquals(new Point(x + aw, y + h), outlineSegments[5].getP1()); + assertEquals(new Point(x + w - aw, y + h), outlineSegments[6].getP1()); + assertEquals(new Point(x + w, y + h - ah), outlineSegments[7].getP1()); + } + + @Test + public void test_toString() { + assertEquals("RoundedRectangle(" + x + ", " + y + ", " + w + ", " + h + + ", " + aw + ", " + ah + ")", rr.toString()); + } + + @Test + public void test_getOutline() { + // coherence with getOutlineSegments + ICurve[] outlineSegments = rr.getOutlineSegments(); + ICurve[] outlineCurves = rr.getOutline().getCurves(); + assertEquals(outlineSegments.length, outlineCurves.length); + for (int i = 0; i < 8; i++) + assertEquals(outlineSegments[i], outlineCurves[i]); + } + + @Test + public void test_contains_shape() { + // translate it by some values and test that the translated versions are + // not contained + for (double tx : new double[] { -1, 1 }) + for (double ty : new double[] { -1, 1 }) + assertFalse(rr.contains(rr.getTranslated(tx, ty))); + + // scale it down by some values and test that the smaller versions are + // contained + for (double s = 1; s > 0; s -= 0.1) + assertTrue(rr.contains(rr.getScaled(s))); + + // scale it up by some values and test that the greater versions are not + // contained + for (double s = 1.1; s < 2; s += 0.1) + assertFalse(rr.contains(rr.getScaled(s))); + } + + private void check_Point_containment(IGeometry g) { + assertTrue(g.contains(new Point(3.5, 5))); + assertTrue(g.contains(new Point(1.5, 5))); + assertTrue(g.contains(new Point(1, 5))); + assertTrue(g.contains(new Point(5.5, 5))); + // TODO: next test is commented out because the AWT Path does not + // recognize points on the right and bottom sides + // assertTrue(g.contains(new Point(6, 5))); + assertTrue(g.contains(new Point(3.5, 2.5))); + assertTrue(g.contains(new Point(3.5, 2))); + assertTrue(g.contains(new Point(3.5, 7.5))); + // TODO: next test is commented out because the AWT Path does not + // recognize points on the right and bottom sides + // assertTrue(g.contains(new Point(3.5, 8))); + assertTrue(g.contains(new Point(1.5, 3.5))); + assertTrue(g.contains(new Point(5.5, 3.5))); + assertTrue(g.contains(new Point(1.5, 6.5))); + assertTrue(g.contains(new Point(5.5, 6.5))); + assertFalse(g.contains(new Point(0, 0))); + assertFalse(g.contains(new Point(4, 0))); + assertFalse(g.contains(new Point(7, 0))); + assertFalse(g.contains(new Point(0, 5))); + assertFalse(g.contains(new Point(7, 5))); + assertFalse(g.contains(new Point(0, 9))); + assertFalse(g.contains(new Point(4, 9))); + assertFalse(g.contains(new Point(7, 9))); + assertFalse(g.contains(new Point(1, 2))); + assertFalse(g.contains(new Point(6, 2))); + assertFalse(g.contains(new Point(1, 8))); + assertFalse(g.contains(new Point(6, 8))); + } + + private void check_values_with_getters(RoundedRectangle r, double px, + double py, double pw, double ph, double paw, double pah) { + assertTrue(PrecisionUtils.equal(px, r.getX())); + assertTrue(PrecisionUtils.equal(py, r.getY())); + assertTrue(PrecisionUtils.equal(pw, r.getWidth())); + assertTrue(PrecisionUtils.equal(ph, r.getHeight())); + assertTrue(PrecisionUtils.equal(paw, r.getArcWidth())); + assertTrue(PrecisionUtils.equal(pah, r.getArcHeight())); + assertEquals(new Point(px, py), r.getLocation()); + assertEquals(new Rectangle(px, py, pw, ph), r.getBounds()); + + // generated arcs have double width and height as specified so that the + // underlying ellipse fits into the respective rectangle + assertEquals( + new Arc(px + pw - 2 * paw, py, 2 * paw, 2 * pah, + Angle.fromDeg(0), Angle.fromDeg(90)), + r.getTopRightArc()); + assertEquals( + new Arc(px, py, 2 * paw, 2 * pah, Angle.fromDeg(90), + Angle.fromDeg(90)), r.getTopLeftArc()); + assertEquals( + new Arc(px, py + ph - 2 * pah, 2 * paw, 2 * pah, + Angle.fromDeg(180), Angle.fromDeg(90)), + r.getBottomLeftArc()); + assertEquals(new Arc(px + pw - 2 * paw, py + ph - 2 * pah, 2 * paw, + 2 * pah, Angle.fromDeg(270), Angle.fromDeg(90)), + r.getBottomRightArc()); + + assertEquals(new Line(px + paw, py, px + pw - paw, py), r.getTop()); + assertEquals(new Line(px + paw, py + ph, px + pw - paw, py + ph), + r.getBottom()); + assertEquals(new Line(px, py + pah, px, py + ph - pah), r.getLeft()); + assertEquals(new Line(px + pw, py + pah, px + pw, py + ph - pah), + r.getRight()); + } + +} diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/StraightTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/StraightTests.java index c861033..4420f5a 100644 --- a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/StraightTests.java +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/StraightTests.java @@ -328,6 +328,10 @@ public class StraightTests { assertTrue(s1.getPointAt(0).equals(new Point())); assertTrue(s1.getPointAt(1).equals(new Point(1, 0))); assertTrue(s1.getPointAt(-1).equals(new Point(-1, 0))); + + // test 0/0 straight (not a straight anymore) + s1 = new Straight(new Point(), new Point()); + assertTrue(PrecisionUtils.equal(0, s1.getParameterAt(new Point()))); } @Test diff --git a/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/Vector3DTests.java b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/Vector3DTests.java new file mode 100644 index 0000000..10031da --- /dev/null +++ b/org.eclipse.gef4.geometry.tests/src/org/eclipse/gef4/geometry/tests/Vector3DTests.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.projective.Vector3D; +import org.eclipse.gef4.geometry.utils.PrecisionUtils; +import org.junit.Test; + +public class Vector3DTests { + + @Test + public void test_equals() { + Vector3D v0 = new Vector3D(1, 1, 1); + assertFalse(v0.equals(null)); + assertFalse(v0.equals(new Point())); + assertEquals(v0, v0); + assertEquals(v0, new Vector3D(1, 1, 1)); + assertEquals(v0, new Vector3D(new Point(1, 1))); + assertEquals(v0, new Vector3D(2, 2, 2)); + } + + @Test + public void test_getCopy() { + Vector3D v0 = new Vector3D(1, 2, 3); + Vector3D v1 = v0.getCopy(); + assertEquals(v0, v1); + assertNotSame(v0, v1); + v0.x++; + v0.y--; + assertFalse(v0.equals(v1)); + } + + @Test + public void test_toString() { + Vector3D v0 = new Vector3D(1, 2, 3); + assertEquals("Vector3D(1.0, 2.0, 3.0)", v0.toString()); + } + + @Test + public void test_getAdded() { + Vector3D v0 = new Vector3D(1, 0, 5); + Vector3D v1 = new Vector3D(0, 1, 5); + assertEquals(new Vector3D(1, 0, 5), v0.getAdded(v0)); + assertEquals(new Vector3D(0, 1, 5), v1.getAdded(v1)); + assertEquals(new Vector3D(1, 1, 10), v0.getAdded(v1)); + assertEquals(new Vector3D(1, 1, 10), v1.getAdded(v0)); + assertEquals(new Vector3D(2, 2, 20), v0.getAdded(v1)); + assertEquals(new Vector3D(2, 2, 20), v1.getAdded(v0)); + } + + @Test + public void test_getSubtracted() { + Vector3D v0 = new Vector3D(10, 5, 1); + Vector3D v1 = new Vector3D(5, 10, 1); + assertEquals(new Vector3D(0, 0, 0), v0.getSubtracted(v0)); + assertEquals(new Vector3D(0, 0, 0), v1.getSubtracted(v1)); + assertFalse(v0.getSubtracted(v1).equals(new Vector3D(0, 0, 1))); + assertEquals(new Vector3D(5, -5, 0), v0.getSubtracted(v1)); + assertEquals(new Vector3D(5, -5, 0), v1.getSubtracted(v0)); + assertEquals(new Vector3D(1, -1, 1 / 5), v0.getSubtracted(v1)); + assertEquals(new Vector3D(1, -1, 1 / 5), v1.getSubtracted(v0)); + } + + @Test + public void test_getScaled() { + Vector3D v0 = new Vector3D(1, 2, 3); + for (double s = -1.1; s <= 1.1; s += 0.2) + assertEquals(new Vector3D(1, 2, 3), v0.getScaled(s)); + } + + @Test + public void test_getDot() { + Vector3D v0 = new Vector3D(1, 0, 1); + Vector3D v1 = new Vector3D(0, 1, 1); + assertTrue(PrecisionUtils.equal(1, v0.getDot(v1))); + assertTrue(PrecisionUtils.equal(1, v1.getDot(v0))); + + v0 = new Vector3D(1, 2, 3); + v1 = new Vector3D(3, 2, 1); + assertTrue(PrecisionUtils.equal(10, v0.getDot(v1))); + assertTrue(PrecisionUtils.equal(10, v1.getDot(v0))); + } + + @Test + public void test_getCrossed() { + Vector3D v0 = new Vector3D(1, 0, 1); + Vector3D v1 = new Vector3D(0, 1, 1); + assertEquals(new Vector3D(-1, -1, 1), v0.getCrossed(v1)); + assertEquals(new Vector3D(1, 1, -1), v1.getCrossed(v0)); + } + + @Test + public void test_getRatio() { + Vector3D v0 = new Vector3D(0, 0, 1); + Vector3D v1 = new Vector3D(10, 10, 1); + assertEquals(new Vector3D(5, 5, 1), v0.getRatio(v1, 0.5)); + assertEquals(new Vector3D(5, 5, 1), v1.getRatio(v0, 0.5)); + } + +} diff --git a/org.eclipse.gef4.geometry/META-INF/MANIFEST.MF b/org.eclipse.gef4.geometry/META-INF/MANIFEST.MF index 7f37060..cd045d8 100644 --- a/org.eclipse.gef4.geometry/META-INF/MANIFEST.MF +++ b/org.eclipse.gef4.geometry/META-INF/MANIFEST.MF @@ -7,7 +7,9 @@ Bundle-Vendor: Eclipse.org Bundle-RequiredExecutionEnvironment: J2SE-1.5 Require-Bundle: org.eclipse.swt;bundle-version="[3.2.0,4.0.0)" Export-Package: org.eclipse.gef4.geometry, + org.eclipse.gef4.geometry.convert, org.eclipse.gef4.geometry.euclidean, org.eclipse.gef4.geometry.planar, + org.eclipse.gef4.geometry.projective, org.eclipse.gef4.geometry.transform, org.eclipse.gef4.geometry.utils;x-friends:="org.eclipse.gef4.geometry.tests" diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/Point.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/Point.java index 362daaa..9f3e3ab 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/Point.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/Point.java @@ -216,8 +216,7 @@ public class Point implements Cloneable, Serializable { * @return The new, scaled {@link Point} */ public Point getScaled(double factorX, double factorY, Point center) { - return getTranslated(center.getNegated()).scale(factorX, factorY) - .translate(center); + return getCopy().scale(factorX, factorY, center); } /** diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/euclidean/Straight.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/euclidean/Straight.java index bdacc4e..d55ee0b 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/euclidean/Straight.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/euclidean/Straight.java @@ -297,7 +297,7 @@ public class Straight implements Cloneable, Serializable { * {@link Point} p */ public double getParameterAt(Point p) { - if (direction.x != 0) { + if (Math.abs(direction.x) > Math.abs(direction.y)) { return (p.x - position.x) / direction.x; } if (direction.y != 0) { @@ -473,4 +473,4 @@ public class Straight implements Cloneable, Serializable { return -line.getSignedDistanceCW(new Vector3D(r)); } -} \ No newline at end of file +} diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractArcBasedGeometry.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractArcBasedGeometry.java new file mode 100644 index 0000000..10e7e1a --- /dev/null +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractArcBasedGeometry.java @@ -0,0 +1,300 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - contribution for Bugzilla #355997 + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.planar; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.gef4.geometry.Angle; +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.utils.CurveUtils; + +/** + * An {@link AbstractArcBasedGeometry} describes the arc of an {@link Ellipse}. + * It provides functionality to modify and query attributes of the arc and to + * compute a Bezier approximation of the arc (the outline). + * + * @param + * type of the inheriting class + */ +public abstract class AbstractArcBasedGeometry> + extends AbstractRectangleBasedGeometry { + + private static final long serialVersionUID = 1L; + + /** + * The CCW (counter-clock-wise) {@link Angle} to the x-axis at which this + * {@link AbstractArcBasedGeometry} begins. + */ + protected Angle startAngle; + + /** + * The CCW (counter-clock-wise) {@link Angle} that spans this + * {@link AbstractArcBasedGeometry}. + */ + protected Angle angularExtent; + + /** + * Constructs a new {@link AbstractArcBasedGeometry} so that it is fully + * contained within the framing rectangle defined by (x, y, width, height), + * spanning the given extend (in CCW direction) from the given start angle + * (relative to the x-axis). + * + * @param x + * the x-coordinate of the framing rectangle + * @param y + * the y-coordinate of the framing rectangle + * @param width + * @param height + * @param startAngle + * @param angularExtent + */ + public AbstractArcBasedGeometry(double x, double y, double width, + double height, Angle startAngle, Angle angularExtent) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.startAngle = startAngle; + this.angularExtent = angularExtent; + } + + /** + * Returns the extension {@link Angle} of this + * {@link AbstractArcBasedGeometry}, i.e. the {@link Angle} defining the + * span of this {@link AbstractArcBasedGeometry}. + * + * @return the extension {@link Angle} of this + * {@link AbstractArcBasedGeometry} + */ + public Angle getAngularExtent() { + return angularExtent; + } + + /** + * Returns a {@link Point} representing the start {@link Point} of this + * {@link AbstractArcBasedGeometry}. + * + * @return the start {@link Point} of this {@link AbstractArcBasedGeometry} + */ + public Point getP1() { + return getPoint(Angle.fromRad(0)); + } + + /** + * Returns a {@link Point} representing the end {@link Point} of this + * {@link AbstractArcBasedGeometry}. + * + * @return the end {@link Point} of this {@link AbstractArcBasedGeometry} + */ + public Point getP2() { + return getPoint(angularExtent); + } + + /** + * Computes a {@link Point} on this {@link AbstractArcBasedGeometry}. The + * {@link Point}'s coordinates are calculated by moving the given + * {@link Angle} on this {@link AbstractArcBasedGeometry} starting at the + * {@link AbstractArcBasedGeometry}'s start {@link Point}. + * + * @param angularExtent + * @return the {@link Point} at the given {@link Angle} + */ + public Point getPoint(Angle angularExtent) { + double a = width / 2; + double b = height / 2; + + // // calculate start and end points of the arc from start to end + return new Point(x + a + a + * Math.cos(startAngle.rad() + angularExtent.rad()), y + b - b + * Math.sin(startAngle.rad() + angularExtent.rad())); + } + + /** + * Returns this {@link AbstractArcBasedGeometry}'s start {@link Angle}. + * + * @return this {@link AbstractArcBasedGeometry}'s start {@link Angle} + */ + public Angle getStartAngle() { + return startAngle; + } + + /** + * Returns the x-coordinate of the start {@link Point} of this + * {@link AbstractArcBasedGeometry}. + * + * @return the x-coordinate of the start {@link Point} of this + * {@link AbstractArcBasedGeometry} + */ + public double getX1() { + return getP1().x; + } + + /** + * Returns the x-coordinate of the end {@link Point} of this + * {@link AbstractArcBasedGeometry}. + * + * @return the x-coordinate of the end {@link Point} of this + * {@link AbstractArcBasedGeometry} + */ + public double getX2() { + return getP2().x; + } + + /** + * Returns the y-coordinate of the start {@link Point} of this + * {@link AbstractArcBasedGeometry}. + * + * @return the y-coordinate of the start {@link Point} of this + * {@link AbstractArcBasedGeometry} + */ + public double getY1() { + return getP1().y; + } + + /** + * Returns the y-coordinate of the end {@link Point} of this + * {@link AbstractArcBasedGeometry}. + * + * @return the y-coordinate of the end {@link Point} of this + * {@link AbstractArcBasedGeometry} + */ + public double getY2() { + return getP2().y; + } + + /** + * Sets the extension {@link Angle} of this {@link AbstractArcBasedGeometry} + * . + * + * @param angularExtent + * the new extension {@link Angle} for this + * {@link AbstractArcBasedGeometry} + */ + public void setAngularExtent(Angle angularExtent) { + this.angularExtent = angularExtent; + } + + /** + * Sets the start {@link Angle} of this {@link AbstractArcBasedGeometry}. + * + * @param startAngle + * the new start {@link Angle} for this + * {@link AbstractArcBasedGeometry} + */ + public void setStartAngle(Angle startAngle) { + this.startAngle = startAngle; + } + + /** + * Computes a Bezier approximation for this {@link AbstractArcBasedGeometry} + * . It is approximated by at most four {@link CubicCurve}s which span at + * most 90 degrees. + * + * @return a Bezier approximation for this {@link AbstractArcBasedGeometry} + */ + protected CubicCurve[] computeBezierApproximation() { + double start = getStartAngle().rad(); + double end = getStartAngle().rad() + getAngularExtent().rad(); + + // approximation is for arcs with angle < 90 degrees, so we may have to + // split the arc into up to 4 cubic curves + List segments = new ArrayList(); + if (angularExtent.deg() <= 90.0) { + segments.add(CurveUtils.computeEllipticalArcApproximation(x, y, + width, height, Angle.fromRad(start), Angle.fromRad(end))); + } else { + // two or more segments, the first will be an ellipse segment + // approximation + segments.add(CurveUtils.computeEllipticalArcApproximation(x, y, + width, height, Angle.fromRad(start), + Angle.fromRad(start + Math.PI / 2))); + if (angularExtent.deg() <= 180.0) { + // two segments, calculate the second (which is below 90 + // degrees) + segments.add(CurveUtils.computeEllipticalArcApproximation(x, y, + width, height, Angle.fromRad(start + Math.PI / 2), + Angle.fromRad(end))); + } else { + // three or more segments, so calculate the second one + segments.add(CurveUtils.computeEllipticalArcApproximation(x, y, + width, height, Angle.fromRad(start + Math.PI / 2), + Angle.fromRad(start + Math.PI))); + if (angularExtent.deg() <= 270.0) { + // three segments, calculate the third (which is below 90 + // degrees) + segments.add(CurveUtils.computeEllipticalArcApproximation( + x, y, width, height, + Angle.fromRad(start + Math.PI), Angle.fromRad(end))); + } else { + // four segments (fourth below 90 degrees), so calculate the + // third and fourth + segments.add(CurveUtils.computeEllipticalArcApproximation( + x, y, width, height, + Angle.fromRad(start + Math.PI), + Angle.fromRad(start + 3 * Math.PI / 2))); + segments.add(CurveUtils.computeEllipticalArcApproximation( + x, y, width, height, + Angle.fromRad(start + 3 * Math.PI / 2), + Angle.fromRad(end))); + } + } + } + return segments.toArray(new CubicCurve[] {}); + } + + /** + * @see IGeometry#toPath() + */ + public Path toPath() { + return CurveUtils.toPath(computeBezierApproximation()); + } + + @SuppressWarnings("unchecked") + public T getRotatedCCW(Angle angle) { + return (T) ((T) getCopy()).rotateCCW(angle); + } + + /** + * Rotates this {@link AbstractArcBasedGeometry} counter-clock-wise (CCW) by + * the given {@link Angle} around its center {@link Point}. + * + * @param angle + * the rotation {@link Angle} + * @return this for convenience + */ + @SuppressWarnings("unchecked") + public T rotateCCW(Angle angle) { + startAngle.setRad(startAngle.getAdded(angle).rad()); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T getRotatedCW(Angle angle) { + return (T) ((T) getCopy()).rotateCW(angle); + } + + /** + * Rotates this {@link AbstractArcBasedGeometry} clock-wise (CW) by the + * given {@link Angle} around its center {@link Point}. + * + * @param angle + * the rotation {@link Angle} + * @return this for convenience + */ + @SuppressWarnings("unchecked") + public T rotateCW(Angle angle) { + startAngle.setRad(startAngle.getAdded(angle.getOppositeFull()).rad()); + return (T) this; + } + +} diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractPointListBasedGeometry.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractPointListBasedGeometry.java index 5ecb1fc..b8f8309 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractPointListBasedGeometry.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractPointListBasedGeometry.java @@ -15,10 +15,14 @@ package org.eclipse.gef4.geometry.planar; import org.eclipse.gef4.geometry.Angle; import org.eclipse.gef4.geometry.Point; import org.eclipse.gef4.geometry.euclidean.Vector; +import org.eclipse.gef4.geometry.transform.IRotatable; +import org.eclipse.gef4.geometry.transform.IScalable; +import org.eclipse.gef4.geometry.transform.ITranslatable; import org.eclipse.gef4.geometry.utils.PointListUtils; abstract class AbstractPointListBasedGeometry> - extends AbstractGeometry { + extends AbstractGeometry implements ITranslatable, IScalable, + IRotatable { private static final long serialVersionUID = 1L; @@ -54,28 +58,7 @@ abstract class AbstractPointListBasedGeometry seen, + Stack addends, Line toAdd, Point start, Point end) { + if (!start.equals(end)) { + Line rest = new Line(start, end); + if (start.equals(toAdd.getP1()) || start.equals(toAdd.getP2())) { + // System.out + // .println(" pushing rest (" + rest + ") to addends"); + addends.push(rest); + } else { + // System.out.println(" marking rest (" + rest + + // ") as seen"); + seen.put(rest, + seen.containsKey(rest) && seen.get(rest) == 2 ? 2 : 1); + } + } + } + + public boolean contains(Point p) { + for (IShape s : getShapes()) { + if (s.contains(p)) { + return true; + } + } + return false; + } + + /** + * Inner segments are identified by a segment count of exactly 2. + * + * @param seen + */ + private void filterOutInnerSegments(HashMap seen) { + for (Line seg : new HashSet(seen.keySet())) { + if (seen.get(seg) == 2) { + seen.remove(seg); + } + } + } + + /** + * Collects all edges of the internal {@link IShape}s. For a {@link Region} + * the internal {@link IShape}s are {@link Rectangle}s. For a {@link Ring} + * the internal {@link IShape}s are {@link Polygon}s (triangles). + * + * The internal edges are needed to determine inner and outer segments of + * the {@link IPolyShape}. Based on the outline of the {@link IPolyShape}, + * the outline intersections can be computed. These outline intersections + * are required to test if an {@link ICurve} is fully-contained by the + * {@link IPolyShape}. + * + * @return the edges of all internal {@link IShape}s + */ + abstract protected Line[] getAllEdges(); + + /** + * Computes the outline of this {@link AbstractPolyShape}. + * + * @return the outline of this {@link AbstractPolyShape} + * @see #getOutlineSegments() + */ + public Polyline getOutline() { + return new Polyline(getOutlineSegments()); + } + + /** + * Computes the outline segments of this {@link AbstractPolyShape}. + * + * The outline segments are those outline segments of the internal + * {@link Rectangle}s that only exist once. + * + * @return the outline segments of this {@link AbstractPolyShape} + */ + public Line[] getOutlineSegments() { + // System.out.println("collecting all edges..."); + HashMap seen = new HashMap(); + Stack elementsToAdd = new Stack(); + for (Line e : getAllEdges()) + elementsToAdd.push(e); + + int c = 0; + addingElements: while (c++ < 1000 && !elementsToAdd.empty()) { + Line toAdd = elementsToAdd.pop(); + // System.out.println("adding " + toAdd + "..."); + for (Line seg : new HashSet(seen.keySet())) { + if (seg.overlaps(toAdd)) { + // System.out.println(" overlaps with " + seg); + Point[] p = getSortedEndpoints(toAdd, seg); + seen.remove(seg); + assignRemainingSegment(seen, elementsToAdd, toAdd, p[0], + p[1]); + assignRemainingSegment(seen, elementsToAdd, toAdd, p[3], + p[2]); + markOverlap(seen, p[1], p[2]); + continue addingElements; + } + } + // System.out.println(" did not overlap"); + seen.put(toAdd, 1); + } + + // System.out.println("filter out inner segments..."); + filterOutInnerSegments(seen); + + return seen.keySet().toArray(new Line[] {}); + } + + /** + * Sorts the end {@link Point}s of two {@link Line}s that do overlap by + * their coordinate values. + * + * @param toAdd + * @param seg + * @return the sorted {@link Point}s + */ + private Point[] getSortedEndpoints(Line toAdd, Line seg) { + final Point[] p = new Point[] { seg.getP1(), seg.getP2(), + toAdd.getP1(), toAdd.getP2() }; + Arrays.sort(p, new Comparator() { + public int compare(Point p1, Point p2) { + if (PrecisionUtils.equal(p1.x, p2.x)) { + return p1.y < p2.y ? 1 : -1; + } + return p1.x < p2.x ? 1 : -1; + } + }); + return p; + } + + /** + * Marks a given segment from start to end {@link Point} as an overlap in + * the seen {@link HashMap} if the segment is not degenerated, i.e. it is + * not just a single {@link Point}. + * + * @param seen + * @param start + * @param end + */ + private void markOverlap(HashMap seen, Point start, Point end) { + if (!start.equals(end)) { + // Count an overlapping segment twice to assure that it is going to + // get deleted afterwards. + Line overlap = new Line(start, end); + seen.put(overlap, 2); + // System.out.println(" mark segment " + overlap + + // " as overlap"); + } + } + +} diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractRectangleBasedGeometry.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractRectangleBasedGeometry.java index b1760d8..94d8318 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractRectangleBasedGeometry.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/AbstractRectangleBasedGeometry.java @@ -14,16 +14,32 @@ package org.eclipse.gef4.geometry.planar; import org.eclipse.gef4.geometry.Dimension; import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.transform.IRotatable; +import org.eclipse.gef4.geometry.transform.IScalable; +import org.eclipse.gef4.geometry.transform.ITranslatable; /** + *

* Abstract superclass of geometries that are defined by means of their upper * left coordinate (x,y) and a given width and height. + *

* - * @author anyssen + *

+ * The type parameter T specifies the type of the inheriting class. + * This is to be able to return the correct type, so that a type cast is + * unnecessary. + *

+ * + *

+ * The type parameter S specifies the result type of all rotation + * short-cut methods. See {@link IRotatable} for more information. + *

* + * @author anyssen */ -abstract class AbstractRectangleBasedGeometry> - extends AbstractGeometry { +abstract class AbstractRectangleBasedGeometry, S extends IGeometry> + extends AbstractGeometry implements ITranslatable, IScalable, + IRotatable { private static final long serialVersionUID = 1L; @@ -53,29 +69,28 @@ abstract class AbstractRectangleBasedGeometrythis for convenience - */ - public T scale(double factor) { - return scale(factor, factor); + @SuppressWarnings("unchecked") + public T scale(double fx, double fy, double cx, double cy) { + x = (x - cx) * fx + cx; + y = (y - cy) * fy + cy; + width *= fx; + height *= fy; + return (T) this; } - public T scale(double factorX, double factorY) { - return scale(factorX, factorY, getCentroid()); + public T scale(double fx, double fy, Point center) { + return scale(fx, fy, center.x, center.y); } - @SuppressWarnings("unchecked") - public T scale(double factorX, double factorY, Point center) { - double nx = (x - center.x) * factorX + center.x; - double ny = (y - center.y) * factorY + center.y; - width = (x + width - center.x) * factorX + center.x - nx; - height = (y + height - center.y) * factorY + center.y - ny; - x = nx; - y = ny; - return (T) this; + public T scale(double fx, double fy) { + return scale(fx, fy, getCentroid()); + } + + public T scale(double factor) { + return scale(factor, factor); } public T scale(double factor, Point center) { - return scale(factor, factor, center); + return scale(factor, center.x, center.y); + } + + public T scale(double factor, double cx, double cy) { + return scale(factor, factor, cx, cy); } /** @@ -355,4 +367,4 @@ abstract class AbstractRectangleBasedGeometry implements - ICurve { +public final class Arc extends AbstractArcBasedGeometry implements ICurve { private static final long serialVersionUID = 1L; - // TODO: move to utilities - private static final Path toPath(CubicCurve... curves) { - Path p = new Path(); - for (int i = 0; i < curves.length; i++) { - if (i == 0) { - p.moveTo(curves[i].getX1(), curves[i].getY1()); - } - p.curveTo(curves[i].getCtrlX1(), curves[i].getCtrlY1(), - curves[i].getCtrlX2(), curves[i].getCtrlY2(), - curves[i].getX2(), curves[i].getY2()); - } - return p; - } - - private Angle startAngle; - private Angle angularExtent; - /** - * Constructs a new {@link Arc} so that it is fully contained within the - * framing rectangle defined by (x, y, width, height), spanning the given - * extend (in CCW direction) from the given start angle (relative to the - * x-axis). + * Constructs a new {@link Arc} of the given values. A {@link Rectangle} is + * used to define the {@link Ellipse} from which the {@link Arc} is cut out. + * The start {@link Angle} is the CCW (counter-clock-wise) {@link Angle} to + * the x-axis at which the {@link Arc} begins. The angular extent is the CCW + * {@link Angle} that spans the {@link Arc}, i.e. the resulting end + * {@link Angle} of the {@link Arc} is the sum of the start {@link Angle} + * and the angular extent. * * @param x - * the x-coordinate of the framing rectangle * @param y - * the y-coordinate of the framing rectangle * @param width * @param height * @param startAngle @@ -65,56 +47,22 @@ public final class Arc extends AbstractRectangleBasedGeometry implements */ public Arc(double x, double y, double width, double height, Angle startAngle, Angle angularExtent) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.startAngle = startAngle; - this.angularExtent = angularExtent; + super(x, y, width, height, startAngle, angularExtent); } - private CubicCurve computeApproximation(double start, double end) { - // compute major and minor axis length - double a = width / 2; - double b = height / 2; - - // // calculate start and end points of the arc from start to end - Point startPoint = new Point(x + a + a * Math.cos(start), y + b - b - * Math.sin(start)); - Point endPoint = new Point(x + a + a * Math.cos(end), y + b - b - * Math.sin(end)); - - // approximation by cubic Bezier according to approximation provided in: - // http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf - double t = Math.tan((end - start) / 2); - double alpha = Math.sin(end - start) - * (Math.sqrt(4.0d + 3.0d * t * t) - 1) / 3; - Point controlPoint1 = new Point(startPoint.x + alpha * -a - * Math.sin(start), startPoint.y - alpha * b * Math.cos(start)); - Point controlPoint2 = new Point( - endPoint.x - alpha * -a * Math.sin(end), endPoint.y + alpha * b - * Math.cos(end)); - - Point[] points = new Point[] { startPoint, controlPoint1, - controlPoint2, endPoint }; - return new CubicCurve(points); - } - - /** - * @see IGeometry#contains(Point) - */ - public boolean contains(Point p) { - return false; - } - - /** - * Returns the extension {@link Angle} of this {@link Arc}, i.e. the - * {@link Angle} defining the span of the {@link Arc}. - * - * @return the extension {@link Angle} of this {@link Arc} - */ - public Angle getAngularExtent() { - return angularExtent; + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof Arc)) { + return false; + } + Arc o = (Arc) obj; + return PrecisionUtils.equal(x, o.x) + && PrecisionUtils.equal(y, o.y) + && PrecisionUtils.equal(width, o.width) + && PrecisionUtils.equal(height, o.height) + && PrecisionUtils.equal(angularExtent.rad(), + o.angularExtent.rad()) + && PrecisionUtils.equal(startAngle.rad(), o.startAngle.rad()); } /** @@ -124,64 +72,27 @@ public final class Arc extends AbstractRectangleBasedGeometry implements return new Arc(x, y, width, height, startAngle, angularExtent); } - public Point[] getIntersections(ICurve g) { - return CurveUtils.getIntersections(this, g); - } - - /** - * Returns a {@link Point} representing the start point of this {@link Arc}. - * - * @return the start {@link Point} of this {@link Arc} - */ - public Point getP1() { - return getPoint(Angle.fromRad(0)); - } - - public Point getP2() { - return getPoint(angularExtent); - } - /** - * Computes a {@link Point} on this {@link Arc}. The {@link Point}'s - * coordinates are calculated by moving the given {@link Angle} on the - * {@link Arc} starting at the {@link Arc} start {@link Point}. - * - * @param angularExtent - * @return the {@link Point} at the given {@link Angle} + * @see IGeometry#contains(Point) */ - public Point getPoint(Angle angularExtent) { - double a = width / 2; - double b = height / 2; - - // // calculate start and end points of the arc from start to end - return new Point(x + a + a - * Math.cos(startAngle.rad() + angularExtent.rad()), y + b - b - * Math.sin(startAngle.rad() + angularExtent.rad())); + public boolean contains(Point p) { + for (CubicCurve c : computeBezierApproximation()) { + if (c.contains(p)) { + return true; + } + } + return false; } /** - * Returns this {@link Arc}'s start {@link Angle}. + * Computes the {@link Point}s of intersection of this {@link Arc} and the + * given {@link ICurve}. * - * @return this {@link Arc}'s start {@link Angle} + * @param c + * @return the intersection {@link Point}s */ - public Angle getStartAngle() { - return startAngle; - } - - public double getX1() { - return getP1().x; - } - - public double getX2() { - return getP2().x; - } - - public double getY1() { - return getP1().y; - } - - public double getY2() { - return getP2().y; + public Point[] getIntersections(ICurve c) { + return CurveUtils.getIntersections(this, c); } public boolean intersects(ICurve c) { @@ -189,7 +100,7 @@ public final class Arc extends AbstractRectangleBasedGeometry implements } public boolean overlaps(ICurve c) { - for (BezierCurve seg1 : toBezier()) { + for (BezierCurve seg1 : computeBezierApproximation()) { if (seg1.overlaps(c)) { return true; } @@ -197,68 +108,35 @@ public final class Arc extends AbstractRectangleBasedGeometry implements return false; } - /** - * Sets the extension {@link Angle} of this {@link Arc}. - * - * @param angularExtent - * the new extension {@link Angle} for this {@link Arc} - */ - public void setAngularExtent(Angle angularExtent) { - this.angularExtent = angularExtent; + public CubicCurve[] toBezier() { + return computeBezierApproximation(); } - /** - * Sets the start {@link Angle} of this {@link Arc}. - * - * @param startAngle - * the new start {@link Angle} for this {@link Arc} - */ - public void setStartAngle(Angle startAngle) { - this.startAngle = startAngle; + @Override + public String toString() { + return "Arc(" + "x = " + x + ", y = " + y + ", width = " + width + + ", height = " + height + ", startAngle = " + startAngle.deg() + + ", angularExtend = " + angularExtent.deg() + ")"; } - public CubicCurve[] toBezier() { - double start = getStartAngle().rad(); - double end = getStartAngle().rad() + getAngularExtent().rad(); + public PolyBezier getRotatedCCW(Angle angle, double cx, double cy) { + return new PolyBezier(computeBezierApproximation()).rotateCCW(angle, + cx, cy); + } - // approximation is for arcs with angle < 90 degrees, so we may have to - // split the arc into up to 4 cubic curves - List segments = new ArrayList(); - if (angularExtent.deg() <= 90.0) { - segments.add(computeApproximation(start, end)); - } else { - // two or more segments, the first will be an ellipse segment - // approximation - segments.add(computeApproximation(start, start + Math.PI / 2)); - if (angularExtent.deg() <= 180.0) { - // two segments, calculate the second (which is below 90 - // degrees) - segments.add(computeApproximation(start + Math.PI / 2, end)); - } else { - // three or more segments, so calculate the second one - segments.add(computeApproximation(start + Math.PI / 2, start - + Math.PI)); - if (angularExtent.deg() <= 270.0) { - // three segments, calculate the third (which is below 90 - // degrees) - segments.add(computeApproximation(start + Math.PI, end)); - } else { - // four segments (fourth below 90 degrees), so calculate the - // third and fourth - segments.add(computeApproximation(start + Math.PI, start - + 3 * Math.PI / 2)); - segments.add(computeApproximation(start + 3 * Math.PI / 2, - end)); - } - } - } - return segments.toArray(new CubicCurve[] {}); + public PolyBezier getRotatedCCW(Angle angle, Point center) { + return new PolyBezier(computeBezierApproximation()).rotateCCW(angle, + center); } - /** - * @see IGeometry#toPath() - */ - public Path toPath() { - return toPath(toBezier()); + public PolyBezier getRotatedCW(Angle angle, double cx, double cy) { + return new PolyBezier(computeBezierApproximation()).rotateCW(angle, cx, + cy); + } + + public PolyBezier getRotatedCW(Angle angle, Point center) { + return new PolyBezier(computeBezierApproximation()).rotateCW(angle, + center); } + } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/BezierCurve.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/BezierCurve.java index 8b2bae2..1f1bdfd 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/BezierCurve.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/BezierCurve.java @@ -14,8 +14,10 @@ package org.eclipse.gef4.geometry.planar; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; -import java.util.List; +import java.util.Iterator; import java.util.Set; import java.util.Stack; @@ -24,21 +26,20 @@ import org.eclipse.gef4.geometry.Point; import org.eclipse.gef4.geometry.euclidean.Vector; import org.eclipse.gef4.geometry.projective.Straight3D; import org.eclipse.gef4.geometry.projective.Vector3D; +import org.eclipse.gef4.geometry.transform.IRotatable; +import org.eclipse.gef4.geometry.transform.IScalable; +import org.eclipse.gef4.geometry.transform.ITranslatable; import org.eclipse.gef4.geometry.utils.PointListUtils; import org.eclipse.gef4.geometry.utils.PrecisionUtils; /** * Abstract base class of Bezier Curves. * - * TODO: make concrete -> leaf specializations in place but delegate - * functionality to here. - * * @author anyssen - * */ -public class BezierCurve extends AbstractGeometry implements ICurve { - - private static final long serialVersionUID = 1L; +public class BezierCurve extends AbstractGeometry implements ICurve, + ITranslatable, IScalable, + IRotatable { private static class FatLine { public static FatLine from(BezierCurve c, boolean ortho) { @@ -186,6 +187,19 @@ public class BezierCurve extends AbstractGeometry implements ICurve { } /** + * Expands this {@link Interval} to include the given other + * {@link Interval}. + * + * @param i + */ + public void expand(Interval i) { + if (i.a < a) + a = i.a; + if (i.b > b) + b = i.b; + } + + /** * Returns a copy of this {@link Interval}. * * @return a copy of this {@link Interval} @@ -305,6 +319,22 @@ public class BezierCurve extends AbstractGeometry implements ICurve { } /** + * Expands this {@link IntervalPair} to include the given other + * {@link IntervalPair}. + * + * @param ip + */ + public void expand(IntervalPair ip) { + if (p == ip.p) { + pi.expand(ip.pi); + qi.expand(ip.qi); + } else { + pi.expand(ip.qi); + qi.expand(ip.pi); + } + } + + /** * Returns a copy of this {@link IntervalPair}. The underlying * {@link BezierCurve}s are only shallow copied. The corresponding * parameter {@link Interval}s are truly copied. @@ -323,7 +353,7 @@ public class BezierCurve extends AbstractGeometry implements ICurve { * @return the first sub-curve of this {@link IntervalPair} */ public BezierCurve getPClipped() { - return p.getClipped(pi.a, pi.b); + return p.getClipped(Math.max(pi.a, 0), Math.min(pi.b, 1)); } /** @@ -350,7 +380,7 @@ public class BezierCurve extends AbstractGeometry implements ICurve { * @return the second sub-curve of this {@link IntervalPair} */ public BezierCurve getQClipped() { - return q.getClipped(qi.a, qi.b); + return q.getClipped(Math.max(qi.a, 0), Math.min(qi.b, 1)); } /** @@ -403,6 +433,8 @@ public class BezierCurve extends AbstractGeometry implements ICurve { public boolean pIsBetterThanQ(Point p, Point q); } + private static final long serialVersionUID = 1L; + // TODO: use constants that limit the number of iterations for the // different iterative/recursive algorithms: // INTERSECTIONS_MAX_ITERATIONS, APPROXIMATION_MAX_ITERATIONS @@ -418,32 +450,49 @@ public class BezierCurve extends AbstractGeometry implements ICurve { private static IntervalPair[] clusterChunks(IntervalPair[] intervalPairs, int shift) { - List clusters = new ArrayList(); + ArrayList ips = new ArrayList(); - // TODO: do something intelligent instead! - boolean isCompletelyClustered = true; + ips.addAll(Arrays.asList(intervalPairs)); - for (IntervalPair ip : intervalPairs) { - boolean isExpansion = false; - - for (IntervalPair cluster : clusters) { - if (isNextTo(cluster, ip, shift)) { - expand(cluster, ip); - isExpansion = true; - break; + Collections.sort(ips, new Comparator() { + public int compare(IntervalPair i, IntervalPair j) { + return i.pi.a <= j.pi.a ? -1 : 1; + } + }); + + // for (IntervalPair ip : ips) { + // System.out.println("P [" + ip.pi.a + ";" + ip.pi.b + "] Q [" + // + ip.qi.a + ";" + ip.qi.b + "]"); + // } + + ArrayList clusters = new ArrayList(); + IntervalPair current = null; + boolean couldMerge; + + do { + clusters.clear(); + couldMerge = false; + for (IntervalPair i : ips) { + if (current == null) { + current = i.getCopy(); + } else if (isNextTo(current, i, shift)) { + couldMerge = true; + current.expand(i); + } else { + isNextTo(current, i, shift); + clusters.add(current); + current = i.getCopy(); } } - - if (!isExpansion) { - clusters.add(ip); - } else { - isCompletelyClustered = false; + if (current != null) { + clusters.add(current); + current = null; } - } + ips.clear(); + ips.addAll(clusters); + } while (couldMerge); - IntervalPair[] clustersArray = clusters.toArray(new IntervalPair[] {}); - return isCompletelyClustered ? clustersArray : clusterChunks( - clustersArray, shift); + return clusters.toArray(new IntervalPair[] {}); } private static void copyIntervalPair(IntervalPair a, IntervalPair b) { @@ -453,19 +502,65 @@ public class BezierCurve extends AbstractGeometry implements ICurve { a.qi = b.qi; } - private static void expand(IntervalPair group, IntervalPair newcomer) { - if (group.pi.a > newcomer.pi.a) { - group.pi.a = newcomer.pi.a; + private static IntervalPair extractOverlap( + IntervalPair[] intersectionCandidates, IntervalPair[] endPoints) { + // merge intersection candidates and end points + IntervalPair[] fineChunks = new IntervalPair[intersectionCandidates.length + + endPoints.length]; + for (int i = 0; i < intersectionCandidates.length; i++) { + fineChunks[i] = intersectionCandidates[i]; } - if (group.pi.b < newcomer.pi.b) { - group.pi.b = newcomer.pi.b; + for (int i = 0; i < endPoints.length; i++) { + fineChunks[intersectionCandidates.length + i] = endPoints[i]; } - if (group.qi.a > newcomer.qi.a) { - group.qi.a = newcomer.qi.a; + + if (fineChunks.length == 0) { + return null; } - if (group.qi.b < newcomer.qi.b) { - group.qi.b = newcomer.qi.b; + + // recluster chunks + normalizeIntervalPairs(fineChunks); + IntervalPair[] chunks = clusterChunks(fineChunks, CHUNK_SHIFT - 1); + + /* + * if they overlap, the chunk has to start/end in a start-/endpoint of + * the curves. + */ + + for (IntervalPair overlap : chunks) { + if (PrecisionUtils.smallerEqual(overlap.pi.a, 0) + && PrecisionUtils.greaterEqual(overlap.pi.b, 1) + || PrecisionUtils.smallerEqual(overlap.qi.a, 0) + && PrecisionUtils.greaterEqual(overlap.qi.b, 1) + || (PrecisionUtils.smallerEqual(overlap.pi.a, 0) || PrecisionUtils + .greaterEqual(overlap.pi.b, 1)) + && (PrecisionUtils.smallerEqual(overlap.qi.a, 0) || PrecisionUtils + .greaterEqual(overlap.qi.b, 1))) { + // it overlaps + if (PrecisionUtils.smallerEqual(overlap.pi.a, 0, + CHUNK_SHIFT - 1) + && PrecisionUtils.smallerEqual(overlap.pi.b, 0, + CHUNK_SHIFT - 1) + || PrecisionUtils.greaterEqual(overlap.pi.a, 1, + CHUNK_SHIFT - 1) + && PrecisionUtils.greaterEqual(overlap.pi.b, 1, + CHUNK_SHIFT - 1) + || PrecisionUtils.smallerEqual(overlap.qi.a, 0, + CHUNK_SHIFT - 1) + && PrecisionUtils.smallerEqual(overlap.qi.b, 0, + CHUNK_SHIFT - 1) + || PrecisionUtils.greaterEqual(overlap.qi.a, 1, + CHUNK_SHIFT - 1) + && PrecisionUtils.greaterEqual(overlap.qi.b, 1, + CHUNK_SHIFT - 1)) { + // only end-point-intersection + return null; + } + return refineOverlap(overlap); + } } + + return null; } /** @@ -515,84 +610,13 @@ public class BezierCurve extends AbstractGeometry implements ICurve { return (y - p.y + m * p.x) / m; } - private static boolean isNextTo(IntervalPair a, IntervalPair b, int shift) { - boolean isPNeighbour = PrecisionUtils.greaterEqual(a.pi.a, b.pi.a, - shift) - && PrecisionUtils.smallerEqual(a.pi.a, b.pi.b, shift) - || PrecisionUtils.smallerEqual(a.pi.a, b.pi.a, shift) - && PrecisionUtils.greaterEqual(a.pi.b, b.pi.a, shift); - boolean isQNeighbour = PrecisionUtils.greaterEqual(a.qi.a, b.qi.a, - shift) - && PrecisionUtils.smallerEqual(a.qi.a, b.qi.b, shift) - || PrecisionUtils.smallerEqual(a.qi.a, b.qi.a, shift) - && PrecisionUtils.greaterEqual(a.qi.b, b.qi.a, shift); - - return isPNeighbour && isQNeighbour; + private static boolean isNextTo(Interval i, Interval j, int shift) { + return PrecisionUtils.smallerEqual(j.a, i.b, shift) + && PrecisionUtils.greaterEqual(j.b, i.a, shift); } - private static IntervalPair isOverlap( - IntervalPair[] intersectionCandidates, IntervalPair[] endPoints) { - // merge intersection candidates and end points - IntervalPair[] fineChunks = new IntervalPair[intersectionCandidates.length - + endPoints.length]; - for (int i = 0; i < intersectionCandidates.length; i++) { - fineChunks[i] = intersectionCandidates[i]; - } - for (int i = 0; i < endPoints.length; i++) { - fineChunks[intersectionCandidates.length + i] = endPoints[i]; - } - - if (fineChunks.length == 0) { - return new IntervalPair(null, null, null, null); - } - - // recluster chunks - normalizeIntervalPairs(fineChunks); - IntervalPair[] chunks = clusterChunks(fineChunks, CHUNK_SHIFT - 1); - - // we should have a single chunk now - if (chunks.length != 1) { - return new IntervalPair(null, null, null, null); - } - - IntervalPair overlap = chunks[0]; - - /* - * if they do overlap in a single point, the point of intersection has - * to be an end-point of both curves. therefore, we do not have to - * consider this case here, because it is already checked in the main - * intersection method. - * - * if they overlap, the chunk has to start/end in a start-/endpoint of - * the curves. - */ - - if (PrecisionUtils.equal(overlap.pi.a, 0) - && PrecisionUtils.equal(overlap.pi.b, 1) - || PrecisionUtils.equal(overlap.qi.a, 0) - && PrecisionUtils.equal(overlap.qi.b, 1) - || (PrecisionUtils.equal(overlap.pi.a, 0) || PrecisionUtils - .equal(overlap.pi.b, 1)) - && (PrecisionUtils.equal(overlap.qi.a, 0) || PrecisionUtils - .equal(overlap.qi.b, 1))) { - // it overlaps - - if (PrecisionUtils.equal(overlap.pi.a, 0, CHUNK_SHIFT - 1) - && PrecisionUtils.equal(overlap.pi.b, 0, CHUNK_SHIFT - 1) - || PrecisionUtils.equal(overlap.pi.a, 1, CHUNK_SHIFT - 1) - && PrecisionUtils.equal(overlap.pi.b, 1, CHUNK_SHIFT - 1) - || PrecisionUtils.equal(overlap.qi.a, 0, CHUNK_SHIFT - 1) - && PrecisionUtils.equal(overlap.qi.b, 0, CHUNK_SHIFT - 1) - || PrecisionUtils.equal(overlap.qi.a, 1, CHUNK_SHIFT - 1) - && PrecisionUtils.equal(overlap.qi.b, 1, CHUNK_SHIFT - 1)) { - // end-point-intersection - return new IntervalPair(null, null, null, null); - } - - return overlap; - } - - return new IntervalPair(null, null, null, null); + private static boolean isNextTo(IntervalPair a, IntervalPair b, int shift) { + return isNextTo(a.pi, b.pi, shift) && isNextTo(a.qi, b.qi, shift); } private static void normalizeIntervalPairs(IntervalPair[] intervalPairs) { @@ -621,6 +645,79 @@ public class BezierCurve extends AbstractGeometry implements ICurve { && PrecisionUtils.equal(p1.y, p2.y, shift); } + /** + * Binary search from the intervals' limits to the intervals' inner values. + * + * @param overlap + * {@link IntervalPair} representing the overlap of two + * {@link BezierCurve}s + * @return refined overlap + */ + private static IntervalPair refineOverlap(IntervalPair overlap) { + Interval piLo = refineOverlapLo(overlap.p, overlap.pi.a, + overlap.pi.getMid(), overlap.q); + Interval piHi = refineOverlapHi(overlap.p, overlap.pi.getMid(), + overlap.pi.b, overlap.q); + Interval qiLo = refineOverlapLo(overlap.q, overlap.qi.a, + overlap.qi.getMid(), overlap.p); + Interval qiHi = refineOverlapHi(overlap.q, overlap.qi.getMid(), + overlap.qi.b, overlap.p); + overlap.pi.a = piLo.b; + overlap.pi.b = piHi.a; + overlap.qi.a = qiLo.b; + overlap.qi.b = qiHi.a; + return overlap; + } + + private static Interval refineOverlapHi(BezierCurve p, double mid, + double b, BezierCurve q) { + Interval i = new Interval(Math.max(mid, 0), Math.min(b, 1)); + double prevLo; + Point pLo; + int c = 0; + + while (c++ < 30 && !i.converges()) { + prevLo = i.a; + i.a = i.getMid(); + pLo = p.get(i.a); + + if (!q.contains(pLo)) { + i.b = i.a; + i.a = prevLo; + } + } + + return i; + } + + /** + * @param p + * @param a + * @param mid + * @param q + * @return + */ + private static Interval refineOverlapLo(BezierCurve p, double a, + double mid, BezierCurve q) { + Interval i = new Interval(Math.max(a, 0), Math.min(mid, 1)); + double prevHi; + Point pHi; + int c = 0; + + while (c++ < 30 && !i.converges()) { + prevHi = i.b; + i.b = i.getMid(); + pHi = p.get(i.b); + + if (!q.contains(pHi)) { + i.a = i.b; + i.b = prevHi; + } + } + + return i; + } + private Vector3D[] points; private static final IPointCmp xminCmp = new IPointCmp() { @@ -816,6 +913,27 @@ public class BezierCurve extends AbstractGeometry implements ICurve { } /** + *

+ * Tests if this {@link BezierCurve} contains the given other + * {@link BezierCurve}. + *

+ * + *

+ * The other {@link BezierCurve} is regarded to be contained if its start + * and end {@link Point} lie on this {@link BezierCurve} and an overlapping + * segment of the two curves can be detected. + *

+ * + * @param o + * @return true if the given {@link BezierCurve} is contained + * by this {@link BezierCurve}, otherwise false + */ + public boolean contains(BezierCurve o) { + return contains(o.getP1()) && contains(o.getP2()) + && getOverlap(o) != null; + } + + /** * Returns true if the given {@link Point} lies on this {@link BezierCurve}. * Returns false, otherwise. * @@ -831,6 +949,23 @@ public class BezierCurve extends AbstractGeometry implements ICurve { return containmentParameter(this, new double[] { 0, 1 }, p); } + @Override + public boolean equals(Object obj) { + if (obj instanceof BezierCurve) { + BezierCurve o = (BezierCurve) obj; + BezierCurve t = this; + while (o.points.length < t.points.length) + o = o.getElevated(); + while (t.points.length < o.points.length) + t = t.getElevated(); + Point[] oPoints = o.getPoints(); + Point[] tPoints = t.getPoints(); + return PointListUtils.equals(oPoints, tPoints) + || PointListUtils.equalsReverse(oPoints, tPoints); + } + return false; + } + private void findEndPointIntersections(IntervalPair ip, Set endPointIntervalPairs, Set intersections) { final double CHUNK_SHIFT_EPSILON = PrecisionUtils @@ -954,8 +1089,12 @@ public class BezierCurve extends AbstractGeometry implements ICurve { if (L1 == null || L2 == null) { // q is degenerated Point poi = ip.q.getHC(ip.qi.getMid()).toPoint(); - if (ip.p.contains(poi)) { + double[] interval = new double[] { 0, 1 }; + if (poi != null && containmentParameter(ip.p, interval, poi)) { intersections.add(poi); + // intervalPairs.add(new IntervalPair(ip.p, + // new Interval(interval), ip.q, new Interval(ip.qi + // .getMid(), ip.qi.getMid()))); } return; } @@ -1195,6 +1334,25 @@ public class BezierCurve extends AbstractGeometry implements ICurve { } /** + * Computes a {@link BezierCurve} with a degree of one higher than this + * {@link BezierCurve}'s degree but of the same shape. + * + * @return a {@link BezierCurve} of the same shape as this + * {@link BezierCurve} but with one more control {@link Point} + */ + public BezierCurve getElevated() { + Point[] p = getPoints(); + Point[] q = new Point[p.length + 1]; + q[0] = p[0]; + q[p.length] = p[p.length - 1]; + for (int i = 1; i < p.length; i++) { + double c = (double) i / (double) (p.length); + q[i] = p[i - 1].getScaled(c).getTranslated(p[i].getScaled(1 - c)); + } + return new BezierCurve(q); + } + + /** * Returns the {@link Point} at the given parameter value t. * * @param t @@ -1259,29 +1417,43 @@ public class BezierCurve extends AbstractGeometry implements ICurve { IntervalPair[] clusters = clusterChunks( intervalPairs.toArray(new IntervalPair[] {}), 0); - if (isOverlap(clusters, - endPointIntervalPairs.toArray(new IntervalPair[] {})).p != null) { - return new HashSet(0); - } + IntervalPair overlapIntervalPair = extractOverlap(clusters, + endPointIntervalPairs.toArray(new IntervalPair[] {})); + BezierCurve overlap = overlapIntervalPair == null ? null + : overlapIntervalPair.getPClipped(); Set results = new HashSet(); - results.addAll(endPointIntervalPairs); - outer: for (IntervalPair cluster : clusters) { - for (IntervalPair epip : endPointIntervalPairs) { - if (isNextTo(cluster, epip, CHUNK_SHIFT)) { - continue outer; + for (IntervalPair epip : endPointIntervalPairs) { + if (overlapIntervalPair == null + || !isNextTo(overlapIntervalPair, epip, CHUNK_SHIFT)) { + results.add(epip); + } else { + for (Iterator iterator = intersections.iterator(); iterator + .hasNext();) { + if (overlap.contains(iterator.next())) { + iterator.remove(); + } } } + } + + outer: for (IntervalPair cluster : clusters) { + if (overlapIntervalPair != null) + if (isNextTo(overlapIntervalPair, cluster, CHUNK_SHIFT)) + continue outer; + + for (IntervalPair epip : endPointIntervalPairs) + if (isNextTo(cluster, epip, CHUNK_SHIFT)) + continue outer; // a.t.m. assume for every cluster just a single point of // intersection: Point poi = findSinglePreciseIntersection(cluster); if (poi != null) { + intersections.add(poi); if (cluster.converges()) { results.add(cluster.getCopy()); - } else { - intersections.add(poi); } } } @@ -1299,38 +1471,7 @@ public class BezierCurve extends AbstractGeometry implements ICurve { */ public Point[] getIntersections(BezierCurve other) { Set intersections = new HashSet(); - Set intervalPairs = new HashSet(); - Set endPointIntervalPairs = new HashSet(); - - IntervalPair ip = new IntervalPair(this, Interval.getFull(), other, - Interval.getFull()); - - findEndPointIntersections(ip, endPointIntervalPairs, intersections); - findIntersectionChunks(ip, intervalPairs, intersections); - normalizeIntervalPairs(intervalPairs.toArray(new IntervalPair[] {})); - IntervalPair[] clusters = clusterChunks( - intervalPairs.toArray(new IntervalPair[] {}), 0); - - if (isOverlap(clusters, - endPointIntervalPairs.toArray(new IntervalPair[] {})).p != null) { - return new Point[] {}; - } - - outer: for (IntervalPair cluster : clusters) { - for (IntervalPair epip : endPointIntervalPairs) { - if (isNextTo(cluster, epip, CHUNK_SHIFT)) { - continue outer; - } - } - - // a.t.m. assume for every cluster just a single point of - // intersection: - Point poi = findSinglePreciseIntersection(cluster); - if (poi != null) { - intersections.add(poi); - } - } - + getIntersectionIntervalPairs(other, intersections); return intersections.toArray(new Point[] {}); } @@ -1366,16 +1507,14 @@ public class BezierCurve extends AbstractGeometry implements ICurve { findEndPointIntersections(ip, endPointIntervalPairs, intersections); findIntersectionChunks(ip, intervalPairs, intersections); - IntervalPair[] clusters = clusterChunks( - intervalPairs.toArray(new IntervalPair[] {}), 0); + IntervalPair[] intervalPairs2 = intervalPairs + .toArray(new IntervalPair[] {}); + normalizeIntervalPairs(intervalPairs2); + IntervalPair[] clusters = clusterChunks(intervalPairs2, 0); - IntervalPair overlap = isOverlap(clusters, + IntervalPair overlap = extractOverlap(clusters, endPointIntervalPairs.toArray(new IntervalPair[] {})); - - if (overlap.p != null) { - return overlap.getPClipped(); - } - return null; + return overlap == null ? null : overlap.getPClipped(); } public Point getP1() { @@ -1455,22 +1594,60 @@ public class BezierCurve extends AbstractGeometry implements ICurve { return copy; } - /** - * Creates a new {@link BezierCurve} with all points translated by the given - * {@link Point}. - * - * @param p - * @return a new {@link BezierCurve} with all points translated by the given - * {@link Point} - */ - public BezierCurve getTranslated(Point p) { - Point[] translated = new Point[points.length]; + public BezierCurve getRotatedCCW(Angle angle) { + return getCopy().rotateCCW(angle); + } - for (int i = 0; i < translated.length; i++) { - translated[i] = points[i].toPoint().getTranslated(p); - } + public BezierCurve getRotatedCCW(Angle angle, double cx, double cy) { + return getCopy().rotateCCW(angle, cx, cy); + } + + public BezierCurve getRotatedCCW(Angle angle, Point center) { + return getCopy().rotateCCW(angle, center); + } + + public BezierCurve getRotatedCW(Angle angle) { + return getCopy().rotateCW(angle); + } + + public BezierCurve getRotatedCW(Angle angle, double cx, double cy) { + return getCopy().rotateCW(angle, cx, cy); + } + + public BezierCurve getRotatedCW(Angle angle, Point center) { + return getCopy().rotateCW(angle, center); + } - return new BezierCurve(translated); + public BezierCurve getScaled(double factor) { + return getCopy().getScaled(factor); + } + + public BezierCurve getScaled(double fx, double fy) { + return getCopy().getScaled(fx, fy); + } + + public BezierCurve getScaled(double factor, double cx, double cy) { + return getCopy().getScaled(factor, cx, cy); + } + + public BezierCurve getScaled(double fx, double fy, double cx, double cy) { + return getCopy().getScaled(fx, fy, cx, cy); + } + + public BezierCurve getScaled(double fx, double fy, Point center) { + return getCopy().getScaled(fx, fy, center); + } + + public BezierCurve getScaled(double factor, Point center) { + return getCopy().getScaled(factor, center); + } + + public BezierCurve getTranslated(double dx, double dy) { + return getCopy().translate(dx, dy); + } + + public BezierCurve getTranslated(Point d) { + return getCopy().translate(d.x, d.y); } public double getX1() { @@ -1520,20 +1697,7 @@ public class BezierCurve extends AbstractGeometry implements ICurve { * overlap, otherwise false */ public boolean overlaps(BezierCurve other) { - Set intersections = new HashSet(); - Set intervalPairs = new HashSet(); - Set endPointIntervalPairs = new HashSet(); - - IntervalPair ip = new IntervalPair(this, Interval.getFull(), other, - Interval.getFull()); - - findEndPointIntersections(ip, endPointIntervalPairs, intersections); - findIntersectionChunks(ip, intervalPairs, intersections); - IntervalPair[] clusters = clusterChunks( - intervalPairs.toArray(new IntervalPair[] {}), 0); - - return isOverlap(clusters, - endPointIntervalPairs.toArray(new IntervalPair[] {})).p != null; + return getOverlap(other) != null; } public final boolean overlaps(ICurve c) { @@ -1545,16 +1709,75 @@ public class BezierCurve extends AbstractGeometry implements ICurve { return false; } - /** - * @param alpha - * @param center - */ - public void rotateCCW(Angle alpha, Point center) { + public BezierCurve rotateCCW(Angle angle) { + Point centroid = PointListUtils.computeCentroid(getPoints()); + return rotateCCW(angle, centroid.x, centroid.y); + } + + public BezierCurve rotateCCW(Angle angle, double cx, double cy) { + Point[] realPoints = getPoints(); + PointListUtils.rotateCCW(realPoints, angle, cx, cy); + for (int i = 0; i < realPoints.length; i++) { + setPoint(i, realPoints[i]); + } + return this; + } + + public BezierCurve rotateCCW(Angle alpha, Point center) { for (int i = 0; i < points.length; i++) { points[i] = new Vector3D(new Vector(points[i].toPoint() .getTranslated(center.getNegated())).getRotatedCCW(alpha) .toPoint().getTranslated(center)); } + return this; + } + + public BezierCurve rotateCW(Angle angle) { + Point centroid = PointListUtils.computeCentroid(getPoints()); + return rotateCW(angle, centroid.x, centroid.y); + } + + public BezierCurve rotateCW(Angle angle, double cx, double cy) { + Point[] realPoints = getPoints(); + PointListUtils.rotateCW(realPoints, angle, cx, cy); + for (int i = 0; i < realPoints.length; i++) { + setPoint(i, realPoints[i]); + } + return this; + } + + public BezierCurve rotateCW(Angle angle, Point center) { + return rotateCW(angle, center.x, center.y); + } + + public BezierCurve scale(double factor) { + return scale(factor, factor); + } + + public BezierCurve scale(double fx, double fy) { + Point centroid = PointListUtils.computeCentroid(getPoints()); + return scale(fx, fy, centroid.x, centroid.y); + } + + public BezierCurve scale(double factor, double cx, double cy) { + return scale(factor, factor, cx, cy); + } + + public BezierCurve scale(double fx, double fy, double cx, double cy) { + Point[] realPoints = getPoints(); + PointListUtils.scale(realPoints, fx, fy, cx, cy); + for (int i = 0; i < realPoints.length; i++) { + setPoint(i, realPoints[i]); + } + return this; + } + + public BezierCurve scale(double fx, double fy, Point center) { + return scale(fx, fy, center.x, center.y); + } + + public BezierCurve scale(double factor, Point center) { + return scale(factor, factor, center.x, center.y); } /** @@ -1641,15 +1864,17 @@ public class BezierCurve extends AbstractGeometry implements ICurve { /** * Returns a hard approximation of this {@link BezierCurve} as a * {@link CubicCurve}. The new {@link CubicCurve} is constructed from the - * first four {@link Point}s in this {@link BezierCurve}'s {@link Point}s - * array. If this {@link BezierCurve} is not of degree four or higher, i.e. - * it does not have four or more control {@link Point}s (including start and - * end {@link Point}), null is returned. + * first three {@link Point}s in this {@link BezierCurve}'s {@link Point}s + * array and the end {@link Point} of this {@link BezierCurve}. If this + * {@link BezierCurve} is not of degree four or higher, i.e. it does not + * have four or more control {@link Point}s (including start and end + * {@link Point}), null is returned. * - * @return a new {@link CubicCurve} that is constructed by the first four - * control {@link Point}s of this {@link BezierCurve} or - * null if this {@link BezierCurve} does not have at - * least four control {@link Point}s + * @return a new {@link CubicCurve} that is constructed by the first three + * {@link Point}s and the end {@link Point} of this + * {@link BezierCurve} or null if this + * {@link BezierCurve} does not have at least four control + * {@link Point}s */ public CubicCurve toCubic() { if (points.length > 3) { @@ -1819,15 +2044,17 @@ public class BezierCurve extends AbstractGeometry implements ICurve { /** * Returns a hard approximation of this {@link BezierCurve} as a * {@link QuadraticCurve}. The new {@link QuadraticCurve} is constructed - * from the first three {@link Point}s in this {@link BezierCurve}'s - * {@link Point}s array. If this {@link BezierCurve} is not of degree three + * from the first two {@link Point}s in this {@link BezierCurve}'s + * {@link Point}s array and the end {@link Point} of this + * {@link BezierCurve}. If this {@link BezierCurve} is not of degree three * or higher, i.e. it does not have three or more control {@link Point}s * (including start and end {@link Point}), null is returned. * - * @return a new {@link QuadraticCurve} that is constructed by the first - * three control {@link Point}s of this {@link BezierCurve} or - * null if this {@link BezierCurve} does not have at - * least three control {@link Point}s + * @return a new {@link QuadraticCurve} that is constructed by the first two + * {@link Point}s and the end {@link Point} of this + * {@link BezierCurve} or null if this + * {@link BezierCurve} does not have at least three control + * {@link Point}s */ public QuadraticCurve toQuadratic() { if (points.length > 2) { @@ -1837,261 +2064,17 @@ public class BezierCurve extends AbstractGeometry implements ICurve { return null; } - // double x1; - // double y1; - // double x2; - // double y2; - // - // // TODO: use point array instead - // double[] ctrlCoordinates = null; - // - // public BezierCurve(double... coordinates) { - // if (coordinates.length < 4) { - // throw new IllegalArgumentException( - // "A bezier curve needs at least a start and an end point"); - // } - // this.x1 = coordinates[0]; - // this.y1 = coordinates[1]; - // this.x2 = coordinates[coordinates.length - 2]; - // this.y2 = coordinates[coordinates.length - 1]; - // if (coordinates.length > 4) { - // this.ctrlCoordinates = new double[coordinates.length - 4]; - // System.arraycopy(coordinates, 2, ctrlCoordinates, 0, - // coordinates.length - 4); - // } - // } - // - // public BezierCurve(Point... points) { - // this(PointListUtils.toCoordinatesArray(points)); - // } - // - // public final boolean contains(Rectangle r) { - // // TODO: may contain the rectangle only in case the rectangle is - // // degenerated... - // return false; - // } - // - // public Point getCtrl(int i) { - // return new Point(getCtrlX(i), getCtrlY(i)); - // } - // - // /** - // * Returns the point-wise coordinates (i.e. x1, y1, x2, y2, etc.) of the - // * inner control points of this {@link BezierCurve}, i.e. exclusive of the - // * start and end points. - // * - // * @see BezierCurve#getCtrls() - // * - // * @return an array containing the inner control points' coordinates - // */ - // public double[] getCtrlCoordinates() { - // return PointListUtils.getCopy(ctrlCoordinates); - // - // } - // - // /** - // * Returns an array of points representing the inner control points of - // this - // * curve, i.e. excluding the start and end points. In case of s linear - // * curve, no control points will be returned, in case of a quadratic - // curve, - // * one control point, and so on. - // * - // * @return an array of points with the coordinates of the inner control - // * points of this {@link BezierCurve}, i.e. exclusive of the start - // * and end point. The number of control points will depend on the - // * degree ({@link #getDegree()}) of the curve, so in case of a line - // * (linear curve) the array will be empty, in case of a quadratic - // * curve, it will be of size 1, in case of a cubic - // * curve of size 2, etc.. - // */ - // public Point[] getCtrls() { - // return PointListUtils.toPointsArray(ctrlCoordinates); - // } - // - // public double getCtrlX(int i) { - // return ctrlCoordinates[2 * i]; - // } - // - // public double getCtrlY(int i) { - // return ctrlCoordinates[2 * i + 1]; - // } - // - // /** - // * Returns the degree of this curve which corresponds to the number of - // * overall control points (including start and end point) used to define - // the - // * curve. The degree is zero-based, so a line (linear curve) will have - // * degree 1, a quadratic curve will have degree - // 2, - // * and so on. 1 in case of a - // * - // * @return The degree of this {@link ICurve}, which corresponds to the - // * zero-based overall number of control points (including start and - // * end point) used to define this {@link ICurve}. - // */ - // public int getDegree() { - // return getCtrls().length + 1; - // } - // - // /** - // * Returns an array of points that represent this {@link BezierCurve}, - // i.e. - // * the start point, the inner control points, and the end points. - // * - // * @return an array of points representing the control points (including - // * start and end point) of this {@link BezierCurve} - // */ - // public Point[] getPoints() { - // Point[] points = new Point[ctrlCoordinates.length / 2 + 2]; - // points[0] = new Point(x1, y1); - // points[points.length - 1] = new Point(x2, y2); - // for (int i = 1; i < points.length - 1; i++) { - // points[i] = new Point(ctrlCoordinates[2 * i - 2], - // ctrlCoordinates[2 * i - 1]); - // } - // return points; - // } - // - // /** - // * {@inheritDoc} - // * - // * @see org.eclipse.gef4.geometry.planar.ICurve#getP1() - // */ - // public Point getP1() { - // return new Point(x1, y1); - // } - // - // /** - // * {@inheritDoc} - // * - // * @see org.eclipse.gef4.geometry.planar.ICurve#getP2() - // */ - // public Point getP2() { - // return new Point(x2, y2); - // } - // - // /** - // * {@inheritDoc} - // * - // * @see org.eclipse.gef4.geometry.planar.ICurve#getX1() - // */ - // public double getX1() { - // return x1; - // } - // - // /** - // * {@inheritDoc} - // * - // * @see org.eclipse.gef4.geometry.planar.ICurve#getX2() - // */ - // public double getX2() { - // return x2; - // } - // - // /** - // * {@inheritDoc} - // * - // * @see org.eclipse.gef4.geometry.planar.ICurve#getY1() - // */ - // public double getY1() { - // return y1; - // } - // - // /** - // * {@inheritDoc} - // * - // * @see org.eclipse.gef4.geometry.planar.ICurve#getY2() - // */ - // public double getY2() { - // return y2; - // } - // - // protected void setCtrl(int i, Point p) { - // setCtrlX(i, p.x); - // setCtrlY(i, p.y); - // } - // - // public void setCtrls(Point... ctrls) { - // ctrlCoordinates = PointListUtils.toCoordinatesArray(ctrls); - // } - // - // protected void setCtrlX(int i, double x) { - // // TODO: enlarge array if its too small - // ctrlCoordinates[2 * i] = x; - // } - // - // protected void setCtrlY(int i, double y) { - // // TODO: enlarge array if its too small - // ctrlCoordinates[2 * i + 1] = y; - // } - // - // /** - // * Sets the start {@link Point} of this {@link BezierCurve} to the given - // * {@link Point} p1. - // * - // * @param p1 - // * the new start {@link Point} - // */ - // public void setP1(Point p1) { - // this.x1 = p1.x; - // this.y1 = p1.y; - // } - // - // /** - // * Sets the end {@link Point} of this {@link BezierCurve} to the given - // * {@link Point} p2. - // * - // * @param p2 - // * the new end {@link Point} - // */ - // public void setP2(Point p2) { - // this.x2 = p2.x; - // this.y2 = p2.y; - // } - // - // /** - // * Sets the x-coordinate of the start {@link Point} of this - // * {@link BezierCurve} to x1. - // * - // * @param x1 - // * the new start {@link Point}'s x-coordinate - // */ - // public void setX1(double x1) { - // this.x1 = x1; - // } - // - // /** - // * Sets the x-coordinate of the end {@link Point} of this - // * {@link BezierCurve} to x2. - // * - // * @param x2 - // * the new end {@link Point}'s x-coordinate - // */ - // public void setX2(double x2) { - // this.x2 = x2; - // } - // - // /** - // * Sets the y-coordinate of the start {@link Point} of this - // * {@link BezierCurve} to y1. - // * - // * @param y1 - // * the new start {@link Point}'s y-coordinate - // */ - // public void setY1(double y1) { - // this.y1 = y1; - // } - // - // /** - // * Sets the y-coordinate of the end {@link Point} of this - // * {@link BezierCurve} to y2. - // * - // * @param y2 - // * the new end {@link Point}'s y-coordinate - // */ - // public void setY2(double y2) { - // this.y2 = y2; - // } + public BezierCurve translate(double dx, double dy) { + Point[] realPoints = getPoints(); + PointListUtils.translate(realPoints, dx, dy); + for (int i = 0; i < realPoints.length; i++) { + setPoint(i, realPoints[i]); + } + return this; + } + + public BezierCurve translate(Point d) { + return translate(d.x, d.y); + } } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/BezierSpline.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/BezierSpline.java index 0373f1e..ce94af9 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/BezierSpline.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/BezierSpline.java @@ -14,7 +14,7 @@ package org.eclipse.gef4.geometry.planar; import org.eclipse.gef4.geometry.Point; import org.eclipse.gef4.geometry.transform.AffineTransform; -public class BezierSpline implements ICurve { +public class BezierSpline extends AbstractGeometry implements ICurve { public boolean contains(Point p) { // TODO Auto-generated method stub diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/CubicCurve.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/CubicCurve.java index e77beb5..9fe1bd9 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/CubicCurve.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/CubicCurve.java @@ -14,13 +14,14 @@ package org.eclipse.gef4.geometry.planar; import org.eclipse.gef4.geometry.Point; import org.eclipse.gef4.geometry.transform.AffineTransform; -import org.eclipse.gef4.geometry.utils.PolynomCalculationUtils; /** * Represents the geometric shape of a cubic Bézier curve. * - * @author anyssen + * TODO: Overwrite all BezierCurve methods that return a BezierCurve and add a + * cast to a CubicCurve. OR: Make BezierCurve parameterized * + * @author anyssen */ public class CubicCurve extends BezierCurve { @@ -124,70 +125,40 @@ public class CubicCurve extends BezierCurve { } /** + * Erroneous getBounds() implementation... use the generic one instead. + * + * TODO: find out why the mathematical solution is erroneous in some cases. + * * @see IGeometry#getBounds() */ - @Override - public Rectangle getBounds() { - // extremes of the x(t) and y(t) functions: - double[] xts; - try { - xts = PolynomCalculationUtils.getQuadraticRoots(-3 * getX1() + 9 - * getCtrlX1() - 9 * getCtrlX2() + 3 * getX2(), 6 * getX1() - - 12 * getCtrlX1() + 6 * getCtrlX2(), 3 * getCtrlX1() - 3 - * getX1()); - } catch (ArithmeticException x) { - return new Rectangle(getP1(), getP2()); - } - - double xmin = getX1(), xmax = getX1(); - if (getX2() < xmin) { - xmin = getX2(); - } else { - xmax = getX2(); - } - - for (double t : xts) { - if (t >= 0 && t <= 1) { - double x = get(t).x; - if (x < xmin) { - xmin = x; - } else if (x > xmax) { - xmax = x; - } - } - } - - double[] yts; - try { - yts = PolynomCalculationUtils.getQuadraticRoots(-3 * getY1() + 9 - * getCtrlY1() - 9 * getCtrlY2() + 3 * getY2(), 6 * getY1() - - 12 * getCtrlY1() + 6 * getCtrlY2(), 3 * getCtrlY1() - 3 - * getY1()); - } catch (ArithmeticException x) { - return new Rectangle(new Point(xmin, getP1().y), new Point(xmax, - getP2().y)); - } - - double ymin = getY1(), ymax = getY1(); - if (getY2() < ymin) { - ymin = getY2(); - } else { - ymax = getY2(); - } - - for (double t : yts) { - if (t >= 0 && t <= 1) { - double y = get(t).y; - if (y < ymin) { - ymin = y; - } else if (y > ymax) { - ymax = y; - } - } - } - - return new Rectangle(new Point(xmin, ymin), new Point(xmax, ymax)); - } + /* + * public Rectangle getBounds() { // extremes of the x(t) and y(t) + * functions: double[] xts; try { xts = + * PolynomCalculationUtils.getQuadraticRoots(-3 * getX1() + 9 getCtrlX1() - + * 9 * getCtrlX2() + 3 * getX2(), 6 * getX1() - 12 * getCtrlX1() + 6 * + * getCtrlX2(), 3 * getCtrlX1() - 3 getX1()); } catch (ArithmeticException + * x) { return new Rectangle(getP1(), getP2()); } + * + * double xmin = getX1(), xmax = getX1(); if (getX2() < xmin) { xmin = + * getX2(); } else { xmax = getX2(); } + * + * for (double t : xts) { if (t >= 0 && t <= 1) { double x = get(t).x; if (x + * < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } } } + * + * double[] yts; try { yts = PolynomCalculationUtils.getQuadraticRoots(-3 * + * getY1() + 9 getCtrlY1() - 9 * getCtrlY2() + 3 * getY2(), 6 * getY1() - 12 + * * getCtrlY1() + 6 * getCtrlY2(), 3 * getCtrlY1() - 3 getY1()); } catch + * (ArithmeticException x) { return new Rectangle(new Point(xmin, + * getP1().y), new Point(xmax, getP2().y)); } + * + * double ymin = getY1(), ymax = getY1(); if (getY2() < ymin) { ymin = + * getY2(); } else { ymax = getY2(); } + * + * for (double t : yts) { if (t >= 0 && t <= 1) { double y = get(t).y; if (y + * < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } } } + * + * return new Rectangle(new Point(xmin, ymin), new Point(xmax, ymax)); } + */ private Polygon getControlPolygon() { return new Polygon(getP1(), getCtrl1(), getCtrl2(), getP2()); diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Ellipse.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Ellipse.java index e9d41ce..868f8be 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Ellipse.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Ellipse.java @@ -18,6 +18,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import org.eclipse.gef4.geometry.Angle; import org.eclipse.gef4.geometry.Point; import org.eclipse.gef4.geometry.transform.AffineTransform; import org.eclipse.gef4.geometry.utils.CurveUtils; @@ -34,8 +35,8 @@ import org.eclipse.gef4.geometry.utils.PrecisionUtils; * @author anyssen * @author Matthias Wienand */ -public class Ellipse extends AbstractRectangleBasedGeometry implements - IShape { +public class Ellipse extends + AbstractRectangleBasedGeometry implements IShape { private static final long serialVersionUID = 1L; @@ -344,40 +345,15 @@ public class Ellipse extends AbstractRectangleBasedGeometry implements * @return border-segments */ public CubicCurve[] getOutlineSegments() { - CubicCurve[] segs = new CubicCurve[4]; - // see http://whizkidtech.redprince.net/bezier/circle/kappa/ for details - // on the approximation used here - final double kappa = 4.0d * (Math.sqrt(2.0d) - 1.0d) / 3.0d; - double a = width / 2; - double b = height / 2; - - double ox = x + a; - double oy = y; - - segs[0] = new CubicCurve(ox, oy, x + a + kappa * a, y, x + width, y + b - - kappa * b, x + width, y + b); - - ox = x + width; - oy = y + b; - - segs[1] = new CubicCurve(ox, oy, x + width, y + b + kappa * b, x + a - + kappa * a, y + height, x + a, y + height); - - ox = x + a; - oy = y + height; - - segs[2] = new CubicCurve(ox, oy, x + width / 2 - kappa * width / 2, y - + height, x, y + height / 2 + kappa * height / 2, x, y + height - / 2); - - ox = x; - oy = y + height / 2; - - segs[3] = new CubicCurve(ox, oy, x, - y + height / 2 - kappa * height / 2, x + width / 2 - kappa - * width / 2, y, x + width / 2, y); - - return segs; + return new CubicCurve[] { + CurveUtils.computeEllipticalArcApproximation(x, y, width, + height, Angle.fromDeg(0), Angle.fromDeg(90)), + CurveUtils.computeEllipticalArcApproximation(x, y, width, + height, Angle.fromDeg(90), Angle.fromDeg(180)), + CurveUtils.computeEllipticalArcApproximation(x, y, width, + height, Angle.fromDeg(180), Angle.fromDeg(270)), + CurveUtils.computeEllipticalArcApproximation(x, y, width, + height, Angle.fromDeg(270), Angle.fromDeg(360)), }; } /** @@ -398,26 +374,44 @@ public class Ellipse extends AbstractRectangleBasedGeometry implements public Path toPath() { // see http://whizkidtech.redprince.net/bezier/circle/kappa/ for details // on the approximation used here - final double kappa = 4.0d * (Math.sqrt(2.0d) - 1.0d) / 3.0d; - final Path p = new Path(); - double a = width / 2; - double b = height / 2; - p.moveTo(x + a, y); - p.curveTo(x + a + kappa * a, y, x + width, y + b - kappa * b, - x + width, y + b); - p.curveTo(x + width, y + b + kappa * b, x + a + kappa * a, y + height, - x + a, y + height); - p.curveTo(x + width / 2 - kappa * width / 2, y + height, x, y + height - / 2 + kappa * height / 2, x, y + height / 2); - p.curveTo(x, y + height / 2 - kappa * height / 2, x + width / 2 - kappa - * width / 2, y, x + width / 2, y); - return p; + return CurveUtils.toPath(CurveUtils.computeEllipticalArcApproximation( + x, y, width, height, Angle.fromDeg(0), Angle.fromDeg(90)), + CurveUtils.computeEllipticalArcApproximation(x, y, width, + height, Angle.fromDeg(90), Angle.fromDeg(180)), + CurveUtils.computeEllipticalArcApproximation(x, y, width, + height, Angle.fromDeg(180), Angle.fromDeg(270)), + CurveUtils.computeEllipticalArcApproximation(x, y, width, + height, Angle.fromDeg(270), Angle.fromDeg(360))); } @Override public String toString() { - return "Ellipse: (" + x + ", " + y + ", " + //$NON-NLS-3$//$NON-NLS-2$//$NON-NLS-1$ + return "Ellipse (" + x + ", " + y + ", " + //$NON-NLS-3$//$NON-NLS-2$//$NON-NLS-1$ width + ", " + height + ")";//$NON-NLS-2$//$NON-NLS-1$ } + public PolyBezier getRotatedCCW(Angle angle) { + return new PolyBezier(getOutlineSegments()).rotateCCW(angle); + } + + public PolyBezier getRotatedCCW(Angle angle, double cx, double cy) { + return new PolyBezier(getOutlineSegments()).rotateCCW(angle, cx, cy); + } + + public PolyBezier getRotatedCCW(Angle angle, Point center) { + return new PolyBezier(getOutlineSegments()).rotateCCW(angle, center); + } + + public PolyBezier getRotatedCW(Angle angle) { + return new PolyBezier(getOutlineSegments()).rotateCW(angle); + } + + public PolyBezier getRotatedCW(Angle angle, double cx, double cy) { + return new PolyBezier(getOutlineSegments()).rotateCW(angle, cx, cy); + } + + public PolyBezier getRotatedCW(Angle angle, Point center) { + return new PolyBezier(getOutlineSegments()).rotateCW(angle, center); + } + } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/IPolyShape.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/IPolyShape.java index 3898f50..bd3e9af 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/IPolyShape.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/IPolyShape.java @@ -25,6 +25,30 @@ public interface IPolyShape extends IGeometry { */ IShape[] getShapes(); - // contains() + /** + *

+ * Computes the outline segments of this {@link IPolyShape}. + *

+ * + *

+ * Each {@link ICurve} segment of the outline of the internal {@link IShape} + * s can be either an inner segment or an outer segment. This method + * extracts only the outer segments. The segments bordering voids are + * considered to be outer segments, too. + *

+ * + * @return the outline segments of this {@link IPolyShape} + */ + public ICurve[] getOutlineSegments(); + + /** + * Checks if the given {@link IGeometry} is fully contained by this + * {@link IPolyShape}. + * + * @param g + * @return true if the {@link IGeometry} is contained by this + * {@link IPolyShape}, otherwise false + */ + public boolean contains(final IGeometry g); } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Line.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Line.java index 7ab0e71..49948ff 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Line.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Line.java @@ -12,6 +12,9 @@ *******************************************************************************/ package org.eclipse.gef4.geometry.planar; +import java.util.HashSet; +import java.util.Set; + import org.eclipse.gef4.geometry.Point; import org.eclipse.gef4.geometry.euclidean.Straight; import org.eclipse.gef4.geometry.euclidean.Vector; @@ -198,6 +201,43 @@ public class Line extends BezierCurve { : null; } + /** + * Provides an optimized version of the + * {@link BezierCurve#getIntersectionIntervalPairs(BezierCurve, Set)} + * method. + * + * @param other + * @param intersections + * @return see + * {@link BezierCurve#getIntersectionIntervalPairs(BezierCurve, Set)} + */ + public Set getIntersectionIntervalPairs(Line other, + Set intersections) { + Straight s1 = new Straight(this); + Straight s2 = new Straight(other); + Vector vi = s1.getIntersection(s2); + if (vi != null) { + Point pi = vi.toPoint(); + if (contains(pi)) { + double param1 = s1.getParameterAt(pi); + double param2 = s2.getParameterAt(pi); + HashSet intervalPairs = new HashSet(); + intervalPairs.add(new IntervalPair(this, new Interval(param1, + param1), other, new Interval(param2, param2))); + return intervalPairs; + } + } + return new HashSet(); + } + + public Set getIntersectionIntervalPairs(BezierCurve other, + Set intersections) { + if (other instanceof Line) { + return getIntersectionIntervalPairs((Line) other, intersections); + } + return super.getIntersectionIntervalPairs(other, intersections); + } + @Override public Point[] getIntersections(BezierCurve curve) { if (curve instanceof Line) { @@ -233,6 +273,13 @@ public class Line extends BezierCurve { return new Line(transformed[0], transformed[1]); } + /** + * Provides an optimized version of the + * {@link BezierCurve#intersects(ICurve)} method. + * + * @param l + * @return see {@link BezierCurve#intersects(ICurve)} + */ public boolean intersects(Line l) { return getIntersection(l) != null; } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Pie.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Pie.java index effb8f7..90c69d2 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Pie.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Pie.java @@ -7,28 +7,110 @@ * * Contributors: * Alexander Nyßen (itemis AG) - initial API and implementation + * Matthias Wienand (itemis AG) - contribution for Bugzilla #355997 * *******************************************************************************/ package org.eclipse.gef4.geometry.planar; +import org.eclipse.gef4.geometry.Angle; import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.euclidean.Vector; +import org.eclipse.gef4.geometry.utils.CurveUtils; +import org.eclipse.gef4.geometry.utils.PrecisionUtils; -public class Pie extends AbstractGeometry implements IGeometry { +/** + * The {@link Pie} is a closed {@link AbstractArcBasedGeometry}. It is the + * complement of the {@link Arc}, which is an open + * {@link AbstractArcBasedGeometry}. + * + * The {@link Pie} covers an area, therefore it implements the {@link IShape} + * interface. + */ +public class Pie extends AbstractArcBasedGeometry implements IShape { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new {@link Pie} from the given values. + * + * @see AbstractArcBasedGeometry#AbstractArcBasedGeometry(double, double, + * double, double, Angle, Angle) + * + * @param x + * @param y + * @param width + * @param height + * @param startAngle + * @param angularExtent + */ + public Pie(double x, double y, double width, double height, + Angle startAngle, Angle angularExtent) { + super(x, y, width, height, startAngle, angularExtent); + } + + /** + * @see org.eclipse.gef4.geometry.planar.IGeometry#getCopy() + */ + public Pie getCopy() { + return new Pie(x, y, width, height, startAngle, angularExtent); + } + + public PolyBezier getOutline() { + return new PolyBezier(computeBezierApproximation()); + } + + public CubicCurve[] getOutlineSegments() { + return computeBezierApproximation(); + } public boolean contains(Point p) { - throw new UnsupportedOperationException("Not yet implemented."); + // check if the point is in the arc's angle + Angle pAngle = new Vector(1, 0) + .getAngleCCW(new Vector(getCentroid(), p)); + if (!(PrecisionUtils.greater(pAngle.rad(), startAngle.rad()) && PrecisionUtils + .smaller(pAngle.rad(), startAngle.getAdded(angularExtent).rad()))) { + return false; + } + + // angle is correct, check if the point is inside the bounding ellipse + return new Ellipse(x, y, width, height).contains(p); } - public Rectangle getBounds() { - throw new UnsupportedOperationException("Not yet implemented."); + public boolean contains(IGeometry g) { + return CurveUtils.contains(this, g); } public Path toPath() { - throw new UnsupportedOperationException("Not yet implemented."); + CubicCurve[] arc = computeBezierApproximation(); + Line endToMid = new Line(arc[arc.length - 1].getP2(), getCentroid()); + Line midToStart = new Line(getCentroid(), arc[0].getP1()); + ICurve[] curves = new ICurve[arc.length + 2]; + for (int i = 0; i < arc.length; i++) { + curves[i] = arc[i]; + } + curves[arc.length] = endToMid; + curves[arc.length + 1] = midToStart; + return CurveUtils.toPath(curves); + } + + public Path getRotatedCCW(Angle angle, double cx, double cy) { + return new PolyBezier(computeBezierApproximation()).rotateCCW(angle, + cx, cy).toPath(); + } + + public Path getRotatedCCW(Angle angle, Point center) { + return new PolyBezier(computeBezierApproximation()).rotateCCW(angle, + center).toPath(); + } + + public Path getRotatedCW(Angle angle, double cx, double cy) { + return new PolyBezier(computeBezierApproximation()).rotateCW(angle, cx, + cy).toPath(); } - public IGeometry getCopy() { - throw new UnsupportedOperationException("Not yet implemented."); + public Path getRotatedCW(Angle angle, Point center) { + return new PolyBezier(computeBezierApproximation()).rotateCW(angle, + center).toPath(); } } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/PolyBezier.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/PolyBezier.java index 5124233..4d487ad 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/PolyBezier.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/PolyBezier.java @@ -11,16 +11,37 @@ *******************************************************************************/ package org.eclipse.gef4.geometry.planar; +import java.util.ArrayList; +import java.util.Arrays; + +import org.eclipse.gef4.geometry.Angle; import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.transform.IRotatable; +import org.eclipse.gef4.geometry.transform.IScalable; +import org.eclipse.gef4.geometry.transform.ITranslatable; import org.eclipse.gef4.geometry.utils.CurveUtils; +import org.eclipse.gef4.geometry.utils.PointListUtils; /** * A {@link PolyBezier} is an {@link IPolyCurve} which consists of one or more * connected {@link BezierCurve}s. */ -public class PolyBezier extends AbstractGeometry implements IPolyCurve { +public class PolyBezier extends AbstractGeometry implements IPolyCurve, + ITranslatable, IScalable, + IRotatable { private static final long serialVersionUID = 1L; + + private static BezierCurve[] copy(BezierCurve... beziers) { + BezierCurve[] copy = new BezierCurve[beziers.length]; + + for (int i = 0; i < beziers.length; i++) { + copy[i] = beziers[i].getCopy(); + } + + return copy; + } + private BezierCurve[] beziers; /** @@ -54,45 +75,96 @@ public class PolyBezier extends AbstractGeometry implements IPolyCurve { return bounds; } - public Path toPath() { - // TODO: need a Path.append(Path) - throw new UnsupportedOperationException("Not yet implemented."); + public PolyBezier getCopy() { + return new PolyBezier(beziers); } - public IGeometry getCopy() { - return new PolyBezier(beziers); + public BezierCurve[] getCurves() { + return copy(beziers); } - public double getY2() { - return getP2().y; + public Point[] getIntersections(ICurve g) { + return CurveUtils.getIntersections(g, this); } - public double getY1() { - return getP1().y; + public Point getP1() { + return beziers[0].getP1(); } - public double getX2() { - return getP2().x; + public Point getP2() { + return beziers[beziers.length - 1].getP2(); } - public double getX1() { - return getP1().x; + public PolyBezier getRotatedCCW(Angle angle) { + return getCopy().rotateCCW(angle); } - public Point getP2() { - return beziers[beziers.length - 1].getP2(); + public PolyBezier getRotatedCCW(Angle angle, double cx, double cy) { + return getCopy().getRotatedCCW(angle, cx, cy); } - public Point getP1() { - return beziers[0].getP1(); + public PolyBezier getRotatedCCW(Angle angle, Point center) { + return getCopy().getRotatedCCW(angle, center); } - public BezierCurve[] toBezier() { - return copy(beziers); + public PolyBezier getRotatedCW(Angle angle) { + return getCopy().getRotatedCW(angle); } - public Point[] getIntersections(ICurve g) { - return CurveUtils.getIntersections(g, this); + public PolyBezier getRotatedCW(Angle angle, double cx, double cy) { + return getCopy().getRotatedCW(angle, cx, cy); + } + + public PolyBezier getRotatedCW(Angle angle, Point center) { + return getCopy().getRotatedCW(angle, center); + } + + public PolyBezier getScaled(double factor) { + return getCopy().scale(factor); + } + + public PolyBezier getScaled(double fx, double fy) { + return getCopy().scale(fx, fy); + } + + public PolyBezier getScaled(double factor, double cx, double cy) { + return getCopy().scale(factor, cx, cy); + } + + public PolyBezier getScaled(double fx, double fy, double cx, double cy) { + return getCopy().scale(fx, fy, cx, cy); + } + + public PolyBezier getScaled(double fx, double fy, Point center) { + return getCopy().scale(fx, fy, center); + } + + public PolyBezier getScaled(double factor, Point center) { + return getCopy().scale(factor, center); + } + + public PolyBezier getTranslated(double dx, double dy) { + return getCopy().translate(dx, dy); + } + + public PolyBezier getTranslated(Point d) { + return getCopy().translate(d.x, d.y); + } + + public double getX1() { + return getP1().x; + } + + public double getX2() { + return getP2().x; + } + + public double getY1() { + return getP1().y; + } + + public double getY2() { + return getP2().y; } public boolean intersects(ICurve c) { @@ -103,18 +175,98 @@ public class PolyBezier extends AbstractGeometry implements IPolyCurve { return CurveUtils.overlaps(c, this); } - public BezierCurve[] getCurves() { + public PolyBezier rotateCCW(Angle angle) { + ArrayList points = new ArrayList(); + for (BezierCurve c : beziers) { + points.addAll(Arrays.asList(c.getPoints())); + } + Point centroid = PointListUtils.computeCentroid(points + .toArray(new Point[] {})); + return rotateCCW(angle, centroid.x, centroid.y); + } + + public PolyBezier rotateCCW(Angle angle, double cx, double cy) { + for (BezierCurve c : beziers) { + c.rotateCCW(angle, cx, cy); + } + return this; + } + + public PolyBezier rotateCCW(Angle angle, Point center) { + return rotateCCW(angle, center.x, center.y); + } + + public PolyBezier rotateCW(Angle angle) { + ArrayList points = new ArrayList(); + for (BezierCurve c : beziers) { + points.addAll(Arrays.asList(c.getPoints())); + } + Point centroid = PointListUtils.computeCentroid(points + .toArray(new Point[] {})); + return rotateCW(angle, centroid.x, centroid.y); + } + + public PolyBezier rotateCW(Angle angle, double cx, double cy) { + for (BezierCurve c : beziers) { + c.rotateCW(angle, cx, cy); + } + return this; + } + + public PolyBezier rotateCW(Angle angle, Point center) { + return rotateCW(angle, center.x, center.y); + } + + public PolyBezier scale(double factor) { + return scale(factor, factor); + } + + public PolyBezier scale(double fx, double fy) { + ArrayList points = new ArrayList(); + for (BezierCurve c : beziers) { + points.addAll(Arrays.asList(c.getPoints())); + } + Point centroid = PointListUtils.computeCentroid(points + .toArray(new Point[] {})); + return scale(fx, fy, centroid.x, centroid.y); + } + + public PolyBezier scale(double factor, double cx, double cy) { + return scale(factor, factor, cx, cy); + } + + public PolyBezier scale(double fx, double fy, double cx, double cy) { + for (BezierCurve c : beziers) { + c.scale(fx, fy, cx, cy); + } + return this; + } + + public PolyBezier scale(double fx, double fy, Point center) { + return scale(fx, fx, center.x, center.y); + } + + public PolyBezier scale(double factor, Point center) { + return scale(factor, factor, center.x, center.y); + } + + public BezierCurve[] toBezier() { return copy(beziers); } - private static BezierCurve[] copy(BezierCurve... beziers) { - BezierCurve[] copy = new BezierCurve[beziers.length]; + public Path toPath() { + return CurveUtils.toPath(beziers); + } - for (int i = 0; i < beziers.length; i++) { - copy[i] = beziers[i].getCopy(); + public PolyBezier translate(double dx, double dy) { + for (BezierCurve c : beziers) { + c.translate(dx, dy); } + return this; + } - return copy; + public PolyBezier translate(Point d) { + return translate(d.x, d.y); } } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Polygon.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Polygon.java index d366acd..5e9e3cf 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Polygon.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Polygon.java @@ -13,6 +13,10 @@ package org.eclipse.gef4.geometry.planar; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; import org.eclipse.gef4.geometry.Point; import org.eclipse.gef4.geometry.transform.AffineTransform; @@ -35,6 +39,24 @@ public class Polygon extends AbstractPointListBasedGeometry implements IShape { /** + * The {@link NonSimplePolygonException} is thrown if a non-simple + * {@link Polygon}, i.e. a {@link Polygon} with self-intersections, is asked + * for its triangulation ({@link Polygon#getTriangulation()}). + */ + @SuppressWarnings("serial") + public class NonSimplePolygonException extends RuntimeException { + @SuppressWarnings("javadoc") + public NonSimplePolygonException() { + super(); + } + + @SuppressWarnings("javadoc") + public NonSimplePolygonException(String s) { + super(s); + } + } + + /** * Pair of {@link Line} segment and integer counter to count segments of * {@link Polygon}s. */ @@ -73,6 +95,87 @@ public class Polygon extends AbstractPointListBasedGeometry implements private static final long serialVersionUID = 1L; + private static Polygon clipEar(Polygon p, int[] ear, ArrayList ears) { + Point[] points = p.getPoints(); + ears.add(new Polygon(points[ear[0]], points[ear[1]], points[ear[2]])); + return new Polygon(getPointsWithout(points, ear[1])); + } + + /** + * Searches the given list of {@link Point}s for a vertex that starts an + * ear. An ear is a list of 3 vertices which build up a triangle that lies + * inside the {@link Polygon} respective to the list of {@link Point}s and + * can be clipped out of it so that the remaining {@link Polygon} remains + * simple. + * + * @param points + * @return + */ + private static int[] findEarVertex(Polygon p) { + Point[] points = p.getPoints(); + + for (int start = 0; start < points.length; start++) { + int mid = start == points.length - 1 ? 0 : start + 1; + int end = start == points.length - 2 ? 0 + : start == points.length - 1 ? 1 : start + 2; + + if (p.contains(new Line(points[start], points[end]))) { + return new int[] { start, mid, end }; + } + } + + // this should never happen (for simple polygons) + return null; + } + + private static Point[] getPointsWithout(Point[] points, + int... indicesToRemove) { + Point[] rest = new Point[points.length - indicesToRemove.length]; + Arrays.sort(indicesToRemove); + for (int i = 0, j = 0; i < indicesToRemove.length; i++) { + for (int r = j; r < indicesToRemove[i]; r++) + rest[r - i] = points[r]; + j = indicesToRemove[i] + 1; + } + for (int i = indicesToRemove[indicesToRemove.length - 1] + 1; i < points.length; i++) + rest[i - indicesToRemove.length] = points[i]; + return rest; + } + + /** + * Clips exactly one ear off of the given {@link Polygon} and adds it to the + * list of ears. If the resulting {@link Polygon} is a triangle, this is + * added to the list of ears, too. Otherwise, the method recurses. + * + * @param p + * @param ears + */ + private static void triangulate(Polygon p, ArrayList ears) { + if (p == null) { + throw new IllegalArgumentException( + "The given Polygon may not be null."); + } + if (ears == null) { + throw new IllegalArgumentException( + "The given ear-list may not be null."); + } + if (p.points.length < 3) { + throw new IllegalArgumentException( + "The given Polygon may not have less than three vertices."); + } + + if (p.points.length == 3) { + ears.add(p.getCopy()); + return; + } + + int[] ear = findEarVertex(p); + Polygon rest = clipEar(p, ear, ears); + + // recurse + triangulate(rest, ears); + } + /** * Constructs a new {@link Polygon} from a even-numbered sequence of * coordinates. Similar to {@link Polygon#Polygon(Point...)}, only that @@ -102,6 +205,34 @@ public class Polygon extends AbstractPointListBasedGeometry implements } /** + * Assures that this {@link Polygon} is simple, i.e. it does not have any + * self-intersections. We do not need to test for voids as they are not + * considered in the interpretation of the {@link Polygon}'s {@link Point}s. + * + * If the {@link Polygon} does not have at least three vertices, a + * {@link NonSimplePolygonException} is thrown. + * + * The edges are added to the {@link Polygon} one after the other. If a + * self-intersection is found an {@link NonSimplePolygonException} is + * thrown. + */ + private void assureSimplicity() throws NonSimplePolygonException { + if (points.length < 3) + throw new NonSimplePolygonException( + "A polygon can only be constructed of at least 3 vertices."); + + for (Line e1 : getOutlineSegments()) + for (Line e2 : getOutlineSegments()) + if (!e1.getP1().equals(e2.getP1()) + && !e1.getP2().equals(e2.getP1()) + && !e1.getP1().equals(e2.getP2()) + && !e1.getP2().equals(e2.getP2())) + if (e1.touches(e2)) + throw new NonSimplePolygonException( + "Only simple polygons allowed. A polygon without any self-intersections is considered to be simple. This polygon is not simple."); + } + + /** * Checks whether the point that is represented by its x- and y-coordinates * is contained within this {@link Polygon}. * @@ -117,6 +248,67 @@ public class Polygon extends AbstractPointListBasedGeometry implements return contains(new Point(x, y)); } + public boolean contains(IGeometry g) { + if (g instanceof Line) { + return contains((Line) g); + } else if (g instanceof Polygon) { + return contains((Polygon) g); + } else if (g instanceof Polyline) { + return contains((Polyline) g); + } else if (g instanceof Rectangle) { + return contains((Rectangle) g); + } + return CurveUtils.contains(this, g); + } + + /** + * Checks whether the given {@link Line} is fully contained within this + * {@link Polygon}. + * + * @param line + * The {@link Line} to test for containment + * @return true if the given {@link Line} is fully contained, + * false otherwise + */ + public boolean contains(Line line) { + // quick rejection test: if the end points are not contained, the line + // may not be contained + if (!contains(line.getP1()) || !contains(line.getP2())) { + return false; + } + + Set intersectionParams = new HashSet(); + + for (Line seg : getOutlineSegments()) { + Point poi = seg.getIntersection(line); + if (poi != null) + intersectionParams.add(line.getParameterAt(poi)); + } + + if (intersectionParams.size() <= 1) { + return true; + } + + Double[] poiParams = intersectionParams.toArray(new Double[] {}); + Arrays.sort(poiParams, new Comparator() { + public int compare(Double t, Double u) { + double d = t - u; + return d < 0 ? -1 : d > 0 ? 1 : 0; + } + }); + + // check the points between the intersections for containment + if (!contains(line.get(poiParams[0] / 2))) { + return false; + } + for (int i = 0; i < poiParams.length - 1; i++) { + if (!contains(line.get((poiParams[i] + poiParams[i + 1]) / 2))) { + return false; + } + } + return contains(line.get((poiParams[poiParams.length - 1] + 1) / 2)); + } + /** * @see IGeometry#contains(Point) */ @@ -211,6 +403,57 @@ public class Polygon extends AbstractPointListBasedGeometry implements } } + /** + * Checks whether the given {@link Polygon} is fully contained within this + * {@link Polygon}. + * + * @param p + * The {@link Polygon} to test for containment + * @return true if the given {@link Polygon} is fully + * contained, false otherwise. + */ + public boolean contains(Polygon p) { + // all segments of the given polygon have to be contained + Line[] otherSegments = p.getOutlineSegments(); + for (int i = 0; i < otherSegments.length; i++) { + if (!contains(otherSegments[i])) { + return false; + } + } + return true; + } + + /** + * Tests if the given {@link Polyline} p is contained in this + * {@link Polygon}. + * + * @param p + * @return true if it is contained, false otherwise + */ + public boolean contains(Polyline p) { + // all segments of the given polygon have to be contained + Line[] otherSegments = p.getCurves(); + for (int i = 0; i < otherSegments.length; i++) { + if (!contains(otherSegments[i])) { + return false; + } + } + return true; + } + + /** + * Checks whether the given {@link Rectangle} is fully contained within this + * {@link Polygon}. + * + * @param r + * the {@link Rectangle} to test for containment + * @return true if the given {@link Rectangle} is fully + * contained, false otherwise. + */ + public boolean contains(Rectangle r) { + return contains(r.toPolygon()); + } + @Override public boolean equals(Object o) { if (this == o) @@ -272,21 +515,7 @@ public class Polygon extends AbstractPointListBasedGeometry implements * @return the area of this {@link Polygon} */ public double getArea() { - if (points.length < 3) { - return 0; - } - - double area = 0; - for (int i = 0; i < points.length - 1; i++) { - area += points[i].x * points[i + 1].y - points[i].y - * points[i + 1].x; - } - - // closing segment - area += points[points.length - 2].x * points[points.length - 1].y - - points[points.length - 2].y * points[points.length - 1].x; - - return Math.abs(area) * 0.5; + return Math.abs(getSignedArea()); } /** @@ -299,6 +528,10 @@ public class Polygon extends AbstractPointListBasedGeometry implements return new Polygon(getPoints()); } + public Polyline getOutline() { + return new Polyline(PointListUtils.toSegmentsArray(points, true)); + } + /** * Returns a sequence of {@link Line}s, representing the segments that are * obtained by linking each two successive point of this {@link Polygon} @@ -312,6 +545,31 @@ public class Polygon extends AbstractPointListBasedGeometry implements } /** + * Computes the signed area of this {@link Polygon}. The sign of the area is + * negative for counter clockwise ordered vertices. It is positive for + * clockwise ordered vertices. + * + * @return the signed area of this {@link Polygon} + */ + public double getSignedArea() { + if (points.length < 3) { + return 0; + } + + double area = 0; + for (int i = 0; i < points.length - 1; i++) { + area += points[i].x * points[i + 1].y - points[i].y + * points[i + 1].x; + } + + // closing segment + area += points[points.length - 2].x * points[points.length - 1].y + - points[points.length - 2].y * points[points.length - 1].x; + + return area * 0.5; + } + + /** * @see IGeometry#getTransformed(AffineTransform) */ @Override @@ -321,6 +579,21 @@ public class Polygon extends AbstractPointListBasedGeometry implements } /** + * Naive, recursive ear-clipping algorithm to triangulate this simple, + * planar {@link Polygon}. + * + * @return triangulation {@link Polygon}s (triangles) + * @throws NonSimplePolygonException + * if this is a non-simple {@link Polygon} + */ + public Polygon[] getTriangulation() throws NonSimplePolygonException { + assureSimplicity(); + ArrayList ears = new ArrayList(points.length - 2); + triangulate(this, ears); + return ears.toArray(new Polygon[] {}); + } + + /** * @see IGeometry#toPath() */ public Path toPath() { @@ -351,110 +624,4 @@ public class Polygon extends AbstractPointListBasedGeometry implements return stringBuffer.toString(); } - public Polyline getOutline() { - return new Polyline(PointListUtils.toSegmentsArray(points, true)); - } - - /** - * Checks whether the given {@link Line} is fully contained within this - * {@link Polygon}. - * - * @param line - * The {@link Line} to test for containment - * @return true if the given {@link Line} is fully contained, - * false otherwise - */ - public boolean contains(Line line) { - // quick rejection test: if the end points are not contained, the line - // may not be contained - if (!contains(line.getP1()) || !contains(line.getP2())) { - return false; - } - - // check for intersections with the segments of this polygon - for (int i = 0; i < points.length; i++) { - Point p1 = points[i]; - Point p2 = i + 1 < points.length ? points[i + 1] : points[0]; - Line segment = new Line(p1, p2); - if (line.intersects(segment)) { - Point intersection = line.getIntersection(segment); - if (intersection != null && !line.getP1().equals(intersection) - && !line.getP2().equals(intersection) - && !segment.getP1().equals(intersection) - && !segment.getP2().equals(intersection)) { - // if we have a single intersection point and this does not - // match one of the end points of the line, the line is not - // contained - return false; - } - } - } - return true; - } - - /** - * Checks whether the given {@link Polygon} is fully contained within this - * {@link Polygon}. - * - * @param p - * The {@link Polygon} to test for containment - * @return true if the given {@link Polygon} is fully - * contained, false otherwise. - */ - public boolean contains(Polygon p) { - // all segments of the given polygon have to be contained - Line[] otherSegments = p.getOutlineSegments(); - for (int i = 0; i < otherSegments.length; i++) { - if (!contains(otherSegments[i])) { - return false; - } - } - return true; - } - - /** - * Checks whether the given {@link Rectangle} is fully contained within this - * {@link Polygon}. - * - * @param r - * the {@link Rectangle} to test for containment - * @return true if the given {@link Rectangle} is fully - * contained, false otherwise. - */ - public boolean contains(Rectangle r) { - return contains(r.toPolygon()); - } - - /** - * Tests if the given {@link Polyline} p is contained in this - * {@link Polygon}. - * - * @param p - * @return true if it is contained, false otherwise - */ - public boolean contains(Polyline p) { - // all segments of the given polygon have to be contained - Line[] otherSegments = p.getCurves(); - for (int i = 0; i < otherSegments.length; i++) { - if (!contains(otherSegments[i])) { - return false; - } - } - return true; - } - - public boolean contains(IGeometry g) { - if (g instanceof Line) { - return contains((Line) g); - } else if (g instanceof Polygon) { - return contains((Polygon) g); - } else if (g instanceof Polyline) { - return contains((Polyline) g); - } else if (g instanceof Rectangle) { - return contains((Rectangle) g); - } - return CurveUtils.contains(this, g); - } - - // TODO: union point, rectangle, polygon, etc. } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Polyline.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Polyline.java index 3347b5b..6701353 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Polyline.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Polyline.java @@ -48,6 +48,16 @@ public class Polyline extends AbstractPointListBasedGeometry } /** + * Constructs a new {@link Polyline} from the given array of {@link Line} + * segments. + * + * @param segmentsArray + */ + public Polyline(Line[] segmentsArray) { + super(PointListUtils.toPointsArray(segmentsArray, false)); + } + + /** * Constructs a new {@link Polyline} from the given sequence of * {@link Point} s. The {@link Polyline} that is created will be * automatically closed, i.e. it will not only contain a segment between @@ -63,16 +73,6 @@ public class Polyline extends AbstractPointListBasedGeometry } /** - * Constructs a new {@link Polyline} from the given array of {@link Line} - * segments. - * - * @param segmentsArray - */ - public Polyline(Line[] segmentsArray) { - super(PointListUtils.toPointsArray(segmentsArray, false)); - } - - /** * Checks whether the point that is represented by its x- and y-coordinates * is contained within this {@link Polyline}. * @@ -106,7 +106,7 @@ public class Polyline extends AbstractPointListBasedGeometry public boolean equals(Object o) { if (this == o) return true; - if (o instanceof Polygon) { + if (o instanceof Polyline) { Polyline p = (Polyline) o; return equals(p.getPoints()); } @@ -129,7 +129,15 @@ public class Polyline extends AbstractPointListBasedGeometry if (points.length != this.points.length) { return false; } - return PointListUtils.equals(this.points, points); + return PointListUtils.equals(this.points, points) + || PointListUtils.equalsReverse(this.points, points); + } + + /** + * @see org.eclipse.gef4.geometry.planar.IGeometry#getCopy() + */ + public Polyline getCopy() { + return new Polyline(getPoints()); } /** @@ -144,6 +152,18 @@ public class Polyline extends AbstractPointListBasedGeometry return PointListUtils.toSegmentsArray(points, false); } + public Point[] getIntersections(ICurve c) { + return CurveUtils.getIntersections(c, this); + } + + public Point getP1() { + return points[0].getCopy(); + } + + public Point getP2() { + return points[points.length - 1].getCopy(); + } + /** * @see IGeometry#getTransformed(AffineTransform) */ @@ -152,18 +172,32 @@ public class Polyline extends AbstractPointListBasedGeometry return new Polyline(t.getTransformed(points)); } - /** - * Tests whether this {@link Polyline} and the given {@link Rectangle} - * touch, i.e. they have at least one {@link Point} in common. - * - * @param rect - * the {@link Rectangle} to test - * @return true if this {@link Polyline} and the - * {@link Rectangle} touch, otherwise false - * @see IGeometry#touches(IGeometry) - */ - public boolean touches(Rectangle rect) { - throw new UnsupportedOperationException("Not yet implemented."); + public double getX1() { + return getP1().x; + } + + public double getX2() { + return getP2().x; + } + + public double getY1() { + return getP1().y; + } + + public double getY2() { + return getP2().y; + } + + public boolean intersects(ICurve c) { + return CurveUtils.intersects(c, this); + } + + public boolean overlaps(ICurve c) { + return CurveUtils.overlaps(c, this); + } + + public Line[] toBezier() { + return PointListUtils.toSegmentsArray(points, false); } /** @@ -208,50 +242,17 @@ public class Polyline extends AbstractPointListBasedGeometry } /** - * @see org.eclipse.gef4.geometry.planar.IGeometry#getCopy() + * Tests whether this {@link Polyline} and the given {@link Rectangle} + * touch, i.e. they have at least one {@link Point} in common. + * + * @param rect + * the {@link Rectangle} to test + * @return true if this {@link Polyline} and the + * {@link Rectangle} touch, otherwise false + * @see IGeometry#touches(IGeometry) */ - public Polyline getCopy() { - return new Polyline(getPoints()); - } - - public double getY2() { - return getP2().y; - } - - public double getY1() { - return getP1().y; - } - - public double getX2() { - return getP2().x; - } - - public double getX1() { - return getP1().x; - } - - public Point getP2() { - return points[points.length - 1].getCopy(); - } - - public Point getP1() { - return points[0].getCopy(); - } - - public Line[] toBezier() { - return PointListUtils.toSegmentsArray(points, false); - } - - public Point[] getIntersections(ICurve c) { - return CurveUtils.getIntersections(c, this); - } - - public boolean intersects(ICurve c) { - return CurveUtils.intersects(c, this); - } - - public boolean overlaps(ICurve c) { - return CurveUtils.overlaps(c, this); + public boolean touches(Rectangle rect) { + throw new UnsupportedOperationException("Not yet implemented."); } } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/QuadraticCurve.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/QuadraticCurve.java index dbc1d1d..fb52aea 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/QuadraticCurve.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/QuadraticCurve.java @@ -13,7 +13,6 @@ package org.eclipse.gef4.geometry.planar; import org.eclipse.gef4.geometry.Point; -import org.eclipse.gef4.geometry.utils.PolynomCalculationUtils; /** * Represents the geometric shape of a quadratic Bézier curve. @@ -88,19 +87,6 @@ public class QuadraticCurve extends BezierCurve { this(p1.x, p1.y, pCtrl.x, pCtrl.y, p2.x, p2.y); } - /** - * Clips this {@link QuadraticCurve} at parameter values t1 and t2 so that - * the resulting {@link QuadraticCurve} is the section of the original - * {@link QuadraticCurve} for the parameter interval [t1, t2]. - * - * @param t1 - * @param t2 - * @return the {@link QuadraticCurve} on the interval [t1, t2] - */ - public QuadraticCurve clip(double t1, double t2) { - return super.getClipped(t1, t2).toQuadratic(); - } - @Override public boolean equals(Object other) { QuadraticCurve o = (QuadraticCurve) other; @@ -112,68 +98,45 @@ public class QuadraticCurve extends BezierCurve { } /** + * Erroneous getBounds() implementation... use the generic one instead. + * + * TODO: find out why the mathematical solution is erroneous in some cases. + * * Returns the bounds of this QuadraticCurve. The bounds are calculated by * examining the extreme points of the x(t) and y(t) function * representations of this QuadraticCurve. * * @return the bounds {@link Rectangle} */ - @Override - public Rectangle getBounds() { - // extremes of the x(t) and y(t) functions: - double[] xts; - try { - xts = PolynomCalculationUtils.getLinearRoots(2 * (getX1() - 2 - * getCtrlX() + getX2()), 2 * (getCtrlX() - getX1())); - } catch (ArithmeticException x) { - return new Rectangle(getP1(), getP2()); - } - - double xmin = getX1(), xmax = getX1(); - if (getX2() < xmin) { - xmin = getX2(); - } else { - xmax = getX2(); - } - - for (double t : xts) { - if (t >= 0 && t <= 1) { - double x = get(t).x; - if (x < xmin) { - xmin = x; - } else if (x > xmax) { - xmax = x; - } - } - } - - double[] yts; - try { - yts = PolynomCalculationUtils.getLinearRoots(2 * (getY1() - 2 - * getCtrlY() + getY2()), 2 * (getCtrlY() - getY1())); - } catch (ArithmeticException x) { - return new Rectangle(getP1(), getP2()); - } - - double ymin = getY1(), ymax = getY1(); - if (getY2() < ymin) { - ymin = getY2(); - } else { - ymax = getY2(); - } - - for (double t : yts) { - if (t >= 0 && t <= 1) { - double y = get(t).y; - if (y < ymin) { - ymin = y; - } else if (y > ymax) { - ymax = y; - } - } - } + /* + * public Rectangle getBounds() { // extremes of the x(t) and y(t) + * functions: double[] xts; try { xts = + * PolynomCalculationUtils.getLinearRoots(2 * (getX1() - 2 getCtrlX() + + * getX2()), 2 * (getCtrlX() - getX1())); } catch (ArithmeticException x) { + * return new Rectangle(getP1(), getP2()); } + * + * double xmin = getX1(), xmax = getX1(); if (getX2() < xmin) { xmin = + * getX2(); } else { xmax = getX2(); } + * + * for (double t : xts) { if (t >= 0 && t <= 1) { double x = get(t).x; if (x + * < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } } } + * + * double[] yts; try { yts = PolynomCalculationUtils.getLinearRoots(2 * + * (getY1() - 2 getCtrlY() + getY2()), 2 * (getCtrlY() - getY1())); } catch + * (ArithmeticException x) { return new Rectangle(getP1(), getP2()); } + * + * double ymin = getY1(), ymax = getY1(); if (getY2() < ymin) { ymin = + * getY2(); } else { ymax = getY2(); } + * + * for (double t : yts) { if (t >= 0 && t <= 1) { double y = get(t).y; if (y + * < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } } } + * + * return new Rectangle(new Point(xmin, ymin), new Point(xmax, ymax)); } + */ - return new Rectangle(new Point(xmin, ymin), new Point(xmax, ymax)); + @Override + public QuadraticCurve getClipped(double t1, double t2) { + return super.getClipped(t1, t2).toQuadratic(); } private Polygon getControlPolygon() { diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Rectangle.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Rectangle.java index efa9a75..447eaf8 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Rectangle.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Rectangle.java @@ -39,8 +39,8 @@ import org.eclipse.gef4.geometry.utils.PrecisionUtils; * @author ahunter * @author anyssen */ -public final class Rectangle extends AbstractRectangleBasedGeometry - implements IShape { +public final class Rectangle extends + AbstractRectangleBasedGeometry implements IShape { private static final long serialVersionUID = 1L; @@ -149,6 +149,37 @@ public final class Rectangle extends AbstractRectangleBasedGeometry } /** + * Returns true in case the rectangle specified by (x, y, width, height) is + * contained within this {@link Rectangle}. + * + * @param x + * The x coordinate of the rectangle to be tested for containment + * @param y + * The y coordinate of the rectangle to be tested for containment + * @param width + * The width of the rectangle to be tested for containment + * @param height + * The height of the rectangle to be tested for containment + * @return true if the rectangle characterized by (x,y, width, + * height) is (imprecisely) fully contained within this + * {@link Rectangle}, false otherwise + */ + public boolean contains(double x, double y, double width, double height) { + return PrecisionUtils.smallerEqual(this.x, x) + && PrecisionUtils.smallerEqual(this.y, y) + && PrecisionUtils.greaterEqual(this.x + this.width, x + width) + && PrecisionUtils + .greaterEqual(this.y + this.height, y + height); + } + + public boolean contains(IGeometry g) { + if (g instanceof Rectangle) { + return contains((Rectangle) g); + } + return CurveUtils.contains(this, g); + } + + /** * Returns whether the given point is within the boundaries of this * Rectangle. The boundaries are inclusive of the top and left edges, but * exclusive of the bottom and right edges. @@ -163,6 +194,21 @@ public final class Rectangle extends AbstractRectangleBasedGeometry } /** + * Tests whether this {@link Rectangle} fully contains the given other + * {@link Rectangle}. + * + * @param r + * the other {@link Rectangle} to test for being contained by + * this {@link Rectangle} + * @return true if this {@link Rectangle} contains the other + * {@link Rectangle}, otherwise false + * @see IShape#contains(IGeometry) + */ + public boolean contains(Rectangle r) { + return contains(r.x, r.y, r.width, r.height); + } + + /** * Returns true if this Rectangle's x, y, width, and height * values are identical to the provided ones. * @@ -259,33 +305,6 @@ public final class Rectangle extends AbstractRectangleBasedGeometry } /** - * Returns an array of {@link Point}s representing the top-left, top-right, - * bottom-right, and bottom-left border points of this {@link Rectangle}. - * - * @return An array containing the border points of this {@link Rectangle} - */ - public Point[] getPoints() { - return new Point[] { getTopLeft(), getTopRight(), getBottomRight(), - getBottomLeft() }; - } - - /** - * Returns an array of {@link Line}s representing the top, right, bottom, - * and left borders of this {@link Rectangle}. - * - * @return An array containing {@link Line} representations of this - * {@link Rectangle}'s borders. - */ - public Line[] getOutlineSegments() { - Line[] segments = new Line[4]; - segments[0] = new Line(x, y, x + width, y); - segments[1] = new Line(x + width, y, x + width, y + height); - segments[2] = new Line(x + width, y + height, x, y + height); - segments[3] = new Line(x, y + height, x, y); - return segments; - } - - /** * Returns a new Point representing the middle point of the bottom side of * this Rectangle. * @@ -383,6 +402,38 @@ public final class Rectangle extends AbstractRectangleBasedGeometry return new Point(x, y + height / 2); } + public Polyline getOutline() { + return new Polyline(x, y, x + width, y, x + width, y + height, x, y + + height, x, y); + } + + /** + * Returns an array of {@link Line}s representing the top, right, bottom, + * and left borders of this {@link Rectangle}. + * + * @return An array containing {@link Line} representations of this + * {@link Rectangle}'s borders. + */ + public Line[] getOutlineSegments() { + Line[] segments = new Line[4]; + segments[0] = new Line(x, y, x + width, y); + segments[1] = new Line(x + width, y, x + width, y + height); + segments[2] = new Line(x + width, y + height, x, y + height); + segments[3] = new Line(x, y + height, x, y); + return segments; + } + + /** + * Returns an array of {@link Point}s representing the top-left, top-right, + * bottom-right, and bottom-left border points of this {@link Rectangle}. + * + * @return An array containing the border points of this {@link Rectangle} + */ + public Point[] getPoints() { + return new Point[] { getTopLeft(), getTopRight(), getBottomRight(), + getBottomLeft() }; + } + /** * Returns a new Point which represents the middle point of the right hand * side of this Rectangle. @@ -394,21 +445,42 @@ public final class Rectangle extends AbstractRectangleBasedGeometry } /** - * Rotates this {@link Rectangle} clock-wise by the given {@link Angle} - * around the center ({@link #getCentroid()}) of this {@link Rectangle}. + * Rotates this {@link Rectangle} counter-clock-wise by the given + * {@link Angle} around the center {@link Point} of this {@link Rectangle} + * (see {@link #getCentroid()}). * - * @see #getRotatedCW(Angle, Point) + * @see #getRotatedCCW(Angle, Point) * @param alpha - * the rotation {@link Angle} * @return the resulting {@link Polygon} */ - public Polygon getRotatedCW(Angle alpha) { - return getRotatedCW(alpha, getCentroid()); + public Polygon getRotatedCCW(Angle alpha) { + Point centroid = getCentroid(); + return toPolygon().rotateCCW(alpha, centroid.x, centroid.y); } /** - * Rotates this {@link Rectangle} clock-wise by the given {@link Angle} - * alpha around the given {@link Point}. + * Rotates this {@link Rectangle} counter-clock-wise by the given + * {@link Angle} around the given {@link Point}. + * + * If the rotation {@link Angle} is not an integer multiple of 90 degrees, + * the resulting figure cannot be expressed as a {@link Rectangle} object. + * That's why this method returns a {@link Polygon} instead. + * + * @param alpha + * the rotation angle + * @param cx + * x-component of the center point for the rotation + * @param cy + * y-component of the center point for the rotation + * @return the resulting {@link Polygon} + */ + public Polygon getRotatedCCW(Angle alpha, double cx, double cy) { + return toPolygon().rotateCCW(alpha, cx, cy); + } + + /** + * Rotates this {@link Rectangle} counter-clock-wise by the given + * {@link Angle} around the given {@link Point}. * * If the rotation {@link Angle} is not an integer multiple of 90 degrees, * the resulting figure cannot be expressed as a {@link Rectangle} object. @@ -420,26 +492,47 @@ public final class Rectangle extends AbstractRectangleBasedGeometry * the center point for the rotation * @return the resulting {@link Polygon} */ - public Polygon getRotatedCW(Angle alpha, Point center) { - return toPolygon().rotateCW(alpha, center); + public Polygon getRotatedCCW(Angle alpha, Point center) { + return toPolygon().rotateCCW(alpha, center.x, center.y); } /** - * Rotates this {@link Rectangle} counter-clock-wise by the given - * {@link Angle} around the center {@link Point} of this {@link Rectangle} - * (see {@link #getCentroid()}). + * Rotates this {@link Rectangle} clock-wise by the given {@link Angle} + * around the center ({@link #getCentroid()}) of this {@link Rectangle}. * - * @see #getRotatedCCW(Angle, Point) + * @see #getRotatedCW(Angle, Point) * @param alpha + * the rotation {@link Angle} * @return the resulting {@link Polygon} */ - public Polygon getRotatedCCW(Angle alpha) { - return getRotatedCCW(alpha, getCentroid()); + public Polygon getRotatedCW(Angle alpha) { + Point centroid = getCentroid(); + return toPolygon().rotateCW(alpha, centroid.x, centroid.y); } /** - * Rotates this {@link Rectangle} counter-clock-wise by the given - * {@link Angle} around the given {@link Point}. + * Rotates this {@link Rectangle} clock-wise by the given {@link Angle} + * alpha around the given {@link Point} (cx, cy). + * + * If the rotation {@link Angle} is not an integer multiple of 90 degrees, + * the resulting figure cannot be expressed as a {@link Rectangle} object. + * That's why this method returns a {@link Polygon} instead. + * + * @param alpha + * the rotation angle + * @param cx + * x-component of the center point for the rotation + * @param cy + * y-component of the center point for the rotation + * @return the resulting {@link Polygon} + */ + public Polygon getRotatedCW(Angle alpha, double cx, double cy) { + return toPolygon().rotateCW(alpha, cx, cy); + } + + /** + * Rotates this {@link Rectangle} clock-wise by the given {@link Angle} + * alpha around the given {@link Point}. * * If the rotation {@link Angle} is not an integer multiple of 90 degrees, * the resulting figure cannot be expressed as a {@link Rectangle} object. @@ -451,8 +544,8 @@ public final class Rectangle extends AbstractRectangleBasedGeometry * the center point for the rotation * @return the resulting {@link Polygon} */ - public Polygon getRotatedCCW(Angle alpha, Point center) { - return toPolygon().rotateCCW(alpha, center); + public Polygon getRotatedCW(Angle alpha, Point center) { + return toPolygon().rotateCW(alpha, center.x, center.y); } /** @@ -592,58 +685,6 @@ public final class Rectangle extends AbstractRectangleBasedGeometry } /** - * Tests whether this {@link Rectangle} and the given {@link Line} touch, - * i.e. whether they have at least one point in common. - * - * @param l - * The {@link Line} to test. - * @return true if this {@link Rectangle} and the given - * {@link Line} share at least one common point, false - * otherwise. - */ - public boolean touches(Line l) { - if (contains(l.getP1()) || contains(l.getP2())) { - return true; - } - - for (Line segment : getOutlineSegments()) { - if (segment.intersects(l)) { - return true; - } - } - return false; - } - - /** - * Tests whether this {@link Rectangle} and the given other - * {@link Rectangle} touch, i.e. whether they have at least one point in - * common. - * - * @param r - * The {@link Rectangle} to test - * @return true if this {@link Rectangle} and the given - * {@link Rectangle} share at least one common point, - * false otherwise. - * @see IGeometry#touches(IGeometry) - */ - public boolean touches(Rectangle r) { - return PrecisionUtils.smallerEqual(r.x, x + width) - && PrecisionUtils.smallerEqual(r.y, y + height) - && PrecisionUtils.greaterEqual(r.x + r.width, x) - && PrecisionUtils.greaterEqual(r.y + r.height, y); - } - - @Override - public boolean touches(IGeometry g) { - if (g instanceof Line) { - return touches((Line) g); - } else if (g instanceof Rectangle) { - return touches((Rectangle) g); - } - return super.touches(g); - } - - /** * Returns true if this Rectangle's width or height is less * than or equal to 0. * @@ -744,6 +785,57 @@ public final class Rectangle extends AbstractRectangleBasedGeometry (int) Math.ceil(height + y - Math.floor(y))); } + public boolean touches(IGeometry g) { + if (g instanceof Line) { + return touches((Line) g); + } else if (g instanceof Rectangle) { + return touches((Rectangle) g); + } + return super.touches(g); + } + + /** + * Tests whether this {@link Rectangle} and the given {@link Line} touch, + * i.e. whether they have at least one point in common. + * + * @param l + * The {@link Line} to test. + * @return true if this {@link Rectangle} and the given + * {@link Line} share at least one common point, false + * otherwise. + */ + public boolean touches(Line l) { + if (contains(l.getP1()) || contains(l.getP2())) { + return true; + } + + for (Line segment : getOutlineSegments()) { + if (segment.intersects(l)) { + return true; + } + } + return false; + } + + /** + * Tests whether this {@link Rectangle} and the given other + * {@link Rectangle} touch, i.e. whether they have at least one point in + * common. + * + * @param r + * The {@link Rectangle} to test + * @return true if this {@link Rectangle} and the given + * {@link Rectangle} share at least one common point, + * false otherwise. + * @see IGeometry#touches(IGeometry) + */ + public boolean touches(Rectangle r) { + return PrecisionUtils.smallerEqual(r.x, x + width) + && PrecisionUtils.smallerEqual(r.y, y + height) + && PrecisionUtils.greaterEqual(r.x + r.width, x) + && PrecisionUtils.greaterEqual(r.y + r.height, y); + } + /** * Switches the x and y values, as well as the width and height of this * Rectangle. Useful for orientation changes. @@ -836,55 +928,4 @@ public final class Rectangle extends AbstractRectangleBasedGeometry return union(r.x, r.y, r.width, r.height); } - public Polyline getOutline() { - return new Polyline(x, y, x + width, y, x + width, y + height, x, y - + height, x, y); - } - - public boolean contains(IGeometry g) { - if (g instanceof Rectangle) { - return contains((Rectangle) g); - } - return CurveUtils.contains(this, g); - } - - /** - * Returns true in case the rectangle specified by (x, y, width, height) is - * contained within this {@link Rectangle}. - * - * @param x - * The x coordinate of the rectangle to be tested for containment - * @param y - * The y coordinate of the rectangle to be tested for containment - * @param width - * The width of the rectangle to be tested for containment - * @param height - * The height of the rectangle to be tested for containment - * @return true if the rectangle characterized by (x,y, width, - * height) is (imprecisely) fully contained within this - * {@link Rectangle}, false otherwise - */ - public boolean contains(double x, double y, double width, double height) { - return PrecisionUtils.smallerEqual(this.x, x) - && PrecisionUtils.smallerEqual(this.y, y) - && PrecisionUtils.greaterEqual(this.x + this.width, x + width) - && PrecisionUtils - .greaterEqual(this.y + this.height, y + height); - } - - /** - * Tests whether this {@link Rectangle} fully contains the given other - * {@link Rectangle}. - * - * @param r - * the other {@link Rectangle} to test for being contained by - * this {@link Rectangle} - * @return true if this {@link Rectangle} contains the other - * {@link Rectangle}, otherwise false - * @see IShape#contains(IGeometry) - */ - public boolean contains(Rectangle r) { - return contains(r.x, r.y, r.width, r.height); - } - } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Region.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Region.java index b821610..ea0806b 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Region.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Region.java @@ -7,46 +7,377 @@ * * Contributors: * Alexander Nyßen (itemis AG) - initial API and implementation + * Matthias Wienand (itemis AG) - contribution for Bugzilla #355997 * *******************************************************************************/ package org.eclipse.gef4.geometry.planar; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Stack; + +import org.eclipse.gef4.geometry.Angle; import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.transform.IRotatable; +import org.eclipse.gef4.geometry.transform.IScalable; +import org.eclipse.gef4.geometry.transform.ITranslatable; +import org.eclipse.gef4.geometry.utils.CurveUtils; /** - * a combination of rectangles... + * A combination of {@link Rectangle}s. The {@link Rectangle}s that build up a + * {@link Region} do not have to be touching. The area covered by the + * {@link Region} is exactly the area that all of its corresponding + * {@link Rectangle}s are covering. + * + * A {@link Region} differentiates between the internal {@link Rectangle}s and + * the external {@link Rectangle}s. The external {@link Rectangle}s are those + * that you feed it, in order to construct the {@link Region}. The internal + * {@link Rectangle}s are those used for computations of the {@link Region}. + * They are defined to not share any area, so that only their borders can be + * overlapping. * * @author nyssen * */ -public class Region extends AbstractGeometry implements IPolyShape { +public class Region extends AbstractPolyShape implements ITranslatable, + IScalable, IRotatable { - public Rectangle[] getShapes() { - throw new UnsupportedOperationException("Not yet implemented."); + private static final long serialVersionUID = 1L; + + /** + * Cuts the given {@link Rectangle}s along the given parallel to the x-axis. + * + * @param y + * the distance of the cut-line to the x-axis + * @param parts + * the {@link Rectangle}s to cut along that line + */ + private static void cutH(double y, ArrayList parts) { + for (Rectangle r : new ArrayList(parts)) { + if (r.y < y && y < r.y + r.height) { + parts.remove(r); + parts.add(new Rectangle(r.x, r.y, r.width, y - r.y)); + parts.add(new Rectangle(r.x, y, r.width, r.y + r.height - y)); + } + } + } + + /** + * Cuts the given {@link Rectangle}s along the given parallel to the y-axis. + * + * @param x + * the distance of the cut-line to the y-axis + * @param parts + * the {@link Rectangle}s to cut along that line + */ + private static void cutV(double x, ArrayList parts) { + for (Rectangle r : new ArrayList(parts)) { + if (r.x < x && x < r.x + r.width) { + parts.remove(r); + parts.add(new Rectangle(r.x, r.y, x - r.x, r.height)); + parts.add(new Rectangle(x, r.y, r.x + r.width - x, r.height)); + } + } + } + + private ArrayList rects; + + /** + * Constructs a new {@link Region} not covering any area. + */ + public Region() { + rects = new ArrayList(); + } + + /** + * Constructs a new {@link Region} from the given list of {@link Rectangle} + * s. + * + * The given {@link Rectangle}s are {@link #add(Rectangle)}ed to the + * {@link Region} one after the other. + * + * @param rectangles + */ + public Region(Rectangle... rectangles) { + this(); + rects.add(rectangles[0].getCopy()); + + for (int i = 1; i < rectangles.length; i++) { + add(rectangles[i].getCopy()); + } + } + + /** + * Constructs a new {@link Region} from the given other {@link Region}. In + * other words, it copies the given other {@link Region}. + * + * @param other + */ + public Region(Region other) { + rects = new ArrayList(other.rects.size()); + + for (Rectangle or : other.rects) { + rects.add(or.getCopy()); + } + } + + /** + * Adds the given {@link Rectangle} to this {@link Region}. + * + * To assure the required conditions for internal {@link Rectangle}s, the + * given {@link Rectangle} is cut into several sub-{@link Rectangle}s so + * that no internal {@link Rectangle}s share any area. + * + * @param rectangle + * the {@link Rectangle} to add to this {@link Region} + * @return this for convenience + */ + public Region add(Rectangle rectangle) { + ArrayList toAdd = new ArrayList(1); + + toAdd.add(rectangle.getCopy()); + + for (Rectangle retain : rects) { + for (Rectangle addend : new ArrayList(toAdd)) { + ArrayList parts = new ArrayList(8); + parts.add(addend); + + if (addend.x <= retain.x && retain.x <= addend.x + addend.width + || retain.x <= addend.x + && addend.x <= retain.x + retain.width) { + cutH(retain.y, parts); + cutH(retain.y + retain.height, parts); + } + + if (addend.y <= retain.y + && retain.y <= addend.y + addend.height + || retain.y <= addend.y + && addend.y <= retain.y + retain.height) { + cutV(retain.x, parts); + cutV(retain.x + retain.width, parts); + } + + // filter inner parts: + for (Iterator p = parts.iterator(); p.hasNext();) { + if (retain.contains(p.next())) { + p.remove(); + } + } + + toAdd.remove(addend); + toAdd.addAll(parts); + } + } + + rects.addAll(toAdd); + + return this; } - public boolean contains(Point p) { - throw new UnsupportedOperationException("Not yet implemented."); + public boolean contains(IGeometry g) { + return CurveUtils.contains(this, g); } - public boolean contains(Rectangle r) { - throw new UnsupportedOperationException("Not yet implemented."); + /** + * Collects all outline segments of the internal {@link Rectangle}s. + * + * @return all the outline segments of the internal {@link Rectangle}s + */ + protected Line[] getAllEdges() { + Stack edges = new Stack(); + + for (Rectangle r : rects) { + for (Line e : r.getOutlineSegments()) { + edges.push(e); + } + } + return edges.toArray(new Line[] {}); } public Rectangle getBounds() { - throw new UnsupportedOperationException("Not yet implemented."); + if (rects.size() == 0) + return null; + + Rectangle bounds = rects.get(0).getBounds(); + for (int i = 1; i < rects.size(); i++) + bounds.union(rects.get(i).getBounds()); + + return bounds; + } + + public Region getCopy() { + return new Region(this); } - public boolean touches(Rectangle r) { - throw new UnsupportedOperationException("Not yet implemented."); + /** + * Computes the {@link Point}s of intersection of this {@link Region} with + * the given {@link ICurve}. + * + * @param c + * @return the intersection {@link Point}s + */ + public Point[] getOutlineIntersections(ICurve c) { + Set intersections = new HashSet(0); + + for (Line seg : getOutlineSegments()) { + intersections.addAll(Arrays.asList(seg.getIntersections(c))); + } + + return intersections.toArray(new Point[] {}); + } + + public Rectangle[] getShapes() { + return rects.toArray(new Rectangle[] {}); } public Path toPath() { - throw new UnsupportedOperationException("Not yet implemented."); + return getOutline().toPath(); + } + + /** + * Constructs a new {@link Ring} that covers the same area as this + * {@link Region}. + * + * @return a new {@link Ring} that covers the same area as this + * {@link Region} + */ + public Ring toRing() { + Polygon[] polys = new Polygon[rects.size()]; + Iterator i = rects.iterator(); + for (int j = 0; j < rects.size(); j++) { + polys[j] = i.next().toPolygon(); + } + return new Ring(polys); + } + + /** + *

+ * Constructs a new {@link org.eclipse.swt.graphics.Region} that covers the + * same area as this {@link Region}. This is to ease the use of a + * {@link Region} for clipping: + *

+ * + *

+ * gc.setClipping(region.toSWTRegion()); + *

+ * + * @return a new {@link org.eclipse.swt.graphics.Region} that covers the + * same area as this {@link Region} + */ + public org.eclipse.swt.graphics.Region toSWTRegion() { + org.eclipse.swt.graphics.Region swtRegion = new org.eclipse.swt.graphics.Region(); + + for (Rectangle r : rects) { + swtRegion.add(r.toSWTRectangle()); + } + + return swtRegion; + } + + public Ring getRotatedCCW(Angle angle) { + Point centroid = getBounds().getCentroid(); + return getRotatedCCW(angle, centroid.x, centroid.y); + } + + public Ring getRotatedCCW(Angle angle, double cx, double cy) { + Polygon[] polys = new Polygon[rects.size()]; + for (int i = 0; i < polys.length; i++) + polys[i] = rects.get(i).getRotatedCCW(angle, cx, cy); + return new Ring(polys); + } + + public Ring getRotatedCCW(Angle angle, Point center) { + return getRotatedCCW(angle, center.x, center.y); + } + + public Ring getRotatedCW(Angle angle) { + Point centroid = getBounds().getCentroid(); + return getRotatedCW(angle, centroid.x, centroid.y); + } + + public Ring getRotatedCW(Angle angle, double cx, double cy) { + Polygon[] polys = new Polygon[rects.size()]; + for (int i = 0; i < polys.length; i++) + polys[i] = rects.get(i).getRotatedCW(angle, cx, cy); + return new Ring(polys); + } + + public Ring getRotatedCW(Angle angle, Point center) { + return getRotatedCW(angle, center.x, center.y); + } + + public Region scale(double factor) { + return scale(factor, factor); + } + + public Region scale(double factor, double cx, double cy) { + return scale(factor, factor, cx, cy); + } + + public Region scale(double factor, Point center) { + return scale(factor, factor, center.x, center.y); + } + + public Region scale(double fx, double fy) { + Point centroid = getBounds().getCentroid(); + return scale(fx, fy, centroid.x, centroid.y); + } + + public Region scale(double fx, double fy, double cx, double cy) { + for (Rectangle r : rects) { + r.scale(fx, fy, cx, cy); + } + return this; + } + + public Region scale(double fx, double fy, Point center) { + return scale(fx, fy, center.x, center.y); + } + + public Region getScaled(double factor) { + return getCopy().scale(factor); + } + + public Region getScaled(double factor, double cx, double cy) { + return getCopy().scale(factor, cx, cy); + } + + public Region getScaled(double factor, Point center) { + return getCopy().scale(factor, center); + } + + public Region getScaled(double fx, double fy) { + return getCopy().scale(fx, fy); + } + + public Region getScaled(double fx, double fy, double cx, double cy) { + return getCopy().scale(fx, fy, cx, cy); + } + + public Region getScaled(double fx, double fy, Point center) { + return getCopy().scale(fx, fy, center); + } + + public Region translate(double dx, double dy) { + for (Rectangle r : rects) { + r.translate(dx, dy); + } + return this; + } + + public Region translate(Point d) { + return translate(d.x, d.y); + } + + public Region getTranslated(double dx, double dy) { + return getCopy().translate(dx, dy); } - public IGeometry getCopy() { - throw new UnsupportedOperationException("Not yet implemented."); + public Region getTranslated(Point d) { + return getCopy().translate(d.x, d.y); } } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Ring.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Ring.java index 403c453..806122b 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Ring.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/Ring.java @@ -7,11 +7,23 @@ * * Contributors: * Alexander Nyßen (itemis AG) - initial API and implementation + * Matthias Wienand (itemis AG) - contribution for Bugzilla #355997 * *******************************************************************************/ package org.eclipse.gef4.geometry.planar; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Stack; + +import org.eclipse.gef4.geometry.Angle; import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.euclidean.Straight; +import org.eclipse.gef4.geometry.euclidean.Vector; +import org.eclipse.gef4.geometry.transform.IRotatable; +import org.eclipse.gef4.geometry.transform.IScalable; +import org.eclipse.gef4.geometry.transform.ITranslatable; +import org.eclipse.gef4.geometry.utils.CurveUtils; /** * @@ -20,34 +32,502 @@ import org.eclipse.gef4.geometry.Point; * @author anyssen * */ -public class Ring extends AbstractGeometry implements IPolyShape { +public class Ring extends AbstractPolyShape implements ITranslatable, + IScalable, IRotatable { - public Polygon[] getShapes() { - throw new UnsupportedOperationException("Not yet implemented."); + private static final long serialVersionUID = 1L; + + /** + * Triangulates the given triangle ({@link Polygon}) at the given + * {@link Line}. The triangulation is done using the simpler + * {@link #triangulate(Polygon, Point, Point)} method. The real and + * imaginary {@link Point}s of intersection of the {@link Line} and the + * {@link Polygon} are used as the split {@link Point}s. + * + * The triangulation is only done, if the {@link Line} intersects the + * {@link Polygon}, i.e. at least one {@link Point} of the {@link Line} lies + * inside the {@link Polygon} but not on its outline. + * + * @param p + * the triangle ({@link Polygon}) to triangulate + * @param l + * the line that determines the split {@link Point}s + * @return at least one and up to three {@link Polygon}s which are the + * resulting triangles + */ + private static Polygon[] triangulate(Polygon p, Line l) { + if (p == null) { + throw new IllegalArgumentException( + "The given Polygon parameter may not be null."); + } + if (l == null) { + throw new IllegalArgumentException( + "The given Line parameter may not be null."); + } + + boolean intersecting = l.getIntersections(p.getOutline()).length == 2; + + if (!intersecting) { + // test if at least one point of the line is really inside the + // polygon + for (Point lp : l.getPoints()) { + if (p.contains(lp) && !p.getOutline().contains(lp)) { + intersecting = true; + break; + } + } + + if (!intersecting) { + return new Polygon[] { p.getCopy() }; + } + } + + // calculate real and imaginary intersection points + Straight s = new Straight(l); + Point inters[] = new Point[2]; + int i = 0; + + for (Line e : p.getOutlineSegments()) { + Vector vi = s.getIntersection(new Straight(e)); + if (vi != null) { + Point poi = vi.toPoint(); + if (e.contains(poi)) + if (i > 0 && inters[0].equals(poi)) + continue; + else + inters[i++] = poi; + } + if (i > 1) + break; + } + + if (inters[0] == null || inters[1] == null) { + throw new IllegalStateException( + "The determined points of intersection do not lie on the polygon."); + } + + return triangulate(p, inters[0], inters[1]); + } + + /** + *

+ * Splits a triangle at the line through points p1 and p2, which are + * required to lie on the outline of the triangle. + *

+ * + *

+ * If the points p1 and p2 lie on the same edge on the triangle, a copy of + * the given {@link Polygon} is returned. + *

+ * + *

+ * If one of the points lies on an edge, and the other point lies on a + * vertex of the triangle, two {@link Polygon}s are returned. They represent + * the areas left and right to the line through p1 and p2. + *

+ * + *

+ * If both points lie on different edges, three {@link Polygon}s are + * returned. One of them represents the triangle which lies on one side of + * the line through p1 and p2. The other two triangles are the triangulation + * of the tetragon on the other side of the line through p1 and p2. + *

+ * + * @param p + * @param p1 + * @param p2 + * @return + */ + private static Polygon[] triangulate(Polygon p, Point p1, Point p2) { + Point[] v = p == null ? new Point[] {} : p.getPoints(); + if (v.length != 3) { + throw new IllegalArgumentException( + "Only triangles are allowed as the Polygon parameter."); + } + if (p1 == null) { + throw new IllegalArgumentException( + "The given p1 Point parameter may not be null."); + } + if (p2 == null) { + throw new IllegalArgumentException( + "The given p2 Point parameter may not be null."); + } + + Line[] e = new Line[] { new Line(v[0], v[1]), new Line(v[1], v[2]), + new Line(v[2], v[0]) }; + + // determine the edges on which the points lie + boolean p1_on_e0 = e[0].contains(p1); + boolean p1_on_e1 = e[1].contains(p1); + boolean p1_on_e2 = e[2].contains(p1); + boolean p2_on_e0 = e[0].contains(p2); + boolean p2_on_e1 = e[1].contains(p2); + boolean p2_on_e2 = e[2].contains(p2); + + // if both points lie on the same edge, we have nothing to do + if (p1_on_e0 && p2_on_e0 || p1_on_e1 && p2_on_e1 || p1_on_e2 + && p2_on_e2) { + return new Polygon[] { p.getCopy() }; + } + + // check if both points are on the triangle + else if (!(p1_on_e0 || p1_on_e1 || p1_on_e2) + || !(p2_on_e0 || p2_on_e1 || p2_on_e2)) { + throw new IllegalArgumentException( + "The Point objects have to lie on the outline of the Polygon object."); + } + + // determine if one of the points lies on a vertex + else if (p1.equals(v[0])) { + return new Polygon[] { new Polygon(v[0], v[1], p2), + new Polygon(v[0], v[2], p2) }; + } else if (p1.equals(v[1])) { + return new Polygon[] { new Polygon(v[0], v[1], p2), + new Polygon(v[1], v[2], p2) }; + } else if (p1.equals(v[2])) { + return new Polygon[] { new Polygon(v[0], v[2], p2), + new Polygon(v[1], v[2], p2) }; + } else if (p2.equals(v[0])) { + return new Polygon[] { new Polygon(v[0], v[1], p1), + new Polygon(v[0], v[2], p1) }; + } else if (p2.equals(v[1])) { + return new Polygon[] { new Polygon(v[0], v[1], p1), + new Polygon(v[1], v[2], p1) }; + } else if (p2.equals(v[2])) { + return new Polygon[] { new Polygon(v[0], v[2], p1), + new Polygon(v[1], v[2], p1) }; + } + + // both points on different edges, determine isolated vertex + else if (p1_on_e0 && p2_on_e2 || p1_on_e2 && p2_on_e0) { + // v0 isolated + return new Polygon[] { new Polygon(v[0], p1, p2), + new Polygon(p1, p2, v[1]), + new Polygon(p1_on_e0 ? p2 : p1, v[1], v[2]) }; + } else if (p1_on_e0 && p2_on_e1 || p1_on_e1 && p2_on_e0) { + // v1 isolated + return new Polygon[] { new Polygon(v[1], p1, p2), + new Polygon(p1, p2, v[0]), + new Polygon(p1_on_e0 ? p2 : p1, v[0], v[2]) }; + } else if (p1_on_e1 && p2_on_e2 || p1_on_e2 && p2_on_e1) { + // v2 isolated + return new Polygon[] { new Polygon(v[2], p1, p2), + new Polygon(p1, p2, v[1]), + new Polygon(p1_on_e1 ? p2 : p1, v[1], v[0]) }; + } else { + throw new IllegalStateException( + "Unreachable, because for two points on a triangle, they have to be located either (edge, edge), (vertex, edge), or (edge, vertex)."); + } } - public boolean contains(Point p) { - throw new UnsupportedOperationException("Not yet implemented."); + private ArrayList triangles; + + /** + * Constructs a new empty {@link Ring}. + */ + public Ring() { + triangles = new ArrayList(); } - public boolean contains(Rectangle r) { - throw new UnsupportedOperationException("Not yet implemented."); + /** + * Constructs a new {@link Ring} from the given {@link Polygon}s. + * + * @param polygons + */ + public Ring(Polygon... polygons) { + this(); + for (Polygon p : polygons) { + add(p); + } + } + + /** + * Constructs a new {@link Ring} of the given other {@link Ring}. The + * internal {@link IShape}s of the other {@link Ring} are copied to prevent + * actions at a distance. + * + * @param other + */ + public Ring(Ring other) { + this(); + for (Polygon p : other.triangles) { + add(p); + } + } + + /** + * Adds the given {@link Polygon} to this {@link Ring}. + * + * @param p + * @return this for convenience + */ + public Ring add(Polygon p) { + Stack toAdd = new Stack(); + for (Polygon triangleToAdd : p.getTriangulation()) + toAdd.push(triangleToAdd); + + while (!toAdd.empty()) { + Polygon triangleToAdd = toAdd.pop(); + Stack localAddends = new Stack(); + localAddends.push(triangleToAdd); + for (Polygon triangleAlreadyThere : triangles) { + for (Line e : triangleAlreadyThere.getOutlineSegments()) { + Stack nextAddends = new Stack(); + for (Iterator i = localAddends.iterator(); i + .hasNext();) { + Polygon addend = i.next(); + i.remove(); + for (Polygon subTriangleToAdd : triangulate(addend, e)) + if (!triangleAlreadyThere + .contains(subTriangleToAdd)) + nextAddends.push(subTriangleToAdd); + } + localAddends = nextAddends; + } + } + for (Polygon addend : localAddends) { + triangles.add(addend); + } + } + + optimizeTriangles(); + + return this; + } + + public boolean contains(IGeometry g) { + return CurveUtils.contains(this, g); + } + + private boolean findSharedAndOuterVertices(Polygon t1, Polygon t2, + Point[] shared, Point[] outer) { + Point[] t1Points = t1.getPoints(); + Point[] t2Points = t2.getPoints(); + boolean[] t1IsShared = new boolean[] { false, false, false }; + boolean[] t2IsShared = new boolean[] { false, false, false }; + + int sc = 0; + for (int i = 0; i < t1Points.length; i++) { + for (int j = 0; j < t2Points.length; j++) { + if (t1Points[i].equals(t2Points[j])) { + if (sc++ == 2) { + return false; + } + t1IsShared[i] = true; + t2IsShared[j] = true; + } + } + } + if (sc != 2) { + return false; + } + + for (int i = 0, c = 0; i < t1Points.length; i++) { + if (t1IsShared[i]) { + shared[c++] = t1Points[i]; + } else { + outer[0] = t1Points[i]; + } + + if (!t2IsShared[i]) { + outer[1] = t2Points[i]; + } + } + + return true; + } + + @Override + protected Line[] getAllEdges() { + Stack edges = new Stack(); + + for (Polygon t : triangles) { + for (Line e : t.getOutlineSegments()) { + edges.push(e); + } + } + return edges.toArray(new Line[] {}); } public Rectangle getBounds() { - throw new UnsupportedOperationException("Not yet implemented."); + if (triangles.size() == 0) + return null; + + Rectangle bounds = triangles.get(0).getBounds(); + for (int i = 1; i < triangles.size(); i++) + bounds.union(triangles.get(i).getBounds()); + + return bounds; } - public boolean touches(Rectangle r) { - throw new UnsupportedOperationException("Not yet implemented."); + public Ring getCopy() { + return new Ring(this); + } + + public Polygon[] getShapes() { + return triangles.toArray(new Polygon[] {}); + } + + private Polygon mergeTriangles(Polygon t1, Polygon t2) { + Point[] shared = new Point[2], outer = new Point[2]; + boolean found = findSharedAndOuterVertices(t1, t2, shared, outer); + if (found) { + Line outerLink = new Line(outer[0], outer[1]); + if (outerLink.contains(shared[0])) { + return new Polygon(outer[0], outer[1], shared[1]); + } else if (outerLink.contains(shared[1])) { + return new Polygon(outer[0], outer[1], shared[0]); + } + } + + return null; + } + + private void optimizeTriangles() { + for (int i = 0; i < triangles.size(); i++) { + Polygon t1 = triangles.get(i); + for (int j = i + 1; j < triangles.size(); j++) { + Polygon t2 = triangles.get(j); + Polygon merge = mergeTriangles(t1, t2); + if (merge != null) { + triangles.set(i, merge); + t1 = merge; + triangles.remove(j); + j = i; + } + } + } } public Path toPath() { - throw new UnsupportedOperationException("Not yet implemented."); + return getOutline().toPath(); + } + + public Ring getRotatedCCW(Angle angle) { + return getCopy().rotateCCW(angle); + } + + public Ring getRotatedCCW(Angle angle, double cx, double cy) { + return getCopy().rotateCCW(angle, cx, cy); + } + + public Ring getRotatedCCW(Angle angle, Point center) { + return getCopy().rotateCCW(angle, center); + } + + public Ring getRotatedCW(Angle angle) { + return getCopy().rotateCW(angle); + } + + public Ring getRotatedCW(Angle angle, double cx, double cy) { + return getCopy().rotateCW(angle, cx, cy); + } + + public Ring getRotatedCW(Angle angle, Point center) { + return getCopy().rotateCW(angle, center); + } + + public Ring rotateCCW(Angle angle) { + Point centroid = getBounds().getCentroid(); + return rotateCCW(angle, centroid.x, centroid.y); + } + + public Ring rotateCCW(Angle angle, double cx, double cy) { + for (Polygon p : triangles) { + p.rotateCCW(angle, cx, cy); + } + return this; + } + + public Ring rotateCCW(Angle angle, Point center) { + return rotateCCW(angle, center.x, center.y); + } + + public Ring rotateCW(Angle angle) { + Point centroid = getBounds().getCentroid(); + return rotateCW(angle, centroid.x, centroid.y); + } + + public Ring rotateCW(Angle angle, double cx, double cy) { + for (Polygon p : triangles) { + p.rotateCW(angle, cx, cy); + } + return this; + } + + public Ring rotateCW(Angle angle, Point center) { + return rotateCW(angle, center.x, center.y); + } + + public Ring scale(double factor) { + return scale(factor, factor); + } + + public Ring scale(double factor, double cx, double cy) { + return scale(factor, factor, cx, cy); + } + + public Ring scale(double factor, Point center) { + return scale(factor, factor, center.x, center.y); + } + + public Ring scale(double fx, double fy) { + Point centroid = getBounds().getCentroid(); + return scale(fx, fy, centroid.x, centroid.y); + } + + public Ring scale(double fx, double fy, double cx, double cy) { + for (Polygon p : triangles) { + p.scale(fx, fy, cx, cy); + } + return this; + } + + public Ring scale(double fx, double fy, Point center) { + return scale(fx, fy, center.x, center.y); + } + + public Ring getScaled(double factor) { + return getCopy().scale(factor); + } + + public Ring getScaled(double factor, double cx, double cy) { + return getCopy().scale(factor, cx, cy); + } + + public Ring getScaled(double factor, Point center) { + return getCopy().scale(factor, center); + } + + public Ring getScaled(double fx, double fy) { + return getCopy().scale(fx, fy); + } + + public Ring getScaled(double fx, double fy, double cx, double cy) { + return getCopy().scale(fx, fy, cx, cy); + } + + public Ring getScaled(double fx, double fy, Point center) { + return getCopy().scale(fx, fy, center); + } + + public Ring translate(double dx, double dy) { + for (Polygon p : triangles) { + p.translate(dx, dy); + } + return this; + } + + public Ring translate(Point d) { + return translate(d.x, d.y); + } + + public Ring getTranslated(double dx, double dy) { + return getCopy().translate(dx, dy); } - public IGeometry getCopy() { - throw new UnsupportedOperationException("Not yet implemented."); + public Ring getTranslated(Point d) { + return getCopy().translate(d.x, d.y); } } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/RoundedRectangle.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/RoundedRectangle.java index 028d581..2e975ca 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/RoundedRectangle.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/planar/RoundedRectangle.java @@ -29,7 +29,8 @@ import org.eclipse.gef4.geometry.utils.PrecisionUtils; * @author anyssen */ public final class RoundedRectangle extends - AbstractRectangleBasedGeometry implements IShape { + AbstractRectangleBasedGeometry implements + IShape { private static final long serialVersionUID = 1L; @@ -85,6 +86,10 @@ public final class RoundedRectangle extends arcHeight); } + public boolean contains(IGeometry g) { + return CurveUtils.contains(this, g); + } + /** * @see IGeometry#contains(Point) */ @@ -126,6 +131,19 @@ public final class RoundedRectangle extends return false; } + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof RoundedRectangle)) { + return false; + } + RoundedRectangle o = (RoundedRectangle) obj; + return PrecisionUtils.equal(x, o.x) && PrecisionUtils.equal(y, o.y) + && PrecisionUtils.equal(width, o.width) + && PrecisionUtils.equal(height, o.height) + && PrecisionUtils.equal(arcWidth, o.arcWidth) + && PrecisionUtils.equal(arcHeight, o.arcHeight); + } + /** * Returns the arc height of this {@link RoundedRectangle}, which is the * height of the arc used to define its rounded corners. @@ -147,63 +165,81 @@ public final class RoundedRectangle extends } /** - * Sets the arc height of this {@link RoundedRectangle}, which is the height - * of the arc used to define its rounded corners. + * Returns the bottom edge of this {@link RoundedRectangle}. * - * @param arcHeight - * the new arc height + * @return the bottom edge of this {@link RoundedRectangle}. */ - public void setArcHeight(double arcHeight) { - this.arcHeight = arcHeight; + public Line getBottom() { + return new Line(x + arcWidth, y + height, x + width - arcWidth, y + + height); } /** - * Sets the arc width of this {@link RoundedRectangle}, which is the width - * of the arc used to define its rounded corners. + * Returns the bottom left {@link Arc} of this {@link RoundedRectangle}. * - * @param arcWidth - * the new arc width + * @return the bottom left {@link Arc} of this {@link RoundedRectangle}. */ - public void setArcWidth(double arcWidth) { - this.arcWidth = arcWidth; + public Arc getBottomLeftArc() { + return new Arc(x, y + height - 2 * arcHeight, 2 * arcWidth, + 2 * arcHeight, Angle.fromDeg(180), Angle.fromDeg(90)); } /** - * @see IGeometry#toPath() + * Returns the bottom right {@link Arc} of this {@link RoundedRectangle}. + * + * @return the bottom right {@link Arc} of this {@link RoundedRectangle}. */ - public Path toPath() { - // overwritten to optimize w.r.t. object creation (could otherwise use - // the segments) - Path path = new Path(); - path.moveTo(x, y - arcHeight); - path.quadTo(x + arcWidth, y, x, y); - path.lineTo(x + width - arcWidth * 2, y); - path.quadTo(x + width, y + arcHeight, x + width, y); - path.lineTo(x + width, y + height - arcHeight * 2); - path.quadTo(x + width - arcWidth * 2, y + height, x + width, y + width); - path.lineTo(x + arcWidth, y + height); - path.quadTo(x, y + height - arcHeight * 2, x, y + height); - path.close(); - return path; + public Arc getBottomRightArc() { + return new Arc(x + width - 2 * arcWidth, y + height - 2 * arcHeight, + 2 * arcWidth, 2 * arcHeight, Angle.fromDeg(270), + Angle.fromDeg(90)); } /** - * Returns the top edge of this {@link RoundedRectangle}. - * - * @return the top edge of this {@link RoundedRectangle}. + * @see IGeometry#getCopy() */ - public Line getTop() { - return new Line(x + arcWidth, y, x + width - arcWidth, y); + public RoundedRectangle getCopy() { + return new RoundedRectangle(x, y, width, height, arcWidth, arcHeight); } /** - * Returns the bottom edge of this {@link RoundedRectangle}. + * Returns the left edge of this {@link RoundedRectangle}. * - * @return the bottom edge of this {@link RoundedRectangle}. + * @return the left edge of this {@link RoundedRectangle}. */ - public Line getBottom() { - return new Line(x + arcWidth, y + height - arcHeight, x + width - - arcWidth, y + height - arcHeight); + public Line getLeft() { + return new Line(x, y + arcHeight, x, y + height - arcHeight); + } + + public PolyBezier getOutline() { + return CurveUtils.getOutline(this); + } + + /** + * @see org.eclipse.gef4.geometry.planar.IShape#getOutlineSegments() + */ + public ICurve[] getOutlineSegments() { + // see http://whizkidtech.redprince.net/bezier/circle/kappa/ for details + // on the approximation used here + return new ICurve[] { + CurveUtils.computeEllipticalArcApproximation(x + width - 2 + * arcWidth, y, 2 * arcWidth, 2 * arcHeight, + Angle.fromDeg(0), Angle.fromDeg(90)), + new Line(x + width - arcWidth, y, x + arcWidth, y), + CurveUtils.computeEllipticalArcApproximation(x, y, + 2 * arcWidth, 2 * arcHeight, Angle.fromDeg(90), + Angle.fromDeg(180)), + new Line(x, y + arcHeight, x, y + height - arcHeight), + CurveUtils.computeEllipticalArcApproximation(x, y + height - 2 + * arcHeight, 2 * arcWidth, 2 * arcHeight, + Angle.fromDeg(180), Angle.fromDeg(270)), + new Line(x + arcWidth, y + height, x + width - arcWidth, y + + height), + CurveUtils.computeEllipticalArcApproximation(x + width - 2 + * arcWidth, y + height - 2 * arcHeight, 2 * arcWidth, + 2 * arcHeight, Angle.fromDeg(270), Angle.fromDeg(360)), + new Line(x + width, y + height - arcHeight, x + width, y + + arcHeight) }; } /** @@ -216,32 +252,37 @@ public final class RoundedRectangle extends - arcHeight); } - /** - * @see org.eclipse.gef4.geometry.planar.IShape#getOutlineSegments() - */ - public ICurve[] getOutlineSegments() { - return new ICurve[] { - new QuadraticCurve(x, y - arcHeight, x + arcWidth, y, x, y), - new Line(x, y, x + width - arcWidth * 2, y), - new QuadraticCurve(x + width - arcWidth * 2, y, x + width, y - + arcHeight, x + width, y), - new Line(x + width, y, x + width, y + height - arcHeight * 2), - new QuadraticCurve(x + width, y + height - arcHeight * 2, x - + width - arcWidth * 2, y + height, x + width, y - + width), - new Line(x + width, y + width, x + arcWidth, y + height), - new QuadraticCurve(x + arcWidth, y + height, x, y + height - - arcHeight * 2, x, y + height), - new Line(x, y + height, x, y - arcHeight) }; + public PolyBezier getRotatedCCW(Angle angle) { + return getOutline().rotateCCW(angle); + } + + public PolyBezier getRotatedCCW(Angle angle, double cx, double cy) { + return getOutline().rotateCCW(angle, cx, cy); + } + + public PolyBezier getRotatedCCW(Angle angle, Point center) { + return getOutline().rotateCCW(angle, center); + } + + public PolyBezier getRotatedCW(Angle angle) { + return getOutline().rotateCW(angle); + } + + public PolyBezier getRotatedCW(Angle angle, double cx, double cy) { + return getOutline().rotateCW(angle, cx, cy); + } + + public PolyBezier getRotatedCW(Angle angle, Point center) { + return getOutline().rotateCW(angle, center); } /** - * Returns the left edge of this {@link RoundedRectangle}. + * Returns the top edge of this {@link RoundedRectangle}. * - * @return the left edge of this {@link RoundedRectangle}. + * @return the top edge of this {@link RoundedRectangle}. */ - public Line getLeft() { - return new Line(x, y + arcHeight, x, y + height - arcHeight); + public Line getTop() { + return new Line(x + arcWidth, y, x + width - arcWidth, y); } /** @@ -250,7 +291,7 @@ public final class RoundedRectangle extends * @return the top left {@link Arc} of this {@link RoundedRectangle}. */ public Arc getTopLeftArc() { - return new Arc(x, y, arcWidth, arcHeight, Angle.fromDeg(90), + return new Arc(x, y, 2 * arcWidth, 2 * arcHeight, Angle.fromDeg(90), Angle.fromDeg(90)); } @@ -260,48 +301,56 @@ public final class RoundedRectangle extends * @return the top right {@link Arc} of this {@link RoundedRectangle}. */ public Arc getTopRightArc() { - return new Arc(x + width - arcWidth, y, arcWidth, arcHeight, - Angle.fromDeg(0), Angle.fromDeg(90)); + return new Arc(x + width - 2 * arcWidth, y, 2 * arcWidth, + 2 * arcHeight, Angle.fromDeg(0), Angle.fromDeg(90)); } /** - * Returns the bottom left {@link Arc} of this {@link RoundedRectangle}. + * Sets the arc height of this {@link RoundedRectangle}, which is the height + * of the arc used to define its rounded corners. * - * @return the bottom left {@link Arc} of this {@link RoundedRectangle}. + * @param arcHeight + * the new arc height */ - public Arc getBottomLeftArc() { - return new Arc(x, y + height - arcHeight, arcWidth, arcHeight, - Angle.fromDeg(180), Angle.fromDeg(90)); + public void setArcHeight(double arcHeight) { + this.arcHeight = arcHeight; } /** - * Returns the bottom right {@link Arc} of this {@link RoundedRectangle}. + * Sets the arc width of this {@link RoundedRectangle}, which is the width + * of the arc used to define its rounded corners. * - * @return the bottom right {@link Arc} of this {@link RoundedRectangle}. + * @param arcWidth + * the new arc width */ - public Arc getBottomRightArc() { - return new Arc(x + width - arcWidth, y + height - arcHeight, arcWidth, - arcHeight, Angle.fromDeg(270), Angle.fromDeg(90)); - } - - @Override - public String toString() { - return "RoundedRectangle: (" + x + ", " + y + ", " + width + ", " + height + ", " + arcWidth + ", " + arcHeight; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ + public void setArcWidth(double arcWidth) { + this.arcWidth = arcWidth; } /** - * @see IGeometry#getCopy() + * @see IGeometry#toPath() */ - public RoundedRectangle getCopy() { - return new RoundedRectangle(x, y, width, height, arcWidth, arcHeight); - } - - public IPolyCurve getOutline() { - return CurveUtils.getOutline(this); + public Path toPath() { + // return CurveUtils.toPath(getOutlineSegments()); + // TODO: use cubic curves instead of quadratic curves here! + // overwritten to optimize w.r.t. object creation (could otherwise use + // the segments) + Path path = new Path(); + path.moveTo(x, y + arcHeight); + path.quadTo(x, y, x + arcWidth, y); + path.lineTo(x + width - arcWidth, y); + path.quadTo(x + width, y, x + width, y + arcHeight); + path.lineTo(x + width, y + height - arcHeight); + path.quadTo(x + width, y + height, x + width - arcWidth, y + height); + path.lineTo(x + arcWidth, y + height); + path.quadTo(x, y + height, x, y + height - arcHeight); + path.close(); + return path; } - public boolean contains(IGeometry g) { - return CurveUtils.contains(this, g); + @Override + public String toString() { + return "RoundedRectangle(" + x + ", " + y + ", " + width + ", " + height + ", " + arcWidth + ", " + arcHeight + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ } } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/projective/Vector3D.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/projective/Vector3D.java index 4b238ed..5cc50cd 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/projective/Vector3D.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/projective/Vector3D.java @@ -184,7 +184,7 @@ public final class Vector3D { @Override public String toString() { - return "Vector3D (" + x + ", " + y + ", " + z + ")"; + return "Vector3D(" + x + ", " + y + ", " + z + ")"; } @Override @@ -193,4 +193,4 @@ public final class Vector3D { // comparisons return 0; } -} \ No newline at end of file +} diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/IRotatable.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/IRotatable.java new file mode 100644 index 0000000..96413b3 --- /dev/null +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/IRotatable.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.transform; + +import org.eclipse.gef4.geometry.Angle; +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.planar.Ellipse; +import org.eclipse.gef4.geometry.planar.IGeometry; +import org.eclipse.gef4.geometry.planar.Rectangle; +import org.eclipse.gef4.geometry.planar.Region; +import org.eclipse.gef4.geometry.planar.RoundedRectangle; + +/** + *

+ * The {@link IRotatable} interface collects the out-of-place rotation short-cut + * methods. + *

+ * + *

+ * Rotation cannot be applied directly to all {@link IGeometry}s. For example, + * {@link Rectangle}, {@link Ellipse}, {@link Region} and + * {@link RoundedRectangle} cannot be slanted. Therefore, you have to specify + * the result type for the rotation methods via a type parameter. + *

+ * + *

+ * There are two directions of rotation: clock-wise (CW) and counter-clock-wise + * (CCW). The individual method names reflect the direction of rotation that is + * used. These are the rotation methods: {@link #getRotatedCCW(Angle)}, + * {@link #getRotatedCCW(Angle, Point)}, + * {@link #getRotatedCCW(Angle, double, double)}, {@link #getRotatedCW(Angle)}, + * {@link #getRotatedCW(Angle, Point)}, + * {@link #getRotatedCW(Angle, double, double)}. + *

+ * + *

+ * If you do not specify a {@link Point} to rotate around, the implementation + * can appropriately choose one. In most cases, this will be the center + * {@link Point} of the rotated object. + *

+ * + * @param + * type of the rotation results + */ +public interface IRotatable { + + /** + * Rotates the calling object by specified {@link Angle} counter-clock-wise + * (CCW) around its center {@link Point}. Does not necessarily return an + * object of the same type. + * + * @param angle + * rotation {@link Angle} + * @return an {@link IGeometry} representing the result of the rotation + */ + public T getRotatedCCW(Angle angle); + + /** + * Rotates the calling object by the specified {@link Angle} + * counter-clock-wise (CCW) around the specified center {@link Point} (cx, + * cy). Does not necessarily return an object of the same type. + * + * @param angle + * rotation {@link Angle} + * @param cx + * x-coordinate of the relative {@link Point} for the rotation + * @param cy + * y-coordinate of the relative {@link Point} for the rotation + * @return an {@link IGeometry} representing the result of the rotation + */ + public T getRotatedCCW(Angle angle, double cx, double cy); + + /** + * Rotates the calling object by the specified {@link Angle} + * counter-clock-wise (CCW) around the specified center {@link Point}. Does + * not necessarily return an object of the same type. + * + * @param angle + * rotation {@link Angle} + * @param center + * relative {@link Point} for the rotation + * @return an {@link IGeometry} representing the result of the rotation + */ + public T getRotatedCCW(Angle angle, Point center); + + /** + * Rotates the calling object by specified {@link Angle} clock-wise (CW) + * around its center {@link Point}. Does not necessarily return an object of + * the same type. + * + * @param angle + * rotation {@link Angle} + * @return an {@link IGeometry} representing the result of the rotation + */ + public T getRotatedCW(Angle angle); + + /** + * Rotates the calling object by the specified {@link Angle} clock-wise (CW) + * around the specified center {@link Point} (cx, cy). Does not necessarily + * return an object of the same type. + * + * @param angle + * rotation {@link Angle} + * @param cx + * x-coordinate of the relative {@link Point} for the rotation + * @param cy + * y-coordinate of the relative {@link Point} for the rotation + * @return an {@link IGeometry} representing the result of the rotation + */ + public T getRotatedCW(Angle angle, double cx, double cy); + + /** + * Rotates the calling object by the specified {@link Angle} clock-wise (CW) + * around the specified center {@link Point}. Does not necessarily return an + * object of the same type. + * + * @param angle + * rotation {@link Angle} + * @param center + * relative {@link Point} for the rotation + * @return an {@link IGeometry} representing the result of the rotation + */ + public T getRotatedCW(Angle angle, Point center); + +} diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/IScalable.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/IScalable.java new file mode 100644 index 0000000..9200f0a --- /dev/null +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/IScalable.java @@ -0,0 +1,207 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.transform; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.planar.IGeometry; + +/** + *

+ * The {@link IScalable} interface collects all scaling short-cut methods. + *

+ * + *

+ * The {@link #scale(double)}, {@link #scale(double, double)}, + * {@link #scale(double, Point)}, {@link #scale(double, double, double)}, + * {@link #scale(double, double, Point)} and + * {@link #scale(double, double, double, double)} methods are directly applied + * to the calling object. They scale it by the given factor(s) around the given + * {@link Point} or an appropriate default. + *

+ * + *

+ * On the other hand, the {@link #getScaled(double)}, + * {@link #getScaled(double, double)}, {@link #getScaled(double, Point)}, + * {@link #getScaled(double, double, double)}, + * {@link #getScaled(double, double, Point)} and + * {@link #getScaled(double, double, double, double)} methods are applied to a + * copy of the calling object. + *

+ * + *

+ * If you do not specify the relative {@link Point} for the scaling, the + * implementation will appropriately choose one. In most cases, this will be the + * center of the scaled object. + *

+ * + * @param + * the implementing type + */ +public interface IScalable { + + /** + * Scales the calling object by the given factor relative to its center + * {@link Point}. + * + * @param factor + * scale-factor + * @return this for convenience + */ + public T scale(double factor); + + /** + * Scales the calling object by the given factor relative to the given + * center {@link Point} (cx, cy). + * + * @param factor + * scale-factor + * @param cx + * x-coordinate of the relative {@link Point} for the scaling + * @param cy + * y-coordinate of the relative {@link Point} for the scaling + * @return this for convenience + */ + public T scale(double factor, double cx, double cy); + + /** + * Scales the calling object by the given factor relative to the given + * center {@link Point}. + * + * @param factor + * scale-factor + * @param center + * relative {@link Point} for the scaling + * @return this for convenience + */ + public T scale(double factor, Point center); + + /** + * Scales the calling object by the given factors relative to the given + * center {@link Point}. + * + * @param fx + * x-scale-factor + * @param fy + * y-scale-factor + * @return this for convenience + */ + public T scale(double fx, double fy); + + /** + * Scales the calling object by the given factors relative to the given + * center {@link Point} (cx, cy). + * + * @param fx + * x-scale-factor + * @param fy + * y-scale-factor + * @param cx + * x-coordinate of the relative {@link Point} for the scaling + * @param cy + * y-coordinate of the relative {@link Point} for the scaling + * @return this for convenience + */ + public T scale(double fx, double fy, double cx, double cy); + + /** + * Scales the calling object by the given factors relative to the given + * center {@link Point}. + * + * @param fx + * x-scale-factor + * @param fy + * y-scale-factor + * @param center + * relative {@link Point} for the scaling + * @return this for convenience + */ + public T scale(double fx, double fy, Point center); + + /** + * Scales a copy of the calling object by the given factor relative to its + * center {@link Point}. + * + * @param factor + * scale-factor + * @return the new, scaled object + */ + public T getScaled(double factor); + + /** + * Scales a copy of the calling object by the given factor relative to the + * given center {@link Point} (cx, cy). + * + * @param factor + * scale-factor + * @param cx + * x-coordinate of the relative {@link Point} for the scaling + * @param cy + * y-coordinate of the relative {@link Point} for the scaling + * @return the new, scaled object + */ + public T getScaled(double factor, double cx, double cy); + + /** + * Scales a copy of the calling object by the given factor relative to the + * given center {@link Point}. + * + * @param factor + * scale-factor + * @param center + * relative {@link Point} for the scaling + * @return the new, scaled object + */ + public T getScaled(double factor, Point center); + + /** + * Scales a copy of the calling object by the given factors relative to its + * center {@link Point}. + * + * @param fx + * x-scale-factor + * @param fy + * y-scale-factor + * @return the new, scaled object + */ + public T getScaled(double fx, double fy); + + /** + * Scales a copy of the calling object by the given factors relative to the + * given center {@link Point} (cx, cy). + * + * @param fx + * x-scale-factor + * @param fy + * y-scale-factor + * @param cx + * x-coordinate of the relative {@link Point} for the scaling + * @param cy + * y-coordinate of the relative {@link Point} for the scaling + * @return the new, scaled object + */ + public T getScaled(double fx, double fy, double cx, double cy); + + /** + * Scales a copy of the calling object by the given factors relative to the + * given center {@link Point}. + * + * @param fx + * x-scale-factor + * @param fy + * y-scale-factor + * @param center + * relative {@link Point} for the scaling + * @return the new, scaled object + */ + public T getScaled(double fx, double fy, Point center); + +} diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/ITranslatable.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/ITranslatable.java new file mode 100644 index 0000000..063301b --- /dev/null +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/transform/ITranslatable.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2012 itemis AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Matthias Wienand (itemis AG) - initial API and implementation + * + *******************************************************************************/ +package org.eclipse.gef4.geometry.transform; + +import org.eclipse.gef4.geometry.Point; +import org.eclipse.gef4.geometry.planar.IGeometry; + +/** + *

+ * The {@link ITranslatable} interface collects all translation short-cut + * methods. + *

+ * + *

+ * Translation can be applied directly on an object via the + * {@link #translate(Point)} and {@link #translate(double, double)} methods. + * They return the scaled, calling object for convenience. + *

+ * + *

+ * On the other hand, the {@link #getTranslated(Point)} and + * {@link #getTranslated(double, double)} methods create a translated copy of + * the original object. + *

+ * + * @param + * the implementing type + */ +public interface ITranslatable { + + /** + * Translates the object by the given values in x and y direction. + * + * @param dx + * x-translation + * @param dy + * y-translation + * @return this for convenience + */ + public T translate(double dx, double dy); + + /** + * Translates the object by the given {@link Point}. + * + * @param d + * translation {@link Point} + * @return this for convenience + */ + public T translate(Point d); + + /** + * Translates a copy of this object by the given values in x and y + * direction. + * + * @param dx + * x-translation + * @param dy + * y-translation + * @return a new, translated object + */ + public T getTranslated(double dx, double dy); + + /** + * Translates a copy of this object by the given {@link Point}. + * + * @param d + * translation {@link Point} + * @return a new, translated object + */ + public T getTranslated(Point d); + +} diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/CurveUtils.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/CurveUtils.java index 4605a63..8115b18 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/CurveUtils.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/CurveUtils.java @@ -17,17 +17,22 @@ import java.util.Comparator; import java.util.HashSet; import java.util.Set; +import org.eclipse.gef4.geometry.Angle; import org.eclipse.gef4.geometry.Point; import org.eclipse.gef4.geometry.euclidean.Straight; +import org.eclipse.gef4.geometry.planar.Arc; import org.eclipse.gef4.geometry.planar.BezierCurve; import org.eclipse.gef4.geometry.planar.BezierCurve.IntervalPair; +import org.eclipse.gef4.geometry.planar.CubicCurve; import org.eclipse.gef4.geometry.planar.ICurve; import org.eclipse.gef4.geometry.planar.IGeometry; import org.eclipse.gef4.geometry.planar.IPolyCurve; import org.eclipse.gef4.geometry.planar.IPolyShape; import org.eclipse.gef4.geometry.planar.IShape; import org.eclipse.gef4.geometry.planar.Line; +import org.eclipse.gef4.geometry.planar.Path; import org.eclipse.gef4.geometry.planar.PolyBezier; +import org.eclipse.gef4.geometry.planar.QuadraticCurve; /** * The {@link CurveUtils} class provides functionality that can be used for @@ -259,12 +264,7 @@ public class CurveUtils { for (ICurve segC : shape.getOutlineSegments()) { for (BezierCurve seg : segC.toBezier()) { Set inters = new HashSet(); - Set ips = c.getIntersectionIntervalPairs( - new BezierCurve(seg.getP1(), seg.getP2()), inters); - for (IntervalPair ip : ips) { - intersectionParams.add(ip.p == c ? ip.pi.getMid() : ip.qi - .getMid()); - } + c.getIntersectionIntervalPairs(seg, inters); for (Point poi : inters) { intersectionParams.add(c.getParameterAt(poi)); } @@ -278,7 +278,7 @@ public class CurveUtils { * * TODO: Special case! There is a special case where the Bezier curve * leaves and enters the shape in the same point. This is only possible - * if the Bezier curve has a self intersections at that point. + * if the Bezier curve has a self intersection at that point. */ if (intersectionParams.size() <= 1) { return true; @@ -305,6 +305,155 @@ public class CurveUtils { } /** + * TODO: generalize the contains() method for IShape and IPolyShape. + * + * @param polyShape + * @param c + * @return true if the {@link BezierCurve} is contained by the + * {@link IPolyShape}, otherwise false + */ + public static boolean contains(IPolyShape polyShape, BezierCurve c) { + if (!(polyShape.contains(c.getP1()) && polyShape.contains(c.getP2()))) { + return false; + } + + Set intersectionParams = new HashSet(); + + for (ICurve segC : polyShape.getOutlineSegments()) { + for (BezierCurve seg : segC.toBezier()) { + Set inters = new HashSet(); + Set ips = c.getIntersectionIntervalPairs(seg, + inters); + for (IntervalPair ip : ips) { + intersectionParams.add(ip.p == c ? ip.pi.getMid() : ip.qi + .getMid()); + } + for (Point poi : inters) { + intersectionParams.add(c.getParameterAt(poi)); + } + } + } + + /* + * Start and end point of the curve are guaranteed to lie inside the + * IPolyShape. If the curve would not be contained by the shape, at + * least two intersections could be found. + * + * TODO: Special case! There is a special case where the Bezier curve + * leaves and enters the shape in the same point. This is only possible + * if the Bezier curve has a self intersection at that point. + */ + if (intersectionParams.size() <= 1) { + return true; + } + + Double[] poiParams = intersectionParams.toArray(new Double[] {}); + Arrays.sort(poiParams, new Comparator() { + public int compare(Double t, Double u) { + double d = t - u; + return d < 0 ? -1 : d > 0 ? 1 : 0; + } + }); + + // check the points between the intersections for containment + if (!polyShape.contains(c.get(poiParams[0] / 2))) { + return false; + } + for (int i = 0; i < poiParams.length - 1; i++) { + if (!polyShape.contains(c + .get((poiParams[i] + poiParams[i + 1]) / 2))) { + return false; + } + } + return polyShape.contains(c + .get((poiParams[poiParams.length - 1] + 1) / 2)); + } + + /** + * Checks if the given {@link ICurve} is contained by the given + * {@link IPolyShape}. + * + * @param ps + * @param c + * @return true if the {@link ICurve} is contained by the + * {@link IPolyShape}, otherwise false + */ + public static boolean contains(IPolyShape ps, ICurve c) { + for (BezierCurve bc : c.toBezier()) + if (!contains(ps, bc)) + return false; + return true; + } + + /** + * Checks if the {@link IShape} is contained by the {@link IPolyShape}. + * + * @param ps + * @param s + * @return true if the {@link IShape} is contained by the + * {@link IPolyShape}, otherwise false + */ + public static boolean contains(IPolyShape ps, IShape s) { + for (ICurve c : s.getOutlineSegments()) + if (!contains(ps, c)) + return false; + return true; + } + + /** + * Checks if the {@link IPolyCurve} is contained by the {@link IPolyShape}. + * + * @param ps + * @param pc + * @return true if the {@link IPolyCurve} is contained by the + * {@link IPolyShape}, otherwise false + */ + public static boolean contains(IPolyShape ps, IPolyCurve pc) { + for (ICurve c : pc.getCurves()) + if (!contains(ps, c)) + return false; + return true; + } + + /** + * Checks if the second {@link IPolyShape} is contained by the first + * {@link IPolyShape}. + * + * @param ps + * @param ps2 + * @return true if the second {@link IPolyShape} is contained + * by the first {@link IPolyShape}, otherwise false + */ + public static boolean contains(IPolyShape ps, IPolyShape ps2) { + for (IShape s : ps2.getShapes()) + if (!contains(ps, s)) + return false; + return true; + } + + /** + * Checks if the {@link IGeometry} is contained by the {@link IPolyShape}. + * + * @param ps + * @param g + * @return true if the {@link IGeometry} is contained by the + * {@link IPolyShape}, otherwise false + */ + public static boolean contains(IPolyShape ps, IGeometry g) { + if (g instanceof ICurve) { + return contains(ps, (ICurve) g); + } else if (g instanceof IShape) { + return contains(ps, (IShape) g); + } else if (g instanceof IPolyCurve) { + return contains(ps, (IPolyCurve) g); + } else if (g instanceof IPolyShape) { + return contains(ps, (IPolyShape) g); + } else { + throw new UnsupportedOperationException("Not yet implemented."); + } + } + + /** * Returns true if the given {@link IShape} fully contains the * given {@link ICurve}. Otherwise, false is returned. A * {@link ICurve} is contained by a {@link IShape} if the {@link ICurve}'s @@ -457,7 +606,7 @@ public class CurveUtils { if (geom1 instanceof IShape) { return contains((IShape) geom1, geom2); } else if (geom1 instanceof IPolyShape) { - throw new UnsupportedOperationException("Not yet implemented."); + return contains((IPolyShape) geom1, geom2); } else { return false; } @@ -487,4 +636,102 @@ public class CurveUtils { return new PolyBezier(beziers.toArray(new BezierCurve[] {})); } + + /** + * Builds up a {@link Path} from the given {@link ICurve}s. Only + * {@link Line}, {@link QuadraticCurve} and {@link CubicCurve} objects can + * be integrated into the constructed {@link Path}. + * + * @param curves + * @return a {@link Path} representing the given {@link ICurve}s + */ + public static final Path toPath(ICurve... curves) { + Path p = new Path(); + for (int i = 0; i < curves.length; i++) { + if (i == 0) { + p.moveTo(curves[i].getX1(), curves[i].getY1()); + } + ICurve c = curves[i]; + if (c instanceof Line) { + p.lineTo(c.getX2(), c.getY2()); + } else if (c instanceof QuadraticCurve) { + p.quadTo(((QuadraticCurve) c).getCtrlX(), + ((QuadraticCurve) c).getCtrlY(), c.getX2(), c.getY2()); + } else if (c instanceof CubicCurve) { + p.curveTo(((CubicCurve) c).getCtrlX1(), + ((CubicCurve) c).getCtrlY1(), + ((CubicCurve) c).getCtrlX2(), + ((CubicCurve) c).getCtrlY2(), ((CubicCurve) c).getX2(), + ((CubicCurve) c).getY2()); + } else { + throw new UnsupportedOperationException( + "This type of ICurve is not yet implemented."); + } + } + return p; + } + + /** + *

+ * Computes a {@link CubicCurve} that approximates the elliptical + * {@link Arc} given by the location, the width, and the height of the + * implied ellipse and the start and end {@link Angle}s of the arc. + *

+ * + *

+ * The given start and end {@link Angle}s may not span an {@link Angle} of + * more than 90 degrees. + *

+ * + * @param x + * left coordinate value of the aforementioned ellipse + * @param y + * top coordinate value of the aforementioned ellipse + * @param width + * width of the aforementioned ellipse + * @param height + * height of the aforementioned ellipse + * @param start + * start angle (in radiant) of the elliptical arc + * @param end + * end angle (in radiant) of the elliptical arc + * @return {@link CubicCurve} approximating the determinated elliptical arc + */ + public static CubicCurve computeEllipticalArcApproximation(double x, + double y, double width, double height, Angle start, Angle end) { + // TODO: verify that the following test is valid + if (!PrecisionUtils.smallerEqual(end.getAdded(start.getOppositeFull()) + .deg(), 90)) { + throw new IllegalArgumentException( + "Only angular extents of up to 90 degrees are allowed."); + } + + // compute major and minor axis length + double a = width / 2; + double b = height / 2; + + double srad = start.rad(); + double erad = end.rad(); + + // calculate start and end points of the arc from start to end + Point startPoint = new Point(x + a + a * Math.cos(srad), y + b - b + * Math.sin(srad)); + Point endPoint = new Point(x + a + a * Math.cos(erad), y + b - b + * Math.sin(erad)); + + // approximation by cubic Bezier according to approximation provided in: + // http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf + double t = Math.tan((erad - srad) / 2); + double alpha = Math.sin(erad - srad) + * (Math.sqrt(4.0d + 3.0d * t * t) - 1) / 3; + Point controlPoint1 = new Point(startPoint.x + alpha * -a + * Math.sin(srad), startPoint.y - alpha * b * Math.cos(srad)); + Point controlPoint2 = new Point(endPoint.x - alpha * -a + * Math.sin(erad), endPoint.y + alpha * b * Math.cos(erad)); + + Point[] points = new Point[] { startPoint, controlPoint1, + controlPoint2, endPoint }; + return new CubicCurve(points); + } + } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/PointListUtils.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/PointListUtils.java index 5911ef9..e373eef 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/PointListUtils.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/PointListUtils.java @@ -16,8 +16,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import org.eclipse.gef4.geometry.Angle; import org.eclipse.gef4.geometry.Point; import org.eclipse.gef4.geometry.euclidean.Straight; +import org.eclipse.gef4.geometry.euclidean.Vector; import org.eclipse.gef4.geometry.planar.Line; import org.eclipse.gef4.geometry.planar.Polygon; import org.eclipse.gef4.geometry.planar.Polyline; @@ -32,13 +34,15 @@ import org.eclipse.gef4.geometry.planar.Rectangle; public class PointListUtils { /** - * Compares two array of {@link Point} for equality. + * Compares two arrays of {@link Point} for equality. + * + * TODO: What is the benefit over using Arrays.equals()? * * @param p1 * the first array of points to compare * @param p2 * the second array of points to compare - * @return true in case both arrays are of the same lenght and + * @return true in case both arrays are of the same length and * for each index i it holds that p1[i] * equals p2[i], false otherwise */ @@ -55,6 +59,29 @@ public class PointListUtils { } /** + * Compares two arrays of {@link Point} for reverse equality, i.e. if one + * array is the reverse of the other array. + * + * @param p1 + * the first array of {@link Point} to compare + * @param p2 + * the second array of {@link Point} to compare + * @return true in case one array is the reverse of the other + * array, false otherwise + */ + public static boolean equalsReverse(Point[] p1, Point[] p2) { + if (p1.length != p2.length) { + return false; + } + for (int i = 0; i < p1.length; i++) { + if (!p1[i].equals(p2[p1.length - i - 1])) { + return false; + } + } + return true; + } + + /** * Returns the smallest {@link Rectangle} that encloses all {@link Point}s * in the given sequence. Note that the right and bottom borders of a * {@link Rectangle} are regarded as being part of the {@link Rectangle}. @@ -323,4 +350,99 @@ public class PointListUtils { return points; } + /** + * Computes the centroid of the given {@link Point}s. The centroid is the + * "center of gravity", i.e. assuming the {@link Polygon} spanned by the + * {@link Point}s is made of a material of constant density, it will be in a + * balanced state, if you put it on a pin that is placed exactly on its + * centroid. + * + * @param points + * @return the center {@link Point} (or centroid) of the given {@link Point} + * s + */ + public static Point computeCentroid(Point... points) { + if (points.length == 0) { + return null; + } else if (points.length == 1) { + return points[0].getCopy(); + } + + double cx = 0, cy = 0, a, sa = 0; + for (int i = 0; i < points.length - 1; i++) { + a = points[i].x * points[i + 1].y - points[i].y * points[i + 1].x; + sa += a; + cx += (points[i].x + points[i + 1].x) * a; + cy += (points[i].y + points[i + 1].y) * a; + } + + // closing segment + a = points[points.length - 2].x * points[points.length - 1].y + - points[points.length - 2].y * points[points.length - 1].x; + sa += a; + cx += (points[points.length - 2].x + points[points.length - 1].x) * a; + cy += (points[points.length - 2].x + points[points.length - 1].x) * a; + + return new Point(cx / (3 * sa), cy / (3 * sa)); + } + + /** + * Rotates (in-place) the given {@link Point}s counter-clock-wise (CCW) by + * the specified {@link Angle} around the given center {@link Point}. + * + * @param points + * @param angle + * @param cx + * @param cy + */ + public static void rotateCCW(Point[] points, Angle angle, double cx, + double cy) { + translate(points, -cx, -cy); + for (Point p : points) { + Point np = new Vector(p).rotateCCW(angle).toPoint(); + p.x = np.x; + p.y = np.y; + } + translate(points, cx, cy); + } + + /** + * Rotates (in-place) the given {@link Point}s clock-wise (CW) by the + * specified {@link Angle} around the given center {@link Point}. + * + * @param points + * @param angle + * @param cx + * @param cy + */ + public static void rotateCW(Point[] points, Angle angle, double cx, + double cy) { + translate(points, -cx, -cy); + for (Point p : points) { + Point np = new Vector(p).rotateCW(angle).toPoint(); + p.x = np.x; + p.y = np.y; + } + translate(points, cx, cy); + } + + /** + * Scales the given array of {@link Point}s by the given x and y scale + * factors around the given center {@link Point} (cx, cy). + * + * @param points + * @param fx + * @param fy + * @param cx + * @param cy + */ + public static void scale(Point[] points, double fx, double fy, double cx, + double cy) { + translate(points, -cx, -cy); + for (Point p : points) { + p.scale(fx, fy); + } + translate(points, cx, cy); + } + } diff --git a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/PolynomCalculationUtils.java b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/PolynomCalculationUtils.java index 452fded..a936b33 100644 --- a/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/PolynomCalculationUtils.java +++ b/org.eclipse.gef4.geometry/src/org/eclipse/gef4/geometry/utils/PolynomCalculationUtils.java @@ -80,8 +80,8 @@ public final class PolynomCalculationUtils { public static final double[] getCubicRoots(double A, double B, double C, double D) { // TODO: use an algorithm that abstracts the polynom's order. A - // possibility would be to use the CurveUtils$BezierCurve#contains(Point - // p) method. + // possibility would be to use the BezierCurve#contains(Point p) + // method. if (A == 0) { return getQuadraticRoots(B, C, D); -- 1.7.4.1