├── .gitignore ├── Clipper Console ├── Test.clip ├── Test.sub ├── .project ├── .classpath ├── pom.xml └── src │ └── de │ └── lighti │ └── clipper │ └── console │ ├── SVGBuilder.java │ └── Program.java ├── Clipper GUI ├── aust.bin ├── .project ├── .classpath ├── pom.xml └── src │ └── de │ └── lighti │ └── clipper │ └── gui │ ├── StatusBar.java │ ├── LittleEndianDataInputStream.java │ ├── SVGBuilder.java │ ├── PolygonCanvas.java │ └── ClipperDialog.java ├── Clipper ├── src │ └── de │ │ └── lighti │ │ └── clipper │ │ ├── LongRect.java │ │ ├── PolyTree.java │ │ ├── Clipper.java │ │ ├── PolyNode.java │ │ ├── Paths.java │ │ ├── Point.java │ │ ├── Edge.java │ │ ├── Path.java │ │ ├── ClipperOffset.java │ │ └── ClipperBase.java ├── .project ├── .classpath └── pom.xml ├── README.md └── license.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .metadata 2 | target 3 | bin 4 | .settings 5 | -------------------------------------------------------------------------------- /Clipper Console/Test.clip: -------------------------------------------------------------------------------- 1 | 1 2 | 3 3 | -10 -10 4 | -10 10 5 | 10 10 -------------------------------------------------------------------------------- /Clipper Console/Test.sub: -------------------------------------------------------------------------------- 1 | 1 2 | 4 3 | -5 -5 4 | -5 5 5 | 5 5 6 | 5 -5 -------------------------------------------------------------------------------- /Clipper GUI/aust.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lightbringer/clipper-java/HEAD/Clipper GUI/aust.bin -------------------------------------------------------------------------------- /Clipper/src/de/lighti/clipper/LongRect.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper; 2 | 3 | public class LongRect { 4 | public long left; 5 | public long top; 6 | public long right; 7 | public long bottom; 8 | 9 | public LongRect() { 10 | 11 | } 12 | 13 | public LongRect( long l, long t, long r, long b ) { 14 | left = l; 15 | top = t; 16 | right = r; 17 | bottom = b; 18 | } 19 | 20 | public LongRect( LongRect ir ) { 21 | left = ir.left; 22 | top = ir.top; 23 | right = ir.right; 24 | bottom = ir.bottom; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Clipper/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Clipper 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.m2e.core.maven2Nature 21 | org.eclipse.jdt.core.javanature 22 | 23 | 24 | -------------------------------------------------------------------------------- /Clipper GUI/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Clipper GUI 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.m2e.core.maven2Nature 21 | org.eclipse.jdt.core.javanature 22 | 23 | 24 | -------------------------------------------------------------------------------- /Clipper Console/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Clipper Console 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.m2e.core.maven2Nature 21 | org.eclipse.jdt.core.javanature 22 | 23 | 24 | -------------------------------------------------------------------------------- /Clipper/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Clipper GUI/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Clipper Console/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Clipper/src/de/lighti/clipper/PolyTree.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class PolyTree extends PolyNode { 7 | private final List allPolys = new ArrayList(); 8 | 9 | public void Clear() { 10 | allPolys.clear(); 11 | childs.clear(); 12 | } 13 | 14 | public List getAllPolys() { 15 | return allPolys; 16 | } 17 | 18 | public PolyNode getFirst() { 19 | if (!childs.isEmpty()) { 20 | return childs.get( 0 ); 21 | } 22 | else { 23 | return null; 24 | } 25 | } 26 | 27 | public int getTotalSize() { 28 | int result = allPolys.size(); 29 | //with negative offsets, ignore the hidden outer polygon ... 30 | if (result > 0 && childs.get( 0 ) != allPolys.get( 0 )) { 31 | result--; 32 | } 33 | return result; 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Clipper/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | de.lighti 5 | Clipper 6 | 6.4.2 7 | A Polygon clipper for Java 8 | Polygon Clipper is a library to execute various boolean operations (Union, Difference, XOR, etc.) on arbitrary 2D polygons, e.g. calculate the area in which two polygons overlap. It comes with two Demo applications, one for the console and one using a Swing based GUI. 9 | 10 | src 11 | 12 | 13 | maven-compiler-plugin 14 | 3.5.1 15 | 16 | 1.8 17 | 1.8 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Clipper Console/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | de.lighti 4 | ClipperConsole 5 | 6.4.2 6 | Clipper Console Application 7 | A command line tool to read and process polygon files 8 | 9 | src 10 | 11 | 12 | maven-compiler-plugin 13 | 3.5.1 14 | 15 | 1.8 16 | 1.8 17 | 18 | 19 | 20 | 21 | 22 | 23 | de.lighti 24 | Clipper 25 | 6.4.2 26 | 27 | 28 | -------------------------------------------------------------------------------- /Clipper GUI/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | de.lighti 4 | ClipperGUI 5 | 6.4.2 6 | Clipper GUI Application 7 | A swing-based application to visualize the various operations of the Clipper library 8 | 9 | src 10 | 11 | 12 | maven-compiler-plugin 13 | 3.5.1 14 | 15 | 1.8 16 | 1.8 17 | 18 | 19 | 20 | 21 | 22 | 23 | de.lighti 24 | Clipper 25 | 6.4.2 26 | 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Deprecation notice Oct 2024:** I wrote this code many years ago and stopped maintaining it a long time ago. But since it still seems very popular, and I get the occasional email about it: 2 | 3 | > [!NOTE] 4 | > There exists a version 2.0 of the orignal library, and someone already made a [Java port](https://github.com/micycle1/Clipper2-java/) of that. 5 | 6 | Original readme below: 7 | 8 | # clipper-java 9 | A Polygon clipper for Java 10 | 11 | Polygon Clipper is a library to execute various boolean operations (Union, Difference, XOR, etc.) on arbitrary 2D polygons, e.g. calculate the area in which two polygons overlap. It comes with two Demo applications, one for the console and one using a Swing based GUI. 12 | 13 | It’s a 1:1 Java portation of the Clipper project developed by Angus Johnson, which as an implementation of the algorithm proposed by Bala R. Vatti. I just converted the code to Java 8, based on his C# Clipper version 6.4.2. If you’re interested in the documentation or have questions regarding the algorithm, please head over to his homepage. However, feel free to contact me if you found a bug or have a question regarding the Java version. 14 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Clipper/src/de/lighti/clipper/Clipper.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper; 2 | 3 | import de.lighti.clipper.Point.LongPoint; 4 | 5 | public interface Clipper { 6 | enum ClipType { 7 | INTERSECTION, UNION, DIFFERENCE, XOR 8 | } 9 | 10 | enum Direction { 11 | RIGHT_TO_LEFT, LEFT_TO_RIGHT 12 | } 13 | 14 | enum EndType { 15 | CLOSED_POLYGON, CLOSED_LINE, OPEN_BUTT, OPEN_SQUARE, OPEN_ROUND 16 | } 17 | 18 | enum JoinType { 19 | SQUARE, ROUND, MITER 20 | } 21 | 22 | enum PolyFillType { 23 | EVEN_ODD, NON_ZERO, POSITIVE, NEGATIVE 24 | } 25 | 26 | enum PolyType { 27 | SUBJECT, CLIP 28 | } 29 | 30 | interface ZFillCallback { 31 | void zFill(LongPoint bot1, LongPoint top1, LongPoint bot2, LongPoint top2, LongPoint pt); 32 | } 33 | 34 | //InitOptions that can be passed to the constructor ... 35 | int REVERSE_SOLUTION = 1; 36 | 37 | int STRICTLY_SIMPLE = 2; 38 | 39 | int PRESERVE_COLINEAR = 4; 40 | 41 | boolean addPath(Path pg, PolyType polyType, boolean Closed); 42 | 43 | boolean addPaths(Paths ppg, PolyType polyType, boolean closed); 44 | 45 | void clear(); 46 | 47 | boolean execute(ClipType clipType, Paths solution); 48 | 49 | boolean execute(ClipType clipType, Paths solution, PolyFillType subjFillType, PolyFillType clipFillType); 50 | 51 | boolean execute(ClipType clipType, PolyTree polytree); 52 | 53 | boolean execute(ClipType clipType, PolyTree polytree, PolyFillType subjFillType, PolyFillType clipFillType); 54 | } 55 | -------------------------------------------------------------------------------- /Clipper/src/de/lighti/clipper/PolyNode.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper; 2 | 3 | import de.lighti.clipper.Clipper.EndType; 4 | import de.lighti.clipper.Clipper.JoinType; 5 | import de.lighti.clipper.Point.LongPoint; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | public class PolyNode { 12 | 13 | enum NodeType { 14 | ANY, OPEN, CLOSED 15 | } 16 | 17 | private PolyNode parent; 18 | private final Path polygon = new Path(); 19 | private int index; 20 | private JoinType joinType; 21 | private EndType endType; 22 | final List childs = new ArrayList<>(); 23 | private boolean isOpen; 24 | 25 | public void addChild( PolyNode child ) { 26 | final int cnt = childs.size(); 27 | childs.add( child ); 28 | child.parent = this; 29 | child.index = cnt; 30 | } 31 | 32 | public int getChildCount() { 33 | return childs.size(); 34 | } 35 | 36 | public List getChilds() { 37 | return Collections.unmodifiableList( childs ); 38 | } 39 | 40 | public List getContour() { 41 | return polygon; 42 | } 43 | 44 | public EndType getEndType() { 45 | return endType; 46 | } 47 | 48 | public JoinType getJoinType() { 49 | return joinType; 50 | } 51 | 52 | public PolyNode getNext() { 53 | if (!childs.isEmpty()) { 54 | return childs.get( 0 ); 55 | } 56 | else { 57 | return getNextSiblingUp(); 58 | } 59 | } 60 | 61 | private PolyNode getNextSiblingUp() { 62 | if (parent == null) { 63 | return null; 64 | } 65 | else if (index == parent.childs.size() - 1) { 66 | return parent.getNextSiblingUp(); 67 | } 68 | else { 69 | return parent.childs.get( index + 1 ); 70 | } 71 | } 72 | 73 | public PolyNode getParent() { 74 | return parent; 75 | } 76 | 77 | public Path getPolygon() { 78 | return polygon; 79 | } 80 | 81 | public boolean isHole() { 82 | return isHoleNode(); 83 | } 84 | 85 | private boolean isHoleNode() { 86 | boolean result = true; 87 | PolyNode node = parent; 88 | while (node != null) { 89 | result = !result; 90 | node = node.parent; 91 | } 92 | return result; 93 | } 94 | 95 | public boolean isOpen() { 96 | return isOpen; 97 | } 98 | 99 | public void setEndType( EndType value ) { 100 | endType = value; 101 | } 102 | 103 | public void setJoinType( JoinType value ) { 104 | joinType = value; 105 | } 106 | 107 | public void setOpen( boolean isOpen ) { 108 | this.isOpen = isOpen; 109 | } 110 | 111 | public void setParent( PolyNode n ) { 112 | parent = n; 113 | 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /Clipper GUI/src/de/lighti/clipper/gui/StatusBar.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper.gui; 2 | 3 | import java.awt.BorderLayout; 4 | import java.awt.Color; 5 | import java.awt.Component; 6 | import java.awt.Dimension; 7 | import java.awt.Graphics; 8 | import java.awt.SystemColor; 9 | 10 | import javax.swing.Icon; 11 | import javax.swing.JLabel; 12 | import javax.swing.JPanel; 13 | 14 | public class StatusBar extends JPanel { 15 | static class AngledLinesWindowsCornerIcon implements Icon { 16 | private static final Color WHITE_LINE_COLOR = new Color( 255, 255, 255 ); 17 | 18 | private static final Color GRAY_LINE_COLOR = new Color( 172, 168, 153 ); 19 | private static final int WIDTH = 13; 20 | 21 | private static final int HEIGHT = 13; 22 | 23 | @Override 24 | public int getIconHeight() { 25 | return WIDTH; 26 | } 27 | 28 | @Override 29 | public int getIconWidth() { 30 | return HEIGHT; 31 | } 32 | 33 | @Override 34 | public void paintIcon( Component c, Graphics g, int x, int y ) { 35 | 36 | g.setColor( WHITE_LINE_COLOR ); 37 | g.drawLine( 0, 12, 12, 0 ); 38 | g.drawLine( 5, 12, 12, 5 ); 39 | g.drawLine( 10, 12, 12, 10 ); 40 | 41 | g.setColor( GRAY_LINE_COLOR ); 42 | g.drawLine( 1, 12, 12, 1 ); 43 | g.drawLine( 2, 12, 12, 2 ); 44 | g.drawLine( 3, 12, 12, 3 ); 45 | 46 | g.drawLine( 6, 12, 12, 6 ); 47 | g.drawLine( 7, 12, 12, 7 ); 48 | g.drawLine( 8, 12, 12, 8 ); 49 | 50 | g.drawLine( 11, 12, 12, 11 ); 51 | g.drawLine( 12, 12, 12, 12 ); 52 | 53 | } 54 | } 55 | 56 | private final JLabel text; 57 | 58 | /** 59 | * 60 | */ 61 | private static final long serialVersionUID = 3434051407308227123L; 62 | 63 | public StatusBar() { 64 | setLayout( new BorderLayout() ); 65 | setPreferredSize( new Dimension( 10, 23 ) ); 66 | 67 | final JPanel rightPanel = new JPanel( new BorderLayout() ); 68 | rightPanel.add( new JLabel( new AngledLinesWindowsCornerIcon() ), BorderLayout.SOUTH ); 69 | rightPanel.setOpaque( false ); 70 | 71 | text = new JLabel(); 72 | add( text, BorderLayout.WEST ); 73 | add( rightPanel, BorderLayout.EAST ); 74 | setBackground( SystemColor.control ); 75 | } 76 | 77 | @Override 78 | protected void paintComponent( Graphics g ) { 79 | super.paintComponent( g ); 80 | 81 | int y = 0; 82 | g.setColor( new Color( 156, 154, 140 ) ); 83 | g.drawLine( 0, y, getWidth(), y ); 84 | y++; 85 | g.setColor( new Color( 196, 194, 183 ) ); 86 | g.drawLine( 0, y, getWidth(), y ); 87 | y++; 88 | g.setColor( new Color( 218, 215, 201 ) ); 89 | g.drawLine( 0, y, getWidth(), y ); 90 | y++; 91 | g.setColor( new Color( 233, 231, 217 ) ); 92 | g.drawLine( 0, y, getWidth(), y ); 93 | 94 | y = getHeight() - 3; 95 | g.setColor( new Color( 233, 232, 218 ) ); 96 | g.drawLine( 0, y, getWidth(), y ); 97 | y++; 98 | g.setColor( new Color( 233, 231, 216 ) ); 99 | g.drawLine( 0, y, getWidth(), y ); 100 | y = getHeight() - 1; 101 | g.setColor( new Color( 221, 221, 220 ) ); 102 | g.drawLine( 0, y, getWidth(), y ); 103 | 104 | } 105 | 106 | public void setText( String text ) { 107 | this.text.setText( text ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Clipper/src/de/lighti/clipper/Paths.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * A pure convenience class to avoid writing List everywhere. 7 | * 8 | * @author Tobias Mahlmann 9 | * 10 | */ 11 | public class Paths extends ArrayList { 12 | 13 | public static Paths closedPathsFromPolyTree(PolyTree polytree ) { 14 | final Paths result = new Paths(); 15 | // result.Capacity = polytree.Total; 16 | result.addPolyNode( polytree, PolyNode.NodeType.CLOSED ); 17 | return result; 18 | } 19 | 20 | public static Paths makePolyTreeToPaths(PolyTree polytree ) { 21 | 22 | final Paths result = new Paths(); 23 | // result.Capacity = polytree.Total; 24 | result.addPolyNode( polytree, PolyNode.NodeType.ANY ); 25 | return result; 26 | } 27 | 28 | public static Paths openPathsFromPolyTree(PolyTree polytree ) { 29 | final Paths result = new Paths(); 30 | // result.Capacity = polytree.ChildCount; 31 | for (final PolyNode c : polytree.getChilds()) { 32 | if (c.isOpen()) { 33 | result.add( c.getPolygon() ); 34 | } 35 | } 36 | return result; 37 | } 38 | 39 | /** 40 | * 41 | */ 42 | private static final long serialVersionUID = 1910552127810480852L; 43 | 44 | public Paths() { 45 | super(); 46 | } 47 | 48 | public Paths( int initialCapacity ) { 49 | super( initialCapacity ); 50 | } 51 | 52 | public void addPolyNode(PolyNode polynode, PolyNode.NodeType nt ) { 53 | boolean match = true; 54 | switch (nt) { 55 | case OPEN: 56 | return; 57 | case CLOSED: 58 | match = !polynode.isOpen(); 59 | break; 60 | default: 61 | break; 62 | } 63 | 64 | if (polynode.getPolygon().size() > 0 && match) { 65 | add( polynode.getPolygon() ); 66 | } 67 | for (final PolyNode pn : polynode.getChilds()) { 68 | addPolyNode( pn, nt ); 69 | } 70 | } 71 | 72 | public Paths cleanPolygons() { 73 | return cleanPolygons( 1.415 ); 74 | } 75 | 76 | public Paths cleanPolygons(double distance ) { 77 | final Paths result = new Paths( size() ); 78 | for (int i = 0; i < size(); i++) { 79 | result.add( get( i ).cleanPolygon( distance ) ); 80 | } 81 | return result; 82 | } 83 | 84 | public LongRect getBounds() { 85 | 86 | int i = 0; 87 | final int cnt = size(); 88 | final LongRect result = new LongRect(); 89 | while (i < cnt && get( i ).isEmpty()) { 90 | i++; 91 | } 92 | if (i == cnt) { 93 | return result; 94 | } 95 | 96 | result.left = get( i ).get( 0 ).getX(); 97 | result.right = result.left; 98 | result.top = get( i ).get( 0 ).getY(); 99 | result.bottom = result.top; 100 | for (; i < cnt; i++) { 101 | for (int j = 0; j < get( i ).size(); j++) { 102 | if (get( i ).get( j ).getX() < result.left) { 103 | result.left = get( i ).get( j ).getX(); 104 | } 105 | else if (get( i ).get( j ).getX() > result.right) { 106 | result.right = get( i ).get( j ).getX(); 107 | } 108 | if (get( i ).get( j ).getY() < result.top) { 109 | result.top = get( i ).get( j ).getY(); 110 | } 111 | else if (get( i ).get( j ).getY() > result.bottom) { 112 | result.bottom = get( i ).get( j ).getY(); 113 | } 114 | } 115 | } 116 | return result; 117 | } 118 | 119 | public void reversePaths() { 120 | for (final Path poly : this) { 121 | poly.reverse(); 122 | } 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /Clipper GUI/src/de/lighti/clipper/gui/LittleEndianDataInputStream.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper.gui; 2 | 3 | import java.io.DataInput; 4 | import java.io.EOFException; 5 | import java.io.FilterInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | 9 | /** 10 | * Works like DataInputStream but reverses the bits (big/little endian) when reading 11 | * values 12 | * 13 | * @author Tobias Mahlmann 14 | * 15 | */ 16 | public class LittleEndianDataInputStream extends FilterInputStream implements DataInput { 17 | private final byte w[]; 18 | 19 | public LittleEndianDataInputStream( InputStream in ) { 20 | super( in ); 21 | 22 | w = new byte[8]; 23 | } 24 | 25 | @Override 26 | public boolean readBoolean() throws IOException { 27 | final int ch = in.read(); 28 | if (ch < 0) { 29 | throw new EOFException(); 30 | } 31 | return ch != 0; 32 | } 33 | 34 | @Override 35 | public byte readByte() throws IOException { 36 | final int ch = in.read(); 37 | if (ch < 0) { 38 | throw new EOFException(); 39 | } 40 | return (byte) ch; 41 | } 42 | 43 | /** 44 | * like DataInputStream.readChar except little endian. 45 | */ 46 | @Override 47 | public final char readChar() throws IOException { 48 | readFully( w, 0, 2 ); 49 | return (char) ((w[1] & 0xff) << 8 | w[0] & 0xff); 50 | } 51 | 52 | @Override 53 | public final double readDouble() throws IOException { 54 | return Double.longBitsToDouble( readLong() ); 55 | } 56 | 57 | @Override 58 | public final float readFloat() throws IOException { 59 | return Float.intBitsToFloat( readInt() ); 60 | } 61 | 62 | @Override 63 | public void readFully( byte[] b ) throws IOException { 64 | readFully( b, 0, b.length ); 65 | } 66 | 67 | @Override 68 | public void readFully( byte[] b, int off, int len ) throws IOException { 69 | if (len < 0) { 70 | throw new IndexOutOfBoundsException(); 71 | } 72 | int n = 0; 73 | while (n < len) { 74 | final int count = in.read( b, off + n, len - n ); 75 | if (count < 0) { 76 | throw new EOFException(); 77 | } 78 | n += count; 79 | } 80 | } 81 | 82 | /** 83 | * like DataInputStream.readInt except little endian. 84 | */ 85 | @Override 86 | public final int readInt() throws IOException { 87 | readFully( w, 0, 4 ); 88 | return w[3] << 24 | (w[2] & 0xff) << 16 | (w[1] & 0xff) << 8 | w[0] & 0xff; 89 | } 90 | 91 | @Override 92 | public String readLine() throws IOException { 93 | throw new UnsupportedOperationException(); 94 | } 95 | 96 | /** 97 | * like DataInputStream.readLong except little endian. 98 | */ 99 | @Override 100 | public final long readLong() throws IOException { 101 | readFully( w, 0, 8 ); 102 | return (long) w[7] << 56 | (long) (w[6] & 0xff) << 48 | (long) (w[5] & 0xff) << 40 | (long) (w[4] & 0xff) << 32 | (long) (w[3] & 0xff) << 24 103 | | (long) (w[2] & 0xff) << 16 | (long) (w[1] & 0xff) << 8 | w[0] & 0xff; 104 | } 105 | 106 | @Override 107 | public final short readShort() throws IOException { 108 | readFully( w, 0, 2 ); 109 | return (short) ((w[1] & 0xff) << 8 | w[0] & 0xff); 110 | } 111 | 112 | @Override 113 | public int readUnsignedByte() throws IOException { 114 | final int ch = in.read(); 115 | if (ch < 0) { 116 | throw new EOFException(); 117 | } 118 | return ch; 119 | } 120 | 121 | @Override 122 | public final int readUnsignedShort() throws IOException { 123 | readFully( w, 0, 2 ); 124 | return (w[1] & 0xff) << 8 | w[0] & 0xff; 125 | } 126 | 127 | @Override 128 | public String readUTF() throws IOException { 129 | throw new UnsupportedOperationException(); 130 | } 131 | 132 | @Override 133 | public int skipBytes( int n ) throws IOException { 134 | int total = 0; 135 | int cur = 0; 136 | 137 | while (total < n && (cur = (int) in.skip( n - total )) > 0) { 138 | total += cur; 139 | } 140 | 141 | return total; 142 | } 143 | 144 | } -------------------------------------------------------------------------------- /Clipper/src/de/lighti/clipper/Point.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper; 2 | 3 | import java.util.Comparator; 4 | 5 | public abstract class Point> { 6 | public static class DoublePoint extends Point { 7 | public DoublePoint() { 8 | this( 0, 0 ); 9 | } 10 | 11 | public DoublePoint( double x, double y ) { 12 | this( x, y, 0 ); 13 | } 14 | 15 | public DoublePoint( double x, double y, double z ) { 16 | super( x, y, z ); 17 | } 18 | 19 | public DoublePoint( DoublePoint other ) { 20 | super( other ); 21 | } 22 | 23 | public double getX() { 24 | return x; 25 | } 26 | 27 | public double getY() { 28 | return y; 29 | } 30 | 31 | public double getZ() { 32 | return z; 33 | } 34 | } 35 | 36 | public static class LongPoint extends Point { 37 | public static double getDeltaX(LongPoint pt1, LongPoint pt2 ) { 38 | if (pt1.getY() == pt2.getY()) { 39 | return Edge.HORIZONTAL; 40 | } 41 | else { 42 | return (double) (pt2.getX() - pt1.getX()) / (pt2.getY() - pt1.getY()); 43 | } 44 | } 45 | 46 | public LongPoint() { 47 | this( 0, 0 ); 48 | } 49 | 50 | public LongPoint( long x, long y ) { 51 | this( x, y, 0 ); 52 | } 53 | 54 | public LongPoint( long x, long y, long z ) { 55 | super( x, y, z ); 56 | } 57 | 58 | public LongPoint( LongPoint other ) { 59 | super( other ); 60 | } 61 | 62 | public long getX() { 63 | return x; 64 | } 65 | 66 | public long getY() { 67 | return y; 68 | } 69 | 70 | public long getZ() { 71 | return z; 72 | } 73 | } 74 | 75 | private static class NumberComparator> implements Comparator { 76 | 77 | @Override 78 | public int compare( T a, T b ) throws ClassCastException { 79 | return a.compareTo( b ); 80 | } 81 | } 82 | 83 | static boolean arePointsClose(Point pt1, Point pt2, double distSqrd ) { 84 | final double dx = pt1.x.doubleValue() - pt2.x.doubleValue(); 85 | final double dy = pt1.y.doubleValue() - pt2.y.doubleValue(); 86 | return dx * dx + dy * dy <= distSqrd; 87 | } 88 | 89 | static double distanceFromLineSqrd(Point pt, Point ln1, Point ln2 ) { 90 | //The equation of a line in general form (Ax + By + C = 0) 91 | //given 2 points (x¹,y¹) & (x²,y²) is ... 92 | //(y¹ - y²)x + (x² - x¹)y + (y² - y¹)x¹ - (x² - x¹)y¹ = 0 93 | //A = (y¹ - y²); B = (x² - x¹); C = (y² - y¹)x¹ - (x² - x¹)y¹ 94 | //perpendicular distance of point (x³,y³) = (Ax³ + By³ + C)/Sqrt(A² + B²) 95 | //see http://en.wikipedia.org/wiki/Perpendicular_distance 96 | final double A = ln1.y.doubleValue() - ln2.y.doubleValue(); 97 | final double B = ln2.x.doubleValue() - ln1.x.doubleValue(); 98 | double C = A * ln1.x.doubleValue() + B * ln1.y.doubleValue(); 99 | C = A * pt.x.doubleValue() + B * pt.y.doubleValue() - C; 100 | return C * C / (A * A + B * B); 101 | } 102 | 103 | static DoublePoint getUnitNormal( LongPoint pt1, LongPoint pt2 ) { 104 | double dx = pt2.x - pt1.x; 105 | double dy = pt2.y - pt1.y; 106 | if (dx == 0 && dy == 0) { 107 | return new DoublePoint(); 108 | } 109 | 110 | final double f = 1 * 1.0 / Math.sqrt( dx * dx + dy * dy ); 111 | dx *= f; 112 | dy *= f; 113 | 114 | return new DoublePoint( dy, -dx ); 115 | } 116 | 117 | protected static boolean isPt2BetweenPt1AndPt3( LongPoint pt1, LongPoint pt2, LongPoint pt3 ) { 118 | if (pt1.equals( pt3 ) || pt1.equals( pt2 ) || pt3.equals( pt2 )) { 119 | return false; 120 | } 121 | else if (pt1.x != pt3.x) { 122 | return pt2.x > pt1.x == pt2.x < pt3.x; 123 | } 124 | else { 125 | return pt2.y > pt1.y == pt2.y < pt3.y; 126 | } 127 | } 128 | 129 | protected static boolean slopesEqual( LongPoint pt1, LongPoint pt2, LongPoint pt3 ) { 130 | return (pt1.y - pt2.y) * (pt2.x - pt3.x) - (pt1.x - pt2.x) * (pt2.y - pt3.y) == 0; 131 | } 132 | 133 | protected static boolean slopesEqual( LongPoint pt1, LongPoint pt2, LongPoint pt3, LongPoint pt4 ) { 134 | return (pt1.y - pt2.y) * (pt3.x - pt4.x) - (pt1.x - pt2.x) * (pt3.y - pt4.y) == 0; 135 | } 136 | 137 | static boolean slopesNearCollinear( LongPoint pt1, LongPoint pt2, LongPoint pt3, double distSqrd ) { 138 | //this function is more accurate when the point that's GEOMETRICALLY 139 | //between the other 2 points is the one that's tested for distance. 140 | //nb: with 'spikes', either pt1 or pt3 is geometrically between the other pts 141 | if (Math.abs( pt1.x - pt2.x ) > Math.abs( pt1.y - pt2.y )) { 142 | if (pt1.x > pt2.x == pt1.x < pt3.x) { 143 | return distanceFromLineSqrd( pt1, pt2, pt3 ) < distSqrd; 144 | } 145 | else if (pt2.x > pt1.x == pt2.x < pt3.x) { 146 | return distanceFromLineSqrd( pt2, pt1, pt3 ) < distSqrd; 147 | } 148 | else { 149 | return distanceFromLineSqrd( pt3, pt1, pt2 ) < distSqrd; 150 | } 151 | } 152 | else { 153 | if (pt1.y > pt2.y == pt1.y < pt3.y) { 154 | return distanceFromLineSqrd( pt1, pt2, pt3 ) < distSqrd; 155 | } 156 | else if (pt2.y > pt1.y == pt2.y < pt3.y) { 157 | return distanceFromLineSqrd( pt2, pt1, pt3 ) < distSqrd; 158 | } 159 | else { 160 | return distanceFromLineSqrd( pt3, pt1, pt2 ) < distSqrd; 161 | } 162 | } 163 | } 164 | 165 | private final static NumberComparator NUMBER_COMPARATOR = new NumberComparator(); 166 | 167 | protected T x; 168 | 169 | protected T y; 170 | 171 | protected T z; 172 | 173 | protected Point( Point pt ) { 174 | this( pt.x, pt.y, pt.z ); 175 | } 176 | 177 | protected Point( T x, T y, T z ) { 178 | this.x = x; 179 | this.y = y; 180 | this.z = z; 181 | } 182 | 183 | @Override 184 | public boolean equals( Object obj ) { 185 | if (obj == null) { 186 | return false; 187 | } 188 | if (obj instanceof Point) { 189 | final Point a = (Point) obj; 190 | return NUMBER_COMPARATOR.compare( x, a.x ) == 0 && NUMBER_COMPARATOR.compare( y, a.y ) == 0; 191 | } 192 | else { 193 | return false; 194 | } 195 | } 196 | 197 | public void set( Point other ) { 198 | x = other.x; 199 | y = other.y; 200 | z = other.z; 201 | } 202 | 203 | public void setX( T x ) { 204 | this.x = x; 205 | } 206 | 207 | public void setY( T y ) { 208 | this.y = y; 209 | } 210 | 211 | public void setZ( T z ) { 212 | this.z = z; 213 | } 214 | 215 | @Override 216 | public String toString() { 217 | return "Point [x=" + x + ", y=" + y + ", z=" + z + "]"; 218 | } 219 | 220 | }// end struct IntPoint -------------------------------------------------------------------------------- /Clipper Console/src/de/lighti/clipper/console/SVGBuilder.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper.console; 2 | 3 | import java.awt.Color; 4 | import java.io.BufferedWriter; 5 | import java.io.FileWriter; 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import de.lighti.clipper.Clipper.PolyFillType; 11 | import de.lighti.clipper.LongRect; 12 | import de.lighti.clipper.Path; 13 | import de.lighti.clipper.Paths; 14 | import de.lighti.clipper.Point.LongPoint; 15 | 16 | //a very simple class that builds an SVG file with any number of 17 | //polygons of the specified formats ... 18 | class SVGBuilder { 19 | 20 | public class PolyInfo { 21 | public Paths polygons; 22 | public StyleInfo si; 23 | } 24 | 25 | public class StyleInfo { 26 | public PolyFillType pft; 27 | public Color brushClr; 28 | public Color penClr; 29 | public double penWidth; 30 | public int[] dashArray; 31 | public boolean showCoords; 32 | 33 | public StyleInfo() { 34 | pft = PolyFillType.NON_ZERO; 35 | brushClr = Color.RED; 36 | dashArray = null; 37 | penClr = Color.BLACK; 38 | penWidth = 0.8; 39 | showCoords = false; 40 | } 41 | 42 | public StyleInfo Clone() { 43 | final StyleInfo si = new StyleInfo(); 44 | si.pft = pft; 45 | si.brushClr = brushClr; 46 | si.dashArray = dashArray; 47 | si.penClr = penClr; 48 | si.penWidth = penWidth; 49 | si.showCoords = showCoords; 50 | return si; 51 | } 52 | } 53 | 54 | //////////////////////////////////////////////// 55 | 56 | //////////////////////////////////////////////// 57 | 58 | //////////////////////////////////////////////// 59 | public StyleInfo style; 60 | 61 | private final List PolyInfoList; 62 | 63 | //////////////////////////////////////////////// 64 | private final static String SVG_HEADER = "\n" + "\n\n" + "\n\n"; 67 | 68 | private final static String SVG_PATH_FORMAT = "\"%n style=\"fill:#%s;" + " fill-opacity:%.2f; fill-rule:%s; stroke:#%s;" 69 | + " stroke-opacity:%.2f; stroke-width:%.2f;\"/>%n%n"; 70 | 71 | //////////////////////////////////////////////// 72 | 73 | public SVGBuilder() { 74 | PolyInfoList = new ArrayList(); 75 | style = new StyleInfo(); 76 | } 77 | 78 | //////////////////////////////////////////////// 79 | public void addPaths( Paths poly ) { 80 | if (poly.size() == 0) { 81 | return; 82 | } 83 | final PolyInfo pi = new PolyInfo(); 84 | pi.polygons = poly; 85 | pi.si = style.Clone(); 86 | PolyInfoList.add( pi ); 87 | } 88 | 89 | public boolean saveToFile( String filename, double scale ) throws IOException { 90 | return SaveToFile( filename, scale, 10 ); 91 | } 92 | 93 | public boolean SaveToFile( String filename ) throws IOException { 94 | return saveToFile( filename, 1 ); 95 | } 96 | 97 | public boolean SaveToFile( String filename, double scale, int margin ) throws IOException { 98 | if (scale == 0) { 99 | scale = 1.0; 100 | } 101 | if (margin < 0) { 102 | margin = 0; 103 | } 104 | 105 | //calculate the bounding rect ... 106 | int i = 0, j = 0; 107 | while (i < PolyInfoList.size()) { 108 | j = 0; 109 | while (j < PolyInfoList.get( i ).polygons.size() && PolyInfoList.get( i ).polygons.get( j ).size() == 0) { 110 | j++; 111 | } 112 | if (j < PolyInfoList.get( i ).polygons.size()) { 113 | break; 114 | } 115 | i++; 116 | } 117 | if (i == PolyInfoList.size()) { 118 | return false; 119 | } 120 | final LongRect rec = new LongRect(); 121 | rec.left = PolyInfoList.get( i ).polygons.get( j ).get( 0 ).getX(); 122 | rec.right = rec.left; 123 | rec.top = PolyInfoList.get( 0 ).polygons.get( j ).get( 0 ).getY(); 124 | rec.bottom = rec.top; 125 | 126 | for (; i < PolyInfoList.size(); i++) { 127 | for (final Path pg : PolyInfoList.get( i ).polygons) { 128 | for (final LongPoint pt : pg) { 129 | if (pt.getX() < rec.left) { 130 | rec.left = pt.getX(); 131 | } 132 | else if (pt.getX() > rec.right) { 133 | rec.right = pt.getX(); 134 | } 135 | if (pt.getY() < rec.top) { 136 | rec.top = pt.getY(); 137 | } 138 | else if (pt.getY() > rec.bottom) { 139 | rec.bottom = pt.getY(); 140 | } 141 | } 142 | } 143 | } 144 | 145 | rec.left = (int) (rec.left * scale); 146 | rec.top = (int) (rec.top * scale); 147 | rec.right = (int) (rec.right * scale); 148 | rec.bottom = (int) (rec.bottom * scale); 149 | final long offsetX = -rec.left + margin; 150 | final long offsetY = -rec.top + margin; 151 | 152 | final BufferedWriter writer = new BufferedWriter( new FileWriter( filename ) ); 153 | try { 154 | writer.write( String.format( SVG_HEADER, rec.right - rec.left + margin * 2, rec.bottom - rec.top + margin * 2, rec.right - rec.left + margin * 2, 155 | rec.bottom - rec.top + margin * 2 ) ); 156 | 157 | for (final PolyInfo pi : PolyInfoList) { 158 | writer.write( " %n%n" ) ); 176 | for (final Path p : pi.polygons) { 177 | for (final LongPoint pt : p) { 178 | final long x = pt.getX(); 179 | final long y = pt.getY(); 180 | writer.write( String.format( "%d,%d\n", (int) (x * scale + offsetX), (int) (y * scale + offsetY), x, 181 | y ) ); 182 | 183 | } 184 | writer.write( String.format( "%n" ) ); 185 | } 186 | writer.write( String.format( "%n" ) ); 187 | } 188 | } 189 | writer.write( String.format( "%n" ) ); 190 | writer.close(); 191 | return true; 192 | 193 | } 194 | finally { 195 | writer.close(); 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /Clipper GUI/src/de/lighti/clipper/gui/SVGBuilder.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper.gui; 2 | 3 | import java.awt.Color; 4 | import java.io.BufferedWriter; 5 | import java.io.FileWriter; 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Locale; 10 | 11 | import de.lighti.clipper.Clipper.PolyFillType; 12 | import de.lighti.clipper.LongRect; 13 | import de.lighti.clipper.Path; 14 | import de.lighti.clipper.Paths; 15 | import de.lighti.clipper.Point.LongPoint; 16 | 17 | //a very simple class that builds an SVG file with any number of 18 | //polygons of the specified formats ... 19 | public class SVGBuilder { 20 | 21 | public class PolyInfo { 22 | public Path polygons; 23 | public StyleInfo si; 24 | } 25 | 26 | public class StyleInfo { 27 | public PolyFillType pft; 28 | public Color brushClr; 29 | public Color penClr; 30 | public double penWidth; 31 | public int[] dashArray; 32 | public boolean showCoords; 33 | 34 | public StyleInfo() { 35 | pft = PolyFillType.NON_ZERO; 36 | brushClr = Color.WHITE; 37 | dashArray = null; 38 | penClr = Color.BLACK; 39 | penWidth = 0.8; 40 | showCoords = false; 41 | } 42 | 43 | @Override 44 | public StyleInfo clone() { 45 | final StyleInfo si = new StyleInfo(); 46 | si.pft = pft; 47 | si.brushClr = brushClr; 48 | si.dashArray = dashArray; 49 | si.penClr = penClr; 50 | si.penWidth = penWidth; 51 | si.showCoords = showCoords; 52 | return si; 53 | } 54 | } 55 | 56 | //////////////////////////////////////////////// 57 | 58 | //////////////////////////////////////////////// 59 | 60 | //////////////////////////////////////////////// 61 | public StyleInfo style; 62 | 63 | private final List PolyInfoList; 64 | 65 | //////////////////////////////////////////////// 66 | private final static String SVG_HEADER = "\n" + "\n\n" + "\n\n"; 69 | 70 | private final static String SVG_PATH_FORMAT = "\"%n style=\"fill:#%s;" + " fill-opacity:%.2f; fill-rule:%s; stroke:#%s;" 71 | + " stroke-opacity:%.2f; stroke-width:%.2f;\"/>%n%n"; 72 | 73 | //////////////////////////////////////////////// 74 | 75 | public SVGBuilder() { 76 | PolyInfoList = new ArrayList(); 77 | style = new StyleInfo(); 78 | } 79 | 80 | //////////////////////////////////////////////// 81 | public void addPaths( Paths poly ) { 82 | if (poly.size() == 0) { 83 | return; 84 | } 85 | for (final Path p : poly) { 86 | final PolyInfo pi = new PolyInfo(); 87 | pi.polygons = p; 88 | pi.si = style.clone(); 89 | PolyInfoList.add( pi ); 90 | } 91 | } 92 | 93 | public boolean saveToFile( String filename, double scale ) throws IOException { 94 | return SaveToFile( filename, scale, 10 ); 95 | } 96 | 97 | public boolean SaveToFile( String filename ) throws IOException { 98 | return saveToFile( filename, 1 ); 99 | } 100 | 101 | public boolean SaveToFile( String filename, double scale, int margin ) throws IOException { 102 | //Temporarily set the locale to US to avoid decimal confusion 103 | //in SVG. Apparently has to be #.## 104 | final Locale loc = Locale.getDefault(); 105 | Locale.setDefault( Locale.US ); 106 | 107 | if (scale == 0) { 108 | scale = 1.0; 109 | } 110 | if (margin < 0) { 111 | margin = 0; 112 | } 113 | 114 | //calculate the bounding rect ... 115 | int i = 0, j = 0; 116 | while (i < PolyInfoList.size()) { 117 | j = 0; 118 | while (j < PolyInfoList.get( i ).polygons.size() && PolyInfoList.get( i ).polygons.size() == 0) { 119 | j++; 120 | } 121 | if (j < PolyInfoList.get( i ).polygons.size()) { 122 | break; 123 | } 124 | i++; 125 | } 126 | if (i == PolyInfoList.size()) { 127 | return false; 128 | } 129 | final LongRect rec = new LongRect(); 130 | rec.left = PolyInfoList.get( i ).polygons.get( j ).getX(); 131 | rec.right = rec.left; 132 | rec.top = PolyInfoList.get( 0 ).polygons.get( j ).getY(); 133 | rec.bottom = rec.top; 134 | 135 | for (; i < PolyInfoList.size(); i++) { 136 | 137 | for (final LongPoint pt : PolyInfoList.get( i ).polygons) { 138 | if (pt.getX() < rec.left) { 139 | rec.left = pt.getX(); 140 | } 141 | else if (pt.getX() > rec.right) { 142 | rec.right = pt.getX(); 143 | } 144 | if (pt.getY() < rec.top) { 145 | rec.top = pt.getY(); 146 | } 147 | else if (pt.getY() > rec.bottom) { 148 | rec.bottom = pt.getY(); 149 | } 150 | } 151 | 152 | } 153 | 154 | rec.left = (int) (rec.left * scale); 155 | rec.top = (int) (rec.top * scale); 156 | rec.right = (int) (rec.right * scale); 157 | rec.bottom = (int) (rec.bottom * scale); 158 | final long offsetX = -rec.left + margin; 159 | final long offsetY = -rec.top + margin; 160 | 161 | final BufferedWriter writer = new BufferedWriter( new FileWriter( filename ) ); 162 | try { 163 | writer.write( String.format( SVG_HEADER, rec.right - rec.left + margin * 2, rec.bottom - rec.top + margin * 2, rec.right - rec.left + margin * 2, 164 | rec.bottom - rec.top + margin * 2 ) ); 165 | 166 | for (final PolyInfo pi : PolyInfoList) { 167 | writer.write( " %n%n" ) ); 184 | 185 | for (final LongPoint pt : p) { 186 | final long x = pt.getX(); 187 | final long y = pt.getY(); 188 | writer.write( String.format( "%d,%d\n", (int) (x * scale + offsetX), (int) (y * scale + offsetY), x, y ) ); 189 | 190 | } 191 | writer.write( String.format( "%n" ) ); 192 | 193 | writer.write( String.format( "%n" ) ); 194 | } 195 | } 196 | writer.write( String.format( "%n" ) ); 197 | writer.close(); 198 | return true; 199 | 200 | } 201 | finally { 202 | //Reset locale 203 | Locale.setDefault( loc ); 204 | writer.close(); 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /Clipper/src/de/lighti/clipper/Edge.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper; 2 | 3 | import de.lighti.clipper.Clipper.ClipType; 4 | import de.lighti.clipper.Clipper.Direction; 5 | import de.lighti.clipper.Clipper.PolyFillType; 6 | import de.lighti.clipper.Clipper.PolyType; 7 | import de.lighti.clipper.Point.LongPoint; 8 | 9 | import java.util.logging.Logger; 10 | 11 | class Edge { 12 | enum Side { 13 | LEFT, RIGHT 14 | } 15 | 16 | static boolean doesE2InsertBeforeE1(Edge e1, Edge e2 ) { 17 | if (e2.current.getX() == e1.current.getX()) { 18 | if (e2.top.getY() > e1.top.getY()) { 19 | return e2.top.getX() < topX( e1, e2.top.getY() ); 20 | } 21 | else { 22 | return e1.top.getX() > topX( e2, e1.top.getY() ); 23 | } 24 | } 25 | else { 26 | return e2.current.getX() < e1.current.getX(); 27 | } 28 | } 29 | 30 | static boolean slopesEqual(Edge e1, Edge e2 ) { 31 | return e1.getDelta().getY() * e2.getDelta().getX() == e1.getDelta().getX() * e2.getDelta().getY(); 32 | 33 | } 34 | 35 | static void swapPolyIndexes(Edge edge1, Edge edge2 ) { 36 | final int outIdx = edge1.outIdx; 37 | edge1.outIdx = edge2.outIdx; 38 | edge2.outIdx = outIdx; 39 | } 40 | 41 | static void swapSides(Edge edge1, Edge edge2 ) { 42 | final Edge.Side side = edge1.side; 43 | edge1.side = edge2.side; 44 | edge2.side = side; 45 | } 46 | 47 | static long topX(Edge edge, long currentY ) { 48 | if (currentY == edge.getTop().getY()) { 49 | return edge.getTop().getX(); 50 | } 51 | return edge.getBot().getX() + Math.round( edge.deltaX * (currentY - edge.getBot().getY()) ); 52 | } 53 | 54 | private final LongPoint bot; 55 | 56 | private final LongPoint current; //current (updated for every new scanbeam) 57 | 58 | private final LongPoint top; 59 | 60 | private final LongPoint delta; 61 | double deltaX; 62 | 63 | PolyType polyTyp; 64 | 65 | Edge.Side side; //side only refers to current side of solution poly 66 | 67 | int windDelta; //1 or -1 depending on winding direction 68 | 69 | int windCnt; 70 | int windCnt2; //winding count of the opposite polytype 71 | int outIdx; 72 | Edge next; 73 | Edge prev; 74 | Edge nextInLML; 75 | Edge nextInAEL; 76 | Edge prevInAEL; 77 | Edge nextInSEL; 78 | Edge prevInSEL; 79 | 80 | protected final static int SKIP = -2; 81 | 82 | protected final static int UNASSIGNED = -1; 83 | 84 | protected final static double HORIZONTAL = -3.4E+38; 85 | 86 | private final static Logger LOGGER = Logger.getLogger( Edge.class.getName() ); 87 | 88 | public Edge() { 89 | delta = new LongPoint(); 90 | top = new LongPoint(); 91 | bot = new LongPoint(); 92 | current = new LongPoint(); 93 | } 94 | 95 | public Edge findNextLocMin() { 96 | Edge e = this; 97 | Edge e2; 98 | for (;;) { 99 | while (!e.bot.equals( e.prev.bot ) || e.current.equals( e.top )) { 100 | e = e.next; 101 | } 102 | if (e.deltaX != Edge.HORIZONTAL && e.prev.deltaX != Edge.HORIZONTAL) { 103 | break; 104 | } 105 | while (e.prev.deltaX == Edge.HORIZONTAL) { 106 | e = e.prev; 107 | } 108 | e2 = e; 109 | while (e.deltaX == Edge.HORIZONTAL) { 110 | e = e.next; 111 | } 112 | if (e.top.getY() == e.prev.bot.getY()) { 113 | continue; //ie just an intermediate horz. 114 | } 115 | if (e2.prev.bot.getX() < e.bot.getX()) { 116 | e = e2; 117 | } 118 | break; 119 | } 120 | return e; 121 | } 122 | 123 | public LongPoint getBot() { 124 | return bot; 125 | } 126 | 127 | public LongPoint getCurrent() { 128 | return current; 129 | } 130 | 131 | public LongPoint getDelta() { 132 | return delta; 133 | } 134 | 135 | public Edge getMaximaPair() { 136 | if (next.top.equals( top ) && next.nextInLML == null) { 137 | return next; 138 | } 139 | else if (prev.top.equals( top ) && prev.nextInLML == null) { 140 | return prev; 141 | } 142 | return null; 143 | } 144 | 145 | Edge getMaximaPairEx() { 146 | //as above but returns null if MaxPair isn't in AEL (unless it's horizontal) 147 | Edge result = getMaximaPair(); 148 | if (result == null || result.outIdx == Edge.SKIP || 149 | ((result.nextInAEL == result.prevInAEL) && !result.isHorizontal())) { 150 | return null; 151 | } 152 | return result; 153 | } 154 | 155 | public Edge getNextInAEL(Direction direction ) { 156 | return direction == Direction.LEFT_TO_RIGHT ? nextInAEL : prevInAEL; 157 | } 158 | 159 | public LongPoint getTop() { 160 | return top; 161 | } 162 | 163 | public boolean isContributing(PolyFillType clipFillType, PolyFillType subjFillType, ClipType clipType ) { 164 | LOGGER.entering( Edge.class.getName(), "isContributing" ); 165 | 166 | PolyFillType pft, pft2; 167 | if (polyTyp == PolyType.SUBJECT) { 168 | pft = subjFillType; 169 | pft2 = clipFillType; 170 | } 171 | else { 172 | pft = clipFillType; 173 | pft2 = subjFillType; 174 | } 175 | 176 | switch (pft) { 177 | case EVEN_ODD: 178 | //return false if a subj line has been flagged as inside a subj polygon 179 | if (windDelta == 0 && windCnt != 1) { 180 | return false; 181 | } 182 | break; 183 | case NON_ZERO: 184 | if (Math.abs( windCnt ) != 1) { 185 | return false; 186 | } 187 | break; 188 | case POSITIVE: 189 | if (windCnt != 1) { 190 | return false; 191 | } 192 | break; 193 | default: //PolyFillType.pftNegative 194 | if (windCnt != -1) { 195 | return false; 196 | } 197 | break; 198 | } 199 | 200 | switch (clipType) { 201 | case INTERSECTION: 202 | switch (pft2) { 203 | case EVEN_ODD: 204 | case NON_ZERO: 205 | return windCnt2 != 0; 206 | case POSITIVE: 207 | return windCnt2 > 0; 208 | default: 209 | return windCnt2 < 0; 210 | } 211 | case UNION: 212 | switch (pft2) { 213 | case EVEN_ODD: 214 | case NON_ZERO: 215 | return windCnt2 == 0; 216 | case POSITIVE: 217 | return windCnt2 <= 0; 218 | default: 219 | return windCnt2 >= 0; 220 | } 221 | case DIFFERENCE: 222 | if (polyTyp == PolyType.SUBJECT) { 223 | switch (pft2) { 224 | case EVEN_ODD: 225 | case NON_ZERO: 226 | return windCnt2 == 0; 227 | case POSITIVE: 228 | return windCnt2 <= 0; 229 | default: 230 | return windCnt2 >= 0; 231 | } 232 | } 233 | else { 234 | switch (pft2) { 235 | case EVEN_ODD: 236 | case NON_ZERO: 237 | return windCnt2 != 0; 238 | case POSITIVE: 239 | return windCnt2 > 0; 240 | default: 241 | return windCnt2 < 0; 242 | } 243 | } 244 | case XOR: 245 | if (windDelta == 0) { 246 | switch (pft2) { 247 | case EVEN_ODD: 248 | case NON_ZERO: 249 | return windCnt2 == 0; 250 | case POSITIVE: 251 | return windCnt2 <= 0; 252 | default: 253 | return windCnt2 >= 0; 254 | } 255 | } 256 | else { 257 | return true; 258 | } 259 | } 260 | return true; 261 | } 262 | 263 | public boolean isEvenOddAltFillType(PolyFillType clipFillType, PolyFillType subjFillType ) { 264 | if (polyTyp == PolyType.SUBJECT) { 265 | return clipFillType == PolyFillType.EVEN_ODD; 266 | } 267 | else { 268 | return subjFillType == PolyFillType.EVEN_ODD; 269 | } 270 | } 271 | 272 | public boolean isEvenOddFillType(PolyFillType clipFillType, PolyFillType subjFillType ) { 273 | if (polyTyp == PolyType.SUBJECT) { 274 | return subjFillType == PolyFillType.EVEN_ODD; 275 | } 276 | else { 277 | return clipFillType == PolyFillType.EVEN_ODD; 278 | } 279 | } 280 | 281 | public boolean isHorizontal() { 282 | return delta.getY() == 0; 283 | } 284 | 285 | public boolean isIntermediate( double y ) { 286 | return top.getY() == y && nextInLML != null; 287 | } 288 | 289 | public boolean isMaxima( double Y ) { 290 | return top.getY() == Y && nextInLML == null; 291 | } 292 | 293 | public void reverseHorizontal() { 294 | //swap horizontal edges' top and bottom x's so they follow the natural 295 | //progression of the bounds - ie so their xbots will align with the 296 | //adjoining lower edge. [Helpful in the ProcessHorizontal() method.] 297 | long temp = top.getX(); 298 | top.setX( bot.getX() ); 299 | bot.setX( temp ); 300 | 301 | temp = top.getZ(); 302 | top.setZ( bot.getZ() ); 303 | bot.setZ( temp ); 304 | 305 | } 306 | 307 | public void setBot( LongPoint bot ) { 308 | this.bot.set( bot ); 309 | } 310 | 311 | public void setCurrent( LongPoint current ) { 312 | this.current.set( current ); 313 | } 314 | 315 | public void setTop( LongPoint top ) { 316 | this.top.set( top ); 317 | } 318 | 319 | @Override 320 | public String toString() { 321 | return "TEdge [Bot=" + bot + ", Curr=" + current + ", Top=" + top + ", Delta=" + delta + ", Dx=" + deltaX + ", PolyTyp=" + polyTyp + ", Side=" + side 322 | + ", WindDelta=" + windDelta + ", WindCnt=" + windCnt + ", WindCnt2=" + windCnt2 + ", OutIdx=" + outIdx + ", Next=" + next + ", Prev=" 323 | + prev + ", NextInLML=" + nextInLML + ", NextInAEL=" + nextInAEL + ", PrevInAEL=" + prevInAEL + ", NextInSEL=" + nextInSEL 324 | + ", PrevInSEL=" + prevInSEL + "]"; 325 | } 326 | 327 | public void updateDeltaX() { 328 | 329 | delta.setX( top.getX() - bot.getX() ); 330 | delta.setY( top.getY() - bot.getY() ); 331 | if (delta.getY() == 0) { 332 | deltaX = Edge.HORIZONTAL; 333 | } 334 | else { 335 | deltaX = (double) delta.getX() / delta.getY(); 336 | } 337 | } 338 | 339 | } 340 | -------------------------------------------------------------------------------- /Clipper GUI/src/de/lighti/clipper/gui/PolygonCanvas.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper.gui; 2 | 3 | import java.awt.Color; 4 | import java.awt.Graphics; 5 | import java.awt.Point; 6 | import java.awt.Polygon; 7 | import java.awt.event.MouseAdapter; 8 | import java.awt.event.MouseEvent; 9 | import java.awt.event.MouseWheelEvent; 10 | import java.awt.geom.Ellipse2D; 11 | import java.awt.geom.PathIterator; 12 | import java.io.FileInputStream; 13 | import java.io.IOException; 14 | import java.util.Random; 15 | 16 | import javax.swing.JPanel; 17 | 18 | import de.lighti.clipper.Clipper; 19 | import de.lighti.clipper.Clipper.ClipType; 20 | import de.lighti.clipper.Clipper.EndType; 21 | import de.lighti.clipper.Clipper.JoinType; 22 | import de.lighti.clipper.Clipper.PolyFillType; 23 | import de.lighti.clipper.Clipper.PolyType; 24 | import de.lighti.clipper.ClipperOffset; 25 | import de.lighti.clipper.DefaultClipper; 26 | import de.lighti.clipper.Path; 27 | import de.lighti.clipper.Paths; 28 | import de.lighti.clipper.Point.LongPoint; 29 | 30 | public class PolygonCanvas extends JPanel { 31 | 32 | private final Paths subjects = new Paths(); 33 | 34 | private final Paths clips = new Paths(); 35 | 36 | private final Paths solution = new Paths(); 37 | 38 | /** 39 | * 40 | */ 41 | private static final long serialVersionUID = 3171660079906158448L; 42 | 43 | private final static int MARGIN = 10; 44 | 45 | private ClipType clipType = ClipType.INTERSECTION; 46 | 47 | private PolyFillType fillType = PolyFillType.EVEN_ODD; 48 | private float zoom = 0.000001f; 49 | private int vertexCount = ClipperDialog.DEFAULT_VERTEX_COUNT; 50 | 51 | private final StatusBar statusBar; 52 | private float offset; 53 | 54 | private Point origin; 55 | private final Point cur; 56 | 57 | public PolygonCanvas( StatusBar statusBar ) { 58 | setBackground( Color.WHITE ); 59 | this.statusBar = statusBar; 60 | zoom = 1f; 61 | origin = new Point(); 62 | cur = new Point(); 63 | 64 | addMouseWheelListener( e -> { 65 | 66 | if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) { 67 | zoom += zoom * e.getUnitsToScroll() / 10f; 68 | statusBar.setText( "Zoomlevel: " + zoom ); 69 | PolygonCanvas.this.repaint(); 70 | } 71 | 72 | } ); 73 | addMouseListener( new MouseAdapter() { 74 | 75 | @Override 76 | public void mousePressed( MouseEvent e ) { 77 | origin = e.getPoint(); 78 | } 79 | } ); 80 | addMouseMotionListener( new MouseAdapter() { 81 | 82 | @Override 83 | public void mouseDragged( MouseEvent e ) { 84 | final Point p = e.getPoint(); 85 | 86 | cur.x = p.x; 87 | cur.y = p.y; 88 | repaint(); 89 | } 90 | } ); 91 | } 92 | 93 | private Polygon createPolygonFromPath( Path p ) { 94 | final int[] x = new int[p.size()]; 95 | final int[] y = new int[p.size()]; 96 | 97 | for (int i = p.size() - 1; i >= 0; i--) { 98 | final LongPoint ip = p.get( i ); 99 | x[i] = (int) (ip.getX() * zoom) + cur.x - origin.x; 100 | y[i] = (int) (ip.getY() * zoom) + cur.y - origin.y; 101 | } 102 | 103 | return new Polygon( x, y, p.size() ); 104 | } 105 | 106 | public void generateAustPlusRandomEllipses() { 107 | final int scale = 1; 108 | subjects.clear(); 109 | //load map of Australia from resource ... 110 | LittleEndianDataInputStream polyStream = null; 111 | try { 112 | 113 | polyStream = new LittleEndianDataInputStream( new FileInputStream( "aust.bin" ) ); 114 | 115 | final int polyCnt = polyStream.readInt(); 116 | for (int i = 0; i < polyCnt; ++i) { 117 | final int vertCnt = polyStream.readInt(); 118 | final Path pg = new Path( vertCnt ); 119 | for (int j = 0; j < vertCnt; ++j) { 120 | final float x = polyStream.readFloat() * scale; 121 | final float y = polyStream.readFloat() * scale; 122 | pg.add( new LongPoint( (int) x, (int) y ) ); 123 | } 124 | subjects.add( pg ); 125 | } 126 | } 127 | catch (final IOException e) { 128 | statusBar.setText( "Error: " + e.getMessage() ); 129 | } 130 | finally { 131 | try { 132 | if (polyStream != null) { 133 | polyStream.close(); 134 | } 135 | } 136 | catch (final IOException e) { 137 | statusBar.setText( "Error: " + e.getMessage() ); 138 | } 139 | } 140 | clips.clear(); 141 | final Random rand = new Random(); 142 | 143 | final int ellipse_size = 100, margin = 10; 144 | for (int i = 0; i < vertexCount; ++i) { 145 | final int w = getWidth() - ellipse_size - margin * 2; 146 | final int h = getHeight() - ellipse_size - margin * 2; 147 | 148 | final int x = rand.nextInt( w ) + margin; 149 | final int y = rand.nextInt( h ) + margin; 150 | final int size = rand.nextInt( ellipse_size - 20 ) + 20; 151 | final Ellipse2D path = new Ellipse2D.Float( x, y, size, size ); 152 | final PathIterator pit = path.getPathIterator( null, 0.1 ); 153 | final double[] coords = new double[6]; 154 | 155 | final Path clip = new Path(); 156 | while (!pit.isDone()) { 157 | final int type = pit.currentSegment( coords ); 158 | switch (type) { 159 | case PathIterator.SEG_LINETO: 160 | clip.add( new LongPoint( (int) (coords[0] * scale), (int) (coords[1] * scale) ) ); 161 | break; 162 | default: 163 | break; 164 | } 165 | 166 | pit.next(); 167 | } 168 | 169 | clips.add( clip ); 170 | updateSolution(); 171 | } 172 | 173 | } 174 | 175 | private LongPoint GenerateRandomPoint( int l, int t, int r, int b, Random rand ) { 176 | return new LongPoint( rand.nextInt( r ) + l, rand.nextInt( b ) + t ); 177 | } 178 | 179 | public void generateRandomPolygon() { 180 | 181 | final Random rand = new Random(); 182 | final int l = MARGIN; 183 | final int t = MARGIN; 184 | final int r = getWidth() - MARGIN; 185 | final int b = getHeight() - MARGIN; 186 | 187 | subjects.clear(); 188 | clips.clear(); 189 | 190 | final Path subj = new Path( vertexCount ); 191 | for (int i = 0; i < vertexCount; ++i) { 192 | subj.add( GenerateRandomPoint( l, t, r, b, rand ) ); 193 | } 194 | subjects.add( subj ); 195 | 196 | final Path clip = new Path( vertexCount ); 197 | for (int i = 0; i < vertexCount; ++i) { 198 | clip.add( GenerateRandomPoint( l, t, r, b, rand ) ); 199 | } 200 | clips.add( clip ); 201 | 202 | updateSolution(); 203 | } 204 | 205 | public Paths getClips() { 206 | return clips; 207 | } 208 | 209 | public ClipType getClipType() { 210 | return clipType; 211 | } 212 | 213 | public PolyFillType getFillType() { 214 | return fillType; 215 | } 216 | 217 | public Paths getSolution() { 218 | return solution; 219 | } 220 | 221 | public Paths getSubjects() { 222 | return subjects; 223 | } 224 | 225 | @Override 226 | protected void paintComponent( Graphics g ) { 227 | super.paintComponent( g ); 228 | 229 | for (final Path p : subjects) { 230 | final Polygon s = createPolygonFromPath( p ); 231 | g.setColor( new Color( 0xC3, 0xC9, 0xCF, 196 ) ); 232 | g.drawPolygon( s ); 233 | g.setColor( new Color( 0xDD, 0xDD, 0xF0, 127 ) ); 234 | g.fillPolygon( s ); 235 | } 236 | 237 | for (final Path p : clips) { 238 | final Polygon s = createPolygonFromPath( p ); 239 | 240 | g.setColor( new Color( 0xF9, 0xBE, 0xA6, 196 ) ); 241 | g.drawPolygon( s ); 242 | g.setColor( new Color( 0xFF, 0xE0, 0xE0, 127 ) ); 243 | g.fillPolygon( s ); 244 | } 245 | 246 | for (final Path p : solution) { 247 | final Polygon s = createPolygonFromPath( p ); 248 | if (p.orientation()) { 249 | g.setColor( new Color( 0, 0x33, 0, 255 ) ); 250 | } 251 | else { 252 | g.setColor( new Color( 0x33, 0, 0, 255 ) ); 253 | } 254 | g.drawPolygon( s ); 255 | if (p.orientation()) { 256 | g.setColor( new Color( 0x66, 0xEF, 0x7F, 127 ) ); 257 | } 258 | else { 259 | g.setColor( new Color( 0x66, 0x00, 0x00, 127 ) ); 260 | } 261 | 262 | g.fillPolygon( s ); 263 | } 264 | 265 | } 266 | 267 | public void setClipType( ClipType clipType ) { 268 | final ClipType oldType = this.clipType; 269 | this.clipType = clipType; 270 | if (oldType != clipType) { 271 | updateSolution(); 272 | 273 | } 274 | } 275 | 276 | public void setFillType( PolyFillType fillType ) { 277 | final PolyFillType oldType = this.fillType; 278 | this.fillType = fillType; 279 | if (oldType != fillType) { 280 | updateSolution(); 281 | 282 | } 283 | } 284 | 285 | public void setOffset( float offset ) { 286 | this.offset = offset; 287 | } 288 | 289 | public void setPolygon( PolyType type, Paths paths ) { 290 | switch (type) { 291 | case CLIP: 292 | clips.clear(); 293 | clips.addAll( paths ); 294 | break; 295 | case SUBJECT: 296 | subjects.clear(); 297 | subjects.addAll( paths ); 298 | break; 299 | default: 300 | throw new IllegalStateException(); 301 | 302 | } 303 | updateSolution(); 304 | 305 | } 306 | 307 | public void setVertexCount( int value ) { 308 | vertexCount = value; 309 | 310 | } 311 | 312 | public void updateSolution() { 313 | solution.clear(); 314 | if (clipType != null) { 315 | final DefaultClipper clp = new DefaultClipper( Clipper.STRICTLY_SIMPLE ); 316 | clp.addPaths( subjects, PolyType.SUBJECT, true ); 317 | clp.addPaths( clips, PolyType.CLIP, true ); 318 | if (clp.execute( clipType, solution, fillType, fillType )) { 319 | if (offset > 0f) { 320 | final ClipperOffset clo = new ClipperOffset(); 321 | clo.addPaths( solution, JoinType.ROUND, EndType.CLOSED_POLYGON ); 322 | clo.execute( clips, offset ); 323 | } 324 | int sum = 0; 325 | for (final Path p : solution) { 326 | sum += p.size(); 327 | } 328 | statusBar.setText( "Operation successful. Solution has " + sum + " vertices." ); 329 | } 330 | else { 331 | statusBar.setText( "Operation failed" ); 332 | } 333 | } 334 | else { 335 | statusBar.setText( "Operation successful. Solution cleared" ); 336 | } 337 | repaint(); 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /Clipper Console/src/de/lighti/clipper/console/Program.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper.console; 2 | 3 | import java.awt.Color; 4 | import java.io.BufferedReader; 5 | import java.io.BufferedWriter; 6 | import java.io.File; 7 | import java.io.FileReader; 8 | import java.io.FileWriter; 9 | import java.io.IOException; 10 | import java.util.Locale; 11 | import java.util.Random; 12 | import java.util.StringTokenizer; 13 | 14 | import de.lighti.clipper.Clipper; 15 | import de.lighti.clipper.Clipper.ClipType; 16 | import de.lighti.clipper.Clipper.PolyFillType; 17 | import de.lighti.clipper.Clipper.PolyType; 18 | import de.lighti.clipper.DefaultClipper; 19 | import de.lighti.clipper.Path; 20 | import de.lighti.clipper.Paths; 21 | import de.lighti.clipper.Point.LongPoint; 22 | 23 | public class Program { 24 | 25 | static Path IntsToPolygon( int[] ints ) { 26 | final int len1 = ints.length / 2; 27 | final Path result = new Path( len1 ); 28 | for (int i = 0; i < len1; i++) { 29 | result.add( new LongPoint( ints[i * 2], ints[i * 2 + 1] ) ); 30 | } 31 | return result; 32 | } 33 | 34 | static boolean LoadFromFile( String filename, Paths ppg, int dec_places ) throws IOException { 35 | return LoadFromFile( filename, ppg, dec_places, 0, 0 ); 36 | } 37 | 38 | static boolean LoadFromFile( String filename, Paths ppg, int dec_places, int xOffset, int yOffset ) throws IOException { 39 | final double scaling = Math.pow( 10, dec_places ); 40 | 41 | ppg.clear(); 42 | if (!new File( filename ).exists()) { 43 | return false; 44 | } 45 | final String delimiters = ", "; 46 | final BufferedReader sr = new BufferedReader( new FileReader( filename ) ); 47 | try { 48 | String line; 49 | if ((line = sr.readLine()) == null) { 50 | return false; 51 | } 52 | final int polyCnt = Integer.parseInt( line ); 53 | if (polyCnt < 0) { 54 | return false; 55 | } 56 | 57 | for (int i = 0; i < polyCnt; i++) { 58 | if ((line = sr.readLine()) == null) { 59 | return false; 60 | } 61 | final int vertCnt = Integer.parseInt( line ); 62 | if (vertCnt < 0) { 63 | return false; 64 | } 65 | final Path pg = new Path( vertCnt ); 66 | ppg.add( pg ); 67 | if (scaling > 0.999 & scaling < 1.001) { 68 | for (int j = 0; j < vertCnt; j++) { 69 | int x, y; 70 | if ((line = sr.readLine()) == null) { 71 | return false; 72 | } 73 | final StringTokenizer tokens = new StringTokenizer( line, delimiters ); 74 | 75 | if (tokens.countTokens() < 2) { 76 | return false; 77 | } 78 | 79 | x = Integer.parseInt( tokens.nextToken() ); 80 | y = Integer.parseInt( tokens.nextToken() ); 81 | 82 | x = x + xOffset; 83 | y = y + yOffset; 84 | pg.add( new LongPoint( x, y ) ); 85 | } 86 | } 87 | else { 88 | for (int j = 0; j < vertCnt; j++) { 89 | double x, y; 90 | if ((line = sr.readLine()) == null) { 91 | return false; 92 | } 93 | final StringTokenizer tokens = new StringTokenizer( line, delimiters ); 94 | 95 | if (tokens.countTokens() < 2) { 96 | return false; 97 | } 98 | x = Double.parseDouble( tokens.nextToken() ); 99 | y = Double.parseDouble( tokens.nextToken() ); 100 | 101 | x = x * scaling + xOffset; 102 | y = y * scaling + yOffset; 103 | pg.add( new LongPoint( (int) Math.round( x ), (int) Math.round( y ) ) ); 104 | } 105 | } 106 | } 107 | return true; 108 | } 109 | finally { 110 | sr.close(); 111 | } 112 | } 113 | 114 | public static void main( String[] args ) { 115 | //Enforce a US locale, or the SVG data will have decimals in form ##,## in countries 116 | //where ',' is the decimal separator. SVG requires a '.' instead 117 | Locale.setDefault( Locale.US ); 118 | 119 | if (args.length < 5) { 120 | final String appname = System.getProperty( "sun.java.command" ); 121 | System.out.println( "" ); 122 | System.out.println( "Usage:" ); 123 | System.out.println( String.format( " %1$s CLIPTYPE s_file c_file INPUT_DEC_PLACES SVG_SCALE [S_FILL, C_FILL]", appname ) ); 124 | System.out.println( " where ..." ); 125 | System.out.println( " CLIPTYPE = INTERSECTION|UNION|DIFFERENCE|XOR" ); 126 | System.out.println( " FILLMODE = NONZERO|EVENODD" ); 127 | System.out.println( " INPUT_DEC_PLACES = signific. decimal places for subject & clip coords." ); 128 | System.out.println( " SVG_SCALE = scale of SVG image as power of 10. (Fractions are accepted.)" ); 129 | System.out.println( " both S_FILL and C_FILL are optional. The default is EVENODD." ); 130 | System.out.println( "Example:" ); 131 | System.out.println( " Intersect polygons, rnd to 4 dec places, SVG is 1/100 normal size ..." ); 132 | System.out.println( String.format( " %1$s INTERSECTION subj.txt clip.txt 0 0 NONZERO NONZERO", appname ) ); 133 | return; 134 | } 135 | 136 | ClipType ct; 137 | switch (args[0].toUpperCase()) { 138 | case "INTERSECTION": 139 | ct = ClipType.INTERSECTION; 140 | break; 141 | case "UNION": 142 | ct = ClipType.UNION; 143 | break; 144 | case "DIFFERENCE": 145 | ct = ClipType.DIFFERENCE; 146 | break; 147 | case "XOR": 148 | ct = ClipType.XOR; 149 | break; 150 | default: 151 | System.out.println( "Error: invalid operation - " + args[0] ); 152 | return; 153 | } 154 | 155 | final String subjFilename = args[1]; 156 | final String clipFilename = args[2]; 157 | if (!new File( subjFilename ).exists()) { 158 | System.out.println( "Error: file - " + subjFilename + " - does not exist." ); 159 | return; 160 | } 161 | if (!new File( clipFilename ).exists()) { 162 | System.out.println( "Error: file - " + clipFilename + " - does not exist." ); 163 | return; 164 | } 165 | 166 | int decimal_places = Integer.parseInt( args[3] ); 167 | if (decimal_places < 0) { 168 | System.out.println( "Error: invalid number of decimal places - " + args[3] ); 169 | return; 170 | } 171 | if (decimal_places > 8) { 172 | decimal_places = 8; 173 | } 174 | else if (decimal_places < 0) { 175 | decimal_places = 0; 176 | } 177 | 178 | double svg_scale = Double.parseDouble( args[4] ); 179 | if (svg_scale < 0) { 180 | System.out.println( "Error: invalid value for SVG_SCALE - " + args[4] ); 181 | return; 182 | } 183 | if (svg_scale < -18) { 184 | svg_scale = -18; 185 | } 186 | else if (svg_scale > 18) { 187 | svg_scale = 18; 188 | } 189 | svg_scale = Math.pow( 10, svg_scale - decimal_places );//nb: also compensate for decimal places 190 | 191 | PolyFillType pftSubj = PolyFillType.EVEN_ODD; 192 | PolyFillType pftClip = PolyFillType.EVEN_ODD; 193 | if (args.length > 6) { 194 | switch (args[5].toUpperCase()) { 195 | case "EVENODD": 196 | pftSubj = PolyFillType.EVEN_ODD; 197 | break; 198 | case "NONZERO": 199 | pftSubj = PolyFillType.NON_ZERO; 200 | break; 201 | default: 202 | System.out.println( "Error: invalid cliptype - " + args[5] ); 203 | return; 204 | } 205 | switch (args[6].toUpperCase()) { 206 | case "EVENODD": 207 | pftClip = PolyFillType.EVEN_ODD; 208 | break; 209 | case "NONZERO": 210 | pftClip = PolyFillType.NON_ZERO; 211 | break; 212 | default: 213 | System.out.println( "Error: invalid cliptype - " + args[6] ); 214 | return; 215 | } 216 | } 217 | try { 218 | final Paths subjs = new Paths(); 219 | final Paths clips = new Paths(); 220 | if (!LoadFromFile( subjFilename, subjs, decimal_places )) { 221 | System.out.println( "Error processing subject polygons file - " + subjFilename ); 222 | OutputFileFormat(); 223 | return; 224 | } 225 | if (!LoadFromFile( clipFilename, clips, decimal_places )) { 226 | System.out.println( "Error processing clip polygons file - " + clipFilename ); 227 | OutputFileFormat(); 228 | return; 229 | } 230 | 231 | System.out.println( "wait ..." ); 232 | final DefaultClipper cp = new DefaultClipper( Clipper.STRICTLY_SIMPLE ); 233 | cp.addPaths( subjs, PolyType.SUBJECT, true ); 234 | cp.addPaths( clips, PolyType.CLIP, true ); 235 | 236 | final Paths solution = new Paths(); 237 | //Paths solution = new Paths(); 238 | if (cp.execute( ct, solution, pftSubj, pftClip )) { 239 | saveToFile( "solution.txt", solution, decimal_places ); 240 | 241 | //solution = Clipper.OffsetPolygons(solution, -4, JoinType.jtRound); 242 | 243 | final SVGBuilder svg = new SVGBuilder(); 244 | svg.style.showCoords = true; 245 | svg.style.brushClr = new Color( 0, 0, 0x9c, 0x20 ); 246 | svg.style.penClr = new Color( 0xd3, 0xd3, 0xda ); 247 | svg.addPaths( subjs ); 248 | svg.style.brushClr = new Color( 0x20, 0x9c, 0, 0 ); 249 | svg.style.penClr = new Color( 0xff, 0xa0, 0x7a ); 250 | svg.addPaths( clips ); 251 | svg.style.brushClr = new Color( 0x80, 0xff, 0x9c, 0xAA ); 252 | svg.style.penClr = new Color( 0, 0x33, 0 ); 253 | svg.addPaths( solution ); 254 | svg.saveToFile( "solution.svg", svg_scale ); 255 | 256 | System.out.println( "finished!" ); 257 | } 258 | else { 259 | System.out.println( "failed!" ); 260 | } 261 | } 262 | catch (final IOException e) { 263 | System.out.println( "An error occured: " + e.getMessage() ); 264 | } 265 | } 266 | 267 | static Path MakeRandomPolygon( Random r, int maxWidth, int maxHeight, int edgeCount ) { 268 | return MakeRandomPolygon( r, maxWidth, maxHeight, edgeCount, 1 ); 269 | } 270 | 271 | static Path MakeRandomPolygon( Random r, int maxWidth, int maxHeight, int edgeCount, int scale ) { 272 | final Path result = new Path( edgeCount ); 273 | for (int i = 0; i < edgeCount; i++) { 274 | result.add( new LongPoint( r.nextInt( maxWidth ) * scale, r.nextInt( maxHeight ) * scale ) ); 275 | } 276 | return result; 277 | } 278 | 279 | static void OutputFileFormat() { 280 | System.out.println( "The expected (text) file format is ..." ); 281 | System.out.println( "Polygon Count" ); 282 | System.out.println( "First polygon vertex count" ); 283 | System.out.println( "first X, Y coordinate of first polygon" ); 284 | System.out.println( "second X, Y coordinate of first polygon" ); 285 | System.out.println( "etc." ); 286 | System.out.println( "Second polygon vertex count (if there is one)" ); 287 | System.out.println( "first X, Y coordinate of second polygon" ); 288 | System.out.println( "second X, Y coordinate of second polygon" ); 289 | System.out.println( "etc." ); 290 | } 291 | 292 | static void saveToFile( String filename, Paths ppg, int dec_places ) throws IOException { 293 | final double scaling = Math.pow( 10, dec_places ); 294 | final BufferedWriter writer = new BufferedWriter( new FileWriter( filename ) ); 295 | try { 296 | writer.write( String.format( "%d%n", ppg.size() ) ); 297 | for (final Path pg : ppg) { 298 | writer.write( String.format( "%d%n", pg.size() ) ); 299 | for (final LongPoint ip : pg) { 300 | writer.write( String.format( "%.2f, %.2f%n", ip.getX() / scaling, ip.getY() / scaling ) ); 301 | } 302 | } 303 | } 304 | finally { 305 | writer.close(); 306 | } 307 | } 308 | } //class Program 309 | 310 | -------------------------------------------------------------------------------- /Clipper/src/de/lighti/clipper/Path.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper; 2 | 3 | import de.lighti.clipper.Point.LongPoint; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | 8 | /** 9 | * A pure convenience class to avoid writing List everywhere. 10 | * 11 | * @author Tobias Mahlmann 12 | * 13 | */ 14 | public class Path extends ArrayList { 15 | static class Join { 16 | Path.OutPt outPt1; 17 | Path.OutPt outPt2; 18 | private LongPoint offPt; 19 | 20 | public LongPoint getOffPt() { 21 | return offPt; 22 | } 23 | 24 | public void setOffPt( LongPoint offPt ) { 25 | this.offPt = offPt; 26 | } 27 | 28 | } 29 | 30 | static class OutPt { 31 | public static OutRec getLowerMostRec( OutRec outRec1, OutRec outRec2 ) { 32 | //work out which polygon fragment has the correct hole state ... 33 | if (outRec1.bottomPt == null) { 34 | outRec1.bottomPt = outRec1.pts.getBottomPt(); 35 | } 36 | if (outRec2.bottomPt == null) { 37 | outRec2.bottomPt = outRec2.pts.getBottomPt(); 38 | } 39 | final Path.OutPt bPt1 = outRec1.bottomPt; 40 | final Path.OutPt bPt2 = outRec2.bottomPt; 41 | if (bPt1.getPt().getY() > bPt2.getPt().getY()) { 42 | return outRec1; 43 | } 44 | else if (bPt1.getPt().getY() < bPt2.getPt().getY()) { 45 | return outRec2; 46 | } 47 | else if (bPt1.getPt().getX() < bPt2.getPt().getX()) { 48 | return outRec1; 49 | } 50 | else if (bPt1.getPt().getX() > bPt2.getPt().getX()) { 51 | return outRec2; 52 | } 53 | else if (bPt1.next == bPt1) { 54 | return outRec2; 55 | } 56 | else if (bPt2.next == bPt2) { 57 | return outRec1; 58 | } 59 | else if (isFirstBottomPt( bPt1, bPt2 )) { 60 | return outRec1; 61 | } 62 | else { 63 | return outRec2; 64 | } 65 | } 66 | 67 | private static boolean isFirstBottomPt(Path.OutPt btmPt1, Path.OutPt btmPt2 ) { 68 | Path.OutPt p = btmPt1.prev; 69 | while (p.getPt().equals( btmPt1.getPt() ) && !p.equals( btmPt1 )) { 70 | p = p.prev; 71 | } 72 | final double dx1p = Math.abs( LongPoint.getDeltaX( btmPt1.getPt(), p.getPt() ) ); 73 | p = btmPt1.next; 74 | while (p.getPt().equals( btmPt1.getPt() ) && !p.equals( btmPt1 )) { 75 | p = p.next; 76 | } 77 | final double dx1n = Math.abs( LongPoint.getDeltaX( btmPt1.getPt(), p.getPt() ) ); 78 | 79 | p = btmPt2.prev; 80 | while (p.getPt().equals( btmPt2.getPt() ) && !p.equals( btmPt2 )) { 81 | p = p.prev; 82 | } 83 | final double dx2p = Math.abs( LongPoint.getDeltaX( btmPt2.getPt(), p.getPt() ) ); 84 | p = btmPt2.next; 85 | while (p.getPt().equals( btmPt2.getPt() ) && p.equals( btmPt2 )) { 86 | p = p.next; 87 | } 88 | final double dx2n = Math.abs( LongPoint.getDeltaX( btmPt2.getPt(), p.getPt() ) ); 89 | 90 | if (Math.max( dx1p, dx1n ) == Math.max( dx2p, dx2n ) && Math.min( dx1p, dx1n ) == Math.min( dx2p, dx2n )) { 91 | return btmPt1.area() > 0; //if otherwise identical use orientation 92 | } 93 | else { 94 | return dx1p >= dx2p && dx1p >= dx2n || dx1n >= dx2p && dx1n >= dx2n; 95 | } 96 | } 97 | 98 | int idx; 99 | private LongPoint pt; 100 | OutPt next; 101 | 102 | OutPt prev; 103 | 104 | public Path.OutPt duplicate(boolean InsertAfter ) { 105 | final Path.OutPt result = new Path.OutPt(); 106 | result.setPt( new LongPoint( getPt() ) ); 107 | result.idx = idx; 108 | if (InsertAfter) { 109 | result.next = next; 110 | result.prev = this; 111 | next.prev = result; 112 | next = result; 113 | } 114 | else { 115 | result.prev = prev; 116 | result.next = this; 117 | prev.next = result; 118 | prev = result; 119 | } 120 | return result; 121 | } 122 | 123 | Path.OutPt getBottomPt() { 124 | Path.OutPt dups = null; 125 | Path.OutPt p = next; 126 | Path.OutPt pp = this; 127 | while (p != pp) { 128 | if (p.getPt().getY() > pp.getPt().getY()) { 129 | pp = p; 130 | dups = null; 131 | } 132 | else if (p.getPt().getY() == pp.getPt().getY() && p.getPt().getX() <= pp.getPt().getX()) { 133 | if (p.getPt().getX() < pp.getPt().getX()) { 134 | dups = null; 135 | pp = p; 136 | } 137 | else { 138 | if (p.next != pp && p.prev != pp) { 139 | dups = p; 140 | } 141 | } 142 | } 143 | p = p.next; 144 | } 145 | if (dups != null) { 146 | //there appears to be at least 2 vertices at bottomPt so ... 147 | while (dups != p) { 148 | if (!isFirstBottomPt( p, dups )) { 149 | pp = dups; 150 | } 151 | dups = dups.next; 152 | while (!dups.getPt().equals( pp.getPt() )) { 153 | dups = dups.next; 154 | } 155 | } 156 | } 157 | return pp; 158 | } 159 | 160 | public static int getPointCount( OutPt pts ) { 161 | if (pts == null) return 0; 162 | int result = 0; 163 | OutPt p = pts; 164 | do { 165 | result++; 166 | p = p.next; 167 | } 168 | while (p != pts); 169 | return result; 170 | } 171 | 172 | public LongPoint getPt() { 173 | return pt; 174 | } 175 | 176 | public void reversePolyPtLinks() { 177 | 178 | Path.OutPt pp1; 179 | Path.OutPt pp2; 180 | pp1 = this; 181 | do { 182 | pp2 = pp1.next; 183 | pp1.next = pp1.prev; 184 | pp1.prev = pp2; 185 | pp1 = pp2; 186 | } 187 | while (pp1 != this); 188 | } 189 | 190 | public void setPt( LongPoint pt ) { 191 | this.pt = pt; 192 | } 193 | 194 | private double area() { 195 | Path.OutPt op = this; 196 | double a = 0; 197 | do { 198 | a = a + (double) (op.prev.getPt().getX() + op.getPt().getX()) * (double) (op.prev.getPt().getY() - op.getPt().getY()); 199 | op = op.next; 200 | } while (op != this); 201 | return a * 0.5; 202 | } 203 | } 204 | 205 | /** OutRec: contains a path in the clipping solution. Edges in the AEL will 206 | carry a pointer to an OutRec when they are part of the clipping solution.*/ 207 | static class OutRec { 208 | int Idx; 209 | 210 | boolean isHole; 211 | 212 | boolean isOpen; 213 | OutRec firstLeft; //see comments in clipper.pas 214 | private Path.OutPt pts; 215 | Path.OutPt bottomPt; 216 | PolyNode polyNode; 217 | 218 | public double area() { 219 | return pts.area(); 220 | } 221 | 222 | public void fixHoleLinkage() { 223 | //skip if an outermost polygon or 224 | //already already points to the correct FirstLeft ... 225 | if (firstLeft == null || isHole != firstLeft.isHole && firstLeft.pts != null) { 226 | return; 227 | } 228 | 229 | OutRec orfl = firstLeft; 230 | while (orfl != null && (orfl.isHole == isHole || orfl.pts == null)) { 231 | orfl = orfl.firstLeft; 232 | } 233 | firstLeft = orfl; 234 | } 235 | 236 | public Path.OutPt getPoints() { 237 | return pts; 238 | } 239 | 240 | public static OutRec parseFirstLeft( OutRec firstLeft ) { 241 | while (firstLeft != null && firstLeft.getPoints() == null) { 242 | firstLeft = firstLeft.firstLeft; 243 | } 244 | return firstLeft; 245 | } 246 | 247 | public void setPoints( Path.OutPt pts ) { 248 | this.pts = pts; 249 | } 250 | } 251 | 252 | private static Path.OutPt excludeOp(Path.OutPt op ) { 253 | final Path.OutPt result = op.prev; 254 | result.next = op.next; 255 | op.next.prev = result; 256 | result.idx = 0; 257 | return result; 258 | } 259 | 260 | /** 261 | * 262 | */ 263 | private static final long serialVersionUID = -7120161578077546673L; 264 | 265 | public Path() { 266 | super(); 267 | 268 | } 269 | 270 | public Path( int cnt ) { 271 | super( cnt ); 272 | } 273 | 274 | public double area() { 275 | final int cnt = size(); 276 | if (cnt < 3) { 277 | return 0; 278 | } 279 | double a = 0; 280 | for (int i = 0, j = cnt - 1; i < cnt; ++i) { 281 | a += ((double) get( j ).getX() + get( i ).getX()) * ((double) get( j ).getY() - get( i ).getY()); 282 | j = i; 283 | } 284 | return -a * 0.5; 285 | } 286 | 287 | public Path cleanPolygon() { 288 | return cleanPolygon( 1.415 ); 289 | } 290 | 291 | public Path cleanPolygon(double distance ) { 292 | //distance = proximity in units/pixels below which vertices will be stripped. 293 | //Default ~= sqrt(2) so when adjacent vertices or semi-adjacent vertices have 294 | //both x & y coords within 1 unit, then the second vertex will be stripped. 295 | 296 | int cnt = size(); 297 | 298 | if (cnt == 0) { 299 | return new Path(); 300 | } 301 | 302 | Path.OutPt[] outPts = new Path.OutPt[cnt]; 303 | for (int i = 0; i < cnt; ++i) { 304 | outPts[i] = new Path.OutPt(); 305 | } 306 | 307 | for (int i = 0; i < cnt; ++i) { 308 | outPts[i].pt = get( i ); 309 | outPts[i].next = outPts[(i + 1) % cnt]; 310 | outPts[i].next.prev = outPts[i]; 311 | outPts[i].idx = 0; 312 | } 313 | 314 | final double distSqrd = distance * distance; 315 | Path.OutPt op = outPts[0]; 316 | while (op.idx == 0 && op.next != op.prev) { 317 | if (Point.arePointsClose( op.pt, op.prev.pt, distSqrd )) { 318 | op = excludeOp( op ); 319 | cnt--; 320 | } 321 | else if (Point.arePointsClose( op.prev.pt, op.next.pt, distSqrd )) { 322 | excludeOp( op.next ); 323 | op = excludeOp( op ); 324 | cnt -= 2; 325 | } 326 | else if (Point.slopesNearCollinear( op.prev.pt, op.pt, op.next.pt, distSqrd )) { 327 | op = excludeOp( op ); 328 | cnt--; 329 | } 330 | else { 331 | op.idx = 1; 332 | op = op.next; 333 | } 334 | } 335 | 336 | if (cnt < 3) { 337 | cnt = 0; 338 | } 339 | final Path result = new Path( cnt ); 340 | for (int i = 0; i < cnt; ++i) { 341 | result.add( op.pt ); 342 | op = op.next; 343 | } 344 | outPts = null; 345 | return result; 346 | } 347 | 348 | public int isPointInPolygon( LongPoint pt ) { 349 | //returns 0 if false, +1 if true, -1 if pt ON polygon boundary 350 | //See "The Point in Polygon Problem for Arbitrary Polygons" by Hormann & Agathos 351 | //http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.88.5498&rep=rep1&type=pdf 352 | int result = 0; 353 | final int cnt = size(); 354 | if (cnt < 3) { 355 | return 0; 356 | } 357 | LongPoint ip = get( 0 ); 358 | for (int i = 1; i <= cnt; ++i) { 359 | final LongPoint ipNext = i == cnt ? get( 0 ) : get( i ); 360 | if (ipNext.getY() == pt.getY()) { 361 | if (ipNext.getX() == pt.getX() || ip.getY() == pt.getY() && ipNext.getX() > pt.getX() == ip.getX() < pt.getX()) { 362 | return -1; 363 | } 364 | } 365 | if (ip.getY() < pt.getY() != ipNext.getY() < pt.getY()) { 366 | if (ip.getX() >= pt.getX()) { 367 | if (ipNext.getX() > pt.getX()) { 368 | result = 1 - result; 369 | } 370 | else { 371 | final double d = (double) (ip.getX() - pt.getX()) * (ipNext.getY() - pt.getY()) - (double) (ipNext.getX() - pt.getX()) 372 | * (ip.getY() - pt.getY()); 373 | if (d == 0) { 374 | return -1; 375 | } 376 | else if (d > 0 == ipNext.getY() > ip.getY()) { 377 | result = 1 - result; 378 | } 379 | } 380 | } 381 | else { 382 | if (ipNext.getX() > pt.getX()) { 383 | final double d = (double) (ip.getX() - pt.getX()) * (ipNext.getY() - pt.getY()) - (double) (ipNext.getX() - pt.getX()) 384 | * (ip.getY() - pt.getY()); 385 | if (d == 0) { 386 | return -1; 387 | } 388 | else if (d > 0 == ipNext.getY() > ip.getY()) { 389 | result = 1 - result; 390 | } 391 | } 392 | } 393 | } 394 | ip = ipNext; 395 | } 396 | return result; 397 | } 398 | 399 | public boolean orientation() { 400 | return area() >= 0; 401 | } 402 | 403 | public void reverse() { 404 | Collections.reverse( this ); 405 | } 406 | 407 | public Path TranslatePath(LongPoint delta ) { 408 | final Path outPath = new Path( size() ); 409 | for (int i = 0; i < size(); i++) { 410 | outPath.add( new LongPoint( get( i ).getX() + delta.getX(), get( i ).getY() + delta.getY() ) ); 411 | } 412 | return outPath; 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /Clipper/src/de/lighti/clipper/ClipperOffset.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper; 2 | 3 | import de.lighti.clipper.Clipper.*; 4 | import de.lighti.clipper.Point.DoublePoint; 5 | import de.lighti.clipper.Point.LongPoint; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | public class ClipperOffset { 12 | private static boolean nearZero( double val ) { 13 | return val > -TOLERANCE && val < TOLERANCE; 14 | } 15 | 16 | private Paths destPolys; 17 | private Path srcPoly; 18 | private Path destPoly; 19 | 20 | private final List normals; 21 | private double delta, inA, sin, cos; 22 | 23 | private double miterLim, stepsPerRad; 24 | private LongPoint lowest; 25 | 26 | private final PolyNode polyNodes; 27 | private final double arcTolerance; 28 | 29 | private final double miterLimit; 30 | private final static double TWO_PI = Math.PI * 2; 31 | 32 | private final static double DEFAULT_ARC_TOLERANCE = 0.25; 33 | 34 | private final static double TOLERANCE = 1.0E-20; 35 | 36 | public ClipperOffset() { 37 | this( 2, DEFAULT_ARC_TOLERANCE ); 38 | } 39 | 40 | public ClipperOffset( double miterLimit, double arcTolerance ) { 41 | this.miterLimit = miterLimit; 42 | this.arcTolerance = arcTolerance; 43 | lowest = new LongPoint(); 44 | lowest.setX( -1L ); 45 | polyNodes = new PolyNode(); 46 | normals = new ArrayList<>(); 47 | } 48 | 49 | public void addPath(Path path, JoinType joinType, EndType endType ) { 50 | int highI = path.size() - 1; 51 | if (highI < 0) { 52 | return; 53 | } 54 | final PolyNode newNode = new PolyNode(); 55 | newNode.setJoinType( joinType ); 56 | newNode.setEndType( endType ); 57 | 58 | //strip duplicate points from path and also get index to the lowest point ... 59 | if (endType == EndType.CLOSED_LINE || endType == EndType.CLOSED_POLYGON) { 60 | while (highI > 0 && path.get( 0 ) == path.get( highI )) { 61 | highI--; 62 | } 63 | } 64 | 65 | newNode.getPolygon().add( path.get( 0 ) ); 66 | int j = 0, k = 0; 67 | for (int i = 1; i <= highI; i++) { 68 | if (newNode.getPolygon().get( j ) != path.get( i )) { 69 | j++; 70 | newNode.getPolygon().add( path.get( i ) ); 71 | if (path.get( i ).getY() > newNode.getPolygon().get( k ).getY() || path.get( i ).getY() == newNode.getPolygon().get( k ).getY() 72 | && path.get( i ).getX() < newNode.getPolygon().get( k ).getX()) { 73 | k = j; 74 | } 75 | } 76 | } 77 | if (endType == EndType.CLOSED_POLYGON && j < 2) { 78 | return; 79 | } 80 | 81 | polyNodes.addChild( newNode ); 82 | 83 | //if this path's lowest pt is lower than all the others then update m_lowest 84 | if (endType != EndType.CLOSED_POLYGON) { 85 | return; 86 | } 87 | if (lowest.getX() < 0) { 88 | lowest = new LongPoint( polyNodes.getChildCount() - 1, k ); 89 | } 90 | else { 91 | final LongPoint ip = polyNodes.getChilds().get( (int) lowest.getX() ).getPolygon().get( (int) lowest.getY() ); 92 | if (newNode.getPolygon().get( k ).getY() > ip.getY() || newNode.getPolygon().get( k ).getY() == ip.getY() 93 | && newNode.getPolygon().get( k ).getX() < ip.getX()) { 94 | lowest = new LongPoint( polyNodes.getChildCount() - 1, k ); 95 | } 96 | } 97 | } 98 | 99 | public void addPaths(Paths paths, JoinType joinType, EndType endType ) { 100 | for (final Path p : paths) { 101 | addPath( p, joinType, endType ); 102 | } 103 | } 104 | 105 | public void clear() { 106 | polyNodes.getChilds().clear(); 107 | lowest.setX( -1L ); 108 | } 109 | 110 | private void doMiter( int j, int k, double r ) { 111 | final double q = delta / r; 112 | destPoly.add( new LongPoint( Math.round( srcPoly.get( j ).getX() + (normals.get( k ).getX() + normals.get( j ).getX()) * q ), Math 113 | .round( srcPoly.get( j ).getY() + (normals.get( k ).getY() + normals.get( j ).getY()) * q ) ) ); 114 | } 115 | 116 | private void doOffset( double delta ) { 117 | destPolys = new Paths(); 118 | this.delta = delta; 119 | 120 | //if Zero offset, just copy any CLOSED polygons to m_p and return ... 121 | if (nearZero( delta )) { 122 | for (int i = 0; i < polyNodes.getChildCount(); i++) { 123 | final PolyNode node = polyNodes.getChilds().get( i ); 124 | if (node.getEndType() == EndType.CLOSED_POLYGON) { 125 | destPolys.add( node.getPolygon() ); 126 | } 127 | } 128 | return; 129 | } 130 | 131 | //see offset_triginometry3.svg in the documentation folder ... 132 | if (miterLimit > 2) { 133 | miterLim = 2 / (miterLimit * miterLimit); 134 | } 135 | else { 136 | miterLim = 0.5; 137 | } 138 | 139 | double y; 140 | if (arcTolerance <= 0.0) { 141 | y = DEFAULT_ARC_TOLERANCE; 142 | } 143 | else if (arcTolerance > Math.abs( delta ) * DEFAULT_ARC_TOLERANCE) { 144 | y = Math.abs( delta ) * DEFAULT_ARC_TOLERANCE; 145 | } 146 | else { 147 | y = arcTolerance; 148 | } 149 | //see offset_triginometry2.svg in the documentation folder ... 150 | final double steps = Math.PI / Math.acos( 1 - y / Math.abs( delta ) ); 151 | sin = Math.sin( TWO_PI / steps ); 152 | cos = Math.cos( TWO_PI / steps ); 153 | stepsPerRad = steps / TWO_PI; 154 | if (delta < 0.0) { 155 | sin = -sin; 156 | } 157 | 158 | for (int i = 0; i < polyNodes.getChildCount(); i++) { 159 | final PolyNode node = polyNodes.getChilds().get( i ); 160 | srcPoly = node.getPolygon(); 161 | 162 | final int len = srcPoly.size(); 163 | 164 | if (len == 0 || delta <= 0 && (len < 3 || node.getEndType() != EndType.CLOSED_POLYGON)) { 165 | continue; 166 | } 167 | 168 | destPoly = new Path(); 169 | 170 | if (len == 1) { 171 | if (node.getJoinType() == JoinType.ROUND) { 172 | double X = 1.0, Y = 0.0; 173 | for (int j = 1; j <= steps; j++) { 174 | destPoly.add( new LongPoint( Math.round( srcPoly.get( 0 ).getX() + X * delta ), Math.round( srcPoly.get( 0 ).getY() + Y 175 | * delta ) ) ); 176 | final double X2 = X; 177 | X = X * cos - sin * Y; 178 | Y = X2 * sin + Y * cos; 179 | } 180 | } 181 | else { 182 | double X = -1.0, Y = -1.0; 183 | for (int j = 0; j < 4; ++j) { 184 | destPoly.add( new LongPoint( Math.round( srcPoly.get( 0 ).getX() + X * delta ), Math.round( srcPoly.get( 0 ).getY() + Y 185 | * delta ) ) ); 186 | if (X < 0) { 187 | X = 1; 188 | } 189 | else if (Y < 0) { 190 | Y = 1; 191 | } 192 | else { 193 | X = -1; 194 | } 195 | } 196 | } 197 | destPolys.add( destPoly ); 198 | continue; 199 | } 200 | 201 | //build m_normals ... 202 | normals.clear(); 203 | for (int j = 0; j < len - 1; j++) { 204 | normals.add( Point.getUnitNormal( srcPoly.get( j ), srcPoly.get( j + 1 ) ) ); 205 | } 206 | if (node.getEndType() == EndType.CLOSED_LINE || node.getEndType() == EndType.CLOSED_POLYGON) { 207 | normals.add( Point.getUnitNormal( srcPoly.get( len - 1 ), srcPoly.get( 0 ) ) ); 208 | } 209 | else { 210 | normals.add( new DoublePoint( normals.get( len - 2 ) ) ); 211 | } 212 | 213 | if (node.getEndType() == EndType.CLOSED_POLYGON) { 214 | final int[] k = new int[] { len - 1 }; 215 | for (int j = 0; j < len; j++) { 216 | offsetPoint( j, k, node.getJoinType() ); 217 | } 218 | destPolys.add( destPoly ); 219 | } 220 | else if (node.getEndType() == EndType.CLOSED_LINE) { 221 | final int[] k = new int[] { len - 1 }; 222 | for (int j = 0; j < len; j++) { 223 | offsetPoint( j, k, node.getJoinType() ); 224 | } 225 | destPolys.add( destPoly ); 226 | destPoly = new Path(); 227 | //re-build m_normals ... 228 | final DoublePoint n = normals.get( len - 1 ); 229 | for (int j = len - 1; j > 0; j--) { 230 | normals.set( j, new DoublePoint( -normals.get( j - 1 ).getX(), -normals.get( j - 1 ).getY() ) ); 231 | } 232 | normals.set( 0, new DoublePoint( -n.getX(), -n.getY(), 0 ) ); 233 | k[0] = 0; 234 | for (int j = len - 1; j >= 0; j--) { 235 | offsetPoint( j, k, node.getJoinType() ); 236 | } 237 | destPolys.add( destPoly ); 238 | } 239 | else { 240 | final int[] k = new int[1]; 241 | for (int j = 1; j < len - 1; ++j) { 242 | offsetPoint( j, k, node.getJoinType() ); 243 | } 244 | 245 | LongPoint pt1; 246 | if (node.getEndType() == EndType.OPEN_BUTT) { 247 | final int j = len - 1; 248 | pt1 = new LongPoint( Math.round( srcPoly.get( j ).getX() + normals.get( j ).getX() * delta ), Math.round( srcPoly.get( j ) 249 | .getY() + normals.get( j ).getY() * delta ), 0 ); 250 | destPoly.add( pt1 ); 251 | pt1 = new LongPoint( Math.round( srcPoly.get( j ).getX() - normals.get( j ).getX() * delta ), Math.round( srcPoly.get( j ) 252 | .getY() - normals.get( j ).getY() * delta ), 0 ); 253 | destPoly.add( pt1 ); 254 | } 255 | else { 256 | final int j = len - 1; 257 | k[0] = len - 2; 258 | inA = 0; 259 | normals.set( j, new DoublePoint( -normals.get( j ).getX(), -normals.get( j ).getY() ) ); 260 | if (node.getEndType() == EndType.OPEN_SQUARE) { 261 | doSquare( j, k[0] ); 262 | } 263 | else { 264 | doRound( j, k[0] ); 265 | } 266 | } 267 | 268 | //re-build m_normals ... 269 | for (int j = len - 1; j > 0; j--) { 270 | normals.set( j, new DoublePoint( -normals.get( j - 1 ).getX(), -normals.get( j - 1 ).getY() ) ); 271 | } 272 | 273 | normals.set( 0, new DoublePoint( -normals.get( 1 ).getX(), -normals.get( 1 ).getY() ) ); 274 | 275 | k[0] = len - 1; 276 | for (int j = k[0] - 1; j > 0; --j) { 277 | offsetPoint( j, k, node.getJoinType() ); 278 | } 279 | 280 | if (node.getEndType() == EndType.OPEN_BUTT) { 281 | pt1 = new LongPoint( Math.round( srcPoly.get( 0 ).getX() - normals.get( 0 ).getX() * delta ), Math.round( srcPoly.get( 0 ) 282 | .getY() - normals.get( 0 ).getY() * delta ) ); 283 | destPoly.add( pt1 ); 284 | pt1 = new LongPoint( Math.round( srcPoly.get( 0 ).getX() + normals.get( 0 ).getX() * delta ), Math.round( srcPoly.get( 0 ) 285 | .getY() + normals.get( 0 ).getY() * delta ) ); 286 | destPoly.add( pt1 ); 287 | } 288 | else { 289 | k[0] = 1; 290 | inA = 0; 291 | if (node.getEndType() == EndType.OPEN_SQUARE) { 292 | doSquare( 0, 1 ); 293 | } 294 | else { 295 | doRound( 0, 1 ); 296 | } 297 | } 298 | destPolys.add( destPoly ); 299 | } 300 | } 301 | } 302 | 303 | private void doRound( int j, int k ) { 304 | final double a = Math.atan2( inA, normals.get( k ).getX() * normals.get( j ).getX() + normals.get( k ).getY() * normals.get( j ).getY() ); 305 | final int steps = Math.max( (int) Math.round( stepsPerRad * Math.abs( a ) ), 1 ); 306 | 307 | double X = normals.get( k ).getX(), Y = normals.get( k ).getY(), X2; 308 | for (int i = 0; i < steps; ++i) { 309 | destPoly.add( new LongPoint( Math.round( srcPoly.get( j ).getX() + X * delta ), Math.round( srcPoly.get( j ).getY() + Y * delta ) ) ); 310 | X2 = X; 311 | X = X * cos - sin * Y; 312 | Y = X2 * sin + Y * cos; 313 | } 314 | destPoly.add( new LongPoint( Math.round( srcPoly.get( j ).getX() + normals.get( j ).getX() * delta ), Math.round( srcPoly.get( j ).getY() 315 | + normals.get( j ).getY() * delta ) ) ); 316 | } 317 | 318 | private void doSquare( int j, int k ) { 319 | final double nkx = normals.get( k ).getX(); 320 | final double nky = normals.get( k ).getY(); 321 | final double njx = normals.get( j ).getX(); 322 | final double njy = normals.get( j ).getY(); 323 | final double sjx = srcPoly.get( j ).getX(); 324 | final double sjy = srcPoly.get( j ).getY(); 325 | final double dx = Math.tan( Math.atan2( inA, nkx * njx + nky * njy ) / 4 ); 326 | destPoly.add( new LongPoint( Math.round( sjx + delta * (nkx - nky * dx) ), Math.round( sjy + delta * (nky + nkx * dx) ), 0 ) ); 327 | destPoly.add( new LongPoint( Math.round( sjx + delta * (njx + njy * dx) ), Math.round( sjy + delta * (njy - njx * dx) ), 0 ) ); 328 | } 329 | 330 | //------------------------------------------------------------------------------ 331 | 332 | public void execute(Paths solution, double delta ) { 333 | solution.clear(); 334 | fixOrientations(); 335 | doOffset( delta ); 336 | //now clean up 'corners' ... 337 | final DefaultClipper clpr = new DefaultClipper( Clipper.REVERSE_SOLUTION ); 338 | clpr.addPaths( destPolys, PolyType.SUBJECT, true ); 339 | if (delta > 0) { 340 | clpr.execute( ClipType.UNION, solution, PolyFillType.POSITIVE, PolyFillType.POSITIVE ); 341 | } 342 | else { 343 | final LongRect r = destPolys.getBounds(); 344 | final Path outer = new Path( 4 ); 345 | 346 | outer.add( new LongPoint( r.left - 10, r.bottom + 10, 0 ) ); 347 | outer.add( new LongPoint( r.right + 10, r.bottom + 10, 0 ) ); 348 | outer.add( new LongPoint( r.right + 10, r.top - 10, 0 ) ); 349 | outer.add( new LongPoint( r.left - 10, r.top - 10, 0 ) ); 350 | 351 | clpr.addPath( outer, PolyType.SUBJECT, true ); 352 | 353 | clpr.execute( ClipType.UNION, solution, PolyFillType.NEGATIVE, PolyFillType.NEGATIVE ); 354 | if (solution.size() > 0) { 355 | solution.remove( 0 ); 356 | } 357 | } 358 | } 359 | 360 | //------------------------------------------------------------------------------ 361 | 362 | public void execute(PolyTree solution, double delta ) { 363 | solution.Clear(); 364 | fixOrientations(); 365 | doOffset( delta ); 366 | 367 | //now clean up 'corners' ... 368 | final DefaultClipper clpr = new DefaultClipper( Clipper.REVERSE_SOLUTION ); 369 | clpr.addPaths( destPolys, PolyType.SUBJECT, true ); 370 | if (delta > 0) { 371 | clpr.execute( ClipType.UNION, solution, PolyFillType.POSITIVE, PolyFillType.POSITIVE ); 372 | } 373 | else { 374 | final LongRect r = destPolys.getBounds(); 375 | final Path outer = new Path( 4 ); 376 | 377 | outer.add( new LongPoint( r.left - 10, r.bottom + 10, 0 ) ); 378 | outer.add( new LongPoint( r.right + 10, r.bottom + 10, 0 ) ); 379 | outer.add( new LongPoint( r.right + 10, r.top - 10, 0 ) ); 380 | outer.add( new LongPoint( r.left - 10, r.top - 10, 0 ) ); 381 | 382 | clpr.addPath( outer, PolyType.SUBJECT, true ); 383 | 384 | clpr.execute( ClipType.UNION, solution, PolyFillType.NEGATIVE, PolyFillType.NEGATIVE ); 385 | //remove the outer PolyNode rectangle ... 386 | if (solution.getChildCount() == 1 && solution.getChilds().get( 0 ).getChildCount() > 0) { 387 | final PolyNode outerNode = solution.getChilds().get( 0 ); 388 | solution.getChilds().set( 0, outerNode.getChilds().get( 0 ) ); 389 | solution.getChilds().get( 0 ).setParent( solution ); 390 | for (int i = 1; i < outerNode.getChildCount(); i++) { 391 | solution.addChild( outerNode.getChilds().get( i ) ); 392 | } 393 | } 394 | else { 395 | solution.Clear(); 396 | } 397 | } 398 | } 399 | 400 | //------------------------------------------------------------------------------ 401 | 402 | private void fixOrientations() { 403 | //fixup orientations of all closed paths if the orientation of the 404 | //closed path with the lowermost vertex is wrong ... 405 | if (lowest.getX() >= 0 && !polyNodes.childs.get( (int) lowest.getX() ).getPolygon().orientation()) { 406 | for (int i = 0; i < polyNodes.getChildCount(); i++) { 407 | final PolyNode node = polyNodes.childs.get( i ); 408 | if (node.getEndType() == EndType.CLOSED_POLYGON || node.getEndType() == EndType.CLOSED_LINE && node.getPolygon().orientation()) { 409 | Collections.reverse( node.getPolygon() ); 410 | 411 | } 412 | } 413 | } 414 | else { 415 | for (int i = 0; i < polyNodes.getChildCount(); i++) { 416 | final PolyNode node = polyNodes.childs.get( i ); 417 | if (node.getEndType() == EndType.CLOSED_LINE && !node.getPolygon().orientation()) { 418 | Collections.reverse( node.getPolygon() ); 419 | } 420 | } 421 | } 422 | } 423 | 424 | private void offsetPoint( int j, int[] kV, JoinType jointype ) { 425 | //cross product ... 426 | final int k = kV[0]; 427 | final double nkx = normals.get( k ).getX(); 428 | final double nky = normals.get( k ).getY(); 429 | final double njy = normals.get( j ).getY(); 430 | final double njx = normals.get( j ).getX(); 431 | final long sjx = srcPoly.get( j ).getX(); 432 | final long sjy = srcPoly.get( j ).getY(); 433 | inA = nkx * njy - njx * nky; 434 | 435 | if (Math.abs( inA * delta ) < 1.0) { 436 | //dot product ... 437 | 438 | final double cosA = nkx * njx + njy * nky; 439 | if (cosA > 0) // angle ==> 0 degrees 440 | { 441 | destPoly.add( new LongPoint( Math.round( sjx + nkx * delta ), Math.round( sjy + nky * delta ), 0 ) ); 442 | return; 443 | } 444 | //else angle ==> 180 degrees 445 | } 446 | else if (inA > 1.0) { 447 | inA = 1.0; 448 | } 449 | else if (inA < -1.0) { 450 | inA = -1.0; 451 | } 452 | 453 | if (inA * delta < 0) { 454 | destPoly.add( new LongPoint( Math.round( sjx + nkx * delta ), Math.round( sjy + nky * delta ) ) ); 455 | destPoly.add( srcPoly.get( j ) ); 456 | destPoly.add( new LongPoint( Math.round( sjx + njx * delta ), Math.round( sjy + njy * delta ) ) ); 457 | } 458 | else { 459 | switch (jointype) { 460 | case MITER: { 461 | final double r = 1 + njx * nkx + njy * nky; 462 | if (r >= miterLim) { 463 | doMiter( j, k, r ); 464 | } 465 | else { 466 | doSquare( j, k ); 467 | } 468 | break; 469 | } 470 | case SQUARE: 471 | doSquare( j, k ); 472 | break; 473 | case ROUND: 474 | doRound( j, k ); 475 | break; 476 | } 477 | } 478 | kV[0] = j; 479 | } 480 | //------------------------------------------------------------------------------ 481 | } -------------------------------------------------------------------------------- /Clipper GUI/src/de/lighti/clipper/gui/ClipperDialog.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper.gui; 2 | 3 | import java.awt.BorderLayout; 4 | import java.awt.Color; 5 | import java.awt.Dimension; 6 | import java.awt.FlowLayout; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.KeyEvent; 9 | import java.io.BufferedReader; 10 | import java.io.File; 11 | import java.io.FileReader; 12 | import java.io.IOException; 13 | import java.text.DecimalFormat; 14 | import java.util.StringTokenizer; 15 | 16 | import javax.swing.AbstractAction; 17 | import javax.swing.Action; 18 | import javax.swing.BorderFactory; 19 | import javax.swing.ButtonGroup; 20 | import javax.swing.JButton; 21 | import javax.swing.JComponent; 22 | import javax.swing.JFileChooser; 23 | import javax.swing.JFrame; 24 | import javax.swing.JLabel; 25 | import javax.swing.JMenuBar; 26 | import javax.swing.JMenuItem; 27 | import javax.swing.JPanel; 28 | import javax.swing.JRadioButton; 29 | import javax.swing.JSpinner; 30 | import javax.swing.JSplitPane; 31 | import javax.swing.KeyStroke; 32 | import javax.swing.SpinnerNumberModel; 33 | import javax.swing.UIManager; 34 | import javax.swing.UnsupportedLookAndFeelException; 35 | 36 | import de.lighti.clipper.Clipper.ClipType; 37 | import de.lighti.clipper.Clipper.PolyFillType; 38 | import de.lighti.clipper.Clipper.PolyType; 39 | import de.lighti.clipper.Path; 40 | import de.lighti.clipper.Paths; 41 | import de.lighti.clipper.Point.LongPoint; 42 | 43 | public class ClipperDialog extends JFrame { 44 | static boolean loadFromFile( String filename, Paths ppg, int dec_places ) throws IOException { 45 | return loadFromFile( filename, ppg, dec_places, 0, 0 ); 46 | } 47 | 48 | static boolean loadFromFile( String filename, Paths ppg, int dec_places, long xOffset, long yOffset ) throws IOException { 49 | final double scaling = Math.pow( 10, dec_places ); 50 | 51 | ppg.clear(); 52 | if (!new File( filename ).exists()) { 53 | return false; 54 | } 55 | final String delimiters = ", "; 56 | final BufferedReader sr = new BufferedReader( new FileReader( filename ) ); 57 | try { 58 | String line; 59 | if ((line = sr.readLine()) == null) { 60 | return false; 61 | } 62 | final int polyCnt = Integer.parseInt( line ); 63 | if (polyCnt < 0) { 64 | return false; 65 | } 66 | 67 | for (int i = 0; i < polyCnt; i++) { 68 | if ((line = sr.readLine()) == null) { 69 | return false; 70 | } 71 | final int vertCnt = Integer.parseInt( line ); 72 | if (vertCnt < 0) { 73 | return false; 74 | } 75 | final Path pg = new Path( vertCnt ); 76 | ppg.add( pg ); 77 | if (scaling > 0.999 & scaling < 1.001) { 78 | for (int j = 0; j < vertCnt; j++) { 79 | long x; 80 | long y; 81 | if ((line = sr.readLine()) == null) { 82 | return false; 83 | } 84 | final StringTokenizer tokens = new StringTokenizer( line, delimiters ); 85 | 86 | if (tokens.countTokens() < 2) { 87 | return false; 88 | } 89 | 90 | x = Long.parseLong( tokens.nextToken() ); 91 | y = Long.parseLong( tokens.nextToken() ); 92 | 93 | x = x + xOffset; 94 | y = y + yOffset; 95 | pg.add( new LongPoint( x, y ) ); 96 | } 97 | } 98 | else { 99 | for (int j = 0; j < vertCnt; j++) { 100 | double x, y; 101 | if ((line = sr.readLine()) == null) { 102 | return false; 103 | } 104 | final StringTokenizer tokens = new StringTokenizer( line, delimiters ); 105 | 106 | if (tokens.countTokens() < 2) { 107 | return false; 108 | } 109 | x = Double.parseDouble( tokens.nextToken() ); 110 | y = Double.parseDouble( tokens.nextToken() ); 111 | 112 | x = x * scaling + xOffset; 113 | y = y * scaling + yOffset; 114 | pg.add( new LongPoint( (int) Math.round( x ), (int) Math.round( y ) ) ); 115 | } 116 | } 117 | } 118 | return true; 119 | } 120 | finally { 121 | sr.close(); 122 | } 123 | } 124 | 125 | public static void main( String[] args ) { 126 | new ClipperDialog().setVisible( true ); 127 | } 128 | 129 | public static int DEFAULT_VERTEX_COUNT = 5; 130 | private static final long serialVersionUID = 7437089068822709778L; 131 | private StatusBar statusStrip1; 132 | private JPanel panel1; 133 | private JPanel groupBox3; 134 | private JRadioButton rbNone; 135 | private JRadioButton rbXor; 136 | private JRadioButton rbDifference; 137 | private JRadioButton rbUnion; 138 | private JRadioButton rbIntersect; 139 | private JPanel groupBox2; 140 | private JRadioButton rbTest2; 141 | private JRadioButton rbTest1; 142 | private JPanel groupBox1; 143 | private JLabel label2; 144 | private JSpinner nudOffset; 145 | private JLabel lblCount; 146 | private JSpinner nudCount; 147 | private JRadioButton rbNonZero; 148 | private JRadioButton rbEvenOdd; 149 | private JButton bRefresh; 150 | private JPanel panel2; 151 | private PolygonCanvas pictureBox1; 152 | private JButton bSave; 153 | 154 | public ClipperDialog() { 155 | try { 156 | UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); 157 | UIManager.getDefaults().put( "Button.showMnemonics", Boolean.TRUE ); 158 | } 159 | catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) { 160 | //Too bad ... 161 | } 162 | setJMenuBar( createMenuBar() ); 163 | createControls(); 164 | 165 | setDefaultCloseOperation( EXIT_ON_CLOSE ); 166 | setPreferredSize( new Dimension( 716, 521 ) ); 167 | setResizable( false ); 168 | setTitle( "Clipper Java Demo" ); 169 | pack(); 170 | 171 | } 172 | 173 | private void createControls() { 174 | 175 | statusStrip1 = new StatusBar(); 176 | // this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel(); 177 | panel1 = new JPanel(); 178 | bSave = new JButton(); 179 | groupBox3 = new JPanel(); 180 | rbNone = new JRadioButton(); 181 | rbXor = new JRadioButton(); 182 | rbDifference = new JRadioButton(); 183 | rbUnion = new JRadioButton(); 184 | rbIntersect = new JRadioButton(); 185 | groupBox2 = new JPanel(); 186 | rbTest2 = new JRadioButton(); 187 | rbTest1 = new JRadioButton(); 188 | groupBox1 = new JPanel(); 189 | label2 = new JLabel(); 190 | nudOffset = new JSpinner(); 191 | lblCount = new JLabel(); 192 | nudCount = new JSpinner(); 193 | rbNonZero = new JRadioButton(); 194 | rbEvenOdd = new JRadioButton(); 195 | bRefresh = new JButton(); 196 | panel2 = new JPanel(); 197 | pictureBox1 = new PolygonCanvas( statusStrip1 ); 198 | 199 | // 200 | // panel1 201 | // 202 | 203 | panel1.setLayout( new FlowLayout( FlowLayout.LEFT ) ); 204 | panel1.add( groupBox3 ); 205 | 206 | panel1.add( groupBox1 ); 207 | panel1.add( groupBox2 ); 208 | panel1.add( bRefresh ); 209 | panel1.add( bSave ); 210 | 211 | panel1.setPreferredSize( new Dimension( 121, 459 ) ); 212 | 213 | // 214 | // bSave 215 | // 216 | 217 | bSave.setPreferredSize( new Dimension( 100, 25 ) ); 218 | 219 | final AbstractAction bSaveAction = new AbstractAction( "Save SVG" ) { 220 | /** 221 | * 222 | */ 223 | private static final long serialVersionUID = -8863563653315329743L; 224 | 225 | @Override 226 | public void actionPerformed( ActionEvent e ) { 227 | final JFileChooser fc = new JFileChooser( System.getProperty( "user.dir" ) ); 228 | final int returnVal = fc.showSaveDialog( ClipperDialog.this ); 229 | 230 | if (returnVal == JFileChooser.APPROVE_OPTION) { 231 | try { 232 | 233 | final File file = fc.getSelectedFile(); 234 | final SVGBuilder svg = new SVGBuilder(); 235 | svg.style.brushClr = new Color( 0, 0, 0x9c, 0x20 ); 236 | svg.style.penClr = new Color( 0xd3, 0xd3, 0xda ); 237 | svg.addPaths( pictureBox1.getSubjects() ); 238 | svg.style.brushClr = new Color( 0x20, 0x9c, 0, 0 ); 239 | svg.style.penClr = new Color( 0xff, 0xa0, 0x7a ); 240 | svg.addPaths( pictureBox1.getClips() ); 241 | svg.style.brushClr = new Color( 0x80, 0xff, 0x9c, 0xAA ); 242 | svg.style.penClr = new Color( 0, 0x33, 0 ); 243 | svg.addPaths( pictureBox1.getSolution() ); 244 | svg.saveToFile( file.getAbsolutePath(), 1 ); 245 | 246 | statusStrip1.setText( "Save successful" ); 247 | } 248 | catch (final IOException ex) { 249 | statusStrip1.setText( "Error: " + ex.getMessage() ); 250 | } 251 | } 252 | 253 | } 254 | }; 255 | bSaveAction.putValue( Action.MNEMONIC_KEY, KeyEvent.VK_A ); 256 | bSave.setAction( bSaveAction ); 257 | bSave.getInputMap( JComponent.WHEN_IN_FOCUSED_WINDOW ).put( KeyStroke.getKeyStroke( KeyEvent.VK_A, 0 ), "Save" ); 258 | bSave.getActionMap().put( "Save", bSaveAction ); 259 | 260 | // 261 | // groupBox3 262 | // 263 | groupBox3.add( rbIntersect ); 264 | groupBox3.add( rbUnion ); 265 | groupBox3.add( rbDifference ); 266 | groupBox3.add( rbXor ); 267 | groupBox3.add( rbNone ); 268 | 269 | groupBox3.setBorder( BorderFactory.createTitledBorder( "Boolean Op" ) ); 270 | groupBox3.setLayout( new FlowLayout( FlowLayout.LEFT, 0, 0 ) ); 271 | groupBox3.setPreferredSize( new Dimension( 100, 135 ) ); 272 | 273 | final ButtonGroup group3 = new ButtonGroup(); 274 | group3.add( rbNone ); 275 | group3.add( rbXor ); 276 | group3.add( rbDifference ); 277 | group3.add( rbUnion ); 278 | group3.add( rbIntersect ); 279 | 280 | // 281 | // rbNone 282 | // 283 | rbNone.setAction( new AbstractAction( "None" ) { 284 | private static final long serialVersionUID = 4405963373838217293L; 285 | 286 | @Override 287 | public void actionPerformed( ActionEvent e ) { 288 | pictureBox1.setClipType( null ); 289 | } 290 | } ); 291 | // 292 | // rbXor 293 | // 294 | 295 | rbXor.setAction( new AbstractAction( "XOR" ) { 296 | private static final long serialVersionUID = -4865012993106866716L; 297 | 298 | @Override 299 | public void actionPerformed( ActionEvent e ) { 300 | pictureBox1.setClipType( ClipType.XOR ); 301 | } 302 | } ); 303 | // 304 | // rbDifference 305 | // 306 | rbDifference.setAction( new AbstractAction( "Difference" ) { 307 | private static final long serialVersionUID = -619610168436846559L; 308 | 309 | @Override 310 | public void actionPerformed( ActionEvent e ) { 311 | pictureBox1.setClipType( ClipType.DIFFERENCE ); 312 | } 313 | } ); 314 | // 315 | // rbUnion 316 | // 317 | rbUnion.setAction( new AbstractAction( "Union" ) { 318 | private static final long serialVersionUID = -8369519233115242994L; 319 | 320 | @Override 321 | public void actionPerformed( ActionEvent e ) { 322 | pictureBox1.setClipType( ClipType.UNION ); 323 | } 324 | } ); 325 | // 326 | // rbIntersect 327 | // 328 | 329 | rbIntersect.setSelected( true ); 330 | rbIntersect.setAction( new AbstractAction( "Intersect" ) { 331 | private static final long serialVersionUID = 5202593451595347999L; 332 | 333 | @Override 334 | public void actionPerformed( ActionEvent e ) { 335 | pictureBox1.setClipType( ClipType.INTERSECTION ); 336 | } 337 | } ); 338 | 339 | // 340 | // groupBox2 341 | // 342 | groupBox2.add( rbTest1 ); 343 | groupBox2.add( rbTest2 ); 344 | 345 | final ButtonGroup group2 = new ButtonGroup(); 346 | group2.add( rbTest1 ); 347 | group2.add( rbTest2 ); 348 | 349 | groupBox2.setLayout( new FlowLayout( FlowLayout.LEFT, 0, 0 ) ); 350 | groupBox2.setBorder( BorderFactory.createTitledBorder( "Sample" ) ); 351 | groupBox2.setPreferredSize( new Dimension( 100, 61 ) ); 352 | // 353 | // rbTest2 354 | // 355 | rbTest2.setText( "Two" ); 356 | // 357 | // rbTest1 358 | // 359 | rbTest1.setText( "One" ); 360 | rbTest1.setSelected( true ); 361 | // 362 | // groupBox1 363 | // 364 | groupBox1.setLayout( new FlowLayout( FlowLayout.LEFT, 0, 0 ) ); 365 | groupBox1.add( rbEvenOdd ); 366 | groupBox1.add( rbNonZero ); 367 | groupBox1.add( lblCount ); 368 | groupBox1.add( nudCount ); 369 | groupBox1.add( label2 ); 370 | groupBox1.add( nudOffset ); 371 | 372 | final ButtonGroup group1 = new ButtonGroup(); 373 | group1.add( rbEvenOdd ); 374 | group1.add( rbNonZero ); 375 | 376 | groupBox1.setBorder( BorderFactory.createTitledBorder( "Options" ) ); 377 | groupBox1.setPreferredSize( new Dimension( 100, 159 ) ); 378 | 379 | // 380 | // label2 381 | // 382 | label2.setText( "Offset:" ); 383 | 384 | // 385 | // nudOffset 386 | nudOffset.setPreferredSize( new Dimension( 54, 20 ) ); 387 | final SpinnerNumberModel nudOffsetModel = new SpinnerNumberModel( 0f, -10f, 10f, 1f ); 388 | nudOffsetModel.addChangeListener( e -> pictureBox1.setOffset( nudOffsetModel.getNumber().floatValue() ) ); 389 | nudOffset.setModel( nudOffsetModel ); 390 | final JSpinner.NumberEditor nudOffsetEditor = (JSpinner.NumberEditor) nudOffset.getEditor(); 391 | final DecimalFormat nudOffsetEditorFormat = nudOffsetEditor.getFormat(); 392 | nudOffsetEditorFormat.setMinimumFractionDigits( 1 ); 393 | 394 | // 395 | // lblCount 396 | // 397 | lblCount.setText( "Vertex Count:" ); 398 | // 399 | // nudCount 400 | // 401 | 402 | final SpinnerNumberModel nudCountModel = new SpinnerNumberModel( DEFAULT_VERTEX_COUNT, 3, 100, 1 ); 403 | nudCountModel.addChangeListener( e -> pictureBox1.setVertexCount( nudCountModel.getNumber().intValue() ) ); 404 | nudCount.setModel( nudCountModel ); 405 | final JSpinner.NumberEditor nudCountEditor = (JSpinner.NumberEditor) nudCount.getEditor(); 406 | final DecimalFormat nudCountEditorFormat = nudCountEditor.getFormat(); 407 | nudCountEditorFormat.setMaximumFractionDigits( 0 ); 408 | nudCount.setPreferredSize( new Dimension( 54, 20 ) ); 409 | 410 | // 411 | // rbNonZero 412 | // 413 | rbNonZero.setAction( new AbstractAction( "NonZero" ) { 414 | private static final long serialVersionUID = 5202593451595347999L; 415 | 416 | @Override 417 | public void actionPerformed( ActionEvent e ) { 418 | pictureBox1.setFillType( PolyFillType.NON_ZERO ); 419 | } 420 | } ); 421 | 422 | // 423 | // rbEvenOdd 424 | // 425 | 426 | rbEvenOdd.setAction( new AbstractAction( "EvenOdd" ) { 427 | private static final long serialVersionUID = 5202593451595347999L; 428 | 429 | @Override 430 | public void actionPerformed( ActionEvent e ) { 431 | pictureBox1.setFillType( PolyFillType.EVEN_ODD ); 432 | } 433 | } ); 434 | rbEvenOdd.setSelected( true ); 435 | 436 | // 437 | // bRefresh 438 | // 439 | 440 | bRefresh.setPreferredSize( new Dimension( 100, 25 ) ); 441 | final AbstractAction bRefreshAction = new AbstractAction( "New Sample" ) { 442 | 443 | /** 444 | * 445 | */ 446 | private static final long serialVersionUID = 4405963373838217293L; 447 | 448 | @Override 449 | public void actionPerformed( ActionEvent e ) { 450 | if (rbTest1.isSelected()) { 451 | pictureBox1.generateRandomPolygon(); 452 | } 453 | else { 454 | pictureBox1.generateAustPlusRandomEllipses(); 455 | } 456 | } 457 | }; 458 | bRefreshAction.putValue( Action.MNEMONIC_KEY, KeyEvent.VK_N ); 459 | 460 | bRefresh.setAction( bRefreshAction ); 461 | bRefresh.getInputMap( JComponent.WHEN_IN_FOCUSED_WINDOW ).put( KeyStroke.getKeyStroke( KeyEvent.VK_N, 0 ), "Refresh" ); 462 | bRefresh.getActionMap().put( "Refresh", bRefreshAction ); 463 | 464 | // 465 | // panel2 466 | // 467 | panel2.add( pictureBox1 ); 468 | // this.panel2.Dock = System.Windows.Forms.DockStyle.Fill; 469 | panel2.setPreferredSize( new Dimension( 595, 459 ) ); 470 | 471 | // 472 | // pictureBox1 473 | // 474 | 475 | pictureBox1.setPreferredSize( new Dimension( 591, 455 ) ); 476 | 477 | // 478 | // Form1 479 | // 480 | 481 | final JSplitPane root = new JSplitPane( JSplitPane.HORIZONTAL_SPLIT ); 482 | root.setLeftComponent( panel1 ); 483 | root.setRightComponent( panel2 ); 484 | root.setDividerLocation( panel1.getPreferredSize().width ); 485 | root.setDividerSize( 1 ); 486 | 487 | setContentPane( new JPanel( new BorderLayout() ) ); 488 | getContentPane().add( root, BorderLayout.CENTER ); 489 | getContentPane().add( statusStrip1, BorderLayout.SOUTH ); 490 | 491 | } 492 | 493 | private JMenuBar createMenuBar() { 494 | final JMenuBar menubar = new JMenuBar(); 495 | menubar.setLayout( new FlowLayout( FlowLayout.LEFT, 0, 0 ) ); 496 | final JMenuItem loadSubjectItem = new JMenuItem( new AbstractAction( "Load Subject" ) { 497 | 498 | /** 499 | * 500 | */ 501 | private static final long serialVersionUID = 5372200924672915516L; 502 | 503 | @Override 504 | public void actionPerformed( ActionEvent e ) { 505 | loadFile( PolyType.SUBJECT ); 506 | 507 | } 508 | } ); 509 | menubar.add( loadSubjectItem ); 510 | 511 | final JMenuItem loadCLipItem = new JMenuItem( new AbstractAction( "Load Clip" ) { 512 | 513 | /** 514 | * 515 | */ 516 | private static final long serialVersionUID = -6723609311301727992L; 517 | 518 | @Override 519 | public void actionPerformed( ActionEvent e ) { 520 | loadFile( PolyType.CLIP ); 521 | 522 | } 523 | } ); 524 | menubar.add( loadCLipItem ); 525 | 526 | return menubar; 527 | } 528 | 529 | private void loadFile( PolyType type ) { 530 | final JFileChooser fc = new JFileChooser( System.getProperty( "user.dir" ) ); 531 | final int returnVal = fc.showOpenDialog( ClipperDialog.this ); 532 | 533 | if (returnVal == JFileChooser.APPROVE_OPTION) { 534 | final File file = fc.getSelectedFile(); 535 | //This is where a real application would open the file. 536 | 537 | try { 538 | final Paths paths = new Paths(); 539 | final boolean success = loadFromFile( file.getAbsolutePath(), paths, 0 ); 540 | 541 | if (!success) { 542 | statusStrip1.setText( "Error: check file syntax" ); 543 | } 544 | else { 545 | pictureBox1.setPolygon( type, paths ); 546 | statusStrip1.setText( "File loaded successful" ); 547 | } 548 | } 549 | catch (final IOException e) { 550 | statusStrip1.setText( "Error: " + e.getMessage() ); 551 | } 552 | 553 | } 554 | else { 555 | statusStrip1.setText( "User cancelled" ); 556 | } 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /Clipper/src/de/lighti/clipper/ClipperBase.java: -------------------------------------------------------------------------------- 1 | package de.lighti.clipper; 2 | 3 | import de.lighti.clipper.Path.OutRec; 4 | import de.lighti.clipper.Point.LongPoint; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.logging.Logger; 9 | 10 | public abstract class ClipperBase implements Clipper { 11 | protected class LocalMinima { 12 | long y; 13 | Edge leftBound; 14 | Edge rightBound; 15 | LocalMinima next; 16 | } 17 | 18 | protected class Scanbeam { 19 | long y; 20 | Scanbeam next; 21 | } 22 | 23 | protected class Maxima { 24 | long x; 25 | Maxima next; 26 | Maxima prev; 27 | } 28 | 29 | private static void initEdge(Edge e, Edge eNext, Edge ePrev, LongPoint pt ) { 30 | e.next = eNext; 31 | e.prev = ePrev; 32 | e.setCurrent( new LongPoint( pt ) ); 33 | e.outIdx = Edge.UNASSIGNED; 34 | } 35 | 36 | private static void initEdge2(Edge e, PolyType polyType ) { 37 | if (e.getCurrent().getY() >= e.next.getCurrent().getY()) { 38 | e.setBot( new LongPoint( e.getCurrent() ) ); 39 | e.setTop( new LongPoint( e.next.getCurrent() ) ); 40 | } 41 | else { 42 | e.setTop( new LongPoint( e.getCurrent() ) ); 43 | e.setBot( new LongPoint( e.next.getCurrent() ) ); 44 | } 45 | e.updateDeltaX(); 46 | e.polyTyp = polyType; 47 | } 48 | 49 | private static void rangeTest( LongPoint Pt ) { 50 | 51 | if (Pt.getX() > LOW_RANGE || Pt.getY() > LOW_RANGE || -Pt.getX() > LOW_RANGE || -Pt.getY() > LOW_RANGE) { 52 | if (Pt.getX() > HI_RANGE || Pt.getY() > HI_RANGE || -Pt.getX() > HI_RANGE || -Pt.getY() > HI_RANGE) { 53 | throw new IllegalStateException( "Coordinate outside allowed range" ); 54 | } 55 | } 56 | } 57 | 58 | private static Edge removeEdge(Edge e ) { 59 | //removes e from double_linked_list (but without removing from memory) 60 | e.prev.next = e.next; 61 | e.next.prev = e.prev; 62 | final Edge result = e.next; 63 | e.prev = null; //flag as removed (see ClipperBase.Clear) 64 | return result; 65 | } 66 | 67 | private final static long LOW_RANGE = 0x3FFFFFFF; 68 | 69 | private final static long HI_RANGE = 0x3FFFFFFFFFFFFFFFL; 70 | 71 | protected LocalMinima minimaList; 72 | 73 | protected LocalMinima currentLM; 74 | 75 | protected Scanbeam scanbeam; 76 | 77 | protected final List polyOuts = new ArrayList<>(); 78 | 79 | protected Edge activeEdges; 80 | 81 | protected boolean hasOpenPaths; 82 | 83 | protected final boolean preserveCollinear; 84 | 85 | private final static Logger LOGGER = Logger.getLogger( Clipper.class.getName() ); 86 | 87 | protected ClipperBase( boolean preserveCollinear ) //constructor (nb: no external instantiation) 88 | { 89 | this.preserveCollinear = preserveCollinear; 90 | minimaList = null; 91 | currentLM = null; 92 | hasOpenPaths = false; 93 | } 94 | 95 | @Override 96 | public boolean addPath(Path pg, PolyType polyType, boolean Closed ) { 97 | 98 | if (!Closed && polyType == PolyType.CLIP) { 99 | throw new IllegalStateException( "AddPath: Open paths must be subject." ); 100 | } 101 | 102 | int highI = pg.size() - 1; 103 | if (Closed) { 104 | while (highI > 0 && pg.get( highI ).equals( pg.get( 0 ) )) { 105 | --highI; 106 | } 107 | } 108 | while (highI > 0 && pg.get( highI ).equals( pg.get( highI - 1 ) )) { 109 | --highI; 110 | } 111 | if (Closed && highI < 2 || !Closed && highI < 1) { 112 | return false; 113 | } 114 | 115 | //create a new edge array ... 116 | final List edges = new ArrayList<>( highI + 1 ); 117 | for (int i = 0; i <= highI; i++) { 118 | edges.add( new Edge() ); 119 | } 120 | 121 | boolean IsFlat = true; 122 | 123 | //1. Basic (first) edge initialization ... 124 | edges.get( 1 ).setCurrent( new LongPoint( pg.get( 1 ) ) ); 125 | rangeTest( pg.get( 0 ) ); 126 | rangeTest( pg.get( highI ) ); 127 | initEdge( edges.get( 0 ), edges.get( 1 ), edges.get( highI ), pg.get( 0 ) ); 128 | initEdge( edges.get( highI ), edges.get( 0 ), edges.get( highI - 1 ), pg.get( highI ) ); 129 | for (int i = highI - 1; i >= 1; --i) { 130 | rangeTest( pg.get( i ) ); 131 | initEdge( edges.get( i ), edges.get( i + 1 ), edges.get( i - 1 ), pg.get( i ) ); 132 | } 133 | Edge eStart = edges.get( 0 ); 134 | 135 | //2. Remove duplicate vertices, and (when closed) collinear edges ... 136 | Edge e = eStart, eLoopStop = eStart; 137 | for (;;) { 138 | //nb: allows matching start and end points when not Closed ... 139 | if (e.getCurrent().equals( e.next.getCurrent() ) && (Closed || !e.next.equals( eStart ))) { 140 | if (e == e.next) { 141 | break; 142 | } 143 | if (e == eStart) { 144 | eStart = e.next; 145 | } 146 | e = removeEdge( e ); 147 | eLoopStop = e; 148 | continue; 149 | } 150 | if (e.prev == e.next) { 151 | break; //only two vertices 152 | } 153 | else if (Closed && Point.slopesEqual( e.prev.getCurrent(), e.getCurrent(), e.next.getCurrent() ) 154 | && (!isPreserveCollinear() || !Point.isPt2BetweenPt1AndPt3( e.prev.getCurrent(), e.getCurrent(), e.next.getCurrent() ))) { 155 | //Collinear edges are allowed for open paths but in closed paths 156 | //the default is to merge adjacent collinear edges into a single edge. 157 | //However, if the PreserveCollinear property is enabled, only overlapping 158 | //collinear edges (ie spikes) will be removed from closed paths. 159 | if (e == eStart) { 160 | eStart = e.next; 161 | } 162 | e = removeEdge( e ); 163 | e = e.prev; 164 | eLoopStop = e; 165 | continue; 166 | } 167 | e = e.next; 168 | if (e == eLoopStop || !Closed && e.next == eStart) { 169 | break; 170 | } 171 | } 172 | 173 | if (!Closed && e == e.next || Closed && e.prev == e.next) { 174 | return false; 175 | } 176 | 177 | if (!Closed) { 178 | hasOpenPaths = true; 179 | eStart.prev.outIdx = Edge.SKIP; 180 | } 181 | 182 | //3. Do second stage of edge initialization ... 183 | e = eStart; 184 | do { 185 | initEdge2( e, polyType ); 186 | e = e.next; 187 | if (IsFlat && e.getCurrent().getY() != eStart.getCurrent().getY()) { 188 | IsFlat = false; 189 | } 190 | } 191 | while (e != eStart); 192 | 193 | //4. Finally, add edge bounds to LocalMinima list ... 194 | 195 | //Totally flat paths must be handled differently when adding them 196 | //to LocalMinima list to avoid endless loops etc ... 197 | if (IsFlat) { 198 | if (Closed) { 199 | return false; 200 | } 201 | e.prev.outIdx = Edge.SKIP; 202 | final LocalMinima locMin = new LocalMinima(); 203 | locMin.next = null; 204 | locMin.y = e.getBot().getY(); 205 | locMin.leftBound = null; 206 | locMin.rightBound = e; 207 | locMin.rightBound.side = Edge.Side.RIGHT; 208 | locMin.rightBound.windDelta = 0; 209 | for ( ; ; ) { 210 | if (e.getBot().getX() != e.prev.getTop().getX()) { 211 | e.reverseHorizontal(); 212 | } 213 | if (e.next.outIdx == Edge.SKIP) break; 214 | e.nextInLML = e.next; 215 | e = e.next; 216 | } 217 | insertLocalMinima( locMin ); 218 | return true; 219 | } 220 | 221 | boolean leftBoundIsForward; 222 | Edge EMin = null; 223 | 224 | //workaround to avoid an endless loop in the while loop below when 225 | //open paths have matching start and end points ... 226 | if (e.prev.getBot().equals( e.prev.getTop() )) { 227 | e = e.next; 228 | } 229 | 230 | for (;;) { 231 | e = e.findNextLocMin(); 232 | if (e == EMin) { 233 | break; 234 | } 235 | else if (EMin == null) { 236 | EMin = e; 237 | } 238 | 239 | //E and E.Prev now share a local minima (left aligned if horizontal). 240 | //Compare their slopes to find which starts which bound ... 241 | final LocalMinima locMin = new LocalMinima(); 242 | locMin.next = null; 243 | locMin.y = e.getBot().getY(); 244 | if (e.deltaX < e.prev.deltaX) { 245 | locMin.leftBound = e.prev; 246 | locMin.rightBound = e; 247 | leftBoundIsForward = false; //Q.nextInLML = Q.prev 248 | } 249 | else { 250 | locMin.leftBound = e; 251 | locMin.rightBound = e.prev; 252 | leftBoundIsForward = true; //Q.nextInLML = Q.next 253 | } 254 | locMin.leftBound.side = Edge.Side.LEFT; 255 | locMin.rightBound.side = Edge.Side.RIGHT; 256 | 257 | if (!Closed) { 258 | locMin.leftBound.windDelta = 0; 259 | } 260 | else if (locMin.leftBound.next == locMin.rightBound) { 261 | locMin.leftBound.windDelta = -1; 262 | } 263 | else { 264 | locMin.leftBound.windDelta = 1; 265 | } 266 | locMin.rightBound.windDelta = -locMin.leftBound.windDelta; 267 | 268 | e = processBound( locMin.leftBound, leftBoundIsForward ); 269 | if (e.outIdx == Edge.SKIP) { 270 | e = processBound( e, leftBoundIsForward ); 271 | } 272 | 273 | Edge E2 = processBound( locMin.rightBound, !leftBoundIsForward ); 274 | if (E2.outIdx == Edge.SKIP) { 275 | E2 = processBound( E2, !leftBoundIsForward ); 276 | } 277 | 278 | if (locMin.leftBound.outIdx == Edge.SKIP) { 279 | locMin.leftBound = null; 280 | } 281 | else if (locMin.rightBound.outIdx == Edge.SKIP) { 282 | locMin.rightBound = null; 283 | } 284 | insertLocalMinima( locMin ); 285 | if (!leftBoundIsForward) { 286 | e = E2; 287 | } 288 | } 289 | return true; 290 | 291 | } 292 | 293 | @Override 294 | public boolean addPaths(Paths paths, PolyType polyType, boolean closed ) { 295 | boolean result = false; 296 | for (Path path : paths) { 297 | if (addPath(path, polyType, closed)) { 298 | result = true; 299 | } 300 | } 301 | return result; 302 | } 303 | 304 | @Override 305 | public void clear() { 306 | disposeLocalMinimaList(); 307 | hasOpenPaths = false; 308 | } 309 | 310 | private void disposeLocalMinimaList() { 311 | while (minimaList != null) { 312 | final LocalMinima tmpLm = minimaList.next; 313 | minimaList = null; 314 | minimaList = tmpLm; 315 | } 316 | currentLM = null; 317 | } 318 | 319 | private void insertLocalMinima( LocalMinima newLm ) { 320 | if (minimaList == null) { 321 | minimaList = newLm; 322 | } 323 | else if (newLm.y >= minimaList.y) { 324 | newLm.next = minimaList; 325 | minimaList = newLm; 326 | } 327 | else { 328 | LocalMinima tmpLm = minimaList; 329 | while (tmpLm.next != null && newLm.y < tmpLm.next.y) { 330 | tmpLm = tmpLm.next; 331 | } 332 | newLm.next = tmpLm.next; 333 | tmpLm.next = newLm; 334 | } 335 | } 336 | private boolean isPreserveCollinear() { 337 | return preserveCollinear; 338 | } 339 | 340 | protected boolean popLocalMinima( long y, LocalMinima[] current ) { 341 | LOGGER.entering( ClipperBase.class.getName(), "popLocalMinima" ); 342 | current[0] = currentLM; 343 | if (currentLM != null && currentLM.y == y) { 344 | currentLM = currentLM.next; 345 | return true; 346 | } 347 | return false; 348 | } 349 | 350 | private Edge processBound(Edge e, boolean LeftBoundIsForward ) { 351 | Edge EStart, result = e; 352 | Edge Horz; 353 | 354 | if (result.outIdx == Edge.SKIP) { 355 | //check if there are edges beyond the skip edge in the bound and if so 356 | //create another LocMin and calling ProcessBound once more ... 357 | e = result; 358 | if (LeftBoundIsForward) { 359 | while (e.getTop().getY() == e.next.getBot().getY()) { 360 | e = e.next; 361 | } 362 | while (e != result && e.deltaX == Edge.HORIZONTAL) { 363 | e = e.prev; 364 | } 365 | } 366 | else { 367 | while (e.getTop().getY() == e.prev.getBot().getY()) { 368 | e = e.prev; 369 | } 370 | while (e != result && e.deltaX == Edge.HORIZONTAL) { 371 | e = e.next; 372 | } 373 | } 374 | if (e == result) { 375 | if (LeftBoundIsForward) { 376 | result = e.next; 377 | } 378 | else { 379 | result = e.prev; 380 | } 381 | } 382 | else { 383 | //there are more edges in the bound beyond result starting with E 384 | if (LeftBoundIsForward) { 385 | e = result.next; 386 | } 387 | else { 388 | e = result.prev; 389 | } 390 | final LocalMinima locMin = new LocalMinima(); 391 | locMin.next = null; 392 | locMin.y = e.getBot().getY(); 393 | locMin.leftBound = null; 394 | locMin.rightBound = e; 395 | e.windDelta = 0; 396 | result = processBound( e, LeftBoundIsForward ); 397 | insertLocalMinima( locMin ); 398 | } 399 | return result; 400 | } 401 | 402 | if (e.deltaX == Edge.HORIZONTAL) { 403 | //We need to be careful with open paths because this may not be a 404 | //true local minima (ie E may be following a skip edge). 405 | //Also, consecutive horz. edges may start heading left before going right. 406 | if (LeftBoundIsForward) { 407 | EStart = e.prev; 408 | } 409 | else { 410 | EStart = e.next; 411 | } 412 | if (EStart.deltaX == Edge.HORIZONTAL) //ie an adjoining horizontal skip edge 413 | { 414 | if (EStart.getBot().getX() != e.getBot().getX() && EStart.getTop().getX() != e.getBot().getX()) { 415 | e.reverseHorizontal(); 416 | } 417 | } 418 | else if (EStart.getBot().getX() != e.getBot().getX()) { 419 | e.reverseHorizontal(); 420 | } 421 | } 422 | 423 | EStart = e; 424 | if (LeftBoundIsForward) { 425 | while (result.getTop().getY() == result.next.getBot().getY() && result.next.outIdx != Edge.SKIP) { 426 | result = result.next; 427 | } 428 | if (result.deltaX == Edge.HORIZONTAL && result.next.outIdx != Edge.SKIP) { 429 | //nb: at the top of a bound, horizontals are added to the bound 430 | //only when the preceding edge attaches to the horizontal's left vertex 431 | //unless a Skip edge is encountered when that becomes the top divide 432 | Horz = result; 433 | while (Horz.prev.deltaX == Edge.HORIZONTAL) { 434 | Horz = Horz.prev; 435 | } 436 | if (Horz.prev.getTop().getX() > result.next.getTop().getX()) { 437 | result = Horz.prev; 438 | } 439 | } 440 | while (e != result) { 441 | e.nextInLML = e.next; 442 | if (e.deltaX == Edge.HORIZONTAL && e != EStart && e.getBot().getX() != e.prev.getTop().getX()) { 443 | e.reverseHorizontal(); 444 | } 445 | e = e.next; 446 | } 447 | if (e.deltaX == Edge.HORIZONTAL && e != EStart && e.getBot().getX() != e.prev.getTop().getX()) { 448 | e.reverseHorizontal(); 449 | } 450 | result = result.next; //move to the edge just beyond current bound 451 | } 452 | else { 453 | while (result.getTop().getY() == result.prev.getBot().getY() && result.prev.outIdx != Edge.SKIP) { 454 | result = result.prev; 455 | } 456 | if (result.deltaX == Edge.HORIZONTAL && result.prev.outIdx != Edge.SKIP) { 457 | Horz = result; 458 | while (Horz.next.deltaX == Edge.HORIZONTAL) { 459 | Horz = Horz.next; 460 | } 461 | if (Horz.next.getTop().getX() == result.prev.getTop().getX() || 462 | Horz.next.getTop().getX() > result.prev.getTop().getX()) { 463 | result = Horz.next; 464 | } 465 | } 466 | 467 | while (e != result) { 468 | e.nextInLML = e.prev; 469 | if (e.deltaX == Edge.HORIZONTAL && e != EStart && e.getBot().getX() != e.next.getTop().getX()) { 470 | e.reverseHorizontal(); 471 | } 472 | e = e.prev; 473 | } 474 | if (e.deltaX == Edge.HORIZONTAL && e != EStart && e.getBot().getX() != e.next.getTop().getX()) { 475 | e.reverseHorizontal(); 476 | } 477 | result = result.prev; //move to the edge just beyond current bound 478 | } 479 | return result; 480 | } 481 | 482 | protected void reset() { 483 | currentLM = minimaList; 484 | if (currentLM == null) { 485 | return; //ie nothing to process 486 | } 487 | 488 | //reset all edges ... 489 | scanbeam = null; 490 | LocalMinima lm = minimaList; 491 | while (lm != null) { 492 | insertScanbeam(lm.y); 493 | Edge e = lm.leftBound; 494 | if (e != null) { 495 | e.setCurrent( new LongPoint( e.getBot() ) ); 496 | e.outIdx = Edge.UNASSIGNED; 497 | } 498 | e = lm.rightBound; 499 | if (e != null) { 500 | e.setCurrent( new LongPoint( e.getBot() ) ); 501 | e.outIdx = Edge.UNASSIGNED; 502 | } 503 | lm = lm.next; 504 | } 505 | activeEdges = null; 506 | } 507 | 508 | protected void insertScanbeam( long y ) { 509 | LOGGER.entering( ClipperBase.class.getName(), "insertScanbeam" ); 510 | 511 | //single-linked list: sorted descending, ignoring dups. 512 | if (scanbeam == null) { 513 | scanbeam = new Scanbeam(); 514 | scanbeam.next = null; 515 | scanbeam.y = y; 516 | } 517 | else if (y > scanbeam.y) { 518 | final Scanbeam newSb = new Scanbeam(); 519 | newSb.y = y; 520 | newSb.next = scanbeam; 521 | scanbeam = newSb; 522 | } 523 | else { 524 | Scanbeam sb2 = scanbeam; 525 | while (sb2.next != null && (y <= sb2.next.y)) { 526 | sb2 = sb2.next; 527 | } 528 | if (y == sb2.y) { 529 | return; //ie ignores duplicates 530 | } 531 | final Scanbeam newSb = new Scanbeam(); 532 | newSb.y = y; 533 | newSb.next = sb2.next; 534 | sb2.next = newSb; 535 | } 536 | } 537 | 538 | protected boolean popScanbeam( long[] y ) { 539 | if (scanbeam == null) { 540 | y[0] = 0; 541 | return false; 542 | } 543 | y[0] = scanbeam.y; 544 | scanbeam = scanbeam.next; 545 | return true; 546 | } 547 | 548 | protected final boolean localMinimaPending() { 549 | return currentLM != null; 550 | } 551 | 552 | protected OutRec createOutRec() { 553 | OutRec result = new OutRec(); 554 | result.Idx = Edge.UNASSIGNED; 555 | result.isHole = false; 556 | result.isOpen = false; 557 | result.firstLeft = null; 558 | result.setPoints( null ); 559 | result.bottomPt = null; 560 | result.polyNode = null; 561 | polyOuts.add( result ); 562 | result.Idx = polyOuts.size() - 1; 563 | return result; 564 | } 565 | 566 | protected void disposeOutRec( int index ) { 567 | OutRec outRec = polyOuts.get( index ); 568 | outRec.setPoints( null ); 569 | outRec = null; 570 | polyOuts.set( index, null ); 571 | } 572 | 573 | protected void updateEdgeIntoAEL( Edge e ) { 574 | if (e.nextInLML == null) { 575 | throw new IllegalStateException("UpdateEdgeIntoAEL: invalid call"); 576 | } 577 | final Edge aelPrev = e.prevInAEL; 578 | final Edge aelNext = e.nextInAEL; 579 | e.nextInLML.outIdx = e.outIdx; 580 | if (aelPrev != null) { 581 | aelPrev.nextInAEL = e.nextInLML; 582 | } 583 | else { 584 | activeEdges = e.nextInLML; 585 | } 586 | if (aelNext != null) { 587 | aelNext.prevInAEL = e.nextInLML; 588 | } 589 | e.nextInLML.side = e.side; 590 | e.nextInLML.windDelta = e.windDelta; 591 | e.nextInLML.windCnt = e.windCnt; 592 | e.nextInLML.windCnt2 = e.windCnt2; 593 | e = e.nextInLML; 594 | e.setCurrent(e.getBot()); 595 | e.prevInAEL = aelPrev; 596 | e.nextInAEL = aelNext; 597 | if (e.isHorizontal()) { 598 | insertScanbeam(e.getTop().getY()); 599 | } 600 | } 601 | 602 | protected void swapPositionsInAEL(Edge edge1, Edge edge2 ) { 603 | LOGGER.entering( ClipperBase.class.getName(), "swapPositionsInAEL" ); 604 | 605 | //check that one or other edge hasn't already been removed from AEL ... 606 | if (edge1.nextInAEL == edge1.prevInAEL || edge2.nextInAEL == edge2.prevInAEL) { 607 | return; 608 | } 609 | 610 | if (edge1.nextInAEL == edge2) { 611 | final Edge next = edge2.nextInAEL; 612 | if (next != null) { 613 | next.prevInAEL = edge1; 614 | } 615 | final Edge prev = edge1.prevInAEL; 616 | if (prev != null) { 617 | prev.nextInAEL = edge2; 618 | } 619 | edge2.prevInAEL = prev; 620 | edge2.nextInAEL = edge1; 621 | edge1.prevInAEL = edge2; 622 | edge1.nextInAEL = next; 623 | } 624 | else if (edge2.nextInAEL == edge1) { 625 | final Edge next = edge1.nextInAEL; 626 | if (next != null) { 627 | next.prevInAEL = edge2; 628 | } 629 | final Edge prev = edge2.prevInAEL; 630 | if (prev != null) { 631 | prev.nextInAEL = edge1; 632 | } 633 | edge1.prevInAEL = prev; 634 | edge1.nextInAEL = edge2; 635 | edge2.prevInAEL = edge1; 636 | edge2.nextInAEL = next; 637 | } 638 | else { 639 | final Edge next = edge1.nextInAEL; 640 | final Edge prev = edge1.prevInAEL; 641 | edge1.nextInAEL = edge2.nextInAEL; 642 | if (edge1.nextInAEL != null) { 643 | edge1.nextInAEL.prevInAEL = edge1; 644 | } 645 | edge1.prevInAEL = edge2.prevInAEL; 646 | if (edge1.prevInAEL != null) { 647 | edge1.prevInAEL.nextInAEL = edge1; 648 | } 649 | edge2.nextInAEL = next; 650 | if (edge2.nextInAEL != null) { 651 | edge2.nextInAEL.prevInAEL = edge2; 652 | } 653 | edge2.prevInAEL = prev; 654 | if (edge2.prevInAEL != null) { 655 | edge2.prevInAEL.nextInAEL = edge2; 656 | } 657 | } 658 | 659 | if (edge1.prevInAEL == null) { 660 | activeEdges = edge1; 661 | } 662 | else if (edge2.prevInAEL == null) { 663 | activeEdges = edge2; 664 | } 665 | 666 | LOGGER.exiting( ClipperBase.class.getName(), "swapPositionsInAEL" ); 667 | } 668 | 669 | protected void deleteFromAEL( Edge e ) { 670 | LOGGER.entering( ClipperBase.class.getName(), "deleteFromAEL" ); 671 | 672 | Edge aelPrev = e.prevInAEL; 673 | Edge aelNext = e.nextInAEL; 674 | if (aelPrev == null && aelNext == null && (e != activeEdges)) { 675 | return; //already deleted 676 | } 677 | if (aelPrev != null) { 678 | aelPrev.nextInAEL = aelNext; 679 | } 680 | else { 681 | activeEdges = aelNext; 682 | } 683 | if (aelNext != null) { 684 | aelNext.prevInAEL = aelPrev; 685 | } 686 | e.nextInAEL = null; 687 | e.prevInAEL = null; 688 | 689 | LOGGER.exiting( ClipperBase.class.getName(), "deleteFromAEL" ); 690 | } 691 | } --------------------------------------------------------------------------------