├── .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 extends Number> pt1, Point extends Number> 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 extends Number> pt, Point extends Number> ln1, Point extends Number> 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" ) );
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" ) );
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 | }
--------------------------------------------------------------------------------